diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/TestCoinType.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/TestCoinType.kt index d565a1a08be..6c518ea54e7 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/TestCoinType.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/TestCoinType.kt @@ -52,9 +52,9 @@ class TestCoinType { fun testDerivationPath() { var res = CoinType.createFromValue(CoinType.BITCOIN.value()).derivationPath().toString() assertEquals(res, "m/84'/0'/0'/0/0") - res = CoinType.createFromValue(CoinType.BITCOIN.value()).derivationPathWithDerivation(Derivation.BITCOINLEGACY).toString() + res = CoinType.createFromValue(CoinType.BITCOIN.value()).derivationPathWithDerivation(Derivation.LEGACY).toString() assertEquals(res, "m/44'/0'/0'/0/0") - res = CoinType.createFromValue(CoinType.SOLANA.value()).derivationPathWithDerivation(Derivation.SOLANASOLANA).toString() + res = CoinType.createFromValue(CoinType.SOLANA.value()).derivationPathWithDerivation(Derivation.SOLANA).toString() assertEquals(res, "m/44'/501'/0'/0'") } @@ -62,7 +62,7 @@ class TestCoinType { fun testDeriveAddressFromPublicKeyAndDerivation() { val publicKey = PublicKey("0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798".toHexByteArray(), PublicKeyType.SECP256K1) - val address = CoinType.BITCOIN.deriveAddressFromPublicKeyAndDerivation(publicKey, Derivation.BITCOINSEGWIT) + val address = CoinType.BITCOIN.deriveAddressFromPublicKeyAndDerivation(publicKey, Derivation.SEGWIT) assertEquals(address, "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") } } diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoin/TestBitcoinFee.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoin/TestBitcoinFee.kt deleted file mode 100644 index 15d4ee2d593..00000000000 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/bitcoin/TestBitcoinFee.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.trustwallet.core.app.blockchains.bitcoin - -import com.trustwallet.core.app.utils.Numeric -import com.trustwallet.core.app.utils.toHexBytes -import org.junit.Assert.assertEquals -import org.junit.Test -import wallet.core.jni.BitcoinFee - -class TestBitcoinFee { - - init { - System.loadLibrary("TrustWalletCore") - } - - @Test - fun P2pkhCalculateFee() { - val satVb = "10" - val tx = Numeric.hexStringToByteArray("02000000017be4e642bb278018ab12277de9427773ad1c5f5b1d164a157e0d99aa48dc1c1e000000006a473044022078eda020d4b86fcb3af78ef919912e6d79b81164dbbb0b0b96da6ac58a2de4b102201a5fd8d48734d5a02371c4b5ee551a69dca3842edbf577d863cf8ae9fdbbd4590121036666dd712e05a487916384bfcd5973eb53e8038eccbbf97f7eed775b87389536ffffffff01c0aff629010000001976a9145eaaa4f458f9158f86afcba08dd7448d27045e3d88ac00000000") - var fee = BitcoinFee.calculateFee(tx, satVb).toLong() - - assertEquals(fee, 191 * satVb.toLong()) - } - - @Test - fun P2wpkhCalculateFee() { - val satVb = "12" - val tx = Numeric.hexStringToByteArray("020000000111b9f62923af73e297abb69f749e7a1aa2735fbdfd32ac5f6aa89e5c96841c18000000006b483045022100df9ed0b662b759e68b89a42e7144cddf787782a7129d4df05642dd825930e6e6022051a08f577f11cc7390684bbad2951a6374072253ffcf2468d14035ed0d8cd6490121028d7dce6d72fb8f7af9566616c6436349c67ad379f2404dd66fe7085fe0fba28fffffffff01c0aff629010000001600140d0e1cec6c2babe8badde5e9b3dea667da90036d00000000") - var fee = BitcoinFee.calculateFee(tx, satVb).toLong() - - assertEquals(fee, 189 * satVb.toLong()) - } - - @Test - // Metadata can be observed live on: - // https://www.blockchain.com/explorer/transactions/btc/797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1 - // - // Fee/VB 19.608 sat/vByte - // Size 235 Bytes - // Weight 610 - fun Brc20TransferCommitCalculateFee() { - val satVb = "19" - val tx = Numeric.hexStringToByteArray("02000000000101089098890d2653567b9e8df2d1fbe5c3c8bf1910ca7184e301db0ad3b495c88e0100000000ffffffff02581b000000000000225120e8b706a97732e705e22ae7710703e7f589ed13c636324461afa443016134cc051040000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d02483045022100a44aa28446a9a886b378a4a65e32ad9a3108870bd725dc6105160bed4f317097022069e9de36422e4ce2e42b39884aa5f626f8f94194d1013007d5a1ea9220a06dce0121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000") - var fee = BitcoinFee.calculateFee(tx, satVb).toLong() - - assertEquals(fee, 153 * satVb.toLong()) // 153 = ceil(610/4) - } -} 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 0aa1ddac322..9d6bcdab802 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 @@ -16,6 +16,7 @@ import wallet.core.jni.PublicKey import wallet.core.jni.PublicKeyType import wallet.core.jni.proto.Bitcoin import wallet.core.jni.proto.Bitcoin.SigningOutput +import wallet.core.jni.proto.BitcoinV2 import wallet.core.jni.proto.Common.SigningError class TestBitcoinSigning { @@ -160,842 +161,196 @@ class TestBitcoinSigning { 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 dustSatoshis = 546.toLong() + val txId = Numeric.hexStringToByteArray("8ec895b4d30adb01e38471ca1019bfc8c3e5fbd1f28d9e7b5653260d89989008").reversedArray() 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()) + val publicKey = ByteString.copyFrom(privateKey.getPublicKeySecp256k1(true).data()) + + val utxo0 = BitcoinV2.Input.newBuilder() + .setOutPoint(BitcoinV2.OutPoint.newBuilder().apply { + hash = ByteString.copyFrom(txId) + vout = 1 + }) + .setValue(26_400) + .setSighashType(BitcoinSigHashType.ALL.value()) + .setScriptBuilder(BitcoinV2.Input.InputBuilder.newBuilder().apply { + p2Wpkh = BitcoinV2.PublicKeyOrHash.newBuilder().setPubkey(publicKey).build() + }) + + val out0 = BitcoinV2.Output.newBuilder() + .setValue(7_000) + .setBuilder(BitcoinV2.Output.OutputBuilder.newBuilder().apply { + brc20Inscribe = BitcoinV2.Output.OutputBrc20Inscription.newBuilder().apply { + inscribeTo = publicKey + ticker = "oadf" + transferAmount = "20" + }.build() + }) + + val changeOutput = BitcoinV2.Output.newBuilder() + .setValue(16_400) + .setBuilder(BitcoinV2.Output.OutputBuilder.newBuilder().apply { + p2Wpkh = BitcoinV2.PublicKeyOrHash.newBuilder().setPubkey(publicKey).build() + }) + + val signingInput = BitcoinV2.SigningInput.newBuilder() + .setVersion(BitcoinV2.TransactionVersion.V2) + .addPrivateKeys(ByteString.copyFrom(privateKeyData)) + .addInputs(utxo0) + .addOutputs(out0) + .addOutputs(changeOutput) + .setInputSelector(BitcoinV2.InputSelector.UseAll) + .setChainInfo(BitcoinV2.ChainInfo.newBuilder().apply { + p2PkhPrefix = 0 + p2ShPrefix = 5 + }) + .setDangerousUseFixedSchnorrRng(true) + .setFixedDustThreshold(dustSatoshis) + .build() + + val legacySigningInput = Bitcoin.SigningInput.newBuilder().apply { + signingV2 = signingInput + } + + val output = AnySigner.sign(legacySigningInput.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) + assertEquals(output.signingResultV2.error, SigningError.OK) + assertEquals(Numeric.toHexString(output.signingResultV2.encoded.toByteArray()), "0x02000000000101089098890d2653567b9e8df2d1fbe5c3c8bf1910ca7184e301db0ad3b495c88e0100000000ffffffff02581b000000000000225120e8b706a97732e705e22ae7710703e7f589ed13c636324461afa443016134cc051040000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d02483045022100a44aa28446a9a886b378a4a65e32ad9a3108870bd725dc6105160bed4f317097022069e9de36422e4ce2e42b39884aa5f626f8f94194d1013007d5a1ea9220a06dce0121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000") + assertEquals(Numeric.toHexString(output.signingResultV2.txid.toByteArray()), "0x797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1") } @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) - } - - @Test - fun testSignNftInscriptionCommit() { - // Successfully broadcasted: https://www.blockchain.com/explorer/transactions/btc/f1e708e5c5847339e16accf8716c14b33717c14d6fe68f9db36627cecbde7117 - - val privateKeyData = (Numeric.hexStringToByteArray("e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129")) - val fullAmount = 32400 - val minerFee = 1300 - val inscribeAmount = fullAmount - minerFee; - val txId = Numeric.hexStringToByteArray("992faa0d60f29d77cdae687c300d288a3b075b3c7e1e3b42ad537222c3909557") - val payload = Numeric.hexStringToByteArray(nftInscriptionImageData) + val dustSatoshis = 546.toLong() + val txIdCommit = Numeric.hexStringToByteArray("797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1").reversedArray() 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.buildOrdinalNftInscription("image/png", payload, 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(0) - .build() - - val utxo = Bitcoin.UnspentTransaction.newBuilder() - .setScript(ByteString.copyFrom(p2wpkh.data())) - .setAmount(fullAmount.toLong()) - .setVariant(Bitcoin.TransactionVariant.P2WPKH) - .setOutPoint(unspentOutputPoint) - .build() - input.addUtxo(utxo) - - val utxoPlan = Bitcoin.UnspentTransaction.newBuilder() - .setScript(outputInscribeProto.script) - .setAmount(inscribeAmount.toLong()) - .setVariant(Bitcoin.TransactionVariant.NFTINSCRIPTION) - .build() - - val plan = Bitcoin.TransactionPlan.newBuilder() - .addUtxos(utxoPlan) - .build() - input.plan = plan - - val output = AnySigner.sign(input.build(), BITCOIN, SigningOutput.parser()) + val publicKey = ByteString.copyFrom(privateKey.getPublicKeySecp256k1(true).data()) + + val utxo0 = BitcoinV2.Input.newBuilder() + .setOutPoint(BitcoinV2.OutPoint.newBuilder().apply { + hash = ByteString.copyFrom(txIdCommit) + vout = 0 + }) + .setValue(7_000) + .setSighashType(BitcoinSigHashType.ALL.value()) + .setScriptBuilder(BitcoinV2.Input.InputBuilder.newBuilder().apply { + brc20Inscribe = BitcoinV2.Input.InputBrc20Inscription.newBuilder().apply { + inscribeTo = publicKey + ticker = "oadf" + transferAmount = "20" + }.build() + }) + + val out0 = BitcoinV2.Output.newBuilder() + .setValue(dustSatoshis) + .setBuilder(BitcoinV2.Output.OutputBuilder.newBuilder().apply { + p2Wpkh = BitcoinV2.PublicKeyOrHash.newBuilder().setPubkey(publicKey).build() + }) + + val signingInput = BitcoinV2.SigningInput.newBuilder() + .setVersion(BitcoinV2.TransactionVersion.V2) + .addPrivateKeys(ByteString.copyFrom(privateKeyData)) + .addInputs(utxo0) + .addOutputs(out0) + .setInputSelector(BitcoinV2.InputSelector.UseAll) + .setChainInfo(BitcoinV2.ChainInfo.newBuilder().apply { + p2PkhPrefix = 0 + p2ShPrefix = 5 + }) + .setDangerousUseFixedSchnorrRng(true) + .setFixedDustThreshold(dustSatoshis) + .build() + + val legacySigningInput = Bitcoin.SigningInput.newBuilder().apply { + signingV2 = signingInput + } + + val output = AnySigner.sign(legacySigningInput.build(), BITCOIN, SigningOutput.parser()) assertEquals(output.error, SigningError.OK) - assertEquals(output.transactionId, "f1e708e5c5847339e16accf8716c14b33717c14d6fe68f9db36627cecbde7117") - assertEquals(Numeric.toHexString(output.encoded.toByteArray()), "0x02000000000101992faa0d60f29d77cdae687c300d288a3b075b3c7e1e3b42ad537222c39095570000000000ffffffff017c790000000000002251202ac69a7e9dba801e9fcba826055917b84ca6fba4d51a29e47d478de603eedab602473044022054212984443ed4c66fc103d825bfd2da7baf2ab65d286e3c629b36b98cd7debd022050214cfe5d3b12a17aaaf1a196bfeb2f0ad15ffb320c4717eb7614162453e4fe0121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000") - - val signedTransaction = output.transaction - assert(signedTransaction.isInitialized) + assertEquals(output.signingResultV2.error, SigningError.OK) + assertEquals(Numeric.toHexString(output.signingResultV2.encoded.toByteArray()), "0x02000000000101b11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d790000000000ffffffff012202000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d03406a35548b8fa4620028e021a944c1d3dc6e947243a7bfc901bf63fefae0d2460efa149a6440cab51966aa4f09faef2d1e5efcba23ab4ca6e669da598022dbcfe35b0063036f7264010118746578742f706c61696e3b636861727365743d7574662d3800377b2270223a226272632d3230222c226f70223a227472616e73666572222c227469636b223a226f616466222c22616d74223a223230227d6821c00f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000") + assertEquals(Numeric.toHexString(output.signingResultV2.txid.toByteArray()), "0x7046dc2689a27e143ea2ad1039710885147e9485ab6453fa7e87464aa7dd3eca") } @Test - fun testSignNftInscriptionReveal() { - // Successfully broadcasted: https://www.blockchain.com/explorer/transactions/btc/173f8350b722243d44cc8db5584de76b432eb6d0888d9e66e662db51584f44ac - + fun testSignBrc20Transfer() { + // Successfully broadcasted: https://www.blockchain.com/explorer/transactions/btc/3e3576eb02667fac284a5ecfcb25768969680cc4c597784602d0a33ba7c654b7 val privateKeyData = (Numeric.hexStringToByteArray("e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129")) - val inscribeAmount = 31100 - val dustSatoshis = 546 - val txId = Numeric.hexStringToByteArray("1771decbce2766b39d8fe66f4dc11737b3146c71f8cc6ae1397384c5e508e7f1") - val payload = Numeric.hexStringToByteArray(nftInscriptionImageData) + val dustSatoshis = 546.toLong() + 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 p2wpkh = BitcoinScript.buildPayToWitnessPubkeyHash(pubKeyHash) - val outputInscribe = BitcoinScript.buildOrdinalNftInscription("image/png", payload, 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 utxo = Bitcoin.UnspentTransaction.newBuilder() - .setScript(outputInscribeProto.script) - .setAmount(inscribeAmount.toLong()) - .setVariant(Bitcoin.TransactionVariant.NFTINSCRIPTION) - .setSpendingScript(outputInscribeProto.spendingScript) - .setOutPoint(unspentOutputPoint0) - .build() - - input.addUtxo(utxo) - - val utxoPlan = Bitcoin.UnspentTransaction.newBuilder() - .setScript(ByteString.copyFrom(p2wpkh.data())) - .setAmount(dustSatoshis.toLong()) - .setVariant(Bitcoin.TransactionVariant.P2WPKH) - .build() - - val plan = Bitcoin.TransactionPlan.newBuilder() - .addUtxos(utxoPlan) - .build() - input.plan = plan - - val output = AnySigner.sign(input.build(), BITCOIN, SigningOutput.parser()) + val publicKey = ByteString.copyFrom(privateKey.getPublicKeySecp256k1(true).data()) + val bobAddress = "bc1qazgc2zhu2kmy42py0vs8d7yff67l3zgpwfzlpk" + + val utxo0 = BitcoinV2.Input.newBuilder() + .setOutPoint(BitcoinV2.OutPoint.newBuilder().apply { + hash = ByteString.copyFrom(txIdInscription) + vout = 0 + }) + .setValue(dustSatoshis) + .setSighashType(BitcoinSigHashType.ALL.value()) + .setScriptBuilder(BitcoinV2.Input.InputBuilder.newBuilder().apply { + p2Wpkh = BitcoinV2.PublicKeyOrHash.newBuilder().setPubkey(publicKey).build() + }) + + val utxo1 = BitcoinV2.Input.newBuilder() + .setOutPoint(BitcoinV2.OutPoint.newBuilder().apply { + hash = ByteString.copyFrom(txIdForFees) + vout = 1 + }) + .setValue(16_400) + .setSighashType(BitcoinSigHashType.ALL.value()) + .setScriptBuilder(BitcoinV2.Input.InputBuilder.newBuilder().apply { + p2Wpkh = BitcoinV2.PublicKeyOrHash.newBuilder().setPubkey(publicKey).build() + }) + + val out0 = BitcoinV2.Output.newBuilder() + .setValue(dustSatoshis) + .setToAddress(bobAddress) + + val changeOutput = BitcoinV2.Output.newBuilder() + .setValue(13_400) + .setBuilder(BitcoinV2.Output.OutputBuilder.newBuilder().apply { + p2Wpkh = BitcoinV2.PublicKeyOrHash.newBuilder().setPubkey(publicKey).build() + }) + + val signingInput = BitcoinV2.SigningInput.newBuilder() + .setVersion(BitcoinV2.TransactionVersion.V2) + .addPrivateKeys(ByteString.copyFrom(privateKeyData)) + .addInputs(utxo0) + .addInputs(utxo1) + .addOutputs(out0) + .addOutputs(changeOutput) + .setInputSelector(BitcoinV2.InputSelector.UseAll) + .setChainInfo(BitcoinV2.ChainInfo.newBuilder().apply { + p2PkhPrefix = 0 + p2ShPrefix = 5 + }) + .setDangerousUseFixedSchnorrRng(true) + .setFixedDustThreshold(dustSatoshis) + .build() + + val legacySigningInput = Bitcoin.SigningInput.newBuilder().apply { + signingV2 = signingInput + } + + val output = AnySigner.sign(legacySigningInput.build(), BITCOIN, SigningOutput.parser()) assertEquals(output.error, SigningError.OK) - assertEquals(output.transactionId, "173f8350b722243d44cc8db5584de76b432eb6d0888d9e66e662db51584f44ac") - - val encodedHex = Numeric.toHexString(output.encoded.toByteArray()) - val expectedHex = nftInscriptionRawHex - - // Offset is the 0x prefix. - val offset = 2; - assertEquals(encodedHex.length, 15658 + offset) - assertEquals(encodedHex.substring(0 + offset, 164 + offset), expectedHex.substring(0, 164)) - assertEquals(encodedHex.substring(292 + offset, 15658 + offset), expectedHex.substring(292, 15658)) - - val signedTransaction = output.transaction - assert(signedTransaction.isInitialized) + assertEquals(output.signingResultV2.error, SigningError.OK) + assertEquals(Numeric.toHexString(output.signingResultV2.encoded.toByteArray()), "0x02000000000102ca3edda74a46877efa5364ab85947e148508713910ada23e147ea28926dc46700000000000ffffffffb11f1782607a1fe5f033ccf9dc17404db020a0dedff94183596ee67ad4177d790100000000ffffffff022202000000000000160014e891850afc55b64aa8247b2076f8894ebdf889015834000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d024830450221008798393eb0b7390217591a8c33abe18dd2f7ea7009766e0d833edeaec63f2ec302200cf876ff52e68dbaf108a3f6da250713a9b04949a8f1dcd1fb867b24052236950121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb0248304502210096bbb9d1f0596d69875646689e46f29485e8ceccacde9d0025db87fd96d3066902206d6de2dd69d965d28df3441b94c76e812384ab9297e69afe3480ee4031e1b2060121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000") + assertEquals(Numeric.toHexString(output.signingResultV2.txid.toByteArray()), "0x3e3576eb02667fac284a5ecfcb25768969680cc4c597784602d0a33ba7c654b7") } } - -const val nftInscriptionImageData = -"89504e470d0a1a0a0000000d4948445200000360000002be0803000000f3" + -"0f8d7d000000d8504c54450000003070bf3070af3173bd3078b73870b730" + -"70b73575ba3075ba3075b53070ba3078bb3474bb3474b73074bb3074b733" + -"76b93076b93373bc3373b93073b93376bc3275ba3075ba3572ba3272ba33" + -"75bc3276b83474bb3274bb3274b83276bd3276bb3474bd3474bb3276b934" + -"74bb3474b93274bb3274b93476bb3276bb3375ba3275ba3373bc3273ba32" + -"77bc3375bc3375ba3373bc3374ba3174b93376bb3375bc3375bb3375b933" + -"75bb3374bb3376bb3475bb3375bb3375ba3374bb3276bc3276ba3375bc32" + -"75bc3375bb3275bb3374bc3374bb3375bb7edf10e10000004774524e5300" + -"10101f20202030303030404040404050505050505f606060606f70707070" + -"7f7f7f7f80808080808f8f909090909f9f9f9fa0a0afafafb0bfbfcfcfcf" + -"cfcfdfdfdfdfefefefef6a89059600001c294944415478daeddd6b7bd3d6" + -"ba2ee0d8350eb00c81ec5533cbac69d6da99383334dbe510904b66129383" + -"feff3fda17506888751892255b52eefb634b1259d2a3f1ead5d0f0d61600" + -"000000000000000000000000000000000000000000000000000000000000" + -"000000000000000000000000000000000000000000000000000000000000" + -"000000000000000000000000000000000000000000000000000000000000" + -"000000000000000000000000000000000084eaef3c9b4e274f87f604546e" + -"27ba8abf3a19db1b50a96114dfb01031a8d0cf57f18f0e7a760a54e4b778" + -"c9c2ad1854a27f1c27586cdb3350c1edd7224eb667dfc0aa9e5fc569666e" + -"c460b5f2f0df71063762b08afb8b389b3211ca978771aec82006a5fcf870" + -"39cd8587ce5066f8ba8ac3cc0c6250d0cec738d8c53fed2f2820bb79a89d" + -"08ab185fc545a91321b03a8ce21216ea44c8372c152f2fb1400d375fb722" + -"b6630f427abcf6aee2d544220675c54bc420adb5318b2be25e0c6ec72b8a" + -"2bb4186bdac3dfb5e1495cb5d903fb15bed48657711d4ec67d3b973b6eb8" + -"574fbafe1ac6343cb8cba5e1248a6bb69031eeead8557bba648c3b3a74ed" + -"1c9cc56b74f54e5b913b13aebde82a5ebf9399af8da0ebe17a7ab09170fd" + -"1db2b1ee3dddbce57a3a79b77a5978383e5fb95c8c0e9eba29a33ba3d683" + -"6793d94925e3d6f9686b6bf0a69ab12c3a98ec2819696baa86c39d67cfa6" + -"b377671556846fbe3e385e7d10fbdb59f4ee60fa6c6767e899340d8dd2b3" + -"835974d3c9d9592db759974fbefdc98a06b1a5b09d9dfdf041a2d94429c9" + -"66ed44ebea4f1cde1c63aa1cc4b29fa1ed2923d9dce8b5b6787dbefbbaa9" + -"a641ccbb2f3448eec2f1955587fbcbb748bbeb1ac4e203479a8de46b5d4f" + -"b4e683c4bfbf7fbda6bf3f73ac59bfe16233d5e106ea446318eb77bc9eea" + -"f0d7ac6d189cac27618f1c6ed66cbca99baf5b9bb1965bb185e3cd9a9dae" + -"bb35bfd18819c258af876be82d0c4207d3fa231639e2acd5a431f15a4bc4" + -"2e7cdd3a6bf57ec3f75eeb8e98b75ce8cc2d58f1787d89d8bcce80fde290" + -"b34ef5bdf13f2f3d37a9cee762024617027679385a65ab06b5558a0246fb" + -"03369facfe5ad6e88d802160950f5e3787b1530143c07e48d7eb51955b37" + -"383c1730da2caa345d35ac22bf5d6dc6048c7606ecf27054d77a18db93ea" + -"6ac5270e39ed0bd87c7f54ef660ec6c7972623721703f6e970773d4b398d" + -"267301e34e05ecf470cddfdd359acc2f058c3b10b04faf27a3cd2c42b8bd" + -"7b583e6502c65a9598ec7b393fdc54b66ea66c7a7c2a6034dddb2283d6e9" + -"ebe978bb496be76eef4e0ee7a702467b03f6e9d3fcf8f574321edd6bee87" + -"e88f46e3e9e1f1fcf4536ee9e875159a12b07fddbb77af7d1fa87fefde76" + -"c6f4949f1c721a12b0f64e7a1030044cc0b8038ed2cfc51702062b9aa69f" + -"8bd3d67ea86b0143c0ea93d14574c411300143c0040c5609d82b01831565" + -"7cf7c31f020602b66490b174b6238e80d51730df5f848009187721601f04" + -"0c56f43863259bb67ea68cef3cfbd31147c07c2604cc6782554ec63f050c" + -"3404040c015ba72e764611b03604ec0f479ca604ec42c060551d9c183b11" + -"3004ac3e9d7cc70d011330b8ad830bc40818cdd1c125ce8e040c01ab4f27" + -"175345c0040c6e3bedde57fdbc17301a23ea5ec03af891e862c09e0818a8" + -"a7040c1d81353af3059708d84602e6cb5558b30e3e95f5ed45344707e715" + -"59391b011330ee84eebd3cd513309aa37bafff5ad8977604ec8380c18a76" + -"3bb7c65917977aa4b51edfa580591691757bd8b9824ac070c7e2ae923b1f" + -"b08bce05ccaa6dac5bf79e1a4d058c06b94b01b3e60d6bd7b9a9b11695a2" + -"493af77247c60b382f1c6e1a14b076be9e68cd1b9aa4732fd877709511ba" + -"19b05f5c31a0be5b967606ec4cc0d013d848c0ac18c0da75aeab9df160af" + -"e770b36e19cf655f752d608e366bd7b5a97b0301a325016be5e4f3875e68" + -"a649baf64ab3179a69cb09b9e8d8e7f1be258dba676965c0bc6f89a6408d" + -"265e07a3513af6dcc8eb60344bc75e087b2b60344ac7de57f1b60acdd2b1" + -"af41f7b60aad39237f71bd80fa6e5ada389dde17c8d22c1d9b4e1f7b5b85" + -"46e9d63a82be1d8c86e9d674fa879d5ba998ee06ac8593f71e9b4c8f53b2" + -"3ebbe6fad22cddfafa8789b9beb4a72dd0bec988befa81a6e95463db5444" + -"9aa6538f66232bd3d330a75d9a2b652a224df3be4b177debfae2b6c50d25" + -"7748971a6f5645a471ba3495e3b1995234cd6e87a672ec5a1591a6e9d2fc" + -"d8899952b4e9bea56d53398e4ce4a071e2ee3c697e6f22078d73d69d87b3" + -"9135a568d359d9b627cdd79e33d3386fbbf31d7cb1256fd019a8cd43df1f" + -"4bf38c3bf3f0c873661a68b733a7a5c7603450771e841d593080e6e975a6" + -"35f0be3bed1a3ae4ac2b4f8f4ebdcf4cbbcecb76cd7f883d06a381de76a4" + -"4fffd063309a68da913efd63af5bd244e38ef4e927de06a36da5559b56b2" + -"38f2188c26caead3b7693e7da44b4f235d77a3bd7dad4b4f239d76a28dd8" + -"d3a5a799de76a23bf0d8a28834d3a4136dc489b9f434d36e27aefdef3511" + -"69a64127da88a7e6d2d350d75d6870c79a883454d4818bff634d449aeaa8" + -"03fd8189053968aa7107ba1c334d449a2a6b36625bdeb93cd544a4a9b226" + -"41bcf211604519ab0644edef71f87a66362c63b2d4453b3a0453af33d35c" + -"93d6f7b8233d0edad9e568c7c237b11e070d76ddf29bb0c7adbf42d06951" + -"cb6fc2a6e671d064472d3f4123f33868b2dd763f46ca7a0a6645299ca177" + -"fbfa40f79db57a3ae2cc6366da7b13d6fcd7a916195b3f7070d9bcac09f5" + -"8d6fd43f760b468b6fc21adf879bb905a3e94e5b5c232edc82d1e69bb086" + -"d7888f634fc168ba517bcfd299898834df756ba7f35d5b508ae6cb78272c" + -"fed8e40dcf6a807a178c569ca74d9e8f98350ff1dc71a521321bf50d6e73" + -"0c634d7ada206a679b63167b558536c85a37a0b96d8ecc014c85484b6ac4" + -"c6be76993980cd1c555a5223367408cb1cc0f4106992710b87b0990a91d6" + -"d488d7ad1bc2b207b0b1634a931c659eaec3b60d605e05a35932e72336f1" + -"59d8fdcc0df6c596b4a9cdd1c0373f162ddb5eeeb8cc4761f1a2697d8edf" + -"622d0ebad3e668da63a5ec0e871607cdf332fb9cfd6793b6b59f5d206a71" + -"d0c0212cfb9cbd68522731bb836816074df4266ecb6d58f60d98018c461a" + -"e59cb68de9d5ff1c1bc068a1a81d27eefd2b03185d1cc2e2bd56e4cb0046" + -"3befc21a91b0dc7c19c068aac175dcf4d161e72a3680d1562ff3cedef8dd" + -"667b893fe76ee0b9018cc6eae50e61f16293cfc37ecbddbcf8574791e69a" + -"e49fc18b8d4da4ed1fe76f9d5988345a947f0e6faad5717f11b06ddb0e21" + -"edee73c4717cb28932f179c086e970d0fe3ec7e73271edd3d5872143ab0e" + -"07dd2812e378b6de41ecf955d0568d1c3e3a5124c6f1628defaf840d5f71" + -"fc2f478fe69b849dcd6b6bd8f7f7023748079156f83df0848e0fd611b19d" + -"45e0d65cba01a3157a27a109abbfd9713f0add16eb04d0b1dbb02f11dba9" + -"b53afc77f086b801a33db6c3cfeb1afb89fdbdabf0cd78e3a8d11ee378f3" + -"117bb228b00dffe93b68b4c87e91842d6a983cb51315d9024f98e972c22a" + -"ef760c0bc54bbee87ac22a8d5891de867c713712164715dd8a15ea6dc817" + -"77266195743b0ac74bbeb833095b3d62e3452c5fdc1593c2095bada158ac" + -"75f8c55c7f9ef61a5d178f58e96ec7fde2f18a0f1d23da6c705efca42fb7" + -"6a47d1d6a1f951742261c725cefbe2b762c57b1b9fe7cf7bc192bbd8ea28" + -"1eb1e2bd8dcfd3a3b437e8c48d588932b1d01bcf257a1b6ebfe85099f8a6" + -"4c0042bb1dc35999dfae3ca44326d775d589a56ebe74e7e9da2056aa8a8b" + -"f7f296b2df59941abe2c904de706b1f3eaebc461b9d8ce75377027f6edfb" + -"5886155787e74f1c0b3a697c5eae4eacb23a8c0fdd7dd159a59e8925d689" + -"fde372d5e17d070175626e3ff179a9eaf0d2d26ca813f307b172cd8dcb7d" + -"d5212296ffc2f35ea951f058ef903b5227febe4ab363e763a99b2f333770" + -"2b96ff7511a5de4a89cfc58b3b16b179a941ac546fdec40ddc8ad5466f03" + -"11ab8f07cbdc5dfb7547ccb443743bea8b97de06225657bcb40ea1744351" + -"eb1036d6edd03a84fa2236132ff8a14edcd7db8016743bf43620d1fdf32a" + -"6ebeec47a8eb56ccbc0dc8aa137f77f3057546ecbd275fd0bc3a517508b5" + -"d589aa43088fd8b9ea106a54e8eb22ac67034507b1379e2cc3e69b1d9a1b" + -"50db2066f882fa0631c317d4368819bea0be41ccf005b50d62862fa844e2" + -"33b163c3175434889d9bba0135dab79e28ace94eecc4175542e5111b1f9f" + -"7efaf4e9f850730300000000000000000000000000000000000000000000" + -"000000000000000000000000000000000000000000000000000000000000" + -"00000000000000000068bff16c49effbfffc9fe5ff698f41016fe3253f7d" + -"ff9fd1f2ffb4c740c040c040c0046c15b3aa0ceedcae1b26ec8583b01f7d" + -"b2f483e3b01f7cbef483ff2b608d1657e5d19ddb75bdebd2bb61f9bcbd08" + -"fb9bcb7f7226600276778aab781a34f695dd7fbb35ec770113b0861a25ec" + -"8628e407c7093ff82aa8a05ffab173f7600276a76ac49f4a55888135620d" + -"15a28009585dfefbd992b5d488c3b23b30a1427cd2fa80f5968fc28e8075" + -"22602b9f3d256bc471e20e0ca81167657b234d0ed860f94ffc216002965a" + -"23f64a55884159a9a3421430016b6cc0126bc417e52ac4803d584b852860" + -"02d6dc808dcbd488e3943d987bf7564b85286002d6dc8095aa11a3943db8" + -"c8fb6b8b3a2a440113b0e6062c312c2fca5588b9bbf0f1f24f8c054cc03a" + -"1db01235e23875174e8b5688010d150113b036072ca946bce895aa107393" + -"b95c217ed8123001eb74c012e3f24bb90a316716484d15a2800958930356" + -"b8461c67ecc3e9062a4401abd728d5ff4938030ed3ff79ff6e06ac708d78" + -"9a11b0680315a2806dcaa068f17327039658233e2a5721660e4975558802" + -"d6ad800dd734d2f587c3dc3f55c9d933890b9d2093cc80bd2854210e56fb" + -"f8b5062cfcef0b5858c03e1fd21ffdb083fb3b93d9c9591cc7f3bf767fd6" + -"3fcefdd5c3f403fbec60767276f56d83cfa2e860b2d3af3160bdb8508d78" + -"9a19b0a84885384fd8530f6e7ffc77d3a70fd619b0efc7f9b3ab93e820ef" + -"cf0b5860c01e67eca6fe24baba795a14dca7d3c0226c78e3cfdc9a24f16e" + -"b25353c08ad588c39c5651af7c85387c363b4bfea557efc6c3b5042cf900" + -"2c664f05acce80ed1c5fddbaeed611b0fe5e9473f2465f4fb383e89b8493" + -"21baa5648df8aa5c85985123e654883b7b27d9bff8ddd3ba03d69fa41f80" + -"abd952c2bfefe2840d5fdc3a0a0702961eb061b45cd8541fb0fede55c8b3" + -"84689cf5a0372e35a2f58abc7a729ab781652ac47e5ebabe9eb5e33a0396" + -"7b00a25b254491a33017b0d4803dbf8aeb0f5860bcbe9c66412763a192b1" + -"408d38ccfd83bdc215e2f020f4c32f86b505ec79c036fc388a09581501eb" + -"1f27eeac8a03b6b388eb52b68ff8aa5c8598ba87532bc4e1acc8c7d9ab27" + -"603b1f8bff7901ab2060c38ff11a02f63c8e371ab00235e269ee1f8c8a55" + -"88e143f75727c31a02167e006e8ca102b67ac0fa8b780d01fb2dde70c0c2" + -"6bc4fc0a31a5c39f52210e3f16fe40cb65e2aa01eb17b9a95d6c0b587501" + -"3b8ed710b0c2f95a541eb049e8b4c2e57f781db68b8f922bc4de79bc7ac2" + -"560cd8b0607dfeb3805515b0ab780d012b9caf595479c0926ac4455085f8" + -"e751d88e48ab10c72506e545afca800d0bdffffe2c6015052c5e43c08685" + -"cfaf41f5010bad119737f6c528a8467c98da432c3184dd5e6760a5800d4b" + -"f4971e09587b02b6287e76d510b0c01a719250e75d8724f328f529739921" + -"ecd65254ab04ac4cbee28ba180b52560c5cfaf411d01eb85b503972bc4a4" + -"ecfc115221fef9ed7f9519c27e1c245709585426dff1494fc05a12b01203" + -"581d014bfc9d3fe55788e3a4c5812f422ac4ef53aa4665cef06945012bdb" + -"c03d10b076042cf96f5ebe9eec6edffb6c7bb43b3d3ebdf57375042ca946" + -"7c1152216e85d488e9156272043e7d9acf4f3f850e61e5039676037c7a38" + -"feb2ff479337e7a9b76102d682801d25fdfed1d2839addefc779be554fc0" + -"826ac4d3c4f3e6287f16484685786b08bb3c9eecdefbf67fb677d3ceef1f" + -"c25f3e6089f5c3e5e1f6cd6d1f9fa73c4e17b016042c6162c4bf927fc5e8" + -"cdf7dedbd1fc9b84b7fde7b784eeaca4d4f6022ac4901af1617640a2bf4f" + -"eda56b4bcaf9fd43f84b072c7102c77c10b409e3adbff77142486f1d05b3" + -"e9730336ff7665bd77afc28005e7ebf3dfdc3fbffd6d7515be0fff32bf46" + -"7c995ce7f5726bc4495685f83da1cb23f7d7b1f5f7c4e3d15b3d60891dc4" + -"5f93f6fc49e66342ef83ad1cb0cbfd7eeeaf2e13b0e54b7bf6f73dee3fa9" + -"2d6083fc1a719152f8bccdab114fb32ac4bf3e464abcbe7cea9c1ab16cc0" + -"923a1cff480ef971ea733c01ab2060fbfd805f5d2660cb7fb2d83a4b55ae" + -"e8925b233e4c3bc7f26ac48456c28f091cc5e7a3ac4d4b1ac3fe583d608b" + -"b0f1eb4bc24e32ae3e02b65ac0920e7e6d01fb636301cbad118fd2eabcbc" + -"1a31a142bcb5ccc5ef39cbcb649768250336ce9d2272f380677c46015b29" + -"60e783ad3506eccf8d052cb7465ca4b6c6a2ece754a7050be1840e4fe6e8" + -"5a326051d8a14e8de32b01ab22609783ad7506ecfb5cedb5072ce997dd7c" + -"dcf430fd2e649c35bee4578801aeb306c172011b66dd5685ec9e85805511" + -"b0f1568d014bdac27f6e2a602fb377d7517a2730bb46ccaf10f31d656d5a" + -"b9808d836629670d613f09d8ea017bbf5567c0121f562e0e1e6c24603935" + -"e222e3e169668db87a859878bff462c580bd2f7007967c15792160ab07ec" + -"51bd014b5d073075add1fa0296b4317f5fd41f668dede38c64565121263d" + -"aa9eae18b045ce1cfd25efd38eb880950fd89f5bf506ec28e3d1f655c692" + -"beb504ec65d605e628eb59712fe3141f075fb58a1dca57ab05ac97fdf03b" + -"c124ede410b0f2017b5173c0f2a7929f640d66d5066c94f5b8699139bd2e" + -"a3468c8a5588fd074f9f3d9bde7694b5bf4b052ce1684fb31da73ded13b0" + -"f2017b5073c002df863a3978daaf3f60893562488598d4c888d22bc4b47b" + -"9de1647612bec6d48a012bf5a267f25f11b0d201bbd8aa3b6093e0a3194d" + -"8675072ca3463cca2ea87aa9cfa9422bc49d8382ebb7ad18b06905017b20" + -"602b066c5e7bc00abd7b727bf1e6aa03965423be0aa91093b6e445810ab1" + -"3f39297c76af18b0a30a02f648c0560cd887fa033628f4cafcadaf20a8fa" + -"dbe5526bc48779cf07d36ac4a00af149998531560cd8db0a02f68b80ad18" + -"b069fd01dbda2e94b08b719d014bad118ff25a6e6935624085d83f2e7576" + -"0b98800505ace018f6c3f2e855072cb546ccab10536bc4fc0a7158725d7e" + -"0113b0b0806d0dde944d58e55f407c9d5c23e656886935627e8558365f4d" + -"08d813016b48c08e720aa571a141ec9ff505ec28b946ccad10d36ac4dc0a" + -"b174be9a10304d8ea604ec7dee9d48a1883daa2d604935e234a4424cda96" + -"5f022ac459bca18069d3b72d6059ef229f063c0c1abd09ce58545bc0926a" + -"c445488598d42089f22bc4151ef7ae18b0490501f3a0799d01cb7a553270" + -"16f1f6f8f8bcd010567dc0926ac407b390697bcbbbe4a2975b21a61488a7" + -"afa7d3f14d93ca03b6bbfcdfffefb8a02d015b63c02e0afde6d409af83db" + -"2b8d665dbdab0f58628d18522126d68851ce5e4a1cc0e6a37eb192bc54c0" + -"06e1134f4b9c6702b662c07a71d6410d6c1d64d8de9dbe9e5fa63f0dab2d" + -"604935e2554885985823e6558809ff2079f99bea0396f04123016b4cc092" + -"8abef40be0a270c0be8d6687f3ccdbeb1a0216348568103af8a575b6532f" + -"5329cb62d410b0f7a97b55c01a10b0b338fc02388e4b06eccb8748ec2ffe" + -"525bc04262320fde27d91562caf7caae27609390495c02b6a98025dd260d" + -"0b0c60455e3adc4fddb01a0296542306a6e065c05cca9c87d3e75b6b0bd8" + -"60b5832260f506ec6d1c7c01fc2d5e31600927c9abfa027654b2420c1afc" + -"9ee4fca90feb0b58d2ff59f4caecb15eb16736021612b0497068eec72b07" + -"6c9c7676bd2fd26aa9ac469c17a89b333badef8377f86e0d017b19172e12" + -"9f6f07de902f046cc580259e868b8422f1fe555ec0a271de07495d00f86d" + -"75bde60235e2b8f4e0f72170640e18c3570f58efba60c2faffefaf2f8f0d" + -"d8633d015b2d608947272161cf73a73b8de378b193fd4126691b7654a0d5" + -"525d8d38283df88df346b03fc2db442b072cf9839ea4dd496f0d3fa65c43" + -"9346eea980ad16b0b43791f77ef8473b51e07cc245e6287692364e8d736f" + -"736aa811e7e507bf5ede8d6cf2d3fac419c1ab076c90b88d2987a2bf7795" + -"5aa52454ea1743015b2d606973d916b3bf1681eaefec4501b39d1e7fffb9" + -"d4a730bfa5fef4c3a4fb9c9bf7093b653acfd7252bc4dcc1ef43fe3f4fba" + -"3cf43fc6b5046cebf7383462dfe295dc0839ca1b099f8f05ac68c07a1927" + -"d2d5d9d959de0dffb780cd12b2f9e3e53be985dfcc8dd81b7e8f57a99b81" + -"b7252bc4dcc16f9c5ff9250c10c38f714d01eb5da75e259fded88e0793e8" + -"e68df4492fa0057323a6fdff8e2a28dcef5cc08a7d53726ac06ecf373f39" + -"98ecfc7d6cfb3b9328732848de88ab683a9d1ebcbb2a7933302a5b21e60d" + -"7ebd8003b494b09d455c57c09293f17d2746b3d96c169de53742522eb657" + -"ef0ea6d3d9d7703e10b0a2011b5512b0e4f7a1cecea2283a39bbca1d0af2" + -"9fed7e2cf15ce7ba64859853237e08cbe3cd857dfacfa29a5e57f9ea4da9" + -"63372b7eb19d0a58d180ad38843d4a1cc0c29c0715aae56727bc2d320c85" + -"5f7596a399727d880e9eedececec3c3b88aef2df28582960bdf35207602f" + -"f0863ce85d0b01ab65087b94318015e876e7a7bcf8372d647eb20fe51b24" + -"83a02314aa9280155d71282561bdeb3aae73773d60694da802011b5e97f9" + -"d179a1945f146f73649d2fe3d2835fd2cddbf1a603b6b55dea10dcfe16b7" + -"fc523d12b0c2012b595fdc08d86fa50ac441b142b5c4dc8eb7252bc4ccc0" + -"27457370bde980954cd83f0a0f613d012b7c6bba7dbd62c0ca2ca97479bf" + -"e0295ae2da392a5b21669d6983228f1397ae2ad77505ac5495f88fe29f63" + -"2a60c5774ae86dd87f0e13035666c997cb27f90f9352cfb6d56bc471e9c1" + -"2fa5bd1fd8c71b9fd516b0ad41d185f12fff51e273440256e2aab31b3486" + -"cdfbd3c480bd2c9eaff3fb4bdbb05fc3b5f37de9426754309abda0b3fb70" + -"abc68005ecc2bc6310f2391e0958895333a4be384c5a85efcbee1e15ad4e" + -"e6497556dee951a2453c2e5921660c7e6913407a018d8e375bf506ac5099" + -"78dc2f77a578256065aefdb9eb5e5ffeba951ab0adadf1bc4869f26bf236" + -"4c2abf76a6c524604e5d547802c87e40be6a0ed8d6d6243062e74fca7695" + -"2f7a0256aab8ca5e94f7eb98334d3fe907a10b8e5eeef7cb8da39725a69a" + -"46a55b61e3e2d1ccd9fc2f9795ba03b635d83f5fe918e49e09ff190858c9" + -"bb97f41d3bff6b11b269e6a8b21b90b193cc439b756c67fd12fb6e5cb242" + -"4c1dfc06652f52b3af3f597bc002ca89f92467570ef62fd3a3a9c9b1427b" + -"20f1d05cbeffbec6df34af6c1b4d3396418ce7fba3fc4d382e7ec92d1a93" + -"71e9c16f9edb2e4adefcc36fc15c47c03eafe1355fe5187cfe05a7a5a2d9" + -"4abd84d58e0785fef9fdf03fd6df3dbc1991cbd3c39b0bd4de0fd990edcf" + -"8bfade8ad9e5e9ebc9a85f6a13e24fc79351e99db79bb45674d05dc420e9" + -"27ef17dffccb1f4ee9a5edf9fb2df0ffcadad0ff4a5df13ab50f3ab97d14" + -"3e1d1fee86076430fe7169e64faf3b99ae8de86f8fbe1cc2d1bd157ec7bd" + -"d1d75f32de1ddd2b7e64faa36f9bd0caa3fa7df3b7fb9b3e92bb7f6d48a9" + -"63f9d741dc5de54c00000000000000000000000000000000000000000000" + -"000000000000000000000000000000000000000000000000000000000000" + -"000000000000000000000000000000000000000000000000000000000000" + -"00000000000000000000000000000000000000000000008066fbffddd184" + -"8d4adc88950000000049454e44ae426082" - -const val nftInscriptionRawHex = -"020000000001011771decbce2766b39d8fe66f4dc11737b3146c71f8cc6a" + -"e1397384c5e508e7f10000000000ffffffff012202000000000000160014" + -"e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d0340cc1e7b0b5fa18b28" + -"dce702e4e8ed2e91069d682b8daa3a773774bfc7d0e6f737d403016a9016" + -"b58a92592ad0b41682e6209167444eb56605532b28e9be922d3afdda1d00" + -"63036f7264010109696d6167652f706e67004d080289504e470d0a1a0a00" + -"00000d4948445200000360000002be0803000000f30f8d7d000000d8504c" + -"54450000003070bf3070af3173bd3078b73870b73070b73575ba3075ba30" + -"75b53070ba3078bb3474bb3474b73074bb3074b73376b93076b93373bc33" + -"73b93073b93376bc3275ba3075ba3572ba3272ba3375bc3276b83474bb32" + -"74bb3274b83276bd3276bb3474bd3474bb3276b93474bb3474b93274bb32" + -"74b93476bb3276bb3375ba3275ba3373bc3273ba3277bc3375bc3375ba33" + -"73bc3374ba3174b93376bb3375bc3375bb3375b93375bb3374bb3376bb34" + -"75bb3375bb3375ba3374bb3276bc3276ba3375bc3275bc3375bb3275bb33" + -"74bc3374bb3375bb7edf10e10000004774524e530010101f202020303030" + -"30404040404050505050505f606060606f707070707f7f7f7f8080808080" + -"8f8f909090909f9f9f9fa0a0afafafb0bfbfcfcfcfcfcfdfdfdfdfefefef" + -"ef6a89059600001c294944415478daeddd6b7bd3d6ba2ee0d8350eb00c81" + -"ec5533cbac69d6da99383334dbe510904b66129383feff3fda1750688875" + -"1892255b52eefb634b1259d2a3f1ead5d0f0d61600000000000000000000" + -"000000000000000000000000000000000000000000000000000000000000" + -"000000000000000000000000000000000000000000000000000000000000" + -"000000000000000000000000000000000000000000000000000000000000" + -"004d08020000000000000084eaef3c9b4e274f87f604546e27ba8abf3a19" + -"db1b50a96114dfb01031a8d0cf57f18f0e7a760a54e4b778c9c2ad1854a2" + -"7f1c27586cdb3350c1edd7224eb667dfc0aa9e5fc569666ec460b5f2f0df" + -"71063762b08afb8b389b3211ca978771aec82006a5fcf87039cd8587ce50" + -"66f8ba8ac3cc0c6250d0cec738d8c53fed2f2820bb79a89d08ab185fc545" + -"a91321b03a8ce21216ea44c8372c152f2fb1400d375fb722b6630f427abc" + -"f6aee2d544220675c54bc420adb5318b2be25e0c6ec72b8a2bb4186bdac3" + -"dfb5e1495cb5d903fb15bed48657711d4ec67d3b973b6eb8574fbafe1ac6" + -"343cb8cba5e1248a6bb69031eeead8557bba648c3b3a74ed1c9cc56b74f5" + -"4e5b913b13aebde82a5ebf9399af8da0ebe17a7ab09170fd1db2b1ee3ddd" + -"bce57a3a79b77a5978383e5fb95c8c0e9eba29a33ba3d6836793d94925e3" + -"d6f9686b6bf0a69ab12c3a98ec2819696baa86c39d67cfa6b37767155684" + -"6fbe3e385e7d10fbdb59f4ee60fa6c6767e899340d8dd2b3835974d3c9d9" + -"592db759974fbefdc98a06b1a5b09d9dfdf041a2d94429c966ed44ebea4f" + -"1cde1c63aa1cc4b29fa1ed2923d9dce8b5b6787dbefbbaa9a641ccbb2f34" + -"48eec2f1955587fbcbb748bbeb1ac4e203479a8de46b5d4fb4e683c4bfbf" + -"7fbda6bf3f73ac59bfe16233d5e106ea446318eb77bc9eeaf0d7ac6d189c" + -"ac27618f1c6ed66cbca99baf5b9b4d0802b1965bb185e3cd9a9daebb35bf" + -"d18819c258af876be82d0c4207d3fa231639e2acd5a431f15a4bc42e7cdd" + -"3a6bf57ec3f75eeb8e98b75ce8cc2d58f1787d89d8bcce80fde290b34ef5" + -"bdf13f2f3d37a9cee762024617027679385a65ab06b5558a0246fb03369f" + -"acfe5ad6e88d802160950f5e3787b1530143c07e48d7eb51955b37383c17" + -"30da2caa345d35ac22bf5d6dc6048c7606ecf27054d77a18db93ea6ac527" + -"0e39ed0bd87c7f54ef660ec6c7972623721703f6e970773d4b398d267301" + -"e34e05ecf470cddfdd359acc2f058c3b10b04faf27a3cd2c42b8bd7b583e" + -"6502c65a9598ec7b393fdc54b66ea66c7a7c2a6034dddb2283d6e9ebe978" + -"bb496be76eef4e0ee7a702467b03f6e9d3fcf8f574321edd6bee87e88f46" + -"e3e9e1f1fcf4536ee9e875159a12b07fddbb77af7d1fa87fefde76c6f494" + -"9f1c721a12b0f64e7a1030044cc0b8038ed2cfc51702062b9aa69f8bd3d6" + -"7ea86b0143c0ea93d14574c411300143c0040c5609d82b018315657cf7c3" + -"1f020602b66490b174b6238e80d51730df5f848009187721601f040c56f4" + -"3863259bb67ea68cef3cfbd31147c07c2604cc6782554ec63f050c340404" + -"0c015ba72e764611b03604ec0f479ca604ec42c060551d9c183b113004ac" + -"3e9d7cc70d011330b8ad830bc40818cdd1c125ce8e040c01ab4f27175345" + -"c0040c6e3bedde57fdbc17301a23ea5ec03af891e862c09e0818a84d0802" + -"a7040c1d81353af3059708d84602e6cb5558b30e3e95f5ed45344707e715" + -"59391b011330ee84eebd3cd513309aa37bafff5ad8977604ec8380c18a76" + -"3bb7c65917977aa4b51edfa580591691757bd8b9824ac070c7e2ae923b1f" + -"b08bce05ccaa6dac5bf79e1a4d058c06b94b01b3e60d6bd7b9a9b11695a2" + -"493af77247c60b382f1c6e1a14b076be9e68cd1b9aa4732fd877709511ba" + -"19b05f5c31a0be5b967606ec4cc0d013d848c0ac18c0da75aeab9df160af" + -"e770b36e19cf655f752d608e366bd7b5a97b0301a325016be5e4f3875e68" + -"a649baf64ab3179a69cb09b9e8d8e7f1be258dba676965c0bc6f89a6408d" + -"265e07a3513af6dcc8eb60344bc75e087b2b60344ac7de57f1b60acdd2b1" + -"af41f7b60aad39237f71bd80fa6e5ada389dde17c8d22c1d9b4e1f7b5b85" + -"46e9d63a82be1d8c86e9d674fa879d5ba998ee06ac8593f71e9b4c8f53b2" + -"3ebbe6fad22cddfafa8789b9beb4a72dd0bec988befa81a6e95463db5444" + -"9aa6538f66232bd3d330a75d9a2b652a224df3be4b177debfae2b6c50d25" + -"7748971a6f5645a471ba3495e3b1995234cd6e87a672ec5a1591a6e9d2fc" + -"d8899952b4e9bea56d53398e4ce4a071e2ee3c697e6f22078d73d69d87b3" + -"9135a568d359d9b627cdd79e33d3386fbbf31d7cb1256fd019a8cd43df1f" + -"4bf38c3bf3f0c873661a68b733a7a5c7603450771e841d593080e6e975a6" + -"35f0be3bed1a3ae4ac2b4d08024f8f4ebdcf4cbbcecb76cd7f883d06a381" + -"de76a44fffd063309a68da913efd63af5bd244e38ef4e927de06a36da555" + -"9b56b238f2188c26caead3b7693e7da44b4f235d77a3bd7dad4b4f239d76" + -"a28dd8d3a5a799de76a23bf0d8a28834d3a4136dc489b9f434d36e27aefd" + -"ef351169a64127da88a7e6d2d350d75d6870c79a883454d4818bff634d44" + -"9aeaa803fd8189053968aa7107ba1c334d449a2a6b36625bdeb93cd544a4" + -"a9b22641bcf211604519ab0644edef71f87a66362c63b2d4453b3a0453af" + -"33d35c93d6f7b8233d0edad9e568c7c237b11e070d76ddf29bb0c7adbf42" + -"d06951cb6fc2a6e671d064472d3f4123f33868b2dd763f46ca7a0a664529" + -"9ca177fbfa40f79db57a3ae2cc6366da7b13d6fcd7a916195b3f7070d9bc" + -"ac09f58d6fd43f760b468b6fc21adf879bb905a3e94e5b5c232edc82d1e6" + -"9bb086d7888f634fc168ba517bcfd299898834df756ba7f35d5b508ae6cb" + -"78272cfed8e40dcf6a807a178c569ca74d9e8f98350ff1dc71a521321bf5" + -"0d6e730c634d7ada206a679b63167b558536c85a37a0b96d8ecc014c8548" + -"4b6ac4c6be76993980cd1c555a5223367408cb1cc0f4106992710b87b099" + -"0a91d6d488d7ad1bc2b207b0b1634a931c659eaec3b60d605e05a35932e7" + -"2336f159d8fdcc0df6c596b4a9cdd1c0373f162ddb5eeeb8cc4761f1a269" + -"7d8edf622d0ebad3e668da63a5ec0e871607cdf332fb9c4d0802fd6793b6" + -"b59f5d206a71d0c0212cfb9cbd68522731bb836816074df4266ecb6d58f6" + -"0d98018c461ae59cb68de9d5ff1c1bc068a1a81d27eefd2b03185d1cc2e2" + -"bd56e4cb00463befc21a91b0dc7c19c068aac175dcf4d161e72a3680d156" + -"2ff3cedef8dd667b893fe76ee0b9018cc6eae50e61f16293cfc37ecbddbc" + -"f8574791e69ae49fc18b8d4da4ed1fe76f9d5988345a947f0e6faad5717f" + -"11b06ddb0e21edee73c4717cb28932f179c086e970d0fe3ec7e73271edd3" + -"d5872143ab0e07dd2812e378b6de41ecf955d0568d1c3e3a5124c6f1628d" + -"efaf840d5f71fc2f478fe69b849dcd6b6bd8f7f7023748079156f83df084" + -"8e0fd611b19d45e0d65cba01a3157a27a109abbfd9713f0add16eb04d0b1" + -"dbb02f11dba9b53afc77f086b801a33db6c3cfeb1afb89fdbdabf0cd78e3" + -"a8d11ee378f3117bb228b00dffe93b68b4c87e91842d6a983cb51315d902" + -"4f98e972c22aef760c0bc54bbee87ac22a8d5891de867c713712164715dd" + -"8a15ea6dc81777266195743b0ac74bbeb833095b3d62e3452c5fdc1593c2" + -"095bada158ac75f8c55c7f9ef61a5d178f58e96ec7fde2f18a0f1d23da6c" + -"705efca42fb76a47d1d6a1f951742261c725cefbe2b762c57b1b9fe7cf7b" + -"c192bbd8ea281eb1e2bd8dcfd3a3b437e8c48d588932b1d01bcf257a1b6e" + -"bfe85099f8a64c0042bb1dc35999dfae3ca44326d775d589a56ebe74e7e9" + -"da2056aa8a8b4d0802f7f296b2df59941abe2c904de706b1f3eaebc461b9" + -"d8ce75377027f6edfb5886155787e74f1c0b3a697c5eae4eacb23a8c0fdd" + -"7dd159a59e8925d689fde372d5e17d070175626e3ff179a9eaf0d2d26ca8" + -"13f307b172cd8dcb7dd5212296ffc2f35ea951f058ef903b5227febe4ab3" + -"63e763a99b2f3337702b96ff7511a5de4a89cfc58b3b16b179a941ac546f" + -"dec40ddc8ad5466f0311ab8f07cbdc5dfb7547ccb443743bea8b97de0622" + -"5657bcb40ea1744351eb1036d6edd03a84fa2236132ff8a14edcd7db8016" + -"743bf43620d1fdf32a6ebeec47a8eb56ccbc0dc8aa137f77f3057546ecbd" + -"275fd0bc3a517508b5d589aa43088fd8b9ea106a54e8eb22ac67034507b1" + -"379e2cc3e69b1d9a1b50db2066f882fa0631c317d4368819bea0be41ccf0" + -"05b50d62862fa844e233b163c3175434889d9bba0135dab79e28ace94eec" + -"c4175542e5111b1f9f7efaf4e9f850730300000000000000000000000000" + -"000000000000000000000000000000000000000000000000000000000000" + -"00000000000000000000000000000000000068bff16c49effbfffc9fe5ff" + -"698f41016fe3253f7dff9fd1f2ffb4c740c040c040c0046c15b3aa0ceedc" + -"ae1b26ec8583b01f7db2f483e3b01f7cbef483ff2b608d1657e5d19ddb75" + -"bdebd2bb61f9bcbd08fb9bcb7f7226600276778aab781a34f695dd7fbb35" + -"ec770113b0861a25ec8628e407c7093ff82aa84d0802a05ffab173f76002" + -"76a76ac49f4a55888135620d15a28009585dfefbd992b5d488c3b23b30a1" + -"427cd2fa80f5968fc28e807522602b9f3d256bc471e20e0ca81167657b23" + -"4d0ed860f94ffc216002965a23f64a55884159a9a3421430016b6cc0126b" + -"c417e52ac4803d584b85286002d6dc808dcbd488e3943d987bf7564b8528" + -"6002d6dc8095aa11a3943db8c8fb6b8b3a2a440113b0e6062c312c2fca55" + -"88b9bbf0f1f24f8c054cc03a1db01235e23875174e8b5688010d150113b0" + -"36072ca946bce895aa107393b95c217ed8123001eb74c012e3f24bb90a31" + -"6716484d15a2800958930356b8461c67ecc3e9062a4401abd728d5ff4938" + -"030ed3ff79ff6e06ac708d789a11b0680315a2806dcaa068f17327039658" + -"233e2a5721660e4975558802d6ad800dd734d2f587c3dc3f55c9d933890b" + -"9d2093cc80bd2854210e56fbf8b5062cfcef0b5858c03e1fd21ffdb083fb" + -"3b93d9c9591cc7f3bf767fd63fcefdd5c3f403fbec60767276f56d83cfa2" + -"e860b2d3af3160bdb8508d789a19b0a84885384fd8530f6e7ffc77d3a70f" + -"d619b0efc7f9b3ab93e820efcf0b5860c01e67eca6fe24baba795a14dca7" + -"d3c0226c78e3cfdc9a24f16eb25353c08ad588c39c5651af7c85387c363b" + -"4bfea557efc6c3b5042cf9002c664f05acce80ed1c5fddbaeed611b0fe5e" + -"9473f2465f4fb383e89b849321baa5648df8aa5c85985123e654883b7b27" + -"d9bf4d0802f8ddd3ba03d69fa41f80abd952c2bfefe2840d5fdc3a0a0702" + -"961eb061b45cd8541fb0fede55c8b384689cf5a0372e35a2f58abc7a729a" + -"b781652ac47e5ebabe9eb5e33a03967b00a25b254491a33017b0d4803dbf" + -"8aeb0f5860bcbe9c66412763a192b1408d38ccfd83bdc215e2f020f4c32f" + -"86b505ec79c036fc388a09581501eb1f27eeac8a03b6b388eb52b68ff8aa" + -"5c8598ba87532bc4e1acc8c7d9ab27603b1f8bff7901ab2060c38ff11a02" + -"f63c8e371ab00235e269ee1f8c8a5588e143f75727c31a02167e006e8ca1" + -"02b67ac0fa8b780d01fb2dde70c0c26bc4fc0a31a5c39f52210e3f16fe40" + -"cb65e2aa01eb17b9a95d6c0b5875013b8ed710b0c2f95a541eb049e8b4c2" + -"e57f781db68b8f922bc4de79bc7ac2560cd8b0607dfeb3805515b0ab780d" + -"012b9caf595479c0926ac4455085f8e751d88e48ab10c72506e545afca80" + -"0d0bdffffe2c6015052c5e43c08685cfaf41f5010bad119737f6c528a846" + -"7c98da432c3184dd5e6760a5800d4bf4971e09587b02b6287e76d510b0c0" + -"1a719250e75d8724f328f529739921ecd65254ab04ac4cbee28ba180b525" + -"60c5cfaf411d01eb85b503972bc4a4ecfc115221fef9ed7f9519c27e1c24" + -"5709585426dff1494fc05a12b01203581d014bfc9d3fe55788e3a4c5812f" + -"422ac4ef53aa4665cef06945012bdbc03d10b076042cf96f5ebe9eec6edf" + -"fb6c7bb43b3d3ebdf57375042ca9464d08027c1152216e85d488e9156272" + -"043e7d9acf4f3f850e61e5039676037c7a38feb2ff479337e7a9b76102d6" + -"82801d25fdfed1d2839addefc779be554fc0826ac4d3c4f3e6287f164846" + -"85786b08bb3c9eecdefbf67fb677d3ceef1fc25f3e6089f5c3e5e1f6cd6d" + -"1f9fa73c4e17b016042c6162c4bf927fc5e8cdf7dedbd1fc9b84b7fde7b7" + -"84eeaca4d4f6022ac4901af1617640a2bf4feda56b4bcaf9fd43f84b072c" + -"7102c77c10b409e3adbff77142486f1d05b3e9730336ff7665bd77afc280" + -"05e7ebf3dfdc3fbffd6d7515be0fff32bf467c995ce7f5726bc4495685f8" + -"3da1cb23f7d7b1f5f7c4e3d15b3d60891dc45f93f6fc49e66342ef83ad1c" + -"b0cbfd7eeeaf2e13b0e54b7bf6f73dee3fa92d6083fc1a719152f8bccdab" + -"114fb32ac4bf3e464abcbe7cea9c1ab16cc0923a1cff480ef971ea733c01" + -"ab2060fbfd805f5d2660cb7fb2d83a4b55aee8925b233e4c3bc7f26ac484" + -"56c28f091cc5e7a3ac4d4b1ac3fe583d608bb0f1eb4bc24e32ae3e02b65a" + -"c0920e7e6d01fb636301cbad118fd2eabcbc1a31a142bcb5ccc5ef39cbcb" + -"649768250336ce9d2272f380677c46015b2960e783ad3506eccf8d052cb7" + -"465ca4b6c6a2ece754a7050be1840e4fe6e85a326051d8a14e8de32b01ab" + -"22609783ad7506ecfb5cedb5072ce997dd7cdcf430fd2e649c35bee45788" + -"01aeb306c172011b66dd5685ec9e85805511b0f1568d014bdac27f6e4d08" + -"022a602fb377d7517a2730bb46ccaf10f31d656d5ab9808d836629670d61" + -"3f09d8ea017bbf5567c0121f562e0e1e6c24603935e222e3e169668db87a" + -"859878bff462c580bd2f7007967c15792160ab07ec51bd014b5d073075ad" + -"d1fa0296b4317f5fd41f668dede38c64565121263daa9eae18b045ce1cfd" + -"25efd38eb880950fd89f5bf506ec28e3d1f655c692beb504ec65d605e628" + -"eb59712fe3141f075fb58a1dca57ab05ac97fdf03bc124ede410b0f2017b" + -"5173c0f2a7929f640d66d5066c94f5b8699139bd2ea3468c8a5588fd074f" + -"9f3d9bde7694b5bf4b052ce1684fb31da73ded13b0f2017b5073c002df86" + -"3a3978daaf3f60893562488598d4c888d22bc4b47b9de1647612bec6d48a" + -"012bf5a267f25f11b0d201bbd8aa3b6093e0a3194d8675072ca3463cca2e" + -"a87aa9cfa9422bc49d8382ebb7ad18b06905017b20602b066c5e7bc00abd" + -"7b727bf1e6aa03965423be0aa91093b6e445810ab13f39297c76af18b0a3" + -"0a02f648c0560cd887fa033628f4cafcadaf20a8fadbe5526bc48779cf07" + -"d36ac4a00af149998531560cd8db0a02f68b80ad18b069fd01dbda2e94b0" + -"8b719d014bad118ff25a6e6935624085d83f2e75760b98800505ace018f6" + -"c3f2e855072cb546ccab10536bc4fc0a7158725d7e0113b0b0806d0dde94" + -"4d58e55f407c9d5c23e656886935627e8558365f4d08d813016b48c08e72" + -"0aa571a141ec9ff505ec284d0802b946ccad10d36ac4dc0ab174be9a1030" + -"4d8ea604ec7dee9d48a1883daa2d604935e234a4424cda965f022ac459bc" + -"a18069d3b72d6059ef229f063c0c1abd09ce58545bc0926ac445488598d4" + -"2089f22bc4151ef7ae18b0490501f3a0799d01cb7a55327016f1f6f8f8bc" + -"d010567dc0926ac407b390697bcbbbe4a2975b21a61488a7afa7d3f14d93" + -"ca03b6bbfcdfffefb8a02d015b63c02e0afde6d409af83db2b8d665dbdab" + -"0f58628d18522126d68851ce5e4a1cc0e6a37eb192bc54c006e1134f4b9c" + -"6702b662c07a71d6410d6c1d64d8de9dbe9e5fa63f0dab2d604935e25548" + -"85985823e6558809ff2079f99bea0396f04123016b4cc0928abef40be0a2" + -"70c0be8d6687f3ccdbeb1a0216348568103af8a575b6532f5329cb62d410" + -"b0f7a97b55c01a10b0b338fc02388e4b06eccb8748ec2ffe525bc0426232" + -"0fde27d91562caf7caae27609390495c02b6a98025dd260d0b0c60455e3a" + -"dc4fddb01a0296542306a6e065c05cca9c87d3e75b6b0bd860b5832260f5" + -"06ec6d1c7c01fc2d5e31600927c9abfa027654b2420c1afc9ee4fca90feb" + -"0b58d2ff59f4caecb15eb16736021612b0497068eec72b076c9c7676bd2f" + -"d26aa9ac469c17a89b333badef8377f86e0d017b19172e129f6f07de902f" + -"046cc580259e868b8422f1fe555ec0a271de07495d00f86d75bde60235e2" + -"b8f4e0f72170640e18c3570f58efba60c2faffefaf2f8f0d4d0802d8633d" + -"015b2d608947272161cf73a73b8de378b193fd4126691b7654a0d5525d8d" + -"38283df88df346b03fc2db442b072cf9839ea4dd496f0d3fa65c439346ee" + -"a980ad16b0b43791f77ef8473b51e07cc245e6287692364e8d736f736aa8" + -"11e7e507bf5ede8d6cf2d3fac419c1ab076c90b88d2987a2bf77955aa524" + -"54ea1743015b2d606973d916b3bf1681eaefec4501b39d1e7fffb9d4a730" + -"bfa5fef4c3a4fb9c9bf7093b653acfd7252bc4dcc1ef43fe3f4fba3cf43f" + -"c6b5046cebf7383462dfe295dc0839ca1b099f8f05ac68c07a1927d2d5d9" + -"d959de0dffb780cd12b2f9e3e53be985dfcc8dd81b7e8f57a99b81b7252b" + -"c4dcc16f9c5ff9250c10c38f714d01eb5da75e259fded88e0793e8e68df4" + -"492fa0057323a6fdff8e2a28dcef5cc08a7d53726ac06ecf373f3998ecfc" + -"7d6cfb3b9328732848de88ab683a9d1ebcbb2a7933302a5b21e60d7ebd80" + -"03b494b09d455c57c09293f17d2746b3d96c169de53742522eb657ef0ea6" + -"d3d9d7703e10b0a2011b5512b0e4f7a1cecea2283a39bbca1d0af29fed7e" + -"2cf15ce7ba64859853237e08cbe3cd857dfacfa29a5e57f9ea4da963372b" + -"7eb19d0a58d180ad38843d4a1cc0c29c0715aae56727bc2d320c855f7596" + -"a399727d880e9eedececec3c3b88aef2df28582960bdf35207602ff0863c" + -"e85d0b01ab65087b94318015e876e7a7bcf8372d647eb20fe51b2483a023" + -"14aa9280155d714d0802282561bdeb3aae73773d60694da802011b5e97f9" + -"d179a1945f146f73649d2fe3d2835fd2cddbf1a603b6b55dea10dcfe16b7" + -"fc523d12b0c2012b595fdc08d86fa50ac441b142b5c4dc8eb7252bc4ccc0" + -"27457370bde980954cd83f0a0f613d012b7c6bba7dbd62c0ca2ca97479bf" + -"e0295ae2da392a5b21669d6983228f1397ae2ad77505ac5495f88fe29f63" + -"2a60c5774ae86dd87f0e13035666c997cb27f90f9352cfb6d56bc471e9c1" + -"2fa5bd1fd8c71b9fd516b0ad41d185f12fff51e273440256e2aab31b3486" + -"cdfbd3c480bd2c9eaff3fb4bdbb05fc3b5f37de9426754309abda0b3fb70" + -"abc68005ecc2bc6310f2391e0958895333a4be384c5a85efcbee1e15ad4e" + -"e6497556dee951a2453c2e5921660c7e6913407a018d8e375bf506ac5099" + -"78dc2f77a578256065aefdb9eb5e5ffeba951ab0adadf1bc4869f26bf236" + -"4c2abf76a6c524604e5d547802c87e40be6a0ed8d6d6243062e74fca7695" + -"2f7a0256aab8ca5e94f7eb98334d3fe907a10b8e5eeef7cb8da39725a69a" + -"46a55b61e3e2d1ccd9fc2f9795ba03b635d83f5fe918e49e09ff190858c9" + -"bb97f41d3bff6b11b269e6a8b21b90b193cc439b756c67fd12fb6e5cb242" + -"4c1dfc06652f52b3af3f597bc002ca89f92467570ef62fd3a3a9c9b1427b" + -"20f1d05cbeffbec6df34af6c1b4d3396418ce7fba3fc4d382e7ec92d1a93" + -"71e9c16f9edb2e4adefcc36fc15c47c03eafe1354d29015fe5187cfe05a7" + -"a5a2d94abd84d58e0785fef9fdf03fd6df3dbc1991cbd3c39b0bd4de0fd9" + -"90edcf8bfade8ad9e5e9ebc9a85f6a13e24fc79351e99db79bb45674d05d" + -"c420e927ef17dffccb1f4ee9a5edf9fb2df0ffcadad0ff4a5df13ab50f3a" + -"b97d143e1d1fee86076430fe7169e64faf3b99ae8de86f8fbe1cc2d1bd15" + -"7ec7bdd1d75f32de1ddd2b7e64faa36f9bd0caa3fa7df3b7fb9b3e92bb7f" + -"6d48a963f9d741dc5de54c00000000000000000000000000000000000000" + -"000000000000000000000000000000000000000000000000000000000000" + -"000000000000000000000000000000000000000000000000000000000000" + -"00000000000000000000000000000000000000000000000000008066fbff" + -"ddd1848d4adc88950000000049454e44ae4260826821c00f209b6ada5edb" + -"42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000" diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestAnyAddress.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestAnyAddress.kt index ae4b47b4bd0..40f9fe147c9 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestAnyAddress.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestAnyAddress.kt @@ -53,10 +53,10 @@ class TestAnyAddress { fun testCreateWithPublicKeyDerivation() { val coin = CoinType.BITCOIN val pubkey = PublicKey(any_address_test_pubkey.toHexByteArray(), PublicKeyType.SECP256K1) - val address1 = AnyAddress(pubkey, coin, Derivation.BITCOINSEGWIT) + val address1 = AnyAddress(pubkey, coin, Derivation.SEGWIT) assertEquals(address1.description(), any_address_test_address) - val address2 = AnyAddress(pubkey, coin, Derivation.BITCOINLEGACY) + val address2 = AnyAddress(pubkey, coin, Derivation.LEGACY) assertEquals(address2.description(), "1JvRfEQFv5q5qy9uTSAezH7kVQf4hqnHXx") } diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestHDWallet.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestHDWallet.kt index e3ec6f29b12..6e6d2e995b9 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestHDWallet.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestHDWallet.kt @@ -113,13 +113,13 @@ class TestHDWallet { val coin = CoinType.BITCOIN val wallet = HDWallet(words, password) - val key1 = wallet.getKeyDerivation(coin, Derivation.BITCOINSEGWIT) + val key1 = wallet.getKeyDerivation(coin, Derivation.SEGWIT) assertEquals(key1.data().toHex(), "0x1901b5994f075af71397f65bd68a9fff8d3025d65f5a2c731cf90f5e259d6aac") - val key2 = wallet.getKeyDerivation(coin, Derivation.BITCOINLEGACY) + val key2 = wallet.getKeyDerivation(coin, Derivation.LEGACY) assertEquals(key2.data().toHex(), "0x28071bf4e2b0340db41b807ed8a5514139e5d6427ff9d58dbd22b7ed187103a4") - val key3 = wallet.getKeyDerivation(coin, Derivation.BITCOINTESTNET) + val key3 = wallet.getKeyDerivation(coin, Derivation.TESTNET) assertEquals(key3.data().toHex(), "0xca5845e1b43e3adf577b7f110b60596479425695005a594c88f9901c3afe864f") } @@ -137,13 +137,13 @@ class TestHDWallet { val coin = CoinType.BITCOIN val wallet = HDWallet(words, password) - val address1 = wallet.getAddressDerivation(coin, Derivation.BITCOINSEGWIT) + val address1 = wallet.getAddressDerivation(coin, Derivation.SEGWIT) assertEquals(address1, "bc1qumwjg8danv2vm29lp5swdux4r60ezptzz7ce85") - val address2 = wallet.getAddressDerivation(coin, Derivation.BITCOINLEGACY) + val address2 = wallet.getAddressDerivation(coin, Derivation.LEGACY) assertEquals(address2, "1PeUvjuxyf31aJKX6kCXuaqxhmG78ZUdL1") - val address3 = wallet.getAddressDerivation(coin, Derivation.BITCOINTESTNET) + val address3 = wallet.getAddressDerivation(coin, Derivation.TESTNET) assertEquals(address3, "tb1qwgpxgwn33z3ke9s7q65l976pseh4edrzfmyvl0") } diff --git a/codegen/bin/coins b/codegen/bin/coins index a5ea5909e05..4c25b67fc50 100755 --- a/codegen/bin/coins +++ b/codegen/bin/coins @@ -31,9 +31,9 @@ def self.derivation_name(deriv) deriv['name'].downcase end -def self.derivation_enum_name(deriv, coin) +def self.derivation_enum_name(deriv) return "TWDerivationDefault" if deriv['name'].nil? - "TWDerivation" + format_name(coin['name']) + camel_case(deriv['name']) + "TWDerivation" + camel_case(deriv['name']) end def self.coin_img(coin) @@ -53,6 +53,7 @@ coins = JSON.parse(json_string).sort_by { |x| x['coinId'] } # used in some cases for numbering enum values enum_count = 0 +derivations = {} erbs = [ {'template' => 'TWDerivation.h.erb', 'folder' => 'include/TrustWalletCore', 'file' => 'TWDerivation.h'}, diff --git a/codegen/lib/templates/CoinInfoData.cpp.erb b/codegen/lib/templates/CoinInfoData.cpp.erb index d2bbb5d6b71..6e0d29a074e 100644 --- a/codegen/lib/templates/CoinInfoData.cpp.erb +++ b/codegen/lib/templates/CoinInfoData.cpp.erb @@ -51,7 +51,7 @@ const CoinInfo getCoinInfo(TWCoinType coin) { TWCurve<%= format_name(coin['curve']) %>, { <% coin['derivation'].each do |deriv| -%>{ - <%= derivation_enum_name(deriv, coin) %>, + <%= derivation_enum_name(deriv) %>, "<%= deriv['path'] %>", "<%= derivation_name(deriv) %>", TWHDVersion<% if deriv['xpub'].nil? -%>None<% else -%><%= format_name(deriv['xpub']) %><% end -%>, diff --git a/codegen/lib/templates/TWDerivation.h.erb b/codegen/lib/templates/TWDerivation.h.erb index eb7759a55c2..6ef63d074b3 100644 --- a/codegen/lib/templates/TWDerivation.h.erb +++ b/codegen/lib/templates/TWDerivation.h.erb @@ -18,11 +18,10 @@ enum TWDerivation { TWDerivationCustom = 1, // custom, for any coin <% enum_count += 1 -%> <% coins.each do |coin| -%> -<% if coin['derivation'].count > 1 -%> <% coin['derivation'].each_with_index do |deriv, index| -%> -<% if index > 0 or !deriv['name'].nil? -%> - <%= derivation_enum_name(deriv, coin) %> = <% enum_count += 1 -%><%= enum_count %>, -<% end -%> +<% if !deriv['name'].nil? and !derivations.has_key?(deriv['name']) -%> +<% derivations[deriv['name']] = true -%> + <%= derivation_enum_name(deriv) %> = <% enum_count += 1 -%><%= enum_count %>, <% end -%> <% end -%> <% end -%> diff --git a/include/TrustWalletCore/TWBitcoinFee.h b/include/TrustWalletCore/TWBitcoinFee.h deleted file mode 100644 index be4f116d644..00000000000 --- a/include/TrustWalletCore/TWBitcoinFee.h +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#pragma once - -#include "TWData.h" -#include "TWString.h" - -TW_EXTERN_C_BEGIN - -TW_EXPORT_CLASS -struct TWBitcoinFee; - -/// Calculates the fee of any Bitcoin transaction. -/// -/// \param data: the signed transaction in its final form. -/// \param satVb: the satoshis per vbyte amount. The passed on string is interpreted as a unit64_t. -/// \returns the fee denominated in satoshis or nullptr if the transaction failed to be decoded. -TW_EXPORT_STATIC_METHOD -TWString* _Nullable TWBitcoinFeeCalculateFee(TWData* _Nonnull data, TWString* _Nonnull satVb); - -TW_EXTERN_C_END diff --git a/include/TrustWalletCore/TWBitcoinScript.h b/include/TrustWalletCore/TWBitcoinScript.h index f1a7844bfb9..b49e65ae1f8 100644 --- a/include/TrustWalletCore/TWBitcoinScript.h +++ b/include/TrustWalletCore/TWBitcoinScript.h @@ -191,26 +191,6 @@ 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 the Ordinals inscripton for NFT construction. -/// -/// \param mimeType the MIME type of the payload -/// \param payload the payload to inscribe -/// \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 TWBitcoinScriptBuildOrdinalNftInscription(TWString* _Nonnull mimeType, TWData* _Nonnull payload, TWData* _Nonnull pubkey); - /// Builds a appropriate lock script for the given address.. /// /// \param address Non-null pointer to an address diff --git a/registry.json b/registry.json index f3a8dca671d..39d399fac92 100644 --- a/registry.json +++ b/registry.json @@ -56,6 +56,7 @@ "blockchain": "Bitcoin", "derivation": [ { + "name": "segwit", "path": "m/84'/2'/0'/0/0", "xpub": "zpub", "xprv": "zprv" @@ -159,9 +160,16 @@ "blockchain": "Bitcoin", "derivation": [ { + "name": "segwit", "path": "m/84'/14'/0'/0/0", "xpub": "zpub", "xprv": "zprv" + }, + { + "name": "legacy", + "path": "m/44'/14'/0'/0/0", + "xpub": "xpub", + "xprv": "xprv" } ], "curve": "secp256k1", @@ -225,6 +233,7 @@ "blockchain": "Bitcoin", "derivation": [ { + "name": "segwit", "path": "m/84'/20'/0'/0/0", "xpub": "zpub", "xprv": "zprv" @@ -258,9 +267,16 @@ "blockchain": "Bitcoin", "derivation": [ { + "name": "legacy", "path": "m/44'/22'/0'/0/0", "xpub": "xpub", "xprv": "xprv" + }, + { + "name": "segwit", + "path": "m/84'/22'/0'/0/0", + "xpub": "zpub", + "xprv": "zprv" } ], "curve": "secp256k1", @@ -324,9 +340,16 @@ "blockchain": "Bitcoin", "derivation": [ { + "name": "segwit", "path": "m/84'/57'/0'/0/0", "xpub": "zpub", "xprv": "zprv" + }, + { + "name": "legacy", + "path": "m/44'/57'/0'/0/0", + "xpub": "xpub", + "xprv": "xprv" } ], "curve": "secp256k1", @@ -1517,9 +1540,16 @@ "blockchain": "Bitcoin", "derivation": [ { + "name": "segwit", "path": "m/84'/156'/0'/0/0", "xpub": "zpub", "xprv": "zprv" + }, + { + "name": "legacy", + "path": "m/44'/156'/0'/0/0", + "xpub": "xpub", + "xprv": "xprv" } ], "curve": "secp256k1", @@ -2842,9 +2872,16 @@ "blockchain": "Bitcoin", "derivation": [ { + "name": "legacy", "path": "m/44'/2301'/0'/0/0", "xpub": "xpub", "xprv": "xprv" + }, + { + "name": "segwit", + "path": "m/84'/2301'/0'/0/0", + "xpub": "zpub", + "xprv": "zprv" } ], "curve": "secp256k1", @@ -4243,6 +4280,7 @@ "blockchain": "Bitcoin", "derivation": [ { + "name": "segwit", "path": "m/44'/105105'/0'/0/0", "xpub": "xpub", "xprv": "xprv" diff --git a/rust/Cargo.lock b/rust/Cargo.lock index e94bd038f53..0eb28d98085 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -476,6 +476,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "derivation-path" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5c37193a1db1d8ed868c03ec7b152175f26160a5b740e5e484143877e0adf0" + [[package]] name = "derivative" version = "2.2.0" @@ -1633,6 +1639,7 @@ dependencies = [ "tw_misc", "tw_number", "tw_proto", + "tw_utxo", ] [[package]] @@ -1652,6 +1659,16 @@ dependencies = [ "tw_proto", ] +[[package]] +name = "tw_base58_address" +version = "0.1.0" +dependencies = [ + "serde", + "tw_coin_entry", + "tw_encoding", + "tw_hash", +] + [[package]] name = "tw_bech32_address" version = "0.1.0" @@ -1693,8 +1710,11 @@ dependencies = [ "secp256k1", "serde", "serde_json", + "tw_base58_address", + "tw_bech32_address", "tw_coin_entry", "tw_encoding", + "tw_hash", "tw_keypair", "tw_memory", "tw_misc", @@ -1707,6 +1727,8 @@ dependencies = [ name = "tw_coin_entry" version = "0.1.0" dependencies = [ + "derivation-path", + "serde", "serde_json", "tw_encoding", "tw_hash", @@ -1884,6 +1906,7 @@ name = "tw_keypair" version = "0.1.0" dependencies = [ "arbitrary 1.3.0", + "bitcoin", "blake2", "curve25519-dalek", "der", @@ -1895,6 +1918,7 @@ dependencies = [ "pkcs8", "rfc6979", "ring", + "secp256k1", "serde", "serde_json", "sha2", @@ -1902,6 +1926,7 @@ dependencies = [ "starknet-ff", "tw_encoding", "tw_hash", + "tw_keypair", "tw_memory", "tw_misc", "zeroize", @@ -2027,12 +2052,20 @@ dependencies = [ name = "tw_utxo" version = "0.1.0" dependencies = [ + "bech32", "bitcoin", + "byteorder", + "itertools", "secp256k1", + "strum_macros", + "tw_base58_address", + "tw_bech32_address", "tw_coin_entry", "tw_encoding", + "tw_hash", "tw_keypair", "tw_memory", + "tw_misc", "tw_proto", ] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 125780d2be9..2b0bfa58765 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -12,7 +12,9 @@ members = [ "chains/tw_solana", "chains/tw_sui", "chains/tw_thorchain", + "frameworks/tw_utxo", "tw_any_coin", + "tw_base58_address", "tw_bech32_address", "tw_bitcoin", "tw_coin_entry", @@ -26,7 +28,6 @@ members = [ "tw_misc", "tw_number", "tw_proto", - "tw_utxo", "wallet_core_bin", "wallet_core_rs", ] diff --git a/rust/chains/tw_solana/src/address.rs b/rust/chains/tw_solana/src/address.rs index 9832509cdfa..cb2051be84c 100644 --- a/rust/chains/tw_solana/src/address.rs +++ b/rust/chains/tw_solana/src/address.rs @@ -140,7 +140,7 @@ impl FromStr for SolanaAddress { fn from_str(s: &str) -> Result { let bytes = - base58::decode(s, &SOLANA_ALPHABET).map_err(|_| AddressError::FromBase58Error)?; + base58::decode(s, SOLANA_ALPHABET).map_err(|_| AddressError::FromBase58Error)?; let bytes = H256::try_from(bytes.as_slice()).map_err(|_| AddressError::InvalidInput)?; Ok(SolanaAddress { bytes }) } @@ -160,7 +160,7 @@ impl fmt::Debug for SolanaAddress { impl fmt::Display for SolanaAddress { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let encoded = base58::encode(self.bytes.as_slice(), &SOLANA_ALPHABET); + let encoded = base58::encode(self.bytes.as_slice(), SOLANA_ALPHABET); write!(f, "{}", encoded) } } diff --git a/rust/chains/tw_solana/src/blockhash.rs b/rust/chains/tw_solana/src/blockhash.rs index 8c7844dae26..1cfe0a92fa5 100644 --- a/rust/chains/tw_solana/src/blockhash.rs +++ b/rust/chains/tw_solana/src/blockhash.rs @@ -25,7 +25,7 @@ impl Blockhash { impl fmt::Display for Blockhash { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", base58::encode(self.0.as_slice(), &SOLANA_ALPHABET)) + write!(f, "{}", base58::encode(self.0.as_slice(), SOLANA_ALPHABET)) } } @@ -33,7 +33,7 @@ impl FromStr for Blockhash { type Err = EncodingError; fn from_str(s: &str) -> Result { - let bytes = base58::decode(s, &SOLANA_ALPHABET)?; + let bytes = base58::decode(s, SOLANA_ALPHABET)?; let bytes = H256::try_from(bytes.as_slice()).map_err(|_| EncodingError::InvalidInput)?; Ok(Blockhash(bytes)) } diff --git a/rust/chains/tw_solana/src/compiler.rs b/rust/chains/tw_solana/src/compiler.rs index 483ddac98cb..f3341bbd907 100644 --- a/rust/chains/tw_solana/src/compiler.rs +++ b/rust/chains/tw_solana/src/compiler.rs @@ -68,7 +68,7 @@ impl SolanaCompiler { public_keys: Vec, ) -> SigningResult> { let encode = move |data| match input.tx_encoding { - Proto::Encoding::Base58 => base58::encode(data, &SOLANA_ALPHABET), + Proto::Encoding::Base58 => base58::encode(data, SOLANA_ALPHABET), Proto::Encoding::Base64 => base64::encode(data, false), }; diff --git a/rust/chains/tw_solana/src/lib.rs b/rust/chains/tw_solana/src/lib.rs index 7e945d862f0..5bf3b779386 100644 --- a/rust/chains/tw_solana/src/lib.rs +++ b/rust/chains/tw_solana/src/lib.rs @@ -16,4 +16,4 @@ pub mod signer; pub mod transaction; // cbindgen:ignore -pub const SOLANA_ALPHABET: Alphabet = *Alphabet::BITCOIN; +pub const SOLANA_ALPHABET: Alphabet = Alphabet::Bitcoin; diff --git a/rust/chains/tw_solana/src/modules/compiled_instructions.rs b/rust/chains/tw_solana/src/modules/compiled_instructions.rs index 4e0e02a0c9c..28b0f30cd6b 100644 --- a/rust/chains/tw_solana/src/modules/compiled_instructions.rs +++ b/rust/chains/tw_solana/src/modules/compiled_instructions.rs @@ -54,7 +54,7 @@ mod tests { fn test_compile_instruction() { let public_0 = base58::decode( "GymAh18wHuFTytfSJWi8eYTA9x5S3sNb9CJSGBWoPRE3", - &SOLANA_ALPHABET, + SOLANA_ALPHABET, ) .unwrap(); let public_0 = ed25519::sha512::PublicKey::try_from(public_0.as_slice()).unwrap(); @@ -62,7 +62,7 @@ mod tests { let public_1 = base58::decode( "2oKoYSAHgveX91917v4DUEuN8BNKXDg8KJWpaGyEay9V", - &SOLANA_ALPHABET, + SOLANA_ALPHABET, ) .unwrap(); let public_1 = ed25519::sha512::PublicKey::try_from(public_1.as_slice()).unwrap(); diff --git a/rust/chains/tw_solana/src/modules/utils.rs b/rust/chains/tw_solana/src/modules/utils.rs index 0bf4655d161..cc9a1ebf4ec 100644 --- a/rust/chains/tw_solana/src/modules/utils.rs +++ b/rust/chains/tw_solana/src/modules/utils.rs @@ -40,7 +40,7 @@ impl SolanaTransaction { bincode::deserialize(&tx_bytes).map_err(|_| SigningErrorType::Error_input_parse)?; let mut msg_to_sign = tx_to_sign.message; - let new_blockchain_hash = base58::decode(recent_blockhash, &SOLANA_ALPHABET)?; + let new_blockchain_hash = base58::decode(recent_blockhash, SOLANA_ALPHABET)?; let new_blockchain_hash = H256::try_from(new_blockchain_hash.as_slice()) .tw_err(|_| SigningErrorType::Error_invalid_params)?; diff --git a/rust/chains/tw_solana/src/signer.rs b/rust/chains/tw_solana/src/signer.rs index 11b3e8bd2f9..4d3db06c69a 100644 --- a/rust/chains/tw_solana/src/signer.rs +++ b/rust/chains/tw_solana/src/signer.rs @@ -29,7 +29,7 @@ impl SolanaSigner { input: Proto::SigningInput<'_>, ) -> SigningResult> { let encode = move |data| match input.tx_encoding { - Proto::Encoding::Base58 => base58::encode(data, &SOLANA_ALPHABET), + Proto::Encoding::Base58 => base58::encode(data, SOLANA_ALPHABET), Proto::Encoding::Base64 => base64::encode(data, false), }; diff --git a/rust/chains/tw_solana/src/transaction/mod.rs b/rust/chains/tw_solana/src/transaction/mod.rs index 7ba4da1ac63..b9875f15c74 100644 --- a/rust/chains/tw_solana/src/transaction/mod.rs +++ b/rust/chains/tw_solana/src/transaction/mod.rs @@ -53,7 +53,7 @@ impl FromStr for Signature { type Err = SigningError; fn from_str(s: &str) -> Result { - let data = base58::decode(s, &SOLANA_ALPHABET) + let data = base58::decode(s, SOLANA_ALPHABET) .tw_err(|_| SigningErrorType::Error_input_parse) .context("Error decoding Solana Signature from base58")?; H512::try_from(data.as_slice()) @@ -65,7 +65,7 @@ impl FromStr for Signature { impl fmt::Display for Signature { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", base58::encode(self.0.as_slice(), &SOLANA_ALPHABET)) + write!(f, "{}", base58::encode(self.0.as_slice(), SOLANA_ALPHABET)) } } @@ -82,11 +82,11 @@ mod tests { use tw_memory::Data; fn base58_decode(s: &'static str) -> Data { - base58::decode(s, &SOLANA_ALPHABET).unwrap() + base58::decode(s, SOLANA_ALPHABET).unwrap() } fn base58_decode_h256(s: &'static str) -> H256 { - let bytes = base58::decode(s, &SOLANA_ALPHABET).unwrap(); + let bytes = base58::decode(s, SOLANA_ALPHABET).unwrap(); H256::try_from(bytes.as_slice()).unwrap() } diff --git a/rust/chains/tw_solana/tests/update_blockhash_and_sign.rs b/rust/chains/tw_solana/tests/update_blockhash_and_sign.rs index 6dffaa32a63..5521e7069af 100644 --- a/rust/chains/tw_solana/tests/update_blockhash_and_sign.rs +++ b/rust/chains/tw_solana/tests/update_blockhash_and_sign.rs @@ -15,7 +15,7 @@ fn test_update_recent_blockhash_and_sign() { let new_blockhash = "CyPYVsYWrsJNfVpi8aazu7WsrswNFuDd385z6GNoBGUg"; let private_key = base58::decode( "A7psj2GW7ZMdY4E5hJq14KMeYg7HFjULSsWSrTXZLvYr", - &SOLANA_ALPHABET, + SOLANA_ALPHABET, ) .unwrap(); diff --git a/rust/chains/tw_sui/src/transaction/sui_types.rs b/rust/chains/tw_sui/src/transaction/sui_types.rs index 625e6bde69e..1948789adf9 100644 --- a/rust/chains/tw_sui/src/transaction/sui_types.rs +++ b/rust/chains/tw_sui/src/transaction/sui_types.rs @@ -39,7 +39,7 @@ impl FromStr for ObjectDigest { type Err = SigningError; fn from_str(s: &str) -> Result { - let bytes = base58::decode(s, Alphabet::BITCOIN) + let bytes = base58::decode(s, Alphabet::Bitcoin) .tw_err(|_| SigningErrorType::Error_invalid_params) .context("Invalid Object Digest: expected valid base58 string")?; H256::try_from(bytes.as_slice()) diff --git a/rust/frameworks/tw_utxo/Cargo.toml b/rust/frameworks/tw_utxo/Cargo.toml new file mode 100644 index 00000000000..08009365f7f --- /dev/null +++ b/rust/frameworks/tw_utxo/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "tw_utxo" +version = "0.1.0" +edition = "2021" + +[dependencies] +bech32 = "0.9.1" +bitcoin = { version = "0.30.0", features = ["rand-std"] } +byteorder = "1.4" +itertools = "0.10.5" +secp256k1 = { version = "0.27.0", features = ["rand-std"] } +strum_macros = "0.25" +tw_base58_address = { path = "../../tw_base58_address" } +tw_bech32_address = { path = "../../tw_bech32_address" } +tw_coin_entry = { path = "../../tw_coin_entry" } +tw_encoding = { path = "../../tw_encoding" } +tw_hash = { path = "../../tw_hash" } +tw_keypair = { path = "../../tw_keypair" } +tw_memory = { path = "../../tw_memory" } +tw_misc = { path = "../../tw_misc" } +tw_proto = { path = "../../tw_proto" } + +[dev-dependencies] +tw_coin_entry = { path = "../../tw_coin_entry", features = ["test-utils"] } diff --git a/rust/frameworks/tw_utxo/src/address/derivation.rs b/rust/frameworks/tw_utxo/src/address/derivation.rs new file mode 100644 index 00000000000..bd6cb3cfaf0 --- /dev/null +++ b/rust/frameworks/tw_utxo/src/address/derivation.rs @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::derivation::Derivation; + +pub enum BitcoinDerivation { + Legacy, + Segwit, +} + +impl BitcoinDerivation { + /// TrustWallet derivation inherited from: + /// https://github.com/trustwallet/wallet-core/blob/b65adc4c86e49eb905f659ade025185a62e87ca9/src/Bitcoin/Entry.cpp#L67 + pub fn tw_derivation(coin: &dyn CoinContext, derivation: Derivation) -> BitcoinDerivation { + match derivation { + Derivation::Default + // Please note that testnet derivation is no longer supported. Instead, use address prefix. + | Derivation::Testnet => { + let Some(default_derivation) = coin.derivations().first() else { + return BitcoinDerivation::Segwit; + }; + if default_derivation.name == Derivation::Segwit { + return BitcoinDerivation::Segwit; + } + BitcoinDerivation::Legacy + }, + Derivation::Segwit => BitcoinDerivation::Segwit, + Derivation::Legacy => BitcoinDerivation::Legacy, + } + } + + /// TrustWallet behaviour inherited from: + /// https://github.com/trustwallet/wallet-core/blob/b65adc4c86e49eb905f659ade025185a62e87ca9/src/Bitcoin/Entry.cpp#L14 + pub fn tw_supports_segwit(coin: &dyn CoinContext) -> bool { + coin.derivations() + .iter() + .any(|der| der.name == Derivation::Segwit) + } +} diff --git a/rust/frameworks/tw_utxo/src/address/legacy.rs b/rust/frameworks/tw_utxo/src/address/legacy.rs new file mode 100644 index 00000000000..8efbb1f9c43 --- /dev/null +++ b/rust/frameworks/tw_utxo/src/address/legacy.rs @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::script::Script; +use std::fmt; +use std::str::FromStr; +use tw_base58_address::Base58Address; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::prefix::BitcoinBase58Prefix; +use tw_encoding::base58::Alphabet; +use tw_hash::hasher::{sha256_ripemd, Hasher}; +use tw_hash::H160; +use tw_keypair::{ecdsa, tw}; + +pub const BITCOIN_ADDRESS_SIZE: usize = 21; +pub const BITCOIN_ADDRESS_CHECKSUM_SIZE: usize = 4; + +type BitcoinBase58Address = Base58Address; + +#[derive(Debug, Eq, PartialEq)] +pub struct LegacyAddress(BitcoinBase58Address); + +impl LegacyAddress { + pub fn new(prefix: u8, data: &[u8]) -> AddressResult { + let mut bytes = Vec::with_capacity(data.len() + 1); + // Insert the prefix to the beginning of the address bytes array. + bytes.push(prefix); + bytes.extend_from_slice(data); + + BitcoinBase58Address::new(&bytes, Alphabet::Bitcoin, Hasher::Sha256d).map(LegacyAddress) + } + + pub fn p2pkh_with_public_key( + p2pkh_prefix: u8, + public_key: &ecdsa::secp256k1::PublicKey, + ) -> AddressResult { + let public_key_hash = sha256_ripemd(public_key.compressed().as_slice()); + LegacyAddress::new(p2pkh_prefix, &public_key_hash) + } + + /// Tries to parse a `LegacyAddress` and check if + pub fn p2pkh_with_coin_and_prefix( + coin: &dyn CoinContext, + public_key: &tw::PublicKey, + prefix: Option, + ) -> AddressResult { + let p2pkh_prefix = match prefix { + Some(prefix) => prefix.p2pkh, + None => coin.p2pkh_prefix().ok_or(AddressError::InvalidRegistry)?, + }; + + let ecdsa_public_key = public_key + .to_secp256k1() + .ok_or(AddressError::PublicKeyTypeMismatch)?; + + LegacyAddress::p2pkh_with_public_key(p2pkh_prefix, ecdsa_public_key) + } + + pub fn p2sh_with_prefix_byte( + redeem_script: &Script, + p2sh_prefix: u8, + ) -> AddressResult { + let script_hash = sha256_ripemd(redeem_script.as_slice()); + LegacyAddress::new(p2sh_prefix, &script_hash) + } + + pub fn from_str_with_coin_and_prefix( + coin: &dyn CoinContext, + s: &str, + prefix: Option, + ) -> AddressResult { + let base58_prefix = match prefix { + Some(base58_prefix) => base58_prefix, + None => { + let p2pkh = coin.p2pkh_prefix().ok_or(AddressError::InvalidRegistry)?; + let p2sh = coin.p2sh_prefix().ok_or(AddressError::InvalidRegistry)?; + BitcoinBase58Prefix { p2pkh, p2sh } + }, + }; + + LegacyAddress::from_str_checked(s, base58_prefix.p2pkh, base58_prefix.p2sh) + } + + pub fn from_str_checked( + s: &str, + p2pkh_prefix: u8, + p2sh_prefix: u8, + ) -> AddressResult { + let addr = LegacyAddress::from_str(s)?; + if addr.prefix() == p2pkh_prefix || addr.prefix() == p2sh_prefix { + Ok(addr) + } else { + Err(AddressError::UnexpectedAddressPrefix) + } + } + + pub fn prefix(&self) -> u8 { + self.bytes()[0] + } + + pub fn bytes(&self) -> &[u8] { + self.0.as_ref() + } + + pub fn payload(&self) -> H160 { + debug_assert_eq!(self.bytes().len(), H160::LEN + 1); + H160::try_from(&self.0.as_ref()[1..]).expect("Legacy address must be exactly 21 bytes") + } +} + +impl FromStr for LegacyAddress { + type Err = AddressError; + + fn from_str(s: &str) -> Result { + BitcoinBase58Address::from_str_with_alphabet(s, Alphabet::Bitcoin, Hasher::Sha256d) + .map(LegacyAddress) + } +} + +impl<'a> TryFrom<&'a [u8]> for LegacyAddress { + type Error = AddressError; + + fn try_from(bytes: &'a [u8]) -> Result { + BitcoinBase58Address::new(bytes, Alphabet::Bitcoin, Hasher::Sha256d).map(LegacyAddress) + } +} + +impl fmt::Display for LegacyAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/rust/frameworks/tw_utxo/src/address/mod.rs b/rust/frameworks/tw_utxo/src/address/mod.rs new file mode 100644 index 00000000000..c7c4539b421 --- /dev/null +++ b/rust/frameworks/tw_utxo/src/address/mod.rs @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod derivation; +pub mod legacy; +pub mod segwit; +pub mod standard_bitcoin; +pub mod taproot; +pub mod witness_program; + +type Bech32Prefix = tw_bech32_address::bech32_prefix::Bech32Prefix; diff --git a/rust/frameworks/tw_utxo/src/address/segwit.rs b/rust/frameworks/tw_utxo/src/address/segwit.rs new file mode 100644 index 00000000000..8098fc4ad93 --- /dev/null +++ b/rust/frameworks/tw_utxo/src/address/segwit.rs @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use super::Bech32Prefix; +use crate::address::witness_program::{WitnessProgram, WITNESS_V0}; +use crate::script::Script; +use core::fmt; +use std::str::FromStr; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_hash::hasher::sha256_ripemd; +use tw_hash::sha2::sha256; +use tw_hash::{H160, H256}; +use tw_keypair::tw; +use tw_memory::Data; + +/// Witness program sizes valid for V0 (Segwit). +const WITNESS_V0_VALID_PROGRAM_SIZES: [usize; 2] = [H160::LEN, H256::LEN]; + +#[derive(Debug, Eq, PartialEq)] +pub struct SegwitAddress { + inner: WitnessProgram, +} + +impl SegwitAddress { + pub fn new(hrp: String, witness_program: Data) -> AddressResult { + // Specific segwit v0 check. These addresses can never spend funds sent to them. + if !WITNESS_V0_VALID_PROGRAM_SIZES.contains(&witness_program.len()) { + return Err(AddressError::InvalidInput); + } + + let inner = WitnessProgram::new(hrp, WITNESS_V0, witness_program, bech32::Variant::Bech32)?; + Ok(SegwitAddress { inner }) + } + + pub fn p2wpkh_with_coin_and_prefix( + coin: &dyn CoinContext, + public_key: &tw::PublicKey, + prefix: Option, + ) -> AddressResult { + let hrp = match prefix { + Some(Bech32Prefix { hrp }) => hrp, + None => coin.hrp().ok_or(AddressError::InvalidRegistry)?, + }; + + let public_key_bytes = public_key + .to_secp256k1() + .ok_or(AddressError::PublicKeyTypeMismatch)? + .compressed(); + let public_key_hash = sha256_ripemd(public_key_bytes.as_slice()); + + Self::new(hrp, public_key_hash.to_vec()) + } + + pub fn p2wsh_with_hrp(redeem_script: &Script, hrp: String) -> AddressResult { + let script_hash = sha256(redeem_script.as_slice()); + Self::new(hrp, script_hash) + } + + pub fn from_str_checked(s: &str, expected_hrp: &str) -> AddressResult { + let address = Self::from_str(s)?; + if address.inner.hrp() != expected_hrp { + return Err(AddressError::InvalidHrp); + } + Ok(address) + } + + pub fn from_str_with_coin_and_prefix( + coin: &dyn CoinContext, + s: &str, + prefix: Option, + ) -> AddressResult { + let hrp = match prefix { + Some(Bech32Prefix { hrp }) => hrp, + None => coin.hrp().ok_or(AddressError::InvalidRegistry)?, + }; + SegwitAddress::from_str_checked(s, &hrp) + } + + pub fn witness_program(&self) -> &[u8] { + self.inner.witness_program() + } +} + +impl FromStr for SegwitAddress { + type Err = AddressError; + + fn from_str(s: &str) -> Result { + let inner = WitnessProgram::from_str_checked( + s, + WITNESS_V0, + bech32::Variant::Bech32, + &WITNESS_V0_VALID_PROGRAM_SIZES, + )?; + Ok(SegwitAddress { inner }) + } +} + +impl fmt::Display for SegwitAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.inner) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tw_encoding::hex::DecodeHex; + + struct TestInputValid { + str: &'static str, + normalized: &'static str, + expected: SegwitAddress, + } + + #[track_caller] + fn segwit_addr(hrp: &str, program: &str) -> SegwitAddress { + SegwitAddress::new(hrp.to_string(), program.decode_hex().unwrap()) + .expect("Cannot construct a SegwitAddress from the input") + } + + /// Tests if the given `s` string representation is converted from and to `expected` segwit address. + #[track_caller] + fn test_to_from_str_valid(input: TestInputValid) { + let actual = SegwitAddress::from_str(input.str).expect("Expected a valid address"); + assert_eq!(actual, input.expected, "String -> SegwitAddress"); + + let actual_str = actual.to_string(); + assert_eq!(actual_str, input.normalized, "SegwitAddress -> String"); + } + + #[track_caller] + fn test_from_str_invalid(str: &str) { + let _ = SegwitAddress::from_str(str).expect_err("Expected an invalid Segwit address"); + } + + #[test] + fn test_segwit_address_to_from_str() { + test_to_from_str_valid(TestInputValid { + str: "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", + normalized: "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", + expected: segwit_addr("bc", "751e76e8199196d454941c45d1b3a323f1433bd6"), + }); + + test_to_from_str_valid(TestInputValid { + str: "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", + normalized: "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", + expected: segwit_addr( + "tb", + "1863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262", + ), + }); + + test_to_from_str_valid(TestInputValid { + str: "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", + normalized: "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", + expected: segwit_addr( + "tb", + "000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433", + ), + }); + + test_to_from_str_valid(TestInputValid { + str: "bc1qpjult34k9spjfym8hss2jrwjgf0xjf40ze0pp8", + normalized: "bc1qpjult34k9spjfym8hss2jrwjgf0xjf40ze0pp8", + expected: segwit_addr("bc", "0cb9f5c6b62c03249367bc20a90dd2425e6926af"), + }); + + test_to_from_str_valid(TestInputValid { + str: "bc1qm9jzmujvdqjj6y28hptk859zs3yyv78hpqqjfj", + normalized: "bc1qm9jzmujvdqjj6y28hptk859zs3yyv78hpqqjfj", + expected: segwit_addr("bc", "d9642df24c68252d1147b85763d0a284484678f7"), + }); + + test_to_from_str_valid(TestInputValid { + str: "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", + normalized: "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", + expected: segwit_addr( + "tb", + "000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433", + ), + }); + } + + #[test] + fn test_segwit_address_from_str_invalid() { + // witness program size 38 + test_from_str_invalid( + "bc1q0xlxvlhemja6c4dqv22uapctqupfhlxm0xlxvlhemja6c4dqv22uapctqupfkpvgusg", + ); + + // version 1 + test_from_str_invalid("bc1ptmsk7c2yut2xah4pgflpygh2s7fh0cpfkrza9cjj29awapv53mrslgd5cf"); + + // version 1 + test_from_str_invalid( + "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y", + ); + + // version 2 + test_from_str_invalid("bc1zw508d6qejxtdg4y5r3zarvaryvaxxpcs"); + + // version 16 + test_from_str_invalid("BC1SW50QGDZ25J"); + } +} diff --git a/rust/frameworks/tw_utxo/src/address/standard_bitcoin.rs b/rust/frameworks/tw_utxo/src/address/standard_bitcoin.rs new file mode 100644 index 00000000000..6f582b0976b --- /dev/null +++ b/rust/frameworks/tw_utxo/src/address/standard_bitcoin.rs @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +//! This module contains a standard Bitcoin address enumeration each of these exist on the mainnet network. +//! TODO consider moving the file to `tw_bitcoin`. + +use crate::address::derivation::BitcoinDerivation; +use crate::address::legacy::LegacyAddress; +use crate::address::segwit::SegwitAddress; +use crate::address::taproot::TaprootAddress; +use crate::address::Bech32Prefix; +use std::fmt; +use std::str::FromStr; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::CoinAddress; +use tw_coin_entry::derivation::Derivation; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::prefix::{AddressPrefix, BitcoinBase58Prefix}; +use tw_keypair::tw; +use tw_memory::Data; + +/// A standard set of Bitcoin address prefixes. +/// The set of address prefixes can differ for Bitcoin forks. +/// TODO add `TaprootBech32` enum variant. +pub enum StandardBitcoinPrefix { + Base58(BitcoinBase58Prefix), + Bech32(Bech32Prefix), +} + +impl TryFrom for StandardBitcoinPrefix { + type Error = AddressError; + + fn try_from(prefix: AddressPrefix) -> Result { + match prefix { + AddressPrefix::BitcoinBase58(base58) => Ok(StandardBitcoinPrefix::Base58(base58)), + AddressPrefix::Hrp(hrp) => Ok(StandardBitcoinPrefix::Bech32(Bech32Prefix { hrp })), + } + } +} + +/// A standard set of Bitcoin address types. +/// +/// The set of address types can differ for Bitcoin forks. +/// For example, Zcash does not support segwit addresses. +#[derive(Debug, Eq, PartialEq)] +pub enum StandardBitcoinAddress { + Legacy(LegacyAddress), + Segwit(SegwitAddress), + Taproot(TaprootAddress), +} + +impl StandardBitcoinAddress { + /// Tries to parse one of the `BitcoinAddress` variants + /// and validates if the result address matches the given `prefix` address or belongs to the `coin` network. + pub fn from_str_with_coin_and_prefix( + coin: &dyn CoinContext, + s: &str, + prefix: Option, + ) -> AddressResult { + match prefix { + Some(StandardBitcoinPrefix::Base58(base58)) => { + LegacyAddress::from_str_with_coin_and_prefix(coin, s, Some(base58)) + .map(StandardBitcoinAddress::Legacy) + }, + Some(StandardBitcoinPrefix::Bech32(bech32)) => { + SegwitAddress::from_str_with_coin_and_prefix(coin, s, Some(bech32)) + .map(StandardBitcoinAddress::Segwit) + }, + None => StandardBitcoinAddress::from_str_checked(coin, s), + } + } + + /// Tries to parse one of the `BitcoinAddress` variants + /// and validates if the result address belongs to the `coin` network. + pub fn from_str_checked( + coin: &dyn CoinContext, + s: &str, + ) -> AddressResult { + // Try to parse a Segwit address if the coin supports it. + if BitcoinDerivation::tw_supports_segwit(coin) { + if let Ok(segwit) = SegwitAddress::from_str_with_coin_and_prefix(coin, s, None) { + return Ok(StandardBitcoinAddress::Segwit(segwit)); + } + + // TODO use `BitcoinDerivation::tw_supports_taproot` based on `registry.json`. + if let Ok(taproot) = TaprootAddress::from_str_with_coin_and_prefix(coin, s, None) { + return Ok(StandardBitcoinAddress::Taproot(taproot)); + } + } + + // Otherwise, try to parse a Legacy address. + if let Ok(legacy) = LegacyAddress::from_str_with_coin_and_prefix(coin, s, None) { + return Ok(StandardBitcoinAddress::Legacy(legacy)); + } + + Err(AddressError::InvalidInput) + } + + /// TrustWallet derivation inherited from: + /// https://github.com/trustwallet/wallet-core/blob/b65adc4c86e49eb905f659ade025185a62e87ca9/src/Bitcoin/Entry.cpp#L67 + pub fn derive_as_tw( + coin: &dyn CoinContext, + public_key: &tw::PublicKey, + derivation: Derivation, + maybe_prefix: Option, + ) -> AddressResult { + match maybe_prefix { + Some(StandardBitcoinPrefix::Base58(prefix)) => { + return LegacyAddress::p2pkh_with_coin_and_prefix(coin, public_key, Some(prefix)) + .map(StandardBitcoinAddress::Legacy); + }, + Some(StandardBitcoinPrefix::Bech32(prefix)) => { + return SegwitAddress::p2wpkh_with_coin_and_prefix(coin, public_key, Some(prefix)) + .map(StandardBitcoinAddress::Segwit); + }, + // Derive an address as declared in registry.json. + None => (), + } + + match BitcoinDerivation::tw_derivation(coin, derivation) { + BitcoinDerivation::Legacy => { + LegacyAddress::p2pkh_with_coin_and_prefix(coin, public_key, None) + .map(StandardBitcoinAddress::Legacy) + }, + BitcoinDerivation::Segwit => { + SegwitAddress::p2wpkh_with_coin_and_prefix(coin, public_key, None) + .map(StandardBitcoinAddress::Segwit) + }, + } + } +} + +impl FromStr for StandardBitcoinAddress { + type Err = AddressError; + + fn from_str(s: &str) -> Result { + if let Ok(legacy) = LegacyAddress::from_str(s) { + return Ok(StandardBitcoinAddress::Legacy(legacy)); + } + if let Ok(segwit) = SegwitAddress::from_str(s) { + return Ok(StandardBitcoinAddress::Segwit(segwit)); + } + if let Ok(taproot) = TaprootAddress::from_str(s) { + return Ok(StandardBitcoinAddress::Taproot(taproot)); + } + Err(AddressError::InvalidInput) + } +} + +impl fmt::Display for StandardBitcoinAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + StandardBitcoinAddress::Legacy(legacy) => write!(f, "{legacy}"), + StandardBitcoinAddress::Segwit(segwit) => write!(f, "{segwit}"), + StandardBitcoinAddress::Taproot(taproot) => write!(f, "{taproot}"), + } + } +} + +impl CoinAddress for StandardBitcoinAddress { + fn data(&self) -> Data { + match self { + StandardBitcoinAddress::Legacy(legacy) => legacy.bytes().to_vec(), + StandardBitcoinAddress::Segwit(segwit) => segwit.witness_program().to_vec(), + StandardBitcoinAddress::Taproot(taproot) => taproot.witness_program().to_vec(), + } + } +} diff --git a/rust/frameworks/tw_utxo/src/address/taproot.rs b/rust/frameworks/tw_utxo/src/address/taproot.rs new file mode 100644 index 00000000000..d0f18810a20 --- /dev/null +++ b/rust/frameworks/tw_utxo/src/address/taproot.rs @@ -0,0 +1,250 @@ +use super::Bech32Prefix; +use crate::address::witness_program::WitnessProgram; +use bitcoin::key::TapTweak; +use core::fmt; +use std::str::FromStr; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::error::prelude::*; +use tw_hash::{H256, H264}; +use tw_keypair::tw; +use tw_memory::Data; +use tw_misc::traits::ToBytesVec; + +/// cbindgen:ignore +pub const WITNESS_V1: u8 = 1; +/// Witness program sizes valid for V1 (Taproot). +/// cbindgen:ignore +pub const WITNESS_V1_VALID_PROGRAM_SIZES: [usize; 1] = [H256::LEN]; + +#[derive(Debug, Eq, PartialEq)] +pub struct TaprootAddress { + inner: WitnessProgram, +} + +impl TaprootAddress { + pub fn new(hrp: String, witness_program: Data) -> AddressResult { + // Specific Taproot V1 check. These addresses can never spend funds sent to them. + if !WITNESS_V1_VALID_PROGRAM_SIZES.contains(&witness_program.len()) { + return Err(AddressError::InvalidInput); + } + + let inner = + WitnessProgram::new(hrp, WITNESS_V1, witness_program, bech32::Variant::Bech32m)?; + Ok(TaprootAddress { inner }) + } + + /// Create a Taproot address from a public key and an optional merkle root. + /// Taproot transactions come in two variants: + /// + /// * P2TR key-path: which is used for "normal" balance transfers and is + /// internally _tweaked_ with an empty (None) merkle root. + /// * P2TR script-path: which is used for complex scripts, such as + /// Ordinals/BRC20, and is internally _tweaked_ with a merkle root of all + /// possible spending conditions. + pub fn p2tr_with_public_key( + hrp: String, + internal_pubkey: &H264, + merkle_root: Option<&H256>, + ) -> AddressResult { + // We're relying on the `bitcoin` crate to generate anything Taproot related. + + // Convert the native `H256` to `TapNodeHash` from the `bitcoin` crate. + let merkle_root = merkle_root.map(|hash| { + let tap_hash = + as bitcoin::hashes::Hash>::from_slice( + hash.as_slice(), + ) + .expect("merkle_root length is 32 bytes"); + + bitcoin::taproot::TapNodeHash::from_raw_hash(tap_hash) + }); + + // Tweak the public key with the (empty) merkle root. + let pubkey = bitcoin::PublicKey::from_slice(internal_pubkey.as_slice()).unwrap(); + let internal_key = bitcoin::secp256k1::XOnlyPublicKey::from(pubkey.inner); + let (output_key, _parity) = + internal_key.tap_tweak(&bitcoin::secp256k1::Secp256k1::new(), merkle_root); + + Self::new(hrp, output_key.serialize().to_vec()) + } + + /// Create a Taproot address from a public key and an optional merkle root. + /// Taproot transactions come in two variants: + /// + /// * P2TR key-path: which is used for "normal" balance transfers and is + /// internally _tweaked_ with an empty (None) merkle root. + /// * P2TR script-path: which is used for complex scripts, such as + /// Ordinals/BRC20, and is internally _tweaked_ with a merkle root of all + /// possible spending conditions. + pub fn p2tr_with_coin_and_prefix( + coin: &dyn CoinContext, + public_key: &tw::PublicKey, + prefix: Option, + merkle_root: Option<&H256>, + ) -> AddressResult { + let hrp = match prefix { + Some(Bech32Prefix { hrp }) => hrp, + None => coin.hrp().ok_or(AddressError::InvalidRegistry)?, + }; + + let public_key_bytes = public_key + .to_secp256k1() + .ok_or(AddressError::PublicKeyTypeMismatch)? + .compressed(); + + Self::p2tr_with_public_key(hrp, &public_key_bytes, merkle_root) + } + + pub fn from_str_checked(s: &str, expected_hrp: &str) -> AddressResult { + let address = Self::from_str(s)?; + if address.inner.hrp() != expected_hrp { + return Err(AddressError::InvalidHrp); + } + Ok(address) + } + + pub fn from_str_with_coin_and_prefix( + coin: &dyn CoinContext, + s: &str, + prefix: Option, + ) -> AddressResult { + let hrp = match prefix { + Some(Bech32Prefix { hrp }) => hrp, + None => coin.hrp().ok_or(AddressError::InvalidRegistry)?, + }; + TaprootAddress::from_str_checked(s, &hrp) + } + + pub fn witness_program(&self) -> &[u8] { + self.inner.witness_program() + } +} + +impl FromStr for TaprootAddress { + type Err = AddressError; + + fn from_str(s: &str) -> Result { + let inner = WitnessProgram::from_str_checked( + s, + WITNESS_V1, + bech32::Variant::Bech32m, + &WITNESS_V1_VALID_PROGRAM_SIZES, + )?; + Ok(TaprootAddress { inner }) + } +} + +impl fmt::Display for TaprootAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.inner) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tw_coin_entry::test_utils::test_context::TestCoinContext; + use tw_encoding::hex::DecodeHex; + + struct TestInputValid { + str: &'static str, + normalized: &'static str, + expected: TaprootAddress, + } + + #[track_caller] + fn taproot_addr(hrp: &str, program: &str) -> TaprootAddress { + TaprootAddress::new(hrp.to_string(), program.decode_hex().unwrap()) + .expect("Cannot construct a TaprootAddress from the input") + } + + /// Tests if the given `s` string representation is converted from and to `expected` segwit address. + #[track_caller] + fn test_to_from_str_valid(input: TestInputValid) { + let actual = TaprootAddress::from_str(input.str).expect("Expected a valid address"); + assert_eq!(actual, input.expected, "String -> TaprootAddress"); + + let actual_str = actual.to_string(); + assert_eq!(actual_str, input.normalized, "TaprootAddress -> String"); + } + + #[track_caller] + fn test_from_str_invalid(str: &str) { + let _ = TaprootAddress::from_str(str).expect_err("Expected an invalid Taproot address"); + } + + #[test] + fn test_segwit_address_to_from_str() { + test_to_from_str_valid(TestInputValid { + str: "bc1ptmsk7c2yut2xah4pgflpygh2s7fh0cpfkrza9cjj29awapv53mrslgd5cf", + normalized: "bc1ptmsk7c2yut2xah4pgflpygh2s7fh0cpfkrza9cjj29awapv53mrslgd5cf", + expected: taproot_addr( + "bc", + "5ee16f6144e2d46edea1427e1222ea879377e029b0c5d2e252517aee85948ec7", + ), + }); + + test_to_from_str_valid(TestInputValid { + str: "tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c", + normalized: "tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c", + expected: taproot_addr( + "tb", + "000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433", + ), + }); + + test_to_from_str_valid(TestInputValid { + str: "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0", + normalized: "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0", + expected: taproot_addr( + "bc", + "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + ), + }); + } + + #[test] + fn test_taproot_address_from_str_invalid() { + // version 0 + test_from_str_invalid("tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy"); + + // version 2 + test_from_str_invalid("bc1zw508d6qejxtdg4y5r3zarvaryvaxxpcs"); + + // version 2 + test_from_str_invalid("bc1zw508d6qejxtdg4y5r3zarvaryvaxxpcs"); + + // version 16 + test_from_str_invalid("BC1SW50QGDZ25J"); + + // program size 40 + test_from_str_invalid( + "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y", + ); + } + + #[test] + fn test_taproot_address_create_with_coin_and_prefix() { + let coin = TestCoinContext::default(); + let hrp = "bc".to_string(); + let merkle_root = None; + + let public_key_bytes = "03cdf7e208a0146c3a35c181944a96a15b2a58256be69adad640a9a97d408b9b44" + .decode_hex() + .unwrap(); + let public_key = + tw::PublicKey::new(public_key_bytes.clone(), tw::PublicKeyType::Secp256k1).unwrap(); + + let addr = TaprootAddress::p2tr_with_coin_and_prefix( + &coin, + &public_key, + Some(Bech32Prefix { hrp }), + merkle_root, + ) + .unwrap(); + assert_eq!( + addr.to_string(), + "bc1purekytqrqzfzdulufmll8a335jhvw9x4glhzp8fv76yxlsxeyptsfylq9h" + ); + } +} diff --git a/rust/frameworks/tw_utxo/src/address/witness_program.rs b/rust/frameworks/tw_utxo/src/address/witness_program.rs new file mode 100644 index 00000000000..0edb5ea65be --- /dev/null +++ b/rust/frameworks/tw_utxo/src/address/witness_program.rs @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use bech32::FromBase32; +use std::fmt; +use std::ops::RangeInclusive; +use tw_coin_entry::error::prelude::*; +use tw_memory::Data; + +/// cbindgen:ignore +pub const WITNESS_V0: u8 = 0; +/// cbindgen:ignore +pub const MAX_WITNESS_VERSION: u8 = 16; +/// cbindgen:ignore +pub const WITNESS_VERSIONS: RangeInclusive = WITNESS_V0..=MAX_WITNESS_VERSION; +/// Witness program sizes valid for most of the witness versions. +/// Please note that V0 is more constraint. +/// cbindgen:ignore +pub const WITNESS_VALID_PROGRAM_SIZES: RangeInclusive = 2..=40; + +/// A segwit address implementation that supports various program versions. +/// For example: +/// * witness V0 is Segwit address +/// * witness V1 is Taproot address +#[derive(Debug, Eq, PartialEq)] +pub struct WitnessProgram { + hrp: String, + witness_version: u8, + witness_program: Data, + /// An address string created from this `hrp`, `witness_version` and `witness_program`. + address_str: String, + bech32_variant: bech32::Variant, +} + +impl WitnessProgram { + pub fn new( + hrp: String, + witness_version: u8, + witness_program: Data, + bech32_variant: bech32::Variant, + ) -> AddressResult { + if !WITNESS_VERSIONS.contains(&witness_version) { + return Err(AddressError::Unsupported); + } + + if !WITNESS_VALID_PROGRAM_SIZES.contains(&witness_program.len()) { + return Err(AddressError::InvalidInput); + } + + let address_str = + Self::fmt_internal(&hrp, witness_version, &witness_program, bech32_variant)?; + Ok(WitnessProgram { + hrp, + witness_version, + witness_program, + address_str, + bech32_variant, + }) + } + + pub fn witness_version(&self) -> u8 { + self.witness_version + } + + pub fn witness_program(&self) -> &[u8] { + &self.witness_program + } + + pub fn hrp(&self) -> &str { + &self.hrp + } + + pub fn from_str_checked( + s: &str, + expected_version: u8, + expected_checksum_type: bech32::Variant, + valid_program_sizes: &[usize], + ) -> AddressResult { + let (hrp, payload_u5, checksum_variant) = + bech32::decode(s).map_err(|_| AddressError::FromBech32Error)?; + + if payload_u5.is_empty() { + return Err(AddressError::InvalidInput); + } + + // Get the script version and program (converted from 5-bit to 8-bit) + let (version, program) = payload_u5.split_at(1); + let version = version[0].to_u8(); + let program = Data::from_base32(program).map_err(|_| AddressError::FromBech32Error)?; + + // Check witness version. + if version != expected_version { + return Err(AddressError::Unsupported); + } + + // Check encoding. + if checksum_variant != expected_checksum_type { + return Err(AddressError::InvalidInput); + } + + // Check witness program sizes. + if !valid_program_sizes.contains(&program.len()) { + return Err(AddressError::InvalidInput); + } + + WitnessProgram::new(hrp, version, program, checksum_variant) + } + + fn fmt_internal( + hrp: &str, + witness_version: u8, + witness_program: &[u8], + bech32_variant: bech32::Variant, + ) -> AddressResult { + const STRING_CAPACITY: usize = 100; + + let mut result_addr = String::with_capacity(STRING_CAPACITY); + + let version_u5 = + bech32::u5::try_from_u8(witness_version).expect("WitnessVersion must be 0..=16"); + + { + let mut bech32_writer = + bech32::Bech32Writer::new(hrp, bech32_variant, &mut result_addr) + .map_err(|_| AddressError::FromBech32Error)?; + bech32::WriteBase32::write_u5(&mut bech32_writer, version_u5) + .map_err(|_| AddressError::FromBech32Error)?; + bech32::ToBase32::write_base32(&witness_program, &mut bech32_writer) + .map_err(|_| AddressError::FromBech32Error)?; + } + + Ok(result_addr) + } +} + +impl fmt::Display for WitnessProgram { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.address_str) + } +} diff --git a/rust/frameworks/tw_utxo/src/constants.rs b/rust/frameworks/tw_utxo/src/constants.rs new file mode 100644 index 00000000000..0a99f519fac --- /dev/null +++ b/rust/frameworks/tw_utxo/src/constants.rs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +/// A standard transaction is limited to 400k weight units (WU). +/// https://bitcoin.stackexchange.com/questions/35570/what-is-the-maximum-number-of-inputs-outputs-a-transaction-can-have +pub const MAX_TRANSACTION_WEIGHT: usize = 400_000; diff --git a/rust/frameworks/tw_utxo/src/dust/dust_filter.rs b/rust/frameworks/tw_utxo/src/dust/dust_filter.rs new file mode 100644 index 00000000000..0d68ee106dc --- /dev/null +++ b/rust/frameworks/tw_utxo/src/dust/dust_filter.rs @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::dust::DustPolicy; +use crate::script::standard_script::conditions; +use crate::transaction::transaction_interface::{TransactionInterface, TxOutputInterface}; +use crate::transaction::unsigned_transaction::UnsignedTransaction; +use std::marker::PhantomData; +use tw_coin_entry::error::prelude::*; + +pub struct DustFilter { + dust_policy: DustPolicy, + _phantom: PhantomData, +} + +impl DustFilter { + pub fn new(dust_policy: DustPolicy) -> Self { + DustFilter { + dust_policy, + _phantom: PhantomData, + } + } + + /// Filter dust UTXOs out. + /// Returns an error if there are no valid UTXOs. + pub fn filter_inputs( + &self, + mut transaction: UnsignedTransaction, + ) -> SigningResult> { + let dust_threshold = self.dust_policy.dust_threshold(); + + transaction.retain_inputs(|_utxo, utxo_args| utxo_args.amount >= dust_threshold)?; + + Ok(transaction) + } + + /// Checks if all transaction output amounts are greater or equal to a dust threshold. + pub fn check_outputs( + &self, + transaction: &UnsignedTransaction, + ) -> SigningResult<()> { + let dust_threshold = self.dust_policy.dust_threshold(); + + let has_dust_output = transaction.transaction().outputs().iter().any(|output| { + if conditions::is_op_return(output.script_pubkey()) { + // Ignore the OP_RETURN output value. It can (or even should) be 0. + return false; + } + output.value() < dust_threshold + }); + + if has_dust_output { + return SigningError::err(SigningErrorType::Error_dust_amount_requested); + } + Ok(()) + } +} diff --git a/rust/frameworks/tw_utxo/src/dust/mod.rs b/rust/frameworks/tw_utxo/src/dust/mod.rs new file mode 100644 index 00000000000..5e605ae241f --- /dev/null +++ b/rust/frameworks/tw_utxo/src/dust/mod.rs @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::transaction::transaction_parts::Amount; + +pub mod dust_filter; + +/// Transaction dust amount calculator. +/// Later, we plan to add support for `DynamicDust` policy with a `min_relay_fee` amount. +#[derive(Clone, Copy)] +pub enum DustPolicy { + FixedAmount(Amount), +} + +impl DustPolicy { + pub fn dust_threshold(&self) -> Amount { + match self { + DustPolicy::FixedAmount(amount) => *amount, + } + } +} diff --git a/rust/frameworks/tw_utxo/src/encode/compact_integer.rs b/rust/frameworks/tw_utxo/src/encode/compact_integer.rs new file mode 100644 index 00000000000..4a95a378699 --- /dev/null +++ b/rust/frameworks/tw_utxo/src/encode/compact_integer.rs @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::encode::stream::Stream; +use crate::encode::Encodable; +use std::ops::RangeInclusive; + +const ONE_BYTE_RANGE: RangeInclusive = 0..=0xFC; +const TWO_BYTES_RANGE: RangeInclusive = 0xFD..=0xFFFF; +const FOUR_BYTES_RANGE: RangeInclusive = 0x10000..=0xFFFF_FFFF; + +const TWO_BYTES_FLAG: u8 = 0xFD_u8; +const FOUR_BYTES_FLAG: u8 = 0xFE_u8; +const EIGHT_BYTES_FLAG: u8 = 0xFF_u8; + +/// A type of variable-length integer commonly used in the Bitcoin P2P protocol and Bitcoin serialized data structures. +#[derive(Default, Debug, Clone, Copy, PartialEq)] +pub struct CompactInteger(u64); + +impl From for CompactInteger { + fn from(value: usize) -> Self { + CompactInteger(value as u64) + } +} + +impl Encodable for CompactInteger { + fn encode(&self, stream: &mut Stream) { + let v = self.0; + + if ONE_BYTE_RANGE.contains(&v) { + stream.append(&(v as u8)); + } else if TWO_BYTES_RANGE.contains(&v) { + stream.append(&TWO_BYTES_FLAG).append(&(v as u16)); + } else if FOUR_BYTES_RANGE.contains(&v) { + stream.append(&FOUR_BYTES_FLAG).append(&(v as u32)); + } else { + stream.append(&EIGHT_BYTES_FLAG).append(&v); + } + } + + fn encoded_size(&self) -> usize { + const BYTE_FLAG: usize = 1; + + let v = self.0; + if ONE_BYTE_RANGE.contains(&v) { + BYTE_FLAG + } else if TWO_BYTES_RANGE.contains(&v) { + BYTE_FLAG + 2 + } else if FOUR_BYTES_RANGE.contains(&v) { + BYTE_FLAG + 4 + } else { + BYTE_FLAG + 8 + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_compact_integer_stream() { + let mut stream = Stream::default(); + + stream + .append(&CompactInteger::from(0_usize)) + .append(&CompactInteger::from(0xfc_usize)) + .append(&CompactInteger::from(0xfd_usize)) + .append(&CompactInteger::from(0xffff_usize)) + .append(&CompactInteger::from(0x10000_usize)) + .append(&CompactInteger::from(0xffff_ffff_usize)) + .append(&CompactInteger(0x1_0000_0000_u64)); + + let expected = vec![ + 0_u8, 0xfc, 0xfd, 0xfd, 0x00, 0xfd, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x01, 0x00, 0xfe, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + ]; + + assert_eq!(stream.out(), expected); + } +} diff --git a/rust/frameworks/tw_utxo/src/encode/impls.rs b/rust/frameworks/tw_utxo/src/encode/impls.rs new file mode 100644 index 00000000000..b21b1567cc1 --- /dev/null +++ b/rust/frameworks/tw_utxo/src/encode/impls.rs @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::encode::compact_integer::CompactInteger; +use crate::encode::stream::Stream; +use crate::encode::Encodable; +use byteorder::{LittleEndian, WriteBytesExt}; +use tw_hash::Hash; +use tw_memory::Data; + +impl Encodable for Data { + fn encode(&self, stream: &mut Stream) { + stream + .append(&CompactInteger::from(self.len())) + .append_raw_slice(self.as_slice()); + } + + fn encoded_size(&self) -> usize { + CompactInteger::from(self.len()).encoded_size() + self.len() + } +} + +impl Encodable for Hash { + #[inline] + fn encode(&self, stream: &mut Stream) { + stream.append_raw_slice(self.as_slice()); + } + + #[inline] + fn encoded_size(&self) -> usize { + N + } +} + +impl Encodable for u8 { + #[inline] + fn encode(&self, s: &mut Stream) { + s.write_u8(*self).unwrap(); + } + + #[inline] + fn encoded_size(&self) -> usize { + 1 + } +} + +macro_rules! impl_encodable_for_int { + ($int:ty, $size:literal, $write_fn:tt) => { + impl Encodable for $int { + #[inline] + fn encode(&self, s: &mut Stream) { + s.$write_fn::(*self).unwrap(); + } + + #[inline] + fn encoded_size(&self) -> usize { + $size + } + } + }; +} + +impl_encodable_for_int!(i32, 4, write_i32); +impl_encodable_for_int!(i64, 8, write_i64); +impl_encodable_for_int!(u16, 2, write_u16); +impl_encodable_for_int!(u32, 4, write_u32); +impl_encodable_for_int!(u64, 8, write_u64); + +#[cfg(test)] +mod tests { + use super::*; + use crate::encode::encode; + use tw_encoding::hex::{DecodeHex, ToHex}; + + #[test] + fn test_stream_append() { + let mut stream = Stream::default(); + + stream + .append(&1u8) + .append(&2u16) + .append(&3u32) + .append(&4u64); + + let expected = vec![1_u8, 2, 0, 3, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0]; + assert_eq!(stream.out(), expected); + } + + #[test] + fn test_bytes_serialize() { + let expected = "020145".decode_hex().unwrap(); + let bytes = "0145".decode_hex().unwrap(); + assert_eq!(expected, encode(&bytes)); + } + + #[test] + fn test_steam_append_slice() { + let mut slice = [0u8; 4]; + slice[0] = 0x64; + let mut stream = Stream::default(); + stream.append_raw_slice(&slice); + assert_eq!(stream.out().to_hex(), "64000000"); + } +} diff --git a/rust/frameworks/tw_utxo/src/encode/mod.rs b/rust/frameworks/tw_utxo/src/encode/mod.rs new file mode 100644 index 00000000000..14eaad21574 --- /dev/null +++ b/rust/frameworks/tw_utxo/src/encode/mod.rs @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::encode::stream::Stream; +use tw_memory::Data; + +pub mod compact_integer; +pub mod impls; +pub mod stream; + +pub fn encode(t: &T) -> Data +where + T: Encodable, +{ + let mut stream = Stream::default(); + stream.append(t); + stream.out() +} + +pub trait Encodable { + /// Serialize the struct and appends it to the end of stream. + fn encode(&self, stream: &mut Stream); + + /// Hint about the size of serialized struct. + fn encoded_size(&self) -> usize; +} diff --git a/rust/frameworks/tw_utxo/src/encode/stream.rs b/rust/frameworks/tw_utxo/src/encode/stream.rs new file mode 100644 index 00000000000..566debfd62b --- /dev/null +++ b/rust/frameworks/tw_utxo/src/encode/stream.rs @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::encode::compact_integer::CompactInteger; +use crate::encode::Encodable; +use std::io; +use std::io::Write; +use tw_memory::Data; + +/// Stream used for serialization of Bitcoin structures +#[derive(Default)] +pub struct Stream { + buffer: Data, +} + +impl Stream { + /// New stream + pub fn new() -> Self { + Stream { + buffer: Data::default(), + } + } + + /// Serializes the struct and appends it to the end of stream. + pub fn append(&mut self, t: &T) -> &mut Self + where + T: Encodable, + { + t.encode(self); + self + } + + /// Appends raw bytes to the end of the stream. + pub fn append_raw_slice(&mut self, bytes: &[u8]) -> &mut Self { + // discard error for now, since we write to simple vector + self.buffer.write_all(bytes).unwrap(); + self + } + + /// Appends a list of serializable structs to the end of the stream. + pub fn append_list(&mut self, t: &[T]) -> &mut Self { + CompactInteger::from(t.len()).encode(self); + for i in t { + i.encode(self); + } + self + } + + /// Full stream. + pub fn out(self) -> Data { + self.buffer + } +} + +impl Write for Stream { + #[inline] + fn write(&mut self, buf: &[u8]) -> Result { + self.buffer.write(buf) + } + + #[inline] + fn flush(&mut self) -> Result<(), io::Error> { + self.buffer.flush() + } +} diff --git a/rust/frameworks/tw_utxo/src/lib.rs b/rust/frameworks/tw_utxo/src/lib.rs new file mode 100644 index 00000000000..84748580ded --- /dev/null +++ b/rust/frameworks/tw_utxo/src/lib.rs @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod address; +pub mod constants; +pub mod dust; +pub mod encode; +pub mod modules; +pub mod script; +pub mod sighash; +pub mod signature; +pub mod signing_mode; +pub mod spending_data; +pub mod transaction; diff --git a/rust/frameworks/tw_utxo/src/modules/fee_estimator.rs b/rust/frameworks/tw_utxo/src/modules/fee_estimator.rs new file mode 100644 index 00000000000..f6fc0242ffb --- /dev/null +++ b/rust/frameworks/tw_utxo/src/modules/fee_estimator.rs @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::transaction::transaction_interface::TransactionInterface; +use crate::transaction::transaction_parts::Amount; +use std::marker::PhantomData; +use tw_coin_entry::error::prelude::*; + +pub struct FeeEstimator { + _phantom: PhantomData, +} + +impl FeeEstimator { + pub fn estimate_fee(tx: &Transaction, fee_rate: Amount) -> SigningResult { + let vsize = tx.vsize(); + Amount::try_from(vsize) + .ok() + .and_then(|vsize| vsize.checked_mul(fee_rate)) + .or_tw_err(SigningErrorType::Error_wrong_fee) + .with_context(|| format!("feePerVByte is too large: '{vsize} * {fee_rate}' overflow")) + } +} diff --git a/rust/frameworks/tw_utxo/src/modules/keys_manager.rs b/rust/frameworks/tw_utxo/src/modules/keys_manager.rs new file mode 100644 index 00000000000..cb272d37529 --- /dev/null +++ b/rust/frameworks/tw_utxo/src/modules/keys_manager.rs @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::modules::sighash_computer::TaprootTweak; +use std::collections::HashMap; +use tw_coin_entry::error::prelude::*; +use tw_hash::H264; +use tw_keypair::{ecdsa, schnorr}; + +/// Standard Bitcoin keys manager. +/// Supports ecdsa and schnorr private keys. +#[derive(Default)] +pub struct KeysManager { + /// Ecdsa public to private keys. + ecdsa_public_private_map: HashMap, + /// Schnorr private keys. + schnorr_private_keys: Vec, +} + +impl KeysManager { + pub fn add_ecdsa_private(&mut self, private: ecdsa::secp256k1::PrivateKey) -> &mut Self { + self.ecdsa_public_private_map + .insert(private.public().compressed(), private); + self + } + + pub fn add_schnorr_private(&mut self, private: schnorr::PrivateKey) -> &mut Self { + self.schnorr_private_keys.push(private); + self + } + + pub fn get_ecdsa_private( + &self, + public: &ecdsa::secp256k1::PublicKey, + ) -> SigningResult<&ecdsa::secp256k1::PrivateKey> { + let pubkey_bytes = public.compressed(); + + self.ecdsa_public_private_map + .get(&pubkey_bytes) + .or_tw_err(SigningErrorType::Error_missing_private_key) + .with_context(|| format!("Cannot find a private key corresponding to the ecdsa public key: {pubkey_bytes}")) + } + + /// Gets a schnorr private key by an either tweaked or untweaked x-only public key. + /// The function iterates over the private keys, tweaks them if specified in `taproot_tweak`, + /// and returns `Ok(schnorr::PrivateKey)` if found. + pub fn get_schnorr_private( + &self, + public: &schnorr::XOnlyPublicKey, + taproot_tweak: &Option, + ) -> SigningResult { + let pubkey_bytes = public.bytes(); + + for private_key in self.schnorr_private_keys.iter() { + match taproot_tweak { + Some(ref tweak) => { + let tweaked_private = private_key.clone().tweak(tweak.merkle_root); + if tweaked_private.public().x_only().bytes() == pubkey_bytes { + return Ok(tweaked_private); + } + // Otherwise, continue searching for a private key. + }, + None => { + if private_key.public().x_only().bytes() == pubkey_bytes { + return Ok(private_key.clone()); + } + // Otherwise, continue searching for a private key. + }, + } + } + + SigningError::err(SigningErrorType::Error_missing_private_key) + .context(format!("Cannot find a private key corresponding to the x-only schnorr public key: {pubkey_bytes}")) + } +} diff --git a/rust/frameworks/tw_utxo/src/modules/mod.rs b/rust/frameworks/tw_utxo/src/modules/mod.rs new file mode 100644 index 00000000000..9d59f9124d1 --- /dev/null +++ b/rust/frameworks/tw_utxo/src/modules/mod.rs @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod fee_estimator; +pub mod keys_manager; +pub mod sighash_computer; +pub mod sighash_verifier; +pub mod tx_compiler; +pub mod tx_planner; +pub mod tx_signer; +pub mod utxo_selector; diff --git a/rust/frameworks/tw_utxo/src/modules/sighash_computer.rs b/rust/frameworks/tw_utxo/src/modules/sighash_computer.rs new file mode 100644 index 00000000000..0bf50529664 --- /dev/null +++ b/rust/frameworks/tw_utxo/src/modules/sighash_computer.rs @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::script::Script; +use crate::signing_mode::SigningMethod; +use crate::transaction::transaction_interface::TransactionInterface; +use crate::transaction::transaction_parts::Amount; +use crate::transaction::unsigned_transaction::UnsignedTransaction; +use crate::transaction::{ + TransactionPreimage, UtxoPreimageArgs, UtxoTaprootPreimageArgs, UtxoToSign, +}; +use std::marker::PhantomData; +use tw_coin_entry::coin_entry::PublicKeyBytes; +use tw_coin_entry::error::prelude::SigningResult; +use tw_hash::H256; + +#[derive(Debug, Clone)] +pub struct TxPreimage { + /// Transaction signatures in the same order as the transaction UTXOs. + pub sighashes: Vec, +} + +#[derive(Debug, Clone)] +pub struct UtxoSighash { + /// The signing method needs to be used for this sighash. + pub signing_method: SigningMethod, + pub sighash: H256, + pub signer_pubkey: PublicKeyBytes, + /// Taproot tweak if [`SigningMethod::Taproot`] signing method is used. + /// Empty if there is no need to tweak the private to sign the sighash. + pub taproot_tweak: Option, +} + +#[derive(Debug, Clone)] +pub struct TaprootTweak { + /// 32 bytes merkle root of the script tree. + /// `None` if there are no scripts, and the private key should be tweaked without a merkle root. + pub merkle_root: Option, +} + +/// Sighash Computer with a standard Bitcoin behaviour. +/// +/// # Important +/// +/// If needed to implement a custom logic, consider adding a different Sighash Computer. +pub struct SighashComputer { + _phantom: PhantomData, +} + +impl SighashComputer +where + Transaction: TransactionPreimage + TransactionInterface, +{ + /// Computes sighashes of [`SighashComputer::transaction`]. + pub fn preimage_tx( + unsigned_tx: &UnsignedTransaction, + ) -> SigningResult { + unsigned_tx + .input_args() + .iter() + .enumerate() + .map(|(input_index, utxo)| { + let signing_method = utxo.signing_method; + + let utxo_args = UtxoPreimageArgs { + input_index, + script_pubkey: utxo.script_pubkey.clone(), + amount: utxo.amount, + // TODO move `leaf_hash_code_separator` to `UtxoTaprootPreimageArgs`. + leaf_hash_code_separator: utxo.leaf_hash_code_separator, + sighash_ty: utxo.sighash_ty, + tx_hasher: utxo.tx_hasher, + signing_method, + }; + + let (sighash, taproot_tweak) = match signing_method { + SigningMethod::Legacy | SigningMethod::Segwit => { + let sighash = unsigned_tx.transaction().preimage_tx(&utxo_args)?; + (sighash, None) + }, + SigningMethod::Taproot => { + let tr_spent_amounts: Vec = unsigned_tx + .input_args() + .iter() + .map(|utxo| utxo.amount) + .collect(); + + let tr_spent_script_pubkeys: Vec