-
-
Save lukechampine/e25330d7cc636cd37d4abe4211dedb40 to your computer and use it in GitHub Desktop.
Sia Ledger Sweep
This file contains 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" | |
"encoding/json" | |
"flag" | |
"fmt" | |
"io" | |
"log" | |
"net/http" | |
"os" | |
"strings" | |
"go.sia.tech/core/types" | |
"go.sia.tech/coreutils/chain" | |
) | |
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())) | |
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++ { | |
uc := types.StandardUnlockConditions(bip32(seed, uint32(i)).PublicKey()) | |
addr := uc.UnlockHash() | |
fmt.Printf("\rChecking address %v (%x...%x)...", i, addr[:4], addr[28:]) | |
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!") | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Binaries (last updated: Jan 29 2025)