UNPKG

@atomiqlabs/chain-starknet

Version:
811 lines (734 loc) 36.3 kB
import { BitcoinRpc, BtcTx, RelaySynchronizer, SpvVaultContract, SpvVaultTokenData, SpvWithdrawalClaimedState, SpvWithdrawalClosedState, SpvWithdrawalFrontedState, SpvWithdrawalState, SpvWithdrawalStateType, SpvWithdrawalTransactionData, TransactionConfirmationOptions } from "@atomiqlabs/base"; import {Buffer} from "buffer"; import {StarknetTx} from "../chain/modules/StarknetTransactions"; import {StarknetContractBase} from "../contract/StarknetContractBase"; import {StarknetChainInterface} from "../chain/StarknetChainInterface"; import {StarknetBtcRelay} from "../btcrelay/StarknetBtcRelay"; import {cairo, constants} from "starknet"; import {StarknetAction} from "../chain/StarknetAction"; import {SpvVaultContractAbi} from "./SpvVaultContractAbi"; import {StarknetSigner} from "../wallet/StarknetSigner"; import {StarknetSpvVaultData} from "./StarknetSpvVaultData"; import {StarknetSpvWithdrawalData} from "./StarknetSpvWithdrawalData"; import {bigNumberishToBuffer, bufferToByteArray, bufferToU32Array, getLogger, toBigInt, toHex} from "../../utils/Utils"; import {StarknetBtcStoredHeader} from "../btcrelay/headers/StarknetBtcStoredHeader"; import {StarknetAddresses} from "../chain/modules/StarknetAddresses"; import {StarknetFees} from "../chain/modules/StarknetFees"; import {StarknetAbiEvent} from "../contract/modules/StarknetContractEvents"; const spvVaultContractAddreses = { [constants.StarknetChainId.SN_SEPOLIA]: "0x02d581ea838cd5ca46ba08660eddd064d50a0392f618e95310432147928d572e", [constants.StarknetChainId.SN_MAIN]: "0x01932042992647771f3d0aa6ee526e65359c891fe05a285faaf4d3ffa373e132" }; const spvVaultContractDeploymentHeights = { [constants.StarknetChainId.SN_SEPOLIA]: 1118191, [constants.StarknetChainId.SN_MAIN]: 1617295 }; const STARK_PRIME_MOD: bigint = 2n**251n + 17n * 2n**192n + 1n; function decodeUtxo(utxo: string): {txHash: bigint, vout: bigint} { const [txId, vout] = utxo.split(":"); return { txHash: BigInt("0x"+Buffer.from(txId, "hex").reverse().toString("hex")), vout: BigInt(vout) } } /** * Starknet SPV vault (UTXO-controlled vault) contract representation * * @category Swaps */ export class StarknetSpvVaultContract extends StarknetContractBase<typeof SpvVaultContractAbi> implements SpvVaultContract< StarknetTx, StarknetSigner, "STARKNET", StarknetSpvWithdrawalData, StarknetSpvVaultData > { private static readonly GasCosts = { DEPOSIT: {l1DataGas: 400, l2Gas: 4_000_000, l1Gas: 0}, OPEN: {l1DataGas: 1200, l2Gas: 8_000_000, l1Gas: 0}, FRONT: {l1DataGas: 800, l2Gas: 12_000_000, l1Gas: 0}, CLAIM: {l1DataGas: 1000, l2Gas: 400_000_000, l1Gas: 0}, CLAIM_OPTIMISTIC_ESTIMATE: {l1DataGas: 1000, l2Gas: 80_000_000, l1Gas: 0} //If claimer uses sierra 1.7.0 or later }; readonly chainId = "STARKNET"; readonly claimTimeout: number = 180; readonly maxClaimsPerTx: number = 10; private readonly btcRelay: StarknetBtcRelay<any>; private readonly bitcoinRpc: BitcoinRpc<any>; private readonly logger = getLogger("StarknetSpvVaultContract: "); constructor( chainInterface: StarknetChainInterface, btcRelay: StarknetBtcRelay<any>, bitcoinRpc: BitcoinRpc<any>, contractAddress: string = spvVaultContractAddreses[chainInterface.starknetChainId], contractDeploymentHeight?: number ) { super( chainInterface, contractAddress, SpvVaultContractAbi, contractDeploymentHeight ?? (spvVaultContractAddreses[chainInterface.starknetChainId]===contractAddress ? spvVaultContractDeploymentHeights[chainInterface.starknetChainId] : undefined) ); this.btcRelay = btcRelay; this.bitcoinRpc = bitcoinRpc; } /** * Returns a {@link StarknetAction} that opens up the spv vault with the passed data * * @param signer A starknet signer's address * @param vault Vault data and configuration */ public Open(signer: string, vault: StarknetSpvVaultData): StarknetAction { const {txHash, vout} = decodeUtxo(vault.getUtxo()); const tokens = vault.getTokenData(); if(tokens.length!==2) throw new Error("Must specify exactly 2 tokens for vault!"); return new StarknetAction(signer, this.Chain, this.contract.populateTransaction.open( vault.getVaultId(), this.btcRelay.contract.address, cairo.tuple(cairo.uint256(txHash), vout), vault.getConfirmations(), tokens[0].token, tokens[1].token, tokens[0].multiplier, tokens[1].multiplier ), StarknetSpvVaultContract.GasCosts.OPEN ); } /** * Returns a {@link StarknetAction} that deposits assets to the spv vault, amounts have to be already scaled! * This also doesn't add the approval call! * * @param signer A starknet signer's address * @param vault Vault data and configuration * @param rawAmounts An array of amounts to deposit, since the vault supports 2 tokens, up to 2 amounts are allowed */ public Deposit(signer: string, vault: StarknetSpvVaultData, rawAmounts: bigint[]): StarknetAction { return new StarknetAction(signer, this.Chain, this.contract.populateTransaction.deposit(vault.getOwner(), vault.getVaultId(), rawAmounts[0], rawAmounts[1] ?? 0n), StarknetSpvVaultContract.GasCosts.DEPOSIT ); } /** * Returns a {@link StarknetAction} that fronts the vault withdrawal. This doesn't add the approval call! * * @param signer A starknet signer's address * @param vault Vault data and configuration * @param data Vault withdrawal transaction data to front * @param withdrawalSequence Which withdrawal in sequence is this, used to prevent race conditions when 2 parties * were to front at the same time */ public Front(signer: string, vault: StarknetSpvVaultData, data: StarknetSpvWithdrawalData, withdrawalSequence: number) { return new StarknetAction(signer, this.Chain, this.contract.populateTransaction.front( vault.getOwner(), vault.getVaultId(), BigInt(withdrawalSequence), data.getTxHash(), data.serializeToStruct() ), StarknetSpvVaultContract.GasCosts.FRONT ); } /** * Returns a {@link StarknetAction} that submits the withdrawal data and executes the vault withdrawal * * @param signer A starknet signer's address * @param vault Vault data and configuration * @param data Vault withdrawal transaction data to execute and claim assets based on it * @param blockheader A stored and committed bitcoin blockheader where the bitcoin transaction got confirmed * @param merkle Merkle proof for the bitcoin transaction * @param position Position of the bitcoin transaction in the block - used for the merkle proof verification */ public Claim( signer: string, vault: StarknetSpvVaultData, data: StarknetSpvWithdrawalData, blockheader: StarknetBtcStoredHeader, merkle: Buffer[], position: number ) { return new StarknetAction(signer, this.Chain, { contractAddress: this.contract.address, entrypoint: "claim", calldata: [ vault.getOwner(), vault.getVaultId(), ...bufferToByteArray(Buffer.from(data.btcTx.hex, "hex")), ...blockheader.serialize(), merkle.length, ...merkle.map(bufferToU32Array).flat(), position, ].map(val => toHex(val, 0)) }, StarknetSpvVaultContract.GasCosts.CLAIM ); } /** * @inheritDoc */ async checkWithdrawalTx(tx: SpvWithdrawalTransactionData): Promise<void> { const result = await this.Chain.provider.callContract({ contractAddress: this.contract.address, entrypoint: "parse_bitcoin_tx", calldata: bufferToByteArray(Buffer.from(tx.btcTx.hex, "hex")) }); if(result==null) throw new Error("Failed to parse transaction!"); } /** * @inheritDoc */ createVaultData(owner: string, vaultId: bigint, utxo: string, confirmations: number, tokenData: SpvVaultTokenData[]): Promise<StarknetSpvVaultData> { if(tokenData.length!==2) throw new Error("Must specify 2 tokens in tokenData!"); return Promise.resolve(new StarknetSpvVaultData({ owner, vaultId, struct: { relay_contract: this.btcRelay.contract.address, token_0: tokenData[0].token, token_1: tokenData[1].token, token_0_multiplier: tokenData[0].multiplier, token_1_multiplier: tokenData[1].multiplier, utxo: cairo.tuple(cairo.uint256(0), 0), confirmations: confirmations, withdraw_count: 0, deposit_count: 0, token_0_amount: 0n, token_1_amount: 0n }, initialUtxo: utxo })); } //Getters /** * @inheritDoc */ async getVaultData(owner: string, vaultId: bigint): Promise<StarknetSpvVaultData | null> { const struct = await this.contract.get_vault(owner, vaultId); if(toHex(struct.relay_contract)!==toHex(this.btcRelay.contract.address)) return null; return new StarknetSpvVaultData({ owner, vaultId, struct }); } /** * @inheritDoc */ async getMultipleVaultData(vaults: {owner: string, vaultId: bigint}[]): Promise<{[owner: string]: {[vaultId: string]: StarknetSpvVaultData | null}}> { const result: {[owner: string]: {[vaultId: string]: StarknetSpvVaultData | null}} = {}; let promises: Promise<void>[] = []; //TODO: We can upgrade this to use multicall for(let {owner, vaultId} of vaults) { promises.push(this.getVaultData(owner, vaultId).then(val => { result[owner] ??= {}; result[owner][vaultId.toString(10)] = val; })); if(promises.length>=this.Chain.config.maxParallelCalls!) { await Promise.all(promises); promises = []; } } await Promise.all(promises); return result; } /** * @inheritDoc */ async getVaultLatestUtxo(owner: string, vaultId: bigint): Promise<string | null> { const vault = await this.getVaultData(owner, vaultId); if(vault==null) return null; if(!vault.isOpened()) return null; return vault.getUtxo(); } /** * @inheritDoc */ async getVaultLatestUtxos(vaults: {owner: string, vaultId: bigint}[]): Promise<{[owner: string]: {[vaultId: string]: string | null}}> { const result: {[owner: string]: {[vaultId: string]: string | null}} = {}; let promises: Promise<void>[] = []; //TODO: We can upgrade this to use multicall for(let {owner, vaultId} of vaults) { promises.push(this.getVaultLatestUtxo(owner, vaultId).then(val => { result[owner] ??= {}; result[owner][vaultId.toString(10)] = val; })); if(promises.length>=this.Chain.config.maxParallelCalls!) { await Promise.all(promises); promises = []; } } await Promise.all(promises); return result; } /** * @inheritDoc */ async getAllVaults(owner?: string): Promise<StarknetSpvVaultData[]> { const openedVaults = new Set<string>(); await this._Events.findInContractEventsForward( ["spv_swap_vault::events::Opened", "spv_swap_vault::events::Closed"], owner==null ? null : [null, null, owner], (event) => { const owner = toHex(event.params.owner); const vaultId = toBigInt(event.params.vault_id); const vaultIdentifier = owner+":"+vaultId.toString(10); if(event.name==="spv_swap_vault::events::Opened") { openedVaults.add(vaultIdentifier); } else { openedVaults.delete(vaultIdentifier); } return Promise.resolve(null); } ); const fetchedVaultData = await this.getMultipleVaultData([...openedVaults.keys()].map(identifier => { const [owner, vaultIdStr] = identifier.split(":"); return {owner, vaultId: BigInt(vaultIdStr)} })); const vaults: StarknetSpvVaultData[] = []; for(let owner in fetchedVaultData) { for(let vaultIdStr in fetchedVaultData[owner]) { const vault = fetchedVaultData[owner][vaultIdStr]; if(vault!=null) vaults.push(vault); } } return vaults; } /** * @inheritDoc */ async getFronterAddress(owner: string, vaultId: bigint, withdrawal: StarknetSpvWithdrawalData): Promise<string | null> { const fronterAddress = await this.contract.get_fronter_address_by_id(owner, vaultId, "0x"+withdrawal.getFrontingId()); if(toHex(fronterAddress, 64)==="0x0000000000000000000000000000000000000000000000000000000000000000") return null; return fronterAddress; } /** * @inheritDoc */ async getFronterAddresses(withdrawals: {owner: string, vaultId: bigint, withdrawal: StarknetSpvWithdrawalData}[]): Promise<{[btcTxId: string]: string | null}> { const result: { [btcTxId: string]: string | null } = {}; let promises: Promise<void>[] = []; //TODO: We can upgrade this to use multicall for(let {owner, vaultId, withdrawal} of withdrawals) { promises.push(this.getFronterAddress(owner, vaultId, withdrawal).then(val => { result[withdrawal.getTxId()] = val; })); if(promises.length>=this.Chain.config.maxParallelCalls!) { await Promise.all(promises); promises = []; } } await Promise.all(promises); return result; } /** * * @param event * @private */ private parseWithdrawalEvent( event: StarknetAbiEvent<typeof SpvVaultContractAbi, "spv_swap_vault::events::Fronted"> | StarknetAbiEvent<typeof SpvVaultContractAbi, "spv_swap_vault::events::Claimed"> | StarknetAbiEvent<typeof SpvVaultContractAbi, "spv_swap_vault::events::Closed"> ): ((SpvWithdrawalFrontedState | SpvWithdrawalClaimedState | SpvWithdrawalClosedState) & {btcTxId: string}) | null { switch(event.name) { case "spv_swap_vault::events::Fronted": return { type: SpvWithdrawalStateType.FRONTED, btcTxId: bigNumberishToBuffer(event.params.btc_tx_hash, 32).reverse().toString("hex"), owner: toHex(event.params.owner), vaultId: toBigInt(event.params.vault_id), recipient: toHex(event.params.recipient), fronter: toHex(event.params.caller), txId: event.txHash, getTxBlock: async() => ({ blockHeight: event.blockNumber!, blockTime: await this.Chain.Blocks.getBlockTime(event.blockNumber!) }) }; case "spv_swap_vault::events::Claimed": return { type: SpvWithdrawalStateType.CLAIMED, btcTxId: bigNumberishToBuffer(event.params.btc_tx_hash, 32).reverse().toString("hex"), owner: toHex(event.params.owner), vaultId: toBigInt(event.params.vault_id), recipient: toHex(event.params.recipient), claimer: toHex(event.params.caller), fronter: toHex(event.params.fronting_address), txId: event.txHash, getTxBlock: async() => ({ blockHeight: event.blockNumber!, blockTime: await this.Chain.Blocks.getBlockTime(event.blockNumber!) }) }; case "spv_swap_vault::events::Closed": return { type: SpvWithdrawalStateType.CLOSED, btcTxId: bigNumberishToBuffer(event.params.btc_tx_hash, 32).reverse().toString("hex"), owner: toHex(event.params.owner), vaultId: toBigInt(event.params.vault_id), error: bigNumberishToBuffer(event.params.error).toString(), txId: event.txHash, getTxBlock: async() => ({ blockHeight: event.blockNumber!, blockTime: await this.Chain.Blocks.getBlockTime(event.blockNumber!) }) }; default: return null; } } /** * @inheritDoc */ async getWithdrawalStates(withdrawalTxs: {withdrawal: StarknetSpvWithdrawalData, scStartBlockheight?: number}[]): Promise<{[btcTxId: string]: SpvWithdrawalState}> { const result: {[btcTxId: string]: SpvWithdrawalState} = {}; withdrawalTxs.forEach(withdrawalTx => { result[withdrawalTx.withdrawal.getTxId()] = { type: SpvWithdrawalStateType.NOT_FOUND }; }); const events: ["spv_swap_vault::events::Fronted", "spv_swap_vault::events::Claimed", "spv_swap_vault::events::Closed"] = ["spv_swap_vault::events::Fronted", "spv_swap_vault::events::Claimed", "spv_swap_vault::events::Closed"]; for(let i=0;i<withdrawalTxs.length;i+=this.Chain.config.maxGetLogKeys!) { const checkWithdrawalTxs = withdrawalTxs.slice(i, i+this.Chain.config.maxGetLogKeys!); const lows: string[] = []; const highs: string[] = []; let startHeight: number | null | undefined = undefined; checkWithdrawalTxs.forEach(withdrawalTx => { const txHash = Buffer.from(withdrawalTx.withdrawal.getTxId(), "hex").reverse(); const txHashU256 = cairo.uint256("0x"+txHash.toString("hex")); lows.push(toHex(txHashU256.low)); highs.push(toHex(txHashU256.high)); if(startHeight!==null) { if(withdrawalTx.scStartBlockheight==null) { startHeight = null; } else { startHeight = Math.min(startHeight ?? Infinity, withdrawalTx.scStartBlockheight); } } }); await this._Events.findInContractEventsForward( events,[lows, highs], async (event) => { const txId = bigNumberishToBuffer(event.params.btc_tx_hash, 32).reverse().toString("hex"); if(result[txId]==null) { this.logger.warn(`getWithdrawalStates(): findInContractEvents-callback: loaded event for ${txId}, but transaction not found in input params!`) return; } const eventResult = this.parseWithdrawalEvent(event); if(eventResult!=null) result[txId] = eventResult; }, startHeight ); } return result; } /** * @inheritDoc */ async getWithdrawalState(withdrawalTx: StarknetSpvWithdrawalData, scStartBlockheight?: number): Promise<SpvWithdrawalState> { const txHash = Buffer.from(withdrawalTx.getTxId(), "hex").reverse(); const txHashU256 = cairo.uint256("0x"+txHash.toString("hex")); let result: SpvWithdrawalState = { type: SpvWithdrawalStateType.NOT_FOUND }; const events: ["spv_swap_vault::events::Fronted", "spv_swap_vault::events::Claimed", "spv_swap_vault::events::Closed"] = ["spv_swap_vault::events::Fronted", "spv_swap_vault::events::Claimed", "spv_swap_vault::events::Closed"]; const keys = [toHex(txHashU256.low), toHex(txHashU256.high)]; await this._Events.findInContractEventsForward( events, keys, async (event) => { const eventResult = this.parseWithdrawalEvent(event); if(eventResult!=null) result = eventResult; }, scStartBlockheight ); return result; } async getHistoricalWithdrawalStates(recipient: string, startBlockheight?: number): Promise<{ withdrawals: { [btcTxId: string]: SpvWithdrawalClaimedState | SpvWithdrawalFrontedState }; latestBlockheight?: number }> { const {height: latestBlockheight} = await this.Chain.getFinalizedBlock(); const withdrawals: { [btcTxId: string]: SpvWithdrawalClaimedState | SpvWithdrawalFrontedState } = {}; const eventTypes: ["spv_swap_vault::events::Fronted", "spv_swap_vault::events::Claimed"] = ["spv_swap_vault::events::Fronted", "spv_swap_vault::events::Claimed"]; await this._Events.findInContractEventsForward( eventTypes, [null, null, null, null, recipient], async (_event) => { const eventResult = this.parseWithdrawalEvent(_event); if(eventResult==null || eventResult.type===SpvWithdrawalStateType.CLOSED) return null; withdrawals[eventResult.btcTxId] = eventResult; }, startBlockheight ); return { withdrawals, latestBlockheight } } /** * @inheritDoc */ getWithdrawalData(btcTx: BtcTx): Promise<StarknetSpvWithdrawalData> { return Promise.resolve(new StarknetSpvWithdrawalData(btcTx)); } //OP_RETURN data encoding/decoding /** * @inheritDoc */ fromOpReturnData(data: Buffer): { recipient: string; rawAmounts: bigint[]; executionHash?: string } { return StarknetSpvVaultContract.fromOpReturnData(data); } /** * Parses withdrawal params from OP_RETURN data * * @param data data as specified in the OP_RETURN output of the transaction */ static fromOpReturnData(data: Buffer): { recipient: string; rawAmounts: bigint[]; executionHash?: string } { let rawAmount0: bigint = 0n; let rawAmount1: bigint = 0n; let executionHash: string | undefined = undefined; if(data.length===40) { rawAmount0 = data.readBigInt64LE(32).valueOf(); } else if(data.length===48) { rawAmount0 = data.readBigInt64LE(32).valueOf(); rawAmount1 = data.readBigInt64LE(40).valueOf(); } else if(data.length===72) { rawAmount0 = data.readBigInt64LE(32).valueOf(); executionHash = data.slice(40, 72).toString("hex"); } else if(data.length===80) { rawAmount0 = data.readBigInt64LE(32).valueOf(); rawAmount1 = data.readBigInt64LE(40).valueOf(); executionHash = data.slice(48, 80).toString("hex"); } else { throw new Error("Invalid OP_RETURN data length!"); } if(executionHash!=undefined) { const executionHashValue = BigInt("0x"+executionHash); if(executionHashValue >= STARK_PRIME_MOD) throw new Error("Execution hash not in range of starknet prime"); } const recipient = "0x"+data.slice(0, 32).toString("hex"); if(!StarknetAddresses.isValidAddress(recipient)) throw new Error("Invalid recipient specified"); return {executionHash, rawAmounts: [rawAmount0, rawAmount1], recipient}; } /** * @inheritDoc */ toOpReturnData(recipient: string, rawAmounts: bigint[], executionHash?: string): Buffer { return StarknetSpvVaultContract.toOpReturnData(recipient, rawAmounts, executionHash); } /** * Serializes the withdrawal params to the OP_RETURN data * * @param recipient Recipient of the withdrawn tokens * @param rawAmounts Raw amount of tokens to withdraw * @param executionHash Optional execution hash of the actions to execute */ static toOpReturnData(recipient: string, rawAmounts: bigint[], executionHash?: string): Buffer { if(!StarknetAddresses.isValidAddress(recipient)) throw new Error("Invalid recipient specified"); if(rawAmounts.length < 1) throw new Error("At least 1 amount needs to be specified"); if(rawAmounts.length > 2) throw new Error("At most 2 amounts need to be specified"); rawAmounts.forEach(val => { if(val < 0n) throw new Error("Negative raw amount specified"); if(val >= 2n**64n) throw new Error("Raw amount overflow"); }); if(executionHash!=null) { const executionHashValue = toBigInt(executionHash); if(executionHashValue < 0n) throw new Error("Execution hash negative"); if(executionHashValue >= STARK_PRIME_MOD) throw new Error("Execution hash not in range of starknet prime"); } const recipientBuffer = Buffer.from(recipient.substring(2).padStart(64, "0"), "hex"); const amount0Buffer = Buffer.from(rawAmounts[0].toString(16).padStart(16, "0"), "hex"); const amount1Buffer = rawAmounts[1]==null || rawAmounts[1]===0n ? Buffer.alloc(0) : Buffer.from(rawAmounts[1].toString(16).padStart(16, "0"), "hex"); const executionHashBuffer = executionHash==null ? Buffer.alloc(0) : Buffer.from(executionHash.substring(2).padStart(64, "0"), "hex"); return Buffer.concat([ recipientBuffer, amount0Buffer.reverse(), amount1Buffer.reverse(), executionHashBuffer ]); } //Actions /** * @inheritDoc */ async claim(signer: StarknetSigner, vault: StarknetSpvVaultData, txs: {tx: StarknetSpvWithdrawalData, storedHeader?: StarknetBtcStoredHeader}[], synchronizer?: RelaySynchronizer<any, any, any>, initAta?: boolean, txOptions?: TransactionConfirmationOptions): Promise<string> { const result = await this.txsClaim(signer.getAddress(), vault, txs, synchronizer, initAta, txOptions?.feeRate); const [signature] = await this.Chain.sendAndConfirm(signer, result, txOptions?.waitForConfirmation, txOptions?.abortSignal); return signature; } /** * @inheritDoc */ async deposit(signer: StarknetSigner, vault: StarknetSpvVaultData, rawAmounts: bigint[], txOptions?: TransactionConfirmationOptions): Promise<string> { const result = await this.txsDeposit(signer.getAddress(), vault, rawAmounts, txOptions?.feeRate); const [signature] = await this.Chain.sendAndConfirm(signer, result, txOptions?.waitForConfirmation, txOptions?.abortSignal); return signature; } /** * @inheritDoc */ async frontLiquidity(signer: StarknetSigner, vault: StarknetSpvVaultData, realWithdrawalTx: StarknetSpvWithdrawalData, withdrawSequence: number, txOptions?: TransactionConfirmationOptions): Promise<string> { const result = await this.txsFrontLiquidity(signer.getAddress(), vault, realWithdrawalTx, withdrawSequence, txOptions?.feeRate); const [signature] = await this.Chain.sendAndConfirm(signer, result, txOptions?.waitForConfirmation, txOptions?.abortSignal); return signature; } /** * @inheritDoc */ async open(signer: StarknetSigner, vault: StarknetSpvVaultData, txOptions?: TransactionConfirmationOptions): Promise<string> { const result = await this.txsOpen(signer.getAddress(), vault, txOptions?.feeRate); const [signature] = await this.Chain.sendAndConfirm(signer, result, txOptions?.waitForConfirmation, txOptions?.abortSignal); return signature; } //Transactions /** * @inheritDoc */ async txsClaim( signer: string, vault: StarknetSpvVaultData, txs: { tx: StarknetSpvWithdrawalData, storedHeader?: StarknetBtcStoredHeader }[], synchronizer?: RelaySynchronizer<any, any, any>, initAta?: boolean, feeRate?: string ): Promise<StarknetTx[]> { if(!vault.isOpened()) throw new Error("Cannot claim from a closed vault!"); feeRate ??= await this.Chain.Fees.getFeeRate(); const txsWithMerkleProofs: { tx: StarknetSpvWithdrawalData, reversedTxId: Buffer, pos: number, blockheight: number, merkle: Buffer[], storedHeader?: StarknetBtcStoredHeader }[] = []; for(let tx of txs) { if(tx.tx.btcTx.blockhash==null) throw new Error(`Transaction ${tx.tx.btcTx.txid} doesn't have any blockhash, unconfirmed?`); const merkleProof = await this.bitcoinRpc.getMerkleProof(tx.tx.btcTx.txid, tx.tx.btcTx.blockhash); if(merkleProof==null) throw new Error(`Failed to get merkle proof for tx: ${tx.tx.btcTx.txid}!`); this.logger.debug("txsClaim(): merkle proof computed: ", merkleProof); txsWithMerkleProofs.push({ ...merkleProof, ...tx }); } const starknetTxs: StarknetTx[] = []; const storedHeaders = await StarknetBtcRelay.getCommitedHeadersAndSynchronize( signer, this.btcRelay, txsWithMerkleProofs.filter(tx => tx.storedHeader==null).map(tx => { return { blockhash: tx.tx.btcTx.blockhash!, blockheight: tx.blockheight, requiredConfirmations: vault.getConfirmations() } }), starknetTxs, synchronizer, feeRate ); if(storedHeaders==null) throw new Error("Cannot fetch committed header!"); const actions = txsWithMerkleProofs.map(tx => { return this.Claim(signer, vault, tx.tx, tx.storedHeader ?? storedHeaders[tx.tx.btcTx.blockhash!], tx.merkle, tx.pos); }); let starknetAction = new StarknetAction(signer, this.Chain); for(let action of actions) { starknetAction.add(action); if(starknetAction.ixsLength() >= this.maxClaimsPerTx) { await starknetAction.addToTxs(starknetTxs, feeRate); starknetAction = new StarknetAction(signer, this.Chain); } } if(starknetAction.ixsLength() > 0) { await starknetAction.addToTxs(starknetTxs, feeRate); } this.logger.debug("txsClaim(): "+starknetTxs.length+" claim TXs created claiming "+txs.length+" txs, owner: "+vault.getOwner()+ " vaultId: "+vault.getVaultId().toString(10)); return starknetTxs; } /** * @inheritDoc */ async txsDeposit(signer: string, vault: StarknetSpvVaultData, rawAmounts: bigint[], feeRate?: string): Promise<StarknetTx[]> { if(!vault.isOpened()) throw new Error("Cannot deposit to a closed vault!"); //Approve first const vaultTokens = vault.getTokenData(); const action = new StarknetAction(signer, this.Chain); let realAmount0 = 0n; let realAmount1 = 0n; if(rawAmounts[0]!=null && rawAmounts[0]!==0n) { realAmount0 = rawAmounts[0] * vaultTokens[0].multiplier; action.add(this.Chain.Tokens.Approve(signer, this.contract.address, vaultTokens[0].token, realAmount0)); } if(rawAmounts[1]!=null && rawAmounts[1]!==0n) { realAmount1 = rawAmounts[1] * vaultTokens[1].multiplier; action.add(this.Chain.Tokens.Approve(signer, this.contract.address, vaultTokens[1].token, realAmount1)); } action.add(this.Deposit(signer, vault, rawAmounts)); feeRate ??= await this.Chain.Fees.getFeeRate(); this.logger.debug("txsDeposit(): deposit TX created,"+ " token0: "+vaultTokens[0].token+" rawAmount0: "+rawAmounts[0].toString(10)+" amount0: "+realAmount0.toString(10)+ " token1: "+vaultTokens[1].token+" rawAmount1: "+(rawAmounts[1] ?? 0n).toString(10)+" amount1: "+realAmount1.toString(10)); return [await action.tx(feeRate)]; } /** * @inheritDoc */ async txsFrontLiquidity(signer: string, vault: StarknetSpvVaultData, realWithdrawalTx: StarknetSpvWithdrawalData, withdrawSequence: number, feeRate?: string): Promise<StarknetTx[]> { if(!vault.isOpened()) throw new Error("Cannot front on a closed vault!"); //Approve first const vaultTokens = vault.getTokenData(); const action = new StarknetAction(signer, this.Chain); const rawAmounts = realWithdrawalTx.getFrontingAmount(); let realAmount0 = 0n; let realAmount1 = 0n; if(rawAmounts[0]!=null && rawAmounts[0]!==0n) { realAmount0 = rawAmounts[0] * vaultTokens[0].multiplier; action.add(this.Chain.Tokens.Approve(signer, this.contract.address, vaultTokens[0].token, realAmount0)); } if(rawAmounts[1]!=null && rawAmounts[1]!==0n) { realAmount1 = rawAmounts[1] * vaultTokens[1].multiplier; action.add(this.Chain.Tokens.Approve(signer, this.contract.address, vaultTokens[1].token, realAmount1)); } action.add(this.Front(signer, vault, realWithdrawalTx, withdrawSequence)); feeRate ??= await this.Chain.Fees.getFeeRate(); this.logger.debug("txsFrontLiquidity(): front TX created,"+ " token0: "+vaultTokens[0].token+" rawAmount0: "+rawAmounts[0].toString(10)+" amount0: "+realAmount0.toString(10)+ " token1: "+vaultTokens[1].token+" rawAmount1: "+(rawAmounts[1] ?? 0n).toString(10)+" amount1: "+realAmount1.toString(10)); return [await action.tx(feeRate)]; } /** * @inheritDoc */ async txsOpen(signer: string, vault: StarknetSpvVaultData, feeRate?: string): Promise<StarknetTx[]> { if(vault.isOpened()) throw new Error("Cannot open an already opened vault!"); const action = this.Open(signer, vault); feeRate ??= await this.Chain.Fees.getFeeRate(); this.logger.debug("txsOpen(): open TX created, owner: "+vault.getOwner()+ " vaultId: "+vault.getVaultId().toString(10)); return [await action.tx(feeRate)]; } /** * @inheritDoc */ async getClaimFee(signer: string, vault: StarknetSpvVaultData, withdrawalData: StarknetSpvWithdrawalData, feeRate?: string): Promise<bigint> { feeRate ??= await this.Chain.Fees.getFeeRate(); return StarknetFees.getGasFee( withdrawalData==null ? StarknetSpvVaultContract.GasCosts.CLAIM_OPTIMISTIC_ESTIMATE : StarknetSpvVaultContract.GasCosts.CLAIM, feeRate ); } /** * @inheritDoc */ async getFrontFee(signer: string, vault: StarknetSpvVaultData, withdrawalData: StarknetSpvWithdrawalData, feeRate?: string): Promise<bigint> { feeRate ??= await this.Chain.Fees.getFeeRate(); return StarknetFees.getGasFee(StarknetSpvVaultContract.GasCosts.FRONT, feeRate); } }