diff --git a/.github/workflows/ios-ci.yml b/.github/workflows/ios-ci.yml index 8144167a794..7dcc89a194b 100644 --- a/.github/workflows/ios-ci.yml +++ b/.github/workflows/ios-ci.yml @@ -35,7 +35,7 @@ jobs: - name: Run iOS tests run: | - tools/generate-files + tools/generate-files ios tools/ios-test - name: Build sample app run: | diff --git a/.github/workflows/kotlin-ci.yml b/.github/workflows/kotlin-ci.yml index e53e08d116b..f22e812b7e5 100644 --- a/.github/workflows/kotlin-ci.yml +++ b/.github/workflows/kotlin-ci.yml @@ -48,7 +48,7 @@ jobs: if: steps.internal_cache.outputs.cache-hit != 'true' - name: Generate files - run: tools/generate-files + run: tools/generate-files android - name: Build Kotlin Multiplatform run: tools/kotlin-build diff --git a/.github/workflows/linux-ci-sonarcloud.yml b/.github/workflows/linux-ci-sonarcloud.yml index a8f13964585..d8154a0c9b4 100644 --- a/.github/workflows/linux-ci-sonarcloud.yml +++ b/.github/workflows/linux-ci-sonarcloud.yml @@ -35,7 +35,7 @@ jobs: if: steps.internal_cache.outputs.cache-hit != 'true' - name: Code generation run: | - tools/generate-files + tools/generate-files native env: CC: /usr/bin/clang CXX: /usr/bin/clang++ diff --git a/.github/workflows/linux-ci.yml b/.github/workflows/linux-ci.yml index cf9cfa4e8bb..8eb3d94358e 100644 --- a/.github/workflows/linux-ci.yml +++ b/.github/workflows/linux-ci.yml @@ -42,7 +42,7 @@ jobs: - name: Code generation run: | - tools/generate-files + tools/generate-files native env: CC: /usr/bin/clang CXX: /usr/bin/clang++ diff --git a/.github/workflows/linux-sampleapp-ci.yml b/.github/workflows/linux-sampleapp-ci.yml index e6c2278bedb..ec1b8ec665d 100644 --- a/.github/workflows/linux-sampleapp-ci.yml +++ b/.github/workflows/linux-sampleapp-ci.yml @@ -35,7 +35,7 @@ jobs: if: steps.internal_cache.outputs.cache-hit != 'true' - name: Code generation run: | - tools/generate-files + tools/generate-files native env: CC: /usr/bin/clang CXX: /usr/bin/clang++ diff --git a/.github/workflows/wasm-ci.yml b/.github/workflows/wasm-ci.yml index e8b9c7fae5e..da063d565be 100644 --- a/.github/workflows/wasm-ci.yml +++ b/.github/workflows/wasm-ci.yml @@ -39,7 +39,9 @@ jobs: if: steps.internal_cache.outputs.cache-hit != 'true' - name: Code generation - run: tools/generate-files + run: | + source emsdk/emsdk_env.sh + tools/generate-files wasm env: CC: /usr/bin/clang CXX: /usr/bin/clang++ diff --git a/Dockerfile b/Dockerfile index 318103c1618..ff8083071cf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -54,7 +54,7 @@ WORKDIR /wallet-core RUN tools/install-dependencies # Build: generate files and rust lib -RUN tools/generate-files +RUN tools/generate-files native # Build: cmake + make wallet core RUN cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=Debug \ diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoin/TestBitcoinSigning.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoin/TestBitcoinSigning.kt index 45d627d8b46..8e7f3ba6f00 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoin/TestBitcoinSigning.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoin/TestBitcoinSigning.kt @@ -10,6 +10,10 @@ 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.Common.SigningError @@ -155,4 +159,203 @@ class TestBitcoinSigning { assertEquals("0x01000000000102fff7f7881a8099afa6940d42d1e7f6362bec38171ea3edf433541db4e4ad969f00000000494830450221008d49c4d7cc5ab93c01a67ce3f4ed2c45c59d4da6c76c891a9b56e67eda2e8cb4022078849134c697b1c70c1a19b900d94d8cab00ad7bcc8afe7ad1f6b184c13effa601ffffffffef51e1b804cc89d182d279655c3aa89e815b1b309fe287d9b2b55d57b90ec68a0100000000ffffffff02d8d60000000000001976a914a6d85a488bb777a540f24bf777d30d1486036f6188acee430000000000001976a9147d77e6cfb05a9cfc123824279f6caf8b66ac267688ac0002473044022074573d7f7828ae193fbea6d72c0fe2df6cee5c02bf455ea3d9312e16d6a9576502203861c5a3b3a83d4fe372034073f60201a8a944fb4536be0ea7544ab177b967600121025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee635700000000", Numeric.toHexString(encoded.toByteArray())); } + + @Test + fun testSignBrc20Transfer() { + // Successfully broadcasted: https://www.blockchain.com/explorer/transactions/btc/3e3576eb02667fac284a5ecfcb25768969680cc4c597784602d0a33ba7c654b7 + val privateKeyData = (Numeric.hexStringToByteArray("e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129")) + val fullAmount = 26400 + val minerFee = 3000 + val brcInscribeAmount = 7000 + val dustSatoshis = 546 + val forFeeAmount = fullAmount - brcInscribeAmount - minerFee + val txIdInscription = Numeric.hexStringToByteArray("7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca").reversedArray() + val txIDForFees = Numeric.hexStringToByteArray("797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1").reversedArray() + + val privateKey = PrivateKey(privateKeyData) + val publicKey = privateKey.getPublicKeySecp256k1(true) + val pubKeyHash = Hash.ripemd(Hash.sha256(publicKey.data())) + val bobPubkey = PublicKey(Numeric.hexStringToByteArray("02f453bb46e7afc8796a9629e89e07b5cb0867e9ca340b571e7bcc63fc20c43f2e"), PublicKeyType.SECP256K1) + val bobPubkeyHash = Hash.ripemd(Hash.sha256(bobPubkey.data())) + + val p2wpkh = BitcoinScript.buildPayToWitnessPubkeyHash(pubKeyHash) + val outputP2wpkh = BitcoinScript.buildPayToWitnessPubkeyHash(bobPubkeyHash) + + val input = Bitcoin.SigningInput.newBuilder() + .setIsItBrcOperation(true) + .addPrivateKey(ByteString.copyFrom(privateKeyData)) + + val unspentOutputPoint0 = Bitcoin.OutPoint.newBuilder() + .setHash(ByteString.copyFrom(txIdInscription)) + .setIndex(0) + .build() + val utxo0 = Bitcoin.UnspentTransaction.newBuilder() + .setScript(ByteString.copyFrom(p2wpkh.data())) + .setAmount(dustSatoshis.toLong()) + .setVariant(Bitcoin.TransactionVariant.P2WPKH) + .setOutPoint(unspentOutputPoint0) + .build() + + val unspentOutputPoint1 = Bitcoin.OutPoint.newBuilder() + .setHash(ByteString.copyFrom(txIDForFees)) + .setIndex(1) + .build() + val utxo1 = Bitcoin.UnspentTransaction.newBuilder() + .setScript(ByteString.copyFrom(p2wpkh.data())) + .setAmount(forFeeAmount.toLong()) + .setVariant(Bitcoin.TransactionVariant.P2WPKH) + .setOutPoint(unspentOutputPoint1) + .build() + + input.addUtxo(utxo0) + input.addUtxo(utxo1) + + val utxoPlan0 = Bitcoin.UnspentTransaction.newBuilder() + .setScript(ByteString.copyFrom(outputP2wpkh.data())) + .setAmount(dustSatoshis.toLong()) + .setVariant(Bitcoin.TransactionVariant.P2WPKH) + .build() + val utxoPlan1 = Bitcoin.UnspentTransaction.newBuilder() + .setScript(ByteString.copyFrom(p2wpkh.data())) + .setAmount((forFeeAmount - minerFee).toLong()) + .setVariant(Bitcoin.TransactionVariant.P2WPKH) + .build() + + val plan = Bitcoin.TransactionPlan.newBuilder() + .addUtxos(utxoPlan0) + .addUtxos(utxoPlan1) + .build() + input.plan = plan + + val output = AnySigner.sign(input.build(), BITCOIN, SigningOutput.parser()) + + assertEquals(output.error, SigningError.OK) + assertEquals(output.transactionId, "3e3576eb02667fac284a5ecfcb25768969680cc4c597784602d0a33ba7c654b7") + assertEquals(Numeric.toHexString(output.encoded.toByteArray()), "0x02000000000102ca3edda74a46877efa5364ab85947e148508713910ada23e147ea28926dc46700000000000ffffffffb11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d790100000000ffffffff022202000000000000160014e891850afc55b64aa8247b2076f8894ebdf889015834000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d024830450221008798393eb0b7390217591a8c33abe18dd2f7ea7009766e0d833edeaec63f2ec302200cf876ff52e68dbaf108a3f6da250713a9b04949a8f1dcd1fb867b24052236950121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb0248304502210096bbb9d1f0596d69875646689e46f29485e8ceccacde9d0025db87fd96d3066902206d6de2dd69d965d28df3441b94c76e812384ab9297e69afe3480ee4031e1b2060121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000") + + val signedTransaction = output.transaction + assert(signedTransaction.isInitialized) + } + + @Test + fun testSignBrc20Commit() { + // Successfully broadcasted: https://www.blockchain.com/explorer/transactions/btc/797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1 + val privateKeyData = (Numeric.hexStringToByteArray("e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129")) + val fullAmount = 26400 + val minerFee = 3000 + val brcInscribeAmount = 7000 + val forFeeAmount = fullAmount - brcInscribeAmount - minerFee + val txId = Numeric.hexStringToByteArray("089098890d2653567b9e8df2d1fbe5c3c8bf1910ca7184e301db0ad3b495c88e") + val brc20Ticker = "oadf" + val brc20Amount = "20" + + val privateKey = PrivateKey(privateKeyData) + val publicKey = privateKey.getPublicKeySecp256k1(true) + val pubKeyHash = Hash.ripemd(Hash.sha256(publicKey.data())) + + val p2wpkh = BitcoinScript.buildPayToWitnessPubkeyHash(pubKeyHash) + val outputInscribe = BitcoinScript.buildBRC20InscribeTransfer(brc20Ticker, brc20Amount, publicKey.data()) + val outputInscribeProto = Bitcoin.TransactionOutput.parseFrom(outputInscribe) + + val input = Bitcoin.SigningInput.newBuilder() + .setIsItBrcOperation(true) + .addPrivateKey(ByteString.copyFrom(privateKeyData)) + + val unspentOutputPoint = Bitcoin.OutPoint.newBuilder() + .setHash(ByteString.copyFrom(txId)) + .setIndex(1) + .build() + + val utxo0 = Bitcoin.UnspentTransaction.newBuilder() + .setScript(ByteString.copyFrom(p2wpkh.data())) + .setAmount(fullAmount.toLong()) + .setVariant(Bitcoin.TransactionVariant.P2WPKH) + .setOutPoint(unspentOutputPoint) + .build() + input.addUtxo(utxo0) + + val utxoPlan0 = Bitcoin.UnspentTransaction.newBuilder() + .setScript(outputInscribeProto.script) + .setAmount(brcInscribeAmount.toLong()) + .setVariant(Bitcoin.TransactionVariant.BRC20TRANSFER) + .build() + val utxoPlan1 = Bitcoin.UnspentTransaction.newBuilder() + .setScript(ByteString.copyFrom(p2wpkh.data())) + .setAmount(forFeeAmount.toLong()) + .setVariant(Bitcoin.TransactionVariant.P2WPKH) + .build() + + val plan = Bitcoin.TransactionPlan.newBuilder() + .addUtxos(utxoPlan0) + .addUtxos(utxoPlan1) + .build() + input.plan = plan + + val output = AnySigner.sign(input.build(), BITCOIN, SigningOutput.parser()) + + assertEquals(output.error, SigningError.OK) + assertEquals(output.transactionId, "797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1") + assertEquals(Numeric.toHexString(output.encoded.toByteArray()), "0x02000000000101089098890d2653567b9e8df2d1fbe5c3c8bf1910ca7184e301db0ad3b495c88e0100000000ffffffff02581b000000000000225120e8b706a97732e705e22ae7710703e7f589ed13c636324461afa443016134cc051040000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d02483045022100a44aa28446a9a886b378a4a65e32ad9a3108870bd725dc6105160bed4f317097022069e9de36422e4ce2e42b39884aa5f626f8f94194d1013007d5a1ea9220a06dce0121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000") + + val signedTransaction = output.transaction + assert(signedTransaction.isInitialized) + } + + @Test + fun testSignBrc20Reveal() { + // Successfully broadcasted: https://www.blockchain.com/explorer/transactions/btc/7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca + val privateKeyData = (Numeric.hexStringToByteArray("e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129")) + val brcInscribeAmount = 7000 + val dustSatoshis = 546 + val txId = Numeric.hexStringToByteArray("b11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d79") + val brc20Ticker = "oadf" + val brc20Amount = "20" + + val privateKey = PrivateKey(privateKeyData) + val publicKey = privateKey.getPublicKeySecp256k1(true) + val pubKeyHash = Hash.ripemd(Hash.sha256(publicKey.data())) + val p2wpkh = BitcoinScript.buildPayToWitnessPubkeyHash(pubKeyHash) + val outputInscribe = BitcoinScript.buildBRC20InscribeTransfer(brc20Ticker, brc20Amount, publicKey.data()) + val outputInscribeProto = Bitcoin.TransactionOutput.parseFrom(outputInscribe) + + val input = Bitcoin.SigningInput.newBuilder() + .setIsItBrcOperation(true) + .addPrivateKey(ByteString.copyFrom(privateKeyData)) + + val unspentOutputPoint0 = Bitcoin.OutPoint.newBuilder() + .setHash(ByteString.copyFrom(txId)) + .setIndex(0) + .build() + val utxo0 = Bitcoin.UnspentTransaction.newBuilder() + .setScript(outputInscribeProto.script) + .setAmount(brcInscribeAmount.toLong()) + .setVariant(Bitcoin.TransactionVariant.BRC20TRANSFER) + .setSpendingScript(outputInscribeProto.spendingScript) + .setOutPoint(unspentOutputPoint0) + .build() + + input.addUtxo(utxo0) + + val utxoPlan0 = Bitcoin.UnspentTransaction.newBuilder() + .setScript(ByteString.copyFrom(p2wpkh.data())) + .setAmount(dustSatoshis.toLong()) + .setVariant(Bitcoin.TransactionVariant.P2WPKH) + .build() + + val plan = Bitcoin.TransactionPlan.newBuilder() + .addUtxos(utxoPlan0) + .build() + input.plan = plan + + val output = AnySigner.sign(input.build(), BITCOIN, SigningOutput.parser()) + + assertEquals(output.error, SigningError.OK) + assertEquals(output.transactionId, "7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca") + val encodedHex = Numeric.toHexString(output.encoded.toByteArray()) + assert(encodedHex.startsWith("0x02000000000101b11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d790000000000ffffffff012202000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d0340")) + assert(encodedHex.endsWith("5b0063036f7264010118746578742f706c61696e3b636861727365743d7574662d3800377b2270223a226272632d3230222c226f70223a227472616e73666572222c227469636b223a226f616466222c22616d74223a223230227d6821c00f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000")) + + val signedTransaction = output.transaction + assert(signedTransaction.isInitialized) + } } diff --git a/include/TrustWalletCore/TWBitcoinScript.h b/include/TrustWalletCore/TWBitcoinScript.h index f8db46847d6..8023015f80a 100644 --- a/include/TrustWalletCore/TWBitcoinScript.h +++ b/include/TrustWalletCore/TWBitcoinScript.h @@ -193,6 +193,16 @@ struct TWBitcoinScript* _Nonnull TWBitcoinScriptBuildPayToWitnessPubkeyHash(TWDa TW_EXPORT_STATIC_METHOD struct TWBitcoinScript* _Nonnull TWBitcoinScriptBuildPayToWitnessScriptHash(TWData* _Nonnull scriptHash); +/// Builds the Ordinals inscripton for BRC20 transfer. +/// +/// \param ticker ticker of the brc20 +/// \param amount uint64 transfer amount +/// \param pubkey Non-null pointer to a pubkey +/// \note Must be deleted with \TWBitcoinScriptDelete +/// \return A pointer to the built script +TW_EXPORT_STATIC_METHOD +TWData* _Nonnull TWBitcoinScriptBuildBRC20InscribeTransfer(TWString* _Nonnull ticker, TWString* _Nonnull amount, TWData* _Nonnull pubkey); + /// Builds a appropriate lock script for the given address.. /// /// \param address Non-null pointer to an address diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 96854c2978a..3199454c20e 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -117,6 +117,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + [[package]] name = "bigdecimal" version = "0.3.1" @@ -129,6 +135,34 @@ dependencies = [ "serde", ] +[[package]] +name = "bitcoin" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b36f4c848f6bd9ff208128f08751135846cc23ae57d66ab10a22efff1c675f3c" +dependencies = [ + "bech32", + "bitcoin-private", + "bitcoin_hashes", + "hex_lit", + "secp256k1", +] + +[[package]] +name = "bitcoin-private" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73290177011694f38ec25e165d0387ab7ea749a4b81cd4c80dae5988229f7a57" + +[[package]] +name = "bitcoin_hashes" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d7066118b13d4b20b23645932dfb3a81ce7e29f95726c2036fa33cd7b092501" +dependencies = [ + "bitcoin-private", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -197,6 +231,12 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + [[package]] name = "cfg-if" version = "1.0.0" @@ -357,6 +397,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + [[package]] name = "hmac" version = "0.12.1" @@ -384,6 +430,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + [[package]] name = "js-sys" version = "0.3.61" @@ -642,6 +694,32 @@ dependencies = [ "semver", ] +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "secp256k1" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" +dependencies = [ + "bitcoin_hashes", + "rand", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a129b9e9efbfb223753b9163c4ab3b13cff7fd9c7f010fbac25ab4099fa07e" +dependencies = [ + "cc", +] + [[package]] name = "semver" version = "1.0.17" @@ -677,6 +755,17 @@ dependencies = [ "syn 2.0.18", ] +[[package]] +name = "serde_json" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "sha1" version = "0.10.5" @@ -847,6 +936,20 @@ dependencies = [ "syn 1.0.107", ] +[[package]] +name = "tw_bitcoin" +version = "0.1.0" +dependencies = [ + "bitcoin", + "secp256k1", + "serde", + "serde_json", + "tw_encoding", + "tw_memory", + "tw_misc", + "tw_proto", +] + [[package]] name = "tw_encoding" version = "0.1.0" @@ -888,6 +991,13 @@ dependencies = [ name = "tw_memory" version = "0.1.0" +[[package]] +name = "tw_misc" +version = "0.1.0" +dependencies = [ + "zeroize", +] + [[package]] name = "tw_move_parser" version = "0.1.0" @@ -959,6 +1069,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" name = "wallet-core-rs" version = "0.1.0" dependencies = [ + "tw_bitcoin", "tw_encoding", "tw_hash", "tw_keypair", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 7383d94bdd8..2a3f3dd5dfc 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,9 +1,11 @@ [workspace] members = [ + "tw_bitcoin", "tw_encoding", "tw_hash", "tw_keypair", "tw_memory", + "tw_misc", "tw_move_parser", "tw_proto", "tw_starknet", diff --git a/rust/coverage.stats b/rust/coverage.stats index 7f44617b0b9..594a79c35fe 100644 --- a/rust/coverage.stats +++ b/rust/coverage.stats @@ -1 +1 @@ -94.2 \ No newline at end of file +87.8 \ No newline at end of file diff --git a/rust/tw_bitcoin/Cargo.toml b/rust/tw_bitcoin/Cargo.toml new file mode 100644 index 00000000000..82ef70900ab --- /dev/null +++ b/rust/tw_bitcoin/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "tw_bitcoin" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bitcoin = "0.30.0" +secp256k1 = { version = "0.27.0", features = [ "global-context", "rand-std" ] } +serde = { version = "1.0.163", features = [ "derive" ] } +serde_json = "1.0.96" +tw_encoding = { path = "../tw_encoding" } +tw_memory = { path = "../tw_memory" } +tw_misc = { path = "../tw_misc" } +tw_proto = { path = "../tw_proto" } diff --git a/rust/tw_bitcoin/src/brc20.rs b/rust/tw_bitcoin/src/brc20.rs new file mode 100644 index 00000000000..e2070059c2b --- /dev/null +++ b/rust/tw_bitcoin/src/brc20.rs @@ -0,0 +1,211 @@ +use crate::ordinals::OrdinalsInscription; +use crate::{Error, Recipient, Result}; +use bitcoin::PublicKey; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BRC20Payload { + #[serde(rename = "p")] + protocol: String, + #[serde(rename = "op")] + operation: String, + #[serde(flatten)] + inner: T, +} + +impl BRC20Payload { + const PROTOCOL_ID: &str = "brc-20"; + const MIME: &[u8] = b"text/plain;charset=utf-8"; +} + +// Convenience aliases. +pub type BRC20DeployPayload = BRC20Payload; +pub type BRC20MintPayload = BRC20Payload; +pub type BRC20TransferPayload = BRC20Payload; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Ticker(String); + +impl Ticker { + pub fn new(string: String) -> Result { + // Ticker must be a 4-letter identifier. + if string.len() != 4 { + return Err(Error::Todo); + } + + Ok(Ticker(string)) + } + pub fn to_byte_array(&self) -> [u8; 4] { + self.0 + .as_bytes() + .try_into() + .expect("ticker must be four bytes") + } +} + +impl TryFrom for Ticker { + type Error = Error; + + fn try_from(string: String) -> Result { + Self::new(string) + } +} + +impl BRC20DeployPayload { + const OPERATION: &str = "deploy"; + + pub fn new(ticker: Ticker, max: usize, limit: Option, decimals: Option) -> Self { + BRC20Payload { + protocol: Self::PROTOCOL_ID.to_string(), + operation: Self::OPERATION.to_string(), + inner: DeployPayload { + tick: ticker, + max: max.to_string(), + lim: limit.map(|l| l.to_string()), + dec: decimals.map(|d| d.to_string()), + }, + } + } +} + +impl BRC20TransferPayload { + const OPERATION: &str = "transfer"; + + pub fn new(ticker: Ticker, amount: u64) -> Self { + BRC20Payload { + protocol: Self::PROTOCOL_ID.to_string(), + operation: Self::OPERATION.to_string(), + inner: TransferPayload { + tick: ticker, + amt: amount.to_string(), + }, + } + } +} + +impl BRC20MintPayload { + const OPERATION: &str = "mint"; + + pub fn new(ticker: Ticker, amount: u64) -> Self { + BRC20Payload { + protocol: Self::PROTOCOL_ID.to_string(), + operation: Self::OPERATION.to_string(), + inner: MintPayload { + tick: ticker, + amt: amount.to_string(), + }, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DeployPayload { + pub tick: Ticker, + pub max: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub lim: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub dec: Option, +} + +#[derive(Debug, Clone)] +pub struct BRC20DeployInscription(OrdinalsInscription); + +impl BRC20DeployInscription { + pub fn new( + recipient: Recipient, + ticker: Ticker, + max: usize, + limit: Option, + decimals: Option, + ) -> Result { + let data = BRC20DeployPayload::new(ticker, max, limit, decimals); + + Self::from_payload(data, recipient) + } + pub fn from_payload( + data: BRC20DeployPayload, + recipient: Recipient, + ) -> Result { + let inscription = OrdinalsInscription::new( + BRC20Payload::::MIME, + &serde_json::to_vec(&data).unwrap(), + recipient, + )?; + + Ok(BRC20DeployInscription(inscription)) + } + pub fn inscription(&self) -> &OrdinalsInscription { + &self.0 + } +} + +#[derive(Serialize, Deserialize)] +pub struct TransferPayload { + pub tick: Ticker, + pub amt: String, +} + +pub struct BRC20TransferInscription(OrdinalsInscription); + +impl BRC20TransferInscription { + pub fn new( + recipient: Recipient, + ticker: Ticker, + amount: u64, + ) -> Result { + let data = BRC20TransferPayload::new(ticker, amount); + Self::from_payload(data, recipient) + } + pub fn from_payload( + data: BRC20TransferPayload, + recipient: Recipient, + ) -> Result { + let inscription = OrdinalsInscription::new( + BRC20Payload::::MIME, + &serde_json::to_vec(&data).unwrap(), + recipient, + )?; + + Ok(BRC20TransferInscription(inscription)) + } + pub fn inscription(&self) -> &OrdinalsInscription { + &self.0 + } +} + +/// The structure is the same as `TransferPayload`, but we'll keep it separate +/// for clarity. +#[derive(Serialize, Deserialize)] +pub struct MintPayload { + pub tick: Ticker, + pub amt: String, +} + +pub struct BRC20MintInscription(OrdinalsInscription); + +impl BRC20MintInscription { + pub fn new( + recipient: Recipient, + ticker: Ticker, + amount: u64, + ) -> Result { + let data = BRC20MintPayload::new(ticker, amount); + Self::from_payload(data, recipient) + } + pub fn from_payload( + data: BRC20MintPayload, + recipient: Recipient, + ) -> Result { + let inscription = OrdinalsInscription::new( + BRC20Payload::::MIME, + &serde_json::to_vec(&data).unwrap(), + recipient, + )?; + + Ok(BRC20MintInscription(inscription)) + } + pub fn inscription(&self) -> &OrdinalsInscription { + &self.0 + } +} diff --git a/rust/tw_bitcoin/src/claim.rs b/rust/tw_bitcoin/src/claim.rs new file mode 100644 index 00000000000..02edc331ec5 --- /dev/null +++ b/rust/tw_bitcoin/src/claim.rs @@ -0,0 +1,194 @@ +use crate::{ + Error, Recipient, Result, TaprootScript, TxInputP2PKH, TxInputP2TRKeyPath, + TxInputP2TRScriptPath, TxInputP2WPKH, +}; +use bitcoin::key::{KeyPair, PublicKey, TapTweak, TweakedKeyPair, TweakedPublicKey}; +use bitcoin::secp256k1::Secp256k1; +use bitcoin::sighash::{EcdsaSighashType, TapSighashType}; +use bitcoin::taproot::{LeafVersion, Signature}; +use bitcoin::{ScriptBuf, Witness}; + +#[derive(Debug, Clone)] +pub enum ClaimLocation { + Script(ScriptBuf), + Witness(Witness), +} + +pub trait TransactionSigner { + /// Claiming mechanism for (legacy) P2PKH outputs. + fn claim_p2pkh( + &self, + input: &TxInputP2PKH, + sighash: secp256k1::Message, + sighash_type: EcdsaSighashType, + ) -> Result; + /// Claiming mechanism for SegWit P2WPKH outputs. + fn claim_p2wpkh( + &self, + input: &TxInputP2WPKH, + sighash: secp256k1::Message, + sighash_type: EcdsaSighashType, + ) -> Result; + /// Claiming mechanism for Taproot P2TR key-path outputs. + fn claim_p2tr_key_path( + &self, + input: &TxInputP2TRKeyPath, + sighash: secp256k1::Message, + sighash_type: TapSighashType, + ) -> Result; + /// Claiming mechanism for Taproot P2TR script-path outputs. + fn claim_p2tr_script_path( + &self, + input: &TxInputP2TRScriptPath, + sighash: secp256k1::Message, + sighash_type: TapSighashType, + ) -> Result; +} + +// Contains the `scriptBuf` that must be included in the transaction when +// spending the P2PKH input. +pub struct ClaimP2PKH(pub ScriptBuf); + +// Contains the Witness that must be included in the transaction when spending +// the SegWit P2WPKH input. +pub struct ClaimP2WPKH(pub Witness); + +// Contains the Witness that must be included in the transaction when spending +// the Taproot P2TR key-path input. +pub struct ClaimP2TRKeyPath(pub Witness); + +// Contains the Witness that must be included in the transaction when spending +// the Taproot P2TR script-path input. +pub struct ClaimP2TRScriptPath(pub Witness); + +impl TransactionSigner for KeyPair { + fn claim_p2pkh( + &self, + input: &TxInputP2PKH, + sighash: secp256k1::Message, + sighash_type: EcdsaSighashType, + ) -> Result { + let me = Recipient::::from_keypair(self); + + // Check whether we can actually claim the input. + if input.recipient().pubkey_hash() != &me.pubkey_hash() { + return Err(Error::Todo); + } + + // Construct the ECDSA signature. + let sig = bitcoin::ecdsa::Signature { + sig: self.secret_key().sign_ecdsa(sighash), + hash_ty: sighash_type, + }; + + // Construct the Script for claiming. + let script = ScriptBuf::builder() + .push_slice(sig.serialize()) + .push_key(&me.public_key()) + .into_script(); + + Ok(ClaimP2PKH(script)) + } + fn claim_p2wpkh( + &self, + input: &TxInputP2WPKH, + sighash: secp256k1::Message, + sighash_type: EcdsaSighashType, + ) -> Result { + let me = Recipient::::from_keypair(self); + + if input.recipient().wpubkey_hash() != &me.wpubkey_hash()? { + return Err(Error::Todo); + } + + // Construct the ECDSA signature. + let sig = bitcoin::ecdsa::Signature { + sig: self.secret_key().sign_ecdsa(sighash), + hash_ty: sighash_type, + }; + + // Construct the Witness for claiming. + let mut witness = Witness::new(); + witness.push(sig.serialize()); + // Serialize public key. + witness.push(me.public_key().to_bytes()); + + Ok(ClaimP2WPKH(witness)) + } + fn claim_p2tr_key_path( + &self, + input: &TxInputP2TRKeyPath, + sighash: secp256k1::Message, + sighash_type: TapSighashType, + ) -> Result { + let me = Recipient::::from(self); + + // Check whether we can actually claim the input. + if input.recipient() != &me { + return Err(Error::Todo); + } + + let secp = Secp256k1::new(); + + // Tweak keypair for P2TR key-path (ie. zeroed Merkle root). + let tapped: TweakedKeyPair = self.tap_tweak(&secp, None); + let tweaked = KeyPair::from(tapped); + + // Construct the Schnorr signature. + #[cfg(not(test))] + let schnorr = secp.sign_schnorr(&sighash, &tweaked); + #[cfg(test)] + // For tests, we disable the included randomness in order to create + // reproducible signatures. Randomness should ALWAYS be used in + // production. + let schnorr = secp.sign_schnorr_no_aux_rand(&sighash, &tweaked); + + let sig = bitcoin::taproot::Signature { + sig: schnorr, + hash_ty: sighash_type, + }; + + // Construct the witness for claiming. + let mut witness = Witness::new(); + witness.push(sig.to_vec()); + + Ok(ClaimP2TRKeyPath(witness)) + } + fn claim_p2tr_script_path( + &self, + input: &TxInputP2TRScriptPath, + sighash: secp256k1::Message, + sighash_type: TapSighashType, + ) -> Result { + // Tweak our public key with the Merkle root of the Script to be claimed. + let me = Recipient::::from_keypair(self, input.recipient().merkle_root()); + + // Check whether we can actually claim the input. + if input.recipient() != &me { + return Err(Error::Todo); + } + + // The control block contains information on which script of the + // script-path is being executed. + let control_block = input + .spend_info() + .control_block(&(input.witness().clone(), LeafVersion::TapScript)) + .ok_or(Error::Todo)?; + + // Construct the Schnorr signature. We leave the keypair untweaked, + // unlike for key-path. + let sig = Signature { + sig: Secp256k1::new().sign_schnorr(&sighash, self), + hash_ty: sighash_type, + }; + + // Construct the Witness for claiming. + let mut witness = Witness::new(); + // Serialize signature. + witness.push(&sig.to_vec()); + witness.push(input.witness()); + witness.push(control_block.serialize()); + + Ok(ClaimP2TRScriptPath(witness)) + } +} diff --git a/rust/tw_bitcoin/src/ffi/address.rs b/rust/tw_bitcoin/src/ffi/address.rs new file mode 100644 index 00000000000..63c34dc31fc --- /dev/null +++ b/rust/tw_bitcoin/src/ffi/address.rs @@ -0,0 +1,98 @@ +use super::CTaprootError; +use crate::Recipient; +use bitcoin::PublicKey; +use std::ffi::CString; +use tw_memory::ffi::c_byte_array_ref::CByteArrayRef; +use tw_memory::ffi::c_result::CStrMutResult; + +#[no_mangle] +pub unsafe extern "C" fn tw_legacy_address_string( + pubkey: *const u8, + pubkey_len: usize, + network: Network, +) -> CStrMutResult { + // Convert Recipient. + let Some(slice) = CByteArrayRef::new(pubkey, pubkey_len).as_slice() else { + return CStrMutResult::error(CTaprootError::InvalidSlice); + }; + + let Ok(recipient) = Recipient::::from_slice(slice) else { + return CStrMutResult::error(CTaprootError::InvalidPubkey); + }; + + let address = recipient.legacy_address_string(network.into()); + let c_string = CString::new(address) + .expect("legacy address contains an internal 0 byte") + .into_raw(); + + CStrMutResult::ok(c_string) +} + +#[no_mangle] +pub unsafe extern "C" fn tw_segwit_address_string( + pubkey: *const u8, + pubkey_len: usize, + network: Network, +) -> CStrMutResult { + // Convert Recipient. + let Some(slice) = CByteArrayRef::new(pubkey, pubkey_len).as_slice() else { + return CStrMutResult::error(CTaprootError::InvalidSlice); + }; + + let Ok(recipient) = Recipient::::from_slice(slice) else { + return CStrMutResult::error(CTaprootError::InvalidPubkey); + }; + + let Ok(address) = recipient.segwit_address_string(network.into()) else { + return CStrMutResult::error(CTaprootError::InvalidSegwitPukey); + }; + + let c_string = CString::new(address) + .expect("legacy address contains an internal 0 byte") + .into_raw(); + + CStrMutResult::ok(c_string) +} + +#[no_mangle] +pub unsafe extern "C" fn tw_taproot_address_string( + pubkey: *const u8, + pubkey_len: usize, + network: Network, +) -> CStrMutResult { + // Convert Recipient. + let Some(slice) = CByteArrayRef::new(pubkey, pubkey_len).as_slice() else { + return CStrMutResult::error(CTaprootError::InvalidSlice); + }; + + let Ok(recipient) = Recipient::::from_slice(slice) else { + return CStrMutResult::error(CTaprootError::InvalidPubkey); + }; + + let address = recipient.taproot_address_string(network.into()); + let c_string = CString::new(address) + .expect("legacy address contains an internal 0 byte") + .into_raw(); + + CStrMutResult::ok(c_string) +} + +// A custom reimplementation of of `bitcoin::Network`. +#[repr(C)] +pub enum Network { + Bitcoin = 0, + Testnet = 1, + Signet = 2, + Regtest = 3, +} + +impl From for bitcoin::Network { + fn from(n: Network) -> Self { + match n { + Network::Bitcoin => bitcoin::Network::Bitcoin, + Network::Testnet => bitcoin::Network::Testnet, + Network::Signet => bitcoin::Network::Signet, + Network::Regtest => bitcoin::Network::Regtest, + } + } +} diff --git a/rust/tw_bitcoin/src/ffi/mod.rs b/rust/tw_bitcoin/src/ffi/mod.rs new file mode 100644 index 00000000000..855d03f0c38 --- /dev/null +++ b/rust/tw_bitcoin/src/ffi/mod.rs @@ -0,0 +1,252 @@ +#![allow(clippy::missing_safety_doc)] + +use crate::{Error, Result, TXOutputP2TRScriptPath, TaprootScript, TxInputP2TRScriptPath}; +use bitcoin::{ + taproot::{NodeInfo, TapNodeHash, TaprootSpendInfo}, + PublicKey, ScriptBuf, Txid, +}; +use secp256k1::hashes::Hash; +use secp256k1::KeyPair; +use std::borrow::Cow; +use tw_memory::ffi::c_byte_array::CByteArray; +use tw_memory::ffi::c_byte_array_ref::CByteArrayRef; +use tw_memory::ffi::c_result::ErrorCode; +use tw_misc::try_or_else; +use tw_proto::Bitcoin::Proto::{ + OutPoint, SigningInput, SigningOutput, Transaction, TransactionInput, TransactionOutput, + TransactionVariant as TrVariant, +}; + +pub mod address; +pub mod scripts; + +// Re-exports +pub use address::*; +pub use scripts::*; + +use crate::{ + Recipient, TransactionBuilder, TxInput, TxInputP2PKH, TxInputP2TRKeyPath, TxInputP2WPKH, + TxOutput, TxOutputP2PKH, TxOutputP2TRKeyPath, TxOutputP2WPKH, +}; + +#[no_mangle] +pub unsafe extern "C" fn tw_taproot_build_and_sign_transaction( + input: *const u8, + input_len: usize, +) -> CByteArray { + let data = CByteArrayRef::new(input, input_len) + .to_vec() + .unwrap_or_default(); + + let proto: SigningInput = try_or_else!(tw_proto::deserialize(&data), CByteArray::null); + let signing = try_or_else!(taproot_build_and_sign_transaction(proto), CByteArray::null); + + let serialized = tw_proto::serialize(&signing).expect("failed to serialize signed transaction"); + + CByteArray::from(serialized) +} + +/// Note: many of the fields used in the `SigningInput` are currently unused. We +/// can later easily replicate the funcationlity and behavior of the C++ +/// implemenation. +/// +/// Additionally, the `SigningInput` supports two ways of operating (which +/// should probably be separated anyway): one way where the `TransactionPlan` is +/// skipped (and hence automatically constructed) and the other way where the +/// `TransactionPlan` is created manually. As of now, it's expected that the +/// `TransactionPlan` is created manually, meaning that the caller must careful +/// construct the outputs, which must include the return/change transaction and +/// how much goes to the miner as fee ( minus +/// ). +pub(crate) fn taproot_build_and_sign_transaction(proto: SigningInput) -> Result { + let privkey = proto.private_key.get(0).ok_or(Error::Todo)?; + + // Prepare keypair and derive corresponding public key. + let keypair = KeyPair::from_seckey_slice(&secp256k1::Secp256k1::new(), privkey.as_ref()) + .map_err(|_| crate::Error::Todo)?; + + let my_pubkey = Recipient::::from(keypair); + + let mut builder = TransactionBuilder::new(); + + // Process inputs. + for input in proto.utxo { + let my_pubkey = my_pubkey.clone(); + + let out_point = input.out_point.ok_or(Error::Todo)?; + let txid = Txid::from_slice(&out_point.hash).map_err(|_| crate::Error::Todo)?; + let vout = out_point.index; + let satoshis = input.amount as u64; + + let script_buf = ScriptBuf::from_bytes(input.script.to_vec()); + + let tx: TxInput = match input.variant { + TrVariant::P2PKH => { + TxInputP2PKH::new_with_script(txid, vout, my_pubkey.into(), satoshis, script_buf) + .into() + }, + TrVariant::P2WPKH => TxInputP2WPKH::new_with_script( + txid, + vout, + my_pubkey.try_into()?, + satoshis, + script_buf, + ) + .into(), + TrVariant::P2TRKEYPATH => TxInputP2TRKeyPath::new_with_script( + txid, + vout, + my_pubkey.into(), + satoshis, + script_buf, + ) + .into(), + TrVariant::BRC20TRANSFER => { + // We construct the merkle root for the given spending script. + let spending_script = ScriptBuf::from_bytes(input.spendingScript.to_vec()); + let merkle_root = TapNodeHash::from_script( + spending_script.as_script(), + bitcoin::taproot::LeafVersion::TapScript, + ); + + // Convert to tapscript recipient with the given merkle root. + let recipient = + Recipient::::from_pubkey_recipient(my_pubkey, merkle_root); + + // Derive the spending information for the taproot recipient. + let spend_info = TaprootSpendInfo::from_node_info( + &secp256k1::Secp256k1::new(), + recipient.untweaked_pubkey(), + NodeInfo::new_leaf_with_ver( + spending_script.clone(), + bitcoin::taproot::LeafVersion::TapScript, + ), + ); + + TxInputP2TRScriptPath::new_with_script( + txid, + vout, + recipient, + satoshis, + script_buf, + spending_script, + spend_info, + ) + .into() + }, + }; + + builder = builder.add_input(tx); + } + + // Process outputs. + for output in proto.plan.ok_or(Error::Todo)?.utxos { + let script_buf = ScriptBuf::from_bytes(output.script.to_vec()); + let satoshis = output.amount as u64; + + #[rustfmt::skip] + let tx: TxOutput = match output.variant { + TrVariant::P2PKH => { + TxOutputP2PKH::new_with_script(satoshis, script_buf).into() + }, + TrVariant::P2WPKH => { + TxOutputP2WPKH::new_with_script(satoshis, script_buf).into() + }, + TrVariant::P2TRKEYPATH => { + TxOutputP2TRKeyPath::new_with_script(satoshis, script_buf).into() + }, + TrVariant::BRC20TRANSFER => { + TXOutputP2TRScriptPath::new_with_script(satoshis, script_buf).into() + }, + }; + + builder = builder.add_output(tx); + } + + // Copy those values before `builder` gets consumed. + let version = builder.version; + let lock_time = builder.lock_time.to_consensus_u32(); + + // Sign transaction and create protobuf structures. + let tx = builder.sign_inputs(keypair)?; + + // Create Protobuf structures of inputs. + let mut proto_inputs = vec![]; + for input in &tx.inner.input { + let txid: Vec = input + .previous_output + .txid + .as_byte_array() + .iter() + .cloned() + .rev() + .collect(); + + proto_inputs.push(TransactionInput { + previousOutput: Some(OutPoint { + hash: Cow::from(txid), + index: input.previous_output.vout, + sequence: input.sequence.to_consensus_u32(), + // Unused. + tree: 0, + }), + sequence: input.sequence.to_consensus_u32(), + script: { + // If `scriptSig` is empty, then the Witness is being used. + if input.script_sig.is_empty() { + // TODO: `to_vec` returns a `Vec>` representing + // individual items. Is it appropriate to simply merge + // everything here? + let witness: Vec = input.witness.to_vec().into_iter().flatten().collect(); + Cow::from(witness) + } else { + Cow::from(input.script_sig.to_bytes()) + } + }, + }); + } + + // Create Protobuf structures of outputs. + let mut proto_outputs = vec![]; + for output in &tx.inner.output { + proto_outputs.push(TransactionOutput { + value: output.value as i64, + script: Cow::from(output.script_pubkey.to_bytes()), + spendingScript: Cow::default(), + }) + } + + // Create Protobuf structure of the full transaction. + let mut signing = SigningOutput { + transaction: Some(Transaction { + version, + lockTime: lock_time, + inputs: proto_inputs, + outputs: proto_outputs, + }), + encoded: Cow::default(), + transaction_id: Cow::from(tx.inner.txid().to_string()), + error: tw_proto::Common::Proto::SigningError::OK, + error_message: Cow::default(), + }; + + // Sign transaction and update Protobuf structure. + let signed = tx.serialize()?; + signing.encoded = Cow::from(signed); + + Ok(signing) +} + +#[repr(C)] +pub enum CTaprootError { + Ok = 0, + InvalidSlice = 1, + InvalidPubkey = 2, + InvalidSegwitPukey = 3, +} + +impl From for ErrorCode { + fn from(error: CTaprootError) -> Self { + error as ErrorCode + } +} diff --git a/rust/tw_bitcoin/src/ffi/scripts.rs b/rust/tw_bitcoin/src/ffi/scripts.rs new file mode 100644 index 00000000000..1444ae7bd9a --- /dev/null +++ b/rust/tw_bitcoin/src/ffi/scripts.rs @@ -0,0 +1,144 @@ +use crate::brc20::{BRC20TransferInscription, Ticker}; +use crate::{ + Recipient, TXOutputP2TRScriptPath, TxOutputP2PKH, TxOutputP2TRKeyPath, TxOutputP2WPKH, +}; +use bitcoin::{PublicKey, WPubkeyHash}; +use std::borrow::Cow; +use tw_memory::ffi::c_byte_array::CByteArray; +use tw_memory::ffi::c_byte_array_ref::CByteArrayRef; +use tw_misc::try_or_else; +use tw_proto::Bitcoin::Proto::TransactionOutput; + +#[no_mangle] +// Builds the P2PKH scriptPubkey. +pub unsafe extern "C" fn tw_build_p2pkh_script( + satoshis: i64, + pubkey: *const u8, + pubkey_len: usize, +) -> CByteArray { + // Convert Recipient + let slice = try_or_else!( + CByteArrayRef::new(pubkey, pubkey_len).as_slice(), + CByteArray::null + ); + let recipient = try_or_else!(Recipient::::from_slice(slice), CByteArray::null); + + let tx_out = TxOutputP2PKH::new(satoshis as u64, recipient); + + // Prepare and serialize protobuf structure. + let proto = TransactionOutput { + value: satoshis, + script: Cow::from(tx_out.script_pubkey.as_bytes()), + spendingScript: Cow::default(), + }; + + let serialized = tw_proto::serialize(&proto).expect("failed to serialized transaction output"); + + CByteArray::from(serialized) +} + +#[no_mangle] +// Builds the P2WPKH scriptPubkey. +pub unsafe extern "C" fn tw_build_p2wpkh_script( + satoshis: i64, + pubkey: *const u8, + pubkey_len: usize, +) -> CByteArray { + // Convert Recipient + let slice = try_or_else!( + CByteArrayRef::new(pubkey, pubkey_len).as_slice(), + CByteArray::null + ); + let recipient = try_or_else!( + Recipient::::from_slice(slice), + CByteArray::null + ); + + let tx_out = TxOutputP2WPKH::new(satoshis as u64, recipient); + + // Prepare and serialize protobuf structure. + let proto = TransactionOutput { + value: satoshis, + script: Cow::from(tx_out.script_pubkey.as_bytes()), + spendingScript: Cow::default(), + }; + + let serialized = tw_proto::serialize(&proto).expect("failed to serialized transaction output"); + + CByteArray::from(serialized) +} + +#[no_mangle] +// Builds the P2TR key-path scriptPubkey. +pub unsafe extern "C" fn tw_build_p2tr_key_path_script( + satoshis: i64, + pubkey: *const u8, + pubkey_len: usize, +) -> CByteArray { + // Convert Recipient + let slice = try_or_else!( + CByteArrayRef::new(pubkey, pubkey_len).as_slice(), + CByteArray::null + ); + let recipient = try_or_else!(Recipient::::from_slice(slice), CByteArray::null); + + let tx_out = TxOutputP2TRKeyPath::new(satoshis as u64, recipient.into()); + + // Prepare and serialize protobuf structure. + let proto = TransactionOutput { + value: satoshis, + script: Cow::from(tx_out.script_pubkey.as_bytes()), + spendingScript: Cow::default(), + }; + + let serialized = tw_proto::serialize(&proto).expect("failed to serialized transaction output"); + + CByteArray::from(serialized) +} + +#[no_mangle] +// Builds the Ordinals inscripton for BRC20 transfer. +pub unsafe extern "C" fn tw_build_brc20_inscribe_transfer( + // The 4-byte ticker. + ticker: *const u8, + amount: u64, + satoshis: i64, + pubkey: *const u8, + pubkey_len: usize, +) -> CByteArray { + // Convert ticket. + let slice = try_or_else!(CByteArrayRef::new(ticker, 4).as_slice(), CByteArray::null); + + if slice.len() != 4 { + return CByteArray::null(); + } + + let string = try_or_else!(String::from_utf8(slice.to_vec()), CByteArray::null); + let ticker = Ticker::new(string).expect("ticker must be 4 bytes"); + + // Convert Recipient + let slice = try_or_else!( + CByteArrayRef::new(pubkey, pubkey_len).as_slice(), + CByteArray::null + ); + + let recipient = try_or_else!(Recipient::::from_slice(slice), CByteArray::null); + + // Build transfer inscription. + let transfer = BRC20TransferInscription::new(recipient, ticker, amount) + .expect("transfer inscription implemented wrongly"); + + let tx_out = TXOutputP2TRScriptPath::new(satoshis as u64, transfer.inscription().recipient()); + let spending_script = transfer.inscription().taproot_program(); + + // Prepare and serialize protobuf structure. + let proto = TransactionOutput { + value: satoshis, + script: Cow::from(tx_out.script_pubkey.as_bytes()), + spendingScript: Cow::from(spending_script.as_bytes()), + }; + + let serialized = tw_proto::serialize(&proto).expect("failed to serialized transaction output"); + + CByteArray::from(serialized) +} diff --git a/rust/tw_bitcoin/src/input/mod.rs b/rust/tw_bitcoin/src/input/mod.rs new file mode 100644 index 00000000000..ebe68369c2d --- /dev/null +++ b/rust/tw_bitcoin/src/input/mod.rs @@ -0,0 +1,92 @@ +use bitcoin::{OutPoint, ScriptBuf, Sequence, TxIn, TxOut, Witness}; + +mod p2pkh; +mod p2tr_key_path; +mod p2tr_script_path; +mod p2wpkh; + +pub use p2pkh::*; +pub use p2tr_key_path::*; +pub use p2tr_script_path::*; +pub use p2wpkh::*; + +#[derive(Debug, Clone)] +pub struct InputContext { + pub previous_output: OutPoint, + pub value: u64, + // The condition for claiming the output. + pub script_pubkey: ScriptBuf, + pub sequence: Sequence, + // Witness data for Segwit/Taproot transactions. +} + +impl InputContext { + pub fn new(utxo: TxOut, point: OutPoint) -> Self { + InputContext { + previous_output: point, + value: utxo.value, + script_pubkey: utxo.script_pubkey, + // Default value of `0xFFFFFFFF = 4294967295`. + sequence: Sequence::default(), + } + } +} + +#[derive(Debug, Clone)] +pub enum TxInput { + P2PKH(TxInputP2PKH), + P2WPKH(TxInputP2WPKH), + P2TRKeyPath(TxInputP2TRKeyPath), + P2TRScriptPath(TxInputP2TRScriptPath), +} + +impl From for TxInput { + fn from(input: TxInputP2PKH) -> Self { + TxInput::P2PKH(input) + } +} + +impl From for TxInput { + fn from(input: TxInputP2WPKH) -> Self { + TxInput::P2WPKH(input) + } +} + +impl From for TxInput { + fn from(input: TxInputP2TRKeyPath) -> Self { + TxInput::P2TRKeyPath(input) + } +} + +impl From for TxInput { + fn from(input: TxInputP2TRScriptPath) -> Self { + TxInput::P2TRScriptPath(input) + } +} + +impl From for TxIn { + fn from(input: TxInput) -> Self { + let ctx = input.ctx(); + + TxIn { + previous_output: ctx.previous_output, + script_sig: ScriptBuf::new(), + sequence: ctx.sequence, + witness: Witness::default(), + } + } +} + +impl TxInput { + pub fn ctx(&self) -> &InputContext { + match self { + TxInput::P2PKH(t) => t.ctx(), + TxInput::P2WPKH(t) => t.ctx(), + TxInput::P2TRKeyPath(t) => t.ctx(), + TxInput::P2TRScriptPath(t) => t.ctx(), + } + } + pub fn satoshis(&self) -> u64 { + self.ctx().value + } +} diff --git a/rust/tw_bitcoin/src/input/p2pkh.rs b/rust/tw_bitcoin/src/input/p2pkh.rs new file mode 100644 index 00000000000..66bfe3de73b --- /dev/null +++ b/rust/tw_bitcoin/src/input/p2pkh.rs @@ -0,0 +1,86 @@ +use crate::{Error, InputContext, Recipient, Result}; +use bitcoin::{OutPoint, PubkeyHash, ScriptBuf, Sequence, Txid}; + +#[derive(Debug, Clone)] +pub struct TxInputP2PKH { + ctx: InputContext, + recipient: Recipient, +} + +impl TxInputP2PKH { + pub fn new(txid: Txid, vout: u32, recipient: Recipient, satoshis: u64) -> Self { + let script = ScriptBuf::new_p2pkh(recipient.pubkey_hash()); + Self::new_with_script(txid, vout, recipient, satoshis, script) + } + pub fn new_with_script( + txid: Txid, + vout: u32, + recipient: Recipient, + satoshis: u64, + script: ScriptBuf, + ) -> Self { + TxInputP2PKH { + ctx: InputContext { + previous_output: OutPoint { txid, vout }, + value: satoshis, + script_pubkey: script, + sequence: Sequence::default(), + }, + recipient, + } + } + pub fn builder() -> TxInputP2PKHBuilder { + TxInputP2PKHBuilder::new() + } + /// Read-only exposure to the context. + pub fn ctx(&self) -> &InputContext { + &self.ctx + } + /// Read-only exposure to the recipient. + pub fn recipient(&self) -> &Recipient { + &self.recipient + } +} + +#[derive(Debug, Clone, Default)] +pub struct TxInputP2PKHBuilder { + txid: Option, + vout: Option, + recipient: Option>, + satoshis: Option, +} + +impl TxInputP2PKHBuilder { + pub fn new() -> TxInputP2PKHBuilder { + TxInputP2PKHBuilder { + txid: None, + vout: None, + recipient: None, + satoshis: None, + } + } + pub fn txid(mut self, txid: Txid) -> TxInputP2PKHBuilder { + self.txid = Some(txid); + self + } + pub fn vout(mut self, vout: u32) -> TxInputP2PKHBuilder { + self.vout = Some(vout); + self + } + pub fn recipient(mut self, recipient: impl Into>) -> TxInputP2PKHBuilder { + self.recipient = Some(recipient.into()); + self + } + pub fn satoshis(mut self, satoshis: u64) -> TxInputP2PKHBuilder { + self.satoshis = Some(satoshis); + self + } + pub fn build(self) -> Result { + Ok(TxInputP2PKH::new( + self.txid.ok_or(Error::Todo)?, + self.vout.ok_or(Error::Todo)?, + self.recipient.ok_or(Error::Todo)?, + self.satoshis.ok_or(Error::Todo)?, + )) + } +} diff --git a/rust/tw_bitcoin/src/input/p2tr_key_path.rs b/rust/tw_bitcoin/src/input/p2tr_key_path.rs new file mode 100644 index 00000000000..29bf0ff07f5 --- /dev/null +++ b/rust/tw_bitcoin/src/input/p2tr_key_path.rs @@ -0,0 +1,90 @@ +use crate::{Error, InputContext, Recipient, Result}; +use bitcoin::key::TweakedPublicKey; +use bitcoin::{OutPoint, ScriptBuf, Sequence, Txid}; + +#[derive(Debug, Clone)] +pub struct TxInputP2TRKeyPath { + ctx: InputContext, + recipient: Recipient, +} + +impl TxInputP2TRKeyPath { + pub fn new( + txid: Txid, + vout: u32, + recipient: Recipient, + satoshis: u64, + ) -> Self { + let script = ScriptBuf::new_v1_p2tr_tweaked(recipient.tweaked_pubkey()); + Self::new_with_script(txid, vout, recipient, satoshis, script) + } + pub fn new_with_script( + txid: Txid, + vout: u32, + recipient: Recipient, + satoshis: u64, + script: ScriptBuf, + ) -> Self { + TxInputP2TRKeyPath { + ctx: InputContext { + previous_output: OutPoint { txid, vout }, + value: satoshis, + script_pubkey: script, + sequence: Sequence::default(), + }, + recipient, + } + } + pub fn builder() -> TxInputP2TRKeyPathBuilder { + TxInputP2TRKeyPathBuilder::new() + } + /// Read-only exposure to the context. + pub fn ctx(&self) -> &InputContext { + &self.ctx + } + /// Read-only exposure to the recipient. + pub fn recipient(&self) -> &Recipient { + &self.recipient + } +} + +#[derive(Debug, Clone, Default)] +pub struct TxInputP2TRKeyPathBuilder { + txid: Option, + vout: Option, + recipient: Option>, + satoshis: Option, +} + +impl TxInputP2TRKeyPathBuilder { + pub fn new() -> TxInputP2TRKeyPathBuilder { + Self::default() + } + pub fn txid(mut self, txid: Txid) -> TxInputP2TRKeyPathBuilder { + self.txid = Some(txid); + self + } + pub fn vout(mut self, vout: u32) -> TxInputP2TRKeyPathBuilder { + self.vout = Some(vout); + self + } + pub fn recipient( + mut self, + recipient: impl Into>, + ) -> TxInputP2TRKeyPathBuilder { + self.recipient = Some(recipient.into()); + self + } + pub fn satoshis(mut self, satoshis: u64) -> TxInputP2TRKeyPathBuilder { + self.satoshis = Some(satoshis); + self + } + pub fn build(self) -> Result { + Ok(TxInputP2TRKeyPath::new( + self.txid.ok_or(Error::Todo)?, + self.vout.ok_or(Error::Todo)?, + self.recipient.ok_or(Error::Todo)?, + self.satoshis.ok_or(Error::Todo)?, + )) + } +} diff --git a/rust/tw_bitcoin/src/input/p2tr_script_path.rs b/rust/tw_bitcoin/src/input/p2tr_script_path.rs new file mode 100644 index 00000000000..af02c107533 --- /dev/null +++ b/rust/tw_bitcoin/src/input/p2tr_script_path.rs @@ -0,0 +1,127 @@ +use crate::{Error, InputContext, Recipient, Result, TaprootScript}; +use bitcoin::script::ScriptBuf; +use bitcoin::taproot::TaprootSpendInfo; +use bitcoin::{OutPoint, Sequence, Txid}; + +#[derive(Debug, Clone)] +pub struct TxInputP2TRScriptPath { + ctx: InputContext, + recipient: Recipient, + witness: ScriptBuf, + spend_info: TaprootSpendInfo, +} + +impl TxInputP2TRScriptPath { + pub fn new( + txid: Txid, + vout: u32, + recipient: Recipient, + satoshis: u64, + witness: ScriptBuf, + spend_info: TaprootSpendInfo, + ) -> Self { + let script = ScriptBuf::new_v1_p2tr( + &secp256k1::Secp256k1::new(), + recipient.untweaked_pubkey(), + Some(recipient.merkle_root()), + ); + + Self::new_with_script(txid, vout, recipient, satoshis, script, witness, spend_info) + } + pub fn new_with_script( + txid: Txid, + vout: u32, + recipient: Recipient, + satoshis: u64, + script: ScriptBuf, + witness: ScriptBuf, + spend_info: TaprootSpendInfo, + ) -> Self { + TxInputP2TRScriptPath { + ctx: InputContext { + previous_output: OutPoint { txid, vout }, + value: satoshis, + script_pubkey: script, + sequence: Sequence::default(), + }, + recipient, + witness, + spend_info, + } + } + pub fn builder() -> TxInputP2TRScriptPathBuilder { + TxInputP2TRScriptPathBuilder::new() + } + pub fn ctx(&self) -> &InputContext { + &self.ctx + } + pub fn recipient(&self) -> &Recipient { + &self.recipient + } + pub fn witness(&self) -> &ScriptBuf { + &self.witness + } + pub fn spend_info(&self) -> &TaprootSpendInfo { + &self.spend_info + } +} + +#[derive(Debug, Clone, Default)] +pub struct TxInputP2TRScriptPathBuilder { + txid: Option, + vout: Option, + recipient: Option>, + satoshis: Option, + script: Option, + spend_info: Option, +} + +impl TxInputP2TRScriptPathBuilder { + pub fn new() -> TxInputP2TRScriptPathBuilder { + TxInputP2TRScriptPathBuilder { + txid: None, + vout: None, + recipient: None, + satoshis: None, + script: None, + spend_info: None, + } + } + pub fn txid(mut self, txid: Txid) -> TxInputP2TRScriptPathBuilder { + self.txid = Some(txid); + self + } + pub fn vout(mut self, vout: u32) -> TxInputP2TRScriptPathBuilder { + self.vout = Some(vout); + self + } + pub fn recipient( + mut self, + recipient: Recipient, + ) -> TxInputP2TRScriptPathBuilder { + self.recipient = Some(recipient); + self + } + pub fn satoshis(mut self, satoshis: u64) -> TxInputP2TRScriptPathBuilder { + self.satoshis = Some(satoshis); + self + } + pub fn script(mut self, script: ScriptBuf) -> TxInputP2TRScriptPathBuilder { + self.script = Some(script); + self + } + pub fn spend_info(mut self, spend_info: TaprootSpendInfo) -> TxInputP2TRScriptPathBuilder { + self.spend_info = Some(spend_info); + self + } + pub fn build(self) -> Result { + Ok(TxInputP2TRScriptPath::new( + self.txid.ok_or(Error::Todo)?, + self.vout.ok_or(Error::Todo)?, + self.recipient.ok_or(Error::Todo)?, + self.satoshis.ok_or(Error::Todo)?, + self.script.ok_or(Error::Todo)?, + self.spend_info.ok_or(Error::Todo)?, + )) + } +} diff --git a/rust/tw_bitcoin/src/input/p2wpkh.rs b/rust/tw_bitcoin/src/input/p2wpkh.rs new file mode 100644 index 00000000000..4270fe73688 --- /dev/null +++ b/rust/tw_bitcoin/src/input/p2wpkh.rs @@ -0,0 +1,81 @@ +use crate::{Error, InputContext, Recipient, Result}; +use bitcoin::{OutPoint, ScriptBuf, Sequence, Txid, WPubkeyHash}; + +#[derive(Debug, Clone)] +pub struct TxInputP2WPKH { + ctx: InputContext, + recipient: Recipient, +} + +impl TxInputP2WPKH { + pub fn new(txid: Txid, vout: u32, recipient: Recipient, satoshis: u64) -> Self { + let script = ScriptBuf::new_v0_p2wpkh(recipient.wpubkey_hash()); + Self::new_with_script(txid, vout, recipient, satoshis, script) + } + pub fn new_with_script( + txid: Txid, + vout: u32, + recipient: Recipient, + satoshis: u64, + script: ScriptBuf, + ) -> Self { + TxInputP2WPKH { + ctx: InputContext { + previous_output: OutPoint { txid, vout }, + value: satoshis, + script_pubkey: script, + sequence: Sequence::default(), + }, + recipient, + } + } + pub fn builder() -> TxInputP2WPKHBuilder { + TxInputP2WPKHBuilder::new() + } + /// Read-only exposure to the context. + pub fn ctx(&self) -> &InputContext { + &self.ctx + } + /// Read-only exposure to the recipient. + pub fn recipient(&self) -> &Recipient { + &self.recipient + } +} + +#[derive(Debug, Clone, Default)] +pub struct TxInputP2WPKHBuilder { + txid: Option, + vout: Option, + recipient: Option>, + satoshis: Option, +} + +impl TxInputP2WPKHBuilder { + pub fn new() -> TxInputP2WPKHBuilder { + Self::default() + } + pub fn txid(mut self, txid: Txid) -> TxInputP2WPKHBuilder { + self.txid = Some(txid); + self + } + pub fn vout(mut self, vout: u32) -> TxInputP2WPKHBuilder { + self.vout = Some(vout); + self + } + pub fn recipient(mut self, recipient: Recipient) -> TxInputP2WPKHBuilder { + self.recipient = Some(recipient); + self + } + pub fn satoshis(mut self, satoshis: u64) -> TxInputP2WPKHBuilder { + self.satoshis = Some(satoshis); + self + } + pub fn build(self) -> Result { + Ok(TxInputP2WPKH::new( + self.txid.ok_or(Error::Todo)?, + self.vout.ok_or(Error::Todo)?, + self.recipient.ok_or(Error::Todo)?, + self.satoshis.ok_or(Error::Todo)?, + )) + } +} diff --git a/rust/tw_bitcoin/src/lib.rs b/rust/tw_bitcoin/src/lib.rs new file mode 100644 index 00000000000..b31eedd7cd2 --- /dev/null +++ b/rust/tw_bitcoin/src/lib.rs @@ -0,0 +1,27 @@ +extern crate serde; + +pub mod brc20; +pub mod claim; +pub mod ffi; +pub mod input; +pub mod ordinals; +pub mod output; +pub mod recipient; +#[cfg(test)] +mod tests; +pub mod transaction; +pub mod utils; + +// Reexports +pub use input::*; +pub use output::*; +pub use recipient::Recipient; +pub use transaction::*; +pub use utils::*; + +pub type Result = std::result::Result; + +#[derive(Debug, Clone)] +pub enum Error { + Todo, +} diff --git a/rust/tw_bitcoin/src/ordinals.rs b/rust/tw_bitcoin/src/ordinals.rs new file mode 100644 index 00000000000..aaeca5530f9 --- /dev/null +++ b/rust/tw_bitcoin/src/ordinals.rs @@ -0,0 +1,108 @@ +use crate::{Error, Recipient, Result, TaprootProgram, TaprootScript}; +use bitcoin::script::{PushBytesBuf, ScriptBuf}; +use bitcoin::secp256k1::XOnlyPublicKey; +use bitcoin::taproot::{TaprootBuilder, TaprootSpendInfo}; +use bitcoin::{PublicKey, Script}; + +#[derive(Debug, Clone)] +pub struct OrdinalsInscription { + envelope: TaprootProgram, + recipient: Recipient, +} + +impl OrdinalsInscription { + /// Creates a new Ordinals Inscription ("commit stage"). + pub fn new( + mime: &[u8], + data: &[u8], + recipient: Recipient, + ) -> Result { + // Create the envelope, containing the inscription content. + let envelope = create_envelope(mime, data, recipient.public_key())?; + + // Compute the merkle root of the inscription. + let merkle_root = envelope + .spend_info + .merkle_root() + .expect("Ordinals envelope not constructed correctly"); + + Ok(OrdinalsInscription { + envelope, + recipient: Recipient::::from_pubkey_recipient(recipient, merkle_root), + }) + } + pub fn taproot_program(&self) -> &Script { + self.envelope.script.as_script() + } + pub fn spend_info(&self) -> &TaprootSpendInfo { + &self.envelope.spend_info + } + pub fn recipient(&self) -> &Recipient { + &self.recipient + } +} + +/// Creates an [Ordinals Inscription](https://docs.ordinals.com/inscriptions.html). +/// This function is used for two purposes: +/// +/// 1. It creates the spending condition for the given `internal_key`. This +/// associates the public key of the recipient with the Merkle root of the +/// Inscription on-chain, but it does not actually reveal the script to +/// anyone ("commit stage"). +/// 2. The same function can then be used by the spender/claimer to actually +/// transfer the Inscripion by sending a transaction with the Inscription +/// script in the Witness ("reveal stage"). +/// +/// Do note that the `internal_key` can be different for each stage, but it +/// could also be the same entity. Stage one, the `internal_key` is the +/// recipient. Stage two, the `internal_key` is the claimer of the transaction +/// (where the Inscription script is available in the Witness). +fn create_envelope(mime: &[u8], data: &[u8], internal_key: PublicKey) -> Result { + use bitcoin::opcodes::all::*; + use bitcoin::opcodes::*; + + // Create MIME buffer. + let mut mime_buf = PushBytesBuf::new(); + mime_buf.extend_from_slice(mime).map_err(|_| Error::Todo)?; + + // Create data buffer. + let mut data_buf = PushBytesBuf::new(); + data_buf.extend_from_slice(data).map_err(|_| Error::Todo)?; + + // Create an Ordinals Inscription. + let script = ScriptBuf::builder() + .push_opcode(OP_FALSE) + .push_opcode(OP_IF) + .push_slice(b"ord") + // Separator. + .push_opcode(OP_PUSHBYTES_1) + // This seems to be necessary for now and indicates the size of the + // length indicator. The method `push_slice` prefixes the data with the + // length, but does not specify how many bytes that prefix requires. + // + // TODO: Look up if this could be somehow improved or if the `bitcoin` + // crate can/should handle that. + .push_opcode(OP_PUSHBYTES_1) + .push_slice(mime_buf.as_push_bytes()) + // Separator. + .push_opcode(OP_PUSHBYTES_0) + // The payload itself. + .push_slice(data_buf) + .push_opcode(OP_ENDIF) + .into_script(); + + // Generate the necessary spending information. As mentioned in the + // documentation of this function at the top, this serves two purposes; + // setting the spending condition and actually claiming the spending + // condition. + let spend_info = TaprootBuilder::new() + .add_leaf(0, script.clone()) + .expect("Ordinals Inscription spending info must always build") + .finalize( + &secp256k1::Secp256k1::new(), + XOnlyPublicKey::from(internal_key.inner), + ) + .expect("Ordinals Inscription spending info must always build"); + + Ok(TaprootProgram { script, spend_info }) +} diff --git a/rust/tw_bitcoin/src/output/mod.rs b/rust/tw_bitcoin/src/output/mod.rs new file mode 100644 index 00000000000..a93ccda8c87 --- /dev/null +++ b/rust/tw_bitcoin/src/output/mod.rs @@ -0,0 +1,75 @@ +mod p2pkh; +mod p2tr_key_path; +mod p2tr_script_path; +mod p2wpkh; + +pub use p2pkh::*; +pub use p2tr_key_path::*; +pub use p2tr_script_path::*; +pub use p2wpkh::*; + +#[derive(Debug, Clone)] +pub enum TxOutput { + P2PKH(TxOutputP2PKH), + P2WPKH(TxOutputP2WPKH), + P2TRKeyPath(TxOutputP2TRKeyPath), + P2TRScriptPath(TXOutputP2TRScriptPath), +} + +impl TxOutput { + pub fn satoshis(&self) -> u64 { + match self { + TxOutput::P2PKH(p) => p.satoshis, + TxOutput::P2WPKH(p) => p.satoshis, + TxOutput::P2TRKeyPath(p) => p.satoshis, + TxOutput::P2TRScriptPath(p) => p.satoshis, + } + } +} + +impl From for TxOutput { + fn from(output: TxOutputP2PKH) -> Self { + TxOutput::P2PKH(output) + } +} + +impl From for TxOutput { + fn from(output: TxOutputP2TRKeyPath) -> Self { + TxOutput::P2TRKeyPath(output) + } +} + +impl From for TxOutput { + fn from(output: TxOutputP2WPKH) -> Self { + TxOutput::P2WPKH(output) + } +} + +impl From for TxOutput { + fn from(output: TXOutputP2TRScriptPath) -> Self { + TxOutput::P2TRScriptPath(output) + } +} + +impl From for bitcoin::TxOut { + fn from(out: TxOutput) -> Self { + match out { + TxOutput::P2PKH(p) => Self { + value: p.satoshis, + script_pubkey: p.script_pubkey, + }, + TxOutput::P2WPKH(p) => Self { + value: p.satoshis, + script_pubkey: p.script_pubkey, + }, + TxOutput::P2TRKeyPath(p) => Self { + value: p.satoshis, + script_pubkey: p.script_pubkey, + }, + TxOutput::P2TRScriptPath(p) => Self { + value: p.satoshis, + script_pubkey: p.script_pubkey, + }, + } + } +} diff --git a/rust/tw_bitcoin/src/output/p2pkh.rs b/rust/tw_bitcoin/src/output/p2pkh.rs new file mode 100644 index 00000000000..168d260339d --- /dev/null +++ b/rust/tw_bitcoin/src/output/p2pkh.rs @@ -0,0 +1,60 @@ +use crate::{Error, Recipient, Result}; +use bitcoin::{PubkeyHash, ScriptBuf}; + +#[derive(Debug, Clone)] +pub struct TxOutputP2PKH { + pub(crate) satoshis: u64, + pub(crate) script_pubkey: ScriptBuf, +} + +impl TxOutputP2PKH { + pub fn new(satoshis: u64, recipient: impl Into>) -> Self { + let recipient: Recipient = recipient.into(); + + TxOutputP2PKH { + satoshis, + script_pubkey: ScriptBuf::new_p2pkh(recipient.pubkey_hash()), + } + } + pub fn new_with_script(satoshis: u64, script_pubkey: ScriptBuf) -> Self { + TxOutputP2PKH { + satoshis, + script_pubkey, + } + } + pub fn builder() -> TxOutputP2PKHBuilder { + TxOutputP2PKHBuilder::new() + } +} + +#[derive(Debug, Clone, Default)] +pub struct TxOutputP2PKHBuilder { + satoshis: Option, + recipient: Option>, +} + +impl TxOutputP2PKHBuilder { + pub fn new() -> TxOutputP2PKHBuilder { + TxOutputP2PKHBuilder { + satoshis: None, + recipient: None, + } + } + pub fn satoshis(mut self, satoshis: u64) -> TxOutputP2PKHBuilder { + self.satoshis = Some(satoshis); + self + } + pub fn recipient( + mut self, + recipient: impl Into>, + ) -> TxOutputP2PKHBuilder { + self.recipient = Some(recipient.into()); + self + } + pub fn build(self) -> Result { + Ok(TxOutputP2PKH::new( + self.satoshis.ok_or(Error::Todo)?, + self.recipient.ok_or(Error::Todo)?, + )) + } +} diff --git a/rust/tw_bitcoin/src/output/p2tr_key_path.rs b/rust/tw_bitcoin/src/output/p2tr_key_path.rs new file mode 100644 index 00000000000..94e166fd4ff --- /dev/null +++ b/rust/tw_bitcoin/src/output/p2tr_key_path.rs @@ -0,0 +1,56 @@ +use crate::{Error, Recipient, Result}; +use bitcoin::key::TweakedPublicKey; +use bitcoin::script::ScriptBuf; + +#[derive(Debug, Clone)] +pub struct TxOutputP2TRKeyPath { + pub(crate) satoshis: u64, + pub(crate) script_pubkey: ScriptBuf, +} + +impl TxOutputP2TRKeyPath { + pub fn new(satoshis: u64, recipient: Recipient) -> Self { + TxOutputP2TRKeyPath { + satoshis, + script_pubkey: ScriptBuf::new_v1_p2tr_tweaked(recipient.tweaked_pubkey()), + } + } + pub fn new_with_script(satoshis: u64, script_pubkey: ScriptBuf) -> Self { + TxOutputP2TRKeyPath { + satoshis, + script_pubkey, + } + } + pub fn builder() -> TxOutputP2TRKeyPathBuilder { + TxOutputP2TRKeyPathBuilder::new() + } +} + +#[derive(Debug, Clone, Default)] +pub struct TxOutputP2TRKeyPathBuilder { + satoshis: Option, + recipient: Option>, +} + +impl TxOutputP2TRKeyPathBuilder { + pub fn new() -> Self { + Self::default() + } + pub fn satoshis(mut self, satoshis: u64) -> TxOutputP2TRKeyPathBuilder { + self.satoshis = Some(satoshis); + self + } + pub fn recipient( + mut self, + recipient: impl Into>, + ) -> TxOutputP2TRKeyPathBuilder { + self.recipient = Some(recipient.into()); + self + } + pub fn build(self) -> Result { + Ok(TxOutputP2TRKeyPath::new( + self.satoshis.ok_or(Error::Todo)?, + self.recipient.ok_or(Error::Todo)?, + )) + } +} diff --git a/rust/tw_bitcoin/src/output/p2tr_script_path.rs b/rust/tw_bitcoin/src/output/p2tr_script_path.rs new file mode 100644 index 00000000000..cf7e4f55a8c --- /dev/null +++ b/rust/tw_bitcoin/src/output/p2tr_script_path.rs @@ -0,0 +1,77 @@ +use crate::{Error, Recipient, Result}; +use bitcoin::key::PublicKey; +use bitcoin::script::ScriptBuf; +use bitcoin::secp256k1; +use bitcoin::taproot::{TapNodeHash, TaprootSpendInfo}; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct TaprootScript { + pub pubkey: PublicKey, + pub merkle_root: TapNodeHash, +} + +#[derive(Debug, Clone)] +pub struct TaprootProgram { + pub script: ScriptBuf, + pub spend_info: TaprootSpendInfo, +} + +#[derive(Debug, Clone)] +pub struct TXOutputP2TRScriptPath { + pub(crate) satoshis: u64, + pub(crate) script_pubkey: ScriptBuf, +} + +impl TXOutputP2TRScriptPath { + pub fn new(satoshis: u64, recipient: &Recipient) -> Self { + let script_pubkey = ScriptBuf::new_v1_p2tr( + &secp256k1::Secp256k1::new(), + recipient.untweaked_pubkey(), + Some(recipient.merkle_root()), + ); + + TXOutputP2TRScriptPath { + satoshis, + script_pubkey, + } + } + pub fn new_with_script(satoshis: u64, script_pubkey: ScriptBuf) -> Self { + TXOutputP2TRScriptPath { + satoshis, + script_pubkey, + } + } + pub fn builder() -> TxOutputP2TRScriptPathBuilder { + TxOutputP2TRScriptPathBuilder::new() + } +} + +#[derive(Debug, Clone, Default)] +pub struct TxOutputP2TRScriptPathBuilder { + satoshis: Option, + recipient: Option>, +} + +impl TxOutputP2TRScriptPathBuilder { + pub fn new() -> Self { + Self::default() + } + pub fn satoshis(mut self, satoshis: u64) -> TxOutputP2TRScriptPathBuilder { + self.satoshis = Some(satoshis); + self + } + pub fn recipient( + mut self, + recipient: Recipient, + ) -> TxOutputP2TRScriptPathBuilder { + self.recipient = Some(recipient); + self + } + pub fn build(self) -> Result { + let recipient = self.recipient.ok_or(Error::Todo)?; + Ok(TXOutputP2TRScriptPath::new( + self.satoshis.ok_or(Error::Todo)?, + &recipient, + )) + } +} diff --git a/rust/tw_bitcoin/src/output/p2wpkh.rs b/rust/tw_bitcoin/src/output/p2wpkh.rs new file mode 100644 index 00000000000..82877517874 --- /dev/null +++ b/rust/tw_bitcoin/src/output/p2wpkh.rs @@ -0,0 +1,52 @@ +use crate::{Error, Recipient, Result}; +use bitcoin::{ScriptBuf, WPubkeyHash}; + +#[derive(Debug, Clone)] +pub struct TxOutputP2WPKH { + pub(crate) satoshis: u64, + pub(crate) script_pubkey: ScriptBuf, +} + +impl TxOutputP2WPKH { + pub fn new(satoshis: u64, recipient: Recipient) -> Self { + TxOutputP2WPKH { + satoshis, + script_pubkey: ScriptBuf::new_v0_p2wpkh(recipient.wpubkey_hash()), + } + } + pub fn new_with_script(satoshis: u64, script_pubkey: ScriptBuf) -> Self { + TxOutputP2WPKH { + satoshis, + script_pubkey, + } + } + pub fn builder() -> TxOutputP2WPKHBuilder { + TxOutputP2WPKHBuilder::new() + } +} + +#[derive(Debug, Clone, Default)] +pub struct TxOutputP2WPKHBuilder { + satoshis: Option, + recipient: Option>, +} + +impl TxOutputP2WPKHBuilder { + pub fn new() -> TxOutputP2WPKHBuilder { + Self::default() + } + pub fn satoshis(mut self, satoshis: u64) -> TxOutputP2WPKHBuilder { + self.satoshis = Some(satoshis); + self + } + pub fn recipient(mut self, recipient: Recipient) -> TxOutputP2WPKHBuilder { + self.recipient = Some(recipient); + self + } + pub fn build(self) -> Result { + Ok(TxOutputP2WPKH::new( + self.satoshis.ok_or(Error::Todo)?, + self.recipient.ok_or(Error::Todo)?, + )) + } +} diff --git a/rust/tw_bitcoin/src/recipient.rs b/rust/tw_bitcoin/src/recipient.rs new file mode 100644 index 00000000000..f284ac84ab3 --- /dev/null +++ b/rust/tw_bitcoin/src/recipient.rs @@ -0,0 +1,260 @@ +use std::str::FromStr; + +use crate::output::TaprootScript; +use crate::{tweak_pubkey, Error, Result}; +use bitcoin::key::{KeyPair, PublicKey, TweakedPublicKey, UntweakedPublicKey}; +use bitcoin::taproot::TapNodeHash; +use bitcoin::{ + secp256k1::{self, XOnlyPublicKey}, + Address, Network, PubkeyHash, WPubkeyHash, +}; + +/// This type is used to specify the recipient of a Bitcoin transaction, +/// depending on the required information that's required. For example, P2PKH +/// only requires the public key hash (`Recipient`), while P2TR +/// key-path requires the actual (tweaked) public key (`Recipient`). +/// +/// The recipient can easily downgrade, such as +/// ```rust,ignore +/// let pubkey = Recipient::::from_keypair(keypair); +/// let hash: Recipient = pubkey.into(); +/// ``` +/// +/// But it cannot, for example, derive a public key from just the hash. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Recipient { + inner: T, +} + +impl Recipient { + pub fn from_keypair(keypair: &KeyPair) -> Self { + Recipient { + inner: PublicKey::new(keypair.public_key()), + } + } + pub fn public_key(&self) -> PublicKey { + self.inner + } + pub fn pubkey_hash(&self) -> PubkeyHash { + PubkeyHash::from(self.inner) + } + pub fn wpubkey_hash(&self) -> Result { + self.inner.wpubkey_hash().ok_or(Error::Todo) + } + pub fn tweaked_pubkey(&self) -> TweakedPublicKey { + tweak_pubkey(self.inner) + } + pub fn untweaked_pubkey(&self) -> UntweakedPublicKey { + XOnlyPublicKey::from(self.inner.inner) + } + pub fn legacy_address(&self, network: Network) -> Address { + Address::p2pkh(&self.inner, network) + } + pub fn segwit_address(&self, network: Network) -> Result
{ + Address::p2wpkh(&self.inner, network).map_err(|_| Error::Todo) + } + pub fn taproot_address(&self, network: Network) -> Address { + let untweaked = UntweakedPublicKey::from(self.inner.inner); + Address::p2tr(&secp256k1::Secp256k1::new(), untweaked, None, network) + } + pub fn legacy_address_string(&self, network: Network) -> String { + self.legacy_address(network).to_string() + } + pub fn segwit_address_string(&self, network: Network) -> Result { + self.segwit_address(network).map(|addr| addr.to_string()) + } + pub fn taproot_address_string(&self, network: Network) -> String { + self.taproot_address(network).to_string() + } +} + +impl Recipient { + pub fn pubkey_hash(&self) -> &PubkeyHash { + &self.inner + } +} + +impl Recipient { + pub fn from_slice(slice: &[u8]) -> Result { + Ok(Recipient { + inner: PublicKey::from_slice(slice) + .map_err(|_| Error::Todo)? + .wpubkey_hash() + .ok_or(Error::Todo)?, + }) + } + pub fn wpubkey_hash(&self) -> &WPubkeyHash { + &self.inner + } +} + +impl Recipient { + pub fn from_slice(slice: &[u8]) -> Result { + Ok(Recipient { + inner: PublicKey::from_slice(slice).map_err(|_| Error::Todo)?, + }) + } +} + +impl FromStr for Recipient { + type Err = Error; + + fn from_str(string: &str) -> Result { + Self::from_slice(string.as_bytes()) + } +} + +impl From for Recipient { + fn from(inner: PublicKey) -> Self { + Recipient { inner } + } +} + +impl From for Recipient { + fn from(keypair: KeyPair) -> Self { + Self::from(&keypair) + } +} + +impl From<&KeyPair> for Recipient { + fn from(keypair: &KeyPair) -> Self { + Recipient { + inner: PublicKey::new(keypair.public_key()), + } + } +} + +impl From for Recipient { + fn from(pubkey: PublicKey) -> Self { + let tweaked = tweak_pubkey(pubkey); + Recipient { inner: tweaked } + } +} + +impl From> for Recipient { + fn from(recipient: Recipient) -> Self { + Recipient { + inner: Self::from(recipient.inner).inner, + } + } +} + +impl From for Recipient { + fn from(keypair: KeyPair) -> Self { + Self::from(&keypair) + } +} + +impl From<&KeyPair> for Recipient { + fn from(keypair: &KeyPair) -> Self { + let pk = Recipient::::from(keypair); + let tweaked = Recipient::::from(pk); + + Recipient { + inner: tweaked.inner, + } + } +} + +impl TryFrom for Recipient { + type Error = Error; + + fn try_from(pubkey: PublicKey) -> Result { + Ok(Recipient { + inner: pubkey.wpubkey_hash().unwrap(), + }) + } +} + +impl TryFrom> for Recipient { + type Error = Error; + + fn try_from(recipient: Recipient) -> Result { + Ok(Recipient { + inner: Self::try_from(recipient.inner)?.inner, + }) + } +} + +impl TryFrom<&KeyPair> for Recipient { + type Error = Error; + + fn try_from(keypair: &KeyPair) -> Result { + let pubkey = Recipient::::from(keypair); + Self::try_from(pubkey.inner) + } +} + +impl TryFrom for Recipient { + type Error = Error; + + fn try_from(keypair: KeyPair) -> Result { + Self::try_from(&keypair) + } +} + +impl From for Recipient { + fn from(pubkey: PublicKey) -> Self { + Recipient { + inner: pubkey.into(), + } + } +} + +impl From> for Recipient { + fn from(recipient: Recipient) -> Self { + Recipient { + inner: Self::from(recipient.inner).inner, + } + } +} + +impl From for Recipient { + fn from(keypair: KeyPair) -> Self { + Self::from(&keypair) + } +} + +impl From<&KeyPair> for Recipient { + fn from(keypair: &KeyPair) -> Self { + let pk = Recipient::::from(keypair); + + Recipient { + inner: pk.inner.into(), + } + } +} + +impl Recipient { + pub fn tweaked_pubkey(&self) -> TweakedPublicKey { + self.inner + } +} + +impl Recipient { + pub fn from_keypair(keypair: &KeyPair, merkle_root: TapNodeHash) -> Self { + Recipient { + inner: TaprootScript { + pubkey: PublicKey::new(keypair.public_key()), + merkle_root, + }, + } + } + pub fn from_pubkey_recipient( + recipient: Recipient, + merkle_root: TapNodeHash, + ) -> Self { + Recipient { + inner: TaprootScript { + pubkey: recipient.inner, + merkle_root, + }, + } + } + pub fn untweaked_pubkey(&self) -> UntweakedPublicKey { + XOnlyPublicKey::from(self.inner.pubkey.inner) + } + pub fn merkle_root(&self) -> TapNodeHash { + self.inner.merkle_root + } +} diff --git a/rust/tw_bitcoin/src/tests/address.rs b/rust/tw_bitcoin/src/tests/address.rs new file mode 100644 index 00000000000..032771bb1b7 --- /dev/null +++ b/rust/tw_bitcoin/src/tests/address.rs @@ -0,0 +1,24 @@ +use crate::{keypair_from_wif, Recipient}; +use bitcoin::{Network, PublicKey}; + +// This private key was used in a Bitcoin regtest environment. +pub const ALICE_WIF: &str = "cQUNzeMnF9xPPLqZhH7hMVYGwSuu3b78zznuc5UrxgXnYQBq6Bx1"; + +#[test] +fn addresses() { + let alice = keypair_from_wif(ALICE_WIF).unwrap(); + let recipient = Recipient::::from(&alice); + + assert_eq!( + recipient.legacy_address_string(Network::Bitcoin), + "1MrZNGN7mfWZiZNQttrzHjfw72jnJC2JNx" + ); + assert_eq!( + recipient.segwit_address_string(Network::Bitcoin).unwrap(), + "bc1qunq74p3h8425hr6wllevlvqqr6sezfxj262rff" + ); + assert_eq!( + recipient.taproot_address_string(Network::Bitcoin), + "bc1pwse34zfpvt344rvlt7tw0ngjtfh9xasc4q03avf0lk74jzjpzjuqaz7ks5" + ); +} diff --git a/rust/tw_bitcoin/src/tests/brc20_transfer.rs b/rust/tw_bitcoin/src/tests/brc20_transfer.rs new file mode 100644 index 00000000000..99ba26d0ce5 --- /dev/null +++ b/rust/tw_bitcoin/src/tests/brc20_transfer.rs @@ -0,0 +1,191 @@ +use crate::{ + brc20::{BRC20TransferInscription, Ticker}, + keypair_from_wif, TXOutputP2TRScriptPath, TransactionBuilder, TxInputP2TRScriptPath, + TxInputP2WPKH, TxOutputP2WPKH, +}; +use bitcoin::Txid; +use std::str::FromStr; +use tw_encoding::hex; + +// Those private keys were used for Bitcoin mainnet tests and have a transaction +// history. BTC holdings have been emptied. +pub const ALICE_WIF: &str = "L4of5AJ6aKmvChg7gQ7m2RzHFgpWe5Uirmuey1fXJ1FtfmXj59LW"; +pub const BOB_WIF: &str = "L59WHi2hj1HnMAYaFyMqR4Z36HrUDTZQCixzTHachAxbUU9VUCjp"; + +pub const FULL_SATOSHIS: u64 = 26_400; +pub const MINER_FEE: u64 = 3_000; + +pub const BRC20_TICKER: &str = "oadf"; +pub const BRC20_AMOUNT: u64 = 20; +pub const BRC20_INSCRIBE_SATOSHIS: u64 = 7_000; +pub const BRC20_DUST_SATOSHIS: u64 = 546; + +pub const FOR_FEE_SATOSHIS: u64 = FULL_SATOSHIS - BRC20_INSCRIBE_SATOSHIS - MINER_FEE; + +// Used for the committing the Inscription. +// https://www.blockchain.com/explorer/transactions/btc/797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1 +pub const COMMIT_TXID: &str = "8ec895b4d30adb01e38471ca1019bfc8c3e5fbd1f28d9e7b5653260d89989008"; +pub const COMMIT_TX_RAW: &str = "02000000000101089098890d2653567b9e8df2d1fbe5c3c8bf1910ca7184e301db0ad3b495c88e0100000000ffffffff02581b000000000000225120e8b706a97732e705e22ae7710703e7f589ed13c636324461afa443016134cc051040000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d02483045022100a44aa28446a9a886b378a4a65e32ad9a3108870bd725dc6105160bed4f317097022069e9de36422e4ce2e42b39884aa5f626f8f94194d1013007d5a1ea9220a06dce0121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000"; + +// Used for revealing the Inscription. +// https://www.blockchain.com/explorer/transactions/btc/7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca +pub const REVEAL_TXID: &str = "797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1"; +pub const REVEAL_RAW: &str = "02000000000101b11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d790000000000ffffffff012202000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d0340de6fd13e43700f59876d305e5a4a5c41ad7ada10bc5a4e4bdd779eb0060c0a78ebae9c33daf77bb3725172edb5bd12e26f00c08f9263e480d53b93818138ad0b5b0063036f7264010118746578742f706c61696e3b636861727365743d7574662d3800377b2270223a226272632d3230222c226f70223a227472616e73666572222c227469636b223a226f616466222c22616d74223a223230227d6821c00f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000"; +pub const REVEAL_RAW_P1: &str = "02000000000101b11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d790000000000ffffffff012202000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d0340"; +pub const REVEAL_RAW_SCHNORR: &str = "de6fd13e43700f59876d305e5a4a5c41ad7ada10bc5a4e4bdd779eb0060c0a78ebae9c33daf77bb3725172edb5bd12e26f00c08f9263e480d53b93818138ad0b"; +pub const REVEAL_RAW_P2: &str = "5b0063036f7264010118746578742f706c61696e3b636861727365743d7574662d3800377b2270223a226272632d3230222c226f70223a227472616e73666572222c227469636b223a226f616466222c22616d74223a223230227d6821c00f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000"; + +// Used for transfering the Inscription ("BRC20 transfer"). +// https://www.blockchain.com/explorer/transactions/btc/3e3576eb02667fac284a5ecfcb25768969680cc4c597784602d0a33ba7c654b7 +pub use skip::*; +// We skip formatting for the `skip` module, re-exporting everything. +#[rustfmt::skip] +mod skip { +pub const TRANSFER_TXID_INSCRIPTION: &str = "7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca"; +pub const TRANSFER_TXID_FOR_FEES: &str = "797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1"; +pub const TRANSFER_RAW: &str = "02000000000102ca3edda74a46877efa5364ab85947e148508713910ada23e147ea28926dc46700000000000ffffffffb11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d790100000000ffffffff022202000000000000160014e891850afc55b64aa8247b2076f8894ebdf889015834000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d024830450221008798393eb0b7390217591a8c33abe18dd2f7ea7009766e0d833edeaec63f2ec302200cf876ff52e68dbaf108a3f6da250713a9b04949a8f1dcd1fb867b24052236950121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb0248304502210096bbb9d1f0596d69875646689e46f29485e8ceccacde9d0025db87fd96d3066902206d6de2dd69d965d28df3441b94c76e812384ab9297e69afe3480ee4031e1b2060121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000"; +} + +#[test] +fn brc20_transfer() { + let ticker = Ticker::new(BRC20_TICKER.to_string()).unwrap(); + + let alice = keypair_from_wif(ALICE_WIF).unwrap(); + let bob = keypair_from_wif(BOB_WIF).unwrap(); + + let txid = Txid::from_str(COMMIT_TXID).unwrap(); + + // # Make "available" tokens "transferable". + // Based on Bitcoin transaction: + // https://www.blockchain.com/explorer/transactions/btc/797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1 + + // Commit transfer. + let input = TxInputP2WPKH::builder() + .txid(txid) + .vout(1) + .recipient(alice.try_into().unwrap()) + .satoshis(FULL_SATOSHIS) + .build() + .unwrap(); + + let transfer = BRC20TransferInscription::new(alice.into(), ticker, BRC20_AMOUNT).unwrap(); + + let output = TXOutputP2TRScriptPath::builder() + .recipient(transfer.inscription().recipient().clone()) + .satoshis(BRC20_INSCRIBE_SATOSHIS) + .build() + .unwrap(); + + let output_change = TxOutputP2WPKH::builder() + .recipient(alice.try_into().unwrap()) + .satoshis(FOR_FEE_SATOSHIS) + .build() + .unwrap(); + + let transaction = TransactionBuilder::new() + .add_input(input.into()) + .add_output(output.into()) + .add_output(output_change.into()) + .sign_inputs(alice) + .unwrap() + .serialize() + .unwrap(); + + // Encode the signed transaction. + let hex = hex::encode(&transaction, false); + assert_eq!(hex, COMMIT_TX_RAW); + + // # Reveal transfer. + // Based on Bitcoin transaction: + // https://www.blockchain.com/explorer/transactions/btc/7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca + + let txid = + Txid::from_str("797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1").unwrap(); + + let input = TxInputP2TRScriptPath::builder() + .txid(txid) + .vout(0) + .recipient(transfer.inscription().recipient().clone()) + .satoshis(BRC20_INSCRIBE_SATOSHIS) + .script(transfer.inscription().taproot_program().to_owned()) + .spend_info(transfer.inscription().spend_info().clone()) + .build() + .unwrap(); + + let output = TxOutputP2WPKH::builder() + .recipient(alice.try_into().unwrap()) + .satoshis(BRC20_DUST_SATOSHIS) + .build() + .unwrap(); + + let transaction = TransactionBuilder::new() + .add_input(input.into()) + .add_output(output.into()) + .sign_inputs(alice) + .unwrap() + .serialize() + .unwrap(); + + // Encode the signed transaction. + let hex = hex::encode(&transaction, false); + + assert_eq!( + REVEAL_RAW, + [REVEAL_RAW_P1, REVEAL_RAW_SCHNORR, REVEAL_RAW_P2].concat() + ); + + assert_eq!(&hex[..164], REVEAL_RAW_P1); + // We ignore the 64-byte Schnorr signature, since it uses random data for + // signing on each construction and is therefore not reproducible. + assert_ne!(&hex[164..292], REVEAL_RAW_SCHNORR); + assert_eq!(&hex[292..], REVEAL_RAW_P2); + + // # Actually transfer the "transferable" tokens. + // Based on Bitcoin transaction: + // https://www.blockchain.com/explorer/transactions/btc/3e3576eb02667fac284a5ecfcb25768969680cc4c597784602d0a33ba7c654b7 + + // We use a normal P2WPKH output for this. + let input_for_brc20_transfer = TxInputP2WPKH::builder() + .txid(Txid::from_str(TRANSFER_TXID_INSCRIPTION).unwrap()) + .vout(0) + .recipient(alice.try_into().unwrap()) + .satoshis(BRC20_DUST_SATOSHIS) + .build() + .unwrap(); + + let input_for_fee = TxInputP2WPKH::builder() + .txid(Txid::from_str(TRANSFER_TXID_FOR_FEES).unwrap()) + .vout(1) + .recipient(alice.try_into().unwrap()) + .satoshis(FOR_FEE_SATOSHIS) + .build() + .unwrap(); + + // We transfer the tokens to Bob. + let output_brc20_transfer = TxOutputP2WPKH::builder() + .recipient(bob.try_into().unwrap()) + .satoshis(BRC20_DUST_SATOSHIS) + .build() + .unwrap(); + + let output_change = TxOutputP2WPKH::builder() + .recipient(alice.try_into().unwrap()) + .satoshis(FOR_FEE_SATOSHIS - MINER_FEE) + .build() + .unwrap(); + + // We carefully add the BRC20 transfer in the first position for both input and output. + let transaction = TransactionBuilder::new() + .add_input(input_for_brc20_transfer.into()) + .add_input(input_for_fee.into()) + .add_output(output_brc20_transfer.into()) + .add_output(output_change.into()) + .sign_inputs(alice) + .unwrap() + .serialize() + .unwrap(); + + // Encode the signed transaction. + let hex = hex::encode(&transaction, false); + assert_eq!(hex, TRANSFER_RAW); +} diff --git a/rust/tw_bitcoin/src/tests/ffi/brc20_transfer.rs b/rust/tw_bitcoin/src/tests/ffi/brc20_transfer.rs new file mode 100644 index 00000000000..efb2fb7c6f2 --- /dev/null +++ b/rust/tw_bitcoin/src/tests/ffi/brc20_transfer.rs @@ -0,0 +1,235 @@ +use crate::brc20::{BRC20TransferInscription, Ticker}; +use crate::ffi::taproot_build_and_sign_transaction; +use crate::tests::ffi::utils::{ + call_ffi_build_brc20_transfer_script, call_ffi_build_p2wpkh_script, reverse_txid, + ProtoSigningInputBuilder, ProtoTransactionBuilder, +}; +use crate::tests::p2pkh::ALICE_WIF; +use crate::{keypair_from_wif, Recipient, TXOutputP2TRScriptPath}; +use bitcoin::PublicKey; +use std::borrow::Cow; +use tw_encoding::hex; +use tw_proto::Bitcoin::Proto::{TransactionOutput, TransactionVariant}; + +#[test] +fn proto_brc20_transfer_script() { + let keypair: secp256k1::KeyPair = keypair_from_wif(ALICE_WIF).unwrap(); + let recipient = Recipient::::from(keypair); + + let satoshis: u64 = 1_000; + let brc20_amount = 20; + let ticker = "oadf"; + + // Call FFI function. + let ffi_out = call_ffi_build_brc20_transfer_script(ticker, brc20_amount, satoshis, &recipient); + + // Compare with native call. + let transfer = BRC20TransferInscription::new( + recipient, + Ticker::new(ticker.to_string()).unwrap(), + brc20_amount, + ) + .unwrap(); + + let tapscript = transfer.inscription().recipient().clone(); + let spending_script = transfer.inscription().taproot_program(); + + let tx_out = TXOutputP2TRScriptPath::new(satoshis, &tapscript); + // Wrap in Protobuf structure. + let proto = TransactionOutput { + value: satoshis as i64, + script: Cow::from(tx_out.script_pubkey.as_bytes()), + spendingScript: Cow::from(spending_script.as_bytes()), + }; + + assert_eq!(ffi_out, proto); +} + +/// Commit the Inscription. +#[test] +fn proto_sign_brc20_transfer_inscription_commit() { + use crate::tests::brc20_transfer::*; + + // Prepare keys. + let alice = keypair_from_wif(ALICE_WIF).unwrap(); + let alice_privkey = alice.secret_bytes(); + let alice_recipient = Recipient::::from(&alice); + + // Note that the Txid must be reversed. + let txid = reverse_txid(COMMIT_TXID); + + // Build input script. + let input = call_ffi_build_p2wpkh_script(FULL_SATOSHIS, &alice_recipient); + + // Build inscription output. + let output_inscribe = call_ffi_build_brc20_transfer_script( + BRC20_TICKER, + BRC20_AMOUNT, + BRC20_INSCRIBE_SATOSHIS, + &alice_recipient, + ); + + // Build change output. + let output_change = call_ffi_build_p2wpkh_script(FOR_FEE_SATOSHIS, &alice_recipient); + + // Construct Protobuf payload. + let signing = ProtoSigningInputBuilder::new() + .private_key(&alice_privkey) + .input( + ProtoTransactionBuilder::new() + .txid(&txid) + .vout(1) + .script_pubkey(&input.script) + .satoshis(FULL_SATOSHIS) + .variant(TransactionVariant::P2WPKH) + .build(), + ) + .output( + ProtoTransactionBuilder::new() + .script_pubkey(&output_inscribe.script) + .satoshis(BRC20_INSCRIBE_SATOSHIS) + .variant(TransactionVariant::BRC20TRANSFER) + .build(), + ) + .output( + ProtoTransactionBuilder::new() + .script_pubkey(&output_change.script) + .satoshis(FOR_FEE_SATOSHIS) + .variant(TransactionVariant::P2WPKH) + .build(), + ) + .build(); + + let signed = taproot_build_and_sign_transaction(signing).unwrap(); + assert_eq!(hex::encode(&signed.encoded, false), COMMIT_TX_RAW); +} + +/// Reveal the Inscription. +#[test] +fn proto_sign_brc20_transfer_inscription_reveal() { + use crate::tests::brc20_transfer::*; + + // Prepare keys. + let alice = keypair_from_wif(ALICE_WIF).unwrap(); + let alice_privkey = alice.secret_bytes(); + let alice_recipient = Recipient::::from(&alice); + + // Note that the Txid must be reversed. + let txid = reverse_txid(REVEAL_TXID); + + // Build input script. + let input = call_ffi_build_brc20_transfer_script( + BRC20_TICKER, + BRC20_AMOUNT, + BRC20_INSCRIBE_SATOSHIS, + &alice_recipient, + ); + + // Build inscription output. + let output_p2wpkh = call_ffi_build_p2wpkh_script(BRC20_DUST_SATOSHIS, &alice_recipient); + + // Construct Protobuf payload. + let signing = ProtoSigningInputBuilder::new() + .private_key(&alice_privkey) + .input( + ProtoTransactionBuilder::new() + .txid(&txid) + .vout(0) + .script_pubkey(&input.script) + .satoshis(BRC20_INSCRIBE_SATOSHIS) + .variant(TransactionVariant::BRC20TRANSFER) + // IMPORANT: include the witness containing the actual inscription. + .spending_script(&input.spendingScript) + .build(), + ) + .output( + ProtoTransactionBuilder::new() + .script_pubkey(&output_p2wpkh.script) + .satoshis(BRC20_DUST_SATOSHIS) + .variant(TransactionVariant::P2WPKH) + .build(), + ) + .build(); + + let signed = taproot_build_and_sign_transaction(signing).unwrap(); + let hex = hex::encode(&signed.encoded, false); + + assert_eq!(&hex[..164], REVEAL_RAW_P1); + // We ignore the 64-byte Schnorr signature, since it uses random data for + // signing on each construction and is therefore not reproducible. + assert_ne!(&hex[164..292], REVEAL_RAW_SCHNORR); + assert_eq!(&hex[292..], REVEAL_RAW_P2); +} + +/// Transfer the Inscription with P2WPKH. +#[test] +fn proto_sign_brc20_transfer_inscription_p2wpkh_transfer() { + use crate::tests::brc20_transfer::*; + + // Prepare keys. + let alice = keypair_from_wif(ALICE_WIF).unwrap(); + let alice_privkey = alice.secret_bytes(); + let alice_recipient = Recipient::::from(&alice); + + let bob = keypair_from_wif(BOB_WIF).unwrap(); + let bob_recipient = Recipient::::from(&bob); + + // The Txid to reference the Inscription. + let txid_inscription = reverse_txid(TRANSFER_TXID_INSCRIPTION); + + // The Txid for paying fees. + let txid_for_fees = reverse_txid(TRANSFER_TXID_FOR_FEES); + + // Build input script for Inscription transfer. + let input_transfer = call_ffi_build_p2wpkh_script(BRC20_DUST_SATOSHIS, &alice_recipient); + + // Build input for paying fees. + let input_fees = call_ffi_build_p2wpkh_script(FOR_FEE_SATOSHIS, &alice_recipient); + + // Build Inscription transfer output with Bob as recipient. + let output_transfer = call_ffi_build_p2wpkh_script(BRC20_DUST_SATOSHIS, &bob_recipient); + + // Build change output. + let output_change = + call_ffi_build_p2wpkh_script(FOR_FEE_SATOSHIS - MINER_FEE, &alice_recipient); + + // Construct Protobuf payload. + let signing = ProtoSigningInputBuilder::new() + .private_key(&alice_privkey) + .input( + ProtoTransactionBuilder::new() + .txid(&txid_inscription) + .vout(0) + .script_pubkey(&input_transfer.script) + .satoshis(BRC20_DUST_SATOSHIS) + .variant(TransactionVariant::P2WPKH) + .build(), + ) + .input( + ProtoTransactionBuilder::new() + .txid(&txid_for_fees) + .vout(1) + .script_pubkey(&input_fees.script) + .satoshis(FOR_FEE_SATOSHIS) + .variant(TransactionVariant::P2WPKH) + .build(), + ) + .output( + ProtoTransactionBuilder::new() + .script_pubkey(&output_transfer.script) + .satoshis(BRC20_DUST_SATOSHIS) + .variant(TransactionVariant::P2WPKH) + .build(), + ) + .output( + ProtoTransactionBuilder::new() + .script_pubkey(&output_change.script) + .satoshis(FOR_FEE_SATOSHIS - MINER_FEE) + .variant(TransactionVariant::P2WPKH) + .build(), + ) + .build(); + + let signed = taproot_build_and_sign_transaction(signing).unwrap(); + assert_eq!(hex::encode(&signed.encoded, false), TRANSFER_RAW); +} diff --git a/rust/tw_bitcoin/src/tests/ffi/mod.rs b/rust/tw_bitcoin/src/tests/ffi/mod.rs new file mode 100644 index 00000000000..e767b175d8f --- /dev/null +++ b/rust/tw_bitcoin/src/tests/ffi/mod.rs @@ -0,0 +1,4 @@ +mod brc20_transfer; +mod scripts; +mod transaction; +mod utils; diff --git a/rust/tw_bitcoin/src/tests/ffi/scripts.rs b/rust/tw_bitcoin/src/tests/ffi/scripts.rs new file mode 100644 index 00000000000..b0cc8853f7e --- /dev/null +++ b/rust/tw_bitcoin/src/tests/ffi/scripts.rs @@ -0,0 +1,77 @@ +use crate::tests::ffi::utils::{ + call_ffi_build_p2pkh_script, call_ffi_build_p2tr_key_path_script, call_ffi_build_p2wpkh_script, +}; +use crate::tests::p2pkh::ALICE_WIF; +use crate::{keypair_from_wif, Recipient, TxOutputP2PKH, TxOutputP2TRKeyPath, TxOutputP2WPKH}; +use bitcoin::PublicKey; +use std::borrow::Cow; +use tw_proto::Bitcoin::Proto::TransactionOutput; + +#[test] +fn proto_build_p2pkh_script() { + // Prepare keys. + let keypair: secp256k1::KeyPair = keypair_from_wif(ALICE_WIF).unwrap(); + let recipient = Recipient::::from(keypair); + + let satoshis: u64 = 1_000; + + // Call FFI function. + let ffi_out = call_ffi_build_p2pkh_script(satoshis, &recipient); + + // Compare with native call. + let tx_out = TxOutputP2PKH::new(satoshis, recipient); + // Wrap in Protobuf structure. + let proto = TransactionOutput { + value: satoshis as i64, + script: Cow::from(tx_out.script_pubkey.as_bytes()), + spendingScript: Cow::default(), + }; + + assert_eq!(ffi_out, proto); +} + +#[test] +fn proto_build_p2wpkh_script() { + // Prepare keys. + let keypair: secp256k1::KeyPair = keypair_from_wif(ALICE_WIF).unwrap(); + let recipient = Recipient::::from(keypair); + + let satoshis: u64 = 1_000; + + // Call FFI function. + let ffi_out = call_ffi_build_p2wpkh_script(satoshis, &recipient); + + // Compare with native call. + let tx_out = TxOutputP2WPKH::new(satoshis, recipient.try_into().unwrap()); + // Wrap in Protobuf structure. + let proto = TransactionOutput { + value: satoshis as i64, + script: Cow::from(tx_out.script_pubkey.as_bytes()), + spendingScript: Cow::default(), + }; + + assert_eq!(ffi_out, proto); +} + +#[test] +fn proto_build_p2tr_key_path_script() { + // Prepare keys. + let keypair: secp256k1::KeyPair = keypair_from_wif(ALICE_WIF).unwrap(); + let recipient = Recipient::::from(keypair); + + let satoshis: u64 = 1_000; + + // Call FFI function. + let ffi_out = call_ffi_build_p2tr_key_path_script(satoshis, &recipient); + + // Compare with native call. + let tx_out = TxOutputP2TRKeyPath::new(satoshis, recipient.try_into().unwrap()); + // Wrap in Protobuf structure. + let proto = TransactionOutput { + value: satoshis as i64, + script: Cow::from(tx_out.script_pubkey.as_bytes()), + spendingScript: Cow::default(), + }; + + assert_eq!(ffi_out, proto); +} diff --git a/rust/tw_bitcoin/src/tests/ffi/transaction.rs b/rust/tw_bitcoin/src/tests/ffi/transaction.rs new file mode 100644 index 00000000000..10d8035d92b --- /dev/null +++ b/rust/tw_bitcoin/src/tests/ffi/transaction.rs @@ -0,0 +1,138 @@ +use crate::ffi::taproot_build_and_sign_transaction; +use crate::tests::ffi::utils::{ + call_ffi_build_p2pkh_script, call_ffi_build_p2tr_key_path_script, call_ffi_build_p2wpkh_script, + reverse_txid, ProtoSigningInputBuilder, ProtoTransactionBuilder, +}; +use crate::{keypair_from_wif, Recipient}; +use bitcoin::PublicKey; +use tw_encoding::hex; +use tw_proto::Bitcoin::Proto::TransactionVariant; + +#[test] +pub fn proto_sign_input_p2pkh_output_p2pkh() { + use crate::tests::p2pkh::*; + + // Prepare keys. + let alice: secp256k1::KeyPair = keypair_from_wif(ALICE_WIF).unwrap(); + let alice_privkey = alice.secret_bytes(); + let alice_recipient = Recipient::::from_keypair(&alice); + + let bob = keypair_from_wif(BOB_WIF).unwrap(); + let bob_recipient = Recipient::::from_keypair(&bob); + + let txid = reverse_txid(TXID); + + // Prepare the scripts. + let input = call_ffi_build_p2pkh_script(FULL_SATOSHIS, &alice_recipient); + let output = call_ffi_build_p2pkh_script(SEND_SATOSHIS, &bob_recipient); + + // Construct Protobuf payload. + let signing = ProtoSigningInputBuilder::new() + .private_key(&alice_privkey) + .input( + ProtoTransactionBuilder::new() + .txid(&txid) + .vout(0) + .script_pubkey(&input.script) + .satoshis(FULL_SATOSHIS) + .variant(TransactionVariant::P2PKH) + .build(), + ) + .output( + ProtoTransactionBuilder::new() + .script_pubkey(&output.script) + .satoshis(SEND_SATOSHIS) + .variant(TransactionVariant::P2PKH) + .build(), + ) + .build(); + + let signed = taproot_build_and_sign_transaction(signing).unwrap(); + assert_eq!(hex::encode(&signed.encoded, false), TX_RAW); +} + +#[test] +pub fn proto_sign_input_p2pkh_output_p2wpkh() { + use crate::tests::p2wpkh::*; + + // Prepare keys. + let alice: secp256k1::KeyPair = keypair_from_wif(ALICE_WIF).unwrap(); + let alice_privkey = alice.secret_bytes(); + let alice_recipient = Recipient::::from_keypair(&alice); + + let bob = keypair_from_wif(BOB_WIF).unwrap(); + let bob_recipient = Recipient::::from_keypair(&bob); + + let txid = reverse_txid(TXID); + + // Prepare the scripts. + let input = call_ffi_build_p2pkh_script(FULL_SATOSHIS, &alice_recipient); + let output = call_ffi_build_p2wpkh_script(SEND_SATOSHIS, &bob_recipient); + + // Construct Protobuf payload. + let signing = ProtoSigningInputBuilder::new() + .private_key(&alice_privkey) + .input( + ProtoTransactionBuilder::new() + .txid(&txid) + .vout(0) + .script_pubkey(&input.script) + .satoshis(FULL_SATOSHIS) + .variant(TransactionVariant::P2PKH) + .build(), + ) + .output( + ProtoTransactionBuilder::new() + .script_pubkey(&output.script) + .satoshis(SEND_SATOSHIS) + .variant(TransactionVariant::P2WPKH) + .build(), + ) + .build(); + + let signed = taproot_build_and_sign_transaction(signing).unwrap(); + assert_eq!(hex::encode(&signed.encoded, false), TX_RAW); +} + +#[test] +pub fn proto_sign_input_p2pkh_output_p2tr_key_path() { + use crate::tests::p2tr_key_path::*; + + // Prepare keys. + let alice: secp256k1::KeyPair = keypair_from_wif(ALICE_WIF).unwrap(); + let alice_privkey = alice.secret_bytes(); + let alice_recipient = Recipient::::from_keypair(&alice); + + let bob = keypair_from_wif(BOB_WIF).unwrap(); + let bob_recipient = Recipient::::from_keypair(&bob); + + let txid = reverse_txid(FIRST_TXID); + + // Prepare the scripts. + let input = call_ffi_build_p2pkh_script(FULL_SATOSHIS, &alice_recipient); + let output = call_ffi_build_p2tr_key_path_script(SEND_SATOSHIS_TO_BOB, &bob_recipient); + + // Construct Protobuf payload. + let signing = ProtoSigningInputBuilder::new() + .private_key(&alice_privkey) + .input( + ProtoTransactionBuilder::new() + .txid(&txid) + .vout(0) + .script_pubkey(&input.script) + .satoshis(FULL_SATOSHIS) + .variant(TransactionVariant::P2PKH) + .build(), + ) + .output( + ProtoTransactionBuilder::new() + .script_pubkey(&output.script) + .satoshis(SEND_SATOSHIS_TO_BOB) + .variant(TransactionVariant::P2TRKEYPATH) + .build(), + ) + .build(); + + let signed = taproot_build_and_sign_transaction(signing).unwrap(); + assert_eq!(hex::encode(&signed.encoded, false), FIRST_TX_RAW); +} diff --git a/rust/tw_bitcoin/src/tests/ffi/utils.rs b/rust/tw_bitcoin/src/tests/ffi/utils.rs new file mode 100644 index 00000000000..2fec206f829 --- /dev/null +++ b/rust/tw_bitcoin/src/tests/ffi/utils.rs @@ -0,0 +1,199 @@ +use crate::ffi::{ + tw_build_brc20_inscribe_transfer, tw_build_p2pkh_script, tw_build_p2tr_key_path_script, + tw_build_p2wpkh_script, +}; +use crate::Recipient; +use bitcoin::PublicKey; +use std::borrow::Cow; +use tw_proto::Bitcoin::Proto::{ + OutPoint, SigningInput, TransactionOutput, TransactionPlan, TransactionVariant, + UnspentTransaction, +}; + +/// Convenience function for reversing the Txid before it's being passed on to +/// the FFI. +pub fn reverse_txid(txid: &str) -> Vec { + tw_encoding::hex::decode(txid) + .unwrap() + .into_iter() + .rev() + .collect() +} + +/// Convenience wrapper over `tw_build_p2pkh_script` with Protobuf +/// deserialization support. +pub fn call_ffi_build_p2pkh_script<'a, 'b>( + satoshis: u64, + // We use 'b to clarify that `recipient` is not tied to the return value. + recipient: &'b Recipient, +) -> TransactionOutput<'a> { + let pubkey = recipient.public_key().to_bytes(); + + let raw = + unsafe { tw_build_p2pkh_script(satoshis as i64, pubkey.as_ptr(), pubkey.len()).into_vec() }; + + let des: TransactionOutput = tw_proto::deserialize(&raw).unwrap(); + + // We convert the referenced data into owned data since `raw` goes out of + // scope at the end of the function. + TransactionOutput { + value: des.value, + script: des.script.into_owned().into(), + spendingScript: des.spendingScript.into_owned().into(), + } +} + +/// Convenience wrapper over `tw_build_p2wpkh_script` with Protobuf +/// deserialization support. +pub fn call_ffi_build_p2wpkh_script<'a, 'b>( + satoshis: u64, + // We use 'b to clarify that `recipient` is not tied to the return value. + recipient: &'b Recipient, +) -> TransactionOutput<'a> { + let pubkey = recipient.public_key().to_bytes(); + + let raw = unsafe { + tw_build_p2wpkh_script(satoshis as i64, pubkey.as_ptr(), pubkey.len()).into_vec() + }; + + let des: TransactionOutput = tw_proto::deserialize(&raw).unwrap(); + + // We convert the referenced data into owned data since `raw` goes out of + // scope at the end of the function. + TransactionOutput { + value: des.value, + script: des.script.into_owned().into(), + spendingScript: des.spendingScript.into_owned().into(), + } +} + +/// Convenience wrapper over `tw_build_p2tr_key_path_script` with Protobuf +/// deserialization support. +pub fn call_ffi_build_p2tr_key_path_script<'a, 'b>( + satoshis: u64, + // We use 'b to clarify that `recipient` is not tied to the return value. + recipient: &'b Recipient, +) -> TransactionOutput<'a> { + let pubkey = recipient.public_key().to_bytes(); + + let raw = unsafe { + tw_build_p2tr_key_path_script(satoshis as i64, pubkey.as_ptr(), pubkey.len()).into_vec() + }; + + let des: TransactionOutput = tw_proto::deserialize(&raw).unwrap(); + + // We convert the referenced data into owned data since `raw` goes out of + // scope at the end of the function. + TransactionOutput { + value: des.value, + script: des.script.into_owned().into(), + spendingScript: des.spendingScript.into_owned().into(), + } +} + +/// Convenience wrapper over `tw_build_brc20_inscribe_transfer` with Protobuf +/// deserialization support. +pub fn call_ffi_build_brc20_transfer_script<'a, 'b>( + ticker: &str, + brc20_amount: u64, + satoshis: u64, + // We use 'b to clarify that `recipient` is not tied to the return value. + recipient: &'b Recipient, +) -> TransactionOutput<'a> { + let pubkey = recipient.public_key().to_bytes(); + + let raw = unsafe { + tw_build_brc20_inscribe_transfer( + ticker.as_bytes().as_ptr(), + brc20_amount, + satoshis as i64, + pubkey.as_ptr(), + pubkey.len(), + ) + .into_vec() + }; + + let des: TransactionOutput = tw_proto::deserialize(&raw).unwrap(); + + // We convert the referenced data into owned data since `raw` goes out of + // scope at the end of the function. + TransactionOutput { + value: des.value, + script: des.script.into_owned().into(), + spendingScript: des.spendingScript.into_owned().into(), + } +} + +/// Builder for creating the `SigningInput` Protobuf structure. +pub struct ProtoSigningInputBuilder<'a> { + inner: SigningInput<'a>, +} + +impl<'a> ProtoSigningInputBuilder<'a> { + pub fn new() -> Self { + let signing = SigningInput { + plan: Some(TransactionPlan::default()), + ..Default::default() + }; + + ProtoSigningInputBuilder { inner: signing } + } + pub fn private_key(mut self, privkey: &'a [u8]) -> Self { + self.inner.private_key = vec![Cow::from(privkey)]; + self + } + pub fn input(mut self, tx: UnspentTransaction<'a>) -> Self { + self.inner.utxo.push(tx); + self + } + pub fn output(mut self, tx: UnspentTransaction<'a>) -> Self { + self.inner.plan.as_mut().unwrap().utxos.push(tx); + self + } + pub fn build(self) -> SigningInput<'a> { + self.inner + } +} + +/// Builder for creating the `UnspentTransaction` Protobuf structure. +pub struct ProtoTransactionBuilder<'a> { + inner: UnspentTransaction<'a>, +} + +impl<'a> ProtoTransactionBuilder<'a> { + pub fn new() -> Self { + let unspent = UnspentTransaction { + out_point: Some(OutPoint::default()), + ..Default::default() + }; + + ProtoTransactionBuilder { inner: unspent } + } + pub fn txid(mut self, slice: &'a [u8]) -> Self { + self.inner.out_point.as_mut().unwrap().hash = slice.into(); + self + } + pub fn vout(mut self, vout: u32) -> Self { + self.inner.out_point.as_mut().unwrap().index = vout; + self + } + pub fn variant(mut self, variant: TransactionVariant) -> Self { + self.inner.variant = variant; + self + } + pub fn satoshis(mut self, satoshis: u64) -> Self { + self.inner.amount = satoshis as i64; + self + } + pub fn script_pubkey(mut self, script: &'a [u8]) -> Self { + self.inner.script = script.into(); + self + } + pub fn spending_script(mut self, script: &'a [u8]) -> Self { + self.inner.spendingScript = script.into(); + self + } + pub fn build(self) -> UnspentTransaction<'a> { + self.inner + } +} diff --git a/rust/tw_bitcoin/src/tests/mod.rs b/rust/tw_bitcoin/src/tests/mod.rs new file mode 100644 index 00000000000..e067091ca35 --- /dev/null +++ b/rust/tw_bitcoin/src/tests/mod.rs @@ -0,0 +1,8 @@ +mod address; +mod brc20_transfer; +mod ffi; +mod p2pkh; +mod p2tr_key_path; +mod p2wpkh; + +pub const ONE_BTC: u64 = 100_000_000; diff --git a/rust/tw_bitcoin/src/tests/p2pkh.rs b/rust/tw_bitcoin/src/tests/p2pkh.rs new file mode 100644 index 00000000000..238ba909abf --- /dev/null +++ b/rust/tw_bitcoin/src/tests/p2pkh.rs @@ -0,0 +1,54 @@ +use super::*; +use crate::{keypair_from_wif, TransactionBuilder, TxInputP2PKH, TxOutputP2PKH}; +use bitcoin::Txid; +use std::str::FromStr; +use tw_encoding::hex; + +// Those private keys were used in a Bitcoin regtest environment. +pub const ALICE_WIF: &str = "cQUNzeMnF9xPPLqZhH7hMVYGwSuu3b78zznuc5UrxgXnYQBq6Bx1"; +pub const BOB_WIF: &str = "cTk5wSci88FPka7JwHpNEA82dUMjAysdDbCiuYB2fegfgGESAZVn"; +pub const TXID: &str = "1e1cdc48aa990d7e154a161d5b5f1cad737742e97d2712ab188027bb42e6e47b"; + +pub const FULL_SATOSHIS: u64 = ONE_BTC * 50; +pub const MINER_FEE: u64 = ONE_BTC / 100; +pub const SEND_SATOSHIS: u64 = FULL_SATOSHIS - MINER_FEE; + +// This passed the `bitcoin-cli -retest testmempoolaccept` command. +pub const TX_RAW: &str = "02000000017be4e642bb278018ab12277de9427773ad1c5f5b1d164a157e0d99aa48dc1c1e000000006a473044022078eda020d4b86fcb3af78ef919912e6d79b81164dbbb0b0b96da6ac58a2de4b102201a5fd8d48734d5a02371c4b5ee551a69dca3842edbf577d863cf8ae9fdbbd4590121036666dd712e05a487916384bfcd5973eb53e8038eccbbf97f7eed775b87389536ffffffff01c0aff629010000001976a9145eaaa4f458f9158f86afcba08dd7448d27045e3d88ac00000000"; + +#[test] +fn sign_input_p2pkh_output_p2pkh() { + // This passed the `bitcoin-cli -retest testmempoolaccept` command. + + let alice = keypair_from_wif(ALICE_WIF).unwrap(); + let bob = keypair_from_wif(BOB_WIF).unwrap(); + + // Prepare inputs for Alice. + let input = TxInputP2PKH::builder() + .txid(Txid::from_str(TXID).unwrap()) + .vout(0) + .recipient(alice) + .satoshis(FULL_SATOSHIS) + .build() + .unwrap(); + + // Prepare outputs for Bob. + let output = TxOutputP2PKH::builder() + .satoshis(SEND_SATOSHIS) + .recipient(bob) + .build() + .unwrap(); + + // Alice signs the transaction. + let signed_transaction = TransactionBuilder::new() + .miner_fee(MINER_FEE) + .add_input(input.into()) + .add_output(output.into()) + .sign_inputs(alice) + .unwrap() + .serialize() + .unwrap(); + + let hex = hex::encode(&signed_transaction, false); + assert_eq!(&hex, TX_RAW); +} diff --git a/rust/tw_bitcoin/src/tests/p2tr_key_path.rs b/rust/tw_bitcoin/src/tests/p2tr_key_path.rs new file mode 100644 index 00000000000..cf876fb8975 --- /dev/null +++ b/rust/tw_bitcoin/src/tests/p2tr_key_path.rs @@ -0,0 +1,94 @@ +use super::ONE_BTC; +use crate::{ + keypair_from_wif, TransactionBuilder, TxInputP2PKH, TxInputP2TRKeyPath, TxOutputP2TRKeyPath, +}; +use bitcoin::Txid; +use std::str::FromStr; +use tw_encoding::hex; + +// Those private keys were used in a Bitcoin regtest environment. +pub const ALICE_WIF: &str = "cNDFvH3TXCjxgWeVc7vbu4Jw5m2Lu8FkQ69Z2XvFUD9D9rGjofN1"; +pub const BOB_WIF: &str = "cNt3XNHiJdJpoX5zt3CXY8ncgrCted8bxmFBzcGeTZbBw6jkByWB"; + +pub const FULL_SATOSHIS: u64 = ONE_BTC * 50; +pub const MINER_FEE: u64 = ONE_BTC / 100; +pub const SEND_SATOSHIS_TO_BOB: u64 = FULL_SATOSHIS - MINER_FEE; + +// The raw transactions passed the `bitcoin-cli -retest testmempoolaccept` command. +pub const FIRST_TXID: &str = "c50563913e5a838f937c94232f5a8fc74e58b629fae41dfdffcc9a70f833b53a"; +pub const FIRST_TX_RAW: &str = "02000000013ab533f8709accfffd1de4fa29b6584ec78f5a2f23947c938f835a3e916305c5000000006b48304502210086ab2c2192e2738529d6cd9604d8ee75c5b09b0c2f4066a5c5fa3f87a26c0af602202afc7096aaa992235c43e712146057b5ed6a776d82b9129620bc5a21991c0a5301210351e003fdc48e7f31c9bc94996c91f6c3273b7ef4208a1686021bedf7673bb058ffffffff01c0aff62901000000225120e01cfdd05da8fa1d71f987373f3790d45dea9861acb0525c86656fe50f4397a600000000"; + +pub const SEND_SATOSHIS_TO_ALICE: u64 = SEND_SATOSHIS_TO_BOB - MINER_FEE; +pub const SECOND_TXID: &str = "9a582032f6a50cedaff77d3d5604b33adf8bc31bdaef8de977c2187e395860ac"; +pub const SECOND_TX_RAW: &str = "02000000000101ac6058397e18c277e98defda1bc38bdf3ab304563d7df7afed0ca5f63220589a0000000000ffffffff01806de72901000000225120a5c027857e359d19f625e52a106b8ac6ca2d6a8728f6cf2107cd7958ee0787c20140ec2d3910d41506b60aaa20520bb72f15e2d2cbd97e3a8e26ee7bad5f4c56b0f2fb0ceaddac33cb2813a33ba017ba6b1d011bab74a0426f12a2bcf47b4ed5bc8600000000"; + +#[test] +fn sign_input_p2pkh_output_p2tr_key_path() { + let alice = keypair_from_wif(ALICE_WIF).unwrap(); + let bob = keypair_from_wif(BOB_WIF).unwrap(); + + // # First transaction: Alice spends the P2PKH coinbase input and creates + // # a P2WPKH output for Bob. + + // Prepare inputs for Alice. + let input = TxInputP2PKH::builder() + .txid(Txid::from_str(FIRST_TXID).unwrap()) + .vout(0) + .recipient(alice) + .satoshis(FULL_SATOSHIS) + .build() + .unwrap(); + + // Prepare outputs for Bob. + let output = TxOutputP2TRKeyPath::builder() + .recipient(bob) + .satoshis(SEND_SATOSHIS_TO_BOB) + .build() + .unwrap(); + + // Alice signs the transaction. + let signed_transaction = TransactionBuilder::new() + .miner_fee(MINER_FEE) + .add_input(input.into()) + .add_output(output.into()) + .sign_inputs(alice) + .unwrap() + .serialize() + .unwrap(); + + let hex = hex::encode(&signed_transaction, false); + assert_eq!(&hex, FIRST_TX_RAW); + + // # Second transaction: Bob spends the P2WPKH input and creates + // # a P2WPKH output for Alice. + + // Transaction was submitted in regtest env via `sendrawtransaction` and + // mined with `-generate 1` + let input = TxInputP2TRKeyPath::builder() + .txid(Txid::from_str(SECOND_TXID).unwrap()) + .vout(0) + .recipient(bob) + .satoshis(SEND_SATOSHIS_TO_BOB) + .build() + .unwrap(); + + // Prepare outputs for Bob. + let output = TxOutputP2TRKeyPath::builder() + .recipient(alice) + .satoshis(SEND_SATOSHIS_TO_ALICE) + .build() + .unwrap(); + + // Alice signs the transaction. + let signed_transaction = TransactionBuilder::new() + .miner_fee(MINER_FEE) + .add_input(input.into()) + .add_output(output.into()) + .sign_inputs(bob) + .unwrap() + .serialize() + .unwrap(); + + let hex = hex::encode(&signed_transaction, false); + assert_eq!(hex, SECOND_TX_RAW); +} diff --git a/rust/tw_bitcoin/src/tests/p2wpkh.rs b/rust/tw_bitcoin/src/tests/p2wpkh.rs new file mode 100644 index 00000000000..1422e825896 --- /dev/null +++ b/rust/tw_bitcoin/src/tests/p2wpkh.rs @@ -0,0 +1,92 @@ +use super::*; +use crate::{keypair_from_wif, TransactionBuilder, TxInputP2PKH, TxInputP2WPKH, TxOutputP2WPKH}; +use bitcoin::Txid; +use std::str::FromStr; +use tw_encoding::hex; + +// Those private keys were used in a Bitcoin regtest environment. +pub const ALICE_WIF: &str = "cQX5ePcXjTx7C5p6xV8zkp2NN9unhZx4a8RQVPiHd52WxoApV6yK"; +pub const BOB_WIF: &str = "cMn7SSCtE5yt2PS97P4NCMvxpCVvT4cBuHiCzKFW5XMvio4fQbD1"; +pub const TXID: &str = "181c84965c9ea86a5fac32fdbd5f73a21a7a9e749fb6ab97e273af2329f6b911"; + +pub const FULL_SATOSHIS: u64 = ONE_BTC * 50; +pub const MINER_FEE: u64 = ONE_BTC / 100; +pub const SEND_SATOSHIS: u64 = FULL_SATOSHIS - MINER_FEE; + +// This passed the `bitcoin-cli -retest testmempoolaccept` command. +pub const TX_RAW: &str = "020000000111b9f62923af73e297abb69f749e7a1aa2735fbdfd32ac5f6aa89e5c96841c18000000006b483045022100df9ed0b662b759e68b89a42e7144cddf787782a7129d4df05642dd825930e6e6022051a08f577f11cc7390684bbad2951a6374072253ffcf2468d14035ed0d8cd6490121028d7dce6d72fb8f7af9566616c6436349c67ad379f2404dd66fe7085fe0fba28fffffffff01c0aff629010000001600140d0e1cec6c2babe8badde5e9b3dea667da90036d00000000"; + +#[test] +fn sign_input_p2pkh_and_p2wpkh_output_p2wpkh() { + let alice = keypair_from_wif(ALICE_WIF).unwrap(); + let bob = keypair_from_wif(BOB_WIF).unwrap(); + + // # First transaction: Alice spends the P2PKH coinbase input and creates + // # a P2WPKH output for Bob. + + // Prepare inputs for Alice. + let input = TxInputP2PKH::builder() + .txid(Txid::from_str(TXID).unwrap()) + .vout(0) + .recipient(alice) + .satoshis(FULL_SATOSHIS) + .build() + .unwrap(); + + // Prepare outputs for Bob. + let output = TxOutputP2WPKH::builder() + .recipient(bob.try_into().unwrap()) + .satoshis(SEND_SATOSHIS) + .build() + .unwrap(); + + // Alice signs the transaction. + let signed_transaction = TransactionBuilder::new() + .miner_fee(MINER_FEE) + .add_input(input.into()) + .add_output(output.into()) + .sign_inputs(alice) + .unwrap() + .serialize() + .unwrap(); + + let hex = hex::encode(&signed_transaction, false); + assert_eq!(&hex, TX_RAW); + + // # Second transaction: Bob spends the P2WPKH input and creates + // # a P2WPKH output for Alice. + + // Transaction was submitted in regtest env via `sendrawtransaction` and + // mined with `-generate 1` + const TX_RAW_SECOND: &str = "020000000001016e1f16dcfafbb3a83697f6c23c624cd71085a7f8a25ce0bd9743a41d0a458e850000000000ffffffff01806de7290100000016001460cda7b50f14c152d7401c28ae773c698db9237302483045022100a9b517de5a5e036d7133df499b5b751db6f9a01576a6c5dc38229ec08b6c45cd02200e42c9f8c707c9bf0ceab4f739ec8d683dc1f1f29e195a8da9bc183584d624a60121025a0af1510f0f24d40dd00d7c0e51605ca504bbc177c3e19b065f373a1efdd22f00000000"; + const LATEST_TXID: &str = "858e450a1da44397bde05ca2f8a78510d74c623cc2f69736a8b3fbfadc161f6e"; + const SEND_TO_ALICE: u64 = SEND_SATOSHIS - MINER_FEE; + + let input = TxInputP2WPKH::builder() + .txid(Txid::from_str(LATEST_TXID).unwrap()) + .vout(0) + .recipient(bob.try_into().unwrap()) + .satoshis(SEND_SATOSHIS) + .build() + .unwrap(); + + // Prepare outputs for Bob. + let output = TxOutputP2WPKH::builder() + .recipient(alice.try_into().unwrap()) + .satoshis(SEND_TO_ALICE) + .build() + .unwrap(); + + // Alice signs the transaction. + let signed_transaction = TransactionBuilder::new() + .miner_fee(MINER_FEE) + .add_input(input.into()) + .add_output(output.into()) + .sign_inputs(bob) + .unwrap() + .serialize() + .unwrap(); + + let hex = hex::encode(&signed_transaction, false); + assert_eq!(&hex, TX_RAW_SECOND); +} diff --git a/rust/tw_bitcoin/src/transaction.rs b/rust/tw_bitcoin/src/transaction.rs new file mode 100644 index 00000000000..a05d1f122ba --- /dev/null +++ b/rust/tw_bitcoin/src/transaction.rs @@ -0,0 +1,248 @@ +use crate::claim::{ClaimLocation, TransactionSigner}; +use crate::input::*; +use crate::output::*; +use crate::{Error, Result}; +use bitcoin::blockdata::locktime::absolute::{Height, LockTime}; +use bitcoin::consensus::Encodable; +use bitcoin::sighash::{EcdsaSighashType, SighashCache, TapSighashType}; +use bitcoin::taproot::{LeafVersion, TapLeafHash}; +use bitcoin::transaction::Transaction; +use bitcoin::{secp256k1, Address, TxIn, TxOut}; + +#[derive(Debug, Clone)] +pub struct TransactionBuilder { + pub version: i32, + pub lock_time: LockTime, + inputs: Vec, + outputs: Vec, + miner_fee: Option, + return_address: Option
, + contains_taproot: bool, +} + +impl Default for TransactionBuilder { + fn default() -> Self { + TransactionBuilder { + version: 2, + // No lock time, transaction is immediately spendable. + lock_time: LockTime::Blocks(Height::ZERO), + inputs: vec![], + outputs: vec![], + miner_fee: None, + return_address: None, + contains_taproot: false, + } + } +} + +impl TransactionBuilder { + pub fn new() -> Self { + Self::default() + } + pub fn version(mut self, version: i32) -> Self { + self.version = version; + self + } + pub fn lock_time_height(mut self, height: u32) -> Result { + self.lock_time = LockTime::Blocks(Height::from_consensus(height).map_err(|_| Error::Todo)?); + Ok(self) + } + pub fn return_address(mut self, address: Address) -> Self { + self.return_address = Some(address); + self + } + pub fn miner_fee(mut self, satoshis: u64) -> Self { + self.miner_fee = Some(satoshis); + self + } + pub fn add_input(mut self, input: TxInput) -> Self { + match input { + TxInput::P2TRKeyPath(_) | TxInput::P2TRScriptPath(_) => self.contains_taproot = true, + _ => {}, + } + + self.inputs.push(input); + self + } + pub fn add_output(mut self, output: TxOutput) -> Self { + self.outputs.push(output); + self + } + pub fn sign_inputs(self, signer: S) -> Result + where + S: TransactionSigner, + { + self.sign_inputs_fn(|input, sighash| match input { + TxInput::P2PKH(p) => signer + .claim_p2pkh(p, sighash, EcdsaSighashType::All) + .map(|claim| ClaimLocation::Script(claim.0)), + TxInput::P2WPKH(p) => signer + .claim_p2wpkh(p, sighash, EcdsaSighashType::All) + .map(|claim| ClaimLocation::Witness(claim.0)), + TxInput::P2TRKeyPath(p) => signer + .claim_p2tr_key_path(p, sighash, TapSighashType::Default) + .map(|claim| ClaimLocation::Witness(claim.0)), + TxInput::P2TRScriptPath(p) => signer + .claim_p2tr_script_path(p, sighash, TapSighashType::Default) + .map(|claim| ClaimLocation::Witness(claim.0)), + }) + } + pub fn sign_inputs_fn(self, signer: F) -> Result + where + F: Fn(&TxInput, secp256k1::Message) -> Result, + { + // Prepare boilerplate transaction for `bitcoin` crate. + let mut tx = Transaction { + version: self.version, + lock_time: self.lock_time, + input: vec![], + output: vec![], + }; + + // Prepare the inputs for `bitcoin` crate. + for input in self.inputs.iter().cloned() { + let btxin = TxIn::from(input); + tx.input.push(btxin); + } + + // Prepare the outputs for `bitcoin` crate. + for output in self.outputs.iter().cloned() { + let btc_txout = TxOut::from(output); + tx.output.push(btc_txout); + } + + // Satoshi output check + /* + // TODO: This should be enabled, eventually. + let miner_fee = self.miner_fee.ok_or(Error::Todo)?; + if total_satoshis_outputs + miner_fee > total_satoshi_inputs { + return Err(Error::Todo); + } + */ + + // If Taproot is enabled, we prepare the full `TxOuts` (value and + // scriptPubKey) for hashing, which will then be signed. What + // distinguishes this from legacy signing is that the output value in + // satoshis is actually part of the signature. + let mut prevouts = vec![]; + if self.contains_taproot { + for input in &self.inputs { + prevouts.push(TxOut { + value: input.ctx().value, + script_pubkey: input.ctx().script_pubkey.clone(), + }); + } + } + + let mut cache = SighashCache::new(tx); + + let mut claims = vec![]; + + // For each input (index), we create a hash which is to be signed. + for (index, input) in self.inputs.iter().enumerate() { + match input { + TxInput::P2PKH(p2pkh) => { + let hash = cache + .legacy_signature_hash( + index, + &p2pkh.ctx().script_pubkey, + EcdsaSighashType::All.to_u32(), + ) + .map_err(|_| Error::Todo)?; + + let message = secp256k1::Message::from_slice(hash.as_ref()) + .expect("Sighash must always convert to secp256k1::Message"); + let updated = signer(input, message)?; + + claims.push((index, updated)); + }, + TxInput::P2WPKH(p2wpkh) => { + let hash = cache + .segwit_signature_hash( + index, + p2wpkh + .ctx() + .script_pubkey + .p2wpkh_script_code() + .as_ref() + .expect("P2WPKH builder must set the script code correctly"), + p2wpkh.ctx().value, + EcdsaSighashType::All, + ) + .map_err(|_| Error::Todo)?; + + let message = secp256k1::Message::from_slice(hash.as_ref()) + .expect("Sighash must always convert to secp256k1::Message"); + let updated = signer(input, message)?; + + claims.push((index, updated)); + }, + TxInput::P2TRKeyPath(_) => { + let hash = cache + .taproot_key_spend_signature_hash( + index, + &bitcoin::sighash::Prevouts::All(&prevouts), + TapSighashType::Default, + ) + .map_err(|_| Error::Todo)?; + + let message = secp256k1::Message::from_slice(hash.as_ref()) + .expect("Sighash must always convert to secp256k1::Message"); + let updated = signer(input, message)?; + + claims.push((index, updated)); + }, + TxInput::P2TRScriptPath(p2trsp) => { + let leaf_hash = + TapLeafHash::from_script(p2trsp.witness(), LeafVersion::TapScript); + + let hash = cache + .taproot_script_spend_signature_hash( + index, + &bitcoin::sighash::Prevouts::All(&prevouts), + leaf_hash, + TapSighashType::Default, + ) + .map_err(|_| Error::Todo)?; + + let message = secp256k1::Message::from_slice(hash.as_ref()) + .expect("Sighash must always convert to secp256k1::Message"); + let updated = signer(input, message)?; + + claims.push((index, updated)); + }, + }; + } + + let mut tx = cache.into_transaction(); + + // Update the transaction with the updated scriptSig/Witness. + for (index, claim_loc) in claims { + match claim_loc { + ClaimLocation::Script(script) => { + tx.input[index].script_sig = script; + }, + ClaimLocation::Witness(witness) => { + tx.input[index].witness = witness; + }, + } + } + + Ok(TransactionSigned { inner: tx }) + } +} + +pub struct TransactionSigned { + pub inner: Transaction, +} + +impl TransactionSigned { + pub fn serialize(&self) -> Result> { + let mut buffer = vec![]; + self.inner + .consensus_encode(&mut buffer) + .map_err(|_| Error::Todo)?; + + Ok(buffer) + } +} diff --git a/rust/tw_bitcoin/src/utils.rs b/rust/tw_bitcoin/src/utils.rs new file mode 100644 index 00000000000..399a84526e5 --- /dev/null +++ b/rust/tw_bitcoin/src/utils.rs @@ -0,0 +1,15 @@ +use crate::{Error, Result}; +use bitcoin::key::{KeyPair, PrivateKey, PublicKey, TapTweak, TweakedPublicKey}; +use bitcoin::secp256k1::{self, XOnlyPublicKey}; + +pub fn keypair_from_wif(string: &str) -> Result { + let pk = PrivateKey::from_wif(string).map_err(|_| Error::Todo)?; + let keypair = KeyPair::from_secret_key(&secp256k1::Secp256k1::new(), &pk.inner); + Ok(keypair) +} + +pub(crate) fn tweak_pubkey(pubkey: PublicKey) -> TweakedPublicKey { + let xonly = XOnlyPublicKey::from(pubkey.inner); + let (tweaked, _) = xonly.tap_tweak(&secp256k1::Secp256k1::new(), None); + tweaked +} diff --git a/rust/tw_memory/src/ffi/mod.rs b/rust/tw_memory/src/ffi/mod.rs index 1193bfece9c..058f841646a 100644 --- a/rust/tw_memory/src/ffi/mod.rs +++ b/rust/tw_memory/src/ffi/mod.rs @@ -16,6 +16,33 @@ pub mod c_result; /// \param ptr *non-null* C-compatible, nul-terminated string. #[no_mangle] pub unsafe extern "C" fn free_string(ptr: *const c_char) { - // Take the ownership back to rust and drop the owner + // Take the ownership back to rust and drop the owner. let _ = CString::from_raw(ptr as *mut _); } + +pub trait RawPtrTrait: Sized { + fn into_ptr(self) -> *mut Self { + Box::into_raw(Box::new(self)) + } + + unsafe fn from_ptr(raw: *mut Self) -> Option { + if raw.is_null() { + return None; + } + Some(*Box::from_raw(raw)) + } + + unsafe fn from_ptr_as_ref(raw: *mut Self) -> Option<&'static Self> { + if raw.is_null() { + return None; + } + Some(&*raw) + } + + unsafe fn from_ptr_as_box(raw: *mut Self) -> Option> { + if raw.is_null() { + return None; + } + Some(Box::from_raw(raw)) + } +} diff --git a/rust/tw_misc/Cargo.toml b/rust/tw_misc/Cargo.toml new file mode 100644 index 00000000000..ac5867c20dc --- /dev/null +++ b/rust/tw_misc/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "tw_misc" +version = "0.1.0" +edition = "2021" + +[dependencies] +zeroize = "1.6.0" diff --git a/rust/tw_misc/src/lib.rs b/rust/tw_misc/src/lib.rs new file mode 100644 index 00000000000..1f73752d7a1 --- /dev/null +++ b/rust/tw_misc/src/lib.rs @@ -0,0 +1,2 @@ +pub mod macros; +pub mod traits; diff --git a/rust/tw_misc/src/macros.rs b/rust/tw_misc/src/macros.rs new file mode 100644 index 00000000000..e75c03de50b --- /dev/null +++ b/rust/tw_misc/src/macros.rs @@ -0,0 +1,25 @@ +// Copyright © 2017-2023 Trust Wallet. +// +// This file is part of Trust. The full Trust copyright notice, including +// terms governing use, modification, and redistribution, is contained in the +// file LICENSE at the root of the source code distribution tree. + +#[macro_export] +macro_rules! try_or_false { + ($exp:expr) => {{ + match $crate::traits::IntoOption::into_option($exp) { + Some(res) => res, + None => return false, + } + }}; +} + +#[macro_export] +macro_rules! try_or_else { + ($exp:expr, $or_else:expr) => {{ + match $crate::traits::IntoOption::into_option($exp) { + Some(res) => res, + None => return $or_else(), + } + }}; +} diff --git a/rust/tw_misc/src/traits.rs b/rust/tw_misc/src/traits.rs new file mode 100644 index 00000000000..1b8900eb019 --- /dev/null +++ b/rust/tw_misc/src/traits.rs @@ -0,0 +1,40 @@ +// Copyright © 2017-2023 Trust Wallet. +// +// This file is part of Trust. The full Trust copyright notice, including +// terms governing use, modification, and redistribution, is contained in the +// file LICENSE at the root of the source code distribution tree. + +use zeroize::Zeroizing; + +pub trait ToBytesVec { + fn to_vec(&self) -> Vec; +} + +impl ToBytesVec for T +where + T: AsRef<[u8]>, +{ + fn to_vec(&self) -> Vec { + self.as_ref().to_vec() + } +} + +pub trait ToBytesZeroizing { + fn to_zeroizing_vec(&self) -> Zeroizing>; +} + +pub trait IntoOption { + fn into_option(self) -> Option; +} + +impl IntoOption for Result { + fn into_option(self) -> Option { + self.ok() + } +} + +impl IntoOption for Option { + fn into_option(self) -> Option { + self + } +} diff --git a/rust/wallet_core_rs/Cargo.toml b/rust/wallet_core_rs/Cargo.toml index a81dd65d554..87386edb3d6 100644 --- a/rust/wallet_core_rs/Cargo.toml +++ b/rust/wallet_core_rs/Cargo.toml @@ -14,4 +14,5 @@ tw_keypair = { path = "../tw_keypair" } tw_memory = { path = "../tw_memory" } tw_move_parser = { path = "../tw_move_parser" } tw_proto = { path = "../tw_proto" } +tw_bitcoin = { path = "../tw_bitcoin" } tw_starknet = { path = "../tw_starknet" } diff --git a/rust/wallet_core_rs/cbindgen.toml b/rust/wallet_core_rs/cbindgen.toml index e818f96bfbf..f2a1fc06b70 100644 --- a/rust/wallet_core_rs/cbindgen.toml +++ b/rust/wallet_core_rs/cbindgen.toml @@ -6,5 +6,5 @@ namespaces = ["TW", "Rust"] [parse] parse_deps = true -extra_bindings = ["tw_memory", "tw_encoding", "tw_hash", "tw_keypair", "tw_move_parser", "tw_proto", "tw_starknet"] -include = ["tw_memory", "tw_encoding", "tw_hash", "tw_keypair", "tw_move_parser", "tw_proto", "tw_starknet"] +extra_bindings = ["tw_bitcoin", "tw_memory", "tw_encoding", "tw_hash", "tw_keypair", "tw_move_parser", "tw_proto", "tw_starknet"] +include = ["tw_bitcoin", "tw_memory", "tw_encoding", "tw_hash", "tw_keypair", "tw_move_parser", "tw_proto", "tw_starknet"] diff --git a/rust/wallet_core_rs/src/lib.rs b/rust/wallet_core_rs/src/lib.rs index e330d6b5d4d..b3eb03421d4 100644 --- a/rust/wallet_core_rs/src/lib.rs +++ b/rust/wallet_core_rs/src/lib.rs @@ -4,6 +4,7 @@ // terms governing use, modification, and redistribution, is contained in the // file LICENSE at the root of the source code distribution tree. +pub extern crate tw_bitcoin; pub extern crate tw_encoding; pub extern crate tw_hash; pub extern crate tw_keypair; diff --git a/src/Bitcoin/Script.cpp b/src/Bitcoin/Script.cpp index 367061872af..0745c052c68 100644 --- a/src/Bitcoin/Script.cpp +++ b/src/Bitcoin/Script.cpp @@ -9,6 +9,7 @@ #include "OpCodes.h" #include "Script.h" #include "SegwitAddress.h" +#include "proto/Bitcoin.pb.h" #include #include "../BinaryCoding.h" @@ -527,5 +528,13 @@ Script Script::lockScriptForAddress(const std::string& string, enum TWCoinType c return lockScriptForAddress(string, coin); } +Proto::TransactionOutput Script::buildBRC20InscribeTransfer(const std::string& ticker, uint64_t amount, const Data& publicKey) { + TW::Bitcoin::Proto::TransactionOutput out; + auto tickerBytes = data(ticker); + Rust::CByteArrayWrapper res = TW::Rust::tw_build_brc20_inscribe_transfer(tickerBytes.data(), amount, 0, publicKey.data(), publicKey.size()); + auto result = res.data; + out.ParseFromArray(result.data(), static_cast(result.size())); + return out; +} } // namespace TW::Bitcoin diff --git a/src/Bitcoin/Script.h b/src/Bitcoin/Script.h index 100a7ada6df..e26b06c8cdf 100644 --- a/src/Bitcoin/Script.h +++ b/src/Bitcoin/Script.h @@ -10,6 +10,7 @@ #include "OpCodes.h" #include +#include "proto/Bitcoin.pb.h" #include #include @@ -114,6 +115,9 @@ class Script { /// Builds a V1 pay-to-witness-program script, P2TR (from a 32-byte Schnorr public key). static Script buildPayToV1WitnessProgram(const Data& publicKey); + /// Builds the Ordinals inscripton for BRC20 transfer. + static Proto::TransactionOutput buildBRC20InscribeTransfer(const std::string& ticker, uint64_t amount, const Data& publicKey); + /// Builds an OP_RETURN script with given data. Returns empty script on error, if data is too long (>80). static Script buildOpReturnScript(const Data& data); diff --git a/src/Bitcoin/Signer.cpp b/src/Bitcoin/Signer.cpp index 58ee1d17127..ff33e295c98 100644 --- a/src/Bitcoin/Signer.cpp +++ b/src/Bitcoin/Signer.cpp @@ -10,6 +10,7 @@ #include "Transaction.h" #include "TransactionBuilder.h" #include "TransactionSigner.h" +#include "rust/bindgen/WalletCoreRSBindgen.h" #include "proto/Common.pb.h" @@ -22,12 +23,17 @@ Proto::TransactionPlan Signer::plan(const Proto::SigningInput& input) noexcept { Proto::SigningOutput Signer::sign(const Proto::SigningInput& input, std::optional optionalExternalSigs) noexcept { Proto::SigningOutput output; + if (input.is_it_brc_operation()) { + auto serializedInput = data(input.SerializeAsString()); + Rust::CByteArrayWrapper res = Rust::tw_taproot_build_and_sign_transaction(serializedInput.data(), serializedInput.size()); + output.ParseFromArray(res.data.data(), static_cast(res.data.size())); + return output; + } auto result = TransactionSigner::sign(input, false, optionalExternalSigs); if (!result) { output.set_error(result.error()); return output; } - const auto& tx = result.payload(); *output.mutable_transaction() = tx.proto(); diff --git a/src/Bitcoin/TransactionOutput.h b/src/Bitcoin/TransactionOutput.h index 05738aafb63..10db28c12e4 100644 --- a/src/Bitcoin/TransactionOutput.h +++ b/src/Bitcoin/TransactionOutput.h @@ -9,6 +9,7 @@ #include "Amount.h" #include "Script.h" #include "Data.h" +#include "PublicKey.h" #include diff --git a/src/Cardano/Signer.cpp b/src/Cardano/Signer.cpp index 1879016e26d..f91bf1ac5ee 100644 --- a/src/Cardano/Signer.cpp +++ b/src/Cardano/Signer.cpp @@ -311,7 +311,7 @@ std::vector Signer::selectInputsWithTokens(const std::vector& // Create a simple plan, used for estimation TransactionPlan simplePlan(Amount amount, const TokenBundle& requestedTokens, const std::vector& selectedInputs, bool maxAmount, uint64_t deposit, uint64_t undeposit, const std::vector& extraOutputs) { - TransactionPlan plan{.utxos = selectedInputs, .amount = amount, .deposit = deposit, .undeposit = undeposit, .extraOutputs = extraOutputs}; + TransactionPlan plan{.utxos = selectedInputs, .extraOutputs = extraOutputs, .amount = amount, .deposit = deposit, .undeposit = undeposit}; // Sum availableAmount plan.availableAmount = 0; for (auto& u : plan.utxos) { diff --git a/src/interface/TWBitcoinScript.cpp b/src/interface/TWBitcoinScript.cpp index c79764940cd..ba9746c56d2 100644 --- a/src/interface/TWBitcoinScript.cpp +++ b/src/interface/TWBitcoinScript.cpp @@ -8,6 +8,7 @@ #include "../Bitcoin/Script.h" #include "../Bitcoin/SigHashType.h" +#include "Data.h" #include @@ -164,3 +165,12 @@ struct TWBitcoinScript *_Nonnull TWBitcoinScriptLockScriptForAddressReplay(TWStr uint32_t TWBitcoinScriptHashTypeForCoin(enum TWCoinType coinType) { return TW::Bitcoin::hashTypeForCoin(coinType); } + +TWData *_Nullable TWBitcoinScriptBuildBRC20InscribeTransfer(TWString* ticker, TWString* amount, TWData* pubkey) { + auto* brcTicker = reinterpret_cast(ticker); + auto* brcAmount = reinterpret_cast(amount); + auto* brcPubkey = reinterpret_cast(pubkey); + auto script = TW::Bitcoin::Script::buildBRC20InscribeTransfer(*brcTicker, std::stoull(*brcAmount), *brcPubkey); + auto serialized = TW::data(script.SerializeAsString()); + return TWDataCreateWithBytes(serialized.data(), serialized.size()); +} diff --git a/src/proto/Bitcoin.proto b/src/proto/Bitcoin.proto index 5cd8636d728..4d900c50eac 100644 --- a/src/proto/Bitcoin.proto +++ b/src/proto/Bitcoin.proto @@ -54,6 +54,9 @@ message TransactionOutput { // Usually contains the public key as a Bitcoin script setting up conditions to claim this output. bytes script = 2; + + // Optional spending script for P2TR script-path transactions. + bytes spendingScript = 5; } // An unspent transaction output, that can serve as input to a transaction @@ -66,6 +69,19 @@ message UnspentTransaction { // Amount of the UTXO int64 amount = 3; + + // The transaction variant + TransactionVariant variant = 4; + + // Optional spending script for P2TR script-path transactions. + bytes spendingScript = 5; +} + +enum TransactionVariant { + P2PKH = 0; + P2WPKH = 1; + P2TRKEYPATH = 2; + BRC20TRANSFER = 3; } // Pair of destination address and amount, used for extra outputs @@ -123,7 +139,7 @@ message SigningInput { // Optional zero-amount, OP_RETURN output bytes output_op_return = 13; - + // Optional additional destination addresses, additional to first to_address output repeated OutputAddress extra_outputs = 14; @@ -135,6 +151,8 @@ message SigningInput { // transaction creation time that will be used for verge(xvg) uint32 time = 17; + + bool is_it_brc_operation = 18; } // Describes a preliminary transaction plan. diff --git a/src/rust/Wrapper.h b/src/rust/Wrapper.h index 66baaa405eb..e3392401eaa 100644 --- a/src/rust/Wrapper.h +++ b/src/rust/Wrapper.h @@ -88,6 +88,11 @@ class CResult { return code == OK_CODE; } + /// Whether the result contains an error. + bool isErr() const { + return !isOk(); + } + private: ErrorCode code; std::optional inner; diff --git a/swift/Podfile.lock b/swift/Podfile.lock index 5fd52c0fed2..a8504fac8bd 100644 --- a/swift/Podfile.lock +++ b/swift/Podfile.lock @@ -13,4 +13,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: aac2324ba35cdd5631cb37618cd483887bab9cfd -COCOAPODS: 1.12.0 +COCOAPODS: 1.11.3 diff --git a/swift/Tests/Blockchains/BitcoinTests.swift b/swift/Tests/Blockchains/BitcoinTests.swift index a0f50736ef3..3d9b657ce07 100644 --- a/swift/Tests/Blockchains/BitcoinTests.swift +++ b/swift/Tests/Blockchains/BitcoinTests.swift @@ -11,6 +11,176 @@ class BitcoinTransactionSignerTests: XCTestCase { override func setUp() { continueAfterFailure = false } + + func testSignBrc20Transfer() throws { + // Successfully broadcasted: https://www.blockchain.com/explorer/transactions/btc/3e3576eb02667fac284a5ecfcb25768969680cc4c597784602d0a33ba7c654b7 + let privateKeyData = Data(hexString: "e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129")! + let fullAmount = 26400 as Int64; + let minerFee = 3000 as Int64; + let brcInscribeAmount = 7000 as Int64; + let dustSatoshis = 546 as Int64 + let forFeeAmount = fullAmount - brcInscribeAmount - minerFee; + let txIdInscription = Data.reverse(hexString: "7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca"); + let txIDForFees = Data.reverse(hexString: "797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1"); + + let privateKey = PrivateKey(data: privateKeyData)! + let publicKey = privateKey.getPublicKeySecp256k1(compressed: false) + let pubKeyHash = publicKey.bitcoinKeyHash + let bobPubkeyHash = PublicKey(data: Data(hexString: "02f453bb46e7afc8796a9629e89e07b5cb0867e9ca340b571e7bcc63fc20c43f2e")!, type: .secp256k1)!.bitcoinKeyHash + let p2wpkh = BitcoinScript.buildPayToWitnessPubkeyHash(hash: pubKeyHash) + let outputP2wpkh = BitcoinScript.buildPayToWitnessPubkeyHash(hash: bobPubkeyHash) + let outputInscribe = BitcoinScript.buildBRC20InscribeTransfer(ticker: "oadf", amount: "20", pubkey: publicKey.data) + let outputProto = try BitcoinTransactionOutput(serializedData: outputInscribe) + + var input = BitcoinSigningInput.with { + $0.isItBrcOperation = true + $0.privateKey = [privateKeyData] + } + let utxo0 = BitcoinUnspentTransaction.with { + $0.script = p2wpkh.data + $0.amount = dustSatoshis + $0.variant = .p2Wpkh + $0.outPoint.hash = txIdInscription + $0.outPoint.index = 0 + } + let utxo1 = BitcoinUnspentTransaction.with { + $0.script = p2wpkh.data + $0.amount = forFeeAmount + $0.variant = .p2Wpkh + $0.outPoint.hash = txIDForFees + $0.outPoint.index = 1 + } + input.utxo.append(utxo0) + input.utxo.append(utxo1) + + let utxos = [ + BitcoinUnspentTransaction.with { + $0.script = outputP2wpkh.data + $0.amount = dustSatoshis + $0.variant = .p2Wpkh + }, + BitcoinUnspentTransaction.with { + $0.script = p2wpkh.data + $0.amount = forFeeAmount - minerFee + $0.variant = .p2Wpkh + } + ] + + let plan = BitcoinTransactionPlan.with { + $0.utxos = utxos + } + input.plan = plan + + let output: BitcoinSigningOutput = AnySigner.sign(input: input, coin: .bitcoin) + let transactionId = output.transactionID + XCTAssertEqual(transactionId, "3e3576eb02667fac284a5ecfcb25768969680cc4c597784602d0a33ba7c654b7") + let encoded = output.encoded + XCTAssertEqual(encoded.hexString, "02000000000102ca3edda74a46877efa5364ab85947e148508713910ada23e147ea28926dc46700000000000ffffffffb11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d790100000000ffffffff022202000000000000160014e891850afc55b64aa8247b2076f8894ebdf889015834000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d024830450221008798393eb0b7390217591a8c33abe18dd2f7ea7009766e0d833edeaec63f2ec302200cf876ff52e68dbaf108a3f6da250713a9b04949a8f1dcd1fb867b24052236950121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb0248304502210096bbb9d1f0596d69875646689e46f29485e8ceccacde9d0025db87fd96d3066902206d6de2dd69d965d28df3441b94c76e812384ab9297e69afe3480ee4031e1b2060121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000"); + } + + func testSignBrc20Commit() throws { + // Successfully broadcasted: https://www.blockchain.com/explorer/transactions/btc/797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1 + let privateKeyData = Data(hexString: "e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129")! + let fullAmount = 26400 as Int64; + let minerFee = 3000 as Int64; + let brcInscribeAmount = 7000 as Int64; + let forFeeAmount = fullAmount - brcInscribeAmount - minerFee; + let txId = Data(hexString: "089098890d2653567b9e8df2d1fbe5c3c8bf1910ca7184e301db0ad3b495c88e")!; + + let privateKey = PrivateKey(data: privateKeyData)! + let publicKey = privateKey.getPublicKeySecp256k1(compressed: false) + let pubKeyHash = publicKey.bitcoinKeyHash + let p2wpkh = BitcoinScript.buildPayToWitnessPubkeyHash(hash: pubKeyHash) + let outputInscribe = BitcoinScript.buildBRC20InscribeTransfer(ticker: "oadf", amount: "20", pubkey: publicKey.data) + let outputProto = try BitcoinTransactionOutput(serializedData: outputInscribe) + + var input = BitcoinSigningInput.with { + $0.isItBrcOperation = true + $0.privateKey = [privateKeyData] + } + let utxo0 = BitcoinUnspentTransaction.with { + $0.script = p2wpkh.data + $0.amount = fullAmount + $0.variant = .p2Wpkh + $0.outPoint.hash = txId + $0.outPoint.index = 1 + } + input.utxo.append(utxo0) + + let utxos = [ + BitcoinUnspentTransaction.with { + $0.script = outputProto.script + $0.amount = brcInscribeAmount + $0.variant = .brc20Transfer + }, + BitcoinUnspentTransaction.with { + $0.script = p2wpkh.data + $0.amount = forFeeAmount + $0.variant = .p2Wpkh + } + ] + + let plan = BitcoinTransactionPlan.with { + $0.utxos = utxos + } + input.plan = plan + + let output: BitcoinSigningOutput = AnySigner.sign(input: input, coin: .bitcoin) + let transactionId = output.transactionID + XCTAssertEqual(transactionId, "797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1") + let encoded = output.encoded + XCTAssertEqual(encoded.hexString, "02000000000101089098890d2653567b9e8df2d1fbe5c3c8bf1910ca7184e301db0ad3b495c88e0100000000ffffffff02581b000000000000225120e8b706a97732e705e22ae7710703e7f589ed13c636324461afa443016134cc051040000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d02483045022100a44aa28446a9a886b378a4a65e32ad9a3108870bd725dc6105160bed4f317097022069e9de36422e4ce2e42b39884aa5f626f8f94194d1013007d5a1ea9220a06dce0121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000"); + } + + func testSignBrc20Reveal() throws { + // Successfully broadcasted: https://www.blockchain.com/explorer/transactions/btc/7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca + let privateKeyData = Data(hexString: "e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129")! + let dustSatoshis = 546 as Int64; + let brcInscribeAmount = 7000 as Int64; + let txId = Data(hexString: "b11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d79")!; + + let privateKey = PrivateKey(data: privateKeyData)! + let publicKey = privateKey.getPublicKeySecp256k1(compressed: false) + let pubKeyHash = publicKey.bitcoinKeyHash + let p2wpkh = BitcoinScript.buildPayToWitnessPubkeyHash(hash: pubKeyHash) + let outputInscribe = BitcoinScript.buildBRC20InscribeTransfer(ticker: "oadf", amount: "20", pubkey: publicKey.data) + let outputProto = try BitcoinTransactionOutput(serializedData: outputInscribe) + + var input = BitcoinSigningInput.with { + $0.isItBrcOperation = true + $0.privateKey = [privateKeyData] + } + let utxo0 = BitcoinUnspentTransaction.with { + $0.script = outputProto.script + $0.amount = brcInscribeAmount + $0.variant = .brc20Transfer + $0.spendingScript = outputProto.spendingScript + $0.outPoint.hash = txId + $0.outPoint.index = 0 + } + input.utxo.append(utxo0) + + let utxos = [ + BitcoinUnspentTransaction.with { + $0.script = p2wpkh.data + $0.amount = dustSatoshis + $0.variant = .p2Wpkh + } + ] + + let plan = BitcoinTransactionPlan.with { + $0.utxos = utxos + } + input.plan = plan + + let output: BitcoinSigningOutput = AnySigner.sign(input: input, coin: .bitcoin) + let transactionId = output.transactionID + XCTAssertEqual(transactionId, "7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca") + let encoded = output.encoded + XCTAssertTrue(encoded.hexString.hasPrefix("02000000000101b11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d790000000000ffffffff012202000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d0340")); + + XCTAssertTrue(encoded.hexString.hasSuffix("5b0063036f7264010118746578742f706c61696e3b636861727365743d7574662d3800377b2270223a226272632d3230222c226f70223a227472616e73666572222c227469636b223a226f616466222c22616d74223a223230227d6821c00f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000")); + } func testSignP2WSH() throws { // set up input diff --git a/tests/chains/Bitcoin/TWBitcoinSigningTests.cpp b/tests/chains/Bitcoin/TWBitcoinSigningTests.cpp index 93a218564f4..9e370c77baf 100644 --- a/tests/chains/Bitcoin/TWBitcoinSigningTests.cpp +++ b/tests/chains/Bitcoin/TWBitcoinSigningTests.cpp @@ -18,7 +18,9 @@ #include "PrivateKey.h" #include "TxComparisonHelper.h" #include "proto/Bitcoin.pb.h" +#include "TestUtilities.h" +#include #include #include #include @@ -80,6 +82,172 @@ SigningInput buildInputP2PKH(bool omitKey = false) { return input; } +TEST(BitcoinSigning, SignBRC20TransferCommit) { + auto privateKey = parse_hex("e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129"); + auto fullAmount = 26400; + auto minerFee = 3000; + auto brcInscribeAmount = 7000; + auto forFeeAmount = fullAmount - brcInscribeAmount - minerFee; + auto txId = parse_hex("089098890d2653567b9e8df2d1fbe5c3c8bf1910ca7184e301db0ad3b495c88e"); + + PrivateKey key(privateKey); + auto pubKey = key.getPublicKey(TWPublicKeyTypeSECP256k1); + auto utxoPubKeyHash = Hash::ripemd(Hash::sha256(pubKey.bytes)); + auto inputP2wpkh = TW::Bitcoin::Script::buildPayToWitnessPublicKeyHash(utxoPubKeyHash); + auto outputInscribe = TW::Bitcoin::Script::buildBRC20InscribeTransfer("oadf", 20, pubKey.bytes); + + Proto::SigningInput input; + input.set_is_it_brc_operation(true); + input.add_private_key(key.bytes.data(), key.bytes.size()); + input.set_coin_type(TWCoinTypeBitcoin); + + auto& utxo = *input.add_utxo(); + utxo.set_amount(fullAmount); + utxo.set_script(inputP2wpkh.bytes.data(), inputP2wpkh.bytes.size()); + utxo.set_variant(Proto::TransactionVariant::P2WPKH); + + Proto::OutPoint out; + out.set_index(1); + out.set_hash(txId.data(), txId.size()); + *utxo.mutable_out_point() = out; + + Proto::TransactionPlan plan; + auto& utxo1 = *plan.add_utxos(); + utxo1.set_amount(brcInscribeAmount); + utxo1.set_script(outputInscribe.script()); + utxo1.set_variant(Proto::TransactionVariant::BRC20TRANSFER); + + auto& utxo2 = *plan.add_utxos(); + utxo2.set_amount(forFeeAmount); + utxo2.set_script(inputP2wpkh.bytes.data(), inputP2wpkh.bytes.size()); + utxo2.set_variant(Proto::TransactionVariant::P2WPKH); + + *input.mutable_plan() = plan; + Proto::SigningOutput output; + + ANY_SIGN(input, TWCoinTypeBitcoin); + ASSERT_EQ(hex(output.encoded()), "02000000000101089098890d2653567b9e8df2d1fbe5c3c8bf1910ca7184e301db0ad3b495c88e0100000000ffffffff02581b000000000000225120e8b706a97732e705e22ae7710703e7f589ed13c636324461afa443016134cc051040000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d02483045022100a44aa28446a9a886b378a4a65e32ad9a3108870bd725dc6105160bed4f317097022069e9de36422e4ce2e42b39884aa5f626f8f94194d1013007d5a1ea9220a06dce0121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000"); + ASSERT_EQ(output.transaction_id(), "797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1"); + ASSERT_EQ(output.error(), Common::Proto::OK); + + // Successfully broadcasted: https://www.blockchain.com/explorer/transactions/btc/797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1 +} + +TEST(BitcoinSigning, SignBRC20TransferReveal) { + auto privateKey = parse_hex("e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129"); + auto dustSatoshi = 546; + auto brcInscribeAmount = 7000; + auto txId = parse_hex("b11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d79"); + + PrivateKey key(privateKey); + auto pubKey = key.getPublicKey(TWPublicKeyTypeSECP256k1); + auto utxoPubKeyHash = Hash::ripemd(Hash::sha256(pubKey.bytes)); + auto inputP2wpkh = TW::Bitcoin::Script::buildPayToWitnessPublicKeyHash(utxoPubKeyHash); + auto outputInscribe = TW::Bitcoin::Script::buildBRC20InscribeTransfer("oadf", 20, pubKey.bytes); + + Proto::SigningInput input; + input.set_is_it_brc_operation(true); + input.add_private_key(key.bytes.data(), key.bytes.size()); + input.set_coin_type(TWCoinTypeBitcoin); + + auto& utxo = *input.add_utxo(); + utxo.set_amount(brcInscribeAmount); + utxo.set_script(outputInscribe.script()); + utxo.set_variant(Proto::TransactionVariant::BRC20TRANSFER); + utxo.set_spendingscript(outputInscribe.spendingscript()); + + Proto::OutPoint out; + out.set_index(0); + out.set_hash(txId.data(), txId.size()); + *utxo.mutable_out_point() = out; + + Proto::TransactionPlan plan; + auto& utxo1 = *plan.add_utxos(); + utxo1.set_amount(dustSatoshi); + utxo1.set_script(inputP2wpkh.bytes.data(), inputP2wpkh.bytes.size()); + utxo1.set_variant(Proto::TransactionVariant::P2WPKH); + + *input.mutable_plan() = plan; + Proto::SigningOutput output; + + ANY_SIGN(input, TWCoinTypeBitcoin); + auto result = hex(output.encoded()); + ASSERT_EQ(result.substr(0, 164), "02000000000101b11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d790000000000ffffffff012202000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d0340"); + ASSERT_EQ(result.substr(292, result.size() - 292), "5b0063036f7264010118746578742f706c61696e3b636861727365743d7574662d3800377b2270223a226272632d3230222c226f70223a227472616e73666572222c227469636b223a226f616466222c22616d74223a223230227d6821c00f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000"); + ASSERT_EQ(output.transaction_id(), "7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca"); + ASSERT_EQ(output.error(), Common::Proto::OK); + + // Successfully broadcasted: https://www.blockchain.com/explorer/transactions/btc/7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca +} + +TEST(BitcoinSigning, SignBRC20TransferInscription) { + auto privateKey = parse_hex("e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129"); + auto dustSatoshi = 546; + auto brcInscribeAmount = 7000; + auto fullAmount = 26400; + auto minerFee = 3000; + auto forFeeAmount = fullAmount - brcInscribeAmount - minerFee; + auto txIDInscription = parse_hex("7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca"); + std::reverse(begin(txIDInscription), end(txIDInscription)); + auto txIDForFees = parse_hex("797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1"); + std::reverse(begin(txIDForFees), end(txIDForFees)); + + PrivateKey key(privateKey); + auto pubKey = key.getPublicKey(TWPublicKeyTypeSECP256k1); + auto utxoPubKeyHash = Hash::ripemd(Hash::sha256(pubKey.bytes)); + auto utxoPubKeyHashBob = Hash::ripemd(Hash::sha256(parse_hex("02f453bb46e7afc8796a9629e89e07b5cb0867e9ca340b571e7bcc63fc20c43f2e"))); + auto inputP2wpkh = TW::Bitcoin::Script::buildPayToWitnessPublicKeyHash(utxoPubKeyHash); + auto outputP2wpkh = TW::Bitcoin::Script::buildPayToWitnessPublicKeyHash(utxoPubKeyHashBob); + auto outputInscribe = TW::Bitcoin::Script::buildBRC20InscribeTransfer("oadf", 20, pubKey.bytes); + + Proto::SigningInput input; + input.set_is_it_brc_operation(true); + input.add_private_key(key.bytes.data(), key.bytes.size()); + input.set_coin_type(TWCoinTypeBitcoin); + + auto& utxo0 = *input.add_utxo(); + utxo0.set_amount(dustSatoshi); + utxo0.set_script(inputP2wpkh.bytes.data(), inputP2wpkh.bytes.size()); + utxo0.set_variant(Proto::TransactionVariant::P2WPKH); + + Proto::OutPoint out0; + out0.set_index(0); + out0.set_hash(txIDInscription.data(), txIDInscription.size()); + *utxo0.mutable_out_point() = out0; + + auto& utxo1 = *input.add_utxo(); + utxo1.set_amount(forFeeAmount); + utxo1.set_script(inputP2wpkh.bytes.data(), inputP2wpkh.bytes.size()); + utxo1.set_variant(Proto::TransactionVariant::P2WPKH); + + Proto::OutPoint out1; + out1.set_index(1); + out1.set_hash(txIDForFees.data(), txIDForFees.size()); + *utxo1.mutable_out_point() = out1; + + Proto::TransactionPlan plan; + auto& utxo2 = *plan.add_utxos(); + utxo2.set_amount(dustSatoshi); + utxo2.set_script(outputP2wpkh.bytes.data(), outputP2wpkh.bytes.size()); + utxo2.set_variant(Proto::TransactionVariant::P2WPKH); + + auto& utxo3 = *plan.add_utxos(); + utxo3.set_amount(forFeeAmount - minerFee); + utxo3.set_script(inputP2wpkh.bytes.data(), inputP2wpkh.bytes.size()); + utxo3.set_variant(Proto::TransactionVariant::P2WPKH); + + *input.mutable_plan() = plan; + Proto::SigningOutput output; + + ANY_SIGN(input, TWCoinTypeBitcoin); + auto result = hex(output.encoded()); + ASSERT_EQ(hex(output.encoded()), "02000000000102ca3edda74a46877efa5364ab85947e148508713910ada23e147ea28926dc46700000000000ffffffffb11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d790100000000ffffffff022202000000000000160014e891850afc55b64aa8247b2076f8894ebdf889015834000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d024830450221008798393eb0b7390217591a8c33abe18dd2f7ea7009766e0d833edeaec63f2ec302200cf876ff52e68dbaf108a3f6da250713a9b04949a8f1dcd1fb867b24052236950121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb0248304502210096bbb9d1f0596d69875646689e46f29485e8ceccacde9d0025db87fd96d3066902206d6de2dd69d965d28df3441b94c76e812384ab9297e69afe3480ee4031e1b2060121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000"); + ASSERT_EQ(output.transaction_id(), "3e3576eb02667fac284a5ecfcb25768969680cc4c597784602d0a33ba7c654b7"); + ASSERT_EQ(output.error(), Common::Proto::OK); + + // Successfully broadcasted: https://www.blockchain.com/explorer/transactions/btc/3e3576eb02667fac284a5ecfcb25768969680cc4c597784602d0a33ba7c654b7 +} + TEST(BitcoinSigning, SignP2PKH) { auto input = buildInputP2PKH(); diff --git a/tools/build-and-test b/tools/build-and-test index d02b887e424..bd4ba92c6cf 100755 --- a/tools/build-and-test +++ b/tools/build-and-test @@ -7,7 +7,7 @@ set -e echo "#### Generating files... ####" -tools/generate-files +tools/generate-files native echo "#### Building... ####" cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ diff --git a/tools/generate-files b/tools/generate-files index 2f92cab109b..35616f51fe7 100755 --- a/tools/generate-files +++ b/tools/generate-files @@ -56,7 +56,12 @@ tools/doxygen_convert_comments tools/rust-bindgen $1 # Generate Java, C++ and Swift Protobuf files -if [ -x "$(command -v protoc-gen-swift)" ] && [ $# -eq 0 ]; then +IOS="false" +if [[ "$1" == "ios" ]] || [[ "$1" == "" ]]; then + IOS="true" +fi + +if [ -x "$(command -v protoc-gen-swift)" ] && [[ "$IOS" == "true" ]]; then "$PROTOC" -I=$PREFIX/include -I=src/proto --cpp_out=src/proto --java_out=lite:jni/proto --swift_out=swift/Sources/Generated/Protobuf --swift_opt=Visibility=Public src/proto/*.proto else "$PROTOC" -I=$PREFIX/include -I=src/proto --cpp_out=src/proto --java_out=lite:jni/proto src/proto/*.proto @@ -74,7 +79,7 @@ fi "$PROTOC" -I=$PREFIX/include -I=src/proto --plugin=$PREFIX/bin/protoc-gen-swift-typealias --swift-typealias_out swift/Sources/Generated/Protobuf src/proto/*.proto # Generate Xcode project -if [ -x "$(command -v xcodegen)" ] && [ $# -eq 0 ]; then +if [ -x "$(command -v xcodegen)" ] && [[ "$IOS" == "true" ]]; then pushd swift xcodegen pod install diff --git a/tools/rust-bindgen b/tools/rust-bindgen index 6c8350cf7f4..431df1a44e9 100755 --- a/tools/rust-bindgen +++ b/tools/rust-bindgen @@ -7,6 +7,7 @@ TARGET_XCFRAMEWORK_NAME=../swift/WalletCoreRs.xcframework BUILD_FOLDER=../rust/target CRATE="wallet-core-rs" HEADER_NAME="WalletCoreRSBindgen.h" +NDK_API_LEVEL=21 create_xc_framework() { rm -rf $TARGET_XCFRAMEWORK_NAME @@ -15,35 +16,103 @@ create_xc_framework() { cp $BUILD_FOLDER/catalyst/$TARGET_NAME $TARGET_XCFRAMEWORK_NAME/ios-arm64_x86_64-maccatalyst } +find_android_ndk() { + if [[ "$ANDROID_NDK_HOME" != "" ]]; then + echo "Use ANDROID_NDK_HOME" + elif [[ "$ANDROID_HOME" != "" ]]; then + echo "ANDROID_NDK_HOME is not set. Use ANDROID_HOME value instead" + ANDROID_NDK_HOME="$ANDROID_HOME/ndk" + else + echo "WARNING: ANDROID_HOME is not set. Use a default path" + ANDROID_NDK_HOME="$HOME/Library/Android/sdk/ndk" + fi + + TEST_CLANG="aarch64-linux-android$NDK_API_LEVEL-clang" + PATH_TO_CLANG=$(find "$ANDROID_NDK_HOME" -iname $TEST_CLANG -print -quit) + + if [[ "$PATH_TO_CLANG" == "" ]]; then + echo "ERROR: cannot find NDK tools within '$ANDROID_NDK_HOME'" + return 1 + fi + + NDK_BIN_PATH=$(dirname "$PATH_TO_CLANG") + export NDK_BIN_PATH + + return 0 +} + cd rust -if [[ "$1" != "android" ]]; then - echo "Generating Native targets" +NATIVE="false" +ANDROID="false" +IOS="false" +WASM="false" + +# Whether to generate bindings for native platform. +if [[ "$1" == "native" ]] || [[ "$1" == "" ]]; then + NATIVE="true" +fi + +# Generate bindings for mobile platforms on MacOS only. +if [[ `uname` == "Darwin" ]]; then + # Whether to generate bindings for Android. + if [[ "$1" == "android" ]] || [[ "$1" == "" ]]; then + ANDROID="true" + fi + +# Whether to generate bindings for iOS. + if [[ "$1" == "ios" ]] || [[ "$1" == "" ]]; then + IOS="true" + fi +fi + +# Whether to generate bindings for WASM. +if [[ "$1" == "wasm" ]] || [[ "$1" == "" ]]; then + WASM="true" +fi + +if [[ "$NATIVE" == "true" ]]; then + echo "Generating Native target" cargo build --release +fi + +if [[ "$WASM" == "true" ]]; then + echo "Generating WASM target" cargo build --target wasm32-unknown-emscripten --release fi -if [[ `uname` == "Darwin" ]]; then + +if [[ "$ANDROID" == "true" ]]; then + find_android_ndk + + export AR="$NDK_BIN_PATH/llvm-ar" + export CC_aarch64_linux_android="$NDK_BIN_PATH/aarch64-linux-android$NDK_API_LEVEL-clang" + export CC_x86_64_linux_android="$NDK_BIN_PATH/x86_64-linux-android$NDK_API_LEVEL-clang" + export CC_i686_linux_android="$NDK_BIN_PATH/i686-linux-android$NDK_API_LEVEL-clang" + export CC_armv7_linux_androideabi="$NDK_BIN_PATH/armv7a-linux-androideabi$NDK_API_LEVEL-clang" + echo "Generating Android targets" cargo build --target aarch64-linux-android --target armv7-linux-androideabi --target x86_64-linux-android --target i686-linux-android --release - if [[ "$1" != "android" ]]; then - echo "Generating iOS targets" - cargo build --target aarch64-apple-ios --target aarch64-apple-ios-sim --target x86_64-apple-ios --target aarch64-apple-darwin --target x86_64-apple-darwin --release & - cargo +nightly build -Z build-std --target aarch64-apple-ios-macabi --target x86_64-apple-ios-macabi --release & - wait - lipo $BUILD_FOLDER/x86_64-apple-ios/release/$TARGET_NAME $BUILD_FOLDER/aarch64-apple-ios-sim/release/$TARGET_NAME -create -output $BUILD_FOLDER/$TARGET_NAME - mkdir -p $BUILD_FOLDER/darwin_universal - lipo $BUILD_FOLDER/x86_64-apple-darwin/release/$TARGET_NAME $BUILD_FOLDER/aarch64-apple-darwin/release/$TARGET_NAME -create -output $BUILD_FOLDER/darwin_universal/$TARGET_NAME - mkdir -p $BUILD_FOLDER/catalyst - lipo $BUILD_FOLDER/aarch64-apple-ios-macabi/release/$TARGET_NAME $BUILD_FOLDER/x86_64-apple-ios-macabi/release/$TARGET_NAME -create -output $BUILD_FOLDER/catalyst/$TARGET_NAME - - create_xc_framework - fi fi + +if [[ "$IOS" == "true" ]]; then + echo "Generating iOS targets" + cargo build --target aarch64-apple-ios --target aarch64-apple-ios-sim --target x86_64-apple-ios --target aarch64-apple-darwin --target x86_64-apple-darwin --release & + cargo +nightly build -Z build-std --target aarch64-apple-ios-macabi --target x86_64-apple-ios-macabi --release & + wait + lipo $BUILD_FOLDER/x86_64-apple-ios/release/$TARGET_NAME $BUILD_FOLDER/aarch64-apple-ios-sim/release/$TARGET_NAME -create -output $BUILD_FOLDER/$TARGET_NAME + mkdir -p $BUILD_FOLDER/darwin_universal + lipo $BUILD_FOLDER/x86_64-apple-darwin/release/$TARGET_NAME $BUILD_FOLDER/aarch64-apple-darwin/release/$TARGET_NAME -create -output $BUILD_FOLDER/darwin_universal/$TARGET_NAME + mkdir -p $BUILD_FOLDER/catalyst + lipo $BUILD_FOLDER/aarch64-apple-ios-macabi/release/$TARGET_NAME $BUILD_FOLDER/x86_64-apple-ios-macabi/release/$TARGET_NAME -create -output $BUILD_FOLDER/catalyst/$TARGET_NAME + + create_xc_framework +fi + cbindgen --crate $CRATE --output ../src/rust/bindgen/$HEADER_NAME cd - [[ -e rust/target/release/${TARGET_NAME} ]] && cp rust/target/release/${TARGET_NAME} build/local/lib/ -if [[ `uname` == "Darwin" ]] && [[ "$1" != "android" ]]; then +if [[ "$IOS" == "true" ]]; then cd rust cat > $TARGET_XCFRAMEWORK_NAME/Info.plist << EOF diff --git a/wasm/tests/Blockchain/Bitcoin.test.ts b/wasm/tests/Blockchain/Bitcoin.test.ts index 4cad6e61e5d..d516512bb54 100644 --- a/wasm/tests/Blockchain/Bitcoin.test.ts +++ b/wasm/tests/Blockchain/Bitcoin.test.ts @@ -7,10 +7,199 @@ import "mocha"; import { assert } from "chai"; import { TW } from "../../dist"; +import Long = require("long"); describe("Bitcoin", () => { it("test Bitcoin SigningInput / SigningOutput", () => { assert.isNotNull(TW.Bitcoin.Proto.SigningInput); assert.isNotNull(TW.Binance.Proto.SigningOutput); }); + + // Successfully broadcasted: https://www.blockchain.com/explorer/transactions/btc/3e3576eb02667fac284a5ecfcb25768969680cc4c597784602d0a33ba7c654b7 + it("test Bitcoin sign BRC20 Transfer", () => { + const { AnySigner, BitcoinScript, PrivateKey, PublicKey, PublicKeyType, HexCoding, Hash, CoinType } = globalThis.core; + + const privateKeyData = HexCoding.decode("e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129"); + const fullAmount = 26400 + const minerFee = 3000 + const brcInscribeAmount = 7000 + const dustSatoshis = 546 + const forFeeAmount = fullAmount - brcInscribeAmount - minerFee + const txIdInscription = HexCoding.decode("7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca").reverse() + const txIDForFees = HexCoding.decode("797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1").reverse() + + const privateKey = PrivateKey.createWithData(privateKeyData) + const publicKey = privateKey.getPublicKeySecp256k1(true) + const pubKeyHash = Hash.ripemd(Hash.sha256(publicKey.data())) + const bobPubkey = PublicKey.createWithData(HexCoding.decode("02f453bb46e7afc8796a9629e89e07b5cb0867e9ca340b571e7bcc63fc20c43f2e"), PublicKeyType.secp256k1) + const bobPubkeyHash = Hash.ripemd(Hash.sha256(bobPubkey.data())) + + const p2wpkh = BitcoinScript.buildPayToWitnessPubkeyHash(pubKeyHash) + const outputP2wpkh = BitcoinScript.buildPayToWitnessPubkeyHash(bobPubkeyHash) + + const utxo0 = TW.Bitcoin.Proto.UnspentTransaction.create({ + script: p2wpkh.data(), + amount: new Long(dustSatoshis), + variant: TW.Bitcoin.Proto.TransactionVariant.P2WPKH, + outPoint: TW.Bitcoin.Proto.OutPoint.create({ + hash: txIdInscription, + index: 0, + }) + }) + const utxo1 = TW.Bitcoin.Proto.UnspentTransaction.create({ + script: p2wpkh.data(), + amount: new Long(forFeeAmount), + variant: TW.Bitcoin.Proto.TransactionVariant.P2WPKH, + outPoint: TW.Bitcoin.Proto.OutPoint.create({ + hash: txIDForFees, + index: 1, + }) + }) + + const utxoPlan0 = TW.Bitcoin.Proto.UnspentTransaction.create({ + script: outputP2wpkh.data(), + amount: new Long(dustSatoshis), + variant: TW.Bitcoin.Proto.TransactionVariant.P2WPKH + }) + const utxoPlan1 = TW.Bitcoin.Proto.UnspentTransaction.create({ + script: p2wpkh.data(), + amount: new Long(forFeeAmount - minerFee), + variant: TW.Bitcoin.Proto.TransactionVariant.P2WPKH + }) + + const input = TW.Bitcoin.Proto.SigningInput.create({ + isItBrcOperation: true, + privateKey: [privateKeyData], + utxo: [utxo0, utxo1], + plan: TW.Bitcoin.Proto.TransactionPlan.create({ + utxos: [utxoPlan0, utxoPlan1] + }) + }) + + const encoded = TW.Bitcoin.Proto.SigningInput.encode(input).finish(); + const outputData = AnySigner.sign(encoded, CoinType.bitcoin); + const output = TW.Bitcoin.Proto.SigningOutput.decode(outputData); + assert.equal( + HexCoding.encode(output.encoded), + "0x02000000000102ca3edda74a46877efa5364ab85947e148508713910ada23e147ea28926dc46700000000000ffffffffb11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d790100000000ffffffff022202000000000000160014e891850afc55b64aa8247b2076f8894ebdf889015834000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d024830450221008798393eb0b7390217591a8c33abe18dd2f7ea7009766e0d833edeaec63f2ec302200cf876ff52e68dbaf108a3f6da250713a9b04949a8f1dcd1fb867b24052236950121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb0248304502210096bbb9d1f0596d69875646689e46f29485e8ceccacde9d0025db87fd96d3066902206d6de2dd69d965d28df3441b94c76e812384ab9297e69afe3480ee4031e1b2060121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000" + ); + assert.equal(output.transactionId, "3e3576eb02667fac284a5ecfcb25768969680cc4c597784602d0a33ba7c654b7"); + }); + + // Successfully broadcasted: https://www.blockchain.com/explorer/transactions/btc/797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1 + it("test Bitcoin sign BRC20 Commit", () => { + const { AnySigner, BitcoinScript, PrivateKey, HexCoding, Hash, CoinType } = globalThis.core; + + const privateKeyData = HexCoding.decode("e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129"); + const fullAmount = 26400 + const minerFee = 3000 + const brcInscribeAmount = 7000 + const forFeeAmount = fullAmount - brcInscribeAmount - minerFee + const txId = HexCoding.decode("089098890d2653567b9e8df2d1fbe5c3c8bf1910ca7184e301db0ad3b495c88e") + const brc20Ticker = "oadf" + const brc20Amount = "20" + + const privateKey = PrivateKey.createWithData(privateKeyData) + const publicKey = privateKey.getPublicKeySecp256k1(true) + const pubKeyHash = Hash.ripemd(Hash.sha256(publicKey.data())) + + const p2wpkh = BitcoinScript.buildPayToWitnessPubkeyHash(pubKeyHash) + const outputInscribe = BitcoinScript.buildBRC20InscribeTransfer(brc20Ticker, brc20Amount, publicKey.data()) + const outputInscribeProto = TW.Bitcoin.Proto.TransactionOutput.decode(outputInscribe) + + const utxo0 = TW.Bitcoin.Proto.UnspentTransaction.create({ + script: p2wpkh.data(), + amount: new Long(fullAmount), + variant: TW.Bitcoin.Proto.TransactionVariant.P2WPKH, + outPoint: TW.Bitcoin.Proto.OutPoint.create({ + hash: txId, + index: 1, + }) + }) + + const utxoPlan0 = TW.Bitcoin.Proto.UnspentTransaction.create({ + script: outputInscribeProto.script, + amount: new Long(brcInscribeAmount), + variant: TW.Bitcoin.Proto.TransactionVariant.BRC20TRANSFER + }) + const utxoPlan1 = TW.Bitcoin.Proto.UnspentTransaction.create({ + script: p2wpkh.data(), + amount: new Long(forFeeAmount), + variant: TW.Bitcoin.Proto.TransactionVariant.P2WPKH + }) + + const input = TW.Bitcoin.Proto.SigningInput.create({ + isItBrcOperation: true, + privateKey: [privateKeyData], + utxo: [utxo0], + plan: TW.Bitcoin.Proto.TransactionPlan.create({ + utxos: [utxoPlan0, utxoPlan1] + }) + }) + + const encoded = TW.Bitcoin.Proto.SigningInput.encode(input).finish(); + const outputData = AnySigner.sign(encoded, CoinType.bitcoin); + const output = TW.Bitcoin.Proto.SigningOutput.decode(outputData); + assert.equal( + HexCoding.encode(output.encoded), + "0x02000000000101089098890d2653567b9e8df2d1fbe5c3c8bf1910ca7184e301db0ad3b495c88e0100000000ffffffff02581b000000000000225120e8b706a97732e705e22ae7710703e7f589ed13c636324461afa443016134cc051040000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d02483045022100a44aa28446a9a886b378a4a65e32ad9a3108870bd725dc6105160bed4f317097022069e9de36422e4ce2e42b39884aa5f626f8f94194d1013007d5a1ea9220a06dce0121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000" + ); + assert.equal(output.transactionId, "797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1"); + }); + + // Successfully broadcasted: https://www.blockchain.com/explorer/transactions/btc/7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca + it("test Bitcoin sign BRC20 Reveal", () => { + const { AnySigner, BitcoinScript, PrivateKey, HexCoding, Hash, CoinType } = globalThis.core; + + const privateKeyData = HexCoding.decode("e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129"); + const fullAmount = 26400 + const minerFee = 3000 + const brcInscribeAmount = 7000 + const dustSatoshis = 546 + const txId = HexCoding.decode("b11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d79") + const brc20Ticker = "oadf" + const brc20Amount = "20" + + const privateKey = PrivateKey.createWithData(privateKeyData) + const publicKey = privateKey.getPublicKeySecp256k1(true) + const pubKeyHash = Hash.ripemd(Hash.sha256(publicKey.data())) + + const p2wpkh = BitcoinScript.buildPayToWitnessPubkeyHash(pubKeyHash) + const outputInscribe = BitcoinScript.buildBRC20InscribeTransfer(brc20Ticker, brc20Amount, publicKey.data()) + const outputInscribeProto = TW.Bitcoin.Proto.TransactionOutput.decode(outputInscribe) + + const utxo0 = TW.Bitcoin.Proto.UnspentTransaction.create({ + script: outputInscribeProto.script, + amount: new Long(brcInscribeAmount), + variant: TW.Bitcoin.Proto.TransactionVariant.BRC20TRANSFER, + spendingScript: outputInscribeProto.spendingScript, + outPoint: TW.Bitcoin.Proto.OutPoint.create({ + hash: txId, + index: 0, + }) + }) + + const utxoPlan0 = TW.Bitcoin.Proto.UnspentTransaction.create({ + script: p2wpkh.data(), + amount: new Long(dustSatoshis), + variant: TW.Bitcoin.Proto.TransactionVariant.P2WPKH + }) + + const input = TW.Bitcoin.Proto.SigningInput.create({ + isItBrcOperation: true, + privateKey: [privateKeyData], + utxo: [utxo0], + plan: TW.Bitcoin.Proto.TransactionPlan.create({ + utxos: [utxoPlan0] + }) + }) + + const encoded = TW.Bitcoin.Proto.SigningInput.encode(input).finish(); + const outputData = AnySigner.sign(encoded, CoinType.bitcoin); + const output = TW.Bitcoin.Proto.SigningOutput.decode(outputData); + const outputHex = HexCoding.encode(output.encoded) + assert.isTrue(outputHex.startsWith("0x02000000000101b11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d790000000000ffffffff012202000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d0340")) + assert.isTrue(outputHex.endsWith("5b0063036f7264010118746578742f706c61696e3b636861727365743d7574662d3800377b2270223a226272632d3230222c226f70223a227472616e73666572222c227469636b223a226f616466222c22616d74223a223230227d6821c00f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000")) + assert.equal(output.transactionId, "7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca"); + }); });