diff --git a/core/btc/interface_test.go b/core/btc/interface_test.go index 3ffc5fe..b914d1a 100644 --- a/core/btc/interface_test.go +++ b/core/btc/interface_test.go @@ -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) ) diff --git a/core/btc/transaction_build.go b/core/btc/transaction_build.go new file mode 100644 index 0000000..adfacfb --- /dev/null +++ b/core/btc/transaction_build.go @@ -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() +} diff --git a/core/btc/transaction_sign.go b/core/btc/transaction_sign.go new file mode 100644 index 0000000..a384b69 --- /dev/null +++ b/core/btc/transaction_sign.go @@ -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() +} diff --git a/util/hexutil/hexutil.go b/util/hexutil/hexutil.go index 64a565c..ed1ba2d 100644 --- a/util/hexutil/hexutil.go +++ b/util/hexutil/hexutil.go @@ -1,6 +1,7 @@ package hexutil import ( + "encoding/hex" "errors" "fmt" "math" @@ -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) +}