-
-
Save lukechampine/e25330d7cc636cd37d4abe4211dedb40 to your computer and use it in GitHub Desktop.
Sia Ledger Sweep
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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()) | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Binaries (last updated: Jul 28 2025)