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/public/chain-logos/chain-placeholder.png b/advanced/wallets/react-wallet-v2/public/chain-logos/chain-placeholder.png
new file mode 100644
index 000000000..12975dfa5
Binary files /dev/null and b/advanced/wallets/react-wallet-v2/public/chain-logos/chain-placeholder.png differ
diff --git a/advanced/wallets/react-wallet-v2/public/chain-logos/chain-placeholder.svg b/advanced/wallets/react-wallet-v2/public/chain-logos/chain-placeholder.svg
deleted file mode 100644
index ada9bf6a4..000000000
--- a/advanced/wallets/react-wallet-v2/public/chain-logos/chain-placeholder.svg
+++ /dev/null
@@ -1,14 +0,0 @@
-
\ No newline at end of file
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 000000000..0ba7236a9
Binary files /dev/null and b/advanced/wallets/react-wallet-v2/public/token-logos/SOL.png differ
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 000000000..466fa52f3
Binary files /dev/null and b/advanced/wallets/react-wallet-v2/public/token-logos/token-placeholder.png differ
diff --git a/advanced/wallets/react-wallet-v2/public/token-logos/token-placeholder.svg b/advanced/wallets/react-wallet-v2/public/token-logos/token-placeholder.svg
deleted file mode 100644
index bfecbba6c..000000000
--- a/advanced/wallets/react-wallet-v2/public/token-logos/token-placeholder.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
\ 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/data/SolanaData.ts b/advanced/wallets/react-wallet-v2/src/data/SolanaData.ts
index 86711c328..c8066ab0c 100644
--- a/advanced/wallets/react-wallet-v2/src/data/SolanaData.ts
+++ b/advanced/wallets/react-wallet-v2/src/data/SolanaData.ts
@@ -63,5 +63,6 @@ export const SOLANA_SIGNING_METHODS = {
SOLANA_SIGN_TRANSACTION: 'solana_signTransaction',
SOLANA_SIGN_MESSAGE: 'solana_signMessage',
SOLANA_SIGN_AND_SEND_TRANSACTION: 'solana_signAndSendTransaction',
- SOLANA_SIGN_ALL_TRANSACTIONS: 'solana_signAllTransactions'
+ SOLANA_SIGN_ALL_TRANSACTIONS: 'solana_signAllTransactions',
+ SOLANA_WALLET_CHECKOUT: 'wallet_checkout'
}
diff --git a/advanced/wallets/react-wallet-v2/src/data/tokenUtil.ts b/advanced/wallets/react-wallet-v2/src/data/tokenUtil.ts
index 461f1f991..d64234eaa 100644
--- a/advanced/wallets/react-wallet-v2/src/data/tokenUtil.ts
+++ b/advanced/wallets/react-wallet-v2/src/data/tokenUtil.ts
@@ -1,7 +1,7 @@
export type EIP155Token = {
name: string
icon: string
- address?: string
+ assetAddress?: string
symbol: string
decimals: number
}
@@ -24,9 +24,31 @@ const ALL_TOKENS: EIP155Token[] = [
icon: '/token-logos/ETH.png',
symbol: 'ETH',
decimals: 18
+ },
+ {
+ name: 'SOL',
+ icon: '/token-logos/SOL.png',
+ symbol: 'SOL',
+ decimals: 9
}
]
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'
+ ]
+ }
+]
+
+export function getSolanaTokenData(caip19AssetAddress: string) {
+ return SOLANA_KNOWN_TOKENS.find(token => token.assetAddress.includes(caip19AssetAddress))
+}
diff --git a/advanced/wallets/react-wallet-v2/src/hooks/useWalletConnectEventsManager.ts b/advanced/wallets/react-wallet-v2/src/hooks/useWalletConnectEventsManager.ts
index e9d9f00d2..28bfdd32d 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) {
/******************************************************************************
@@ -95,10 +97,18 @@ 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) {
+ // 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/lib/SolanaLib.ts b/advanced/wallets/react-wallet-v2/src/lib/SolanaLib.ts
index 5999ee5fb..c57487244 100644
--- a/advanced/wallets/react-wallet-v2/src/lib/SolanaLib.ts
+++ b/advanced/wallets/react-wallet-v2/src/lib/SolanaLib.ts
@@ -1,6 +1,20 @@
-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 +115,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 +126,124 @@ 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 })
+ 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
+ }
+
+ /**
+ * 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 {
+ 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
+ }
}
export namespace SolanaLib {
diff --git a/advanced/wallets/react-wallet-v2/src/schema/WalletCheckoutSchema.ts b/advanced/wallets/react-wallet-v2/src/schema/WalletCheckoutSchema.ts
index 06cca5441..3af464aa4 100644
--- a/advanced/wallets/react-wallet-v2/src/schema/WalletCheckoutSchema.ts
+++ b/advanced/wallets/react-wallet-v2/src/schema/WalletCheckoutSchema.ts
@@ -1,63 +1,16 @@
-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 +26,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 +35,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 +45,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
- return (
- typeof contractInteraction === 'object' &&
- typeof contractInteraction.type === 'string' &&
- contractInteraction.type.trim() !== '' &&
- typeof contractInteraction.data === 'object' &&
- contractInteraction.data !== null
- )
+ // 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
+ }
+}
+
+/**
+ * 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 +105,257 @@ 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] !== '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]}`
+ )
+ }
+}
+
+/**
+ * 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()
+})
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..877420fe3 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,25 @@ 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
diff --git a/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts b/advanced/wallets/react-wallet-v2/src/utils/PaymentValidatorUtil.ts
index bf4eb70dd..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,28 @@
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 { getTokenData } from '@/data/tokenUtil'
+import { getSolanaTokenData, getTokenData } from '@/data/tokenUtil'
import { getChainById } from './ChainUtil'
-import { EIP155_CHAINS } from '@/data/EIP155Data'
-
+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
*/
@@ -22,8 +38,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
@@ -77,24 +93,19 @@ 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 solana token
+ return ['erc20', 'slip44', 'token'].includes(assetNamespace)
}
- /**
- * 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
- */
+ // methods to get token details
+
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({
@@ -109,14 +120,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,
@@ -124,7 +127,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({
@@ -148,15 +151,110 @@ 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
@@ -165,123 +263,160 @@ export class PaymentValidationUtils {
return false
}
+ 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 {
- 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
+ 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
}
}
-
- /**
- * 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,
+ private static async simulateSolanaNativeTransfer(params: {
+ account: 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}`
- )
+ chainId: string
+ }) {
+ try {
+ const { account, recipientAddress, amount, chainId } = params
+ const rpc = { ...SOLANA_TEST_CHAINS, ...SOLANA_MAINNET_CHAINS }[chainId]?.rpc
+ if (!rpc) {
+ return false
+ }
- // 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
+ }
+
+ const toTokenAccountAddress = await getAssociatedTokenAddress(mintAddress, toPubkey)
+ const recipientTokenAccount = await connection.getAccountInfo(toTokenAccountAddress)
- // Get native token details
- const tokenDetails = canTransactionSucceed
- ? await PaymentValidationUtils.getNativeAssetDetails(
- Number(chainId),
- senderAccount as `0x${string}`
+ // Create transaction
+ const transaction = new Transaction()
+
+ // Add instruction to create recipient token account if needed
+ if (!recipientTokenAccount) {
+ const createAccountInstruction = createAssociatedTokenAccountInstruction(
+ fromPubkey,
+ toTokenAccountAddress,
+ toPubkey,
+ mintAddress
)
- : { decimals: 18, symbol: 'ETH', name: 'Ethereum', balance: BigInt(0) }
+ 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,
@@ -310,17 +445,7 @@ 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 getDetailedDirectPaymentOption(
- payment: PaymentOption,
- account: string
- ): Promise<{
+ private static async getDetailedDirectPaymentOption(payment: PaymentOption): Promise<{
validatedPayment: DetailedPaymentOption | null
hasMatchingAsset: boolean
}> {
@@ -340,61 +465,190 @@ export class PaymentValidationUtils {
return { validatedPayment: null, hasMatchingAsset: false }
}
- let validationResult
+ 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 }
+ }
- // 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(
+ return result
+ } catch (error) {
+ console.error('Error validating payment option:', error)
+ return { validatedPayment: null, hasMatchingAsset: false }
+ }
+ }
+
+ private static async processSolanaDirectPayment(
+ payment: PaymentOption,
+ recipientAddress: string,
+ chainId: string,
+ assetAddress: string,
+ assetNamespace: string
+ ): Promise<{
+ validatedPayment: DetailedPaymentOption | null
+ hasMatchingAsset: boolean
+ }> {
+ 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 }
+ }
+
+ // 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,
- account,
- recipientAddress,
- payment.amount
+ 'solana'
)
- }
+ : null
- // Check if user has the asset (balance > 0)
- const hasMatchingAsset = validationResult.tokenDetails.balance > BigInt(0)
+ return { validatedPayment: detailedPayment, hasMatchingAsset: true }
+ }
- if (!validationResult.isValid) {
- return { validatedPayment: null, hasMatchingAsset }
- }
+ private static async processEvmDirectPayment(
+ payment: PaymentOption,
+ recipientAddress: string,
+ chainId: string,
+ assetAddress: string,
+ assetNamespace: string
+ ): Promise<{
+ validatedPayment: DetailedPaymentOption | null
+ hasMatchingAsset: boolean
+ }> {
+ const account = SettingsStore.state.eip155Address as `0x${string}`
+ let tokenDetails: TokenDetails | undefined
+ let simulationResult: boolean | undefined
- // Create detailed payment option with metadata
- const detailedPayment = PaymentValidationUtils.createDetailedPaymentOption(
- payment,
- validationResult.tokenDetails,
- assetNamespace,
+ 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,
+ 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,
- chainNamespace
+ 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 }
+ }
- return { validatedPayment: detailedPayment, hasMatchingAsset: true }
- } catch (error) {
- console.error('Error validating payment option:', error)
+ // 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 getDetailedContractPaymentOption(
- payment: PaymentOption,
- account: string
- ): Promise<{
+ private static async getDetailedContractPaymentOption(payment: PaymentOption): Promise<{
validatedPayment: DetailedPaymentOption | null
hasMatchingAsset: boolean
}> {
@@ -409,121 +663,195 @@ export class PaymentValidationUtils {
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}`
- )
+ 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 }
}
- // 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
- }
+ return result
} catch (error) {
console.error('Error validating contract payment option:', error)
return { validatedPayment: null, hasMatchingAsset: false }
}
}
- /**
- * 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 processSolanaContractPayment(
+ payment: PaymentOption,
+ chainId: string,
+ assetAddress: string,
+ assetNamespace: string,
+ contractInteraction: any
+ ): Promise<{
+ validatedPayment: DetailedPaymentOption | null
+ hasMatchingAsset: boolean
}> {
- let isUserHaveAtleastOneMatchingAssets = false
- const account = SettingsStore.state.eip155Address
+ const account = SettingsStore.state.solanaAddress
+ let tokenDetails: TokenDetails | undefined
+ let isValid = false
- // Validate each payment option
- const results = await Promise.all(
- directPayments.map(payment =>
- PaymentValidationUtils.getDetailedDirectPaymentOption(payment, account)
+ 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}`)
+ } else if (assetNamespace === 'token') {
+ // SPL token
+ tokenDetails = await this.getSplTokenDetails(
+ assetAddress,
+ account,
+ `solana:${chainId}`,
+ payment.asset
)
- )
+ } else {
+ return { validatedPayment: null, hasMatchingAsset: false }
+ }
- // Collect results
- const feasibleDirectPayments: DetailedPaymentOption[] = []
+ // Check if token details were assigned
+ if (!tokenDetails) {
+ return { validatedPayment: null, hasMatchingAsset: false }
+ }
- for (const result of results) {
- if (result.hasMatchingAsset) {
- isUserHaveAtleastOneMatchingAssets = true
- }
+ // Check if user has the asset (balance > 0)
+ const hasMatchingAsset = tokenDetails.balance > BigInt(0)
- if (result.validatedPayment) {
- feasibleDirectPayments.push(result.validatedPayment)
- }
+ // Create detailed payment option with metadata
+ const detailedPayment = isValid
+ ? PaymentValidationUtils.createDetailedPaymentOption(
+ payment,
+ tokenDetails,
+ assetNamespace,
+ chainId,
+ 'solana'
+ )
+ : null
+ return { validatedPayment: detailedPayment, hasMatchingAsset }
+ }
+
+ private static async processEvmContractPayment(
+ payment: PaymentOption,
+ chainId: string,
+ assetAddress: string,
+ assetNamespace: string,
+ contractInteraction: any
+ ): Promise<{
+ validatedPayment: DetailedPaymentOption | null
+ hasMatchingAsset: boolean
+ }> {
+ 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 account = SettingsStore.state.eip155Address
- // Validate each contract payment option
const results = await Promise.all(
- contractPayments.map(payment =>
- PaymentValidationUtils.getDetailedContractPaymentOption(payment, account)
- )
+ 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) {
@@ -531,12 +859,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..67a5a1c66 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,14 +62,48 @@ 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
+ }
}
}
diff --git a/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutPaymentHandler.ts b/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutPaymentHandler.ts
index 21091965b..f59b51922 100644
--- a/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutPaymentHandler.ts
+++ b/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutPaymentHandler.ts
@@ -1,8 +1,16 @@
import { encodeFunctionData } from 'viem'
import { erc20Abi } from 'viem'
-
-import { DetailedPaymentOption, CheckoutErrorCode, CheckoutError } from '@/types/wallet_checkout'
-import { Wallet } from 'ethers'
+import { Connection, PublicKey, Transaction, TransactionInstruction } from '@solana/web3.js'
+import { Buffer } from 'buffer'
+
+import {
+ DetailedPaymentOption,
+ CheckoutErrorCode,
+ CheckoutError,
+ SolanaContractInteraction
+} from '@/types/wallet_checkout'
+import { SOLANA_MAINNET_CHAINS } from '@/data/SolanaData'
+import { SOLANA_TEST_CHAINS } from '@/data/SolanaData'
export interface PaymentResult {
txHash: string
@@ -21,13 +29,121 @@ const WalletCheckoutPaymentHandler = {
}
},
+ /**
+ * Process a Solana contract interaction
+ */
+ async processSolanaContractInteraction(
+ wallet: any,
+ contractInteraction: SolanaContractInteraction,
+ chainId: string
+ ): Promise {
+ 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
+
+ const txHash = await connection.sendRawTransaction(transaction.serialize())
+ await connection.confirmTransaction(txHash, 'confirmed')
+
+ return { txHash }
+ },
/**
* 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 &&
+ 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]
+ 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') {
+ const txHash = await wallet.sendSol(
+ recipientAddress,
+ `${chainNamespace}:${chainId}`,
+ BigInt(payment.amount)
+ )
+ return { txHash }
+ }
+
+ // Handle SPL token transfers (token:)
+ if (assetNamespace === 'token') {
+ const txHash = await wallet.sendSplToken(
+ assetReference,
+ recipientAddress,
+ `${chainNamespace}:${chainId}`,
+ BigInt(payment.amount)
+ )
+ return { txHash }
+ }
+ }
+ }
+ // 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) {
const { asset, amount, assetMetadata } = payment
@@ -58,37 +174,32 @@ 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
- }
-
- return { txHash: lastTxHash }
+ if (
+ contractInteraction &&
+ !recipient &&
+ 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
}
- 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)
diff --git a/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutUtil.ts b/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutUtil.ts
index 0136887ed..f32d83769 100644
--- a/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutUtil.ts
+++ b/advanced/wallets/react-wallet-v2/src/utils/WalletCheckoutUtil.ts
@@ -11,47 +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
* Shortens the address with ellipsis for better display
@@ -92,29 +51,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(
@@ -126,47 +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 => {
- // Skip if payment is undefined or null
- 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
@@ -174,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
diff --git a/advanced/wallets/react-wallet-v2/src/views/SessionCheckoutModal.tsx b/advanced/wallets/react-wallet-v2/src/views/SessionCheckoutModal.tsx
index 22a6c46de..7bb354d51 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,34 @@ 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 +135,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) {
@@ -139,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')
@@ -154,7 +168,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/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 =>
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"