Not so long ago, the first release of fabric-contract-api-go was released - the implementation of the new software model for the RFC 0001 chaincode . Let's see what it is and how to use it.Here I prepared a repository with a simple Fabric network, where peers run in dev mode. Follow the instructions from the repository to start the network and return (it will take no more than 5 minutes).Now that you have the network running and the chaincode is installed, let's look at the insides of the chaincode working in the new model.In SimpleContract.go, we import the module with the new API:github.com/hyperledger/fabric-contract-api-go/contractapi
Next, we describe our contract using the SimpleContract structure into which the Contract structure is embedded :type SimpleContract struct {
contractapi.Contract
}
Itβs imperative to build in Contract so that our contract satisfies the ContractInterface interface . Here you should make a reservation and say that the contract! = Chaincode. A chaincode is a container of an indefinite number of contracts. Chaincode stores its contracts in the map, as seen in this listing:
type ContractChaincode struct {
DefaultContract string
contracts map[string]contractChaincodeContract
metadata metadata.ContractChaincodeMetadata
Info metadata.InfoMetadata
TransactionSerializer serializer.TransactionSerializer
}
Map contracts are used internally by Invoke to route requests:func (cc *ContractChaincode) Invoke(stub shim.ChaincodeStubInterface) peer.Response {
nsFcn, params := stub.GetFunctionAndParameters()
li := strings.LastIndex(nsFcn, ":")
var ns string
var fn string
if li == -1 {
ns = cc.DefaultContract
fn = nsFcn
} else {
ns = nsFcn[:li]
fn = nsFcn[li+1:]
}
...
nsContract := cc.contracts[ns]
...
successReturn, successIFace, errorReturn = nsContract.functions[fn].Call(ctx, transactionSchema, &cc.metadata.Components, serializer, params...)
...
return shim.Success([]byte(successReturn))
}
So back to SimpleContract. All methods must have a ctx parameter that satisfies the TransactionContextInterface interface . By default, all methods get a standard TransactionContext , which in most cases is enough.This context allows us to work with ClientIdentity , for example, like this:func (sc *SimpleContract) Whois(ctx contractapi.TransactionContextInterface) (string, error) {
return ctx.GetClientIdentity().GetID()
}
Or get the familiar stub (shim.ChaincodeStubInterface) to perform all the usual actions for interacting with the ledger:func (sc *SimpleContract) Write(ctx contractapi.TransactionContextInterface, key string, value []byte) error {
return ctx.GetStub().PutState(key, value)
}
But! In the code of our demo repository, you can see a completely different context in the methods:func (sc *SimpleContract) Create(ctx CustomTransactionContextInterface, key string, value string) error {
existing := ctx.GetData()
if existing != nil {
return fmt.Errorf("Cannot create world state pair with key %s. Already exists", key)
}
err := ctx.GetStub().PutState(key, []byte(value))
if err != nil {
return errors.New("Unable to interact with world state")
}
return nil
}
This is a custom context. It is created very simply. Pay attention to context.go from our repository:1. We declare an interface compatible with contractapi.TransactionContextInterfacetype CustomTransactionContextInterface interface {
contractapi.TransactionContextInterface
GetData() []byte
SetData([]byte)
}
2. The structure into which we embed contractapi.TransactionContexttype CustomTransactionContext struct {
contractapi.TransactionContext
data []byte
}
3. We implement the declared methods
func (ctc *CustomTransactionContext) GetData() []byte {
return ctc.data
}
func (ctc *CustomTransactionContext) SetData(data []byte) {
ctc.data = data
}
Now, when initializing the contract, we simply pass this structure as a handler:simpleContract := new(SimpleContract)
simpleContract.TransactionContextHandler = new(CustomTransactionContext)
And all the methods of our contract now instead of ctx contractapi.TransactionContextInterface accept ctx CustomTransactionContextInterface .A custom context is needed to push a state through transactional hooks . Transactional hooks are a beautiful name for middleware that fires before or after calling the contract method.An example of a hook that, before calling a method, extracts from the ledger the value of the key passed in by the first parameter in the transaction:SimpleContract.gofunc GetWorldState(ctx CustomTransactionContextInterface) error {
_, params := ctx.GetStub().GetFunctionAndParameters()
if len(params) < 1 {
return errors.New("Missing key for world state")
}
existing, err := ctx.GetStub().GetState(params[0])
if err != nil {
return errors.New("Unable to interact with world state")
}
ctx.SetData(existing)
return nil
}
main.gosimpleContract.BeforeTransaction = GetWorldState
Now we can get the value of the requested key in the methods a little more concise:SimpleContract.gofunc (sc *SimpleContract) Read(ctx CustomTransactionContextInterface, key string) (string, error) {
existing := ctx.GetData()
if existing == nil {
return "", fmt.Errorf("Cannot read world state pair with key %s. Does not exist", key)
}
return string(existing), nil
}
The hook after the method call is almost identical, except that in addition to context, it accepts an empty interface (why we need it, we will figure it out later):YetAnotherContract.gofunc After(ctx contractapi.TransactionContextInterface, beforeValue interface{}) error {
fmt.Println(ctx.GetStub().GetTxID())
fmt.Println("beforeValue", beforeValue)
return nil
}
This hook displays the transaction id and the value that the method returned before the hook . To check this posthook, you can go into the CLI container and call the contract method: Switch to the terminal where the code is running, the output will look something like this:docker exec -it cli sh
peer chaincode query -n mycc -c '{"Args":["YetAnotherContract:SayHi"]}' -C myc
e503e98e4c71285722f244a481fbcbf0ff4120adcd2f9067089104e5c3ed0efe # txid
beforeValue Hi there # value from the previous method
What if we want to process requests with a non-existent function name? For this, any contract has an UnknownTransaction field :unknown_handler.gofunc UnknownTransactionHandler(ctx CustomTransactionContextInterface) error {
fcn, args := ctx.GetStub().GetFunctionAndParameters()
return fmt.Errorf("Invalid function %s passed with args %v", fcn, args)
}
main.gosimpleContract.UnknownTransaction = UnknownTransactionHandler
This can also be verified through the CLI: Conclusion:docker exec -it cli sh
peer chaincode query -n mycc -c '{"Args":["BadRequest", "BadKey"]}' -C myc
Error: endorsement failure during query. response: status: 500 message: "Invalid function BadRequest passed with args [BadKey]"
In order for the code to start on the peer, we must call the Start () method as before, before transferring all our contracts to the code :main.gocc, err := contractapi.NewChaincode(simpleContract, yetAnotherContract)
if err != nil {
panic(err.Error())
}
if err := cc.Start(); err != nil {
panic(err.Error())
}
Total
The new model of the codecode solved the problem of routing, middleware, serialization of return values, deserialization of string arguments (any types except interface {} can be used ). Now it remains to wait for the implementation of the new model for the Go SDK.Thank you for the attention.Part two