In our last post we introduced TzGo v1 for Hangzhou which was the first release that also allowed developers to send transactions. With our new release you can more easily send smart contract calls, use custom signers and work with FA token standards. Oh, and it's also compatible with Ithaca. For documentation visit the TzGo docs page and for source code head over to our Github.

What's new?

At a glance, we have

  • added Ithaca support (mostly new hashes, RPC calls, and operations)
  • added generic smart contract call support and support for calling off-chain views
  • added support for FA1.2 and FA2  transfers, allowances and views
  • added cost estimation for gas, storage, fees and flexible configuration options to manage costs (i.e. overwrite simulated values)
  • improved Micheline encoding and decoding performance at least 2x by reimplementing the JSON encoder for primitives
  • written more examples to show how to use contract and operation sending features

Structural changes

With the introduction of Tenderbake in Ithaca there has been a multitude of changes to how transaction data is serialized (new operations and operation tags) and it makes less sense to support older protocols at the level of encoding/signing. TzGo now produces Ithaca compatible operation encodings by default. You can find (and change) the defaults at tezos/params.go:

var (
    // DefaultParams defines the blockchain configuration for Mainnet under the latest
    // protocol. It is used to generate compliant transaction encodings. To change,
    // either overwrite this default or set custom params per operation using
    // op.WithParams().
    DefaultParams = NewParams().
        ForNetwork(Mainnet).
        ForProtocol(ProtoV012_2).
        Mixin(&Params{
            OperationTagsVersion:         2,
            MaxOperationsTTL:             120,
            HardGasLimitPerOperation:     1040000,
            HardGasLimitPerBlock:         5200000,
            OriginationSize:              257,
            CostPerByte:                  250,
            HardStorageLimitPerOperation: 60000,
            MinimalBlockDelay:            30 * time.Second,
        })
)

The RPC package still supports all previous protocols, so that reading old transaction receipts remains possible. As rights and snapshots have significantly changed in Ithaca, the RPC package does a few more calls internally to determine which protocol was active at a requested block if that's important to fetch or decode the correct data.

It turned out that the wallet package we introduced in our tech preview last time was not so useful after all. We decided to move all its logic for completing, simulating and broadcasting transactions into the existing rpc package and added a new signer package with a public interface.

type Signer interface {
	// Return a list of addresses the signer manages.
	ListAddresses(context.Context) ([]tezos.Address, error)

	// Returns the public key for a managed address. Required for reveal ops.
	GetKey(context.Context, tezos.Address) (tezos.Key, error)

	// Sign an arbitrary text message wrapped into a failing noop.
	SignMessage(context.Context, tezos.Address, string) (tezos.Signature, error)

	// Sign an operation.
	SignOperation(context.Context, tezos.Address, *codec.Op) (tezos.Signature, error)

	// Sign a block header.
	SignBlock(context.Context, tezos.Address, *codec.BlockHeader) (tezos.Signature, error)
}

The signer is used by the rpc.Send() and contract.Call() family of functions, but you can always use it directly and attach the signature to an operation with op.WithSignature()

As initial signers we support an in-memory signer that can be created from a private key and a client for the Tezos remote signer interface. Feel free to add your own custom signer implementation, but it should be compatible with the interface above to use the convenience wrappers.

Sending Transactions

We made it easy to send simple transactions and even batches, but at the same time allow full control of all the details. The shortest complete example for transferring tez (ignoring errors) is this:

import (
    "context"
    "blockwatch.cc/tzgo/codec"
    "blockwatch.cc/tzgo/rpc"
    "blockwatch.cc/tzgo/signer"
    "blockwatch.cc/tzgo/tezos"
)

// load key and receiver
sk := tezos.MustParsePrivateKey("...private_key...")
to := tezos.MustParseAddress("...receiver...")

// create an RPC client
c, _ := rpc.NewClient("https://rpc.tzstats.com", nil)

// use private key to sign
c.Signer = signer.NewFromKey(sk)

// construct a transfer operation
op := codec.NewOp().WithTransfer(to, 1_000_000)

// send (will complete reveal, simulate cost, add cost, sign, broadcast, wait)
rcpt, _ := c.Send(ctx, op, nil)

// do smth with receipt, e.g. check for success, read costs
_ = rcpt.IsSuccess()
_ = rcpt.TotalCosts()

For more control, use optional features from the Op struct or provide extra call options as a third parameter to rpc.Client.Send():

// Use different protocol parameters
func (o *Op) WithParams(p *tezos.Params) *Op

// Set a custom TTL or directly set the branch to use
func (o *Op) WithTTL(n int64) *Op
func (o *Op) WithBranch(hash tezos.BlockHash) *Op

// Set custom gas, storage, fees for each operation in a batch
func (o *Op) WithLimits(limits []tezos.Limits, margin int64) *Op

// Use custom call options for rpc.Client.Send()
type CallOptions struct {
    // number of confirmations to wait after broadcast
    Confirmations int64         
    // max acceptable fee, optional (default = 0)
	MaxFee        int64         
    // max lifetime for operations in blocks
	TTL           int64         
    // ignore simulated limits and use user-defined limits from op.WithLimits()
	IgnoreLimits  bool          
    // custom signer to use for signing the transaction
	Signer        signer.Signer 
    // custom address to sign for (use when signer manages multiple addresses)
	Sender        tezos.Address 
    // custom block observer for waiting on confirmations
	Observer      *Observer     
}

TzGo simulates every operation before signing and then uses the simulated gas and storage costs to avoid rejection. In some cases, usually for high throughput dapps that send many transactions per block, it is possible that the Tezos node underestimates storage costs. This is a known and unresolved issue. If this happens frequently, please set custom fees/costs for such operations and disable the use of simulated values with CallOptions.IgnoreLimits = true (make sure you set a reasonable baker fee as well).

If no user-defined fees are set (default), TzGo calculates minimum fee using the minimal fee algorithm published here. Setting IgnoreLimits = true disables this behavior.

Calling Smart Contracts

TzGo v1.12 adds another package contract that simplifies working with smart contracts and FA tokens. It also allows you to batch several contract calls into the same transaction. A simple FA2 transfer can be implemented as

import (
    "context"
    "blockwatch.cc/tzgo/codec"
    "blockwatch.cc/tzgo/rpc"
    "blockwatch.cc/tzgo/signer"
    "blockwatch.cc/tzgo/tezos"
)

// create an RPC client
c, _ := rpc.NewClient("https://rpc.tzstats.com", nil)

// use private key to sign
c.Signer = signer.NewFromKey(tezos.MustParsePrivateKey("SK..."))

// constuct a new contract
con := contract.NewContract(tezos.MustParseAddress("KT1..."), c)

// construct an FA2 token (use token id 0)
token := con.AsFA2(0)

// construct simple transfer arguments
args := token.Transfer(
    tezos.MustParseAddress("tz..."), // from
    tezos.MustParseAddress("tz..."), // to
    tezos.NewZ(1_000_000),           // amount
)

// execute the call (will complete reveal, simulate cost, add cost, sign, broadcast, wait)
rcpt, _ := con.Call(ctx, args, nil)

In more complex scenarios you can add multiple transfers to the same FA2 transfer list

// construct complex transfer args
args := contract.NewFA2TransferArgs()
args.WithTransfer(from_1, to_2, token_id_1, amount_1)
args.WithTransfer(from_2, to_2, token_id_2, amount_2)

// optional, optimize args (for minimal parameter size)
args.Optimize()

// execute the call
rcpt, _ := con.Call(ctx, args, nil)

or you can batch multiple calls

// append multiple calls (silly example, but you get the point)
args := []contract.CallArguments{
    fa2token.AddOperator(owner, operator),
    fa2token.Transfer(from, to, amount),
    fa2token.Transfer(from, to, amount),
    fa2token.RemoveOperator(owner, operator),
}

// execute all calls in a single batch transaction
rcpt, _ := con.CallMulti(ctx, args, nil)

Outlook

With the current release, TzGo has all the tools you need to write complex Tezos applications in Go. A few things are left on our TODO list which will make TzGo even more useful in specific scenarios. Upcoming releases may include

  • a more powerful Observer that can filter by addresses and monitor the Tezos mempool (all the basics already exist in packages tezos and rpc)
  • create keys and signers from mnemonic seed phrases and faucets
  • helpers to access contract storage and bigmaps more easily
  • reading and writing contract/token metadata

Conclusion

If you like TzGo or if you have an idea on how we can improve or extend it in the future, please reach out on Twitter, Discord or email.

Happy building!