diff --git a/.changeset/smart-pigs-compete.md b/.changeset/smart-pigs-compete.md new file mode 100644 index 00000000..22ec59a6 --- /dev/null +++ b/.changeset/smart-pigs-compete.md @@ -0,0 +1,5 @@ +--- +"@rgbpp-sdk/btc": minor +--- + +Add p-limit and batch queries in the sendRgbppUtxos() and TxBuilder.validateInputs() to improve construction time diff --git a/packages/btc/package.json b/packages/btc/package.json index 5140f506..51b20c03 100644 --- a/packages/btc/package.json +++ b/packages/btc/package.json @@ -24,7 +24,8 @@ "bip32": "^4.0.0", "bitcoinjs-lib": "^6.1.5", "ecpair": "^2.1.0", - "lodash": "^4.17.21" + "lodash": "^4.17.21", + "p-limit": "^3.1.0" }, "devDependencies": { "@types/lodash": "^4.17.0", diff --git a/packages/btc/src/api/sendRgbppUtxos.ts b/packages/btc/src/api/sendRgbppUtxos.ts index 631f636a..22a52516 100644 --- a/packages/btc/src/api/sendRgbppUtxos.ts +++ b/packages/btc/src/api/sendRgbppUtxos.ts @@ -9,6 +9,7 @@ import { InitOutput, TxAddressOutput, TxBuilder } from '../transaction/build'; import { networkTypeToConfig } from '../preset/config'; import { unpackRgbppLockArgs } from '../ckb/molecule'; import { createSendUtxosBuilder } from './sendUtxos'; +import { limitPromiseBatchSize } from '../utils'; export interface SendRgbppUtxosProps { ckbVirtualTx: CKBComponents.RawTransaction; @@ -47,12 +48,36 @@ export async function createSendRgbppUtxosBuilder(props: SendRgbppUtxosProps): P const config = networkTypeToConfig(props.source.networkType); const isCkbMainnet = props.source.networkType === NetworkType.MAINNET; + // Batch querying live cells from CkbCollector + const ckbLiveCells = await Promise.all( + ckbVirtualTx.inputs.map((ckbInput) => { + return limitPromiseBatchSize(async () => { + const ckbLiveCell = await props.ckbCollector.getLiveCell(ckbInput.previousOutput!); + const isRgbppLock = isRgbppLockCell(ckbLiveCell.output, isCkbMainnet); + const lockArgs = isRgbppLock ? unpackRgbppLockArgs(ckbLiveCell.output.lock.args) : undefined; + return { + ckbLiveCell, + isRgbppLock, + lockArgs, + }; + }); + }), + ); + + // Batch querying UTXO from BtcAssetsApi + const btcUtxos = await Promise.all( + ckbLiveCells.map(({ ckbLiveCell, isRgbppLock }) => { + if (isRgbppLock) { + const args = unpackRgbppLockArgs(ckbLiveCell.output.lock.args); + return limitPromiseBatchSize(() => props.source.getUtxo(args.btcTxid, args.outIndex, props.onlyConfirmedUtxos)); + } + return undefined; + }), + ); + // Handle and check inputs for (let i = 0; i < ckbVirtualTx.inputs.length; i++) { - const ckbInput = ckbVirtualTx.inputs[i]; - - const ckbLiveCell = await props.ckbCollector.getLiveCell(ckbInput.previousOutput!); - const isRgbppLock = isRgbppLockCell(ckbLiveCell.output, isCkbMainnet); + const { lockArgs, isRgbppLock } = ckbLiveCells[i]; // If input.lock == RgbppLock, add to inputs if: // 1. input.lock.args can be unpacked to RgbppLockArgs @@ -60,8 +85,8 @@ export async function createSendRgbppUtxosBuilder(props: SendRgbppUtxosProps): P // 3. utxo.scriptPk == addressToScriptPk(props.from) // 4. utxo is not duplicated in the inputs if (isRgbppLock) { - const args = unpackRgbppLockArgs(ckbLiveCell.output.lock.args); - const utxo = await props.source.getUtxo(args.btcTxid, args.outIndex, props.onlyConfirmedUtxos); + const args = lockArgs!; + const utxo = btcUtxos[i]; if (!utxo) { throw TxBuildError.withComment(ErrorCodes.CANNOT_FIND_UTXO, `hash: ${args.btcTxid}, index: ${args.outIndex}`); } diff --git a/packages/btc/src/transaction/build.ts b/packages/btc/src/transaction/build.ts index 3c54b5aa..7d34ab7b 100644 --- a/packages/btc/src/transaction/build.ts +++ b/packages/btc/src/transaction/build.ts @@ -6,6 +6,7 @@ import { NetworkType, RgbppBtcConfig } from '../preset/types'; import { AddressType, addressToScriptPublicKeyHex, getAddressType, isSupportedFromAddress } from '../address'; import { dataToOpReturnScriptPubkey, isOpReturnScriptPubkey } from './embed'; import { networkTypeToConfig } from '../preset/config'; +import { limitPromiseBatchSize } from '../utils'; import { Utxo, utxoToInput } from './utxo'; import { FeeEstimator } from './fee'; @@ -86,15 +87,19 @@ export class TxBuilder { } async validateInputs() { - for (const input of this.inputs) { - const transactionConfirmed = await this.source.isTransactionConfirmed(input.data.hash); - if (!transactionConfirmed) { - throw TxBuildError.withComment( - ErrorCodes.UNCONFIRMED_UTXO, - `hash: ${input.data.hash}, index: ${input.data.index}`, - ); - } - } + await Promise.all( + this.inputs.map(async (input) => { + return limitPromiseBatchSize(async () => { + const transactionConfirmed = await this.source.isTransactionConfirmed(input.data.hash); + if (!transactionConfirmed) { + throw TxBuildError.withComment( + ErrorCodes.UNCONFIRMED_UTXO, + `hash: ${input.data.hash}, index: ${input.data.index}`, + ); + } + }); + }), + ); } addOutput(output: InitOutput) { diff --git a/packages/btc/src/utils.ts b/packages/btc/src/utils.ts index bc63977f..22438d5d 100644 --- a/packages/btc/src/utils.ts +++ b/packages/btc/src/utils.ts @@ -1,3 +1,4 @@ +import limitPromiseConcurrency from 'p-limit'; import { bitcoin, ecc, ECPair } from './bitcoin'; import { bytes } from '@ckb-lumos/codec'; @@ -75,3 +76,15 @@ export function transactionToHex(tx: bitcoin.Transaction, withWitness?: boolean) const buffer: Buffer = tx['__toBuffer'](undefined, undefined, withWitness ?? false); return buffer.toString('hex'); } + +/** + * Limits the batch size of promises when querying with Promise.all(). + * @example + * await Promise.all([ + * limitPromiseBatchSize(() => asyncDoSomething()), + * limitPromiseBatchSize(() => asyncDoSomething()), + * limitPromiseBatchSize(() => asyncDoSomething()), + * ... + * ]); + */ +export const limitPromiseBatchSize = limitPromiseConcurrency(10); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d0a391de..9efb6d18 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -204,6 +204,9 @@ importers: lodash: specifier: ^4.17.21 version: 4.17.21 + p-limit: + specifier: ^3.1.0 + version: 3.1.0 devDependencies: '@types/lodash': specifier: ^4.17.0 @@ -4750,7 +4753,6 @@ packages: engines: {node: '>=10'} dependencies: yocto-queue: 0.1.0 - dev: true /p-limit@5.0.0: resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} @@ -6387,7 +6389,6 @@ packages: /yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - dev: true /yocto-queue@1.0.0: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==}