Skip to content

Commit bbb9913

Browse files
[BTC]: Add support for signPSBT (#4032)
* 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
1 parent d66b0b9 commit bbb9913

File tree

170 files changed

+1548
-151
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

170 files changed

+1548
-151
lines changed
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package com.trustwallet.core.app.blockchains.bitcoin
2+
3+
import com.google.protobuf.ByteString
4+
import com.trustwallet.core.app.utils.Numeric
5+
import com.trustwallet.core.app.utils.toHex
6+
import com.trustwallet.core.app.utils.toHexBytes
7+
import com.trustwallet.core.app.utils.toHexBytesInByteString
8+
import org.junit.Assert.assertEquals
9+
import org.junit.Test
10+
import wallet.core.jni.BitcoinPsbt
11+
import wallet.core.jni.BitcoinScript
12+
import wallet.core.jni.BitcoinSigHashType
13+
import wallet.core.jni.CoinType
14+
import wallet.core.jni.CoinType.BITCOIN
15+
import wallet.core.jni.Hash
16+
import wallet.core.jni.PrivateKey
17+
import wallet.core.jni.PublicKey
18+
import wallet.core.jni.PublicKeyType
19+
import wallet.core.jni.proto.Bitcoin
20+
import wallet.core.jni.proto.Bitcoin.SigningOutput
21+
import wallet.core.jni.proto.BitcoinV2
22+
import wallet.core.jni.proto.Common.SigningError
23+
24+
class TestBitcoinPsbt {
25+
26+
init {
27+
System.loadLibrary("TrustWalletCore")
28+
}
29+
30+
@Test
31+
fun testSignThorSwap() {
32+
// Successfully broadcasted tx: https://mempool.space/tx/634a416e82ac710166725f6a4090ac7b5db69687e86b2d2e38dcb3d91c956c32
33+
34+
val privateKey = "f00ffbe44c5c2838c13d2778854ac66b75e04eb6054f0241989e223223ad5e55".toHexBytesInByteString()
35+
val psbt = "70736274ff0100bc0200000001147010db5fbcf619067c1090fec65c131443fbc80fb4aaeebe940e44206098c60000000000ffffffff0360ea000000000000160014f22a703617035ef7f490743d50f26ae08c30d0a70000000000000000426a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a35303e12000000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d000000000001011f6603010000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d00000000".toHexBytesInByteString()
36+
37+
val input = BitcoinV2.PsbtSigningInput.newBuilder()
38+
.setPsbt(psbt)
39+
.addPrivateKeys(privateKey)
40+
.build()
41+
42+
val outputData = BitcoinPsbt.sign(input.toByteArray(), BITCOIN)
43+
val output = BitcoinV2.PsbtSigningOutput.parseFrom(outputData)
44+
45+
assertEquals(output.error, SigningError.OK)
46+
assertEquals(
47+
output.psbt.toByteArray().toHex(),
48+
"0x70736274ff0100bc0200000001147010db5fbcf619067c1090fec65c131443fbc80fb4aaeebe940e44206098c60000000000ffffffff0360ea000000000000160014f22a703617035ef7f490743d50f26ae08c30d0a70000000000000000426a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a35303e12000000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d000000000001011f6603010000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d01086c02483045022100b1229a008f20691639767bf925d6b8956ea957ccc633ad6b5de3618733a55e6b02205774d3320489b8a57a6f8de07f561de3e660ff8e587f6ac5422c49020cd4dc9101210306d8c664ea8fd2683eebea1d3114d90e0a5429e5783ba49b80ddabce04ff28f300000000"
49+
)
50+
assertEquals(
51+
output.encoded.toByteArray().toHex(),
52+
"0x02000000000101147010db5fbcf619067c1090fec65c131443fbc80fb4aaeebe940e44206098c60000000000ffffffff0360ea000000000000160014f22a703617035ef7f490743d50f26ae08c30d0a70000000000000000426a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a35303e12000000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d02483045022100b1229a008f20691639767bf925d6b8956ea957ccc633ad6b5de3618733a55e6b02205774d3320489b8a57a6f8de07f561de3e660ff8e587f6ac5422c49020cd4dc9101210306d8c664ea8fd2683eebea1d3114d90e0a5429e5783ba49b80ddabce04ff28f300000000"
53+
)
54+
assertEquals(
55+
output.txid.toByteArray().toHex(),
56+
"0x634a416e82ac710166725f6a4090ac7b5db69687e86b2d2e38dcb3d91c956c32"
57+
)
58+
}
59+
60+
@Test
61+
fun testPlanThorSwap() {
62+
// Successfully broadcasted tx: https://mempool.space/tx/634a416e82ac710166725f6a4090ac7b5db69687e86b2d2e38dcb3d91c956c32
63+
64+
val privateKey = PrivateKey("f00ffbe44c5c2838c13d2778854ac66b75e04eb6054f0241989e223223ad5e55".toHexBytes())
65+
val publicKey = privateKey.getPublicKeySecp256k1(true)
66+
val psbt = "70736274ff0100bc0200000001147010db5fbcf619067c1090fec65c131443fbc80fb4aaeebe940e44206098c60000000000ffffffff0360ea000000000000160014f22a703617035ef7f490743d50f26ae08c30d0a70000000000000000426a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a35303e12000000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d000000000001011f6603010000000000160014b139199ec796f36fc42e637f42da8e3e6720aa9d00000000".toHexBytesInByteString()
67+
68+
val input = BitcoinV2.PsbtSigningInput.newBuilder()
69+
.setPsbt(psbt)
70+
.addPublicKeys(ByteString.copyFrom(publicKey.data()))
71+
.build()
72+
73+
val outputData = BitcoinPsbt.plan(input.toByteArray(), BITCOIN)
74+
val output = BitcoinV2.TransactionPlan.parseFrom(outputData)
75+
76+
assertEquals(output.error, SigningError.OK)
77+
78+
assertEquals(output.getInputs(0).receiverAddress, "bc1qkyu3n8k8jmekl3pwvdl59k5w8enjp25akz2r3z")
79+
assertEquals(output.getInputs(0).value, 66_406)
80+
81+
// Vault transfer
82+
assertEquals(output.getOutputs(0).toAddress, "bc1q7g48qdshqd000aysws74pun2uzxrp598gcfum0")
83+
assertEquals(output.getOutputs(0).value, 60_000)
84+
85+
// OP_RETURN
86+
assertEquals(
87+
output.getOutputs(1).customScriptPubkey.toByteArray().toHex(),
88+
"0x6a403d3a474149412e41544f4d3a636f736d6f7331737377797a666d743675396a373437773537753438746778646575393573757a666c6d7175753a303a743a3530"
89+
)
90+
assertEquals(output.getOutputs(1).value, 0)
91+
92+
// Change output
93+
assertEquals(output.getOutputs(2).toAddress, "bc1qkyu3n8k8jmekl3pwvdl59k5w8enjp25akz2r3z")
94+
assertEquals(output.getOutputs(2).value, 4_670)
95+
96+
assertEquals(output.feeEstimate, 1736)
97+
// Please note that `change` in PSBT planning is always 0.
98+
// That's because we aren't able to determine which output is an actual change from PSBT.
99+
assertEquals(output.change, 0)
100+
}
101+
}

codegen-v2/src/codegen/rust/coin_address_derivation_test_generator.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// Copyright © 2017 Trust Wallet.
44

5-
use crate::codegen::rust::tw_any_coin_directory;
5+
use crate::codegen::rust::tw_tests_directory;
66
use crate::registry::CoinItem;
77
use crate::utils::FileContent;
88
use crate::Result;
@@ -14,7 +14,7 @@ const EVM_ADDRESS_DERIVATION_TEST_END: &str =
1414
"end_of_evm_address_derivation_tests_marker_do_not_modify";
1515

1616
pub fn coin_address_derivation_test_path() -> PathBuf {
17-
tw_any_coin_directory()
17+
tw_tests_directory()
1818
.join("tests")
1919
.join("coin_address_derivation_test.rs")
2020
}

codegen-v2/src/codegen/rust/coin_integration_tests.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// Copyright © 2017 Trust Wallet.
44

5-
use crate::codegen::rust::tw_any_coin_directory;
5+
use crate::codegen::rust::tw_tests_directory;
66
use crate::codegen::template_generator::TemplateGenerator;
77
use crate::coin_id::CoinId;
88
use crate::registry::CoinItem;
@@ -20,15 +20,15 @@ const MOD_ADDRESS_TESTS_TEMPLATE: &str = include_str!("templates/integration_tes
2020
const SIGN_TESTS_TEMPLATE: &str = include_str!("templates/integration_tests/sign_tests.rs");
2121

2222
pub fn chains_integration_tests_directory() -> PathBuf {
23-
tw_any_coin_directory().join("tests").join("chains")
23+
tw_tests_directory().join("tests").join("chains")
2424
}
2525

2626
pub fn coin_integration_tests_directory(id: &CoinId) -> PathBuf {
2727
chains_integration_tests_directory().join(id.as_str())
2828
}
2929

3030
pub fn coin_address_derivation_test_path() -> PathBuf {
31-
tw_any_coin_directory()
31+
tw_tests_directory()
3232
.join("tests")
3333
.join("coin_address_derivation_test.rs")
3434
}

codegen-v2/src/codegen/rust/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ pub fn chains_directory() -> PathBuf {
2626
rust_source_directory().join("chains")
2727
}
2828

29-
pub fn tw_any_coin_directory() -> PathBuf {
30-
rust_source_directory().join("tw_any_coin")
29+
pub fn tw_tests_directory() -> PathBuf {
30+
rust_source_directory().join("tw_tests")
3131
}
3232

3333
pub fn workspace_toml_path() -> PathBuf {

docs/registry.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ This list is generated from [./registry.json](../registry.json)
130130
| 10004689 | IoTeX EVM | IOTX | <img src="https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/iotexevm/info/logo.png" width="32" /> | <https://iotex.io/> |
131131
| 10007000 | NativeZetaChain | ZETA | <img src="https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/zetachain/info/logo.png" width="32" /> | <https://www.zetachain.com/> |
132132
| 10007700 | NativeCanto | CANTO | <img src="https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/nativecanto/info/logo.png" width="32" /> | <https://canto.io/> |
133-
| 10008217 | Kaia | KLAY | <img src="https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/kaia/info/logo.png" width="32" /> | <https://kaia.io> |
133+
| 10008217 | Kaia | KLAY | <img src="https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/kaia/info/logo.png" width="32" /> | <https://kaia.io> |
134134
| 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/> |
135135
| 10009001 | Evmos | EVMOS | <img src="https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/evmos/info/logo.png" width="32" /> | <https://evmos.org/> |
136136
| 10042170 | Arbitrum Nova | ETH | <img src="https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/arbitrumnova/info/logo.png" width="32" /> | <https://nova.arbitrum.io> |
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
//
3+
// Copyright © 2017 Trust Wallet.
4+
5+
#pragma once
6+
7+
#include "TWBase.h"
8+
#include "TWBitcoinSigHashType.h"
9+
#include "TWCoinType.h"
10+
#include "TWData.h"
11+
#include "TWPublicKey.h"
12+
13+
TW_EXTERN_C_BEGIN
14+
15+
/// Represents a signer to sign/plan PSBT for Bitcoin blockchains.
16+
TW_EXPORT_CLASS
17+
struct TWBitcoinPsbt;
18+
19+
/// Signs a PSBT (Partially Signed Bitcoin Transaction) specified by the signing input and coin type.
20+
///
21+
/// \param input The serialized data of a signing input (e.g. `TW.BitcoinV2.Proto.PsbtSigningInput`)
22+
/// \param coin The given coin type to sign the PSBT for.
23+
/// \return The serialized data of a `Proto.PsbtSigningOutput` proto object (e.g. `TW.BitcoinV2.Proto.PsbtSigningOutput`).
24+
TW_EXPORT_STATIC_METHOD
25+
TWData* _Nonnull TWBitcoinPsbtSign(TWData* _Nonnull input, enum TWCoinType coin);
26+
27+
/// Plans a PSBT (Partially Signed Bitcoin Transaction).
28+
/// Can be used to get the transaction detailed decoded from PSBT.
29+
///
30+
/// \param input The serialized data of a signing input (e.g. `TW.BitcoinV2.Proto.PsbtSigningInput`)
31+
/// \param coin The given coin type to sign the PSBT for.
32+
/// \return The serialized data of a `Proto.TransactionPlan` proto object (e.g. `TW.BitcoinV2.Proto.TransactionPlan`).
33+
TW_EXPORT_STATIC_METHOD
34+
TWData* _Nonnull TWBitcoinPsbtPlan(TWData* _Nonnull input, enum TWCoinType coin);
35+
36+
TW_EXTERN_C_END

rust/Cargo.lock

Lines changed: 26 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
members = [
33
"chains/tw_aptos",
44
"chains/tw_binance",
5+
"chains/tw_bitcoin",
56
"chains/tw_cosmos",
67
"chains/tw_ethereum",
78
"chains/tw_greenfield",
@@ -18,7 +19,6 @@ members = [
1819
"tw_any_coin",
1920
"tw_base58_address",
2021
"tw_bech32_address",
21-
"tw_bitcoin",
2222
"tw_coin_entry",
2323
"tw_coin_registry",
2424
"tw_cosmos_sdk",
@@ -30,6 +30,7 @@ members = [
3030
"tw_misc",
3131
"tw_number",
3232
"tw_proto",
33+
"tw_tests",
3334
"wallet_core_bin",
3435
"wallet_core_rs",
3536
]

rust/chains/tw_bitcoin/Cargo.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[package]
2+
name = "tw_bitcoin"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
bitcoin = { version = "0.30.0", features = ["rand-std"] }
8+
secp256k1 = { version = "0.27.0", features = ["global-context", "rand-std"] }
9+
serde = { version = "1.0", features = ["derive"] }
10+
serde_json = "1.0"
11+
tw_bech32_address = { path = "../../tw_bech32_address" }
12+
tw_base58_address = { path = "../../tw_base58_address" }
13+
tw_coin_entry = { path = "../../tw_coin_entry", features = ["test-utils"] }
14+
tw_encoding = { path = "../../tw_encoding" }
15+
tw_hash = { path = "../../tw_hash" }
16+
tw_keypair = { path = "../../tw_keypair" }
17+
tw_memory = { path = "../../tw_memory" }
18+
tw_misc = { path = "../../tw_misc" }
19+
tw_proto = { path = "../../tw_proto" }
20+
tw_utxo = { path = "../../frameworks/tw_utxo" }

rust/tw_bitcoin/src/entry.rs renamed to rust/chains/tw_bitcoin/src/entry.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::modules::compiler::BitcoinCompiler;
22
use crate::modules::planner::BitcoinPlanner;
3+
use crate::modules::psbt_planner::PsbtPlanner;
34
use crate::modules::signer::BitcoinSigner;
45
use crate::modules::transaction_util::BitcoinTransactionUtil;
56
use std::str::FromStr;
@@ -14,6 +15,7 @@ use tw_coin_entry::modules::wallet_connector::NoWalletConnector;
1415
use tw_keypair::tw::PublicKey;
1516
use tw_proto::BitcoinV2::Proto;
1617
use tw_utxo::address::standard_bitcoin::{StandardBitcoinAddress, StandardBitcoinPrefix};
18+
use tw_utxo::utxo_entry::UtxoEntry;
1719

1820
pub struct BitcoinEntry;
1921

@@ -97,3 +99,27 @@ impl CoinEntry for BitcoinEntry {
9799
Some(BitcoinTransactionUtil)
98100
}
99101
}
102+
103+
impl UtxoEntry for BitcoinEntry {
104+
type PsbtSigningInput<'a> = Proto::PsbtSigningInput<'a>;
105+
type PsbtSigningOutput = Proto::PsbtSigningOutput<'static>;
106+
type PsbtTransactionPlan = Proto::TransactionPlan<'static>;
107+
108+
#[inline]
109+
fn sign_psbt(
110+
&self,
111+
coin: &dyn CoinContext,
112+
input: Self::PsbtSigningInput<'_>,
113+
) -> Self::PsbtSigningOutput {
114+
BitcoinSigner::sign_psbt(coin, &input)
115+
}
116+
117+
#[inline]
118+
fn plan_psbt(
119+
&self,
120+
coin: &dyn CoinContext,
121+
input: Self::PsbtSigningInput<'_>,
122+
) -> Self::PsbtTransactionPlan {
123+
PsbtPlanner::plan_psbt(coin, &input)
124+
}
125+
}

0 commit comments

Comments
 (0)