A small Go program that sends a confirmed transaction on the Mintlayer testnet, end to end: derive a key from a mnemonic, fetch UTXOs, build and sign a transfer, broadcast it, and wait for confirmation. It uses the official mintlayer/go-sdk and talks only to the public indexer over HTTP, so there's no node or wallet daemon to run.
I put this together to get hands-on with the SDK and understand the UTXO flow. Mintlayer is a Bitcoin sidechain, so it's UTXO-based (like Bitcoin) rather than account-based like Ethereum, and non-EVM. My notes from getting it working are at the bottom.
The flow: mnemonic → address → fetch UTXOs → build tx (recipient + change) → sign each input → broadcast → poll for confirmation → explorer link.
- Go 1.21+ (
go version) - A throwaway testnet mnemonic. Don't use a mainnet one; it's a private key and gets passed on the command line.
- Testnet coins from https://faucet.mintlayer.org (step 2).
git clone https://github.com/oliv3rdrt/mintlayer-quickstart.git
cd mintlayer-quickstart
go test ./srcTestBuildSignOffline builds and signs a real transaction against the SDK's embedded WASM crypto with a mocked indexer, so the whole path is covered before spending anything.
go run ./src -mnemonic "your twelve word testnet mnemonic ..." \
-amount 100000000 -broadcast=falseThis prints spending from: tmt1q... and then reports there are no UTXOs yet. Copy the address, request coins at https://faucet.mintlayer.org, and give it a block to arrive.
To keep the mnemonic out of your shell history, export it instead (every command below also reads ML_MNEMONIC):
export ML_MNEMONIC="your twelve word testnet mnemonic ..."go run ./src -amount 100000000 -waitspending from: tmt1q...
no -to given, sending to self
found 1 UTXO(s), balance 200 ML
selected 1 input(s), total 200 ML, change 199.499 ML, fee 0.5 ML
signed tx id: <txid> (... bytes)
broadcast, tx id: <txid>
explorer: https://lovelace.explorer.mintlayer.org/tx/<txid>
waiting for confirmation...
confirmed: https://lovelace.explorer.mintlayer.org/tx/<txid>
Open the explorer link to verify it on-chain.
| Flag | Default | Meaning |
|---|---|---|
-mnemonic |
$ML_MNEMONIC |
BIP-39 mnemonic (12/24 words) |
-amount |
required | Amount in atoms (1 ML = 100,000,000,000 atoms) |
-to |
your own address | Recipient tmt1q... address |
-fee |
50000000000 (0.5 ML) |
Flat fee in atoms, taken from change |
-indexer |
public Lovelace indexer | Indexer base URL ($ML_INDEXER) |
-wait |
off | Poll until confirmed |
-broadcast |
true |
Set -broadcast=false for a dry run: build and sign, print the hex, don't submit |
src/integration_test.go does a real testnet transaction. It's behind the integration build tag and skips unless ML_MNEMONIC is set, so a normal go test never spends coins:
export ML_MNEMONIC="your funded testnet mnemonic ..."
go test -tags integration -run TestSendTestnetTransaction -v ./srcSee .env.example for the supported environment variables.
src/main.go build → sign → broadcast flow
src/transfer.go coin/UTXO helpers (no crypto, no network)
src/transfer_test.go unit tests, incl. an offline build+sign
src/integration_test.go live testnet test (build tag: integration)
Things I ran into, mostly small, in case they're useful to anyone else:
- The SDK's
send-coinsexample hardcodesMainnet. On testnet you have to passTestneteverywhere, otherwise you derive a mainnet-prefixed address that owns nothing and it fails with no obvious reason why. - Signing kept failing with
invalid type: null, expected a mapuntil I realisedTxAdditionalInfo{}marshals itsPoolInfo/OrderInfomaps asnull. Initialising them as empty maps fixes it. This one cost me the most time because the error doesn't point at the cause. - I couldn't find the testnet indexer URL in the docs. I pulled it from the explorer subdomain (
api-server-lovelace.mintlayer.org) and confirmed it with/api/v2/chain/tip. A short "testnet endpoints" table in the docs would help. - The REST indexer has no fee-rate endpoint (the node RPC does), so an indexer-only client can't look up the minimum fee. My first broadcast got rejected:
min_relay_fee: 0.20300000000for a 203-byte tx, i.e. about 0.001 ML/byte. Since I can't query that rate, I use a flat fee (0.5 ML by default) deducted from change, overridable with-fee. - The example sends all UTXOs into one output with no change, so it can't really do a partial send or pay an explicit fee. I added simple coin selection and a change output.
Overall the Go SDK was nice to work with: go get, embedded WASM, no CGO, and it built and ran first try. The rough edges are mostly docs and examples.
MIT.