From 0af0634b6775a03e6de34064b24f0380161f4029 Mon Sep 17 00:00:00 2001 From: Satoshi Otomakan Date: Fri, 20 Sep 2024 17:44:50 +0200 Subject: [PATCH] feat(btc): Add `TWBitcoinPsbtSign` and `TWBitcoinPsbtPlan` C interface --- include/TrustWalletCore/TWBitcoinPsbt.h | 36 +++++++++++ .../tests/chains/common/bitcoin/psbt_plan.rs | 4 +- .../tests/chains/common/bitcoin/psbt_sign.rs | 4 +- rust/wallet_core_rs/src/ffi/bitcoin/psbt.rs | 12 ++-- src/Bitcoin/Psbt.cpp | 22 +++++++ src/Bitcoin/Psbt.h | 22 +++++++ src/interface/TWBitcoinPsbt.cpp | 20 ++++++ tests/chains/Bitcoin/TWBitcoinPsbtTests.cpp | 63 +++++++++++++++++++ 8 files changed, 173 insertions(+), 10 deletions(-) create mode 100644 include/TrustWalletCore/TWBitcoinPsbt.h create mode 100644 src/Bitcoin/Psbt.cpp create mode 100644 src/Bitcoin/Psbt.h create mode 100644 src/interface/TWBitcoinPsbt.cpp create mode 100644 tests/chains/Bitcoin/TWBitcoinPsbtTests.cpp 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/tw_tests/tests/chains/common/bitcoin/psbt_plan.rs b/rust/tw_tests/tests/chains/common/bitcoin/psbt_plan.rs index 3a8054c3dbd..d754b7b6c9a 100644 --- a/rust/tw_tests/tests/chains/common/bitcoin/psbt_plan.rs +++ b/rust/tw_tests/tests/chains/common/bitcoin/psbt_plan.rs @@ -6,7 +6,7 @@ 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_plan_psbt; +use wallet_core_rs::ffi::bitcoin::psbt::tw_bitcoin_psbt_plan; pub struct BitcoinPsbtPlanHelper<'a> { input: &'a Proto::PsbtSigningInput<'a>, @@ -36,7 +36,7 @@ impl<'a> BitcoinPsbtPlanHelper<'a> { let input = TWDataHelper::create(input); let output = - TWDataHelper::wrap(unsafe { tw_bitcoin_plan_psbt(coin_type as u32, input.ptr()) }); + 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(); diff --git a/rust/tw_tests/tests/chains/common/bitcoin/psbt_sign.rs b/rust/tw_tests/tests/chains/common/bitcoin/psbt_sign.rs index 24af4b2f78f..5cd7e72ff99 100644 --- a/rust/tw_tests/tests/chains/common/bitcoin/psbt_sign.rs +++ b/rust/tw_tests/tests/chains/common/bitcoin/psbt_sign.rs @@ -8,7 +8,7 @@ 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_sign_psbt; +use wallet_core_rs::ffi::bitcoin::psbt::tw_bitcoin_psbt_sign; pub struct Expected { /// Hex encoded PSBT. @@ -48,7 +48,7 @@ impl<'a> BitcoinPsbtSignHelper<'a> { let input = TWDataHelper::create(input); let output = - TWDataHelper::wrap(unsafe { tw_bitcoin_sign_psbt(coin_type as u32, input.ptr()) }); + 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(); diff --git a/rust/wallet_core_rs/src/ffi/bitcoin/psbt.rs b/rust/wallet_core_rs/src/ffi/bitcoin/psbt.rs index eac05194ecc..ceec4d34ab9 100644 --- a/rust/wallet_core_rs/src/ffi/bitcoin/psbt.rs +++ b/rust/wallet_core_rs/src/ffi/bitcoin/psbt.rs @@ -12,11 +12,11 @@ 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 `Proto.PsbtSigningInput` protobuf message. +/// \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` protobuf message. +/// \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_sign_psbt(coin: u32, input: *const TWData) -> *mut TWData { +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); @@ -31,11 +31,11 @@ pub unsafe extern "C" fn tw_bitcoin_sign_psbt(coin: u32, input: *const TWData) - /// 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 `Proto.PsbtSigningInput` protobuf message. +/// \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` protobuf message. +/// \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_plan_psbt(coin: u32, input: *const TWData) -> *mut TWData { +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); 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/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); +} + +}