Skip to content

Commit

Permalink
feat: support btc transaction build.
Browse files Browse the repository at this point in the history
  • Loading branch information
Zhangguiguang committed May 22, 2024
1 parent 55a8365 commit 4225611
Show file tree
Hide file tree
Showing 4 changed files with 284 additions and 0 deletions.
2 changes: 2 additions & 0 deletions core/btc/interface_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ var (
_ base.SignedTransaction = (*Brc20MintTransaction)(nil)
_ base.Transaction = (*PsbtTransaction)(nil)
_ base.SignedTransaction = (*SignedPsbtTransaction)(nil)
_ base.Transaction = (*Transaction)(nil)
_ base.SignedTransaction = (*SignedTransaction)(nil)
)
144 changes: 144 additions & 0 deletions core/btc/transaction_build.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package btc

import (
"errors"
"strings"

"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/coming-chat/wallet-SDK/util/hexutil"
)

type Transaction struct {
inputs []input
outputs []output
netParams *chaincfg.Params
}

type input struct {
outPoint *wire.OutPoint
prevOut *wire.TxOut
}

type output *wire.TxOut

func NewTransaction(chainnet string) (*Transaction, error) {
net, err := netParamsOf(chainnet)
if err != nil {
return nil, err
}
return &Transaction{netParams: net}, nil
}

func (t *Transaction) TotalInputValue() int64 {
total := int64(0)
for _, v := range t.inputs {
total += v.prevOut.Value
}
return total
}

func (t *Transaction) TotalOutputValue() int64 {
total := int64(0)
for _, v := range t.outputs {
total += v.Value
}
return total
}

func (t *Transaction) AddInput(txId string, index int64, address string, value int64) error {
outPoint, err := outPoint(txId, uint32(index))
if err != nil {
return err
}
pkScript, err := addrToPkScript(address, t.netParams)
if err != nil {
return err
}
input := input{
outPoint: outPoint,
prevOut: wire.NewTxOut(value, pkScript),
}
t.inputs = append(t.inputs, input)
return nil
}

func (t *Transaction) AddInput2(txId string, index int64, prevTx string) error {
outPoint, err := outPoint(txId, uint32(index))
if err != nil {
return err
}
prevOut, err := prevTxOut(prevTx, uint32(index))
if err != nil {
return err
}
input := input{
outPoint: outPoint,
prevOut: prevOut,
}
t.inputs = append(t.inputs, input)
return nil
}

func (t *Transaction) AddOutput(address string, value int64) error {
pkScript, err := addrToPkScript(address, t.netParams)
if err != nil {
return err
}
output := wire.NewTxOut(value, pkScript)
t.outputs = append(t.outputs, output)
return nil
}

func (t *Transaction) AddOpReturn(opReturn string) error {
data := []byte(opReturn)
script, err := buildOpReturnScript(data)
if err != nil {
return err
}
output := wire.NewTxOut(0, script)
t.outputs = append(t.outputs, output)
return nil
}

func outPoint(txId string, index uint32) (*wire.OutPoint, error) {
txId = strings.TrimPrefix(txId, "0x")
txHash, err := chainhash.NewHashFromStr(txId)
if err != nil {
return nil, err
}
return wire.NewOutPoint(txHash, index), nil
}

func prevTxOut(preTx string, index uint32) (*wire.TxOut, error) {
txData, err := hexutil.HexDecodeString(preTx)
if err != nil {
return nil, err
}
tx, err := btcutil.NewTxFromBytes(txData)
if err != nil {
return nil, err
}
outs := tx.MsgTx().TxOut
if len(outs) > int(index) {
return outs[index], nil
} else {
return nil, errors.New("invalid output index")
}
}

func addrToPkScript(addr string, network *chaincfg.Params) ([]byte, error) {
address, err := btcutil.DecodeAddress(addr, network)
if err != nil {
return nil, err
}

return txscript.PayToAddrScript(address)
}

func buildOpReturnScript(data []byte) ([]byte, error) {
return txscript.NewScriptBuilder().AddOp(txscript.OP_RETURN).AddData(data).Script()
}
117 changes: 117 additions & 0 deletions core/btc/transaction_sign.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package btc

import (
"bytes"
"errors"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/coming-chat/wallet-SDK/core/base"
"github.com/coming-chat/wallet-SDK/util/hexutil"
)

type SignedTransaction struct {
msgTx *wire.MsgTx
}

func (t *Transaction) SignWithAccount(account base.Account) (signedTxn *base.OptionalString, err error) {
txn, err := t.SignedTransactionWithAccount(account)
if err != nil {
return nil, err
}
return txn.HexString()
}

func (t *Transaction) SignedTransactionWithAccount(account base.Account) (signedTxn base.SignedTransaction, err error) {
if len(t.inputs) == 0 || len(t.outputs) == 0 {
return nil, errors.New("invalid inputs or outputs")
}

btcAcc, ok := account.(*Account)
if !ok {
return nil, base.ErrInvalidAccountType
}
privateKey := btcAcc.privateKey

tx := wire.NewMsgTx(wire.TxVersion)
prevOutFetcher := txscript.NewMultiPrevOutFetcher(nil)
for _, input := range t.inputs {
txIn := wire.NewTxIn(input.outPoint, nil, nil)
tx.TxIn = append(tx.TxIn, txIn)
prevOutFetcher.AddPrevOut(*input.outPoint, input.prevOut)
}
for _, output := range t.outputs {
tx.TxOut = append(tx.TxOut, output)
}

err = Sign(tx, privateKey, prevOutFetcher)
if err != nil {
return nil, err
}
return &SignedTransaction{
msgTx: tx,
}, nil
}

func (t *SignedTransaction) HexString() (res *base.OptionalString, err error) {
var buf bytes.Buffer
if err := t.msgTx.Serialize(&buf); err != nil {
return nil, err
}
str := hexutil.HexEncodeToString(buf.Bytes())
return base.NewOptionalString(str), nil
}

func Sign(tx *wire.MsgTx, privKey *btcec.PrivateKey, prevOutFetcher *txscript.MultiPrevOutFetcher) error {
for i, in := range tx.TxIn {
prevOut := prevOutFetcher.FetchPrevOutput(in.PreviousOutPoint)
txSigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher)
if txscript.IsPayToTaproot(prevOut.PkScript) {
witness, err := txscript.TaprootWitnessSignature(tx, txSigHashes, i, prevOut.Value, prevOut.PkScript, txscript.SigHashDefault, privKey)
if err != nil {
return err
}
in.Witness = witness
} else if txscript.IsPayToPubKeyHash(prevOut.PkScript) {
sigScript, err := txscript.SignatureScript(tx, i, prevOut.PkScript, txscript.SigHashAll, privKey, true)
if err != nil {
return err
}
in.SignatureScript = sigScript
} else {
pubKeyBytes := privKey.PubKey().SerializeCompressed()
script, err := PayToPubKeyHashScript(btcutil.Hash160(pubKeyBytes))
if err != nil {
return err
}
amount := prevOut.Value
witness, err := txscript.WitnessSignature(tx, txSigHashes, i, amount, script, txscript.SigHashAll, privKey, true)
if err != nil {
return err
}
in.Witness = witness

if txscript.IsPayToScriptHash(prevOut.PkScript) {
redeemScript, err := PayToWitnessPubKeyHashScript(btcutil.Hash160(pubKeyBytes))
if err != nil {
return err
}
in.SignatureScript = append([]byte{byte(len(redeemScript))}, redeemScript...)
}
}
}

return nil
}

func PayToPubKeyHashScript(pubKeyHash []byte) ([]byte, error) {
return txscript.NewScriptBuilder().AddOp(txscript.OP_DUP).AddOp(txscript.OP_HASH160).
AddData(pubKeyHash).AddOp(txscript.OP_EQUALVERIFY).AddOp(txscript.OP_CHECKSIG).
Script()
}

func PayToWitnessPubKeyHashScript(pubKeyHash []byte) ([]byte, error) {
return txscript.NewScriptBuilder().AddOp(txscript.OP_0).AddData(pubKeyHash).Script()
}
21 changes: 21 additions & 0 deletions util/hexutil/hexutil.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package hexutil

import (
"encoding/hex"
"errors"
"fmt"
"math"
Expand Down Expand Up @@ -160,3 +161,23 @@ func Reverse(s string) string {
}
return strings.Join(out, "")
}

// HexDecodeString decodes bytes from a hex string. Contrary to hex.DecodeString, this function does not error if "0x"
// is prefixed, and adds an extra 0 if the hex string has an odd length.
func HexDecodeString(s string) ([]byte, error) {
if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") {
s = s[2:]
}

if len(s)%2 != 0 {
s = "0" + s
}

return hex.DecodeString(s)
}

// HexEncode encodes bytes to a hex string. Contrary to hex.EncodeToString, this function prefixes the hex string
// with "0x"
func HexEncodeToString(b []byte) string {
return "0x" + hex.EncodeToString(b)
}

0 comments on commit 4225611

Please sign in to comment.