Skip to content

Commit

Permalink
[BTC]: Add support for signPSBT (#4032)
Browse files Browse the repository at this point in the history
* feat(btc): Move `tw_bitcoin` to `rust/chains/`

* feat(btc): Add PSBT signing of `witness_utxo`

* TODO add support for signing of `non_witness_utxo`
* TODO add support for PSBT planning

* feat(btc): Add PSBT signing of `non_witness_utxo`

* feat(btc): Add `planPSBT`

* Add `ChainInfo.hrp`

* feat(btc): Move all tests from `tw_any_coin` to `tw_tests`

* feat(btc): Minor changes in `tw_tests`

* feat(btc): Move all tests from `wallet_core_rs` to `tw_tests`

* feat(btc): Add `tw_bitcoin_sign_psbt` and `tw_bitcoin_plan_psbt`

* feat(btc): Add `TWBitcoinPsbtSign` and `TWBitcoinPsbtPlan` C interface

* feat(btc): Add Android, iOS tests

* [CI] Trigger CI

* chore(codegen): Fix codegen-v2
  • Loading branch information
satoshiotomakan authored Sep 24, 2024
1 parent d66b0b9 commit bbb9913
Show file tree
Hide file tree
Showing 170 changed files with 1,548 additions and 151 deletions.
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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")
}
Expand Down
6 changes: 3 additions & 3 deletions codegen-v2/src/codegen/rust/coin_integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,15 +20,15 @@ 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 {
chains_integration_tests_directory().join(id.as_str())
}

pub fn coin_address_derivation_test_path() -> PathBuf {
tw_any_coin_directory()
tw_tests_directory()
.join("tests")
.join("coin_address_derivation_test.rs")
}
Expand Down
4 changes: 2 additions & 2 deletions codegen-v2/src/codegen/rust/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion docs/registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ This list is generated from [./registry.json](../registry.json)
| 10004689 | IoTeX EVM | IOTX | <img src="https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/iotexevm/info/logo.png" width="32" /> | <https://iotex.io/> |
| 10007000 | NativeZetaChain | ZETA | <img src="https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/zetachain/info/logo.png" width="32" /> | <https://www.zetachain.com/> |
| 10007700 | NativeCanto | CANTO | <img src="https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/nativecanto/info/logo.png" width="32" /> | <https://canto.io/> |
| 10008217 | Kaia | KLAY | <img src="https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/kaia/info/logo.png" width="32" /> | <https://kaia.io> |
| 10008217 | Kaia | KLAY | <img src="https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/kaia/info/logo.png" width="32" /> | <https://kaia.io> |
| 10009000 | Avalanche C-Chain | AVAX | <img src="https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/avalanchec/info/logo.png" width="32" /> | <https://www.avalabs.org/> |
| 10009001 | Evmos | EVMOS | <img src="https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/evmos/info/logo.png" width="32" /> | <https://evmos.org/> |
| 10042170 | Arbitrum Nova | ETH | <img src="https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/arbitrumnova/info/logo.png" width="32" /> | <https://nova.arbitrum.io> |
Expand Down
36 changes: 36 additions & 0 deletions include/TrustWalletCore/TWBitcoinPsbt.h
Original file line number Diff line number Diff line change
@@ -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
36 changes: 26 additions & 10 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
members = [
"chains/tw_aptos",
"chains/tw_binance",
"chains/tw_bitcoin",
"chains/tw_cosmos",
"chains/tw_ethereum",
"chains/tw_greenfield",
Expand All @@ -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",
Expand All @@ -30,6 +30,7 @@ members = [
"tw_misc",
"tw_number",
"tw_proto",
"tw_tests",
"wallet_core_bin",
"wallet_core_rs",
]
Expand Down
20 changes: 20 additions & 0 deletions rust/chains/tw_bitcoin/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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" }
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::modules::compiler::BitcoinCompiler;
use crate::modules::planner::BitcoinPlanner;
use crate::modules::psbt_planner::PsbtPlanner;
use crate::modules::signer::BitcoinSigner;
use crate::modules::transaction_util::BitcoinTransactionUtil;
use std::str::FromStr;
Expand All @@ -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;

Expand Down Expand Up @@ -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)
}
}
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
File renamed without changes.
Loading

0 comments on commit bbb9913

Please sign in to comment.