Skip to content

Instantly share code, notes, and snippets.

@lukechampine
Last active July 2, 2025 03:45
Show Gist options
  • Save lukechampine/38c9c45cb327993d962219e34e50a6c7 to your computer and use it in GitHub Desktop.
Save lukechampine/38c9c45cb327993d962219e34e50a6c7 to your computer and use it in GitHub Desktop.
og-sweep: Sweep v033x and siag keys
package main
import (
"flag"
"fmt"
"log"
"os"
"go.sia.tech/core/types"
"go.sia.tech/walletd/v2/api"
)
func readFile(path string, v types.DecoderFrom) {
data, err := os.ReadFile(path)
if err != nil {
log.Fatalln("Error reading file:", err)
}
d := types.NewBufDecoder(data)
v.DecodeFrom(d)
if d.Err() != nil {
log.Fatalln("Failed to decode file:", d.Err())
}
}
type savedKey033x struct {
SecretKey types.PrivateKey
UnlockConditions types.UnlockConditions
Visible bool
}
func (sk *savedKey033x) DecodeFrom(d *types.Decoder) {
var key [64]byte
d.Read(key[:])
sk.SecretKey = types.PrivateKey(key[:])
sk.UnlockConditions.DecodeFrom(d)
sk.Visible = d.ReadBool()
}
func extract033x(path string) (keys []savedKey033x) {
readFile(path, types.DecoderFunc(func(d *types.Decoder) {
types.DecodeSlice(d, &keys)
}))
return
}
type siagKey struct {
Header string
Version string
Index int
SecretKey types.PrivateKey
UnlockConditions types.UnlockConditions
}
func (sk *siagKey) DecodeFrom(d *types.Decoder) {
sk.Header = d.ReadString()
sk.Version = d.ReadString()
sk.Index = int(d.ReadUint64())
sk.SecretKey = make(types.PrivateKey, 64)
d.Read(sk.SecretKey[:])
sk.UnlockConditions.DecodeFrom(d)
}
func extractSiag(path string) (key siagKey) {
readFile(path, &key)
return
}
type signer struct {
UnlockConditions types.UnlockConditions
Keys []types.PrivateKey
}
func utxos(addr types.Address) (scs []types.SiacoinElement, sfs []types.SiafundElement, basis types.ChainIndex, err error) {
c := api.NewClient("https://api.siascan.com/wallet", "")
again:
sces, scBasis, err := c.AddressSiacoinOutputs(addr, false, 0, 10000)
if err != nil {
return nil, nil, types.ChainIndex{}, fmt.Errorf("failed to get siacoin elements for address %s: %w", addr, err)
}
sfes, sfBasis, err := c.AddressSiafundOutputs(addr, false, 0, 10000)
if err != nil {
return nil, nil, types.ChainIndex{}, fmt.Errorf("failed to get siafund elements for address %s: %w", addr, err)
}
if scBasis != sfBasis {
goto again
}
for _, sce := range sces {
scs = append(scs, sce.SiacoinElement)
}
for _, sfe := range sfes {
sfs = append(sfs, sfe.SiafundElement)
}
basis = scBasis
return
}
func main() {
log.SetFlags(log.Lshortfile)
v033x := flag.Bool("v033x", false, "decode a v033x wallet key")
siag := flag.Bool("siag", false, "decode a set of .siakey files")
flag.Parse()
var signers []signer
if *v033x {
keys := extract033x(flag.Arg(0))
if len(keys) == 0 {
fmt.Println("No keys found")
return
}
for _, key := range keys {
signers = append(signers, signer{
UnlockConditions: key.UnlockConditions,
Keys: []types.PrivateKey{key.SecretKey},
})
}
} else if *siag {
if len(flag.Args()) == 0 {
fmt.Println("No .siakey files provided")
return
}
keys := make([]types.PrivateKey, 0, len(flag.Args()))
var uc types.UnlockConditions
for i, arg := range flag.Args() {
key := extractSiag(arg)
if i == 0 {
uc = key.UnlockConditions
} else {
if key.UnlockConditions.UnlockHash() != uc.UnlockHash() {
fmt.Println("All keys must have the same unlock conditions")
return
}
}
keys = append(keys, key.SecretKey)
}
signers = append(signers, signer{
UnlockConditions: uc,
Keys: keys,
})
} else {
fmt.Println("Usage: og-sweep [-v033x] [-siag] <files>")
fmt.Println(" -v033x: decode a v033x wallet key")
fmt.Println(" -siag: decode a set of .siakey files")
return
}
retry:
var setBasis types.ChainIndex
var txn types.V2Transaction
var totalSC types.Currency
var totalSF uint64
for i, sp := range signers {
fmt.Print("\rChecking address: ", sp.UnlockConditions.UnlockHash(), "... ")
sces, sfes, basis, err := utxos(sp.UnlockConditions.UnlockHash())
if err != nil {
fmt.Println()
log.Fatal(err)
} else if setBasis != (types.ChainIndex{}) && basis != setBasis {
goto retry
} else if len(sces) == 0 && len(sfes) == 0 {
if i == len(signers)-1 {
fmt.Println()
}
continue
}
setBasis = basis
var sc types.Currency
var sf uint64
for i := range sces {
txn.SiacoinInputs = append(txn.SiacoinInputs, types.V2SiacoinInput{Parent: sces[i]})
sc = sc.Add(sces[i].SiacoinOutput.Value)
}
for i := range sfes {
txn.SiafundInputs = append(txn.SiafundInputs, types.V2SiafundInput{Parent: sfes[i]})
sf += sfes[i].SiafundOutput.Value
}
fmt.Printf("Found %v and %v SF\n", sc, sf)
totalSC = totalSC.Add(sc)
totalSF += sf
}
if totalSC.IsZero() && totalSF == 0 {
fmt.Println("No SC or SF found.")
return
}
fmt.Printf("Total amount to sweep: %v and %v SF.\n", totalSC, totalSF)
again:
fmt.Print("Enter destination address: ")
var destStr string
fmt.Scanln(&destStr)
dest, err := types.ParseAddress(destStr)
if err != nil {
fmt.Println("Invalid address:", err)
goto again
}
if !totalSC.IsZero() {
txn.SiacoinOutputs = []types.SiacoinOutput{{Value: totalSC, Address: dest}}
}
if totalSF != 0 {
txn.SiafundOutputs = []types.SiafundOutput{{Value: totalSF, Address: dest}}
}
// sign transaction
c := api.NewClient("https://api.siascan.com/wallet", "")
cs, err := c.ConsensusTipState()
if err != nil {
log.Fatalln("Failed to get consensus tip state:", err)
}
sigHash := cs.InputSigHash(txn)
sps := make(map[types.Address]types.SatisfiedPolicy)
sign := func(addr types.Address) types.SatisfiedPolicy {
if sp, ok := sps[addr]; ok {
return sp
}
for _, s := range signers {
if s.UnlockConditions.UnlockHash() == addr {
sp := types.SatisfiedPolicy{Policy: types.SpendPolicy{Type: types.PolicyTypeUnlockConditions(s.UnlockConditions)}}
for _, key := range s.Keys[:s.UnlockConditions.SignaturesRequired] {
sp.Signatures = append(sp.Signatures, key.SignHash(sigHash))
}
sps[addr] = sp
return sp
}
}
log.Fatalln("Failed to find signer for address:", addr)
panic("unreachable")
}
for i := range txn.SiacoinInputs {
txn.SiacoinInputs[i].SatisfiedPolicy = sign(txn.SiacoinInputs[i].Parent.SiacoinOutput.Address)
}
for i := range txn.SiafundInputs {
txn.SiafundInputs[i].SatisfiedPolicy = sign(txn.SiafundInputs[i].Parent.SiafundOutput.Address)
}
confirm:
fmt.Printf("Broadcast transaction %v? [y/n] ", txn.ID())
var confirm string
fmt.Scanln(&confirm)
if confirm == "n" {
fmt.Println("Sweep cancelled.")
return
} else if confirm != "y" {
goto confirm
}
_, err = c.TxpoolBroadcast(setBasis, nil, []types.V2Transaction{txn})
if err != nil {
log.Fatalln("Failed to broadcast transaction:", err)
}
fmt.Println("Transaction broadcast successfully.")
}
@lukechampine
Copy link
Author

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