The wallet daemon handles transaction building automatically for most use cases. Use the wasm package directly when you need:
- Full custody (no wallet daemon)
- Custom output types or complex spending conditions
- Integration testing or tooling
The examples/send-coins/ program demonstrates this flow end to end.
Building a transaction manually requires these steps:
- Derive the spending key and address from a mnemonic
- Fetch spendable UTXOs from the indexer
- Encode each input as binary
- Encode each output as binary
- Build the unsigned transaction
- Sign each input to produce witness bytes
- Assemble the signed transaction
- Submit to the network
import mintlayer "github.com/mintlayer/go-sdk/wasm"
ctx := context.Background()
c, err := mintlayer.New(ctx)
if err != nil {
log.Fatal(err)
}
defer c.Close()
mnemonic := "word1 word2 ... word12"
accountKey, err := c.MakeDefaultAccountPrivkey(mnemonic, mintlayer.Mainnet)
if err != nil {
log.Fatal(err)
}
// key index 0 = first receiving address
spendKey, err := c.MakeReceivingAddress(accountKey, 0)
pubKey, err := c.PublicKeyFromPrivateKey(spendKey)
fromAddr, err := c.PubkeyToPubkeyHashAddress(pubKey, mintlayer.Mainnet)import "github.com/mintlayer/go-sdk/indexer"
idxClient := indexer.New("http://127.0.0.1:3000")
utxos, err := idxClient.GetSpendableUTXOs(ctx, fromAddr)
if err != nil {
log.Fatal(err)
}
if len(utxos) == 0 {
log.Fatal("no spendable UTXOs")
}Each input requires:
- Hex-decode the source transaction ID
- Encode the outpoint source ID (
EncodeOutpointSourceId) - Encode the input (
EncodeInputForUtxo)
import "encoding/hex"
var encodedInputs []byte
for _, u := range utxos {
txIDBytes, err := hex.DecodeString(u.Outpoint.SourceID)
if err != nil {
log.Fatal(err)
}
srcID, err := c.EncodeOutpointSourceId(txIDBytes, mintlayer.SourceTransaction)
if err != nil {
log.Fatal(err)
}
inp, err := c.EncodeInputForUtxo(srcID, u.Outpoint.Index)
if err != nil {
log.Fatal(err)
}
encodedInputs = append(encodedInputs, inp...)
}output, err := c.EncodeOutputTransfer(
mintlayer.NewAmount("100000000000"), // 1 ML in atoms
"mxtc1qrecipient...",
mintlayer.Mainnet,
)
if err != nil {
log.Fatal(err)
}For multiple outputs, concatenate them:
changeOutput, err := c.EncodeOutputTransfer(
mintlayer.NewAmount(changeAtoms.String()),
fromAddr, // send change back to sender
mintlayer.Mainnet,
)
allOutputs := append(output, changeOutput...)tx, err := c.EncodeTransaction(encodedInputs, output, 0 /*flags*/)
if err != nil {
log.Fatal(err)
}
txID, err := c.GetTransactionID(tx, true)
log.Printf("unsigned tx id: %s", txID)The sighash computation requires access to the UTXO being spent. Build a per-input slice where each entry is either:
0x01followed by the re-encoded output bytes (recommended for coin transfers)0x00alone (acceptable for some output types)
var allUtxoBytes []byte
for _, u := range utxos {
var raw struct {
Type string `json:"type"`
Value struct {
Type string `json:"type"`
Amount struct {
Atoms string `json:"atoms"`
} `json:"amount"`
} `json:"value"`
Destination string `json:"destination"`
}
if err := json.Unmarshal(u.Output, &raw); err != nil {
allUtxoBytes = append(allUtxoBytes, 0x00)
continue
}
if raw.Type == "Transfer" && raw.Value.Type == "Coin" {
encoded, err := c.EncodeOutputTransfer(
mintlayer.NewAmount(raw.Value.Amount.Atoms),
raw.Destination,
mintlayer.Mainnet,
)
if err == nil {
allUtxoBytes = append(allUtxoBytes, 0x01)
allUtxoBytes = append(allUtxoBytes, encoded...)
continue
}
}
allUtxoBytes = append(allUtxoBytes, 0x00)
}Call EncodeWitness once per input. Concatenate results.
var witnessBytes []byte
for i := range utxos {
w, err := c.EncodeWitness(
mintlayer.SigHashAll,
spendKey,
fromAddr,
tx,
allUtxoBytes,
uint32(i),
mintlayer.TxAdditionalInfo{}, // empty for standard transfers
0, // block height (0 when no timelock constraint)
mintlayer.Mainnet,
)
if err != nil {
log.Fatalf("sign input %d: %v", i, err)
}
witnessBytes = append(witnessBytes, w...)
}signedTx, err := c.EncodeSignedTransaction(tx, witnessBytes)
if err != nil {
log.Fatal(err)
}
signedHex := hex.EncodeToString(signedTx)
// Submit via the indexer (requires --enable-post-routes)
submittedTxID, err := idxClient.SubmitTransaction(ctx, signedHex)
if err != nil {
log.Fatal(err)
}
fmt.Printf("submitted: %s\n", submittedTxID)
// Alternative: broadcast via the node daemon
// err = nodeClient.P2PSubmitTransaction(ctx, signedHex, node.TrustPolicyUntrusted)Compute the fee before constructing outputs so you can deduct it from the change:
// Collect destination addresses (one per input, in input order)
destAddresses := make([]string, len(utxos))
for i := range utxos {
destAddresses[i] = fromAddr
}
estimatedSize, err := c.EstimateTransactionSize(encodedInputs, destAddresses, allOutputs, mintlayer.Mainnet)
if err != nil {
log.Fatal(err)
}
// GetFeeRate returns atoms per KB for the top 1 MB of the mempool
feeRateStr, err := idxClient.GetFeeRate(ctx, 1)
if err != nil {
log.Fatal(err)
}
feeRate, _ := new(big.Int).SetString(feeRateStr, 10)
sizeKB := new(big.Int).SetUint64(uint64(estimatedSize))
fee := new(big.Int).Mul(sizeKB, feeRate)
fee.Div(fee, big.NewInt(1000))
// Subtract fee from the amount going to the recipient or from the change output.To send coins that cannot be spent for a period of time:
// Unlock after 1000 blocks
lock, err := c.EncodeLockForBlockCount(1000)
output, err := c.EncodeOutputLockThenTransfer(
mintlayer.NewAmount("100000000000"),
"mxtc1qrecipient...",
lock,
mintlayer.Mainnet,
)Sending fungible tokens uses the same flow, with a different output encoder:
tokenOutput, err := c.EncodeOutputTokenTransfer(
mintlayer.NewAmount("1000"), // token amount in smallest units
"mxtc1qrecipient...",
"ttml1tokenid...",
mintlayer.Mainnet,
)Note that a token transfer transaction must also include a coin output (or coin inputs) to cover the network fee.