Skip to content

Instantly share code, notes, and snippets.

@lukechampine
Last active July 28, 2025 14:33
Show Gist options
  • Select an option

  • Save lukechampine/e25330d7cc636cd37d4abe4211dedb40 to your computer and use it in GitHub Desktop.

Select an option

Save lukechampine/e25330d7cc636cd37d4abe4211dedb40 to your computer and use it in GitHub Desktop.
Sia Ledger Sweep
package main
import (
"bufio"
"bytes"
"crypto/hmac"
"crypto/sha512"
"encoding/binary"
"flag"
"fmt"
"log"
"os"
"strings"
"go.sia.tech/core/types"
"go.sia.tech/walletd/v2/api"
)
var bipPassphrase string
func mac(key, data []byte) []byte {
hash := hmac.New(sha512.New, key)
hash.Write(data)
return hash.Sum(nil)
}
func pbkdf2(password []byte) []byte {
T := mac(password, binary.BigEndian.AppendUint32([]byte("mnemonic"+bipPassphrase), 1))
U := append([]byte(nil), T...)
for n := 2; n <= 2048; n++ {
U = mac(password, U)
for i := range U {
T[i] ^= U[i]
}
}
return T
}
func bip32(seed []byte, i uint32) types.PrivateKey {
node := mac([]byte("ed25519 seed"), seed)
for _, n := range []uint32{44, 93, i, 0, 0} {
node = mac(node[32:], binary.BigEndian.AppendUint32(append([]byte{0x0}, node[:32]...), 0x80000000|n))
}
return types.NewPrivateKeyFromSeed(node[:32])
}
func readLine() string {
buf, _ := bufio.NewReader(os.Stdin).ReadBytes('\n')
return string(bytes.TrimSpace(buf))
}
func main() {
log.SetFlags(log.Lshortfile)
flag.StringVar(&bipPassphrase, "passphrase", "", "BIP39 passphrase")
flag.Parse()
fmt.Print("Seed: ")
seed := pbkdf2([]byte(readLine()))
wc := api.NewClient("https://api.siascan.com/wallet", "")
fmt.Println("Locating UTXOs...")
again:
var inputs []types.V2SiacoinInput
var inputSum types.Currency
addrs := make(map[types.Address]uint32)
var lastFound uint32
var txnBasis types.ChainIndex
for i := uint32(0); ; i++ {
addr := types.StandardUnlockHash(bip32(seed, uint32(i)).PublicKey())
fmt.Printf("\rChecking address %v (%x...%x)...", i, addr[:4], addr[28:])
utxos, basis, err := wc.AddressSiacoinOutputs(addr, false, 0, 1000)
if err != nil {
log.Fatal(err)
}
if txnBasis == (types.ChainIndex{}) {
txnBasis = basis
} else if txnBasis != basis {
log.Println("\nBasis changed, restarting...")
goto again
}
if len(utxos) > 0 {
fmt.Printf("found %v UTXOs!\n", len(utxos))
addrs[addr] = uint32(i)
for _, utxo := range utxos {
inputs = append(inputs, types.V2SiacoinInput{
Parent: utxo.SiacoinElement,
})
inputSum = inputSum.Add(utxo.SiacoinOutput.Value)
}
lastFound = i
} else if i-lastFound > 10 {
// probably no more addresses
fmt.Println("stopping search.")
break
}
}
if inputSum.IsZero() {
fmt.Println("No UTXOs found.")
return
}
fmt.Printf("Found %v unspent outputs, totaling %v (%d H)\n", len(inputs), inputSum, inputSum)
fmt.Print("Enter destination address: ")
destAddr, err := types.ParseAddress(readLine())
if err != nil {
log.Fatal(err)
}
// construct transaction
minerFee := types.Siacoins(1).Div64(10)
if inputSum.Cmp(minerFee) <= 0 {
log.Fatal("insufficient funds")
}
txn := types.V2Transaction{
SiacoinInputs: inputs,
SiacoinOutputs: []types.SiacoinOutput{{
Address: destAddr,
Value: inputSum.Sub(minerFee),
}},
MinerFee: minerFee,
}
fmt.Println("Note that a 0.1 SC miner fee will be deducted from the output value.")
fmt.Printf("Send %v to %v? [y/n] ", txn.SiacoinOutputs[0].Value, strings.TrimPrefix(txn.SiacoinOutputs[0].Address.String(), "addr:"))
if strings.ToLower(readLine()) != "y" {
return
}
// sign transaction
checkpoint, err := wc.ConsensusCheckpointID(txnBasis.ID)
if err != nil {
log.Fatal(err)
}
cs := checkpoint.State
txid := cs.InputSigHash(txn)
for i := range txn.SiacoinInputs {
sci := &txn.SiacoinInputs[i]
key := bip32(seed, addrs[sci.Parent.SiacoinOutput.Address])
sci.SatisfiedPolicy = types.SatisfiedPolicy{
Policy: types.SpendPolicy{Type: types.PolicyTypeUnlockConditions(types.StandardUnlockConditions(key.PublicKey()))},
Signatures: []types.Signature{key.SignHash(txid)},
}
}
// broadcast
resp, err := wc.TxpoolBroadcast(txnBasis, nil, []types.V2Transaction{txn})
if err != nil {
log.Fatal(err)
} else if len(resp.V2Transactions) == 0 {
log.Println("Transaction has already been confirmed.")
return
}
fmt.Println("Transaction broadcast successfully.")
fmt.Println("Transaction ID:", resp.V2Transactions[0].ID())
}
@lukechampine
Copy link
Author

lukechampine commented Jul 11, 2023

Binaries (last updated: Jul 28 2025)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment