diff --git a/ops/mainnet/mark/config.tf b/ops/mainnet/mark/config.tf index 83105f8e..1ddba629 100644 --- a/ops/mainnet/mark/config.tf +++ b/ops/mainnet/mark/config.tf @@ -138,8 +138,6 @@ locals { SOLANA_PRIVATE_KEY = local.mark_config.solana.privateKey SOLANA_RPC_URL = local.mark_config.solana.rpcUrl SOLANA_SIGNER_ADDRESS = local.mark_config.solanaSignerAddress - # ptUSDe SPL token mint on Solana (from SSM config) - PTUSDE_SOLANA_MINT = local.mark_config.solana.ptUsdeMint } ) diff --git a/ops/mainnet/mason/config.tf b/ops/mainnet/mason/config.tf index bba59575..7a7997ad 100644 --- a/ops/mainnet/mason/config.tf +++ b/ops/mainnet/mason/config.tf @@ -137,8 +137,6 @@ locals { SOLANA_PRIVATE_KEY = local.mark_config.solana.privateKey SOLANA_RPC_URL = local.mark_config.solana.rpcUrl SOLANA_SIGNER_ADDRESS = local.mark_config.solanaSignerAddress - # ptUSDe SPL token mint on Solana (from SSM config) - PTUSDE_SOLANA_MINT = local.mark_config.solana.ptUsdeMint } ) diff --git a/ops/mainnet/mason/main.tf b/ops/mainnet/mason/main.tf index 6e061e0a..5650fddd 100644 --- a/ops/mainnet/mason/main.tf +++ b/ops/mainnet/mason/main.tf @@ -362,7 +362,7 @@ module "mark_solana_usdc_poller" { subnet_ids = module.network.private_subnets security_group_id = module.sgs.lambda_sg_id container_env_vars = local.solana_usdc_poller_env_vars - schedule_expression = "rate(30 minutes)" + schedule_expression = "rate(5 minutes)" } # TAC-only Lambda - runs TAC USDT rebalancing every 1 minute diff --git a/packages/adapters/chainservice/src/solana.ts b/packages/adapters/chainservice/src/solana.ts index e54f13cb..60e189ca 100644 --- a/packages/adapters/chainservice/src/solana.ts +++ b/packages/adapters/chainservice/src/solana.ts @@ -26,6 +26,8 @@ import { TransactionInstruction, sendAndConfirmTransaction, ComputeBudgetProgram, + MessageV0, + AddressLookupTableAccount, } from '@solana/web3.js'; import bs58 from 'bs58'; @@ -77,6 +79,8 @@ export interface SolanaTransactionRequest { computeUnitPrice?: number; /** Optional compute unit limit */ computeUnitLimit?: number; + /** Optional address lookup table addresses for versioned transactions */ + addressLookupTableAddresses?: PublicKey[]; } /** @@ -151,6 +155,13 @@ export class SolanaSigner { return this.keypair.publicKey.toBase58(); } + /** + * Get the underlying keypair for direct access + */ + getKeypair(): Keypair { + return this.keypair; + } + /** * Get the underlying connection for read operations */ @@ -213,23 +224,97 @@ export class SolanaSigner { return transaction; } + /** + * Build a versioned transaction with address lookup tables for reduced transaction size. + */ + async buildVersionedTransaction(request: SolanaTransactionRequest): Promise { + const { instructions, feePayer, computeUnitPrice, computeUnitLimit, addressLookupTableAddresses } = request; + + const allInstructions: TransactionInstruction[] = []; + + // Add compute budget instructions if specified (for priority fees) + if (computeUnitLimit) { + allInstructions.push( + ComputeBudgetProgram.setComputeUnitLimit({ + units: computeUnitLimit, + }), + ); + } + + if (computeUnitPrice) { + allInstructions.push( + ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: computeUnitPrice, + }), + ); + } + + // Add user instructions + allInstructions.push(...instructions); + + // Get recent blockhash + const { blockhash } = await this.connection.getLatestBlockhash(this.config.commitment); + + // Fetch address lookup table accounts if provided + const lookupTableAccounts: AddressLookupTableAccount[] = []; + if (addressLookupTableAddresses && addressLookupTableAddresses.length > 0) { + for (const address of addressLookupTableAddresses) { + const lookupTableAccount = await this.connection.getAddressLookupTable(address); + if (lookupTableAccount.value) { + lookupTableAccounts.push(lookupTableAccount.value); + } + } + } + + // Create versioned transaction message using MessageV0 + const messageV0 = MessageV0.compile({ + payerKey: feePayer || this.keypair.publicKey, + recentBlockhash: blockhash, + instructions: allInstructions, + addressLookupTableAccounts: lookupTableAccounts, + }); + + return new VersionedTransaction(messageV0); + } + /** * Sign and send a transaction with automatic retry and confirmation */ async signAndSendTransaction(request: SolanaTransactionRequest): Promise { - // Build the transaction - const transaction = await this.buildTransaction(request); + const useVersionedTransaction = + request.addressLookupTableAddresses && request.addressLookupTableAddresses.length > 0; // Sign and send with retries let lastError: Error | null = null; for (let attempt = 1; attempt <= this.config.maxRetries; attempt++) { try { - const signature = await sendAndConfirmTransaction(this.connection, transaction, [this.keypair], { - commitment: this.config.commitment, - skipPreflight: this.config.skipPreflight, - maxRetries: 0, // We handle retries ourselves - }); + let signature: string; + + if (useVersionedTransaction) { + // Build and send versioned transaction with lookup tables + const versionedTx = await this.buildVersionedTransaction(request); + versionedTx.sign([this.keypair]); + + signature = await this.connection.sendTransaction(versionedTx, { + skipPreflight: this.config.skipPreflight, + maxRetries: 0, + }); + + // Wait for confirmation + const confirmation = await this.connection.confirmTransaction(signature, this.config.commitment); + if (confirmation.value.err) { + throw new Error(`Transaction failed: ${JSON.stringify(confirmation.value.err)}`); + } + } else { + // Build and send legacy transaction + const transaction = await this.buildTransaction(request); + signature = await sendAndConfirmTransaction(this.connection, transaction, [this.keypair], { + commitment: this.config.commitment, + skipPreflight: this.config.skipPreflight, + maxRetries: 0, // We handle retries ourselves + }); + } // Get transaction details const txDetails = await this.connection.getTransaction(signature, { @@ -259,11 +344,6 @@ export class SolanaSigner { // Exponential backoff const backoffMs = Math.min(1000 * Math.pow(2, attempt - 1), 10000); await this.delay(backoffMs); - - // Get fresh blockhash for retry - const { blockhash, lastValidBlockHeight } = await this.connection.getLatestBlockhash(this.config.commitment); - transaction.recentBlockhash = blockhash; - transaction.lastValidBlockHeight = lastValidBlockHeight; } } diff --git a/packages/adapters/database/src/db.ts b/packages/adapters/database/src/db.ts index dc5511a7..ce00f121 100644 --- a/packages/adapters/database/src/db.ts +++ b/packages/adapters/database/src/db.ts @@ -712,6 +712,7 @@ export async function getRebalanceOperations( offset?: number, filter?: { status?: RebalanceOperationStatus | RebalanceOperationStatus[]; + bridge?: string | string[]; chainId?: number; earmarkId?: string | null; invoiceId?: string; @@ -737,6 +738,17 @@ export async function getRebalanceOperations( paramCount++; } + if (filter.bridge) { + if (Array.isArray(filter.bridge)) { + conditions.push(`ro.bridge = ANY($${paramCount})`); + values.push(filter.bridge); + } else { + conditions.push(`ro.bridge = $${paramCount}`); + values.push(filter.bridge); + } + paramCount++; + } + if (filter.chainId !== undefined) { conditions.push(`ro."origin_chain_id" = $${paramCount}`); values.push(filter.chainId); diff --git a/packages/adapters/rebalance/package.json b/packages/adapters/rebalance/package.json index 177f6c34..54fed484 100644 --- a/packages/adapters/rebalance/package.json +++ b/packages/adapters/rebalance/package.json @@ -18,12 +18,13 @@ "test:unit": "jest --coverage --testPathIgnorePatterns='.*\\.integration\\.spec\\.ts$'" }, "dependencies": { - "@chainlink/ccip-js": "^0.2.6", + "@chainlink/ccip-sdk": "^0.93.0", "@cowprotocol/cow-sdk": "^7.1.2-beta.0", "@defuse-protocol/one-click-sdk-typescript": "^0.1.5", "@mark/core": "workspace:*", "@mark/database": "workspace:*", "@mark/logger": "workspace:*", + "@solana/web3.js": "^1.98.0", "@tonappchain/sdk": "0.7.1", "axios": "1.9.0", "bs58": "^6.0.0", diff --git a/packages/adapters/rebalance/src/adapters/ccip/ccip.ts b/packages/adapters/rebalance/src/adapters/ccip/ccip.ts index 5284d8b5..e1162842 100644 --- a/packages/adapters/rebalance/src/adapters/ccip/ccip.ts +++ b/packages/adapters/rebalance/src/adapters/ccip/ccip.ts @@ -1,85 +1,25 @@ -import { TransactionReceipt, createPublicClient, http, fallback, encodeFunctionData, erc20Abi, Address } from 'viem'; -import { mainnet } from 'viem/chains'; +import { TransactionReceipt, createPublicClient, http, fallback, Address } from 'viem'; import { SupportedBridge, RebalanceRoute, ChainConfiguration } from '@mark/core'; import { jsonifyError, Logger } from '@mark/logger'; import { BridgeAdapter, MemoizedTransactionRequest, RebalanceTransactionMemo } from '../../types'; +import { SVMExtraArgsV1, SDKAnyMessage } from './types'; import { - CCIPMessage, CCIPTransferStatus, CHAIN_SELECTORS, CCIP_ROUTER_ADDRESSES, CCIP_SUPPORTED_CHAINS, CHAIN_ID_TO_CCIP_SELECTOR, SOLANA_CHAIN_ID_NUMBER, + CCIPRequestTx, } from './types'; -import bs58 from 'bs58'; - -// Type for CCIP module and client - using type-only import for types, dynamic import for runtime -// The dynamic import returns the module namespace, so we extract types from it -type CCIPModuleType = typeof import('@chainlink/ccip-js'); -type CCIPClient = ReturnType; - -// Chainlink CCIP Router ABI -const CCIP_ROUTER_ABI = [ - { - inputs: [ - { name: 'destinationChainSelector', type: 'uint64' }, - { - name: 'message', - type: 'tuple', - components: [ - { name: 'receiver', type: 'bytes' }, - { name: 'data', type: 'bytes' }, - { - name: 'tokenAmounts', - type: 'tuple[]', - components: [ - { name: 'token', type: 'address' }, - { name: 'amount', type: 'uint256' }, - ], - }, - { name: 'extraArgs', type: 'bytes' }, - { name: 'feeToken', type: 'address' }, - ], - }, - ], - name: 'getFee', - outputs: [{ name: 'fee', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { name: 'destinationChainSelector', type: 'uint64' }, - { - name: 'message', - type: 'tuple', - components: [ - { name: 'receiver', type: 'bytes' }, - { name: 'data', type: 'bytes' }, - { - name: 'tokenAmounts', - type: 'tuple[]', - components: [ - { name: 'token', type: 'address' }, - { name: 'amount', type: 'uint256' }, - ], - }, - { name: 'extraArgs', type: 'bytes' }, - { name: 'feeToken', type: 'address' }, - ], - }, - ], - name: 'ccipSend', - outputs: [{ name: 'messageId', type: 'bytes32' }], - stateMutability: 'payable', - type: 'function', - }, -] as const; - +import { Connection } from '@solana/web3.js'; +import { Wallet } from '@coral-xyz/anchor'; +import { TransactionRequest } from 'ethers'; export class CCIPBridgeAdapter implements BridgeAdapter { - private ccipClient: CCIPClient | null = null; - private ccipModule: CCIPModuleType | null = null; + // Lazy-load bs58 to avoid CJS/ESM interop issues under Node16 resolution + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private bs58Module?: Promise; + private bs58Decode?: (value: string) => Uint8Array; constructor( protected readonly chains: Record, @@ -88,20 +28,28 @@ export class CCIPBridgeAdapter implements BridgeAdapter { this.logger.debug('Initializing CCIPBridgeAdapter'); } - /** - * Lazy-load the CCIP module and client to handle ES module import - */ - private async getCcipClient(): Promise { - if (this.ccipClient) { - return this.ccipClient; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected async importBs58Module(): Promise { + return import('bs58'); + } + + private async getBs58Decode(): Promise<(value: string) => Uint8Array> { + if (!this.bs58Module) { + this.bs58Module = this.importBs58Module(); } - if (!this.ccipModule) { - this.ccipModule = await import('@chainlink/ccip-js'); + const mod = await this.bs58Module; + const decode = + (mod as { decode?: unknown }).decode ?? + (mod as { default?: { decode?: unknown } }).default?.decode ?? + (mod as { default?: unknown }).default; + + if (typeof decode !== 'function') { + throw new Error('bs58 decode function is unavailable'); } - this.ccipClient = this.ccipModule.createClient(); - return this.ccipClient; + this.bs58Decode = this.bs58Decode ?? (decode as (value: string) => Uint8Array); + return this.bs58Decode; } type(): SupportedBridge { @@ -120,6 +68,65 @@ export class CCIPBridgeAdapter implements BridgeAdapter { return chainId === SOLANA_CHAIN_ID_NUMBER; } + /** + * Check message status using Chainlink CCIP Atlas API + * This is a fallback/alternative to the SDK's getExecutionReceipts method + * API docs: https://ccip.chain.link/api/h/atlas/message/{messageId} + */ + private async getMessageStatusFromAtlasAPI(messageId: string): Promise { + try { + const apiUrl = `https://ccip.chain.link/api/h/atlas/message/${messageId}`; + this.logger.debug('Checking message status via Chainlink Atlas API', { messageId, apiUrl }); + + const response = await fetch(apiUrl, { + method: 'GET', + headers: { + Accept: 'application/json', + }, + }); + + if (!response.ok) { + if (response.status === 404) { + // Message not found in Atlas API yet + return null; + } + throw new Error(`Chainlink Atlas API returned ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + + // Map API state to our status + // state: 0 = Untouched, 1 = InProgress, 2 = Success, 3 = Failure + const state = data.state; + if (state === 2) { + return { + status: 'SUCCESS', + message: 'CCIP transfer completed successfully (via Atlas API)', + messageId: messageId, + }; + } else if (state === 3) { + return { + status: 'FAILURE', + message: 'CCIP transfer failed (via Atlas API)', + messageId: messageId, + }; + } else { + // state 0 or 1, or other values + return { + status: 'PENDING', + message: `CCIP transfer pending (state: ${state})`, + messageId: messageId, + }; + } + } catch (error) { + this.logger.warn('Failed to check message status via Chainlink Atlas API', { + error: jsonifyError(error), + messageId, + }); + return null; // Return null to indicate API check failed, fallback to SDK + } + } + private validateCCIPRoute(route: RebalanceRoute): void { const originChainId = route.origin; const destinationChainId = route.destination; @@ -164,10 +171,11 @@ export class CCIPBridgeAdapter implements BridgeAdapter { * Encode a Solana base58 address as bytes for CCIP receiver field * CCIP expects Solana addresses as 32-byte public keys */ - private encodeSolanaAddress(solanaAddress: string): `0x${string}` { + private async encodeSolanaAddress(solanaAddress: string): Promise<`0x${string}`> { try { + const decode = await this.getBs58Decode(); // Decode base58 Solana address to get the 32-byte public key - const publicKeyBytes = bs58.decode(solanaAddress); + const publicKeyBytes = decode(solanaAddress); if (publicKeyBytes.length !== 32) { throw new Error(`Invalid Solana address length: expected 32 bytes, got ${publicKeyBytes.length}`); @@ -183,7 +191,7 @@ export class CCIPBridgeAdapter implements BridgeAdapter { /** * Encode recipient address based on destination chain type */ - private encodeRecipientAddress(address: string, destinationChainId: number): `0x${string}` { + private async encodeRecipientAddress(address: string, destinationChainId: number): Promise<`0x${string}`> { // Check if destination is Solana if (this.isSolanaChain(destinationChainId)) { return this.encodeSolanaAddress(address); @@ -217,13 +225,15 @@ export class CCIPBridgeAdapter implements BridgeAdapter { * @param tokenReceiver - Solana address (base58) receiving tokens. Required for token transfers. * @param accounts - Additional accounts needed. Empty for token-only transfers. */ - private encodeSVMExtraArgsV1( + private async encodeSVMExtraArgsV1( computeUnits: number, accountIsWritableBitmap: bigint, allowOutOfOrderExecution: boolean, tokenReceiver: string, accounts: string[] = [], - ): `0x${string}` { + ): Promise { + const decode = await this.getBs58Decode(); + // SVM_EXTRA_ARGS_V1_TAG: 0x1f3b3aba (4 bytes, big-endian) const typeTag = Buffer.alloc(4); typeTag.writeUInt32BE(0x1f3b3aba, 0); @@ -246,69 +256,27 @@ export class CCIPBridgeAdapter implements BridgeAdapter { tokenReceiverBuf = Buffer.from(tokenReceiver.slice(2), 'hex'); } else { // Assume base58 Solana address - tokenReceiverBuf = Buffer.from(bs58.decode(tokenReceiver)); + tokenReceiverBuf = Buffer.from(decode(tokenReceiver)); } if (tokenReceiverBuf.length !== 32) { throw new Error(`Invalid tokenReceiver length: expected 32 bytes, got ${tokenReceiverBuf.length}`); } - // accounts: Vec<[u8; 32]> - 4 bytes length (u32 LE) + 32 bytes per account - const accountsLengthBuf = Buffer.alloc(4); - accountsLengthBuf.writeUInt32LE(accounts.length, 0); - - const accountBuffers: Buffer[] = []; - for (const account of accounts) { - let accountBuf: Buffer; - if (account.startsWith('0x')) { - accountBuf = Buffer.from(account.slice(2), 'hex'); - } else { - accountBuf = Buffer.from(bs58.decode(account)); - } - if (accountBuf.length !== 32) { - throw new Error(`Invalid account length: expected 32 bytes, got ${accountBuf.length}`); + const accountsHex = accounts.map((account) => { + const buf = account.startsWith('0x') ? Buffer.from(account.slice(2), 'hex') : Buffer.from(decode(account)); + if (buf.length !== 32) { + throw new Error(`Invalid account length: expected 32 bytes, got ${buf.length}`); } - accountBuffers.push(accountBuf); - } - - return `0x${Buffer.concat([ - typeTag, - computeUnitsBuf, - bitmapBuf, - oooBuf, - tokenReceiverBuf, - accountsLengthBuf, - ...accountBuffers, - ]).toString('hex')}` as `0x${string}`; - } - - /** - * Build CCIP EVMExtraArgsV2 for EVM destination (Borsh serialized) - * See: https://docs.chain.link/ccip/api-reference/svm/v1.6.0/messages#evmextraargsv2 - * - * Format: - * - Tag: 4 bytes big-endian (0x181dcf10) - * - gas_limit: u128 (16 bytes LE) - * - allow_out_of_order_execution: bool (1 byte) - * - * @param gasLimit - Gas limit for EVM execution. MUST be 0 for token-only transfers. - * @param allowOutOfOrderExecution - Whether to allow out-of-order execution - */ - private encodeEVMExtraArgsV2(gasLimit: number, allowOutOfOrderExecution: boolean): `0x${string}` { - // EVM_EXTRA_ARGS_V2_TAG: 0x181dcf10 (4 bytes, big-endian) - const typeTag = Buffer.alloc(4); - typeTag.writeUInt32BE(0x181dcf10, 0); - - // gas_limit: u128 little-endian (16 bytes) - const gasLimitBuf = Buffer.alloc(16); - const gasLimitBigInt = BigInt(gasLimit); - gasLimitBuf.writeBigUInt64LE(gasLimitBigInt & BigInt('0xFFFFFFFFFFFFFFFF'), 0); - gasLimitBuf.writeBigUInt64LE(gasLimitBigInt >> BigInt(64), 8); - - // allow_out_of_order_execution: bool (1 byte) - const oooBuf = Buffer.alloc(1); - oooBuf.writeUInt8(allowOutOfOrderExecution ? 1 : 0, 0); + return `0x${buf.toString('hex')}` as `0x${string}`; + }); - return `0x${Buffer.concat([typeTag, gasLimitBuf, oooBuf]).toString('hex')}` as `0x${string}`; + return { + computeUnits: BigInt(computeUnits), + accountIsWritableBitmap, + allowOutOfOrderExecution, + tokenReceiver: `0x${tokenReceiverBuf.toString('hex')}` as `0x${string}`, + accounts: accountsHex, + }; } async getReceivedAmount(amount: string, route: RebalanceRoute): Promise { @@ -333,6 +301,58 @@ export class CCIPBridgeAdapter implements BridgeAdapter { } } + async sendSolanaToMainnet( + sender: string, + recipient: string, + amount: string, + connection: Connection, + wallet: Wallet, + route: RebalanceRoute, + ): Promise { + // Dynamic import for ES module compatibility; use eval to prevent TS from downleveling to require() + const { SolanaChain } = await import('@chainlink/ccip-sdk'); + const solanaChain = await SolanaChain.fromConnection(connection); + + // Create extra args + const extraArgs = { + gasLimit: 0n, // No execution on destination for token transfers + allowOutOfOrderExecution: true, + }; + + // Get fee first + const fee = await solanaChain.getFee({ + router: CCIP_ROUTER_ADDRESSES[route.origin], + destChainSelector: BigInt(CHAIN_ID_TO_CCIP_SELECTOR[route.destination]), + message: { + receiver: recipient, + data: Buffer.from(''), + tokenAmounts: [{ token: route.asset, amount: BigInt(amount) }], + extraArgs: extraArgs, + }, + }); + + const result = await solanaChain.sendMessage({ + wallet: wallet, + router: CCIP_ROUTER_ADDRESSES[route.origin], + destChainSelector: BigInt(CHAIN_ID_TO_CCIP_SELECTOR[route.destination]), + message: { + receiver: recipient, + data: Buffer.from(''), + tokenAmounts: [{ token: route.asset, amount: BigInt(amount) }], + extraArgs: extraArgs, + fee: fee, + }, + }); + + return { + hash: result.tx.hash, + logs: result.tx.logs, + blockNumber: result.tx.blockNumber, + timestamp: result.tx.timestamp, + from: sender, + }; + } + async send( sender: string, recipient: string, @@ -361,15 +381,38 @@ export class CCIPBridgeAdapter implements BridgeAdapter { // Determine if destination is Solana for special handling const isSolanaDestination = this.isSolanaChain(route.destination); + if (!isSolanaDestination) { + throw new Error('Destination chain must be an Solana chain'); + } + + // Get providers for the origin chain + const providers = this.chains[originChainId.toString()]?.providers ?? []; + if (!providers.length) { + throw new Error(`No providers found for origin chain ${originChainId}`); + } + + // Dynamic import for ES module compatibility; use eval to prevent TS from downleveling to require() + const { EVMChain } = await import('@chainlink/ccip-sdk'); + const sourceChain = await EVMChain.fromUrl(providers[0]); + const destChainSelector = BigInt(CHAIN_ID_TO_CCIP_SELECTOR[route.destination]); + // Create CCIP message with proper encoding based on destination chain // For Solana: receiver must be zero address, actual recipient goes in tokenReceiver (extraArgs) // For EVM: receiver is the actual recipient padded to 32 bytes - const ccipMessage: CCIPMessage = { + const receiver = '0x0000000000000000000000000000000000000000000000000000000000000000' as `0x${string}`; + + const extraArgs = await this.encodeSVMExtraArgsV1( + 0, // computeUnits: 0 for token-only transfers + 0n, // accountIsWritableBitmap: 0 for token-only + true, // allowOutOfOrderExecution: MUST be true for Solana + recipient, // tokenReceiver: actual Solana recipient address + [], // accounts: empty for token-only transfers + ); + + const ccipMessage: SDKAnyMessage = { // For Solana token-only transfers: receiver MUST be zero address // The actual recipient is specified in tokenReceiver field of SVMExtraArgsV1 - receiver: isSolanaDestination - ? ('0x0000000000000000000000000000000000000000000000000000000000000000' as `0x${string}`) - : this.encodeRecipientAddress(recipient, route.destination), + receiver, data: '0x' as `0x${string}`, // No additional data for simple token transfer tokenAmounts: [ { @@ -379,131 +422,68 @@ export class CCIPBridgeAdapter implements BridgeAdapter { ], // For Solana: SVMExtraArgsV1 with tokenReceiver set to actual recipient // For EVM: EVMExtraArgsV2 with gasLimit=0 for token-only transfers - extraArgs: isSolanaDestination - ? this.encodeSVMExtraArgsV1( - 0, // computeUnits: 0 for token-only transfers - 0n, // accountIsWritableBitmap: 0 for token-only - true, // allowOutOfOrderExecution: MUST be true for Solana - recipient, // tokenReceiver: actual Solana recipient address - [], // accounts: empty for token-only transfers - ) - : this.encodeEVMExtraArgsV2( - 0, // gasLimit: 0 for token-only transfers - true, // allowOutOfOrderExecution: recommended true - ), + extraArgs, feeToken: '0x0000000000000000000000000000000000000000' as Address, // Pay fees in native token }; - this.logger.debug('CCIP message constructed', { - isSolanaDestination, - receiver: ccipMessage.receiver, - extraArgsLength: ccipMessage.extraArgs.length, - tokenAmount: tokenAmount.toString(), - }); - - // Get providers for the origin chain - const providers = this.chains[originChainId.toString()]?.providers ?? []; - if (!providers.length) { - throw new Error(`No providers found for origin chain ${originChainId}`); - } - - const transports = providers.map((p: string) => http(p)); - const transport = transports.length === 1 ? transports[0] : fallback(transports, { rank: true }); - const client = createPublicClient({ transport }); - - // Get CCIP fee estimate - const ccipFee = await client.readContract({ - address: routerAddress, - abi: CCIP_ROUTER_ABI, - functionName: 'getFee', - args: [ - BigInt(destinationChainSelector), - { - receiver: ccipMessage.receiver, - data: ccipMessage.data, - tokenAmounts: ccipMessage.tokenAmounts, - extraArgs: ccipMessage.extraArgs, - feeToken: ccipMessage.feeToken, - }, - ], + // Get fee first + const fee = await sourceChain.getFee({ + router: routerAddress as `0x${string}`, + destChainSelector: BigInt(CHAIN_ID_TO_CCIP_SELECTOR[route.destination]), + message: ccipMessage, }); this.logger.info('CCIP fee calculated', { - fee: ccipFee.toString(), + fee: fee.toString(), originChainId, }); - // Check token allowance for CCIP router - const currentAllowance = await client.readContract({ - address: tokenAddress, - abi: erc20Abi, - functionName: 'allowance', - args: [sender as Address, routerAddress], + const unsignedTx = await sourceChain.generateUnsignedSendMessage({ + sender, // Your wallet address + router: routerAddress as `0x${string}`, + destChainSelector, + message: { + ...ccipMessage, + fee, + }, }); - const transactions: MemoizedTransactionRequest[] = []; + this.logger.info('CCIP transfer transactions prepared', { + originChainId, + totalTransactions: unsignedTx.transactions.length, + needsApproval: unsignedTx.transactions.length > 1, + ccipFee: fee.toString(), + effectiveAmount: amount, + }); - // Add approval transaction if needed - if (currentAllowance < tokenAmount) { - this.logger.info('Adding approval transaction for CCIP transfer', { - originChainId, - tokenAddress, - routerAddress, - currentAllowance: currentAllowance.toString(), - requiredAmount: tokenAmount.toString(), - }); + const txs = unsignedTx.transactions; + const approveTxs = txs.slice(0, txs.length - 1); + const sendTx: TransactionRequest = txs[txs.length - 1]!; - const approvalTx: MemoizedTransactionRequest = { + return [ + ...approveTxs.map((tx: TransactionRequest) => ({ transaction: { - to: tokenAddress, - data: encodeFunctionData({ - abi: erc20Abi, - functionName: 'approve', - args: [routerAddress, tokenAmount], - }), - value: BigInt(0), - funcSig: 'approve(address,uint256)', + to: tx.to as `0x${string}`, + from: tx.from as `0x${string}`, + data: tx.data as `0x${string}`, + value: tx.value as bigint, + nonce: tx.nonce as number, }, memo: RebalanceTransactionMemo.Approval, - }; - transactions.push(approvalTx); - } - - // Add CCIP send transaction - const ccipTx: MemoizedTransactionRequest = { - transaction: { - to: routerAddress, - data: encodeFunctionData({ - abi: CCIP_ROUTER_ABI, - functionName: 'ccipSend', - args: [ - BigInt(destinationChainSelector), - { - receiver: ccipMessage.receiver, - data: ccipMessage.data, - tokenAmounts: ccipMessage.tokenAmounts, - extraArgs: ccipMessage.extraArgs, - feeToken: ccipMessage.feeToken, - }, - ], - }), - value: ccipFee, // Pay fee in native token - funcSig: 'ccipSend(uint64,(bytes,bytes,(address,uint256)[],bytes,address))', + effectiveAmount: amount, + })), + { + transaction: { + to: sendTx.to as `0x${string}`, + from: sendTx.from as `0x${string}`, + data: sendTx.data as `0x${string}`, + value: sendTx.value as bigint, + nonce: sendTx.nonce as number, + }, + memo: RebalanceTransactionMemo.Rebalance, + effectiveAmount: amount, }, - memo: RebalanceTransactionMemo.Rebalance, - effectiveAmount: amount, - }; - transactions.push(ccipTx); - - this.logger.info('CCIP transfer transactions prepared', { - originChainId, - totalTransactions: transactions.length, - needsApproval: currentAllowance < tokenAmount, - ccipFee: ccipFee.toString(), - effectiveAmount: amount, - }); - - return transactions; + ]; } catch (error) { this.logger.error('Failed to prepare CCIP transfer transactions', { error: jsonifyError(error), @@ -583,6 +563,15 @@ export class CCIPBridgeAdapter implements BridgeAdapter { */ async extractMessageIdFromReceipt(transactionHash: string, originChainId: number): Promise { try { + // Skip for Solana chains - can't use eth_getTransactionReceipt on Solana RPC + if (this.isSolanaChain(originChainId)) { + this.logger.debug('Skipping message ID extraction for Solana origin chain', { + transactionHash, + originChainId, + }); + return null; + } + const providers = this.chains[originChainId.toString()]?.providers ?? []; if (!providers.length) { return null; @@ -654,69 +643,153 @@ export class CCIPBridgeAdapter implements BridgeAdapter { destinationChainId, }); - // First, try to extract the message ID from the transaction logs - const messageId = await this.extractMessageIdFromReceipt(transactionHash, originChainId); + // Create a public client for the destination chain to check status + let destinationChain, sourceChain; + + const destinationProviders = this.chains[destinationChainId.toString()]?.providers ?? []; + const originProviders = this.chains[originChainId.toString()]?.providers ?? []; + if (!destinationProviders.length) { + throw new Error(`No providers found for destination chain ${destinationChainId}`); + } + if (!originProviders.length) { + throw new Error(`No providers found for origin chain ${originChainId}`); + } + + // Dynamic import for ES module compatibility; use eval to prevent TS from downleveling to require() + const { SolanaChain, EVMChain, discoverOffRamp, ExecutionState, MessageStatus } = await import( + '@chainlink/ccip-sdk' + ); + if (this.isSolanaChain(destinationChainId)) { + destinationChain = await SolanaChain.fromUrl(destinationProviders[0]); + sourceChain = await EVMChain.fromUrl(originProviders[0]); + } else { + destinationChain = await EVMChain.fromUrl(destinationProviders[0]); + sourceChain = await SolanaChain.fromUrl(originProviders[0]); + } - if (!messageId) { - this.logger.warn('Could not extract CCIP message ID, will try using transaction hash', { + // First, try to extract the message ID from the transaction logs + const requests = await sourceChain.getMessagesInTx(transactionHash); + if (!requests.length) { + this.logger.warn('Could not extract CCIP message ID from transaction', { transactionHash, originChainId, }); + return { + status: 'PENDING', + message: 'Could not extract CCIP message ID from transaction', + messageId: undefined, + }; } - const idToCheck = messageId || transactionHash; - - // Create a public client for the destination chain to check status - let destinationClient; + const request = requests[0]; + const messageId = request.message.messageId; - if (this.isSolanaChain(destinationChainId)) { - // For Solana destination, use Ethereum mainnet client (CCIP hub) - destinationClient = createPublicClient({ - chain: mainnet, - transport: http(), + // Try Atlas API first (faster, more reliable, no rate limits) + this.logger.debug('Trying Atlas API first for message status', { messageId }); + const atlasStatus = await this.getMessageStatusFromAtlasAPI(messageId); + if (atlasStatus) { + this.logger.debug('Successfully retrieved status from Atlas API', { + messageId, + status: atlasStatus.status, }); - } else { - // For EVM destinations, create client for that specific chain - const providers = this.chains[destinationChainId.toString()]?.providers ?? []; - if (!providers.length) { - throw new Error(`No providers found for destination chain ${destinationChainId}`); - } - - const transports = providers.map((p: string) => http(p)); - const transport = transports.length === 1 ? transports[0] : fallback(transports, { rank: true }); - destinationClient = createPublicClient({ transport }); + return atlasStatus; } - // For Solana destination, use Ethereum router as the check point - const destinationRouterAddress = this.isSolanaChain(destinationChainId) - ? CCIP_ROUTER_ADDRESSES[1] // Ethereum mainnet router - : CCIP_ROUTER_ADDRESSES[destinationChainId]; + // Atlas API failed or returned null, fall back to SDK method + this.logger.debug('Atlas API unavailable or message not found, falling back to SDK method', { + messageId, + transactionHash, + }); - if (!destinationRouterAddress) { - throw new Error(`No router address for destination chain ${destinationChainId}`); - } + const offRamp = await discoverOffRamp(sourceChain, destinationChain, request.lane.onRamp); + let transferStatus; + + // Add retry logic with exponential backoff to handle rate limits + // Solana gets more retries due to higher rate limit issues, but EVM chains also benefit from retries + const isSolanaDestination = this.isSolanaChain(destinationChainId); + const maxRetries = isSolanaDestination ? 3 : 2; // 3 retries for Solana, 2 for EVM chains + let retryCount = 0; + let lastError: Error | null = null; + + while (retryCount <= maxRetries) { + try { + // Add delay between retries (exponential backoff) + if (retryCount > 0) { + const delayMs = Math.min(1000 * Math.pow(2, retryCount - 1), 20000); // Max 20 seconds + this.logger.debug('Retrying getExecutionReceipts after rate limit', { + retryCount, + delayMs, + transactionHash, + }); + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } - const sourceChainSelector = this.getDestinationChainSelector(originChainId); + // For Solana, add delay between iterations to avoid rate limits + const receiptIterator = destinationChain.getExecutionReceipts({ + offRamp, + messageId: messageId, + sourceChainSelector: request.message.sourceChainSelector, + startTime: request.tx.timestamp, + }); + + for await (const receipt of receiptIterator) { + transferStatus = + receipt.receipt.state === ExecutionState.Success ? MessageStatus.Success : MessageStatus.Failed; + + // For Solana, add a small delay between receipt checks to avoid rate limits + if (isSolanaDestination) { + await new Promise((resolve) => setTimeout(resolve, 500)); // 500ms delay + } + } - // Use the CCIP SDK to check transfer status - // Note: Type bridge via `unknown` required because @chainlink/ccip-js bundles its own - // viem version with incompatible types. At runtime, the PublicClient works correctly. - const ccipClient = await this.getCcipClient(); + // Successfully got receipts, break out of retry loop + break; + } catch (error) { + lastError = error as Error; + const errorMessage = (error as Error).message || ''; + const isRateLimitError = + errorMessage.includes('Too Many Requests') || + errorMessage.includes('429') || + errorMessage.includes('rate limit') || + errorMessage.toLowerCase().includes('rate limit'); + + if (isRateLimitError) { + if (retryCount < maxRetries) { + retryCount++; + this.logger.warn('Rate limit hit on getExecutionReceipts, will retry', { + retryCount, + maxRetries, + transactionHash, + destinationChainId, + error: errorMessage, + }); + continue; + } else { + // Exhausted retries, return pending + this.logger.error('Max retries exceeded for getExecutionReceipts', { + transactionHash, + destinationChainId, + error: jsonifyError(lastError), + }); + return { + status: 'PENDING', + message: `Rate limit error after ${maxRetries} retries: ${lastError.message}`, + messageId: messageId || undefined, + }; + } + } - const transferStatus = await ccipClient.getTransferStatus({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - client: destinationClient as any, - destinationRouterAddress, - sourceChainSelector, - messageId: idToCheck as `0x${string}`, - }); + // Not a rate limit error, throw immediately + throw error; + } + } this.logger.debug('CCIP SDK transfer status response', { transactionHash, - messageId: idToCheck, + messageId: messageId, transferStatus, - sourceChainSelector, - destinationRouterAddress, + sourceChainSelector: request.message.sourceChainSelector, + destinationRouterAddress: offRamp, }); if (transferStatus === null) { @@ -729,26 +802,18 @@ export class CCIPBridgeAdapter implements BridgeAdapter { // TransferStatus enum: Untouched = 0, InProgress = 1, Success = 2, Failure = 3 switch (transferStatus) { - case 2: // Success + case MessageStatus.Success: // Success return { status: 'SUCCESS', message: 'CCIP transfer completed successfully', messageId: messageId || undefined, - destinationTransactionHash: transactionHash, }; - case 3: // Failure + case MessageStatus.Failed: // Failure return { status: 'FAILURE', message: 'CCIP transfer failed', messageId: messageId || undefined, }; - case 1: // InProgress - return { - status: 'PENDING', - message: 'CCIP transfer in progress', - messageId: messageId || undefined, - }; - case 0: // Untouched default: return { status: 'PENDING', @@ -771,4 +836,16 @@ export class CCIPBridgeAdapter implements BridgeAdapter { }; } } + + /** + * Check CCIP message status directly by messageId using Chainlink Atlas API + * This is a lightweight alternative to getTransferStatus that doesn't require transaction hash + * + * @param messageId - The CCIP message ID (0x-prefixed hex string) + * @returns Transfer status or null if message not found + */ + async getTransferStatusByMessageId(messageId: string): Promise { + this.logger.debug('Checking CCIP transfer status by messageId via Atlas API', { messageId }); + return await this.getMessageStatusFromAtlasAPI(messageId); + } } diff --git a/packages/adapters/rebalance/src/adapters/ccip/types.ts b/packages/adapters/rebalance/src/adapters/ccip/types.ts index 3ad892d9..8f1de030 100644 --- a/packages/adapters/rebalance/src/adapters/ccip/types.ts +++ b/packages/adapters/rebalance/src/adapters/ccip/types.ts @@ -1,5 +1,38 @@ import { Address } from 'viem'; +// Solana (SVM) extra arguments structure for CCIP +export interface SVMExtraArgsV1 { + computeUnits: bigint; + accountIsWritableBitmap: bigint; + allowOutOfOrderExecution: boolean; + tokenReceiver: `0x${string}`; + accounts: `0x${string}`[]; +} + +// Minimal AnyMessage shape used when calling the CCIP SDK +export interface SDKAnyMessage { + receiver: `0x${string}`; + data: `0x${string}`; + extraArgs: SVMExtraArgsV1; + tokenAmounts?: { token: Address; amount: bigint }[]; + feeToken?: Address; + fee?: bigint; +} + +export interface CCIPRequestTx { + /** Transaction hash. */ + hash: string; + /** Logs emitted by this transaction. */ + logs: readonly unknown[]; + /** Block number containing this transaction. */ + blockNumber: number; + /** Unix timestamp of the block. */ + timestamp: number; + /** Sender address. */ + from: string; + /** Optional error if transaction failed. */ + error?: unknown; +} export interface CCIPMessage { receiver: `0x${string}`; data: `0x${string}`; @@ -36,6 +69,7 @@ export const CHAIN_ID_TO_CCIP_SELECTOR: Record = { 10: CHAIN_SELECTORS.OPTIMISM, 137: CHAIN_SELECTORS.POLYGON, 8453: CHAIN_SELECTORS.BASE, + 1399811149: CHAIN_SELECTORS.SOLANA, }; // Solana chain ID as used in the system (from @mark/core SOLANA_CHAINID) @@ -43,12 +77,13 @@ export const SOLANA_CHAIN_ID_NUMBER = 1399811149; // CCIP Router addresses by chain ID // See: https://docs.chain.link/ccip/directory/mainnet -export const CCIP_ROUTER_ADDRESSES: Record = { +export const CCIP_ROUTER_ADDRESSES: Record = { 1: '0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D', // Ethereum Mainnet 42161: '0x141fa059441E0ca23ce184B6A78bafD2A517DdE8', // Arbitrum 10: '0x261c05167db67B2b619f9d312e0753f3721ad6E8', // Optimism 137: '0x849c5ED5a80F5B408Dd4969b78c2C8fdf0565Bfe', // Polygon 8453: '0x881e3A65B4d4a04dD529061dd0071cf975F58bCD', // Base + 1399811149: 'Ccip842gzYHhvdDkSyi2YVCoAWPbYJoApMFzSxQroE9C', // Solana }; // Supported chains for CCIP operations (EVM only) @@ -68,3 +103,61 @@ export interface SolanaAddressEncoding { address: string; encoding: 'base58' | 'hex'; } + +// Chainlink CCIP Router ABI +export const CCIP_ROUTER_ABI = [ + { + inputs: [ + { name: 'destinationChainSelector', type: 'uint64' }, + { + name: 'message', + type: 'tuple', + components: [ + { name: 'receiver', type: 'bytes' }, + { name: 'data', type: 'bytes' }, + { + name: 'tokenAmounts', + type: 'tuple[]', + components: [ + { name: 'token', type: 'address' }, + { name: 'amount', type: 'uint256' }, + ], + }, + { name: 'extraArgs', type: 'bytes' }, + { name: 'feeToken', type: 'address' }, + ], + }, + ], + name: 'getFee', + outputs: [{ name: 'fee', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { name: 'destinationChainSelector', type: 'uint64' }, + { + name: 'message', + type: 'tuple', + components: [ + { name: 'receiver', type: 'bytes' }, + { name: 'data', type: 'bytes' }, + { + name: 'tokenAmounts', + type: 'tuple[]', + components: [ + { name: 'token', type: 'address' }, + { name: 'amount', type: 'uint256' }, + ], + }, + { name: 'extraArgs', type: 'bytes' }, + { name: 'feeToken', type: 'address' }, + ], + }, + ], + name: 'ccipSend', + outputs: [{ name: 'messageId', type: 'bytes32' }], + stateMutability: 'payable', + type: 'function', + }, +] as const; diff --git a/packages/adapters/rebalance/src/adapters/cowswap/types.ts b/packages/adapters/rebalance/src/adapters/cowswap/types.ts index d09a6833..ac2c7cdb 100644 --- a/packages/adapters/rebalance/src/adapters/cowswap/types.ts +++ b/packages/adapters/rebalance/src/adapters/cowswap/types.ts @@ -14,7 +14,7 @@ export const SUPPORTED_NETWORKS: Record = { export const USDC_USDT_PAIRS: Record = { 1: { - usdc: '0xA0b86a33E6417fad52e9d5e5d12a0749A9e9ad2B', + usdc: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', usdt: '0xdAC17F958D2ee523a2206206994597C13D831ec7', }, 100: { diff --git a/packages/adapters/rebalance/src/adapters/pendle/pendle.ts b/packages/adapters/rebalance/src/adapters/pendle/pendle.ts index cd9bfa5b..1b5496ad 100644 --- a/packages/adapters/rebalance/src/adapters/pendle/pendle.ts +++ b/packages/adapters/rebalance/src/adapters/pendle/pendle.ts @@ -89,7 +89,7 @@ export class PendleBridgeAdapter implements BridgeAdapter { const url = `${PENDLE_API_BASE_URL}/${route.origin}/convert`; const params = new URLSearchParams({ - receiver: '0x0000000000000000000000000000000000000000', + receiver: '0x000000000000000000000000000000000000dead', slippage: '0.005', tokensIn, tokensOut, diff --git a/packages/adapters/rebalance/src/index.ts b/packages/adapters/rebalance/src/index.ts index ecc00a1e..b7cc2f67 100644 --- a/packages/adapters/rebalance/src/index.ts +++ b/packages/adapters/rebalance/src/index.ts @@ -1,5 +1,6 @@ export { RebalanceAdapter } from './adapters'; export * from './types'; export { USDC_PTUSDE_PAIRS, PENDLE_SUPPORTED_CHAINS, PENDLE_API_BASE_URL } from './adapters/pendle/types'; +export { PendleBridgeAdapter } from './adapters/pendle'; export { CHAIN_SELECTORS, CCIP_ROUTER_ADDRESSES, CCIP_SUPPORTED_CHAINS } from './adapters/ccip/types'; export { CCIPBridgeAdapter } from './adapters/ccip'; diff --git a/packages/adapters/rebalance/test/adapters/ccip/ccip.spec.ts b/packages/adapters/rebalance/test/adapters/ccip/ccip.spec.ts index 16fa422f..f2afe781 100644 --- a/packages/adapters/rebalance/test/adapters/ccip/ccip.spec.ts +++ b/packages/adapters/rebalance/test/adapters/ccip/ccip.spec.ts @@ -26,6 +26,17 @@ const mockChains = { multicall3: '0x0000000000000000000000000000000000000003', }, }, + [SOLANA_CHAIN_ID_NUMBER.toString()]: { + providers: ['https://mock-sol-rpc'], + assets: [], + invoiceAge: 0, + gasThreshold: '0', + deployments: { + everclear: 'Ccip842gzYHhvdDkSyi2YVCoAWPbYJoApMFzSxQroE9C', + permit2: '0x' + '0'.repeat(40), + multicall3: '0x' + '0'.repeat(40), + }, + }, '42161': { providers: ['https://mock-arb-rpc'], assets: [], @@ -46,6 +57,12 @@ const usdcAddress = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'; const evmToEvmRoute = { asset: usdcAddress, origin: 1, destination: 42161 }; const evmToSolanaRoute = { asset: usdcAddress, origin: 1, destination: SOLANA_CHAIN_ID_NUMBER }; +const mockExecutionReceipt = { receipt: { state: 2 } }; +const mockGetExecutionReceipts: any = jest.fn().mockImplementation(async function* () { + yield mockExecutionReceipt; +}); +const mockGetMessagesInTx: any = jest.fn(); + const mockReceipt = { blockHash: '0xblock', blockNumber: 1n, @@ -68,13 +85,99 @@ const mockCcipClient = { getTransferStatus: jest.fn<() => Promise>(), }; -jest.mock('@chainlink/ccip-js', () => ({ - createClient: () => mockCcipClient, -}), { virtual: true }); +// Mock CCIP SDK before importing adapter +jest.mock('@chainlink/ccip-sdk', () => { + type UnsignedTx = { + transactions: Array<{ to: `0x${string}`; from: `0x${string}`; data: `0x${string}`; value: bigint; nonce: number }>; + }; + const mockGetFee = jest.fn<() => Promise>().mockResolvedValue(0n); + const mockGenerateUnsignedSendMessage = jest.fn<() => Promise>().mockResolvedValue({ + transactions: [ + { + to: CCIP_ROUTER_ADDRESSES[1] as `0x${string}`, + from: sender as `0x${string}`, + data: '0x' as `0x${string}`, + value: 0n, + nonce: 0, + }, + ], + }); + const mockSendMessage = jest + .fn<() => Promise<{ tx: { hash: string; logs: unknown[]; blockNumber: number; timestamp: number; from: string } }>>() + .mockResolvedValue({ + tx: { + hash: '0xsolanatx', + logs: [], + blockNumber: 1, + timestamp: 0, + from: sender, + }, + }); + + const mockEvmChain = { + getFee: mockGetFee, + generateUnsignedSendMessage: mockGenerateUnsignedSendMessage, + }; + + const mockSolanaChain = { + getFee: mockGetFee, + generateUnsignedSendMessage: mockGenerateUnsignedSendMessage, + }; + + const mockSolanaConnChain = { + getFee: mockGetFee, + sendMessage: mockSendMessage, + }; + + mockGetMessagesInTx.mockResolvedValue([ + { + message: { + messageId: '0xmsgid', + sourceChainSelector: BigInt(CHAIN_SELECTORS.ETHEREUM), + }, + tx: { timestamp: 0 }, + lane: { onRamp: '0xonramp' }, + }, + ]); + + return { + EVMChain: { + fromUrl: jest.fn((): Promise => + Promise.resolve({ + ...mockEvmChain, + getMessagesInTx: mockGetMessagesInTx, + getExecutionReceipts: mockGetExecutionReceipts, + }), + ), + }, + SolanaChain: { + fromUrl: jest.fn((): Promise => + Promise.resolve({ + ...mockSolanaChain, + getMessagesInTx: mockGetMessagesInTx, + getExecutionReceipts: mockGetExecutionReceipts, + }), + ), + fromConnection: jest.fn((): Promise => Promise.resolve(mockSolanaConnChain)), + }, + ExecutionState: { Success: 2, Failed: 3 } as any, + MessageStatus: { Success: 'SUCCESS', Failed: 'FAILED' } as any, + CHAIN_FAMILY: { EVM: 'EVM', SOLANA: 'SOLANA' }, + discoverOffRamp: jest.fn((): Promise => Promise.resolve('0xofframp')), + } as any; +}); // Import adapter after mocks are set up import { CCIPBridgeAdapter } from '../../../src/adapters/ccip/ccip'; +// Create a testable subclass that overrides the protected importCcipModule method +class TestableCCIPBridgeAdapter extends CCIPBridgeAdapter { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected async importCcipModule(): Promise { + return { createClient: () => mockCcipClient }; + } +} + // Mock viem jest.mock('viem', () => { const actual = jest.requireActual('viem'); @@ -116,12 +219,24 @@ jest.mock('bs58', () => { }); describe('CCIPBridgeAdapter', () => { - let adapter: CCIPBridgeAdapter; + let adapter: TestableCCIPBridgeAdapter; + // Mock global fetch for Atlas API + const mockFetch = jest.fn(); + let originalFetch: typeof fetch; + + beforeAll(() => { + originalFetch = global.fetch; + global.fetch = mockFetch as typeof fetch; + }); + + afterAll(() => { + global.fetch = originalFetch; + }); beforeEach(() => { jest.clearAllMocks(); mockCcipClient.getTransferStatus.mockResolvedValue(null); - adapter = new CCIPBridgeAdapter(mockChains, mockLogger); + adapter = new TestableCCIPBridgeAdapter(mockChains, mockLogger); }); describe('constructor and type', () => { @@ -184,38 +299,64 @@ describe('CCIPBridgeAdapter', () => { }); describe('address encoding', () => { - it('encodes EVM address with 32-byte padding', () => { - const encoded = (adapter as any).encodeRecipientAddress(recipient, 1); + it('encodes EVM address with 32-byte padding', async () => { + const encoded = await (adapter as any).encodeRecipientAddress(recipient, 1); // Should be 0x + 24 zeros + 40 char address (without 0x prefix) expect(encoded.length).toBe(66); // 0x + 64 hex chars expect(encoded.startsWith('0x000000000000000000000000')).toBe(true); }); - it('throws for invalid EVM address format', () => { - expect(() => (adapter as any).encodeRecipientAddress('invalid', 1)).toThrow( - 'Invalid EVM address format: invalid' + it('throws for invalid EVM address format', async () => { + await expect((adapter as any).encodeRecipientAddress('0x1234', 1)).rejects.toThrow( + 'Invalid EVM address format: 0x1234', ); }); - it('encodes Solana address using bs58 decode', () => { + it('encodes Solana address through encodeRecipientAddress', async () => { + const solanaAddress = 'PTSg1sXMujX5bgTM88C2PMksHG5w2bqvXJrG9uUdzpA'; + const encoded = await (adapter as any).encodeRecipientAddress(solanaAddress, SOLANA_CHAIN_ID_NUMBER); + expect(encoded.startsWith('0x')).toBe(true); + expect(encoded.length).toBe(66); + }); + + it('encodes Solana address using bs58 decode', async () => { const solanaAddress = 'PTSg1sXMujX5bgTM88C2PMksHG5w2bqvXJrG9uUdzpA'; - const encoded = (adapter as any).encodeSolanaAddress(solanaAddress); + const encoded = await (adapter as any).encodeSolanaAddress(solanaAddress); expect(encoded.startsWith('0x')).toBe(true); expect(encoded.length).toBe(66); // 0x + 64 hex chars (32 bytes) }); + + it('throws when Solana address is invalid', async () => { + await expect((adapter as any).encodeSolanaAddress('short')).rejects.toThrow( + /Failed to encode Solana address 'short'/, + ); + }); + }); + + describe('SVM extra args encoding', () => { + it('returns hex-encoded tokenReceiver and accounts', async () => { + const solanaAddress = 'PTSg1sXMujX5bgTM88C2PMksHG5w2bqvXJrG9uUdzpA'; + const extra = await (adapter as any).encodeSVMExtraArgsV1(0, 0n, true, solanaAddress, [solanaAddress]); + expect(extra.tokenReceiver.startsWith('0x')).toBe(true); + expect(extra.tokenReceiver.length).toBe(66); + expect(extra.accounts[0]?.startsWith('0x')).toBe(true); + expect(extra.accounts[0]?.length).toBe(66); + expect(extra.allowOutOfOrderExecution).toBe(true); + }); + + it('throws when accounts are not 32 bytes', async () => { + const solanaAddress = 'PTSg1sXMujX5bgTM88C2PMksHG5w2bqvXJrG9uUdzpA'; + await expect( + (adapter as any).encodeSVMExtraArgsV1(0, 0n, true, solanaAddress, ['0x1234']), + ).rejects.toThrow(/Invalid account length/); + }); }); describe('send', () => { - it('returns approval and send transactions for EVM to EVM', async () => { - const txs = await adapter.send(sender, recipient, amount, evmToEvmRoute); - - // Should have at least one transaction (approval if needed + send) - expect(txs.length).toBeGreaterThanOrEqual(1); - - // Last transaction should be the CCIP send - const sendTx = txs.find(tx => tx.memo === RebalanceTransactionMemo.Rebalance); - expect(sendTx).toBeDefined(); - expect(sendTx?.transaction.to).toBe(CCIP_ROUTER_ADDRESSES[1]); + it('throws for non-Solana destination', async () => { + await expect(adapter.send(sender, recipient, amount, evmToEvmRoute)).rejects.toThrow( + 'Destination chain must be an Solana chain', + ); }); it('throws for unsupported origin chain', async () => { @@ -225,35 +366,72 @@ describe('CCIPBridgeAdapter', () => { ); }); - it('includes effectiveAmount on send transaction', async () => { - const txs = await adapter.send(sender, recipient, amount, evmToEvmRoute); + it('returns send transaction for EVM to Solana route', async () => { + const solanaRecipient = 'PTSg1sXMujX5bgTM88C2PMksHG5w2bqvXJrG9uUdzpA'; + const txs = await adapter.send(sender, solanaRecipient, amount, evmToSolanaRoute); const sendTx = txs.find(tx => tx.memo === RebalanceTransactionMemo.Rebalance); + expect(sendTx).toBeDefined(); + expect(sendTx?.transaction.to).toBe(CCIP_ROUTER_ADDRESSES[1]); expect(sendTx?.effectiveAmount).toBe(amount); }); + + it('throws when no providers exist for origin chain', async () => { + const adapterNoProviders = new TestableCCIPBridgeAdapter( + { ...mockChains, '1': { ...mockChains['1'], providers: [] } }, + mockLogger, + ); + await expect(adapterNoProviders.send(sender, recipient, amount, evmToSolanaRoute)).rejects.toThrow( + 'No providers found for origin chain 1', + ); + }); }); describe('readyOnDestination', () => { it('returns false if origin transaction is not successful', async () => { const failedReceipt = { ...mockReceipt, status: 'reverted' }; - const ready = await adapter.readyOnDestination(amount, evmToEvmRoute, failedReceipt); + const ready = await adapter.readyOnDestination(amount, evmToSolanaRoute, failedReceipt); expect(ready).toBe(false); }); + it('treats numeric status 1 as successful', async () => { + jest.spyOn(adapter as any, 'getTransferStatus').mockResolvedValue({ + status: 'SUCCESS', + message: 'ok', + }); + const ready = await adapter.readyOnDestination(amount, evmToSolanaRoute, { ...mockReceipt, status: 1 } as any); + expect(ready).toBe(true); + }); + it('returns true when CCIP status is SUCCESS', async () => { - mockCcipClient.getTransferStatus.mockResolvedValue(2); // Success - const ready = await adapter.readyOnDestination(amount, evmToEvmRoute, mockReceipt); + jest.spyOn(adapter as any, 'getTransferStatus').mockResolvedValue({ + status: 'SUCCESS', + message: 'ok', + }); + const ready = await adapter.readyOnDestination(amount, evmToSolanaRoute, mockReceipt); expect(ready).toBe(true); }); it('returns false when CCIP status is PENDING', async () => { - mockCcipClient.getTransferStatus.mockResolvedValue(1); // InProgress - const ready = await adapter.readyOnDestination(amount, evmToEvmRoute, mockReceipt); + jest.spyOn(adapter as any, 'getTransferStatus').mockResolvedValue({ + status: 'PENDING', + message: 'pending', + }); + const ready = await adapter.readyOnDestination(amount, evmToSolanaRoute, mockReceipt); expect(ready).toBe(false); }); it('returns false when CCIP status is null', async () => { - mockCcipClient.getTransferStatus.mockResolvedValue(null); - const ready = await adapter.readyOnDestination(amount, evmToEvmRoute, mockReceipt); + jest.spyOn(adapter as any, 'getTransferStatus').mockResolvedValue({ + status: 'PENDING', + message: 'pending', + }); + const ready = await adapter.readyOnDestination(amount, evmToSolanaRoute, mockReceipt); + expect(ready).toBe(false); + }); + + it('returns false when getTransferStatus throws', async () => { + jest.spyOn(adapter as any, 'getTransferStatus').mockRejectedValue(new Error('boom')); + const ready = await adapter.readyOnDestination(amount, evmToSolanaRoute, mockReceipt); expect(ready).toBe(false); }); }); @@ -266,30 +444,323 @@ describe('CCIPBridgeAdapter', () => { }); describe('getTransferStatus', () => { - it('returns PENDING when status is null', async () => { - mockCcipClient.getTransferStatus.mockResolvedValue(null); + beforeEach(() => { + jest.clearAllMocks(); + mockGetMessagesInTx.mockResolvedValue([ + { + message: { + messageId: '0xmsgid', + sourceChainSelector: BigInt(CHAIN_SELECTORS.ETHEREUM), + }, + tx: { timestamp: 0 }, + lane: { onRamp: '0xonramp' }, + }, + ]); + mockGetExecutionReceipts.mockImplementation(async function* () { + yield mockExecutionReceipt; + }); + // Default: Atlas API returns null (not found), so we fall back to SDK + mockFetch.mockResolvedValue({ + ok: false, + status: 404, + statusText: 'Not Found', + json: async () => ({}), + } as Response); + }); + + it('returns SUCCESS from Atlas API when available', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + statusText: 'OK', + json: async () => ({ state: 2, messageId: '0xmsgid' }), + } as Response); + const status = await adapter.getTransferStatus('0xhash', 1, 42161); - expect(status.status).toBe('PENDING'); + expect(status.status).toBe('SUCCESS'); + expect(status.messageId).toBe('0xmsgid'); + expect(status.message).toContain('via Atlas API'); + expect(mockGetExecutionReceipts).not.toHaveBeenCalled(); // SDK should not be called }); - it('returns SUCCESS when status is 2', async () => { - mockCcipClient.getTransferStatus.mockResolvedValue(2); + it('falls back to SDK when Atlas API returns 404', async () => { + // Atlas API returns 404 (default in beforeEach) const status = await adapter.getTransferStatus('0xhash', 1, 42161); expect(status.status).toBe('SUCCESS'); + expect(status.messageId).toBe('0xmsgid'); + expect(mockGetExecutionReceipts).toHaveBeenCalled(); // SDK should be called as fallback }); - it('returns FAILURE when status is 3', async () => { - mockCcipClient.getTransferStatus.mockResolvedValue(3); + it('returns SUCCESS when execution receipt shows success (SDK fallback)', async () => { + const status = await adapter.getTransferStatus('0xhash', 1, 42161); + expect(status.status).toBe('SUCCESS'); + expect(status.messageId).toBe('0xmsgid'); + }); + + it('returns FAILURE from Atlas API when state is 3', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + statusText: 'OK', + json: async () => ({ state: 3, messageId: '0xmsgid' }), + } as Response); + const status = await adapter.getTransferStatus('0xhash', 1, 42161); expect(status.status).toBe('FAILURE'); + expect(status.message).toContain('via Atlas API'); + expect(mockGetExecutionReceipts).not.toHaveBeenCalled(); + }); + + it('returns PENDING from Atlas API when state is 1 (InProgress)', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + statusText: 'OK', + json: async () => ({ state: 1, messageId: '0xmsgid' }), + } as Response); + + const status = await adapter.getTransferStatus('0xhash', 1, 42161); + expect(status.status).toBe('PENDING'); + expect(status.message).toContain('pending (state: 1)'); + expect(mockGetExecutionReceipts).not.toHaveBeenCalled(); + }); + + it('returns FAILURE when execution receipt shows failure (SDK fallback)', async () => { + mockGetExecutionReceipts.mockImplementation(async function* () { + yield { receipt: { state: 3 } }; + }); + const status = await adapter.getTransferStatus('0xhash', 1, 42161); + expect(status.status).toBe('FAILURE'); + }); + + it('falls back to SDK when Atlas API throws error', async () => { + mockFetch.mockRejectedValueOnce(new Error('Network error')); + mockGetExecutionReceipts.mockImplementation(async function* () { + yield mockExecutionReceipt; + }); + + const status = await adapter.getTransferStatus('0xhash', 1, 42161); + expect(status.status).toBe('SUCCESS'); + expect(mockGetExecutionReceipts).toHaveBeenCalled(); // SDK should be called as fallback + }); + + it('falls back to SDK when Atlas API returns non-200, non-404 status', async () => { + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 500, + statusText: 'Internal Server Error', + json: async () => ({}), + } as Response); + mockGetExecutionReceipts.mockImplementation(async function* () { + yield mockExecutionReceipt; + }); + + const status = await adapter.getTransferStatus('0xhash', 1, 42161); + expect(status.status).toBe('SUCCESS'); + expect(mockGetExecutionReceipts).toHaveBeenCalled(); // SDK should be called as fallback + }); + + it('returns PENDING when no execution receipts found (SDK fallback)', async () => { + mockGetExecutionReceipts.mockImplementation(async function* () { + // Empty generator - no receipts + }); + const status = await adapter.getTransferStatus('0xhash', 1, 42161); + expect(status.status).toBe('PENDING'); + expect(status.message).toContain('CCIP transfer pending or not yet started'); }); it('returns PENDING on SDK error', async () => { - mockCcipClient.getTransferStatus.mockRejectedValue(new Error('Network error')); + mockGetExecutionReceipts.mockImplementation(async function* () { + throw new Error('Network error'); + }); const status = await adapter.getTransferStatus('0xhash', 1, 42161); expect(status.status).toBe('PENDING'); expect(status.message).toContain('Error checking status'); }); + + it('returns PENDING when no message is found', async () => { + mockGetMessagesInTx.mockResolvedValueOnce([]); + const status = await adapter.getTransferStatus('0xhash', 1, 42161); + expect(status.status).toBe('PENDING'); + expect(status.message).toContain('Could not extract CCIP message ID'); + }); + + it('returns SUCCESS on Solana destination branch', async () => { + const status = await adapter.getTransferStatus('0xhash', 1, SOLANA_CHAIN_ID_NUMBER); + expect(status.status).toBe('SUCCESS'); + }); + + it('retries on rate limit error for Solana and eventually succeeds', async () => { + let callCount = 0; + mockGetExecutionReceipts.mockImplementation(async function* () { + callCount++; + if (callCount === 1) { + throw new Error('Too Many Requests'); + } + yield mockExecutionReceipt; + }); + + const status = await adapter.getTransferStatus('0xhash', 1, SOLANA_CHAIN_ID_NUMBER); + expect(status.status).toBe('SUCCESS'); + expect(callCount).toBe(2); // Should retry once + }); + + it('retries on 429 error for Solana', async () => { + let callCount = 0; + mockGetExecutionReceipts.mockImplementation(async function* () { + callCount++; + if (callCount === 1) { + throw new Error('429 Too Many Requests'); + } + yield mockExecutionReceipt; + }); + + const status = await adapter.getTransferStatus('0xhash', 1, SOLANA_CHAIN_ID_NUMBER); + expect(status.status).toBe('SUCCESS'); + expect(callCount).toBe(2); + }); + + it('retries on rate limit error (case insensitive) for Solana', async () => { + let callCount = 0; + mockGetExecutionReceipts.mockImplementation(async function* () { + callCount++; + if (callCount === 1) { + throw new Error('Rate Limit Exceeded'); + } + yield mockExecutionReceipt; + }); + + const status = await adapter.getTransferStatus('0xhash', 1, SOLANA_CHAIN_ID_NUMBER); + expect(status.status).toBe('SUCCESS'); + expect(callCount).toBe(2); + }); + + it('returns PENDING after max retries exceeded for Solana', async () => { + mockGetExecutionReceipts.mockImplementation(async function* () { + throw new Error('Too Many Requests'); + }); + + const status = await adapter.getTransferStatus('0xhash', 1, SOLANA_CHAIN_ID_NUMBER); + expect(status.status).toBe('PENDING'); + expect(status.message).toContain('Rate limit error after 3 retries'); + }); + + it('does not retry non-rate-limit errors', async () => { + mockGetExecutionReceipts.mockImplementation(async function* () { + throw new Error('Network timeout'); + }); + + const status = await adapter.getTransferStatus('0xhash', 1, SOLANA_CHAIN_ID_NUMBER); + expect(status.status).toBe('PENDING'); + expect(status.message).toContain('Error checking status'); + expect(mockGetExecutionReceipts).toHaveBeenCalledTimes(1); // No retries + }); + + it('returns PENDING when no destination providers', async () => { + const adapterNoDest = new TestableCCIPBridgeAdapter( + { + ...mockChains, + [SOLANA_CHAIN_ID_NUMBER]: { + ...mockChains[SOLANA_CHAIN_ID_NUMBER], + providers: [], + } as any, + }, + mockLogger, + ); + const status = await adapterNoDest.getTransferStatus('0xhash', 1, SOLANA_CHAIN_ID_NUMBER); + expect(status.status).toBe('PENDING'); + expect(status.message).toContain('No providers found for destination chain'); + }); + + it('returns PENDING when no origin providers', async () => { + const adapterNoOrigin = new TestableCCIPBridgeAdapter( + { ...mockChains, '1': { ...(mockChains as any)['1'], providers: [] } }, + mockLogger, + ); + const status = await adapterNoOrigin.getTransferStatus('0xhash', 1, 42161); + expect(status.status).toBe('PENDING'); + expect(status.message).toContain('No providers found for origin chain'); + }); + }); + + describe('getTransferStatusByMessageId', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns SUCCESS when Atlas API returns state 2', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + statusText: 'OK', + json: async () => ({ state: 2, messageId: '0xmsgid' }), + } as Response); + + const status = await adapter.getTransferStatusByMessageId('0xmsgid'); + expect(status).not.toBeNull(); + expect(status?.status).toBe('SUCCESS'); + expect(status?.messageId).toBe('0xmsgid'); + expect(status?.message).toContain('via Atlas API'); + }); + + it('returns FAILURE when Atlas API returns state 3', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + statusText: 'OK', + json: async () => ({ state: 3, messageId: '0xmsgid' }), + } as Response); + + const status = await adapter.getTransferStatusByMessageId('0xmsgid'); + expect(status).not.toBeNull(); + expect(status?.status).toBe('FAILURE'); + expect(status?.message).toContain('via Atlas API'); + }); + + it('returns PENDING when Atlas API returns state 1', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + statusText: 'OK', + json: async () => ({ state: 1, messageId: '0xmsgid' }), + } as Response); + + const status = await adapter.getTransferStatusByMessageId('0xmsgid'); + expect(status).not.toBeNull(); + expect(status?.status).toBe('PENDING'); + expect(status?.message).toContain('pending (state: 1)'); + }); + + it('returns null when Atlas API returns 404', async () => { + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: 'Not Found', + json: async () => ({}), + } as Response); + + const status = await adapter.getTransferStatusByMessageId('0xmsgid'); + expect(status).toBeNull(); + }); + + it('returns null when Atlas API throws error', async () => { + mockFetch.mockRejectedValueOnce(new Error('Network error')); + + const status = await adapter.getTransferStatusByMessageId('0xmsgid'); + expect(status).toBeNull(); + }); + + it('returns null when Atlas API returns non-200 status', async () => { + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 500, + statusText: 'Internal Server Error', + json: async () => ({}), + } as Response); + + const status = await adapter.getTransferStatusByMessageId('0xmsgid'); + expect(status).toBeNull(); + }); }); describe('CCIP constants', () => { diff --git a/packages/adapters/rebalance/tsconfig.json b/packages/adapters/rebalance/tsconfig.json index 4670bfa5..bd45d9dc 100644 --- a/packages/adapters/rebalance/tsconfig.json +++ b/packages/adapters/rebalance/tsconfig.json @@ -4,7 +4,9 @@ "rootDir": "./src", "outDir": "./dist", "baseUrl": ".", + "module": "Node16", "composite": true, + "moduleResolution": "node16", "paths": { "zapatos/schema": ["../database/src/zapatos/zapatos/schema"], "zapatos/db": ["../database/node_modules/zapatos/dist/db"] diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index d4307a36..125a5605 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -441,6 +441,33 @@ export async function loadConfiguration(): Promise { undefined, // Max amount per operation (optional cap) }, }, + solanaPtusdeRebalance: { + enabled: + parseBooleanValue(configJson.solanaPtusdeRebalance?.enabled) ?? + parseBooleanValue(await fromEnv('SOLANA_PTUSDE_REBALANCE_ENABLED', true)) ?? + true, + ptUsdeThreshold: + configJson.solanaPtusdeRebalance?.ptUsdeThreshold ?? + (await fromEnv('SOLANA_PTUSDE_REBALANCE_THRESHOLD', true)) ?? + '100000000000', // 100 ptUSDe (9 decimals on Solana) + ptUsdeTarget: + configJson.solanaPtusdeRebalance?.ptUsdeTarget ?? + (await fromEnv('SOLANA_PTUSDE_REBALANCE_TARGET', true)) ?? + '500000000000', // 500 ptUSDe (9 decimals on Solana) + bridge: { + slippageDbps: + configJson.solanaPtusdeRebalance?.bridge?.slippageDbps ?? + parseInt((await fromEnv('SOLANA_PTUSDE_REBALANCE_BRIDGE_SLIPPAGE_DBPS', true)) ?? '50', 10), // 0.5% default + minRebalanceAmount: + configJson.solanaPtusdeRebalance?.bridge?.minRebalanceAmount ?? + (await fromEnv('SOLANA_PTUSDE_REBALANCE_BRIDGE_MIN_REBALANCE_AMOUNT', true)) ?? + '1000000', // 1 USDC minimum (6 decimals) + maxRebalanceAmount: + configJson.solanaPtusdeRebalance?.bridge?.maxRebalanceAmount ?? + (await fromEnv('SOLANA_PTUSDE_REBALANCE_BRIDGE_MAX_REBALANCE_AMOUNT', true)) ?? + '100000000', // 100 USDC max (6 decimals) + }, + }, redis: configJson.redis ?? { host: await requireEnv('REDIS_HOST'), port: parseInt(await requireEnv('REDIS_PORT')), diff --git a/packages/core/src/types/config.ts b/packages/core/src/types/config.ts index 4a080851..fa494c2a 100644 --- a/packages/core/src/types/config.ts +++ b/packages/core/src/types/config.ts @@ -141,6 +141,23 @@ export interface TokenRebalanceConfig { maxRebalanceAmount?: string; // Max amount per operation (optional cap) }; } + +/** + * Solana USDC/ptUSDe rebalancing configuration. + * Supports threshold-based rebalancing: Solana USDC → Mainnet USDC → ptUSDe → Solana ptUSDe + */ +export interface SolanaRebalanceConfig { + enabled: boolean; + // ptUSDe threshold configuration (balance in 9 decimals - Solana ptUSDe) + ptUsdeThreshold: string; // Min ptUSDe balance that triggers rebalancing (e.g., "100000000000" = 100 ptUSDe) + ptUsdeTarget: string; // Target ptUSDe balance after rebalancing (e.g., "500000000000" = 500 ptUSDe) + // Bridge configuration (matches TAC rebalancer structure) + bridge: { + slippageDbps: number; // Slippage tolerance for Pendle swap (default: 500 = 5%) + minRebalanceAmount: string; // Min USDC amount per operation (6 decimals, e.g., "1000000" = 1 USDC) + maxRebalanceAmount?: string; // Max USDC amount per operation (optional cap) + }; +} export interface RedisConfig { host: string; port: number; @@ -192,6 +209,7 @@ export interface MarkConfiguration extends RebalanceConfig { privateKey?: string; // Solana wallet private key (base58 encoded) rpcUrl?: string; // Solana RPC endpoint (defaults to mainnet-beta) }; + solanaPtusdeRebalance?: SolanaRebalanceConfig; tacRebalance?: TokenRebalanceConfig; methRebalance?: TokenRebalanceConfig; // Mantle bridge configuration diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 520a786d..66d02dc1 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -1,7 +1,22 @@ /** * Serializes an object containing BigInt values by converting them to strings * This is necessary because JSON.stringify() cannot serialize BigInt values + * Also handles circular references by tracking seen objects */ export const serializeBigInt = (obj: unknown): unknown => { - return JSON.parse(JSON.stringify(obj, (_, value) => (typeof value === 'bigint' ? value.toString() : value))); + const seen = new WeakSet(); + return JSON.parse( + JSON.stringify(obj, (_, value) => { + if (typeof value === 'bigint') { + return value.toString(); + } + if (typeof value === 'object' && value !== null) { + if (seen.has(value)) { + return undefined; // Remove circular reference + } + seen.add(value); + } + return value; + }), + ); }; diff --git a/packages/poller/package.json b/packages/poller/package.json index eef89882..39ceb77c 100644 --- a/packages/poller/package.json +++ b/packages/poller/package.json @@ -21,7 +21,6 @@ "test:coverage": "jest --coverage" }, "dependencies": { - "@chainlink/ccip-js": "^0.2.6", "@mark/cache": "workspace:*", "@mark/chainservice": "workspace:*", "@mark/core": "workspace:*", @@ -31,12 +30,16 @@ "@mark/prometheus": "workspace:*", "@mark/rebalance": "workspace:*", "@mark/web3signer": "workspace:*", + "@metaplex-foundation/mpl-token-metadata": "^3.4.0", + "@metaplex-foundation/umi": "^1.4.1", + "@metaplex-foundation/umi-bundle-defaults": "^1.4.1", "@solana/spl-token": "^0.4.9", "@solana/web3.js": "^1.98.0", "aws-lambda": "1.0.7", "bs58": "^6.0.0", "datadog-lambda-js": "10.123.0", "dd-trace": "5.42.0", + "loglevel": "^1.9.2", "tronweb": "6.0.3", "viem": "2.33.3" }, diff --git a/packages/poller/src/dev.ts b/packages/poller/src/dev.ts index ad981168..11c4f407 100644 --- a/packages/poller/src/dev.ts +++ b/packages/poller/src/dev.ts @@ -10,6 +10,7 @@ if (typeof (global as typeof globalThis & { crypto?: Crypto }).crypto === 'undef (global as typeof globalThis & { crypto: Crypto }).crypto = webcrypto as Crypto; } +import './polyfills'; import { initPoller } from './init'; initPoller() diff --git a/packages/poller/src/helpers/contracts.ts b/packages/poller/src/helpers/contracts.ts index f9d61e68..060d5867 100644 --- a/packages/poller/src/helpers/contracts.ts +++ b/packages/poller/src/helpers/contracts.ts @@ -393,7 +393,8 @@ export const getProviderUrls = (chainId: string, config: MarkConfiguration): str // Singleton map for viem clients const viemClients = new Map>(); -export const createClient = (chainId: string, config: MarkConfiguration) => { +// Explicitly annotate return type to avoid viem internal type leakage +export const createClient = (chainId: string, config: MarkConfiguration): ReturnType => { if (viemClients.has(chainId)) { return viemClients.get(chainId)!; } diff --git a/packages/poller/src/index.ts b/packages/poller/src/index.ts index e3e1fb00..eac5850c 100644 --- a/packages/poller/src/index.ts +++ b/packages/poller/src/index.ts @@ -1,3 +1,4 @@ +import './polyfills'; import { Logger } from '@mark/logger'; import { logFileDescriptorUsage, shouldExitForFileDescriptors } from '@mark/core'; import { initPoller } from './init'; diff --git a/packages/poller/src/init.ts b/packages/poller/src/init.ts index 8d676ad4..bb956d36 100644 --- a/packages/poller/src/init.ts +++ b/packages/poller/src/init.ts @@ -126,7 +126,7 @@ function validateSingleTokenRebalanceConfig( if (mm?.address && config.ownAddress && mm.address.toLowerCase() !== config.ownAddress.toLowerCase()) { warnings.push( `${configName} MM address (${mm.address}) differs from ownAddress (${config.ownAddress}). ` + - 'Funds sent to MM may not be usable for intent filling by this Mark instance.', + 'Funds sent to MM may not be usable for intent filling by this Mark instance.', ); } @@ -320,9 +320,6 @@ export const initPoller = async (): Promise<{ statusCode: number; body: string } }; } - // TODO: sanitize sensitive vars - logger.debug('Created config', { config }); - // Validate token rebalance config if enabled (fail fast on misconfiguration) validateTokenRebalanceConfig(config, logger); @@ -342,7 +339,7 @@ export const initPoller = async (): Promise<{ statusCode: number; body: string } await cleanupExpiredEarmarks(context); await cleanupExpiredRegularRebalanceOps(context); - logger.debug('Logging run mode of the instance', { runMode: process.env.RUN_MODE }) + logger.debug('Logging run mode of the instance', { runMode: process.env.RUN_MODE }); if (process.env.RUN_MODE === 'methOnly') { logger.info('Starting meth rebalancing', { diff --git a/packages/poller/src/polyfills.ts b/packages/poller/src/polyfills.ts new file mode 100644 index 00000000..a3f928e9 --- /dev/null +++ b/packages/poller/src/polyfills.ts @@ -0,0 +1,22 @@ +/** + * Runtime polyfills that are safe to load before any dependencies. + * Ensures Array.prototype.toReversed exists for environments missing ES2023 helpers. + */ +declare global { + interface Array { + toReversed(): T[]; + } +} + +if (!Array.prototype.toReversed) { + Object.defineProperty(Array.prototype, 'toReversed', { + value: function toReversed(this: T[]) { + // Return a shallow copy reversed without mutating the original array. + return [...this].reverse(); + }, + writable: true, + configurable: true, + }); +} + +export {}; diff --git a/packages/poller/src/rebalance/mantleEth.ts b/packages/poller/src/rebalance/mantleEth.ts index 9c65d58d..ea93e47b 100644 --- a/packages/poller/src/rebalance/mantleEth.ts +++ b/packages/poller/src/rebalance/mantleEth.ts @@ -923,6 +923,7 @@ export const executeMethCallbacks = async (context: ProcessingContext): Promise< // Get all pending operations from database const { operations } = await db.getRebalanceOperations(undefined, undefined, { status: [RebalanceOperationStatus.PENDING, RebalanceOperationStatus.AWAITING_CALLBACK], + bridge: [SupportedBridge.Mantle, `${SupportedBridge.Across}-mantle`], }); logger.debug(`Found ${operations.length} meth rebalance operations`, { @@ -987,6 +988,7 @@ export const executeMethCallbacks = async (context: ProcessingContext): Promise< logger.warn('Operation is not a mantle bridge', logContext); continue; } + const adapter = rebalance.getAdapter(bridgeType as SupportedBridge); // Get origin transaction hash from JSON field diff --git a/packages/poller/src/rebalance/solanaUsdc.ts b/packages/poller/src/rebalance/solanaUsdc.ts index da7da429..4c76da35 100644 --- a/packages/poller/src/rebalance/solanaUsdc.ts +++ b/packages/poller/src/rebalance/solanaUsdc.ts @@ -1,244 +1,176 @@ import { TransactionReceipt as ViemTransactionReceipt } from 'viem'; -import { convertToNativeUnits } from '../helpers'; +import { safeParseBigInt } from '../helpers'; import { jsonifyError } from '@mark/logger'; import { - getDecimalsFromConfig, RebalanceOperationStatus, RebalanceAction, SupportedBridge, MAINNET_CHAIN_ID, SOLANA_CHAINID, getTokenAddressFromConfig, - EarmarkStatus, WalletType, + SolanaRebalanceConfig, } from '@mark/core'; import { ProcessingContext } from '../init'; -import { PublicKey, TransactionInstruction, SystemProgram, Connection } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID, getAssociatedTokenAddress, getAccount } from '@solana/spl-token'; +import { PublicKey } from '@solana/web3.js'; +import { getAssociatedTokenAddress, getAccount } from '@solana/spl-token'; +import { Wallet } from '@coral-xyz/anchor'; import { SolanaSigner } from '@mark/chainservice'; -import { - createEarmark, - createRebalanceOperation, - Earmark, - getActiveEarmarkForInvoice, - TransactionReceipt, -} from '@mark/database'; -import { IntentStatus } from '@mark/everclear'; -import { submitTransactionWithLogging } from '../helpers/transactions'; -import { RebalanceTransactionMemo, USDC_PTUSDE_PAIRS, CCIPBridgeAdapter } from '@mark/rebalance'; - -// USDC ticker hash - string identifier used for cross-chain asset matching -// This matches the tickerHash field in AssetConfiguration -const USDC_TICKER_HASH = 'USDC'; - -// Minimum rebalancing amount (1 USDC in 6 decimals) -const MIN_REBALANCING_AMOUNT = 1000000n; +import { createRebalanceOperation, TransactionReceipt } from '@mark/database'; +import { submitTransactionWithLogging, TransactionSubmissionResult } from '../helpers/transactions'; +import { RebalanceTransactionMemo, USDC_PTUSDE_PAIRS, CCIPBridgeAdapter, PendleBridgeAdapter } from '@mark/rebalance'; -// Chainlink CCIP constants for Solana -// See: https://docs.chain.link/ccip/directory/mainnet/chain/solana-mainnet -const CCIP_ROUTER_PROGRAM_ID = new PublicKey('Ccip842gzYHhvdDkSyi2YVCoAWPbYJoApMFzSxQroE9C'); -const CCIP_FEE_QUOTER_PROGRAM_ID = new PublicKey('FeeQPGkKDeRV1MgoYfMH6L8o3KeuYjwUZrgn4LRKfjHi'); -const CCIP_RMN_REMOTE_PROGRAM_ID = new PublicKey('RmnXLft1mSEwDgMKu2okYuHkiazxntFFcZFrrcXxYg7'); -const CCIP_LOCK_RELEASE_POOL_PROGRAM_ID = new PublicKey('8eqh8wppT9c5rw4ERqNCffvU6cNFJWff9WmkcYtmGiqC'); -const SOLANA_CHAIN_SELECTOR = '124615329519749607'; -const ETHEREUM_CHAIN_SELECTOR = '5009297550715157269'; -const USDC_SOLANA_MINT = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'); -const PTUSDE_SOLANA_MINT = new PublicKey('PTSg1sXMujX5bgTM88C2PMksHG5w2bqvXJrG9uUdzpA'); -const LINK_TOKEN_MINT = new PublicKey('LinkhB3afbBKb2EQQu7s7umdZceV3wcvAUJhQAfQ23L'); -const WSOL_MINT = new PublicKey('So11111111111111111111111111111111111111112'); +// Ticker hash from chaindata/everclear.json for cross-chain asset matching +const USDC_TICKER_HASH = '0xd6aca1be9729c13d677335161321649cccae6a591554772516700f986f942eaa'; + +// Token decimals on Solana +const PTUSDE_SOLANA_DECIMALS = 9; // PT-sUSDE has 9 decimals on Solana +const USDC_SOLANA_DECIMALS = 6; // USDC has 6 decimals on Solana +const PTUSDE_MAINNET_DECIMALS = 18; // PT-sUSDE has 18 decimals on Mainnet + +// Default operation timeout: 24 hours (in minutes) +const DEFAULT_OPERATION_TTL_MINUTES = 24 * 60; /** - * Derive CCIP Router PDAs - * See: https://docs.chain.link/ccip/api-reference/svm/v1.6.0/router + * Get Solana rebalance configuration from context. + * Config is loaded from environment variables or config file in @mark/core config.ts + * with built-in defaults: + * - SOLANA_PTUSDE_REBALANCE_ENABLED (default: true) + * - SOLANA_PTUSDE_REBALANCE_THRESHOLD (default: 100 ptUSDe = "100000000000") + * - SOLANA_PTUSDE_REBALANCE_TARGET (default: 500 ptUSDe = "500000000000") + * - SOLANA_PTUSDE_REBALANCE_BRIDGE_SLIPPAGE_DBPS (default: 50 = 0.5%) + * - SOLANA_PTUSDE_REBALANCE_BRIDGE_MIN_REBALANCE_AMOUNT (default: "1000000" = 1 USDC) + * - SOLANA_PTUSDE_REBALANCE_BRIDGE_MAX_REBALANCE_AMOUNT (default: "100000000" = 100 USDC) */ -function deriveCCIPRouterPDAs( - destChainSelector: bigint, - userPubkey: PublicKey, -): { - config: PublicKey; - destChainState: PublicKey; - nonce: PublicKey; - feeBillingSigner: PublicKey; -} { - // Config: ["config"] - const [config] = PublicKey.findProgramAddressSync([Buffer.from('config')], CCIP_ROUTER_PROGRAM_ID); - - // Destination Chain State: ["dest_chain_state", destChainSelector (u64 LE)] - const destChainSelectorBuf = Buffer.alloc(8); - destChainSelectorBuf.writeBigUInt64LE(destChainSelector, 0); - const [destChainState] = PublicKey.findProgramAddressSync( - [Buffer.from('dest_chain_state'), destChainSelectorBuf], - CCIP_ROUTER_PROGRAM_ID, - ); - - // Nonce: ["nonce", destChainSelector (u64 LE), userPubkey] - const [nonce] = PublicKey.findProgramAddressSync( - [Buffer.from('nonce'), destChainSelectorBuf, userPubkey.toBytes()], - CCIP_ROUTER_PROGRAM_ID, - ); - - // Fee Billing Signer: ["fee_billing_signer"] - const [feeBillingSigner] = PublicKey.findProgramAddressSync( - [Buffer.from('fee_billing_signer')], - CCIP_ROUTER_PROGRAM_ID, - ); - - return { config, destChainState, nonce, feeBillingSigner }; +function getSolanaRebalanceConfig(config: ProcessingContext['config']): SolanaRebalanceConfig { + if (!config.solanaPtusdeRebalance) { + throw new Error('solanaPtusdeRebalance config not found - this should be provided by @mark/core config loader'); + } + return config.solanaPtusdeRebalance; } /** - * Derive Fee Quoter PDAs + * Check if an operation has exceeded its TTL (time-to-live). + * Operations stuck in PENDING or AWAITING_CALLBACK for too long should be marked as failed. + * + * @param createdAt - Operation creation timestamp + * @param ttlMinutes - TTL in minutes (default: 24 hours) + * @returns true if operation has timed out */ -function deriveFeeQuoterPDAs( - destChainSelector: bigint, - billingTokenMint: PublicKey, - linkTokenMint: PublicKey, -): { - config: PublicKey; - destChain: PublicKey; - billingTokenConfig: PublicKey; - linkTokenConfig: PublicKey; -} { - const destChainSelectorBuf = Buffer.alloc(8); - destChainSelectorBuf.writeBigUInt64LE(destChainSelector, 0); - - // Config: ["config"] - const [config] = PublicKey.findProgramAddressSync([Buffer.from('config')], CCIP_FEE_QUOTER_PROGRAM_ID); - - // Dest Chain: ["dest_chain", destChainSelector (u64 LE)] - const [destChain] = PublicKey.findProgramAddressSync( - [Buffer.from('dest_chain'), destChainSelectorBuf], - CCIP_FEE_QUOTER_PROGRAM_ID, - ); - - // Billing Token Config: ["billing_token_config", tokenMint] - const [billingTokenConfig] = PublicKey.findProgramAddressSync( - [Buffer.from('billing_token_config'), billingTokenMint.toBytes()], - CCIP_FEE_QUOTER_PROGRAM_ID, - ); - - // Link Token Config: ["billing_token_config", linkTokenMint] - const [linkTokenConfig] = PublicKey.findProgramAddressSync( - [Buffer.from('billing_token_config'), linkTokenMint.toBytes()], - CCIP_FEE_QUOTER_PROGRAM_ID, - ); - - return { config, destChain, billingTokenConfig, linkTokenConfig }; +function isOperationTimedOut(createdAt: Date, ttlMinutes: number = DEFAULT_OPERATION_TTL_MINUTES): boolean { + const maxAgeMs = ttlMinutes * 60 * 1000; + const operationAgeMs = Date.now() - createdAt.getTime(); + return operationAgeMs > maxAgeMs; } /** - * Derive RMN Remote PDAs + * Get the expected ptUSDe output for a given USDC input using Pendle API. + * + * @param pendleAdapter - Pendle bridge adapter instance + * @param usdcAmount - USDC amount in 6 decimals + * @param logger - Logger instance + * @returns Expected ptUSDe output in 18 decimals (Mainnet), or null if quote fails */ -function deriveRMNRemotePDAs(): { - curses: PublicKey; - config: PublicKey; -} { - // Curses: ["curses"] - const [curses] = PublicKey.findProgramAddressSync([Buffer.from('curses')], CCIP_RMN_REMOTE_PROGRAM_ID); +async function getPtUsdeOutputForUsdc( + pendleAdapter: PendleBridgeAdapter, + usdcAmount: bigint, + logger: ProcessingContext['logger'], +): Promise { + try { + const tokenPair = USDC_PTUSDE_PAIRS[Number(MAINNET_CHAIN_ID)]; + if (!tokenPair) { + logger.warn('USDC/ptUSDe pair not configured for mainnet'); + return null; + } + + const pendleRoute = { + asset: tokenPair.usdc, + origin: Number(MAINNET_CHAIN_ID), + destination: Number(MAINNET_CHAIN_ID), + swapOutputAsset: tokenPair.ptUSDe, + }; - // Config: ["config"] - const [config] = PublicKey.findProgramAddressSync([Buffer.from('config')], CCIP_RMN_REMOTE_PROGRAM_ID); + // Get quote from Pendle API (returns ptUSDe in 18 decimals) + const ptUsdeOutput = await pendleAdapter.getReceivedAmount(usdcAmount.toString(), pendleRoute); - return { curses, config }; + logger.debug('Pendle API quote received', { + usdcInput: usdcAmount.toString(), + ptUsdeOutput, + route: pendleRoute, + }); + + return BigInt(ptUsdeOutput); + } catch (error) { + logger.warn('Failed to get Pendle quote', { + error: jsonifyError(error), + usdcAmount: usdcAmount.toString(), + }); + return null; + } } /** - * Fetch the Token Pool Lookup Table address from the Token Admin Registry - * The lookup table address is stored in the registry account data - * - * TokenAdminRegistry PDA layout (Anchor/Borsh serialized): - * - discriminator: 8 bytes (Anchor account discriminator) - * - administrator: 32 bytes (Pubkey) - * - pending_administrator: 32 bytes (Pubkey) - * - pool_lookuptable: 32 bytes (Pubkey) + * Calculate required USDC to achieve target ptUSDe balance using Pendle pricing. + * Returns null if Pendle API is unavailable - callers should skip rebalancing in this case. * - * Total offset to pool_lookuptable: 8 + 32 + 32 = 72 bytes - * - * See: - * - https://docs.chain.link/ccip/concepts/cross-chain-token/svm/architecture - * - https://docs.chain.link/ccip/concepts/cross-chain-token/svm/token-pools + * @param ptUsdeShortfall - Required ptUSDe in Solana decimals (9 decimals) + * @param pendleAdapter - Pendle bridge adapter + * @param logger - Logger instance + * @returns Required USDC amount in 6 decimals, or null if Pendle API unavailable */ -async function fetchTokenPoolLookupTable(connection: Connection, tokenMint: PublicKey): Promise { - // Derive Token Admin Registry PDA - const [tokenAdminRegistry] = PublicKey.findProgramAddressSync( - [Buffer.from('token_admin_registry'), tokenMint.toBytes()], - CCIP_ROUTER_PROGRAM_ID, - ); - - // Fetch the account data - const accountInfo = await connection.getAccountInfo(tokenAdminRegistry); - if (!accountInfo || !accountInfo.data) { - throw new Error(`Token Admin Registry not found for mint: ${tokenMint.toBase58()}`); - } - - // Parse the pool_lookuptable address from the account data - // Layout: discriminator (8) + administrator (32) + pending_administrator (32) + pool_lookuptable (32) - const ANCHOR_DISCRIMINATOR_SIZE = 8; - const lookupTableOffset = ANCHOR_DISCRIMINATOR_SIZE + 32 + 32; // = 72 bytes +async function calculateRequiredUsdcForPtUsde( + ptUsdeShortfall: bigint, + pendleAdapter: PendleBridgeAdapter, + logger: ProcessingContext['logger'], +): Promise { + // Convert Solana ptUSDe (9 decimals) to Mainnet ptUSDe (18 decimals) for calculation + const ptUsdeShortfallMainnet = ptUsdeShortfall * BigInt(10 ** (PTUSDE_MAINNET_DECIMALS - PTUSDE_SOLANA_DECIMALS)); + + // Estimate USDC amount using decimal conversion (ptUSDe 18 decimals → USDC 6 decimals) + const estimatedUsdcAmount = ptUsdeShortfallMainnet / BigInt(10 ** (PTUSDE_MAINNET_DECIMALS - USDC_SOLANA_DECIMALS)); + + // Get Pendle quote for the estimated amount to account for actual price impact at this size + const ptUsdeOutput = await getPtUsdeOutputForUsdc(pendleAdapter, estimatedUsdcAmount, logger); + + if (ptUsdeOutput && ptUsdeOutput > 0n) { + // If estimated USDC gives us ptUsdeOutput, we need: (shortfall / ptUsdeOutput) * estimatedUsdc + const requiredUsdc = (ptUsdeShortfallMainnet * estimatedUsdcAmount) / ptUsdeOutput; + + logger.info('Calculated USDC requirement using Pendle API pricing', { + ptUsdeShortfallSolana: ptUsdeShortfall.toString(), + ptUsdeShortfallMainnet: ptUsdeShortfallMainnet.toString(), + estimatedUsdcAmount: estimatedUsdcAmount.toString(), + ptUsdeOutput: ptUsdeOutput.toString(), + requiredUsdc: requiredUsdc.toString(), + effectiveRate: (Number(ptUsdeOutput) / Number(estimatedUsdcAmount) / 1e12).toFixed(6), + }); - const minRequiredSize = lookupTableOffset + 32; - if (accountInfo.data.length < minRequiredSize) { - throw new Error( - `Token Admin Registry data too short: expected at least ${minRequiredSize} bytes, got ${accountInfo.data.length}`, - ); + return requiredUsdc; } - const lookupTableBytes = accountInfo.data.slice(lookupTableOffset, lookupTableOffset + 32); - const poolLookupTable = new PublicKey(lookupTableBytes); - - // Validate the lookup table is not zero/default pubkey - if (poolLookupTable.equals(PublicKey.default)) { - throw new Error(`Token ${tokenMint.toBase58()} is not enabled for CCIP (pool_lookuptable is zero address).`); - } + // Pendle API unavailable - return null to signal failure + logger.error('Pendle API unavailable - cannot calculate USDC requirement, skipping rebalancing', { + ptUsdeShortfall: ptUsdeShortfall.toString(), + ptUsdeShortfallMainnet: ptUsdeShortfallMainnet.toString(), + }); - return poolLookupTable; + return null; } +// Chainlink CCIP constants for Solana +// See: https://docs.chain.link/ccip/directory/mainnet/chain/solana-mainnet +const CCIP_ROUTER_PROGRAM_ID = new PublicKey('Ccip842gzYHhvdDkSyi2YVCoAWPbYJoApMFzSxQroE9C'); +const SOLANA_CHAIN_SELECTOR = '124615329519749607'; +const ETHEREUM_CHAIN_SELECTOR = '5009297550715157269'; +const USDC_SOLANA_MINT = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'); +const PTUSDE_SOLANA_MINT = new PublicKey('PTSg1sXMujX5bgTM88C2PMksHG5w2bqvXJrG9uUdzpA'); + /** - * Derive Token Pool PDAs for CCIP token transfers + * Get or create lookup table for CCIP transaction accounts + * This ensures we can use versioned transactions while preserving account order */ -function deriveTokenPoolPDAs( - destChainSelector: bigint, - tokenMint: PublicKey, - poolProgram: PublicKey, -): { - tokenAdminRegistry: PublicKey; - poolChainConfig: PublicKey; - poolSigner: PublicKey; - routerPoolsSigner: PublicKey; - poolConfig: PublicKey; -} { - const destChainSelectorBuf = Buffer.alloc(8); - destChainSelectorBuf.writeBigUInt64LE(destChainSelector, 0); - - // Token Admin Registry: ["token_admin_registry", tokenMint] from CCIP Router - const [tokenAdminRegistry] = PublicKey.findProgramAddressSync( - [Buffer.from('token_admin_registry'), tokenMint.toBytes()], - CCIP_ROUTER_PROGRAM_ID, - ); - - // Pool Chain Config: ["ccip_tokenpool_chainconfig", destChainSelector, tokenMint] from Pool - const [poolChainConfig] = PublicKey.findProgramAddressSync( - [Buffer.from('ccip_tokenpool_chainconfig'), destChainSelectorBuf, tokenMint.toBytes()], - poolProgram, - ); - - // Pool Signer: ["ccip_tokenpool_signer"] from Pool - const [poolSigner] = PublicKey.findProgramAddressSync([Buffer.from('ccip_tokenpool_signer')], poolProgram); - - // Pool Config: ["ccip_tokenpool_config"] from Pool - const [poolConfig] = PublicKey.findProgramAddressSync([Buffer.from('ccip_tokenpool_config')], poolProgram); - - // CCIP Router Pools Signer: ["external_token_pools_signer", poolProgram] from CCIP Router - const [routerPoolsSigner] = PublicKey.findProgramAddressSync( - [Buffer.from('external_token_pools_signer'), poolProgram.toBytes()], - CCIP_ROUTER_PROGRAM_ID, - ); - - return { tokenAdminRegistry, poolChainConfig, poolSigner, routerPoolsSigner, poolConfig }; -} -type ExecuteBridgeContext = Pick; +type ExecuteBridgeContext = Pick; interface SolanaToMainnetBridgeParams { context: ExecuteBridgeContext; @@ -255,126 +187,7 @@ interface SolanaToMainnetBridgeParams { interface SolanaToMainnetBridgeResult { receipt?: TransactionReceipt; effectiveBridgedAmount: string; -} - -/** - * SVM2AnyMessage structure for CCIP Solana to EVM transfers - * See: https://docs.chain.link/ccip/architecture#svm2any-messages - * - * IMPORTANT: The actual CCIP Solana SDK instruction format may differ. - * This implementation is based on available documentation and may need - * updates when the official @chainlink/ccip-solana-sdk is released. - */ -interface SVM2AnyMessage { - receiver: Uint8Array; // EVM address padded to 32 bytes - data: Uint8Array; // Empty for token-only transfers - tokenAmounts: Array<{ - token: Uint8Array; // SPL token mint address (32 bytes) - amount: bigint; // Amount in base units - }>; - feeToken: Uint8Array; // PublicKey.default for native SOL payment - extraArgs: Uint8Array; // CCIP execution parameters (gas limit, etc.) -} - -/** - * Encode an EVM address as 32-byte receiver for CCIP - */ -function encodeEvmReceiverForCCIP(evmAddress: string): Uint8Array { - // Remove 0x prefix and convert to bytes - const addressBytes = Buffer.from(evmAddress.slice(2), 'hex'); - if (addressBytes.length !== 20) { - throw new Error(`Invalid EVM address format: ${evmAddress}`); - } - // Pad to 32 bytes (left-padded with zeros) - const padded = Buffer.alloc(32); - addressBytes.copy(padded, 12); // Copy to last 20 bytes - return padded; -} - -/** - * Build CCIP EVMExtraArgsV2 for EVM destination (Borsh serialized) - * See: https://docs.chain.link/ccip/api-reference/svm/v1.6.0/messages#evmextraargsv2 - * - * Format: - * - Tag: 4 bytes big-endian (0x181dcf10) - * - gas_limit: u128 (16 bytes little-endian, Borsh) - * - allow_out_of_order_execution: bool (1 byte) - */ -function buildEVMExtraArgsV2(gasLimit: number = 0, allowOutOfOrderExecution: boolean = true): Uint8Array { - // EVM_EXTRA_ARGS_V2_TAG: 0x181dcf10 (4 bytes, big-endian) - const typeTag = Buffer.alloc(4); - typeTag.writeUInt32BE(0x181dcf10, 0); - - // gas_limit: u128 little-endian (16 bytes) - Borsh format - // For token-only transfers, gas_limit MUST be 0 - const gasLimitBuf = Buffer.alloc(16); - const gasLimitBigInt = BigInt(gasLimit); - gasLimitBuf.writeBigUInt64LE(gasLimitBigInt & BigInt('0xFFFFFFFFFFFFFFFF'), 0); - gasLimitBuf.writeBigUInt64LE(gasLimitBigInt >> BigInt(64), 8); - - // allow_out_of_order_execution: bool (1 byte) - // MUST be true when sending from Solana - const oooBuf = Buffer.alloc(1); - oooBuf.writeUInt8(allowOutOfOrderExecution ? 1 : 0, 0); - - return Buffer.concat([typeTag, gasLimitBuf, oooBuf]); -} - -/** - * Build CCIP send instruction data using Borsh-like serialization - * - * NOTE: This is a placeholder implementation. The actual serialization - * format should match the CCIP Solana program's expected format. - * When Chainlink releases the official SDK, this should be replaced. - */ -function buildCCIPInstructionData(message: SVM2AnyMessage, destChainSelector: bigint): Buffer { - // Instruction discriminator (placeholder - needs to match actual program) - const CCIP_SEND_DISCRIMINATOR = Buffer.from([0x01]); // Placeholder - - // Serialize destination chain selector (8 bytes, little-endian) - const selectorBuffer = Buffer.alloc(8); - selectorBuffer.writeBigUInt64LE(destChainSelector, 0); - - // Serialize receiver (32 bytes) - const receiverBuffer = Buffer.from(message.receiver); - - // Serialize data length + data - const dataLenBuffer = Buffer.alloc(4); - dataLenBuffer.writeUInt32LE(message.data.length, 0); - const dataBuffer = Buffer.from(message.data); - - // Serialize token amounts array - const tokenCountBuffer = Buffer.alloc(4); - tokenCountBuffer.writeUInt32LE(message.tokenAmounts.length, 0); - - const tokenBuffers: Buffer[] = []; - for (const tokenAmount of message.tokenAmounts) { - const tokenBuf = Buffer.from(tokenAmount.token); - const amountBuf = Buffer.alloc(8); - amountBuf.writeBigUInt64LE(tokenAmount.amount, 0); - tokenBuffers.push(Buffer.concat([tokenBuf, amountBuf])); - } - - // Serialize extra args - const extraArgsLenBuffer = Buffer.alloc(4); - extraArgsLenBuffer.writeUInt32LE(message.extraArgs.length, 0); - const extraArgsBuffer = Buffer.from(message.extraArgs); - - // Serialize fee token (32 bytes) - const feeTokenBuffer = Buffer.from(message.feeToken); - - return Buffer.concat([ - CCIP_SEND_DISCRIMINATOR, - selectorBuffer, - receiverBuffer, - dataLenBuffer, - dataBuffer, - tokenCountBuffer, - ...tokenBuffers, - extraArgsLenBuffer, - extraArgsBuffer, - feeTokenBuffer, - ]); + messageId?: string; // CCIP message ID for tracking cross-chain transfers } /** @@ -418,6 +231,8 @@ async function executeSolanaToMainnetBridge({ // Get associated token accounts const sourceTokenAccount = await getAssociatedTokenAddress(USDC_SOLANA_MINT, walletPublicKey); + logger.info('Checking source token', { requestId, tokenAccount: sourceTokenAccount, walletPublicKey }); + // Verify USDC balance try { const tokenAccountInfo = await getAccount(connection, sourceTokenAccount); @@ -440,167 +255,31 @@ async function executeSolanaToMainnetBridge({ throw error; } - // Build CCIP message - const ccipMessage: SVM2AnyMessage = { - receiver: encodeEvmReceiverForCCIP(recipientAddress), - data: new Uint8Array(0), // No additional data for token transfer - tokenAmounts: [ - { - token: USDC_SOLANA_MINT.toBytes(), - amount: amountToBridge, - }, - ], - feeToken: PublicKey.default.toBytes(), // Pay with native SOL - extraArgs: buildEVMExtraArgsV2(0, true), // gasLimit=0 for token-only, OOO=true required for Solana - }; - logger.info('CCIP message prepared', { requestId, destinationChain: ETHEREUM_CHAIN_SELECTOR, tokenAmount: amountToBridge.toString(), recipient: recipientAddress, - receiverHex: Buffer.from(ccipMessage.receiver).toString('hex'), - }); - - // Build instruction data - const instructionData = buildCCIPInstructionData(ccipMessage, BigInt(ETHEREUM_CHAIN_SELECTOR)); - - // Derive all required PDAs for CCIP send instruction - // See: https://docs.chain.link/ccip/tutorials/svm/source/token-transfers - const destChainSelector = BigInt(ETHEREUM_CHAIN_SELECTOR); - - // Core Router PDAs - const routerPDAs = deriveCCIPRouterPDAs(destChainSelector, walletPublicKey); - - // Fee Quoter PDAs - using WSOL as fee token since we pay in native SOL - const feeQuoterPDAs = deriveFeeQuoterPDAs(destChainSelector, WSOL_MINT, LINK_TOKEN_MINT); - - // RMN Remote PDAs - const rmnPDAs = deriveRMNRemotePDAs(); - - // Token Pool PDAs for USDC (using LockRelease pool) - const tokenPoolPDAs = deriveTokenPoolPDAs(destChainSelector, USDC_SOLANA_MINT, CCIP_LOCK_RELEASE_POOL_PROGRAM_ID); - - // Fetch the Token Pool Lookup Table address from Token Admin Registry - const tokenPoolLookupTable = await fetchTokenPoolLookupTable(connection, USDC_SOLANA_MINT); - - logger.debug('Fetched Token Pool Lookup Table', { - requestId, - tokenMint: USDC_SOLANA_MINT.toBase58(), - lookupTable: tokenPoolLookupTable.toBase58(), }); - // Get pool's token account for USDC (where locked tokens go) - const poolTokenAccount = await getAssociatedTokenAddress(USDC_SOLANA_MINT, tokenPoolPDAs.poolSigner, true); - - // Fee receiver account - derived from fee billing signer - const [feeReceiver] = PublicKey.findProgramAddressSync( - [Buffer.from('fee_receiver'), WSOL_MINT.toBytes()], - CCIP_ROUTER_PROGRAM_ID, + const ccipAdapter = context.rebalance.getAdapter(SupportedBridge.CCIP) as CCIPBridgeAdapter; + const ccipTx = await ccipAdapter.sendSolanaToMainnet( + walletPublicKey.toBase58(), + recipientAddress, + amountToBridge.toString(), + connection, + new Wallet(solanaSigner.getKeypair()), + route, ); - logger.debug('CCIP PDAs derived', { - requestId, - routerConfig: routerPDAs.config.toBase58(), - destChainState: routerPDAs.destChainState.toBase58(), - nonce: routerPDAs.nonce.toBase58(), - tokenAdminRegistry: tokenPoolPDAs.tokenAdminRegistry.toBase58(), - poolChainConfig: tokenPoolPDAs.poolChainConfig.toBase58(), - }); - - // Create CCIP send instruction with all required accounts - // See: https://docs.chain.link/ccip/tutorials/svm/source/token-transfers#account-requirements - const ccipSendInstruction = new TransactionInstruction({ - keys: [ - // === Core Accounts (indices 0-4) === - { pubkey: routerPDAs.config, isSigner: false, isWritable: false }, // 0: Config PDA - { pubkey: routerPDAs.destChainState, isSigner: false, isWritable: true }, // 1: Destination Chain State (writable) - { pubkey: routerPDAs.nonce, isSigner: false, isWritable: true }, // 2: Nonce (writable) - { pubkey: walletPublicKey, isSigner: true, isWritable: true }, // 3: Authority/Signer (writable, signer) - { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, // 4: System Program - - // === Fee Payment Accounts (indices 5-9) === - { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, // 5: Fee Token Program - { pubkey: WSOL_MINT, isSigner: false, isWritable: false }, // 6: Fee Token Mint (WSOL for internal accounting) - { pubkey: PublicKey.default, isSigner: false, isWritable: false }, // 7: User's Fee Token Account - { pubkey: feeReceiver, isSigner: false, isWritable: true }, // 8: Fee Receiver (writable) - { pubkey: routerPDAs.feeBillingSigner, isSigner: false, isWritable: false }, // 9: Fee Billing Signer PDA - - // === Fee Quoter Accounts (indices 10-14) === - { pubkey: CCIP_FEE_QUOTER_PROGRAM_ID, isSigner: false, isWritable: false }, // 10: Fee Quoter Program - { pubkey: feeQuoterPDAs.config, isSigner: false, isWritable: false }, // 11: Fee Quoter Config - { pubkey: feeQuoterPDAs.destChain, isSigner: false, isWritable: false }, // 12: Fee Quoter Dest Chain - { pubkey: feeQuoterPDAs.billingTokenConfig, isSigner: false, isWritable: false }, // 13: Fee Quoter Billing Token Config - { pubkey: feeQuoterPDAs.linkTokenConfig, isSigner: false, isWritable: false }, // 14: Fee Quoter Link Token Config - - // === RMN Remote Accounts (indices 15-17) === - { pubkey: CCIP_RMN_REMOTE_PROGRAM_ID, isSigner: false, isWritable: false }, // 15: RMN Remote Program - { pubkey: rmnPDAs.curses, isSigner: false, isWritable: false }, // 16: RMN Remote Curses - { pubkey: rmnPDAs.config, isSigner: false, isWritable: false }, // 17: RMN Remote Config - - // === Token Transfer Accounts (for USDC) === - // Per CCIP API Reference, token accounts must be in remaining_accounts with this structure: - // See: https://docs.chain.link/ccip/api-reference/svm/v1.6.0/router - { pubkey: sourceTokenAccount, isSigner: false, isWritable: true }, // 18: User Token Account (writable) - { pubkey: feeQuoterPDAs.billingTokenConfig, isSigner: false, isWritable: false }, // 19: Token Billing Config (USDC) - { pubkey: tokenPoolPDAs.poolChainConfig, isSigner: false, isWritable: true }, // 20: Pool Chain Config (writable) - { pubkey: tokenPoolLookupTable, isSigner: false, isWritable: false }, // 21: Token Pool Lookup Table - { pubkey: tokenPoolPDAs.tokenAdminRegistry, isSigner: false, isWritable: false }, // 22: Token Admin Registry - { pubkey: CCIP_LOCK_RELEASE_POOL_PROGRAM_ID, isSigner: false, isWritable: false }, // 23: Pool Program - { pubkey: tokenPoolPDAs.poolConfig, isSigner: false, isWritable: false }, // 24: Pool Config - { pubkey: poolTokenAccount, isSigner: false, isWritable: true }, // 25: Pool Token Account (writable) - { pubkey: tokenPoolPDAs.poolSigner, isSigner: false, isWritable: false }, // 26: Pool Signer - { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, // 27: Token Program - { pubkey: USDC_SOLANA_MINT, isSigner: false, isWritable: false }, // 28: Token Mint - { pubkey: feeQuoterPDAs.billingTokenConfig, isSigner: false, isWritable: false }, // 29: Fee Token Config (for USDC billing) - { pubkey: tokenPoolPDAs.routerPoolsSigner, isSigner: false, isWritable: false }, // 30: CCIP Router Pools Signer - ], - programId: CCIP_ROUTER_PROGRAM_ID, - data: instructionData, - }); - - logger.info('CCIP instruction built with full account list', { - requestId, - totalAccounts: ccipSendInstruction.keys.length, - instructionDataLength: instructionData.length, - }); - - logger.info('Sending CCIP transaction to Solana via SolanaSigner', { - requestId, - transaction: { - feePayer: walletPublicKey.toBase58(), - instructionDataLength: instructionData.length, - }, - }); - - // Use SolanaSigner to sign and send transaction with built-in retry logic - const result = await solanaSigner.signAndSendTransaction({ - instructions: [ccipSendInstruction], - computeUnitPrice: 50000, // Priority fee for faster inclusion - computeUnitLimit: 200000, // Compute units for CCIP instruction - }); - - if (!result.success) { - throw new Error(`Solana transaction failed: ${result.error || 'Unknown error'}`); - } - - logger.info('CCIP bridge transaction successful', { - requestId, - signature: result.signature, - slot: result.slot, - amountBridged: amountToBridge.toString(), - recipient: recipientAddress, - fee: result.fee, - logs: result.logs, - }); - // Create transaction receipt const receipt: TransactionReceipt = { - transactionHash: result.signature, - status: result.success ? 1 : 0, - blockNumber: result.slot, - logs: result.logs, - cumulativeGasUsed: result.fee.toString(), + transactionHash: ccipTx.hash, + status: 1, // Success if we got here + blockNumber: ccipTx.blockNumber, // Will be filled in later when we get transaction details + // ccipTx.logs can be readonly; clone to a mutable array to satisfy TransactionReceipt + logs: [...(ccipTx.logs ?? [])] as unknown[], + cumulativeGasUsed: '0', // Will be filled in later effectiveGasPrice: '0', from: walletPublicKey.toBase58(), to: CCIP_ROUTER_PROGRAM_ID.toBase58(), @@ -623,15 +302,21 @@ async function executeSolanaToMainnetBridge({ } export async function rebalanceSolanaUsdc(context: ProcessingContext): Promise { - const { logger, requestId, config, chainService, rebalance, everclear, solanaSigner } = context; + const { logger, requestId, config, chainService, rebalance, solanaSigner } = context; const rebalanceOperations: RebalanceAction[] = []; + logger.debug('Solana rebalancing initialized', { + requestId, + solanaConfigured: !!config.solana, + signerConfigured: !!solanaSigner, + }); + // Check if SolanaSigner is available if (!solanaSigner) { logger.warn('SolanaSigner not configured - Solana USDC rebalancing is disabled', { requestId, reason: 'Missing solana.privateKey in configuration', - action: 'Configure SOLANA_PRIVATE_KEY in SSM Parameter Store', + action: 'Configure SOLANA_PRIVATE_KEY in SSM Parameter Store to enable', }); return rebalanceOperations; } @@ -645,6 +330,13 @@ export async function rebalanceSolanaUsdc(context: ProcessingContext): Promise= ptUsdeThreshold) { + logger.info('ptUSDe balance is above threshold, no rebalancing needed', { + requestId, + ptUsdeBalance: solanaPtUsdeBalance.toString(), + ptUsdeThreshold: ptUsdeThreshold.toString(), + }); + return rebalanceOperations; + } + + // Calculate how much USDC to bridge based on ptUSDe deficit and available Solana USDC + const ptUsdeShortfall = ptUsdeTarget - solanaPtUsdeBalance; - if (!intent.hub_settlement_domain) { - logger.warn('Intent does not have a hub settlement domain, skipping', { requestId, intent }); - continue; - } + // Get Pendle adapter for accurate pricing + const pendleAdapter = context.rebalance.getAdapter(SupportedBridge.Pendle) as PendleBridgeAdapter; - if (intent.destinations.length !== 1 || intent.destinations[0] !== SOLANA_CHAINID) { - logger.warn('Intent does not have exactly one destination - Solana, skipping', { requestId, intent }); - continue; - } + // Calculate required USDC using Pendle API pricing + const usdcNeeded = await calculateRequiredUsdcForPtUsde(ptUsdeShortfall, pendleAdapter, logger); - // Check if an active earmark already exists for this intent - const existingActive = await getActiveEarmarkForInvoice(intent.intent_id); - if (existingActive) { - logger.warn('Active earmark already exists for intent, skipping rebalance operations', { - requestId, - invoiceId: intent.intent_id, - existingEarmarkId: existingActive.id, - existingStatus: existingActive.status, - }); - continue; - } + // If Pendle API is unavailable, skip rebalancing + if (usdcNeeded === null) { + logger.error('Skipping rebalancing due to Pendle API unavailability', { + requestId, + ptUsdeShortfall: ptUsdeShortfall.toString(), + reason: 'Cannot determine accurate USDC requirement without Pendle API', + }); + return rebalanceOperations; + } - const origin = Number(intent.hub_settlement_domain); - const destination = SOLANA_CHAINID; + // Calculate amount to bridge: min(shortfall, available balance, max per operation) + let amountToBridge = usdcNeeded; + if (amountToBridge > solanaUsdcBalance) { + amountToBridge = solanaUsdcBalance; + } + if (maxRebalanceAmount && maxRebalanceAmount > 0n && amountToBridge > maxRebalanceAmount) { + amountToBridge = maxRebalanceAmount; + } - // USDC intent should be settled with USDC address on settlement domain - const ticker = USDC_TICKER_HASH; - const decimals = getDecimalsFromConfig(ticker, origin.toString(), config); + // Check minimum rebalancing amount from config + if (amountToBridge < minRebalanceAmount) { + logger.warn('Calculated bridge amount is below minimum threshold, skipping rebalancing', { + requestId, + calculatedAmount: amountToBridge.toString(), + calculatedAmountFormatted: (Number(amountToBridge) / 10 ** USDC_SOLANA_DECIMALS).toFixed(6), + minAmount: minRebalanceAmount.toString(), + minAmountFormatted: (Number(minRebalanceAmount) / 10 ** USDC_SOLANA_DECIMALS).toFixed(6), + reason: 'Calculated bridge amount too small to be effective', + }); + return rebalanceOperations; + } - // Convert min amount and intent amount from standardized decimals to asset's native decimals - const minAmount = convertToNativeUnits(BigInt(MIN_REBALANCING_AMOUNT), decimals); - const intentAmount = convertToNativeUnits(BigInt(intent.amount_out_min), decimals); - if (intentAmount < minAmount) { - logger.warn('Intent amount is less than min rebalancing amount, skipping', { - requestId, - intent, - intentAmount: intentAmount.toString(), - minAmount: minAmount.toString(), - }); - continue; - } + logger.info('Calculated bridge amount based on ptUSDe deficit and available balance', { + requestId, + balanceChecks: { + ptUsdeShortfall: ptUsdeShortfall.toString(), + ptUsdeShortfallFormatted: (Number(ptUsdeShortfall) / 10 ** PTUSDE_SOLANA_DECIMALS).toFixed(6), + usdcNeeded: usdcNeeded.toString(), + usdcNeededFormatted: (Number(usdcNeeded) / 10 ** USDC_SOLANA_DECIMALS).toFixed(6), + availableSolanaUsdc: solanaUsdcBalance.toString(), + availableSolanaUsdcFormatted: (Number(solanaUsdcBalance) / 10 ** USDC_SOLANA_DECIMALS).toFixed(6), + maxRebalanceAmount: maxRebalanceAmount?.toString() ?? 'unlimited', + maxRebalanceAmountFormatted: maxRebalanceAmount + ? (Number(maxRebalanceAmount) / 10 ** USDC_SOLANA_DECIMALS).toFixed(6) + : 'unlimited', + }, + bridgeDecision: { + finalAmountToBridge: amountToBridge.toString(), + finalAmountToBridgeFormatted: (Number(amountToBridge) / 10 ** USDC_SOLANA_DECIMALS).toFixed(6), + isPartialBridge: solanaUsdcBalance < usdcNeeded, + utilizationPercentage: ((Number(amountToBridge) / Number(solanaUsdcBalance)) * 100).toFixed(2) + '%', + }, + }); - // Check if ptUSDe balance is below threshold to trigger rebalancing - // The logic is: if ptUSDe is low, we bridge USDC from Solana to eventually get more ptUSDe - const ptUsdeBalance = solanaPtUsdeBalance; // Use direct Solana ptUSDe balance - const ptUsdeThresholdEnv = process.env[`PTUSDE_${SOLANA_CHAINID}_THRESHOLD`]; - const ptUsdeThreshold = ptUsdeThresholdEnv - ? BigInt(ptUsdeThresholdEnv) - : convertToNativeUnits(MIN_REBALANCING_AMOUNT * 10n, 18); // ptUSDe has 18 decimals, use 10x threshold as fallback + // Check for in-flight operations to prevent overlapping rebalances + const { operations: inFlightSolanaOps } = await context.database.getRebalanceOperations(undefined, undefined, { + status: [RebalanceOperationStatus.PENDING, RebalanceOperationStatus.AWAITING_CALLBACK], + chainId: Number(SOLANA_CHAINID), + bridge: 'ccip-solana-mainnet', + }); - logger.info('Checking ptUSDe balance threshold for rebalancing decision', { + if (inFlightSolanaOps.length > 0) { + logger.info('In-flight Solana rebalance operations exist, skipping new rebalance to prevent overlap', { requestId, - intentId: intent.intent_id, - ptUsdeBalance: ptUsdeBalance.toString(), - ptUsdeBalanceFormatted: (Number(ptUsdeBalance) / 1e18).toFixed(6), - ptUsdeThreshold: ptUsdeThreshold.toString(), - ptUsdeThresholdFormatted: (Number(ptUsdeThreshold) / 1e18).toFixed(6), - shouldTriggerRebalance: ptUsdeBalance < ptUsdeThreshold, - availableSolanaUsdc: solanaUsdcBalance.toString(), - availableSolanaUsdcFormatted: (Number(solanaUsdcBalance) / 1_000_000).toFixed(6), + inFlightCount: inFlightSolanaOps.length, + inFlightOperationIds: inFlightSolanaOps.map((op) => op.id), }); + return rebalanceOperations; + } - if (ptUsdeBalance >= ptUsdeThreshold) { - logger.info('ptUSDe balance is above threshold, no rebalancing needed', { - requestId, - intentId: intent.intent_id, - ptUsdeBalance: ptUsdeBalance.toString(), - ptUsdeThreshold: ptUsdeThreshold.toString(), - }); - continue; - } - - // Calculate how much USDC to bridge based on ptUSDe deficit and available Solana USDC - const ptUsdeDeficit = ptUsdeThreshold - ptUsdeBalance; - // Approximate 1:1 ratio between USDC and ptUSDe for initial calculation - const usdcNeeded = convertToNativeUnits(ptUsdeDeficit, 6); // Convert to USDC decimals (6) - const currentBalance = solanaUsdcBalance; + // Prepare route for Solana to Mainnet bridge + const solanaToMainnetRoute = { + origin: Number(SOLANA_CHAINID), + destination: Number(MAINNET_CHAIN_ID), + asset: USDC_SOLANA_MINT.toString(), + }; - if (currentBalance <= minAmount) { - logger.warn('Solana USDC balance is below minimum rebalancing threshold, skipping intent', { - requestId, - intentId: intent.intent_id, - currentBalance: currentBalance.toString(), - currentBalanceFormatted: (Number(currentBalance) / 1_000_000).toFixed(6), - minAmount: minAmount.toString(), - minAmountFormatted: (Number(minAmount) / 1_000_000).toFixed(6), - reason: 'Insufficient balance for rebalancing', - }); - continue; - } + logger.info('Starting Leg 1: Solana to Mainnet CCIP bridge (threshold-based)', { + requestId, + route: solanaToMainnetRoute, + amountToBridge: amountToBridge.toString(), + amountToBridgeInUsdc: (Number(amountToBridge) / 10 ** USDC_SOLANA_DECIMALS).toFixed(6), + recipientAddress: config.ownAddress, + trigger: 'threshold-based', + ptUsdeBalance: solanaPtUsdeBalance.toString(), + ptUsdeThreshold: ptUsdeThreshold.toString(), + }); - // Check if we have enough USDC to meaningfully address the ptUSDe deficit - if (currentBalance < usdcNeeded) { - logger.warn('Solana USDC balance is insufficient to fully cover ptUSDe deficit', { - requestId, - intentId: intent.intent_id, - currentBalance: currentBalance.toString(), - currentBalanceFormatted: (Number(currentBalance) / 1_000_000).toFixed(6), - usdcNeeded: usdcNeeded.toString(), - usdcNeededFormatted: (Number(usdcNeeded) / 1_000_000).toFixed(6), - shortfall: (usdcNeeded - currentBalance).toString(), - shortfallFormatted: (Number(usdcNeeded - currentBalance) / 1_000_000).toFixed(6), - decision: 'Will bridge all available USDC (partial rebalancing)', - }); + try { + // Pre-flight checks + if (!config.ownAddress) { + throw new Error('Recipient address (config.ownAddress) not configured'); } - // Calculate amount to bridge based on ptUSDe deficit and available Solana USDC - // Bridge the minimum of: what we need, what we have available, and the intent amount - const amountToBridge = - currentBalance < usdcNeeded - ? currentBalance // Bridge all available if insufficient - : usdcNeeded < intentAmount - ? usdcNeeded - : intentAmount; // Otherwise bridge what's needed or intent amount - - // Final validation - ensure we're bridging a meaningful amount - if (amountToBridge < minAmount) { - logger.warn('Calculated bridge amount is below minimum threshold, skipping intent', { - requestId, - intentId: intent.intent_id, - calculatedAmount: amountToBridge.toString(), - calculatedAmountFormatted: (Number(amountToBridge) / 1_000_000).toFixed(6), - minAmount: minAmount.toString(), - minAmountFormatted: (Number(minAmount) / 1_000_000).toFixed(6), - reason: 'Calculated bridge amount too small to be effective', - }); - continue; + // Validate balance + if (solanaUsdcBalance < amountToBridge) { + throw new Error( + `Insufficient Solana USDC balance. Required: ${amountToBridge.toString()}, Available: ${solanaUsdcBalance.toString()}`, + ); } - logger.info('Calculated bridge amount based on ptUSDe deficit and available balance', { + logger.info('Performing pre-bridge validation checks', { requestId, - intentId: intent.intent_id, - balanceChecks: { - ptUsdeDeficit: ptUsdeDeficit.toString(), - usdcNeeded: usdcNeeded.toString(), - usdcNeededFormatted: (Number(usdcNeeded) / 1_000_000).toFixed(6), - availableSolanaUsdc: currentBalance.toString(), - availableSolanaUsdcFormatted: (Number(currentBalance) / 1_000_000).toFixed(6), - hasSufficientBalance: currentBalance >= usdcNeeded, - intentAmount: intentAmount.toString(), - intentAmountFormatted: (Number(intentAmount) / 1_000_000).toFixed(6), - }, - bridgeDecision: { - finalAmountToBridge: amountToBridge.toString(), - finalAmountToBridgeFormatted: (Number(amountToBridge) / 1_000_000).toFixed(6), - isPartialBridge: currentBalance < usdcNeeded, - utilizationPercentage: ((Number(amountToBridge) / Number(currentBalance)) * 100).toFixed(2) + '%', + trigger: 'threshold-based', + checks: { + solanaUsdcBalance: solanaUsdcBalance.toString(), + solanaUsdcBalanceFormatted: (Number(solanaUsdcBalance) / 10 ** USDC_SOLANA_DECIMALS).toFixed(6), + amountToBridge: amountToBridge.toString(), + amountToBridgeFormatted: (Number(amountToBridge) / 10 ** USDC_SOLANA_DECIMALS).toFixed(6), + hasSufficientBalance: solanaUsdcBalance >= amountToBridge, + recipientValid: !!config.ownAddress, + recipient: config.ownAddress, }, }); - let earmark: Earmark; - try { - earmark = await createEarmark({ - invoiceId: intent.intent_id, - designatedPurchaseChain: Number(destination), - tickerHash: ticker, - minAmount: amountToBridge.toString(), - status: EarmarkStatus.PENDING, - }); - } catch (error: unknown) { - logger.error('Failed to create earmark for intent', { - requestId, - intent, - error: jsonifyError(error), - }); - throw error; - } - - logger.info('Created earmark for intent', { - requestId, - earmarkId: earmark.id, - invoiceId: intent.intent_id, + // Execute Leg 1: Solana to Mainnet bridge + const bridgeResult = await executeSolanaToMainnetBridge({ + context: { requestId, logger, config, chainService, rebalance: context.rebalance }, + solanaSigner, + route: solanaToMainnetRoute, + amountToBridge, + recipientAddress: config.ownAddress, }); - let rebalanceSuccessful = false; - - // Prepare route for Solana to Mainnet bridge - const solanaToMainnetRoute = { - origin: Number(SOLANA_CHAINID), - destination: Number(MAINNET_CHAIN_ID), - asset: USDC_SOLANA_MINT.toString(), - }; + if (!bridgeResult.receipt || bridgeResult.receipt.status !== 1) { + throw new Error(`Bridge transaction failed: ${bridgeResult.receipt?.transactionHash || 'Unknown transaction'}`); + } - logger.info('Starting Leg 1: Solana to Mainnet CCIP bridge', { + logger.info('Leg 1 bridge completed successfully', { requestId, - intentId: intent.intent_id, - earmarkId: earmark.id, - route: solanaToMainnetRoute, - amountToBridge: amountToBridge.toString(), - amountToBridgeInUsdc: (Number(amountToBridge) / 1_000_000).toFixed(6), - recipientAddress: config.ownAddress, + transactionHash: bridgeResult.receipt.transactionHash, + effectiveAmount: bridgeResult.effectiveBridgedAmount, + blockNumber: bridgeResult.receipt.blockNumber, + solanaSlot: bridgeResult.receipt.blockNumber, }); + // Create rebalance operation record for tracking all 3 legs (no earmark for threshold-based) try { - // Pre-flight checks - logger.info('Performing pre-bridge validation checks', { - requestId, - intentId: intent.intent_id, - checks: { - solanaBalance: currentBalance.toString(), - requiredAmount: amountToBridge.toString(), - hasSufficientBalance: currentBalance >= amountToBridge, - recipientValid: !!config.ownAddress, - }, - }); - - if (currentBalance < amountToBridge) { - throw new Error(`Insufficient Solana USDC balance. Required: ${amountToBridge}, Available: ${currentBalance}`); - } - - if (!config.ownAddress) { - throw new Error('Recipient address (config.ownAddress) not configured'); - } - - // Execute Leg 1: Solana to Mainnet bridge - const bridgeResult = await executeSolanaToMainnetBridge({ - context: { requestId, logger, config, chainService }, - solanaSigner, - route: solanaToMainnetRoute, - amountToBridge, - recipientAddress: config.ownAddress, // needs to go on solver + await createRebalanceOperation({ + earmarkId: null, // No earmark for threshold-based rebalancing + originChainId: Number(SOLANA_CHAINID), + destinationChainId: Number(MAINNET_CHAIN_ID), + tickerHash: USDC_TICKER_HASH, + amount: bridgeResult.effectiveBridgedAmount, + slippage: 1000, // 1% slippage + status: RebalanceOperationStatus.PENDING, // pending as CCIP takes 20 mins to bridge + bridge: 'ccip-solana-mainnet', + transactions: { [SOLANA_CHAINID]: bridgeResult.receipt }, + recipient: config.ownAddress, }); - if (!bridgeResult.receipt || bridgeResult.receipt.status !== 1) { - throw new Error(`Bridge transaction failed: ${bridgeResult.receipt?.transactionHash || 'Unknown transaction'}`); - } - - logger.info('Leg 1 bridge completed successfully', { + logger.info('Rebalance operation record created for Leg 1', { requestId, - intentId: intent.intent_id, - earmarkId: earmark.id, - transactionHash: bridgeResult.receipt.transactionHash, - effectiveAmount: bridgeResult.effectiveBridgedAmount, - blockNumber: bridgeResult.receipt.blockNumber, - solanaSlot: bridgeResult.receipt.blockNumber, + operationStatus: RebalanceOperationStatus.PENDING, + note: 'Status is PENDING because CCIP takes ~20 minutes to complete', }); - // Create rebalance operation record for tracking all 3 legs - try { - await createRebalanceOperation({ - earmarkId: earmark.id, - originChainId: Number(SOLANA_CHAINID), - destinationChainId: Number(MAINNET_CHAIN_ID), - tickerHash: ticker, - amount: bridgeResult.effectiveBridgedAmount, - slippage: 1000, // 1% slippage - status: RebalanceOperationStatus.PENDING, // pending as CCIP takes 20 mins to bridge - bridge: 'ccip-solana-mainnet', - transactions: { [SOLANA_CHAINID]: bridgeResult.receipt }, - recipient: config.ownAddress, - }); - - logger.info('Rebalance operation record created for Leg 1', { - requestId, - intentId: intent.intent_id, - earmarkId: earmark.id, - operationStatus: RebalanceOperationStatus.PENDING, - note: 'Status is PENDING because CCIP takes ~20 minutes to complete', - }); + const rebalanceAction: RebalanceAction = { + bridge: SupportedBridge.CCIP, + amount: bridgeResult.effectiveBridgedAmount, + origin: Number(SOLANA_CHAINID), + destination: Number(MAINNET_CHAIN_ID), + asset: USDC_SOLANA_MINT.toString(), + transaction: bridgeResult.receipt.transactionHash, + recipient: config.ownAddress, + }; + rebalanceOperations.push(rebalanceAction); - const rebalanceAction: RebalanceAction = { - bridge: SupportedBridge.CCIP, - amount: bridgeResult.effectiveBridgedAmount, - origin: Number(SOLANA_CHAINID), - destination: Number(MAINNET_CHAIN_ID), - asset: USDC_SOLANA_MINT.toString(), - transaction: bridgeResult.receipt.transactionHash, - recipient: config.ownAddress, - }; - rebalanceOperations.push(rebalanceAction); - - rebalanceSuccessful = true; - - logger.info('Leg 1 rebalance completed successfully', { - requestId, - intentId: intent.intent_id, - earmarkId: earmark.id, - bridgedAmount: bridgeResult.effectiveBridgedAmount, - bridgedAmountInUsdc: (Number(bridgeResult.effectiveBridgedAmount) / 1_000_000).toFixed(6), - transactionHash: bridgeResult.receipt.transactionHash, - }); - } catch (dbError) { - logger.error('Failed to create rebalance operation record', { - requestId, - intentId: intent.intent_id, - earmarkId: earmark.id, - error: jsonifyError(dbError), - }); - // Don't throw here - the bridge was successful, just the record creation failed - } - } catch (bridgeError) { - logger.error('Leg 1 bridge operation failed', { + logger.info('Leg 1 rebalance completed successfully', { requestId, - intentId: intent.intent_id, - earmarkId: earmark.id, - route: solanaToMainnetRoute, - amountToBridge: amountToBridge.toString(), - error: jsonifyError(bridgeError), - errorMessage: (bridgeError as Error)?.message, - errorStack: (bridgeError as Error)?.stack, + bridgedAmount: bridgeResult.effectiveBridgedAmount, + bridgedAmountInUsdc: (Number(bridgeResult.effectiveBridgedAmount) / 10 ** USDC_SOLANA_DECIMALS).toFixed(6), + transactionHash: bridgeResult.receipt.transactionHash, }); - - // Continue to next intent instead of throwing to allow processing other intents - continue; - } - - if (!rebalanceSuccessful) { - logger.warn('Failed to complete Leg 1 rebalance for intent', { + } catch (dbError) { + logger.error('Failed to create rebalance operation record', { requestId, - intentId: intent.intent_id, - route: solanaToMainnetRoute, - amountToBridge: amountToBridge.toString(), + error: jsonifyError(dbError), }); + // Don't throw here - the bridge was successful, just the record creation failed } + } catch (bridgeError) { + logger.error('Leg 1 bridge operation failed', { + requestId, + route: solanaToMainnetRoute, + amountToBridge: amountToBridge.toString(), + error: jsonifyError(bridgeError), + errorMessage: (bridgeError as Error)?.message, + errorStack: (bridgeError as Error)?.stack, + }); } logger.info('Completed rebalancing Solana USDC', { requestId }); - // TODO: other two legs - // Leg 2: Use pendle adapter to get ptUSDe, - // further bridge to solana for ptUSDe is added in destinationCallback in pendle handler @preetham return rebalanceOperations; } @@ -1061,17 +651,19 @@ export const executeSolanaUsdcCallbacks = async (context: ProcessingContext): Pr logger.info('Executing destination callbacks for Solana USDC rebalance', { requestId }); // Get all pending CCIP operations from Solana to Mainnet - const { operations } = await db.getRebalanceOperations(undefined, undefined, { + const { operations: pendingSolanaOps } = await db.getRebalanceOperations(undefined, undefined, { status: [RebalanceOperationStatus.PENDING], + chainId: Number(SOLANA_CHAINID), + bridge: 'ccip-solana-mainnet', }); logger.debug('Found pending Solana USDC rebalance operations', { - count: operations.length, + count: pendingSolanaOps.length, requestId, status: RebalanceOperationStatus.PENDING, }); - for (const operation of operations) { + for (const operation of pendingSolanaOps) { const logContext = { requestId, operationId: operation.id, @@ -1080,11 +672,6 @@ export const executeSolanaUsdcCallbacks = async (context: ProcessingContext): Pr destinationChain: operation.destinationChainId, }; - // Only process Solana -> Mainnet CCIP operations - if (!operation.bridge || operation.bridge !== 'ccip-solana-mainnet') { - continue; - } - if ( operation.originChainId !== Number(SOLANA_CHAINID) || operation.destinationChainId !== Number(MAINNET_CHAIN_ID) @@ -1092,6 +679,19 @@ export const executeSolanaUsdcCallbacks = async (context: ProcessingContext): Pr continue; } + // Check for operation timeout - mark as failed if stuck for too long + if (operation.createdAt && isOperationTimedOut(new Date(operation.createdAt))) { + logger.warn('Operation has exceeded TTL, marking as FAILED', { + ...logContext, + createdAt: operation.createdAt, + ttlMinutes: DEFAULT_OPERATION_TTL_MINUTES, + }); + await db.updateRebalanceOperation(operation.id, { + status: RebalanceOperationStatus.EXPIRED, + }); + continue; + } + logger.info('Checking if CCIP bridge completed and USDC arrived on Mainnet', { ...logContext, bridge: operation.bridge, @@ -1146,7 +746,6 @@ export const executeSolanaUsdcCallbacks = async (context: ProcessingContext): Pr logger.info('CCIP bridge completed successfully, initiating Leg 2: USDC → ptUSDe swap', { ...logContext, solanaTransactionHash, - destinationTransactionHash: ccipStatus.destinationTransactionHash, proceedingToLeg2: true, }); @@ -1184,7 +783,7 @@ export const executeSolanaUsdcCallbacks = async (context: ProcessingContext): Pr if (!tokenPair?.ptUSDe) { logger.error('ptUSDe address not configured for mainnet in USDC_PTUSDE_PAIRS', logContext); await db.updateRebalanceOperation(operation.id, { - status: RebalanceOperationStatus.FAILED, + status: RebalanceOperationStatus.CANCELLED, }); continue; } @@ -1289,9 +888,12 @@ export const executeSolanaUsdcCallbacks = async (context: ProcessingContext): Pr }; // Execute Leg 3 CCIP transactions - const ccipTxRequests = await ccipAdapter.send(recipient, recipient, effectivePtUsdeAmount, ccipRoute); + const solanaRecipient = context.solanaSigner?.getAddress(); + if (!solanaRecipient) throw new Error('Solana signer address unavailable for CCIP leg 3'); - let leg3CcipTxHash: string | undefined; + const ccipTxRequests = await ccipAdapter.send(recipient, solanaRecipient, effectivePtUsdeAmount, ccipRoute); + + let leg3CcipTx: TransactionSubmissionResult | undefined; for (const { transaction, memo } of ccipTxRequests) { logger.info('Submitting CCIP ptUSDe → Solana transaction', { @@ -1326,37 +928,26 @@ export const executeSolanaUsdcCallbacks = async (context: ProcessingContext): Pr // Store the CCIP bridge transaction hash (not approval) if (memo === RebalanceTransactionMemo.Rebalance) { - leg3CcipTxHash = result.hash; + leg3CcipTx = result; } } // Update operation with Leg 3 CCIP transaction hash for status tracking - if (leg3CcipTxHash) { - const leg3Receipt: TransactionReceipt = { - transactionHash: leg3CcipTxHash, - blockNumber: 0, - from: rebalanceConfig.ownAddress, - to: '', - cumulativeGasUsed: '0', - effectiveGasPrice: '0', - logs: [], - status: 1, - confirmations: 1, - }; + if (leg3CcipTx) { + const leg3Receipt: TransactionReceipt = leg3CcipTx.receipt!; - const updatedTransactions = { - ...operation.transactions, + const insertedTransactions = { [MAINNET_CHAIN_ID]: leg3Receipt, }; await db.updateRebalanceOperation(operation.id, { - txHashes: updatedTransactions, + txHashes: insertedTransactions, }); logger.info('Stored Leg 3 CCIP transaction hash for status tracking', { requestId, operationId: operation.id, - leg3CcipTxHash, + leg3CcipTxHash: leg3CcipTx.hash, }); } @@ -1369,17 +960,17 @@ export const executeSolanaUsdcCallbacks = async (context: ProcessingContext): Pr status: 'AWAITING_CALLBACK', }); } catch (pendleError) { - logger.error('Failed to execute Leg 2 Pendle swap', { + logger.error('Failed to execute Leg 2/3', { ...logContext, error: jsonifyError(pendleError), }); - // Mark operation as FAILED since Leg 2 failed + // Mark operation as FAILED since Leg 2/3 failed await db.updateRebalanceOperation(operation.id, { - status: RebalanceOperationStatus.FAILED, + status: RebalanceOperationStatus.CANCELLED, }); - logger.info('Marked operation as FAILED due to Leg 2 Pendle swap failure', { + logger.info('Marked operation as FAILED due to Leg 2/3 failure', { ...logContext, note: 'Funds are on Mainnet as USDC - manual intervention may be required', }); @@ -1394,7 +985,7 @@ export const executeSolanaUsdcCallbacks = async (context: ProcessingContext): Pr // Mark operation as FAILED since CCIP bridge failed await db.updateRebalanceOperation(operation.id, { - status: RebalanceOperationStatus.FAILED, + status: RebalanceOperationStatus.CANCELLED, }); logger.info('Marked operation as FAILED due to CCIP bridge failure', { @@ -1436,6 +1027,7 @@ export const executeSolanaUsdcCallbacks = async (context: ProcessingContext): Pr // Check operations in AWAITING_CALLBACK status for Leg 3 (ptUSDe → Solana CCIP) completion const { operations: awaitingCallbackOps } = await db.getRebalanceOperations(undefined, undefined, { status: [RebalanceOperationStatus.AWAITING_CALLBACK], + bridge: 'ccip-solana-mainnet', }); logger.debug('Found operations awaiting Leg 3 CCIP completion', { @@ -1453,8 +1045,17 @@ export const executeSolanaUsdcCallbacks = async (context: ProcessingContext): Pr destinationChain: operation.destinationChainId, }; - // Only process operations that should have Leg 3 CCIP (ptUSDe → Solana) - if (operation.bridge !== 'ccip-solana-mainnet') { + // Check for operation timeout - mark as failed if stuck for too long + if (operation.createdAt && isOperationTimedOut(new Date(operation.createdAt))) { + logger.warn('AWAITING_CALLBACK operation has exceeded TTL, marking as FAILED', { + ...logContext, + createdAt: operation.createdAt, + ttlMinutes: DEFAULT_OPERATION_TTL_MINUTES, + note: 'Leg 3 CCIP may have failed or taken too long', + }); + await db.updateRebalanceOperation(operation.id, { + status: RebalanceOperationStatus.EXPIRED, + }); continue; } diff --git a/packages/poller/src/rebalance/tacUsdt.ts b/packages/poller/src/rebalance/tacUsdt.ts index a0345184..f4efa0c7 100644 --- a/packages/poller/src/rebalance/tacUsdt.ts +++ b/packages/poller/src/rebalance/tacUsdt.ts @@ -1699,15 +1699,11 @@ const executeTacCallbacks = async (context: ProcessingContext): Promise => const operationTtlMinutes = config.regularRebalanceOpTTLMinutes ?? DEFAULT_OPERATION_TTL_MINUTES; // Get all pending TAC operations - const { operations } = await db.getRebalanceOperations(undefined, undefined, { + const { operations: tacOperations } = await db.getRebalanceOperations(undefined, undefined, { status: [RebalanceOperationStatus.PENDING, RebalanceOperationStatus.AWAITING_CALLBACK], + bridge: [`${SupportedBridge.Stargate}-tac`, SupportedBridge.TacInner], }); - // Filter for TAC-related operations - const tacOperations = operations.filter( - (op) => op.bridge === 'stargate-tac' || op.bridge === SupportedBridge.TacInner, - ); - // SERIALIZATION CHECK: Only allow one Leg 2 (TacInner) operation in-flight at a time // This prevents mixing funds from multiple flows when they complete close together const pendingTacInnerOps = tacOperations.filter( diff --git a/packages/poller/test/rebalance/solanaUsdc.spec.ts b/packages/poller/test/rebalance/solanaUsdc.spec.ts index baa6d762..87d04a08 100644 --- a/packages/poller/test/rebalance/solanaUsdc.spec.ts +++ b/packages/poller/test/rebalance/solanaUsdc.spec.ts @@ -182,33 +182,17 @@ describe('Solana USDC Rebalancing', () => { expect(result).toEqual([]); }); - it('should skip intent if active earmark already exists', async () => { - // Mock intent - mockEverclear.fetchIntents.resolves([ - { - intent_id: 'intent-123', - amount_out_min: '1000000', - hub_settlement_domain: '1', - destinations: [SOLANA_CHAINID], - }, - ]); - - // Mock existing active earmark - (database.getActiveEarmarkForInvoice as jest.Mock).mockResolvedValue({ - id: 'existing-earmark', - invoiceId: 'intent-123', - designatedPurchaseChain: Number(SOLANA_CHAINID), - tickerHash: 'USDC', - minAmount: '1000000', - status: EarmarkStatus.PENDING, - createdAt: new Date(), - updatedAt: new Date(), + it('should skip rebalancing when ptUSDe balance is above threshold', async () => { + // Threshold-based rebalancing: skips when ptUSDe balance is sufficient + // Mock in-flight operations check + (mockDatabase.getRebalanceOperations as SinonStub).resolves({ + operations: [], + total: 0, }); const result = await rebalanceSolanaUsdc(mockContext as unknown as ProcessingContext); expect(result).toEqual([]); - expect(mockLogger.warn.calledWithMatch('Active earmark already exists')).toBe(true); }); }); diff --git a/packages/poller/tsconfig.json b/packages/poller/tsconfig.json index 8941b1da..89d0f20b 100644 --- a/packages/poller/tsconfig.json +++ b/packages/poller/tsconfig.json @@ -11,6 +11,8 @@ "composite": true, "moduleResolution": "node", "module": "commonjs", + "typeRoots": ["./src/types", "./node_modules/@types", "../../node_modules/@types"], + "allowSyntheticDefaultImports": true, "types": ["node", "jest"] }, "include": ["src/**/*", "test/**/*"], diff --git a/yarn.lock b/yarn.lock index 1bb0dd2d..d98caaba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17,6 +17,19 @@ __metadata: languageName: node linkType: hard +"@0no-co/graphqlsp@npm:^1.12.13": + version: 1.15.2 + resolution: "@0no-co/graphqlsp@npm:1.15.2" + dependencies: + "@gql.tada/internal": ^1.0.0 + graphql: ^15.5.0 || ^16.0.0 || ^17.0.0 + peerDependencies: + graphql: ^15.5.0 || ^16.0.0 || ^17.0.0 + typescript: ^5.0.0 + checksum: 783cb9c98d3de2da616011a072fb603c9e581902227129b20c19ad03fb9fee14d99f8ccedcd40a95f27ecb661613711a41b6e471089dbbb69f74f8606bf0597a + languageName: node + linkType: hard + "@adraffy/ens-normalize@npm:1.10.0": version: 1.10.0 resolution: "@adraffy/ens-normalize@npm:1.10.0" @@ -31,13 +44,51 @@ __metadata: languageName: node linkType: hard -"@adraffy/ens-normalize@npm:1.11.0, @adraffy/ens-normalize@npm:^1.11.0": +"@adraffy/ens-normalize@npm:^1.11.0": version: 1.11.0 resolution: "@adraffy/ens-normalize@npm:1.11.0" checksum: b2911269e3e0ec6396a2e5433a99e0e1f9726befc6c167994448cd0e53dbdd0be22b4835b4f619558b568ed9aa7312426b8fa6557a13999463489daa88169ee5 languageName: node linkType: hard +"@aptos-labs/aptos-cli@npm:^1.0.2": + version: 1.1.1 + resolution: "@aptos-labs/aptos-cli@npm:1.1.1" + dependencies: + commander: ^12.1.0 + bin: + aptos: dist/aptos.js + checksum: 89bba7f7aafb6ac081286600c085642ca67bf89d269836034db237a13c2db95e96e536c5dafdc1b4f6a38449eb6ba3aff63479fe3c3f6fc1d0fda84ad8cbba2c + languageName: node + linkType: hard + +"@aptos-labs/aptos-client@npm:^2.1.0": + version: 2.1.0 + resolution: "@aptos-labs/aptos-client@npm:2.1.0" + peerDependencies: + got: ^11.8.6 + checksum: 88676d5eed10e79f4a8e2f98bbff573737bad131efa9b909edb5706a68301a6a4b93b8da2ac7a02d44e2842d6ba1ffbe620aab2d2d1666f8a1611d99e4644abe + languageName: node + linkType: hard + +"@aptos-labs/ts-sdk@npm:^5.2.0": + version: 5.2.0 + resolution: "@aptos-labs/ts-sdk@npm:5.2.0" + dependencies: + "@aptos-labs/aptos-cli": ^1.0.2 + "@aptos-labs/aptos-client": ^2.1.0 + "@noble/curves": ^1.9.0 + "@noble/hashes": ^1.5.0 + "@scure/bip32": ^1.4.0 + "@scure/bip39": ^1.3.0 + eventemitter3: ^5.0.1 + js-base64: ^3.7.7 + jwt-decode: ^4.0.0 + poseidon-lite: ^0.2.0 + checksum: 6f2da4319c48d84ed0449300a12e306791dacb86b58f5815ba44d8e830da8a1b82d7c24e58bcdcca207f9c8e8d11cf4d6f9dbedf101efa31db14679d5a912107 + languageName: node + linkType: hard + "@assemblyscript/loader@npm:^0.9.4": version: 0.9.4 resolution: "@assemblyscript/loader@npm:0.9.4" @@ -1548,22 +1599,32 @@ __metadata: languageName: node linkType: hard -"@chainlink/ccip-js@npm:^0.2.6": - version: 0.2.6 - resolution: "@chainlink/ccip-js@npm:0.2.6" +"@chainlink/ccip-sdk@npm:^0.93.0": + version: 0.93.0 + resolution: "@chainlink/ccip-sdk@npm:0.93.0" dependencies: - "@nomicfoundation/hardhat-chai-matchers": ^2.0.8 - "@nomicfoundation/hardhat-ethers": ^3.0.8 - "@nomicfoundation/hardhat-toolbox": ^5.0.0 - "@nomicfoundation/hardhat-viem": ^2.0.5 - "@openzeppelin/contracts": ^5.1.0 - chai: ^5.2.0 - ethers: 6.13.4 - mocha: ^11.1.0 - ts-jest: ^29.2.5 - typescript: ^5.8.2 - viem: 2.21.25 - checksum: df60dbd7a165cd74bcba38dd11d2ba5ac2860240f8b59ec426047653cee0e2cf9ba6b699e31ffbaf507b10c57363183b5745fe853624c26606d0cdd537082bbd + "@aptos-labs/ts-sdk": ^5.2.0 + "@coral-xyz/anchor": ^0.29.0 + "@mysten/bcs": ^1.9.2 + "@mysten/sui": ^1.45.2 + "@solana/spl-token": 0.4.14 + "@solana/web3.js": ^1.98.4 + "@ton/core": 0.62.0 + "@ton/ton": ^16.1.0 + abitype: 1.2.3 + bn.js: ^5.2.2 + borsh: ^2.0.0 + bs58: ^6.0.0 + ethers: 6.16.0 + micro-memoize: ^5.1.1 + type-fest: ^5.3.1 + yaml: 2.8.2 + peerDependencies: + viem: ^2.0.0 + peerDependenciesMeta: + viem: + optional: true + checksum: f0b131ba97f2e90d5daad0a6a861c4aadc301c4e39fa57c8dd91666cb7bc8427318a194a9c3756c5f8ea2c930309dde70f78f8be1dd97e653920fa9c3291adbc languageName: node linkType: hard @@ -1894,6 +1955,28 @@ __metadata: languageName: node linkType: hard +"@coral-xyz/anchor@npm:^0.29.0": + version: 0.29.0 + resolution: "@coral-xyz/anchor@npm:0.29.0" + dependencies: + "@coral-xyz/borsh": ^0.29.0 + "@noble/hashes": ^1.3.1 + "@solana/web3.js": ^1.68.0 + bn.js: ^5.1.2 + bs58: ^4.0.1 + buffer-layout: ^1.2.2 + camelcase: ^6.3.0 + cross-fetch: ^3.1.5 + crypto-hash: ^1.3.0 + eventemitter3: ^4.0.7 + pako: ^2.0.3 + snake-case: ^3.0.4 + superstruct: ^0.15.4 + toml: ^3.0.0 + checksum: 10c4e6c5557653419683f5ae22ec47ac266b64e5b422d466885cf2dc7efa8f836239bdf321495d3e2b3ce03e766667c0e2192cc573fbd66bc12cc652f5146e10 + languageName: node + linkType: hard + "@coral-xyz/anchor@npm:^0.30.1": version: 0.30.1 resolution: "@coral-xyz/anchor@npm:0.30.1" @@ -1917,6 +2000,18 @@ __metadata: languageName: node linkType: hard +"@coral-xyz/borsh@npm:^0.29.0": + version: 0.29.0 + resolution: "@coral-xyz/borsh@npm:0.29.0" + dependencies: + bn.js: ^5.1.2 + buffer-layout: ^1.2.0 + peerDependencies: + "@solana/web3.js": ^1.68.0 + checksum: 37006c75cd012672adf48e10234062624634da2a9335e34b7ff30969f58aff78cc3073b66a3edc806b52f038469f0c477a5a3ed35aaa075f3cbd44d7133ac218 + languageName: node + linkType: hard + "@coral-xyz/borsh@npm:^0.30.1": version: 0.30.1 resolution: "@coral-xyz/borsh@npm:0.30.1" @@ -3435,6 +3530,49 @@ __metadata: languageName: node linkType: hard +"@gql.tada/cli-utils@npm:1.7.2": + version: 1.7.2 + resolution: "@gql.tada/cli-utils@npm:1.7.2" + dependencies: + "@0no-co/graphqlsp": ^1.12.13 + "@gql.tada/internal": 1.0.8 + graphql: ^15.5.0 || ^16.0.0 || ^17.0.0 + peerDependencies: + "@0no-co/graphqlsp": ^1.12.13 + "@gql.tada/svelte-support": 1.0.1 + "@gql.tada/vue-support": 1.0.1 + graphql: ^15.5.0 || ^16.0.0 || ^17.0.0 + typescript: ^5.0.0 + peerDependenciesMeta: + "@gql.tada/svelte-support": + optional: true + "@gql.tada/vue-support": + optional: true + checksum: cfa3cd5749e90549edc8819a26f96d8c8ea17b9866d965a1802b2a2826a5175459d18cc3cd88d581f248de1f16f04b3d2bd3fc9d9a36da517689e3726d67f464 + languageName: node + linkType: hard + +"@gql.tada/internal@npm:1.0.8, @gql.tada/internal@npm:^1.0.0": + version: 1.0.8 + resolution: "@gql.tada/internal@npm:1.0.8" + dependencies: + "@0no-co/graphql.web": ^1.0.5 + peerDependencies: + graphql: ^15.5.0 || ^16.0.0 || ^17.0.0 + typescript: ^5.0.0 + checksum: 8046283fa29e382c2a56ce293cb1aeb6a864cfab0f5476e22faf6e1d52a6e89a04a34bcd0342744bd2835bceb9d555c9965b3f23102bce7e4d7d11ce7d5fb8c6 + languageName: node + linkType: hard + +"@graphql-typed-document-node/core@npm:^3.2.0": + version: 3.2.0 + resolution: "@graphql-typed-document-node/core@npm:3.2.0" + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: fa44443accd28c8cf4cb96aaaf39d144a22e8b091b13366843f4e97d19c7bfeaf609ce3c7603a4aeffe385081eaf8ea245d078633a7324c11c5ec4b2011bb76d + languageName: node + linkType: hard + "@humanfs/core@npm:^0.19.1": version: 0.19.1 resolution: "@humanfs/core@npm:0.19.1" @@ -4599,7 +4737,6 @@ __metadata: version: 0.0.0-use.local resolution: "@mark/poller@workspace:packages/poller" dependencies: - "@chainlink/ccip-js": ^0.2.6 "@mark/cache": "workspace:*" "@mark/chainservice": "workspace:*" "@mark/core": "workspace:*" @@ -4609,6 +4746,9 @@ __metadata: "@mark/prometheus": "workspace:*" "@mark/rebalance": "workspace:*" "@mark/web3signer": "workspace:*" + "@metaplex-foundation/mpl-token-metadata": ^3.4.0 + "@metaplex-foundation/umi": ^1.4.1 + "@metaplex-foundation/umi-bundle-defaults": ^1.4.1 "@solana/spl-token": ^0.4.9 "@solana/web3.js": ^1.98.0 "@types/aws-lambda": 8.10.147 @@ -4622,6 +4762,7 @@ __metadata: dd-trace: 5.42.0 eslint: 9.17.0 jest: ^30.0.5 + loglevel: ^1.9.2 rimraf: 6.0.1 sinon: 17.0.1 tronweb: 6.0.3 @@ -4656,12 +4797,13 @@ __metadata: version: 0.0.0-use.local resolution: "@mark/rebalance@workspace:packages/adapters/rebalance" dependencies: - "@chainlink/ccip-js": ^0.2.6 + "@chainlink/ccip-sdk": ^0.93.0 "@cowprotocol/cow-sdk": ^7.1.2-beta.0 "@defuse-protocol/one-click-sdk-typescript": ^0.1.5 "@mark/core": "workspace:*" "@mark/database": "workspace:*" "@mark/logger": "workspace:*" + "@solana/web3.js": ^1.98.0 "@ton/crypto": ^3.3.0 "@ton/ton": ^16.1.0 "@tonappchain/sdk": 0.7.1 @@ -4729,6 +4871,207 @@ __metadata: languageName: node linkType: hard +"@metaplex-foundation/mpl-token-metadata@npm:^3.4.0": + version: 3.4.0 + resolution: "@metaplex-foundation/mpl-token-metadata@npm:3.4.0" + dependencies: + "@metaplex-foundation/mpl-toolbox": ^0.10.0 + peerDependencies: + "@metaplex-foundation/umi": ">= 0.8.2 <= 1" + checksum: 1ed4e901938a3865de5683dbdaa4f20ee719b08e86b8824325d6b4561066c8ced25562f0970f58ac415fc3f28a4455bf7ae7fab4eef85b24b86623d6ac5a70ca + languageName: node + linkType: hard + +"@metaplex-foundation/mpl-toolbox@npm:^0.10.0": + version: 0.10.0 + resolution: "@metaplex-foundation/mpl-toolbox@npm:0.10.0" + peerDependencies: + "@metaplex-foundation/umi": ">= 0.8.2 <= 1" + checksum: 8970586ce8a3684aa2cb274d3579450711fc636626dd9582fc9ae4213c0b3025f97062ad3f7561ed13d6d27b52c26f6a6b2f9d1685e6426d069335b1a8d4188e + languageName: node + linkType: hard + +"@metaplex-foundation/umi-bundle-defaults@npm:^1.4.1": + version: 1.4.1 + resolution: "@metaplex-foundation/umi-bundle-defaults@npm:1.4.1" + dependencies: + "@metaplex-foundation/umi-downloader-http": ^1.4.1 + "@metaplex-foundation/umi-eddsa-web3js": ^1.4.1 + "@metaplex-foundation/umi-http-fetch": ^1.4.1 + "@metaplex-foundation/umi-program-repository": ^1.4.1 + "@metaplex-foundation/umi-rpc-chunk-get-accounts": ^1.4.1 + "@metaplex-foundation/umi-rpc-web3js": ^1.4.1 + "@metaplex-foundation/umi-serializer-data-view": ^1.4.1 + "@metaplex-foundation/umi-transaction-factory-web3js": ^1.4.1 + peerDependencies: + "@metaplex-foundation/umi": ^1.4.1 + "@solana/web3.js": ^1.72.0 + checksum: 460947132de2953b36af5f836048077fada91aec7dea313ca073e782f989c6bb47e90c9188b43b29a24b90c02a78f7a638c6c3170bed809643d2223f8b78093a + languageName: node + linkType: hard + +"@metaplex-foundation/umi-downloader-http@npm:^1.4.1": + version: 1.4.1 + resolution: "@metaplex-foundation/umi-downloader-http@npm:1.4.1" + peerDependencies: + "@metaplex-foundation/umi": ^1.4.1 + checksum: afedfbe02d9945c74b4524bb663f6cf525310c294bd8eec615b068db7b399177e4b1c6349168382b91d9c80446591f9957d05db0644d0c6c55af705706dddfc9 + languageName: node + linkType: hard + +"@metaplex-foundation/umi-eddsa-web3js@npm:^1.4.1": + version: 1.4.1 + resolution: "@metaplex-foundation/umi-eddsa-web3js@npm:1.4.1" + dependencies: + "@metaplex-foundation/umi-web3js-adapters": ^1.4.1 + "@noble/curves": ^1.0.0 + yaml: ^2.7.0 + peerDependencies: + "@metaplex-foundation/umi": ^1.4.1 + "@solana/web3.js": ^1.72.0 + checksum: 49f33b109441a68c821a49c2786432bec20fb3f9132cf22209dc5cd703d3dfdb2c0a9958ed401ce302484f0a74dda3e4a6c739da714baf191d3880f1ebcbbdbd + languageName: node + linkType: hard + +"@metaplex-foundation/umi-http-fetch@npm:^1.4.1": + version: 1.4.1 + resolution: "@metaplex-foundation/umi-http-fetch@npm:1.4.1" + dependencies: + node-fetch: ^2.6.7 + peerDependencies: + "@metaplex-foundation/umi": ^1.4.1 + checksum: d553b330bb5ec31e9ded81b8b0d70533e0a7bb4d10e9aa34183007862f7a4a81c209e350da6dd28ed33ea1743cdef96afc2489d27931465332177081141b5872 + languageName: node + linkType: hard + +"@metaplex-foundation/umi-options@npm:^1.4.1": + version: 1.4.1 + resolution: "@metaplex-foundation/umi-options@npm:1.4.1" + checksum: e043a99bf7c9618dc700fb480dfaf6382a98e830e7254e1804a3b6ef71f7ac95d870057809991c5e99cb953a521763aacafb71f1e37c2599ba3d41de28115f1e + languageName: node + linkType: hard + +"@metaplex-foundation/umi-program-repository@npm:^1.4.1": + version: 1.4.1 + resolution: "@metaplex-foundation/umi-program-repository@npm:1.4.1" + peerDependencies: + "@metaplex-foundation/umi": ^1.4.1 + checksum: 66f27ad83c490967326697de66da4acbb7727e64a9d7280a5ea6fc0dc4a6fb8398ddedf6b0f2e019677adc03709d5e52fcf5c22143c192ea74627dee3ac9d3de + languageName: node + linkType: hard + +"@metaplex-foundation/umi-public-keys@npm:^1.4.1": + version: 1.4.1 + resolution: "@metaplex-foundation/umi-public-keys@npm:1.4.1" + dependencies: + "@metaplex-foundation/umi-serializers-encodings": ^1.4.1 + checksum: a770931eef05db104adb05658228392353bafb3baddc8ad57a8e58b765eaa48f3d58d27ab5e135cf78925f0379f9b5e4f1ecbf83f55f799e5c6af3caf41cde83 + languageName: node + linkType: hard + +"@metaplex-foundation/umi-rpc-chunk-get-accounts@npm:^1.4.1": + version: 1.4.1 + resolution: "@metaplex-foundation/umi-rpc-chunk-get-accounts@npm:1.4.1" + peerDependencies: + "@metaplex-foundation/umi": ^1.4.1 + checksum: 22ce5af44ab0992801faefca13d0db5b069b758ef95fc7ac4b613c6e941219bd8597a293c18adf826cb3a4e6501a50b6ec157998fa7ea2504916ef671c470fc6 + languageName: node + linkType: hard + +"@metaplex-foundation/umi-rpc-web3js@npm:^1.4.1": + version: 1.4.1 + resolution: "@metaplex-foundation/umi-rpc-web3js@npm:1.4.1" + dependencies: + "@metaplex-foundation/umi-web3js-adapters": ^1.4.1 + peerDependencies: + "@metaplex-foundation/umi": ^1.4.1 + "@solana/web3.js": ^1.72.0 + checksum: b00284d25cb72f385c94d869a447b881c2384f32aa1551b73b7aced901dd66a6891c1bd823855c94fe43cf7c83f9d3c078c45f0cf0f3a69e2c22fa92bbc195e3 + languageName: node + linkType: hard + +"@metaplex-foundation/umi-serializer-data-view@npm:^1.4.1": + version: 1.4.1 + resolution: "@metaplex-foundation/umi-serializer-data-view@npm:1.4.1" + peerDependencies: + "@metaplex-foundation/umi": ^1.4.1 + checksum: 1787a3e56bd3c49c02ae5f710becc741318d27055a7e173694fbb7fd5917fe7edaaae7ca1968af24a41abe912800a5317f9021e6388d0dbfe2383df753963bf8 + languageName: node + linkType: hard + +"@metaplex-foundation/umi-serializers-core@npm:^1.4.1": + version: 1.4.1 + resolution: "@metaplex-foundation/umi-serializers-core@npm:1.4.1" + checksum: 67b6aec6d5d048b33a39fb78acdae83f6d8970255bff2317d5fddbe972dd89251db8f608a2a655105870fccc77a9ccd6dddc06e15cc8343e8b0767416ffb24f9 + languageName: node + linkType: hard + +"@metaplex-foundation/umi-serializers-encodings@npm:^1.4.1": + version: 1.4.1 + resolution: "@metaplex-foundation/umi-serializers-encodings@npm:1.4.1" + dependencies: + "@metaplex-foundation/umi-serializers-core": ^1.4.1 + checksum: e09becf1c4645a0b4555fb333acf7b5ccc6b1e99625bec45420508bb082acfaf2ae057f9cd24af9c9f0fd2dcd59e9b71e4013a1a6e5aa2b511ef04a2a1ea1bf2 + languageName: node + linkType: hard + +"@metaplex-foundation/umi-serializers-numbers@npm:^1.4.1": + version: 1.4.1 + resolution: "@metaplex-foundation/umi-serializers-numbers@npm:1.4.1" + dependencies: + "@metaplex-foundation/umi-serializers-core": ^1.4.1 + checksum: de051d4b7debf57afba66c3bb9e06b82f031592f08e6ec70227634fe3cda0acc7c8bef3bcd3e69d44cd47921a1904c7ec57091c0158c80ca5af03a8d41a19568 + languageName: node + linkType: hard + +"@metaplex-foundation/umi-serializers@npm:^1.4.1": + version: 1.4.1 + resolution: "@metaplex-foundation/umi-serializers@npm:1.4.1" + dependencies: + "@metaplex-foundation/umi-options": ^1.4.1 + "@metaplex-foundation/umi-public-keys": ^1.4.1 + "@metaplex-foundation/umi-serializers-core": ^1.4.1 + "@metaplex-foundation/umi-serializers-encodings": ^1.4.1 + "@metaplex-foundation/umi-serializers-numbers": ^1.4.1 + checksum: 37e9ef7d703874d0518aebdb40ce291db80b0dfca23eed7cd032c97a423de609ccdc4816be5e9a215786eff028820db74b93711628885825d006c1dc771ed70e + languageName: node + linkType: hard + +"@metaplex-foundation/umi-transaction-factory-web3js@npm:^1.4.1": + version: 1.4.1 + resolution: "@metaplex-foundation/umi-transaction-factory-web3js@npm:1.4.1" + dependencies: + "@metaplex-foundation/umi-web3js-adapters": ^1.4.1 + peerDependencies: + "@metaplex-foundation/umi": ^1.4.1 + "@solana/web3.js": ^1.72.0 + checksum: cada4377dba76ab07e42d8f4555c435743adc55257fe8f0294e270e96a0f7b0b482770b4a2882e14c71df0467bf05878797732287f116980b0eee4a86b2be22e + languageName: node + linkType: hard + +"@metaplex-foundation/umi-web3js-adapters@npm:^1.4.1": + version: 1.4.1 + resolution: "@metaplex-foundation/umi-web3js-adapters@npm:1.4.1" + dependencies: + buffer: ^6.0.3 + peerDependencies: + "@metaplex-foundation/umi": ^1.4.1 + "@solana/web3.js": ^1.72.0 + checksum: f102bb12dc7adf8af95bf31f6e435fc816a8a38d04fa38a76240b150630fc2bb381679fae20d3a7a90be21b38d1c53e6d5429b95d27778ab1a97ccf3cdda9158 + languageName: node + linkType: hard + +"@metaplex-foundation/umi@npm:^1.4.1": + version: 1.4.1 + resolution: "@metaplex-foundation/umi@npm:1.4.1" + dependencies: + "@metaplex-foundation/umi-options": ^1.4.1 + "@metaplex-foundation/umi-public-keys": ^1.4.1 + "@metaplex-foundation/umi-serializers": ^1.4.1 + checksum: 0d839423f3d9c91cc9078e8ae937d84aaa80c5e12113013c6423902f3773ecdc019bef7e3fff217fc2e6dcc5d9496432456da327a5f0fee3b4b4c30d55e928c7 + languageName: node + linkType: hard + "@multiformats/base-x@npm:^4.0.1": version: 4.0.1 resolution: "@multiformats/base-x@npm:4.0.1" @@ -4736,6 +5079,48 @@ __metadata: languageName: node linkType: hard +"@mysten/bcs@npm:1.9.2, @mysten/bcs@npm:^1.9.2": + version: 1.9.2 + resolution: "@mysten/bcs@npm:1.9.2" + dependencies: + "@mysten/utils": 0.2.0 + "@scure/base": ^1.2.6 + checksum: 670fe20ec65a3a3e7f44b454bbb2657cee060ba03eb2748bf385d2d46ed9fef74da1ca0e275f3ec7c26603d400f070afbdeaabded9aebe215eb8bf750ca5aeb4 + languageName: node + linkType: hard + +"@mysten/sui@npm:^1.45.2": + version: 1.45.2 + resolution: "@mysten/sui@npm:1.45.2" + dependencies: + "@graphql-typed-document-node/core": ^3.2.0 + "@mysten/bcs": 1.9.2 + "@mysten/utils": 0.2.0 + "@noble/curves": =1.9.4 + "@noble/hashes": ^1.8.0 + "@protobuf-ts/grpcweb-transport": ^2.11.1 + "@protobuf-ts/runtime": ^2.11.1 + "@protobuf-ts/runtime-rpc": ^2.11.1 + "@scure/base": ^1.2.6 + "@scure/bip32": ^1.7.0 + "@scure/bip39": ^1.6.0 + gql.tada: ^1.8.13 + graphql: ^16.11.0 + poseidon-lite: 0.2.1 + valibot: ^1.2.0 + checksum: b47aff184b31bc0081e9ea7ec19ade06d6dc0b76262beef76565daa1f4b5d1cb17816da10966750814feacdad1953a906fb4d06d235095e196afba8d4190fcb6 + languageName: node + linkType: hard + +"@mysten/utils@npm:0.2.0": + version: 0.2.0 + resolution: "@mysten/utils@npm:0.2.0" + dependencies: + "@scure/base": ^1.2.6 + checksum: 1426ba29fd795d380ba01387197e9a722e4df0d8d46bfb73923c8445c9439ee16b2292b3719fa742dc82b3efc2c41847d52e49c2906c4f1fcca1f4da58ffe327 + languageName: node + linkType: hard + "@napi-rs/wasm-runtime@npm:^0.2.11": version: 0.2.12 resolution: "@napi-rs/wasm-runtime@npm:0.2.12" @@ -4772,15 +5157,6 @@ __metadata: languageName: node linkType: hard -"@noble/curves@npm:1.6.0, @noble/curves@npm:~1.6.0": - version: 1.6.0 - resolution: "@noble/curves@npm:1.6.0" - dependencies: - "@noble/hashes": 1.5.0 - checksum: 258f3feb2a6098cf35521562ecb7d452fd728e8a008ff9f1ef435184f9d0c782ceb8f7b7fa8df3317c3be7a19f53995ee124cd05c8080b130bd42e3cb072f24d - languageName: node - linkType: hard - "@noble/curves@npm:1.9.1": version: 1.9.1 resolution: "@noble/curves@npm:1.9.1" @@ -4799,7 +5175,16 @@ __metadata: languageName: node linkType: hard -"@noble/curves@npm:^1.4.0, @noble/curves@npm:^1.4.2, @noble/curves@npm:^1.6.0, @noble/curves@npm:^1.9.1, @noble/curves@npm:~1.9.0": +"@noble/curves@npm:=1.9.4": + version: 1.9.4 + resolution: "@noble/curves@npm:1.9.4" + dependencies: + "@noble/hashes": 1.8.0 + checksum: 464813a81982ad670d2ae38452eea389066cf3b8d976ec2992dfa7c47b809a3703e7cf4f0915c559792fff97284563176e2ac5d06c353434292789404cbfc3dd + languageName: node + linkType: hard + +"@noble/curves@npm:^1.0.0, @noble/curves@npm:^1.4.2, @noble/curves@npm:^1.6.0, @noble/curves@npm:^1.9.0, @noble/curves@npm:^1.9.1, @noble/curves@npm:~1.9.0": version: 1.9.7 resolution: "@noble/curves@npm:1.9.7" dependencies: @@ -4829,14 +5214,7 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:1.5.0, @noble/hashes@npm:~1.5.0": - version: 1.5.0 - resolution: "@noble/hashes@npm:1.5.0" - checksum: 9cc031d5c888c455bfeef76af649b87f75380a4511405baea633c1e4912fd84aff7b61e99716f0231d244c9cfeda1fafd7d718963e6a0c674ed705e9b1b4f76b - languageName: node - linkType: hard - -"@noble/hashes@npm:1.8.0, @noble/hashes@npm:^1, @noble/hashes@npm:^1.0.0, @noble/hashes@npm:^1.2.0, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:^1.4.0, @noble/hashes@npm:^1.8.0, @noble/hashes@npm:~1.8.0": +"@noble/hashes@npm:1.8.0, @noble/hashes@npm:^1, @noble/hashes@npm:^1.0.0, @noble/hashes@npm:^1.2.0, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:^1.4.0, @noble/hashes@npm:^1.5.0, @noble/hashes@npm:^1.8.0, @noble/hashes@npm:~1.8.0": version: 1.8.0 resolution: "@noble/hashes@npm:1.8.0" checksum: c94e98b941963676feaba62475b1ccfa8341e3f572adbb3b684ee38b658df44100187fa0ef4220da580b13f8d27e87d5492623c8a02ecc61f23fb9960c7918f5 @@ -4877,75 +5255,6 @@ __metadata: languageName: node linkType: hard -"@nomicfoundation/hardhat-chai-matchers@npm:^2.0.8": - version: 2.1.2 - resolution: "@nomicfoundation/hardhat-chai-matchers@npm:2.1.2" - dependencies: - "@types/chai-as-promised": ^7.1.3 - chai-as-promised: ^7.1.1 - deep-eql: ^4.0.1 - ordinal: ^1.0.3 - peerDependencies: - "@nomicfoundation/hardhat-ethers": ^3.1.0 - chai: ^4.2.0 - ethers: ^6.14.0 - hardhat: ^2.26.0 - checksum: 7c783ccfe5bd3ceb5810df53bf2c1cbf374db42e53e96a523a6f239bbc633449abbe36c184c94945769e900601b149df8f041be3d69493730245ae8bb6e408dc - languageName: node - linkType: hard - -"@nomicfoundation/hardhat-ethers@npm:^3.0.8": - version: 3.1.3 - resolution: "@nomicfoundation/hardhat-ethers@npm:3.1.3" - dependencies: - debug: ^4.1.1 - lodash.isequal: ^4.5.0 - peerDependencies: - ethers: ^6.14.0 - hardhat: ^2.28.0 - checksum: e42bd298fbd6b747524cd9a84712dae7c094bbc6d0cc93a3cbec8a6907f06ac5c81d46e82e74cbc91fab3effed5ebe2906988eb674064b8f2305a4c2f9b12a7b - languageName: node - linkType: hard - -"@nomicfoundation/hardhat-toolbox@npm:^5.0.0": - version: 5.0.0 - resolution: "@nomicfoundation/hardhat-toolbox@npm:5.0.0" - peerDependencies: - "@nomicfoundation/hardhat-chai-matchers": ^2.0.0 - "@nomicfoundation/hardhat-ethers": ^3.0.0 - "@nomicfoundation/hardhat-ignition-ethers": ^0.15.0 - "@nomicfoundation/hardhat-network-helpers": ^1.0.0 - "@nomicfoundation/hardhat-verify": ^2.0.0 - "@typechain/ethers-v6": ^0.5.0 - "@typechain/hardhat": ^9.0.0 - "@types/chai": ^4.2.0 - "@types/mocha": ">=9.1.0" - "@types/node": ">=18.0.0" - chai: ^4.2.0 - ethers: ^6.4.0 - hardhat: ^2.11.0 - hardhat-gas-reporter: ^1.0.8 - solidity-coverage: ^0.8.1 - ts-node: ">=8.0.0" - typechain: ^8.3.0 - typescript: ">=4.5.0" - checksum: 18890eaf1cc130afb7dc83ea48cb6ef23c499eb5d28c3fbb36e706082383a320118ee6d4491ede64acf684d2f1ffa117cf84ad80d8ebde9fa52a443f8780a898 - languageName: node - linkType: hard - -"@nomicfoundation/hardhat-viem@npm:^2.0.5": - version: 2.1.3 - resolution: "@nomicfoundation/hardhat-viem@npm:2.1.3" - dependencies: - abitype: ^0.9.8 - lodash.memoize: ^4.1.2 - peerDependencies: - hardhat: ^2.26.0 - viem: ^2.7.6 - checksum: 1de5502159a0d4a2f9c0b24d56d082818bab5b31a3c7e7094e17664b1d76b9bd22b7c4f61f62959131db2e98a25ec2670c0aa1b86b2fe75b10545a48bb9fbb80 - languageName: node - linkType: hard - "@npmcli/agent@npm:^3.0.0": version: 3.0.0 resolution: "@npmcli/agent@npm:3.0.0" @@ -5030,13 +5339,6 @@ __metadata: languageName: node linkType: hard -"@openzeppelin/contracts@npm:^5.1.0": - version: 5.5.0 - resolution: "@openzeppelin/contracts@npm:5.5.0" - checksum: 77a5140be7f57190e0bae53ead7ff2f266d418ade90c2b281fddc3e812d8c26a7b4143dd290e68cf4635dcdbb147699603f0c50ea1dacec0bfba59b3ce2c75bc - languageName: node - linkType: hard - "@orbs-network/ton-access@npm:^2.3.3": version: 2.3.3 resolution: "@orbs-network/ton-access@npm:2.3.3" @@ -5078,6 +5380,32 @@ __metadata: languageName: node linkType: hard +"@protobuf-ts/grpcweb-transport@npm:^2.11.1": + version: 2.11.1 + resolution: "@protobuf-ts/grpcweb-transport@npm:2.11.1" + dependencies: + "@protobuf-ts/runtime": ^2.11.1 + "@protobuf-ts/runtime-rpc": ^2.11.1 + checksum: 9c6bbb26e9127e55dd139012d6469c0b8e1d6fde1e52a34a475696d196f83dca3c6939ac5fd287df5f51c17606a9de9c0cd18960395253a2ac4d92cfc3c12613 + languageName: node + linkType: hard + +"@protobuf-ts/runtime-rpc@npm:^2.11.1": + version: 2.11.1 + resolution: "@protobuf-ts/runtime-rpc@npm:2.11.1" + dependencies: + "@protobuf-ts/runtime": ^2.11.1 + checksum: 18eb78adcf13371ebff274e560bbfabea71771bf2f4a7bd02298472e401b18a918f181f6b8ecffa51e3976f1964abe5bd637fde05c504af4d2f44c7f35a1b911 + languageName: node + linkType: hard + +"@protobuf-ts/runtime@npm:^2.11.1": + version: 2.11.1 + resolution: "@protobuf-ts/runtime@npm:2.11.1" + checksum: f06be086ee261c7840783f4054167b215d9f8a2e22ced2fe2198574c54293ce099d635b59b90c156c3efcd66d9401880f1e3ecd56c779eb4a89dc27a12d1b6b3 + languageName: node + linkType: hard + "@protobufjs/aspromise@npm:^1.1.1, @protobufjs/aspromise@npm:^1.1.2": version: 1.1.2 resolution: "@protobufjs/aspromise@npm:1.1.2" @@ -5258,20 +5586,20 @@ __metadata: languageName: node linkType: hard -"@scure/base@npm:~1.1.0, @scure/base@npm:~1.1.2, @scure/base@npm:~1.1.6, @scure/base@npm:~1.1.7, @scure/base@npm:~1.1.8": - version: 1.1.9 - resolution: "@scure/base@npm:1.1.9" - checksum: 120820a37dfe9dfe4cab2b7b7460552d08e67dee8057ed5354eb68d8e3440890ae983ce3bee957d2b45684950b454a2b6d71d5ee77c1fd3fddc022e2a510337f - languageName: node - linkType: hard - -"@scure/base@npm:~1.2.5": +"@scure/base@npm:^1.2.6, @scure/base@npm:~1.2.5": version: 1.2.6 resolution: "@scure/base@npm:1.2.6" checksum: 1058cb26d5e4c1c46c9cc0ae0b67cc66d306733baf35d6ebdd8ddaba242b80c3807b726e3b48cb0411bb95ec10d37764969063ea62188f86ae9315df8ea6b325 languageName: node linkType: hard +"@scure/base@npm:~1.1.0, @scure/base@npm:~1.1.2, @scure/base@npm:~1.1.6": + version: 1.1.9 + resolution: "@scure/base@npm:1.1.9" + checksum: 120820a37dfe9dfe4cab2b7b7460552d08e67dee8057ed5354eb68d8e3440890ae983ce3bee957d2b45684950b454a2b6d71d5ee77c1fd3fddc022e2a510337f + languageName: node + linkType: hard + "@scure/bip32@npm:1.3.2": version: 1.3.2 resolution: "@scure/bip32@npm:1.3.2" @@ -5294,18 +5622,7 @@ __metadata: languageName: node linkType: hard -"@scure/bip32@npm:1.5.0": - version: 1.5.0 - resolution: "@scure/bip32@npm:1.5.0" - dependencies: - "@noble/curves": ~1.6.0 - "@noble/hashes": ~1.5.0 - "@scure/base": ~1.1.7 - checksum: 2e119525cdffccc3aad7ca64aec22df2101233708111dfb551410f82aae85fe14acf39dc87cea1a535adc327451f9c3dea3c6a2dd22b859508025bc46a7a80ce - languageName: node - linkType: hard - -"@scure/bip32@npm:1.7.0, @scure/bip32@npm:^1.7.0": +"@scure/bip32@npm:1.7.0, @scure/bip32@npm:^1.4.0, @scure/bip32@npm:^1.7.0": version: 1.7.0 resolution: "@scure/bip32@npm:1.7.0" dependencies: @@ -5336,17 +5653,7 @@ __metadata: languageName: node linkType: hard -"@scure/bip39@npm:1.4.0": - version: 1.4.0 - resolution: "@scure/bip39@npm:1.4.0" - dependencies: - "@noble/hashes": ~1.5.0 - "@scure/base": ~1.1.8 - checksum: 211f2c01361993bfe54c0e4949f290224381457c7f76d7cd51d6a983f3f4b6b9f85adfd0e623977d777ed80417a5fe729eb19dd34e657147810a0e58a8e7b9e0 - languageName: node - linkType: hard - -"@scure/bip39@npm:1.6.0, @scure/bip39@npm:^1.6.0": +"@scure/bip39@npm:1.6.0, @scure/bip39@npm:^1.3.0, @scure/bip39@npm:^1.6.0": version: 1.6.0 resolution: "@scure/bip39@npm:1.6.0" dependencies: @@ -6638,32 +6945,32 @@ __metadata: languageName: node linkType: hard -"@solana/spl-token@npm:^0.3.8": - version: 0.3.11 - resolution: "@solana/spl-token@npm:0.3.11" +"@solana/spl-token@npm:0.4.14, @solana/spl-token@npm:^0.4.8, @solana/spl-token@npm:^0.4.9": + version: 0.4.14 + resolution: "@solana/spl-token@npm:0.4.14" dependencies: "@solana/buffer-layout": ^4.0.0 "@solana/buffer-layout-utils": ^0.2.0 - "@solana/spl-token-metadata": ^0.1.2 + "@solana/spl-token-group": ^0.0.7 + "@solana/spl-token-metadata": ^0.1.6 buffer: ^6.0.3 peerDependencies: - "@solana/web3.js": ^1.88.0 - checksum: 84faef5e8ed798e21870728817f650d572a0d0b8c8ac6591f75325d7e89831df396f48384083a65f8b79c30ea4cbfabd0ccb4fbc7a4f20953d133b746ed8b99d + "@solana/web3.js": ^1.95.5 + checksum: 71419c84f6c5bc0e0741b86c7c8448ec98e298164d930a89b5c2603bb38dbe6d111230959aa5d81675129e6061f3ce6cf4521808da3a448f6747202deda95c41 languageName: node linkType: hard -"@solana/spl-token@npm:^0.4.8, @solana/spl-token@npm:^0.4.9": - version: 0.4.14 - resolution: "@solana/spl-token@npm:0.4.14" +"@solana/spl-token@npm:^0.3.8": + version: 0.3.11 + resolution: "@solana/spl-token@npm:0.3.11" dependencies: "@solana/buffer-layout": ^4.0.0 "@solana/buffer-layout-utils": ^0.2.0 - "@solana/spl-token-group": ^0.0.7 - "@solana/spl-token-metadata": ^0.1.6 + "@solana/spl-token-metadata": ^0.1.2 buffer: ^6.0.3 peerDependencies: - "@solana/web3.js": ^1.95.5 - checksum: 71419c84f6c5bc0e0741b86c7c8448ec98e298164d930a89b5c2603bb38dbe6d111230959aa5d81675129e6061f3ce6cf4521808da3a448f6747202deda95c41 + "@solana/web3.js": ^1.88.0 + checksum: 84faef5e8ed798e21870728817f650d572a0d0b8c8ac6591f75325d7e89831df396f48384083a65f8b79c30ea4cbfabd0ccb4fbc7a4f20953d133b746ed8b99d languageName: node linkType: hard @@ -6753,7 +7060,7 @@ __metadata: languageName: node linkType: hard -"@solana/web3.js@npm:^1.32.0, @solana/web3.js@npm:^1.68.0, @solana/web3.js@npm:^1.78.0, @solana/web3.js@npm:^1.98.0": +"@solana/web3.js@npm:^1.32.0, @solana/web3.js@npm:^1.68.0, @solana/web3.js@npm:^1.78.0, @solana/web3.js@npm:^1.98.0, @solana/web3.js@npm:^1.98.4": version: 1.98.4 resolution: "@solana/web3.js@npm:1.98.4" dependencies: @@ -6803,7 +7110,7 @@ __metadata: languageName: node linkType: hard -"@ton/core@npm:^0.62.0": +"@ton/core@npm:0.62.0, @ton/core@npm:^0.62.0": version: 0.62.0 resolution: "@ton/core@npm:0.62.0" dependencies: @@ -7095,25 +7402,6 @@ __metadata: languageName: node linkType: hard -"@types/chai-as-promised@npm:^7.1.3": - version: 7.1.8 - resolution: "@types/chai-as-promised@npm:7.1.8" - dependencies: - "@types/chai": "*" - checksum: f0e5eab451b91bc1e289ed89519faf6591932e8a28d2ec9bbe95826eb73d28fe43713633e0c18706f3baa560a7d97e7c7c20dc53ce639e5d75bac46b2a50bf21 - languageName: node - linkType: hard - -"@types/chai@npm:*": - version: 5.2.3 - resolution: "@types/chai@npm:5.2.3" - dependencies: - "@types/deep-eql": "*" - assertion-error: ^2.0.1 - checksum: eb4c2da9ec38b474a983f39bfb5ec4fbcceb5e5d76d184094d2cbc4c41357973eb5769c8972cedac665a233251b0ed754f1e338fcf408d381968af85cdecc596 - languageName: node - linkType: hard - "@types/coingecko-api@npm:^1.0.10": version: 1.0.13 resolution: "@types/coingecko-api@npm:1.0.13" @@ -7139,13 +7427,6 @@ __metadata: languageName: node linkType: hard -"@types/deep-eql@npm:*": - version: 4.0.2 - resolution: "@types/deep-eql@npm:4.0.2" - checksum: 249a27b0bb22f6aa28461db56afa21ec044fa0e303221a62dff81831b20c8530502175f1a49060f7099e7be06181078548ac47c668de79ff9880241968d43d0c - languageName: node - linkType: hard - "@types/estree@npm:^1.0.6": version: 1.0.8 resolution: "@types/estree@npm:1.0.8" @@ -7815,21 +8096,6 @@ __metadata: languageName: node linkType: hard -"abitype@npm:1.0.6": - version: 1.0.6 - resolution: "abitype@npm:1.0.6" - peerDependencies: - typescript: ">=5.0.4" - zod: ^3 >=3.22.0 - peerDependenciesMeta: - typescript: - optional: true - zod: - optional: true - checksum: 0bf6ed5ec785f372746c3ec5d6c87bf4d8cf0b6db30867b8d24e86fbc66d9f6599ae3d463ccd49817e67eedec6deba7cdae317bcf4da85b02bc48009379b9f84 - languageName: node - linkType: hard - "abitype@npm:1.0.8": version: 1.0.8 resolution: "abitype@npm:1.0.8" @@ -7860,18 +8126,18 @@ __metadata: languageName: node linkType: hard -"abitype@npm:^0.9.8": - version: 0.9.10 - resolution: "abitype@npm:0.9.10" +"abitype@npm:1.2.3": + version: 1.2.3 + resolution: "abitype@npm:1.2.3" peerDependencies: typescript: ">=5.0.4" - zod: ^3 >=3.22.0 + zod: ^3.22.0 || ^4.0.0 peerDependenciesMeta: typescript: optional: true zod: optional: true - checksum: de703b58c221395f015c04a8512dde2cff2b9541c577a23cf205204604e624fbfd0e682e82f7954968d5e437cd0d7e630b1c159e73543881a4d0040238bfb13a + checksum: b5b5620f8e55a6dd7ae829630c0ded02b30f589f0f8f5ca931cdfcf6d7daa8154e30e3fe3593b3f6c4872a955ac55d447ccc2f801fd6a6aa698bdad966e3fe2e languageName: node linkType: hard @@ -8297,13 +8563,6 @@ __metadata: languageName: node linkType: hard -"assertion-error@npm:^2.0.1": - version: 2.0.1 - resolution: "assertion-error@npm:2.0.1" - checksum: a0789dd882211b87116e81e2648ccb7f60340b34f19877dd020b39ebb4714e475eb943e14ba3e22201c221ef6645b7bfe10297e76b6ac95b48a9898c1211ce66 - languageName: node - linkType: hard - "async-function@npm:^1.0.0": version: 1.0.0 resolution: "async-function@npm:1.0.0" @@ -8758,7 +9017,7 @@ __metadata: languageName: node linkType: hard -"bn.js@npm:^5.1.2, bn.js@npm:^5.2.0, bn.js@npm:^5.2.1": +"bn.js@npm:^5.1.2, bn.js@npm:^5.2.0, bn.js@npm:^5.2.1, bn.js@npm:^5.2.2": version: 5.2.2 resolution: "bn.js@npm:5.2.2" checksum: 4384d35fef785c757eb050bc1f13d60dd8e37662ca72392ae6678b35cfa2a2ae8f0494291086294683a7d977609c7878ac3cff08ecca7f74c3ca73f3acbadbe8 @@ -8796,6 +9055,13 @@ __metadata: languageName: node linkType: hard +"borsh@npm:^2.0.0": + version: 2.0.0 + resolution: "borsh@npm:2.0.0" + checksum: 1ef6b89e17564b97ee3932fea010fce0f00b02ac426ad32b14a749914b1d21ea68046f83555806fc8afd5e804539128fa337ac14e720dfb3c3fb5f579854a637 + languageName: node + linkType: hard + "bowser@npm:^2.11.0": version: 2.12.1 resolution: "bowser@npm:2.12.1" @@ -8838,13 +9104,6 @@ __metadata: languageName: node linkType: hard -"browser-stdout@npm:^1.3.1": - version: 1.3.1 - resolution: "browser-stdout@npm:1.3.1" - checksum: b717b19b25952dd6af483e368f9bcd6b14b87740c3d226c2977a65e84666ffd67000bddea7d911f111a9b6ddc822b234de42d52ab6507bce4119a4cc003ef7b3 - languageName: node - linkType: hard - "browserify-aes@npm:^1.2.0": version: 1.2.0 resolution: "browserify-aes@npm:1.2.0" @@ -9136,7 +9395,7 @@ __metadata: languageName: node linkType: hard -"camelcase@npm:^6.0.0, camelcase@npm:^6.2.0, camelcase@npm:^6.3.0": +"camelcase@npm:^6.2.0, camelcase@npm:^6.3.0": version: 6.3.0 resolution: "camelcase@npm:6.3.0" checksum: 8c96818a9076434998511251dcb2761a94817ea17dbdc37f47ac080bd088fc62c7369429a19e2178b993497132c8cbcf5cc1f44ba963e76782ba469c0474938d @@ -9175,17 +9434,6 @@ __metadata: languageName: node linkType: hard -"chai-as-promised@npm:^7.1.1": - version: 7.1.2 - resolution: "chai-as-promised@npm:7.1.2" - dependencies: - check-error: ^1.0.2 - peerDependencies: - chai: ">= 2.1.2 < 6" - checksum: 671ee980054eb23a523875c1d22929a2ac05d89b5428e1fd12800f54fc69baf41014667b87e2368e2355ee2a3140d3e3d7d5a1f8638b07cfefd7fe38a149e3f6 - languageName: node - linkType: hard - "chai-subset@npm:1.6.0": version: 1.6.0 resolution: "chai-subset@npm:1.6.0" @@ -9223,19 +9471,6 @@ __metadata: languageName: node linkType: hard -"chai@npm:^5.2.0": - version: 5.3.3 - resolution: "chai@npm:5.3.3" - dependencies: - assertion-error: ^2.0.1 - check-error: ^2.1.1 - deep-eql: ^5.0.1 - loupe: ^3.1.0 - pathval: ^2.0.0 - checksum: bc4091f1cccfee63f6a3d02ce477fe847f5c57e747916a11bd72675c9459125084e2e55dc2363ee2b82b088a878039ee7ee27c75d6d90f7de9202bf1b12ce573 - languageName: node - linkType: hard - "chalk@npm:^4.0.0, chalk@npm:^4.1.0, chalk@npm:^4.1.2": version: 4.1.2 resolution: "chalk@npm:4.1.2" @@ -9276,13 +9511,6 @@ __metadata: languageName: node linkType: hard -"check-error@npm:^2.1.1": - version: 2.1.1 - resolution: "check-error@npm:2.1.1" - checksum: d785ed17b1d4a4796b6e75c765a9a290098cf52ff9728ce0756e8ffd4293d2e419dd30c67200aee34202463b474306913f2fcfaf1890641026d9fc6966fea27a - languageName: node - linkType: hard - "chokidar@npm:^3.5.1, chokidar@npm:^3.5.3": version: 3.6.0 resolution: "chokidar@npm:3.6.0" @@ -9302,15 +9530,6 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:^4.0.1": - version: 4.0.3 - resolution: "chokidar@npm:4.0.3" - dependencies: - readdirp: ^4.0.1 - checksum: a8765e452bbafd04f3f2fad79f04222dd65f43161488bb6014a41099e6ca18d166af613d59a90771908c1c823efa3f46ba36b86ac50b701c20c1b9908c5fe36e - languageName: node - linkType: hard - "chownr@npm:^1.1.4": version: 1.1.4 resolution: "chownr@npm:1.1.4" @@ -10074,7 +10293,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5": +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4": version: 4.4.3 resolution: "debug@npm:4.4.3" dependencies: @@ -10112,13 +10331,6 @@ __metadata: languageName: node linkType: hard -"decamelize@npm:^4.0.0": - version: 4.0.0 - resolution: "decamelize@npm:4.0.0" - checksum: b7d09b82652c39eead4d6678bb578e3bebd848add894b76d0f6b395bc45b2d692fb88d977e7cfb93c4ed6c119b05a1347cef261174916c2e75c0a8ca57da1809 - languageName: node - linkType: hard - "decode-uri-component@npm:^0.2.0": version: 0.2.2 resolution: "decode-uri-component@npm:0.2.2" @@ -10156,7 +10368,7 @@ __metadata: languageName: node linkType: hard -"deep-eql@npm:^4.0.1, deep-eql@npm:^4.1.2, deep-eql@npm:^4.1.3": +"deep-eql@npm:^4.1.2, deep-eql@npm:^4.1.3": version: 4.1.4 resolution: "deep-eql@npm:4.1.4" dependencies: @@ -10165,13 +10377,6 @@ __metadata: languageName: node linkType: hard -"deep-eql@npm:^5.0.1": - version: 5.0.2 - resolution: "deep-eql@npm:5.0.2" - checksum: 6aaaadb4c19cbce42e26b2bbe5bd92875f599d2602635dc97f0294bae48da79e89470aedee05f449e0ca8c65e9fd7e7872624d1933a1db02713d99c2ca8d1f24 - languageName: node - linkType: hard - "deep-is@npm:^0.1.3": version: 0.1.4 resolution: "deep-is@npm:0.1.4" @@ -10311,13 +10516,6 @@ __metadata: languageName: node linkType: hard -"diff@npm:^7.0.0": - version: 7.0.0 - resolution: "diff@npm:7.0.0" - checksum: 5db0d339476b18dfbc8a08a7504fbcc74789eec626c8d20cf2cdd1871f1448962888128f4447c8f50a1e41a80decfe5e8489c375843b8cf1d42b7c2b611da4e1 - languageName: node - linkType: hard - "dir-glob@npm:^3.0.1": version: 3.0.1 resolution: "dir-glob@npm:3.0.1" @@ -11187,9 +11385,9 @@ __metadata: languageName: node linkType: hard -"ethers@npm:6.13.4": - version: 6.13.4 - resolution: "ethers@npm:6.13.4" +"ethers@npm:6.13.5": + version: 6.13.5 + resolution: "ethers@npm:6.13.5" dependencies: "@adraffy/ens-normalize": 1.10.1 "@noble/curves": 1.2.0 @@ -11198,13 +11396,13 @@ __metadata: aes-js: 4.0.0-beta.5 tslib: 2.7.0 ws: 8.17.1 - checksum: a64ad0f05ed7f79bf3092cd54ac11c3ed4a0a3fe8ee00a81053b5b4a34d84728c12fa5aa9bf3e2cc5efabbf1a0a37f62cd3a1852cf780a1ab619421fa03c2713 + checksum: 25700f75c3854fb5043b72748c7a4198efd15d50b4e66d575e6287aab707e855d9aa5ba342fe3d4a4c7943c84a46bcf3702b0f1da1307a82c40e1d08e86078ba languageName: node linkType: hard -"ethers@npm:6.13.5": - version: 6.13.5 - resolution: "ethers@npm:6.13.5" +"ethers@npm:6.16.0, ethers@npm:^6.0.0, ethers@npm:^6.13.5": + version: 6.16.0 + resolution: "ethers@npm:6.16.0" dependencies: "@adraffy/ens-normalize": 1.10.1 "@noble/curves": 1.2.0 @@ -11213,7 +11411,7 @@ __metadata: aes-js: 4.0.0-beta.5 tslib: 2.7.0 ws: 8.17.1 - checksum: 25700f75c3854fb5043b72748c7a4198efd15d50b4e66d575e6287aab707e855d9aa5ba342fe3d4a4c7943c84a46bcf3702b0f1da1307a82c40e1d08e86078ba + checksum: f96c54d35aa09d6700dbbe732db160d66f2a1acd59f2820e307869be478bb5c4c3fd0f34a5d51014cbea04200e6e9776290f521795492688c8d67052bf8a1e2a languageName: node linkType: hard @@ -11255,21 +11453,6 @@ __metadata: languageName: node linkType: hard -"ethers@npm:^6.0.0, ethers@npm:^6.13.5": - version: 6.16.0 - resolution: "ethers@npm:6.16.0" - dependencies: - "@adraffy/ens-normalize": 1.10.1 - "@noble/curves": 1.2.0 - "@noble/hashes": 1.3.2 - "@types/node": 22.7.5 - aes-js: 4.0.0-beta.5 - tslib: 2.7.0 - ws: 8.17.1 - checksum: f96c54d35aa09d6700dbbe732db160d66f2a1acd59f2820e307869be478bb5c4c3fd0f34a5d51014cbea04200e6e9776290f521795492688c8d67052bf8a1e2a - languageName: node - linkType: hard - "ethjs-unit@npm:0.1.6": version: 0.1.6 resolution: "ethjs-unit@npm:0.1.6" @@ -11527,6 +11710,13 @@ __metadata: languageName: node linkType: hard +"fast-equals@npm:^5.3.3": + version: 5.4.0 + resolution: "fast-equals@npm:5.4.0" + checksum: c6661f8b606ba3cb99c42aa23e3367c2ef9843c5564c81e79f1fcba5d299978feeed335fce16ea8a1e3fe609ca4caa1f7624eb808d6e01061a36011f07186ab2 + languageName: node + linkType: hard + "fast-fifo@npm:^1.3.2": version: 1.3.2 resolution: "fast-fifo@npm:1.3.2" @@ -11575,6 +11765,13 @@ __metadata: languageName: node linkType: hard +"fast-stringify@npm:^4.0.0": + version: 4.0.0 + resolution: "fast-stringify@npm:4.0.0" + checksum: 14476c22602a6afc27f7faf301ec098fec5546a2291caa57d4373d0d8232a0be4a3429c9517bd1c7e821d47fdd157088a1e9ae69ade37cd8e099e5b66ebf745d + languageName: node + linkType: hard + "fast-uri@npm:^3.0.1": version: 3.1.0 resolution: "fast-uri@npm:3.1.0" @@ -11726,15 +11923,6 @@ __metadata: languageName: node linkType: hard -"flat@npm:^5.0.2": - version: 5.0.2 - resolution: "flat@npm:5.0.2" - bin: - flat: cli.js - checksum: 12a1536ac746db74881316a181499a78ef953632ddd28050b7a3a43c62ef5462e3357c8c29d76072bb635f147f7a9a1f0c02efef6b4be28f8db62ceb3d5c7f5d - languageName: node - linkType: hard - "flatted@npm:^3.2.9": version: 3.3.3 resolution: "flatted@npm:3.3.3" @@ -12111,22 +12299,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.4.5": - version: 10.5.0 - resolution: "glob@npm:10.5.0" - dependencies: - foreground-child: ^3.1.0 - jackspeak: ^3.1.2 - minimatch: ^9.0.4 - minipass: ^7.1.2 - package-json-from-dist: ^1.0.0 - path-scurry: ^1.11.1 - bin: - glob: dist/esm/bin.mjs - checksum: cda96c074878abca9657bd984d2396945cf0d64283f6feeb40d738fe2da642be0010ad5210a1646244a5fc3511b0cab5a374569b3de5a12b8a63d392f18c6043 - languageName: node - linkType: hard - "glob@npm:^11.0.0": version: 11.0.3 resolution: "glob@npm:11.0.3" @@ -12261,6 +12433,23 @@ __metadata: languageName: node linkType: hard +"gql.tada@npm:^1.8.13": + version: 1.9.0 + resolution: "gql.tada@npm:1.9.0" + dependencies: + "@0no-co/graphql.web": ^1.0.5 + "@0no-co/graphqlsp": ^1.12.13 + "@gql.tada/cli-utils": 1.7.2 + "@gql.tada/internal": 1.0.8 + peerDependencies: + typescript: ^5.0.0 + bin: + gql-tada: bin/cli.js + gql.tada: bin/cli.js + checksum: 59c0c739e32f56e5ffb8baf13e19bee50e21ead1388387beb56f31d6f911218b2c39795638ee0ead48c51ebcf232e166d021c8114788017f4b4af0d2b8d0649d + languageName: node + linkType: hard + "graceful-fs@npm:^4.1.15, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" @@ -12275,6 +12464,13 @@ __metadata: languageName: node linkType: hard +"graphql@npm:^15.5.0 || ^16.0.0 || ^17.0.0, graphql@npm:^16.11.0": + version: 16.12.0 + resolution: "graphql@npm:16.12.0" + checksum: c0d2435425270c575091861c9fd82d7cebc1fb1bd5461e05c36521a988f69c5074461e27b89ab70851fabc72ec9d988235f288ba7bbeff67d08a973e8b9d6d3d + languageName: node + linkType: hard + "hamt-sharding@npm:^2.0.0": version: 2.0.1 resolution: "hamt-sharding@npm:2.0.1" @@ -12424,15 +12620,6 @@ __metadata: languageName: node linkType: hard -"he@npm:^1.2.0": - version: 1.2.0 - resolution: "he@npm:1.2.0" - bin: - he: bin/he - checksum: 3d4d6babccccd79c5c5a3f929a68af33360d6445587d628087f39a965079d84f18ce9c3d3f917ee1e3978916fc833bb8b29377c3b403f919426f91bc6965e7a7 - languageName: node - linkType: hard - "hmac-drbg@npm:^1.0.1": version: 1.0.1 resolution: "hmac-drbg@npm:1.0.1" @@ -13081,13 +13268,6 @@ __metadata: languageName: node linkType: hard -"is-path-inside@npm:^3.0.3": - version: 3.0.3 - resolution: "is-path-inside@npm:3.0.3" - checksum: abd50f06186a052b349c15e55b182326f1936c89a78bf6c8f2b707412517c097ce04bc49a0ca221787bc44e1049f51f09a2ffb63d22899051988d3a618ba13e9 - languageName: node - linkType: hard - "is-plain-obj@npm:^1.1.0": version: 1.1.0 resolution: "is-plain-obj@npm:1.1.0" @@ -13190,13 +13370,6 @@ __metadata: languageName: node linkType: hard -"is-unicode-supported@npm:^0.1.0": - version: 0.1.0 - resolution: "is-unicode-supported@npm:0.1.0" - checksum: a2aab86ee7712f5c2f999180daaba5f361bdad1efadc9610ff5b8ab5495b86e4f627839d085c6530363c6d6d4ecbde340fb8e54bdb83da4ba8e0865ed5513c52 - languageName: node - linkType: hard - "is-weakmap@npm:^2.0.2": version: 2.0.2 resolution: "is-weakmap@npm:2.0.2" @@ -13295,15 +13468,6 @@ __metadata: languageName: node linkType: hard -"isows@npm:1.0.6": - version: 1.0.6 - resolution: "isows@npm:1.0.6" - peerDependencies: - ws: "*" - checksum: ab9e85b50bcc3d70aa5ec875aa2746c5daf9321cb376ed4e5434d3c2643c5d62b1f466d93a05cd2ad0ead5297224922748c31707cb4fbd68f5d05d0479dce99c - languageName: node - linkType: hard - "isows@npm:1.0.7": version: 1.0.7 resolution: "isows@npm:1.0.7" @@ -14400,6 +14564,13 @@ __metadata: languageName: node linkType: hard +"js-base64@npm:^3.7.7": + version: 3.7.8 + resolution: "js-base64@npm:3.7.8" + checksum: 891746b0f23aea7dd466c5ef2d349b093944a25eca6093c09b2cbb99bc47a94237c63b91623bbc203306b7c72aab5112e90378544bceef3fd0eb9ab86d7af496 + languageName: node + linkType: hard + "js-sha3@npm:0.8.0, js-sha3@npm:^0.8.0": version: 0.8.0 resolution: "js-sha3@npm:0.8.0" @@ -14647,6 +14818,13 @@ __metadata: languageName: node linkType: hard +"jwt-decode@npm:^4.0.0": + version: 4.0.0 + resolution: "jwt-decode@npm:4.0.0" + checksum: 390e2edcb31a92e86c8cbdd1edeea4c0d62acd371f8a8f0a8878e499390c0ecf4c658b365c4e941e4ef37d0170e4ca650aaa49f99a45c0b9695a235b210154b0 + languageName: node + linkType: hard + "keccak@npm:^3.0.0": version: 3.0.4 resolution: "keccak@npm:3.0.4" @@ -14910,13 +15088,6 @@ __metadata: languageName: node linkType: hard -"lodash.isequal@npm:^4.5.0": - version: 4.5.0 - resolution: "lodash.isequal@npm:4.5.0" - checksum: da27515dc5230eb1140ba65ff8de3613649620e8656b19a6270afe4866b7bd461d9ba2ac8a48dcc57f7adac4ee80e1de9f965d89d4d81a0ad52bb3eec2609644 - languageName: node - linkType: hard - "lodash.isinteger@npm:^4.0.4": version: 4.0.4 resolution: "lodash.isinteger@npm:4.0.4" @@ -15022,13 +15193,10 @@ __metadata: languageName: node linkType: hard -"log-symbols@npm:^4.1.0": - version: 4.1.0 - resolution: "log-symbols@npm:4.1.0" - dependencies: - chalk: ^4.1.0 - is-unicode-supported: ^0.1.0 - checksum: fce1497b3135a0198803f9f07464165e9eb83ed02ceb2273930a6f8a508951178d8cf4f0378e9d28300a2ed2bc49050995d2bd5f53ab716bb15ac84d58c6ef74 +"loglevel@npm:^1.9.2": + version: 1.9.2 + resolution: "loglevel@npm:1.9.2" + checksum: 896c67b90a507bfcfc1e9a4daa7bf789a441dd70d95cd13b998d6dd46233a3bfadfb8fadb07250432bbfb53bf61e95f2520f9b11f9d3175cc460e5c251eca0af languageName: node linkType: hard @@ -15055,13 +15223,6 @@ __metadata: languageName: node linkType: hard -"loupe@npm:^3.1.0": - version: 3.2.1 - resolution: "loupe@npm:3.2.1" - checksum: 3ce9ecc5b2c56ffc073bf065ad3a4644cccce3eac81e61a8732e9c8ebfe05513ed478592d25f9dba24cfe82766913be045ab384c04711c7c6447deaf800ad94c - languageName: node - linkType: hard - "lower-case@npm:^2.0.2": version: 2.0.2 resolution: "lower-case@npm:2.0.2" @@ -15385,6 +15546,16 @@ __metadata: languageName: node linkType: hard +"micro-memoize@npm:^5.1.1": + version: 5.1.1 + resolution: "micro-memoize@npm:5.1.1" + dependencies: + fast-equals: ^5.3.3 + fast-stringify: ^4.0.0 + checksum: 6fea5c00f59df98bf01eed256fcd11f54929188082300ae130fd41739c4fabeff1dc1cfe231c4497f59b170ab19d9d532ff6b5e260af34c8cb6b781a28231cee + languageName: node + linkType: hard + "micromatch@npm:^4.0.4, micromatch@npm:^4.0.8": version: 4.0.8 resolution: "micromatch@npm:4.0.8" @@ -15496,7 +15667,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^9.0.4, minimatch@npm:^9.0.5": +"minimatch@npm:^9.0.4": version: 9.0.5 resolution: "minimatch@npm:9.0.5" dependencies: @@ -15656,38 +15827,6 @@ __metadata: languageName: node linkType: hard -"mocha@npm:^11.1.0": - version: 11.7.5 - resolution: "mocha@npm:11.7.5" - dependencies: - browser-stdout: ^1.3.1 - chokidar: ^4.0.1 - debug: ^4.3.5 - diff: ^7.0.0 - escape-string-regexp: ^4.0.0 - find-up: ^5.0.0 - glob: ^10.4.5 - he: ^1.2.0 - is-path-inside: ^3.0.3 - js-yaml: ^4.1.0 - log-symbols: ^4.1.0 - minimatch: ^9.0.5 - ms: ^2.1.3 - picocolors: ^1.1.1 - serialize-javascript: ^6.0.2 - strip-json-comments: ^3.1.1 - supports-color: ^8.1.1 - workerpool: ^9.2.0 - yargs: ^17.7.2 - yargs-parser: ^21.1.1 - yargs-unparser: ^2.0.0 - bin: - _mocha: bin/_mocha - mocha: bin/mocha.js - checksum: cdd0c29b4c86472dce7a3e476c3f6ea31e15ce2b11a65aa7e82261512a9b8aaa3f91b6276347d165a7e22361ba45ac535ef8e88bbacd54387cba37ba98a0a0f5 - languageName: node - linkType: hard - "mock-fs@npm:^4.1.0": version: 4.14.0 resolution: "mock-fs@npm:4.14.0" @@ -16310,13 +16449,6 @@ __metadata: languageName: node linkType: hard -"ordinal@npm:^1.0.3": - version: 1.0.3 - resolution: "ordinal@npm:1.0.3" - checksum: 6761c5b7606b6c4b0c22b4097dab4fe7ffcddacc49238eedf9c0ced877f5d4e4ad3f4fd43fefa1cc3f167cc54c7149267441b2ae85b81ccf13f45cf4b7947164 - languageName: node - linkType: hard - "os-tmpdir@npm:~1.0.2": version: 1.0.2 resolution: "os-tmpdir@npm:1.0.2" @@ -16638,13 +16770,6 @@ __metadata: languageName: node linkType: hard -"pathval@npm:^2.0.0": - version: 2.0.1 - resolution: "pathval@npm:2.0.1" - checksum: 280e71cfd86bb5d7ff371fe2752997e5fa82901fcb209abf19d4457b7814f1b4a17845dfb17bd28a596ccdb0ecea178720ce23dacfa9c841f37804b700647810 - languageName: node - linkType: hard - "pbkdf2@npm:^3.0.17": version: 3.1.3 resolution: "pbkdf2@npm:3.1.3" @@ -16909,6 +17034,13 @@ __metadata: languageName: node linkType: hard +"poseidon-lite@npm:0.2.1, poseidon-lite@npm:^0.2.0": + version: 0.2.1 + resolution: "poseidon-lite@npm:0.2.1" + checksum: ecd420d48ffafc99408f9ef6d124d21a0d12d089f1cfc20bcd97a0b0364e7526f8d4747a2b72da2157f18d569c9a4b19beb5958fd8509d87f8a65edf6979c168 + languageName: node + linkType: hard + "possible-typed-array-names@npm:^1.0.0": version: 1.1.0 resolution: "possible-typed-array-names@npm:1.1.0" @@ -17372,13 +17504,6 @@ __metadata: languageName: node linkType: hard -"readdirp@npm:^4.0.1": - version: 4.1.2 - resolution: "readdirp@npm:4.1.2" - checksum: 3242ee125422cb7c0e12d51452e993f507e6ed3d8c490bc8bf3366c5cdd09167562224e429b13e9cb2b98d4b8b2b11dc100d3c73883aa92d657ade5a21ded004 - languageName: node - linkType: hard - "readdirp@npm:~3.6.0": version: 3.6.0 resolution: "readdirp@npm:3.6.0" @@ -17926,15 +18051,6 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.7.3": - version: 7.7.3 - resolution: "semver@npm:7.7.3" - bin: - semver: bin/semver.js - checksum: f013a3ee4607857bcd3503b6ac1d80165f7f8ea94f5d55e2d3e33df82fce487aa3313b987abf9b39e0793c83c9fc67b76c36c067625141a9f6f704ae0ea18db2 - languageName: node - linkType: hard - "send@npm:0.19.0": version: 0.19.0 resolution: "send@npm:0.19.0" @@ -17965,15 +18081,6 @@ __metadata: languageName: node linkType: hard -"serialize-javascript@npm:^6.0.2": - version: 6.0.2 - resolution: "serialize-javascript@npm:6.0.2" - dependencies: - randombytes: ^2.1.0 - checksum: c4839c6206c1d143c0f80763997a361310305751171dd95e4b57efee69b8f6edd8960a0b7fbfc45042aadff98b206d55428aee0dc276efe54f100899c7fa8ab7 - languageName: node - linkType: hard - "serve-static@npm:1.16.2": version: 1.16.2 resolution: "serve-static@npm:1.16.2" @@ -18755,6 +18862,13 @@ __metadata: languageName: node linkType: hard +"tagged-tag@npm:^1.0.0": + version: 1.0.0 + resolution: "tagged-tag@npm:1.0.0" + checksum: e37653df3e495daa7ea7790cb161b810b00075bba2e4d6c93fb06a709e747e3ae9da11a120d0489833203926511b39e038a2affbd9d279cfb7a2f3fcccd30b5d + languageName: node + linkType: hard + "tar@npm:^4.0.2": version: 4.4.19 resolution: "tar@npm:4.4.19" @@ -19121,46 +19235,6 @@ __metadata: languageName: node linkType: hard -"ts-jest@npm:^29.2.5": - version: 29.4.6 - resolution: "ts-jest@npm:29.4.6" - dependencies: - bs-logger: ^0.2.6 - fast-json-stable-stringify: ^2.1.0 - handlebars: ^4.7.8 - json5: ^2.2.3 - lodash.memoize: ^4.1.2 - make-error: ^1.3.6 - semver: ^7.7.3 - type-fest: ^4.41.0 - yargs-parser: ^21.1.1 - peerDependencies: - "@babel/core": ">=7.0.0-beta.0 <8" - "@jest/transform": ^29.0.0 || ^30.0.0 - "@jest/types": ^29.0.0 || ^30.0.0 - babel-jest: ^29.0.0 || ^30.0.0 - jest: ^29.0.0 || ^30.0.0 - jest-util: ^29.0.0 || ^30.0.0 - typescript: ">=4.3 <6" - peerDependenciesMeta: - "@babel/core": - optional: true - "@jest/transform": - optional: true - "@jest/types": - optional: true - babel-jest: - optional: true - esbuild: - optional: true - jest-util: - optional: true - bin: - ts-jest: cli.js - checksum: 07ae4102569565ab57036f095152ea75c85032edf15379043ffc8da2dd0e6e93e84d0c50a24e10a5cddacb5ab773df0f3170f02db6c178edd22a5e485bc57dc7 - languageName: node - linkType: hard - "ts-jest@npm:^29.4.0": version: 29.4.2 resolution: "ts-jest@npm:29.4.2" @@ -19442,6 +19516,15 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^5.3.1": + version: 5.3.1 + resolution: "type-fest@npm:5.3.1" + dependencies: + tagged-tag: ^1.0.0 + checksum: 5edbde057da53a22ba04b8169537ec2a12bb36abe2bce1d855a2a3dbbb72c55b9c088a5162bc4c31a8223e2d27fcc3f838a9644aa07fb6aa018d3c495648d38a + languageName: node + linkType: hard + "type-is@npm:~1.6.18": version: 1.6.18 resolution: "type-is@npm:1.6.18" @@ -19531,16 +19614,6 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.8.2": - version: 5.9.3 - resolution: "typescript@npm:5.9.3" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 0d0ffb84f2cd072c3e164c79a2e5a1a1f4f168e84cb2882ff8967b92afe1def6c2a91f6838fb58b168428f9458c57a2ba06a6737711fdd87a256bbe83e9a217f - languageName: node - linkType: hard - "typescript@patch:typescript@5.7.2#~builtin": version: 5.7.2 resolution: "typescript@patch:typescript@npm%3A5.7.2#~builtin::version=5.7.2&hash=ad5954" @@ -19551,16 +19624,6 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@^5.8.2#~builtin": - version: 5.9.3 - resolution: "typescript@patch:typescript@npm%3A5.9.3#~builtin::version=5.9.3&hash=ad5954" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 8bb8d86819ac86a498eada254cad7fb69c5f74778506c700c2a712daeaff21d3a6f51fd0d534fe16903cb010d1b74f89437a3d02d4d0ff5ca2ba9a4660de8497 - languageName: node - linkType: hard - "ua-parser-js@npm:^1.0.35": version: 1.0.41 resolution: "ua-parser-js@npm:1.0.41" @@ -19922,6 +19985,18 @@ __metadata: languageName: node linkType: hard +"valibot@npm:^1.2.0": + version: 1.2.0 + resolution: "valibot@npm:1.2.0" + peerDependencies: + typescript: ">=5" + peerDependenciesMeta: + typescript: + optional: true + checksum: 2d63ef5e45dc9b0d430640e908f07aa7172e8a3ee1653d92f99d8f5e0b84558e0829a2cda8c75150df91eb2f51ba3222d055753336ea6ca3af82e7ded4e71703 + languageName: node + linkType: hard + "validate-npm-package-license@npm:^3.0.1": version: 3.0.4 resolution: "validate-npm-package-license@npm:3.0.4" @@ -19971,28 +20046,6 @@ __metadata: languageName: node linkType: hard -"viem@npm:2.21.25": - version: 2.21.25 - resolution: "viem@npm:2.21.25" - dependencies: - "@adraffy/ens-normalize": 1.11.0 - "@noble/curves": 1.6.0 - "@noble/hashes": 1.5.0 - "@scure/bip32": 1.5.0 - "@scure/bip39": 1.4.0 - abitype: 1.0.6 - isows: 1.0.6 - webauthn-p256: 0.0.10 - ws: 8.18.0 - peerDependencies: - typescript: ">=5.0.4" - peerDependenciesMeta: - typescript: - optional: true - checksum: 65081b5bb80d81addd90300d6103a4841212e86071d12b3e9d8042deec27b800882a6b476e5a284587272ec6c7c1b4df6be19aed44237a939e9eb3babd39d01a - languageName: node - linkType: hard - "viem@npm:2.33.3": version: 2.33.3 resolution: "viem@npm:2.33.3" @@ -20347,16 +20400,6 @@ __metadata: languageName: node linkType: hard -"webauthn-p256@npm:0.0.10": - version: 0.0.10 - resolution: "webauthn-p256@npm:0.0.10" - dependencies: - "@noble/curves": ^1.4.0 - "@noble/hashes": ^1.4.0 - checksum: 0648a3d78451bfa7105b5151a34bd685ee60e193be9be1981fe73819ed5a92f410973bdeb72427ef03c8c2a848619f818cf3e66b94012d5127b462cb10c24f5d - languageName: node - linkType: hard - "webidl-conversions@npm:^3.0.0": version: 3.0.1 resolution: "webidl-conversions@npm:3.0.1" @@ -20506,13 +20549,6 @@ __metadata: languageName: node linkType: hard -"workerpool@npm:^9.2.0": - version: 9.3.4 - resolution: "workerpool@npm:9.3.4" - checksum: 309c08c10fed93623a2d8954b10277a35b3ffba2f7f33fe4be48fae5d00c9502809ef09ddc67fc8ae2cc19a2abe7d7233bfb2b23801bd010dc2b49842f5ea0de - languageName: node - linkType: hard - "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" @@ -20813,6 +20849,15 @@ __metadata: languageName: node linkType: hard +"yaml@npm:2.8.2, yaml@npm:^2.7.0": + version: 2.8.2 + resolution: "yaml@npm:2.8.2" + bin: + yaml: bin.mjs + checksum: 5ffd9f23bc7a450129cbd49dcf91418988f154ede10c83fd28ab293661ac2783c05da19a28d76a22cbd77828eae25d4bd7453f9a9fe2d287d085d72db46fd105 + languageName: node + linkType: hard + "yaml@npm:^2.4.1": version: 2.8.1 resolution: "yaml@npm:2.8.1" @@ -20846,18 +20891,6 @@ __metadata: languageName: node linkType: hard -"yargs-unparser@npm:^2.0.0": - version: 2.0.0 - resolution: "yargs-unparser@npm:2.0.0" - dependencies: - camelcase: ^6.0.0 - decamelize: ^4.0.0 - flat: ^5.0.2 - is-plain-obj: ^2.1.0 - checksum: 68f9a542c6927c3768c2f16c28f71b19008710abd6b8f8efbac6dcce26bbb68ab6503bed1d5994bdbc2df9a5c87c161110c1dfe04c6a3fe5c6ad1b0e15d9a8a3 - languageName: node - linkType: hard - "yargs@npm:^15.0.2": version: 15.4.1 resolution: "yargs@npm:15.4.1"