import { CreatedPdaAccount, PubkeyUtil } from '@saturnbtcio/arch-sdk'; import { EstimateError, getBitcoinNetwork, PsbtBuilder, toXOnly, TransactionSizeCalculator, } from '@saturnbtcio/psbt'; import { SigHash } from '@scure/btc-signer'; import { PoolErrorException, PoolErrorType } from '../../error/pool.error'; import { SaturnSdkConfig } from '../../saturn-sdk'; import { DUST_LIMIT } from '../../util/constants'; import { getFeeForCreateAccountsStateChange } from '../../util/fee'; import { findBtc } from '../../util/finder'; import { getTxIdsFromUtxos } from '../../util/mempool'; import { validatePoolSdkData } from '../../util/validation'; import { BTC_TOKEN } from '../pool.dto'; import { createAccountsForAddPoolShards } from './add-pool-shards.utils'; import { AddPoolShardsCreateAccountPsbtRequest, InitPoolCreateAccountPsbtRequest, } from './create-accounts.dto'; import { createAccountsForInitializePool } from './initialize-pool.utils'; import { hex } from '@scure/base'; export class CreateAccounts { private readonly config: SaturnSdkConfig; constructor(config: SaturnSdkConfig) { this.config = config; } async initPoolCreateAccountPsbt(request: InitPoolCreateAccountPsbtRequest) { const { feeRate, feeTier, runeAddress, runePublicKey, paymentAddress, paymentPublicKey, shardsLength, token0, token1, } = request; const scureNetwork = getBitcoinNetwork(this.config.network); validatePoolSdkData( request, scureNetwork, ); const wallet = await this.config.bitcoinProvider.getWallet( paymentAddress ?? runeAddress, ); const userArchWallet = await this.config.archProvider.getAccountAddress( PubkeyUtil.fromHex(hex.encode(toXOnly(hex.decode(runePublicKey)))), ); const txBuilder = new PsbtBuilder({ network: this.config.network, }); const accountPromises = createAccountsForInitializePool( shardsLength, feeTier, token0, token1, this.config.programAccount, this.config.archProvider, ); const accounts = await Promise.all(accountPromises); // Shards length + 1 pool config for (const account of accounts) { txBuilder.addOutput({ address: account.address, value: DUST_LIMIT, runes: [], }); } const stateChangeTransactionFee = getFeeForCreateAccountsStateChange( shardsLength, ).calculateFee(feeRate, { network: scureNetwork, }); txBuilder.addOutput({ address: userArchWallet, value: stateChangeTransactionFee, runes: [], }); const walletMempoolStatus = await this.config.bitcoinProvider.getMempoolInfo( getTxIdsFromUtxos(wallet.utxos), ); const btcUtxos = findBtc( wallet, walletMempoolStatus, [], new Map(), txBuilder.getOutputValue(), feeRate, txBuilder, SigHash.ALL, paymentPublicKey ?? runePublicKey, DUST_LIMIT, ); try { const tx = txBuilder.buildAndAdjustChange({ feeRate: BigInt(request.feeRate), dustLimit: DUST_LIMIT, changeAddress: wallet.address, additionalInputAmount: 0n, }); const estimatedByteSize = TransactionSizeCalculator.createFromTransaction( tx.psbt, ).estimateByteSize({ network: scureNetwork }); // FIXME: If this error is thrown, it's caught below and sent to the // handleErrors function. This function in turn will throw an // insufficient funds error, which is quite confusing if (estimatedByteSize >= this.config.maxTxSize) { throw new PoolErrorException({ type: PoolErrorType.InvalidPsbt, message: `Generated PSBT is larger than the transaction size limit.`, }); } return { psbt: tx, }; } catch (err) { throw this.handleErrorsInCreateAccountInstructions(err, btcUtxos.amount); } } async addPoolShardsCreateAccountPsbt( request: AddPoolShardsCreateAccountPsbtRequest, ) { const { feeRate, runeAddress, runePublicKey, paymentAddress, paymentPublicKey, shardsLength, poolId, } = request; const scureNetwork = getBitcoinNetwork(this.config.network); validatePoolSdkData( request, scureNetwork, ); const wallet = await this.config.bitcoinProvider.getWallet( paymentAddress ?? runeAddress, ); const userArchWallet = await this.config.archProvider.getAccountAddress( PubkeyUtil.fromHex(hex.encode(toXOnly(hex.decode(runePublicKey)))), ); const createdPool = await this.config.indexerProvider.getPoolById(poolId); if (!createdPool) { throw new PoolErrorException({ message: `Pool with id ${poolId} not found`, type: PoolErrorType.PoolNotFound, poolId, }); } const txBuilder = new PsbtBuilder({ network: this.config.network, }); // Add new shards const accountPromises: Promise[] = createAccountsForAddPoolShards( createdPool, shardsLength, this.config.programAccount, this.config.archProvider, ); const accounts = await Promise.all(accountPromises); let btcUsed = 0n; for (const account of accounts) { txBuilder.addOutput({ address: account.address, value: DUST_LIMIT, runes: [], }); btcUsed += DUST_LIMIT; } const stateChangeTransactionFee = getFeeForCreateAccountsStateChange( shardsLength, ).calculateFee(feeRate, { network: scureNetwork, }); txBuilder.addOutput({ address: userArchWallet, value: stateChangeTransactionFee, runes: [], }); btcUsed += stateChangeTransactionFee; const walletMempoolStatus = await this.config.bitcoinProvider.getMempoolInfo( getTxIdsFromUtxos(wallet.utxos), ); const btcUtxos = findBtc( wallet, walletMempoolStatus, [], new Map(), btcUsed, feeRate, txBuilder, SigHash.ALL, paymentPublicKey ?? runePublicKey, DUST_LIMIT, ); try { const tx = txBuilder.buildAndAdjustChange({ feeRate: BigInt(request.feeRate), dustLimit: DUST_LIMIT, changeAddress: wallet.address, additionalInputAmount: 0n, }); const estimatedByteSize = TransactionSizeCalculator.createFromTransaction( tx.psbt, ).estimateByteSize({ network: scureNetwork }); // FIXME: If this error is thrown, it's caught below and sent to the // handleErrors function. This function in turn will throw an // insufficient funds error, which is quite confusing if (estimatedByteSize >= this.config.maxTxSize) { throw new PoolErrorException({ type: PoolErrorType.InvalidPsbt, message: `Generated PSBT is larger than the transaction size limit.`, }); } return { psbt: tx, }; } catch (err) { throw this.handleErrorsInCreateAccountInstructions(err, btcUtxos.amount); } } private handleErrorsInCreateAccountInstructions( err: unknown, btxUtxosAmount: bigint, ) { let amount = 0n; if (err instanceof EstimateError) { const error = err as EstimateError; if (error.error.type === 'outputs-spending-more-than-inputs') { amount = error.error.amount; } else if (error.error.type === 'not-enough-funds') { amount = error.error.amount + error.error.fee; } } return new PoolErrorException({ type: PoolErrorType.NotEnoughFunds, message: `Not enough bitcoin. You need at least ${amount.toString()} in your wallet at current fee rate.`, maxAmount: btxUtxosAmount.toString(), minAmount: amount.toString(), token: `${BTC_TOKEN.block}:${BTC_TOKEN.tx}`, }); } }