From 99a4b36a0e81a5b4931d8849ff9d88280e07c422 Mon Sep 17 00:00:00 2001 From: Alex Loiko Date: Tue, 15 Aug 2023 15:22:50 +0300 Subject: [PATCH 1/2] ALL-2392 Added 'replaceable' body parameter for RBF for BTC and DOGE. LTC is not working for some reason. --- .../btc-example/src/app/btc.tx.rbf.example.ts | 64 ++++++++++++++ examples/btc-example/src/index.ts | 4 + .../src/app/doge.tx.rbf.example.ts | 62 ++++++++++++++ examples/doge-example/src/index.ts | 4 + package.json | 2 +- .../blockchain/doge/src/lib/doge.sdk.tx.ts | 53 +++++++++++- .../shared/blockchain/btc-based/src/index.ts | 1 + .../btc-based/src/lib/nested/btc-based.tx.ts | 84 ++++++++++++++++++- .../src/lib/nested/btc-based.types.ts | 83 +++++++++--------- yarn.lock | 6 +- 10 files changed, 312 insertions(+), 51 deletions(-) create mode 100644 examples/btc-example/src/app/btc.tx.rbf.example.ts create mode 100644 examples/doge-example/src/app/doge.tx.rbf.example.ts diff --git a/examples/btc-example/src/app/btc.tx.rbf.example.ts b/examples/btc-example/src/app/btc.tx.rbf.example.ts new file mode 100644 index 0000000000..972340e353 --- /dev/null +++ b/examples/btc-example/src/app/btc.tx.rbf.example.ts @@ -0,0 +1,64 @@ +import { TatumBtcSDK } from '@tatumio/btc' + +const btcSDK = TatumBtcSDK({ apiKey: '75ea3138-d0a1-47df-932e-acb3ee807dab' }) + +export async function btcTransactionRBFExample() { + // Example shows how to prepare replaceable transaction (RBF). It is possible to replace such transaction in mempool with higher fee. + // + // To transfer BTC, please get familiar with UTXO model. + // Prepare unspent output information first. + // It is unspent transaction information for address, that will be used as an input for next BTC tx + // It is possible to have more than one transaction Ids + // As an example, after running wallet example, use this url (https://testnet-faucet.com/btc-testnet/) to faucet the address generated in the example + // The faucet transaction will take some time to be confirmed, you can validate that in https://blockexplorer.one/ + // After it is confirmed, replace the bellow values + const REPLACE_ME_WITH_PRIVATE_KEY = '' + + // Prepare unspent output information first. + // It is unspent transaction information for address, that will be used as an input for next BTC tx + // It is possible to have more than one + const txHash = '1a91340d3ea25d55a4395948d8ace5f2fcc6e1871a494cdb0e0d576e65fe9fc4' + const address = 'tb1qldrj9c68py7eyn6a3m722vn8fpk3lueza2zxff' + const index = 0 + const valueUtxo = 0.00015 + + // Private key for utxo address + const privateKey = REPLACE_ME_WITH_PRIVATE_KEY + + // Set recipient values, amount and address where to send. Because of internal structure of BTC chain it is possible + // to pass several input and output address-value pairs. We will work with one recipient + const valueToSend = 0.00015 + const recipientAddress = 'tb1qzkfcm9sapgxptjd5l7w9s88v8q3994srs8vv7z' + + const fee = '0.00001' + const changeAddress = address // we expect to receive change from transaction to sender address back + + // Transaction - prepare tx to be sent to blockchain + // This method will prepare replaceable (RBF) transaction immediately + const txData = await btcSDK.transaction.prepareSignedReplaceableTransaction( + { + fromUTXO: [ + { + txHash: txHash, + index: index, + privateKey: privateKey, + address: address, + value: valueUtxo, + }, + ], + to: [ + { + address: recipientAddress, + value: valueToSend, + }, + ], + fee: fee, + changeAddress: changeAddress, + }, + { testnet: true }, + ) + console.log(`Tx data: ${txData}`) + + const transactionHash = await btcSDK.blockchain.broadcast({ txData: txData }) + console.log(`Tx hash: ${transactionHash}`) +} diff --git a/examples/btc-example/src/index.ts b/examples/btc-example/src/index.ts index 9f01d37c9f..e6df15cc18 100644 --- a/examples/btc-example/src/index.ts +++ b/examples/btc-example/src/index.ts @@ -8,6 +8,7 @@ import { btcEstimateExample } from './app/btc.estimate.example' import { btcSubscriptionsExample } from './app/btc.subscriptions.example' import { btcVirtualAccountExample } from './app/btc.virtualAccount.example' import { btcBroadcastTransactionsExample } from './app/btc.tx.broadcast.example' +import { btcTransactionRBFExample } from './app/btc.tx.rbf.example' const examples = async () => { console.log(`Running btcBalanceExample`) @@ -37,6 +38,9 @@ const examples = async () => { console.log(`Running btcFromUtxoTransactionsExample`) await btcFromUtxoTransactionsExample() + console.log(`Running btcTransactionRBFExample`) + await btcTransactionRBFExample() + console.log(`Running btcVirtualAccountExample`) await btcVirtualAccountExample() } diff --git a/examples/doge-example/src/app/doge.tx.rbf.example.ts b/examples/doge-example/src/app/doge.tx.rbf.example.ts new file mode 100644 index 0000000000..62ed2f6758 --- /dev/null +++ b/examples/doge-example/src/app/doge.tx.rbf.example.ts @@ -0,0 +1,62 @@ +import { DogeTransactionAddress, DogeTransactionUTXO } from '@tatumio/api-client' +import { TatumDogeSDK } from '@tatumio/doge' + +const dogeSDK = TatumDogeSDK({ apiKey: '75ea3138-d0a1-47df-932e-acb3ee807dab' }) + +export async function dogeTransactionRBFExample() { + // Example shows how to prepare replaceable transaction (RBF). It is possible to replace such transaction in mempool with higher fee. + // + // To transfer DOGE, please get familiar with UTXO model. + // Prepare unspent output information first. + // It is unspent transaction information for address, that will be used as an input for next DOGE tx + // It is possible to have more than one transaction Ids + // As an example, after running wallet example, use this url (https://testnet-faucet.com/doge-testnet/) to faucet the address generated in the example + // The faucet transaction will take some time to be confirmed, you can validate that in https://blockexplorer.one/ + // After to be confirm, replace the bellow values + const txHash = 'fcdc23f5c8bd811195921cd113f5724f3cf8b3fa0287a04366c51b9e8545c4c7' + const address = 'n36h3pAH7sC3z8KMB47BjbqvW2aJd2oTi7' + const value = 60 + const index = 1 + + // Private key for utxo address + const privateKey = 'QTEcWfGqd2RbCRuAvoXAz99D8RwENfy8j6X92vPnUKR7yL1kXouk' + + // Set recipient values, amount and address where to send. Because of internal structure of DOGE chain it is possible + // to pass several input and output address-value pairs. We will work with one recipient + const valueToSend = 0.00015 + const recipientAddress = 'tb1q9x2gqftyxterwt0k6ehzrm2gkzthjly677ucyr' + + const fee = '0.00001' + const changeAddress = address // we expect to receive change from transaction to sender address back + + const options = { testnet: true } + + // Transaction - send to blockchain + // This method will prepare replaceable (RBF) transaction immediately + const txData = await dogeSDK.transaction.prepareSignedReplaceableTransaction( + { + fromUTXO: [ + { + txHash: txHash, + index: index, + privateKey: privateKey, + address: address, + value: value, + }, + ], + to: [ + { + address: recipientAddress, + value: valueToSend, + }, + ], + fee: fee, + changeAddress: changeAddress, + }, + options, + ) + console.log(`Tx data: ${txData}`) + + const transactionHash = await dogeSDK.blockchain.broadcast({ txData: txData }) + console.log(`Tx hash: ${transactionHash}`) +} diff --git a/examples/doge-example/src/index.ts b/examples/doge-example/src/index.ts index 36b8400ac0..09534d4c07 100644 --- a/examples/doge-example/src/index.ts +++ b/examples/doge-example/src/index.ts @@ -4,6 +4,7 @@ import { dogeVirtualAccountExample } from './app/doge.virtualAccount.example' import { dogeTransactionExample } from './app/doge.tx.example' import { dogeSubscriptionsExample } from './app/doge.subscriptions.example' import { dogeTransactionBroadcastExample } from './app/doge.tx.broadcast.example' +import { dogeTransactionRBFExample } from './app/doge.tx.rbf.example' const examples = async () => { console.log(`Running dogeWalletExample`) @@ -18,6 +19,9 @@ const examples = async () => { console.log(`Running dogeTransactionExample`) await dogeTransactionExample() + console.log(`Running dogeTransactionRBFExample`) + await dogeTransactionRBFExample() + console.log(`Running dogeTransactionBroadcastExample`) await dogeTransactionBroadcastExample() diff --git a/package.json b/package.json index bc8fca830e..35f4b9b226 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tatumio", - "version": "2.2.23", + "version": "2.2.24", "license": "MIT", "repository": "https://github.com/tatumio/tatum-js", "scripts": { diff --git a/packages/blockchain/doge/src/lib/doge.sdk.tx.ts b/packages/blockchain/doge/src/lib/doge.sdk.tx.ts index 1ab76a3e8e..0993d99cd5 100644 --- a/packages/blockchain/doge/src/lib/doge.sdk.tx.ts +++ b/packages/blockchain/doge/src/lib/doge.sdk.tx.ts @@ -9,7 +9,7 @@ import { DogeTransactionUTXOKMS, TransactionHash, } from '@tatumio/api-client' -import { BtcBasedTx } from '@tatumio/shared-blockchain-btc-based' +import { BtcBasedFromUtxoReplaceableTypes, BtcBasedTx } from '@tatumio/shared-blockchain-btc-based' import { amountUtils, SdkErrorCode } from '@tatumio/shared-abstract-sdk' import { DogeSdkError } from './doge.sdk.errors' import _ from 'lodash' @@ -121,6 +121,56 @@ export const dogeTransactions = ( } } + const prepareSignedReplaceableTransaction = async ( + body: BtcBasedFromUtxoReplaceableTypes, + ): Promise => { + try { + const { to, fee, changeAddress } = body + const transaction = new Transaction() + + const hasFeeAndChange = !_.isNil(changeAddress) && !_.isNil(fee) + + for (const item of to) { + const amount = amountUtils.toSatoshis(item.value) + transaction.to(item.address, amount) + } + + const privateKeysToSign = [] + for (const item of body.fromUTXO) { + const satoshis = amountUtils.toSatoshis(item.value) + transaction.from([ + Transaction.UnspentOutput.fromObject({ + txId: item.txHash, + outputIndex: item.index, + script: Script.fromAddress(item.address).toString(), + satoshis, + }), + ]) + if ('signatureId' in item) privateKeysToSign.push(item.signatureId) + else if ('privateKey' in item) privateKeysToSign.push(item.privateKey) + } + + if (hasFeeAndChange) { + transaction.change(changeAddress) + transaction.fee(amountUtils.toSatoshis(fee)) + } + + transaction.enableRBF() + + if ('fromUTXO' in body && 'signatureId' in body.fromUTXO[0] && body.fromUTXO[0].signatureId) { + return JSON.stringify(transaction) + } + + for (const pk of privateKeysToSign) { + transaction.sign(PrivateKey.fromWIF(pk)) + } + + return transaction.serialize() + } catch (e: any) { + throw new DogeSdkError(e) + } + } + const validateBody = (body: DogeTransactionTypes) => { if (!('fromUTXO' in body) && !('fromAddress' in body)) { throw new DogeSdkError(SdkErrorCode.BTC_BASED_WRONG_BODY) @@ -166,5 +216,6 @@ export const dogeTransactions = ( return { sendTransaction, prepareSignedTransaction, + prepareSignedReplaceableTransaction, } } diff --git a/packages/shared/blockchain/btc-based/src/index.ts b/packages/shared/blockchain/btc-based/src/index.ts index 5e0f621405..eb6562a37c 100644 --- a/packages/shared/blockchain/btc-based/src/index.ts +++ b/packages/shared/blockchain/btc-based/src/index.ts @@ -1,4 +1,5 @@ export * from './lib/btc-based.sdk' export * from './lib/nested/btc-based.wallet' export * from './lib/nested/btc-based.tx' +export * from './lib/nested/btc-based.types' export * from './lib/btc-based.wallet.utils' diff --git a/packages/shared/blockchain/btc-based/src/lib/nested/btc-based.tx.ts b/packages/shared/blockchain/btc-based/src/lib/nested/btc-based.tx.ts index f2f0e71d0c..3cc3fdb8eb 100644 --- a/packages/shared/blockchain/btc-based/src/lib/nested/btc-based.tx.ts +++ b/packages/shared/blockchain/btc-based/src/lib/nested/btc-based.tx.ts @@ -23,6 +23,7 @@ import { amountUtils, SdkError, SdkErrorCode } from '@tatumio/shared-abstract-sd import { BtcBasedSdkError } from '../btc-based.sdk.errors' import BigNumber from 'bignumber.js' import { BtcBasedWalletUtils } from '../btc-based.wallet.utils' +import { BtcBasedFromUtxoReplaceableTypes } from './btc-based.types' interface BtcBasedTransaction extends Transaction { serialize(unchecked?: boolean): string @@ -31,6 +32,10 @@ interface BtcBasedTransaction extends Transaction { export type BtcBasedTx = { sendTransaction: (body: T, options: BtcBasedTxOptions) => Promise prepareSignedTransaction: (body: T, options: BtcBasedTxOptions) => Promise + prepareSignedReplaceableTransaction: ( + body: BtcBasedFromUtxoReplaceableTypes, + options: BtcBasedTxOptions, + ) => Promise } export type BtcTransactionTypes = @@ -110,13 +115,13 @@ export const btcBasedTransactions = ( if (options.testnet) { return 'litecoin-testnet' } else { - return 'litecoin-mainnet' + return 'litecoin' } } else { if (options.testnet) { return 'bitcoin-testnet' } else { - return 'bitcoin-mainnet' + return 'bitcoin' } } } @@ -246,6 +251,38 @@ export const btcBasedTransactions = ( } } + const privateKeysFromUTXONoChecks = async ( + transaction: Transaction, + body: BtcBasedFromUtxoReplaceableTypes, + ): Promise> => { + try { + const privateKeysToSign = [] + + for (let i = 0; i < body.fromUTXO.length; i++) { + const utxo = body.fromUTXO[i] + + transaction.from([ + prepareUnspentOutput({ + txId: utxo.txHash, + outputIndex: utxo.index, + script: scriptFromAddress(utxo.address).toString(), + satoshis: amountUtils.toSatoshis(utxo.value), + }), + ]) + + if ('signatureId' in utxo) privateKeysToSign.push(utxo.signatureId) + else if ('privateKey' in utxo) privateKeysToSign.push(utxo.privateKey) + } + + return privateKeysToSign + } catch (e: any) { + if (e instanceof SdkError) { + throw e + } + throw new BtcBasedSdkError(e) + } + } + const validateBalanceFromUTXO = async ( body: BtcTransactionTypes | LtcTransactionTypes, utxos: BtcUTXO[] | LtcUTXO[], @@ -335,6 +372,43 @@ export const btcBasedTransactions = ( } } + const prepareSignedReplaceableTransaction = async function ( + body: BtcBasedFromUtxoReplaceableTypes, + ): Promise { + try { + const tx: BtcBasedTransaction = new createTransaction() + + if (body.changeAddress) { + tx.change(body.changeAddress) + } + if (body.fee) { + tx.fee(amountUtils.toSatoshis(body.fee)) + } + body.to.forEach((to) => { + tx.to(to.address, amountUtils.toSatoshis(to.value)) + }) + + const privateKeysToSign: string[] = await privateKeysFromUTXONoChecks(tx, body) + + tx.enableRBF() + const fromUTXO = body.fromUTXO + if (fromUTXO && 'signatureId' in fromUTXO[0] && fromUTXO[0].signatureId) { + return JSON.stringify(tx) + } + + new Set(privateKeysToSign).forEach((key) => { + tx.sign(new createPrivateKey(key)) + }) + + return tx.serialize(true) + } catch (e: any) { + if (e instanceof SdkError) { + throw e + } + throw new BtcBasedSdkError(e) + } + } + const verifyAmounts = (tx: Transaction, body: BtcBasedTransactionTypes) => { const outputsSum: BigNumber = body.to .map((to) => amountUtils.toSatoshis(to.value)) @@ -375,5 +449,11 @@ export const btcBasedTransactions = ( * @returns raw transaction data in hex, to be broadcasted to blockchain. */ prepareSignedTransaction, + /** + * Prepare a signed replaceable (RBF) bitcoin based transaction with the private key locally. Nothing is broadcasted to the blockchain. + * All validations of tx will be skipped, just preparing tx data + * @returns raw transaction data in hex, to be broadcasted to blockchain. + */ + prepareSignedReplaceableTransaction, } } diff --git a/packages/shared/blockchain/btc-based/src/lib/nested/btc-based.types.ts b/packages/shared/blockchain/btc-based/src/lib/nested/btc-based.types.ts index 1c84b2fe25..c8493201ea 100644 --- a/packages/shared/blockchain/btc-based/src/lib/nested/btc-based.types.ts +++ b/packages/shared/blockchain/btc-based/src/lib/nested/btc-based.types.ts @@ -1,50 +1,45 @@ import { - ApiServices, - BtcTransactionFromAddress, - BtcTransactionFromAddressKMS, - BtcTransactionFromUTXO, - BtcTransactionFromUTXOKMS, - LtcTransactionAddress, - LtcTransactionAddressKMS, - LtcTransactionUTXO, - LtcTransactionUTXOKMS, - TransactionHash, + BtcTransactionFromUTXOKMSSource, + BtcTransactionFromUTXOSource, + BtcTransactionFromUTXOTarget, } from '@tatumio/api-client' -export type BtcBasedTx = { - sendTransaction: (body: T, options: { testnet: boolean }) => Promise - prepareSignedTransaction: (body: T, options: { testnet: boolean }) => Promise +export type BtcBasedFromUTXOReplaceable = { + /** + * The array of transaction hashes, indexes of its UTXOs, and the private keys of the associated blockchain addresses + */ + fromUTXO: Array + /** + * The array of blockchain addresses to send the assets to and the amounts that each address should receive (in BTC). The difference between the UTXOs calculated in the fromUTXO section and the total amount to receive calculated in the to section will be used as the gas fee. To explicitly specify the fee amount and the blockchain address where any extra funds remaining after covering the fee will be sent, set the fee and changeAddress parameters. + */ + to: Array + /** + * The fee to be paid for the transaction (in BTC); if you are using this parameter, you have to also use the changeAddress parameter because these two parameters only work together. + */ + fee?: string + /** + * The blockchain address to send any extra assets remaining after covering the fee; if you are using this parameter, you have to also use the fee parameter because these two parameters only work together. + */ + changeAddress?: string } -export type BtcTransactionTypes = - | BtcTransactionFromAddress - | BtcTransactionFromAddressKMS - | BtcTransactionFromUTXO - | BtcTransactionFromUTXOKMS - -export type LtcTransactionTypes = - | LtcTransactionAddress - | LtcTransactionAddressKMS - | LtcTransactionUTXO - | LtcTransactionUTXOKMS - -type BtcBasedTransactionTypes = BtcTransactionTypes | LtcTransactionTypes - -type BtcFromAddressTypes = BtcTransactionFromAddress | BtcTransactionFromAddressKMS - -type LtcFromAddressTypes = LtcTransactionAddress | LtcTransactionAddressKMS - -type BtcFromUtxoTypes = BtcTransactionFromUTXO | BtcTransactionFromUTXOKMS -type LtcFromUtxoTypes = LtcTransactionUTXO | LtcTransactionUTXOKMS - -type GetTxByAddressType = - | typeof ApiServices.blockchain.bitcoin.btcGetTxByAddress - | typeof ApiServices.blockchain.ltc.ltcGetTxByAddress - -type GetUtxoType = - | typeof ApiServices.blockchain.bitcoin.btcGetUtxo - | typeof ApiServices.blockchain.ltc.ltcGetUtxo +export type BtcBasedFromUTXOKMSReplaceable = { + /** + * The array of transaction hashes, indexes of its UTXOs, and the private keys of the associated blockchain addresses + */ + fromUTXO: Array + /** + * The array of blockchain addresses to send the assets to and the amounts that each address should receive (in BTC). The difference between the UTXOs calculated in the fromUTXO section and the total amount to receive calculated in the to section will be used as the gas fee. To explicitly specify the fee amount and the blockchain address where any extra funds remaining after covering the fee will be sent, set the fee and changeAddress parameters. + */ + to: Array + /** + * The fee to be paid for the transaction (in BTC); if you are using this parameter, you have to also use the changeAddress parameter because these two parameters only work together. + */ + fee?: string + /** + * The blockchain address to send any extra assets remaining after covering the fee; if you are using this parameter, you have to also use the fee parameter because these two parameters only work together. + */ + changeAddress?: string +} -type BroadcastType = - | typeof ApiServices.blockchain.bitcoin.btcBroadcast - | typeof ApiServices.blockchain.ltc.ltcBroadcast +export type BtcBasedFromUtxoReplaceableTypes = BtcBasedFromUTXOReplaceable | BtcBasedFromUTXOKMSReplaceable diff --git a/yarn.lock b/yarn.lock index 6f5a454e52..4ea05f1360 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5007,9 +5007,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001394, caniuse-lite@^1.0.30001400, caniuse-lite@^1.0.30001426: - version "1.0.30001427" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001427.tgz#d3a749f74be7ae0671fbec3a4eea18576e8ad646" - integrity sha512-lfXQ73oB9c8DP5Suxaszm+Ta2sr/4tf8+381GkIm1MLj/YdLf+rEDyDSRCzeltuyTVGm+/s18gdZ0q+Wmp8VsQ== + version "1.0.30001520" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001520.tgz" + integrity sha512-tahF5O9EiiTzwTUqAeFjIZbn4Dnqxzz7ktrgGlMYNLH43Ul26IgTMH/zvL3DG0lZxBYnlT04axvInszUsZULdA== caseless@~0.12.0: version "0.12.0" From 556dff917a03b62152ff964c813b2ede29ba901e Mon Sep 17 00:00:00 2001 From: Alex Loiko Date: Tue, 15 Aug 2023 15:31:01 +0300 Subject: [PATCH 2/2] ALL-2392 Reverted non-needed changes --- .../blockchain/btc-based/src/lib/nested/btc-based.tx.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/shared/blockchain/btc-based/src/lib/nested/btc-based.tx.ts b/packages/shared/blockchain/btc-based/src/lib/nested/btc-based.tx.ts index 3cc3fdb8eb..ba6515a2f1 100644 --- a/packages/shared/blockchain/btc-based/src/lib/nested/btc-based.tx.ts +++ b/packages/shared/blockchain/btc-based/src/lib/nested/btc-based.tx.ts @@ -115,13 +115,13 @@ export const btcBasedTransactions = ( if (options.testnet) { return 'litecoin-testnet' } else { - return 'litecoin' + return 'litecoin-mainnet' } } else { if (options.testnet) { return 'bitcoin-testnet' } else { - return 'bitcoin' + return 'bitcoin-mainnet' } } }