Repository navigation
Expand file tree
/
Copy pathmain.go
More file actions
259 lines (225 loc) · 9.2 KB
/
Copy pathmain.go
File metadata and controls
259 lines (225 loc) · 9.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
// Copyright (c) 2026 Mintlayer Institutional FZCO
// Contact: hello@mintlayer.org
//
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
// Command send-coins demonstrates the full manual transaction flow using the
// Mintlayer Go SDK:
//
// 1. Derive an account key and receiving address from a BIP-39 mnemonic.
// 2. Fetch spendable UTXOs for that address from the indexer.
// 3. Build an unsigned transaction (encode inputs and outputs).
// 4. Sign each input with EncodeWitness.
// 5. Submit the signed transaction to the indexer.
//
// Usage:
//
// go run ./examples/send-coins \
// -mnemonic "word1 word2 ... word12" \
// -to mxtc1qrecipient... \
// -amount 100000000000 \
// -indexer http://127.0.0.1:3000
//
// NOTE: This example sends ALL spendable UTXOs to the recipient with no change
// output. It is intentionally minimal. Production code should select UTXOs,
// compute fees, add a change output, and handle errors more robustly.
package main
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"log"
"math/big"
"strings"
"github.com/mintlayer/go-sdk/indexer"
mintlayer "github.com/mintlayer/go-sdk/wasm"
)
func main() {
mnemonic := flag.String("mnemonic", "", "BIP-39 mnemonic (12 or 24 words)")
toAddr := flag.String("to", "", "recipient bech32m address")
amountAtoms := flag.String("amount", "", "amount to send in atoms (e.g. 100000000000 = 1 ML)")
indexerURL := flag.String("indexer", "http://127.0.0.1:3000", "indexer base URL")
keyIndex := flag.Uint("key-index", 0, "receiving address key index")
flag.Parse()
if *mnemonic == "" || *toAddr == "" || *amountAtoms == "" {
flag.Usage()
log.Fatal("required: -mnemonic, -to, -amount")
}
ctx := context.Background()
// ── 1. Initialise the WASM cryptography runtime ───────────────────────────
wasm, err := mintlayer.New(ctx)
must(err, "init wasm")
defer wasm.Close()
// ── 2. Derive the spending key and address ────────────────────────────────
accountKey, err := wasm.MakeDefaultAccountPrivkey(*mnemonic, mintlayer.Mainnet)
must(err, "derive account key")
spendKey, err := wasm.MakeReceivingAddress(accountKey, uint32(*keyIndex))
must(err, "derive receiving key")
pubKey, err := wasm.PublicKeyFromPrivateKey(spendKey)
must(err, "derive public key")
fromAddr, err := wasm.PubkeyToPubkeyHashAddress(pubKey, mintlayer.Mainnet)
must(err, "derive address")
log.Printf("spending from: %s", fromAddr)
// ── 3. Fetch spendable UTXOs ──────────────────────────────────────────────
idxClient := indexer.New(*indexerURL)
utxos, err := idxClient.GetSpendableUTXOs(ctx, fromAddr)
must(err, "fetch UTXOs")
if len(utxos) == 0 {
log.Fatalf("no spendable UTXOs for %s", fromAddr)
}
log.Printf("found %d spendable UTXO(s)", len(utxos))
// Verify we have enough balance.
total := totalAtoms(utxos)
sendAmt := mustParseBig(*amountAtoms)
if total.Cmp(sendAmt) < 0 {
log.Fatalf("insufficient balance: have %s atoms, need %s atoms", total, sendAmt)
}
// ── 4. Encode inputs and collect per-input UTXO bytes ────────────────────
//
// Each input is: EncodeOutpointSourceId → EncodeInputForUtxo.
// The combined inputs slice is the concatenation of all encoded inputs.
//
// We also build the inputUtxos slice used during signing. It is a
// concatenation of one entry per input, each prefixed by:
// 0x00 — no UTXO output data (fallback)
// 0x01 — UTXO output data follows (required for correct sighash)
var (
encodedInputs []byte
allUtxoBytes []byte // one entry per input for the witness call
)
for _, u := range utxos {
// Hex-decode the source transaction ID.
txIDHex := u.Outpoint.SourceID
txIDBytes, err := hex.DecodeString(txIDHex)
must(err, "decode tx id hex")
// EncodeOutpointSourceId packs the tx hash + discriminant.
srcID, err := wasm.EncodeOutpointSourceId(txIDBytes, mintlayer.SourceTransaction)
must(err, "encode outpoint source id")
// EncodeInputForUtxo builds the TxInput binary.
inp, err := wasm.EncodeInputForUtxo(srcID, u.Outpoint.Index)
must(err, "encode input for utxo")
encodedInputs = append(encodedInputs, inp...)
// Re-encode the UTXO output binary so the sighash covers the correct
// value. For Transfer outputs we parse the JSON amount and destination.
utxoEntry := encodeUTXOEntry(wasm, u.Output, mintlayer.Mainnet)
allUtxoBytes = append(allUtxoBytes, utxoEntry...)
}
// ── 5. Encode the transfer output ────────────────────────────────────────
output, err := wasm.EncodeOutputTransfer(
mintlayer.NewAmount(*amountAtoms),
*toAddr,
mintlayer.Mainnet,
)
must(err, "encode output transfer")
// ── 6. Build the unsigned transaction ────────────────────────────────────
tx, err := wasm.EncodeTransaction(encodedInputs, output, 0 /*flags*/)
must(err, "encode transaction")
txID, err := wasm.GetTransactionID(tx, true)
must(err, "get transaction id")
log.Printf("unsigned tx id: %s", txID)
// ── 7. Sign each input and collect witnesses ──────────────────────────────
var witnessBytes []byte
for i := range utxos {
w, err := wasm.EncodeWitness(
mintlayer.SigHashAll,
spendKey,
fromAddr,
tx,
allUtxoBytes,
uint32(i),
mintlayer.TxAdditionalInfo{},
0, // block height (0 = no lock-time constraint)
mintlayer.Mainnet,
)
must(err, fmt.Sprintf("sign input %d", i))
witnessBytes = append(witnessBytes, w...)
}
// ── 8. Assemble the signed transaction ───────────────────────────────────
signedTx, err := wasm.EncodeSignedTransaction(tx, witnessBytes)
must(err, "encode signed transaction")
signedHex := hex.EncodeToString(signedTx)
log.Printf("signed tx (%d bytes)", len(signedTx))
// ── 9. Submit ─────────────────────────────────────────────────────────────
submittedTxID, err := idxClient.SubmitTransaction(ctx, signedHex)
must(err, "submit transaction")
fmt.Printf("submitted: %s\n", submittedTxID)
}
// ── helpers ───────────────────────────────────────────────────────────────────
func must(err error, context string) {
if err != nil {
log.Fatalf("%s: %v", context, err)
}
}
// mustParseBig parses a decimal atom string into a *big.Int.
func mustParseBig(s string) *big.Int {
n := new(big.Int)
if _, ok := n.SetString(strings.TrimSpace(s), 10); !ok {
log.Fatalf("invalid atom amount: %q", s)
}
return n
}
// totalAtoms sums the coin atoms across all UTXOs.
func totalAtoms(utxos []indexer.UTXO) *big.Int {
total := new(big.Int)
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"`
}
if err := json.Unmarshal(u.Output, &raw); err != nil {
continue
}
if raw.Type == "Transfer" && raw.Value.Type == "Coin" {
n := mustParseBig(raw.Value.Amount.Atoms)
total.Add(total, n)
}
}
return total
}
// encodeUTXOEntry re-encodes a JSON UTXO output into the binary form that
// EncodeWitness expects in its inputUtxos slice.
//
// Format: 0x01 + <encoded output bytes> when the output can be re-encoded,
// 0x00 otherwise (signing will still succeed for many output types).
func encodeUTXOEntry(c *mintlayer.Client, utxoJSON json.RawMessage, network mintlayer.Network) []byte {
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(utxoJSON, &raw); err != nil {
return []byte{0x00}
}
if raw.Type == "Transfer" && raw.Value.Type == "Coin" {
encoded, err := c.EncodeOutputTransfer(
mintlayer.NewAmount(raw.Value.Amount.Atoms),
raw.Destination,
network,
)
if err == nil {
return append([]byte{0x01}, encoded...)
}
}
// For non-Transfer outputs or encoding errors, omit the UTXO data.
// The witness will still be computed using the destination passed directly
// to EncodeWitness; some output types do not require the full UTXO body.
return []byte{0x00}
}
// ─── EstimatedFee (informational) ─────────────────────────────────────────────
// In production, call wasm.EstimateTransactionSize and multiply by the fee
// rate obtained from node.GetFeeRate or indexer.GetFeeRate to compute the
// exact fee before building outputs. Deduct it from the change output.
var _ = bytes.Compare // suppress unused import