Skip to content

Commit

Permalink
feat(btc): Add planPSBT
Browse files Browse the repository at this point in the history
* Add `ChainInfo.hrp`
  • Loading branch information
satoshiotomakan committed Sep 19, 2024
1 parent 9615d94 commit f0b72d1
Show file tree
Hide file tree
Showing 17 changed files with 457 additions and 68 deletions.
11 changes: 11 additions & 0 deletions rust/chains/tw_bitcoin/src/entry.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::modules::compiler::BitcoinCompiler;
use crate::modules::planner::BitcoinPlanner;
use crate::modules::psbt_planner::PsbtPlanner;
use crate::modules::signer::BitcoinSigner;
use crate::modules::transaction_util::BitcoinTransactionUtil;
use std::str::FromStr;
Expand Down Expand Up @@ -102,6 +103,7 @@ impl CoinEntry for BitcoinEntry {
impl UtxoEntry for BitcoinEntry {
type PsbtSigningInput<'a> = Proto::PsbtSigningInput<'a>;
type PsbtSigningOutput = Proto::PsbtSigningOutput<'static>;
type PsbtTransactionPlan = Proto::TransactionPlan<'static>;

#[inline]
fn sign_psbt(
Expand All @@ -111,4 +113,13 @@ impl UtxoEntry for BitcoinEntry {
) -> Self::PsbtSigningOutput {
BitcoinSigner::sign_psbt(coin, &input)
}

#[inline]
fn plan_psbt(
&self,
coin: &dyn CoinContext,
input: Self::PsbtSigningInput<'_>,
) -> Self::PsbtTransactionPlan {
PsbtPlanner::plan_psbt(coin, &input)
}
}
1 change: 1 addition & 0 deletions rust/chains/tw_bitcoin/src/modules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub mod compiler;
pub mod planner;
pub mod protobuf_builder;
pub mod psbt;
pub mod psbt_planner;
pub mod psbt_request;
pub mod signer;
pub mod signing_request;
Expand Down
118 changes: 118 additions & 0 deletions rust/chains/tw_bitcoin/src/modules/psbt_planner.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright © 2017 Trust Wallet.

use crate::modules::psbt_request::PsbtRequest;
use crate::modules::signing_request::SigningRequestBuilder;
use crate::modules::tx_builder::script_parser::StandardScriptParser;
use crate::modules::tx_builder::BitcoinChainInfo;
use tw_coin_entry::coin_context::CoinContext;
use tw_coin_entry::error::prelude::*;
use tw_coin_entry::signing_output_error;
use tw_proto::BitcoinV2::Proto;
use tw_proto::BitcoinV2::Proto::mod_Input::OneOfclaiming_script as ClaimingScriptProto;
use tw_proto::BitcoinV2::Proto::mod_Output::OneOfto_recipient as ToRecipientProto;
use tw_utxo::transaction::standard_transaction::{TransactionInput, TransactionOutput};
use tw_utxo::transaction::transaction_interface::TransactionInterface;
use tw_utxo::transaction::UtxoToSign;

pub struct PsbtPlanner;

impl PsbtPlanner {
pub fn plan_psbt(
coin: &dyn CoinContext,
input: &Proto::PsbtSigningInput,
) -> Proto::TransactionPlan<'static> {
Self::plan_psbt_impl(coin, input)
.unwrap_or_else(|e| signing_output_error!(Proto::TransactionPlan, e))
}

pub fn plan_psbt_impl(
coin: &dyn CoinContext,
input: &Proto::PsbtSigningInput,
) -> SigningResult<Proto::TransactionPlan<'static>> {
let chain_info = SigningRequestBuilder::chain_info(coin, &input.chain_info)?;
let PsbtRequest { unsigned_tx, .. } = PsbtRequest::build(input)?;

let total_input = unsigned_tx.total_input()?;
let total_output = unsigned_tx.total_output()?;
let fee_estimate = total_input
.checked_sub(total_output)
.or_tw_err(SigningErrorType::Error_not_enough_utxos)
.context("PSBT sum(input) < sum(output)")?;

let vsize_estimate = unsigned_tx.estimate_transaction().vsize() as u64;

let inputs: Vec<_> = unsigned_tx
.input_args()
.iter()
.zip(unsigned_tx.inputs())
.map(|(unsigned_txin, txin)| Self::utxo_to_proto(unsigned_txin, txin, &chain_info))
.collect::<SigningResult<_>>()?;

let outputs: Vec<_> = unsigned_tx
.outputs()
.iter()
.map(|txout| Self::output_to_proto(txout, &chain_info))
.collect::<SigningResult<_>>()?;

Ok(Proto::TransactionPlan {
inputs,
outputs,
available_amount: total_input,
send_amount: total_input,
vsize_estimate,
fee_estimate,
change: 0,
..Proto::TransactionPlan::default()
})
}

pub fn utxo_to_proto(
unsigned_txin: &UtxoToSign,
txin: &TransactionInput,
chain_info: &BitcoinChainInfo,
) -> SigningResult<Proto::Input<'static>> {
let out_point = Proto::OutPoint {
hash: txin.previous_output.hash.to_vec().into(),
vout: txin.previous_output.index,
};
let sequence = Proto::mod_Input::Sequence {
sequence: txin.sequence,
};

let from_address = StandardScriptParser
.parse(&unsigned_txin.prevout_script_pubkey)?
.try_to_address(chain_info)?
.or_tw_err(SigningErrorType::Error_invalid_utxo)
.context("Unexpected UTXO scriptPubkey")?
.to_string();

Ok(Proto::Input {
out_point: Some(out_point),
value: unsigned_txin.amount,
sighash_type: unsigned_txin.sighash_ty.raw_sighash(),
sequence: Some(sequence),
claiming_script: ClaimingScriptProto::receiver_address(from_address.into()),
})
}

pub fn output_to_proto(
output: &TransactionOutput,
chain_info: &BitcoinChainInfo,
) -> SigningResult<Proto::Output<'static>> {
let to_recipient = match StandardScriptParser
.parse(&output.script_pubkey)?
.try_to_address(chain_info)?
{
Some(to_addr) => ToRecipientProto::to_address(to_addr.to_string().into()),
// Cannot convert the output scriptPubkey into an address. Return it as is.
None => ToRecipientProto::custom_script_pubkey(output.script_pubkey.to_vec().into()),
};

Ok(Proto::Output {
value: output.value,
to_recipient,
})
}
}
50 changes: 27 additions & 23 deletions rust/chains/tw_bitcoin/src/modules/psbt_request/utxo_psbt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// Copyright © 2017 Trust Wallet.

use crate::modules::tx_builder::public_keys::PublicKeys;
use crate::modules::tx_builder::script_parser::{ConditionScript, ConditionScriptParser};
use crate::modules::tx_builder::script_parser::{StandardScript, StandardScriptParser};
use secp256k1::ThirtyTwoByteHash;
use tw_coin_entry::error::prelude::*;
use tw_hash::H256;
Expand Down Expand Up @@ -60,17 +60,7 @@ impl<'a> UtxoPsbt<'a> {
let script = Script::from(prev_out.script_pubkey.to_bytes());
let builder = self.prepare_builder(prev_out.value)?;

match ConditionScriptParser.parse(&script)? {
ConditionScript::P2PK(pubkey) => builder.p2pk(&pubkey),
ConditionScript::P2PKH(pubkey_hash) => {
let pubkey = self.public_keys.get_ecdsa_public_key(&pubkey_hash)?;
builder.p2pkh(&pubkey)
},
ConditionScript::P2WPKH(_) | ConditionScript::P2TR(_) => {
SigningError::err(SigningErrorType::Error_invalid_params)
.context("P2WPKH and P2TR scripts should be specified in 'witness_utxo'")
},
}
self.build_utxo_with_script(builder, &script)
}

pub fn build_witness_utxo(
Expand All @@ -79,27 +69,41 @@ impl<'a> UtxoPsbt<'a> {
) -> SigningResult<(TransactionInput, UtxoToSign)> {
let script = Script::from(witness_utxo.script_pubkey.to_bytes());
let builder = self.prepare_builder(witness_utxo.value)?;
self.build_utxo_with_script(builder, &script)
}

match ConditionScriptParser.parse(&script)? {
ConditionScript::P2PK(_) | ConditionScript::P2PKH(_) => {
SigningError::err(SigningErrorType::Error_invalid_params)
.context("P2PK and P2PKH scripts should be specified in 'non_witness_utxo'")
fn build_utxo_with_script(
&self,
builder: UtxoBuilder,
script: &Script,
) -> SigningResult<(TransactionInput, UtxoToSign)> {
match StandardScriptParser.parse(script)? {
StandardScript::P2PK(pubkey) => builder.p2pk(&pubkey),
StandardScript::P2PKH(pubkey_hash) => {
let pubkey = self.public_keys.get_ecdsa_public_key(&pubkey_hash)?;
builder.p2pkh(&pubkey)
},
ConditionScript::P2WPKH(pubkey_hash) => {
StandardScript::P2WPKH(pubkey_hash) => {
let pubkey = self.public_keys.get_ecdsa_public_key(&pubkey_hash)?;
builder.p2wpkh(&pubkey)
},
ConditionScript::P2TR(_) if self.has_tap_scripts() => {
SigningError::err(SigningErrorType::Error_not_supported)
.context("P2TR script path is not supported for PSBT at the moment")
},
ConditionScript::P2TR(tweaked_pubkey) => {
StandardScript::P2TR(tweaked_pubkey) => {
if self.has_tap_scripts() {
return SigningError::err(SigningErrorType::Error_not_supported)
.context("P2TR script path is not supported for PSBT at the moment");
}
builder.p2tr_key_path_with_tweaked_pubkey(&tweaked_pubkey)
},
StandardScript::P2SH(_) | StandardScript::P2WSH(_) => {
SigningError::err(SigningErrorType::Error_not_supported)
.context("P2SH and P2WSH scriptPubkey's are not supported yet")
},
StandardScript::OpReturn(_) => SigningError::err(SigningErrorType::Error_invalid_utxo)
.context("Cannot spend an OP_RETURN output"),
}
}

pub fn prepare_builder(&self, amount: u64) -> SigningResult<UtxoBuilder> {
fn prepare_builder(&self, amount: u64) -> SigningResult<UtxoBuilder> {
let prevout_hash = H256::from(self.utxo.previous_output.txid.to_raw_hash().into_32());
let prevout_index = self.utxo.previous_output.vout;
let sequence = self.utxo.sequence.0;
Expand Down
8 changes: 7 additions & 1 deletion rust/chains/tw_bitcoin/src/modules/signing_request/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::modules::tx_builder::utxo_protobuf::UtxoProtobuf;
use crate::modules::tx_builder::BitcoinChainInfo;
use tw_coin_entry::coin_context::CoinContext;
use tw_coin_entry::error::prelude::*;
use tw_misc::traits::OptionalEmpty;
use tw_proto::BitcoinV2::Proto;
use tw_utxo::dust::DustPolicy;
use tw_utxo::modules::tx_planner::{PlanRequest, RequestType};
Expand Down Expand Up @@ -138,7 +139,7 @@ impl SigningRequestBuilder {
}
}

fn chain_info(
pub fn chain_info(
coin: &dyn CoinContext,
chain_info: &Option<Proto::ChainInfo>,
) -> SigningResult<BitcoinChainInfo> {
Expand All @@ -150,17 +151,22 @@ impl SigningRequestBuilder {
}

if let Some(info) = chain_info {
let hrp = info.hrp.to_string().empty_or_some();
return Ok(BitcoinChainInfo {
p2pkh_prefix: prefix_to_u8(info.p2pkh_prefix, "p2pkh")?,
p2sh_prefix: prefix_to_u8(info.p2sh_prefix, "p2sh")?,
hrp,
});
}

// Try to get the chain info from the context.
// Note that not all Bitcoin forks support HRP (segwit addresses).
let hrp = coin.hrp();
match (coin.p2pkh_prefix(), coin.p2sh_prefix()) {
(Some(p2pkh_prefix), Some(p2sh_prefix)) => Ok(BitcoinChainInfo {
p2pkh_prefix,
p2sh_prefix,
hrp,
}),
_ => SigningError::err(SigningErrorType::Error_invalid_params)
.context("Neither 'SigningInput.chain_info' nor p2pkh/p2sh prefixes specified in the registry.json")
Expand Down
2 changes: 2 additions & 0 deletions rust/chains/tw_bitcoin/src/modules/tx_builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ pub mod utxo_protobuf;
pub struct BitcoinChainInfo {
pub p2pkh_prefix: u8,
pub p2sh_prefix: u8,
/// Note that not all Bitcoin forks support HRP (segwit addresses).
pub hrp: Option<String>,
}
Loading

0 comments on commit f0b72d1

Please sign in to comment.