Polkadot
This example shows how to use the Builder Vault TSM as a simple Polkadot wallet using the substrate library. Currently this is not completely up to date so the example have to specify the newest structures for it to work.
The example requires that you have access to a Builder Vault that is configured to allow signing with EdDSA keys, and that you have set up a project that can use the Builder Vault SDK as dependency. See one of our Getting Started guides for more on how to do this.
The code goes through the following steps:
- Create a transfer based on input
- Create a wallet. If a key has been generated the id is loaded from file, otherwise a new key is generated and the id saved. The wallet creates a derived key with chain path
m/42/5
for the wallet. This utilizes the Builder Vault TSM - Setup a blockchain handler to handle requests to the block chain - see below
- Check the balance to see that enough funds are available
- Build a transaction based on the transfer and sign it using the Builder Vault TSM
- Send the transaction
Note
When you run this example the first time, a new random account will be created, and the balance will be 0 DOT. This will cause the program to print out the account address and stop. To actually transfer funds, you will need to first insert some test funds on the account address and then run the program again.
The example uses the derivation path m/42/5
. See our section about key derivation for more about this. See the section about key import if you want to migrate a key from an external wallet, such as Metamask, to the TSM. EdDSA does not have a defined non-hardened derivation, so the account key will probably need to be transferred for the process to work.
Blockchain handler
For interaction with the Polkadot network we are using sidecar on the Blockdaemon’s Ubiquity Native APIs. This is used to access the Westend test network:
apiKey := strings.TrimSpace(os.Getenv("API_KEY"))
ubiquityNativeURL := "https://svc.blockdaemon.com/native/v1/polkadot/westend/"
req, err := http.NewRequest(http.MethodGet, ubiquityNativeURL+path, nil)
if err != nil {
panic(err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer " + apiKey)
response, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
If you use Ubiquity, you need to obtain a Ubiquity API key from Blockdaemon and make sure that it is available as the environment variable API_KEY
, for example by running
export API_KEY=put_your_ubiquity_api_key_here
Alternatively, you can modify the example, so it instead connects to a local Polkadot node that you host yourself, or use another 3rd party Polkadot API provider instead of Blockdaemon Ubiquity.
Running the example
By default the example will transfer 0.01 WND to a default destination address. If you want a different address or amount, you can provide these as parameters:
go run example.go --planck=10000000000 --dstAddress=5E5QrYML2MyZhNg1U72y2YqFC18NSuehU73mCxhducVFaq4R
Code Example
The final code example is here:
package main
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"errors"
"flag"
"fmt"
"github.com/centrifuge/go-substrate-rpc-client/v4/scale"
"github.com/centrifuge/go-substrate-rpc-client/v4/types"
"github.com/centrifuge/go-substrate-rpc-client/v4/types/codec"
subkey "github.com/vedhavyas/go-subkey/v2"
"gitlab.com/Blockdaemon/go-tsm-sdkv2/v65/tsm"
"gitlab.com/Blockdaemon/go-tsm-sdkv2/v65/tsm/tsmutils"
"golang.org/x/crypto/blake2b"
"golang.org/x/sync/errgroup"
"io"
"log"
"math"
"math/big"
"math/bits"
"net/http"
"os"
"strconv"
"strings"
"sync"
)
type PolkadotTransfer struct {
destAddress string
destPublicKey []byte
destNetworkID uint16
amountPlanck *big.Int
}
type PolkadotWallet struct {
nodes []*tsm.Client
threshold int
masterKeyID string
chainPath []uint32
publicKey []byte
networkID uint16
address string
}
type PolkadotUnsignedTransaction struct {
payloadBytes []byte
payload string
extrinsic Extrinsic
era types.ExtrinsicEra
nonceEntry types.UCompact
tipEntry types.UCompact
srcPublicKey types.MultiAddress
}
type PolkadotBlockhain struct {
sidecarURL string
apiKey string
}
func main() {
var destAddress string
var amountPlanckStr string
flagSet := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
flagSet.StringVar(&destAddress, "dstAddress", "5E5QrYML2MyZhNg1U72y2YqFC18NSuehU73mCxhducVFaq4R", "Destination address")
flagSet.StringVar(&amountPlanckStr, "planck", "10000000000", "Amount of planck to transfer") // default 0.01 WND
if err := flagSet.Parse(os.Args[1:]); err != nil {
flagSet.Usage()
os.Exit(1)
}
chainPath := []uint32{42, 5}
threshold := 1 // The security threshold for this key
// Setup parts
transfer := createPolkadotTransfer(destAddress, amountPlanckStr)
wallet := createPolkadotWallet(threshold, chainPath)
blockchain := createPolkadotBlockchain()
// Check balance
balanceInfo := wallet.checkSufficientBalance(blockchain, transfer)
// Build unsigned transaction
unsignedTransaction := blockchain.buildUnsignedTransaction(wallet, transfer, balanceInfo)
// Sign transaction
signature := wallet.signTransaction(unsignedTransaction.payload)
// Build signed transaction (combine the unsigned and signed parts into one)
signedTransaction := buildSignedPolkadotTransaction(unsignedTransaction, signature)
//Send transaction
fmt.Println("Send transaction:")
txHash := blockchain.submitTransaction(signedTransaction)
fmt.Println(" - Transaction hash:", txHash)
fmt.Println(" - Explorer Link...:", fmt.Sprintf("https://westend.subscan.io/extrinsic/%s", txHash))
fmt.Println()
}
func createPolkadotTransfer(destAddress, amountPlanckStr string) *PolkadotTransfer {
fmt.Println("Parse input:")
dstNetworkID, dstPublicKey, err := subkey.SS58Decode(destAddress)
if err != nil {
panic(err)
}
fmt.Println(" - Destination Network:", dstNetworkID)
fmt.Println(" - Destination Pub Key:", hex.EncodeToString(dstPublicKey))
fmt.Println(" - Transfer amount....:", amountPlanckStr)
amountPlanck, ok := new(big.Int).SetString(amountPlanckStr, 10)
if !ok {
fmt.Println("Error:")
fmt.Println("could not parse amount")
os.Exit(1)
}
fmt.Println()
return &PolkadotTransfer{
destAddress: destAddress,
destPublicKey: dstPublicKey,
destNetworkID: dstNetworkID,
amountPlanck: amountPlanck,
}
}
func createPolkadotWallet(threshold int, chainPath []uint32) *PolkadotWallet {
fmt.Println("Setup wallet:")
// Create clients for two MPC nodes
fmt.Println(" - Create clients")
configs := []*tsm.Configuration{
tsm.Configuration{URL: "http://localhost:8500"}.WithAPIKeyAuthentication("apikey0"),
tsm.Configuration{URL: "http://localhost:8501"}.WithAPIKeyAuthentication("apikey1"),
}
clients := make([]*tsm.Client, len(configs))
for i, config := range configs {
var err error
if clients[i], err = tsm.NewClient(config); err != nil {
panic(err)
}
}
fmt.Println(" - Get key for test:")
masterKeyID := getPolkadotKeyID(clients, threshold, "ed-key.txt")
fmt.Println()
fmt.Println("Generate public key and address:")
fmt.Println(" - Chain Path..........:", chainPath)
pkixPublicKeys := make([][]byte, len(clients))
for i, client := range clients {
var err error
pkixPublicKeys[i], err = client.Schnorr().PublicKey(context.TODO(), masterKeyID, chainPath)
if err != nil {
panic(err)
}
}
// Validate public keys
for i := 1; i < len(pkixPublicKeys); i++ {
if bytes.Compare(pkixPublicKeys[0], pkixPublicKeys[i]) != 0 {
panic("public keys do not match")
}
}
pkixPublicKey := pkixPublicKeys[0]
fmt.Println(" - PKIX Key............:", hex.EncodeToString(pkixPublicKey))
// Convert the public key into a Polkadot address (42 is Westend chain id)
publicKeyBytes, err := tsmutils.PKIXPublicKeyToCompressedPoint(pkixPublicKey)
if err != nil {
panic(err)
}
fmt.Println(" - Raw compressed point:", hex.EncodeToString(publicKeyBytes))
// Find the relevant network IDs here: https://wiki.polkadot.network/docs/build-protocol-info
networkID := uint16(42)
address := subkey.SS58Encode(publicKeyBytes, networkID)
fmt.Println(" - Polkadot address....:", address)
fmt.Println()
return &PolkadotWallet{
nodes: clients,
threshold: threshold,
masterKeyID: masterKeyID,
chainPath: chainPath,
publicKey: publicKeyBytes,
networkID: networkID,
address: address,
}
}
func getPolkadotKeyID(clients []*tsm.Client, threshold int, keyFile string) (keyID string) {
keyIDBytes, err := os.ReadFile(keyFile)
if err == nil {
keyID = strings.TrimSpace(string(keyIDBytes))
fmt.Println(" - Read key with ID", keyID, "from file", keyFile)
return keyID
}
fmt.Println(" - No key found, generating new key")
if !errors.Is(err, os.ErrNotExist) {
panic(err)
}
sessionConfig := tsm.NewStaticSessionConfig(tsm.GenerateSessionID(), len(clients))
ctx := context.TODO()
masterKeyIDs := make([]string, len(clients))
var eg errgroup.Group
for i, client := range clients {
client, i := client, i
eg.Go(func() error {
var err error
masterKeyIDs[i], err = client.Schnorr().GenerateKey(ctx, sessionConfig, threshold, "ED-25519", "")
return err
})
}
if err := eg.Wait(); err != nil {
panic(err)
}
for i := 1; i < len(masterKeyIDs); i++ {
if masterKeyIDs[0] != masterKeyIDs[i] {
panic("key IDs do not match")
}
}
keyID = masterKeyIDs[0]
fmt.Println(" - Generated master key (m) with ID", keyID, "; saving to file", keyFile)
err = os.WriteFile(keyFile, []byte(keyID+"\n"), 0644)
if err != nil {
panic(err)
}
return keyID
}
func createPolkadotBlockchain() *PolkadotBlockhain {
// Initialize rpc client
fmt.Println("Setup RPC node:")
apiKey := strings.TrimSpace(os.Getenv("API_KEY"))
if apiKey == "" {
fmt.Println("Error:")
fmt.Println("API_KEY environment variable not set")
os.Exit(1)
}
fmt.Println(" - Found API Key")
fmt.Println()
return &PolkadotBlockhain{
sidecarURL: "https://svc.blockdaemon.com/native/v1/polkadot/westend/",
apiKey: apiKey,
}
}
func (blockchain PolkadotBlockhain) buildUnsignedTransaction(wallet *PolkadotWallet, transfer *PolkadotTransfer, balanceInfo BalanceInfo) *PolkadotUnsignedTransaction {
// Validate input
if wallet.networkID != transfer.destNetworkID {
fmt.Println()
fmt.Println("Error:")
fmt.Println("Destination network not the same as sending:", wallet.networkID, "!=", transfer.destNetworkID)
}
// Generate unsigned transaction
fmt.Println("Get information for transaction:")
tm := blockchain.getTransactionMaterial()
fmt.Println(" - Chain name..:", tm.ChainName)
fmt.Println(" - Spec........:", tm.SpecName)
fmt.Println(" - Spec Version:", tm.SpecVersion)
fmt.Println(" - TX Version..:", tm.TxVersion)
fmt.Println(" - Genesis Hash:", tm.GenesisHash)
fmt.Println(" - Parse metadata")
var meta types.Metadata
err := codec.DecodeFromHex(tm.MetaData, &meta)
if err != nil {
panic(err)
}
fmt.Println(" - Generate destination")
dstMultiAddress, err := types.NewMultiAddressFromAccountID(transfer.destPublicKey)
if err != nil {
panic(err)
}
fmt.Println(" - Generate transfer call")
c, err := types.NewCall(&meta, "Balances.transfer_allow_death", dstMultiAddress, types.NewUCompactFromUInt(transfer.amountPlanck.Uint64()))
if err != nil {
panic(err)
}
srcPublicKey, err := types.NewMultiAddressFromAccountID(wallet.publicKey)
if err != nil {
panic(err)
}
// - Get signature options
fmt.Println(" - Generate signature options")
var genesisHash types.Hash
err = codec.DecodeFromHex(tm.GenesisHash, &genesisHash)
if err != nil {
panic(err)
}
var eraBirthBlockHash types.Hash
err = codec.DecodeFromHex(tm.At.Hash, &eraBirthBlockHash)
if err != nil {
panic(err)
}
currentBlockHeight, err := strconv.ParseUint(tm.At.Height, 10, 64)
if err != nil {
panic(err)
}
specVersion64, err := strconv.ParseUint(tm.SpecVersion, 10, 32)
if err != nil {
panic(err)
}
specVersion := types.U32(specVersion64)
txVersion64, err := strconv.ParseUint(tm.TxVersion, 10, 32)
if err != nil {
panic(err)
}
txVersion := types.U32(txVersion64)
nonce, err := strconv.ParseUint(balanceInfo.Nonce, 10, 64)
if err != nil {
panic(err)
}
// Period must be a power of 2 not too large or other calculations are needed (2^11 is the largest that works)
fmt.Println(" - Mortal Era")
period := 64
phase := currentBlockHeight
trailingZeroes := uint16(bits.TrailingZeros(uint(period)) - 1)
encoded := uint16(phase<<4) | trailingZeroes
fmt.Println(" - Period.........:", period)
fmt.Println(" - Phase..........:", phase)
fmt.Println(" - Trailing Zeroes:", trailingZeroes)
fmt.Println(" - Encoded........:", encoded)
era := GetMortalEra(currentBlockHeight, uint64(period))
nonceEntry := types.NewUCompactFromUInt(nonce)
tipEntry := types.NewUCompactFromUInt(0)
fmt.Println(" - Signature payload")
methodBytes, err := codec.Encode(c)
if err != nil {
panic(err)
}
payload := ExtrinsicPayload{
Method: methodBytes,
Era: era,
Nonce: nonceEntry,
SpecVersion: specVersion,
GenesisHash: genesisHash,
Tip: tipEntry,
BlockHash: eraBirthBlockHash,
TransactionVersion: txVersion,
}
payloadBytes, err := codec.Encode(payload)
if err != nil {
panic(err)
}
fmt.Println(" - Payload........:", hex.EncodeToString(payloadBytes))
if len(payloadBytes) > 256 {
h := blake2b.Sum256(payloadBytes)
payloadBytes = h[:]
fmt.Println(" - Reduced Payload:", hex.EncodeToString(payloadBytes))
}
extrinsic := NewExtrinsic(c)
SetExtrinsicSignatureOptions(&extrinsic, srcPublicKey, types.SignatureOptions{
Era: era,
Nonce: nonceEntry,
Tip: tipEntry,
})
stringPayload := codec.HexEncodeToString(payloadBytes)
encodedTx, err := codec.EncodeToHex(extrinsic)
if err != nil {
panic(err)
}
fmt.Println(" - Unsigned TX....:", encodedTx)
fmt.Println()
return &PolkadotUnsignedTransaction{
// remove 0x
payload: stringPayload[2:],
srcPublicKey: srcPublicKey,
extrinsic: extrinsic,
era: era,
nonceEntry: nonceEntry,
tipEntry: tipEntry,
}
}
func GetMortalEra(eraBirthBlockNumber, validityPeriod uint64) types.ExtrinsicEra {
period, phase := mortal(validityPeriod, eraBirthBlockNumber)
return types.ExtrinsicEra{
IsImmortalEra: false,
IsMortalEra: true,
AsMortalEra: newMortalEra(period, phase),
}
}
func newMortalEra(period, phase uint64) types.MortalEra {
quantizeFactor := math.Max(float64(period>>12), 1)
trailingZeros := bits.TrailingZeros16(uint16(period))
encoded := uint16(float64(phase)/quantizeFactor)<<4 | uint16(math.Min(15, math.Max(1, float64(trailingZeros-1))))
return types.MortalEra{First: byte(encoded & 0xff), Second: byte(encoded >> 8)}
}
// mortal describes a mortal era based on a period of validity and a block number on which it should start
func mortal(validityPeriod, eraBirthBlockNumber uint64) (period, phase uint64) {
calPeriod := math.Pow(2, math.Ceil(math.Log2(float64(validityPeriod))))
period = uint64(math.Min(math.Max(calPeriod, 4), 1<<16))
quantizeFactor := math.Max(float64(period>>12), 1)
quantizedPhase := float64(eraBirthBlockNumber%period) / quantizeFactor * quantizeFactor
phase = uint64(quantizedPhase)
return
}
func SetExtrinsicSignatureOptions(e *Extrinsic, fromPubKey types.MultiAddress, o types.SignatureOptions) {
extSig := ExtrinsicSignature{
Signer: fromPubKey,
Signature: types.MultiSignature{IsEd25519: true, AsEd25519: types.Signature{}},
Era: o.Era,
Nonce: o.Nonce,
Tip: o.Tip,
}
e.Signature = extSig
e.Version |= types.ExtrinsicBitSigned
}
func (wallet PolkadotWallet) signTransaction(payloadString string) []byte {
payload, _ := hex.DecodeString(payloadString)
fmt.Println("Signing transaction using Builder Vault")
fmt.Println(" - Do signing")
partialSignaturesLock := sync.Mutex{}
partialSignatures := make([][]byte, 0)
sessionConfig := tsm.NewStaticSessionConfig(tsm.GenerateSessionID(), len(wallet.nodes))
var eg errgroup.Group
for _, client := range wallet.nodes {
client := client
eg.Go(func() error {
partialSignResult, err := client.Schnorr().Sign(context.TODO(), sessionConfig, wallet.masterKeyID, wallet.chainPath, payload)
if err != nil {
return err
}
partialSignaturesLock.Lock()
partialSignatures = append(partialSignatures, partialSignResult.PartialSignature)
partialSignaturesLock.Unlock()
return nil
})
}
if err := eg.Wait(); err != nil {
panic(err)
}
fmt.Println(" - Combine signatures")
signatureBytes, err := tsm.SchnorrFinalizeSignature(payload, partialSignatures)
if err != nil {
panic(err)
}
fmt.Println(" - Signature:", hex.EncodeToString(signatureBytes))
fmt.Println()
return signatureBytes
}
func (wallet PolkadotWallet) checkSufficientBalance(blockchain *PolkadotBlockhain, transfer *PolkadotTransfer) BalanceInfo {
fmt.Println("Get balance of account:")
balanceInfo := blockchain.getBalanceInfo(wallet.address)
fmt.Println(" - Balance:", balanceInfo.Free)
balance := new(big.Int)
balance.SetString(balanceInfo.Free, 10)
if transfer.amountPlanck.Cmp(balance) > 0 {
fmt.Println("Error:")
fmt.Println("Insufficient funds.")
fmt.Println("Insert additional funds at address", wallet.address, ", e.g. by visiting https://faucet.polkadot.io/")
fmt.Println("Then run this program again.")
os.Exit(0)
}
fmt.Println()
return balanceInfo
}
func buildSignedPolkadotTransaction(transaction *PolkadotUnsignedTransaction, signatureBytes []byte) string {
fmt.Println("Combine signed transaction:")
fmt.Println(" - Build signature structure")
signature := ExtrinsicSignature{
Signer: transaction.srcPublicKey,
Signature: types.MultiSignature{IsEd25519: true, AsEd25519: types.NewSignature(signatureBytes)},
Era: transaction.era,
Nonce: transaction.nonceEntry,
Tip: transaction.tipEntry,
}
fmt.Println(" - Insert signature in transaction")
extrinsic := transaction.extrinsic
extrinsic.Signature = signature
if extrinsic.IsSigned() {
extrinsic.Version |= types.ExtrinsicBitSigned
}
extrinsicHex, err := codec.EncodeToHex(extrinsic)
if err != nil {
panic(err)
}
fmt.Println(" - Created signed transaction:", extrinsicHex)
fmt.Println()
return extrinsicHex
}
type At struct {
Hash string `json:"hash"`
Height string `json:"height"`
}
type BalanceInfo struct {
Nonce string `json:"nonce"`
TokenSymbol string `json:"tokenSymbol"`
// Free is the balance of the account. Not equivalent to spendable balance. This is the only balance that matters
// in terms of most operations on tokens.
Free string `json:"free"`
Reserved string `json:"reserved"`
// MiscFrozen is the amount that free may not drop below when withdrawing for anything except transaction fee
// payment.
MiscFrozen string `json:"miscFrozen"`
// FeeFrozen is the amount that free may not drop below when withdrawing specifically for transaction fee payment.
FeeFrozen string `json:"feeFrozen"`
At At `json:"at"`
}
// TransactionMaterial contains stuff used to constructing any signed transaction offline.
type TransactionMaterial struct {
At At `json:"at"`
GenesisHash string `json:"genesisHash"`
ChainName string `json:"chainName"`
SpecName string `json:"specName"`
SpecVersion string `json:"specVersion"`
TxVersion string `json:"txVersion"`
MetaData string `json:"metadata"`
}
func (blockchain *PolkadotBlockhain) submitTransaction(txHex string) (txHash string) {
url := "/transaction"
resBody := blockchain.httpPost(url, `{"tx": "`+txHex+`"}`)
fmt.Println("response:", resBody)
var hash struct {
Hash string `json:"hash"`
}
if err := json.Unmarshal([]byte(resBody), &hash); err != nil {
panic(err)
}
return hash.Hash
}
func (blockchain *PolkadotBlockhain) getBalanceInfo(address string) BalanceInfo {
jsonBody := blockchain.httpGet("/accounts/" + address + "/balance-info")
var bi BalanceInfo
if err := json.Unmarshal([]byte(jsonBody), &bi); err != nil {
fmt.Println("Failed to parse balance info:", jsonBody)
panic(err)
}
return bi
}
func (blockchain *PolkadotBlockhain) getTransactionMaterial() TransactionMaterial {
resBody := blockchain.httpGet("/transaction/material?metadata=scale")
var tm TransactionMaterial
if err := json.Unmarshal([]byte(resBody), &tm); err != nil {
fmt.Println("Failed to parse tx transaction material:", resBody)
panic(err)
}
return tm
}
func (blockchain *PolkadotBlockhain) httpPost(url, body string) string {
jsonBody := []byte(body)
bodyReader := bytes.NewReader(jsonBody)
req, err := http.NewRequest(http.MethodPost, blockchain.sidecarURL+url, bodyReader)
if err != nil {
log.Fatalln("HTTPPost: could not create request:", err)
}
req.Header.Set("Content-Type", "application/json")
if blockchain.apiKey != "" {
req.Header.Set("Authorization", "Bearer "+blockchain.apiKey)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatalln("HTTPPost: error making http request:", err)
}
resBody, err := io.ReadAll(res.Body)
if err != nil {
log.Fatalln("HTTPPost: could not read response body:", err, res.Status)
}
if res.StatusCode != http.StatusOK {
log.Fatalln("HTTPPost:", res.StatusCode, res.Status, string(resBody))
}
var prettyJSON bytes.Buffer
if err = json.Indent(&prettyJSON, resBody, "", " "); err != nil {
log.Fatalln("HTTPPost: bad json:", err)
}
r := string(prettyJSON.Bytes())
return r
}
func (blockchain *PolkadotBlockhain) httpGet(url string) string {
req, err := http.NewRequest(http.MethodGet, blockchain.sidecarURL+url, nil)
if err != nil {
log.Fatalln("HTTPGet: could not create request:", err)
}
req.Header.Set("Content-Type", "application/json")
if blockchain.apiKey != "" {
req.Header.Set("Authorization", "Bearer "+blockchain.apiKey)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatalln("HTTPGet: error making http request:", err)
}
resBody, err := io.ReadAll(res.Body)
if err != nil {
log.Fatalln("HTTPGet: could not read response body:", err, res.Status)
}
if res.StatusCode != http.StatusOK {
log.Fatalln("HTTPGet:", res.StatusCode, res.Status, string(resBody))
}
var prettyJSON bytes.Buffer
if err = json.Indent(&prettyJSON, resBody, "", " "); err != nil {
log.Fatalln("HTTPGet: bad json:", err)
}
r := string(prettyJSON.Bytes())
return r
}
////////////////////////////////////////////////////////////////////////////////////////
// NEWER SUBSTRATE TYPES REQUIRED, THAT ARE NOT IN OFFICIAL SUBSTRATE LIB YET
////////////////////////////////////////////////////////////////////////////////////////
type ExtrinsicPayload struct {
Method types.BytesBare
Era types.ExtrinsicEra // extra via system::CheckEra
Nonce types.UCompact // extra via system::CheckNonce (Compact<Index> where Index is u32)
Tip types.UCompact // extra via balances::TakeFees (Compact<Balance> where Balance is u128)
SpecVersion types.U32 // additional via system::CheckVersion
GenesisHash types.Hash // additional via system::CheckGenesis
BlockHash types.Hash // additional via system::CheckEra
TransactionVersion types.U32
Mode types.UCompact
MetadataHash types.OptionHash
}
func (e ExtrinsicPayload) Encode(encoder scale.Encoder) error {
err := encoder.Encode(e.Method)
if err != nil {
return err
}
err = encoder.Encode(e.Era)
if err != nil {
return err
}
err = encoder.Encode(e.Nonce)
if err != nil {
return err
}
err = encoder.Encode(e.Tip)
if err != nil {
return err
}
err = encoder.Encode(e.Mode)
if err != nil {
return err
}
err = encoder.Encode(e.SpecVersion)
if err != nil {
return err
}
err = encoder.Encode(e.TransactionVersion)
if err != nil {
return err
}
err = encoder.Encode(e.GenesisHash)
if err != nil {
return err
}
err = encoder.Encode(e.BlockHash)
if err != nil {
return err
}
err = encoder.Encode(e.MetadataHash)
if err != nil {
return err
}
return nil
}
type ExtrinsicSignature struct {
Signer types.MultiAddress
Signature types.MultiSignature
Era types.ExtrinsicEra // extra via system::CheckEra
Nonce types.UCompact // extra via system::CheckNonce (Compact<Index> where Index is u32))
Tip types.UCompact // extra via balances::TakeFees (Compact<Balance> where Balance is u128))
Mode types.UCompact
}
type Extrinsic struct {
// Version is the encoded version flag (which encodes the raw transaction version and signing information in one byte)
Version byte
// Signature is the ExtrinsicSignatureV4, it's presence depends on the Version flag
Signature ExtrinsicSignature
// Method is the call this extrinsic wraps
Method types.Call
}
func NewExtrinsic(c types.Call) Extrinsic {
return Extrinsic{
Version: types.ExtrinsicVersion4,
Method: c,
}
}
// IsSigned returns true if the extrinsic is signed
func (e Extrinsic) IsSigned() bool {
return e.Version&types.ExtrinsicBitSigned == types.ExtrinsicBitSigned
}
// Type returns the raw transaction version (not flagged with signing information)
func (e Extrinsic) Type() uint8 {
return e.Version & types.ExtrinsicUnmaskVersion
}
func (e *Extrinsic) Decode(decoder scale.Decoder) error {
// compact length encoding (1, 2, or 4 bytes) (may not be there for Extrinsics older than Jan 11 2019)
_, err := decoder.DecodeUintCompact()
if err != nil {
return err
}
// version, signature bitmask (1 byte)
err = decoder.Decode(&e.Version)
if err != nil {
return err
}
// signature
if e.IsSigned() {
if e.Type() != types.ExtrinsicVersion4 {
return fmt.Errorf("unsupported extrinsic version: %v (isSigned: %v, type: %v)", e.Version, e.IsSigned(),
e.Type())
}
err = decoder.Decode(&e.Signature)
if err != nil {
return err
}
}
// call
err = decoder.Decode(&e.Method)
if err != nil {
return err
}
return nil
}
func (e Extrinsic) Encode(encoder scale.Encoder) error {
if e.Type() != types.ExtrinsicVersion4 {
return fmt.Errorf("unsupported extrinsic version: %v (isSigned: %v, type: %v)", e.Version, e.IsSigned(),
e.Type())
}
// create a temporary buffer that will receive the plain encoded transaction (version, signature (optional),
// method/call)
var bb = bytes.Buffer{}
tempEnc := scale.NewEncoder(&bb)
// encode the version of the extrinsic
err := tempEnc.Encode(e.Version)
if err != nil {
return err
}
// encode the signature if signed
if e.IsSigned() {
err = tempEnc.Encode(e.Signature)
if err != nil {
return err
}
}
// encode the method
err = tempEnc.Encode(e.Method)
if err != nil {
return err
}
// take the temporary buffer to determine length, write that as prefix
eb := bb.Bytes()
err = encoder.EncodeUintCompact(*big.NewInt(0).SetUint64(uint64(len(eb))))
if err != nil {
return err
}
// write the actual encoded transaction
err = encoder.Write(eb)
if err != nil {
return err
}
return nil
}
Updated 28 days ago