From f4f159c50b74d281f9a909400271f6d99dfc1c1b Mon Sep 17 00:00:00 2001 From: Karandeep Singh Date: Fri, 21 Mar 2025 12:06:38 +0530 Subject: [PATCH 01/13] updated the wallet_checkout schema and types --- .../src/schema/WalletCheckoutSchema.ts | 359 ++++++++++++++---- .../src/types/wallet_checkout.ts | 18 + 2 files changed, 300 insertions(+), 77 deletions(-) diff --git a/advanced/wallets/react-wallet-v2/src/schema/WalletCheckoutSchema.ts b/advanced/wallets/react-wallet-v2/src/schema/WalletCheckoutSchema.ts index 06cca5441..8f71685d9 100644 --- a/advanced/wallets/react-wallet-v2/src/schema/WalletCheckoutSchema.ts +++ b/advanced/wallets/react-wallet-v2/src/schema/WalletCheckoutSchema.ts @@ -1,63 +1,11 @@ -import { ContractInteraction } from '@/types/wallet_checkout' +import { CheckoutErrorCode, ContractInteraction, createCheckoutError, SolanaContractInteraction } from '@/types/wallet_checkout' import { z } from 'zod' -// Define Zod schemas for validation -export const ProductMetadataSchema = z.object({ - name: z.string().min(1, 'Product name is required'), - description: z.string().optional(), - imageUrl: z.string().url().optional(), - price: z.string().optional() -}) - -export const ContractInteractionSchema = z.object({ - type: z.string().min(1, 'Contract interaction type is required'), - data: z - .union([ - z.record(z.any()), // Object with string keys - z.array(z.any()) // Array of any values - ]) - .refine( - data => data !== null && (typeof data === 'object' || Array.isArray(data)), - 'Contract data must be an object or an array' - ) -}) - -export const PaymentOptionSchema = z - .object({ - asset: z - .string() - .min(1, 'Asset is required') - .refine(isValidCAIP19AssetId, 'Invalid CAIP-19 asset'), - amount: z.string().regex(/^0x[0-9a-fA-F]+$/, 'Amount must be a hex string'), - recipient: z.string().refine(isValidCAIP10AccountId, 'Invalid CAIP-10 recipient').optional(), - contractInteraction: ContractInteractionSchema.refine( - isValidContractInteraction, - 'Invalid contract interaction' - ).optional() - }) - .refine( - data => - (data.recipient && !data.contractInteraction) || - (!data.recipient && data.contractInteraction), - 'Either recipient or contractInteraction must be provided, but not both' - ) - .refine(data => { - if (!data.recipient) return true - return matchingChainIds(data.asset, data.recipient) - }, 'Asset and recipient must be on the same chain') -export const CheckoutRequestSchema = z.object({ - orderId: z.string().max(128, 'Order ID must not exceed 128 characters'), - acceptedPayments: z.array(PaymentOptionSchema).min(1, 'At least one payment option is required'), - products: z.array(ProductMetadataSchema).optional(), - expiry: z.number().int().optional() -}) +// ======== Helper Validation Functions ======== /** * Validates if a string follows the CAIP-19 format * Simple validation: chainNamespace:chainId/assetNamespace:assetReference - * - * @param assetId - CAIP-19 asset ID to validate - * @returns Whether the asset ID is valid */ export function isValidCAIP19AssetId(assetId: string): boolean { if (typeof assetId !== 'string') return false @@ -73,7 +21,7 @@ export function isValidCAIP19AssetId(assetId: string): boolean { chainParts?.length === 2 && chainParts[0]?.length > 0 && chainParts[1]?.length > 0 && - assetParts?.length === 2 && + assetParts.length === 2 && assetParts[0]?.length > 0 && assetParts[1]?.length > 0 ) @@ -82,9 +30,6 @@ export function isValidCAIP19AssetId(assetId: string): boolean { /** * Validates if a string follows the CAIP-10 format * Simple validation: chainNamespace:chainId:address - * - * @param accountId - CAIP-10 account ID to validate - * @returns Whether the account ID is valid */ export function isValidCAIP10AccountId(accountId: string): boolean { if (typeof accountId !== 'string') return false @@ -95,31 +40,44 @@ export function isValidCAIP10AccountId(accountId: string): boolean { } /** - * Checks if a contract interaction is valid - * - * @param contractInteraction - Contract interaction to validate - * @returns Whether the contract interaction is valid + * Validates if a Solana instruction is valid */ -export function isValidContractInteraction( - contractInteraction: ContractInteraction | undefined -): boolean { - if (!contractInteraction) return false +export function isValidSolanaInstruction(instruction: SolanaContractInteraction['data']): boolean { + try { + if (!instruction || typeof instruction !== 'object') return false + + // Check for required properties + if (!instruction.programId || typeof instruction.programId !== 'string') return false + if (!instruction.accounts || !Array.isArray(instruction.accounts)) return false + if (!instruction.data || typeof instruction.data !== 'string') return false + + // Validate each account + for (const account of instruction.accounts) { + if (!account || typeof account !== 'object') return false + if (!account.pubkey || typeof account.pubkey !== 'string') return false + if (typeof account.isSigner !== 'boolean') return false + if (typeof account.isWritable !== 'boolean') return false + } + + return true + } catch (e) { + return false + } +} - return ( - typeof contractInteraction === 'object' && - typeof contractInteraction.type === 'string' && - contractInteraction.type.trim() !== '' && - typeof contractInteraction.data === 'object' && - contractInteraction.data !== null - ) +/** + * Checks if an EVM call is valid + */ +export function isValidEvmCall(call: {to: string, data: string, value?: string}): boolean { + if (!call.to || typeof call.to !== 'string') return false; + if (!call.data || typeof call.data !== 'string') return false; + // Check value only if it's provided + if (call.value !== undefined && (typeof call.value !== 'string' || !call.value)) return false; + return true; } /** * Checks if the chain IDs in the asset and recipient match - * - * @param assetId - CAIP-19 asset ID - * @param accountId - CAIP-10 account ID - * @returns Whether the chain IDs match */ export function matchingChainIds(assetId: string, accountId: string): boolean { try { @@ -142,3 +100,250 @@ export function matchingChainIds(assetId: string, accountId: string): boolean { return false } } + +/** + * Validates Solana-specific asset format + */ +function validateSolanaAsset(asset: string, ctx: z.RefinementCtx) { + const assetParts = asset.split('/') + if (assetParts.length !== 2) return; + + const chainParts = assetParts[0].split(':') + if (chainParts[0] !== 'solana') return; + + // For Solana assets, validate asset namespace and reference + const assetType = assetParts[1].split(':') + if (assetType.length !== 2) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Invalid Solana asset format: ${asset}` + }); + throw createCheckoutError( + CheckoutErrorCode.INVALID_CHECKOUT_REQUEST, + `Invalid Solana asset format: ${asset}` + ); + } + + // Check supported Solana asset namespaces + if (assetType[0] !== 'slip44' && assetType[0] !== 'spl') { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Unsupported Solana asset namespace: ${assetType[0]}` + }); + throw createCheckoutError( + CheckoutErrorCode.INVALID_CHECKOUT_REQUEST, + `Unsupported Solana asset namespace: ${assetType[0]}` + ); + } + + // For slip44, validate the coin type is 501 for SOL + if (assetType[0] === 'slip44' && assetType[1] !== '501') { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Invalid Solana slip44 asset reference: ${assetType[1]}` + }); + throw createCheckoutError( + CheckoutErrorCode.INVALID_CHECKOUT_REQUEST, + `Invalid Solana slip44 asset reference: ${assetType[1]}` + ); + } +} + +/** + * Validates EVM-specific asset format + */ +function validateEvmAsset(asset: string, ctx: z.RefinementCtx) { + const assetParts = asset.split('/') + if (assetParts.length !== 2) return; + + const chainParts = assetParts[0].split(':') + if (chainParts[0] !== 'eip155') return; + + // For EVM assets, validate asset namespace and reference + const assetType = assetParts[1].split(':') + if (assetType.length !== 2) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Invalid EVM asset format: ${asset}` + }); + throw createCheckoutError( + CheckoutErrorCode.INVALID_CHECKOUT_REQUEST, + `Invalid EVM asset format: ${asset}` + ); + } + + // Check supported EVM asset namespaces + if (assetType[0] !== 'slip44' && assetType[0] !== 'erc20') { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Unsupported EVM asset namespace: ${assetType[0]}` + }); + throw createCheckoutError( + CheckoutErrorCode.INVALID_CHECKOUT_REQUEST, + `Unsupported EVM asset namespace: ${assetType[0]}` + ); + } + + // For slip44, validate the coin type is 60 for ETH + if (assetType[0] === 'slip44' && assetType[1] !== '60') { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Invalid EVM slip44 asset reference: ${assetType[1]}` + }); + throw createCheckoutError( + CheckoutErrorCode.INVALID_CHECKOUT_REQUEST, + `Invalid EVM slip44 asset reference: ${assetType[1]}` + ); + } +} + +/** + * Validates asset format based on chain type + */ +function validateAssetFormat(asset: string, ctx: z.RefinementCtx) { + const assetParts = asset.split('/') + if (assetParts.length !== 2) return; + + const chainParts = assetParts[0].split(':') + + // Validate based on chain namespace + switch (chainParts[0]) { + case 'solana': + validateSolanaAsset(asset, ctx); + break; + case 'eip155': + validateEvmAsset(asset, ctx); + break; + } +} + +// ======== Basic Schema Definitions ======== + +export const ProductMetadataSchema = z.object({ + name: z.string().min(1, 'Product name is required'), + description: z.string().optional(), + imageUrl: z.string().url().optional(), + price: z.string().optional() +}) + +export const SolanaAccountSchema = z.object({ + pubkey: z.string().min(1, 'Account public key is required'), + isSigner: z.boolean(), + isWritable: z.boolean() +}) + +export const SolanaInstructionDataSchema = z.object({ + programId: z.string().min(1, 'Program ID is required'), + accounts: z.array(SolanaAccountSchema).min(1, 'At least one account is required'), + data: z.string().min(1, 'Instruction data is required') +}) + +// ======== Contract Interaction Schemas ======== + +const EvmCallSchema = z.object({ + to: z.string().min(1), + data: z.string().min(1), + value: z.string().optional() +}).refine(isValidEvmCall, { + message: 'Invalid EVM call data' +}); + +const SolanaInstructionSchema = z.object({ + programId: z.string().min(1), + accounts: z.array(SolanaAccountSchema).min(1), + data: z.string().min(1) +}).refine(isValidSolanaInstruction, { + message: 'Invalid Solana instruction data' +}); + +export const ContractInteractionSchema = z.object({ + type: z.string().min(1, 'Contract interaction type is required'), + data: z.any() +}).superRefine((interaction, ctx) => { + // Check if interaction type is supported + if (interaction.type !== 'evm-calls' && interaction.type !== 'solana-instruction') { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Unsupported contract interaction type' + }); + throw createCheckoutError(CheckoutErrorCode.UNSUPPORTED_CONTRACT_INTERACTION); + } + + // Validate based on interaction type + if (interaction.type === 'evm-calls') { + validateEvmCalls(interaction, ctx); + } else if (interaction.type === 'solana-instruction') { + validateSolanaInstruction(interaction, ctx); + } +}); + +// Extracted validation functions for cleaner code +function validateEvmCalls(interaction: any, ctx: z.RefinementCtx) { + if (!interaction.data || !Array.isArray(interaction.data) || interaction.data.length === 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Invalid EVM calls data structure' + }); + throw createCheckoutError(CheckoutErrorCode.INVALID_CONTRACT_INTERACTION_DATA); + } + + // Validate each EVM call + for (const call of interaction.data) { + try { + EvmCallSchema.parse(call); + } catch (e) { + throw createCheckoutError(CheckoutErrorCode.INVALID_CONTRACT_INTERACTION_DATA); + } + } +} + +function validateSolanaInstruction(interaction: any, ctx: z.RefinementCtx) { + if (!interaction.data || typeof interaction.data !== 'object') { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Invalid Solana instruction data structure' + }); + throw createCheckoutError(CheckoutErrorCode.INVALID_CONTRACT_INTERACTION_DATA); + } + + try { + SolanaInstructionSchema.parse(interaction.data); + } catch (e) { + throw createCheckoutError(CheckoutErrorCode.INVALID_CONTRACT_INTERACTION_DATA); + } +} + +// ======== Payment Schema Definitions ======== + +// Asset validation schema with chain-specific checks +const AssetSchema = z.string() + .min(1, 'Asset is required') + .refine(isValidCAIP19AssetId, 'Invalid CAIP-19 asset') + .superRefine(validateAssetFormat); + +export const PaymentOptionSchema = z + .object({ + asset: AssetSchema, + amount: z.string().regex(/^0x[0-9a-fA-F]+$/, 'Amount must be a hex string'), + recipient: z.string().refine(isValidCAIP10AccountId, 'Invalid CAIP-10 recipient').optional(), + contractInteraction: ContractInteractionSchema.optional() + }) + .refine( + data => + (data.recipient && !data.contractInteraction) || + (!data.recipient && data.contractInteraction), + 'Either recipient or contractInteraction must be provided, but not both' + ) + .refine(data => { + if (!data.recipient) return true + return matchingChainIds(data.asset, data.recipient) + }, 'Asset and recipient must be on the same chain'); + +// ======== Checkout Request Schema ======== + +export const CheckoutRequestSchema = z.object({ + orderId: z.string().max(128, 'Order ID must not exceed 128 characters'), + acceptedPayments: z.array(PaymentOptionSchema).min(1, 'At least one payment option is required'), + products: z.array(ProductMetadataSchema).optional(), + expiry: z.number().int().optional() +}); \ No newline at end of file diff --git a/advanced/wallets/react-wallet-v2/src/types/wallet_checkout.ts b/advanced/wallets/react-wallet-v2/src/types/wallet_checkout.ts index 6fb38b12c..bc40d6fa0 100644 --- a/advanced/wallets/react-wallet-v2/src/types/wallet_checkout.ts +++ b/advanced/wallets/react-wallet-v2/src/types/wallet_checkout.ts @@ -52,6 +52,24 @@ export type EvmContractInteraction = { }[] } +/** + * Solana-specific contract interaction + * @property type - Must be "solana-instruction" + * @property data - Array of Solana instruction data objects + */ +export type SolanaContractInteraction = { + type: "solana-instruction"; + data: { + programId: string; // Program ID + accounts: { // Accounts involved in the instruction + pubkey: string; + isSigner: boolean; + isWritable: boolean; + }[]; + data: string; // Base64-encoded instruction data + }; +} + /** * A payment option for the checkout * @property asset - CAIP-19 asset identifier From 43d136b3765bf53cfe4ae102beb9f3203261b9e9 Mon Sep 17 00:00:00 2001 From: Karandeep Singh Date: Fri, 21 Mar 2025 12:32:17 +0530 Subject: [PATCH 02/13] updated findFeasibleContractPayments to consider solana payments also --- .../src/utils/PaymentValidatorUtil.ts | 383 +++++++++++++++++- 1 file changed, 374 insertions(+), 9 deletions(-) diff --git a/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts b/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts index bf4eb70dd..56bfd5be0 100644 --- a/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts +++ b/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts @@ -6,6 +6,8 @@ import SettingsStore from '@/store/SettingsStore' import { getTokenData } from '@/data/tokenUtil' import { getChainById } from './ChainUtil' import { EIP155_CHAINS } from '@/data/EIP155Data' +import { Connection, PublicKey } from '@solana/web3.js' +import SolanaRpcUtil from './SolanaRpcUtil' /** * Interface for token details @@ -77,8 +79,8 @@ export class PaymentValidationUtils { * @returns Whether the namespace is supported */ private static isSupportedAssetNamespace(assetNamespace: string): boolean { - // Currently only support ERC20 tokens and native tokens - return ['erc20', 'slip44'].includes(assetNamespace) + // Support ERC20 tokens, native tokens, and SPL tokens + return ['erc20', 'slip44', 'spl'].includes(assetNamespace) } /** @@ -317,7 +319,7 @@ export class PaymentValidationUtils { * @param account - User account address * @returns Object containing the validated payment (or null) and asset availability flag */ - private static async getDetailedDirectPaymentOption( + private static async getDetailedDirectPaymentOptionEVM( payment: PaymentOption, account: string ): Promise<{ @@ -384,6 +386,177 @@ export class PaymentValidationUtils { } } + /** + * Validates a single direct payment option for Solana and creates a detailed version if valid + * + * @param payment - Payment option to validate + * @param account - Solana account address + * @returns Object containing the validated payment (or null) and asset availability flag + */ + private static async getDetailedDirectPaymentOptionSolana( + payment: PaymentOption, + account: string + ): Promise<{ + validatedPayment: DetailedPaymentOption | null + hasMatchingAsset: boolean + }> { + try { + // Extract recipient address + const recipientAddress = PaymentValidationUtils.extractAddressFromCAIP10(payment.recipient) + if (!recipientAddress) { + return { validatedPayment: null, hasMatchingAsset: false } + } + + // Parse asset details + const { chainId, assetAddress, chainNamespace, assetNamespace } = + PaymentValidationUtils.getAssetDetails(payment.asset) + + // Check if asset namespace is supported + if (!PaymentValidationUtils.isSupportedAssetNamespace(assetNamespace)) { + return { validatedPayment: null, hasMatchingAsset: false } + } + + // Get token details based on asset namespace + let tokenDetails: TokenDetails + + if (assetNamespace === 'slip44' && assetAddress === '501') { + // Native SOL + tokenDetails = await this.getSolNativeAssetDetails(account, chainId) + } + else if (assetNamespace === 'spl') { + // SPL token + tokenDetails = await this.getSplTokenDetails(assetAddress, account, chainId) + } + else { + return { validatedPayment: null, hasMatchingAsset: false } + } + + // Check if user has the asset (balance > 0) + const hasMatchingAsset = tokenDetails.balance > BigInt(0) + + if (!hasMatchingAsset) { + return { validatedPayment: null, hasMatchingAsset } + } + + // Create detailed payment option with metadata + const detailedPayment = PaymentValidationUtils.createDetailedPaymentOption( + payment, + tokenDetails, + assetNamespace, + chainId, + chainNamespace + ) + + return { validatedPayment: detailedPayment, hasMatchingAsset: true } + } catch (error) { + console.error('Error validating Solana payment option:', error) + return { validatedPayment: null, hasMatchingAsset: false } + } + } + + /** + * Validates a single direct payment option and creates a detailed version if valid + * + * @param payment - Payment option to validate + * @param evmAccount - EVM account address (if available) + * @param solanaAccount - Solana account address (if available) + * @returns Object containing the validated payment (or null) and asset availability flag + */ + private static async getDetailedDirectPaymentOption( + payment: PaymentOption, + evmAccount?: string, + solanaAccount?: string + ): Promise<{ + validatedPayment: DetailedPaymentOption | null + hasMatchingAsset: boolean + }> { + try { + // Parse asset details to determine chain + const { chainNamespace } = PaymentValidationUtils.getAssetDetails(payment.asset) + + // Delegate to chain-specific methods + if (chainNamespace === 'eip155' && evmAccount) { + return await this.getDetailedDirectPaymentOptionEVM(payment, evmAccount) + } + else if (chainNamespace === 'solana' && solanaAccount) { + return await this.getDetailedDirectPaymentOptionSolana(payment, solanaAccount) + } + + return { validatedPayment: null, hasMatchingAsset: false } + } catch (error) { + console.error('Error getting detailed payment option:', error) + return { validatedPayment: null, hasMatchingAsset: false } + } + } + + /** + * Validates a contract payment option for Solana and creates a detailed version if valid + * + * @param payment - Payment option to validate + * @param account - Solana account address + * @returns Object containing the validated payment (or null) and asset availability flag + */ + private static async getDetailedContractPaymentOptionSolana( + payment: PaymentOption, + account: string + ): Promise<{ + validatedPayment: DetailedPaymentOption | null + hasMatchingAsset: boolean + }> { + try { + const { asset, contractInteraction } = payment + + if (!contractInteraction || contractInteraction.type !== 'solana-instruction') { + return { validatedPayment: null, hasMatchingAsset: false } + } + + // Parse asset details + const { chainId, assetAddress, chainNamespace, assetNamespace } = + PaymentValidationUtils.getAssetDetails(asset) + + // Check if asset namespace is supported + if (!PaymentValidationUtils.isSupportedAssetNamespace(assetNamespace)) { + return { validatedPayment: null, hasMatchingAsset: false } + } + + // Get token details based on asset namespace + let tokenDetails: TokenDetails + + if (assetNamespace === 'slip44' && assetAddress === '501') { + // Native SOL + tokenDetails = await this.getSolNativeAssetDetails(account, chainId) + } + else if (assetNamespace === 'spl') { + // SPL token + tokenDetails = await this.getSplTokenDetails(assetAddress, account, chainId) + } + else { + return { validatedPayment: null, hasMatchingAsset: false } + } + + // Check if user has the asset (balance > 0) + const hasMatchingAsset = tokenDetails.balance > BigInt(0) + + // For demonstration, we'll assume all Solana contract interactions are feasible + // In a production app, you would validate the instructions using simulation + + // Create detailed payment option with metadata + const detailedPayment = PaymentValidationUtils.createDetailedPaymentOption( + payment, + tokenDetails, + assetNamespace, + chainId, + chainNamespace + ) + + return { validatedPayment: detailedPayment, hasMatchingAsset } + } catch (error) { + console.error('Error validating Solana contract payment option:', error) + return { validatedPayment: null, hasMatchingAsset: false } + } + } + + /** * Validates a contract payment option and creates a detailed version if valid * @@ -391,7 +564,7 @@ export class PaymentValidationUtils { * @param account - User account address * @returns Object containing the validated payment (or null) and asset availability flag */ - private static async getDetailedContractPaymentOption( + private static async getDetailedContractPaymentOptionEVM( payment: PaymentOption, account: string ): Promise<{ @@ -463,6 +636,197 @@ export class PaymentValidationUtils { } } + + /** + * Validates a contract payment option and creates a detailed version if valid + * + * @param payment - Payment option to validate + * @param evmAccount - EVM account address (if available) + * @param solanaAccount - Solana account address (if available) + * @returns Object containing the validated payment (or null) and asset availability flag + */ + private static async getDetailedContractPaymentOption( + payment: PaymentOption, + evmAccount?: string, + solanaAccount?: string + ): Promise<{ + validatedPayment: DetailedPaymentOption | null + hasMatchingAsset: boolean + }> { + try { + // Parse asset details to determine chain + const { chainNamespace } = PaymentValidationUtils.getAssetDetails(payment.asset) + + // Delegate to chain-specific methods + if (chainNamespace === 'eip155' && evmAccount) { + return await this.getDetailedContractPaymentOptionEVM(payment, evmAccount) + } + else if (chainNamespace === 'solana' && solanaAccount) { + return await this.getDetailedContractPaymentOptionSolana(payment, solanaAccount) + } + + return { validatedPayment: null, hasMatchingAsset: false } + } catch (error) { + console.error('Error getting detailed contract payment option:', error) + return { validatedPayment: null, hasMatchingAsset: false } + } + } + + /** + * Gets the Solana RPC URL for a specific chain ID + * @param chainId Solana chain ID + * @returns RPC URL or undefined if not found + */ + private static getSolanaRpcUrl(chainId: string): string | undefined { + try { + return SolanaRpcUtil.getSolanaRpcUrl(chainId); + } catch (error) { + console.warn('Error getting Solana RPC URL:', error); + return undefined; + } + } + + /** + * Gets details for a Solana native asset (SOL) + * + * @param account - Solana account address + * @returns SOL asset details including metadata + */ + private static async getSolNativeAssetDetails( + account: string, + chainId: string = '998' // Default to mainnet + ): Promise { + try { + // Get the RPC URL for the chain + const rpcUrl = this.getSolanaRpcUrl(chainId); + + if (!rpcUrl) { + throw new Error(`No RPC URL found for Solana chain ID: ${chainId}`); + } + + // Connect to Solana + const connection = new Connection(rpcUrl, 'confirmed'); + const publicKey = new PublicKey(account); + + // Get SOL balance + const balance = await connection.getBalance(publicKey); + + return { + balance: BigInt(balance), + decimals: 9, // SOL has 9 decimals + symbol: 'SOL', + name: 'Solana' + }; + } catch (error) { + console.error('Error getting SOL balance:', error); + + // Return default values in case of error + return { + balance: BigInt(0), + decimals: 9, + symbol: 'SOL', + name: 'Solana' + }; + } + } + + /** + * Gets details for an SPL token + * + * @param tokenAddress - Token mint address + * @param account - Account address + * @returns Token details including metadata + */ + private static async getSplTokenDetails( + tokenAddress: string, + account: string, + chainId: string = '998' // Default to mainnet + ): Promise { + try { + // Get the RPC URL for the chain + const rpcUrl = this.getSolanaRpcUrl(chainId); + + if (!rpcUrl) { + throw new Error(`No RPC URL found for Solana chain ID: ${chainId}`); + } + + // Connect to Solana + const connection = new Connection(rpcUrl, 'confirmed'); + const publicKey = new PublicKey(account); + const mintAddress = new PublicKey(tokenAddress); + + try { + // Find the associated token account + const tokenAccounts = await connection.getParsedTokenAccountsByOwner( + publicKey, + { mint: mintAddress } + ); + + // If token account exists, get balance + if (tokenAccounts.value.length > 0) { + const accountInfo = tokenAccounts.value[0].account; + const parsedData = accountInfo.data.parsed.info; + + // Get token metadata if available + let name = 'SPL Token'; + let symbol = tokenAddress.slice(0, 4).toUpperCase(); + let decimals = parsedData.tokenAmount.decimals; + + try { + // Try to get token metadata (this is a simplified approach) + // In a production app, you might want to use the Metaplex or Token Registry + const tokenInfo = await connection.getParsedAccountInfo(mintAddress); + if (tokenInfo.value) { + const parsedTokenInfo = (tokenInfo.value.data as any).parsed?.info; + if (parsedTokenInfo) { + name = parsedTokenInfo.name || name; + symbol = parsedTokenInfo.symbol || symbol; + decimals = parsedTokenInfo.decimals || decimals; + } + } + } catch (metadataError) { + console.warn('Error getting token metadata:', metadataError); + } + + return { + balance: BigInt(parsedData.tokenAmount.amount), + decimals, + symbol, + name + }; + } else { + // User has no tokens of this type + return { + balance: BigInt(0), + decimals: 6, // Default for most SPL tokens + symbol: tokenAddress.slice(0, 4).toUpperCase(), + name: 'SPL Token' + }; + } + } catch (tokenError) { + console.warn('Error getting token account:', tokenError); + + // Return default values for token that user doesn't have + return { + balance: BigInt(0), + decimals: 6, + symbol: tokenAddress.slice(0, 4).toUpperCase(), + name: 'SPL Token' + }; + } + } catch (error) { + console.error('Error getting SPL token details:', error); + + // Return default values in case of error + return { + balance: BigInt(0), + decimals: 6, + symbol: tokenAddress.slice(0, 4).toUpperCase(), + name: 'SPL Token' + }; + } + } + /** * Finds and validates all feasible direct payment options * @@ -474,12 +838,13 @@ export class PaymentValidationUtils { isUserHaveAtleastOneMatchingAssets: boolean }> { let isUserHaveAtleastOneMatchingAssets = false - const account = SettingsStore.state.eip155Address + const evmAccount = SettingsStore.state.eip155Address as `0x${string}` + const solanaAccount = SettingsStore.state.solanaAddress // Validate each payment option const results = await Promise.all( directPayments.map(payment => - PaymentValidationUtils.getDetailedDirectPaymentOption(payment, account) + PaymentValidationUtils.getDetailedDirectPaymentOption(payment, evmAccount, solanaAccount) ) ) @@ -513,12 +878,12 @@ export class PaymentValidationUtils { isUserHaveAtleastOneMatchingAssets: boolean }> { let isUserHaveAtleastOneMatchingAssets = false - const account = SettingsStore.state.eip155Address + const evmAccount = SettingsStore.state.eip155Address as `0x${string}` + const solanaAccount = SettingsStore.state.solanaAddress - // Validate each contract payment option const results = await Promise.all( contractPayments.map(payment => - PaymentValidationUtils.getDetailedContractPaymentOption(payment, account) + PaymentValidationUtils.getDetailedContractPaymentOption(payment, evmAccount, solanaAccount) ) ) From 7fca4ec2c3eba9744b8d1287ba24ea43d2e15651 Mon Sep 17 00:00:00 2001 From: Karandeep Singh Date: Fri, 21 Mar 2025 12:36:34 +0530 Subject: [PATCH 03/13] chore:remove un-used methods from WalletCheckoutUtils.ts --- .../src/utils/WalletCheckoutUtil.ts | 63 ------------------- 1 file changed, 63 deletions(-) diff --git a/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutUtil.ts b/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutUtil.ts index 0136887ed..bfc3a232d 100644 --- a/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutUtil.ts +++ b/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutUtil.ts @@ -11,46 +11,6 @@ import { CheckoutRequestSchema } from '@/schema/WalletCheckoutSchema' import { PaymentValidationUtils } from './PaymentValidatorUtil' const WalletCheckoutUtil = { - /** - * Format the asset ID for display - * Extracts the asset reference from a CAIP-19 asset ID - * - * @param assetId - CAIP-19 asset ID - * @returns The formatted asset display name - */ - formatAsset(assetId: string): string { - try { - const parts = assetId.split('/') - if (parts.length !== 2) return assetId - - const assetParts = parts[1].split(':') - if (assetParts.length !== 2) return parts[1] - - // For ERC20 tokens, return the token address - // In a production app, you might want to map this to token symbols - return assetParts[1] - } catch (e) { - return assetId - } - }, - - /** - * Format the hex amount to a decimal value - * - * @param hexAmount - Hex-encoded amount string - * @param decimals - Number of decimals for the asset (default: 6) - * @returns The formatted amount as a string - */ - formatAmount(hexAmount: string, decimals = 6): string { - try { - if (!hexAmount.startsWith('0x')) return hexAmount - - const amount = parseInt(hexAmount, 16) / Math.pow(10, decimals) - return amount.toFixed(2) - } catch (e) { - return hexAmount - } - }, /** * Format the recipient address for display @@ -92,29 +52,7 @@ const WalletCheckoutUtil = { // Use Zod to validate the checkout request structure CheckoutRequestSchema.parse(checkoutRequest) - - // Additional validation for CAIP formats that Zod can't easily handle - const { acceptedPayments } = checkoutRequest - - for (const payment of acceptedPayments) { - // For contract payments, additional validation - if (payment.contractInteraction) { - // check if contract interaction type is supported - if (payment.contractInteraction.type !== 'evm-calls') { - throw createCheckoutError(CheckoutErrorCode.UNSUPPORTED_CONTRACT_INTERACTION) - } - - // check if contract interaction data is valid - if ( - !payment.contractInteraction.data || - !Array.isArray(payment.contractInteraction.data) - ) { - throw createCheckoutError(CheckoutErrorCode.INVALID_CONTRACT_INTERACTION_DATA) - } - } - } } catch (error) { - // Convert Zod validation errors or custom errors to CheckoutError if (error instanceof z.ZodError) { const errorDetails = error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ') throw createCheckoutError( @@ -138,7 +76,6 @@ const WalletCheckoutUtil = { const contractPayments: PaymentOption[] = [] acceptedPayments.forEach(payment => { - // Skip if payment is undefined or null if (!payment) { return } From 8d29675230e347ec846f515f167f9701755b909b Mon Sep 17 00:00:00 2001 From: Karandeep Singh Date: Mon, 24 Mar 2025 10:52:37 +0530 Subject: [PATCH 04/13] chore: asset namespace change from spl to token --- .../react-wallet-v2/src/schema/WalletCheckoutSchema.ts | 2 +- .../react-wallet-v2/src/utils/PaymentValidatorUtil.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/advanced/wallets/react-wallet-v2/src/schema/WalletCheckoutSchema.ts b/advanced/wallets/react-wallet-v2/src/schema/WalletCheckoutSchema.ts index 8f71685d9..1588e40ac 100644 --- a/advanced/wallets/react-wallet-v2/src/schema/WalletCheckoutSchema.ts +++ b/advanced/wallets/react-wallet-v2/src/schema/WalletCheckoutSchema.ts @@ -125,7 +125,7 @@ function validateSolanaAsset(asset: string, ctx: z.RefinementCtx) { } // Check supported Solana asset namespaces - if (assetType[0] !== 'slip44' && assetType[0] !== 'spl') { + if (assetType[0] !== 'slip44' && assetType[0] !== 'token') { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Unsupported Solana asset namespace: ${assetType[0]}` diff --git a/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts b/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts index 56bfd5be0..570a568a2 100644 --- a/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts +++ b/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts @@ -79,8 +79,8 @@ export class PaymentValidationUtils { * @returns Whether the namespace is supported */ private static isSupportedAssetNamespace(assetNamespace: string): boolean { - // Support ERC20 tokens, native tokens, and SPL tokens - return ['erc20', 'slip44', 'spl'].includes(assetNamespace) + // Support ERC20 tokens, native tokens, and solana token + return ['erc20', 'slip44', 'token'].includes(assetNamespace) } /** @@ -423,7 +423,7 @@ export class PaymentValidationUtils { // Native SOL tokenDetails = await this.getSolNativeAssetDetails(account, chainId) } - else if (assetNamespace === 'spl') { + else if (assetNamespace === 'token') { // SPL token tokenDetails = await this.getSplTokenDetails(assetAddress, account, chainId) } @@ -526,7 +526,7 @@ export class PaymentValidationUtils { // Native SOL tokenDetails = await this.getSolNativeAssetDetails(account, chainId) } - else if (assetNamespace === 'spl') { + else if (assetNamespace === 'token') { // SPL token tokenDetails = await this.getSplTokenDetails(assetAddress, account, chainId) } From 491e9a6c7de837830cf7df81cb1cf24462548680 Mon Sep 17 00:00:00 2001 From: Karandeep Singh Date: Mon, 24 Mar 2025 11:54:12 +0530 Subject: [PATCH 05/13] chore: display solana checkout request --- .../public/token-logos/SOL.png | Bin 0 -> 3213 bytes .../react-wallet-v2/src/data/SolanaData.ts | 3 +- .../react-wallet-v2/src/data/tokenUtil.ts | 23 ++- .../src/utils/PaymentValidatorUtil.ts | 157 +++++++++--------- 4 files changed, 103 insertions(+), 80 deletions(-) create mode 100644 advanced/wallets/react-wallet-v2/public/token-logos/SOL.png diff --git a/advanced/wallets/react-wallet-v2/public/token-logos/SOL.png b/advanced/wallets/react-wallet-v2/public/token-logos/SOL.png new file mode 100644 index 0000000000000000000000000000000000000000..0ba7236a90b2d10de28172891ca795472d24935e GIT binary patch literal 3213 zcmb7{S5Om*5{5%NG${dT5s}cF(n~~;UP2BvH0dQYDF!f90i%EvDM4ul;Q-PCDn+Cd znnn;bL3$SgilG^b^s6&x=HA!)&HlSHyD$Il+oo8W8?&*1SO5S3o2iL`_3xVSM@+Q8 zGt7A(3jkbzn;PiCBWWl-_kZ^Q2_>i1eAR!({}Q|gN)UI$`B=}kQ1ip z17(q;4O`6DAddb;AtZXhD9+YwPIgm?*OK2g@k4FiaI-t*v1AEZ2cW|5X?5;xa(R>tsteLdLh+pro@Dtcja2rVq|!swdn=Ki~bhuV9j+& zMX;@~en@1>5VRjt6dWA?ll8zt@8f9};KNZVGc3RfZ9O)rKc5 z&?6IPkSJSK`CQdAmEQ0`e9>H;dN)sTy$0sDg~-bDne#8gykQhUi05#(LQj?^LybYT z8ThsiW>~3WlvFuaV(+vU4{Ej9KkX}OyA8EWg(Xq;D1$JK?bV^@1ZJO=nQeIc&%MZp?ARelK z+7%t?7^{KG!45QC%(k5th34@MwdF81@0onmea&TdATw=1^3}IT_b1k#!t;ljU&f$#Ngn2& zCL+7ax*0T2>f@A9e$7Uo8F4idn#ecvr@_nqnaG$teFJpDbPxA9aLx>VLbn%?GDBGVM3~_=)}xHGrE)o8;~iv_vZuRH>m@Rb3)qE#_0)zZP}M7$lFNA;*ag1?JXdfBi{XNJ>4mf4p0f!FUNwjToQeNNWYNy@ z0hl0p!I;q6oUsHn^&?dq8D4HsC?JdrEqJ@z%Z(%CTdTf8M-Mkv4v$?f7q|C=MGOE| z3gO$)3{hkaJ4feyx?^lL@`;dVb0wG+^@*K==khe_!Z7-@7`DjHpN*(fIa<}UW3s8+ z|K$sxe~;aQu$$G={#Fyh67nsDIj!D#5aVuD z;C1aeywA*8s>PClwG8B?SRvE)l?IpH=l0Qq;^y2B6ATH&-djAZyK?UbCo!6bSlJOr z!n>q=w^&1a3pC3oZPyi68QfvP?@KYbA#j0k;|iPXM_)KzT~nzL?)L8F(TsaeQoU;q zXZ8yywtr$j)g3GHVl1z`yJ-1>1V59WuCk;7*E?qQB*#TtCu`RdDLAbG&pNvIO|$u?P;tD7pFzIxy!DVqwZE0d9VM8D_Va@JK0+^X-Vcu3bFLVFe)+qPN&oloc~ij z`hi44oQ2=<_RJuFvT9FB2YodK*0&)S4CJ$ zb5>oQ!h6zY9FQJMAns}HGqtWe{^3jjh4=AA2hqi;M}=RPB!*UB4|L@uSt_qy&6yOv zT~xH=V7}qsh3s+EGe`Inp?h^(aRP4B7sY9}wSW1Kn9px3XB~`nRkfp7v*7W%3?YUY><1a65Tqh?WA`+7q zbsHd^mdQJd^j&#|_sUBveNohkU#S0y0%F@OHdtLDAskd`u8C_R9Yx~6uf&=B0&f#ni7y%55wgJ-Z? z5O6QQ_i}{vfakUExEtQC)K;O}P66dt+tIX_v(=#ugfuY65Zp z1p>T(Gpqp&wlEY%^OiQk3-yw~_}W!5qw&Iag@j3~o?pNqx3ftI!l#@GAQw=eJ*mo{WT0br%*S|1;<%Uc1AkPp^l#Fv~2Lm*4b!oK&(hY!1QZpf2a zsWLIoi5Njk>#;uWq){%d(xn720P&tnc&$Z&8vGd+L|u!GU*D;-ku=`3HHgw(IL(WXjT{5f3^aK{ zFZnGkkC)db7w)S()=`tE>d-e%JH|My9tV75)Q%jHmUcWE=Bpd6Fv27>oXodpc6&vV z*BzDQWkQ8PD+R)1BB85h{yAe!Zl0%FN)w2>R-2F?P0}`3T11KSLRz$`p|bnMyT1tQ zZ@@e&OpWOecska-j0Zsv^~QDG??95+l}R#k7!3@q<~P{QXF*5HYayRB{|O_azZEgA z45S_iMjYf9vfWjIwkdwk<$_~F-OXMlFsLy&WO>2sFajBDW+p=v{{@#=>zP?cq9%}4 z>L7tRGaWZan{<8TfL=z-HF5ViVbXuDjFdvNn)I3XE_O3$uFL?K^j1FqbPw@f{S%g} zQ`sZ5=RUWPfZJQ1I12j|-`pK{KE@#sP`^6FdR!Ed`vwF}hy5*+CFii3@^F7@>h*S` z8GA_KSn31G+=;Oc}>SpQxVMMtW34Q(W{wHk(ac zs;i4-uw~Os{-ufacm+uBms?Mn#4BrdzOp@Z5>09}g?57}0U}C*EZlHJRk_Sv75T%J z!ZP0TecN4`#y#s+}>}xzzINL7P#Q!{r>W({c7wZOIa;=|^ z+v8Y$53TL%WxuDHJH{yE!^?+^>2uHKm@)l4eon}QN~T0kB!4PCq)Hwb@5z4XA`HvQ(0z token.symbol === tokenSymbol) } + + +const SOLANA_KNOWN_TOKENS = [ + { + name: 'USDC', + icon: '/token-logos/USDC.png', + symbol: 'USDC', + decimals: 6, + assetAddress:['solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/token:4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU'] + } +] + +export function getSolanaTokenData(caip19AssetAddress: string) { + return SOLANA_KNOWN_TOKENS.find(token => token.assetAddress.includes(caip19AssetAddress)) +} \ No newline at end of file diff --git a/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts b/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts index 570a568a2..d34a2150e 100644 --- a/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts +++ b/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts @@ -3,11 +3,12 @@ import { type PaymentOption, type DetailedPaymentOption, Hex } from '@/types/wal import { createPublicClient, erc20Abi, http, getContract, encodeFunctionData } from 'viem' import TransactionSimulatorUtil from './TransactionSimulatorUtil' import SettingsStore from '@/store/SettingsStore' -import { getTokenData } from '@/data/tokenUtil' +import { getSolanaTokenData, getTokenData } from '@/data/tokenUtil' import { getChainById } from './ChainUtil' import { EIP155_CHAINS } from '@/data/EIP155Data' import { Connection, PublicKey } from '@solana/web3.js' import SolanaRpcUtil from './SolanaRpcUtil' +import { Metaplex } from '@metaplex-foundation/js'; /** * Interface for token details @@ -425,7 +426,7 @@ export class PaymentValidationUtils { } else if (assetNamespace === 'token') { // SPL token - tokenDetails = await this.getSplTokenDetails(assetAddress, account, chainId) + tokenDetails = await this.getSplTokenDetails(assetAddress, account, chainId, payment.asset) } else { return { validatedPayment: null, hasMatchingAsset: false } @@ -528,7 +529,7 @@ export class PaymentValidationUtils { } else if (assetNamespace === 'token') { // SPL token - tokenDetails = await this.getSplTokenDetails(assetAddress, account, chainId) + tokenDetails = await this.getSplTokenDetails(assetAddress, account, chainId, payment.asset) } else { return { validatedPayment: null, hasMatchingAsset: false } @@ -694,7 +695,7 @@ export class PaymentValidationUtils { */ private static async getSolNativeAssetDetails( account: string, - chainId: string = '998' // Default to mainnet + chainId: string ): Promise { try { // Get the RPC URL for the chain @@ -710,10 +711,10 @@ export class PaymentValidationUtils { // Get SOL balance const balance = await connection.getBalance(publicKey); - + console.log('Solana balance', balance) return { balance: BigInt(balance), - decimals: 9, // SOL has 9 decimals + decimals: 9, symbol: 'SOL', name: 'Solana' }; @@ -730,33 +731,42 @@ export class PaymentValidationUtils { } } - /** - * Gets details for an SPL token - * - * @param tokenAddress - Token mint address - * @param account - Account address - * @returns Token details including metadata - */ - private static async getSplTokenDetails( - tokenAddress: string, - account: string, - chainId: string = '998' // Default to mainnet - ): Promise { +/** + * Gets details for a fungible SPL token with known token mapping + * + * @param tokenAddress - Token mint address + * @param account - Account address + * @returns Token details including balance and metadata + */ +private static async getSplTokenDetails( + tokenAddress: string, + account: string, + chainId: string, + caip19AssetAddress: string +): Promise { + try { + // Get the RPC URL for the chain + const rpcUrl = this.getSolanaRpcUrl(chainId); + + if (!rpcUrl) { + throw new Error(`No RPC URL found for Solana chain ID: ${chainId}`); + } + + // Connect to Solana + const connection = new Connection(rpcUrl, 'confirmed'); + const publicKey = new PublicKey(account); + const mintAddress = new PublicKey(tokenAddress); + try { - // Get the RPC URL for the chain - const rpcUrl = this.getSolanaRpcUrl(chainId); - - if (!rpcUrl) { - throw new Error(`No RPC URL found for Solana chain ID: ${chainId}`); - } + // Check if this is a known token + const token = getSolanaTokenData(caip19AssetAddress); - // Connect to Solana - const connection = new Connection(rpcUrl, 'confirmed'); - const publicKey = new PublicKey(account); - const mintAddress = new PublicKey(tokenAddress); + // Get token balance + let balance = BigInt(0); + let decimals = token?.decimals || 6; // Use known token decimals or default try { - // Find the associated token account + // Find the associated token account(s) const tokenAccounts = await connection.getParsedTokenAccountsByOwner( publicKey, { mint: mintAddress } @@ -764,60 +774,38 @@ export class PaymentValidationUtils { // If token account exists, get balance if (tokenAccounts.value.length > 0) { - const accountInfo = tokenAccounts.value[0].account; - const parsedData = accountInfo.data.parsed.info; - - // Get token metadata if available - let name = 'SPL Token'; - let symbol = tokenAddress.slice(0, 4).toUpperCase(); - let decimals = parsedData.tokenAmount.decimals; + const tokenAccountPubkey = tokenAccounts.value[0].pubkey; + const tokenBalance = await connection.getTokenAccountBalance(tokenAccountPubkey); + balance = BigInt(tokenBalance.value.amount); - try { - // Try to get token metadata (this is a simplified approach) - // In a production app, you might want to use the Metaplex or Token Registry - const tokenInfo = await connection.getParsedAccountInfo(mintAddress); - if (tokenInfo.value) { - const parsedTokenInfo = (tokenInfo.value.data as any).parsed?.info; - if (parsedTokenInfo) { - name = parsedTokenInfo.name || name; - symbol = parsedTokenInfo.symbol || symbol; - decimals = parsedTokenInfo.decimals || decimals; - } - } - } catch (metadataError) { - console.warn('Error getting token metadata:', metadataError); + // Update decimals from on-chain data if not a known token + if (!token) { + decimals = tokenBalance.value.decimals; + } + } else if (!token) { + // If no token accounts and not a known token, try to get decimals from mint + const mintInfo = await connection.getParsedAccountInfo(mintAddress); + if (mintInfo.value) { + const parsedMintInfo = (mintInfo.value.data as any).parsed?.info; + decimals = parsedMintInfo?.decimals || decimals; } - - return { - balance: BigInt(parsedData.tokenAmount.amount), - decimals, - symbol, - name - }; - } else { - // User has no tokens of this type - return { - balance: BigInt(0), - decimals: 6, // Default for most SPL tokens - symbol: tokenAddress.slice(0, 4).toUpperCase(), - name: 'SPL Token' - }; } - } catch (tokenError) { - console.warn('Error getting token account:', tokenError); - - // Return default values for token that user doesn't have - return { - balance: BigInt(0), - decimals: 6, - symbol: tokenAddress.slice(0, 4).toUpperCase(), - name: 'SPL Token' - }; + } catch (balanceError) { + console.warn('Error getting token balance:', balanceError); + // Continue with zero balance } - } catch (error) { - console.error('Error getting SPL token details:', error); - // Return default values in case of error + // Return with known metadata or fallback to generic + return { + balance, + decimals, + symbol: token?.symbol || tokenAddress.slice(0, 4).toUpperCase(), + name: token?.name || `SPL Token (${tokenAddress.slice(0, 8)}...)`, + }; + } catch (tokenError) { + console.warn('Error getting token details:', tokenError); + + // Return default values return { balance: BigInt(0), decimals: 6, @@ -825,7 +813,20 @@ export class PaymentValidationUtils { name: 'SPL Token' }; } + } catch (error) { + console.error('Error getting SPL token details:', error); + + // Return default values in case of error + return { + balance: BigInt(0), + decimals: 6, + symbol: tokenAddress.slice(0, 4).toUpperCase(), + name: 'SPL Token' + }; } +} + + /** * Finds and validates all feasible direct payment options From f82e97b32c688c1b2aecc9a53ee833f8bec20c44 Mon Sep 17 00:00:00 2001 From: Karandeep Singh Date: Mon, 24 Mar 2025 12:30:22 +0530 Subject: [PATCH 06/13] chore: fix chainId for solana --- .../src/utils/PaymentValidatorUtil.ts | 46 +++++++------------ 1 file changed, 16 insertions(+), 30 deletions(-) diff --git a/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts b/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts index d34a2150e..54027fdc2 100644 --- a/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts +++ b/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts @@ -7,9 +7,8 @@ import { getSolanaTokenData, getTokenData } from '@/data/tokenUtil' import { getChainById } from './ChainUtil' import { EIP155_CHAINS } from '@/data/EIP155Data' import { Connection, PublicKey } from '@solana/web3.js' -import SolanaRpcUtil from './SolanaRpcUtil' -import { Metaplex } from '@metaplex-foundation/js'; - +import { SOLANA_TEST_CHAINS } from '@/data/SolanaData' +import { SOLANA_MAINNET_CHAINS } from '@/data/SolanaData' /** * Interface for token details */ @@ -422,11 +421,11 @@ export class PaymentValidationUtils { if (assetNamespace === 'slip44' && assetAddress === '501') { // Native SOL - tokenDetails = await this.getSolNativeAssetDetails(account, chainId) + tokenDetails = await this.getSolNativeAssetDetails(account, `${chainNamespace}:${chainId}`) } else if (assetNamespace === 'token') { // SPL token - tokenDetails = await this.getSplTokenDetails(assetAddress, account, chainId, payment.asset) + tokenDetails = await this.getSplTokenDetails(assetAddress, account, `${chainNamespace}:${chainId}`, payment.asset) } else { return { validatedPayment: null, hasMatchingAsset: false } @@ -525,11 +524,11 @@ export class PaymentValidationUtils { if (assetNamespace === 'slip44' && assetAddress === '501') { // Native SOL - tokenDetails = await this.getSolNativeAssetDetails(account, chainId) + tokenDetails = await this.getSolNativeAssetDetails(account, `${chainNamespace}:${chainId}`) } else if (assetNamespace === 'token') { // SPL token - tokenDetails = await this.getSplTokenDetails(assetAddress, account, chainId, payment.asset) + tokenDetails = await this.getSplTokenDetails(assetAddress, account, `${chainNamespace}:${chainId}`, payment.asset) } else { return { validatedPayment: null, hasMatchingAsset: false } @@ -673,19 +672,6 @@ export class PaymentValidationUtils { } } - /** - * Gets the Solana RPC URL for a specific chain ID - * @param chainId Solana chain ID - * @returns RPC URL or undefined if not found - */ - private static getSolanaRpcUrl(chainId: string): string | undefined { - try { - return SolanaRpcUtil.getSolanaRpcUrl(chainId); - } catch (error) { - console.warn('Error getting Solana RPC URL:', error); - return undefined; - } - } /** * Gets details for a Solana native asset (SOL) @@ -699,14 +685,14 @@ export class PaymentValidationUtils { ): Promise { try { // Get the RPC URL for the chain - const rpcUrl = this.getSolanaRpcUrl(chainId); - - if (!rpcUrl) { - throw new Error(`No RPC URL found for Solana chain ID: ${chainId}`); + const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc; + + if (!rpc) { + throw new Error('There is no RPC URL for the provided chain') } // Connect to Solana - const connection = new Connection(rpcUrl, 'confirmed'); + const connection = new Connection(rpc, 'confirmed'); const publicKey = new PublicKey(account); // Get SOL balance @@ -746,14 +732,14 @@ private static async getSplTokenDetails( ): Promise { try { // Get the RPC URL for the chain - const rpcUrl = this.getSolanaRpcUrl(chainId); - - if (!rpcUrl) { - throw new Error(`No RPC URL found for Solana chain ID: ${chainId}`); + const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc; + + if (!rpc) { + throw new Error('There is no RPC URL for the provided chain'); } // Connect to Solana - const connection = new Connection(rpcUrl, 'confirmed'); + const connection = new Connection(rpc, 'confirmed'); const publicKey = new PublicKey(account); const mintAddress = new PublicKey(tokenAddress); From 4088390cb3043c6ed8e51d51816136d9652336f7 Mon Sep 17 00:00:00 2001 From: Karandeep Singh Date: Tue, 25 Mar 2025 09:09:00 +0530 Subject: [PATCH 07/13] add solana chain support for direct and contract interaction payment option --- advanced/wallets/react-wallet-v2/package.json | 1 + .../react-wallet-v2/src/lib/SolanaLib.ts | 185 ++++++++++- .../src/utils/WalletCheckoutPaymentHandler.ts | 313 +++++++++++++++++- .../src/views/SessionCheckoutModal.tsx | 56 ++-- advanced/wallets/react-wallet-v2/yarn.lock | 131 +++++++- 5 files changed, 661 insertions(+), 25 deletions(-) diff --git a/advanced/wallets/react-wallet-v2/package.json b/advanced/wallets/react-wallet-v2/package.json index 2ef1a9fe8..1c57aa881 100644 --- a/advanced/wallets/react-wallet-v2/package.json +++ b/advanced/wallets/react-wallet-v2/package.json @@ -32,6 +32,7 @@ "@reown/appkit-experimental": "1.6.8", "@reown/walletkit": "1.0.0", "@rhinestone/module-sdk": "0.1.25", + "@solana/spl-token": "^0.4.13", "@solana/web3.js": "1.89.2", "@taquito/signer": "^15.1.0", "@taquito/taquito": "^15.1.0", diff --git a/advanced/wallets/react-wallet-v2/src/lib/SolanaLib.ts b/advanced/wallets/react-wallet-v2/src/lib/SolanaLib.ts index 5999ee5fb..c695ce24e 100644 --- a/advanced/wallets/react-wallet-v2/src/lib/SolanaLib.ts +++ b/advanced/wallets/react-wallet-v2/src/lib/SolanaLib.ts @@ -1,6 +1,7 @@ -import { Keypair, Connection, SendOptions, VersionedTransaction } from '@solana/web3.js' +import { Keypair, Connection, SendOptions, VersionedTransaction, PublicKey, Transaction, SystemProgram } from '@solana/web3.js' import bs58 from 'bs58' import nacl from 'tweetnacl' +import { getAssociatedTokenAddress, createTransferInstruction, TOKEN_PROGRAM_ID, getOrCreateAssociatedTokenAccount } from '@solana/spl-token' import { SOLANA_MAINNET_CHAINS, SOLANA_TEST_CHAINS } from '@/data/SolanaData' /** @@ -101,7 +102,9 @@ export default class SolanaLib { try { bytes = bs58.decode(transaction) } catch { - bytes = Buffer.from(transaction, 'base64') + // Convert base64 to Uint8Array to avoid type issues + const buffer = Buffer.from(transaction, 'base64') + bytes = new Uint8Array(buffer) } return VersionedTransaction.deserialize(bytes) @@ -110,6 +113,184 @@ export default class SolanaLib { private sign(transaction: VersionedTransaction) { transaction.sign([this.keypair]) } + + /** + * Send SOL to a recipient + * @param recipientAddress The recipient's address + * @param amount The amount to send in lamports (as a bigint) + * @returns The transaction signature/hash + */ + public async sendSol( + recipientAddress: string, + chainId: string, + amount: bigint + ): Promise { + console.log({chainId}) + try { + const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc + + if (!rpc) { + throw new Error('There is no RPC URL for the provided chain') + } + + const connection = new Connection(rpc, 'confirmed') + const fromPubkey = this.keypair.publicKey + const toPubkey = new PublicKey(recipientAddress) + + // Create a simple SOL transfer transaction + const transaction = new Transaction().add( + SystemProgram.transfer({ + fromPubkey, + toPubkey, + lamports: amount + }) + ) + + // Get recent blockhash + const { blockhash } = await connection.getLatestBlockhash('confirmed') + transaction.recentBlockhash = blockhash + transaction.feePayer = fromPubkey + + // Sign the transaction + transaction.sign(this.keypair) + + // Send and confirm the transaction + const signature = await connection.sendRawTransaction(transaction.serialize()) + + // Wait for confirmation + await connection.confirmTransaction(signature, 'confirmed') + + return signature + } catch (error) { + console.error('Error sending SOL:', error) + throw error + } + } + + /** + * Send an SPL token to a recipient + * @param tokenAddress The token's mint address + * @param recipientAddress The recipient's address + * @param amount The amount to send (as a bigint) + * @returns The transaction signature/hash + */ + public async sendSplToken( + tokenAddress: string, + recipientAddress: string, + chainId: string, + amount: bigint + ): Promise { + try { + const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc + + if (!rpc) { + throw new Error('There is no RPC URL for the provided chain') + } + + const connection = new Connection(rpc, 'confirmed') + const fromWallet = this.keypair + const fromPubkey = fromWallet.publicKey + const toPubkey = new PublicKey(recipientAddress) + const mint = new PublicKey(tokenAddress) + + // Get sender's token account (create if it doesn't exist) + const fromTokenAccount = await getOrCreateAssociatedTokenAccount( + connection, + fromWallet, + mint, + fromPubkey + ) + + // Check if recipient has a token account WITHOUT creating one + const associatedTokenAddress = await getAssociatedTokenAddress( + mint, + toPubkey + ) + + const recipientTokenAccount = await connection.getAccountInfo(associatedTokenAddress) + + if (!recipientTokenAccount) { + throw new Error( + `Recipient ${recipientAddress} doesn't have a token account for this SPL token. Transaction cannot proceed.` + ) + } + + // Create transfer instruction to existing account + const transferInstruction = createTransferInstruction( + fromTokenAccount.address, + associatedTokenAddress, + fromPubkey, + amount, + [], + TOKEN_PROGRAM_ID + ) + + // Create transaction and add the transfer instruction + const transaction = new Transaction().add(transferInstruction) + + // Get recent blockhash + const { blockhash } = await connection.getLatestBlockhash('confirmed') + transaction.recentBlockhash = blockhash + transaction.feePayer = fromPubkey + + // Sign the transaction + transaction.sign(fromWallet) + + // Send and confirm the transaction + const signature = await connection.sendRawTransaction(transaction.serialize()) + + // Wait for confirmation + await connection.confirmTransaction(signature, 'confirmed') + + return signature + } catch (error) { + console.error('Error sending SPL token:', error) + throw error + } + } + + /** + * Send a raw transaction (for contract interactions) + * @param transaction The transaction to sign and send + * @returns The transaction signature + */ + public async sendRawTransaction( + transaction: Transaction, + chainId: string + ): Promise { + try { + const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc + + if (!rpc) { + throw new Error('There is no RPC URL for the provided chain') + } + + const connection = new Connection(rpc, 'confirmed') + + // Set fee payer if not already set + if (!transaction.feePayer) { + transaction.feePayer = this.keypair.publicKey + } + + // Get recent blockhash if not already set + if (!transaction.recentBlockhash) { + const { blockhash } = await connection.getLatestBlockhash('confirmed') + transaction.recentBlockhash = blockhash + } + + // Sign transaction + transaction.sign(this.keypair) + + // Send and confirm transaction + const signature = await connection.sendRawTransaction(transaction.serialize()) + await connection.confirmTransaction(signature, 'confirmed') + + return signature + } catch (error) { + console.error('Error signing and sending transaction:', error) + throw error + } + } } export namespace SolanaLib { diff --git a/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutPaymentHandler.ts b/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutPaymentHandler.ts index 21091965b..9152199b9 100644 --- a/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutPaymentHandler.ts +++ b/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutPaymentHandler.ts @@ -1,8 +1,22 @@ import { encodeFunctionData } from 'viem' import { erc20Abi } from 'viem' +import { Connection, PublicKey, Transaction, TransactionInstruction } from '@solana/web3.js' +import bs58 from 'bs58' +import { Buffer } from 'buffer' -import { DetailedPaymentOption, CheckoutErrorCode, CheckoutError } from '@/types/wallet_checkout' +import { DetailedPaymentOption, CheckoutErrorCode, CheckoutError, SolanaContractInteraction } from '@/types/wallet_checkout' import { Wallet } from 'ethers' +import { SOLANA_MAINNET_CHAINS } from '@/data/SolanaData' +import { SOLANA_TEST_CHAINS } from '@/data/SolanaData' + +// Assume SolanaLib interface based on similar patterns in the codebase +interface SolanaLib { + getAddress(): string; + getChainId(): string; + signAndSendTransaction(transaction: Transaction): Promise; + sendSol(recipientAddress: string, chainId: string, amount: bigint): Promise; + sendSplToken(tokenAddress: string, recipientAddress: string, chainId: string, amount: bigint): Promise; +} export interface PaymentResult { txHash: string @@ -21,12 +35,305 @@ const WalletCheckoutPaymentHandler = { } }, + /** + * Simulates a Solana transaction before sending + */ + async simulateSolanaTransaction( + connection: Connection, + transaction: Transaction, + feePayer: PublicKey + ): Promise { + try { + // Set recent blockhash for the transaction + transaction.recentBlockhash = (await connection.getLatestBlockhash()).blockhash + transaction.feePayer = feePayer + + // Simulate the transaction + const simulation = await connection.simulateTransaction(transaction) + + // Check simulation results + if (simulation.value.err) { + console.error('Simulation error:', simulation.value.err) + throw new CheckoutError( + CheckoutErrorCode.CONTRACT_INTERACTION_FAILED, + `Simulation failed: ${JSON.stringify(simulation.value.err)}` + ) + } + + console.log('Simulation successful:', simulation.value.logs) + } catch (error) { + if (error instanceof CheckoutError) { + throw error + } + throw new CheckoutError( + CheckoutErrorCode.CONTRACT_INTERACTION_FAILED, + `Simulation error: ${error instanceof Error ? error.message : String(error)}` + ) + } + }, + + /** + * Handles a Solana direct payment + */ + async processSolanaDirectPayment( + wallet: SolanaLib, + recipientAddress: string, + amount: string, + chainId: string + ): Promise { + try { + // Send SOL to recipient + const txHash = await wallet.sendSol(recipientAddress, chainId, BigInt(amount)) + return { txHash } + } catch (error) { + console.error('Solana direct payment error:', error) + throw new CheckoutError( + CheckoutErrorCode.DIRECT_PAYMENT_ERROR, + `Failed to send SOL: ${error instanceof Error ? error.message : String(error)}` + ) + } + }, + + /** + * Handles a Solana SPL token payment + */ + async processSolanaSplTokenPayment( + wallet: SolanaLib, + recipientAddress: string, + amount: string, + tokenAddress: string, + chainId: string + ): Promise { + try { + // Send SPL token to recipient + const txHash = await wallet.sendSplToken(tokenAddress, recipientAddress, chainId, BigInt(amount)) + return { txHash } + } catch (error) { + console.error('Solana SPL token payment error:', error) + + // Check if this is a token account error + const errorMessage = error instanceof Error ? error.message : String(error); + if (errorMessage.includes("doesn't have a token account")) { + throw new CheckoutError( + CheckoutErrorCode.DIRECT_PAYMENT_ERROR, + `Recipient doesn't have a token account for this SPL token. The recipient must create a token account before they can receive this token.` + ) + } + + throw new CheckoutError( + CheckoutErrorCode.DIRECT_PAYMENT_ERROR, + `Failed to send SPL token: ${errorMessage}` + ) + } + }, + + /** + * Process a Solana contract interaction + */ + async processSolanaContractInteraction( + wallet: any, + contractInteraction: SolanaContractInteraction, + chainId: string + ): Promise { + try { + const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc; + + if (!rpc) { + throw new Error(`There is no RPC URL for the provided chain ${chainId}`) + } + const connection = new Connection(rpc) + + // Create a new transaction + const transaction = new Transaction() + + const instruction = contractInteraction.data + const accountMetas = instruction.accounts.map(acc => ({ + pubkey: new PublicKey(acc.pubkey), + isSigner: acc.isSigner, + isWritable: acc.isWritable + })) + + // Create the instruction + const txInstruction = new TransactionInstruction({ + programId: new PublicKey(instruction.programId), + keys: accountMetas, + data: Buffer.from(instruction.data, 'base64') + }) + + // Add to transaction + transaction.add(txInstruction) + + // Set the wallet's public key as feePayer + const walletAddress = await wallet.getAddress() + const publicKey = new PublicKey(walletAddress) + transaction.feePayer = publicKey + + // Get recent blockhash from the connection + const { blockhash } = await connection.getLatestBlockhash('confirmed') + transaction.recentBlockhash = blockhash + + // Simulate transaction to check for errors + await this.simulateSolanaTransaction( + connection, + transaction, + publicKey + ) + + // Determine which method to use for signing and sending + let txHash: string + + // Use sendRawTransaction if the wallet supports it (our implementation) + if (typeof wallet.sendRawTransaction === 'function') { + txHash = await wallet.sendRawTransaction(transaction, chainId) + } + // Otherwise use standard signAndSendTransaction + else if (typeof wallet.signAndSendTransaction === 'function') { + // Serialize the transaction to bs58 format + const serializedBytes = transaction.serialize(); + // Convert Buffer to Uint8Array to avoid type issues + const serializedTx = bs58.encode(new Uint8Array(serializedBytes)); + + // Use the wallet's signAndSendTransaction method + const result = await wallet.signAndSendTransaction({ + transaction: serializedTx, + chainId: chainId + }) + + // Handle different response formats from various wallet implementations + if (typeof result === 'string') { + txHash = result; + } else if (result && typeof result === 'object') { + if ('signature' in result && typeof result.signature === 'string') { + txHash = result.signature; + } else if ('txHash' in result && typeof result.txHash === 'string') { + txHash = result.txHash; + } else if ('transactionHash' in result && typeof result.transactionHash === 'string') { + txHash = result.transactionHash; + } else { + // Try to stringify the result if it's not in a recognized format + const stringResult = String(result); + if (stringResult && stringResult !== '[object Object]') { + txHash = stringResult; + } else { + throw new Error('Wallet returned an invalid transaction signature format'); + } + } + } else { + throw new Error('Wallet returned an invalid response format'); + } + + // Wait for transaction confirmation + try { + await connection.confirmTransaction(txHash, 'confirmed') + console.log('Transaction confirmed:', txHash) + } catch (confirmError) { + console.error('Error confirming transaction:', confirmError) + throw new CheckoutError( + CheckoutErrorCode.CONTRACT_INTERACTION_FAILED, + `Transaction was sent but confirmation failed: ${confirmError instanceof Error ? confirmError.message : String(confirmError)}` + ) + } + } + // No compatible method found + else { + throw new CheckoutError( + CheckoutErrorCode.INVALID_CHECKOUT_REQUEST, + 'Wallet does not support Solana transaction sending' + ) + } + + return { txHash } + } catch (error) { + console.error('Solana contract interaction error:', error) + if (error instanceof CheckoutError) { + throw error + } + throw new CheckoutError( + CheckoutErrorCode.CONTRACT_INTERACTION_FAILED, + `Failed to execute Solana transaction: ${error instanceof Error ? error.message : String(error)}` + ) + } + }, + /** * Process any payment type and handle errors */ - async processPayment(wallet: Wallet, payment: DetailedPaymentOption): Promise { + async processPayment(wallet: any, payment: DetailedPaymentOption): Promise { try { - const { contractInteraction, recipient } = payment + const { contractInteraction, recipient, asset, chainMetadata } = payment + const { chainNamespace,chainId } = chainMetadata + + // ------ Process Solana payments ------ + if (chainNamespace === 'solana') { + // Check if wallet supports Solana operations + if (!wallet.getAddress || !wallet.signAndSendTransaction) { + throw new CheckoutError( + CheckoutErrorCode.INVALID_CHECKOUT_REQUEST, + 'Solana payment requires a compatible wallet' + ) + } + + // Contract interaction payment + if (contractInteraction && !recipient) { + if (contractInteraction.type === 'solana-instruction') { + return await this.processSolanaContractInteraction( + wallet, + contractInteraction as SolanaContractInteraction, + `${chainNamespace}:${chainId}` + ) + } + throw new CheckoutError(CheckoutErrorCode.UNSUPPORTED_CONTRACT_INTERACTION) + } + + // Direct payment (with recipient) + if (recipient && !contractInteraction) { + const recipientAddress = recipient.split(':')[2] + const assetParts = asset.split('/') + const assetNamespace = assetParts[1]?.split(':')[0] + const assetReference = assetParts[1]?.split(':')[1] + + // Handle SOL transfers (slip44:501) + if (assetNamespace === 'slip44' && assetReference === '501') { + return await this.processSolanaDirectPayment( + wallet, + recipientAddress, + payment.amount, + `${chainNamespace}:${chainId}` + ) + } + + // Handle SPL token transfers (token:) + if (assetNamespace === 'token') { + return await this.processSolanaSplTokenPayment( + wallet, + recipientAddress, + payment.amount, + assetReference, + `${chainNamespace}:${chainId}` + ) + } + + throw new CheckoutError( + CheckoutErrorCode.UNSUPPORTED_CONTRACT_INTERACTION, + `Unsupported Solana asset type: ${assetNamespace}:${assetReference}` + ) + } + + // Neither or both are present + throw new CheckoutError( + CheckoutErrorCode.INVALID_CHECKOUT_REQUEST, + 'Payment must have either recipient or contractInteraction, not both or neither' + ) + } + + // ------ Process EVM payments (existing code) ------ + // Ensure wallet is an EVM wallet + if (!wallet.sendTransaction) { + throw new CheckoutError( + CheckoutErrorCode.INVALID_CHECKOUT_REQUEST, + 'EVM payment requires an EVM wallet' + ) + } // Direct payment (with recipient) if (recipient && !contractInteraction) { diff --git a/advanced/wallets/react-wallet-v2/src/views/SessionCheckoutModal.tsx b/advanced/wallets/react-wallet-v2/src/views/SessionCheckoutModal.tsx index 22a6c46de..6472ecfaf 100644 --- a/advanced/wallets/react-wallet-v2/src/views/SessionCheckoutModal.tsx +++ b/advanced/wallets/react-wallet-v2/src/views/SessionCheckoutModal.tsx @@ -22,6 +22,8 @@ import { providers } from 'ethers' import { EIP155_CHAINS, TEIP155Chain } from '@/data/EIP155Data' import WalletCheckoutPaymentHandler from '@/utils/WalletCheckoutPaymentHandler' import WalletCheckoutCtrl from '@/store/WalletCheckoutCtrl' +import { solanaWallets } from '@/utils/SolanaWalletUtil' + // Custom styles for the modal const modalStyles = { modal: { @@ -70,7 +72,8 @@ export default function SessionCheckoutModal() { const checkoutRequest = useMemo(() => request?.params?.[0] || ({} as CheckoutRequest), [request]) // Use our custom hook to fetch payments - const address = SettingsStore.state.eip155Address + const eip155Address = SettingsStore.state.eip155Address + const solanaAddress = SettingsStore.state.solanaAddress const feasiblePayments = WalletCheckoutCtrl.state.feasiblePayments // Handle reject action @@ -94,6 +97,35 @@ export default function SessionCheckoutModal() { } }, [requestEvent, topic]) + // Get the appropriate wallet based on the selected payment's chain namespace + const getWalletForPayment = (payment: DetailedPaymentOption) => { + const { chainMetadata } = payment + const { chainNamespace, chainId } = chainMetadata + + if (chainNamespace === 'eip155') { + const wallet = eip155Wallets[eip155Address] + if (!(wallet instanceof EIP155Lib)) { + throw new Error('EVM wallet not available') + } + + // Set up the provider + const provider = new providers.JsonRpcProvider( + EIP155_CHAINS[`eip155:${chainId}` as TEIP155Chain].rpc + ) + return wallet.connect(provider) + } + else if (chainNamespace === 'solana') { + const wallet = solanaWallets[solanaAddress] + console.log({solanaWallet: wallet}) + if (!wallet) { + throw new Error('Solana wallet not available') + } + return wallet + } + + throw new Error(`Unsupported chain namespace: ${chainNamespace}`) + } + // Handle approve action const onApprove = useCallback(async () => { if (!requestEvent || !topic || !selectedPayment) return @@ -104,25 +136,11 @@ export default function SessionCheckoutModal() { // Validate the request before processing WalletCheckoutPaymentHandler.validateCheckoutExpiry(checkoutRequest) - const wallet = eip155Wallets[address] - - if (!(wallet instanceof EIP155Lib)) { - throw new Error('Wallet not available') - } - - // Set up the provider - const { chainMetadata } = selectedPayment - const { chainId } = chainMetadata - const provider = new providers.JsonRpcProvider( - EIP155_CHAINS[`eip155:${chainId}` as TEIP155Chain].rpc - ) - const connectedWallet = wallet.connect(provider) + // Get the wallet for this payment + const wallet = getWalletForPayment(selectedPayment) // Process the payment using the unified method - const result = await WalletCheckoutPaymentHandler.processPayment( - connectedWallet, - selectedPayment - ) + const result = await WalletCheckoutPaymentHandler.processPayment(wallet, selectedPayment) // Handle the result if (result.txHash) { @@ -154,7 +172,7 @@ export default function SessionCheckoutModal() { setIsLoadingApprove(false) ModalStore.close() } - }, [checkoutRequest, requestEvent, selectedPayment, topic, address]) + }, [checkoutRequest, requestEvent, selectedPayment, topic]) // Handle payment selection const onSelectPayment = useCallback((payment: DetailedPaymentOption) => { diff --git a/advanced/wallets/react-wallet-v2/yarn.lock b/advanced/wallets/react-wallet-v2/yarn.lock index 27b7af60e..e38052af9 100644 --- a/advanced/wallets/react-wallet-v2/yarn.lock +++ b/advanced/wallets/react-wallet-v2/yarn.lock @@ -2342,13 +2342,111 @@ "@noble/hashes" "~1.7.1" "@scure/base" "~1.2.4" -"@solana/buffer-layout@^4.0.1": +"@solana/buffer-layout-utils@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@solana/buffer-layout-utils/-/buffer-layout-utils-0.2.0.tgz#b45a6cab3293a2eb7597cceb474f229889d875ca" + integrity sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g== + dependencies: + "@solana/buffer-layout" "^4.0.0" + "@solana/web3.js" "^1.32.0" + bigint-buffer "^1.1.5" + bignumber.js "^9.0.1" + +"@solana/buffer-layout@^4.0.0", "@solana/buffer-layout@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz#b996235eaec15b1e0b5092a8ed6028df77fa6c15" integrity sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA== dependencies: buffer "~6.0.3" +"@solana/codecs-core@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/codecs-core/-/codecs-core-2.0.0-rc.1.tgz#1a2d76b9c7b9e7b7aeb3bd78be81c2ba21e3ce22" + integrity sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ== + dependencies: + "@solana/errors" "2.0.0-rc.1" + +"@solana/codecs-data-structures@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-rc.1.tgz#d47b2363d99fb3d643f5677c97d64a812982b888" + integrity sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog== + dependencies: + "@solana/codecs-core" "2.0.0-rc.1" + "@solana/codecs-numbers" "2.0.0-rc.1" + "@solana/errors" "2.0.0-rc.1" + +"@solana/codecs-numbers@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/codecs-numbers/-/codecs-numbers-2.0.0-rc.1.tgz#f34978ddf7ea4016af3aaed5f7577c1d9869a614" + integrity sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ== + dependencies: + "@solana/codecs-core" "2.0.0-rc.1" + "@solana/errors" "2.0.0-rc.1" + +"@solana/codecs-strings@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/codecs-strings/-/codecs-strings-2.0.0-rc.1.tgz#e1d9167075b8c5b0b60849f8add69c0f24307018" + integrity sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g== + dependencies: + "@solana/codecs-core" "2.0.0-rc.1" + "@solana/codecs-numbers" "2.0.0-rc.1" + "@solana/errors" "2.0.0-rc.1" + +"@solana/codecs@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/codecs/-/codecs-2.0.0-rc.1.tgz#146dc5db58bd3c28e04b4c805e6096c2d2a0a875" + integrity sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ== + dependencies: + "@solana/codecs-core" "2.0.0-rc.1" + "@solana/codecs-data-structures" "2.0.0-rc.1" + "@solana/codecs-numbers" "2.0.0-rc.1" + "@solana/codecs-strings" "2.0.0-rc.1" + "@solana/options" "2.0.0-rc.1" + +"@solana/errors@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/errors/-/errors-2.0.0-rc.1.tgz#3882120886eab98a37a595b85f81558861b29d62" + integrity sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ== + dependencies: + chalk "^5.3.0" + commander "^12.1.0" + +"@solana/options@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/options/-/options-2.0.0-rc.1.tgz#06924ba316dc85791fc46726a51403144a85fc4d" + integrity sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA== + dependencies: + "@solana/codecs-core" "2.0.0-rc.1" + "@solana/codecs-data-structures" "2.0.0-rc.1" + "@solana/codecs-numbers" "2.0.0-rc.1" + "@solana/codecs-strings" "2.0.0-rc.1" + "@solana/errors" "2.0.0-rc.1" + +"@solana/spl-token-group@^0.0.7": + version "0.0.7" + resolved "https://registry.yarnpkg.com/@solana/spl-token-group/-/spl-token-group-0.0.7.tgz#83c00f0cd0bda33115468cd28b89d94f8ec1fee4" + integrity sha512-V1N/iX7Cr7H0uazWUT2uk27TMqlqedpXHRqqAbVO2gvmJyT0E0ummMEAVQeXZ05ZhQ/xF39DLSdBp90XebWEug== + dependencies: + "@solana/codecs" "2.0.0-rc.1" + +"@solana/spl-token-metadata@^0.1.6": + version "0.1.6" + resolved "https://registry.yarnpkg.com/@solana/spl-token-metadata/-/spl-token-metadata-0.1.6.tgz#d240947aed6e7318d637238022a7b0981b32ae80" + integrity sha512-7sMt1rsm/zQOQcUWllQX9mD2O6KhSAtY1hFR2hfFwgqfFWzSY9E9GDvFVNYUI1F0iQKcm6HmePU9QbKRXTEBiA== + dependencies: + "@solana/codecs" "2.0.0-rc.1" + +"@solana/spl-token@^0.4.13": + version "0.4.13" + resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.4.13.tgz#8f65c3c2b315e1a00a91b8d0f60922c6eb71de62" + integrity sha512-cite/pYWQZZVvLbg5lsodSovbetK/eA24gaR0eeUeMuBAMNrT8XFCwaygKy0N2WSg3gSyjjNpIeAGBAKZaY/1w== + 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" + buffer "^6.0.3" + "@solana/web3.js@1.89.2": version "1.89.2" resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.89.2.tgz#d3d732f54eba86d5a13202d95385820ae9d90343" @@ -2370,6 +2468,27 @@ rpc-websockets "^7.5.1" superstruct "^0.14.2" +"@solana/web3.js@^1.32.0": + version "1.98.0" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.98.0.tgz#21ecfe8198c10831df6f0cfde7f68370d0405917" + integrity sha512-nz3Q5OeyGFpFCR+erX2f6JPt3sKhzhYcSycBCSPkWjzSVDh/Rr1FqTVMRe58FKO16/ivTUcuJjeS5MyBvpkbzA== + dependencies: + "@babel/runtime" "^7.25.0" + "@noble/curves" "^1.4.2" + "@noble/hashes" "^1.4.0" + "@solana/buffer-layout" "^4.0.1" + agentkeepalive "^4.5.0" + bigint-buffer "^1.1.5" + bn.js "^5.2.1" + borsh "^0.7.0" + bs58 "^4.0.1" + buffer "6.0.3" + fast-stable-stringify "^1.0.0" + jayson "^4.1.1" + node-fetch "^2.7.0" + rpc-websockets "^9.0.2" + superstruct "^2.0.2" + "@solana/web3.js@^1.66.2": version "1.95.4" resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.95.4.tgz#771603f60d75cf7556ad867e1fd2efae32f9ad09" @@ -3816,6 +3935,11 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^5.3.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.4.1.tgz#1b48bf0963ec158dce2aacf69c093ae2dd2092d8" + integrity sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w== + character-entities-legacy@^1.0.0: version "1.1.4" resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1" @@ -3928,6 +4052,11 @@ comma-separated-tokens@^1.0.0: resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea" integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw== +commander@^12.1.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" + integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== + commander@^2.20.3: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" From 2dfa46d50abac1fe7f63a842af42f1cd7db2a54a Mon Sep 17 00:00:00 2001 From: Karandeep Singh Date: Tue, 25 Mar 2025 09:10:19 +0530 Subject: [PATCH 08/13] chore: run prettier --- .../react-wallet-v2/src/data/tokenUtil.ts | 9 +- .../react-wallet-v2/src/lib/SolanaLib.ts | 109 ++++---- .../src/schema/WalletCheckoutSchema.ts | 184 +++++++------ .../src/types/wallet_checkout.ts | 19 +- .../src/utils/PaymentValidatorUtil.ts | 244 +++++++++--------- .../src/utils/WalletCheckoutPaymentHandler.ts | 123 +++++---- .../src/utils/WalletCheckoutUtil.ts | 1 - .../src/views/SessionCheckoutModal.tsx | 12 +- .../src/views/SessionProposalModal.tsx | 4 +- 9 files changed, 364 insertions(+), 341 deletions(-) diff --git a/advanced/wallets/react-wallet-v2/src/data/tokenUtil.ts b/advanced/wallets/react-wallet-v2/src/data/tokenUtil.ts index dd7d4f79c..d64234eaa 100644 --- a/advanced/wallets/react-wallet-v2/src/data/tokenUtil.ts +++ b/advanced/wallets/react-wallet-v2/src/data/tokenUtil.ts @@ -29,7 +29,7 @@ const ALL_TOKENS: EIP155Token[] = [ name: 'SOL', icon: '/token-logos/SOL.png', symbol: 'SOL', - decimals: 9, + decimals: 9 } ] @@ -37,17 +37,18 @@ export function getTokenData(tokenSymbol: string) { return Object.values(ALL_TOKENS).find(token => token.symbol === tokenSymbol) } - const SOLANA_KNOWN_TOKENS = [ { name: 'USDC', icon: '/token-logos/USDC.png', symbol: 'USDC', decimals: 6, - assetAddress:['solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/token:4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU'] + assetAddress: [ + 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/token:4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU' + ] } ] export function getSolanaTokenData(caip19AssetAddress: string) { return SOLANA_KNOWN_TOKENS.find(token => token.assetAddress.includes(caip19AssetAddress)) -} \ No newline at end of file +} diff --git a/advanced/wallets/react-wallet-v2/src/lib/SolanaLib.ts b/advanced/wallets/react-wallet-v2/src/lib/SolanaLib.ts index c695ce24e..0dab0d7bd 100644 --- a/advanced/wallets/react-wallet-v2/src/lib/SolanaLib.ts +++ b/advanced/wallets/react-wallet-v2/src/lib/SolanaLib.ts @@ -1,7 +1,20 @@ -import { Keypair, Connection, SendOptions, VersionedTransaction, PublicKey, Transaction, SystemProgram } from '@solana/web3.js' +import { + Keypair, + Connection, + SendOptions, + VersionedTransaction, + PublicKey, + Transaction, + SystemProgram +} from '@solana/web3.js' import bs58 from 'bs58' import nacl from 'tweetnacl' -import { getAssociatedTokenAddress, createTransferInstruction, TOKEN_PROGRAM_ID, getOrCreateAssociatedTokenAccount } from '@solana/spl-token' +import { + getAssociatedTokenAddress, + createTransferInstruction, + TOKEN_PROGRAM_ID, + getOrCreateAssociatedTokenAccount +} from '@solana/spl-token' import { SOLANA_MAINNET_CHAINS, SOLANA_TEST_CHAINS } from '@/data/SolanaData' /** @@ -120,23 +133,19 @@ export default class SolanaLib { * @param amount The amount to send in lamports (as a bigint) * @returns The transaction signature/hash */ - public async sendSol( - recipientAddress: string, - chainId: string, - amount: bigint - ): Promise { - console.log({chainId}) + public async sendSol(recipientAddress: string, chainId: string, amount: bigint): Promise { + console.log({ chainId }) try { const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc - if (!rpc) { - throw new Error('There is no RPC URL for the provided chain') - } + if (!rpc) { + throw new Error('There is no RPC URL for the provided chain') + } - const connection = new Connection(rpc, 'confirmed') + const connection = new Connection(rpc, 'confirmed') const fromPubkey = this.keypair.publicKey const toPubkey = new PublicKey(recipientAddress) - + // Create a simple SOL transfer transaction const transaction = new Transaction().add( SystemProgram.transfer({ @@ -145,21 +154,21 @@ export default class SolanaLib { lamports: amount }) ) - + // Get recent blockhash const { blockhash } = await connection.getLatestBlockhash('confirmed') transaction.recentBlockhash = blockhash transaction.feePayer = fromPubkey - + // Sign the transaction transaction.sign(this.keypair) - + // Send and confirm the transaction const signature = await connection.sendRawTransaction(transaction.serialize()) - + // Wait for confirmation await connection.confirmTransaction(signature, 'confirmed') - + return signature } catch (error) { console.error('Error sending SOL:', error) @@ -183,38 +192,35 @@ export default class SolanaLib { try { const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc - if (!rpc) { - throw new Error('There is no RPC URL for the provided chain') - } - + if (!rpc) { + throw new Error('There is no RPC URL for the provided chain') + } + const connection = new Connection(rpc, 'confirmed') const fromWallet = this.keypair const fromPubkey = fromWallet.publicKey const toPubkey = new PublicKey(recipientAddress) const mint = new PublicKey(tokenAddress) - + // Get sender's token account (create if it doesn't exist) const fromTokenAccount = await getOrCreateAssociatedTokenAccount( - connection, - fromWallet, - mint, + connection, + fromWallet, + mint, fromPubkey ) - + // Check if recipient has a token account WITHOUT creating one - const associatedTokenAddress = await getAssociatedTokenAddress( - mint, - toPubkey - ) - + const associatedTokenAddress = await getAssociatedTokenAddress(mint, toPubkey) + const recipientTokenAccount = await connection.getAccountInfo(associatedTokenAddress) - + if (!recipientTokenAccount) { throw new Error( `Recipient ${recipientAddress} doesn't have a token account for this SPL token. Transaction cannot proceed.` ) } - + // Create transfer instruction to existing account const transferInstruction = createTransferInstruction( fromTokenAccount.address, @@ -224,24 +230,24 @@ export default class SolanaLib { [], TOKEN_PROGRAM_ID ) - + // Create transaction and add the transfer instruction const transaction = new Transaction().add(transferInstruction) - + // Get recent blockhash const { blockhash } = await connection.getLatestBlockhash('confirmed') transaction.recentBlockhash = blockhash transaction.feePayer = fromPubkey - + // Sign the transaction transaction.sign(fromWallet) - + // Send and confirm the transaction const signature = await connection.sendRawTransaction(transaction.serialize()) - + // Wait for confirmation await connection.confirmTransaction(signature, 'confirmed') - + return signature } catch (error) { console.error('Error sending SPL token:', error) @@ -254,37 +260,34 @@ export default class SolanaLib { * @param transaction The transaction to sign and send * @returns The transaction signature */ - public async sendRawTransaction( - transaction: Transaction, - chainId: string - ): Promise { + public async sendRawTransaction(transaction: Transaction, chainId: string): Promise { try { const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc - if (!rpc) { - throw new Error('There is no RPC URL for the provided chain') - } - + if (!rpc) { + throw new Error('There is no RPC URL for the provided chain') + } + const connection = new Connection(rpc, 'confirmed') - + // Set fee payer if not already set if (!transaction.feePayer) { transaction.feePayer = this.keypair.publicKey } - + // Get recent blockhash if not already set if (!transaction.recentBlockhash) { const { blockhash } = await connection.getLatestBlockhash('confirmed') transaction.recentBlockhash = blockhash } - + // Sign transaction transaction.sign(this.keypair) - + // Send and confirm transaction const signature = await connection.sendRawTransaction(transaction.serialize()) await connection.confirmTransaction(signature, 'confirmed') - + return signature } catch (error) { console.error('Error signing and sending transaction:', error) diff --git a/advanced/wallets/react-wallet-v2/src/schema/WalletCheckoutSchema.ts b/advanced/wallets/react-wallet-v2/src/schema/WalletCheckoutSchema.ts index 1588e40ac..3af464aa4 100644 --- a/advanced/wallets/react-wallet-v2/src/schema/WalletCheckoutSchema.ts +++ b/advanced/wallets/react-wallet-v2/src/schema/WalletCheckoutSchema.ts @@ -1,4 +1,9 @@ -import { CheckoutErrorCode, ContractInteraction, createCheckoutError, SolanaContractInteraction } from '@/types/wallet_checkout' +import { + CheckoutErrorCode, + ContractInteraction, + createCheckoutError, + SolanaContractInteraction +} from '@/types/wallet_checkout' import { z } from 'zod' // ======== Helper Validation Functions ======== @@ -45,12 +50,12 @@ export function isValidCAIP10AccountId(accountId: string): boolean { export function isValidSolanaInstruction(instruction: SolanaContractInteraction['data']): boolean { try { if (!instruction || typeof instruction !== 'object') return false - + // Check for required properties if (!instruction.programId || typeof instruction.programId !== 'string') return false if (!instruction.accounts || !Array.isArray(instruction.accounts)) return false if (!instruction.data || typeof instruction.data !== 'string') return false - + // Validate each account for (const account of instruction.accounts) { if (!account || typeof account !== 'object') return false @@ -58,7 +63,7 @@ export function isValidSolanaInstruction(instruction: SolanaContractInteraction[ if (typeof account.isSigner !== 'boolean') return false if (typeof account.isWritable !== 'boolean') return false } - + return true } catch (e) { return false @@ -68,12 +73,12 @@ export function isValidSolanaInstruction(instruction: SolanaContractInteraction[ /** * Checks if an EVM call is valid */ -export function isValidEvmCall(call: {to: string, data: string, value?: string}): boolean { - if (!call.to || typeof call.to !== 'string') return false; - if (!call.data || typeof call.data !== 'string') return false; +export function isValidEvmCall(call: { to: string; data: string; value?: string }): boolean { + if (!call.to || typeof call.to !== 'string') return false + if (!call.data || typeof call.data !== 'string') return false // Check value only if it's provided - if (call.value !== undefined && (typeof call.value !== 'string' || !call.value)) return false; - return true; + if (call.value !== undefined && (typeof call.value !== 'string' || !call.value)) return false + return true } /** @@ -106,46 +111,46 @@ export function matchingChainIds(assetId: string, accountId: string): boolean { */ function validateSolanaAsset(asset: string, ctx: z.RefinementCtx) { const assetParts = asset.split('/') - if (assetParts.length !== 2) return; + if (assetParts.length !== 2) return const chainParts = assetParts[0].split(':') - if (chainParts[0] !== 'solana') return; - + if (chainParts[0] !== 'solana') return + // For Solana assets, validate asset namespace and reference const assetType = assetParts[1].split(':') if (assetType.length !== 2) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Invalid Solana asset format: ${asset}` - }); + }) throw createCheckoutError( CheckoutErrorCode.INVALID_CHECKOUT_REQUEST, `Invalid Solana asset format: ${asset}` - ); + ) } - + // Check supported Solana asset namespaces if (assetType[0] !== 'slip44' && assetType[0] !== 'token') { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Unsupported Solana asset namespace: ${assetType[0]}` - }); + }) throw createCheckoutError( CheckoutErrorCode.INVALID_CHECKOUT_REQUEST, `Unsupported Solana asset namespace: ${assetType[0]}` - ); + ) } - + // For slip44, validate the coin type is 501 for SOL if (assetType[0] === 'slip44' && assetType[1] !== '501') { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Invalid Solana slip44 asset reference: ${assetType[1]}` - }); + }) throw createCheckoutError( CheckoutErrorCode.INVALID_CHECKOUT_REQUEST, `Invalid Solana slip44 asset reference: ${assetType[1]}` - ); + ) } } @@ -154,46 +159,46 @@ function validateSolanaAsset(asset: string, ctx: z.RefinementCtx) { */ function validateEvmAsset(asset: string, ctx: z.RefinementCtx) { const assetParts = asset.split('/') - if (assetParts.length !== 2) return; + if (assetParts.length !== 2) return const chainParts = assetParts[0].split(':') - if (chainParts[0] !== 'eip155') return; - + if (chainParts[0] !== 'eip155') return + // For EVM assets, validate asset namespace and reference const assetType = assetParts[1].split(':') if (assetType.length !== 2) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Invalid EVM asset format: ${asset}` - }); + }) throw createCheckoutError( CheckoutErrorCode.INVALID_CHECKOUT_REQUEST, `Invalid EVM asset format: ${asset}` - ); + ) } - + // Check supported EVM asset namespaces if (assetType[0] !== 'slip44' && assetType[0] !== 'erc20') { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Unsupported EVM asset namespace: ${assetType[0]}` - }); + }) throw createCheckoutError( CheckoutErrorCode.INVALID_CHECKOUT_REQUEST, `Unsupported EVM asset namespace: ${assetType[0]}` - ); + ) } - + // For slip44, validate the coin type is 60 for ETH if (assetType[0] === 'slip44' && assetType[1] !== '60') { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Invalid EVM slip44 asset reference: ${assetType[1]}` - }); + }) throw createCheckoutError( CheckoutErrorCode.INVALID_CHECKOUT_REQUEST, `Invalid EVM slip44 asset reference: ${assetType[1]}` - ); + ) } } @@ -202,18 +207,18 @@ function validateEvmAsset(asset: string, ctx: z.RefinementCtx) { */ function validateAssetFormat(asset: string, ctx: z.RefinementCtx) { const assetParts = asset.split('/') - if (assetParts.length !== 2) return; + if (assetParts.length !== 2) return const chainParts = assetParts[0].split(':') - + // Validate based on chain namespace switch (chainParts[0]) { case 'solana': - validateSolanaAsset(asset, ctx); - break; + validateSolanaAsset(asset, ctx) + break case 'eip155': - validateEvmAsset(asset, ctx); - break; + validateEvmAsset(asset, ctx) + break } } @@ -240,42 +245,48 @@ export const SolanaInstructionDataSchema = z.object({ // ======== Contract Interaction Schemas ======== -const EvmCallSchema = z.object({ - to: z.string().min(1), - data: z.string().min(1), - value: z.string().optional() -}).refine(isValidEvmCall, { - message: 'Invalid EVM call data' -}); - -const SolanaInstructionSchema = z.object({ - programId: z.string().min(1), - accounts: z.array(SolanaAccountSchema).min(1), - data: z.string().min(1) -}).refine(isValidSolanaInstruction, { - message: 'Invalid Solana instruction data' -}); - -export const ContractInteractionSchema = z.object({ - type: z.string().min(1, 'Contract interaction type is required'), - data: z.any() -}).superRefine((interaction, ctx) => { - // Check if interaction type is supported - if (interaction.type !== 'evm-calls' && interaction.type !== 'solana-instruction') { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: 'Unsupported contract interaction type' - }); - throw createCheckoutError(CheckoutErrorCode.UNSUPPORTED_CONTRACT_INTERACTION); - } +const EvmCallSchema = z + .object({ + to: z.string().min(1), + data: z.string().min(1), + value: z.string().optional() + }) + .refine(isValidEvmCall, { + message: 'Invalid EVM call data' + }) - // Validate based on interaction type - if (interaction.type === 'evm-calls') { - validateEvmCalls(interaction, ctx); - } else if (interaction.type === 'solana-instruction') { - validateSolanaInstruction(interaction, ctx); - } -}); +const SolanaInstructionSchema = z + .object({ + programId: z.string().min(1), + accounts: z.array(SolanaAccountSchema).min(1), + data: z.string().min(1) + }) + .refine(isValidSolanaInstruction, { + message: 'Invalid Solana instruction data' + }) + +export const ContractInteractionSchema = z + .object({ + type: z.string().min(1, 'Contract interaction type is required'), + data: z.any() + }) + .superRefine((interaction, ctx) => { + // Check if interaction type is supported + if (interaction.type !== 'evm-calls' && interaction.type !== 'solana-instruction') { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Unsupported contract interaction type' + }) + throw createCheckoutError(CheckoutErrorCode.UNSUPPORTED_CONTRACT_INTERACTION) + } + + // Validate based on interaction type + if (interaction.type === 'evm-calls') { + validateEvmCalls(interaction, ctx) + } else if (interaction.type === 'solana-instruction') { + validateSolanaInstruction(interaction, ctx) + } + }) // Extracted validation functions for cleaner code function validateEvmCalls(interaction: any, ctx: z.RefinementCtx) { @@ -283,16 +294,16 @@ function validateEvmCalls(interaction: any, ctx: z.RefinementCtx) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Invalid EVM calls data structure' - }); - throw createCheckoutError(CheckoutErrorCode.INVALID_CONTRACT_INTERACTION_DATA); + }) + throw createCheckoutError(CheckoutErrorCode.INVALID_CONTRACT_INTERACTION_DATA) } - + // Validate each EVM call for (const call of interaction.data) { try { - EvmCallSchema.parse(call); + EvmCallSchema.parse(call) } catch (e) { - throw createCheckoutError(CheckoutErrorCode.INVALID_CONTRACT_INTERACTION_DATA); + throw createCheckoutError(CheckoutErrorCode.INVALID_CONTRACT_INTERACTION_DATA) } } } @@ -302,24 +313,25 @@ function validateSolanaInstruction(interaction: any, ctx: z.RefinementCtx) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Invalid Solana instruction data structure' - }); - throw createCheckoutError(CheckoutErrorCode.INVALID_CONTRACT_INTERACTION_DATA); + }) + throw createCheckoutError(CheckoutErrorCode.INVALID_CONTRACT_INTERACTION_DATA) } - + try { - SolanaInstructionSchema.parse(interaction.data); + SolanaInstructionSchema.parse(interaction.data) } catch (e) { - throw createCheckoutError(CheckoutErrorCode.INVALID_CONTRACT_INTERACTION_DATA); + throw createCheckoutError(CheckoutErrorCode.INVALID_CONTRACT_INTERACTION_DATA) } } // ======== Payment Schema Definitions ======== // Asset validation schema with chain-specific checks -const AssetSchema = z.string() +const AssetSchema = z + .string() .min(1, 'Asset is required') .refine(isValidCAIP19AssetId, 'Invalid CAIP-19 asset') - .superRefine(validateAssetFormat); + .superRefine(validateAssetFormat) export const PaymentOptionSchema = z .object({ @@ -337,7 +349,7 @@ export const PaymentOptionSchema = z .refine(data => { if (!data.recipient) return true return matchingChainIds(data.asset, data.recipient) - }, 'Asset and recipient must be on the same chain'); + }, 'Asset and recipient must be on the same chain') // ======== Checkout Request Schema ======== @@ -346,4 +358,4 @@ export const CheckoutRequestSchema = z.object({ acceptedPayments: z.array(PaymentOptionSchema).min(1, 'At least one payment option is required'), products: z.array(ProductMetadataSchema).optional(), expiry: z.number().int().optional() -}); \ No newline at end of file +}) diff --git a/advanced/wallets/react-wallet-v2/src/types/wallet_checkout.ts b/advanced/wallets/react-wallet-v2/src/types/wallet_checkout.ts index bc40d6fa0..877420fe3 100644 --- a/advanced/wallets/react-wallet-v2/src/types/wallet_checkout.ts +++ b/advanced/wallets/react-wallet-v2/src/types/wallet_checkout.ts @@ -58,16 +58,17 @@ export type EvmContractInteraction = { * @property data - Array of Solana instruction data objects */ export type SolanaContractInteraction = { - type: "solana-instruction"; + type: 'solana-instruction' data: { - programId: string; // Program ID - accounts: { // Accounts involved in the instruction - pubkey: string; - isSigner: boolean; - isWritable: boolean; - }[]; - data: string; // Base64-encoded instruction data - }; + programId: string // Program ID + accounts: { + // Accounts involved in the instruction + pubkey: string + isSigner: boolean + isWritable: boolean + }[] + data: string // Base64-encoded instruction data + } } /** diff --git a/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts b/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts index 54027fdc2..5eef14c95 100644 --- a/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts +++ b/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts @@ -386,7 +386,7 @@ export class PaymentValidationUtils { } } - /** + /** * Validates a single direct payment option for Solana and creates a detailed version if valid * * @param payment - Payment option to validate @@ -418,22 +418,25 @@ export class PaymentValidationUtils { // Get token details based on asset namespace let tokenDetails: TokenDetails - + if (assetNamespace === 'slip44' && assetAddress === '501') { // Native SOL tokenDetails = await this.getSolNativeAssetDetails(account, `${chainNamespace}:${chainId}`) - } - else if (assetNamespace === 'token') { + } else if (assetNamespace === 'token') { // SPL token - tokenDetails = await this.getSplTokenDetails(assetAddress, account, `${chainNamespace}:${chainId}`, payment.asset) - } - else { + tokenDetails = await this.getSplTokenDetails( + assetAddress, + account, + `${chainNamespace}:${chainId}`, + payment.asset + ) + } else { return { validatedPayment: null, hasMatchingAsset: false } } // Check if user has the asset (balance > 0) const hasMatchingAsset = tokenDetails.balance > BigInt(0) - + if (!hasMatchingAsset) { return { validatedPayment: null, hasMatchingAsset } } @@ -456,7 +459,7 @@ export class PaymentValidationUtils { /** * Validates a single direct payment option and creates a detailed version if valid - * + * * @param payment - Payment option to validate * @param evmAccount - EVM account address (if available) * @param solanaAccount - Solana account address (if available) @@ -473,15 +476,14 @@ export class PaymentValidationUtils { try { // Parse asset details to determine chain const { chainNamespace } = PaymentValidationUtils.getAssetDetails(payment.asset) - + // Delegate to chain-specific methods if (chainNamespace === 'eip155' && evmAccount) { return await this.getDetailedDirectPaymentOptionEVM(payment, evmAccount) - } - else if (chainNamespace === 'solana' && solanaAccount) { + } else if (chainNamespace === 'solana' && solanaAccount) { return await this.getDetailedDirectPaymentOptionSolana(payment, solanaAccount) } - + return { validatedPayment: null, hasMatchingAsset: false } } catch (error) { console.error('Error getting detailed payment option:', error) @@ -521,16 +523,19 @@ export class PaymentValidationUtils { // Get token details based on asset namespace let tokenDetails: TokenDetails - + if (assetNamespace === 'slip44' && assetAddress === '501') { // Native SOL tokenDetails = await this.getSolNativeAssetDetails(account, `${chainNamespace}:${chainId}`) - } - else if (assetNamespace === 'token') { + } else if (assetNamespace === 'token') { // SPL token - tokenDetails = await this.getSplTokenDetails(assetAddress, account, `${chainNamespace}:${chainId}`, payment.asset) - } - else { + tokenDetails = await this.getSplTokenDetails( + assetAddress, + account, + `${chainNamespace}:${chainId}`, + payment.asset + ) + } else { return { validatedPayment: null, hasMatchingAsset: false } } @@ -555,8 +560,7 @@ export class PaymentValidationUtils { return { validatedPayment: null, hasMatchingAsset: false } } } - - + /** * Validates a contract payment option and creates a detailed version if valid * @@ -636,10 +640,9 @@ export class PaymentValidationUtils { } } - /** * Validates a contract payment option and creates a detailed version if valid - * + * * @param payment - Payment option to validate * @param evmAccount - EVM account address (if available) * @param solanaAccount - Solana account address (if available) @@ -656,15 +659,14 @@ export class PaymentValidationUtils { try { // Parse asset details to determine chain const { chainNamespace } = PaymentValidationUtils.getAssetDetails(payment.asset) - + // Delegate to chain-specific methods if (chainNamespace === 'eip155' && evmAccount) { return await this.getDetailedContractPaymentOptionEVM(payment, evmAccount) - } - else if (chainNamespace === 'solana' && solanaAccount) { + } else if (chainNamespace === 'solana' && solanaAccount) { return await this.getDetailedContractPaymentOptionSolana(payment, solanaAccount) } - + return { validatedPayment: null, hasMatchingAsset: false } } catch (error) { console.error('Error getting detailed contract payment option:', error) @@ -672,7 +674,6 @@ export class PaymentValidationUtils { } } - /** * Gets details for a Solana native asset (SOL) * @@ -685,134 +686,131 @@ export class PaymentValidationUtils { ): Promise { try { // Get the RPC URL for the chain - const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc; + const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc if (!rpc) { throw new Error('There is no RPC URL for the provided chain') } - + // Connect to Solana - const connection = new Connection(rpc, 'confirmed'); - const publicKey = new PublicKey(account); - + const connection = new Connection(rpc, 'confirmed') + const publicKey = new PublicKey(account) + // Get SOL balance - const balance = await connection.getBalance(publicKey); + const balance = await connection.getBalance(publicKey) console.log('Solana balance', balance) return { balance: BigInt(balance), - decimals: 9, + decimals: 9, symbol: 'SOL', name: 'Solana' - }; + } } catch (error) { - console.error('Error getting SOL balance:', error); - + console.error('Error getting SOL balance:', error) + // Return default values in case of error return { balance: BigInt(0), decimals: 9, symbol: 'SOL', name: 'Solana' - }; + } } } -/** - * Gets details for a fungible SPL token with known token mapping - * - * @param tokenAddress - Token mint address - * @param account - Account address - * @returns Token details including balance and metadata - */ -private static async getSplTokenDetails( - tokenAddress: string, - account: string, - chainId: string, - caip19AssetAddress: string -): Promise { - try { - // Get the RPC URL for the chain - const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc; - - if (!rpc) { - throw new Error('There is no RPC URL for the provided chain'); - } - - // Connect to Solana - const connection = new Connection(rpc, 'confirmed'); - const publicKey = new PublicKey(account); - const mintAddress = new PublicKey(tokenAddress); - + /** + * Gets details for a fungible SPL token with known token mapping + * + * @param tokenAddress - Token mint address + * @param account - Account address + * @returns Token details including balance and metadata + */ + private static async getSplTokenDetails( + tokenAddress: string, + account: string, + chainId: string, + caip19AssetAddress: string + ): Promise { try { - // Check if this is a known token - const token = getSolanaTokenData(caip19AssetAddress); - - // Get token balance - let balance = BigInt(0); - let decimals = token?.decimals || 6; // Use known token decimals or default - + // Get the RPC URL for the chain + const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc + + if (!rpc) { + throw new Error('There is no RPC URL for the provided chain') + } + + // Connect to Solana + const connection = new Connection(rpc, 'confirmed') + const publicKey = new PublicKey(account) + const mintAddress = new PublicKey(tokenAddress) + try { - // Find the associated token account(s) - const tokenAccounts = await connection.getParsedTokenAccountsByOwner( - publicKey, - { mint: mintAddress } - ); - - // If token account exists, get balance - if (tokenAccounts.value.length > 0) { - const tokenAccountPubkey = tokenAccounts.value[0].pubkey; - const tokenBalance = await connection.getTokenAccountBalance(tokenAccountPubkey); - balance = BigInt(tokenBalance.value.amount); - - // Update decimals from on-chain data if not a known token - if (!token) { - decimals = tokenBalance.value.decimals; - } - } else if (!token) { - // If no token accounts and not a known token, try to get decimals from mint - const mintInfo = await connection.getParsedAccountInfo(mintAddress); - if (mintInfo.value) { - const parsedMintInfo = (mintInfo.value.data as any).parsed?.info; - decimals = parsedMintInfo?.decimals || decimals; + // Check if this is a known token + const token = getSolanaTokenData(caip19AssetAddress) + + // Get token balance + let balance = BigInt(0) + let decimals = token?.decimals || 6 // Use known token decimals or default + + try { + // Find the associated token account(s) + const tokenAccounts = await connection.getParsedTokenAccountsByOwner(publicKey, { + mint: mintAddress + }) + + // If token account exists, get balance + if (tokenAccounts.value.length > 0) { + const tokenAccountPubkey = tokenAccounts.value[0].pubkey + const tokenBalance = await connection.getTokenAccountBalance(tokenAccountPubkey) + balance = BigInt(tokenBalance.value.amount) + + // Update decimals from on-chain data if not a known token + if (!token) { + decimals = tokenBalance.value.decimals + } + } else if (!token) { + // If no token accounts and not a known token, try to get decimals from mint + const mintInfo = await connection.getParsedAccountInfo(mintAddress) + if (mintInfo.value) { + const parsedMintInfo = (mintInfo.value.data as any).parsed?.info + decimals = parsedMintInfo?.decimals || decimals + } } + } catch (balanceError) { + console.warn('Error getting token balance:', balanceError) + // Continue with zero balance + } + + // Return with known metadata or fallback to generic + return { + balance, + decimals, + symbol: token?.symbol || tokenAddress.slice(0, 4).toUpperCase(), + name: token?.name || `SPL Token (${tokenAddress.slice(0, 8)}...)` + } + } catch (tokenError) { + console.warn('Error getting token details:', tokenError) + + // Return default values + return { + balance: BigInt(0), + decimals: 6, + symbol: tokenAddress.slice(0, 4).toUpperCase(), + name: 'SPL Token' } - } catch (balanceError) { - console.warn('Error getting token balance:', balanceError); - // Continue with zero balance } - - // Return with known metadata or fallback to generic - return { - balance, - decimals, - symbol: token?.symbol || tokenAddress.slice(0, 4).toUpperCase(), - name: token?.name || `SPL Token (${tokenAddress.slice(0, 8)}...)`, - }; - } catch (tokenError) { - console.warn('Error getting token details:', tokenError); - - // Return default values + } catch (error) { + console.error('Error getting SPL token details:', error) + + // Return default values in case of error return { balance: BigInt(0), decimals: 6, symbol: tokenAddress.slice(0, 4).toUpperCase(), name: 'SPL Token' - }; + } } - } catch (error) { - console.error('Error getting SPL token details:', error); - - // Return default values in case of error - return { - balance: BigInt(0), - decimals: 6, - symbol: tokenAddress.slice(0, 4).toUpperCase(), - name: 'SPL Token' - }; } -} - - /** * Finds and validates all feasible direct payment options diff --git a/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutPaymentHandler.ts b/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutPaymentHandler.ts index 9152199b9..647381b4b 100644 --- a/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutPaymentHandler.ts +++ b/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutPaymentHandler.ts @@ -4,18 +4,28 @@ import { Connection, PublicKey, Transaction, TransactionInstruction } from '@sol import bs58 from 'bs58' import { Buffer } from 'buffer' -import { DetailedPaymentOption, CheckoutErrorCode, CheckoutError, SolanaContractInteraction } from '@/types/wallet_checkout' +import { + DetailedPaymentOption, + CheckoutErrorCode, + CheckoutError, + SolanaContractInteraction +} from '@/types/wallet_checkout' import { Wallet } from 'ethers' import { SOLANA_MAINNET_CHAINS } from '@/data/SolanaData' import { SOLANA_TEST_CHAINS } from '@/data/SolanaData' // Assume SolanaLib interface based on similar patterns in the codebase interface SolanaLib { - getAddress(): string; - getChainId(): string; - signAndSendTransaction(transaction: Transaction): Promise; - sendSol(recipientAddress: string, chainId: string, amount: bigint): Promise; - sendSplToken(tokenAddress: string, recipientAddress: string, chainId: string, amount: bigint): Promise; + getAddress(): string + getChainId(): string + signAndSendTransaction(transaction: Transaction): Promise + sendSol(recipientAddress: string, chainId: string, amount: bigint): Promise + sendSplToken( + tokenAddress: string, + recipientAddress: string, + chainId: string, + amount: bigint + ): Promise } export interface PaymentResult { @@ -39,18 +49,18 @@ const WalletCheckoutPaymentHandler = { * Simulates a Solana transaction before sending */ async simulateSolanaTransaction( - connection: Connection, - transaction: Transaction, + connection: Connection, + transaction: Transaction, feePayer: PublicKey ): Promise { try { // Set recent blockhash for the transaction transaction.recentBlockhash = (await connection.getLatestBlockhash()).blockhash transaction.feePayer = feePayer - + // Simulate the transaction const simulation = await connection.simulateTransaction(transaction) - + // Check simulation results if (simulation.value.err) { console.error('Simulation error:', simulation.value.err) @@ -59,7 +69,7 @@ const WalletCheckoutPaymentHandler = { `Simulation failed: ${JSON.stringify(simulation.value.err)}` ) } - + console.log('Simulation successful:', simulation.value.logs) } catch (error) { if (error instanceof CheckoutError) { @@ -106,20 +116,25 @@ const WalletCheckoutPaymentHandler = { ): Promise { try { // Send SPL token to recipient - const txHash = await wallet.sendSplToken(tokenAddress, recipientAddress, chainId, BigInt(amount)) + const txHash = await wallet.sendSplToken( + tokenAddress, + recipientAddress, + chainId, + BigInt(amount) + ) return { txHash } } catch (error) { console.error('Solana SPL token payment error:', error) - + // Check if this is a token account error - const errorMessage = error instanceof Error ? error.message : String(error); + const errorMessage = error instanceof Error ? error.message : String(error) if (errorMessage.includes("doesn't have a token account")) { throw new CheckoutError( CheckoutErrorCode.DIRECT_PAYMENT_ERROR, `Recipient doesn't have a token account for this SPL token. The recipient must create a token account before they can receive this token.` ) } - + throw new CheckoutError( CheckoutErrorCode.DIRECT_PAYMENT_ERROR, `Failed to send SPL token: ${errorMessage}` @@ -136,92 +151,88 @@ const WalletCheckoutPaymentHandler = { chainId: string ): Promise { try { - const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc; + const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc if (!rpc) { throw new Error(`There is no RPC URL for the provided chain ${chainId}`) } const connection = new Connection(rpc) - + // Create a new transaction const transaction = new Transaction() - + const instruction = contractInteraction.data const accountMetas = instruction.accounts.map(acc => ({ pubkey: new PublicKey(acc.pubkey), isSigner: acc.isSigner, isWritable: acc.isWritable })) - + // Create the instruction const txInstruction = new TransactionInstruction({ programId: new PublicKey(instruction.programId), keys: accountMetas, data: Buffer.from(instruction.data, 'base64') }) - + // Add to transaction transaction.add(txInstruction) - + // Set the wallet's public key as feePayer const walletAddress = await wallet.getAddress() const publicKey = new PublicKey(walletAddress) transaction.feePayer = publicKey - + // Get recent blockhash from the connection const { blockhash } = await connection.getLatestBlockhash('confirmed') transaction.recentBlockhash = blockhash - + // Simulate transaction to check for errors - await this.simulateSolanaTransaction( - connection, - transaction, - publicKey - ) - + await this.simulateSolanaTransaction(connection, transaction, publicKey) + // Determine which method to use for signing and sending let txHash: string - + // Use sendRawTransaction if the wallet supports it (our implementation) if (typeof wallet.sendRawTransaction === 'function') { txHash = await wallet.sendRawTransaction(transaction, chainId) - } + } // Otherwise use standard signAndSendTransaction else if (typeof wallet.signAndSendTransaction === 'function') { // Serialize the transaction to bs58 format - const serializedBytes = transaction.serialize(); + const serializedBytes = transaction.serialize() // Convert Buffer to Uint8Array to avoid type issues - const serializedTx = bs58.encode(new Uint8Array(serializedBytes)); - + const serializedTx = bs58.encode(new Uint8Array(serializedBytes)) + // Use the wallet's signAndSendTransaction method const result = await wallet.signAndSendTransaction({ transaction: serializedTx, chainId: chainId }) - + // Handle different response formats from various wallet implementations if (typeof result === 'string') { - txHash = result; + txHash = result } else if (result && typeof result === 'object') { if ('signature' in result && typeof result.signature === 'string') { - txHash = result.signature; + txHash = result.signature } else if ('txHash' in result && typeof result.txHash === 'string') { - txHash = result.txHash; + txHash = result.txHash } else if ('transactionHash' in result && typeof result.transactionHash === 'string') { - txHash = result.transactionHash; + txHash = result.transactionHash } else { // Try to stringify the result if it's not in a recognized format - const stringResult = String(result); + const stringResult = String(result) if (stringResult && stringResult !== '[object Object]') { - txHash = stringResult; + txHash = stringResult } else { - throw new Error('Wallet returned an invalid transaction signature format'); + throw new Error('Wallet returned an invalid transaction signature format') } } } else { - throw new Error('Wallet returned an invalid response format'); + throw new Error('Wallet returned an invalid response format') } - + // Wait for transaction confirmation try { await connection.confirmTransaction(txHash, 'confirmed') @@ -230,7 +241,9 @@ const WalletCheckoutPaymentHandler = { console.error('Error confirming transaction:', confirmError) throw new CheckoutError( CheckoutErrorCode.CONTRACT_INTERACTION_FAILED, - `Transaction was sent but confirmation failed: ${confirmError instanceof Error ? confirmError.message : String(confirmError)}` + `Transaction was sent but confirmation failed: ${ + confirmError instanceof Error ? confirmError.message : String(confirmError) + }` ) } } @@ -241,7 +254,7 @@ const WalletCheckoutPaymentHandler = { 'Wallet does not support Solana transaction sending' ) } - + return { txHash } } catch (error) { console.error('Solana contract interaction error:', error) @@ -250,7 +263,9 @@ const WalletCheckoutPaymentHandler = { } throw new CheckoutError( CheckoutErrorCode.CONTRACT_INTERACTION_FAILED, - `Failed to execute Solana transaction: ${error instanceof Error ? error.message : String(error)}` + `Failed to execute Solana transaction: ${ + error instanceof Error ? error.message : String(error) + }` ) } }, @@ -261,7 +276,7 @@ const WalletCheckoutPaymentHandler = { async processPayment(wallet: any, payment: DetailedPaymentOption): Promise { try { const { contractInteraction, recipient, asset, chainMetadata } = payment - const { chainNamespace,chainId } = chainMetadata + const { chainNamespace, chainId } = chainMetadata // ------ Process Solana payments ------ if (chainNamespace === 'solana') { @@ -284,14 +299,14 @@ const WalletCheckoutPaymentHandler = { } throw new CheckoutError(CheckoutErrorCode.UNSUPPORTED_CONTRACT_INTERACTION) } - + // Direct payment (with recipient) if (recipient && !contractInteraction) { const recipientAddress = recipient.split(':')[2] const assetParts = asset.split('/') const assetNamespace = assetParts[1]?.split(':')[0] const assetReference = assetParts[1]?.split(':')[1] - + // Handle SOL transfers (slip44:501) if (assetNamespace === 'slip44' && assetReference === '501') { return await this.processSolanaDirectPayment( @@ -301,7 +316,7 @@ const WalletCheckoutPaymentHandler = { `${chainNamespace}:${chainId}` ) } - + // Handle SPL token transfers (token:) if (assetNamespace === 'token') { return await this.processSolanaSplTokenPayment( @@ -312,7 +327,7 @@ const WalletCheckoutPaymentHandler = { `${chainNamespace}:${chainId}` ) } - + throw new CheckoutError( CheckoutErrorCode.UNSUPPORTED_CONTRACT_INTERACTION, `Unsupported Solana asset type: ${assetNamespace}:${assetReference}` @@ -325,12 +340,12 @@ const WalletCheckoutPaymentHandler = { 'Payment must have either recipient or contractInteraction, not both or neither' ) } - + // ------ Process EVM payments (existing code) ------ // Ensure wallet is an EVM wallet if (!wallet.sendTransaction) { throw new CheckoutError( - CheckoutErrorCode.INVALID_CHECKOUT_REQUEST, + CheckoutErrorCode.INVALID_CHECKOUT_REQUEST, 'EVM payment requires an EVM wallet' ) } diff --git a/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutUtil.ts b/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutUtil.ts index bfc3a232d..5eea33db1 100644 --- a/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutUtil.ts +++ b/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutUtil.ts @@ -11,7 +11,6 @@ import { CheckoutRequestSchema } from '@/schema/WalletCheckoutSchema' import { PaymentValidationUtils } from './PaymentValidatorUtil' const WalletCheckoutUtil = { - /** * Format the recipient address for display * Shortens the address with ellipsis for better display diff --git a/advanced/wallets/react-wallet-v2/src/views/SessionCheckoutModal.tsx b/advanced/wallets/react-wallet-v2/src/views/SessionCheckoutModal.tsx index 6472ecfaf..7bb354d51 100644 --- a/advanced/wallets/react-wallet-v2/src/views/SessionCheckoutModal.tsx +++ b/advanced/wallets/react-wallet-v2/src/views/SessionCheckoutModal.tsx @@ -113,10 +113,9 @@ export default function SessionCheckoutModal() { EIP155_CHAINS[`eip155:${chainId}` as TEIP155Chain].rpc ) return wallet.connect(provider) - } - else if (chainNamespace === 'solana') { + } else if (chainNamespace === 'solana') { const wallet = solanaWallets[solanaAddress] - console.log({solanaWallet: wallet}) + console.log({ solanaWallet: wallet }) if (!wallet) { throw new Error('Solana wallet not available') } @@ -157,14 +156,11 @@ export default function SessionCheckoutModal() { await walletkit.respondSessionRequest({ topic, response }) styledToast('Payment approved successfully', 'success') - } + } } catch (error) { // Handle any unexpected errors console.error('Error processing payment:', error) - const response = WalletCheckoutUtil.formatCheckoutErrorResponse( - requestEvent.id, - error - ) + const response = WalletCheckoutUtil.formatCheckoutErrorResponse(requestEvent.id, error) await walletkit.respondSessionRequest({ topic, response }) styledToast((error as Error).message, 'error') diff --git a/advanced/wallets/react-wallet-v2/src/views/SessionProposalModal.tsx b/advanced/wallets/react-wallet-v2/src/views/SessionProposalModal.tsx index bbfc9fa48..91a9e9596 100644 --- a/advanced/wallets/react-wallet-v2/src/views/SessionProposalModal.tsx +++ b/advanced/wallets/react-wallet-v2/src/views/SessionProposalModal.tsx @@ -120,9 +120,7 @@ export default function SessionProposalModal() { return { eip155: { chains: eip155Chains, - methods: eip155Methods - .concat(eip5792Methods) - .concat(eip7715Methods), + methods: eip155Methods.concat(eip5792Methods).concat(eip7715Methods), events: ['accountsChanged', 'chainChanged'], accounts: eip155Chains .map(chain => From f7aabe3151105d6392f106698ee7d603d594a42f Mon Sep 17 00:00:00 2001 From: Karandeep Singh Date: Mon, 31 Mar 2025 23:46:54 +0530 Subject: [PATCH 09/13] chore: use declared const value --- .../react-wallet-v2/src/hooks/useWalletConnectEventsManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/advanced/wallets/react-wallet-v2/src/hooks/useWalletConnectEventsManager.ts b/advanced/wallets/react-wallet-v2/src/hooks/useWalletConnectEventsManager.ts index e9d9f00d2..d28be34cb 100644 --- a/advanced/wallets/react-wallet-v2/src/hooks/useWalletConnectEventsManager.ts +++ b/advanced/wallets/react-wallet-v2/src/hooks/useWalletConnectEventsManager.ts @@ -95,7 +95,7 @@ export default function useWalletConnectEventsManager(initialized: boolean) { return ModalStore.open('SessionSendCallsModal', { requestEvent, requestSession }) } - case 'wallet_checkout': + case EIP155_SIGNING_METHODS.WALLET_CHECKOUT: try { await WalletCheckoutCtrl.actions.prepareFeasiblePayments(request.params[0]) } catch (error) { From 4ff3c5571c1f92c4d5d603a40b91986ba2ee7ec6 Mon Sep 17 00:00:00 2001 From: Karandeep Singh Date: Tue, 1 Apr 2025 10:30:47 +0530 Subject: [PATCH 10/13] chore: refactored prepareFeasiblePayments --- .../public/chain-logos/chain-placeholder.png | Bin 0 -> 483 bytes .../public/chain-logos/chain-placeholder.svg | 14 - .../public/token-logos/token-placeholder.png | Bin 0 -> 518 bytes .../public/token-logos/token-placeholder.svg | 9 - .../react-wallet-v2/src/data/EIP155Data.ts | 2 +- .../hooks/useWalletConnectEventsManager.ts | 10 + .../src/utils/PaymentValidatorUtil.ts | 1174 ++++++++--------- .../src/utils/TransactionSimulatorUtil.ts | 67 +- .../src/utils/WalletCheckoutUtil.ts | 89 +- 9 files changed, 638 insertions(+), 727 deletions(-) create mode 100644 advanced/wallets/react-wallet-v2/public/chain-logos/chain-placeholder.png delete mode 100644 advanced/wallets/react-wallet-v2/public/chain-logos/chain-placeholder.svg create mode 100644 advanced/wallets/react-wallet-v2/public/token-logos/token-placeholder.png delete mode 100644 advanced/wallets/react-wallet-v2/public/token-logos/token-placeholder.svg diff --git a/advanced/wallets/react-wallet-v2/public/chain-logos/chain-placeholder.png b/advanced/wallets/react-wallet-v2/public/chain-logos/chain-placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..12975dfa5cf7d64f41792fec38bb09ab4d793588 GIT binary patch literal 483 zcmV<90UZ8`P)Px$ok>JNR5(wim1~lOAPj`RP~K~plc6UQ?`6Rf9wSbz=+@@fFh!cA(`kigF#`_7 zW8y|Y&%=4^+yW8siC$M2ARDOw81Y*Pk z?-7kpDpq>eM1ut(5|;`5uCxr|3fN{o&v8Y8oq+F$egjqjkKtQZzX7W;8VpBTogRDyO40m5{4dm$jX+ z?8X{`3ZQqjHiJghoO)J!Rk@lOg6k$ - - - \ No newline at end of file diff --git a/advanced/wallets/react-wallet-v2/public/token-logos/token-placeholder.png b/advanced/wallets/react-wallet-v2/public/token-logos/token-placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..466fa52f35b96f4c166412d0613f94ab8c887759 GIT binary patch literal 518 zcmV+h0{Q)kP)Px$z)3_wR5(wKlv#SjAPj?LzTB(yWRR03y;q7G*@nq7OY<@d_>U}v1s|RX$Xnkt z0obsT%oMOKG@$9i#UngLfjfC;HKX(b8f);_{m zOMn5x2)<->;521y+Y(D>Uj+*kIof7Xz@*SmgE`TZ_u{(yL+`GwE;}d#vI2U8C16(I zYqw$S>A)y7Z#-vI#*OjjLrlWA@p8hr*R(p&iX}r$JRkoJ?FZm(ihBlq9DE8JpFc*xn*x{m*lokP;o; zvl%CTJkSvly9FVe`~22OF=y_lAA(Gh@-lSwkIbc1(OQX!{OsevJxiEz4f?>L?gvsN z>9`En;jqv{55Slzw~6vdg6}=vxM$xnt|=*)cydx=>EsgKZ^drRX - - \ No newline at end of file diff --git a/advanced/wallets/react-wallet-v2/src/data/EIP155Data.ts b/advanced/wallets/react-wallet-v2/src/data/EIP155Data.ts index e0df05d71..11b4c701d 100644 --- a/advanced/wallets/react-wallet-v2/src/data/EIP155Data.ts +++ b/advanced/wallets/react-wallet-v2/src/data/EIP155Data.ts @@ -17,7 +17,7 @@ export type EIP155Chain = { namespace: string smartAccountEnabled?: boolean } -const blockchainApiRpc = (chainId: number) => { +export const blockchainApiRpc = (chainId: number) => { return `https://rpc.walletconnect.org/v1?chainId=eip155:${chainId}&projectId=${process.env.NEXT_PUBLIC_PROJECT_ID}` } /** diff --git a/advanced/wallets/react-wallet-v2/src/hooks/useWalletConnectEventsManager.ts b/advanced/wallets/react-wallet-v2/src/hooks/useWalletConnectEventsManager.ts index d28be34cb..fcf2e9629 100644 --- a/advanced/wallets/react-wallet-v2/src/hooks/useWalletConnectEventsManager.ts +++ b/advanced/wallets/react-wallet-v2/src/hooks/useWalletConnectEventsManager.ts @@ -23,6 +23,8 @@ import { EIP7715_METHOD } from '@/data/EIP7715Data' import { refreshSessionsList } from '@/pages/wc' import WalletCheckoutUtil from '@/utils/WalletCheckoutUtil' import WalletCheckoutCtrl from '@/store/WalletCheckoutCtrl' +import { CheckoutErrorCode } from '@/types/wallet_checkout' +import { createCheckoutError } from '@/types/wallet_checkout' export default function useWalletConnectEventsManager(initialized: boolean) { /****************************************************************************** @@ -99,6 +101,14 @@ export default function useWalletConnectEventsManager(initialized: boolean) { try { await WalletCheckoutCtrl.actions.prepareFeasiblePayments(request.params[0]) } catch (error) { + // If it's not a CheckoutError, create one + if (!(error && typeof error === 'object' && 'code' in error)) { + error = createCheckoutError( + CheckoutErrorCode.INVALID_CHECKOUT_REQUEST, + `Unexpected error: ${error instanceof Error ? error.message : String(error)}` + ) + } + return await walletkit.respondSessionRequest({ topic, response: WalletCheckoutUtil.formatCheckoutErrorResponse(id, error) diff --git a/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts b/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts index 5eef14c95..771950de3 100644 --- a/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts +++ b/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts @@ -1,14 +1,17 @@ import { getChainData } from '@/data/chainsUtil' -import { type PaymentOption, type DetailedPaymentOption, Hex } from '@/types/wallet_checkout' +import { type PaymentOption, type DetailedPaymentOption, Hex, SolanaContractInteraction } from '@/types/wallet_checkout' import { createPublicClient, erc20Abi, http, getContract, encodeFunctionData } from 'viem' import TransactionSimulatorUtil from './TransactionSimulatorUtil' import SettingsStore from '@/store/SettingsStore' import { getSolanaTokenData, getTokenData } from '@/data/tokenUtil' import { getChainById } from './ChainUtil' -import { EIP155_CHAINS } from '@/data/EIP155Data' -import { Connection, PublicKey } from '@solana/web3.js' +import { blockchainApiRpc } from '@/data/EIP155Data' +import { Connection, PublicKey, SystemProgram, Transaction, TransactionInstruction } from '@solana/web3.js' import { SOLANA_TEST_CHAINS } from '@/data/SolanaData' import { SOLANA_MAINNET_CHAINS } from '@/data/SolanaData' +import { createTransferInstruction, TOKEN_PROGRAM_ID } from '@solana/spl-token' +import { createAssociatedTokenAccountInstruction } from '@solana/spl-token' +import { getAssociatedTokenAddress } from '@solana/spl-token' /** * Interface for token details */ @@ -24,8 +27,8 @@ interface TokenDetails { */ export class PaymentValidationUtils { // Constants for fallback asset paths - private static readonly PLACEHOLDER_TOKEN_ICON = '/token-logos/token-placeholder.svg' - private static readonly PLACEHOLDER_CHAIN_ICON = '/chain-logos/chain-placeholder.svg' + private static readonly PLACEHOLDER_TOKEN_ICON = '/token-logos/token-placeholder.png' + private static readonly PLACEHOLDER_CHAIN_ICON = '/chain-logos/chain-placeholder.png' /** * Parses and validates a CAIP-19 asset ID @@ -82,21 +85,16 @@ export class PaymentValidationUtils { // Support ERC20 tokens, native tokens, and solana token return ['erc20', 'slip44', 'token'].includes(assetNamespace) } + + // methods to get token details - /** - * Gets details for a native blockchain asset (like ETH) - * - * @param chainId - Chain ID number - * @param account - Account address - * @returns Native asset details including balance and metadata - */ private static async getNativeAssetDetails( chainId: number, account: `0x${string}` ): Promise { const publicClient = createPublicClient({ chain: getChainById(chainId), - transport: http(EIP155_CHAINS[`eip155:${chainId}`].rpc) + transport: http(blockchainApiRpc(Number(chainId))) }) const balance = await publicClient.getBalance({ @@ -111,14 +109,6 @@ export class PaymentValidationUtils { } } - /** - * Gets details for an ERC20 token - * - * @param tokenAddress - Token contract address - * @param chainId - Chain ID number - * @param account - Account address - * @returns Token details including balance and metadata - */ private static async getErc20TokenDetails( tokenAddress: Hex, chainId: number, @@ -126,7 +116,7 @@ export class PaymentValidationUtils { ): Promise { const publicClient = createPublicClient({ chain: getChainById(chainId), - transport: http(EIP155_CHAINS[`eip155:${chainId}`].rpc) + transport: http(blockchainApiRpc(Number(chainId))) }) const contract = getContract({ @@ -150,15 +140,111 @@ export class PaymentValidationUtils { } } - /** - * Validates a contract interaction to ensure it can be executed successfully - * - * @param contractInteraction - The contract interaction data - * @param chainId - Chain ID - * @param account - User account address - * @returns Whether the contract interaction can succeed - */ - private static async simulateContractInteraction( + private static async getSolNativeAssetDetails( + account: string, + chainId: string + ): Promise { + const defaultTokenDetails: TokenDetails = { + balance: BigInt(0), + decimals: 9, + symbol: 'SOL', + name: 'Solana' + } + + try { + // Get the RPC URL for the chain + const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc + + if (!rpc) { + return defaultTokenDetails + } + + // Connect to Solana + const connection = new Connection(rpc, 'confirmed') + const publicKey = new PublicKey(account) + + const balance = await connection.getBalance(publicKey) + return { + ...defaultTokenDetails, + balance: BigInt(balance), + } + } catch (error) { + console.error('Error getting SOL balance:', error) + return defaultTokenDetails + } + } + + private static async getSplTokenDetails( + tokenAddress: string, + account: string, + chainId: string, + caip19AssetAddress: string + ): Promise { + const defaultTokenDetails: TokenDetails = { + balance: BigInt(0), + decimals: 6, + symbol: 'UNK', + name: 'Unknown Token' + } + + try { + const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc + + if (!rpc) { + return defaultTokenDetails + } + + // Connect to Solana + const connection = new Connection(rpc, 'confirmed') + const publicKey = new PublicKey(account) + const mintAddress = new PublicKey(tokenAddress) + + const token = getSolanaTokenData(caip19AssetAddress) + + // Get token balance + let balance = BigInt(0) + let decimals = token?.decimals || 0 // Use known token decimals or default + + // Find the associated token account(s) + const tokenAccounts = await connection.getParsedTokenAccountsByOwner(publicKey, { + mint: mintAddress + }) + + // If token account exists, get balance + if (tokenAccounts.value.length > 0) { + const tokenAccountPubkey = tokenAccounts.value[0].pubkey + const tokenBalance = await connection.getTokenAccountBalance(tokenAccountPubkey) + balance = BigInt(tokenBalance.value.amount) + + // Update decimals from on-chain data if not a known token + if (!token) { + decimals = tokenBalance.value.decimals + } + } else if (!token) { + // If no token accounts and not a known token, try to get decimals from mint + const mintInfo = await connection.getParsedAccountInfo(mintAddress) + if (mintInfo.value) { + const parsedMintInfo = (mintInfo.value.data as any).parsed?.info + decimals = parsedMintInfo?.decimals || decimals + } + } + + // Return with known metadata or fallback to generic + return { + balance, + decimals, + symbol: token?.symbol || tokenAddress.slice(0, 4).toUpperCase(), + name: token?.name || `SPL Token (${tokenAddress.slice(0, 8)}...)` + } + + } catch (error) { + // Return default values in case of error + return defaultTokenDetails + } + } + + // methods to simulate payments + private static async simulateEvmContractInteraction( contractInteraction: any, chainId: string, account: string @@ -167,123 +253,139 @@ export class PaymentValidationUtils { return false } - try { - if (Array.isArray(contractInteraction.data)) { - const canTransactionSucceed = await TransactionSimulatorUtil.canTransactionSucceed( - chainId, - account as `0x${string}`, - contractInteraction.data as { to: string; value: string; data: string }[] - ) - return canTransactionSucceed - } else { - // If data is not an array, it's an invalid format + if (Array.isArray(contractInteraction.data)) { + const simulateEvmTransaction = await TransactionSimulatorUtil.simulateEvmTransaction( + chainId, + account as `0x${string}`, + contractInteraction.data as { to: string; value: string; data: string }[] + ) + return simulateEvmTransaction + } + // If data is not an array, it's an invalid format + return false + } + private static async simulateSolanaContractInteraction(params:{contractInteraction: SolanaContractInteraction, account: string, chainId: string}){ + try{ + const {contractInteraction, account, chainId} = params + const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc + if(!rpc){ return false } - } catch (error) { - console.error('Error validating contract interaction:', error) + + const connection = new Connection(rpc, 'confirmed') + const publicKey = new PublicKey(account) + // Create a new transaction + const transaction = new Transaction() + + const instruction = contractInteraction.data + const accountMetas = instruction.accounts.map(acc => ({ + pubkey: new PublicKey(acc.pubkey), + isSigner: acc.isSigner, + isWritable: acc.isWritable + })) + + // Create the instruction + const txInstruction = new TransactionInstruction({ + programId: new PublicKey(instruction.programId), + keys: accountMetas, + data: Buffer.from(instruction.data, 'base64') + }) + + // Add to transaction + transaction.add(txInstruction) + + const simulationResult = await TransactionSimulatorUtil.simulateSolanaTransaction({connection, transaction, feePayer:publicKey}) + return simulationResult + }catch(e){ return false } } + private static async simulateSolanaNativeTransfer(params:{account:string, recipientAddress:string, amount:string, chainId:string}){ + try{ + const {account, recipientAddress, amount, chainId} = params + const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc + if(!rpc){ + return false + } - /** - * Validates an ERC20 token payment and retrieves token details - * - * @param assetAddress - Token contract address - * @param chainId - Chain ID - * @param senderAccount - Sender's account address - * @param recipientAddress - Recipient's address - * @param amount - Payment amount in hex - * @returns Object containing token details and validation status - */ - private static async simulateAndGetErc20PaymentDetails( - assetAddress: string, - chainId: string, - senderAccount: string, - recipientAddress: string, - amount: string - ): Promise<{ tokenDetails: TokenDetails; isValid: boolean }> { - // Get token details - const tokenDetails = await PaymentValidationUtils.getErc20TokenDetails( - assetAddress as `0x${string}`, - Number(chainId), - senderAccount as `0x${string}` - ) - - // Check if transaction can succeed - const canTransactionSucceed = await TransactionSimulatorUtil.canTransactionSucceed( - chainId, - senderAccount as `0x${string}`, - [ - { - to: assetAddress as `0x${string}`, - value: '0x0', - data: encodeFunctionData({ - abi: erc20Abi, - functionName: 'transfer', - args: [recipientAddress as `0x${string}`, BigInt(amount)] - }) - } - ] - ) + const connection = new Connection(rpc, 'confirmed') + const publicKey = new PublicKey(account) - return { - tokenDetails, - isValid: canTransactionSucceed + const transaction = new Transaction() + transaction.add( + SystemProgram.transfer({ + fromPubkey: publicKey, + toPubkey: new PublicKey(recipientAddress), + lamports: BigInt(amount) + }) + ) + const simulationResult = await TransactionSimulatorUtil.simulateSolanaTransaction({connection, transaction, feePayer:publicKey}) + return simulationResult + }catch(e){ + return false } } + private static async simulateSolanaTokenTransfer(params:{account:string, recipientAddress:string, amount:bigint, tokenAddress:string, chainId:string}){ + try{ + const {account, recipientAddress, amount, tokenAddress, chainId} = params + const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc + if(!rpc){ + return false + } - /** - * Validates a native token payment and retrieves token details - * - * @param chainId - Chain ID - * @param senderAccount - Sender's account address - * @param recipientAddress - Recipient's address - * @param amount - Payment amount in hex - * @returns Object containing token details and validation status - */ - private static async simulateAndGetNativeTokenPaymentDetails( - chainId: string, - senderAccount: string, - recipientAddress: string, - amount: string - ): Promise<{ tokenDetails: TokenDetails; isValid: boolean }> { - // Check if transaction can succeed - const canTransactionSucceed = await TransactionSimulatorUtil.canTransactionSucceed( - chainId, - senderAccount as `0x${string}`, - [ - { - to: recipientAddress as `0x${string}`, - value: amount, - data: '0x' - } - ] - ) + const connection = new Connection(rpc, 'confirmed'); + const fromPubkey = new PublicKey(account); + const mintAddress = new PublicKey(tokenAddress); + const toPubkey = new PublicKey(recipientAddress); + + const fromTokenAccountAddress = await getAssociatedTokenAddress( + mintAddress, + fromPubkey + ); + + const fromTokenAccount = await connection.getAccountInfo(fromTokenAccountAddress); + if (!fromTokenAccount) { + return false; + } - // Get native token details - const tokenDetails = canTransactionSucceed - ? await PaymentValidationUtils.getNativeAssetDetails( - Number(chainId), - senderAccount as `0x${string}` - ) - : { decimals: 18, symbol: 'ETH', name: 'Ethereum', balance: BigInt(0) } + const toTokenAccountAddress = await getAssociatedTokenAddress( + mintAddress, + toPubkey + ); + const recipientTokenAccount = await connection.getAccountInfo(toTokenAccountAddress); + + // Create transaction + const transaction = new Transaction(); + + // Add instruction to create recipient token account if needed + if (!recipientTokenAccount) { + const createAccountInstruction = createAssociatedTokenAccountInstruction( + fromPubkey, + toTokenAccountAddress, + toPubkey, + mintAddress + ); + transaction.add(createAccountInstruction); + } - return { - tokenDetails, - isValid: canTransactionSucceed + // Add transfer instruction + const transferInstruction = createTransferInstruction( + fromTokenAccountAddress, + toTokenAccountAddress, + fromPubkey, + amount, + [], + TOKEN_PROGRAM_ID + ); + transaction.add(transferInstruction); + + const simulationResult = await TransactionSimulatorUtil.simulateSolanaTransaction({connection, transaction, feePayer:fromPubkey}) + return simulationResult + }catch(e){ + return false } } - /** - * Creates a detailed payment option with all necessary metadata - * - * @param payment - Original payment option - * @param tokenDetails - Token details - * @param assetNamespace - Asset namespace - * @param chainId - Chain ID - * @param chainNamespace - Chain namespace - * @returns Detailed payment option - */ private static createDetailedPaymentOption( payment: PaymentOption, tokenDetails: TokenDetails, @@ -312,16 +414,8 @@ export class PaymentValidationUtils { } } - /** - * Validates a single direct payment option and creates a detailed version if valid - * - * @param payment - Payment option to validate - * @param account - User account address - * @returns Object containing the validated payment (or null) and asset availability flag - */ - private static async getDetailedDirectPaymentOptionEVM( + private static async getDetailedDirectPaymentOption( payment: PaymentOption, - account: string ): Promise<{ validatedPayment: DetailedPaymentOption | null hasMatchingAsset: boolean @@ -332,548 +426,404 @@ export class PaymentValidationUtils { if (!recipientAddress) { return { validatedPayment: null, hasMatchingAsset: false } } - + // Parse asset details const { chainId, assetAddress, chainNamespace, assetNamespace } = PaymentValidationUtils.getAssetDetails(payment.asset) - + // Check if asset namespace is supported if (!PaymentValidationUtils.isSupportedAssetNamespace(assetNamespace)) { return { validatedPayment: null, hasMatchingAsset: false } } - - let validationResult - - // Validate based on asset type - if (assetNamespace === 'erc20') { - validationResult = await PaymentValidationUtils.simulateAndGetErc20PaymentDetails( - assetAddress, - chainId, - account, - recipientAddress, - payment.amount - ) - } else { - // slip44 - native token - validationResult = await PaymentValidationUtils.simulateAndGetNativeTokenPaymentDetails( - chainId, - account, - recipientAddress, - payment.amount - ) - } - - // Check if user has the asset (balance > 0) - const hasMatchingAsset = validationResult.tokenDetails.balance > BigInt(0) - - if (!validationResult.isValid) { - return { validatedPayment: null, hasMatchingAsset } + + let result; + + switch(chainNamespace) { + case 'solana': + result = await this.processSolanaDirectPayment( + payment, + recipientAddress, + chainId, + assetAddress, + assetNamespace + ); + break; + + case 'eip155': + result = await this.processEvmDirectPayment( + payment, + recipientAddress, + chainId, + assetAddress, + assetNamespace + ); + break; + + default: + return { validatedPayment: null, hasMatchingAsset: false } } - - // Create detailed payment option with metadata - const detailedPayment = PaymentValidationUtils.createDetailedPaymentOption( - payment, - validationResult.tokenDetails, - assetNamespace, - chainId, - chainNamespace - ) - - return { validatedPayment: detailedPayment, hasMatchingAsset: true } + + return result; } catch (error) { console.error('Error validating payment option:', error) return { validatedPayment: null, hasMatchingAsset: false } } } - - /** - * Validates a single direct payment option for Solana and creates a detailed version if valid - * - * @param payment - Payment option to validate - * @param account - Solana account address - * @returns Object containing the validated payment (or null) and asset availability flag - */ - private static async getDetailedDirectPaymentOptionSolana( + + private static async processSolanaDirectPayment( payment: PaymentOption, - account: string + recipientAddress: string, + chainId: string, + assetAddress: string, + assetNamespace: string ): Promise<{ validatedPayment: DetailedPaymentOption | null hasMatchingAsset: boolean }> { - try { - // Extract recipient address - const recipientAddress = PaymentValidationUtils.extractAddressFromCAIP10(payment.recipient) - if (!recipientAddress) { - return { validatedPayment: null, hasMatchingAsset: false } - } - - // Parse asset details - const { chainId, assetAddress, chainNamespace, assetNamespace } = - PaymentValidationUtils.getAssetDetails(payment.asset) - - // Check if asset namespace is supported - if (!PaymentValidationUtils.isSupportedAssetNamespace(assetNamespace)) { - return { validatedPayment: null, hasMatchingAsset: false } - } - - // Get token details based on asset namespace - let tokenDetails: TokenDetails - - if (assetNamespace === 'slip44' && assetAddress === '501') { - // Native SOL - tokenDetails = await this.getSolNativeAssetDetails(account, `${chainNamespace}:${chainId}`) - } else if (assetNamespace === 'token') { - // SPL token - tokenDetails = await this.getSplTokenDetails( - assetAddress, - account, - `${chainNamespace}:${chainId}`, - payment.asset - ) - } else { - return { validatedPayment: null, hasMatchingAsset: false } - } - - // Check if user has the asset (balance > 0) - const hasMatchingAsset = tokenDetails.balance > BigInt(0) - - if (!hasMatchingAsset) { - return { validatedPayment: null, hasMatchingAsset } - } - - // Create detailed payment option with metadata - const detailedPayment = PaymentValidationUtils.createDetailedPaymentOption( - payment, - tokenDetails, - assetNamespace, - chainId, - chainNamespace - ) - - return { validatedPayment: detailedPayment, hasMatchingAsset: true } - } catch (error) { - console.error('Error validating Solana payment option:', error) - return { validatedPayment: null, hasMatchingAsset: false } + const account = SettingsStore.state.solanaAddress; + let tokenDetails: TokenDetails | undefined; + let simulationResult: boolean | undefined; + + if (assetNamespace === 'slip44' && assetAddress === '501') { + simulationResult = await this.simulateSolanaNativeTransfer({ + account, + recipientAddress: recipientAddress, + amount: payment.amount, + chainId: `solana:${chainId}` + }); + tokenDetails = simulationResult ? await this.getSolNativeAssetDetails(account, `solana:${chainId}`) : undefined; + } else if (assetNamespace === 'token') { + simulationResult = await this.simulateSolanaTokenTransfer({ + account, + recipientAddress: recipientAddress, + amount: BigInt(payment.amount), + tokenAddress: assetAddress, + chainId: `solana:${chainId}` + }); + tokenDetails = simulationResult ? await this.getSplTokenDetails( + assetAddress, + account, + `solana:${chainId}`, + payment.asset + ) : undefined; + } else { + return { validatedPayment: null, hasMatchingAsset: false }; } - } - - /** - * Validates a single direct payment option and creates a detailed version if valid - * - * @param payment - Payment option to validate - * @param evmAccount - EVM account address (if available) - * @param solanaAccount - Solana account address (if available) - * @returns Object containing the validated payment (or null) and asset availability flag - */ - private static async getDetailedDirectPaymentOption( - payment: PaymentOption, - evmAccount?: string, - solanaAccount?: string - ): Promise<{ - validatedPayment: DetailedPaymentOption | null - hasMatchingAsset: boolean - }> { - try { - // Parse asset details to determine chain - const { chainNamespace } = PaymentValidationUtils.getAssetDetails(payment.asset) - - // Delegate to chain-specific methods - if (chainNamespace === 'eip155' && evmAccount) { - return await this.getDetailedDirectPaymentOptionEVM(payment, evmAccount) - } else if (chainNamespace === 'solana' && solanaAccount) { - return await this.getDetailedDirectPaymentOptionSolana(payment, solanaAccount) - } - - return { validatedPayment: null, hasMatchingAsset: false } - } catch (error) { - console.error('Error getting detailed payment option:', error) - return { validatedPayment: null, hasMatchingAsset: false } + + // Check if token details were assigned + if (!tokenDetails) { + return { validatedPayment: null, hasMatchingAsset: false }; } + + // Check if user has the asset (balance > 0) + const hasMatchingAsset = tokenDetails.balance > BigInt(0); + console.log({tokenDetails}); + if (!hasMatchingAsset) { + return { validatedPayment: null, hasMatchingAsset }; + } + + // Create detailed payment option with metadata + const detailedPayment = simulationResult ? PaymentValidationUtils.createDetailedPaymentOption( + payment, + tokenDetails, + assetNamespace, + chainId, + 'solana' + ) : null; + + return { validatedPayment: detailedPayment, hasMatchingAsset: true }; } - - /** - * Validates a contract payment option for Solana and creates a detailed version if valid - * - * @param payment - Payment option to validate - * @param account - Solana account address - * @returns Object containing the validated payment (or null) and asset availability flag - */ - private static async getDetailedContractPaymentOptionSolana( + + private static async processEvmDirectPayment( payment: PaymentOption, - account: string + recipientAddress: string, + chainId: string, + assetAddress: string, + assetNamespace: string ): Promise<{ validatedPayment: DetailedPaymentOption | null hasMatchingAsset: boolean }> { - try { - const { asset, contractInteraction } = payment - - if (!contractInteraction || contractInteraction.type !== 'solana-instruction') { - return { validatedPayment: null, hasMatchingAsset: false } - } - - // Parse asset details - const { chainId, assetAddress, chainNamespace, assetNamespace } = - PaymentValidationUtils.getAssetDetails(asset) - - // Check if asset namespace is supported - if (!PaymentValidationUtils.isSupportedAssetNamespace(assetNamespace)) { - return { validatedPayment: null, hasMatchingAsset: false } - } - - // Get token details based on asset namespace - let tokenDetails: TokenDetails - - if (assetNamespace === 'slip44' && assetAddress === '501') { - // Native SOL - tokenDetails = await this.getSolNativeAssetDetails(account, `${chainNamespace}:${chainId}`) - } else if (assetNamespace === 'token') { - // SPL token - tokenDetails = await this.getSplTokenDetails( - assetAddress, - account, - `${chainNamespace}:${chainId}`, - payment.asset - ) - } else { - return { validatedPayment: null, hasMatchingAsset: false } - } - - // Check if user has the asset (balance > 0) - const hasMatchingAsset = tokenDetails.balance > BigInt(0) - - // For demonstration, we'll assume all Solana contract interactions are feasible - // In a production app, you would validate the instructions using simulation - - // Create detailed payment option with metadata - const detailedPayment = PaymentValidationUtils.createDetailedPaymentOption( - payment, - tokenDetails, - assetNamespace, + const account = SettingsStore.state.eip155Address as `0x${string}`; + let tokenDetails: TokenDetails | undefined; + let simulationResult: boolean | undefined; + + if (assetNamespace === 'erc20') { + simulationResult = await PaymentValidationUtils.simulateEvmContractInteraction( + { + data: [ + { + to: assetAddress as `0x${string}`, + value: '0x0', + data: encodeFunctionData({ + abi: erc20Abi, + functionName: 'transfer', + args: [recipientAddress as `0x${string}`, BigInt(payment.amount)] + }) + } + ] + }, chainId, - chainNamespace - ) - - return { validatedPayment: detailedPayment, hasMatchingAsset } - } catch (error) { - console.error('Error validating Solana contract payment option:', error) - return { validatedPayment: null, hasMatchingAsset: false } + account + ); + tokenDetails = await PaymentValidationUtils.getErc20TokenDetails( + assetAddress as `0x${string}`, + Number(chainId), + account as `0x${string}` + ); + } else if (assetNamespace === 'slip44' && assetAddress === '60') { + // slip44:60 - native ETH token + simulationResult = await TransactionSimulatorUtil.simulateEvmTransaction( + chainId, + account as `0x${string}`, + [ + { + to: recipientAddress as `0x${string}`, + value: payment.amount, + data: '0x' + } + ] + ); + tokenDetails = await PaymentValidationUtils.getNativeAssetDetails( + Number(chainId), + account as `0x${string}` + ); + } else { + return { validatedPayment: null, hasMatchingAsset: false }; } + + // Check if token details were assigned + if (!tokenDetails || simulationResult === undefined) { + return { validatedPayment: null, hasMatchingAsset: false }; + } + + // Check if user has the asset (balance > 0) + const hasMatchingAsset = tokenDetails.balance > BigInt(0); + console.log({tokenDetails}); + if (!hasMatchingAsset) { + return { validatedPayment: null, hasMatchingAsset }; + } + + // Create detailed payment option with metadata + const detailedPayment = simulationResult ? PaymentValidationUtils.createDetailedPaymentOption( + payment, + tokenDetails, + assetNamespace, + chainId, + 'eip155' + ) : null; + + return { validatedPayment: detailedPayment, hasMatchingAsset: true }; } - - /** - * Validates a contract payment option and creates a detailed version if valid - * - * @param payment - Payment option to validate - * @param account - User account address - * @returns Object containing the validated payment (or null) and asset availability flag - */ - private static async getDetailedContractPaymentOptionEVM( + + private static async getDetailedContractPaymentOption( payment: PaymentOption, - account: string ): Promise<{ validatedPayment: DetailedPaymentOption | null hasMatchingAsset: boolean }> { try { const { asset, contractInteraction } = payment - + if (!contractInteraction) { return { validatedPayment: null, hasMatchingAsset: false } } - + // Parse asset details const { chainId, assetAddress, chainNamespace, assetNamespace } = PaymentValidationUtils.getAssetDetails(asset) - - // Validate contract interaction - const isContractValid = await PaymentValidationUtils.simulateContractInteraction( - contractInteraction, - chainId, - account - ) - - if (!isContractValid) { - return { validatedPayment: null, hasMatchingAsset: false } - } - + // Check if asset namespace is supported if (!PaymentValidationUtils.isSupportedAssetNamespace(assetNamespace)) { return { validatedPayment: null, hasMatchingAsset: false } } - - // Get asset details based on asset namespace - let tokenDetails: TokenDetails - if (assetNamespace === 'erc20') { - tokenDetails = await PaymentValidationUtils.getErc20TokenDetails( - assetAddress as `0x${string}`, - Number(chainId), - account as `0x${string}` - ) - } else { - // must be slip44 since we already checked supported namespaces - tokenDetails = await PaymentValidationUtils.getNativeAssetDetails( - Number(chainId), - account as `0x${string}` - ) - } - - // Check if user has the asset (balance > 0) - const hasMatchingAsset = tokenDetails.balance > BigInt(0) - - // Create detailed payment option with metadata (reusing the common method) - const detailedPayment = PaymentValidationUtils.createDetailedPaymentOption( - payment, - tokenDetails, - assetNamespace, - chainId, - chainNamespace - ) - - return { - validatedPayment: detailedPayment, - hasMatchingAsset + + let result; + + switch (chainNamespace) { + case 'solana': + result = await this.processSolanaContractPayment( + payment, + chainId, + assetAddress, + assetNamespace, + contractInteraction + ); + break; + + case 'eip155': + result = await this.processEvmContractPayment( + payment, + chainId, + assetAddress, + assetNamespace, + contractInteraction + ); + break; + + default: + return { validatedPayment: null, hasMatchingAsset: false } } + + return result; } catch (error) { console.error('Error validating contract payment option:', error) return { validatedPayment: null, hasMatchingAsset: false } } } - - /** - * Validates a contract payment option and creates a detailed version if valid - * - * @param payment - Payment option to validate - * @param evmAccount - EVM account address (if available) - * @param solanaAccount - Solana account address (if available) - * @returns Object containing the validated payment (or null) and asset availability flag - */ - private static async getDetailedContractPaymentOption( + + private static async processSolanaContractPayment( payment: PaymentOption, - evmAccount?: string, - solanaAccount?: string + chainId: string, + assetAddress: string, + assetNamespace: string, + contractInteraction: any ): Promise<{ validatedPayment: DetailedPaymentOption | null hasMatchingAsset: boolean }> { - try { - // Parse asset details to determine chain - const { chainNamespace } = PaymentValidationUtils.getAssetDetails(payment.asset) - - // Delegate to chain-specific methods - if (chainNamespace === 'eip155' && evmAccount) { - return await this.getDetailedContractPaymentOptionEVM(payment, evmAccount) - } else if (chainNamespace === 'solana' && solanaAccount) { - return await this.getDetailedContractPaymentOptionSolana(payment, solanaAccount) - } - + const account = SettingsStore.state.solanaAddress; + let tokenDetails: TokenDetails | undefined; + let isValid = false; + + if (contractInteraction.type !== 'solana-instruction') { return { validatedPayment: null, hasMatchingAsset: false } - } catch (error) { - console.error('Error getting detailed contract payment option:', error) + } + + isValid = await this.simulateSolanaContractInteraction({ + contractInteraction: contractInteraction as SolanaContractInteraction, + account, + chainId: `solana:${chainId}` + }); + if (!isValid) { return { validatedPayment: null, hasMatchingAsset: false } } - } - - /** - * Gets details for a Solana native asset (SOL) - * - * @param account - Solana account address - * @returns SOL asset details including metadata - */ - private static async getSolNativeAssetDetails( - account: string, - chainId: string - ): Promise { - try { - // Get the RPC URL for the chain - const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc - - if (!rpc) { - throw new Error('There is no RPC URL for the provided chain') - } - - // Connect to Solana - const connection = new Connection(rpc, 'confirmed') - const publicKey = new PublicKey(account) - - // Get SOL balance - const balance = await connection.getBalance(publicKey) - console.log('Solana balance', balance) - return { - balance: BigInt(balance), - decimals: 9, - symbol: 'SOL', - name: 'Solana' - } - } catch (error) { - console.error('Error getting SOL balance:', error) - - // Return default values in case of error - return { - balance: BigInt(0), - decimals: 9, - symbol: 'SOL', - name: 'Solana' - } + + if (assetNamespace === 'slip44' && assetAddress === '501') { + // Native SOL + tokenDetails = await this.getSolNativeAssetDetails(account, `solana:${chainId}`); + } else if (assetNamespace === 'token') { + // SPL token + tokenDetails = await this.getSplTokenDetails( + assetAddress, + account, + `solana:${chainId}`, + payment.asset + ); + } else { + return { validatedPayment: null, hasMatchingAsset: false } } - } - - /** - * Gets details for a fungible SPL token with known token mapping - * - * @param tokenAddress - Token mint address - * @param account - Account address - * @returns Token details including balance and metadata - */ - private static async getSplTokenDetails( - tokenAddress: string, - account: string, - chainId: string, - caip19AssetAddress: string - ): Promise { - try { - // Get the RPC URL for the chain - const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc - - if (!rpc) { - throw new Error('There is no RPC URL for the provided chain') - } - - // Connect to Solana - const connection = new Connection(rpc, 'confirmed') - const publicKey = new PublicKey(account) - const mintAddress = new PublicKey(tokenAddress) - - try { - // Check if this is a known token - const token = getSolanaTokenData(caip19AssetAddress) - - // Get token balance - let balance = BigInt(0) - let decimals = token?.decimals || 6 // Use known token decimals or default - - try { - // Find the associated token account(s) - const tokenAccounts = await connection.getParsedTokenAccountsByOwner(publicKey, { - mint: mintAddress - }) - - // If token account exists, get balance - if (tokenAccounts.value.length > 0) { - const tokenAccountPubkey = tokenAccounts.value[0].pubkey - const tokenBalance = await connection.getTokenAccountBalance(tokenAccountPubkey) - balance = BigInt(tokenBalance.value.amount) - - // Update decimals from on-chain data if not a known token - if (!token) { - decimals = tokenBalance.value.decimals - } - } else if (!token) { - // If no token accounts and not a known token, try to get decimals from mint - const mintInfo = await connection.getParsedAccountInfo(mintAddress) - if (mintInfo.value) { - const parsedMintInfo = (mintInfo.value.data as any).parsed?.info - decimals = parsedMintInfo?.decimals || decimals - } - } - } catch (balanceError) { - console.warn('Error getting token balance:', balanceError) - // Continue with zero balance - } - - // Return with known metadata or fallback to generic - return { - balance, - decimals, - symbol: token?.symbol || tokenAddress.slice(0, 4).toUpperCase(), - name: token?.name || `SPL Token (${tokenAddress.slice(0, 8)}...)` - } - } catch (tokenError) { - console.warn('Error getting token details:', tokenError) - - // Return default values - return { - balance: BigInt(0), - decimals: 6, - symbol: tokenAddress.slice(0, 4).toUpperCase(), - name: 'SPL Token' - } - } - } catch (error) { - console.error('Error getting SPL token details:', error) - - // Return default values in case of error - return { - balance: BigInt(0), - decimals: 6, - symbol: tokenAddress.slice(0, 4).toUpperCase(), - name: 'SPL Token' - } + + // Check if token details were assigned + if (!tokenDetails) { + return { validatedPayment: null, hasMatchingAsset: false } } + + // Check if user has the asset (balance > 0) + const hasMatchingAsset = tokenDetails.balance > BigInt(0); + + // Create detailed payment option with metadata + const detailedPayment = isValid + ? PaymentValidationUtils.createDetailedPaymentOption( + payment, + tokenDetails, + assetNamespace, + chainId, + 'solana' + ) + : null; + return { validatedPayment: detailedPayment, hasMatchingAsset }; } - - /** - * Finds and validates all feasible direct payment options - * - * @param directPayments - Array of direct payment options - * @returns Object containing feasible payments and asset availability flag - */ - static async findFeasibleDirectPayments(directPayments: PaymentOption[]): Promise<{ - feasibleDirectPayments: DetailedPaymentOption[] - isUserHaveAtleastOneMatchingAssets: boolean + + private static async processEvmContractPayment( + payment: PaymentOption, + chainId: string, + assetAddress: string, + assetNamespace: string, + contractInteraction: any + ): Promise<{ + validatedPayment: DetailedPaymentOption | null + hasMatchingAsset: boolean }> { - let isUserHaveAtleastOneMatchingAssets = false - const evmAccount = SettingsStore.state.eip155Address as `0x${string}` - const solanaAccount = SettingsStore.state.solanaAddress - - // Validate each payment option - const results = await Promise.all( - directPayments.map(payment => - PaymentValidationUtils.getDetailedDirectPaymentOption(payment, evmAccount, solanaAccount) - ) - ) - - // Collect results - const feasibleDirectPayments: DetailedPaymentOption[] = [] - - for (const result of results) { - if (result.hasMatchingAsset) { - isUserHaveAtleastOneMatchingAssets = true - } - - if (result.validatedPayment) { - feasibleDirectPayments.push(result.validatedPayment) - } + const account = SettingsStore.state.eip155Address as `0x${string}`; + let tokenDetails: TokenDetails | undefined; + let isValid = false; + + if (contractInteraction.type !== 'evm-calls') { + return { validatedPayment: null, hasMatchingAsset: false } } - - return { - feasibleDirectPayments, - isUserHaveAtleastOneMatchingAssets + + isValid = await PaymentValidationUtils.simulateEvmContractInteraction( + contractInteraction, + chainId, + account + ); + + if (!isValid) { + return { validatedPayment: null, hasMatchingAsset: false } + } + + if (assetNamespace === 'erc20') { + tokenDetails = await PaymentValidationUtils.getErc20TokenDetails( + assetAddress as `0x${string}`, + Number(chainId), + account as `0x${string}` + ); + } else if (assetNamespace === 'slip44') { + // must be slip44 since we already checked supported namespaces + tokenDetails = await PaymentValidationUtils.getNativeAssetDetails( + Number(chainId), + account as `0x${string}` + ); + } else { + return { validatedPayment: null, hasMatchingAsset: false } } + + // Check if token details were assigned + if (!tokenDetails) { + return { validatedPayment: null, hasMatchingAsset: false } + } + + // Check if user has the asset (balance > 0) + const hasMatchingAsset = tokenDetails.balance > BigInt(0); + + // Create detailed payment option with metadata + const detailedPayment = isValid + ? PaymentValidationUtils.createDetailedPaymentOption( + payment, + tokenDetails, + assetNamespace, + chainId, + 'eip155' + ) + : null; + return { validatedPayment: detailedPayment, hasMatchingAsset }; } - - /** - * Finds and validates all feasible contract payment options - * - * @param contractPayments - Array of contract payment options - * @returns Object containing feasible payments and asset availability flag - */ - static async findFeasibleContractPayments(contractPayments: PaymentOption[]): Promise<{ - feasibleContractPayments: DetailedPaymentOption[] + + static async findFeasiblePayments( + payments: PaymentOption[] + ): Promise<{ + feasiblePayments: DetailedPaymentOption[] isUserHaveAtleastOneMatchingAssets: boolean }> { let isUserHaveAtleastOneMatchingAssets = false - const evmAccount = SettingsStore.state.eip155Address as `0x${string}` - const solanaAccount = SettingsStore.state.solanaAddress - + const results = await Promise.all( - contractPayments.map(payment => - PaymentValidationUtils.getDetailedContractPaymentOption(payment, evmAccount, solanaAccount) - ) + payments.map(async payment => { + if (payment.recipient && !payment.contractInteraction) { + // Direct payment + return await this.getDetailedDirectPaymentOption(payment) + } else if (payment.contractInteraction) { + return await this.getDetailedContractPaymentOption(payment) + } else { + console.warn('Invalid payment: missing both recipient and contractInteraction') + return { validatedPayment: null, hasMatchingAsset: false } + } + }) ) // Collect results - const feasibleContractPayments: DetailedPaymentOption[] = [] + const feasiblePayments: DetailedPaymentOption[] = [] for (const result of results) { if (result.hasMatchingAsset) { @@ -881,12 +831,12 @@ export class PaymentValidationUtils { } if (result.validatedPayment) { - feasibleContractPayments.push(result.validatedPayment) + feasiblePayments.push(result.validatedPayment) } } return { - feasibleContractPayments, + feasiblePayments, isUserHaveAtleastOneMatchingAssets } } diff --git a/advanced/wallets/react-wallet-v2/src/utils/TransactionSimulatorUtil.ts b/advanced/wallets/react-wallet-v2/src/utils/TransactionSimulatorUtil.ts index d7a58cc3a..be145dd3d 100644 --- a/advanced/wallets/react-wallet-v2/src/utils/TransactionSimulatorUtil.ts +++ b/advanced/wallets/react-wallet-v2/src/utils/TransactionSimulatorUtil.ts @@ -1,3 +1,5 @@ +import { blockchainApiRpc } from '@/data/EIP155Data'; +import { Connection, PublicKey, Transaction } from '@solana/web3.js'; import { createPublicClient, http } from 'viem' const TransactionSimulatorUtil = { @@ -10,23 +12,24 @@ const TransactionSimulatorUtil = { * @param calls - Array of transaction details to simulate * @returns Boolean indicating if all transactions would be valid */ - canTransactionSucceed: async ( + simulateEvmTransaction: async ( chainId: string, fromWalletAddress: string, calls: { to: string; value: string; data?: string }[] ) => { - const projectId = process.env.NEXT_PUBLIC_PROJECT_ID || '' - - const client = createPublicClient({ - transport: http( - `https://rpc.walletconnect.org/v1?chainId=eip155:${chainId}&projectId=${projectId}` - ) - }) + if (!calls || calls.length === 0) { + console.warn('No transaction calls provided for simulation') + return false + } try { - // Process all calls in parallel + const client = createPublicClient({ + transport: http(blockchainApiRpc(Number(chainId))), + }) + + // Process all calls in parallel with individual error handling const results = await Promise.all( - calls.map(async call => { + calls.map(async (call, index) => { try { // Get current fee estimates const { maxFeePerGas, maxPriorityFeePerGas } = await client.estimateFeesPerGas() @@ -46,8 +49,8 @@ const TransactionSimulatorUtil = { return true } catch (error) { // Gas estimation failed - transaction would not succeed - console.error( - `Transaction simulation failed: ${ + console.warn( + `Transaction #${index + 1} simulation failed for address ${call.to}: ${ error instanceof Error ? error.message : 'Unknown error' }` ) @@ -59,15 +62,49 @@ const TransactionSimulatorUtil = { // Return true only if all transactions would succeed return results.every(success => success) } catch (error) { - // Handle any unexpected errors console.error( - `Error in transaction simulation: ${ + `Overall transaction simulation process failed: ${ error instanceof Error ? error.message : 'Unknown error' }` ) return false } - } + }, + + /** + * Simulates a Solana transaction + * + * @param connection - Solana connection + * @param transaction - Transaction to simulate + * @param feePayer - Fee payer's public key + * @returns Object with simulation success status and error details if applicable + * @throws CheckoutError if there's a critical simulation error that should block the transaction + */ + async simulateSolanaTransaction(param:{ + connection: Connection, + transaction: Transaction, + feePayer: PublicKey} + ): Promise { + try { + const {connection, transaction, feePayer} = param + // Set recent blockhash for the transaction + transaction.recentBlockhash = (await connection.getLatestBlockhash()).blockhash + transaction.feePayer = feePayer + + // Simulate the transaction + const simulation = await connection.simulateTransaction(transaction) + + // Check simulation results + if (simulation.value.err) { + console.warn('Solana simulation error:', simulation.value.err) + return false + } + + return true + } catch (error) { + return false + } + }, } export default TransactionSimulatorUtil diff --git a/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutUtil.ts b/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutUtil.ts index 5eea33db1..f32d83769 100644 --- a/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutUtil.ts +++ b/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutUtil.ts @@ -63,46 +63,10 @@ const WalletCheckoutUtil = { } }, - /** - * Separates the accepted payments into direct payments and contract payments - * following the CAIP standard requirements - * - * @param acceptedPayments - Array of payment options - * @returns An object containing arrays of direct payments and contract payments - */ - separatePayments(acceptedPayments: PaymentOption[]) { - const directPayments: PaymentOption[] = [] - const contractPayments: PaymentOption[] = [] - - acceptedPayments.forEach(payment => { - if (!payment) { - return - } - - const { recipient, contractInteraction } = payment - const hasRecipient = typeof recipient === 'string' && recipient.trim() !== '' - const hasContractInteraction = contractInteraction !== undefined - - // Direct payment: recipient is present and contractInteraction is absent - if (hasRecipient && !hasContractInteraction) { - directPayments.push(payment) - } - // Contract interaction: contractInteraction is present and recipient is absent - else if (hasContractInteraction && !hasRecipient) { - contractPayments.push(payment) - } - }) - - return { - directPayments, - contractPayments - } - }, /** * Prepares a checkout request by validating it and checking if the user has sufficient balances * for at least one payment option. * - * @param account - User's account address * @param checkoutRequest - The checkout request to prepare * @returns A promise that resolves to an object with feasible payments * @throws CheckoutError if validation or preparation fails @@ -110,51 +74,24 @@ const WalletCheckoutUtil = { async getFeasiblePayments(checkoutRequest: CheckoutRequest): Promise<{ feasiblePayments: DetailedPaymentOption[] }> { - // Validate the checkout request (will throw if invalid) this.validateCheckoutRequest(checkoutRequest) - try { - const { acceptedPayments } = checkoutRequest - - // Separate payments for processing - const { directPayments, contractPayments } = this.separatePayments(acceptedPayments) + const { acceptedPayments } = checkoutRequest - // find feasible direct payments - const { feasibleDirectPayments, isUserHaveAtleastOneMatchingAssets } = - await PaymentValidationUtils.findFeasibleDirectPayments(directPayments) - // find feasible contract payments - const { - feasibleContractPayments, - isUserHaveAtleastOneMatchingAssets: validContractPayments - } = await PaymentValidationUtils.findFeasibleContractPayments(contractPayments) - // This return error if user have no matching assets - if (!isUserHaveAtleastOneMatchingAssets && !validContractPayments) { - throw createCheckoutError(CheckoutErrorCode.NO_MATCHING_ASSETS) - } - - // Combine all feasible payments - const feasiblePayments: DetailedPaymentOption[] = [ - ...feasibleDirectPayments, - ...feasibleContractPayments - ] + // find feasible direct payments + const { feasiblePayments, isUserHaveAtleastOneMatchingAssets } = + await PaymentValidationUtils.findFeasiblePayments(acceptedPayments) - // This return error if user have atleast one matching assets but no feasible payments - if (feasiblePayments.length === 0) { - throw createCheckoutError(CheckoutErrorCode.INSUFFICIENT_FUNDS) - } - - return { feasiblePayments } - } catch (error) { - // If it's already a CheckoutError, rethrow it - if (error && typeof error === 'object' && 'code' in error) { - throw error - } + // This return error if user have no matching assets + if (!isUserHaveAtleastOneMatchingAssets && !feasiblePayments) { + throw createCheckoutError(CheckoutErrorCode.NO_MATCHING_ASSETS) + } - // Otherwise wrap it in a CheckoutError - throw createCheckoutError( - CheckoutErrorCode.INVALID_CHECKOUT_REQUEST, - `Unexpected error: ${error instanceof Error ? error.message : String(error)}` - ) + // This return error if user have atleast one matching assets but no feasible payments + if (feasiblePayments.length === 0) { + throw createCheckoutError(CheckoutErrorCode.INSUFFICIENT_FUNDS) } + + return { feasiblePayments } }, // Add these methods directly to the object From 787312f78a8222668f8e9bfb86e76d6945ee5067 Mon Sep 17 00:00:00 2001 From: Karandeep Singh Date: Tue, 1 Apr 2025 11:05:31 +0530 Subject: [PATCH 11/13] chore: refactor paymentHandler --- .../src/utils/WalletCheckoutPaymentHandler.ts | 282 ++---------------- 1 file changed, 29 insertions(+), 253 deletions(-) diff --git a/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutPaymentHandler.ts b/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutPaymentHandler.ts index 647381b4b..fe03abde8 100644 --- a/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutPaymentHandler.ts +++ b/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutPaymentHandler.ts @@ -1,7 +1,6 @@ import { encodeFunctionData } from 'viem' import { erc20Abi } from 'viem' import { Connection, PublicKey, Transaction, TransactionInstruction } from '@solana/web3.js' -import bs58 from 'bs58' import { Buffer } from 'buffer' import { @@ -10,24 +9,9 @@ import { CheckoutError, SolanaContractInteraction } from '@/types/wallet_checkout' -import { Wallet } from 'ethers' import { SOLANA_MAINNET_CHAINS } from '@/data/SolanaData' import { SOLANA_TEST_CHAINS } from '@/data/SolanaData' -// Assume SolanaLib interface based on similar patterns in the codebase -interface SolanaLib { - getAddress(): string - getChainId(): string - signAndSendTransaction(transaction: Transaction): Promise - sendSol(recipientAddress: string, chainId: string, amount: bigint): Promise - sendSplToken( - tokenAddress: string, - recipientAddress: string, - chainId: string, - amount: bigint - ): Promise -} - export interface PaymentResult { txHash: string } @@ -45,103 +29,6 @@ const WalletCheckoutPaymentHandler = { } }, - /** - * Simulates a Solana transaction before sending - */ - async simulateSolanaTransaction( - connection: Connection, - transaction: Transaction, - feePayer: PublicKey - ): Promise { - try { - // Set recent blockhash for the transaction - transaction.recentBlockhash = (await connection.getLatestBlockhash()).blockhash - transaction.feePayer = feePayer - - // Simulate the transaction - const simulation = await connection.simulateTransaction(transaction) - - // Check simulation results - if (simulation.value.err) { - console.error('Simulation error:', simulation.value.err) - throw new CheckoutError( - CheckoutErrorCode.CONTRACT_INTERACTION_FAILED, - `Simulation failed: ${JSON.stringify(simulation.value.err)}` - ) - } - - console.log('Simulation successful:', simulation.value.logs) - } catch (error) { - if (error instanceof CheckoutError) { - throw error - } - throw new CheckoutError( - CheckoutErrorCode.CONTRACT_INTERACTION_FAILED, - `Simulation error: ${error instanceof Error ? error.message : String(error)}` - ) - } - }, - - /** - * Handles a Solana direct payment - */ - async processSolanaDirectPayment( - wallet: SolanaLib, - recipientAddress: string, - amount: string, - chainId: string - ): Promise { - try { - // Send SOL to recipient - const txHash = await wallet.sendSol(recipientAddress, chainId, BigInt(amount)) - return { txHash } - } catch (error) { - console.error('Solana direct payment error:', error) - throw new CheckoutError( - CheckoutErrorCode.DIRECT_PAYMENT_ERROR, - `Failed to send SOL: ${error instanceof Error ? error.message : String(error)}` - ) - } - }, - - /** - * Handles a Solana SPL token payment - */ - async processSolanaSplTokenPayment( - wallet: SolanaLib, - recipientAddress: string, - amount: string, - tokenAddress: string, - chainId: string - ): Promise { - try { - // Send SPL token to recipient - const txHash = await wallet.sendSplToken( - tokenAddress, - recipientAddress, - chainId, - BigInt(amount) - ) - return { txHash } - } catch (error) { - console.error('Solana SPL token payment error:', error) - - // Check if this is a token account error - const errorMessage = error instanceof Error ? error.message : String(error) - if (errorMessage.includes("doesn't have a token account")) { - throw new CheckoutError( - CheckoutErrorCode.DIRECT_PAYMENT_ERROR, - `Recipient doesn't have a token account for this SPL token. The recipient must create a token account before they can receive this token.` - ) - } - - throw new CheckoutError( - CheckoutErrorCode.DIRECT_PAYMENT_ERROR, - `Failed to send SPL token: ${errorMessage}` - ) - } - }, - /** * Process a Solana contract interaction */ @@ -150,7 +37,6 @@ const WalletCheckoutPaymentHandler = { contractInteraction: SolanaContractInteraction, chainId: string ): Promise { - try { const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc if (!rpc) { @@ -187,89 +73,11 @@ const WalletCheckoutPaymentHandler = { const { blockhash } = await connection.getLatestBlockhash('confirmed') transaction.recentBlockhash = blockhash - // Simulate transaction to check for errors - await this.simulateSolanaTransaction(connection, transaction, publicKey) - - // Determine which method to use for signing and sending - let txHash: string - - // Use sendRawTransaction if the wallet supports it (our implementation) - if (typeof wallet.sendRawTransaction === 'function') { - txHash = await wallet.sendRawTransaction(transaction, chainId) - } - // Otherwise use standard signAndSendTransaction - else if (typeof wallet.signAndSendTransaction === 'function') { - // Serialize the transaction to bs58 format - const serializedBytes = transaction.serialize() - // Convert Buffer to Uint8Array to avoid type issues - const serializedTx = bs58.encode(new Uint8Array(serializedBytes)) - - // Use the wallet's signAndSendTransaction method - const result = await wallet.signAndSendTransaction({ - transaction: serializedTx, - chainId: chainId - }) - - // Handle different response formats from various wallet implementations - if (typeof result === 'string') { - txHash = result - } else if (result && typeof result === 'object') { - if ('signature' in result && typeof result.signature === 'string') { - txHash = result.signature - } else if ('txHash' in result && typeof result.txHash === 'string') { - txHash = result.txHash - } else if ('transactionHash' in result && typeof result.transactionHash === 'string') { - txHash = result.transactionHash - } else { - // Try to stringify the result if it's not in a recognized format - const stringResult = String(result) - if (stringResult && stringResult !== '[object Object]') { - txHash = stringResult - } else { - throw new Error('Wallet returned an invalid transaction signature format') - } - } - } else { - throw new Error('Wallet returned an invalid response format') - } - - // Wait for transaction confirmation - try { - await connection.confirmTransaction(txHash, 'confirmed') - console.log('Transaction confirmed:', txHash) - } catch (confirmError) { - console.error('Error confirming transaction:', confirmError) - throw new CheckoutError( - CheckoutErrorCode.CONTRACT_INTERACTION_FAILED, - `Transaction was sent but confirmation failed: ${ - confirmError instanceof Error ? confirmError.message : String(confirmError) - }` - ) - } - } - // No compatible method found - else { - throw new CheckoutError( - CheckoutErrorCode.INVALID_CHECKOUT_REQUEST, - 'Wallet does not support Solana transaction sending' - ) - } + const txHash = await connection.sendRawTransaction(transaction.serialize()) + await connection.confirmTransaction(txHash, 'confirmed') return { txHash } - } catch (error) { - console.error('Solana contract interaction error:', error) - if (error instanceof CheckoutError) { - throw error - } - throw new CheckoutError( - CheckoutErrorCode.CONTRACT_INTERACTION_FAILED, - `Failed to execute Solana transaction: ${ - error instanceof Error ? error.message : String(error) - }` - ) - } }, - /** * Process any payment type and handle errors */ @@ -287,19 +95,14 @@ const WalletCheckoutPaymentHandler = { 'Solana payment requires a compatible wallet' ) } - // Contract interaction payment - if (contractInteraction && !recipient) { - if (contractInteraction.type === 'solana-instruction') { - return await this.processSolanaContractInteraction( - wallet, - contractInteraction as SolanaContractInteraction, - `${chainNamespace}:${chainId}` - ) - } - throw new CheckoutError(CheckoutErrorCode.UNSUPPORTED_CONTRACT_INTERACTION) + if (contractInteraction && !recipient && contractInteraction.type === 'solana-instruction') { + return await this.processSolanaContractInteraction( + wallet, + contractInteraction as SolanaContractInteraction, + `${chainNamespace}:${chainId}` + ) } - // Direct payment (with recipient) if (recipient && !contractInteraction) { const recipientAddress = recipient.split(':')[2] @@ -309,39 +112,23 @@ const WalletCheckoutPaymentHandler = { // Handle SOL transfers (slip44:501) if (assetNamespace === 'slip44' && assetReference === '501') { - return await this.processSolanaDirectPayment( - wallet, - recipientAddress, - payment.amount, - `${chainNamespace}:${chainId}` - ) + const txHash = await wallet.sendSol(recipientAddress, `${chainNamespace}:${chainId}`, BigInt(payment.amount)) + return { txHash } } // Handle SPL token transfers (token:) if (assetNamespace === 'token') { - return await this.processSolanaSplTokenPayment( - wallet, - recipientAddress, - payment.amount, + const txHash = await wallet.sendSplToken( assetReference, - `${chainNamespace}:${chainId}` + recipientAddress, + `${chainNamespace}:${chainId}`, + BigInt(payment.amount) ) + return { txHash } } - - throw new CheckoutError( - CheckoutErrorCode.UNSUPPORTED_CONTRACT_INTERACTION, - `Unsupported Solana asset type: ${assetNamespace}:${assetReference}` - ) } - - // Neither or both are present - throw new CheckoutError( - CheckoutErrorCode.INVALID_CHECKOUT_REQUEST, - 'Payment must have either recipient or contractInteraction, not both or neither' - ) } - - // ------ Process EVM payments (existing code) ------ + // Ensure wallet is an EVM wallet if (!wallet.sendTransaction) { throw new CheckoutError( @@ -349,7 +136,6 @@ const WalletCheckoutPaymentHandler = { 'EVM payment requires an EVM wallet' ) } - // Direct payment (with recipient) if (recipient && !contractInteraction) { const { asset, amount, assetMetadata } = payment @@ -380,37 +166,27 @@ const WalletCheckoutPaymentHandler = { }) return { txHash: tx.hash } } - - throw new CheckoutError(CheckoutErrorCode.INVALID_CHECKOUT_REQUEST) } // Contract interaction payment - else if (contractInteraction && !recipient) { - // Handle array of calls - if (Array.isArray(contractInteraction.data) && contractInteraction.type === 'evm-calls') { - let lastTxHash = '0x' - - for (const call of contractInteraction.data) { - console.log('Processing contract call:', call) - const tx = await wallet.sendTransaction({ - to: call.to, - value: call.value, - data: call.data - }) - console.log('Transaction sent:', tx) - lastTxHash = tx.hash - } + if (contractInteraction && !recipient && Array.isArray(contractInteraction.data) && contractInteraction.type === 'evm-calls') { + let lastTxHash = '0x' - return { txHash: lastTxHash } + for (const call of contractInteraction.data) { + console.log('Processing contract call:', call) + const tx = await wallet.sendTransaction({ + to: call.to, + value: call.value, + data: call.data + }) + console.log('Transaction sent:', tx) + lastTxHash = tx.hash } - throw new CheckoutError(CheckoutErrorCode.UNSUPPORTED_CONTRACT_INTERACTION) + return { txHash: lastTxHash } } // Neither or both are present - throw new CheckoutError( - CheckoutErrorCode.INVALID_CHECKOUT_REQUEST, - 'Payment must have either recipient or contractInteraction, not both or neither' - ) + throw new CheckoutError( CheckoutErrorCode.INVALID_CHECKOUT_REQUEST) } catch (error) { console.error('Payment processing error:', error) From ffd773af2f0e4916d9bb3245fc28aa4e54627c0d Mon Sep 17 00:00:00 2001 From: Karandeep Singh Date: Tue, 1 Apr 2025 11:13:37 +0530 Subject: [PATCH 12/13] chore: remove unused method --- .../react-wallet-v2/src/lib/SolanaLib.ts | 198 +++++++----------- 1 file changed, 74 insertions(+), 124 deletions(-) diff --git a/advanced/wallets/react-wallet-v2/src/lib/SolanaLib.ts b/advanced/wallets/react-wallet-v2/src/lib/SolanaLib.ts index 0dab0d7bd..c57487244 100644 --- a/advanced/wallets/react-wallet-v2/src/lib/SolanaLib.ts +++ b/advanced/wallets/react-wallet-v2/src/lib/SolanaLib.ts @@ -135,45 +135,40 @@ export default class SolanaLib { */ public async sendSol(recipientAddress: string, chainId: string, amount: bigint): Promise { console.log({ chainId }) - try { - const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc - - if (!rpc) { - throw new Error('There is no RPC URL for the provided chain') - } - - const connection = new Connection(rpc, 'confirmed') - const fromPubkey = this.keypair.publicKey - const toPubkey = new PublicKey(recipientAddress) - - // Create a simple SOL transfer transaction - const transaction = new Transaction().add( - SystemProgram.transfer({ - fromPubkey, - toPubkey, - lamports: amount - }) - ) + const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc - // Get recent blockhash - const { blockhash } = await connection.getLatestBlockhash('confirmed') - transaction.recentBlockhash = blockhash - transaction.feePayer = fromPubkey + if (!rpc) { + throw new Error('There is no RPC URL for the provided chain') + } - // Sign the transaction - transaction.sign(this.keypair) + const connection = new Connection(rpc, 'confirmed') + const fromPubkey = this.keypair.publicKey + const toPubkey = new PublicKey(recipientAddress) - // Send and confirm the transaction - const signature = await connection.sendRawTransaction(transaction.serialize()) + // Create a simple SOL transfer transaction + const transaction = new Transaction().add( + SystemProgram.transfer({ + fromPubkey, + toPubkey, + lamports: amount + }) + ) - // Wait for confirmation - await connection.confirmTransaction(signature, 'confirmed') + // Get recent blockhash + const { blockhash } = await connection.getLatestBlockhash('confirmed') + transaction.recentBlockhash = blockhash + transaction.feePayer = fromPubkey - return signature - } catch (error) { - console.error('Error sending SOL:', error) - throw error - } + // Sign the transaction + transaction.sign(this.keypair) + + // Send and confirm the transaction + const signature = await connection.sendRawTransaction(transaction.serialize()) + + // Wait for confirmation + await connection.confirmTransaction(signature, 'confirmed') + + return signature } /** @@ -189,110 +184,65 @@ export default class SolanaLib { chainId: string, amount: bigint ): Promise { - try { - const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc - - if (!rpc) { - throw new Error('There is no RPC URL for the provided chain') - } - - const connection = new Connection(rpc, 'confirmed') - const fromWallet = this.keypair - const fromPubkey = fromWallet.publicKey - const toPubkey = new PublicKey(recipientAddress) - const mint = new PublicKey(tokenAddress) - - // Get sender's token account (create if it doesn't exist) - const fromTokenAccount = await getOrCreateAssociatedTokenAccount( - connection, - fromWallet, - mint, - fromPubkey - ) - - // Check if recipient has a token account WITHOUT creating one - const associatedTokenAddress = await getAssociatedTokenAddress(mint, toPubkey) - - const recipientTokenAccount = await connection.getAccountInfo(associatedTokenAddress) - - if (!recipientTokenAccount) { - throw new Error( - `Recipient ${recipientAddress} doesn't have a token account for this SPL token. Transaction cannot proceed.` - ) - } - - // Create transfer instruction to existing account - const transferInstruction = createTransferInstruction( - fromTokenAccount.address, - associatedTokenAddress, - fromPubkey, - amount, - [], - TOKEN_PROGRAM_ID - ) + const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc - // Create transaction and add the transfer instruction - const transaction = new Transaction().add(transferInstruction) + if (!rpc) { + throw new Error('There is no RPC URL for the provided chain') + } - // Get recent blockhash - const { blockhash } = await connection.getLatestBlockhash('confirmed') - transaction.recentBlockhash = blockhash - transaction.feePayer = fromPubkey + const connection = new Connection(rpc, 'confirmed') + const fromWallet = this.keypair + const fromPubkey = fromWallet.publicKey + const toPubkey = new PublicKey(recipientAddress) + const mint = new PublicKey(tokenAddress) - // Sign the transaction - transaction.sign(fromWallet) + // Get sender's token account (create if it doesn't exist) + const fromTokenAccount = await getOrCreateAssociatedTokenAccount( + connection, + fromWallet, + mint, + fromPubkey + ) - // Send and confirm the transaction - const signature = await connection.sendRawTransaction(transaction.serialize()) + // Check if recipient has a token account WITHOUT creating one + const associatedTokenAddress = await getAssociatedTokenAddress(mint, toPubkey) - // Wait for confirmation - await connection.confirmTransaction(signature, 'confirmed') + const recipientTokenAccount = await connection.getAccountInfo(associatedTokenAddress) - return signature - } catch (error) { - console.error('Error sending SPL token:', error) - throw error + if (!recipientTokenAccount) { + throw new Error( + `Recipient ${recipientAddress} doesn't have a token account for this SPL token. Transaction cannot proceed.` + ) } - } - /** - * Send a raw transaction (for contract interactions) - * @param transaction The transaction to sign and send - * @returns The transaction signature - */ - public async sendRawTransaction(transaction: Transaction, chainId: string): Promise { - try { - const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc - - if (!rpc) { - throw new Error('There is no RPC URL for the provided chain') - } + // Create transfer instruction to existing account + const transferInstruction = createTransferInstruction( + fromTokenAccount.address, + associatedTokenAddress, + fromPubkey, + amount, + [], + TOKEN_PROGRAM_ID + ) - const connection = new Connection(rpc, 'confirmed') + // Create transaction and add the transfer instruction + const transaction = new Transaction().add(transferInstruction) - // Set fee payer if not already set - if (!transaction.feePayer) { - transaction.feePayer = this.keypair.publicKey - } + // Get recent blockhash + const { blockhash } = await connection.getLatestBlockhash('confirmed') + transaction.recentBlockhash = blockhash + transaction.feePayer = fromPubkey - // Get recent blockhash if not already set - if (!transaction.recentBlockhash) { - const { blockhash } = await connection.getLatestBlockhash('confirmed') - transaction.recentBlockhash = blockhash - } + // Sign the transaction + transaction.sign(fromWallet) - // Sign transaction - transaction.sign(this.keypair) + // Send and confirm the transaction + const signature = await connection.sendRawTransaction(transaction.serialize()) - // Send and confirm transaction - const signature = await connection.sendRawTransaction(transaction.serialize()) - await connection.confirmTransaction(signature, 'confirmed') + // Wait for confirmation + await connection.confirmTransaction(signature, 'confirmed') - return signature - } catch (error) { - console.error('Error signing and sending transaction:', error) - throw error - } + return signature } } From 62f37dbbe25ffccf2b4080ffff3d17307b96256f Mon Sep 17 00:00:00 2001 From: Karandeep Singh Date: Tue, 1 Apr 2025 11:15:23 +0530 Subject: [PATCH 13/13] chore: run prettier --- .../hooks/useWalletConnectEventsManager.ts | 4 +- .../src/utils/PaymentValidatorUtil.ts | 488 +++++++++--------- .../src/utils/TransactionSimulatorUtil.ts | 24 +- .../src/utils/WalletCheckoutPaymentHandler.ts | 85 +-- 4 files changed, 321 insertions(+), 280 deletions(-) diff --git a/advanced/wallets/react-wallet-v2/src/hooks/useWalletConnectEventsManager.ts b/advanced/wallets/react-wallet-v2/src/hooks/useWalletConnectEventsManager.ts index fcf2e9629..28bfdd32d 100644 --- a/advanced/wallets/react-wallet-v2/src/hooks/useWalletConnectEventsManager.ts +++ b/advanced/wallets/react-wallet-v2/src/hooks/useWalletConnectEventsManager.ts @@ -101,14 +101,14 @@ export default function useWalletConnectEventsManager(initialized: boolean) { try { await WalletCheckoutCtrl.actions.prepareFeasiblePayments(request.params[0]) } catch (error) { - // If it's not a CheckoutError, create one + // If it's not a CheckoutError, create one if (!(error && typeof error === 'object' && 'code' in error)) { error = createCheckoutError( CheckoutErrorCode.INVALID_CHECKOUT_REQUEST, `Unexpected error: ${error instanceof Error ? error.message : String(error)}` ) } - + return await walletkit.respondSessionRequest({ topic, response: WalletCheckoutUtil.formatCheckoutErrorResponse(id, error) diff --git a/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts b/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts index 771950de3..f3404131c 100644 --- a/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts +++ b/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts @@ -1,12 +1,23 @@ import { getChainData } from '@/data/chainsUtil' -import { type PaymentOption, type DetailedPaymentOption, Hex, SolanaContractInteraction } from '@/types/wallet_checkout' +import { + type PaymentOption, + type DetailedPaymentOption, + Hex, + SolanaContractInteraction +} from '@/types/wallet_checkout' import { createPublicClient, erc20Abi, http, getContract, encodeFunctionData } from 'viem' import TransactionSimulatorUtil from './TransactionSimulatorUtil' import SettingsStore from '@/store/SettingsStore' import { getSolanaTokenData, getTokenData } from '@/data/tokenUtil' import { getChainById } from './ChainUtil' import { blockchainApiRpc } from '@/data/EIP155Data' -import { Connection, PublicKey, SystemProgram, Transaction, TransactionInstruction } from '@solana/web3.js' +import { + Connection, + PublicKey, + SystemProgram, + Transaction, + TransactionInstruction +} from '@solana/web3.js' import { SOLANA_TEST_CHAINS } from '@/data/SolanaData' import { SOLANA_MAINNET_CHAINS } from '@/data/SolanaData' import { createTransferInstruction, TOKEN_PROGRAM_ID } from '@solana/spl-token' @@ -85,7 +96,7 @@ export class PaymentValidationUtils { // Support ERC20 tokens, native tokens, and solana token return ['erc20', 'slip44', 'token'].includes(assetNamespace) } - + // methods to get token details private static async getNativeAssetDetails( @@ -166,7 +177,7 @@ export class PaymentValidationUtils { const balance = await connection.getBalance(publicKey) return { ...defaultTokenDetails, - balance: BigInt(balance), + balance: BigInt(balance) } } catch (error) { console.error('Error getting SOL balance:', error) @@ -186,7 +197,7 @@ export class PaymentValidationUtils { symbol: 'UNK', name: 'Unknown Token' } - + try { const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc @@ -199,44 +210,43 @@ export class PaymentValidationUtils { const publicKey = new PublicKey(account) const mintAddress = new PublicKey(tokenAddress) - const token = getSolanaTokenData(caip19AssetAddress) + const token = getSolanaTokenData(caip19AssetAddress) - // Get token balance - let balance = BigInt(0) - let decimals = token?.decimals || 0 // Use known token decimals or default + // Get token balance + let balance = BigInt(0) + let decimals = token?.decimals || 0 // Use known token decimals or default - // Find the associated token account(s) - const tokenAccounts = await connection.getParsedTokenAccountsByOwner(publicKey, { - mint: mintAddress - }) + // Find the associated token account(s) + const tokenAccounts = await connection.getParsedTokenAccountsByOwner(publicKey, { + mint: mintAddress + }) - // If token account exists, get balance - if (tokenAccounts.value.length > 0) { - const tokenAccountPubkey = tokenAccounts.value[0].pubkey - const tokenBalance = await connection.getTokenAccountBalance(tokenAccountPubkey) - balance = BigInt(tokenBalance.value.amount) + // If token account exists, get balance + if (tokenAccounts.value.length > 0) { + const tokenAccountPubkey = tokenAccounts.value[0].pubkey + const tokenBalance = await connection.getTokenAccountBalance(tokenAccountPubkey) + balance = BigInt(tokenBalance.value.amount) - // Update decimals from on-chain data if not a known token - if (!token) { - decimals = tokenBalance.value.decimals - } - } else if (!token) { - // If no token accounts and not a known token, try to get decimals from mint - const mintInfo = await connection.getParsedAccountInfo(mintAddress) - if (mintInfo.value) { - const parsedMintInfo = (mintInfo.value.data as any).parsed?.info - decimals = parsedMintInfo?.decimals || decimals - } + // Update decimals from on-chain data if not a known token + if (!token) { + decimals = tokenBalance.value.decimals } - - // Return with known metadata or fallback to generic - return { - balance, - decimals, - symbol: token?.symbol || tokenAddress.slice(0, 4).toUpperCase(), - name: token?.name || `SPL Token (${tokenAddress.slice(0, 8)}...)` + } else if (!token) { + // If no token accounts and not a known token, try to get decimals from mint + const mintInfo = await connection.getParsedAccountInfo(mintAddress) + if (mintInfo.value) { + const parsedMintInfo = (mintInfo.value.data as any).parsed?.info + decimals = parsedMintInfo?.decimals || decimals } - + } + + // Return with known metadata or fallback to generic + return { + balance, + decimals, + symbol: token?.symbol || tokenAddress.slice(0, 4).toUpperCase(), + name: token?.name || `SPL Token (${tokenAddress.slice(0, 8)}...)` + } } catch (error) { // Return default values in case of error return defaultTokenDetails @@ -260,15 +270,19 @@ export class PaymentValidationUtils { contractInteraction.data as { to: string; value: string; data: string }[] ) return simulateEvmTransaction - } + } // If data is not an array, it's an invalid format return false } - private static async simulateSolanaContractInteraction(params:{contractInteraction: SolanaContractInteraction, account: string, chainId: string}){ - try{ - const {contractInteraction, account, chainId} = params + private static async simulateSolanaContractInteraction(params: { + contractInteraction: SolanaContractInteraction + account: string + chainId: string + }) { + try { + const { contractInteraction, account, chainId } = params const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc - if(!rpc){ + if (!rpc) { return false } @@ -294,17 +308,26 @@ export class PaymentValidationUtils { // Add to transaction transaction.add(txInstruction) - const simulationResult = await TransactionSimulatorUtil.simulateSolanaTransaction({connection, transaction, feePayer:publicKey}) + const simulationResult = await TransactionSimulatorUtil.simulateSolanaTransaction({ + connection, + transaction, + feePayer: publicKey + }) return simulationResult - }catch(e){ + } catch (e) { return false } } - private static async simulateSolanaNativeTransfer(params:{account:string, recipientAddress:string, amount:string, chainId:string}){ - try{ - const {account, recipientAddress, amount, chainId} = params + private static async simulateSolanaNativeTransfer(params: { + account: string + recipientAddress: string + amount: string + chainId: string + }) { + try { + const { account, recipientAddress, amount, chainId } = params const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc - if(!rpc){ + if (!rpc) { return false } @@ -319,44 +342,48 @@ export class PaymentValidationUtils { lamports: BigInt(amount) }) ) - const simulationResult = await TransactionSimulatorUtil.simulateSolanaTransaction({connection, transaction, feePayer:publicKey}) + const simulationResult = await TransactionSimulatorUtil.simulateSolanaTransaction({ + connection, + transaction, + feePayer: publicKey + }) return simulationResult - }catch(e){ + } catch (e) { return false } } - private static async simulateSolanaTokenTransfer(params:{account:string, recipientAddress:string, amount:bigint, tokenAddress:string, chainId:string}){ - try{ - const {account, recipientAddress, amount, tokenAddress, chainId} = params + private static async simulateSolanaTokenTransfer(params: { + account: string + recipientAddress: string + amount: bigint + tokenAddress: string + chainId: string + }) { + try { + const { account, recipientAddress, amount, tokenAddress, chainId } = params const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc - if(!rpc){ + if (!rpc) { return false } - const connection = new Connection(rpc, 'confirmed'); - const fromPubkey = new PublicKey(account); - const mintAddress = new PublicKey(tokenAddress); - const toPubkey = new PublicKey(recipientAddress); - - const fromTokenAccountAddress = await getAssociatedTokenAddress( - mintAddress, - fromPubkey - ); - - const fromTokenAccount = await connection.getAccountInfo(fromTokenAccountAddress); + const connection = new Connection(rpc, 'confirmed') + const fromPubkey = new PublicKey(account) + const mintAddress = new PublicKey(tokenAddress) + const toPubkey = new PublicKey(recipientAddress) + + const fromTokenAccountAddress = await getAssociatedTokenAddress(mintAddress, fromPubkey) + + const fromTokenAccount = await connection.getAccountInfo(fromTokenAccountAddress) if (!fromTokenAccount) { - return false; + return false } - const toTokenAccountAddress = await getAssociatedTokenAddress( - mintAddress, - toPubkey - ); - const recipientTokenAccount = await connection.getAccountInfo(toTokenAccountAddress); + const toTokenAccountAddress = await getAssociatedTokenAddress(mintAddress, toPubkey) + const recipientTokenAccount = await connection.getAccountInfo(toTokenAccountAddress) // Create transaction - const transaction = new Transaction(); - + const transaction = new Transaction() + // Add instruction to create recipient token account if needed if (!recipientTokenAccount) { const createAccountInstruction = createAssociatedTokenAccountInstruction( @@ -364,8 +391,8 @@ export class PaymentValidationUtils { toTokenAccountAddress, toPubkey, mintAddress - ); - transaction.add(createAccountInstruction); + ) + transaction.add(createAccountInstruction) } // Add transfer instruction @@ -376,12 +403,16 @@ export class PaymentValidationUtils { amount, [], TOKEN_PROGRAM_ID - ); - transaction.add(transferInstruction); + ) + transaction.add(transferInstruction) - const simulationResult = await TransactionSimulatorUtil.simulateSolanaTransaction({connection, transaction, feePayer:fromPubkey}) + const simulationResult = await TransactionSimulatorUtil.simulateSolanaTransaction({ + connection, + transaction, + feePayer: fromPubkey + }) return simulationResult - }catch(e){ + } catch (e) { return false } } @@ -414,9 +445,7 @@ export class PaymentValidationUtils { } } - private static async getDetailedDirectPaymentOption( - payment: PaymentOption, - ): Promise<{ + private static async getDetailedDirectPaymentOption(payment: PaymentOption): Promise<{ validatedPayment: DetailedPaymentOption | null hasMatchingAsset: boolean }> { @@ -426,50 +455,50 @@ export class PaymentValidationUtils { if (!recipientAddress) { return { validatedPayment: null, hasMatchingAsset: false } } - + // Parse asset details const { chainId, assetAddress, chainNamespace, assetNamespace } = PaymentValidationUtils.getAssetDetails(payment.asset) - + // Check if asset namespace is supported if (!PaymentValidationUtils.isSupportedAssetNamespace(assetNamespace)) { return { validatedPayment: null, hasMatchingAsset: false } } - - let result; - - switch(chainNamespace) { + + let result + + switch (chainNamespace) { case 'solana': result = await this.processSolanaDirectPayment( - payment, - recipientAddress, - chainId, - assetAddress, + payment, + recipientAddress, + chainId, + assetAddress, assetNamespace - ); - break; - + ) + break + case 'eip155': result = await this.processEvmDirectPayment( - payment, - recipientAddress, - chainId, - assetAddress, + payment, + recipientAddress, + chainId, + assetAddress, assetNamespace - ); - break; - + ) + break + default: return { validatedPayment: null, hasMatchingAsset: false } } - - return result; + + return result } catch (error) { console.error('Error validating payment option:', error) return { validatedPayment: null, hasMatchingAsset: false } } } - + private static async processSolanaDirectPayment( payment: PaymentOption, recipientAddress: string, @@ -480,60 +509,61 @@ export class PaymentValidationUtils { validatedPayment: DetailedPaymentOption | null hasMatchingAsset: boolean }> { - const account = SettingsStore.state.solanaAddress; - let tokenDetails: TokenDetails | undefined; - let simulationResult: boolean | undefined; - + const account = SettingsStore.state.solanaAddress + let tokenDetails: TokenDetails | undefined + let simulationResult: boolean | undefined + if (assetNamespace === 'slip44' && assetAddress === '501') { simulationResult = await this.simulateSolanaNativeTransfer({ - account, - recipientAddress: recipientAddress, - amount: payment.amount, + account, + recipientAddress: recipientAddress, + amount: payment.amount, chainId: `solana:${chainId}` - }); - tokenDetails = simulationResult ? await this.getSolNativeAssetDetails(account, `solana:${chainId}`) : undefined; + }) + tokenDetails = simulationResult + ? await this.getSolNativeAssetDetails(account, `solana:${chainId}`) + : undefined } else if (assetNamespace === 'token') { simulationResult = await this.simulateSolanaTokenTransfer({ - account, - recipientAddress: recipientAddress, - amount: BigInt(payment.amount), - tokenAddress: assetAddress, - chainId: `solana:${chainId}` - }); - tokenDetails = simulationResult ? await this.getSplTokenDetails( - assetAddress, account, - `solana:${chainId}`, - payment.asset - ) : undefined; + recipientAddress: recipientAddress, + amount: BigInt(payment.amount), + tokenAddress: assetAddress, + chainId: `solana:${chainId}` + }) + tokenDetails = simulationResult + ? await this.getSplTokenDetails(assetAddress, account, `solana:${chainId}`, payment.asset) + : undefined } else { - return { validatedPayment: null, hasMatchingAsset: false }; + return { validatedPayment: null, hasMatchingAsset: false } } - + // Check if token details were assigned if (!tokenDetails) { - return { validatedPayment: null, hasMatchingAsset: false }; + return { validatedPayment: null, hasMatchingAsset: false } } - + // Check if user has the asset (balance > 0) - const hasMatchingAsset = tokenDetails.balance > BigInt(0); - console.log({tokenDetails}); + const hasMatchingAsset = tokenDetails.balance > BigInt(0) + console.log({ tokenDetails }) if (!hasMatchingAsset) { - return { validatedPayment: null, hasMatchingAsset }; + return { validatedPayment: null, hasMatchingAsset } } - + // Create detailed payment option with metadata - const detailedPayment = simulationResult ? PaymentValidationUtils.createDetailedPaymentOption( - payment, - tokenDetails, - assetNamespace, - chainId, - 'solana' - ) : null; - - return { validatedPayment: detailedPayment, hasMatchingAsset: true }; + const detailedPayment = simulationResult + ? PaymentValidationUtils.createDetailedPaymentOption( + payment, + tokenDetails, + assetNamespace, + chainId, + 'solana' + ) + : null + + return { validatedPayment: detailedPayment, hasMatchingAsset: true } } - + private static async processEvmDirectPayment( payment: PaymentOption, recipientAddress: string, @@ -544,10 +574,10 @@ export class PaymentValidationUtils { validatedPayment: DetailedPaymentOption | null hasMatchingAsset: boolean }> { - const account = SettingsStore.state.eip155Address as `0x${string}`; - let tokenDetails: TokenDetails | undefined; - let simulationResult: boolean | undefined; - + const account = SettingsStore.state.eip155Address as `0x${string}` + let tokenDetails: TokenDetails | undefined + let simulationResult: boolean | undefined + if (assetNamespace === 'erc20') { simulationResult = await PaymentValidationUtils.simulateEvmContractInteraction( { @@ -565,12 +595,12 @@ export class PaymentValidationUtils { }, chainId, account - ); + ) tokenDetails = await PaymentValidationUtils.getErc20TokenDetails( assetAddress as `0x${string}`, Number(chainId), account as `0x${string}` - ); + ) } else if (assetNamespace === 'slip44' && assetAddress === '60') { // slip44:60 - native ETH token simulationResult = await TransactionSimulatorUtil.simulateEvmTransaction( @@ -583,95 +613,95 @@ export class PaymentValidationUtils { data: '0x' } ] - ); + ) tokenDetails = await PaymentValidationUtils.getNativeAssetDetails( Number(chainId), account as `0x${string}` - ); + ) } else { - return { validatedPayment: null, hasMatchingAsset: false }; + return { validatedPayment: null, hasMatchingAsset: false } } - + // Check if token details were assigned if (!tokenDetails || simulationResult === undefined) { - return { validatedPayment: null, hasMatchingAsset: false }; + return { validatedPayment: null, hasMatchingAsset: false } } - + // Check if user has the asset (balance > 0) - const hasMatchingAsset = tokenDetails.balance > BigInt(0); - console.log({tokenDetails}); + const hasMatchingAsset = tokenDetails.balance > BigInt(0) + console.log({ tokenDetails }) if (!hasMatchingAsset) { - return { validatedPayment: null, hasMatchingAsset }; + return { validatedPayment: null, hasMatchingAsset } } - + // Create detailed payment option with metadata - const detailedPayment = simulationResult ? PaymentValidationUtils.createDetailedPaymentOption( - payment, - tokenDetails, - assetNamespace, - chainId, - 'eip155' - ) : null; - - return { validatedPayment: detailedPayment, hasMatchingAsset: true }; + const detailedPayment = simulationResult + ? PaymentValidationUtils.createDetailedPaymentOption( + payment, + tokenDetails, + assetNamespace, + chainId, + 'eip155' + ) + : null + + return { validatedPayment: detailedPayment, hasMatchingAsset: true } } - - private static async getDetailedContractPaymentOption( - payment: PaymentOption, - ): Promise<{ + + private static async getDetailedContractPaymentOption(payment: PaymentOption): Promise<{ validatedPayment: DetailedPaymentOption | null hasMatchingAsset: boolean }> { try { const { asset, contractInteraction } = payment - + if (!contractInteraction) { return { validatedPayment: null, hasMatchingAsset: false } } - + // Parse asset details const { chainId, assetAddress, chainNamespace, assetNamespace } = PaymentValidationUtils.getAssetDetails(asset) - + // Check if asset namespace is supported if (!PaymentValidationUtils.isSupportedAssetNamespace(assetNamespace)) { return { validatedPayment: null, hasMatchingAsset: false } } - - let result; - + + let result + switch (chainNamespace) { case 'solana': result = await this.processSolanaContractPayment( - payment, - chainId, - assetAddress, + payment, + chainId, + assetAddress, assetNamespace, contractInteraction - ); - break; - + ) + break + case 'eip155': result = await this.processEvmContractPayment( - payment, - chainId, - assetAddress, + payment, + chainId, + assetAddress, assetNamespace, contractInteraction - ); - break; - + ) + break + default: return { validatedPayment: null, hasMatchingAsset: false } } - - return result; + + return result } catch (error) { console.error('Error validating contract payment option:', error) return { validatedPayment: null, hasMatchingAsset: false } } } - + private static async processSolanaContractPayment( payment: PaymentOption, chainId: string, @@ -682,26 +712,26 @@ export class PaymentValidationUtils { validatedPayment: DetailedPaymentOption | null hasMatchingAsset: boolean }> { - const account = SettingsStore.state.solanaAddress; - let tokenDetails: TokenDetails | undefined; - let isValid = false; - + const account = SettingsStore.state.solanaAddress + let tokenDetails: TokenDetails | undefined + let isValid = false + if (contractInteraction.type !== 'solana-instruction') { return { validatedPayment: null, hasMatchingAsset: false } } - + isValid = await this.simulateSolanaContractInteraction({ contractInteraction: contractInteraction as SolanaContractInteraction, account, chainId: `solana:${chainId}` - }); + }) if (!isValid) { return { validatedPayment: null, hasMatchingAsset: false } } - + if (assetNamespace === 'slip44' && assetAddress === '501') { // Native SOL - tokenDetails = await this.getSolNativeAssetDetails(account, `solana:${chainId}`); + tokenDetails = await this.getSolNativeAssetDetails(account, `solana:${chainId}`) } else if (assetNamespace === 'token') { // SPL token tokenDetails = await this.getSplTokenDetails( @@ -709,19 +739,19 @@ export class PaymentValidationUtils { account, `solana:${chainId}`, payment.asset - ); + ) } else { return { validatedPayment: null, hasMatchingAsset: false } } - + // Check if token details were assigned if (!tokenDetails) { return { validatedPayment: null, hasMatchingAsset: false } } - + // Check if user has the asset (balance > 0) - const hasMatchingAsset = tokenDetails.balance > BigInt(0); - + const hasMatchingAsset = tokenDetails.balance > BigInt(0) + // Create detailed payment option with metadata const detailedPayment = isValid ? PaymentValidationUtils.createDetailedPaymentOption( @@ -731,10 +761,10 @@ export class PaymentValidationUtils { chainId, 'solana' ) - : null; - return { validatedPayment: detailedPayment, hasMatchingAsset }; + : null + return { validatedPayment: detailedPayment, hasMatchingAsset } } - + private static async processEvmContractPayment( payment: PaymentOption, chainId: string, @@ -745,48 +775,48 @@ export class PaymentValidationUtils { validatedPayment: DetailedPaymentOption | null hasMatchingAsset: boolean }> { - const account = SettingsStore.state.eip155Address as `0x${string}`; - let tokenDetails: TokenDetails | undefined; - let isValid = false; - + const account = SettingsStore.state.eip155Address as `0x${string}` + let tokenDetails: TokenDetails | undefined + let isValid = false + if (contractInteraction.type !== 'evm-calls') { return { validatedPayment: null, hasMatchingAsset: false } } - + isValid = await PaymentValidationUtils.simulateEvmContractInteraction( contractInteraction, chainId, account - ); - + ) + if (!isValid) { return { validatedPayment: null, hasMatchingAsset: false } } - + if (assetNamespace === 'erc20') { tokenDetails = await PaymentValidationUtils.getErc20TokenDetails( assetAddress as `0x${string}`, Number(chainId), account as `0x${string}` - ); + ) } else if (assetNamespace === 'slip44') { // must be slip44 since we already checked supported namespaces tokenDetails = await PaymentValidationUtils.getNativeAssetDetails( Number(chainId), account as `0x${string}` - ); + ) } else { return { validatedPayment: null, hasMatchingAsset: false } } - + // Check if token details were assigned if (!tokenDetails) { return { validatedPayment: null, hasMatchingAsset: false } } - + // Check if user has the asset (balance > 0) - const hasMatchingAsset = tokenDetails.balance > BigInt(0); - + const hasMatchingAsset = tokenDetails.balance > BigInt(0) + // Create detailed payment option with metadata const detailedPayment = isValid ? PaymentValidationUtils.createDetailedPaymentOption( @@ -796,18 +826,16 @@ export class PaymentValidationUtils { chainId, 'eip155' ) - : null; - return { validatedPayment: detailedPayment, hasMatchingAsset }; + : null + return { validatedPayment: detailedPayment, hasMatchingAsset } } - - static async findFeasiblePayments( - payments: PaymentOption[] - ): Promise<{ + + static async findFeasiblePayments(payments: PaymentOption[]): Promise<{ feasiblePayments: DetailedPaymentOption[] isUserHaveAtleastOneMatchingAssets: boolean }> { let isUserHaveAtleastOneMatchingAssets = false - + const results = await Promise.all( payments.map(async payment => { if (payment.recipient && !payment.contractInteraction) { diff --git a/advanced/wallets/react-wallet-v2/src/utils/TransactionSimulatorUtil.ts b/advanced/wallets/react-wallet-v2/src/utils/TransactionSimulatorUtil.ts index be145dd3d..67a5a1c66 100644 --- a/advanced/wallets/react-wallet-v2/src/utils/TransactionSimulatorUtil.ts +++ b/advanced/wallets/react-wallet-v2/src/utils/TransactionSimulatorUtil.ts @@ -1,5 +1,5 @@ -import { blockchainApiRpc } from '@/data/EIP155Data'; -import { Connection, PublicKey, Transaction } from '@solana/web3.js'; +import { blockchainApiRpc } from '@/data/EIP155Data' +import { Connection, PublicKey, Transaction } from '@solana/web3.js' import { createPublicClient, http } from 'viem' const TransactionSimulatorUtil = { @@ -24,7 +24,7 @@ const TransactionSimulatorUtil = { try { const client = createPublicClient({ - transport: http(blockchainApiRpc(Number(chainId))), + transport: http(blockchainApiRpc(Number(chainId))) }) // Process all calls in parallel with individual error handling @@ -71,22 +71,22 @@ const TransactionSimulatorUtil = { } }, - /** + /** * Simulates a Solana transaction - * + * * @param connection - Solana connection * @param transaction - Transaction to simulate * @param feePayer - Fee payer's public key * @returns Object with simulation success status and error details if applicable * @throws CheckoutError if there's a critical simulation error that should block the transaction */ - async simulateSolanaTransaction(param:{ - connection: Connection, - transaction: Transaction, - feePayer: PublicKey} - ): Promise { + async simulateSolanaTransaction(param: { + connection: Connection + transaction: Transaction + feePayer: PublicKey + }): Promise { try { - const {connection, transaction, feePayer} = param + const { connection, transaction, feePayer } = param // Set recent blockhash for the transaction transaction.recentBlockhash = (await connection.getLatestBlockhash()).blockhash transaction.feePayer = feePayer @@ -104,7 +104,7 @@ const TransactionSimulatorUtil = { } catch (error) { return false } - }, + } } export default TransactionSimulatorUtil diff --git a/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutPaymentHandler.ts b/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutPaymentHandler.ts index fe03abde8..f59b51922 100644 --- a/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutPaymentHandler.ts +++ b/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutPaymentHandler.ts @@ -37,46 +37,46 @@ const WalletCheckoutPaymentHandler = { contractInteraction: SolanaContractInteraction, chainId: string ): Promise { - const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc + const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc - if (!rpc) { - throw new Error(`There is no RPC URL for the provided chain ${chainId}`) - } - const connection = new Connection(rpc) + if (!rpc) { + throw new Error(`There is no RPC URL for the provided chain ${chainId}`) + } + const connection = new Connection(rpc) - // Create a new transaction - const transaction = new Transaction() + // Create a new transaction + const transaction = new Transaction() - const instruction = contractInteraction.data - const accountMetas = instruction.accounts.map(acc => ({ - pubkey: new PublicKey(acc.pubkey), - isSigner: acc.isSigner, - isWritable: acc.isWritable - })) + const instruction = contractInteraction.data + const accountMetas = instruction.accounts.map(acc => ({ + pubkey: new PublicKey(acc.pubkey), + isSigner: acc.isSigner, + isWritable: acc.isWritable + })) - // Create the instruction - const txInstruction = new TransactionInstruction({ - programId: new PublicKey(instruction.programId), - keys: accountMetas, - data: Buffer.from(instruction.data, 'base64') - }) + // Create the instruction + const txInstruction = new TransactionInstruction({ + programId: new PublicKey(instruction.programId), + keys: accountMetas, + data: Buffer.from(instruction.data, 'base64') + }) - // Add to transaction - transaction.add(txInstruction) + // Add to transaction + transaction.add(txInstruction) - // Set the wallet's public key as feePayer - const walletAddress = await wallet.getAddress() - const publicKey = new PublicKey(walletAddress) - transaction.feePayer = publicKey + // Set the wallet's public key as feePayer + const walletAddress = await wallet.getAddress() + const publicKey = new PublicKey(walletAddress) + transaction.feePayer = publicKey - // Get recent blockhash from the connection - const { blockhash } = await connection.getLatestBlockhash('confirmed') - transaction.recentBlockhash = blockhash + // Get recent blockhash from the connection + const { blockhash } = await connection.getLatestBlockhash('confirmed') + transaction.recentBlockhash = blockhash - const txHash = await connection.sendRawTransaction(transaction.serialize()) - await connection.confirmTransaction(txHash, 'confirmed') + const txHash = await connection.sendRawTransaction(transaction.serialize()) + await connection.confirmTransaction(txHash, 'confirmed') - return { txHash } + return { txHash } }, /** * Process any payment type and handle errors @@ -96,7 +96,11 @@ const WalletCheckoutPaymentHandler = { ) } // Contract interaction payment - if (contractInteraction && !recipient && contractInteraction.type === 'solana-instruction') { + if ( + contractInteraction && + !recipient && + contractInteraction.type === 'solana-instruction' + ) { return await this.processSolanaContractInteraction( wallet, contractInteraction as SolanaContractInteraction, @@ -112,7 +116,11 @@ const WalletCheckoutPaymentHandler = { // Handle SOL transfers (slip44:501) if (assetNamespace === 'slip44' && assetReference === '501') { - const txHash = await wallet.sendSol(recipientAddress, `${chainNamespace}:${chainId}`, BigInt(payment.amount)) + const txHash = await wallet.sendSol( + recipientAddress, + `${chainNamespace}:${chainId}`, + BigInt(payment.amount) + ) return { txHash } } @@ -128,7 +136,7 @@ const WalletCheckoutPaymentHandler = { } } } - + // Ensure wallet is an EVM wallet if (!wallet.sendTransaction) { throw new CheckoutError( @@ -168,7 +176,12 @@ const WalletCheckoutPaymentHandler = { } } // Contract interaction payment - if (contractInteraction && !recipient && Array.isArray(contractInteraction.data) && contractInteraction.type === 'evm-calls') { + if ( + contractInteraction && + !recipient && + Array.isArray(contractInteraction.data) && + contractInteraction.type === 'evm-calls' + ) { let lastTxHash = '0x' for (const call of contractInteraction.data) { @@ -186,7 +199,7 @@ const WalletCheckoutPaymentHandler = { } // Neither or both are present - throw new CheckoutError( CheckoutErrorCode.INVALID_CHECKOUT_REQUEST) + throw new CheckoutError(CheckoutErrorCode.INVALID_CHECKOUT_REQUEST) } catch (error) { console.error('Payment processing error:', error)