Skip to content

Instantly share code, notes, and snippets.

@lukechampine
Last active April 26, 2024 05:07
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lukechampine/e25330d7cc636cd37d4abe4211dedb40 to your computer and use it in GitHub Desktop.
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"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"go.sia.tech/core/chain"
"go.sia.tech/core/types"
)
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"), 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)
fmt.Print("Seed: ")
seed := pbkdf2([]byte(readLine()))
fmt.Println("Locating UTXOs...")
var inputs []types.SiacoinInput
var inputSum types.Currency
addrs := make(map[types.Address]uint32)
var lastFound uint32
for i := uint32(0); ; i++ {
fmt.Printf("\rChecking address %v...", i)
uc := types.StandardUnlockConditions(bip32(seed, uint32(i)).PublicKey())
addr := uc.UnlockHash()
var resp struct {
SiacoinOutputs []struct{
ID types.SiacoinOutputID `json:"output_id"`
Value types.Currency `json:"value"`
} `json:"siacoin_outputs"`
}
r, err := http.Get(fmt.Sprintf("https://api.siacentral.com/v2/wallet/addresses/%v/utxos/siacoin", strings.TrimPrefix(addr.String(), "addr:")))
if err != nil {
log.Fatal(err)
} else if err := json.NewDecoder(r.Body).Decode(&resp); err != nil {
log.Fatal(err)
}
r.Body.Close()
utxos := resp.SiacoinOutputs
if len(utxos) > 0 {
fmt.Printf("found %v UTXOs!\n", len(utxos))
addrs[addr] = uint32(i)
for _, utxo := range utxos {
inputs = append(inputs, types.SiacoinInput{
ParentID: utxo.ID,
UnlockConditions: uc,
})
inputSum = inputSum.Add(utxo.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.Transaction{
SiacoinInputs: inputs,
SiacoinOutputs: []types.SiacoinOutput{{
Address: destAddr,
Value: inputSum.Sub(minerFee),
}},
MinerFees: []types.Currency{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
n, _ := chain.Mainnet()
cs := n.GenesisState()
cs.Index.Height = n.HardforkFoundation.Height + 1
for _, sci := range txn.SiacoinInputs {
sig := bip32(seed, addrs[sci.UnlockConditions.UnlockHash()]).SignHash(cs.WholeSigHash(txn, types.Hash256(sci.ParentID), 0, 0, nil))
txn.Signatures = append(txn.Signatures, types.TransactionSignature{
ParentID: types.Hash256(sci.ParentID),
PublicKeyIndex: 0,
CoveredFields: types.CoveredFields{WholeTransaction: true},
Signature: sig[:],
})
}
// narwal expects siad-style JSON, so we have to fixup a few fields
//
// (yes, this is scuffed)
js, _ := json.Marshal([]types.Transaction{txn})
js = []byte(strings.NewReplacer(
`"scoid:`, `"`,
`"addr:`, `"`,
`"h:`, `"`,
`"address":`, `"unlockhash":`,
`"signatures":`, `"transactionsignatures":`,
).Replace(string(js)))
// broadcast
resp, err := http.Post("https://narwal.lukechampine.com/wallet/ledger-recovery/broadcast", "application/json", bytes.NewReader(js))
if err != nil {
log.Fatal(err)
}
if resp.StatusCode != 200 {
io.Copy(os.Stderr, resp.Body)
return
}
fmt.Println("Transaction broadcasted successfully!")
}
@lukechampine
Copy link
Author

lukechampine commented Jul 11, 2023

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