import { createInitializePoolMessage, idToToken, InitializePoolInstruction, Token, } from '@saturnbtcio/pool-serde-sdk'; import { getBitcoinNetwork, toXOnly } from '@saturnbtcio/psbt'; import { base64, hex } from '@scure/base'; import { Transaction } from '@scure/btc-signer'; import { PoolErrorException, PoolErrorType } from '../../error/pool.error'; import { SaturnSdkConfig } from '../../saturn-sdk'; import { AVAILABLE_FEE_TIERS, DUST_LIMIT } from '../../util/constants'; import { checkFeeRate } from '../../util/fee'; import { buildUtxoInfoFromOutputs } from '../../util/utxo-info'; import { validatePoolSdkData } from '../../util/validation'; import { BTC_TOKEN } from '../pool.dto'; import { InitializePoolMessageRequest } from './initialize-pool.dto'; import { createAccountsForInitializePool } from './initialize-pool.utils'; import { validateCreateAccountsPsbt, verifyRuneIsEtchedWithEnoughConfirmations, } from './manage-pool.validation'; import { PubkeyUtil } from '@saturnbtcio/arch-sdk'; import { getScriptPubkeyFromAddress } from '../../util/address'; import { createProtocolPda } from '../../account/pda-finder'; import { InitializePoolUtxoMetas } from '@saturnbtcio/pool-serde-sdk/src/instructions/initialize-pool'; export class InitializePool { private readonly config: SaturnSdkConfig; constructor(config: SaturnSdkConfig) { this.config = config; } async initializePoolMessage(request: InitializePoolMessageRequest) { const scureNetwork = getBitcoinNetwork(this.config.network); validatePoolSdkData(request, scureNetwork); if (request.token1 !== '0:0') { throw new PoolErrorException({ message: `Runes can only be traded against BTC`, type: PoolErrorType.InvalidAssetPair, }); } const wallet = await this.config.bitcoinProvider.getWallet( request.paymentAddress ?? request.runeAddress, ); const userArchWallet = await this.config.archProvider.getAccountAddress( PubkeyUtil.fromHex( hex.encode(toXOnly(hex.decode(request.runePublicKey))), ), ); await checkFeeRate(Number(request.feeRate), this.config.bitcoinProvider); const createdPool = await this.config.indexerProvider.getPoolsByTokenIds( request.token0, request.token1, request.feeTier, ); if (createdPool && createdPool.length > 0) { throw new PoolErrorException({ message: `Pool with token names ${request.token0} and ${request.token1} and fee tier ${request.feeTier} already exists`, type: PoolErrorType.PoolAlreadyExists, poolId: createdPool[0].id, }); } const collection = await this.config.indexerProvider.getCollection( request.token0, ); if (!collection) { throw new PoolErrorException({ message: `Collection ${request.token0} not found`, type: PoolErrorType.InvalidToken, token: request.token0, }); } await verifyRuneIsEtchedWithEnoughConfirmations( collection.id, this.config.bitcoinProvider, request.minConfirmations, ); const tx = Transaction.fromPSBT(base64.decode(request.signedPsbt)); const accountPromises = createAccountsForInitializePool( request.shardsLength, request.feeTier, idToToken(request.token0), idToToken(request.token1), this.config.programAccount, this.config.archProvider, ); const accounts = await Promise.all(accountPromises); await validateCreateAccountsPsbt( wallet, request.signedPsbt, accounts, BigInt(request.feeRate), accounts.length, scureNetwork, DUST_LIMIT, this.config.bitcoinProvider, ); tx.finalize(); const utxosInfo = buildUtxoInfoFromOutputs( tx, userArchWallet, accounts, scureNetwork, ); const [block, runeTx] = request.token0.split(':'); const runeId: Token = { block: BigInt(block), tx: Number(runeTx), }; if (!AVAILABLE_FEE_TIERS.includes(request.feeTier)) { throw new PoolErrorException({ message: `Fee tier ${request.feeTier} is not available`, type: PoolErrorType.InvalidFeeTier, feeTier: request.feeTier, }); } const btcChangeScriptPubkey = getScriptPubkeyFromAddress( request.paymentAddress ?? request.runeAddress, scureNetwork, ); const createPoolData: InitializePoolInstruction = { params: { fee: request.feeTier, token_0: runeId, token_1: BTC_TOKEN, btcChangeScriptPubkey, }, utxos: { configUtxo: utxosInfo[0], shardUtxos: utxosInfo.slice(1, 6), btcUtxo: utxosInfo[6], }, }; console.dir(createPoolData, { maxArrayLength: null }); const message = createInitializePoolMessage( this.config.programAccount, hex.encode(toXOnly(hex.decode(request.runePublicKey))), hex.encode(toXOnly(hex.decode(request.feePayerPubkey))), this.config.mempoolInfoOracleAccount, this.config.feeRateOracleAccount, accounts[0].pubkey, accounts.slice(1).map((account) => account.pubkey), createPoolData, request.recentBlockhash, hex.encode(createProtocolPda(hex.decode(this.config.programAccount))[0]), ); return message; } }