diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoin/TestBitcoinPsbt.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoin/TestBitcoinPsbt.kt new file mode 100644 index 00000000000..3f8a083b7de --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoin/TestBitcoinPsbt.kt @@ -0,0 +1,101 @@ +package com.trustwallet.core.app.blockchains.bitcoin + +import com.google.protobuf.ByteString +import com.trustwallet.core.app.utils.Numeric +import com.trustwallet.core.app.utils.toHex +import com.trustwallet.core.app.utils.toHexBytes +import com.trustwallet.core.app.utils.toHexBytesInByteString +import org.junit.Assert.assertEquals +import org.junit.Test +import wallet.core.jni.BitcoinPsbt +import wallet.core.jni.BitcoinScript +import wallet.core.jni.BitcoinSigHashType +import wallet.core.jni.CoinType +import wallet.core.jni.CoinType.BITCOIN +import wallet.core.jni.Hash +import wallet.core.jni.PrivateKey +import wallet.core.jni.PublicKey +import wallet.core.jni.PublicKeyType +import wallet.core.jni.proto.Bitcoin +import wallet.core.jni.proto.Bitcoin.SigningOutput +import wallet.core.jni.proto.BitcoinV2 +import wallet.core.jni.proto.Common.SigningError + +class TestBitcoinPsbt { + + init { + System.loadLibrary("TrustWalletCore") + } + + @Test + fun testSignThorSwap() { + // Successfully broadcasted tx: https://mempool.space/tx/634a416e82ac710166725f6a4090ac7b5db69687e86b2d2e38dcb3d91c956c32 + + val privateKey = "f00ffbe44c5c2838c13d2778854ac66b75e04eb6054f0241989e223223ad5e55".toHexBytesInByteString() + val psbt = "70736274ff0100bc0200000001147010db5fbcf619067c1090fec65c131443fbc80fb4aaeebe940e44206098c60000000000ffffffff0360ea000000000000160014f22a703617035ef7f490743d50f26ae08c30d0a70000000000000000426a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a35303e12000000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d000000000001011f6603010000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d00000000".toHexBytesInByteString() + + val input = BitcoinV2.PsbtSigningInput.newBuilder() + .setPsbt(psbt) + .addPrivateKeys(privateKey) + .build() + + val outputData = BitcoinPsbt.sign(input.toByteArray(), BITCOIN) + val output = BitcoinV2.PsbtSigningOutput.parseFrom(outputData) + + assertEquals(output.error, SigningError.OK) + assertEquals( + output.psbt.toByteArray().toHex(), + "0x70736274ff0100bc0200000001147010db5fbcf619067c1090fec65c131443fbc80fb4aaeebe940e44206098c60000000000ffffffff0360ea000000000000160014f22a703617035ef7f490743d50f26ae08c30d0a70000000000000000426a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a35303e12000000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d000000000001011f6603010000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d01086c02483045022100b1229a008f20691639767bf925d6b8956ea957ccc633ad6b5de3618733a55e6b02205774d3320489b8a57a6f8de07f561de3e660ff8e587f6ac5422c49020cd4dc9101210306d8c664ea8fd2683eebea1d3114d90e0a5429e5783ba49b80ddabce04ff28f300000000" + ) + assertEquals( + output.encoded.toByteArray().toHex(), + "0x02000000000101147010db5fbcf619067c1090fec65c131443fbc80fb4aaeebe940e44206098c60000000000ffffffff0360ea000000000000160014f22a703617035ef7f490743d50f26ae08c30d0a70000000000000000426a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a35303e12000000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d02483045022100b1229a008f20691639767bf925d6b8956ea957ccc633ad6b5de3618733a55e6b02205774d3320489b8a57a6f8de07f561de3e660ff8e587f6ac5422c49020cd4dc9101210306d8c664ea8fd2683eebea1d3114d90e0a5429e5783ba49b80ddabce04ff28f300000000" + ) + assertEquals( + output.txid.toByteArray().toHex(), + "0x634a416e82ac710166725f6a4090ac7b5db69687e86b2d2e38dcb3d91c956c32" + ) + } + + @Test + fun testPlanThorSwap() { + // Successfully broadcasted tx: https://mempool.space/tx/634a416e82ac710166725f6a4090ac7b5db69687e86b2d2e38dcb3d91c956c32 + + val privateKey = PrivateKey("f00ffbe44c5c2838c13d2778854ac66b75e04eb6054f0241989e223223ad5e55".toHexBytes()) + val publicKey = privateKey.getPublicKeySecp256k1(true) + val psbt = "70736274ff0100bc0200000001147010db5fbcf619067c1090fec65c131443fbc80fb4aaeebe940e44206098c60000000000ffffffff0360ea000000000000160014f22a703617035ef7f490743d50f26ae08c30d0a70000000000000000426a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a35303e12000000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d000000000001011f6603010000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d00000000".toHexBytesInByteString() + + val input = BitcoinV2.PsbtSigningInput.newBuilder() + .setPsbt(psbt) + .addPublicKeys(ByteString.copyFrom(publicKey.data())) + .build() + + val outputData = BitcoinPsbt.plan(input.toByteArray(), BITCOIN) + val output = BitcoinV2.TransactionPlan.parseFrom(outputData) + + assertEquals(output.error, SigningError.OK) + + assertEquals(output.getInputs(0).receiverAddress, "bc1qkyu3n8k8jmekl3pwvdl59k5w8enjp25akz2r3z") + assertEquals(output.getInputs(0).value, 66_406) + + // Vault transfer + assertEquals(output.getOutputs(0).toAddress, "bc1q7g48qdshqd000aysws74pun2uzxrp598gcfum0") + assertEquals(output.getOutputs(0).value, 60_000) + + // OP_RETURN + assertEquals( + output.getOutputs(1).customScriptPubkey.toByteArray().toHex(), + "0x6a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a3530" + ) + assertEquals(output.getOutputs(1).value, 0) + + // Change output + assertEquals(output.getOutputs(2).toAddress, "bc1qkyu3n8k8jmekl3pwvdl59k5w8enjp25akz2r3z") + assertEquals(output.getOutputs(2).value, 4_670) + + assertEquals(output.feeEstimate, 1736) + // Please note that `change` in PSBT planning is always 0. + // That's because we aren't able to determine which output is an actual change from PSBT. + assertEquals(output.change, 0) + } +} diff --git a/codegen-v2/src/codegen/rust/coin_address_derivation_test_generator.rs b/codegen-v2/src/codegen/rust/coin_address_derivation_test_generator.rs index 41091af2a76..4d339cb05ad 100644 --- a/codegen-v2/src/codegen/rust/coin_address_derivation_test_generator.rs +++ b/codegen-v2/src/codegen/rust/coin_address_derivation_test_generator.rs @@ -2,7 +2,7 @@ // // Copyright © 2017 Trust Wallet. -use crate::codegen::rust::tw_any_coin_directory; +use crate::codegen::rust::tw_tests_directory; use crate::registry::CoinItem; use crate::utils::FileContent; use crate::Result; @@ -14,7 +14,7 @@ const EVM_ADDRESS_DERIVATION_TEST_END: &str = "end_of_evm_address_derivation_tests_marker_do_not_modify"; pub fn coin_address_derivation_test_path() -> PathBuf { - tw_any_coin_directory() + tw_tests_directory() .join("tests") .join("coin_address_derivation_test.rs") } diff --git a/codegen-v2/src/codegen/rust/coin_integration_tests.rs b/codegen-v2/src/codegen/rust/coin_integration_tests.rs index 6c4ce2b4e0f..067462aacfc 100644 --- a/codegen-v2/src/codegen/rust/coin_integration_tests.rs +++ b/codegen-v2/src/codegen/rust/coin_integration_tests.rs @@ -2,7 +2,7 @@ // // Copyright © 2017 Trust Wallet. -use crate::codegen::rust::tw_any_coin_directory; +use crate::codegen::rust::tw_tests_directory; use crate::codegen::template_generator::TemplateGenerator; use crate::coin_id::CoinId; use crate::registry::CoinItem; @@ -20,7 +20,7 @@ const MOD_ADDRESS_TESTS_TEMPLATE: &str = include_str!("templates/integration_tes const SIGN_TESTS_TEMPLATE: &str = include_str!("templates/integration_tests/sign_tests.rs"); pub fn chains_integration_tests_directory() -> PathBuf { - tw_any_coin_directory().join("tests").join("chains") + tw_tests_directory().join("tests").join("chains") } pub fn coin_integration_tests_directory(id: &CoinId) -> PathBuf { @@ -28,7 +28,7 @@ pub fn coin_integration_tests_directory(id: &CoinId) -> PathBuf { } pub fn coin_address_derivation_test_path() -> PathBuf { - tw_any_coin_directory() + tw_tests_directory() .join("tests") .join("coin_address_derivation_test.rs") } diff --git a/codegen-v2/src/codegen/rust/mod.rs b/codegen-v2/src/codegen/rust/mod.rs index 8905e725a32..a9de11f38fa 100644 --- a/codegen-v2/src/codegen/rust/mod.rs +++ b/codegen-v2/src/codegen/rust/mod.rs @@ -26,8 +26,8 @@ pub fn chains_directory() -> PathBuf { rust_source_directory().join("chains") } -pub fn tw_any_coin_directory() -> PathBuf { - rust_source_directory().join("tw_any_coin") +pub fn tw_tests_directory() -> PathBuf { + rust_source_directory().join("tw_tests") } pub fn workspace_toml_path() -> PathBuf { diff --git a/docs/registry.md b/docs/registry.md index acd061790f2..7606e14a45b 100644 --- a/docs/registry.md +++ b/docs/registry.md @@ -130,7 +130,7 @@ This list is generated from [./registry.json](../registry.json) | 10004689 | IoTeX EVM | IOTX | | | | 10007000 | NativeZetaChain | ZETA | | | | 10007700 | NativeCanto | CANTO | | | -| 10008217 | Kaia | KLAY | | | +| 10008217 | Kaia | KLAY | | | | 10009000 | Avalanche C-Chain | AVAX | | | | 10009001 | Evmos | EVMOS | | | | 10042170 | Arbitrum Nova | ETH | | | diff --git a/include/TrustWalletCore/TWBitcoinPsbt.h b/include/TrustWalletCore/TWBitcoinPsbt.h new file mode 100644 index 00000000000..5860e3332b7 --- /dev/null +++ b/include/TrustWalletCore/TWBitcoinPsbt.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "TWBase.h" +#include "TWBitcoinSigHashType.h" +#include "TWCoinType.h" +#include "TWData.h" +#include "TWPublicKey.h" + +TW_EXTERN_C_BEGIN + +/// Represents a signer to sign/plan PSBT for Bitcoin blockchains. +TW_EXPORT_CLASS +struct TWBitcoinPsbt; + +/// Signs a PSBT (Partially Signed Bitcoin Transaction) specified by the signing input and coin type. +/// +/// \param input The serialized data of a signing input (e.g. `TW.BitcoinV2.Proto.PsbtSigningInput`) +/// \param coin The given coin type to sign the PSBT for. +/// \return The serialized data of a `Proto.PsbtSigningOutput` proto object (e.g. `TW.BitcoinV2.Proto.PsbtSigningOutput`). +TW_EXPORT_STATIC_METHOD +TWData* _Nonnull TWBitcoinPsbtSign(TWData* _Nonnull input, enum TWCoinType coin); + +/// Plans a PSBT (Partially Signed Bitcoin Transaction). +/// Can be used to get the transaction detailed decoded from PSBT. +/// +/// \param input The serialized data of a signing input (e.g. `TW.BitcoinV2.Proto.PsbtSigningInput`) +/// \param coin The given coin type to sign the PSBT for. +/// \return The serialized data of a `Proto.TransactionPlan` proto object (e.g. `TW.BitcoinV2.Proto.TransactionPlan`). +TW_EXPORT_STATIC_METHOD +TWData* _Nonnull TWBitcoinPsbtPlan(TWData* _Nonnull input, enum TWCoinType coin); + +TW_EXTERN_C_END diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 49f8393f329..108bb84e1e0 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1710,21 +1710,14 @@ dependencies = [ name = "tw_any_coin" version = "0.1.0" dependencies = [ - "serde", - "serde_json", - "tw_any_coin", "tw_coin_entry", "tw_coin_registry", - "tw_cosmos_sdk", "tw_encoding", "tw_hash", "tw_keypair", "tw_memory", "tw_misc", - "tw_number", "tw_proto", - "tw_ton_sdk", - "tw_utxo", ] [[package]] @@ -1805,7 +1798,6 @@ dependencies = [ "tw_misc", "tw_proto", "tw_utxo", - "wallet-core-rs", ] [[package]] @@ -1854,6 +1846,7 @@ dependencies = [ "tw_sui", "tw_thorchain", "tw_ton", + "tw_utxo", ] [[package]] @@ -2124,6 +2117,30 @@ dependencies = [ "tw_proto", ] +[[package]] +name = "tw_tests" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "tw_any_coin", + "tw_coin_entry", + "tw_coin_registry", + "tw_cosmos_sdk", + "tw_encoding", + "tw_hash", + "tw_keypair", + "tw_memory", + "tw_misc", + "tw_number", + "tw_proto", + "tw_solana", + "tw_ton", + "tw_ton_sdk", + "tw_utxo", + "wallet-core-rs", +] + [[package]] name = "tw_thorchain" version = "0.1.0" @@ -2145,6 +2162,7 @@ dependencies = [ "tw_hash", "tw_keypair", "tw_memory", + "tw_misc", "tw_number", "tw_proto", "tw_ton_sdk", @@ -2257,10 +2275,8 @@ name = "wallet-core-rs" version = "0.1.0" dependencies = [ "bitreader", - "serde_json", "tw_any_coin", "tw_bitcoin", - "tw_coin_entry", "tw_coin_registry", "tw_encoding", "tw_ethereum", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 6aed21641b8..ca41fb6099d 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -2,6 +2,7 @@ members = [ "chains/tw_aptos", "chains/tw_binance", + "chains/tw_bitcoin", "chains/tw_cosmos", "chains/tw_ethereum", "chains/tw_greenfield", @@ -18,7 +19,6 @@ members = [ "tw_any_coin", "tw_base58_address", "tw_bech32_address", - "tw_bitcoin", "tw_coin_entry", "tw_coin_registry", "tw_cosmos_sdk", @@ -30,6 +30,7 @@ members = [ "tw_misc", "tw_number", "tw_proto", + "tw_tests", "wallet_core_bin", "wallet_core_rs", ] diff --git a/rust/chains/tw_bitcoin/Cargo.toml b/rust/chains/tw_bitcoin/Cargo.toml new file mode 100644 index 00000000000..1d5571814b0 --- /dev/null +++ b/rust/chains/tw_bitcoin/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "tw_bitcoin" +version = "0.1.0" +edition = "2021" + +[dependencies] +bitcoin = { version = "0.30.0", features = ["rand-std"] } +secp256k1 = { version = "0.27.0", features = ["global-context", "rand-std"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tw_bech32_address = { path = "../../tw_bech32_address" } +tw_base58_address = { path = "../../tw_base58_address" } +tw_coin_entry = { path = "../../tw_coin_entry", features = ["test-utils"] } +tw_encoding = { path = "../../tw_encoding" } +tw_hash = { path = "../../tw_hash" } +tw_keypair = { path = "../../tw_keypair" } +tw_memory = { path = "../../tw_memory" } +tw_misc = { path = "../../tw_misc" } +tw_proto = { path = "../../tw_proto" } +tw_utxo = { path = "../../frameworks/tw_utxo" } diff --git a/rust/tw_bitcoin/src/entry.rs b/rust/chains/tw_bitcoin/src/entry.rs similarity index 81% rename from rust/tw_bitcoin/src/entry.rs rename to rust/chains/tw_bitcoin/src/entry.rs index 5d6b81508ec..67d597814b4 100644 --- a/rust/tw_bitcoin/src/entry.rs +++ b/rust/chains/tw_bitcoin/src/entry.rs @@ -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; @@ -14,6 +15,7 @@ use tw_coin_entry::modules::wallet_connector::NoWalletConnector; use tw_keypair::tw::PublicKey; use tw_proto::BitcoinV2::Proto; use tw_utxo::address::standard_bitcoin::{StandardBitcoinAddress, StandardBitcoinPrefix}; +use tw_utxo::utxo_entry::UtxoEntry; pub struct BitcoinEntry; @@ -97,3 +99,27 @@ impl CoinEntry for BitcoinEntry { Some(BitcoinTransactionUtil) } } + +impl UtxoEntry for BitcoinEntry { + type PsbtSigningInput<'a> = Proto::PsbtSigningInput<'a>; + type PsbtSigningOutput = Proto::PsbtSigningOutput<'static>; + type PsbtTransactionPlan = Proto::TransactionPlan<'static>; + + #[inline] + fn sign_psbt( + &self, + coin: &dyn CoinContext, + input: Self::PsbtSigningInput<'_>, + ) -> 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) + } +} diff --git a/rust/tw_bitcoin/src/lib.rs b/rust/chains/tw_bitcoin/src/lib.rs similarity index 100% rename from rust/tw_bitcoin/src/lib.rs rename to rust/chains/tw_bitcoin/src/lib.rs diff --git a/rust/tw_bitcoin/src/modules/compiler.rs b/rust/chains/tw_bitcoin/src/modules/compiler.rs similarity index 100% rename from rust/tw_bitcoin/src/modules/compiler.rs rename to rust/chains/tw_bitcoin/src/modules/compiler.rs diff --git a/rust/tw_bitcoin/src/modules/mod.rs b/rust/chains/tw_bitcoin/src/modules/mod.rs similarity index 79% rename from rust/tw_bitcoin/src/modules/mod.rs rename to rust/chains/tw_bitcoin/src/modules/mod.rs index d7ac86f787f..d275e69e9de 100644 --- a/rust/tw_bitcoin/src/modules/mod.rs +++ b/rust/chains/tw_bitcoin/src/modules/mod.rs @@ -5,6 +5,9 @@ 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; pub mod transaction_util; diff --git a/rust/tw_bitcoin/src/modules/planner.rs b/rust/chains/tw_bitcoin/src/modules/planner.rs similarity index 100% rename from rust/tw_bitcoin/src/modules/planner.rs rename to rust/chains/tw_bitcoin/src/modules/planner.rs diff --git a/rust/tw_bitcoin/src/modules/protobuf_builder/mod.rs b/rust/chains/tw_bitcoin/src/modules/protobuf_builder/mod.rs similarity index 100% rename from rust/tw_bitcoin/src/modules/protobuf_builder/mod.rs rename to rust/chains/tw_bitcoin/src/modules/protobuf_builder/mod.rs diff --git a/rust/chains/tw_bitcoin/src/modules/psbt.rs b/rust/chains/tw_bitcoin/src/modules/psbt.rs new file mode 100644 index 00000000000..58bb5d54a58 --- /dev/null +++ b/rust/chains/tw_bitcoin/src/modules/psbt.rs @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use bitcoin::psbt::Psbt; +use tw_utxo::transaction::standard_transaction::Transaction; + +/// Finalizes the [Partially Signed Bitcoin Transaction](Psbt) +/// by updating the final `script_sig` and/or `witness`. +pub fn update_psbt_signed(psbt: &mut Psbt, signed_tx: &Transaction) { + for (signed_txin, utxo_psbt) in signed_tx.inputs.iter().zip(psbt.inputs.iter_mut()) { + if !signed_txin.script_sig.is_empty() { + utxo_psbt.final_script_sig = Some(bitcoin::ScriptBuf::from_bytes( + signed_txin.script_sig.to_vec(), + )); + } + + if !signed_txin.witness.is_empty() { + let mut final_witness = bitcoin::Witness::new(); + for witness_item in signed_txin.witness.as_items() { + final_witness.push(bitcoin::ScriptBuf::from_bytes(witness_item.to_vec())); + } + utxo_psbt.final_script_witness = Some(final_witness); + } + } +} diff --git a/rust/chains/tw_bitcoin/src/modules/psbt_planner.rs b/rust/chains/tw_bitcoin/src/modules/psbt_planner.rs new file mode 100644 index 00000000000..a57945daeef --- /dev/null +++ b/rust/chains/tw_bitcoin/src/modules/psbt_planner.rs @@ -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> { + 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::>()?; + + let outputs: Vec<_> = unsigned_tx + .outputs() + .iter() + .map(|txout| Self::output_to_proto(txout, &chain_info)) + .collect::>()?; + + 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> { + 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> { + 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, + }) + } +} diff --git a/rust/chains/tw_bitcoin/src/modules/psbt_request/mod.rs b/rust/chains/tw_bitcoin/src/modules/psbt_request/mod.rs new file mode 100644 index 00000000000..dcf92cbe366 --- /dev/null +++ b/rust/chains/tw_bitcoin/src/modules/psbt_request/mod.rs @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::modules::psbt_request::output_psbt::OutputPsbt; +use crate::modules::psbt_request::utxo_psbt::UtxoPsbt; +use crate::modules::tx_builder::public_keys::PublicKeys; +use bitcoin::psbt::Psbt; +use tw_coin_entry::error::prelude::*; +use tw_proto::BitcoinV2::Proto; +use tw_utxo::transaction::standard_transaction::builder::TransactionBuilder; +use tw_utxo::transaction::standard_transaction::Transaction; +use tw_utxo::transaction::unsigned_transaction::UnsignedTransaction; + +pub mod output_psbt; +pub mod utxo_psbt; + +pub struct PsbtRequest { + pub psbt: Psbt, + pub unsigned_tx: UnsignedTransaction, +} + +impl PsbtRequest { + pub fn build(input: &Proto::PsbtSigningInput) -> SigningResult { + let psbt = Psbt::deserialize(input.psbt.as_ref()) + .tw_err(|_| SigningErrorType::Error_input_parse) + .context("Error deserializing PSBT")?; + + let version = psbt + .unsigned_tx + .version + .try_into() + .tw_err(|_| SigningErrorType::Error_invalid_params) + .context("Invalid PSBT transaction version")?; + let lock_time = psbt.unsigned_tx.lock_time.to_consensus_u32(); + + let public_keys = Self::get_public_keys(input)?; + + let mut builder = TransactionBuilder::default(); + builder.version(version).lock_time(lock_time); + + // Add all UTXOs to the unsigned transaction builder. + for (txin, txin_psbt) in psbt.unsigned_tx.input.iter().zip(psbt.inputs.iter()) { + let utxo_builder = UtxoPsbt::new(txin, txin_psbt, &public_keys); + + let (utxo, utxo_args) = utxo_builder + .build() + .context("Error creating UTXO from PSBT")?; + builder.push_input(utxo, utxo_args); + } + + // Add all outputs to the unsigned transaction builder. + for txout in psbt.unsigned_tx.output.iter() { + let output = OutputPsbt::new(txout) + .build() + .context("Error creating Output from PSBT")?; + builder.push_output(output); + } + + let unsigned_tx = builder.build()?; + Ok(PsbtRequest { psbt, unsigned_tx }) + } + + fn get_public_keys(input: &Proto::PsbtSigningInput) -> SigningResult { + let mut public_keys = PublicKeys::default(); + + if input.private_keys.is_empty() { + for public in input.public_keys.iter() { + public_keys.add_public_key(public.to_vec()); + } + } else { + for private in input.private_keys.iter() { + public_keys.add_public_with_ecdsa_private(private)?; + } + } + + Ok(public_keys) + } +} diff --git a/rust/chains/tw_bitcoin/src/modules/psbt_request/output_psbt.rs b/rust/chains/tw_bitcoin/src/modules/psbt_request/output_psbt.rs new file mode 100644 index 00000000000..c691744230d --- /dev/null +++ b/rust/chains/tw_bitcoin/src/modules/psbt_request/output_psbt.rs @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_entry::error::prelude::*; +use tw_utxo::script::Script; +use tw_utxo::transaction::standard_transaction::TransactionOutput; + +/// Currently, we rely on `bitcoin` crate to build our own [`TransactionOutput`]. +pub struct OutputPsbt<'a> { + output: &'a bitcoin::TxOut, +} + +impl<'a> OutputPsbt<'a> { + pub fn new(output: &'a bitcoin::TxOut) -> Self { + OutputPsbt { output } + } + + pub fn build(self) -> SigningResult { + let value = self + .output + .value + .try_into() + .tw_err(|_| SigningErrorType::Error_invalid_utxo_amount) + .context("PSBT Output amount is too large")?; + let script_pubkey = Script::from(self.output.script_pubkey.to_bytes()); + Ok(TransactionOutput { + value, + script_pubkey, + }) + } +} diff --git a/rust/chains/tw_bitcoin/src/modules/psbt_request/utxo_psbt.rs b/rust/chains/tw_bitcoin/src/modules/psbt_request/utxo_psbt.rs new file mode 100644 index 00000000000..58d144de377 --- /dev/null +++ b/rust/chains/tw_bitcoin/src/modules/psbt_request/utxo_psbt.rs @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::modules::tx_builder::public_keys::PublicKeys; +use crate::modules::tx_builder::script_parser::{StandardScript, StandardScriptParser}; +use secp256k1::ThirtyTwoByteHash; +use tw_coin_entry::error::prelude::*; +use tw_hash::H256; +use tw_utxo::script::Script; +use tw_utxo::sighash::SighashType; +use tw_utxo::transaction::standard_transaction::builder::UtxoBuilder; +use tw_utxo::transaction::standard_transaction::TransactionInput; +use tw_utxo::transaction::UtxoToSign; + +/// Currently, we rely on `bitcoin` crate to build our own [`UtxoToSign`]. +pub struct UtxoPsbt<'a> { + utxo: &'a bitcoin::TxIn, + utxo_psbt: &'a bitcoin::psbt::Input, + public_keys: &'a PublicKeys, +} + +impl<'a> UtxoPsbt<'a> { + pub fn new( + utxo: &'a bitcoin::TxIn, + utxo_psbt: &'a bitcoin::psbt::Input, + public_keys: &'a PublicKeys, + ) -> Self { + UtxoPsbt { + utxo, + utxo_psbt, + public_keys, + } + } + + pub fn build(self) -> SigningResult<(TransactionInput, UtxoToSign)> { + if let Some(ref non_witness_utxo) = self.utxo_psbt.non_witness_utxo { + self.build_non_witness_utxo(non_witness_utxo) + } else if let Some(ref witness_utxo) = self.utxo_psbt.witness_utxo { + self.build_witness_utxo(witness_utxo) + } else { + SigningError::err(SigningErrorType::Error_invalid_params) + .context("Neither 'witness_utxo' nor 'non_witness_utxo' are set in the PSBT") + } + } + + pub fn build_non_witness_utxo( + &self, + non_witness_utxo: &bitcoin::Transaction, + ) -> SigningResult<(TransactionInput, UtxoToSign)> { + let prev_out_idx = self.utxo.previous_output.vout as usize; + let prev_out = non_witness_utxo + .output + .get(prev_out_idx) + .or_tw_err(SigningErrorType::Error_invalid_utxo) + .with_context(|| { + format!("'Psbt::non_witness_utxo' does not contain '{prev_out_idx}' output") + })?; + + let script = Script::from(prev_out.script_pubkey.to_bytes()); + let builder = self.prepare_builder(prev_out.value)?; + + self.build_utxo_with_script(builder, &script) + } + + pub fn build_witness_utxo( + &self, + witness_utxo: &bitcoin::TxOut, + ) -> 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) + } + + 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) + }, + StandardScript::P2WPKH(pubkey_hash) => { + let pubkey = self.public_keys.get_ecdsa_public_key(&pubkey_hash)?; + builder.p2wpkh(&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"), + } + } + + fn prepare_builder(&self, amount: u64) -> SigningResult { + 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; + + let sighash_ty = match self.utxo_psbt.sighash_type { + Some(psbt_ty) => SighashType::from_u32(psbt_ty.to_u32())?, + None => SighashType::default(), + }; + + let amount = amount + .try_into() + .tw_err(|_| SigningErrorType::Error_invalid_utxo_amount) + .context("PSBT UTXO amount is too large")?; + + Ok(UtxoBuilder::default() + .prev_txid(prevout_hash) + .prev_index(prevout_index) + .sequence(sequence) + .sighash_type(sighash_ty) + .amount(amount)) + } + + fn has_tap_scripts(&self) -> bool { + !self.utxo_psbt.tap_scripts.is_empty() + } +} diff --git a/rust/tw_bitcoin/src/modules/signer.rs b/rust/chains/tw_bitcoin/src/modules/signer.rs similarity index 55% rename from rust/tw_bitcoin/src/modules/signer.rs rename to rust/chains/tw_bitcoin/src/modules/signer.rs index ccc262288f7..7cd2e7d4749 100644 --- a/rust/tw_bitcoin/src/modules/signer.rs +++ b/rust/chains/tw_bitcoin/src/modules/signer.rs @@ -3,6 +3,8 @@ // Copyright © 2017 Trust Wallet. use crate::modules::protobuf_builder::ProtobufBuilder; +use crate::modules::psbt::update_psbt_signed; +use crate::modules::psbt_request::PsbtRequest; use crate::modules::signing_request::SigningRequestBuilder; use std::borrow::Cow; use tw_coin_entry::coin_context::CoinContext; @@ -15,7 +17,9 @@ use tw_utxo::modules::tx_planner::TxPlanner; use tw_utxo::modules::tx_signer::TxSigner; use tw_utxo::modules::utxo_selector::SelectResult; use tw_utxo::signing_mode::SigningMethod; +use tw_utxo::transaction::standard_transaction::Transaction; use tw_utxo::transaction::transaction_interface::TransactionInterface; +use tw_utxo::transaction::unsigned_transaction::UnsignedTransaction; pub struct BitcoinSigner; @@ -35,6 +39,83 @@ impl BitcoinSigner { let request = SigningRequestBuilder::build(coin, input)?; let SelectResult { unsigned_tx, plan } = TxPlanner::plan(request)?; + let keys_manager = Self::keys_manager_for_tx( + &input.private_keys, + &unsigned_tx, + input.dangerous_use_fixed_schnorr_rng, + )?; + + let signed_tx = + TxSigner::sign_tx(unsigned_tx, &keys_manager).context("Error signing transaction")?; + + Ok(Proto::SigningOutput { + transaction: Some(ProtobufBuilder::tx_to_proto(&signed_tx)), + encoded: Cow::from(signed_tx.encode_out()), + txid: Cow::from(signed_tx.txid()), + // `vsize` could have been changed after the transaction being signed. + vsize: signed_tx.vsize() as u64, + // `fee` should haven't been changed since it's a difference between `sum(inputs)` and `sum(outputs)`. + fee: plan.fee_estimate, + weight: signed_tx.weight() as u64, + ..Proto::SigningOutput::default() + }) + } + + pub fn sign_psbt( + coin: &dyn CoinContext, + input: &Proto::PsbtSigningInput<'_>, + ) -> Proto::PsbtSigningOutput<'static> { + Self::sign_psbt_impl(coin, input) + .unwrap_or_else(|e| signing_output_error!(Proto::PsbtSigningOutput, e)) + } + + pub fn sign_psbt_impl( + _coin: &dyn CoinContext, + input: &Proto::PsbtSigningInput<'_>, + ) -> SigningResult> { + let PsbtRequest { + mut psbt, + unsigned_tx, + } = PsbtRequest::build(input)?; + + let fee = unsigned_tx + .total_input()? + .checked_sub(unsigned_tx.total_output()?) + .or_tw_err(SigningErrorType::Error_not_enough_utxos) + .context("PSBT sum(input) < sum(output)")?; + + let keys_manager = Self::keys_manager_for_tx( + &input.private_keys, + &unsigned_tx, + input.dangerous_use_fixed_schnorr_rng, + )?; + + let signed_tx = + TxSigner::sign_tx(unsigned_tx, &keys_manager).context("Error signing transaction")?; + + update_psbt_signed(&mut psbt, &signed_tx); + + Ok(Proto::PsbtSigningOutput { + transaction: Some(ProtobufBuilder::tx_to_proto(&signed_tx)), + encoded: Cow::from(signed_tx.encode_out()), + txid: Cow::from(signed_tx.txid()), + // `vsize` could have been changed after the transaction being signed. + vsize: signed_tx.vsize() as u64, + fee, + weight: signed_tx.weight() as u64, + psbt: Cow::from(psbt.serialize()), + ..Proto::PsbtSigningOutput::default() + }) + } + + fn keys_manager_for_tx

( + private_keys: &[P], + unsigned_tx: &UnsignedTransaction, + dangerous_use_fixed_schnorr_rng: bool, + ) -> SigningResult + where + P: AsRef<[u8]>, + { let has_taproot = unsigned_tx .input_args() .iter() @@ -43,7 +124,7 @@ impl BitcoinSigner { let mut keys_manager = KeysManager::default(); // Parse private keys and put them to the keys manager. - for private in input.private_keys.iter() { + for private in private_keys.iter() { let ecdsa_private = ecdsa::secp256k1::PrivateKey::try_from(private.as_ref()) .into_tw() .context("Invalid ecdsa secp256k1 private key")?; @@ -54,7 +135,7 @@ impl BitcoinSigner { .into_tw() .context("Invalid schnorr private key")?; - if input.dangerous_use_fixed_schnorr_rng { + if dangerous_use_fixed_schnorr_rng { keys_manager.add_schnorr_private(schnorr_private.no_aux_rand()); } else { keys_manager.add_schnorr_private(schnorr_private); @@ -62,19 +143,6 @@ impl BitcoinSigner { } } - let signed_tx = - TxSigner::sign_tx(unsigned_tx, &keys_manager).context("Error signing transaction")?; - - Ok(Proto::SigningOutput { - transaction: Some(ProtobufBuilder::tx_to_proto(&signed_tx)), - encoded: Cow::from(signed_tx.encode_out()), - txid: Cow::from(signed_tx.txid()), - // `vsize` could have been changed after the transaction being signed. - vsize: signed_tx.vsize() as u64, - // `fee` should haven't been changed since it's a difference between `sum(inputs)` and `sum(outputs)`. - fee: plan.fee_estimate, - weight: signed_tx.weight() as u64, - ..Proto::SigningOutput::default() - }) + Ok(keys_manager) } } diff --git a/rust/tw_bitcoin/src/modules/signing_request/mod.rs b/rust/chains/tw_bitcoin/src/modules/signing_request/mod.rs similarity index 95% rename from rust/tw_bitcoin/src/modules/signing_request/mod.rs rename to rust/chains/tw_bitcoin/src/modules/signing_request/mod.rs index 8795d56fefd..81548a754fb 100644 --- a/rust/tw_bitcoin/src/modules/signing_request/mod.rs +++ b/rust/chains/tw_bitcoin/src/modules/signing_request/mod.rs @@ -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}; @@ -138,7 +139,7 @@ impl SigningRequestBuilder { } } - fn chain_info( + pub fn chain_info( coin: &dyn CoinContext, chain_info: &Option, ) -> SigningResult { @@ -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") diff --git a/rust/tw_bitcoin/src/modules/transaction_util.rs b/rust/chains/tw_bitcoin/src/modules/transaction_util.rs similarity index 100% rename from rust/tw_bitcoin/src/modules/transaction_util.rs rename to rust/chains/tw_bitcoin/src/modules/transaction_util.rs diff --git a/rust/tw_bitcoin/src/modules/tx_builder/mod.rs b/rust/chains/tw_bitcoin/src/modules/tx_builder/mod.rs similarity index 65% rename from rust/tw_bitcoin/src/modules/tx_builder/mod.rs rename to rust/chains/tw_bitcoin/src/modules/tx_builder/mod.rs index c816d0f01cf..37c99c33e53 100644 --- a/rust/tw_bitcoin/src/modules/tx_builder/mod.rs +++ b/rust/chains/tw_bitcoin/src/modules/tx_builder/mod.rs @@ -4,9 +4,12 @@ pub mod output_protobuf; pub mod public_keys; +pub mod script_parser; 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, } diff --git a/rust/tw_bitcoin/src/modules/tx_builder/output_protobuf.rs b/rust/chains/tw_bitcoin/src/modules/tx_builder/output_protobuf.rs similarity index 100% rename from rust/tw_bitcoin/src/modules/tx_builder/output_protobuf.rs rename to rust/chains/tw_bitcoin/src/modules/tx_builder/output_protobuf.rs diff --git a/rust/tw_bitcoin/src/modules/tx_builder/public_keys.rs b/rust/chains/tw_bitcoin/src/modules/tx_builder/public_keys.rs similarity index 83% rename from rust/tw_bitcoin/src/modules/tx_builder/public_keys.rs rename to rust/chains/tw_bitcoin/src/modules/tx_builder/public_keys.rs index 2ff811bd679..6c822c686b6 100644 --- a/rust/tw_bitcoin/src/modules/tx_builder/public_keys.rs +++ b/rust/chains/tw_bitcoin/src/modules/tx_builder/public_keys.rs @@ -41,4 +41,14 @@ impl PublicKeys { .or_tw_err(SigningErrorType::Error_missing_private_key) .with_context(|| format!("Missing either a private or public key corresponding to the pubkey hash: {pubkey_hash}")) } + + pub fn get_ecdsa_public_key( + &self, + pubkey_hash: &H160, + ) -> SigningResult { + let pubkey_data = self.get_public_key(pubkey_hash)?; + ecdsa::secp256k1::PublicKey::try_from(pubkey_data) + .into_tw() + .context("Expected a valid ecdsa secp256k1 public key") + } } diff --git a/rust/chains/tw_bitcoin/src/modules/tx_builder/script_parser.rs b/rust/chains/tw_bitcoin/src/modules/tx_builder/script_parser.rs new file mode 100644 index 00000000000..26d8a70e009 --- /dev/null +++ b/rust/chains/tw_bitcoin/src/modules/tx_builder/script_parser.rs @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::modules::tx_builder::BitcoinChainInfo; +use tw_coin_entry::error::prelude::*; +use tw_hash::{H160, H256}; +use tw_keypair::{ecdsa, schnorr}; +use tw_memory::Data; +use tw_utxo::address::legacy::LegacyAddress; +use tw_utxo::address::segwit::SegwitAddress; +use tw_utxo::address::standard_bitcoin::StandardBitcoinAddress; +use tw_utxo::address::taproot::TaprootAddress; +use tw_utxo::script::standard_script::conditions; +use tw_utxo::script::Script; + +pub enum StandardScript { + /// Compressed or uncompressed public key bytes. + P2PK(ecdsa::secp256k1::PublicKey), + /// Public key hash. + P2PKH(H160), + /// Script hash. + P2SH(H160), + /// Public key hash. + P2WPKH(H160), + /// Script hash. + P2WSH(H256), + /// Tweaked public key. + /// The public key can be tweaked as either key-path or script-path, + P2TR(schnorr::XOnlyPublicKey), + /// OP_RETURN payload. + OpReturn(Data), +} + +impl StandardScript { + pub fn try_to_address( + &self, + chain_info: &BitcoinChainInfo, + ) -> AddressResult> { + let try_hrp = || chain_info.hrp.clone().ok_or(AddressError::MissingPrefix); + + match self { + StandardScript::P2PK(pubkey) => { + // Display P2PK input as P2PKH. + LegacyAddress::p2pkh_with_public_key(chain_info.p2pkh_prefix, pubkey) + .map(StandardBitcoinAddress::Legacy) + .map(Some) + }, + StandardScript::P2PKH(pubkey_hash) => { + LegacyAddress::new(chain_info.p2pkh_prefix, pubkey_hash.as_slice()) + .map(StandardBitcoinAddress::Legacy) + .map(Some) + }, + StandardScript::P2SH(script_hash) => { + LegacyAddress::new(chain_info.p2sh_prefix, script_hash.as_slice()) + .map(StandardBitcoinAddress::Legacy) + .map(Some) + }, + StandardScript::P2WPKH(pubkey_hash) => { + SegwitAddress::new(try_hrp()?, pubkey_hash.to_vec()) + .map(StandardBitcoinAddress::Segwit) + .map(Some) + }, + StandardScript::P2WSH(script_hash) => { + SegwitAddress::new(try_hrp()?, script_hash.to_vec()) + .map(StandardBitcoinAddress::Segwit) + .map(Some) + }, + StandardScript::P2TR(tweaked_pubkey) => { + TaprootAddress::new(try_hrp()?, tweaked_pubkey.bytes().to_vec()) + .map(StandardBitcoinAddress::Taproot) + .map(Some) + }, + StandardScript::OpReturn(_) => Ok(None), + } + } +} + +pub struct StandardScriptParser; + +impl StandardScriptParser { + /// Later, this method can be moved to a trait. + pub fn parse(&self, script: &Script) -> SigningResult { + if let Some(pubkey) = conditions::match_p2pk(script) { + // P2PK + let pubkey = ecdsa::secp256k1::PublicKey::try_from(pubkey) + .into_tw() + .context("P2PK scriptPubkey must contain a valid ecdsa secp256k1 public key")?; + Ok(StandardScript::P2PK(pubkey)) + } else if let Some(pubkey_hash) = conditions::match_p2pkh(script) { + // P2PKH + Ok(StandardScript::P2PKH(pubkey_hash)) + } else if let Some(script_hash) = conditions::match_p2sh(script) { + // P2SH + Ok(StandardScript::P2SH(script_hash)) + } else if let Some(pubkey_hash) = conditions::match_p2wpkh(script) { + // P2WPKH + Ok(StandardScript::P2WPKH(pubkey_hash)) + } else if let Some(script_hash) = conditions::match_p2wsh(script) { + // P2WSH + Ok(StandardScript::P2WSH(script_hash)) + } else if let Some(tweaked_pubkey) = conditions::match_p2tr(script) { + // P2TR + let tweaked_pubkey_x_only = + schnorr::XOnlyPublicKey::try_from(tweaked_pubkey.as_slice()) + .into_tw() + .context("P2TR scriptPubkey must contain a valid tweaked schnorr public key")?; + Ok(StandardScript::P2TR(tweaked_pubkey_x_only)) + } else if let Some(payload) = conditions::match_op_return(script) { + // OP_RETURN + Ok(StandardScript::OpReturn(payload)) + } else { + // Unknown + SigningError::err(SigningErrorType::Error_script_output).context( + "The given custom scriptPubkey is not supported. Consider using a proper Input/Output builder", + ) + } + } +} diff --git a/rust/tw_bitcoin/src/modules/tx_builder/utxo_protobuf.rs b/rust/chains/tw_bitcoin/src/modules/tx_builder/utxo_protobuf.rs similarity index 83% rename from rust/tw_bitcoin/src/modules/tx_builder/utxo_protobuf.rs rename to rust/chains/tw_bitcoin/src/modules/tx_builder/utxo_protobuf.rs index 2c16c029a87..d536915a3c8 100644 --- a/rust/tw_bitcoin/src/modules/tx_builder/utxo_protobuf.rs +++ b/rust/chains/tw_bitcoin/src/modules/tx_builder/utxo_protobuf.rs @@ -3,6 +3,7 @@ // Copyright © 2017 Trust Wallet. use crate::modules::tx_builder::public_keys::PublicKeys; +use crate::modules::tx_builder::script_parser::{StandardScript, StandardScriptParser}; use crate::modules::tx_builder::BitcoinChainInfo; use std::str::FromStr; use tw_coin_entry::error::prelude::*; @@ -15,7 +16,6 @@ use tw_utxo::address::legacy::LegacyAddress; use tw_utxo::address::segwit::SegwitAddress; use tw_utxo::address::standard_bitcoin::StandardBitcoinAddress; use tw_utxo::address::taproot::TaprootAddress; -use tw_utxo::script::standard_script::conditions; use tw_utxo::script::Script; use tw_utxo::sighash::SighashType; use tw_utxo::transaction::standard_transaction::builder::UtxoBuilder; @@ -137,36 +137,25 @@ impl<'a> UtxoProtobuf<'a> { let script = Script::from(script_data); let builder = self.prepare_builder()?; - if let Some(pubkey) = conditions::match_p2pk(&script) { - // P2PK - let pubkey = ecdsa::secp256k1::PublicKey::try_from(pubkey.as_slice()) - .into_tw() - .context("P2PK scriptPubkey must contain a valid ecdsa secp256k1 public key")?; - builder.p2pk(&pubkey) - } else if let Some(pubkey_hash) = conditions::match_p2pkh(&script) { - // P2PKH - let pubkey = self.get_ecdsa_pubkey_from_hash(&pubkey_hash)?; - builder.p2pkh(&pubkey) - } else if let Some(pubkey_hash) = conditions::match_p2wpkh(&script) { - // P2WPKH - let pubkey = self.get_ecdsa_pubkey_from_hash(&pubkey_hash)?; - builder.p2wpkh(&pubkey) - } else if let Some(tweaked_pubkey) = conditions::match_p2tr(&script) { - // P2TR - let tweaked_pubkey_x_only = - schnorr::XOnlyPublicKey::try_from(tweaked_pubkey.as_slice()) - .into_tw() - .context("P2TR scriptPubkey must contain a valid tweaked schnorr public key")?; - builder.p2tr_key_path_with_tweaked_pubkey(&tweaked_pubkey_x_only) - } else if conditions::is_p2sh(&script) || conditions::is_p2wsh(&script) { - // P2SH or P2WSH - SigningError::err(SigningErrorType::Error_script_output) - .context("P2SH and P2WSH scriptPubkey's are not supported yet") - } else { - // Unknown - SigningError::err(SigningErrorType::Error_script_output).context( - "The given custom scriptPubkey is not supported. Consider using a Proto.Input.InputBuilder", - ) + match StandardScriptParser.parse(&script)? { + StandardScript::P2PK(pk) => builder.p2pk(&pk), + StandardScript::P2PKH(pubkey_hash) => { + let pubkey = self.public_keys.get_ecdsa_public_key(&pubkey_hash)?; + builder.p2pkh(&pubkey) + }, + StandardScript::P2WPKH(pubkey_hash) => { + let pubkey = self.public_keys.get_ecdsa_public_key(&pubkey_hash)?; + builder.p2wpkh(&pubkey) + }, + StandardScript::P2TR(tweaked_pubkey) => { + 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"), } } @@ -192,7 +181,7 @@ impl<'a> UtxoProtobuf<'a> { if p2pkh_prefix == addr.prefix() { // P2PKH - let pubkey = self.get_ecdsa_pubkey_from_hash(&addr.payload())?; + let pubkey = self.public_keys.get_ecdsa_public_key(&addr.payload())?; self.prepare_builder()?.p2pkh(&pubkey) } else if p2sh_prefix == addr.prefix() { // P2SH @@ -218,7 +207,7 @@ impl<'a> UtxoProtobuf<'a> { H160::LEN => { let pubkey_hash = H160::try_from(witness_program) .expect("'witness_program' length must be checked already"); - let pubkey = self.get_ecdsa_pubkey_from_hash(&pubkey_hash)?; + let pubkey = self.public_keys.get_ecdsa_public_key(&pubkey_hash)?; self.prepare_builder()?.p2wpkh(&pubkey) }, // P2WSH @@ -273,16 +262,6 @@ impl<'a> UtxoProtobuf<'a> { .sighash_type(sighash_ty)) } - fn get_ecdsa_pubkey_from_hash( - &self, - pubkey_hash: &H160, - ) -> SigningResult { - let pubkey_data = self.public_keys.get_public_key(pubkey_hash)?; - ecdsa::secp256k1::PublicKey::try_from(pubkey_data) - .into_tw() - .context("Expected a valid ecdsa secp256k1 public key") - } - /// Tries to convert [`Proto::PublicKeyOrHash`] to [`Hash`]. /// Please note `P2PKH` and `P2WPKH` use the same `ripemd(sha256(x))` hash function. fn get_ecdsa_pubkey_from_proto( diff --git a/rust/frameworks/tw_utxo/src/lib.rs b/rust/frameworks/tw_utxo/src/lib.rs index 84748580ded..cdfe6be6642 100644 --- a/rust/frameworks/tw_utxo/src/lib.rs +++ b/rust/frameworks/tw_utxo/src/lib.rs @@ -13,3 +13,4 @@ pub mod signature; pub mod signing_mode; pub mod spending_data; pub mod transaction; +pub mod utxo_entry; diff --git a/rust/frameworks/tw_utxo/src/script/standard_script/conditions.rs b/rust/frameworks/tw_utxo/src/script/standard_script/conditions.rs index 6e7245abb32..d16c21c4dd5 100644 --- a/rust/frameworks/tw_utxo/src/script/standard_script/conditions.rs +++ b/rust/frameworks/tw_utxo/src/script/standard_script/conditions.rs @@ -153,11 +153,11 @@ pub fn is_op_return(s: &Script) -> bool { } /// Returns either a compressed or uncompressed public key data if matched. -pub fn match_p2pk(s: &Script) -> Option { +pub fn match_p2pk(s: &Script) -> Option<&[u8]> { let b = s.as_slice(); match s.len() { - 67 if b[0] == OP_PUSHBYTES_65 && b[66] == OP_CHECKSIG => Some(b[1..66].to_vec()), - 35 if b[0] == OP_PUSHBYTES_33 && b[34] == OP_CHECKSIG => Some(b[1..34].to_vec()), + 67 if b[0] == OP_PUSHBYTES_65 && b[66] == OP_CHECKSIG => Some(&b[1..66]), + 35 if b[0] == OP_PUSHBYTES_33 && b[34] == OP_CHECKSIG => Some(&b[1..34]), _ => None, } } @@ -171,6 +171,15 @@ pub fn match_p2pkh(s: &Script) -> Option { } } +/// Returns a script hash if matched. +pub fn match_p2sh(s: &Script) -> Option { + if is_p2sh(s) { + Some(H160::try_from(&s.as_slice()[2..22]).expect("is_p2sh checks the length")) + } else { + None + } +} + /// Returns a public key hash if matched. pub fn match_p2wpkh(s: &Script) -> Option { if is_p2wpkh(s) { @@ -180,6 +189,15 @@ pub fn match_p2wpkh(s: &Script) -> Option { } } +/// Returns a script hash if matched. +pub fn match_p2wsh(s: &Script) -> Option { + if is_p2wsh(s) { + Some(H256::try_from(&s.as_slice()[2..]).expect("is_p2wsh checks the length")) + } else { + None + } +} + /// Returns a tweaked schnorr public key if matched. pub fn match_p2tr(s: &Script) -> Option { if is_p2tr(s) { @@ -188,3 +206,12 @@ pub fn match_p2tr(s: &Script) -> Option { None } } + +/// Returns an OP_RETURN payload. +pub fn match_op_return(s: &Script) -> Option { + if is_op_return(s) { + Some(s.as_slice()[1..].to_vec()) + } else { + None + } +} diff --git a/rust/frameworks/tw_utxo/src/sighash.rs b/rust/frameworks/tw_utxo/src/sighash.rs index 4755dda2f14..1c7c79729e6 100644 --- a/rust/frameworks/tw_utxo/src/sighash.rs +++ b/rust/frameworks/tw_utxo/src/sighash.rs @@ -34,7 +34,7 @@ impl SighashType { /// Creates Sighash from any u32. pub fn from_u32(u: u32) -> SigningResult { let base = match u & BASE_FLAG { - 1 => SighashBase::All, + 0 | 1 => SighashBase::All, 2 => SighashBase::None, 3 => SighashBase::Single, _ => { diff --git a/rust/frameworks/tw_utxo/src/transaction/unsigned_transaction.rs b/rust/frameworks/tw_utxo/src/transaction/unsigned_transaction.rs index 4f553815d7a..75f43f780ea 100644 --- a/rust/frameworks/tw_utxo/src/transaction/unsigned_transaction.rs +++ b/rust/frameworks/tw_utxo/src/transaction/unsigned_transaction.rs @@ -49,6 +49,10 @@ where &self.utxo_args } + pub fn outputs(&self) -> &[Transaction::Output] { + self.transaction.outputs() + } + pub fn outputs_mut(&mut self) -> &mut [Transaction::Output] { self.transaction.outputs_mut() } diff --git a/rust/frameworks/tw_utxo/src/utxo_entry.rs b/rust/frameworks/tw_utxo/src/utxo_entry.rs new file mode 100644 index 00000000000..8d8321b299c --- /dev/null +++ b/rust/frameworks/tw_utxo/src/utxo_entry.rs @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_entry::coin_context::CoinContext; +use tw_memory::Data; +use tw_proto::{deserialize, serialize, MessageRead, MessageWrite, ProtoResult}; + +pub trait UtxoEntry { + type PsbtSigningInput<'a>: MessageRead<'a>; + type PsbtSigningOutput: MessageWrite; + type PsbtTransactionPlan: MessageWrite; + + fn sign_psbt( + &self, + coin: &dyn CoinContext, + input: Self::PsbtSigningInput<'_>, + ) -> Self::PsbtSigningOutput; + + fn plan_psbt( + &self, + coin: &dyn CoinContext, + input: Self::PsbtSigningInput<'_>, + ) -> Self::PsbtTransactionPlan; +} + +pub trait UtxoEntryExt { + fn sign_psbt(&self, coin: &dyn CoinContext, input: &[u8]) -> ProtoResult; + + fn plan_psbt(&self, coin: &dyn CoinContext, input: &[u8]) -> ProtoResult; +} + +impl UtxoEntryExt for T { + fn sign_psbt(&self, coin: &dyn CoinContext, input: &[u8]) -> ProtoResult { + let input: T::PsbtSigningInput<'_> = deserialize(input)?; + let output = ::sign_psbt(self, coin, input); + serialize(&output) + } + + fn plan_psbt(&self, coin: &dyn CoinContext, input: &[u8]) -> ProtoResult { + let input: T::PsbtSigningInput<'_> = deserialize(input)?; + let output = ::plan_psbt(self, coin, input); + serialize(&output) + } +} diff --git a/rust/tw_any_coin/Cargo.toml b/rust/tw_any_coin/Cargo.toml index a0eb245c67e..a62b4ae9ef6 100644 --- a/rust/tw_any_coin/Cargo.toml +++ b/rust/tw_any_coin/Cargo.toml @@ -20,15 +20,3 @@ test-utils = [ "tw_misc/test-utils", "tw_proto" ] - -[dev-dependencies] -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -tw_any_coin = { path = "./", features = ["test-utils"] } -tw_cosmos_sdk = { path = "../tw_cosmos_sdk", features = ["test-utils"] } -tw_keypair = { path = "../tw_keypair", features = ["test-utils"] } -tw_memory = { path = "../tw_memory", features = ["test-utils"] } -tw_misc = { path = "../tw_misc", features = ["test-utils"] } -tw_number = { path = "../tw_number" } -tw_ton_sdk = { path = "../frameworks/tw_ton_sdk" } -tw_utxo = { path = "../frameworks/tw_utxo" } diff --git a/rust/tw_coin_registry/Cargo.toml b/rust/tw_coin_registry/Cargo.toml index 6cc8641f84f..b23f576ab7b 100644 --- a/rust/tw_coin_registry/Cargo.toml +++ b/rust/tw_coin_registry/Cargo.toml @@ -11,7 +11,7 @@ strum = "0.25" strum_macros = "0.25" tw_aptos = { path = "../chains/tw_aptos" } tw_binance = { path = "../chains/tw_binance" } -tw_bitcoin = { path = "../tw_bitcoin" } +tw_bitcoin = { path = "../chains/tw_bitcoin" } tw_coin_entry = { path = "../tw_coin_entry" } tw_cosmos = { path = "../chains/tw_cosmos" } tw_ethereum = { path = "../chains/tw_ethereum" } @@ -29,6 +29,7 @@ tw_solana = { path = "../chains/tw_solana" } tw_sui = { path = "../chains/tw_sui" } tw_thorchain = { path = "../chains/tw_thorchain" } tw_ton = { path = "../chains/tw_ton" } +tw_utxo = { path = "../frameworks/tw_utxo" } [build-dependencies] itertools = "0.10.5" diff --git a/rust/tw_coin_registry/src/dispatcher.rs b/rust/tw_coin_registry/src/dispatcher.rs index bda3fdba931..ec28beb651a 100644 --- a/rust/tw_coin_registry/src/dispatcher.rs +++ b/rust/tw_coin_registry/src/dispatcher.rs @@ -23,9 +23,11 @@ use tw_solana::entry::SolanaEntry; use tw_sui::entry::SuiEntry; use tw_thorchain::entry::ThorchainEntry; use tw_ton::entry::TheOpenNetworkEntry; +use tw_utxo::utxo_entry::UtxoEntryExt; pub type CoinEntryExtStaticRef = &'static dyn CoinEntryExt; pub type EvmEntryExtStaticRef = &'static dyn EvmEntryExt; +pub type UtxoEntryExtStaticRef = &'static dyn UtxoEntryExt; // start_of_blockchain_entries - USED TO GENERATE CODE const APTOS: AptosEntry = AptosEntry; @@ -83,3 +85,11 @@ pub fn evm_dispatcher(coin: CoinType) -> RegistryResult { _ => Err(RegistryError::Unsupported), } } + +pub fn utxo_dispatcher(coin: CoinType) -> RegistryResult { + let item = get_coin_item(coin)?; + match item.blockchain { + BlockchainType::Bitcoin => Ok(&BITCOIN), + _ => Err(RegistryError::Unsupported), + } +} diff --git a/rust/tw_hash/src/hash_array.rs b/rust/tw_hash/src/hash_array.rs index 6bbae8a85db..936e02118f9 100644 --- a/rust/tw_hash/src/hash_array.rs +++ b/rust/tw_hash/src/hash_array.rs @@ -96,7 +96,7 @@ impl Hash { /// This is a [`Hash::split`] helper that ensures that `L + R == N` at compile time. /// Assertion example: -/// ```rust(ignore) +/// ```ignore /// let hash = H256::default(); /// let (left, right): (H128, H160) = hash.split(); /// diff --git a/rust/tw_bitcoin/Cargo.toml b/rust/tw_tests/Cargo.toml similarity index 50% rename from rust/tw_bitcoin/Cargo.toml rename to rust/tw_tests/Cargo.toml index ff89092a2f9..3bc066b06bd 100644 --- a/rust/tw_bitcoin/Cargo.toml +++ b/rust/tw_tests/Cargo.toml @@ -1,23 +1,25 @@ [package] -name = "tw_bitcoin" +name = "tw_tests" version = "0.1.0" edition = "2021" -[dependencies] -bitcoin = { version = "0.30.0", features = ["rand-std"] } -secp256k1 = { version = "0.27.0", features = ["global-context", "rand-std"] } +[dev-dependencies] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +tw_any_coin = { path = "../tw_any_coin", features = ["test-utils"] } tw_coin_entry = { path = "../tw_coin_entry", features = ["test-utils"] } +tw_coin_registry = { path = "../tw_coin_registry" } tw_encoding = { path = "../tw_encoding" } -tw_keypair = { path = "../tw_keypair" } -tw_memory = { path = "../tw_memory" } tw_hash = { path = "../tw_hash" } +tw_keypair = { path = "../tw_keypair", features = ["test-utils"] } +tw_memory = { path = "../tw_memory" } tw_misc = { path = "../tw_misc" } +tw_number = { path = "../tw_number" } tw_proto = { path = "../tw_proto" } -tw_utxo = { path = "../frameworks/tw_utxo" } -tw_bech32_address = { path = "../tw_bech32_address" } -tw_base58_address = { path = "../tw_base58_address" } - -[dev-dependencies] wallet-core-rs = { path = "../wallet_core_rs" } +# Chain specific: +tw_cosmos_sdk = { path = "../tw_cosmos_sdk", features = ["test-utils"] } +tw_solana = { path = "../chains/tw_solana" } +tw_ton = { path = "../chains/tw_ton" } +tw_ton_sdk = { path = "../frameworks/tw_ton_sdk" } +tw_utxo = { path = "../frameworks/tw_utxo" } diff --git a/rust/tw_tests/src/lib.rs b/rust/tw_tests/src/lib.rs new file mode 100644 index 00000000000..7920eae86e8 --- /dev/null +++ b/rust/tw_tests/src/lib.rs @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +//! DO NOT WRITE ANY CODE WITHIN `tw_tests/src`. diff --git a/rust/tw_any_coin/tests/chain_tests.rs b/rust/tw_tests/tests/chain_tests.rs similarity index 100% rename from rust/tw_any_coin/tests/chain_tests.rs rename to rust/tw_tests/tests/chain_tests.rs diff --git a/rust/tw_any_coin/tests/chains/aptos/aptos_address.rs b/rust/tw_tests/tests/chains/aptos/aptos_address.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/aptos/aptos_address.rs rename to rust/tw_tests/tests/chains/aptos/aptos_address.rs diff --git a/rust/tw_any_coin/tests/chains/aptos/aptos_compile.rs b/rust/tw_tests/tests/chains/aptos/aptos_compile.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/aptos/aptos_compile.rs rename to rust/tw_tests/tests/chains/aptos/aptos_compile.rs diff --git a/rust/tw_any_coin/tests/chains/aptos/aptos_sign.rs b/rust/tw_tests/tests/chains/aptos/aptos_sign.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/aptos/aptos_sign.rs rename to rust/tw_tests/tests/chains/aptos/aptos_sign.rs diff --git a/rust/tw_any_coin/tests/chains/aptos/aptos_transaction_util.rs b/rust/tw_tests/tests/chains/aptos/aptos_transaction_util.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/aptos/aptos_transaction_util.rs rename to rust/tw_tests/tests/chains/aptos/aptos_transaction_util.rs diff --git a/rust/tw_any_coin/tests/chains/aptos/mod.rs b/rust/tw_tests/tests/chains/aptos/mod.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/aptos/mod.rs rename to rust/tw_tests/tests/chains/aptos/mod.rs diff --git a/rust/tw_any_coin/tests/chains/aptos/test_cases.rs b/rust/tw_tests/tests/chains/aptos/test_cases.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/aptos/test_cases.rs rename to rust/tw_tests/tests/chains/aptos/test_cases.rs diff --git a/rust/tw_any_coin/tests/chains/binance/binance_address.rs b/rust/tw_tests/tests/chains/binance/binance_address.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/binance/binance_address.rs rename to rust/tw_tests/tests/chains/binance/binance_address.rs diff --git a/rust/tw_any_coin/tests/chains/binance/binance_compile.rs b/rust/tw_tests/tests/chains/binance/binance_compile.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/binance/binance_compile.rs rename to rust/tw_tests/tests/chains/binance/binance_compile.rs diff --git a/rust/tw_any_coin/tests/chains/binance/binance_sign.rs b/rust/tw_tests/tests/chains/binance/binance_sign.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/binance/binance_sign.rs rename to rust/tw_tests/tests/chains/binance/binance_sign.rs diff --git a/rust/tw_any_coin/tests/chains/binance/binance_wallet_connect.rs b/rust/tw_tests/tests/chains/binance/binance_wallet_connect.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/binance/binance_wallet_connect.rs rename to rust/tw_tests/tests/chains/binance/binance_wallet_connect.rs diff --git a/rust/tw_any_coin/tests/chains/binance/data/wc_sign_request_case_1.json b/rust/tw_tests/tests/chains/binance/data/wc_sign_request_case_1.json similarity index 100% rename from rust/tw_any_coin/tests/chains/binance/data/wc_sign_request_case_1.json rename to rust/tw_tests/tests/chains/binance/data/wc_sign_request_case_1.json diff --git a/rust/tw_any_coin/tests/chains/binance/mod.rs b/rust/tw_tests/tests/chains/binance/mod.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/binance/mod.rs rename to rust/tw_tests/tests/chains/binance/mod.rs diff --git a/rust/tw_any_coin/tests/chains/bitcoin/bitcoin_address.rs b/rust/tw_tests/tests/chains/bitcoin/bitcoin_address.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/bitcoin/bitcoin_address.rs rename to rust/tw_tests/tests/chains/bitcoin/bitcoin_address.rs diff --git a/rust/tw_any_coin/tests/chains/bitcoin/bitcoin_compile/brc20.rs b/rust/tw_tests/tests/chains/bitcoin/bitcoin_compile/brc20.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/bitcoin/bitcoin_compile/brc20.rs rename to rust/tw_tests/tests/chains/bitcoin/bitcoin_compile/brc20.rs diff --git a/rust/tw_any_coin/tests/chains/bitcoin/bitcoin_compile/compile_error.rs b/rust/tw_tests/tests/chains/bitcoin/bitcoin_compile/compile_error.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/bitcoin/bitcoin_compile/compile_error.rs rename to rust/tw_tests/tests/chains/bitcoin/bitcoin_compile/compile_error.rs diff --git a/rust/tw_any_coin/tests/chains/bitcoin/bitcoin_compile/mod.rs b/rust/tw_tests/tests/chains/bitcoin/bitcoin_compile/mod.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/bitcoin/bitcoin_compile/mod.rs rename to rust/tw_tests/tests/chains/bitcoin/bitcoin_compile/mod.rs diff --git a/rust/tw_any_coin/tests/chains/bitcoin/bitcoin_compile/p2pkh.rs b/rust/tw_tests/tests/chains/bitcoin/bitcoin_compile/p2pkh.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/bitcoin/bitcoin_compile/p2pkh.rs rename to rust/tw_tests/tests/chains/bitcoin/bitcoin_compile/p2pkh.rs diff --git a/rust/tw_any_coin/tests/chains/bitcoin/bitcoin_plan/mod.rs b/rust/tw_tests/tests/chains/bitcoin/bitcoin_plan/mod.rs similarity index 90% rename from rust/tw_any_coin/tests/chains/bitcoin/bitcoin_plan/mod.rs rename to rust/tw_tests/tests/chains/bitcoin/bitcoin_plan/mod.rs index bc8b8048d52..953c7578968 100644 --- a/rust/tw_any_coin/tests/chains/bitcoin/bitcoin_plan/mod.rs +++ b/rust/tw_tests/tests/chains/bitcoin/bitcoin_plan/mod.rs @@ -6,3 +6,4 @@ mod plan_exact; mod plan_exact_error; mod plan_max; mod plan_max_error; +mod plan_psbt; diff --git a/rust/tw_any_coin/tests/chains/bitcoin/bitcoin_plan/plan_exact.rs b/rust/tw_tests/tests/chains/bitcoin/bitcoin_plan/plan_exact.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/bitcoin/bitcoin_plan/plan_exact.rs rename to rust/tw_tests/tests/chains/bitcoin/bitcoin_plan/plan_exact.rs diff --git a/rust/tw_any_coin/tests/chains/bitcoin/bitcoin_plan/plan_exact_error.rs b/rust/tw_tests/tests/chains/bitcoin/bitcoin_plan/plan_exact_error.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/bitcoin/bitcoin_plan/plan_exact_error.rs rename to rust/tw_tests/tests/chains/bitcoin/bitcoin_plan/plan_exact_error.rs diff --git a/rust/tw_any_coin/tests/chains/bitcoin/bitcoin_plan/plan_max.rs b/rust/tw_tests/tests/chains/bitcoin/bitcoin_plan/plan_max.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/bitcoin/bitcoin_plan/plan_max.rs rename to rust/tw_tests/tests/chains/bitcoin/bitcoin_plan/plan_max.rs diff --git a/rust/tw_any_coin/tests/chains/bitcoin/bitcoin_plan/plan_max_error.rs b/rust/tw_tests/tests/chains/bitcoin/bitcoin_plan/plan_max_error.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/bitcoin/bitcoin_plan/plan_max_error.rs rename to rust/tw_tests/tests/chains/bitcoin/bitcoin_plan/plan_max_error.rs diff --git a/rust/tw_tests/tests/chains/bitcoin/bitcoin_plan/plan_psbt.rs b/rust/tw_tests/tests/chains/bitcoin/bitcoin_plan/plan_psbt.rs new file mode 100644 index 00000000000..70adc5b626a --- /dev/null +++ b/rust/tw_tests/tests/chains/bitcoin/bitcoin_plan/plan_psbt.rs @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::chains::common::bitcoin::input::out_point; +use crate::chains::common::bitcoin::psbt_plan::BitcoinPsbtPlanHelper; +use crate::chains::common::bitcoin::{btc_info, input, output, RecipientType}; +use tw_coin_registry::coin_type::CoinType; +use tw_encoding::hex::DecodeHex; +use tw_keypair::ecdsa; +use tw_proto::BitcoinV2::Proto; + +#[test] +fn test_bitcoin_plan_psbt_thorchain_swap_witness() { + let private_key = ecdsa::secp256k1::PrivateKey::try_from( + "f00ffbe44c5c2838c13d2778854ac66b75e04eb6054f0241989e223223ad5e55", + ) + .unwrap(); + + let psbt = "70736274ff0100bc0200000001147010db5fbcf619067c1090fec65c131443fbc80fb4aaeebe940e44206098c60000000000ffffffff0360ea000000000000160014f22a703617035ef7f490743d50f26ae08c30d0a70000000000000000426a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a35303e12000000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d000000000001011f6603010000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d00000000".decode_hex().unwrap(); + let input = Proto::PsbtSigningInput { + psbt: psbt.into(), + public_keys: vec![private_key.public().compressed().to_vec().into()], + chain_info: btc_info(), + ..Proto::PsbtSigningInput::default() + }; + + let utxo_0 = Proto::Input { + out_point: out_point( + "c6986020440e94beeeaab40fc8fb4314135cc6fe90107c0619f6bc5fdb107014", + 0, + ), + value: 66_406, + sighash_type: 1, + sequence: input::sequence(u32::MAX), + claiming_script: input::receiver_address("bc1qkyu3n8k8jmekl3pwvdl59k5w8enjp25akz2r3z"), + }; + + let out_0 = Proto::Output { + value: 60_000, + to_recipient: output::to_address("bc1q7g48qdshqd000aysws74pun2uzxrp598gcfum0"), + }; + let out_1 = Proto::Output { + value: 0, + to_recipient: RecipientType::custom_script_pubkey( + "6a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a3530" + .decode_hex() + .unwrap() + .into() + ), + }; + let out_2 = Proto::Output { + value: 4_670, + to_recipient: output::to_address("bc1qkyu3n8k8jmekl3pwvdl59k5w8enjp25akz2r3z"), + }; + + let expected = Proto::TransactionPlan { + inputs: vec![utxo_0], + outputs: vec![out_0, out_1, out_2], + available_amount: 66_406, + send_amount: 66_406, + vsize_estimate: 216, + fee_estimate: 1736, + change: 0, + ..Proto::TransactionPlan::default() + }; + + BitcoinPsbtPlanHelper::new(&input) + .coin(CoinType::Bitcoin) + .plan_psbt(expected); +} diff --git a/rust/tw_any_coin/tests/chains/bitcoin/bitcoin_sign/brc20.rs b/rust/tw_tests/tests/chains/bitcoin/bitcoin_sign/brc20.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/bitcoin/bitcoin_sign/brc20.rs rename to rust/tw_tests/tests/chains/bitcoin/bitcoin_sign/brc20.rs diff --git a/rust/tw_any_coin/tests/chains/bitcoin/bitcoin_sign/mod.rs b/rust/tw_tests/tests/chains/bitcoin/bitcoin_sign/mod.rs similarity index 96% rename from rust/tw_any_coin/tests/chains/bitcoin/bitcoin_sign/mod.rs rename to rust/tw_tests/tests/chains/bitcoin/bitcoin_sign/mod.rs index 92e5c0d43f9..2d3540532e6 100644 --- a/rust/tw_any_coin/tests/chains/bitcoin/bitcoin_sign/mod.rs +++ b/rust/tw_tests/tests/chains/bitcoin/bitcoin_sign/mod.rs @@ -11,5 +11,6 @@ mod p2tr_key_path; mod p2tr_script_path; mod p2wpkh; mod p2wsh; +mod psbt; mod send_to_address; mod sighash_single; diff --git a/rust/tw_any_coin/tests/chains/bitcoin/bitcoin_sign/op_return.rs b/rust/tw_tests/tests/chains/bitcoin/bitcoin_sign/op_return.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/bitcoin/bitcoin_sign/op_return.rs rename to rust/tw_tests/tests/chains/bitcoin/bitcoin_sign/op_return.rs diff --git a/rust/tw_any_coin/tests/chains/bitcoin/bitcoin_sign/ordinal_nft.rs b/rust/tw_tests/tests/chains/bitcoin/bitcoin_sign/ordinal_nft.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/bitcoin/bitcoin_sign/ordinal_nft.rs rename to rust/tw_tests/tests/chains/bitcoin/bitcoin_sign/ordinal_nft.rs diff --git a/rust/tw_any_coin/tests/chains/bitcoin/bitcoin_sign/p2pkh.rs b/rust/tw_tests/tests/chains/bitcoin/bitcoin_sign/p2pkh.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/bitcoin/bitcoin_sign/p2pkh.rs rename to rust/tw_tests/tests/chains/bitcoin/bitcoin_sign/p2pkh.rs diff --git a/rust/tw_any_coin/tests/chains/bitcoin/bitcoin_sign/p2sh.rs b/rust/tw_tests/tests/chains/bitcoin/bitcoin_sign/p2sh.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/bitcoin/bitcoin_sign/p2sh.rs rename to rust/tw_tests/tests/chains/bitcoin/bitcoin_sign/p2sh.rs diff --git a/rust/tw_any_coin/tests/chains/bitcoin/bitcoin_sign/p2tr_key_path.rs b/rust/tw_tests/tests/chains/bitcoin/bitcoin_sign/p2tr_key_path.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/bitcoin/bitcoin_sign/p2tr_key_path.rs rename to rust/tw_tests/tests/chains/bitcoin/bitcoin_sign/p2tr_key_path.rs diff --git a/rust/tw_any_coin/tests/chains/bitcoin/bitcoin_sign/p2tr_script_path.rs b/rust/tw_tests/tests/chains/bitcoin/bitcoin_sign/p2tr_script_path.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/bitcoin/bitcoin_sign/p2tr_script_path.rs rename to rust/tw_tests/tests/chains/bitcoin/bitcoin_sign/p2tr_script_path.rs diff --git a/rust/tw_any_coin/tests/chains/bitcoin/bitcoin_sign/p2wpkh.rs b/rust/tw_tests/tests/chains/bitcoin/bitcoin_sign/p2wpkh.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/bitcoin/bitcoin_sign/p2wpkh.rs rename to rust/tw_tests/tests/chains/bitcoin/bitcoin_sign/p2wpkh.rs diff --git a/rust/tw_any_coin/tests/chains/bitcoin/bitcoin_sign/p2wsh.rs b/rust/tw_tests/tests/chains/bitcoin/bitcoin_sign/p2wsh.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/bitcoin/bitcoin_sign/p2wsh.rs rename to rust/tw_tests/tests/chains/bitcoin/bitcoin_sign/p2wsh.rs diff --git a/rust/tw_tests/tests/chains/bitcoin/bitcoin_sign/psbt.rs b/rust/tw_tests/tests/chains/bitcoin/bitcoin_sign/psbt.rs new file mode 100644 index 00000000000..ae660fd50c7 --- /dev/null +++ b/rust/tw_tests/tests/chains/bitcoin/bitcoin_sign/psbt.rs @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::chains::common::bitcoin::psbt_sign::{BitcoinPsbtSignHelper, Expected}; +use tw_coin_registry::coin_type::CoinType; +use tw_encoding::hex::DecodeHex; +use tw_proto::BitcoinV2::Proto; + +#[test] +fn test_bitcoin_sign_psbt_thorchain_swap_witness() { + let private_key = "f00ffbe44c5c2838c13d2778854ac66b75e04eb6054f0241989e223223ad5e55" + .decode_hex() + .unwrap(); + + let psbt = "70736274ff0100bc0200000001147010db5fbcf619067c1090fec65c131443fbc80fb4aaeebe940e44206098c60000000000ffffffff0360ea000000000000160014f22a703617035ef7f490743d50f26ae08c30d0a70000000000000000426a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a35303e12000000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d000000000001011f6603010000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d00000000".decode_hex().unwrap(); + let input = Proto::PsbtSigningInput { + psbt: psbt.into(), + private_keys: vec![private_key.into()], + ..Proto::PsbtSigningInput::default() + }; + + // Successfully broadcasted: https://mempool.space/tx/634a416e82ac710166725f6a4090ac7b5db69687e86b2d2e38dcb3d91c956c32 + BitcoinPsbtSignHelper::new(&input) + .coin(CoinType::Bitcoin) + .sign_psbt(Expected { + psbt: "70736274ff0100bc0200000001147010db5fbcf619067c1090fec65c131443fbc80fb4aaeebe940e44206098c60000000000ffffffff0360ea000000000000160014f22a703617035ef7f490743d50f26ae08c30d0a70000000000000000426a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a35303e12000000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d000000000001011f6603010000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d01086c02483045022100b1229a008f20691639767bf925d6b8956ea957ccc633ad6b5de3618733a55e6b02205774d3320489b8a57a6f8de07f561de3e660ff8e587f6ac5422c49020cd4dc9101210306d8c664ea8fd2683eebea1d3114d90e0a5429e5783ba49b80ddabce04ff28f300000000", + encoded: "02000000000101147010db5fbcf619067c1090fec65c131443fbc80fb4aaeebe940e44206098c60000000000ffffffff0360ea000000000000160014f22a703617035ef7f490743d50f26ae08c30d0a70000000000000000426a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a35303e12000000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d02483045022100b1229a008f20691639767bf925d6b8956ea957ccc633ad6b5de3618733a55e6b02205774d3320489b8a57a6f8de07f561de3e660ff8e587f6ac5422c49020cd4dc9101210306d8c664ea8fd2683eebea1d3114d90e0a5429e5783ba49b80ddabce04ff28f300000000", + txid: "634a416e82ac710166725f6a4090ac7b5db69687e86b2d2e38dcb3d91c956c32", + vsize: 216, + weight: 862, + fee: 1736, + }); +} + +#[test] +fn test_bitcoin_sign_psbt_thorchain_swap_non_witness() { + // 1CKZYtNxAQnTbygz6vyhBYnwx4NvcxURMB + let private_key = "7a87cb2c9fa56f7a63dfc50659dca260473cb6bb0fd4d8a2beeaf5357d41de95" + .decode_hex() + .unwrap(); + + let psbt = "70736274ff01008202000000015c37bcf049b7e62dd5bfd707e0998ce86163b786e3cd45db2336cb794a8d8aa10000000000ffffffff03f82a000000000000160014bf5a13a26791a5db6406304a46952e264c2b28910000000000000000056a032b3a6291950000000000001976a9147c2c0ac72afbde13ecf52fca54368e7883b538b188ac000000000001007e0200000002714916920be4dbc87cbb8697ca9b1420d6b1e47e7d732e2d2e0e7a935087788d0000000000ffffffff326c951cd9b3dc382e2d6be88796b65d7bac90406a5f72660171ac826e414a630200000000ffffffff01efca0000000000001976a9147c2c0ac72afbde13ecf52fca54368e7883b538b188ac0000000000000000".decode_hex().unwrap(); + let input = Proto::PsbtSigningInput { + psbt: psbt.into(), + private_keys: vec![private_key.into()], + ..Proto::PsbtSigningInput::default() + }; + + // Successfully broadcasted: https://mempool.space/tx/710e9270b57720f567ada156c6ac72177aa00a36789e2c6526fd80040fae3ce4 + BitcoinPsbtSignHelper::new(&input) + .coin(CoinType::Bitcoin) + .sign_psbt(Expected { + psbt: "70736274ff01008202000000015c37bcf049b7e62dd5bfd707e0998ce86163b786e3cd45db2336cb794a8d8aa10000000000ffffffff03f82a000000000000160014bf5a13a26791a5db6406304a46952e264c2b28910000000000000000056a032b3a6291950000000000001976a9147c2c0ac72afbde13ecf52fca54368e7883b538b188ac000000000001007e0200000002714916920be4dbc87cbb8697ca9b1420d6b1e47e7d732e2d2e0e7a935087788d0000000000ffffffff326c951cd9b3dc382e2d6be88796b65d7bac90406a5f72660171ac826e414a630200000000ffffffff01efca0000000000001976a9147c2c0ac72afbde13ecf52fca54368e7883b538b188ac0000000001076a473044022057ce7a6147fd9e139df797adcec440bad60770f40cbd609363e3075b64d3eccd02200ae7dce5f7d1fa18c5e907a16c1b078fa90f537d36101447e53fbd058d2d950a0121036c3b7dfd678da989d91593e49918a6c9d8a1d37c7e9c0abeae2118c312e69b3100000000", + encoded: "02000000015c37bcf049b7e62dd5bfd707e0998ce86163b786e3cd45db2336cb794a8d8aa1000000006a473044022057ce7a6147fd9e139df797adcec440bad60770f40cbd609363e3075b64d3eccd02200ae7dce5f7d1fa18c5e907a16c1b078fa90f537d36101447e53fbd058d2d950a0121036c3b7dfd678da989d91593e49918a6c9d8a1d37c7e9c0abeae2118c312e69b31ffffffff03f82a000000000000160014bf5a13a26791a5db6406304a46952e264c2b28910000000000000000056a032b3a6291950000000000001976a9147c2c0ac72afbde13ecf52fca54368e7883b538b188ac00000000", + txid: "710e9270b57720f567ada156c6ac72177aa00a36789e2c6526fd80040fae3ce4", + vsize: 236, + weight: 944, + fee: 2662, + }); +} diff --git a/rust/tw_any_coin/tests/chains/bitcoin/bitcoin_sign/send_to_address.rs b/rust/tw_tests/tests/chains/bitcoin/bitcoin_sign/send_to_address.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/bitcoin/bitcoin_sign/send_to_address.rs rename to rust/tw_tests/tests/chains/bitcoin/bitcoin_sign/send_to_address.rs diff --git a/rust/tw_any_coin/tests/chains/bitcoin/bitcoin_sign/sighash_single.rs b/rust/tw_tests/tests/chains/bitcoin/bitcoin_sign/sighash_single.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/bitcoin/bitcoin_sign/sighash_single.rs rename to rust/tw_tests/tests/chains/bitcoin/bitcoin_sign/sighash_single.rs diff --git a/rust/tw_any_coin/tests/chains/bitcoin/bitcoin_transaction_util.rs b/rust/tw_tests/tests/chains/bitcoin/bitcoin_transaction_util.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/bitcoin/bitcoin_transaction_util.rs rename to rust/tw_tests/tests/chains/bitcoin/bitcoin_transaction_util.rs diff --git a/rust/tw_any_coin/tests/chains/bitcoin/mod.rs b/rust/tw_tests/tests/chains/bitcoin/mod.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/bitcoin/mod.rs rename to rust/tw_tests/tests/chains/bitcoin/mod.rs diff --git a/rust/tw_any_coin/tests/chains/common/bitcoin/compile.rs b/rust/tw_tests/tests/chains/common/bitcoin/compile.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/common/bitcoin/compile.rs rename to rust/tw_tests/tests/chains/common/bitcoin/compile.rs diff --git a/rust/tw_any_coin/tests/chains/common/bitcoin/data.rs b/rust/tw_tests/tests/chains/common/bitcoin/data.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/common/bitcoin/data.rs rename to rust/tw_tests/tests/chains/common/bitcoin/data.rs diff --git a/rust/tw_any_coin/tests/chains/common/bitcoin/mod.rs b/rust/tw_tests/tests/chains/common/bitcoin/mod.rs similarity index 96% rename from rust/tw_any_coin/tests/chains/common/bitcoin/mod.rs rename to rust/tw_tests/tests/chains/common/bitcoin/mod.rs index 510f885184b..1c9c1ecd2f0 100644 --- a/rust/tw_any_coin/tests/chains/common/bitcoin/mod.rs +++ b/rust/tw_tests/tests/chains/common/bitcoin/mod.rs @@ -10,6 +10,8 @@ pub mod compile; pub mod data; pub mod plan; pub mod preimage; +pub mod psbt_plan; +pub mod psbt_sign; pub mod sign; pub const ONE_BTC: i64 = 100_000_000; @@ -38,10 +40,11 @@ pub use tw_proto::BitcoinV2::Proto::mod_PublicKeyOrHash::OneOfvariant as PublicK use tw_proto::BitcoinV2::Proto; -pub fn btc_info() -> Option { +pub fn btc_info() -> Option> { Some(Proto::ChainInfo { p2pkh_prefix: BITCOIN_P2PKH_PREFIX as u32, p2sh_prefix: BITCOIN_P2SH_PREFIX as u32, + hrp: "bc".into(), }) } @@ -65,6 +68,10 @@ pub mod input { }) } + pub fn sequence(sequence: u32) -> Option { + Some(Proto::mod_Input::Sequence { sequence }) + } + pub fn claiming_script_builder(ty: InputBuilderType<'static>) -> ClaimingScriptType<'static> { ClaimingScriptType::script_builder(InputBuilder { variant: ty }) } diff --git a/rust/tw_any_coin/tests/chains/common/bitcoin/plan.rs b/rust/tw_tests/tests/chains/common/bitcoin/plan.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/common/bitcoin/plan.rs rename to rust/tw_tests/tests/chains/common/bitcoin/plan.rs diff --git a/rust/tw_any_coin/tests/chains/common/bitcoin/preimage.rs b/rust/tw_tests/tests/chains/common/bitcoin/preimage.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/common/bitcoin/preimage.rs rename to rust/tw_tests/tests/chains/common/bitcoin/preimage.rs diff --git a/rust/tw_tests/tests/chains/common/bitcoin/psbt_plan.rs b/rust/tw_tests/tests/chains/common/bitcoin/psbt_plan.rs new file mode 100644 index 00000000000..d754b7b6c9a --- /dev/null +++ b/rust/tw_tests/tests/chains/common/bitcoin/psbt_plan.rs @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_registry::coin_type::CoinType; +use tw_memory::test_utils::tw_data_helper::TWDataHelper; +use tw_proto::BitcoinV2::Proto; +use tw_proto::{deserialize, serialize}; +use wallet_core_rs::ffi::bitcoin::psbt::tw_bitcoin_psbt_plan; + +pub struct BitcoinPsbtPlanHelper<'a> { + input: &'a Proto::PsbtSigningInput<'a>, + coin_type: Option, +} + +impl<'a> BitcoinPsbtPlanHelper<'a> { + pub fn new(input: &'a Proto::PsbtSigningInput<'a>) -> Self { + BitcoinPsbtPlanHelper { + input, + coin_type: None, + } + } + + pub fn coin(mut self, coin_type: CoinType) -> Self { + self.coin_type = Some(coin_type); + self + } + + #[track_caller] + pub fn plan_psbt(self, expected: Proto::TransactionPlan) { + let coin_type = self + .coin_type + .expect("'BitcoinSignHelper::coin_type' is not set"); + + let input = serialize(self.input).unwrap(); + let input = TWDataHelper::create(input); + + let output = + TWDataHelper::wrap(unsafe { tw_bitcoin_psbt_plan(input.ptr(), coin_type as u32) }); + let output_bytes = output.to_vec().unwrap(); + + let output: Proto::TransactionPlan = deserialize(&output_bytes).unwrap(); + + assert_eq!(output.error, expected.error, "{}", output.error_message); + + assert_eq!(output.inputs, expected.inputs, "Wrong transaction UTXOs"); + assert_eq!( + output.outputs, expected.outputs, + "Wrong transaction outputs" + ); + + assert_eq!( + output.available_amount, expected.available_amount, + "Wrong available amount" + ); + assert_eq!( + output.send_amount, expected.send_amount, + "Wrong send amount" + ); + assert_eq!( + output.vsize_estimate, expected.vsize_estimate, + "Wrong vsize" + ); + assert_eq!(output.fee_estimate, expected.fee_estimate, "Wrong fee"); + assert_eq!(output.change, expected.change, "Wrong change"); + } +} diff --git a/rust/tw_tests/tests/chains/common/bitcoin/psbt_sign.rs b/rust/tw_tests/tests/chains/common/bitcoin/psbt_sign.rs new file mode 100644 index 00000000000..5cd7e72ff99 --- /dev/null +++ b/rust/tw_tests/tests/chains/common/bitcoin/psbt_sign.rs @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_registry::coin_type::CoinType; +use tw_encoding::hex::ToHex; +use tw_memory::test_utils::tw_data_helper::TWDataHelper; +use tw_proto::BitcoinV2::Proto; +use tw_proto::Common::Proto::SigningError; +use tw_proto::{deserialize, serialize}; +use wallet_core_rs::ffi::bitcoin::psbt::tw_bitcoin_psbt_sign; + +pub struct Expected { + /// Hex encoded PSBT. + pub psbt: &'static str, + pub encoded: &'static str, + pub txid: &'static str, + pub vsize: u64, + pub weight: u64, + pub fee: i64, +} + +pub struct BitcoinPsbtSignHelper<'a> { + input: &'a Proto::PsbtSigningInput<'a>, + coin_type: Option, +} + +impl<'a> BitcoinPsbtSignHelper<'a> { + pub fn new(input: &'a Proto::PsbtSigningInput<'a>) -> Self { + BitcoinPsbtSignHelper { + input, + coin_type: None, + } + } + + pub fn coin(mut self, coin_type: CoinType) -> Self { + self.coin_type = Some(coin_type); + self + } + + #[track_caller] + pub fn sign_psbt(self, expected: Expected) { + let coin_type = self + .coin_type + .expect("'BitcoinSignHelper::coin_type' is not set"); + + let input = serialize(self.input).unwrap(); + let input = TWDataHelper::create(input); + + let output = + TWDataHelper::wrap(unsafe { tw_bitcoin_psbt_sign(input.ptr(), coin_type as u32) }); + let output_bytes = output.to_vec().unwrap(); + + let output: Proto::PsbtSigningOutput = deserialize(&output_bytes).unwrap(); + + assert_eq!(output.error, SigningError::OK, "{}", output.error_message); + assert_eq!(output.psbt.to_hex(), expected.psbt, "Wrong PSBT hex"); + assert_eq!( + output.encoded.to_hex(), + expected.encoded, + "Wrong encoded signed transaction" + ); + assert_eq!(output.txid.to_hex(), expected.txid, "Wrong txid"); + assert_eq!(output.vsize, expected.vsize, "Wrong vsize"); + assert_eq!(output.weight, expected.weight, "Wrong weight"); + assert_eq!(output.fee, expected.fee, "Wrong fee"); + } +} diff --git a/rust/tw_any_coin/tests/chains/common/bitcoin/sign.rs b/rust/tw_tests/tests/chains/common/bitcoin/sign.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/common/bitcoin/sign.rs rename to rust/tw_tests/tests/chains/common/bitcoin/sign.rs diff --git a/rust/tw_any_coin/tests/chains/common/mod.rs b/rust/tw_tests/tests/chains/common/mod.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/common/mod.rs rename to rust/tw_tests/tests/chains/common/mod.rs diff --git a/rust/tw_any_coin/tests/chains/cosmos/cosmos_address.rs b/rust/tw_tests/tests/chains/cosmos/cosmos_address.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/cosmos/cosmos_address.rs rename to rust/tw_tests/tests/chains/cosmos/cosmos_address.rs diff --git a/rust/tw_any_coin/tests/chains/cosmos/cosmos_sign.rs b/rust/tw_tests/tests/chains/cosmos/cosmos_sign.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/cosmos/cosmos_sign.rs rename to rust/tw_tests/tests/chains/cosmos/cosmos_sign.rs diff --git a/rust/tw_any_coin/tests/chains/cosmos/cosmos_transaction_util.rs b/rust/tw_tests/tests/chains/cosmos/cosmos_transaction_util.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/cosmos/cosmos_transaction_util.rs rename to rust/tw_tests/tests/chains/cosmos/cosmos_transaction_util.rs diff --git a/rust/tw_any_coin/tests/chains/cosmos/mod.rs b/rust/tw_tests/tests/chains/cosmos/mod.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/cosmos/mod.rs rename to rust/tw_tests/tests/chains/cosmos/mod.rs diff --git a/rust/tw_any_coin/tests/chains/dydx/dydx_address.rs b/rust/tw_tests/tests/chains/dydx/dydx_address.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/dydx/dydx_address.rs rename to rust/tw_tests/tests/chains/dydx/dydx_address.rs diff --git a/rust/tw_any_coin/tests/chains/dydx/mod.rs b/rust/tw_tests/tests/chains/dydx/mod.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/dydx/mod.rs rename to rust/tw_tests/tests/chains/dydx/mod.rs diff --git a/rust/wallet_core_rs/tests/data/custom.json b/rust/tw_tests/tests/chains/ethereum/data/custom.json similarity index 100% rename from rust/wallet_core_rs/tests/data/custom.json rename to rust/tw_tests/tests/chains/ethereum/data/custom.json diff --git a/rust/wallet_core_rs/tests/data/custom_decoded.json b/rust/tw_tests/tests/chains/ethereum/data/custom_decoded.json similarity index 100% rename from rust/wallet_core_rs/tests/data/custom_decoded.json rename to rust/tw_tests/tests/chains/ethereum/data/custom_decoded.json diff --git a/rust/wallet_core_rs/tests/ethereum/ethereum_abi.rs b/rust/tw_tests/tests/chains/ethereum/ethereum_abi.rs similarity index 97% rename from rust/wallet_core_rs/tests/ethereum/ethereum_abi.rs rename to rust/tw_tests/tests/chains/ethereum/ethereum_abi.rs index 1a9707eb54a..33802575420 100644 --- a/rust/wallet_core_rs/tests/ethereum/ethereum_abi.rs +++ b/rust/tw_tests/tests/chains/ethereum/ethereum_abi.rs @@ -43,8 +43,8 @@ fn number_n(value: u64) -> Proto::NumberNParam<'static> { #[test] fn test_ethereum_abi_decode_contract_call() { - const CUSTOM_ABI_JSON: &str = include_str!("../data/custom.json"); - const CUSTOM_DECODED_JSON: &str = include_str!("../data/custom_decoded.json"); + const CUSTOM_ABI_JSON: &str = include_str!("data/custom.json"); + const CUSTOM_DECODED_JSON: &str = include_str!("data/custom_decoded.json"); let encoded = "ec37a4a000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000067472757374790000000000000000000000000000000000000000000000000000".decode_hex().unwrap(); diff --git a/rust/tw_any_coin/tests/chains/ethereum/ethereum_address.rs b/rust/tw_tests/tests/chains/ethereum/ethereum_address.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/ethereum/ethereum_address.rs rename to rust/tw_tests/tests/chains/ethereum/ethereum_address.rs diff --git a/rust/tw_any_coin/tests/chains/ethereum/ethereum_compile.rs b/rust/tw_tests/tests/chains/ethereum/ethereum_compile.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/ethereum/ethereum_compile.rs rename to rust/tw_tests/tests/chains/ethereum/ethereum_compile.rs diff --git a/rust/tw_any_coin/tests/chains/ethereum/ethereum_message_sign.rs b/rust/tw_tests/tests/chains/ethereum/ethereum_message_sign.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/ethereum/ethereum_message_sign.rs rename to rust/tw_tests/tests/chains/ethereum/ethereum_message_sign.rs diff --git a/rust/wallet_core_rs/tests/ethereum/ethereum_rlp.rs b/rust/tw_tests/tests/chains/ethereum/ethereum_rlp.rs similarity index 100% rename from rust/wallet_core_rs/tests/ethereum/ethereum_rlp.rs rename to rust/tw_tests/tests/chains/ethereum/ethereum_rlp.rs diff --git a/rust/tw_any_coin/tests/chains/ethereum/ethereum_sign.rs b/rust/tw_tests/tests/chains/ethereum/ethereum_sign.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/ethereum/ethereum_sign.rs rename to rust/tw_tests/tests/chains/ethereum/ethereum_sign.rs diff --git a/rust/tw_any_coin/tests/chains/ethereum/ethereum_transaction_util.rs b/rust/tw_tests/tests/chains/ethereum/ethereum_transaction_util.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/ethereum/ethereum_transaction_util.rs rename to rust/tw_tests/tests/chains/ethereum/ethereum_transaction_util.rs diff --git a/rust/tw_any_coin/tests/chains/ethereum/mod.rs b/rust/tw_tests/tests/chains/ethereum/mod.rs similarity index 84% rename from rust/tw_any_coin/tests/chains/ethereum/mod.rs rename to rust/tw_tests/tests/chains/ethereum/mod.rs index 2587748ad06..b962375b4d9 100644 --- a/rust/tw_any_coin/tests/chains/ethereum/mod.rs +++ b/rust/tw_tests/tests/chains/ethereum/mod.rs @@ -2,8 +2,10 @@ // // Copyright © 2017 Trust Wallet. +mod ethereum_abi; mod ethereum_address; mod ethereum_compile; mod ethereum_message_sign; +mod ethereum_rlp; mod ethereum_sign; mod ethereum_transaction_util; diff --git a/rust/tw_any_coin/tests/chains/greenfield/greenfield_address.rs b/rust/tw_tests/tests/chains/greenfield/greenfield_address.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/greenfield/greenfield_address.rs rename to rust/tw_tests/tests/chains/greenfield/greenfield_address.rs diff --git a/rust/tw_any_coin/tests/chains/greenfield/greenfield_compile.rs b/rust/tw_tests/tests/chains/greenfield/greenfield_compile.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/greenfield/greenfield_compile.rs rename to rust/tw_tests/tests/chains/greenfield/greenfield_compile.rs diff --git a/rust/tw_any_coin/tests/chains/greenfield/greenfield_sign.rs b/rust/tw_tests/tests/chains/greenfield/greenfield_sign.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/greenfield/greenfield_sign.rs rename to rust/tw_tests/tests/chains/greenfield/greenfield_sign.rs diff --git a/rust/tw_any_coin/tests/chains/greenfield/mod.rs b/rust/tw_tests/tests/chains/greenfield/mod.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/greenfield/mod.rs rename to rust/tw_tests/tests/chains/greenfield/mod.rs diff --git a/rust/tw_any_coin/tests/chains/internet_computer/internet_computer_address.rs b/rust/tw_tests/tests/chains/internet_computer/internet_computer_address.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/internet_computer/internet_computer_address.rs rename to rust/tw_tests/tests/chains/internet_computer/internet_computer_address.rs diff --git a/rust/tw_any_coin/tests/chains/internet_computer/mod.rs b/rust/tw_tests/tests/chains/internet_computer/mod.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/internet_computer/mod.rs rename to rust/tw_tests/tests/chains/internet_computer/mod.rs diff --git a/rust/tw_any_coin/tests/chains/mod.rs b/rust/tw_tests/tests/chains/mod.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/mod.rs rename to rust/tw_tests/tests/chains/mod.rs diff --git a/rust/tw_any_coin/tests/chains/native_evmos/mod.rs b/rust/tw_tests/tests/chains/native_evmos/mod.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/native_evmos/mod.rs rename to rust/tw_tests/tests/chains/native_evmos/mod.rs diff --git a/rust/tw_any_coin/tests/chains/native_evmos/native_evmos_address.rs b/rust/tw_tests/tests/chains/native_evmos/native_evmos_address.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/native_evmos/native_evmos_address.rs rename to rust/tw_tests/tests/chains/native_evmos/native_evmos_address.rs diff --git a/rust/tw_any_coin/tests/chains/native_evmos/native_evmos_sign.rs b/rust/tw_tests/tests/chains/native_evmos/native_evmos_sign.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/native_evmos/native_evmos_sign.rs rename to rust/tw_tests/tests/chains/native_evmos/native_evmos_sign.rs diff --git a/rust/tw_any_coin/tests/chains/native_injective/mod.rs b/rust/tw_tests/tests/chains/native_injective/mod.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/native_injective/mod.rs rename to rust/tw_tests/tests/chains/native_injective/mod.rs diff --git a/rust/tw_any_coin/tests/chains/native_injective/native_injective_address.rs b/rust/tw_tests/tests/chains/native_injective/native_injective_address.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/native_injective/native_injective_address.rs rename to rust/tw_tests/tests/chains/native_injective/native_injective_address.rs diff --git a/rust/tw_any_coin/tests/chains/native_injective/native_injective_compile.rs b/rust/tw_tests/tests/chains/native_injective/native_injective_compile.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/native_injective/native_injective_compile.rs rename to rust/tw_tests/tests/chains/native_injective/native_injective_compile.rs diff --git a/rust/tw_any_coin/tests/chains/native_injective/native_injective_sign.rs b/rust/tw_tests/tests/chains/native_injective/native_injective_sign.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/native_injective/native_injective_sign.rs rename to rust/tw_tests/tests/chains/native_injective/native_injective_sign.rs diff --git a/rust/tw_any_coin/tests/chains/solana/mod.rs b/rust/tw_tests/tests/chains/solana/mod.rs similarity index 80% rename from rust/tw_any_coin/tests/chains/solana/mod.rs rename to rust/tw_tests/tests/chains/solana/mod.rs index a680f8aba73..01af3fc52b9 100644 --- a/rust/tw_any_coin/tests/chains/solana/mod.rs +++ b/rust/tw_tests/tests/chains/solana/mod.rs @@ -3,8 +3,10 @@ // Copyright © 2017 Trust Wallet. mod solana_address; +mod solana_address_ffi; mod solana_compile; mod solana_sign; mod solana_transaction; +mod solana_transaction_ffi; mod solana_transaction_util; mod solana_wallet_connect; diff --git a/rust/tw_any_coin/tests/chains/solana/solana_address.rs b/rust/tw_tests/tests/chains/solana/solana_address.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/solana/solana_address.rs rename to rust/tw_tests/tests/chains/solana/solana_address.rs diff --git a/rust/wallet_core_rs/tests/solana/solana_address.rs b/rust/tw_tests/tests/chains/solana/solana_address_ffi.rs similarity index 100% rename from rust/wallet_core_rs/tests/solana/solana_address.rs rename to rust/tw_tests/tests/chains/solana/solana_address_ffi.rs diff --git a/rust/tw_any_coin/tests/chains/solana/solana_compile.rs b/rust/tw_tests/tests/chains/solana/solana_compile.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/solana/solana_compile.rs rename to rust/tw_tests/tests/chains/solana/solana_compile.rs diff --git a/rust/tw_any_coin/tests/chains/solana/solana_sign.rs b/rust/tw_tests/tests/chains/solana/solana_sign.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/solana/solana_sign.rs rename to rust/tw_tests/tests/chains/solana/solana_sign.rs diff --git a/rust/tw_any_coin/tests/chains/solana/solana_transaction.rs b/rust/tw_tests/tests/chains/solana/solana_transaction.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/solana/solana_transaction.rs rename to rust/tw_tests/tests/chains/solana/solana_transaction.rs diff --git a/rust/wallet_core_rs/tests/solana/solana_transaction.rs b/rust/tw_tests/tests/chains/solana/solana_transaction_ffi.rs similarity index 100% rename from rust/wallet_core_rs/tests/solana/solana_transaction.rs rename to rust/tw_tests/tests/chains/solana/solana_transaction_ffi.rs diff --git a/rust/tw_any_coin/tests/chains/solana/solana_transaction_util.rs b/rust/tw_tests/tests/chains/solana/solana_transaction_util.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/solana/solana_transaction_util.rs rename to rust/tw_tests/tests/chains/solana/solana_transaction_util.rs diff --git a/rust/tw_any_coin/tests/chains/solana/solana_wallet_connect.rs b/rust/tw_tests/tests/chains/solana/solana_wallet_connect.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/solana/solana_wallet_connect.rs rename to rust/tw_tests/tests/chains/solana/solana_wallet_connect.rs diff --git a/rust/tw_any_coin/tests/chains/sui/mod.rs b/rust/tw_tests/tests/chains/sui/mod.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/sui/mod.rs rename to rust/tw_tests/tests/chains/sui/mod.rs diff --git a/rust/tw_any_coin/tests/chains/sui/sui_address.rs b/rust/tw_tests/tests/chains/sui/sui_address.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/sui/sui_address.rs rename to rust/tw_tests/tests/chains/sui/sui_address.rs diff --git a/rust/tw_any_coin/tests/chains/sui/sui_compile.rs b/rust/tw_tests/tests/chains/sui/sui_compile.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/sui/sui_compile.rs rename to rust/tw_tests/tests/chains/sui/sui_compile.rs diff --git a/rust/tw_any_coin/tests/chains/sui/sui_sign.rs b/rust/tw_tests/tests/chains/sui/sui_sign.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/sui/sui_sign.rs rename to rust/tw_tests/tests/chains/sui/sui_sign.rs diff --git a/rust/tw_any_coin/tests/chains/sui/sui_transaction_util.rs b/rust/tw_tests/tests/chains/sui/sui_transaction_util.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/sui/sui_transaction_util.rs rename to rust/tw_tests/tests/chains/sui/sui_transaction_util.rs diff --git a/rust/tw_any_coin/tests/chains/sui/test_cases.rs b/rust/tw_tests/tests/chains/sui/test_cases.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/sui/test_cases.rs rename to rust/tw_tests/tests/chains/sui/test_cases.rs diff --git a/rust/tw_any_coin/tests/chains/tbinance/mod.rs b/rust/tw_tests/tests/chains/tbinance/mod.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/tbinance/mod.rs rename to rust/tw_tests/tests/chains/tbinance/mod.rs diff --git a/rust/tw_any_coin/tests/chains/tbinance/tbinance_address.rs b/rust/tw_tests/tests/chains/tbinance/tbinance_address.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/tbinance/tbinance_address.rs rename to rust/tw_tests/tests/chains/tbinance/tbinance_address.rs diff --git a/rust/tw_any_coin/tests/chains/thorchain/mod.rs b/rust/tw_tests/tests/chains/thorchain/mod.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/thorchain/mod.rs rename to rust/tw_tests/tests/chains/thorchain/mod.rs diff --git a/rust/tw_any_coin/tests/chains/thorchain/test_cases.rs b/rust/tw_tests/tests/chains/thorchain/test_cases.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/thorchain/test_cases.rs rename to rust/tw_tests/tests/chains/thorchain/test_cases.rs diff --git a/rust/tw_any_coin/tests/chains/thorchain/thorchain_address.rs b/rust/tw_tests/tests/chains/thorchain/thorchain_address.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/thorchain/thorchain_address.rs rename to rust/tw_tests/tests/chains/thorchain/thorchain_address.rs diff --git a/rust/tw_any_coin/tests/chains/thorchain/thorchain_compile.rs b/rust/tw_tests/tests/chains/thorchain/thorchain_compile.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/thorchain/thorchain_compile.rs rename to rust/tw_tests/tests/chains/thorchain/thorchain_compile.rs diff --git a/rust/tw_any_coin/tests/chains/thorchain/thorchain_sign.rs b/rust/tw_tests/tests/chains/thorchain/thorchain_sign.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/thorchain/thorchain_sign.rs rename to rust/tw_tests/tests/chains/thorchain/thorchain_sign.rs diff --git a/rust/tw_any_coin/tests/chains/ton/cell_example.rs b/rust/tw_tests/tests/chains/ton/cell_example.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/ton/cell_example.rs rename to rust/tw_tests/tests/chains/ton/cell_example.rs diff --git a/rust/tw_any_coin/tests/chains/ton/mod.rs b/rust/tw_tests/tests/chains/ton/mod.rs similarity index 74% rename from rust/tw_any_coin/tests/chains/ton/mod.rs rename to rust/tw_tests/tests/chains/ton/mod.rs index ede77ad1aef..14be3a93db3 100644 --- a/rust/tw_any_coin/tests/chains/ton/mod.rs +++ b/rust/tw_tests/tests/chains/ton/mod.rs @@ -4,7 +4,10 @@ mod cell_example; mod ton_address; +mod ton_address_converter; mod ton_compile; +mod ton_message_signer; mod ton_sign; mod ton_sign_wallet_v5r1; mod ton_transaction_util; +mod ton_wallet; diff --git a/rust/tw_any_coin/tests/chains/ton/ton_address.rs b/rust/tw_tests/tests/chains/ton/ton_address.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/ton/ton_address.rs rename to rust/tw_tests/tests/chains/ton/ton_address.rs diff --git a/rust/wallet_core_rs/tests/ton/ton_address_converter.rs b/rust/tw_tests/tests/chains/ton/ton_address_converter.rs similarity index 100% rename from rust/wallet_core_rs/tests/ton/ton_address_converter.rs rename to rust/tw_tests/tests/chains/ton/ton_address_converter.rs diff --git a/rust/tw_any_coin/tests/chains/ton/ton_compile.rs b/rust/tw_tests/tests/chains/ton/ton_compile.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/ton/ton_compile.rs rename to rust/tw_tests/tests/chains/ton/ton_compile.rs diff --git a/rust/wallet_core_rs/tests/ton/ton_message_signer.rs b/rust/tw_tests/tests/chains/ton/ton_message_signer.rs similarity index 100% rename from rust/wallet_core_rs/tests/ton/ton_message_signer.rs rename to rust/tw_tests/tests/chains/ton/ton_message_signer.rs diff --git a/rust/tw_any_coin/tests/chains/ton/ton_sign.rs b/rust/tw_tests/tests/chains/ton/ton_sign.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/ton/ton_sign.rs rename to rust/tw_tests/tests/chains/ton/ton_sign.rs diff --git a/rust/tw_any_coin/tests/chains/ton/ton_sign_wallet_v5r1.rs b/rust/tw_tests/tests/chains/ton/ton_sign_wallet_v5r1.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/ton/ton_sign_wallet_v5r1.rs rename to rust/tw_tests/tests/chains/ton/ton_sign_wallet_v5r1.rs diff --git a/rust/tw_any_coin/tests/chains/ton/ton_transaction_util.rs b/rust/tw_tests/tests/chains/ton/ton_transaction_util.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/ton/ton_transaction_util.rs rename to rust/tw_tests/tests/chains/ton/ton_transaction_util.rs diff --git a/rust/wallet_core_rs/tests/ton/ton_wallet.rs b/rust/tw_tests/tests/chains/ton/ton_wallet.rs similarity index 100% rename from rust/wallet_core_rs/tests/ton/ton_wallet.rs rename to rust/tw_tests/tests/chains/ton/ton_wallet.rs diff --git a/rust/tw_any_coin/tests/chains/zetachain/mod.rs b/rust/tw_tests/tests/chains/zetachain/mod.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/zetachain/mod.rs rename to rust/tw_tests/tests/chains/zetachain/mod.rs diff --git a/rust/tw_any_coin/tests/chains/zetachain/zetachain_address.rs b/rust/tw_tests/tests/chains/zetachain/zetachain_address.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/zetachain/zetachain_address.rs rename to rust/tw_tests/tests/chains/zetachain/zetachain_address.rs diff --git a/rust/tw_any_coin/tests/chains/zetachain/zetachain_sign.rs b/rust/tw_tests/tests/chains/zetachain/zetachain_sign.rs similarity index 100% rename from rust/tw_any_coin/tests/chains/zetachain/zetachain_sign.rs rename to rust/tw_tests/tests/chains/zetachain/zetachain_sign.rs diff --git a/rust/tw_any_coin/tests/coin_address_derivation_test.rs b/rust/tw_tests/tests/coin_address_derivation_test.rs similarity index 100% rename from rust/tw_any_coin/tests/coin_address_derivation_test.rs rename to rust/tw_tests/tests/coin_address_derivation_test.rs diff --git a/rust/tw_any_coin/tests/tw_any_signer_ffi_tests.rs b/rust/tw_tests/tests/tw_any_signer_ffi_tests.rs similarity index 100% rename from rust/tw_any_coin/tests/tw_any_signer_ffi_tests.rs rename to rust/tw_tests/tests/tw_any_signer_ffi_tests.rs diff --git a/rust/wallet_core_rs/tests/bit_reader.rs b/rust/tw_tests/tests/utils/bit_reader.rs similarity index 100% rename from rust/wallet_core_rs/tests/bit_reader.rs rename to rust/tw_tests/tests/utils/bit_reader.rs diff --git a/rust/tw_tests/tests/utils/mod.rs b/rust/tw_tests/tests/utils/mod.rs new file mode 100644 index 00000000000..cf923eed5af --- /dev/null +++ b/rust/tw_tests/tests/utils/mod.rs @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +mod bit_reader; +mod uuid; diff --git a/rust/wallet_core_rs/tests/uuid.rs b/rust/tw_tests/tests/utils/uuid.rs similarity index 100% rename from rust/wallet_core_rs/tests/uuid.rs rename to rust/tw_tests/tests/utils/uuid.rs diff --git a/rust/tw_tests/tests/utils_tests.rs b/rust/tw_tests/tests/utils_tests.rs new file mode 100644 index 00000000000..815dd5a7c46 --- /dev/null +++ b/rust/tw_tests/tests/utils_tests.rs @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +mod utils; diff --git a/rust/wallet_core_rs/Cargo.toml b/rust/wallet_core_rs/Cargo.toml index f75f4859576..1b8f34f853a 100644 --- a/rust/wallet_core_rs/Cargo.toml +++ b/rust/wallet_core_rs/Cargo.toml @@ -18,7 +18,7 @@ default = [ "utils", ] any-coin = ["tw_any_coin"] -bitcoin = ["tw_bitcoin"] +bitcoin = ["tw_bitcoin", "tw_coin_registry"] ethereum = ["tw_ethereum", "tw_coin_registry"] keypair = ["tw_keypair"] solana = ["tw_solana"] @@ -35,7 +35,7 @@ utils = [ [dependencies] bitreader = "0.3.8" tw_any_coin = { path = "../tw_any_coin", optional = true } -tw_bitcoin = { path = "../tw_bitcoin", optional = true } +tw_bitcoin = { path = "../chains/tw_bitcoin", optional = true } tw_coin_registry = { path = "../tw_coin_registry", optional = true } tw_encoding = { path = "../tw_encoding", optional = true } tw_ethereum = { path = "../chains/tw_ethereum", optional = true } @@ -48,11 +48,3 @@ tw_proto = { path = "../tw_proto", optional = true } tw_solana = { path = "../chains/tw_solana", optional = true } tw_ton = { path = "../chains/tw_ton", optional = true } uuid = { version = "1.7", features = ["v4"], optional = true } - -[dev-dependencies] -serde_json = "1.0" -tw_any_coin = { path = "../tw_any_coin", features = ["test-utils"] } -tw_coin_entry = { path = "../tw_coin_entry", features = ["test-utils"] } -tw_encoding = { path = "../tw_encoding" } -tw_memory = { path = "../tw_memory", features = ["test-utils"] } -tw_number = { path = "../tw_number", features = ["helpers"] } diff --git a/rust/wallet_core_rs/src/ffi/bitcoin/mod.rs b/rust/wallet_core_rs/src/ffi/bitcoin/mod.rs index 7632dd9561b..dd6df1c9eda 100644 --- a/rust/wallet_core_rs/src/ffi/bitcoin/mod.rs +++ b/rust/wallet_core_rs/src/ffi/bitcoin/mod.rs @@ -1,3 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 // // Copyright © 2017 Trust Wallet. + +pub mod psbt; diff --git a/rust/wallet_core_rs/src/ffi/bitcoin/psbt.rs b/rust/wallet_core_rs/src/ffi/bitcoin/psbt.rs new file mode 100644 index 00000000000..ceec4d34ab9 --- /dev/null +++ b/rust/wallet_core_rs/src/ffi/bitcoin/psbt.rs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#![allow(clippy::missing_safety_doc)] + +use tw_coin_registry::coin_type::CoinType; +use tw_coin_registry::dispatcher::{coin_dispatcher, utxo_dispatcher}; +use tw_memory::ffi::tw_data::TWData; +use tw_memory::ffi::RawPtrTrait; +use tw_misc::try_or_else; + +/// Signs a PSBT (Partially Signed Bitcoin Transaction) specified by the signing input and coin type. +/// +/// \param input The serialized data of a signing input (e.g. `TW.BitcoinV2.Proto.PsbtSigningInput`) +/// \param coin The given coin type to sign the PSBT for. +/// \return The serialized data of a `Proto.PsbtSigningOutput` proto object (e.g. `TW.BitcoinV2.Proto.PsbtSigningOutput`). +#[no_mangle] +pub unsafe extern "C" fn tw_bitcoin_psbt_sign(input: *const TWData, coin: u32) -> *mut TWData { + let coin = try_or_else!(CoinType::try_from(coin), std::ptr::null_mut); + let input_data = try_or_else!(TWData::from_ptr_as_ref(input), std::ptr::null_mut); + let utxo_dispatcher = try_or_else!(utxo_dispatcher(coin), std::ptr::null_mut); + let (coin_context, _) = try_or_else!(coin_dispatcher(coin), std::ptr::null_mut); + + utxo_dispatcher + .sign_psbt(&coin_context, input_data.as_slice()) + .map(|data| TWData::from(data).into_ptr()) + .unwrap_or_else(|_| std::ptr::null_mut()) +} + +/// Plans a PSBT (Partially Signed Bitcoin Transaction). +/// Can be used to get the transaction detailed decoded from PSBT. +/// +/// \param input The serialized data of a signing input (e.g. `TW.BitcoinV2.Proto.PsbtSigningInput`) +/// \param coin The given coin type to sign the PSBT for. +/// \return The serialized data of a `Proto.TransactionPlan` proto object (e.g. `TW.BitcoinV2.Proto.TransactionPlan`). +#[no_mangle] +pub unsafe extern "C" fn tw_bitcoin_psbt_plan(input: *const TWData, coin: u32) -> *mut TWData { + let coin = try_or_else!(CoinType::try_from(coin), std::ptr::null_mut); + let input_data = try_or_else!(TWData::from_ptr_as_ref(input), std::ptr::null_mut); + let utxo_dispatcher = try_or_else!(utxo_dispatcher(coin), std::ptr::null_mut); + let (coin_context, _) = try_or_else!(coin_dispatcher(coin), std::ptr::null_mut); + + utxo_dispatcher + .plan_psbt(&coin_context, input_data.as_slice()) + .map(|data| TWData::from(data).into_ptr()) + .unwrap_or_else(|_| std::ptr::null_mut()) +} diff --git a/rust/wallet_core_rs/tests/ethereum.rs b/rust/wallet_core_rs/tests/ethereum.rs deleted file mode 100644 index 6ca70fab1de..00000000000 --- a/rust/wallet_core_rs/tests/ethereum.rs +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#[path = "ethereum/ethereum_abi.rs"] -mod ethereum_abi; -#[path = "ethereum/ethereum_rlp.rs"] -mod ethereum_rlp; diff --git a/rust/wallet_core_rs/tests/solana.rs b/rust/wallet_core_rs/tests/solana.rs deleted file mode 100644 index 878cbde2404..00000000000 --- a/rust/wallet_core_rs/tests/solana.rs +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#[path = "solana/solana_address.rs"] -mod solana_address; -#[path = "solana/solana_transaction.rs"] -mod solana_transaction; diff --git a/rust/wallet_core_rs/tests/ton.rs b/rust/wallet_core_rs/tests/ton.rs deleted file mode 100644 index 12a14637b1a..00000000000 --- a/rust/wallet_core_rs/tests/ton.rs +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#[path = "ton/ton_address_converter.rs"] -mod ton_address_converter; -#[path = "ton/ton_message_signer.rs"] -mod ton_message_signer; -#[path = "ton/ton_wallet.rs"] -mod ton_wallet; diff --git a/src/Bitcoin/Psbt.cpp b/src/Bitcoin/Psbt.cpp new file mode 100644 index 00000000000..99484238ea1 --- /dev/null +++ b/src/Bitcoin/Psbt.cpp @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "Psbt.h" +#include "rust/Wrapper.h" + +namespace TW::Bitcoin { + +Data Psbt::sign(const Data &input, TWCoinType coin) { + const Rust::TWDataWrapper inputRust = input; + const Rust::TWDataWrapper outputRust = Rust::tw_bitcoin_psbt_sign(inputRust.get(), static_cast(coin)); + return outputRust.toDataOrDefault(); +} + +Data Psbt::plan(const Data& input, TWCoinType coin) { + const Rust::TWDataWrapper inputRust = input; + const Rust::TWDataWrapper outputRust = Rust::tw_bitcoin_psbt_plan(inputRust.get(), static_cast(coin)); + return outputRust.toDataOrDefault(); +} + +} // namespace TW::Bitcoin diff --git a/src/Bitcoin/Psbt.h b/src/Bitcoin/Psbt.h new file mode 100644 index 00000000000..7ee128ea129 --- /dev/null +++ b/src/Bitcoin/Psbt.h @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "Data.h" +#include "TrustWalletCore/TWCoinType.h" + +namespace TW::Bitcoin { + +class Psbt { +public: + /// Signs a PSBT (Partially Signed Bitcoin Transaction) specified by the signing input and coin type. + static Data sign(const Data& input, TWCoinType coin); + + /// Plans a PSBT (Partially Signed Bitcoin Transaction). + /// Can be used to get the transaction detailed decoded from PSBT. + static Data plan(const Data& input, TWCoinType coin); +}; + +} // namespace TW::Bitcoin diff --git a/src/interface/TWBitcoinPsbt.cpp b/src/interface/TWBitcoinPsbt.cpp new file mode 100644 index 00000000000..54c52a515f9 --- /dev/null +++ b/src/interface/TWBitcoinPsbt.cpp @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "TrustWalletCore/TWBitcoinPsbt.h" +#include "Bitcoin/Psbt.h" + +using namespace TW; + +TWData* _Nonnull TWBitcoinPsbtSign(TWData* _Nonnull input, enum TWCoinType coin) { + const Data& dataIn = *(reinterpret_cast(input)); + const auto dataOut = Bitcoin::Psbt::sign(dataIn, coin); + return TWDataCreateWithBytes(dataOut.data(), dataOut.size()); +} + +TWData* _Nonnull TWBitcoinPsbtPlan(TWData* _Nonnull input, enum TWCoinType coin) { + const Data& dataIn = *(reinterpret_cast(input)); + const auto dataOut = Bitcoin::Psbt::plan(dataIn, coin); + return TWDataCreateWithBytes(dataOut.data(), dataOut.size()); +} diff --git a/src/proto/BitcoinV2.proto b/src/proto/BitcoinV2.proto index f80879e56e2..5f58e161174 100644 --- a/src/proto/BitcoinV2.proto +++ b/src/proto/BitcoinV2.proto @@ -182,8 +182,10 @@ message Output { message ChainInfo { // P2PKH prefix for this chain. uint32 p2pkh_prefix = 1; - // P2SH prefix for this coin type + // P2SH prefix for this coin type. uint32 p2sh_prefix = 2; + // HRP for this coin type if applicable. + string hrp = 3; } enum TransactionVersion { @@ -273,7 +275,7 @@ message Transaction { message TransactionPlan { // A possible error, `OK` if none. Common.Proto.SigningError error = 1; - /// Error description. + // Error description. string error_message = 2; // Selected unspent transaction outputs (subset of all input UTXOs). repeated Input inputs = 3; @@ -301,7 +303,7 @@ message PreSigningOutput { Common.Proto.SigningError error = 1; // Error description. string error_message = 2; - /// The sighashes to be signed; ECDSA for legacy and Segwit, Schnorr for Taproot. + // The sighashes to be signed; ECDSA for legacy and Segwit, Schnorr for Taproot. repeated Sighash sighashes = 4; enum SigningMethod { @@ -340,7 +342,46 @@ message SigningOutput { string error_message = 2; // Resulting transaction. Transaction transaction = 3; - // The encoded transaction that submitted to the network. + // The encoded transaction that can be submitted to the network. + bytes encoded = 4; + // The transaction ID (hash). + bytes txid = 5; + // The total `vsize` in `vbytes`. + // It is used to compare how much blockweight needs to be allocated to confirm a transaction. + // For non-segwit transactions, `vsize` = `size`. + uint64 vsize = 6; + // Transaction weight is defined as Base transaction size * 3 + Total transaction size + // (ie. the same method as calculating Block weight from Base size and Total size). + uint64 weight = 7; + // The total and final fee of the transaction in satoshis. + int64 fee = 8; +} + +message PsbtSigningInput { + // Partly signed transaction to be signed. + bytes psbt = 1; + // User private keys. + // Only required if the `signPSBT` method is called. + repeated bytes private_keys = 2; + // User public keys. + // Only required if the `planPSBT` method is called. + repeated bytes public_keys = 3; + // Chain info includes p2pkh, p2sh, hrp address prefixes. + // The parameter needs to be set when `planPSBT` is called. + ChainInfo chain_info = 4; + // Whether disable auxiliary random data when signing. + // Use for testing **ONLY**. + bool dangerous_use_fixed_schnorr_rng = 5; +} + +message PsbtSigningOutput { + // A possible error, `OK` if none. + Common.Proto.SigningError error = 1; + // Error description. + string error_message = 2; + // Resulting transaction. + Transaction transaction = 3; + // The encoded transaction that can be submitted to the network. bytes encoded = 4; // The transaction ID (hash). bytes txid = 5; @@ -353,4 +394,6 @@ message SigningOutput { uint64 weight = 7; // The total and final fee of the transaction in satoshis. int64 fee = 8; + // Signed transaction serialized as PSBT. + bytes psbt = 9; } diff --git a/swift/Tests/Blockchains/BitcoinTests.swift b/swift/Tests/Blockchains/BitcoinTests.swift index 46aedc1c70f..c33fc2a27f8 100644 --- a/swift/Tests/Blockchains/BitcoinTests.swift +++ b/swift/Tests/Blockchains/BitcoinTests.swift @@ -387,6 +387,65 @@ class BitcoinTransactionSignerTests: XCTestCase { XCTAssertEqual(output.error, .ok) XCTAssertEqual(output.encoded.hexString, "01000000026c90312e53a3411347a197bfd637c2583d617dd2317262a70e1b5245d2f1e36a000000008a47304402201a631068ea5ddea19467ef7c932a0f3b04f366ca2beaf70e18958e47456124980220614816c449e39cf6acc6625e1cf3100db1db7c0b755bdbb6804d4fa3c4b735d10141041b3937fac1f14074447cde9d3a324ed292d2865ed0d7a7da26cb43558ce4db4ef33c47e820e53031ae16bb0c39205def059a5ca8e1d617650eabc72c5206a81dffffffff13bf27945c669cf3c1d70cf3048f4ab14f1ab6acf06d10d425e8288217a81efd000000008a473044022051d381d8f48a9a4866ca4109f12647922514604a4733e8da8aac046e19275f700220797c3ebf20df7d2a9fed283f9d0ad14cbd656cafb5ec70a2b1c85646ea7485190141041b3937fac1f14074447cde9d3a324ed292d2865ed0d7a7da26cb43558ce4db4ef33c47e820e53031ae16bb0c39205def059a5ca8e1d617650eabc72c5206a81dffffffff0194590000000000001976a914a0c0a50f986924e65ae9bd18eafae448f83117ed88ac00000000") } + + func testSignPsbtThorSwap() throws { + let privateKey = Data(hexString: "f00ffbe44c5c2838c13d2778854ac66b75e04eb6054f0241989e223223ad5e55")! + let psbt = Data(hexString: "70736274ff0100bc0200000001147010db5fbcf619067c1090fec65c131443fbc80fb4aaeebe940e44206098c60000000000ffffffff0360ea000000000000160014f22a703617035ef7f490743d50f26ae08c30d0a70000000000000000426a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a35303e12000000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d000000000001011f6603010000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d00000000")! + + let input = BitcoinV2PsbtSigningInput.with { + $0.psbt = psbt + $0.privateKeys = [privateKey] + } + + let outputData = try BitcoinPsbt.sign(input: input.serializedData(), coin: .bitcoin) + let output = try BitcoinV2PsbtSigningOutput(serializedData: outputData) + + XCTAssertEqual(output.error, .ok) + XCTAssertEqual(output.psbt.hexString, "70736274ff0100bc0200000001147010db5fbcf619067c1090fec65c131443fbc80fb4aaeebe940e44206098c60000000000ffffffff0360ea000000000000160014f22a703617035ef7f490743d50f26ae08c30d0a70000000000000000426a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a35303e12000000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d000000000001011f6603010000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d01086c02483045022100b1229a008f20691639767bf925d6b8956ea957ccc633ad6b5de3618733a55e6b02205774d3320489b8a57a6f8de07f561de3e660ff8e587f6ac5422c49020cd4dc9101210306d8c664ea8fd2683eebea1d3114d90e0a5429e5783ba49b80ddabce04ff28f300000000") + XCTAssertEqual(output.encoded.hexString, "02000000000101147010db5fbcf619067c1090fec65c131443fbc80fb4aaeebe940e44206098c60000000000ffffffff0360ea000000000000160014f22a703617035ef7f490743d50f26ae08c30d0a70000000000000000426a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a35303e12000000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d02483045022100b1229a008f20691639767bf925d6b8956ea957ccc633ad6b5de3618733a55e6b02205774d3320489b8a57a6f8de07f561de3e660ff8e587f6ac5422c49020cd4dc9101210306d8c664ea8fd2683eebea1d3114d90e0a5429e5783ba49b80ddabce04ff28f300000000") + XCTAssertEqual(output.txid.hexString, "634a416e82ac710166725f6a4090ac7b5db69687e86b2d2e38dcb3d91c956c32") + } + + func testPlanPsbtThorSwap() throws { + let privateKeyBytes = Data(hexString: "f00ffbe44c5c2838c13d2778854ac66b75e04eb6054f0241989e223223ad5e55")! + let privateKey = PrivateKey(data: privateKeyBytes)! + let publicKey = privateKey.getPublicKeySecp256k1(compressed: true) + + let psbt = Data(hexString: "70736274ff0100bc0200000001147010db5fbcf619067c1090fec65c131443fbc80fb4aaeebe940e44206098c60000000000ffffffff0360ea000000000000160014f22a703617035ef7f490743d50f26ae08c30d0a70000000000000000426a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a35303e12000000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d000000000001011f6603010000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d00000000")! + + let input = BitcoinV2PsbtSigningInput.with { + $0.psbt = psbt + $0.publicKeys = [publicKey.data] + } + + let outputData = try BitcoinPsbt.plan(input: input.serializedData(), coin: .bitcoin) + let output = try BitcoinV2TransactionPlan(serializedData: outputData) + + XCTAssertEqual(output.error, .ok) + + XCTAssertEqual(output.inputs[0].receiverAddress, "bc1qkyu3n8k8jmekl3pwvdl59k5w8enjp25akz2r3z") + XCTAssertEqual(output.inputs[0].value, 66_406) + + // Vault transfer + XCTAssertEqual(output.outputs[0].toAddress, "bc1q7g48qdshqd000aysws74pun2uzxrp598gcfum0") + XCTAssertEqual(output.outputs[0].value, 60_000) + + // OP_RETURN + XCTAssertEqual( + output.outputs[1].customScriptPubkey.hexString, + "6a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a3530" + ) + XCTAssertEqual(output.outputs[1].value, 0) + + // Change output + XCTAssertEqual(output.outputs[2].toAddress, "bc1qkyu3n8k8jmekl3pwvdl59k5w8enjp25akz2r3z") + XCTAssertEqual(output.outputs[2].value, 4_670) + + XCTAssertEqual(output.feeEstimate, 1736) + // Please note that `change` in PSBT planning is always 0. + // That's because we aren't able to determine which output is an actual change from PSBT. + XCTAssertEqual(output.change, 0) + } func testBitcoinMessageSigner() { let verifyResult = BitcoinMessageSigner.verifyMessage( diff --git a/tests/chains/Bitcoin/TWBitcoinPsbtTests.cpp b/tests/chains/Bitcoin/TWBitcoinPsbtTests.cpp new file mode 100644 index 00000000000..616c9913f2a --- /dev/null +++ b/tests/chains/Bitcoin/TWBitcoinPsbtTests.cpp @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "HexCoding.h" +#include "proto/BitcoinV2.pb.h" +#include "PrivateKey.h" +#include "TestUtilities.h" + +#include "TrustWalletCore/TWBitcoinPsbt.h" + +#include + +namespace TW::Bitcoin::PsbtTests { + +const auto gPrivateKey = PrivateKey(parse_hex("f00ffbe44c5c2838c13d2778854ac66b75e04eb6054f0241989e223223ad5e55")); +const auto gPsbt = parse_hex("70736274ff0100bc0200000001147010db5fbcf619067c1090fec65c131443fbc80fb4aaeebe940e44206098c60000000000ffffffff0360ea000000000000160014f22a703617035ef7f490743d50f26ae08c30d0a70000000000000000426a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a35303e12000000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d000000000001011f6603010000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d00000000"); + +TEST(TWBitcoinPsbt, SignThorSwap) { + BitcoinV2::Proto::PsbtSigningInput input; + input.set_psbt(gPsbt.data(), gPsbt.size()); + input.add_private_keys(gPrivateKey.bytes.data(), gPrivateKey.bytes.size()); + + const auto inputData = data(input.SerializeAsString()); + const auto inputPtr = WRAPD(TWDataCreateWithBytes(inputData.data(), inputData.size())); + + const auto outputPtr = WRAPD(TWBitcoinPsbtSign(inputPtr.get(), TWCoinTypeBitcoin)); + + BitcoinV2::Proto::PsbtSigningOutput output; + output.ParseFromArray( + TWDataBytes(outputPtr.get()), + static_cast(TWDataSize(outputPtr.get())) + ); + + EXPECT_EQ(output.error(), Common::Proto::SigningError::OK); + EXPECT_EQ(hex(output.psbt()), "70736274ff0100bc0200000001147010db5fbcf619067c1090fec65c131443fbc80fb4aaeebe940e44206098c60000000000ffffffff0360ea000000000000160014f22a703617035ef7f490743d50f26ae08c30d0a70000000000000000426a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a35303e12000000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d000000000001011f6603010000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d01086c02483045022100b1229a008f20691639767bf925d6b8956ea957ccc633ad6b5de3618733a55e6b02205774d3320489b8a57a6f8de07f561de3e660ff8e587f6ac5422c49020cd4dc9101210306d8c664ea8fd2683eebea1d3114d90e0a5429e5783ba49b80ddabce04ff28f300000000"); + EXPECT_EQ(hex(output.encoded()), "02000000000101147010db5fbcf619067c1090fec65c131443fbc80fb4aaeebe940e44206098c60000000000ffffffff0360ea000000000000160014f22a703617035ef7f490743d50f26ae08c30d0a70000000000000000426a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a35303e12000000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d02483045022100b1229a008f20691639767bf925d6b8956ea957ccc633ad6b5de3618733a55e6b02205774d3320489b8a57a6f8de07f561de3e660ff8e587f6ac5422c49020cd4dc9101210306d8c664ea8fd2683eebea1d3114d90e0a5429e5783ba49b80ddabce04ff28f300000000"); +} + +TEST(TWBitcoinPsbt, PlanThorSwap) { + const auto publicKey = gPrivateKey.getPublicKey(TWPublicKeyTypeSECP256k1); + + BitcoinV2::Proto::PsbtSigningInput input; + input.set_psbt(gPsbt.data(), gPsbt.size()); + input.add_public_keys(publicKey.bytes.data(), publicKey.bytes.size()); + + const auto inputData = data(input.SerializeAsString()); + const auto inputPtr = WRAPD(TWDataCreateWithBytes(inputData.data(), inputData.size())); + + const auto planPtr = WRAPD(TWBitcoinPsbtPlan(inputPtr.get(), TWCoinTypeBitcoin)); + + BitcoinV2::Proto::TransactionPlan plan; + plan.ParseFromArray( + TWDataBytes(planPtr.get()), + static_cast(TWDataSize(planPtr.get())) + ); + + EXPECT_EQ(plan.error(), Common::Proto::SigningError::OK); + EXPECT_EQ(plan.send_amount(), 66'406); + EXPECT_EQ(plan.fee_estimate(), 1'736); +} + +}