diff --git a/.claude/skills/chain-integration/SKILL.md b/.claude/skills/chain-integration/SKILL.md index 8ee4eb20069..9e54b7d0614 100644 --- a/.claude/skills/chain-integration/SKILL.md +++ b/.claude/skills/chain-integration/SKILL.md @@ -698,8 +698,85 @@ export const SECOND_CLASS_CHAINS = [ **Directory**: `packages/chain-adapters/src/[adaptertype]/[chainname]/` -**For EVM chains**: Extend `EvmBaseAdapter` (see Monad example) -**For non-EVM**: Implement `IChainAdapter` interface (see Sui/Tron examples) +#### **For EVM Chains** (SIMPLE!) + +Extend `SecondClassEvmAdapter` - you only need ~50 lines! + +**File**: `packages/chain-adapters/src/evm/[chainname]/[ChainName]ChainAdapter.ts` + +```typescript +import { ASSET_REFERENCE, [chainLower]AssetId } from '@shapeshiftoss/caip' +import type { AssetId } from '@shapeshiftoss/caip' +import type { RootBip44Params } from '@shapeshiftoss/types' +import { KnownChainIds } from '@shapeshiftoss/types' + +import { ChainAdapterDisplayName } from '../../types' +import { SecondClassEvmAdapter } from '../SecondClassEvmAdapter' +import type { TokenInfo } from '../SecondClassEvmAdapter' + +const SUPPORTED_CHAIN_IDS = [KnownChainIds.[ChainName]Mainnet] +const DEFAULT_CHAIN_ID = KnownChainIds.[ChainName]Mainnet + +export type ChainAdapterArgs = { + rpcUrl: string + knownTokens?: TokenInfo[] +} + +export const is[ChainName]ChainAdapter = (adapter: unknown): adapter is ChainAdapter => { + return (adapter as ChainAdapter).getType() === KnownChainIds.[ChainName]Mainnet +} + +export class ChainAdapter extends SecondClassEvmAdapter { + public static readonly rootBip44Params: RootBip44Params = { + purpose: 44, + coinType: Number(ASSET_REFERENCE.[ChainName]), + accountNumber: 0, + } + + constructor(args: ChainAdapterArgs) { + super({ + assetId: [chainLower]AssetId, + chainId: DEFAULT_CHAIN_ID, + rootBip44Params: ChainAdapter.rootBip44Params, + supportedChainIds: SUPPORTED_CHAIN_IDS, + rpcUrl: args.rpcUrl, + knownTokens: args.knownTokens ?? [], + }) + } + + getDisplayName() { + return ChainAdapterDisplayName.[ChainName] + } + + getName() { + return '[ChainName]' + } + + getType(): KnownChainIds.[ChainName]Mainnet { + return KnownChainIds.[ChainName]Mainnet + } + + getFeeAssetId(): AssetId { + return this.assetId + } +} + +export type { TokenInfo } +``` + +**That's it!** SecondClassEvmAdapter automatically provides: +- ✅ Account balance fetching (native + ERC-20 tokens via multicall) +- ✅ Fee estimation +- ✅ Transaction broadcasting +- ✅ Transaction parsing with ERC-20 event decoding (for execution price) +- ✅ Rate limiting via PQueue +- ✅ Multicall batching for token balances + +Just follow the pattern from HyperEVM, Monad, or Plasma adapters. + +#### **For Non-EVM Chains** (COMPLEX) + +Implement `IChainAdapter` interface - requires custom crypto adapters and ~500-1000 lines. **Key Methods to Implement:** - `getAccount()` - Get balances (native + tokens) @@ -712,14 +789,13 @@ export const SECOND_CLASS_CHAINS = [ - `getTxHistory()` - Get tx history (stub out - return empty) **Poor Man's Patterns:** -1. **No Unchained**: Use public RPC directly (ethers.js, @mysten/sui, tronweb, etc.) +1. **No Unchained**: Use public RPC directly (@mysten/sui, tronweb, etc.) 2. **No TX History**: Stub out `getTxHistory()` to return empty array -3. **Multicall for Tokens**: Batch token balance calls where possible -4. **Direct RPC Polling**: Use `eth_getTransactionReceipt` or equivalent for tx status +3. **Direct RPC Polling**: Use chain-specific RPC for tx status -**File**: `packages/chain-adapters/src/[adaptertype]/[chainname]/[ChainName]ChainAdapter.ts` +**File**: `packages/chain-adapters/src/[chainname]/[ChainName]ChainAdapter.ts` -See `MonadChainAdapter.ts` (EVM) or `SuiChainAdapter.ts` (non-EVM) for complete examples. +See `SuiChainAdapter.ts` or `TronChainAdapter.ts` for complete examples. **Export**: ```typescript diff --git a/packages/chain-adapters/src/evm/SecondClassEvmAdapter.ts b/packages/chain-adapters/src/evm/SecondClassEvmAdapter.ts new file mode 100644 index 00000000000..a2c19aea666 --- /dev/null +++ b/packages/chain-adapters/src/evm/SecondClassEvmAdapter.ts @@ -0,0 +1,511 @@ +import type { AssetId, ChainId } from '@shapeshiftoss/caip' +import { ASSET_NAMESPACE, toAssetId } from '@shapeshiftoss/caip' +import type { evm } from '@shapeshiftoss/common-api' +import { MULTICALL3_CONTRACT, viemClientByChainId } from '@shapeshiftoss/contracts' +import type { EvmChainId, RootBip44Params } from '@shapeshiftoss/types' +import { TransferType, TxStatus } from '@shapeshiftoss/unchained-client' +import { Contract, Interface, JsonRpcProvider } from 'ethers' +import PQueue from 'p-queue' +import type { Hex } from 'viem' +import { erc20Abi, getAddress, isAddressEqual, multicall3Abi, parseEventLogs } from 'viem' + +import { ErrorHandler } from '../error/ErrorHandler' +import type { + Account, + BroadcastTransactionInput, + FeeDataEstimate, + GetFeeDataInput, + SubscribeError, + SubscribeTxsInput, + Transaction, + TxHistoryInput, + TxHistoryResponse, +} from '../types' +import { CONTRACT_INTERACTION } from '../types' +import { bn, bnOrZero } from '../utils/bignumber' +import { assertAddressNotSanctioned } from '../utils/validateAddress' +import { EvmBaseAdapter } from './EvmBaseAdapter' +import type { GasFeeDataEstimate } from './types' + +const ERC20_ABI = ['function balanceOf(address) view returns (uint256)'] +const BATCH_SIZE = 500 + +export type TokenInfo = { + assetId: AssetId + contractAddress: string + symbol: string + name: string + precision: number +} + +export type SecondClassEvmAdapterArgs = { + assetId: AssetId + chainId: T + rootBip44Params: RootBip44Params + supportedChainIds: ChainId[] + rpcUrl: string + knownTokens: TokenInfo[] +} + +export abstract class SecondClassEvmAdapter extends EvmBaseAdapter { + protected provider: JsonRpcProvider + protected multicall: Contract + protected erc20Interface: Interface + protected knownTokens: TokenInfo[] + private requestQueue: PQueue + + constructor(args: SecondClassEvmAdapterArgs) { + const dummyParser = { + parse: () => { + throw new Error('Transaction parsing is not supported for second-class chains') + }, + } as any + + super({ + assetId: args.assetId, + chainId: args.chainId, + rootBip44Params: args.rootBip44Params, + parser: dummyParser, + supportedChainIds: args.supportedChainIds, + providers: {} as any, + rpcUrl: args.rpcUrl, + }) + + this.provider = new JsonRpcProvider(args.rpcUrl, undefined, { + staticNetwork: true, + }) + + this.multicall = new Contract(MULTICALL3_CONTRACT, multicall3Abi, this.provider) + this.erc20Interface = new Interface(ERC20_ABI) + this.knownTokens = args.knownTokens + this.requestQueue = new PQueue({ + intervalCap: 1, + interval: 50, + concurrency: 1, + }) + } + + async getAccount(pubkey: string): Promise> { + try { + const [balance, nonce] = await Promise.all([ + this.requestQueue.add(() => this.provider.getBalance(pubkey)), + this.requestQueue.add(() => this.provider.getTransactionCount(pubkey)), + ]) + + let tokens: { + assetId: AssetId + balance: string + symbol: string + name: string + precision: number + }[] = [] + + if (this.knownTokens.length > 0) { + tokens = await this.getTokenBalancesMulticall(pubkey, this.knownTokens) + } + + return { + balance: balance.toString(), + chainId: this.chainId, + assetId: this.assetId, + chain: this.getType(), + chainSpecific: { + nonce, + tokens: tokens.filter(t => t.balance !== '0'), + }, + pubkey, + } as Account + } catch (err) { + throw new Error(`Failed to get account: ${err}`) + } + } + + private async getTokenBalancesMulticall( + pubkey: string, + tokens: TokenInfo[], + ): Promise< + { + assetId: AssetId + balance: string + symbol: string + name: string + precision: number + }[] + > { + try { + const results: { + assetId: AssetId + balance: string + symbol: string + name: string + precision: number + }[] = [] + + for (let i = 0; i < tokens.length; i += BATCH_SIZE) { + const batch = tokens.slice(i, i + BATCH_SIZE) + const batchResults = await this.multicallBatch(pubkey, batch) + results.push(...batchResults) + } + + return results + } catch (error) { + console.warn(`[${this.getName()}] Multicall failed, falling back to individual calls:`, error) + return this.getTokenBalancesIndividual(pubkey, tokens) + } + } + + private async multicallBatch( + pubkey: string, + tokens: TokenInfo[], + ): Promise< + { + assetId: AssetId + balance: string + symbol: string + name: string + precision: number + }[] + > { + const calls = tokens.map(token => ({ + target: token.contractAddress, + allowFailure: true, + callData: this.erc20Interface.encodeFunctionData('balanceOf', [pubkey]), + })) + + const results = await this.requestQueue.add(() => this.multicall.aggregate3(calls)) + + return tokens + .map((token, i) => { + const { success, returnData } = results[i] + + if (!success || returnData === '0x') { + return null + } + + try { + const [balance] = this.erc20Interface.decodeFunctionResult('balanceOf', returnData) + + return { + assetId: token.assetId, + balance: balance.toString(), + symbol: token.symbol, + name: token.name, + precision: token.precision, + } + } catch { + return null + } + }) + .filter((result): result is NonNullable => result !== null) + } + + private async getTokenBalancesIndividual( + pubkey: string, + tokens: TokenInfo[], + ): Promise< + { + assetId: AssetId + balance: string + symbol: string + name: string + precision: number + }[] + > { + const results = await Promise.all( + tokens.map(async token => { + try { + const contract = new Contract(token.contractAddress, ERC20_ABI, this.provider) + + const balance = await this.requestQueue.add(() => contract.balanceOf(pubkey)) + + return { + assetId: token.assetId, + balance: balance.toString(), + symbol: token.symbol, + name: token.name, + precision: token.precision, + } + } catch { + return null + } + }), + ) + + return results.filter((result): result is NonNullable => result !== null) + } + + async getGasFeeData(): Promise { + try { + const feeData = await this.requestQueue.add(() => this.provider.getFeeData()) + + const gasPrice = feeData.gasPrice?.toString() ?? '0' + const maxFeePerGas = feeData.maxFeePerGas?.toString() + const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas?.toString() + + const fees = { + gasPrice, + ...(maxFeePerGas && maxPriorityFeePerGas ? { maxFeePerGas, maxPriorityFeePerGas } : {}), + } + + return { + fast: fees, + average: fees, + slow: fees, + } + } catch (err) { + throw new Error(`Failed to get gas fee data: ${err}`) + } + } + + async getFeeData(input: GetFeeDataInput): Promise> { + try { + const estimateGasBody = this.buildEstimateGasBody(input) + + const gasLimit = await this.requestQueue.add(() => + this.provider.estimateGas({ + from: estimateGasBody.from, + to: estimateGasBody.to, + value: estimateGasBody.value ? BigInt(estimateGasBody.value) : undefined, + data: estimateGasBody.data, + }), + ) + + const { fast, average, slow } = await this.getGasFeeData() + + const gasLimitString = gasLimit.toString() + + return { + fast: { + txFee: bnOrZero(fast.maxFeePerGas ?? fast.gasPrice) + .times(gasLimitString) + .toFixed(0), + chainSpecific: { gasLimit: gasLimitString, ...fast }, + }, + average: { + txFee: bnOrZero(average.maxFeePerGas ?? average.gasPrice) + .times(gasLimitString) + .toFixed(0), + chainSpecific: { gasLimit: gasLimitString, ...average }, + }, + slow: { + txFee: bnOrZero(slow.maxFeePerGas ?? slow.gasPrice) + .times(gasLimitString) + .toFixed(0), + chainSpecific: { gasLimit: gasLimitString, ...slow }, + }, + } as FeeDataEstimate + } catch (err) { + throw new Error(`Failed to get fee data: ${err}`) + } + } + + async broadcastTransaction({ + senderAddress, + receiverAddress, + hex, + }: BroadcastTransactionInput): Promise { + try { + await Promise.all([ + assertAddressNotSanctioned(senderAddress), + receiverAddress !== CONTRACT_INTERACTION && assertAddressNotSanctioned(receiverAddress), + ]) + + const txResponse = await this.requestQueue.add(() => this.provider.broadcastTransaction(hex)) + return txResponse.hash + } catch (err) { + return ErrorHandler(err, { + translation: 'chainAdapters.errors.broadcastTransaction', + }) + } + } + + unsubscribeTxs(_input?: SubscribeTxsInput): void { + return + } + + subscribeTxs( + _input: SubscribeTxsInput, + _onMessage: (msg: Transaction) => void, + _onError: (err: SubscribeError) => void, + ): Promise { + return Promise.resolve() + } + + getTxHistory(_input: TxHistoryInput): Promise { + return Promise.resolve({ + cursor: '', + pubkey: _input.pubkey, + transactions: [], + txIds: [], + }) + } + + async parseTx(txHash: unknown, pubkey: string): Promise { + const hash = txHash as Hex + const viemClient = viemClientByChainId[this.chainId] + + if (!viemClient) { + throw new Error(`No viem client found for chainId: ${this.chainId}`) + } + + try { + const [transaction, receipt] = await Promise.all([ + viemClient.getTransaction({ hash }), + viemClient.getTransactionReceipt({ hash }), + ]) + + if (!transaction || !receipt) { + throw new Error(`Transaction not found: ${hash}`) + } + + const block = receipt.blockHash + ? await viemClient.getBlock({ blockHash: receipt.blockHash }).catch(() => null) + : null + + const transferLogs = parseEventLogs({ + abi: erc20Abi, + logs: receipt.logs, + eventName: 'Transfer', + }) + + const tokenTransfers: evm.TokenTransfer[] = transferLogs + .map(log => { + const tokenInfo = this.knownTokens.find( + t => getAddress(t.contractAddress) === getAddress(log.address), + ) + + if (!tokenInfo) return null + + return { + contract: getAddress(log.address), + decimals: tokenInfo.precision, + name: tokenInfo.name, + symbol: tokenInfo.symbol, + type: 'ERC20' as const, + from: getAddress(log.args.from), + to: getAddress(log.args.to), + value: log.args.value.toString(), + } + }) + .filter((t): t is NonNullable => t !== null) + + const timestamp = block?.timestamp ? Number(block.timestamp) : 0 + const blockNumber = receipt.blockNumber ? Number(receipt.blockNumber) : 0 + const currentBlockNumber = await viemClient.getBlockNumber() + const confirmationsCount = blockNumber > 0 ? Number(currentBlockNumber) - blockNumber + 1 : 0 + const status = receipt.status === 'success' ? 1 : 0 + const fee = bnOrZero(receipt.gasUsed.toString()) + .times(receipt.effectiveGasPrice.toString()) + .toFixed(0) + + const parsedTx: evm.Tx = { + txid: transaction.hash, + blockHash: receipt.blockHash ?? '', + blockHeight: blockNumber, + timestamp, + confirmations: confirmationsCount, + status, + from: getAddress(transaction.from), + to: getAddress(transaction.to ?? '0x0'), + value: transaction.value.toString(), + fee, + gasLimit: transaction.gas.toString(), + gasUsed: receipt.gasUsed.toString(), + gasPrice: receipt.effectiveGasPrice.toString(), + inputData: transaction.input, + tokenTransfers, + } + + return this.parse(parsedTx, pubkey) + } catch (error) { + throw new Error(`Failed to parse transaction: ${error}`) + } + } + + private parse(tx: evm.Tx, pubkey: string): Transaction { + const address = getAddress(pubkey) + const txFrom = getAddress(tx.from) + const txTo = getAddress(tx.to) + const isSend = isAddressEqual(address, txFrom) + const isReceive = isAddressEqual(address, txTo) + const status = tx.status === 1 ? TxStatus.Confirmed : TxStatus.Failed + + const nativeTransfers = [] + if (isSend && bn(tx.value).gt(0)) { + nativeTransfers.push({ + assetId: this.assetId, + from: [tx.from], + to: [tx.to], + type: TransferType.Send, + value: tx.value, + }) + } + + if (isReceive && bn(tx.value).gt(0)) { + nativeTransfers.push({ + assetId: this.assetId, + from: [tx.from], + to: [tx.to], + type: TransferType.Receive, + value: tx.value, + }) + } + + const tokenTransfers = + tx.tokenTransfers?.flatMap(transfer => { + const transferFrom = getAddress(transfer.from) + const transferTo = getAddress(transfer.to) + const assetId = toAssetId({ + chainId: this.chainId, + assetNamespace: ASSET_NAMESPACE.erc20, + assetReference: transfer.contract, + }) + + const token = { + contract: transfer.contract, + decimals: transfer.decimals, + name: transfer.name, + symbol: transfer.symbol, + } + + const transfers = [] + + if (isAddressEqual(address, transferFrom)) { + transfers.push({ + assetId, + from: [transfer.from], + to: [transfer.to], + type: TransferType.Send, + value: transfer.value, + token, + }) + } + + if (isAddressEqual(address, transferTo)) { + transfers.push({ + assetId, + from: [transfer.from], + to: [transfer.to], + type: TransferType.Receive, + value: transfer.value, + token, + }) + } + + return transfers + }) ?? [] + + return { + blockHash: tx.blockHash, + blockHeight: tx.blockHeight, + blockTime: tx.timestamp, + chainId: this.chainId, + confirmations: tx.confirmations, + status, + transfers: [...nativeTransfers, ...tokenTransfers], + txid: tx.txid, + pubkey, + ...(isSend && { fee: { assetId: this.assetId, value: tx.fee } }), + } + } +} diff --git a/packages/chain-adapters/src/evm/hyperevm/HyperEvmChainAdapter.ts b/packages/chain-adapters/src/evm/hyperevm/HyperEvmChainAdapter.ts index 202edc2853f..60e7f5bf1b8 100644 --- a/packages/chain-adapters/src/evm/hyperevm/HyperEvmChainAdapter.ts +++ b/packages/chain-adapters/src/evm/hyperevm/HyperEvmChainAdapter.ts @@ -1,45 +1,15 @@ import type { AssetId } from '@shapeshiftoss/caip' import { ASSET_REFERENCE, hyperEvmAssetId } from '@shapeshiftoss/caip' -import { MULTICALL3_CONTRACT } from '@shapeshiftoss/contracts' import type { RootBip44Params } from '@shapeshiftoss/types' import { KnownChainIds } from '@shapeshiftoss/types' -import { Contract, Interface, JsonRpcProvider } from 'ethers' -import PQueue from 'p-queue' -import { multicall3Abi } from 'viem' -import { ErrorHandler } from '../../error/ErrorHandler' -import type { - Account, - BroadcastTransactionInput, - FeeDataEstimate, - GetFeeDataInput, - SubscribeError, - SubscribeTxsInput, - Transaction, - TxHistoryInput, - TxHistoryResponse, -} from '../../types' -import { ChainAdapterDisplayName, CONTRACT_INTERACTION } from '../../types' -import { bnOrZero } from '../../utils/bignumber' -import { assertAddressNotSanctioned } from '../../utils/validateAddress' -import { EvmBaseAdapter } from '../EvmBaseAdapter' -import type { GasFeeDataEstimate } from '../types' +import { ChainAdapterDisplayName } from '../../types' +import type { TokenInfo } from '../SecondClassEvmAdapter' +import { SecondClassEvmAdapter } from '../SecondClassEvmAdapter' const SUPPORTED_CHAIN_IDS = [KnownChainIds.HyperEvmMainnet] const DEFAULT_CHAIN_ID = KnownChainIds.HyperEvmMainnet -const ERC20_ABI = ['function balanceOf(address) view returns (uint256)'] - -const BATCH_SIZE = 500 // Process 500 tokens per multicall to avoid gas/RPC limits - -export type TokenInfo = { - assetId: AssetId - contractAddress: string - symbol: string - name: string - precision: number -} - export type ChainAdapterArgs = { rpcUrl: string knownTokens?: TokenInfo[] @@ -49,48 +19,21 @@ export const isHyperEvmChainAdapter = (adapter: unknown): adapter is ChainAdapte return (adapter as ChainAdapter).getType() === KnownChainIds.HyperEvmMainnet } -export class ChainAdapter extends EvmBaseAdapter { +export class ChainAdapter extends SecondClassEvmAdapter { public static readonly rootBip44Params: RootBip44Params = { purpose: 44, coinType: Number(ASSET_REFERENCE.HyperEvm), accountNumber: 0, } - protected provider: JsonRpcProvider - protected multicall: Contract - protected erc20Interface: Interface - protected knownTokens: TokenInfo[] - private requestQueue: PQueue - constructor(args: ChainAdapterArgs) { - // Create a dummy parser - we won't use it since we don't support tx history - const dummyParser = { - parse: () => { - throw new Error('Transaction parsing is not supported for HyperEVM') - }, - } as any - super({ assetId: hyperEvmAssetId, chainId: DEFAULT_CHAIN_ID, rootBip44Params: ChainAdapter.rootBip44Params, - parser: dummyParser, supportedChainIds: SUPPORTED_CHAIN_IDS, - providers: {} as any, // We don't use unchained providers rpcUrl: args.rpcUrl, - }) - - this.provider = new JsonRpcProvider(args.rpcUrl, undefined, { - staticNetwork: true, - }) - - this.multicall = new Contract(MULTICALL3_CONTRACT, multicall3Abi, this.provider) - this.erc20Interface = new Interface(ERC20_ABI) - this.knownTokens = args.knownTokens ?? [] - this.requestQueue = new PQueue({ - intervalCap: 1, - interval: 50, - concurrency: 1, + knownTokens: args.knownTokens ?? [], }) } @@ -109,279 +52,6 @@ export class ChainAdapter extends EvmBaseAdapter getFeeAssetId(): AssetId { return this.assetId } - - async getAccount(pubkey: string): Promise> { - try { - const [balance, nonce] = await Promise.all([ - this.requestQueue.add(() => this.provider.getBalance(pubkey)), - this.requestQueue.add(() => this.provider.getTransactionCount(pubkey)), - ]) - - // Get known tokens from asset service for HyperEVM - const knownTokens = await this.getKnownHyperEvmTokens() - - let tokens: { - assetId: AssetId - balance: string - symbol: string - name: string - precision: number - }[] = [] - - if (knownTokens.length > 0) { - tokens = await this.getTokenBalancesMulticall(pubkey, knownTokens) - } - - return { - balance: balance.toString(), - chainId: this.chainId, - assetId: this.assetId, - chain: this.getType(), - chainSpecific: { - nonce, - tokens: tokens.filter(t => t.balance !== '0'), - }, - pubkey, - } - } catch (err) { - throw new Error(`Failed to get account: ${err}`) - } - } - - private getKnownHyperEvmTokens(): Promise { - // Returns the list of known HyperEVM tokens passed in the constructor - // These are fetched from the asset service by the plugin - return Promise.resolve(this.knownTokens) - } - - private async getTokenBalancesMulticall( - pubkey: string, - tokens: TokenInfo[], - ): Promise< - { - assetId: AssetId - balance: string - symbol: string - name: string - precision: number - }[] - > { - try { - const results: { - assetId: AssetId - balance: string - symbol: string - name: string - precision: number - }[] = [] - - // Process tokens in batches to avoid RPC limits - for (let i = 0; i < tokens.length; i += BATCH_SIZE) { - const batch = tokens.slice(i, i + BATCH_SIZE) - const batchResults = await this.multicallBatch(pubkey, batch) - results.push(...batchResults) - } - - return results - } catch (error) { - console.warn('[HyperEVM] Multicall failed, falling back to individual calls:', error) - // Fallback to individual calls if multicall fails - return this.getTokenBalancesIndividual(pubkey, tokens) - } - } - - private async multicallBatch( - pubkey: string, - tokens: TokenInfo[], - ): Promise< - { - assetId: AssetId - balance: string - symbol: string - name: string - precision: number - }[] - > { - // Build multicall calls array - const calls = tokens.map(token => ({ - target: token.contractAddress, - allowFailure: true, // Don't revert entire batch if one token fails - callData: this.erc20Interface.encodeFunctionData('balanceOf', [pubkey]), - })) - - // Execute multicall - const results = await this.requestQueue.add(() => this.multicall.aggregate3(calls)) - - // Decode results - return tokens - .map((token, i) => { - const { success, returnData } = results[i] - - if (!success || returnData === '0x') { - return null - } - - try { - const [balance] = this.erc20Interface.decodeFunctionResult('balanceOf', returnData) - - return { - assetId: token.assetId, - balance: balance.toString(), - symbol: token.symbol, - name: token.name, - precision: token.precision, - } - } catch { - return null - } - }) - .filter((result): result is NonNullable => result !== null) - } - - private async getTokenBalancesIndividual( - pubkey: string, - tokens: TokenInfo[], - ): Promise< - { - assetId: AssetId - balance: string - symbol: string - name: string - precision: number - }[] - > { - const results = await Promise.all( - tokens.map(async token => { - try { - const contract = new Contract(token.contractAddress, ERC20_ABI, this.provider) - - const balance = await this.requestQueue.add(() => contract.balanceOf(pubkey)) - - return { - assetId: token.assetId, - balance: balance.toString(), - symbol: token.symbol, - name: token.name, - precision: token.precision, - } - } catch { - return null - } - }), - ) - - return results.filter((result): result is NonNullable => result !== null) - } - - async getGasFeeData(): Promise { - try { - const feeData = await this.requestQueue.add(() => this.provider.getFeeData()) - - const gasPrice = feeData.gasPrice?.toString() ?? '0' - const maxFeePerGas = feeData.maxFeePerGas?.toString() - const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas?.toString() - - const fees = { - gasPrice, - ...(maxFeePerGas && maxPriorityFeePerGas ? { maxFeePerGas, maxPriorityFeePerGas } : {}), - } - - return { - fast: fees, - average: fees, - slow: fees, - } - } catch (err) { - throw new Error(`Failed to get gas fee data: ${err}`) - } - } - - async getFeeData( - input: GetFeeDataInput, - ): Promise> { - try { - const estimateGasBody = this.buildEstimateGasBody(input) - - const gasLimit = await this.requestQueue.add(() => - this.provider.estimateGas({ - from: estimateGasBody.from, - to: estimateGasBody.to, - value: estimateGasBody.value ? BigInt(estimateGasBody.value) : undefined, - data: estimateGasBody.data, - }), - ) - - const { fast, average, slow } = await this.getGasFeeData() - - const gasLimitString = gasLimit.toString() - - return { - fast: { - txFee: bnOrZero(fast.maxFeePerGas ?? fast.gasPrice) - .times(gasLimitString) - .toFixed(0), - chainSpecific: { gasLimit: gasLimitString, ...fast }, - }, - average: { - txFee: bnOrZero(average.maxFeePerGas ?? average.gasPrice) - .times(gasLimitString) - .toFixed(0), - chainSpecific: { gasLimit: gasLimitString, ...average }, - }, - slow: { - txFee: bnOrZero(slow.maxFeePerGas ?? slow.gasPrice) - .times(gasLimitString) - .toFixed(0), - chainSpecific: { gasLimit: gasLimitString, ...slow }, - }, - } - } catch (err) { - throw new Error(`Failed to get fee data: ${err}`) - } - } - - async broadcastTransaction({ - senderAddress, - receiverAddress, - hex, - }: BroadcastTransactionInput): Promise { - try { - await Promise.all([ - assertAddressNotSanctioned(senderAddress), - receiverAddress !== CONTRACT_INTERACTION && assertAddressNotSanctioned(receiverAddress), - ]) - - const txResponse = await this.requestQueue.add(() => this.provider.broadcastTransaction(hex)) - return txResponse.hash - } catch (err) { - return ErrorHandler(err, { - translation: 'chainAdapters.errors.broadcastTransaction', - }) - } - } - - unsubscribeTxs(_input?: SubscribeTxsInput): void { - return - } - - subscribeTxs( - _input: SubscribeTxsInput, - _onMessage: (msg: Transaction) => void, - _onError: (err: SubscribeError) => void, - ): Promise { - return Promise.resolve() - } - - getTxHistory(_input: TxHistoryInput): Promise { - return Promise.resolve({ - cursor: '', - pubkey: _input.pubkey, - transactions: [], - txIds: [], - }) - } - - parseTx(_tx: unknown, _pubkey: string): Promise { - return Promise.reject(new Error('Transaction parsing is not supported for HyperEVM')) - } } + +export type { TokenInfo } diff --git a/packages/chain-adapters/src/evm/index.ts b/packages/chain-adapters/src/evm/index.ts index 0b5ed4fd1ce..3cfd0bd602c 100644 --- a/packages/chain-adapters/src/evm/index.ts +++ b/packages/chain-adapters/src/evm/index.ts @@ -1,5 +1,7 @@ export type { EvmChainAdapter } from './EvmBaseAdapter' export { isEvmChainId, evmChainIds, EvmBaseAdapter } from './EvmBaseAdapter' +export { SecondClassEvmAdapter } from './SecondClassEvmAdapter' +export type { SecondClassEvmAdapterArgs, TokenInfo } from './SecondClassEvmAdapter' export * as evm from './evm' diff --git a/packages/chain-adapters/src/evm/monad/MonadChainAdapter.ts b/packages/chain-adapters/src/evm/monad/MonadChainAdapter.ts index 8cfaed70996..52380dd92cc 100644 --- a/packages/chain-adapters/src/evm/monad/MonadChainAdapter.ts +++ b/packages/chain-adapters/src/evm/monad/MonadChainAdapter.ts @@ -1,45 +1,15 @@ import type { AssetId } from '@shapeshiftoss/caip' import { ASSET_REFERENCE, monadAssetId } from '@shapeshiftoss/caip' -import { MULTICALL3_CONTRACT } from '@shapeshiftoss/contracts' import type { RootBip44Params } from '@shapeshiftoss/types' import { KnownChainIds } from '@shapeshiftoss/types' -import { Contract, Interface, JsonRpcProvider } from 'ethers' -import PQueue from 'p-queue' -import { multicall3Abi } from 'viem' -import { ErrorHandler } from '../../error/ErrorHandler' -import type { - Account, - BroadcastTransactionInput, - FeeDataEstimate, - GetFeeDataInput, - SubscribeError, - SubscribeTxsInput, - Transaction, - TxHistoryInput, - TxHistoryResponse, -} from '../../types' -import { ChainAdapterDisplayName, CONTRACT_INTERACTION } from '../../types' -import { bnOrZero } from '../../utils/bignumber' -import { assertAddressNotSanctioned } from '../../utils/validateAddress' -import { EvmBaseAdapter } from '../EvmBaseAdapter' -import type { GasFeeDataEstimate } from '../types' +import { ChainAdapterDisplayName } from '../../types' +import type { TokenInfo } from '../SecondClassEvmAdapter' +import { SecondClassEvmAdapter } from '../SecondClassEvmAdapter' const SUPPORTED_CHAIN_IDS = [KnownChainIds.MonadMainnet] const DEFAULT_CHAIN_ID = KnownChainIds.MonadMainnet -const ERC20_ABI = ['function balanceOf(address) view returns (uint256)'] - -const BATCH_SIZE = 500 // Process 500 tokens per multicall to avoid gas/RPC limits - -export type TokenInfo = { - assetId: AssetId - contractAddress: string - symbol: string - name: string - precision: number -} - export type ChainAdapterArgs = { rpcUrl: string knownTokens?: TokenInfo[] @@ -49,48 +19,21 @@ export const isMonadChainAdapter = (adapter: unknown): adapter is ChainAdapter = return (adapter as ChainAdapter).getType() === KnownChainIds.MonadMainnet } -export class ChainAdapter extends EvmBaseAdapter { +export class ChainAdapter extends SecondClassEvmAdapter { public static readonly rootBip44Params: RootBip44Params = { purpose: 44, coinType: Number(ASSET_REFERENCE.Monad), accountNumber: 0, } - protected provider: JsonRpcProvider - protected multicall: Contract - protected erc20Interface: Interface - protected knownTokens: TokenInfo[] - private requestQueue: PQueue - constructor(args: ChainAdapterArgs) { - // Create a dummy parser - we won't use it since we don't support tx history - const dummyParser = { - parse: () => { - throw new Error('Transaction parsing is not supported for Monad') - }, - } as any - super({ assetId: monadAssetId, chainId: DEFAULT_CHAIN_ID, rootBip44Params: ChainAdapter.rootBip44Params, - parser: dummyParser, supportedChainIds: SUPPORTED_CHAIN_IDS, - providers: {} as any, // We don't use unchained providers rpcUrl: args.rpcUrl, - }) - - this.provider = new JsonRpcProvider(args.rpcUrl, undefined, { - staticNetwork: true, - }) - - this.multicall = new Contract(MULTICALL3_CONTRACT, multicall3Abi, this.provider) - this.erc20Interface = new Interface(ERC20_ABI) - this.knownTokens = args.knownTokens ?? [] - this.requestQueue = new PQueue({ - intervalCap: 1, - interval: 50, - concurrency: 1, + knownTokens: args.knownTokens ?? [], }) } @@ -109,279 +52,6 @@ export class ChainAdapter extends EvmBaseAdapter { getFeeAssetId(): AssetId { return this.assetId } - - async getAccount(pubkey: string): Promise> { - try { - const [balance, nonce] = await Promise.all([ - this.requestQueue.add(() => this.provider.getBalance(pubkey)), - this.requestQueue.add(() => this.provider.getTransactionCount(pubkey)), - ]) - - // Get known tokens from asset service for Monad - const knownTokens = await this.getKnownMonadTokens() - - let tokens: { - assetId: AssetId - balance: string - symbol: string - name: string - precision: number - }[] = [] - - if (knownTokens.length > 0) { - tokens = await this.getTokenBalancesMulticall(pubkey, knownTokens) - } - - return { - balance: balance.toString(), - chainId: this.chainId, - assetId: this.assetId, - chain: this.getType(), - chainSpecific: { - nonce, - tokens: tokens.filter(t => t.balance !== '0'), - }, - pubkey, - } - } catch (err) { - throw new Error(`Failed to get account: ${err}`) - } - } - - private getKnownMonadTokens(): Promise { - // Returns the list of known Monad tokens passed in the constructor - // These are fetched from the asset service by the plugin - return Promise.resolve(this.knownTokens) - } - - private async getTokenBalancesMulticall( - pubkey: string, - tokens: TokenInfo[], - ): Promise< - { - assetId: AssetId - balance: string - symbol: string - name: string - precision: number - }[] - > { - try { - const results: { - assetId: AssetId - balance: string - symbol: string - name: string - precision: number - }[] = [] - - // Process tokens in batches to avoid RPC limits - for (let i = 0; i < tokens.length; i += BATCH_SIZE) { - const batch = tokens.slice(i, i + BATCH_SIZE) - const batchResults = await this.multicallBatch(pubkey, batch) - results.push(...batchResults) - } - - return results - } catch (error) { - console.warn('[Monad] Multicall failed, falling back to individual calls:', error) - // Fallback to individual calls if multicall fails - return this.getTokenBalancesIndividual(pubkey, tokens) - } - } - - private async multicallBatch( - pubkey: string, - tokens: TokenInfo[], - ): Promise< - { - assetId: AssetId - balance: string - symbol: string - name: string - precision: number - }[] - > { - // Build multicall calls array - const calls = tokens.map(token => ({ - target: token.contractAddress, - allowFailure: true, // Don't revert entire batch if one token fails - callData: this.erc20Interface.encodeFunctionData('balanceOf', [pubkey]), - })) - - // Execute multicall - const results = await this.requestQueue.add(() => this.multicall.aggregate3(calls)) - - // Decode results - return tokens - .map((token, i) => { - const { success, returnData } = results[i] - - if (!success || returnData === '0x') { - return null - } - - try { - const [balance] = this.erc20Interface.decodeFunctionResult('balanceOf', returnData) - - return { - assetId: token.assetId, - balance: balance.toString(), - symbol: token.symbol, - name: token.name, - precision: token.precision, - } - } catch { - return null - } - }) - .filter((result): result is NonNullable => result !== null) - } - - private async getTokenBalancesIndividual( - pubkey: string, - tokens: TokenInfo[], - ): Promise< - { - assetId: AssetId - balance: string - symbol: string - name: string - precision: number - }[] - > { - const results = await Promise.all( - tokens.map(async token => { - try { - const contract = new Contract(token.contractAddress, ERC20_ABI, this.provider) - - const balance = await this.requestQueue.add(() => contract.balanceOf(pubkey)) - - return { - assetId: token.assetId, - balance: balance.toString(), - symbol: token.symbol, - name: token.name, - precision: token.precision, - } - } catch { - return null - } - }), - ) - - return results.filter((result): result is NonNullable => result !== null) - } - - async getGasFeeData(): Promise { - try { - const feeData = await this.requestQueue.add(() => this.provider.getFeeData()) - - const gasPrice = feeData.gasPrice?.toString() ?? '0' - const maxFeePerGas = feeData.maxFeePerGas?.toString() - const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas?.toString() - - const fees = { - gasPrice, - ...(maxFeePerGas && maxPriorityFeePerGas ? { maxFeePerGas, maxPriorityFeePerGas } : {}), - } - - return { - fast: fees, - average: fees, - slow: fees, - } - } catch (err) { - throw new Error(`Failed to get gas fee data: ${err}`) - } - } - - async getFeeData( - input: GetFeeDataInput, - ): Promise> { - try { - const estimateGasBody = this.buildEstimateGasBody(input) - - const gasLimit = await this.requestQueue.add(() => - this.provider.estimateGas({ - from: estimateGasBody.from, - to: estimateGasBody.to, - value: estimateGasBody.value ? BigInt(estimateGasBody.value) : undefined, - data: estimateGasBody.data, - }), - ) - - const { fast, average, slow } = await this.getGasFeeData() - - const gasLimitString = gasLimit.toString() - - return { - fast: { - txFee: bnOrZero(fast.maxFeePerGas ?? fast.gasPrice) - .times(gasLimitString) - .toFixed(0), - chainSpecific: { gasLimit: gasLimitString, ...fast }, - }, - average: { - txFee: bnOrZero(average.maxFeePerGas ?? average.gasPrice) - .times(gasLimitString) - .toFixed(0), - chainSpecific: { gasLimit: gasLimitString, ...average }, - }, - slow: { - txFee: bnOrZero(slow.maxFeePerGas ?? slow.gasPrice) - .times(gasLimitString) - .toFixed(0), - chainSpecific: { gasLimit: gasLimitString, ...slow }, - }, - } - } catch (err) { - throw new Error(`Failed to get fee data: ${err}`) - } - } - - async broadcastTransaction({ - senderAddress, - receiverAddress, - hex, - }: BroadcastTransactionInput): Promise { - try { - await Promise.all([ - assertAddressNotSanctioned(senderAddress), - receiverAddress !== CONTRACT_INTERACTION && assertAddressNotSanctioned(receiverAddress), - ]) - - const txResponse = await this.requestQueue.add(() => this.provider.broadcastTransaction(hex)) - return txResponse.hash - } catch (err) { - return ErrorHandler(err, { - translation: 'chainAdapters.errors.broadcastTransaction', - }) - } - } - - unsubscribeTxs(_input?: SubscribeTxsInput): void { - return - } - - subscribeTxs( - _input: SubscribeTxsInput, - _onMessage: (msg: Transaction) => void, - _onError: (err: SubscribeError) => void, - ): Promise { - return Promise.resolve() - } - - getTxHistory(_input: TxHistoryInput): Promise { - return Promise.resolve({ - cursor: '', - pubkey: _input.pubkey, - transactions: [], - txIds: [], - }) - } - - parseTx(_tx: unknown, _pubkey: string): Promise { - return Promise.reject(new Error('Transaction parsing is not supported for Monad')) - } } + +export type { TokenInfo } diff --git a/packages/chain-adapters/src/evm/plasma/PlasmaChainAdapter.ts b/packages/chain-adapters/src/evm/plasma/PlasmaChainAdapter.ts index fbd47bf04e2..b62aaab8226 100644 --- a/packages/chain-adapters/src/evm/plasma/PlasmaChainAdapter.ts +++ b/packages/chain-adapters/src/evm/plasma/PlasmaChainAdapter.ts @@ -1,45 +1,15 @@ import type { AssetId } from '@shapeshiftoss/caip' import { ASSET_REFERENCE, plasmaAssetId } from '@shapeshiftoss/caip' -import { MULTICALL3_CONTRACT } from '@shapeshiftoss/contracts' import type { RootBip44Params } from '@shapeshiftoss/types' import { KnownChainIds } from '@shapeshiftoss/types' -import { Contract, Interface, JsonRpcProvider } from 'ethers' -import PQueue from 'p-queue' -import { multicall3Abi } from 'viem' -import { ErrorHandler } from '../../error/ErrorHandler' -import type { - Account, - BroadcastTransactionInput, - FeeDataEstimate, - GetFeeDataInput, - SubscribeError, - SubscribeTxsInput, - Transaction, - TxHistoryInput, - TxHistoryResponse, -} from '../../types' -import { ChainAdapterDisplayName, CONTRACT_INTERACTION } from '../../types' -import { bnOrZero } from '../../utils/bignumber' -import { assertAddressNotSanctioned } from '../../utils/validateAddress' -import { EvmBaseAdapter } from '../EvmBaseAdapter' -import type { GasFeeDataEstimate } from '../types' +import { ChainAdapterDisplayName } from '../../types' +import type { TokenInfo } from '../SecondClassEvmAdapter' +import { SecondClassEvmAdapter } from '../SecondClassEvmAdapter' const SUPPORTED_CHAIN_IDS = [KnownChainIds.PlasmaMainnet] const DEFAULT_CHAIN_ID = KnownChainIds.PlasmaMainnet -const ERC20_ABI = ['function balanceOf(address) view returns (uint256)'] - -const BATCH_SIZE = 500 // Process 500 tokens per multicall to avoid gas/RPC limits - -export type TokenInfo = { - assetId: AssetId - contractAddress: string - symbol: string - name: string - precision: number -} - export type ChainAdapterArgs = { rpcUrl: string knownTokens?: TokenInfo[] @@ -49,47 +19,21 @@ export const isPlasmaChainAdapter = (adapter: unknown): adapter is ChainAdapter return (adapter as ChainAdapter).getType() === KnownChainIds.PlasmaMainnet } -export class ChainAdapter extends EvmBaseAdapter { +export class ChainAdapter extends SecondClassEvmAdapter { public static readonly rootBip44Params: RootBip44Params = { purpose: 44, coinType: Number(ASSET_REFERENCE.Plasma), accountNumber: 0, } - protected provider: JsonRpcProvider - protected multicall: Contract - protected erc20Interface: Interface - protected knownTokens: TokenInfo[] - private requestQueue: PQueue - constructor(args: ChainAdapterArgs) { - const dummyParser = { - parse: () => { - throw new Error('Transaction parsing is not supported for Plasma') - }, - } as any - super({ assetId: plasmaAssetId, chainId: DEFAULT_CHAIN_ID, rootBip44Params: ChainAdapter.rootBip44Params, - parser: dummyParser, supportedChainIds: SUPPORTED_CHAIN_IDS, - providers: {} as any, rpcUrl: args.rpcUrl, - }) - - this.provider = new JsonRpcProvider(args.rpcUrl, undefined, { - staticNetwork: true, - }) - - this.multicall = new Contract(MULTICALL3_CONTRACT, multicall3Abi, this.provider) - this.erc20Interface = new Interface(ERC20_ABI) - this.knownTokens = args.knownTokens ?? [] - this.requestQueue = new PQueue({ - intervalCap: 1, - interval: 50, - concurrency: 1, + knownTokens: args.knownTokens ?? [], }) } @@ -108,271 +52,6 @@ export class ChainAdapter extends EvmBaseAdapter { getFeeAssetId(): AssetId { return this.assetId } - - async getAccount(pubkey: string): Promise> { - try { - const [balance, nonce] = await Promise.all([ - this.requestQueue.add(() => this.provider.getBalance(pubkey)), - this.requestQueue.add(() => this.provider.getTransactionCount(pubkey)), - ]) - - const knownTokens = await this.getKnownPlasmaTokens() - - let tokens: { - assetId: AssetId - balance: string - symbol: string - name: string - precision: number - }[] = [] - - if (knownTokens.length > 0) { - tokens = await this.getTokenBalancesMulticall(pubkey, knownTokens) - } - - return { - balance: balance.toString(), - chainId: this.chainId, - assetId: this.assetId, - chain: this.getType(), - chainSpecific: { - nonce, - tokens: tokens.filter(t => t.balance !== '0'), - }, - pubkey, - } - } catch (err) { - throw new Error(`Failed to get account: ${err}`) - } - } - - private getKnownPlasmaTokens(): Promise { - return Promise.resolve(this.knownTokens) - } - - private async getTokenBalancesMulticall( - pubkey: string, - tokens: TokenInfo[], - ): Promise< - { - assetId: AssetId - balance: string - symbol: string - name: string - precision: number - }[] - > { - try { - const results: { - assetId: AssetId - balance: string - symbol: string - name: string - precision: number - }[] = [] - - for (let i = 0; i < tokens.length; i += BATCH_SIZE) { - const batch = tokens.slice(i, i + BATCH_SIZE) - const batchResults = await this.multicallBatch(pubkey, batch) - results.push(...batchResults) - } - - return results - } catch (error) { - console.warn('[Plasma] Multicall failed, falling back to individual calls:', error) - return this.getTokenBalancesIndividual(pubkey, tokens) - } - } - - private async multicallBatch( - pubkey: string, - tokens: TokenInfo[], - ): Promise< - { - assetId: AssetId - balance: string - symbol: string - name: string - precision: number - }[] - > { - const calls = tokens.map(token => ({ - target: token.contractAddress, - allowFailure: true, - callData: this.erc20Interface.encodeFunctionData('balanceOf', [pubkey]), - })) - - const results = await this.requestQueue.add(() => this.multicall.aggregate3(calls)) - - return tokens - .map((token, i) => { - const { success, returnData } = results[i] - - if (!success || returnData === '0x') { - return null - } - - try { - const [balance] = this.erc20Interface.decodeFunctionResult('balanceOf', returnData) - - return { - assetId: token.assetId, - balance: balance.toString(), - symbol: token.symbol, - name: token.name, - precision: token.precision, - } - } catch { - return null - } - }) - .filter((result): result is NonNullable => result !== null) - } - - private async getTokenBalancesIndividual( - pubkey: string, - tokens: TokenInfo[], - ): Promise< - { - assetId: AssetId - balance: string - symbol: string - name: string - precision: number - }[] - > { - const results = await Promise.all( - tokens.map(async token => { - try { - const contract = new Contract(token.contractAddress, ERC20_ABI, this.provider) - - const balance = await this.requestQueue.add(() => contract.balanceOf(pubkey)) - - return { - assetId: token.assetId, - balance: balance.toString(), - symbol: token.symbol, - name: token.name, - precision: token.precision, - } - } catch { - return null - } - }), - ) - - return results.filter((result): result is NonNullable => result !== null) - } - - async getGasFeeData(): Promise { - try { - const feeData = await this.requestQueue.add(() => this.provider.getFeeData()) - - const gasPrice = feeData.gasPrice?.toString() ?? '0' - const maxFeePerGas = feeData.maxFeePerGas?.toString() - const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas?.toString() - - const fees = { - gasPrice, - ...(maxFeePerGas && maxPriorityFeePerGas ? { maxFeePerGas, maxPriorityFeePerGas } : {}), - } - - return { - fast: fees, - average: fees, - slow: fees, - } - } catch (err) { - throw new Error(`Failed to get gas fee data: ${err}`) - } - } - - async getFeeData( - input: GetFeeDataInput, - ): Promise> { - try { - const estimateGasBody = this.buildEstimateGasBody(input) - - const gasLimit = await this.requestQueue.add(() => - this.provider.estimateGas({ - from: estimateGasBody.from, - to: estimateGasBody.to, - value: estimateGasBody.value ? BigInt(estimateGasBody.value) : undefined, - data: estimateGasBody.data, - }), - ) - - const { fast, average, slow } = await this.getGasFeeData() - - const gasLimitString = gasLimit.toString() - - return { - fast: { - txFee: bnOrZero(fast.maxFeePerGas ?? fast.gasPrice) - .times(gasLimitString) - .toFixed(0), - chainSpecific: { gasLimit: gasLimitString, ...fast }, - }, - average: { - txFee: bnOrZero(average.maxFeePerGas ?? average.gasPrice) - .times(gasLimitString) - .toFixed(0), - chainSpecific: { gasLimit: gasLimitString, ...average }, - }, - slow: { - txFee: bnOrZero(slow.maxFeePerGas ?? slow.gasPrice) - .times(gasLimitString) - .toFixed(0), - chainSpecific: { gasLimit: gasLimitString, ...slow }, - }, - } - } catch (err) { - throw new Error(`Failed to get fee data: ${err}`) - } - } - - async broadcastTransaction({ - senderAddress, - receiverAddress, - hex, - }: BroadcastTransactionInput): Promise { - try { - await Promise.all([ - assertAddressNotSanctioned(senderAddress), - receiverAddress !== CONTRACT_INTERACTION && assertAddressNotSanctioned(receiverAddress), - ]) - - const txResponse = await this.requestQueue.add(() => this.provider.broadcastTransaction(hex)) - return txResponse.hash - } catch (err) { - return ErrorHandler(err, { - translation: 'chainAdapters.errors.broadcastTransaction', - }) - } - } - - unsubscribeTxs(_input?: SubscribeTxsInput): void { - return - } - - subscribeTxs( - _input: SubscribeTxsInput, - _onMessage: (msg: Transaction) => void, - _onError: (err: SubscribeError) => void, - ): Promise { - return Promise.resolve() - } - - getTxHistory(_input: TxHistoryInput): Promise { - return Promise.resolve({ - cursor: '', - pubkey: _input.pubkey, - transactions: [], - txIds: [], - }) - } - - parseTx(_tx: unknown, _pubkey: string): Promise { - return Promise.reject(new Error('Transaction parsing is not supported for Plasma')) - } } + +export type { TokenInfo } diff --git a/src/hooks/useActualBuyAmountCryptoPrecision.ts b/src/hooks/useActualBuyAmountCryptoPrecision.ts index 25084b9f447..9eea546a7d4 100644 --- a/src/hooks/useActualBuyAmountCryptoPrecision.ts +++ b/src/hooks/useActualBuyAmountCryptoPrecision.ts @@ -1,7 +1,11 @@ -import { thorchainChainId } from '@shapeshiftoss/caip' +import { fromAccountId, thorchainChainId } from '@shapeshiftoss/caip' +import type { KnownChainIds } from '@shapeshiftoss/types' import { TransferType } from '@shapeshiftoss/unchained-client' +import { useQuery } from '@tanstack/react-query' import { useMemo } from 'react' +import { SECOND_CLASS_CHAINS } from '@/constants/chains' +import { getChainAdapterManager } from '@/context/PluginProvider/chainAdapterSingleton' import { fromBaseUnit } from '@/lib/math' import { selectTxByFilter } from '@/state/slices/selectors' import { selectSwapById } from '@/state/slices/swapSlice/selectors' @@ -24,11 +28,49 @@ export const useActualBuyAmountCryptoPrecision = ( }), ) + const { data: secondClassChainActualBuyAmount } = useQuery({ + queryKey: ['secondClassChainExecutionPrice', swap?.buyTxHash, swap?.buyAsset?.chainId], + queryFn: async () => { + if (!swap?.buyTxHash || !swap?.buyAsset || !swap?.buyAccountId) return undefined + + try { + const chainAdapterManager = getChainAdapterManager() + const adapter = chainAdapterManager.get(swap.buyAsset.chainId) + + if (!adapter) return undefined + + const { account: address } = fromAccountId(swap.buyAccountId) + const parsedTx = await adapter.parseTx(swap.buyTxHash, address) + + const receiveTransfer = parsedTx.transfers.find( + transfer => + transfer.type === TransferType.Receive && transfer.assetId === swap.buyAsset.assetId, + ) + + return receiveTransfer?.value + } catch (error) { + return undefined + } + }, + enabled: Boolean( + swap?.buyTxHash && + swap?.buyAsset?.chainId && + swap?.buyAccountId && + SECOND_CLASS_CHAINS.includes(swap.buyAsset.chainId as KnownChainIds), + ), + staleTime: Infinity, // Transaction data never changes + gcTime: Infinity, + }) + const actualBuyAmountCryptoPrecision = useMemo(() => { if (swap?.actualBuyAmountCryptoBaseUnit && swap?.buyAsset) { return fromBaseUnit(swap.actualBuyAmountCryptoBaseUnit, swap.buyAsset.precision) } + if (secondClassChainActualBuyAmount && swap?.buyAsset) { + return fromBaseUnit(secondClassChainActualBuyAmount, swap.buyAsset.precision) + } + if (!tx?.transfers?.length || !swap?.buyAsset) return undefined const receiveTransfer = tx.transfers.find( @@ -39,7 +81,7 @@ export const useActualBuyAmountCryptoPrecision = ( return receiveTransfer?.value ? fromBaseUnit(receiveTransfer.value, swap.buyAsset.precision) : undefined - }, [tx, swap]) + }, [tx, swap, secondClassChainActualBuyAmount]) return actualBuyAmountCryptoPrecision }