Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ALL-2392 Added 'replaceable' way of tx preparation (RBF) for BTC and DOGE #890

Merged
merged 2 commits into from
Aug 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions examples/btc-example/src/app/btc.tx.rbf.example.ts
Original file line number Diff line number Diff line change
@@ -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}`)
}
4 changes: 4 additions & 0 deletions examples/btc-example/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down Expand Up @@ -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()
}
Expand Down
62 changes: 62 additions & 0 deletions examples/doge-example/src/app/doge.tx.rbf.example.ts
Original file line number Diff line number Diff line change
@@ -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}`)
}
4 changes: 4 additions & 0 deletions examples/doge-example/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand All @@ -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()

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tatumio",
"version": "2.2.23",
"version": "2.2.24",
"license": "MIT",
"repository": "https://github.com/tatumio/tatum-js",
"scripts": {
Expand Down
53 changes: 52 additions & 1 deletion packages/blockchain/doge/src/lib/doge.sdk.tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -121,6 +121,56 @@ export const dogeTransactions = (
}
}

const prepareSignedReplaceableTransaction = async (
body: BtcBasedFromUtxoReplaceableTypes,
): Promise<string> => {
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)
Expand Down Expand Up @@ -166,5 +216,6 @@ export const dogeTransactions = (
return {
sendTransaction,
prepareSignedTransaction,
prepareSignedReplaceableTransaction,
}
}
1 change: 1 addition & 0 deletions packages/shared/blockchain/btc-based/src/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -31,6 +32,10 @@ interface BtcBasedTransaction extends Transaction {
export type BtcBasedTx<T> = {
sendTransaction: (body: T, options: BtcBasedTxOptions) => Promise<TransactionHash>
prepareSignedTransaction: (body: T, options: BtcBasedTxOptions) => Promise<string>
prepareSignedReplaceableTransaction: (
body: BtcBasedFromUtxoReplaceableTypes,
options: BtcBasedTxOptions,
) => Promise<string>
}

export type BtcTransactionTypes =
Expand Down Expand Up @@ -246,6 +251,38 @@ export const btcBasedTransactions = (
}
}

const privateKeysFromUTXONoChecks = async (
transaction: Transaction,
body: BtcBasedFromUtxoReplaceableTypes,
): Promise<Array<string>> => {
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[],
Expand Down Expand Up @@ -335,6 +372,43 @@ export const btcBasedTransactions = (
}
}

const prepareSignedReplaceableTransaction = async function (
body: BtcBasedFromUtxoReplaceableTypes,
): Promise<string> {
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))
Expand Down Expand Up @@ -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,
}
}
Loading
Loading