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
}