diff --git a/.changeset/big-pumpkins-grab.md b/.changeset/big-pumpkins-grab.md new file mode 100644 index 0000000..a845151 --- /dev/null +++ b/.changeset/big-pumpkins-grab.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/package-lock.json b/package-lock.json index be9782a..16e146e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5269,6 +5269,17 @@ "resolved": "https://registry.npmjs.org/ergo-lib-wasm-nodejs/-/ergo-lib-wasm-nodejs-0.26.0.tgz", "integrity": "sha512-sG+MOwYKrCgcUbCHwnCOvHHS5wxEkaO/G8zUxlMiX6cSAdN06ddVIGflyqzebKe3z6OO1cN9tfMX0W7fJnzKHg==" }, + "node_modules/@rosen-bridge/bitcoin-utxo-selection": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@rosen-bridge/bitcoin-utxo-selection/-/bitcoin-utxo-selection-0.2.0.tgz", + "integrity": "sha512-8z/7HNXfkTcWObLrZGDf9TJHhfmodUbgrPP77RAbcJAk+K8vA7ooP8X1Vi1LRbjliO4TlIpGAwPq1hyPc9++6A==", + "dependencies": { + "@rosen-bridge/abstract-logger": "^1.0.0" + }, + "engines": { + "node": ">=20.11.0" + } + }, "node_modules/@rosen-bridge/changeset-formatter": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@rosen-bridge/changeset-formatter/-/changeset-formatter-0.1.0.tgz", @@ -15813,10 +15824,11 @@ }, "packages/chains/bitcoin": { "name": "@rosen-chains/bitcoin", - "version": "0.0.0", + "version": "0.1.0", "license": "GPL-3.0", "dependencies": { "@rosen-bridge/abstract-logger": "^1.0.0", + "@rosen-bridge/bitcoin-utxo-selection": "^0.2.0", "@rosen-bridge/json-bigint": "^0.1.0", "@rosen-bridge/minimum-fee": "^0.1.13", "@rosen-bridge/rosen-extractor": "^3.4.0", diff --git a/packages/chains/bitcoin/lib/BitcoinChain.ts b/packages/chains/bitcoin/lib/BitcoinChain.ts index 8c48a11..59c6778 100644 --- a/packages/chains/bitcoin/lib/BitcoinChain.ts +++ b/packages/chains/bitcoin/lib/BitcoinChain.ts @@ -2,10 +2,13 @@ import { AbstractLogger } from '@rosen-bridge/abstract-logger'; import { Fee } from '@rosen-bridge/minimum-fee'; import { AbstractUtxoChain, + AssetBalance, BoxInfo, ChainUtils, + CoveringBoxes, EventTrigger, FailedError, + GET_BOX_API_LIMIT, NetworkError, NotEnoughAssetsError, NotEnoughValidBoxesError, @@ -27,6 +30,7 @@ import JsonBigInt from '@rosen-bridge/json-bigint'; import { estimateTxFee, getPsbtTxInputBoxId } from './bitcoinUtils'; import { BITCOIN_CHAIN, SEGWIT_INPUT_WEIGHT_UNIT } from './constants'; import { blake2b } from 'blakejs'; +import { selectBitcoinUtxos } from '@rosen-bridge/bitcoin-utxo-selection'; class BitcoinChain extends AbstractUtxoChain { declare network: AbstractBitcoinNetwork; @@ -68,17 +72,18 @@ class BitcoinChain extends AbstractUtxoChain { txType: TransactionType, order: PaymentOrder, unsignedTransactions: PaymentTransaction[], - serializedSignedTransactions: string[], - ...extra: Array + serializedSignedTransactions: string[] ): Promise => { this.logger.debug( `Generating Bitcoin transaction for Order: ${JsonBigInt.stringify(order)}` ); + const feeRatio = await this.network.getFeeRatio(); + // calculate required assets const requiredAssets = order .map((order) => order.assets) .reduce(ChainUtils.sumAssetBalance, { - nativeToken: await this.minimumMeaningfulSatoshi(), + nativeToken: this.minimumMeaningfulSatoshi(feeRatio), tokens: [], }); this.logger.debug( @@ -107,8 +112,7 @@ class BitcoinChain extends AbstractUtxoChain { this.configs.addresses.lock ); - // TODO: improve box fetching (use bitcoin-box-selection package) - // local:ergo/rosen-bridge/rosen-chains#90 + // fetch input boxes const coveredBoxes = await this.getCoveringBoxes( this.configs.addresses.lock, requiredAssets, @@ -165,7 +169,7 @@ class BitcoinChain extends AbstractUtxoChain { const estimatedFee = estimateTxFee( psbt.txInputs.length, psbt.txOutputs.length + 1, - await this.network.getFeeRatio() + feeRatio ); this.logger.debug(`Estimated Fee: ${estimatedFee}`); remainingBtc -= estimatedFee; @@ -708,14 +712,59 @@ class BitcoinChain extends AbstractUtxoChain { * additional fee for adding it to a tx * @returns the minimum amount */ - minimumMeaningfulSatoshi = async (): Promise => { - const currentFeeRatio = await this.network.getFeeRatio(); + minimumMeaningfulSatoshi = (feeRatio: number): bigint => { return BigInt( Math.ceil( - (currentFeeRatio * SEGWIT_INPUT_WEIGHT_UNIT) / 4 // estimate fee per weight and convert to virtual size + (feeRatio * SEGWIT_INPUT_WEIGHT_UNIT) / 4 // estimate fee per weight and convert to virtual size ) ); }; + + /** + * gets useful, allowable and last boxes for an address until required assets are satisfied + * @param address the address + * @param requiredAssets the required assets + * @param forbiddenBoxIds the id of forbidden boxes + * @param trackMap the mapping of a box id to it's next box + * @returns an object containing the selected boxes with a boolean showing if requirements covered or not + */ + getCoveringBoxes = async ( + address: string, + requiredAssets: AssetBalance, + forbiddenBoxIds: Array, + trackMap: Map + ): Promise> => { + const getAddressBoxes = this.network.getAddressBoxes; + async function* generator() { + let offset = 0; + const limit = GET_BOX_API_LIMIT; + while (true) { + const page = await getAddressBoxes(address, offset, limit); + if (page.length === 0) break; + yield* page; + offset += limit; + } + return undefined; + } + const utxoIterator = generator(); + + // estimate tx weight without considering inputs + // 0 inputs, 2 outputs, 1 for feeRatio to get weights only, multiply by 4 to convert vSize to weight unit + const estimatedTxWeight = Number(estimateTxFee(0, 2, 1)) * 4; + + const feeRatio = await this.network.getFeeRatio(); + return selectBitcoinUtxos( + requiredAssets.nativeToken, + forbiddenBoxIds, + trackMap, + utxoIterator, + this.minimumMeaningfulSatoshi(feeRatio), + SEGWIT_INPUT_WEIGHT_UNIT, + estimatedTxWeight, + feeRatio, + this.logger + ); + }; } export default BitcoinChain; diff --git a/packages/chains/bitcoin/lib/constants.ts b/packages/chains/bitcoin/lib/constants.ts index 1057cc0..27f1a2e 100644 --- a/packages/chains/bitcoin/lib/constants.ts +++ b/packages/chains/bitcoin/lib/constants.ts @@ -2,3 +2,4 @@ export const BITCOIN_CHAIN = 'bitcoin'; export const SEGWIT_INPUT_WEIGHT_UNIT = 272; export const SEGWIT_OUTPUT_WEIGHT_UNIT = 124; +export const CONFIRMATION_TARGET = 6; diff --git a/packages/chains/bitcoin/lib/network/AbstractBitcoinNetwork.ts b/packages/chains/bitcoin/lib/network/AbstractBitcoinNetwork.ts index 2f4cebe..ba580b7 100644 --- a/packages/chains/bitcoin/lib/network/AbstractBitcoinNetwork.ts +++ b/packages/chains/bitcoin/lib/network/AbstractBitcoinNetwork.ts @@ -1,4 +1,7 @@ -import { AbstractUtxoChainNetwork } from '@rosen-chains/abstract-chain'; +import { + AbstractUtxoChainNetwork, + TokenDetail, +} from '@rosen-chains/abstract-chain'; import { Psbt } from 'bitcoinjs-lib'; import { BitcoinTx, BitcoinUtxo } from '../types'; import { BitcoinRosenExtractor } from '@rosen-bridge/rosen-extractor'; @@ -33,6 +36,23 @@ abstract class AbstractBitcoinNetwork extends AbstractUtxoChainNetwork< * @returns */ abstract getMempoolTxIds: () => Promise>; + + /** + * gets all transactions in mempool (returns empty list if the chain has no mempool) + * Note: due to heavy size of transactions in mempool, we ignore getting mempool txs in Bitcoin + * @returns empty list + */ + getMempoolTransactions = async (): Promise> => { + return []; + }; + + /** + * gets token details (name, decimals) + * @param tokenId + */ + getTokenDetail = async (tokenId: string): Promise => { + throw Error(`Bitcoin does not support token`); + }; } export default AbstractBitcoinNetwork; diff --git a/packages/chains/bitcoin/package.json b/packages/chains/bitcoin/package.json index 86585d1..ad9992c 100644 --- a/packages/chains/bitcoin/package.json +++ b/packages/chains/bitcoin/package.json @@ -1,6 +1,6 @@ { "name": "@rosen-chains/bitcoin", - "version": "0.0.0", + "version": "0.1.0", "description": "this project contains bitcoin chain for Rosen-bridge", "repository": "https://github.com/rosen-bridge/rosen-chains", "license": "GPL-3.0", @@ -34,6 +34,7 @@ }, "dependencies": { "@rosen-bridge/abstract-logger": "^1.0.0", + "@rosen-bridge/bitcoin-utxo-selection": "^0.2.0", "@rosen-bridge/json-bigint": "^0.1.0", "@rosen-bridge/minimum-fee": "^0.1.13", "@rosen-bridge/rosen-extractor": "^3.4.0", diff --git a/packages/chains/bitcoin/tests/BitcoinChain.spec.ts b/packages/chains/bitcoin/tests/BitcoinChain.spec.ts index 4ed58c7..858def5 100644 --- a/packages/chains/bitcoin/tests/BitcoinChain.spec.ts +++ b/packages/chains/bitcoin/tests/BitcoinChain.spec.ts @@ -548,7 +548,7 @@ describe('BitcoinChain', () => { 'targetChainTokenId', 'toAddress', 'fromAddress', - ])('should return false when event %p is wrong', async (key: string) => { + ])('should return false when event %s is wrong', async (key: string) => { // mock an event const event = testData.validEvent;