From 01f72a48ad99bcd972427a4bde9670cd644672e2 Mon Sep 17 00:00:00 2001 From: GG <863867759@qq.com> Date: Mon, 15 Apr 2024 11:57:49 +0800 Subject: [PATCH] feat: support decode psbtHex & txHex detail. --- core/btc/account_test.go | 4 +- core/btc/transaction_decode.go | 198 +++++++++++++++++++++++++++++++++ core/btc/transaction_test.go | 22 ++++ 3 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 core/btc/transaction_decode.go diff --git a/core/btc/account_test.go b/core/btc/account_test.go index f11700b..f8e5be1 100644 --- a/core/btc/account_test.go +++ b/core/btc/account_test.go @@ -347,7 +347,7 @@ func TestAccount_SignPsbt(t *testing.T) { acc, err := NewAccountWithMnemonic(testcase.M1, ChainMainnet) require.NoError(t, err) - psbtHex := "010203" + psbtHex := "70736274ff01007d010000000158d38c41272f90e01d8dd3c5f9f6c4d37892ddcbaee155ecd826113c163003700000000000fdffffff02ea0500000000000022512073c9f2168a01fb0f0caa8a3fb7889ce4ab2cec67bfb16d272af4eea91fbd83e011cc240000000000160014f23a59a174bf387281535e2c0f6a395b77eb04a3000000000001011f2dd3240000000000160014f23a59a174bf387281535e2c0f6a395b77eb04a3000000" txn, err := acc.SignPsbt(psbtHex) require.NoError(t, err) require.True(t, txn.Packet.IsComplete()) @@ -361,7 +361,7 @@ func TestChain_PushPsbt(t *testing.T) { chain, err := NewChainWithChainnet(ChainTestnet) require.NoError(t, err) - psbtHex := "010203" + psbtHex := "70736274ff01007d010000000158d38c41272f90e01d8dd3c5f9f6c4d37892ddcbaee155ecd826113c163003700000000000fdffffff02ea0500000000000022512073c9f2168a01fb0f0caa8a3fb7889ce4ab2cec67bfb16d272af4eea91fbd83e011cc240000000000160014f23a59a174bf387281535e2c0f6a395b77eb04a3000000000001011f2dd3240000000000160014f23a59a174bf387281535e2c0f6a395b77eb04a3000000" hash, err := chain.PushPsbt(psbtHex) require.NoError(t, err) t.Log("txn hash = ", hash.Value) diff --git a/core/btc/transaction_decode.go b/core/btc/transaction_decode.go new file mode 100644 index 0000000..4dbc012 --- /dev/null +++ b/core/btc/transaction_decode.go @@ -0,0 +1,198 @@ +package btc + +import ( + "fmt" + "strconv" + + "github.com/btcsuite/btcd/blockchain" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/coming-chat/wallet-SDK/core/base" + "github.com/coming-chat/wallet-SDK/core/base/inter" +) + +type TxOut struct { + Hash string `json:"hash,omitempty"` + Index int64 `json:"index,omitempty"` + + Value int64 `json:"value,omitempty"` + Address string `json:"address,omitempty"` +} + +type TxOutArray struct { + inter.AnyArray[*TxOut] +} + +func (ta *TxOutArray) detailDesc() string { + desc := "" + for idx, out := range ta.AnyArray { + if out.Address != "" { + addr := out.Address + if len(addr) > 13 { + addr = addr[:5] + "..." + addr[len(addr)-5:] + } + valueBTC := btcutil.Amount(out.Value).String() + if idx == 0 { + desc += "\t" + addr + "\t" + valueBTC + } else { + desc += "\n\t" + addr + "\t" + valueBTC + } + } else { + if idx == 0 { + desc += "\thash: " + out.Hash + "\n" + "\tindex: " + strconv.FormatInt(out.Index, 10) + } else { + desc += "\n\thash: " + out.Hash + "\n" + "\tindex: " + strconv.FormatInt(out.Index, 10) + } + } + } + return desc +} + +type TransactionDetail struct { + NetworkFee int64 `json:"networkFee"` + FeeRate float64 `json:"feeRate"` + Inputs *TxOutArray `json:"inputs"` + Outputs *TxOutArray `json:"outputs"` +} + +func (td *TransactionDetail) JsonString() (*base.OptionalString, error) { + return base.JsonString(td) +} + +func (td *TransactionDetail) Desc() string { + feeBTC := "" + feeRate := "" + if td.NetworkFee == 0 { + feeBTC = "Unknow" + feeRate = "Unknow" + } else { + feeBTC = btcutil.Amount(td.NetworkFee).String() + feeRate = fmt.Sprintf("%.2f sat/vB", td.FeeRate) + } + + return fmt.Sprintf(`Network Fee: + %v +Network FeeRate: + %v +Inputs: +%v +Outputs: +%v`, feeBTC, feeRate, td.Inputs.detailDesc(), td.Outputs.detailDesc()) +} + +func DecodePsbtTransactionDetail(psbtHex string, chainnet string) (d *TransactionDetail, err error) { + chainParams, err := netParamsOf(chainnet) + if err != nil { + return + } + packet, err := DecodePsbtTxToPacket(psbtHex) + if err != nil { + return + } + txFee, err := packet.GetTxFee() + if err != nil { + return + } + feeFloat := txFee.ToUnit(btcutil.AmountSatoshi) + vSize := virtualSize(packet.UnsignedTx) + feeRate := feeFloat / float64(vSize) + + inputs := make([]*TxOut, len(packet.Inputs)) + for idx, in := range packet.Inputs { + switch { + case in.WitnessUtxo != nil: + inputs[idx], err = txOutFromWireTxOut(in.WitnessUtxo, chainParams) + if err != nil { + return + } + case in.NonWitnessUtxo != nil: + utxOuts := in.NonWitnessUtxo.TxOut + txIn := packet.UnsignedTx.TxIn[idx] + opIdx := txIn.PreviousOutPoint.Index + txOut := utxOuts[opIdx] + inputs[idx], err = txOutFromWireTxOut(txOut, chainParams) + if err != nil { + return + } + default: + return nil, fmt.Errorf("input %d has no UTXO information", + idx) + } + } + + outputs := make([]*TxOut, len(packet.UnsignedTx.TxOut)) + for idx, out := range packet.UnsignedTx.TxOut { + outputs[idx], err = txOutFromWireTxOut(out, chainParams) + if err != nil { + return + } + } + + return &TransactionDetail{ + NetworkFee: int64(feeFloat), + FeeRate: float64(feeRate), + Inputs: &TxOutArray{inputs}, + Outputs: &TxOutArray{outputs}, + }, nil +} + +func DecodeTxHexTransactionDetail(txHex string, chainnet string) (detail *TransactionDetail, err error) { + chainParams, err := netParamsOf(chainnet) + if err != nil { + return + } + msgTx, err := DecodeTx(txHex) + if err != nil { + return nil, err + } + // msgTx cannot get `fee`, `feeRate` + + inputs := make([]*TxOut, len(msgTx.TxIn)) + for idx, in := range msgTx.TxIn { + inputs[idx] = &TxOut{ + Hash: in.PreviousOutPoint.Hash.String(), + Index: int64(in.PreviousOutPoint.Index), + } + } + outputs := make([]*TxOut, len(msgTx.TxOut)) + for idx, out := range msgTx.TxOut { + outputs[idx], err = txOutFromWireTxOut(out, chainParams) + if err != nil { + return + } + } + + return &TransactionDetail{ + NetworkFee: 0, + FeeRate: 0, + Inputs: &TxOutArray{inputs}, + Outputs: &TxOutArray{outputs}, + }, nil +} + +func txOutFromWireTxOut(txout *wire.TxOut, params *chaincfg.Params) (*TxOut, error) { + pkobj, err := txscript.ParsePkScript(txout.PkScript) + if err != nil { + return nil, err + } + addr, err := pkobj.Address(params) + if err != nil { + return nil, err + } + return &TxOut{ + Value: txout.Value, + Address: addr.EncodeAddress(), + }, nil +} + +// calculation reference: +// https://github.com/btcsuite/btcd/blob/569155bc6a502f45b4a514bc6b9d5f814a980b6c/mempool/policy.go#L382 +func virtualSize(tx *wire.MsgTx) int64 { + // vSize := (((baseSize * 3) + totalSize) + 3) / 4 + baseSize := int64(tx.SerializeSizeStripped()) + totalSize := int64(tx.SerializeSize()) + weight := (baseSize * (blockchain.WitnessScaleFactor - 1)) + totalSize + return (weight + blockchain.WitnessScaleFactor - 1) / blockchain.WitnessScaleFactor +} diff --git a/core/btc/transaction_test.go b/core/btc/transaction_test.go index e0befb4..00d60bb 100644 --- a/core/btc/transaction_test.go +++ b/core/btc/transaction_test.go @@ -104,3 +104,25 @@ func getPsbtPacketWithSegwitV1(t *testing.T) *psbt.Packet { require.NoError(t, err) return packet } + +func TestDecodePsbtHex_DecodeTxHex(t *testing.T) { + psbtHex := "70736274ff01007d010000000158d38c41272f90e01d8dd3c5f9f6c4d37892ddcbaee155ecd826113c163003700000000000fdffffff02ea0500000000000022512073c9f2168a01fb0f0caa8a3fb7889ce4ab2cec67bfb16d272af4eea91fbd83e011cc240000000000160014f23a59a174bf387281535e2c0f6a395b77eb04a3000000000001011f2dd3240000000000160014f23a59a174bf387281535e2c0f6a395b77eb04a3000000" + dd, err := DecodePsbtTransactionDetail(psbtHex, ChainMainnet) + require.NoError(t, err) + json, _ := dd.JsonString() + t.Logf("Json:\n%v", json.Value) + t.Logf("Desc:\n%v", dd.Desc()) + + packet, err := DecodePsbtTxToPacket(psbtHex) + require.NoError(t, err) + + buf := bytes.Buffer{} + err = packet.UnsignedTx.Serialize(&buf) + require.NoError(t, err) + txHex := hex.EncodeToString(buf.Bytes()) + dd2, err := DecodeTxHexTransactionDetail(txHex, ChainMainnet) + require.NoError(t, err) + json, _ = dd2.JsonString() + t.Logf("Json:\n%v", json.Value) + t.Logf("Desc:\n%v", dd2.Desc()) +}