From a4daa08e67af88bfe7e08a2dbbf6e96a3780fc6f Mon Sep 17 00:00:00 2001 From: JoeGruff Date: Wed, 3 Sep 2025 18:19:03 +0900 Subject: [PATCH 1/7] addresses: Add address from extended key. --- cgo/addresses.go | 15 ++++++++ cgo/types.go | 8 +++++ dcr/addresses.go | 89 ++++++++++++++++++++++++++++++++++++++++++++++++ dcr/dcr_test.go | 43 +++++++++++++++++++++++ 4 files changed, 155 insertions(+) create mode 100644 dcr/dcr_test.go diff --git a/cgo/addresses.go b/cgo/addresses.go index 5b77543..186dd01 100644 --- a/cgo/addresses.go +++ b/cgo/addresses.go @@ -9,6 +9,7 @@ import ( dcrwallet "decred.org/dcrwallet/v4/wallet" "decred.org/dcrwallet/v4/wallet/udb" "github.com/decred/dcrd/txscript/v4/stdaddr" + "github.com/decred/libwallet/dcr" ) //export currentReceiveAddress @@ -187,3 +188,17 @@ func defaultPubkey(cName *C.char) *C.char { return successCResponse("%s", pubkey) } + +//export addrFromExtendedKey +func addrFromExtendedKey(cAddrFromExtKeyJSON *C.char) *C.char { + fromExtJSON := goString(cAddrFromExtKeyJSON) + var fromExt AddrFromExtKey + if err := json.Unmarshal([]byte(fromExtJSON), &fromExt); err != nil { + return errCResponse("malformed create addr json: %v", err) + } + addr, err := dcr.AddrFromExtendedKey(fromExt.Key, fromExt.Path, fromExt.AddrType, fromExt.UseChildBIP32Std) + if err != nil { + return errCResponse("unable to create address: %v", err) + } + return successCResponse("%s", addr) +} diff --git a/cgo/types.go b/cgo/types.go index a0cda04..863552d 100644 --- a/cgo/types.go +++ b/cgo/types.go @@ -171,3 +171,11 @@ type Config struct { // Only needed during watching only creation. PubKey string `json:"pubkey"` } + +type AddrFromExtKey struct { + Key string `json:"key"` + Path string `json:"path"` + // Currently support types: P2PKH + AddrType string `json:"addrtype"` + UseChildBIP32Std bool `json:"usechildbip32std"` +} diff --git a/dcr/addresses.go b/dcr/addresses.go index ff866e3..b94e269 100644 --- a/dcr/addresses.go +++ b/dcr/addresses.go @@ -3,7 +3,11 @@ package dcr import ( "context" "errors" + "fmt" + "strconv" + "strings" + "github.com/decred/dcrd/chaincfg/v3" "github.com/decred/dcrd/dcrutil/v4" "github.com/decred/dcrd/hdkeychain/v3" "github.com/decred/dcrd/txscript/v4/stdaddr" @@ -81,3 +85,88 @@ func (w *Wallet) AccountPubkey(ctx context.Context, acct string) (string, error) return xpub.String(), nil } + +const ( + mainnetPrivKeyPrefix = "dprv" + mainnetPubKeyPrefix = "dpub" + + simnetPrivKeyPrefix = "sprv" + simnetPubKeyPrefix = "spub" + + testnetPrivKeyPrefix = "tprv" + testnetPubKeyPrefix = "tpub" +) + +// AddrFromExtendedKey returns an address of the chosen type derived from key at +// the chosen path. The key can be a private or public key. They path must be in +// the form n'/n/... +func AddrFromExtendedKey(key, path, addrType string, useChildBIP32Std bool) (string, error) { + if len(key) < 4 { + return "", errors.New("key is too short") + } + + var ( + net *chaincfg.Params + err error + ) + + switch strings.ToLower(key[:4]) { + case mainnetPrivKeyPrefix, mainnetPubKeyPrefix: + net, err = ParseChainParams("mainnet") + case testnetPrivKeyPrefix, testnetPubKeyPrefix: + net, err = ParseChainParams("testnet") + case simnetPrivKeyPrefix, simnetPubKeyPrefix: + net, err = ParseChainParams("simnet") + default: + return "", errors.New("the key is not from a known network") + } + if err != nil { + return "", err + } + + extKey, err := hdkeychain.NewKeyFromString(key, net) + if err != nil { + return "", err + } + defer extKey.Zero() + + paths := strings.Split(path, "/") + + for _, p := range paths { + if len(p) == 0 { + continue + } + nStr := p + isHardened := p[len(p)-1:] == "'" + if isHardened { + nStr = nStr[:len(p)-1] + } + n, err := strconv.ParseUint(nStr, 10, 32) + if err != nil { + return "", err + } + if isHardened { + n += hdkeychain.HardenedKeyStart + } + if useChildBIP32Std { + extKey, err = extKey.ChildBIP32Std(uint32(n)) + } else { + extKey, err = extKey.Child(uint32(n)) + } + if err != nil { + return "", err + } + } + + switch strings.ToLower(addrType) { + case "p2pkh": + pkHash := stdaddr.Hash160(extKey.SerializedPubKey()) + addr, err := stdaddr.NewAddressPubKeyHashEcdsaSecp256k1V0(pkHash, net) + if err != nil { + return "", err + } + return addr.String(), nil + default: + return "", fmt.Errorf("unknown address type %v", addrType) + } +} diff --git a/dcr/dcr_test.go b/dcr/dcr_test.go new file mode 100644 index 0000000..baa7c19 --- /dev/null +++ b/dcr/dcr_test.go @@ -0,0 +1,43 @@ +package dcr + +import ( + "testing" +) + +func TestAddrFromExtendedKey(t *testing.T) { + + tests := []struct { + name, key, path, addrType, wantAddr string + useChildBIP32Std bool + }{{ + name: "ok same result as https://github.com/decred/dcrd/blob/master/hdkeychain/example_test.go", + key: "dprv3hCznBesA6jBushjx7y9NrfheE4ZshnaKYtsoLXefmLPzrXgEiXkdRMD6UngnmBYZzgNhdEd4K3PidxcaCiR6HC9hmpj8FcrP4Cv7zBwELA", + path: "0'/1/0", + addrType: "p2pkh", + wantAddr: "DsoTyktAyEDkYpgKSex6zx5rrkFDi2gAsHr", + }, { + name: "ok also same result as https://github.com/decred/dcrd/blob/master/hdkeychain/example_test.go", + key: "dprv3hCznBesA6jBushjx7y9NrfheE4ZshnaKYtsoLXefmLPzrXgEiXkdRMD6UngnmBYZzgNhdEd4K3PidxcaCiR6HC9hmpj8FcrP4Cv7zBwELA", + path: "0'/0/10", + addrType: "p2pkh", + wantAddr: "DshMmJ3bfvMDdk1mkXRD3x5xDuPwSxoYGfi", + }, { + name: "ok account pubkey from above", + key: "dpubZBcpPfFZ9PGZdqW64aazy29PVfYHXSSK4VzsR6XUu4XUsXcukg1HMiSyvCbLYhxFTGa9ai9awzJhQiZCNnLwEqkkSLmLDLEiomgsRZUt4ei", + path: "0/10", + addrType: "p2pkh", + wantAddr: "DshMmJ3bfvMDdk1mkXRD3x5xDuPwSxoYGfi", + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + addr, err := AddrFromExtendedKey(test.key, test.path, test.addrType, test.useChildBIP32Std) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + if addr != test.wantAddr { + t.Fatalf("wanted addr %v but got %v", test.wantAddr, addr) + } + }) + } +} From 3c06d6874b70edfb6f9f4b06b12752b1ba7f7d0e Mon Sep 17 00:00:00 2001 From: JoeGruff Date: Fri, 12 Sep 2025 15:18:53 +0900 Subject: [PATCH 2/7] addresses: Add create extended key. --- cgo/addresses.go | 15 +++++++++ cgo/types.go | 10 ++++++ dcr/addresses.go | 85 ++++++++++++++++++++++++++++++++++++++++++++++++ dcr/dcr_test.go | 33 +++++++++++++++++++ 4 files changed, 143 insertions(+) diff --git a/cgo/addresses.go b/cgo/addresses.go index 186dd01..abe14a2 100644 --- a/cgo/addresses.go +++ b/cgo/addresses.go @@ -202,3 +202,18 @@ func addrFromExtendedKey(cAddrFromExtKeyJSON *C.char) *C.char { } return successCResponse("%s", addr) } + +//export createExtendedKey +func createExtendedKey(cCreateExtendedKeyJSON *C.char) *C.char { + createExtJSON := goString(cCreateExtendedKeyJSON) + var createExt CreateExtendedKey + if err := json.Unmarshal([]byte(createExtJSON), &createExt); err != nil { + return errCResponse("malformed ceate extended key json: %v", err) + } + extKey, err := dcr.CreateExtendedKey(createExt.Key, createExt.ParentKey, createExt.ChainCode, + createExt.Network, createExt.Depth, createExt.ChildN, createExt.IsPrivate) + if err != nil { + return errCResponse("unable to create key: %v", err) + } + return successCResponse("%s", extKey) +} diff --git a/cgo/types.go b/cgo/types.go index 863552d..69be954 100644 --- a/cgo/types.go +++ b/cgo/types.go @@ -179,3 +179,13 @@ type AddrFromExtKey struct { AddrType string `json:"addrtype"` UseChildBIP32Std bool `json:"usechildbip32std"` } + +type CreateExtendedKey struct { + Key string `json:"key"` + ParentKey string `json:"parentkey"` + ChainCode string `json:"chaincode"` + Network string `json:"network"` + Depth uint8 `json:"depth"` + ChildN uint32 `json:"childn"` + IsPrivate bool `json:"isprivate"` +} diff --git a/dcr/addresses.go b/dcr/addresses.go index b94e269..b2a5cef 100644 --- a/dcr/addresses.go +++ b/dcr/addresses.go @@ -2,12 +2,18 @@ package dcr import ( "context" + "encoding/binary" + "encoding/hex" "errors" "fmt" "strconv" "strings" + "github.com/decred/base58" "github.com/decred/dcrd/chaincfg/v3" + "github.com/decred/dcrd/crypto/blake256" + "github.com/decred/dcrd/crypto/ripemd160" + "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrutil/v4" "github.com/decred/dcrd/hdkeychain/v3" "github.com/decred/dcrd/txscript/v4/stdaddr" @@ -170,3 +176,82 @@ func AddrFromExtendedKey(key, path, addrType string, useChildBIP32Std bool) (str return "", fmt.Errorf("unknown address type %v", addrType) } } + +// doubleBlake256Cksum returns the first four bytes of BLAKE256(BLAKE256(v)). +func doubleBlake256Cksum(v []byte) []byte { + first := blake256.Sum256(v) + second := blake256.Sum256(first[:]) + return second[:4] +} + +// hash160 returns RIPEMD160(BLAKE256(v)). +func hash160(v []byte) []byte { + blake256Hash := blake256.Sum256(v) + h := ripemd160.New() + h.Write(blake256Hash[:]) + return h.Sum(nil) +} + +// CreateExtendedKey will create an extended key for the chosen network. The +// key can be public or private. The parent key must be a public key. +func CreateExtendedKey(keyHex, parentKeyHex, chainCodeHex, network string, depth uint8, childN uint32, isPrivate bool) (string, error) { + net, err := ParseChainParams(network) + if err != nil { + return "", err + } + chainCode, err := hex.DecodeString(chainCodeHex) + if err != nil { + return "", err + } + if len(chainCode) != 32 { + return "", fmt.Errorf("expected chain code with length of 32 but got %d", len(chainCode)) + } + parentFP := []byte{0x00, 0x00, 0x00, 0x00} + if depth > 0 { + parentKeyB, err := hex.DecodeString(parentKeyHex) + if err != nil { + return "", err + } + parentKey, err := secp256k1.ParsePubKey(parentKeyB) + if err != nil { + return "", err + } + parentFP = hash160(parentKey.SerializeCompressed())[:4] + } + keyB, err := hex.DecodeString(keyHex) + if err != nil { + return "", err + } + var ver [4]byte + if isPrivate { + if len(keyB) != 32 { + return "", fmt.Errorf("expected private key with length of 32 but got %d", len(keyB)) + } + var b [33]byte + copy(b[1:], keyB) + keyB = b[:] + ver = net.HDPrivateKeyID + } else { + key, err := secp256k1.ParsePubKey(keyB) + if err != nil { + return "", err + } + keyB = key.SerializeCompressed() + ver = net.HDPublicKeyID + } + var childNumB [4]byte + binary.BigEndian.PutUint32(childNumB[:], childN) + // The serialized format is: + // version (4) || depth (1) || parent fingerprint (4)) || + // child num (4) || chain code (32) || key data (33) || checksum (4) + const exKeyLen = 4 + 1 + 4 + 4 + 32 + 33 + var extKeyB [exKeyLen]byte + copy(extKeyB[:], ver[:]) + extKeyB[4] = depth + copy(extKeyB[4+1:], parentFP[:]) + copy(extKeyB[4+1+4:], childNumB[:]) + copy(extKeyB[4+1+4+4:], chainCode[:]) + copy(extKeyB[4+1+4+4+32:], keyB[:]) + checkSum := doubleBlake256Cksum(extKeyB[:]) + return base58.Encode(append(extKeyB[:], checkSum...)), nil +} diff --git a/dcr/dcr_test.go b/dcr/dcr_test.go index baa7c19..83e20db 100644 --- a/dcr/dcr_test.go +++ b/dcr/dcr_test.go @@ -2,6 +2,8 @@ package dcr import ( "testing" + + "github.com/decred/dcrd/hdkeychain/v3" ) func TestAddrFromExtendedKey(t *testing.T) { @@ -41,3 +43,34 @@ func TestAddrFromExtendedKey(t *testing.T) { }) } } + +func TestCreateExtendedKey(t *testing.T) { + + tests := []struct { + name, keyHex, parentKeyHex, chainCodeHex, network, wantKey string + depth uint8 + childN uint32 + isPrivate bool + }{{ + name: "ok key from TestAddrFromExtendedKey", + keyHex: "025c2a9436486301dcbdc011548d4ac8b2c0103c0f4af5c860168676ceff4c1979", + parentKeyHex: "021320df92844ed8a74e217c614bc23a59af1998a1187a214d65cd5610fb7ac82b", + chainCodeHex: "93d54677306d74dda8e7b47cc06e87053b26609fa763972deb17bad2d0d73c64", + network: "mainnet", + depth: 1, + childN: hdkeychain.HardenedKeyStart, + wantKey: "dpubZBcpPfFZ9PGZdqW64aazy29PVfYHXSSK4VzsR6XUu4XUsXcukg1HMiSyvCbLYhxFTGa9ai9awzJhQiZCNnLwEqkkSLmLDLEiomgsRZUt4ei", + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + key, err := CreateExtendedKey(test.keyHex, test.parentKeyHex, test.chainCodeHex, test.network, test.depth, test.childN, test.isPrivate) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + if key != test.wantKey { + t.Fatalf("wanted key %v but got %v", test.wantKey, key) + } + }) + } +} From c45cda8d44ca6e0500f293603b39af55530af920 Mon Sep 17 00:00:00 2001 From: JoeGruff Date: Mon, 15 Sep 2025 16:50:10 +0900 Subject: [PATCH 3/7] transactions: Change create sign tx to create tx. --- cgo/transactions.go | 26 ++++++++++++++------------ cgo/types.go | 11 ++++++----- dcr/transactions.go | 17 +++++++++++++---- 3 files changed, 33 insertions(+), 21 deletions(-) diff --git a/cgo/transactions.go b/cgo/transactions.go index e0c1d96..6e2a8cf 100644 --- a/cgo/transactions.go +++ b/cgo/transactions.go @@ -14,14 +14,14 @@ import ( const defaultAccount = "default" -//export createSignedTransaction -func createSignedTransaction(cName, cCreateSignedTxJSONReq *C.char) *C.char { +//export createTransaction +func createTransaction(cName, cCreateTxJSONReq *C.char) *C.char { w, exists := loadedWallet(cName) if !exists { return errCResponse("wallet with name %q does not exist", goString(cName)) } - signSendJSONReq := goString(cCreateSignedTxJSONReq) - var req CreateSignedTxReq + signSendJSONReq := goString(cCreateTxJSONReq) + var req CreateTxReq if err := json.Unmarshal([]byte(signSendJSONReq), &req); err != nil { return errCResponse("malformed sign send request: %v", err) } @@ -53,19 +53,21 @@ func createSignedTransaction(cName, cCreateSignedTxJSONReq *C.char) *C.char { ignoreInputs[i] = o } - if err := w.MainWallet().Unlock(w.ctx, []byte(req.Password), nil); err != nil { - return errCResponse("cannot unlock wallet: %v", err) + if req.Sign { + if err := w.MainWallet().Unlock(w.ctx, []byte(req.Password), nil); err != nil { + return errCResponse("cannot unlock wallet: %v", err) + } + defer w.MainWallet().Lock() } - defer w.MainWallet().Lock() - txBytes, txhash, fee, err := w.CreateSignedTransaction(w.ctx, outputs, inputs, ignoreInputs, uint64(req.FeeRate), req.SendAll) + txBytes, txhash, fee, err := w.CreateTransaction(w.ctx, outputs, inputs, ignoreInputs, uint64(req.FeeRate), req.SendAll, req.Sign) if err != nil { return errCResponse("unable to sign send transaction: %v", err) } - res := &CreateSignedTxRes{ - SignedHex: hex.EncodeToString(txBytes), - Txid: txhash.String(), - Fee: int(fee), + res := &CreateTxRes{ + Hex: hex.EncodeToString(txBytes), + Txid: txhash.String(), + Fee: int(fee), } b, err := json.Marshal(res) diff --git a/cgo/types.go b/cgo/types.go index 69be954..6bcca86 100644 --- a/cgo/types.go +++ b/cgo/types.go @@ -104,19 +104,20 @@ type Output struct { Amount int `json:"amount"` } -type CreateSignedTxReq struct { +type CreateTxReq struct { Outputs []Output `json:"outputs"` Inputs []Input `json:"inputs"` IgnoreInputs []Input `json:"ignoreinputs"` FeeRate int `json:"feerate"` SendAll bool `json:"sendall"` Password string `json:"password"` + Sign bool `json:"sign"` } -type CreateSignedTxRes struct { - SignedHex string `json:"signedhex"` - Txid string `json:"txid"` - Fee int `json:"fee"` +type CreateTxRes struct { + Hex string `json:"hex"` + Txid string `json:"txid"` + Fee int `json:"fee"` } type ListUnspentRes struct { diff --git a/dcr/transactions.go b/dcr/transactions.go index f7d1a63..8595dc1 100644 --- a/dcr/transactions.go +++ b/dcr/transactions.go @@ -76,11 +76,11 @@ func (cs *changeSource) ScriptSize() int { return len(cs.script) } -// CreateSignedTransaction creates a signed transaction. The wallet must be -// unlocked before calling. sendAll will send everything to one output. In that +// CreateTransaction creates a transaction. The wallet must be unlocked before +// calling if signing. sendAll will send everything to one output. In that // case the output's amount is ignored. -func (w *Wallet) CreateSignedTransaction(ctx context.Context, outputs []*Output, - inputs, ignoreInputs []*Input, feeRate uint64, sendAll bool) (signedTx []byte, +func (w *Wallet) CreateTransaction(ctx context.Context, outputs []*Output, + inputs, ignoreInputs []*Input, feeRate uint64, sendAll, sign bool) (signedTx []byte, txid *chainhash.Hash, fee uint64, err error) { if sendAll && len(outputs) > 1 { return nil, nil, 0, errors.New("send all can only be used with one recepient") @@ -238,6 +238,15 @@ func (w *Wallet) CreateSignedTransaction(ctx context.Context, outputs []*Output, fee -= uint64(atx.Tx.TxOut[i].Value) } + if !sign { + txHash := atx.Tx.TxHash() + b, err := atx.Tx.Bytes() + if err != nil { + return nil, nil, 0, err + } + return b, &txHash, fee, nil + } + signedMsgTx, err := w.signRawTransaction(ctx, atx.Tx) if err != nil { return nil, nil, 0, err From 637a8fecb3f084bf1c299196392a518e891d0596 Mon Sep 17 00:00:00 2001 From: JoeGruff Date: Wed, 17 Sep 2025 14:45:19 +0900 Subject: [PATCH 4/7] transactions: Add decode tx. --- cgo/transactions.go | 17 ++++ dcr/dcr_test.go | 69 +++++++++++++++ dcr/transactions.go | 204 +++++++++++++++++++++++++++++++++++++++++++- go.mod | 15 ++-- 4 files changed, 297 insertions(+), 8 deletions(-) diff --git a/cgo/transactions.go b/cgo/transactions.go index 6e2a8cf..dbca866 100644 --- a/cgo/transactions.go +++ b/cgo/transactions.go @@ -216,3 +216,20 @@ func bestBlock(cName *C.char) *C.char { } return successCResponse("%s", b) } + +//export decodeTx +func decodeTx(cName, cTxHex *C.char) *C.char { + w, exists := loadedWallet(cName) + if !exists { + return errCResponse("wallet with name %q does not exist", goString(cName)) + } + decoded, err := w.DecodeTx(goString(cTxHex)) + if err != nil { + return errCResponse("unable to decode tx: %v", err) + } + b, err := json.Marshal(decoded) + if err != nil { + return errCResponse("unable to marshal decoded tx: %v", err) + } + return successCResponse("%s", b) +} diff --git a/dcr/dcr_test.go b/dcr/dcr_test.go index 83e20db..cc204fa 100644 --- a/dcr/dcr_test.go +++ b/dcr/dcr_test.go @@ -3,6 +3,8 @@ package dcr import ( "testing" + "github.com/davecgh/go-spew/spew" + "github.com/decred/dcrd/chaincfg/v3" "github.com/decred/dcrd/hdkeychain/v3" ) @@ -74,3 +76,70 @@ func TestCreateExtendedKey(t *testing.T) { }) } } + +func TestCreateTransaction(t *testing.T) { + + tests := []struct { + name, keyHex, parentKeyHex, chainCodeHex, network, wantKey string + depth uint8 + childN uint32 + isPrivate bool + }{{ + name: "ok key from TestAddrFromExtendedKey", + keyHex: "025c2a9436486301dcbdc011548d4ac8b2c0103c0f4af5c860168676ceff4c1979", + parentKeyHex: "021320df92844ed8a74e217c614bc23a59af1998a1187a214d65cd5610fb7ac82b", + chainCodeHex: "93d54677306d74dda8e7b47cc06e87053b26609fa763972deb17bad2d0d73c64", + network: "mainnet", + depth: 1, + childN: hdkeychain.HardenedKeyStart, + wantKey: "dpubZBcpPfFZ9PGZdqW64aazy29PVfYHXSSK4VzsR6XUu4XUsXcukg1HMiSyvCbLYhxFTGa9ai9awzJhQiZCNnLwEqkkSLmLDLEiomgsRZUt4ei", + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + key, err := CreateExtendedKey(test.keyHex, test.parentKeyHex, test.chainCodeHex, test.network, test.depth, test.childN, test.isPrivate) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + if key != test.wantKey { + t.Fatalf("wanted key %v but got %v", test.wantKey, key) + } + }) + } +} + +func TestDecodeTx(t *testing.T) { + + w := &Wallet{ + chainParams: chaincfg.MainNetParams(), + } + + tests := []struct { + name, hex string + }{{ + name: "ok normal tx", + hex: "0100000004ff42e8491fb20a667ca0a1b364f6bfc016d7122c048bb4f6cc30fb0a46ea87052000000000ffffffff541b3cd370aca98ab4fb6cc729030512ced1bc5e62bc47d2c78b11497044d4211600000000ffffffff7a6c40a68680f70cb9d5da6508a453351396b24a612c24dbc2ff42ca8e8373642d00000000ffffffffb2fbcd201eb5af94e29999a1b32a3b9e8744259d46e55127259abd4137470d732900000000ffffffff081c2472050000000000001976a914ccf79107d1f818d912826a75773f8c9c77884c8c88ac8db557a30100000000001976a914af2205774323a5f0defb6e87ad99247f2600cf4d88acb94da0a30100000000001976a9144ffc3676a46d9fc09cd64fc48181a9ce4e2595e988ac7273bfe80200000000001976a91442b5f8d8fedb0d298001e6cbbbd8866881e8f29188ac000000000400000000001976a91423649fddd9e359f656951fa7617da274014ad9a788ac000000000400000000001976a91474f356aa9a92c979f65530c509de6a4a101dd8de88ac000000000400000000001976a914847bf10d5532c736ec5766a993d619e943349c1888ac000000000400000000001976a914e36c190707b4c3fef35d9747d2d3de55f96c8e7288ac0000000000000000049b57a0a30500000054520f000b0000006a4730440220187bc7151512a24ae6cfcdeab70d1ce1c162817896f7ef1bd0c6511d70b201a002204f1bd145f1451a8a083a34d011c83f17d538416c641e081b540b02a6b1710af50121038ed5b3762d01ee650693915e51fa4717f02bf37aa14c6ff466925913d5397778fe2d7205040000009d4f0f000a0000006b483045022100c1d67e740fac39a294ee675c34ad5012a8702ae3f8b9a5142aaeeb5cbf1b664d022044d90ba48935106339d08e38d4f5532fefdf7c0c26ac9cacd25d35c8f6066185012102a1def88510d7a64499511e20a2c924c3f5d522ac4ec38f07588e716e00279618547dbfe806000000e2540f000b0000006a4730440220597f71c28dd95908168829e08e43f9d6d43420825580dabd044ddcf5a248f47b02201e7ba786856f9acac614d3b57a03e216981b0f3230b0078380411c5909da364101210285ea4a9033d719bcfa0fe2f535dff3f89e15ee0724e9fcfcb498f6d55a6985f46fbf57a305000000dc5d0f00090000006a473044022045078b5694c9ac91dfd4f74800cb9455d5d2a2f60e832e663b3b1e903b15325102201b09a8060ec59562fc0c4cce6c9684a60cfcf0c5148af6b323177225ecc7361d01210363e0523496eef94fc6105f48c0de87493e276a385e3de04415a709db57b1126d", + }, { + name: "ok chainbase", + hex: "03000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff02000000000000000000000e6a0ce55d0f0033613ca1a89ea2194ef75e000000000000001976a914e84caeb864252bace69af9a129a800c0e96ac8f688ac000000000000000001ce055e000000000000000000ffffffff0800002f646372642f", + }, { + name: "ok treasurybase", + hex: "03000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff020d3aac0300000000000001c1000000000000000000000e6a0ce55d0f00cdac931bfecd43700000000000000000010d3aac030000000000000000ffffffff00", + }, { + name: "ok stakebase", + hex: "01000000020000000000000000000000000000000000000000000000000000000000000000ffffffff00fffffffffdb06562f75a62a8a45c0431c685a26a8e5a48d09b28cca888d3299d531211090000000001ffffffff0300000000000000000000266a2468479fa7eb9fbd8d8c5939e62ae3e24f576936a3cad5b225fa44e34241f58bb9e45d0f0000000000000000000000086a0645000a000000e4bf09de0500000000001abb76a914afb6b13194f9f0a5f60d21e3eeb412888d9d9d7088ac000000000000000002889a89060000000000000000ffffffff0200005c2580d705000000f83c0f00140000006b483045022100ebab12e6c032b16c73bd413bbd4065cb72c03c3142ba30b59de4fe4bc66b38f402200c6b6f093565552070af47151afee82c47e4af5a4f41c8ea7aeb2e0108c0dba10121026182324373ff5766cce1783e24a96a88bcf4357b55667ac0fbf0f0fa1c31d650", + }, { + name: "ok revocation", + hex: "0200000001d0da473318d6f1af55a53cbbfd236ce8d0778b2771092ed770928fcf8584aa120000000001ffffffff0121fea7620500000000001abc76a9145902fea3162e16f5fb0886e4302e567500c0250a88ac00000000000000000121fea76205000000afba0e000600000000", + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tx, err := w.DecodeTx(test.hex) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + spew.Dump(tx) + }) + } +} diff --git a/dcr/transactions.go b/dcr/transactions.go index 8595dc1..c20a16f 100644 --- a/dcr/transactions.go +++ b/dcr/transactions.go @@ -1,6 +1,7 @@ package dcr import ( + "bytes" "context" "encoding/hex" "errors" @@ -12,14 +13,25 @@ import ( "decred.org/dcrwallet/v4/wallet" "decred.org/dcrwallet/v4/wallet/txauthor" "decred.org/dcrwallet/v4/wallet/txsizes" + "github.com/decred/dcrd/blockchain/stake/v5" + "github.com/decred/dcrd/blockchain/standalone/v2" "github.com/decred/dcrd/chaincfg/chainhash" + "github.com/decred/dcrd/chaincfg/v3" + "github.com/decred/dcrd/dcrjson/v4" "github.com/decred/dcrd/dcrutil/v4" + dcrdtypes "github.com/decred/dcrd/rpc/jsonrpc/types/v4" "github.com/decred/dcrd/txscript/v4" "github.com/decred/dcrd/txscript/v4/stdaddr" + "github.com/decred/dcrd/txscript/v4/stdscript" "github.com/decred/dcrd/wire" ) -const defaultAccount = "default" +const ( + defaultAccount = "default" + // sstxCommitmentString is the string to insert when a verbose + // transaction output's pkscript type is a ticket commitment. + sstxCommitmentString = "sstxcommitment" +) // newTxOut returns a new transaction output with the given parameters. func newTxOut(amount int64, pkScriptVer uint16, pkScript []byte) *wire.TxOut { @@ -275,3 +287,193 @@ func (w *Wallet) SendRawTransaction(ctx context.Context, txHex string) (*chainha defer w.syncerMtx.RUnlock() return w.mainWallet.PublishTransaction(ctx, msgTx, w.syncer) } + +// createVinList returns a slice of JSON objects for the inputs of the passed +// transaction. +func createVinList(mtx *wire.MsgTx, isTreasuryEnabled bool) []dcrdtypes.Vin { + // Treasurybase transactions only have a single txin by definition. + // + // NOTE: This check MUST come before the coinbase check because a + // treasurybase will be identified as a coinbase as well. + vinList := make([]dcrdtypes.Vin, len(mtx.TxIn)) + if isTreasuryEnabled && standalone.IsTreasuryBase(mtx) { + txIn := mtx.TxIn[0] + vinEntry := &vinList[0] + vinEntry.Treasurybase = true + vinEntry.Sequence = txIn.Sequence + vinEntry.AmountIn = dcrutil.Amount(txIn.ValueIn).ToCoin() + vinEntry.BlockHeight = txIn.BlockHeight + vinEntry.BlockIndex = txIn.BlockIndex + return vinList + } + + // Coinbase transactions only have a single txin by definition. + if standalone.IsCoinBaseTx(mtx, isTreasuryEnabled) { + txIn := mtx.TxIn[0] + vinEntry := &vinList[0] + vinEntry.Coinbase = hex.EncodeToString(txIn.SignatureScript) + vinEntry.Sequence = txIn.Sequence + vinEntry.AmountIn = dcrutil.Amount(txIn.ValueIn).ToCoin() + vinEntry.BlockHeight = txIn.BlockHeight + vinEntry.BlockIndex = txIn.BlockIndex + return vinList + } + + // Treasury spend transactions only have a single txin by definition. + if isTreasuryEnabled && stake.IsTSpend(mtx) { + txIn := mtx.TxIn[0] + vinEntry := &vinList[0] + vinEntry.TreasurySpend = hex.EncodeToString(txIn.SignatureScript) + vinEntry.Sequence = txIn.Sequence + vinEntry.AmountIn = dcrutil.Amount(txIn.ValueIn).ToCoin() + vinEntry.BlockHeight = txIn.BlockHeight + vinEntry.BlockIndex = txIn.BlockIndex + return vinList + } + + // Stakebase transactions (votes) have two inputs: a null stake base + // followed by an input consuming a ticket's stakesubmission. + isSSGen := stake.IsSSGen(mtx) + + for i, txIn := range mtx.TxIn { + // Handle only the null input of a stakebase differently. + if isSSGen && i == 0 { + vinEntry := &vinList[0] + vinEntry.Stakebase = hex.EncodeToString(txIn.SignatureScript) + vinEntry.Sequence = txIn.Sequence + vinEntry.AmountIn = dcrutil.Amount(txIn.ValueIn).ToCoin() + vinEntry.BlockHeight = txIn.BlockHeight + vinEntry.BlockIndex = txIn.BlockIndex + continue + } + + // The disassembled string will contain [error] inline + // if the script doesn't fully parse, so ignore the + // error here. + disbuf, _ := txscript.DisasmString(txIn.SignatureScript) + + vinEntry := &vinList[i] + vinEntry.Txid = txIn.PreviousOutPoint.Hash.String() + vinEntry.Vout = txIn.PreviousOutPoint.Index + vinEntry.Tree = txIn.PreviousOutPoint.Tree + vinEntry.Sequence = txIn.Sequence + vinEntry.AmountIn = dcrutil.Amount(txIn.ValueIn).ToCoin() + vinEntry.BlockHeight = txIn.BlockHeight + vinEntry.BlockIndex = txIn.BlockIndex + vinEntry.ScriptSig = &dcrdtypes.ScriptSig{ + Asm: disbuf, + Hex: hex.EncodeToString(txIn.SignatureScript), + } + } + + return vinList +} + +// createVoutList returns a slice of JSON objects for the outputs of the passed +// transaction. +func createVoutList(mtx *wire.MsgTx, chainParams *chaincfg.Params) ([]dcrdtypes.Vout, error) { + + txType := stake.DetermineTxType(mtx) + voutList := make([]dcrdtypes.Vout, 0, len(mtx.TxOut)) + for i, v := range mtx.TxOut { + // The disassembled string will contain [error] inline if the + // script doesn't fully parse, so ignore the error here. + disbuf, _ := txscript.DisasmString(v.PkScript) + + // Attempt to extract addresses from the public key script. In + // the case of stake submission transactions, the odd outputs + // contain a commitment address, so detect that case + // accordingly. + var addrs []stdaddr.Address + var scriptType string + var reqSigs uint16 + var commitAmt *dcrutil.Amount + if txType == stake.TxTypeSStx && (i%2 != 0) { + scriptType = sstxCommitmentString + addr, err := stake.AddrFromSStxPkScrCommitment(v.PkScript, + chainParams) + if err != nil { + return nil, fmt.Errorf("failed to decode ticket "+ + "commitment addr output for tx hash "+ + "%v, output idx %v", mtx.TxHash(), i) + } else { + addrs = []stdaddr.Address{addr} + } + amt, err := stake.AmountFromSStxPkScrCommitment(v.PkScript) + if err != nil { + return nil, fmt.Errorf("failed to decode ticket "+ + "commitment amt output for tx hash %v"+ + ", output idx %v", mtx.TxHash(), i) + } else { + commitAmt = &amt + } + } else { + // Attempt to extract known addresses associated with the script. + var st stdscript.ScriptType + st, addrs = stdscript.ExtractAddrs(v.Version, v.PkScript, chainParams) + scriptType = st.String() + + // Determine the number of required signatures for known standard + // dcrdtypes. + reqSigs = stdscript.DetermineRequiredSigs(v.Version, v.PkScript) + } + + encodedAddrs := make([]string, len(addrs)) + for j, addr := range addrs { + encodedAddr := addr.String() + encodedAddrs[j] = encodedAddr + } + + var vout dcrdtypes.Vout + voutSPK := &vout.ScriptPubKey + vout.N = uint32(i) + vout.Value = dcrutil.Amount(v.Value).ToCoin() + vout.Version = v.Version + voutSPK.Addresses = encodedAddrs + voutSPK.Asm = disbuf + voutSPK.Hex = hex.EncodeToString(v.PkScript) + voutSPK.Type = scriptType + voutSPK.ReqSigs = int32(reqSigs) + if commitAmt != nil { + voutSPK.CommitAmt = dcrjson.Float64(commitAmt.ToCoin()) + } + voutSPK.Version = v.Version + + voutList = append(voutList, vout) + } + + return voutList, nil +} + +// DecodeTx decodes a transaction from its hex. +func (w *Wallet) DecodeTx(hexStr string) (*dcrdtypes.TxRawDecodeResult, error) { + if len(hexStr)%2 != 0 { + hexStr = "0" + hexStr + } + serializedTx, err := hex.DecodeString(hexStr) + if err != nil { + return nil, err + } + var mtx wire.MsgTx + err = mtx.Deserialize(bytes.NewReader(serializedTx)) + if err != nil { + return nil, err + } + + voutList, err := createVoutList(&mtx, w.chainParams) + if err != nil { + return nil, err + } + + isTreasuryEnabled := true + + // Create and return the result. + return &dcrdtypes.TxRawDecodeResult{ + Txid: mtx.TxHash().String(), + Version: int32(mtx.Version), + Locktime: mtx.LockTime, + Expiry: mtx.Expiry, + Vin: createVinList(&mtx, isTreasuryEnabled), + Vout: voutList, + }, nil +} diff --git a/go.mod b/go.mod index 63689c4..49a2169 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,21 @@ require ( decred.org/dcrdex v1.0.2 decred.org/dcrwallet/v4 v4.3.1 github.com/btcsuite/btclog v1.0.0 + github.com/davecgh/go-spew v1.1.1 + github.com/decred/base58 v1.0.5 github.com/decred/dcrd/addrmgr/v2 v2.0.4 + github.com/decred/dcrd/blockchain/stake/v5 v5.0.1 + github.com/decred/dcrd/blockchain/standalone/v2 v2.2.1 github.com/decred/dcrd/chaincfg/chainhash v1.0.4 github.com/decred/dcrd/chaincfg/v3 v3.2.1 github.com/decred/dcrd/connmgr/v3 v3.1.2 github.com/decred/dcrd/crypto/blake256 v1.1.0 + github.com/decred/dcrd/crypto/ripemd160 v1.0.2 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 + github.com/decred/dcrd/dcrjson/v4 v4.1.0 github.com/decred/dcrd/dcrutil/v4 v4.0.2 github.com/decred/dcrd/hdkeychain/v3 v3.1.2 + github.com/decred/dcrd/rpc/jsonrpc/types/v4 v4.3.0 github.com/decred/dcrd/txscript/v4 v4.1.1 github.com/decred/dcrd/wire v1.7.0 github.com/decred/slog v1.2.0 @@ -24,20 +32,13 @@ require ( github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect github.com/companyzero/sntrup4591761 v0.0.0-20220309191932-9e0f3af2f07a // indirect github.com/dchest/siphash v1.2.3 // indirect - github.com/decred/base58 v1.0.5 // indirect - github.com/decred/dcrd/blockchain/stake/v5 v5.0.1 // indirect - github.com/decred/dcrd/blockchain/standalone/v2 v2.2.1 // indirect github.com/decred/dcrd/container/lru v1.0.0 // indirect github.com/decred/dcrd/crypto/rand v1.0.1 // indirect - github.com/decred/dcrd/crypto/ripemd160 v1.0.2 // indirect github.com/decred/dcrd/database/v3 v3.0.2 // indirect github.com/decred/dcrd/dcrec v1.0.1 // indirect github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/decred/dcrd/dcrjson/v4 v4.1.0 // indirect github.com/decred/dcrd/gcs/v4 v4.1.0 // indirect github.com/decred/dcrd/mixing v0.5.0 // indirect - github.com/decred/dcrd/rpc/jsonrpc/types/v4 v4.3.0 // indirect github.com/decred/go-socks v1.1.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/jrick/bitset v1.0.0 // indirect From 1c1a4b5a8540f0439cea7827fc5852616a97f1f5 Mon Sep 17 00:00:00 2001 From: JoeGruff Date: Wed, 17 Sep 2025 17:51:42 +0900 Subject: [PATCH 5/7] transactions: Add get txn. --- cgo/transactions.go | 30 ++++++++++++++++++++++++++++++ dcr/transactions.go | 21 +++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/cgo/transactions.go b/cgo/transactions.go index dbca866..ef13388 100644 --- a/cgo/transactions.go +++ b/cgo/transactions.go @@ -8,6 +8,7 @@ import ( "strconv" dcrwallet "decred.org/dcrwallet/v4/wallet" + "github.com/decred/dcrd/chaincfg/chainhash" "github.com/decred/dcrd/txscript/v4/stdaddr" "github.com/decred/libwallet/dcr" ) @@ -233,3 +234,32 @@ func decodeTx(cName, cTxHex *C.char) *C.char { } return successCResponse("%s", b) } + +//export getTxn +func getTxn(cName, cHashes *C.char) *C.char { + w, exists := loadedWallet(cName) + if !exists { + return errCResponse("wallet with name %q does not exist", goString(cName)) + } + var txIDs []string + if err := json.Unmarshal([]byte(goString(cHashes)), &txIDs); err != nil { + return errCResponse("unable to unmarshal hashes: %v", err) + } + txHashes := make([]*chainhash.Hash, len(txIDs)) + for i, txID := range txIDs { + txHash, err := chainhash.NewHashFromStr(txID) + if err != nil { + return errCResponse("unable to create tx hash: %v", err) + } + txHashes[i] = txHash + } + hexes, err := w.GetTxn(w.ctx, txHashes) + if err != nil { + return errCResponse("unable to get txn: %v", err) + } + b, err := json.Marshal(hexes) + if err != nil { + return errCResponse("unable to marshal txn: %v", err) + } + return successCResponse("%s", b) +} diff --git a/dcr/transactions.go b/dcr/transactions.go index c20a16f..7cb1f0c 100644 --- a/dcr/transactions.go +++ b/dcr/transactions.go @@ -477,3 +477,24 @@ func (w *Wallet) DecodeTx(hexStr string) (*dcrdtypes.TxRawDecodeResult, error) { Vout: voutList, }, nil } + +// GetTxn returns the hex representation of the full transaction for the tx. +// Transactions that do not concern the wallet will not be found. +func (w *Wallet) GetTxn(ctx context.Context, txHashes []*chainhash.Hash) (txHexes []string, err error) { + txn, _, err := w.mainWallet.GetTransactionsByHashes(ctx, txHashes) + if err != nil { + return nil, err + } + if len(txn) != len(txHashes) { + return nil, errors.New("could not get all txn") + } + txHexes = make([]string, len(txn)) + for i, tx := range txn { + txB, err := tx.Bytes() + if err != nil { + return nil, err + } + txHexes[i] = hex.EncodeToString(txB) + } + return txHexes, nil +} From c8ea19029b94b047592f316fb52a9d5c7994af0f Mon Sep 17 00:00:00 2001 From: JoeGruff Date: Thu, 18 Sep 2025 16:25:58 +0900 Subject: [PATCH 6/7] addresses: Add validate address. --- cgo/addresses.go | 17 ++++++++ dcr/addresses.go | 107 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/cgo/addresses.go b/cgo/addresses.go index abe14a2..0f8e874 100644 --- a/cgo/addresses.go +++ b/cgo/addresses.go @@ -217,3 +217,20 @@ func createExtendedKey(cCreateExtendedKeyJSON *C.char) *C.char { } return successCResponse("%s", extKey) } + +//export validateAddr +func validateAddr(cName, cAddr *C.char) *C.char { + w, exists := loadedWallet(cName) + if !exists { + return errCResponse("wallet with name %q does not exist", goString(cName)) + } + validated, err := w.ValidateAddr(w.ctx, goString(cAddr)) + if err != nil { + return errCResponse("unable to validate address: %v", err) + } + b, err := json.Marshal(validated) + if err != nil { + return errCResponse("unable to marshal validate address: %v", err) + } + return successCResponse("%s", b) +} diff --git a/dcr/addresses.go b/dcr/addresses.go index b2a5cef..3ed3aac 100644 --- a/dcr/addresses.go +++ b/dcr/addresses.go @@ -9,6 +9,9 @@ import ( "strconv" "strings" + walleterrors "decred.org/dcrwallet/v4/errors" + wallettypes "decred.org/dcrwallet/v4/rpc/jsonrpc/types" + "decred.org/dcrwallet/v4/wallet" "github.com/decred/base58" "github.com/decred/dcrd/chaincfg/v3" "github.com/decred/dcrd/crypto/blake256" @@ -17,6 +20,7 @@ import ( "github.com/decred/dcrd/dcrutil/v4" "github.com/decred/dcrd/hdkeychain/v3" "github.com/decred/dcrd/txscript/v4/stdaddr" + "github.com/decred/dcrd/txscript/v4/stdscript" ) // DefaultAccountAddresses returns addresses for the default account. Returns @@ -255,3 +259,106 @@ func CreateExtendedKey(keyHex, parentKeyHex, chainCodeHex, network string, depth checkSum := doubleBlake256Cksum(extKeyB[:]) return base58.Encode(append(extKeyB[:], checkSum...)), nil } + +func decodeAddress(s string, params *chaincfg.Params) (stdaddr.Address, error) { + // Secp256k1 pubkey as a string, handle differently. + if len(s) == 66 || len(s) == 130 { + pubKeyBytes, err := hex.DecodeString(s) + if err != nil { + return nil, err + } + pubKeyAddr, err := stdaddr.NewAddressPubKeyEcdsaSecp256k1V0Raw( + pubKeyBytes, params) + if err != nil { + return nil, err + } + + return pubKeyAddr, nil + } + + addr, err := stdaddr.DecodeAddress(s, params) + if err != nil { + return nil, fmt.Errorf("invalid address %q: decode failed: %#q", s, err) + } + return addr, nil +} + +// ValidateAddr validates an address. +func (w *Wallet) ValidateAddr(ctx context.Context, addrStr string) (*wallettypes.ValidateAddressResult, error) { + result := &wallettypes.ValidateAddressResult{} + addr, err := decodeAddress(addrStr, w.chainParams) + if err != nil { + result.Script = stdscript.STNonStandard.String() + // Use result zero value (IsValid=false). + return result, nil + } + + result.Address = addr.String() + result.IsValid = true + ver, scr := addr.PaymentScript() + class, _ := stdscript.ExtractAddrs(ver, scr, w.ChainParams()) + result.Script = class.String() + if pker, ok := addr.(stdaddr.SerializedPubKeyer); ok { + result.PubKey = hex.EncodeToString(pker.SerializedPubKey()) + result.PubKeyAddr = addr.String() + } + if class == stdscript.STScriptHash { + result.IsScript = true + } + if _, ok := addr.(stdaddr.Hash160er); ok { + result.IsCompressed = true + } + + ka, err := w.KnownAddress(ctx, addr) + if err != nil { + if errors.Is(err, walleterrors.NotExist) { + // No additional information available about the address. + return result, nil + } + return nil, err + } + + // The address lookup was successful which means there is further + // information about it available and it is "mine". + result.IsMine = true + result.Account = ka.AccountName() + + switch ka := ka.(type) { + case wallet.PubKeyHashAddress: + pubKey := ka.PubKey() + result.PubKey = hex.EncodeToString(pubKey) + pubKeyAddr, err := stdaddr.NewAddressPubKeyEcdsaSecp256k1V0Raw(pubKey, w.ChainParams()) + if err != nil { + return nil, err + } + result.PubKeyAddr = pubKeyAddr.String() + case wallet.P2SHAddress: + version, script := ka.RedeemScript() + result.Hex = hex.EncodeToString(script) + + class, addrs := stdscript.ExtractAddrs(version, script, w.ChainParams()) + addrStrings := make([]string, len(addrs)) + for i, a := range addrs { + addrStrings[i] = a.String() + } + result.Addresses = addrStrings + result.Script = class.String() + + // Multi-signature scripts also provide the number of required + // signatures. + if class == stdscript.STMultiSig { + result.SigsRequired = int32(stdscript.DetermineRequiredSigs(version, script)) + } + } + + if ka, ok := ka.(wallet.BIP0044Address); ok { + acct, branch, child := ka.Path() + if ka.AccountKind() != wallet.AccountKindImportedXpub { + result.AccountN = &acct + } + result.Branch = &branch + result.Index = &child + } + + return result, nil +} From aa6639c0e47faa658c2bab6bf351d08aa8657e00 Mon Sep 17 00:00:00 2001 From: JoeGruff Date: Tue, 30 Sep 2025 16:05:29 +0900 Subject: [PATCH 7/7] transactions: Add add sig. --- cgo/transactions.go | 17 +++++++++++++++++ dcr/transactions.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/cgo/transactions.go b/cgo/transactions.go index ef13388..76eb810 100644 --- a/cgo/transactions.go +++ b/cgo/transactions.go @@ -263,3 +263,20 @@ func getTxn(cName, cHashes *C.char) *C.char { } return successCResponse("%s", b) } + +//export addSigs +func addSigs(cName, cTxHex, cSigScripts *C.char) *C.char { + w, exists := loadedWallet(cName) + if !exists { + return errCResponse("wallet with name %q does not exist", goString(cName)) + } + var sigScripts []string + if err := json.Unmarshal([]byte(goString(cSigScripts)), &sigScripts); err != nil { + return errCResponse("unable to unmarshal sig scripts: %v", err) + } + signedHex, err := w.AddSigs(goString(cTxHex), sigScripts) + if err != nil { + return errCResponse("unable sign tx: %v", err) + } + return successCResponse("%s", signedHex) +} diff --git a/dcr/transactions.go b/dcr/transactions.go index 7cb1f0c..7e0a0d0 100644 --- a/dcr/transactions.go +++ b/dcr/transactions.go @@ -498,3 +498,39 @@ func (w *Wallet) GetTxn(ctx context.Context, txHashes []*chainhash.Hash) (txHexe } return txHexes, nil } + +// AddSigs adds signatures to a tx. The number of signature scripts should +// match the number of tx inputs. Blank strings may be used to skip inputs. +func (w *Wallet) AddSigs(hexStr string, sigScripts []string) (signedTxHex string, err error) { + if len(hexStr)%2 != 0 { + hexStr = "0" + hexStr + } + serializedTx, err := hex.DecodeString(hexStr) + if err != nil { + return "", err + } + var mtx wire.MsgTx + err = mtx.Deserialize(bytes.NewReader(serializedTx)) + if err != nil { + return "", err + } + if len(mtx.TxIn) != len(sigScripts) { + return "", errors.New("number of inputs and signatures differ") + } + for i := 0; i < len(mtx.TxIn); i++ { + if len(sigScripts[i]) == 0 { + continue + } + sig, err := hex.DecodeString(sigScripts[i]) + if err != nil { + return "", err + } + mtx.TxIn[i].SignatureScript = sig + } + mtx.SerType = wire.TxSerializeFull + b, err := mtx.Bytes() + if err != nil { + return "", err + } + return hex.EncodeToString(b), nil +}