From 69484abfdc3284902dd7aba74023a4b7fc899936 Mon Sep 17 00:00:00 2001 From: Spencer Stock Date: Thu, 13 Nov 2025 14:09:43 -0500 Subject: [PATCH 01/12] Add sdkConfig param to pay and subscribe functions - Add PaymentSDKConfig type for advanced SDK configuration - Remove redundant walletUrl from PaymentOptions and SubscriptionOptions - Update pay() and subscribe() to accept sdkConfig parameter - Update createEphemeralSDK to merge sdkConfig with defaults - Update executePaymentWithSDK signature to accept sdkConfig - Add test coverage for sdkConfig parameter - Fix existing tests to match new function signatures --- .../src/interface/payment/pay.test.ts | 51 +++++++++++++++++-- .../account-sdk/src/interface/payment/pay.ts | 6 +-- .../src/interface/payment/types.ts | 34 ++++++++++++- .../src/interface/payment/utils/sdkManager.ts | 33 ++++++++---- 4 files changed, 105 insertions(+), 19 deletions(-) diff --git a/packages/account-sdk/src/interface/payment/pay.test.ts b/packages/account-sdk/src/interface/payment/pay.test.ts index 42212d25f..c4da7a3d8 100644 --- a/packages/account-sdk/src/interface/payment/pay.test.ts +++ b/packages/account-sdk/src/interface/payment/pay.test.ts @@ -338,8 +338,8 @@ describe('pay', () => { expect(sdkManager.executePaymentWithSDK).toHaveBeenCalledWith( expect.any(Object), false, - undefined, - false + false, + undefined ); }); @@ -386,8 +386,8 @@ describe('pay', () => { }), }), true, - undefined, - true + true, + undefined ); }); @@ -550,4 +550,47 @@ describe('pay', () => { errorMessage: 'Unknown error occurred', }); }); + + it('should pass sdkConfig to executePaymentWithSDK', async () => { + // Setup mocks + vi.mocked(validation.validateStringAmount).mockReturnValue(undefined); + vi.mocked(translatePayment.translatePaymentToSendCalls).mockReturnValue({ + version: '2.0.0', + chainId: 8453, + calls: [ + { + to: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', + data: '0xabcdef', + value: '0x0', + }, + ], + capabilities: {}, + }); + vi.mocked(sdkManager.executePaymentWithSDK).mockResolvedValue({ + transactionHash: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', + }); + + const sdkConfig = { + preference: { + mode: 'embedded' as const, + attribution: { auto: true }, + }, + appName: 'Test App', + }; + + await pay({ + amount: '10.50', + to: '0xFe21034794A5a574B94fE4fDfD16e005F1C96e51', + testnet: false, + sdkConfig, + }); + + // Verify sdkConfig was passed to executePaymentWithSDK + expect(sdkManager.executePaymentWithSDK).toHaveBeenCalledWith( + expect.any(Object), + false, + true, + sdkConfig + ); + }); }); diff --git a/packages/account-sdk/src/interface/payment/pay.ts b/packages/account-sdk/src/interface/payment/pay.ts index 2008ac585..92caa5755 100644 --- a/packages/account-sdk/src/interface/payment/pay.ts +++ b/packages/account-sdk/src/interface/payment/pay.ts @@ -35,7 +35,7 @@ import { normalizeAddress, validateStringAmount } from './utils/validation.js'; * ``` */ export async function pay(options: PaymentOptions): Promise { - const { amount, to, testnet = false, payerInfo, walletUrl, telemetry = true } = options; + const { amount, to, testnet = false, payerInfo, telemetry = true, sdkConfig } = options; // Generate correlation ID for this payment request const correlationId = crypto.randomUUID(); @@ -61,8 +61,8 @@ export async function pay(options: PaymentOptions): Promise { const executionResult = await executePaymentWithSDK( requestParams, testnet, - walletUrl, - telemetry + telemetry, + sdkConfig ); // Log payment completed diff --git a/packages/account-sdk/src/interface/payment/types.ts b/packages/account-sdk/src/interface/payment/types.ts index af021d9c2..f75d6006c 100644 --- a/packages/account-sdk/src/interface/payment/types.ts +++ b/packages/account-sdk/src/interface/payment/types.ts @@ -53,6 +53,37 @@ export interface PayerInfo { callbackURL?: string; } +/** + * SDK configuration options for payment + * @internal Undocumented parameter for advanced SDK configuration + */ +export interface PaymentSDKConfig { + /** Optional preference settings (mode, attribution, etc.) */ + preference?: { + /** Mode for the SDK: 'embedded' for iframe, 'popup' for new window */ + mode?: 'embedded' | 'popup'; + /** Attribution configuration */ + attribution?: + | { + auto: boolean; + } + | { + dataSuffix: `0x${string}`; + }; + /** Wallet URL override */ + walletUrl?: string; + /** Enable/disable telemetry */ + telemetry?: boolean; + [key: string]: unknown; + }; + /** App metadata overrides */ + appName?: string; + appLogoUrl?: string; + appChainIds?: number[]; + /** Paymaster URLs by chain ID */ + paymasterUrls?: Record; +} + /** * Options for making a payment */ @@ -65,9 +96,10 @@ export interface PaymentOptions { testnet?: boolean; /** Optional payer information configuration for data callbacks */ payerInfo?: PayerInfo; - walletUrl?: string; /** Whether to enable telemetry logging. Defaults to true */ telemetry?: boolean; + /** @internal Advanced SDK configuration (undocumented) */ + sdkConfig?: PaymentSDKConfig; } /** diff --git a/packages/account-sdk/src/interface/payment/utils/sdkManager.ts b/packages/account-sdk/src/interface/payment/utils/sdkManager.ts index d020d8d83..f1db27100 100644 --- a/packages/account-sdk/src/interface/payment/utils/sdkManager.ts +++ b/packages/account-sdk/src/interface/payment/utils/sdkManager.ts @@ -1,7 +1,7 @@ import type { Hex } from 'viem'; import { createBaseAccountSDK } from '../../builder/core/createBaseAccountSDK.js'; import { CHAIN_IDS } from '../constants.js'; -import type { PayerInfoResponses } from '../types.js'; +import type { PayerInfoResponses, PaymentSDKConfig } from '../types.js'; /** * Type for wallet_sendCalls request parameters @@ -39,20 +39,31 @@ export interface PaymentExecutionResult { /** * Creates an ephemeral SDK instance configured for payments * @param chainId - The chain ID to use - * @param walletUrl - Optional wallet URL to use * @param telemetry - Whether to enable telemetry (defaults to true) + * @param sdkConfig - Optional advanced SDK configuration * @returns The configured SDK instance */ -export function createEphemeralSDK(chainId: number, walletUrl?: string, telemetry: boolean = true) { +export function createEphemeralSDK( + chainId: number, + telemetry: boolean = true, + sdkConfig?: PaymentSDKConfig +) { const appName = typeof window !== 'undefined' ? window.location.origin : 'Base Pay SDK'; + // Merge sdkConfig with default settings + // sdkConfig takes precedence over individual parameters const sdk = createBaseAccountSDK({ - appName: appName, - appChainIds: [chainId], + appName: sdkConfig?.appName || appName, + appLogoUrl: sdkConfig?.appLogoUrl, + appChainIds: sdkConfig?.appChainIds || [chainId], preference: { - telemetry: telemetry, - walletUrl, + telemetry: sdkConfig?.preference?.telemetry ?? telemetry, + walletUrl: sdkConfig?.preference?.walletUrl, + mode: sdkConfig?.preference?.mode, + attribution: sdkConfig?.preference?.attribution, + ...sdkConfig?.preference, }, + paymasterUrls: sdkConfig?.paymasterUrls, }); // Chain clients will be automatically created when needed by getClient @@ -116,20 +127,20 @@ export async function executePayment( * Manages the complete payment flow with SDK lifecycle * @param requestParams - The wallet_sendCalls request parameters * @param testnet - Whether to use testnet - * @param walletUrl - Optional wallet URL to use * @param telemetry - Whether to enable telemetry (defaults to true) + * @param sdkConfig - Optional advanced SDK configuration * @returns The payment execution result */ export async function executePaymentWithSDK( requestParams: WalletSendCallsRequestParams, testnet: boolean, - walletUrl?: string, - telemetry: boolean = true + telemetry: boolean = true, + sdkConfig?: PaymentSDKConfig ): Promise { const network = testnet ? 'baseSepolia' : 'base'; const chainId = CHAIN_IDS[network]; - const sdk = createEphemeralSDK(chainId, walletUrl, telemetry); + const sdk = createEphemeralSDK(chainId, telemetry, sdkConfig); const provider = sdk.getProvider(); try { From d913fc4493d534653163f0f7afd57e1beec04fbc Mon Sep 17 00:00:00 2001 From: Spencer Stock Date: Mon, 24 Nov 2025 15:51:35 -0700 Subject: [PATCH 02/12] Add payWithToken functionality for token-based payments --- .../add-sub-account/components/SendCalls.tsx | 215 ++++-- .../components/CodeEditor.module.css | 52 ++ .../pay-playground/components/CodeEditor.tsx | 113 ++- .../pay-playground/components/Output.tsx | 194 ++++- .../pages/pay-playground/constants/index.ts | 5 + .../pay-playground/constants/playground.ts | 707 ++++++++++++++++++ .../pay-playground/hooks/useCodeExecution.ts | 21 +- .../src/pages/pay-playground/index.page.tsx | 114 +++ .../pay-playground/utils/codeSanitizer.ts | 5 +- .../src/pages/pay-playground/utils/index.ts | 2 + .../utils/payerInfoTransform.ts | 89 +++ .../utils/paymasterTransform.ts | 71 ++ packages/account-sdk/src/browser-entry.ts | 7 +- .../src/core/telemetry/events/payment.ts | 75 ++ packages/account-sdk/src/index.ts | 12 + .../src/interface/payment/README.md | 58 +- .../src/interface/payment/constants.ts | 121 ++- .../payment/getPaymentStatus.test.ts | 51 +- .../src/interface/payment/getPaymentStatus.ts | 200 +++-- .../src/interface/payment/index.ts | 6 + .../interface/payment/payWithToken.test.ts | 135 ++++ .../src/interface/payment/payWithToken.ts | 117 +++ .../src/interface/payment/types.ts | 83 +- .../interface/payment/utils/erc3770.test.ts | 69 ++ .../src/interface/payment/utils/erc3770.ts | 153 ++++ .../src/interface/payment/utils/sdkManager.ts | 24 +- .../interface/payment/utils/tokenRegistry.ts | 90 +++ .../payment/utils/translateTokenPayment.ts | 102 +++ .../src/interface/payment/utils/validation.ts | 68 ++ 29 files changed, 2792 insertions(+), 167 deletions(-) create mode 100644 examples/testapp/src/pages/pay-playground/utils/payerInfoTransform.ts create mode 100644 examples/testapp/src/pages/pay-playground/utils/paymasterTransform.ts create mode 100644 packages/account-sdk/src/interface/payment/payWithToken.test.ts create mode 100644 packages/account-sdk/src/interface/payment/payWithToken.ts create mode 100644 packages/account-sdk/src/interface/payment/utils/erc3770.test.ts create mode 100644 packages/account-sdk/src/interface/payment/utils/erc3770.ts create mode 100644 packages/account-sdk/src/interface/payment/utils/tokenRegistry.ts create mode 100644 packages/account-sdk/src/interface/payment/utils/translateTokenPayment.ts diff --git a/examples/testapp/src/pages/add-sub-account/components/SendCalls.tsx b/examples/testapp/src/pages/add-sub-account/components/SendCalls.tsx index af8d1c2f5..e25454267 100644 --- a/examples/testapp/src/pages/add-sub-account/components/SendCalls.tsx +++ b/examples/testapp/src/pages/add-sub-account/components/SendCalls.tsx @@ -1,8 +1,36 @@ -import { createBaseAccountSDK } from '@base-org/account'; -import { Box, Button } from '@chakra-ui/react'; +import type { TokenPaymentSuccess } from '@base-org/account'; +import { createBaseAccountSDK, payWithToken } from '@base-org/account'; +import { CheckCircleIcon, ExternalLinkIcon } from '@chakra-ui/icons'; +import { Box, Button, Flex, HStack, Icon, Link, Text, VStack } from '@chakra-ui/react'; import { useCallback, useState } from 'react'; -import { numberToHex } from 'viem'; -import { baseSepolia } from 'viem/chains'; +import { formatUnits } from 'viem'; + +// Common token symbols and decimals +const TOKEN_CONFIG: Record = { + '0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4': { symbol: 'USDC', decimals: 6 }, // Base USDC + '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913': { symbol: 'USDC', decimals: 6 }, // Base mainnet USDC +}; + +function getTokenInfo(tokenAddress: string) { + const lowerAddress = tokenAddress.toLowerCase(); + return TOKEN_CONFIG[lowerAddress] || { symbol: 'Token', decimals: 18 }; +} + +function stripChainPrefix(txHash: string): string { + // Remove chain prefix if present (e.g., "base:0x..." -> "0x...") + return txHash.includes(':') ? txHash.split(':')[1] : txHash; +} + +function getBlockExplorerUrl(chainId: number, txHash: string): string { + const hash = stripChainPrefix(txHash); + + const explorers: Record = { + 8453: 'https://basescan.org/tx', // Base mainnet + 84532: 'https://sepolia.basescan.org/tx', // Base Sepolia + }; + + return `${explorers[chainId] || 'https://basescan.org/tx'}/${hash}`; +} export function SendCalls({ sdk, @@ -11,48 +39,45 @@ export function SendCalls({ sdk: ReturnType; subAccountAddress: string; }) { - const [state, setState] = useState(); - const handleSendCalls = useCallback(async () => { - if (!sdk) { - return; - } + const [paymentResult, setPaymentResult] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const handlePayWithToken = useCallback(async () => { + setIsLoading(true); + setError(null); + setPaymentResult(null); - const provider = sdk.getProvider(); try { - const response = await provider.request({ - method: 'wallet_sendCalls', - params: [ - { - chainId: numberToHex(baseSepolia.id), - from: subAccountAddress, - calls: [ - { - to: '0x000000000000000000000000000000000000dead', - data: '0x', - value: '0x0', - }, - ], - version: '1', - capabilities: { - paymasterService: { - url: 'https://api.developer.coinbase.com/rpc/v1/base-sepolia/S-fOd2n2Oi4fl4e1Crm83XeDXZ7tkg8O', - }, - }, - }, - ], + // Example payment with token + const result = await payWithToken({ + amount: '100000000000000000000', // 100 tokens (18 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + token: '0xAC1Bd2486aAf3B5C0fc3Fd868558b082a531B2B4', // USDC on Base Sepolia + chainId: 8453, + paymaster: { + url: 'https://api.developer.coinbase.com/rpc/v1/base/S-fOd2n2Oi4fl4e1Crm83XeDXZ7tkg8O', + }, }); - console.info('response', response); - setState(response as string); + + if (result.success) { + setPaymentResult(result); + } } catch (e) { - console.error('error', e); + console.error('Payment error:', e); + setError(e instanceof Error ? e.message : 'Payment failed'); + } finally { + setIsLoading(false); } - }, [sdk, subAccountAddress]); + }, []); return ( - <> + - {state && ( + + {error && ( - {JSON.stringify(state, null, 2)} + + + ❌ Payment Failed + + + + {error} + + + )} + + {paymentResult && ( + + + + + Payment Successful! + + + + + {/* Amount */} + + + Amount + + + {formatUnits( + BigInt(paymentResult.tokenAmount), + getTokenInfo(paymentResult.tokenAddress).decimals + )}{' '} + {paymentResult.token || getTokenInfo(paymentResult.tokenAddress).symbol} + + + + {/* Recipient */} + + + Recipient + + + + {paymentResult.to} + + + + + {/* Transaction ID */} + + + Transaction ID + + + + {stripChainPrefix(paymentResult.id)} + + + + View on Block Explorer + + + + )} - + ); } diff --git a/examples/testapp/src/pages/pay-playground/components/CodeEditor.module.css b/examples/testapp/src/pages/pay-playground/components/CodeEditor.module.css index 013d1fef6..b0e4eec19 100644 --- a/examples/testapp/src/pages/pay-playground/components/CodeEditor.module.css +++ b/examples/testapp/src/pages/pay-playground/components/CodeEditor.module.css @@ -32,6 +32,58 @@ color: #64748b; } +.presetContainer { + padding: 1rem 1.5rem; + border-bottom: 1px solid #e2e8f0; + background: #f8fafc; + display: flex; + align-items: center; + gap: 0.75rem; +} + +.presetLabel { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.875rem; + font-weight: 500; + color: #475569; + white-space: nowrap; +} + +.presetIcon { + width: 16px; + height: 16px; + color: #64748b; +} + +.presetSelect { + flex: 1; + padding: 0.5rem 0.75rem; + font-size: 0.875rem; + color: #0f172a; + background: white; + border: 1px solid #cbd5e1; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s; +} + +.presetSelect:hover:not(:disabled) { + border-color: #94a3b8; +} + +.presetSelect:focus { + outline: none; + border-color: #0052ff; + box-shadow: 0 0 0 3px rgba(0, 82, 255, 0.1); +} + +.presetSelect:disabled { + opacity: 0.5; + cursor: not-allowed; +} + .checkboxContainer { padding: 1rem 1.5rem; border-bottom: 1px solid #e2e8f0; diff --git a/examples/testapp/src/pages/pay-playground/components/CodeEditor.tsx b/examples/testapp/src/pages/pay-playground/components/CodeEditor.tsx index cf0a0c8d8..166c230e0 100644 --- a/examples/testapp/src/pages/pay-playground/components/CodeEditor.tsx +++ b/examples/testapp/src/pages/pay-playground/components/CodeEditor.tsx @@ -1,5 +1,12 @@ +import React from 'react'; import styles from './CodeEditor.module.css'; +export interface Preset { + name: string; + description: string; + code: string; +} + interface CodeEditorProps { code: string; onChange: (code: string) => void; @@ -9,6 +16,12 @@ interface CodeEditorProps { includePayerInfo: boolean; onPayerInfoToggle: (checked: boolean) => void; showPayerInfoToggle?: boolean; + presets?: Preset[]; + transformPresetCode?: (code: string, includePayerInfo: boolean) => string; + paymasterUrl?: string; + onPaymasterUrlChange?: (url: string) => void; + showPaymasterUrl?: boolean; + onPresetChange?: (code: string) => void; } export const CodeEditor = ({ @@ -20,7 +33,42 @@ export const CodeEditor = ({ includePayerInfo, onPayerInfoToggle, showPayerInfoToggle = true, + presets, + transformPresetCode, + paymasterUrl, + onPaymasterUrlChange, + showPaymasterUrl = false, + onPresetChange, }: CodeEditorProps) => { + const presetSelectRef = React.useRef(null); + + const handlePresetChange = (e: React.ChangeEvent) => { + const selectedPreset = presets?.find((p) => p.name === e.target.value); + if (selectedPreset) { + let presetCode = selectedPreset.code; + // Apply payerInfo transformation if toggle is enabled and transform function is provided + if (includePayerInfo && transformPresetCode) { + presetCode = transformPresetCode(presetCode, includePayerInfo); + } + // Use onPresetChange if provided, otherwise use onChange + // onPresetChange will apply the current paymasterUrl to the preset code + if (onPresetChange) { + onPresetChange(presetCode); + } else { + onChange(presetCode); + } + // Presets don't control payer info toggle - it's independent + // Paymaster URL stays intact when switching presets + } + }; + + const handleReset = () => { + if (presetSelectRef.current) { + presetSelectRef.current.value = ''; + } + onReset(); + }; + return (
@@ -40,7 +88,7 @@ export const CodeEditor = ({ Code Editor -
+ {showPaymasterUrl && ( +
+ + onPaymasterUrlChange?.(e.target.value)} + disabled={isLoading} + className={styles.presetSelect} + /> +
+ )} + + {presets && presets.length > 0 && ( +
+ + +
+ )} + {showPayerInfoToggle && (
)} + {result && isTokenPaymentResult(result) && ( +
+
+ {result.success ? ( + <> + + + + + Payment Successful! + + ) : ( + <> + + + + + + Payment Failed + + )} +
+ +
+
+ Amount + + {formatUnits( + BigInt(result.tokenAmount), + getTokenInfo(result.tokenAddress).decimals + )}{' '} + {result.token || getTokenInfo(result.tokenAddress).symbol} + +
+
+ Recipient + {result.to} +
+ {result.success && result.id && ( +
+ Transaction ID + {stripChainPrefix(result.id)} +
+ )} +
+ + {result.success && result.payerInfoResponses && ( +
+
+ + + + + User Info +
+
+ {result.payerInfoResponses.name && ( +
+ Name + + {(() => { + const name = result.payerInfoResponses.name as unknown as { + firstName: string; + familyName: string; + }; + return `${name.firstName} ${name.familyName}`; + })()} + +
+ )} + {result.payerInfoResponses.email && ( +
+ Email + + {result.payerInfoResponses.email} + +
+ )} + {result.payerInfoResponses.phoneNumber && ( +
+ Phone + + {result.payerInfoResponses.phoneNumber.number} ( + {result.payerInfoResponses.phoneNumber.country}) + +
+ )} + {result.payerInfoResponses.physicalAddress && ( +
+ Address + + {(() => { + const addr = result.payerInfoResponses.physicalAddress as unknown as { + address1: string; + address2?: string; + city: string; + state: string; + postalCode: string; + countryCode: string; + name?: { + firstName: string; + familyName: string; + }; + }; + const parts = [ + addr.name ? `${addr.name.firstName} ${addr.name.familyName}` : null, + addr.address1, + addr.address2, + `${addr.city}, ${addr.state} ${addr.postalCode}`, + addr.countryCode, + ].filter(Boolean); + return parts.join(', '); + })()} + +
+ )} + {result.payerInfoResponses.onchainAddress && ( +
+ On-chain Address + + {result.payerInfoResponses.onchainAddress} + +
+ )} +
+
+ )} +
+ )} + {result && isPaymentStatus(result) && (
Transaction ID - {result.id} + {stripChainPrefix(result.id)}
Message diff --git a/examples/testapp/src/pages/pay-playground/constants/index.ts b/examples/testapp/src/pages/pay-playground/constants/index.ts index 9e42f982d..7584ad2d4 100644 --- a/examples/testapp/src/pages/pay-playground/constants/index.ts +++ b/examples/testapp/src/pages/pay-playground/constants/index.ts @@ -1,8 +1,13 @@ export { DEFAULT_GET_PAYMENT_STATUS_CODE, DEFAULT_PAY_CODE, + DEFAULT_PAY_WITH_TOKEN_CODE, GET_PAYMENT_STATUS_QUICK_TIPS, PAY_CODE_WITH_PAYER_INFO, PAY_QUICK_TIPS, + PAY_WITH_TOKEN_CODE_WITH_PAYER_INFO, + PAY_WITH_TOKEN_PRESETS, + PAY_WITH_TOKEN_QUICK_TIPS, QUICK_TIPS, } from './playground'; +export type { PayWithTokenPreset } from './playground'; diff --git a/examples/testapp/src/pages/pay-playground/constants/playground.ts b/examples/testapp/src/pages/pay-playground/constants/playground.ts index 6dc8e54a6..04af86cd5 100644 --- a/examples/testapp/src/pages/pay-playground/constants/playground.ts +++ b/examples/testapp/src/pages/pay-playground/constants/playground.ts @@ -37,6 +37,53 @@ try { throw error; }`; +export const DEFAULT_PAY_WITH_TOKEN_CODE = `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 token (in smallest unit, e.g., 1 USDC = 1000000 for 6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + token: 'USDC', + paymaster: { + url: 'https://paymaster.example.com' + } + // chainId defaults to Base mainnet (8453) if not specified + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`; + +export const PAY_WITH_TOKEN_CODE_WITH_PAYER_INFO = `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 token (in smallest unit, e.g., 1 USDC = 1000000 for 6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + token: 'USDC', + paymaster: { + url: 'https://paymaster.example.com' + }, + payerInfo: { + requests: [ + { type: 'name'}, + { type: 'email' }, + { type: 'phoneNumber', optional: true }, + { type: 'physicalAddress', optional: true }, + { type: 'onchainAddress' } + ] + } + // chainId defaults to Base mainnet (8453) if not specified + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`; + export const DEFAULT_GET_PAYMENT_STATUS_CODE = `import { base } from '@base-org/account' try { @@ -58,6 +105,7 @@ export const PAY_QUICK_TIPS = [ 'Amount is in USDC (e.g., "1" = $1 of USDC)', 'Only USDC on base and base sepolia are supported', 'Use payerInfo to request user information.', + 'Need other ERC20s? Use base.payWithToken with a token and paymaster configuration (amounts are specified in wei). chainId defaults to Base if not specified.', ]; export const GET_PAYMENT_STATUS_QUICK_TIPS = [ @@ -68,4 +116,663 @@ export const GET_PAYMENT_STATUS_QUICK_TIPS = [ 'Make sure to use the same testnet setting as the original payment', ]; +export const PAY_WITH_TOKEN_QUICK_TIPS = [ + 'Amount is specified in the token\'s smallest unit (e.g., wei for ETH, or smallest unit for ERC20 tokens)', + 'For USDC (6 decimals), 1 USDC = 1000000', + 'For tokens with 18 decimals, 1 token = 1000000000000000000', + 'Token can be a contract address or a supported symbol (e.g., "USDC", "WETH")', + 'chainId is optional and defaults to Base mainnet (8453). Specify chainId for other networks.', + 'paymaster.url is required - configure your paymaster service', + 'Use payerInfo to request user information.', + 'Supported tokens vary by chain - check token registry for available options', +]; + +// Preset configurations for payWithToken +export interface PayWithTokenPreset { + name: string; + description: string; + code: string; +} + +export const PAY_WITH_TOKEN_PRESETS: PayWithTokenPreset[] = [ + { + name: 'USDC on Base Mainnet', + description: 'Send 1 USDC on Base mainnet', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDC (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + token: 'USDC', + paymaster: { + url: 'https://paymaster.example.com' + } + // chainId defaults to Base mainnet (8453) + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDC on Base Sepolia', + description: 'Send 1 USDC on Base Sepolia testnet', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDC (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0x14a34', // Base Sepolia (84532) + token: 'USDC', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDC on Optimism Sepolia', + description: 'Send 1 USDC on Optimism Sepolia testnet', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDC (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0xaa37dc', // Optimism Sepolia (11155420) + token: 'USDC', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDC on Arbitrum Sepolia', + description: 'Send 1 USDC on Arbitrum Sepolia testnet', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDC (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0x66eee', // Arbitrum Sepolia (421614) + token: 'USDC', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDC on Polygon Amoy', + description: 'Send 1 USDC on Polygon Amoy testnet', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDC (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0x13882', // Polygon Amoy (80002) + token: 'USDC', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDC on Avalanche Fuji', + description: 'Send 1 USDC on Avalanche Fuji testnet', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDC (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0xa869', // Avalanche Fuji (43113) + token: 'USDC', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDC on BSC Testnet', + description: 'Send 1 USDC on BSC testnet', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDC (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0x61', // BSC Testnet (97) + token: 'USDC', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDC on Ethereum Sepolia', + description: 'Send 1 USDC on Ethereum Sepolia testnet', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDC (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0xaa36a7', // Ethereum Sepolia (11155111) + token: 'USDC', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDT on Base Mainnet', + description: 'Send 1 USDT on Base mainnet', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDT (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + token: 'USDT', + paymaster: { + url: 'https://paymaster.example.com' + } + // chainId defaults to Base mainnet (8453) + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'DAI on Base Mainnet', + description: 'Send 1 DAI on Base mainnet', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000000000000000', // 1 DAI (18 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + token: 'DAI', + paymaster: { + url: 'https://paymaster.example.com' + } + // chainId defaults to Base mainnet (8453) + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDC on Optimism', + description: 'Send 1 USDC on Optimism', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDC (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0xa', // Optimism (10) + token: 'USDC', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDC on Arbitrum', + description: 'Send 1 USDC on Arbitrum', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDC (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0xa4b1', // Arbitrum (42161) + token: 'USDC', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDT on Optimism', + description: 'Send 1 USDT on Optimism', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDT (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0xa', // Optimism (10) + token: 'USDT', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDT on Arbitrum', + description: 'Send 1 USDT on Arbitrum', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDT (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0xa4b1', // Arbitrum (42161) + token: 'USDT', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'DAI on Optimism', + description: 'Send 1 DAI on Optimism', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000000000000000', // 1 DAI (18 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0xa', // Optimism (10) + token: 'DAI', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'DAI on Arbitrum', + description: 'Send 1 DAI on Arbitrum', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000000000000000', // 1 DAI (18 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0xa4b1', // Arbitrum (42161) + token: 'DAI', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDC on Ethereum', + description: 'Send 1 USDC on Ethereum mainnet', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDC (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0x1', // Ethereum mainnet (1) + token: 'USDC', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDT on Ethereum', + description: 'Send 1 USDT on Ethereum mainnet', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDT (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0x1', // Ethereum mainnet (1) + token: 'USDT', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'DAI on Ethereum', + description: 'Send 1 DAI on Ethereum mainnet', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000000000000000', // 1 DAI (18 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0x1', // Ethereum mainnet (1) + token: 'DAI', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDC on Polygon', + description: 'Send 1 USDC on Polygon', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDC (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0x89', // Polygon (137) + token: 'USDC', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDT on Polygon', + description: 'Send 1 USDT on Polygon', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDT (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0x89', // Polygon (137) + token: 'USDT', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'DAI on Polygon', + description: 'Send 1 DAI on Polygon', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000000000000000', // 1 DAI (18 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0x89', // Polygon (137) + token: 'DAI', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDC on Avalanche', + description: 'Send 1 USDC on Avalanche C-Chain', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDC (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0xa86a', // Avalanche C-Chain (43114) + token: 'USDC', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDT on Avalanche', + description: 'Send 1 USDT on Avalanche C-Chain', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDT (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0xa86a', // Avalanche C-Chain (43114) + token: 'USDT', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'DAI on Avalanche', + description: 'Send 1 DAI on Avalanche C-Chain', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000000000000000', // 1 DAI (18 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0xa86a', // Avalanche C-Chain (43114) + token: 'DAI', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDC on BSC', + description: 'Send 1 USDC on Binance Smart Chain', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDC (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0x38', // BSC (56) + token: 'USDC', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDT on BSC', + description: 'Send 1 USDT on Binance Smart Chain', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDT (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0x38', // BSC (56) + token: 'USDT', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'DAI on BSC', + description: 'Send 1 DAI on Binance Smart Chain', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000000000000000', // 1 DAI (18 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0x38', // BSC (56) + token: 'DAI', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'Custom Token Address', + description: 'Send tokens using a custom contract address', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000000000000000', // Amount in token's smallest unit + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + token: '0xYourTokenContractAddressHere', // Custom token address + paymaster: { + url: 'https://paymaster.example.com' + } + // chainId defaults to Base mainnet (8453), specify a different one if needed + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, +]; + export const QUICK_TIPS = PAY_QUICK_TIPS; diff --git a/examples/testapp/src/pages/pay-playground/hooks/useCodeExecution.ts b/examples/testapp/src/pages/pay-playground/hooks/useCodeExecution.ts index c21a8d505..41b1f8f75 100644 --- a/examples/testapp/src/pages/pay-playground/hooks/useCodeExecution.ts +++ b/examples/testapp/src/pages/pay-playground/hooks/useCodeExecution.ts @@ -1,12 +1,16 @@ -import type { PaymentResult, PaymentStatus } from '@base-org/account'; -import { getPaymentStatus, pay } from '@base-org/account'; +import type { PaymentResult, PaymentStatus, PayWithTokenResult } from '@base-org/account'; +import { getPaymentStatus, pay, payWithToken } from '@base-org/account'; +// @ts-ignore - trade module types +import type { SwapResult, SwapQuote, SwapStatus } from '@base-org/account/trade'; +// @ts-ignore - trade module +import { swap, getSwapQuote, getSwapStatus } from '@base-org/account/trade'; import { useCallback, useState } from 'react'; import { transformAndSanitizeCode } from '../utils/codeTransform'; import { useConsoleCapture } from './useConsoleCapture'; export const useCodeExecution = () => { const [isLoading, setIsLoading] = useState(false); - const [result, setResult] = useState(null); + const [result, setResult] = useState(null); const [error, setError] = useState(null); const [consoleOutput, setConsoleOutput] = useState([]); const { captureConsole } = useConsoleCapture(); @@ -49,10 +53,21 @@ export const useCodeExecution = () => { // Individual functions for direct access pay, getPaymentStatus, + payWithToken, + swap, + getSwapQuote, + getSwapStatus, // Namespaced access via base object base: { pay, getPaymentStatus, + payWithToken, + }, + // Namespaced access via trade object + trade: { + swap, + getSwapQuote, + getSwapStatus, }, }; diff --git a/examples/testapp/src/pages/pay-playground/index.page.tsx b/examples/testapp/src/pages/pay-playground/index.page.tsx index 477144558..a7c117d39 100644 --- a/examples/testapp/src/pages/pay-playground/index.page.tsx +++ b/examples/testapp/src/pages/pay-playground/index.page.tsx @@ -3,20 +3,29 @@ import { CodeEditor, Header, Output, QuickTips } from './components'; import { DEFAULT_GET_PAYMENT_STATUS_CODE, DEFAULT_PAY_CODE, + DEFAULT_PAY_WITH_TOKEN_CODE, GET_PAYMENT_STATUS_QUICK_TIPS, PAY_CODE_WITH_PAYER_INFO, PAY_QUICK_TIPS, + PAY_WITH_TOKEN_CODE_WITH_PAYER_INFO, + PAY_WITH_TOKEN_PRESETS, + PAY_WITH_TOKEN_QUICK_TIPS, } from './constants'; import { useCodeExecution } from './hooks'; +import { togglePayerInfoInCode, extractPaymasterUrl, updatePaymasterUrl } from './utils'; import styles from './styles/Home.module.css'; function PayPlayground() { const [includePayerInfo, setIncludePayerInfo] = useState(false); const [payCode, setPayCode] = useState(DEFAULT_PAY_CODE); const [getPaymentStatusCode, setGetPaymentStatusCode] = useState(DEFAULT_GET_PAYMENT_STATUS_CODE); + const [includePayerInfoToken, setIncludePayerInfoToken] = useState(false); + const [payWithTokenCode, setPayWithTokenCode] = useState(DEFAULT_PAY_WITH_TOKEN_CODE); + const [paymasterUrl, setPaymasterUrl] = useState(''); const payExecution = useCodeExecution(); const getPaymentStatusExecution = useCodeExecution(); + const payWithTokenExecution = useCodeExecution(); const handlePayExecute = () => { payExecution.executeCode(payCode); @@ -44,6 +53,47 @@ function PayPlayground() { getPaymentStatusExecution.reset(); }; + const handlePayWithTokenExecute = () => { + payWithTokenExecution.executeCode(payWithTokenCode); + }; + + const handlePayWithTokenReset = () => { + setIncludePayerInfoToken(false); + setPaymasterUrl(''); + setPayWithTokenCode(DEFAULT_PAY_WITH_TOKEN_CODE); + payWithTokenExecution.reset(); + }; + + const handlePayerInfoTokenToggle = (checked: boolean) => { + setIncludePayerInfoToken(checked); + // Modify existing code to add/remove payerInfo instead of replacing it + const modifiedCode = togglePayerInfoInCode(payWithTokenCode, checked); + setPayWithTokenCode(modifiedCode); + payWithTokenExecution.reset(); + }; + + const handlePaymasterUrlChange = (url: string) => { + setPaymasterUrl(url); + // Update the code with the new paymaster URL + const updatedCode = updatePaymasterUrl(payWithTokenCode, url); + setPayWithTokenCode(updatedCode); + }; + + const handlePayWithTokenCodeChange = (code: string) => { + setPayWithTokenCode(code); + // Extract paymaster URL from code and sync the textbox + const extractedUrl = extractPaymasterUrl(code); + if (extractedUrl && extractedUrl !== paymasterUrl) { + setPaymasterUrl(extractedUrl); + } + }; + + const handlePayWithTokenPresetChange = (code: string) => { + // Apply the current paymasterUrl to the preset code (keep paymasterUrl intact) + const updatedCode = paymasterUrl ? updatePaymasterUrl(code, paymasterUrl) : code; + setPayWithTokenCode(updatedCode); + }; + // Watch for successful payment results and update getPaymentStatus code with the transaction ID useEffect(() => { if ( @@ -71,6 +121,33 @@ try { } }, [payExecution.result]); + // Watch for successful payWithToken results and update getPaymentStatus code with the transaction ID + useEffect(() => { + if ( + payWithTokenExecution.result && + 'success' in payWithTokenExecution.result && + payWithTokenExecution.result.success && + payWithTokenExecution.result.id + ) { + const transactionId = payWithTokenExecution.result.id; + const updatedCode = `import { base } from '@base-org/account' + +try { + const result = await base.getPaymentStatus({ + id: '${transactionId}', // Automatically filled with your recent transaction + testnet: true + }) + + return result; +} catch (error) { + // This will catch network errors if any occur + console.error('Failed to check payment status:', error.message); + throw error; +}`; + setGetPaymentStatusCode(updatedCode); + } + }, [payWithTokenExecution.result]); + return (
@@ -107,6 +184,43 @@ try {
+ {/* payWithToken Section */} +
+

payWithToken Function

+

Send ERC20 token payments on any supported chain

+ +
+
+ + +
+ +
+ +
+
+
+ {/* getPaymentStatus Section */}

getPaymentStatus Function

diff --git a/examples/testapp/src/pages/pay-playground/utils/codeSanitizer.ts b/examples/testapp/src/pages/pay-playground/utils/codeSanitizer.ts index 8c909c0c6..a50e7fe9f 100644 --- a/examples/testapp/src/pages/pay-playground/utils/codeSanitizer.ts +++ b/examples/testapp/src/pages/pay-playground/utils/codeSanitizer.ts @@ -3,11 +3,12 @@ import * as acorn from 'acorn'; // Define the whitelist of allowed operations export const WHITELIST = { // Allowed SDK functions - allowedFunctions: ['pay', 'getPaymentStatus'], + allowedFunctions: ['pay', 'getPaymentStatus', 'payWithToken', 'swap', 'getSwapQuote', 'getSwapStatus'], // Allowed object properties and methods allowedObjects: { - base: ['pay', 'getPaymentStatus'], + base: ['pay', 'getPaymentStatus', 'payWithToken'], + trade: ['swap', 'getSwapQuote', 'getSwapStatus'], console: ['log', 'error', 'warn', 'info'], Promise: ['resolve', 'reject', 'all', 'race'], Object: ['keys', 'values', 'entries', 'assign'], diff --git a/examples/testapp/src/pages/pay-playground/utils/index.ts b/examples/testapp/src/pages/pay-playground/utils/index.ts index 9816937d8..67cf6bff2 100644 --- a/examples/testapp/src/pages/pay-playground/utils/index.ts +++ b/examples/testapp/src/pages/pay-playground/utils/index.ts @@ -1,2 +1,4 @@ export { CodeSanitizer, WHITELIST, sanitizeCode } from './codeSanitizer'; export { safeStringify, transformAndSanitizeCode, transformImports } from './codeTransform'; +export { togglePayerInfoInCode } from './payerInfoTransform'; +export { extractPaymasterUrl, updatePaymasterUrl } from './paymasterTransform'; diff --git a/examples/testapp/src/pages/pay-playground/utils/payerInfoTransform.ts b/examples/testapp/src/pages/pay-playground/utils/payerInfoTransform.ts new file mode 100644 index 000000000..b484b297e --- /dev/null +++ b/examples/testapp/src/pages/pay-playground/utils/payerInfoTransform.ts @@ -0,0 +1,89 @@ +/** + * Adds or removes payerInfo from payWithToken code + * @param code - The current code string + * @param includePayerInfo - Whether to include payerInfo + * @returns The modified code with payerInfo added or removed + */ +export function togglePayerInfoInCode(code: string, includePayerInfo: boolean): string { + // Check if code already has payerInfo + const hasPayerInfo = /payerInfo\s*:\s*\{/.test(code); + + if (includePayerInfo && !hasPayerInfo) { + // Add payerInfo before the closing brace of the payWithToken options object + const payerInfoBlock = `, + payerInfo: { + requests: [ + { type: 'name'}, + { type: 'email' }, + { type: 'phoneNumber', optional: true }, + { type: 'physicalAddress', optional: true }, + { type: 'onchainAddress' } + ] + }`; + + // Find the payWithToken call and locate the closing brace of its options object + // Look for base.payWithToken({ ... }) + const payWithTokenMatch = code.match(/base\.payWithToken\s*\(\s*\{/); + if (!payWithTokenMatch) { + return code; // Can't find payWithToken call + } + + const startIndex = payWithTokenMatch.index! + payWithTokenMatch[0].length; + let braceDepth = 1; + let i = startIndex; + + // Find the closing brace of the payWithToken options object + while (i < code.length && braceDepth > 0) { + if (code[i] === '{') { + braceDepth++; + } else if (code[i] === '}') { + braceDepth--; + if (braceDepth === 0) { + // Found the closing brace - insert before it + const beforeBrace = code.substring(0, i).trimEnd(); + const afterBrace = code.substring(i); + + // Check if we need a comma before payerInfo + const needsComma = !beforeBrace.endsWith(',') && + !beforeBrace.endsWith('{') && + beforeBrace.length > 0; + + return beforeBrace + (needsComma ? payerInfoBlock : payerInfoBlock.substring(1)) + afterBrace; + } + } + i++; + } + } else if (!includePayerInfo && hasPayerInfo) { + // Remove payerInfo block using a regex that handles nested objects + // Match: comma, whitespace, payerInfo:, whitespace, {, nested content, } + // The nested content includes requests array with nested objects + let modifiedCode = code; + + // First try to match payerInfo with proper nested structure + // This regex matches from the comma before payerInfo to the closing brace + const payerInfoRegex = /,\s*payerInfo\s*:\s*\{[\s\S]*?requests\s*:\s*\[[\s\S]*?\][\s\S]*?\}/; + modifiedCode = modifiedCode.replace(payerInfoRegex, ''); + + // If that didn't work, try a simpler pattern + if (modifiedCode === code) { + // Fallback: match payerInfo property more broadly + const fallbackRegex = /,\s*payerInfo\s*:\s*\{[^}]*\{[^}]*\}[^}]*\}/s; + modifiedCode = modifiedCode.replace(fallbackRegex, ''); + } + + // Clean up formatting issues + // Remove double commas + modifiedCode = modifiedCode.replace(/,\s*,/g, ','); + // Remove trailing comma before closing brace + modifiedCode = modifiedCode.replace(/,\s*}/g, '}'); + // Remove leading comma after opening brace + modifiedCode = modifiedCode.replace(/{\s*,/g, '{'); + // Clean up extra whitespace + modifiedCode = modifiedCode.replace(/\n\s*\n\s*\n/g, '\n\n'); + + return modifiedCode; + } + + return code; +} + diff --git a/examples/testapp/src/pages/pay-playground/utils/paymasterTransform.ts b/examples/testapp/src/pages/pay-playground/utils/paymasterTransform.ts new file mode 100644 index 000000000..ce5a288b6 --- /dev/null +++ b/examples/testapp/src/pages/pay-playground/utils/paymasterTransform.ts @@ -0,0 +1,71 @@ +/** + * Extracts the paymaster URL from payWithToken code + * @param code - The code string to extract from + * @returns The paymaster URL if found, undefined otherwise + */ +export function extractPaymasterUrl(code: string): string | undefined { + // Match paymaster.url in the code - handle various whitespace patterns + const paymasterMatch = code.match(/paymaster\s*:\s*\{[\s\S]*?url\s*:\s*['"]([^'"]+)['"]/); + return paymasterMatch ? paymasterMatch[1] : undefined; +} + +/** + * Updates the paymaster URL in payWithToken code + * @param code - The code string to update + * @param paymasterUrl - The new paymaster URL to use + * @returns The updated code with the new paymaster URL + */ +export function updatePaymasterUrl(code: string, paymasterUrl: string): string { + // Check if paymaster already exists in the code + const hasPaymaster = /paymaster\s*:\s*\{/.test(code); + + if (!hasPaymaster) { + // If no paymaster exists, we need to add it + // Find the payWithToken call and locate where to insert paymaster + const payWithTokenMatch = code.match(/base\.payWithToken\s*\(\s*\{/); + if (!payWithTokenMatch) { + return code; // Can't find payWithToken call + } + + const startIndex = payWithTokenMatch.index! + payWithTokenMatch[0].length; + let braceDepth = 1; + let i = startIndex; + + // Find the closing brace of the payWithToken options object + while (i < code.length && braceDepth > 0) { + if (code[i] === '{') { + braceDepth++; + } else if (code[i] === '}') { + braceDepth--; + if (braceDepth === 0) { + // Found the closing brace - insert before it + const beforeBrace = code.substring(0, i).trimEnd(); + const afterBrace = code.substring(i); + + // Check if we need a comma before paymaster + const needsComma = !beforeBrace.endsWith(',') && + !beforeBrace.endsWith('{') && + beforeBrace.length > 0; + + const paymasterBlock = `, + paymaster: { + url: '${paymasterUrl}' + }`; + + return beforeBrace + (needsComma ? paymasterBlock : paymasterBlock.substring(1)) + afterBrace; + } + } + i++; + } + } else { + // Update existing paymaster URL - handle various whitespace patterns + const updatedCode = code.replace( + /(paymaster\s*:\s*\{[\s\S]*?url\s*:\s*['"])[^'"]+(['"])/, + `$1${paymasterUrl}$2` + ); + return updatedCode; + } + + return code; +} + diff --git a/packages/account-sdk/src/browser-entry.ts b/packages/account-sdk/src/browser-entry.ts index f950f0fab..f83eb92ab 100644 --- a/packages/account-sdk/src/browser-entry.ts +++ b/packages/account-sdk/src/browser-entry.ts @@ -9,6 +9,7 @@ import { base } from './interface/payment/base.browser.js'; import { CHAIN_IDS, TOKENS } from './interface/payment/constants.js'; import { getPaymentStatus } from './interface/payment/getPaymentStatus.js'; import { pay } from './interface/payment/pay.js'; +import { payWithToken } from './interface/payment/payWithToken.js'; import { subscribe } from './interface/payment/subscribe.js'; import type { InfoRequest, @@ -17,6 +18,8 @@ import type { PaymentResult, PaymentStatus, PaymentStatusOptions, + PayWithTokenOptions, + PayWithTokenResult, SubscriptionOptions, SubscriptionResult, } from './interface/payment/types.js'; @@ -50,7 +53,7 @@ export type { export { PACKAGE_VERSION as VERSION } from './core/constants.js'; export { createBaseAccountSDK } from './interface/builder/core/createBaseAccountSDK.js'; export { getCryptoKeyAccount, removeCryptoKey } from './kms/crypto-key/index.js'; -export { base, CHAIN_IDS, getPaymentStatus, pay, subscribe, TOKENS }; +export { base, CHAIN_IDS, getPaymentStatus, pay, payWithToken, subscribe, TOKENS }; export type { InfoRequest, PayerInfo, @@ -58,6 +61,8 @@ export type { PaymentResult, PaymentStatus, PaymentStatusOptions, + PayWithTokenOptions, + PayWithTokenResult, SubscriptionOptions, SubscriptionResult, }; diff --git a/packages/account-sdk/src/core/telemetry/events/payment.ts b/packages/account-sdk/src/core/telemetry/events/payment.ts index 1df3bf4ba..bde62f125 100644 --- a/packages/account-sdk/src/core/telemetry/events/payment.ts +++ b/packages/account-sdk/src/core/telemetry/events/payment.ts @@ -75,6 +75,81 @@ export const logPaymentCompleted = ({ ); }; +export const logPayWithTokenStarted = ({ + token, + chainId, + correlationId, +}: { + token: string; + chainId: number; + correlationId: string | undefined; +}) => { + logEvent( + 'payment.payWithToken.started', + { + action: ActionType.process, + componentType: ComponentType.unknown, + method: 'payWithToken', + correlationId, + signerType: 'base-account', + token, + chainId, + }, + AnalyticsEventImportance.high + ); +}; + +export const logPayWithTokenCompleted = ({ + token, + chainId, + correlationId, +}: { + token: string; + chainId: number; + correlationId: string | undefined; +}) => { + logEvent( + 'payment.payWithToken.completed', + { + action: ActionType.process, + componentType: ComponentType.unknown, + method: 'payWithToken', + correlationId, + signerType: 'base-account', + token, + chainId, + }, + AnalyticsEventImportance.high + ); +}; + +export const logPayWithTokenError = ({ + token, + chainId, + correlationId, + errorMessage, +}: { + token: string; + chainId: number; + correlationId: string | undefined; + errorMessage: string; +}) => { + logEvent( + 'payment.payWithToken.error', + { + action: ActionType.error, + componentType: ComponentType.unknown, + method: 'payWithToken', + correlationId, + signerType: 'base-account', + token, + chainId, + errorMessage, + }, + AnalyticsEventImportance.high + ); +}; + export const logPaymentStatusCheckStarted = ({ testnet, correlationId, diff --git a/packages/account-sdk/src/index.ts b/packages/account-sdk/src/index.ts index 611749a7f..d526ecc8b 100644 --- a/packages/account-sdk/src/index.ts +++ b/packages/account-sdk/src/index.ts @@ -14,12 +14,14 @@ export { export { PACKAGE_VERSION as VERSION } from './core/constants.js'; +// Payment interface exports export { base, CHAIN_IDS, getPaymentStatus, getSubscriptionStatus, pay, + payWithToken, prepareCharge, subscribe, TOKENS, @@ -41,10 +43,20 @@ export type { PrepareChargeCall, PrepareChargeOptions, PrepareChargeResult, + PrepareRevokeCall, + PrepareRevokeOptions, + PrepareRevokeResult, + RevokeOptions, + RevokeResult, SubscriptionOptions, SubscriptionResult, SubscriptionStatus, SubscriptionStatusOptions, + PayWithTokenOptions, + PayWithTokenResult, + PaymasterOptions, + TokenPaymentSuccess, + TokenInput, } from './interface/payment/index.js'; export { diff --git a/packages/account-sdk/src/interface/payment/README.md b/packages/account-sdk/src/interface/payment/README.md index 8d73e2995..6b6e24fc6 100644 --- a/packages/account-sdk/src/interface/payment/README.md +++ b/packages/account-sdk/src/interface/payment/README.md @@ -21,17 +21,54 @@ if (payment.success) { } ``` +## Token Payments (`payWithToken`) + +Use `payWithToken` to send any ERC20 token (or native ETH via the `0xEeee…` placeholder) by specifying the chain, token, and paymaster configuration. + +```typescript +import { payWithToken } from '@base-org/account'; + +const payment = await payWithToken({ + amount: '1000000', // base units (wei) + token: 'USDC', // symbol or contract address + chainId: '0x2105', // Base mainnet + to: '0xFe21034794A5a574B94fE4fDfD16e005F1C96e51', + paymaster: { + url: 'https://paymaster.example.com', + }, + payerInfo: { + requests: [{ type: 'email' }], + }, +}); + +console.log(`Token payment sent! Chain-specific ID: ${payment.id}`); +``` + +**Token payment notes** + +- `amount` must be provided in the token’s smallest unit (e.g., wei). +- `token` can be an address or a supported symbol (USDC, USDT, DAI). Symbols are validated against the `chainId` you provide. +- `paymaster` is required. Provide either a `url` for a paymaster service or a precomputed `paymasterAndData`. +- The returned `payment.id` uses the [ERC-3770](https://eips.ethereum.org/EIPS/eip-3770) `shortName:hash` format (for example, `base:0x1234…`). Pass this ID directly to `getPaymentStatus`—it already contains the chain context, so you do **not** need to supply `testnet`. + ## Checking Payment Status -You can check the status of a payment using the transaction ID returned from the pay function: +You can check the status of a payment using the ID returned from either `pay()` or `payWithToken()`: ```typescript import { getPaymentStatus } from '@base/account-sdk'; -// Check payment status -const status = await getPaymentStatus({ - id: payment.id, - testnet: true +// Assume tokenPayment/usdcPayment are the results from the examples above. + +// Token payments (ERC-3770 encoded IDs). No testnet flag needed. +const tokenStatus = await getPaymentStatus({ + id: tokenPayment.id, // e.g., "base:0x1234..." +}); + +// USDC payments via pay() still require a testnet flag. +const usdcStatus = await getPaymentStatus({ + id: usdcPayment.id, + testnet: true, }); switch (status.status) { @@ -50,6 +87,11 @@ switch (status.status) { } ``` +The status object now includes: + +- `tokenAmount`, `tokenAddress`, and `tokenSymbol` for any detected ERC20 transfer. +- `amount` (human-readable) when the token is a whitelisted stablecoin (USDC/USDT/DAI). + ## Information Requests (Data Callbacks) You can request additional information from the user during payment using the `payerInfo` parameter: @@ -144,10 +186,8 @@ The payment result is always a successful payment (errors are thrown as exceptio ### `getPaymentStatus(options: PaymentStatusOptions): Promise` -#### PaymentStatusOptions - -- `id: string` - Transaction ID (userOp hash) to check status for -- `testnet?: boolean` - Whether to check on testnet (Base Sepolia). Defaults to false (mainnet) +- `id: string` - Payment ID to check. For `payWithToken()` this is an ERC-3770 value (`shortName:0x…`). For `pay()` this is a plain transaction hash. +- `testnet?: boolean` - Only used for plain hashes returned by `pay()`. Ignored when the ID already encodes the chain (ERC-3770 format). - `telemetry?: boolean` - Whether to enable telemetry logging (default: true) #### PaymentStatus diff --git a/packages/account-sdk/src/interface/payment/constants.ts b/packages/account-sdk/src/interface/payment/constants.ts index 58c11eb50..def1bf16b 100644 --- a/packages/account-sdk/src/interface/payment/constants.ts +++ b/packages/account-sdk/src/interface/payment/constants.ts @@ -1,5 +1,27 @@ +import type { Address } from 'viem'; + +/** + * Chain IDs for supported networks + */ +export const CHAIN_IDS = { + base: 8453, + baseSepolia: 84532, + ethereum: 1, + sepolia: 11155111, + optimism: 10, + optimismSepolia: 11155420, + arbitrum: 42161, + polygon: 137, + 'avalanche-c-chain': 43114, + avalanche: 43114, + baseMainnet: 8453, // alias + zora: 7777777, + BSC: 56, + bsc: 56, +} as const; + /** - * Token configuration for supported payment tokens + * Token configuration for legacy USDC-only payment APIs */ export const TOKENS = { USDC: { @@ -12,13 +34,102 @@ export const TOKENS = { } as const; /** - * Chain IDs for supported networks + * Canonical placeholder used by wallet providers to represent native ETH */ -export const CHAIN_IDS = { - base: 8453, - baseSepolia: 84532, +export const ETH_PLACEHOLDER_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' as const; + +/** + * Registry of whitelisted stablecoins that can be referenced by symbol + * when calling token-aware payment APIs. + * + * NOTE: Not every token is available on every supported chain. Any missing + * addresses will force callers to provide an explicit token contract address. + */ +export const STABLECOIN_WHITELIST = { + USDC: { + symbol: 'USDC', + decimals: 6, + addresses: { + [CHAIN_IDS.ethereum]: '0xA0b86991c6218b36c1d19D4a2e9eb0ce3606eb48', + [CHAIN_IDS.optimism]: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', + [CHAIN_IDS.optimismSepolia]: '0x5fd84259d66cD46123540766Be93DfE6d43130d7', + [CHAIN_IDS.arbitrum]: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + [CHAIN_IDS.polygon]: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', + [CHAIN_IDS['avalanche-c-chain']]: '0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E', + [CHAIN_IDS.base]: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', + [CHAIN_IDS.baseSepolia]: '0x036CbD53842c5426634e7929541eC2318f3dCF7e', + [CHAIN_IDS.BSC]: '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d', + } satisfies Partial>, + }, + USDT: { + symbol: 'USDT', + decimals: 6, + addresses: { + [CHAIN_IDS.ethereum]: '0xdAC17F958D2ee523a2206206994597C13D831ec7', + [CHAIN_IDS.optimism]: '0x94b008aa00579c1307b0ef2c499ad98a8ce58e58', + [CHAIN_IDS.arbitrum]: '0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9', + [CHAIN_IDS.polygon]: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F', + [CHAIN_IDS['avalanche-c-chain']]: '0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7', + [CHAIN_IDS.base]: '0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2', + [CHAIN_IDS.BSC]: '0x55d398326f99059fF775485246999027B3197955', + } satisfies Partial>, + }, + DAI: { + symbol: 'DAI', + decimals: 18, + addresses: { + [CHAIN_IDS.ethereum]: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + [CHAIN_IDS.optimism]: '0xda10009cbd5d07dd0cecc66161fc93d7c9000da1', + [CHAIN_IDS.arbitrum]: '0xda10009cbd5d07dd0cecc66161fc93d7c9000da1', + [CHAIN_IDS.polygon]: '0x8f3cf7ad23cd3cadbd9735aff958023239c6a063', + [CHAIN_IDS['avalanche-c-chain']]: '0xd586E7F844cEa2F87f50152665BCbc2C279D8d70', + [CHAIN_IDS.base]: '0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb', + [CHAIN_IDS.BSC]: '0x1AF3F329e8BE154074D8769D1FFa4eE058B1DBc3', + } satisfies Partial>, + }, + EURC: { + symbol: 'EURC', + decimals: 6, + addresses: { + [CHAIN_IDS.ethereum]: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c', + [CHAIN_IDS.optimism]: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c', + [CHAIN_IDS.arbitrum]: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c', + [CHAIN_IDS.polygon]: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c', + [CHAIN_IDS['avalanche-c-chain']]: '0xC891EB4cbdEFf6e178eE3d4314284F79b81Bd4C7', + [CHAIN_IDS.base]: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c', + [CHAIN_IDS.BSC]: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c', + } satisfies Partial>, + }, } as const; +type StablecoinKey = keyof typeof STABLECOIN_WHITELIST; + +/** + * Lookup map from token address to stablecoin metadata. + */ +export const STABLECOIN_ADDRESS_LOOKUP = Object.entries(STABLECOIN_WHITELIST).reduce< + Record< + string, + { + symbol: StablecoinKey; + decimals: number; + chainId: number; + } + > +>((acc, [symbol, config]) => { + for (const [chainId, address] of Object.entries(config.addresses)) { + if (!address) { + continue; + } + acc[address.toLowerCase()] = { + symbol: symbol as StablecoinKey, + decimals: config.decimals, + chainId: Number(chainId), + }; + } + return acc; +}, {}); + /** * ERC20 transfer function ABI */ diff --git a/packages/account-sdk/src/interface/payment/getPaymentStatus.test.ts b/packages/account-sdk/src/interface/payment/getPaymentStatus.test.ts index 50996a9d5..317e0b57e 100644 --- a/packages/account-sdk/src/interface/payment/getPaymentStatus.test.ts +++ b/packages/account-sdk/src/interface/payment/getPaymentStatus.test.ts @@ -63,6 +63,9 @@ describe('getPaymentStatus', () => { message: 'Payment completed successfully', sender: '0x4A7c6899cdcB379e284fBFd045462e751da4C7ce', amount: '10', + tokenAmount: '10000000', + tokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', + tokenSymbol: 'USDC', recipient: '0xf1DdF1fc0310Cb11F0Ca87508207012F4a9CB336', }); @@ -81,6 +84,45 @@ describe('getPaymentStatus', () => { ); }); + it('should decode ERC-3770 encoded IDs and ignore the legacy testnet flag', async () => { + const transactionHash = + '0xabc1230000000000000000000000000000000000000000000000000000000000'; + const mockReceipt = { + jsonrpc: '2.0', + id: 1, + result: { + success: true, + receipt: { + transactionHash, + logs: [], + }, + sender: '0x4A7c6899cdcB379e284fBFd045462e751da4C7ce', + }, + }; + + vi.mocked(fetch).mockResolvedValueOnce({ + json: async () => mockReceipt, + } as Response); + + const status = await getPaymentStatus({ + id: `base:${transactionHash}`, + testnet: true, + }); + + expect(status.id).toBe(transactionHash); + expect(fetch).toHaveBeenCalledWith( + 'https://api.developer.coinbase.com/rpc/v1/base/S-fOd2n2Oi4fl4e1Crm83XeDXZ7tkg8O', + expect.objectContaining({ + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'eth_getUserOperationReceipt', + params: [transactionHash], + }), + }) + ); + }); + it('should return failed status for failed payment', async () => { const mockReceipt = { jsonrpc: '2.0', @@ -282,6 +324,8 @@ describe('getPaymentStatus', () => { }); expect(status.amount).toBe('10'); + expect(status.tokenAmount).toBe('10000000'); + expect(status.tokenSymbol).toBe('USDC'); expect(status.recipient).toBe('0xf1DdF1fc0310Cb11F0Ca87508207012F4a9CB336'); }); @@ -320,7 +364,7 @@ describe('getPaymentStatus', () => { testnet: false, }) ).rejects.toThrow( - 'Unable to find USDC transfer from sender wallet 0x4A7c6899cdcB379e284fBFd045462e751da4C7ce' + 'Unable to find token transfer from sender wallet 0x4A7c6899cdcB379e284fBFd045462e751da4C7ce' ); }); @@ -367,7 +411,7 @@ describe('getPaymentStatus', () => { testnet: false, }) ).rejects.toThrow( - /Found multiple USDC transfers from sender wallet.*Expected exactly one transfer/ + /Found multiple token transfers from sender wallet.*Expected exactly one transfer/ ); }); @@ -421,6 +465,9 @@ describe('getPaymentStatus', () => { message: 'Payment completed successfully', sender: '0x4A7c6899cdcB379e284fBFd045462e751da4C7ce', amount: '1', // Should pick the 1 USDC from sender, not the 4500 USDC gas payment + tokenAmount: '1000000', + tokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', + tokenSymbol: 'USDC', recipient: '0xf1DdF1fc0310Cb11F0Ca87508207012F4a9CB336', }); }); diff --git a/packages/account-sdk/src/interface/payment/getPaymentStatus.ts b/packages/account-sdk/src/interface/payment/getPaymentStatus.ts index a887b4269..39d406afd 100644 --- a/packages/account-sdk/src/interface/payment/getPaymentStatus.ts +++ b/packages/account-sdk/src/interface/payment/getPaymentStatus.ts @@ -6,22 +6,27 @@ import { logPaymentStatusCheckError, logPaymentStatusCheckStarted, } from ':core/telemetry/events/payment.js'; -import { ERC20_TRANSFER_ABI, TOKENS } from './constants.js'; +import { CHAIN_IDS, ERC20_TRANSFER_ABI } from './constants.js'; import type { PaymentStatus, PaymentStatusOptions } from './types.js'; +import { decodePaymentId, getBundlerUrl } from './utils/erc3770.js'; +import { getStablecoinMetadataByAddress } from './utils/tokenRegistry.js'; /** * Check the status of a payment transaction using its transaction ID (userOp hash) * * @param options - Payment status check options + * @param options.id - Payment ID. For `payWithToken()`, this is ERC-3770 encoded (e.g., "base:0x1234...5678"). + * For `pay()`, this is a plain transaction hash (e.g., "0x1234...5678"). + * @param options.testnet - Whether to use testnet (only used for legacy format from `pay()`) * @returns Promise - Status information about the payment * @throws Error if unable to connect to the RPC endpoint or if the RPC request fails * * @example * ```typescript + * // ERC-3770 encoded ID from payWithToken() * try { * const status = await getPaymentStatus({ - * id: "0x1234...5678", - * testnet: true + * id: "base:0x1234...5678" * }) * * if (status.status === 'failed') { @@ -32,7 +37,21 @@ import type { PaymentStatus, PaymentStatusOptions } from './types.js'; * } * ``` * - * @note The id is the userOp hash returned from the pay function + * @example + * ```typescript + * // Legacy format from pay() - requires testnet flag + * try { + * const status = await getPaymentStatus({ + * id: "0x1234...5678", + * testnet: true + * }) + * } catch (error) { + * console.error('Unable to check payment status:', error.message) + * } + * ``` + * + * @note For `payWithToken()`, the ID is automatically encoded with chain information using ERC-3770 format. + * For `pay()`, the ID is a plain hash and requires the `testnet` flag to determine the chain. */ export async function getPaymentStatus(options: PaymentStatusOptions): Promise { const { id, testnet = false, telemetry = true } = options; @@ -40,17 +59,47 @@ export async function getPaymentStatus(options: PaymentStatusOptions): Promise

res.json()); @@ -70,7 +119,7 @@ export async function getPaymentStatus(options: PaymentStatusOptions): Promise

res.json()); if (userOpResponse.result) { // UserOp exists but no receipt yet - it's pending if (telemetry) { - logPaymentStatusCheckCompleted({ testnet, status: 'pending', correlationId }); + logPaymentStatusCheckCompleted({ testnet: isTestnet, status: 'pending', correlationId }); } const result = { status: 'pending' as const, - id: id as Hex, + id: transactionHash as Hex, message: 'Your payment is being processed. This usually takes a few seconds.', sender: userOpResponse.result.sender, }; @@ -108,11 +157,11 @@ export async function getPaymentStatus(options: PaymentStatusOptions): Promise

= []; - for (let i = 0; i < txReceipt.logs.length; i++) { - const log = txReceipt.logs[i]; + for (const log of txReceipt.logs) { + if (!log.address) { + continue; + } - // Check if this is a USDC log - const logAddressLower = log.address?.toLowerCase(); - const isUsdcLog = logAddressLower === usdcAddress; + try { + const decoded = decodeEventLog({ + abi: ERC20_TRANSFER_ABI, + data: log.data, + topics: log.topics, + }); - if (isUsdcLog) { - try { - const decoded = decodeEventLog({ - abi: ERC20_TRANSFER_ABI, - data: log.data, - topics: log.topics, - }); - - if (decoded.eventName === 'Transfer' && decoded.args) { - const args = decoded.args as { from: string; to: string; value: bigint }; - - if (args.value && args.to && args.from) { - const formattedAmount = formatUnits(args.value, 6); - - usdcTransfers.push({ - from: args.from, - to: args.to, - value: args.value, - formattedAmount, - logIndex: i, - }); - } + if (decoded.eventName === 'Transfer' && decoded.args) { + const args = decoded.args as { from: string; to: string; value: bigint }; + if (args.value && args.to && args.from) { + tokenTransfers.push({ + from: getAddress(args.from), + to: getAddress(args.to), + value: args.value, + contract: getAddress(log.address as Address), + }); } - } catch (_e) { - // Do not fail here - fail when we can't find a single valid transfer } + } catch (_e) { + // Ignore non ERC-20 logs } } - // Now select the correct transfer - // Strategy: Find the transfer from the sender (smart wallet) address - if (usdcTransfers.length > 0 && senderAddress) { - // Look for transfers from the sender address (smart wallet) - // Compare checksummed addresses for consistency - const senderTransfers = usdcTransfers.filter((t) => { + if (tokenTransfers.length > 0 && senderAddress) { + const senderTransfers = tokenTransfers.filter((t) => { try { - return isAddressEqual(t.from as Address, senderAddress!); + return isAddressEqual(t.from, senderAddress); } catch { return false; } }); if (senderTransfers.length === 0) { - // No transfer from the sender wallet was found throw new Error( - `Unable to find USDC transfer from sender wallet ${receipt.result.sender}. ` + - `Found ${usdcTransfers.length} USDC transfer(s) but none originated from the sender wallet.` + `Unable to find token transfer from sender wallet ${receipt.result.sender}. ` + + `Found ${tokenTransfers.length} transfer(s) but none originated from the sender wallet.` ); } + if (senderTransfers.length > 1) { - // Multiple transfers from the sender wallet found const transferDetails = senderTransfers - .map((t) => `${t.formattedAmount} USDC to ${t.to}`) + .map((t) => `${t.value.toString()} wei to ${t.to}`) .join(', '); throw new Error( - `Found multiple USDC transfers from sender wallet ${receipt.result.sender}: ${transferDetails}. Expected exactly one transfer.` + `Found multiple token transfers from sender wallet ${receipt.result.sender}: ${transferDetails}. Expected exactly one transfer.` ); } - // Exactly one transfer from sender found - amount = senderTransfers[0].formattedAmount; - recipient = senderTransfers[0].to; + + const transfer = senderTransfers[0]; + const stablecoinMetadata = getStablecoinMetadataByAddress(transfer.contract); + + tokenAmount = transfer.value.toString(); + tokenAddress = transfer.contract as Address; + tokenSymbol = stablecoinMetadata?.symbol; + recipient = transfer.to; + + if (stablecoinMetadata) { + amount = formatUnits(transfer.value, stablecoinMetadata.decimals); + } } } if (telemetry) { - logPaymentStatusCheckCompleted({ testnet, status: 'completed', correlationId }); + logPaymentStatusCheckCompleted({ testnet: isTestnet, status: 'completed', correlationId }); } const result = { status: 'completed' as const, - id: id as Hex, + id: transactionHash as Hex, message: 'Payment completed successfully', sender: receipt.result.sender, amount, + tokenAmount, + tokenAddress, + tokenSymbol, recipient, }; return result; @@ -240,11 +286,11 @@ export async function getPaymentStatus(options: PaymentStatusOptions): Promise

({ + logPayWithTokenStarted: vi.fn(), + logPayWithTokenCompleted: vi.fn(), + logPayWithTokenError: vi.fn(), +})); + +vi.mock('./utils/validation.js', () => ({ + normalizeAddress: vi.fn((address: string) => address), + normalizeChainId: vi.fn(() => 8453), + validateBaseUnitAmount: vi.fn(() => BigInt(1000)), +})); + +vi.mock('./utils/tokenRegistry.js', () => ({ + resolveTokenAddress: vi.fn(() => ({ + address: '0x0000000000000000000000000000000000000001', + symbol: 'USDC', + decimals: 6, + isNativeEth: false, + })), +})); + +vi.mock('./utils/translateTokenPayment.js', () => ({ + buildTokenPaymentRequest: vi.fn(() => ({ + version: '2.0.0', + chainId: 8453, + calls: [], + capabilities: {}, + })), +})); + +vi.mock('./utils/sdkManager.js', () => ({ + executePaymentOnChain: vi.fn(async () => ({ + transactionHash: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', + payerInfoResponses: { email: 'test@example.com' }, + })), +})); + +describe('payWithToken', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.stubGlobal('crypto', { + randomUUID: vi.fn().mockReturnValue('mock-correlation-id'), + }); + }); + + it('should successfully process a token payment', async () => { + const result = await payWithToken({ + amount: '1000', + to: '0xFe21034794A5a574B94fE4fDfD16e005F1C96e51', + token: 'USDC', + chainId: '0x2105', + paymaster: { url: 'https://paymaster.example.com' }, + }); + + expect(result).toEqual({ + success: true, + id: 'base:0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', + token: 'USDC', + tokenAddress: '0x0000000000000000000000000000000000000001', + tokenAmount: '1000', + chainId: 8453, + to: '0xFe21034794A5a574B94fE4fDfD16e005F1C96e51', + payerInfoResponses: { email: 'test@example.com' }, + }); + + const { buildTokenPaymentRequest } = await import('./utils/translateTokenPayment.js'); + expect(buildTokenPaymentRequest).toHaveBeenCalledWith( + expect.objectContaining({ + recipient: '0xFe21034794A5a574B94fE4fDfD16e005F1C96e51', + amount: BigInt(1000), + chainId: 8453, + paymaster: { url: 'https://paymaster.example.com' }, + }) + ); + }); + + it('should merge walletUrl into sdkConfig and pass it to the executor', async () => { + const { executePaymentOnChain } = await import('./utils/sdkManager.js'); + + await payWithToken({ + amount: '500', + to: '0x0A7c6899cdCb379E284fbFd045462e751dA4C7cE', + token: 'USDT', + chainId: 8453, + paymaster: { paymasterAndData: '0xdeadbeef' as `0x${string}` }, + walletUrl: 'https://wallet.example.com', + sdkConfig: { + preference: { + telemetry: false, + }, + }, + }); + + expect(executePaymentOnChain).toHaveBeenCalledWith( + expect.any(Object), + 8453, + true, + expect.objectContaining({ + preference: expect.objectContaining({ + telemetry: false, + walletUrl: 'https://wallet.example.com', + }), + }) + ); + }); + + it('should propagate errors and log telemetry when execution fails', async () => { + const { executePaymentOnChain } = await import('./utils/sdkManager.js'); + const { logPayWithTokenError } = await import(':core/telemetry/events/payment.js'); + + vi.mocked(executePaymentOnChain).mockRejectedValueOnce(new Error('execution reverted')); + + await expect( + payWithToken({ + amount: '1', + to: '0x000000000000000000000000000000000000dead', + token: 'USDC', + chainId: 8453, + paymaster: { url: 'https://paymaster.example.com' }, + }) + ).rejects.toThrow('execution reverted'); + + expect(logPayWithTokenError).toHaveBeenCalledWith( + expect.objectContaining({ + token: 'USDC', + errorMessage: 'execution reverted', + }) + ); + }); +}); + diff --git a/packages/account-sdk/src/interface/payment/payWithToken.ts b/packages/account-sdk/src/interface/payment/payWithToken.ts new file mode 100644 index 000000000..744ec20ec --- /dev/null +++ b/packages/account-sdk/src/interface/payment/payWithToken.ts @@ -0,0 +1,117 @@ +import { + logPayWithTokenCompleted, + logPayWithTokenError, + logPayWithTokenStarted, +} from ':core/telemetry/events/payment.js'; +import { CHAIN_IDS } from './constants.js'; +import type { PayWithTokenOptions, PayWithTokenResult, PaymentSDKConfig } from './types.js'; +import { encodePaymentId } from './utils/erc3770.js'; +import { executePaymentOnChain } from './utils/sdkManager.js'; +import { resolveTokenAddress } from './utils/tokenRegistry.js'; +import { buildTokenPaymentRequest } from './utils/translateTokenPayment.js'; +import { normalizeAddress, normalizeChainId, validateBaseUnitAmount } from './utils/validation.js'; + +function mergeSdkConfig( + sdkConfig: PaymentSDKConfig | undefined, + walletUrl?: string +): PaymentSDKConfig | undefined { + if (!walletUrl) { + return sdkConfig; + } + + return { + ...sdkConfig, + preference: { + ...(sdkConfig?.preference ?? {}), + walletUrl, + }, + }; +} + +/** + * Pay a specified address with any ERC20 token using an ephemeral smart wallet. + * + * @param options - Payment options + * @returns Promise + */ +export async function payWithToken(options: PayWithTokenOptions): Promise { + const { + amount, + to, + token, + chainId = CHAIN_IDS.base, + paymaster, + payerInfo, + walletUrl, + telemetry = true, + sdkConfig, + } = options; + + const correlationId = crypto.randomUUID(); + const normalizedChainId = normalizeChainId(chainId); + const normalizedRecipient = normalizeAddress(to); + const amountInWei = validateBaseUnitAmount(amount); + const resolvedToken = resolveTokenAddress(token, normalizedChainId); + const tokenLabel = resolvedToken.symbol ?? resolvedToken.address; + + if (telemetry) { + logPayWithTokenStarted({ + token: tokenLabel, + chainId: normalizedChainId, + correlationId, + }); + } + + const mergedSdkConfig = mergeSdkConfig(sdkConfig, walletUrl); + + try { + const requestParams = buildTokenPaymentRequest({ + recipient: normalizedRecipient, + amount: amountInWei, + chainId: normalizedChainId, + token: resolvedToken, + payerInfo, + paymaster, + }); + + const executionResult = await executePaymentOnChain( + requestParams, + normalizedChainId, + telemetry, + mergedSdkConfig + ); + + if (telemetry) { + logPayWithTokenCompleted({ + token: tokenLabel, + chainId: normalizedChainId, + correlationId, + }); + } + + // Encode payment ID with chain ID using ERC-3770 format + const encodedId = encodePaymentId(normalizedChainId, executionResult.transactionHash); + + return { + success: true, + id: encodedId, + token: tokenLabel, + tokenAddress: resolvedToken.address, + tokenAmount: amountInWei.toString(), + chainId: normalizedChainId, + to: normalizedRecipient, + payerInfoResponses: executionResult.payerInfoResponses, + }; + } catch (error) { + if (telemetry) { + logPayWithTokenError({ + token: tokenLabel, + chainId: normalizedChainId, + correlationId, + errorMessage: error instanceof Error ? error.message : 'Unknown error occurred', + }); + } + throw error; + } +} + diff --git a/packages/account-sdk/src/interface/payment/types.ts b/packages/account-sdk/src/interface/payment/types.ts index f75d6006c..3a58dee54 100644 --- a/packages/account-sdk/src/interface/payment/types.ts +++ b/packages/account-sdk/src/interface/payment/types.ts @@ -1,5 +1,7 @@ import type { Address, Hex } from 'viem'; +import type { ERC3770PaymentId } from './utils/erc3770.js'; + /** * Information request type for payment data callbacks */ @@ -85,7 +87,12 @@ export interface PaymentSDKConfig { } /** - * Options for making a payment + * Input supported for token parameters. Accepts either a contract address or a supported symbol. + */ +export type TokenInput = string; + +/** + * Options for making a USDC payment. */ export interface PaymentOptions { /** Amount of USDC to send as a string (e.g., "10.50") */ @@ -103,25 +110,85 @@ export interface PaymentOptions { } /** - * Successful payment result + * Paymaster configuration for token-based payments. + */ +export interface PaymasterOptions { + /** URL for the paymaster service (sponsor) */ + url?: string; + /** Optional contextual data to send to the paymaster service */ + context?: Record; + /** Direct paymasterAndData value for pre-signed operations */ + paymasterAndData?: Hex; +} + +/** + * Options for making a token-denominated payment. + */ +export interface PayWithTokenOptions { + /** Amount to send in the token's smallest unit (e.g., wei) */ + amount: string; + /** Recipient address */ + to: string; + /** Token to transfer (address or whitelisted symbol) */ + token: TokenInput; + /** Chain ID where the token transfer will execute. Defaults to Base (8453) */ + chainId?: number | string; + /** Paymaster configuration (required) */ + paymaster: PaymasterOptions; + /** Optional payer information configuration for data callbacks */ + payerInfo?: PayerInfo; + /** Optional wallet URL override */ + walletUrl?: string; + /** Whether to enable telemetry logging. Defaults to true */ + telemetry?: boolean; + /** @internal Advanced SDK configuration (undocumented) */ + sdkConfig?: PaymentSDKConfig; +} + +/** + * Base shape shared by all successful payment responses. */ -export interface PaymentSuccess { +export interface BasePaymentSuccess { success: true; /** Transaction ID (hash) of the payment */ id: string; - /** The amount that was sent */ - amount: string; /** The address that received the payment */ to: Address; /** Optional responses from information requests */ payerInfoResponses?: PayerInfoResponses; } +/** + * Successful payment result for USDC payments. + */ +export interface PaymentSuccess extends BasePaymentSuccess { + /** The amount that was sent (in USDC) */ + amount: string; +} + /** * Result of a payment transaction */ export type PaymentResult = PaymentSuccess; +/** + * Successful payment result for token payments. + */ +export interface TokenPaymentSuccess extends BasePaymentSuccess { + /** Chain-prefixed ERC-3770 payment ID */ + id: ERC3770PaymentId; + /** Chain ID where the payment executed */ + chainId: number; + /** Token amount transferred in base units (wei) */ + tokenAmount: string; + /** Token contract address or native placeholder */ + tokenAddress: Address; + /** Optional token shorthand (symbol or user-provided value) */ + token?: string; +} + +export type PayWithTokenResult = TokenPaymentSuccess; + /** * Options for checking payment status */ @@ -155,6 +222,12 @@ export interface PaymentStatus { sender?: string; /** Amount sent (present for completed transactions, parsed from logs) */ amount?: string; + /** Token amount in base units (present when transfer is detected) */ + tokenAmount?: string; + /** Token contract address (present when transfer is detected) */ + tokenAddress?: Address; + /** Token shorthand (symbol if recognized, otherwise undefined) */ + tokenSymbol?: string; /** Recipient address (present for completed transactions, parsed from logs) */ recipient?: string; /** Reason for transaction failure (present for failed status - describes why the transaction failed on-chain) */ diff --git a/packages/account-sdk/src/interface/payment/utils/erc3770.test.ts b/packages/account-sdk/src/interface/payment/utils/erc3770.test.ts new file mode 100644 index 000000000..ee455735f --- /dev/null +++ b/packages/account-sdk/src/interface/payment/utils/erc3770.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from 'vitest'; + +import { CHAIN_IDS } from '../constants.js'; +import { + decodePaymentId, + encodePaymentId, + getChainShortName, + isERC3770Format, +} from './erc3770.js'; + +describe('ERC-3770 utilities', () => { + describe('getChainShortName', () => { + it('returns the short name for a supported chain', () => { + expect(getChainShortName(CHAIN_IDS.base)).toBe('base'); + }); + + it('returns null for unsupported chains', () => { + expect(getChainShortName(999999)).toBeNull(); + }); + }); + + describe('encodePaymentId', () => { + it('encodes chainId and transaction hash', () => { + const encoded = encodePaymentId(CHAIN_IDS.base, '0xabc123'); + expect(encoded).toBe('base:0xabc123'); + }); + + it('throws for unsupported chain IDs', () => { + expect(() => encodePaymentId(999999, '0xabc123')).toThrow('Unsupported chain ID'); + }); + + it('throws for invalid transaction hashes', () => { + expect(() => encodePaymentId(CHAIN_IDS.base, 'abc123')).toThrow('Invalid transaction hash'); + }); + }); + + describe('decodePaymentId', () => { + it('returns null for legacy IDs without a short name', () => { + expect(decodePaymentId('0xabc123')).toBeNull(); + }); + + it('decodes ERC-3770 formatted IDs', () => { + expect(decodePaymentId('base:0xabc123')).toEqual({ + chainId: CHAIN_IDS.base, + transactionHash: '0xabc123', + }); + }); + + it('throws when the short name is missing', () => { + expect(() => decodePaymentId(':0xabc123')).toThrow('Invalid ERC-3770 format'); + }); + + it('throws when the transaction hash is invalid', () => { + expect(() => decodePaymentId('base:not-a-hash')).toThrow('Invalid ERC-3770 format'); + }); + }); + + describe('isERC3770Format', () => { + it('detects ERC-3770 IDs', () => { + expect(isERC3770Format('base:0xabc123')).toBe(true); + }); + + it('detects legacy IDs', () => { + expect(isERC3770Format('0xabc123')).toBe(false); + }); + }); +}); + + diff --git a/packages/account-sdk/src/interface/payment/utils/erc3770.ts b/packages/account-sdk/src/interface/payment/utils/erc3770.ts new file mode 100644 index 000000000..3dc1aa0fa --- /dev/null +++ b/packages/account-sdk/src/interface/payment/utils/erc3770.ts @@ -0,0 +1,153 @@ +import { CHAIN_IDS } from '../constants.js'; + +/** + * Mapping of chain IDs to ERC-3770 short names + * Short names are from https://github.com/ethereum-lists/chains + */ +const CHAIN_SHORT_NAMES: Record = { + [CHAIN_IDS.ethereum]: 'eth', + [CHAIN_IDS.sepolia]: 'sep', + [CHAIN_IDS.base]: 'base', + [CHAIN_IDS.baseSepolia]: 'base-sepolia', + [CHAIN_IDS.optimism]: 'oeth', + [CHAIN_IDS.optimismSepolia]: 'oeth-sepolia', + [CHAIN_IDS.arbitrum]: 'arb1', + [CHAIN_IDS.polygon]: 'matic', + [CHAIN_IDS.avalanche]: 'avax', + [CHAIN_IDS.BSC]: 'bnb', + [CHAIN_IDS.zora]: 'zora', +} as const; + +type HexString = `0x${string}`; + +export type ChainShortName = (typeof CHAIN_SHORT_NAMES)[keyof typeof CHAIN_SHORT_NAMES]; +export type ERC3770PaymentId = `${ChainShortName}:${HexString}`; + +/** + * Mapping of chain IDs to bundler URLs + * Format: https://api.developer.coinbase.com/rpc/v1/{chain-name}/{api-key} + */ +const BUNDLER_URLS: Record = { + [CHAIN_IDS.base]: 'https://api.developer.coinbase.com/rpc/v1/base/S-fOd2n2Oi4fl4e1Crm83XeDXZ7tkg8O', + [CHAIN_IDS.baseSepolia]: 'https://api.developer.coinbase.com/rpc/v1/base-sepolia/S-fOd2n2Oi4fl4e1Crm83XeDXZ7tkg8O', + // Add more chains as bundler URLs become available +} as const; + +/** + * Get the ERC-3770 short name for a given chain ID + * @param chainId - The chain ID + * @returns The short name, or null if not found + */ +export function getChainShortName(chainId: number): string | null { + return CHAIN_SHORT_NAMES[chainId] ?? null; +} + +/** + * Get the bundler URL for a given chain ID + * @param chainId - The chain ID + * @returns The bundler URL, or null if not available + */ +export function getBundlerUrl(chainId: number): string | null { + return BUNDLER_URLS[chainId] ?? null; +} + +/** + * Get the chain ID from an ERC-3770 short name + * @param shortName - The ERC-3770 short name + * @returns The chain ID, or null if not found + */ +export function getChainIdFromShortName(shortName: string): number | null { + const entry = Object.entries(CHAIN_SHORT_NAMES).find( + ([, name]) => name === shortName.toLowerCase() + ); + return entry ? Number.parseInt(entry[0], 10) : null; +} + +/** + * Decoded payment ID result + */ +export interface DecodedPaymentId { + chainId: number; + transactionHash: HexString; +} + +/** + * Decode an ERC-3770 encoded payment ID + * @param id - The payment ID (either ERC-3770 encoded or legacy format) + * @returns Decoded result with chainId and transactionHash, or null if legacy format + * @throws Error if the format is invalid + */ +export function decodePaymentId(id: string): DecodedPaymentId | null { + if (!id || typeof id !== 'string') { + throw new Error('Invalid payment ID: must be a non-empty string'); + } + + // Check if it's ERC-3770 format (contains colon) + const colonIndex = id.indexOf(':'); + if (colonIndex === -1) { + // Legacy format - no chain ID encoded + return null; + } + + // Extract short name and transaction hash + const shortName = id.slice(0, colonIndex); + const transactionHash = id.slice(colonIndex + 1) as HexString; + + // Validate short name + if (!shortName || shortName.length === 0) { + throw new Error('Invalid ERC-3770 format: short name is required'); + } + + // Validate transaction hash format (should be hex string starting with 0x) + if (!transactionHash || !/^0x[a-fA-F0-9]+$/.test(transactionHash)) { + throw new Error( + 'Invalid ERC-3770 format: transaction hash must be a valid hex string starting with 0x' + ); + } + + // Get chain ID from short name + const chainId = getChainIdFromShortName(shortName); + if (chainId === null) { + throw new Error(`Unknown chain short name: ${shortName}`); + } + + return { + chainId, + transactionHash, + }; +} + +/** + * Encode a payment ID with chain ID using ERC-3770 format + * @param chainId - The chain ID where the payment was executed + * @param transactionHash - The transaction hash + * @returns ERC-3770 encoded payment ID (format: "shortName:transactionHash") + * @throws Error if chainId is not supported + */ +export function encodePaymentId(chainId: number, transactionHash: string): ERC3770PaymentId { + if (!transactionHash || typeof transactionHash !== 'string') { + throw new Error('Invalid transaction hash: must be a non-empty string'); + } + + // Validate transaction hash format + if (!/^0x[a-fA-F0-9]+$/.test(transactionHash)) { + throw new Error('Invalid transaction hash: must be a valid hex string starting with 0x'); + } + + const shortName = getChainShortName(chainId); + if (shortName === null) { + throw new Error(`Unsupported chain ID: ${chainId}. Cannot encode payment ID.`); + } + + return `${shortName}:${transactionHash}` as ERC3770PaymentId; +} + +/** + * Check if a payment ID is in ERC-3770 format + * @param id - The payment ID to check + * @returns True if the ID contains a colon (ERC-3770 format), false otherwise + */ +export function isERC3770Format(id: string): boolean { + return typeof id === 'string' && id.includes(':'); +} + diff --git a/packages/account-sdk/src/interface/payment/utils/sdkManager.ts b/packages/account-sdk/src/interface/payment/utils/sdkManager.ts index f1db27100..d645af583 100644 --- a/packages/account-sdk/src/interface/payment/utils/sdkManager.ts +++ b/packages/account-sdk/src/interface/payment/utils/sdkManager.ts @@ -6,7 +6,7 @@ import type { PayerInfoResponses, PaymentSDKConfig } from '../types.js'; /** * Type for wallet_sendCalls request parameters */ -type WalletSendCallsRequestParams = { +export type WalletSendCallsRequestParams = { version: string; chainId: number; calls: Array<{ @@ -131,15 +131,12 @@ export async function executePayment( * @param sdkConfig - Optional advanced SDK configuration * @returns The payment execution result */ -export async function executePaymentWithSDK( +export async function executePaymentOnChain( requestParams: WalletSendCallsRequestParams, - testnet: boolean, + chainId: number, telemetry: boolean = true, sdkConfig?: PaymentSDKConfig ): Promise { - const network = testnet ? 'baseSepolia' : 'base'; - const chainId = CHAIN_IDS[network]; - const sdk = createEphemeralSDK(chainId, telemetry, sdkConfig); const provider = sdk.getProvider(); @@ -151,3 +148,18 @@ export async function executePaymentWithSDK( await provider.disconnect(); } } + +/** + * Legacy wrapper that derives the chain ID from the Base / Base Sepolia flag + */ +export async function executePaymentWithSDK( + requestParams: WalletSendCallsRequestParams, + testnet: boolean, + telemetry: boolean = true, + sdkConfig?: PaymentSDKConfig +): Promise { + const network = testnet ? 'baseSepolia' : 'base'; + const chainId = CHAIN_IDS[network]; + + return executePaymentOnChain(requestParams, chainId, telemetry, sdkConfig); +} diff --git a/packages/account-sdk/src/interface/payment/utils/tokenRegistry.ts b/packages/account-sdk/src/interface/payment/utils/tokenRegistry.ts new file mode 100644 index 000000000..70124aa6a --- /dev/null +++ b/packages/account-sdk/src/interface/payment/utils/tokenRegistry.ts @@ -0,0 +1,90 @@ +import { getAddress, isAddress, type Address } from 'viem'; + +import { + ETH_PLACEHOLDER_ADDRESS, + STABLECOIN_ADDRESS_LOOKUP, + STABLECOIN_WHITELIST, +} from '../constants.js'; +import type { TokenInput } from '../types.js'; + +export interface ResolvedToken { + address: Address; + symbol?: string; + decimals?: number; + isNativeEth: boolean; +} + +const ETH_PLACEHOLDER_LOWER = ETH_PLACEHOLDER_ADDRESS.toLowerCase(); +const SUPPORTED_STABLECOIN_SYMBOLS = Object.keys(STABLECOIN_WHITELIST); + +/** + * Checks whether a string represents the native ETH placeholder. + */ +export function isEthPlaceholder(value: string): boolean { + return value.trim().toLowerCase() === ETH_PLACEHOLDER_LOWER; +} + +/** + * Resolves a token input (symbol or address) into a concrete contract address. + */ +export function resolveTokenAddress(token: TokenInput, chainId: number): ResolvedToken { + if (typeof token !== 'string' || token.trim().length === 0) { + throw new Error('Token is required'); + } + + const trimmed = token.trim(); + + if (isEthPlaceholder(trimmed)) { + return { + address: getAddress(ETH_PLACEHOLDER_ADDRESS), + symbol: 'ETH', + decimals: 18, + isNativeEth: true, + }; + } + + if (isAddress(trimmed)) { + return { + address: getAddress(trimmed), + isNativeEth: isEthPlaceholder(trimmed), + }; + } + + const normalizedSymbol = trimmed.toUpperCase(); + if (normalizedSymbol in STABLECOIN_WHITELIST) { + const stablecoin = STABLECOIN_WHITELIST[normalizedSymbol as keyof typeof STABLECOIN_WHITELIST]; + const address = stablecoin.addresses[chainId]; + + if (!address) { + throw new Error( + `Token ${normalizedSymbol} is not whitelisted on chain ${chainId}. Provide a contract address instead.` + ); + } + + return { + address: getAddress(address), + symbol: stablecoin.symbol, + decimals: stablecoin.decimals, + isNativeEth: false, + }; + } + + throw new Error( + `Unknown token "${token}". Provide a contract address or one of the supported symbols: ${SUPPORTED_STABLECOIN_SYMBOLS.join( + ', ' + )}, ETH` + ); +} + +/** + * Returns metadata for a whitelisted stablecoin by address, if available. + */ +export function getStablecoinMetadataByAddress(address?: string) { + if (!address) { + return undefined; + } + + return STABLECOIN_ADDRESS_LOOKUP[address.toLowerCase()]; +} + + diff --git a/packages/account-sdk/src/interface/payment/utils/translateTokenPayment.ts b/packages/account-sdk/src/interface/payment/utils/translateTokenPayment.ts new file mode 100644 index 000000000..2ba8047a5 --- /dev/null +++ b/packages/account-sdk/src/interface/payment/utils/translateTokenPayment.ts @@ -0,0 +1,102 @@ +import { encodeFunctionData, toHex, type Address, type Hex } from 'viem'; + +import { ERC20_TRANSFER_ABI } from '../constants.js'; +import type { PayerInfo, PaymasterOptions } from '../types.js'; +import type { WalletSendCallsRequestParams } from './sdkManager.js'; +import type { ResolvedToken } from './tokenRegistry.js'; + +function buildDataCallbackCapability(payerInfo?: PayerInfo) { + if (!payerInfo || payerInfo.requests.length === 0) { + return undefined; + } + + return { + requests: payerInfo.requests.map((request) => ({ + type: request.type, + optional: request.optional ?? false, + })), + ...(payerInfo.callbackURL && { callbackURL: payerInfo.callbackURL }), + }; +} + +function buildPaymasterCapability(paymaster: PaymasterOptions | undefined) { + if (!paymaster) { + throw new Error('paymaster configuration is required'); + } + + const capability: Record = {}; + + if (paymaster.url) { + capability.url = paymaster.url; + } + + if (paymaster.context) { + capability.context = paymaster.context; + } + + if (paymaster.paymasterAndData) { + capability.paymasterAndData = paymaster.paymasterAndData; + } + + if (Object.keys(capability).length === 0) { + throw new Error('paymaster configuration must include either a url or paymasterAndData value'); + } + + return capability; +} + +export function buildTokenPaymentRequest({ + recipient, + amount, + chainId, + token, + payerInfo, + paymaster, +}: { + recipient: Address; + amount: bigint; + chainId: number; + token: ResolvedToken; + payerInfo?: PayerInfo; + paymaster: PaymasterOptions; +}): WalletSendCallsRequestParams { + const capabilities: Record = {}; + + const paymasterCapability = buildPaymasterCapability(paymaster); + capabilities.paymasterService = paymasterCapability; + + const dataCallbackCapability = buildDataCallbackCapability(payerInfo); + if (dataCallbackCapability) { + capabilities.dataCallback = dataCallbackCapability; + } + + const calls = + token.isNativeEth === true + ? ([ + { + to: recipient as Hex, + data: '0x' as Hex, + value: toHex(amount), + }, + ] as const) + : ([ + { + to: token.address as Hex, + data: encodeFunctionData({ + abi: ERC20_TRANSFER_ABI, + functionName: 'transfer', + args: [recipient, amount], + }), + value: '0x0' as Hex, + }, + ] as const); + + return { + version: '2.0.0', + chainId, + calls: [...calls], + capabilities, + }; +} + + diff --git a/packages/account-sdk/src/interface/payment/utils/validation.ts b/packages/account-sdk/src/interface/payment/utils/validation.ts index 6169cdb1f..aae495149 100644 --- a/packages/account-sdk/src/interface/payment/utils/validation.ts +++ b/packages/account-sdk/src/interface/payment/utils/validation.ts @@ -50,3 +50,71 @@ export function normalizeAddress(address: string): Address { throw new Error('Invalid address: must be a valid Ethereum address'); } } + +/** + * Validates that a base-unit amount (wei) is provided as a positive integer string + * @param amount - Amount expressed in smallest unit (e.g., wei) + * @returns bigint representation of the amount + */ +export function validateBaseUnitAmount(amount: string): bigint { + if (typeof amount !== 'string') { + throw new Error('Invalid amount: must be provided as a string'); + } + + const trimmed = amount.trim(); + if (trimmed.length === 0) { + throw new Error('Invalid amount: value is required'); + } + + if (!/^\d+$/.test(trimmed)) { + throw new Error('Invalid amount: payWithToken expects an integer amount in wei'); + } + + const parsed = BigInt(trimmed); + if (parsed <= 0n) { + throw new Error('Invalid amount: must be greater than 0'); + } + + return parsed; +} + +/** + * Normalizes a user-supplied chain ID (number, decimal string, or hex string) + * into a positive integer. + * @param chainId - Chain identifier + */ +export function normalizeChainId(chainId: number | string): number { + if (typeof chainId === 'number') { + if (!Number.isFinite(chainId) || !Number.isInteger(chainId) || chainId <= 0) { + throw new Error('Invalid chainId: must be a positive integer'); + } + return chainId; + } + + if (typeof chainId !== 'string') { + throw new Error('Invalid chainId: must be a number or a string'); + } + + const trimmed = chainId.trim(); + if (trimmed.length === 0) { + throw new Error('Invalid chainId: value is required'); + } + + let parsedValue: number; + + if (/^0x/i.test(trimmed)) { + try { + parsedValue = Number(BigInt(trimmed)); + } catch { + throw new Error('Invalid chainId: hex string could not be parsed'); + } + } else { + parsedValue = Number.parseInt(trimmed, 10); + } + + if (!Number.isFinite(parsedValue) || !Number.isInteger(parsedValue) || parsedValue <= 0) { + throw new Error('Invalid chainId: must resolve to a positive integer'); + } + + return parsedValue; +} From 81cea6ec7a65642ff1dfba1513fea41bb807f687 Mon Sep 17 00:00:00 2001 From: Spencer Stock Date: Mon, 24 Nov 2025 16:08:53 -0700 Subject: [PATCH 03/12] Fix linting, formatting, and type errors --- .../add-sub-account/components/SendCalls.tsx | 21 ++-- .../src/pages/add-sub-account/index.page.tsx | 2 +- .../pay-playground/constants/playground.ts | 2 +- .../pay-playground/hooks/useCodeExecution.ts | 4 +- .../src/pages/pay-playground/index.page.tsx | 114 ------------------ .../pay-playground/utils/codeSanitizer.ts | 9 +- .../utils/payerInfoTransform.ts | 14 +-- .../utils/paymasterTransform.ts | 12 +- .../src/core/telemetry/logEvent.ts | 2 + .../payment/getPaymentStatus.test.ts | 3 +- .../src/interface/payment/getPaymentStatus.ts | 4 +- .../interface/payment/payWithToken.test.ts | 1 - .../src/interface/payment/payWithToken.ts | 7 +- .../interface/payment/utils/erc3770.test.ts | 9 +- .../src/interface/payment/utils/erc3770.ts | 7 +- .../interface/payment/utils/tokenRegistry.ts | 4 +- .../payment/utils/translateTokenPayment.ts | 2 - .../src/interface/payment/utils/validation.ts | 2 +- .../account-sdk/src/ui/Dialog/Dialog.test.tsx | 14 +-- 19 files changed, 53 insertions(+), 180 deletions(-) diff --git a/examples/testapp/src/pages/add-sub-account/components/SendCalls.tsx b/examples/testapp/src/pages/add-sub-account/components/SendCalls.tsx index e25454267..82853d1f1 100644 --- a/examples/testapp/src/pages/add-sub-account/components/SendCalls.tsx +++ b/examples/testapp/src/pages/add-sub-account/components/SendCalls.tsx @@ -1,5 +1,5 @@ import type { TokenPaymentSuccess } from '@base-org/account'; -import { createBaseAccountSDK, payWithToken } from '@base-org/account'; +import { payWithToken } from '@base-org/account'; import { CheckCircleIcon, ExternalLinkIcon } from '@chakra-ui/icons'; import { Box, Button, Flex, HStack, Icon, Link, Text, VStack } from '@chakra-ui/react'; import { useCallback, useState } from 'react'; @@ -23,22 +23,16 @@ function stripChainPrefix(txHash: string): string { function getBlockExplorerUrl(chainId: number, txHash: string): string { const hash = stripChainPrefix(txHash); - + const explorers: Record = { 8453: 'https://basescan.org/tx', // Base mainnet 84532: 'https://sepolia.basescan.org/tx', // Base Sepolia }; - + return `${explorers[chainId] || 'https://basescan.org/tx'}/${hash}`; } -export function SendCalls({ - sdk, - subAccountAddress, -}: { - sdk: ReturnType; - subAccountAddress: string; -}) { +export function SendCalls() { const [paymentResult, setPaymentResult] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -124,7 +118,12 @@ export function SendCalls({ _dark={{ bg: 'green.900', borderColor: 'green.700' }} > - + Payment Successful! diff --git a/examples/testapp/src/pages/add-sub-account/index.page.tsx b/examples/testapp/src/pages/add-sub-account/index.page.tsx index 987fad107..37f0a5077 100644 --- a/examples/testapp/src/pages/add-sub-account/index.page.tsx +++ b/examples/testapp/src/pages/add-sub-account/index.page.tsx @@ -90,7 +90,7 @@ export default function SubAccounts() { signerFn={getSubAccountSigner} /> - + diff --git a/examples/testapp/src/pages/pay-playground/constants/playground.ts b/examples/testapp/src/pages/pay-playground/constants/playground.ts index 04af86cd5..66a4b9f37 100644 --- a/examples/testapp/src/pages/pay-playground/constants/playground.ts +++ b/examples/testapp/src/pages/pay-playground/constants/playground.ts @@ -117,7 +117,7 @@ export const GET_PAYMENT_STATUS_QUICK_TIPS = [ ]; export const PAY_WITH_TOKEN_QUICK_TIPS = [ - 'Amount is specified in the token\'s smallest unit (e.g., wei for ETH, or smallest unit for ERC20 tokens)', + "Amount is specified in the token's smallest unit (e.g., wei for ETH, or smallest unit for ERC20 tokens)", 'For USDC (6 decimals), 1 USDC = 1000000', 'For tokens with 18 decimals, 1 token = 1000000000000000000', 'Token can be a contract address or a supported symbol (e.g., "USDC", "WETH")', diff --git a/examples/testapp/src/pages/pay-playground/hooks/useCodeExecution.ts b/examples/testapp/src/pages/pay-playground/hooks/useCodeExecution.ts index 41b1f8f75..549c9171b 100644 --- a/examples/testapp/src/pages/pay-playground/hooks/useCodeExecution.ts +++ b/examples/testapp/src/pages/pay-playground/hooks/useCodeExecution.ts @@ -10,7 +10,9 @@ import { useConsoleCapture } from './useConsoleCapture'; export const useCodeExecution = () => { const [isLoading, setIsLoading] = useState(false); - const [result, setResult] = useState(null); + const [result, setResult] = useState< + PaymentResult | PaymentStatus | PayWithTokenResult | SwapResult | SwapQuote | SwapStatus | null + >(null); const [error, setError] = useState(null); const [consoleOutput, setConsoleOutput] = useState([]); const { captureConsole } = useConsoleCapture(); diff --git a/examples/testapp/src/pages/pay-playground/index.page.tsx b/examples/testapp/src/pages/pay-playground/index.page.tsx index a7c117d39..477144558 100644 --- a/examples/testapp/src/pages/pay-playground/index.page.tsx +++ b/examples/testapp/src/pages/pay-playground/index.page.tsx @@ -3,29 +3,20 @@ import { CodeEditor, Header, Output, QuickTips } from './components'; import { DEFAULT_GET_PAYMENT_STATUS_CODE, DEFAULT_PAY_CODE, - DEFAULT_PAY_WITH_TOKEN_CODE, GET_PAYMENT_STATUS_QUICK_TIPS, PAY_CODE_WITH_PAYER_INFO, PAY_QUICK_TIPS, - PAY_WITH_TOKEN_CODE_WITH_PAYER_INFO, - PAY_WITH_TOKEN_PRESETS, - PAY_WITH_TOKEN_QUICK_TIPS, } from './constants'; import { useCodeExecution } from './hooks'; -import { togglePayerInfoInCode, extractPaymasterUrl, updatePaymasterUrl } from './utils'; import styles from './styles/Home.module.css'; function PayPlayground() { const [includePayerInfo, setIncludePayerInfo] = useState(false); const [payCode, setPayCode] = useState(DEFAULT_PAY_CODE); const [getPaymentStatusCode, setGetPaymentStatusCode] = useState(DEFAULT_GET_PAYMENT_STATUS_CODE); - const [includePayerInfoToken, setIncludePayerInfoToken] = useState(false); - const [payWithTokenCode, setPayWithTokenCode] = useState(DEFAULT_PAY_WITH_TOKEN_CODE); - const [paymasterUrl, setPaymasterUrl] = useState(''); const payExecution = useCodeExecution(); const getPaymentStatusExecution = useCodeExecution(); - const payWithTokenExecution = useCodeExecution(); const handlePayExecute = () => { payExecution.executeCode(payCode); @@ -53,47 +44,6 @@ function PayPlayground() { getPaymentStatusExecution.reset(); }; - const handlePayWithTokenExecute = () => { - payWithTokenExecution.executeCode(payWithTokenCode); - }; - - const handlePayWithTokenReset = () => { - setIncludePayerInfoToken(false); - setPaymasterUrl(''); - setPayWithTokenCode(DEFAULT_PAY_WITH_TOKEN_CODE); - payWithTokenExecution.reset(); - }; - - const handlePayerInfoTokenToggle = (checked: boolean) => { - setIncludePayerInfoToken(checked); - // Modify existing code to add/remove payerInfo instead of replacing it - const modifiedCode = togglePayerInfoInCode(payWithTokenCode, checked); - setPayWithTokenCode(modifiedCode); - payWithTokenExecution.reset(); - }; - - const handlePaymasterUrlChange = (url: string) => { - setPaymasterUrl(url); - // Update the code with the new paymaster URL - const updatedCode = updatePaymasterUrl(payWithTokenCode, url); - setPayWithTokenCode(updatedCode); - }; - - const handlePayWithTokenCodeChange = (code: string) => { - setPayWithTokenCode(code); - // Extract paymaster URL from code and sync the textbox - const extractedUrl = extractPaymasterUrl(code); - if (extractedUrl && extractedUrl !== paymasterUrl) { - setPaymasterUrl(extractedUrl); - } - }; - - const handlePayWithTokenPresetChange = (code: string) => { - // Apply the current paymasterUrl to the preset code (keep paymasterUrl intact) - const updatedCode = paymasterUrl ? updatePaymasterUrl(code, paymasterUrl) : code; - setPayWithTokenCode(updatedCode); - }; - // Watch for successful payment results and update getPaymentStatus code with the transaction ID useEffect(() => { if ( @@ -121,33 +71,6 @@ try { } }, [payExecution.result]); - // Watch for successful payWithToken results and update getPaymentStatus code with the transaction ID - useEffect(() => { - if ( - payWithTokenExecution.result && - 'success' in payWithTokenExecution.result && - payWithTokenExecution.result.success && - payWithTokenExecution.result.id - ) { - const transactionId = payWithTokenExecution.result.id; - const updatedCode = `import { base } from '@base-org/account' - -try { - const result = await base.getPaymentStatus({ - id: '${transactionId}', // Automatically filled with your recent transaction - testnet: true - }) - - return result; -} catch (error) { - // This will catch network errors if any occur - console.error('Failed to check payment status:', error.message); - throw error; -}`; - setGetPaymentStatusCode(updatedCode); - } - }, [payWithTokenExecution.result]); - return (

@@ -184,43 +107,6 @@ try {
- {/* payWithToken Section */} -
-

payWithToken Function

-

Send ERC20 token payments on any supported chain

- -
-
- - -
- -
- -
-
-
- {/* getPaymentStatus Section */}

getPaymentStatus Function

diff --git a/examples/testapp/src/pages/pay-playground/utils/codeSanitizer.ts b/examples/testapp/src/pages/pay-playground/utils/codeSanitizer.ts index a50e7fe9f..0975c5826 100644 --- a/examples/testapp/src/pages/pay-playground/utils/codeSanitizer.ts +++ b/examples/testapp/src/pages/pay-playground/utils/codeSanitizer.ts @@ -3,7 +3,14 @@ import * as acorn from 'acorn'; // Define the whitelist of allowed operations export const WHITELIST = { // Allowed SDK functions - allowedFunctions: ['pay', 'getPaymentStatus', 'payWithToken', 'swap', 'getSwapQuote', 'getSwapStatus'], + allowedFunctions: [ + 'pay', + 'getPaymentStatus', + 'payWithToken', + 'swap', + 'getSwapQuote', + 'getSwapStatus', + ], // Allowed object properties and methods allowedObjects: { diff --git a/examples/testapp/src/pages/pay-playground/utils/payerInfoTransform.ts b/examples/testapp/src/pages/pay-playground/utils/payerInfoTransform.ts index b484b297e..024c91506 100644 --- a/examples/testapp/src/pages/pay-playground/utils/payerInfoTransform.ts +++ b/examples/testapp/src/pages/pay-playground/utils/payerInfoTransform.ts @@ -44,11 +44,12 @@ export function togglePayerInfoInCode(code: string, includePayerInfo: boolean): const afterBrace = code.substring(i); // Check if we need a comma before payerInfo - const needsComma = !beforeBrace.endsWith(',') && - !beforeBrace.endsWith('{') && - beforeBrace.length > 0; + const needsComma = + !beforeBrace.endsWith(',') && !beforeBrace.endsWith('{') && beforeBrace.length > 0; - return beforeBrace + (needsComma ? payerInfoBlock : payerInfoBlock.substring(1)) + afterBrace; + return ( + beforeBrace + (needsComma ? payerInfoBlock : payerInfoBlock.substring(1)) + afterBrace + ); } } i++; @@ -66,8 +67,8 @@ export function togglePayerInfoInCode(code: string, includePayerInfo: boolean): // If that didn't work, try a simpler pattern if (modifiedCode === code) { - // Fallback: match payerInfo property more broadly - const fallbackRegex = /,\s*payerInfo\s*:\s*\{[^}]*\{[^}]*\}[^}]*\}/s; + // Fallback: match payerInfo property more broadly (using [\s\S] instead of . with s flag) + const fallbackRegex = /,\s*payerInfo\s*:\s*\{[^}]*\{[^}]*\}[^}]*\}/; modifiedCode = modifiedCode.replace(fallbackRegex, ''); } @@ -86,4 +87,3 @@ export function togglePayerInfoInCode(code: string, includePayerInfo: boolean): return code; } - diff --git a/examples/testapp/src/pages/pay-playground/utils/paymasterTransform.ts b/examples/testapp/src/pages/pay-playground/utils/paymasterTransform.ts index ce5a288b6..bb71844bc 100644 --- a/examples/testapp/src/pages/pay-playground/utils/paymasterTransform.ts +++ b/examples/testapp/src/pages/pay-playground/utils/paymasterTransform.ts @@ -18,7 +18,7 @@ export function extractPaymasterUrl(code: string): string | undefined { export function updatePaymasterUrl(code: string, paymasterUrl: string): string { // Check if paymaster already exists in the code const hasPaymaster = /paymaster\s*:\s*\{/.test(code); - + if (!hasPaymaster) { // If no paymaster exists, we need to add it // Find the payWithToken call and locate where to insert paymaster @@ -43,16 +43,17 @@ export function updatePaymasterUrl(code: string, paymasterUrl: string): string { const afterBrace = code.substring(i); // Check if we need a comma before paymaster - const needsComma = !beforeBrace.endsWith(',') && - !beforeBrace.endsWith('{') && - beforeBrace.length > 0; + const needsComma = + !beforeBrace.endsWith(',') && !beforeBrace.endsWith('{') && beforeBrace.length > 0; const paymasterBlock = `, paymaster: { url: '${paymasterUrl}' }`; - return beforeBrace + (needsComma ? paymasterBlock : paymasterBlock.substring(1)) + afterBrace; + return ( + beforeBrace + (needsComma ? paymasterBlock : paymasterBlock.substring(1)) + afterBrace + ); } } i++; @@ -68,4 +69,3 @@ export function updatePaymasterUrl(code: string, paymasterUrl: string): string { return code; } - diff --git a/packages/account-sdk/src/core/telemetry/logEvent.ts b/packages/account-sdk/src/core/telemetry/logEvent.ts index 66b48423f..c63008a9e 100644 --- a/packages/account-sdk/src/core/telemetry/logEvent.ts +++ b/packages/account-sdk/src/core/telemetry/logEvent.ts @@ -71,6 +71,8 @@ type CCAEventData = { testnet?: boolean; status?: string; periodInDays?: number; + token?: string; + chainId?: number; }; type AnalyticsEventData = { diff --git a/packages/account-sdk/src/interface/payment/getPaymentStatus.test.ts b/packages/account-sdk/src/interface/payment/getPaymentStatus.test.ts index 317e0b57e..ac01ac929 100644 --- a/packages/account-sdk/src/interface/payment/getPaymentStatus.test.ts +++ b/packages/account-sdk/src/interface/payment/getPaymentStatus.test.ts @@ -85,8 +85,7 @@ describe('getPaymentStatus', () => { }); it('should decode ERC-3770 encoded IDs and ignore the legacy testnet flag', async () => { - const transactionHash = - '0xabc1230000000000000000000000000000000000000000000000000000000000'; + const transactionHash = '0xabc1230000000000000000000000000000000000000000000000000000000000'; const mockReceipt = { jsonrpc: '2.0', id: 1, diff --git a/packages/account-sdk/src/interface/payment/getPaymentStatus.ts b/packages/account-sdk/src/interface/payment/getPaymentStatus.ts index 39d406afd..3cc535fba 100644 --- a/packages/account-sdk/src/interface/payment/getPaymentStatus.ts +++ b/packages/account-sdk/src/interface/payment/getPaymentStatus.ts @@ -175,7 +175,7 @@ export async function getPaymentStatus(options: PaymentStatusOptions): Promise

{ ); }); }); - diff --git a/packages/account-sdk/src/interface/payment/payWithToken.ts b/packages/account-sdk/src/interface/payment/payWithToken.ts index 744ec20ec..e8ad80917 100644 --- a/packages/account-sdk/src/interface/payment/payWithToken.ts +++ b/packages/account-sdk/src/interface/payment/payWithToken.ts @@ -1,7 +1,7 @@ import { - logPayWithTokenCompleted, - logPayWithTokenError, - logPayWithTokenStarted, + logPayWithTokenCompleted, + logPayWithTokenError, + logPayWithTokenStarted, } from ':core/telemetry/events/payment.js'; import { CHAIN_IDS } from './constants.js'; import type { PayWithTokenOptions, PayWithTokenResult, PaymentSDKConfig } from './types.js'; @@ -114,4 +114,3 @@ export async function payWithToken(options: PayWithTokenOptions): Promise { describe('getChainShortName', () => { @@ -65,5 +60,3 @@ describe('ERC-3770 utilities', () => { }); }); }); - - diff --git a/packages/account-sdk/src/interface/payment/utils/erc3770.ts b/packages/account-sdk/src/interface/payment/utils/erc3770.ts index 3dc1aa0fa..72b190a87 100644 --- a/packages/account-sdk/src/interface/payment/utils/erc3770.ts +++ b/packages/account-sdk/src/interface/payment/utils/erc3770.ts @@ -28,8 +28,10 @@ export type ERC3770PaymentId = `${ChainShortName}:${HexString}`; * Format: https://api.developer.coinbase.com/rpc/v1/{chain-name}/{api-key} */ const BUNDLER_URLS: Record = { - [CHAIN_IDS.base]: 'https://api.developer.coinbase.com/rpc/v1/base/S-fOd2n2Oi4fl4e1Crm83XeDXZ7tkg8O', - [CHAIN_IDS.baseSepolia]: 'https://api.developer.coinbase.com/rpc/v1/base-sepolia/S-fOd2n2Oi4fl4e1Crm83XeDXZ7tkg8O', + [CHAIN_IDS.base]: + 'https://api.developer.coinbase.com/rpc/v1/base/S-fOd2n2Oi4fl4e1Crm83XeDXZ7tkg8O', + [CHAIN_IDS.baseSepolia]: + 'https://api.developer.coinbase.com/rpc/v1/base-sepolia/S-fOd2n2Oi4fl4e1Crm83XeDXZ7tkg8O', // Add more chains as bundler URLs become available } as const; @@ -150,4 +152,3 @@ export function encodePaymentId(chainId: number, transactionHash: string): ERC37 export function isERC3770Format(id: string): boolean { return typeof id === 'string' && id.includes(':'); } - diff --git a/packages/account-sdk/src/interface/payment/utils/tokenRegistry.ts b/packages/account-sdk/src/interface/payment/utils/tokenRegistry.ts index 70124aa6a..9ded03689 100644 --- a/packages/account-sdk/src/interface/payment/utils/tokenRegistry.ts +++ b/packages/account-sdk/src/interface/payment/utils/tokenRegistry.ts @@ -53,7 +53,7 @@ export function resolveTokenAddress(token: TokenInput, chainId: number): Resolve const normalizedSymbol = trimmed.toUpperCase(); if (normalizedSymbol in STABLECOIN_WHITELIST) { const stablecoin = STABLECOIN_WHITELIST[normalizedSymbol as keyof typeof STABLECOIN_WHITELIST]; - const address = stablecoin.addresses[chainId]; + const address = stablecoin.addresses[chainId as keyof typeof stablecoin.addresses]; if (!address) { throw new Error( @@ -86,5 +86,3 @@ export function getStablecoinMetadataByAddress(address?: string) { return STABLECOIN_ADDRESS_LOOKUP[address.toLowerCase()]; } - - diff --git a/packages/account-sdk/src/interface/payment/utils/translateTokenPayment.ts b/packages/account-sdk/src/interface/payment/utils/translateTokenPayment.ts index 2ba8047a5..59c3b8563 100644 --- a/packages/account-sdk/src/interface/payment/utils/translateTokenPayment.ts +++ b/packages/account-sdk/src/interface/payment/utils/translateTokenPayment.ts @@ -98,5 +98,3 @@ export function buildTokenPaymentRequest({ capabilities, }; } - - diff --git a/packages/account-sdk/src/interface/payment/utils/validation.ts b/packages/account-sdk/src/interface/payment/utils/validation.ts index aae495149..6f273b453 100644 --- a/packages/account-sdk/src/interface/payment/utils/validation.ts +++ b/packages/account-sdk/src/interface/payment/utils/validation.ts @@ -71,7 +71,7 @@ export function validateBaseUnitAmount(amount: string): bigint { } const parsed = BigInt(trimmed); - if (parsed <= 0n) { + if (parsed <= BigInt(0)) { throw new Error('Invalid amount: must be greater than 0'); } diff --git a/packages/account-sdk/src/ui/Dialog/Dialog.test.tsx b/packages/account-sdk/src/ui/Dialog/Dialog.test.tsx index a33f6de96..29e17c8d2 100644 --- a/packages/account-sdk/src/ui/Dialog/Dialog.test.tsx +++ b/packages/account-sdk/src/ui/Dialog/Dialog.test.tsx @@ -13,15 +13,6 @@ const renderDialogContainer = (props?: Partial) => ); describe('DialogContainer', () => { - beforeEach(() => { - vi.useFakeTimers(); - vi.spyOn(window, 'setTimeout'); - }); - - afterEach(() => { - vi.useRealTimers(); - }); - test('renders with title and message', () => { renderDialogContainer(); @@ -30,13 +21,12 @@ describe('DialogContainer', () => { }); test('renders hidden initially', () => { + const setTimeoutSpy = vi.spyOn(window, 'setTimeout'); renderDialogContainer(); const hiddenClass = document.getElementsByClassName('-base-acc-sdk-dialog-instance-hidden'); expect(hiddenClass.length).toEqual(1); - - vi.runAllTimers(); - expect(setTimeout).toHaveBeenCalledTimes(1); + expect(setTimeoutSpy).toHaveBeenCalled(); }); test('shows action button when provided', () => { From da3f46926adea2d866891a05d6d86bb63a2be849 Mon Sep 17 00:00:00 2001 From: Spencer Stock Date: Mon, 24 Nov 2025 22:01:30 -0700 Subject: [PATCH 04/12] Remove unrelated demo and test changes --- .../add-sub-account/components/SendCalls.tsx | 230 +++++------------- .../src/pages/add-sub-account/index.page.tsx | 2 +- .../account-sdk/src/ui/Dialog/Dialog.test.tsx | 14 +- 3 files changed, 71 insertions(+), 175 deletions(-) diff --git a/examples/testapp/src/pages/add-sub-account/components/SendCalls.tsx b/examples/testapp/src/pages/add-sub-account/components/SendCalls.tsx index 82853d1f1..af8d1c2f5 100644 --- a/examples/testapp/src/pages/add-sub-account/components/SendCalls.tsx +++ b/examples/testapp/src/pages/add-sub-account/components/SendCalls.tsx @@ -1,77 +1,58 @@ -import type { TokenPaymentSuccess } from '@base-org/account'; -import { payWithToken } from '@base-org/account'; -import { CheckCircleIcon, ExternalLinkIcon } from '@chakra-ui/icons'; -import { Box, Button, Flex, HStack, Icon, Link, Text, VStack } from '@chakra-ui/react'; +import { createBaseAccountSDK } from '@base-org/account'; +import { Box, Button } from '@chakra-ui/react'; import { useCallback, useState } from 'react'; -import { formatUnits } from 'viem'; - -// Common token symbols and decimals -const TOKEN_CONFIG: Record = { - '0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4': { symbol: 'USDC', decimals: 6 }, // Base USDC - '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913': { symbol: 'USDC', decimals: 6 }, // Base mainnet USDC -}; - -function getTokenInfo(tokenAddress: string) { - const lowerAddress = tokenAddress.toLowerCase(); - return TOKEN_CONFIG[lowerAddress] || { symbol: 'Token', decimals: 18 }; -} - -function stripChainPrefix(txHash: string): string { - // Remove chain prefix if present (e.g., "base:0x..." -> "0x...") - return txHash.includes(':') ? txHash.split(':')[1] : txHash; -} - -function getBlockExplorerUrl(chainId: number, txHash: string): string { - const hash = stripChainPrefix(txHash); - - const explorers: Record = { - 8453: 'https://basescan.org/tx', // Base mainnet - 84532: 'https://sepolia.basescan.org/tx', // Base Sepolia - }; - - return `${explorers[chainId] || 'https://basescan.org/tx'}/${hash}`; -} - -export function SendCalls() { - const [paymentResult, setPaymentResult] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const handlePayWithToken = useCallback(async () => { - setIsLoading(true); - setError(null); - setPaymentResult(null); +import { numberToHex } from 'viem'; +import { baseSepolia } from 'viem/chains'; + +export function SendCalls({ + sdk, + subAccountAddress, +}: { + sdk: ReturnType; + subAccountAddress: string; +}) { + const [state, setState] = useState(); + const handleSendCalls = useCallback(async () => { + if (!sdk) { + return; + } + const provider = sdk.getProvider(); try { - // Example payment with token - const result = await payWithToken({ - amount: '100000000000000000000', // 100 tokens (18 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - token: '0xAC1Bd2486aAf3B5C0fc3Fd868558b082a531B2B4', // USDC on Base Sepolia - chainId: 8453, - paymaster: { - url: 'https://api.developer.coinbase.com/rpc/v1/base/S-fOd2n2Oi4fl4e1Crm83XeDXZ7tkg8O', - }, + const response = await provider.request({ + method: 'wallet_sendCalls', + params: [ + { + chainId: numberToHex(baseSepolia.id), + from: subAccountAddress, + calls: [ + { + to: '0x000000000000000000000000000000000000dead', + data: '0x', + value: '0x0', + }, + ], + version: '1', + capabilities: { + paymasterService: { + url: 'https://api.developer.coinbase.com/rpc/v1/base-sepolia/S-fOd2n2Oi4fl4e1Crm83XeDXZ7tkg8O', + }, + }, + }, + ], }); - - if (result.success) { - setPaymentResult(result); - } + console.info('response', response); + setState(response as string); } catch (e) { - console.error('Payment error:', e); - setError(e instanceof Error ? e.message : 'Payment failed'); - } finally { - setIsLoading(false); + console.error('error', e); } - }, []); + }, [sdk, subAccountAddress]); return ( - + <> - - {error && ( - - - - ❌ Payment Failed - - - - {error} - - - )} - - {paymentResult && ( + {state && ( - - - - Payment Successful! - - - - - {/* Amount */} - - - Amount - - - {formatUnits( - BigInt(paymentResult.tokenAmount), - getTokenInfo(paymentResult.tokenAddress).decimals - )}{' '} - {paymentResult.token || getTokenInfo(paymentResult.tokenAddress).symbol} - - - - {/* Recipient */} - - - Recipient - - - - {paymentResult.to} - - - - - {/* Transaction ID */} - - - Transaction ID - - - - {stripChainPrefix(paymentResult.id)} - - - - View on Block Explorer - - - - + {JSON.stringify(state, null, 2)} )} - + ); } diff --git a/examples/testapp/src/pages/add-sub-account/index.page.tsx b/examples/testapp/src/pages/add-sub-account/index.page.tsx index 37f0a5077..987fad107 100644 --- a/examples/testapp/src/pages/add-sub-account/index.page.tsx +++ b/examples/testapp/src/pages/add-sub-account/index.page.tsx @@ -90,7 +90,7 @@ export default function SubAccounts() { signerFn={getSubAccountSigner} /> - + diff --git a/packages/account-sdk/src/ui/Dialog/Dialog.test.tsx b/packages/account-sdk/src/ui/Dialog/Dialog.test.tsx index 29e17c8d2..a33f6de96 100644 --- a/packages/account-sdk/src/ui/Dialog/Dialog.test.tsx +++ b/packages/account-sdk/src/ui/Dialog/Dialog.test.tsx @@ -13,6 +13,15 @@ const renderDialogContainer = (props?: Partial) => ); describe('DialogContainer', () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.spyOn(window, 'setTimeout'); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + test('renders with title and message', () => { renderDialogContainer(); @@ -21,12 +30,13 @@ describe('DialogContainer', () => { }); test('renders hidden initially', () => { - const setTimeoutSpy = vi.spyOn(window, 'setTimeout'); renderDialogContainer(); const hiddenClass = document.getElementsByClassName('-base-acc-sdk-dialog-instance-hidden'); expect(hiddenClass.length).toEqual(1); - expect(setTimeoutSpy).toHaveBeenCalled(); + + vi.runAllTimers(); + expect(setTimeout).toHaveBeenCalledTimes(1); }); test('shows action button when provided', () => { From f772b90be0a6ce30a1a05c53403c5e094544ad40 Mon Sep 17 00:00:00 2001 From: Spencer Stock Date: Mon, 24 Nov 2025 22:12:29 -0700 Subject: [PATCH 05/12] base only --- .../pay-playground/constants/playground.ts | 554 +----------------- .../src/interface/payment/README.md | 22 +- .../interface/payment/payWithToken.test.ts | 22 +- .../src/interface/payment/payWithToken.ts | 30 +- .../src/interface/payment/types.ts | 12 +- 5 files changed, 52 insertions(+), 588 deletions(-) diff --git a/examples/testapp/src/pages/pay-playground/constants/playground.ts b/examples/testapp/src/pages/pay-playground/constants/playground.ts index 66a4b9f37..fe3fa96b6 100644 --- a/examples/testapp/src/pages/pay-playground/constants/playground.ts +++ b/examples/testapp/src/pages/pay-playground/constants/playground.ts @@ -44,10 +44,10 @@ try { amount: '1000000', // 1 token (in smallest unit, e.g., 1 USDC = 1000000 for 6 decimals) to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', token: 'USDC', + testnet: true, // Use Base Sepolia for testing. Set to false for Base mainnet paymaster: { url: 'https://paymaster.example.com' } - // chainId defaults to Base mainnet (8453) if not specified }) return result; @@ -63,6 +63,7 @@ try { amount: '1000000', // 1 token (in smallest unit, e.g., 1 USDC = 1000000 for 6 decimals) to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', token: 'USDC', + testnet: true, // Use Base Sepolia for testing. Set to false for Base mainnet paymaster: { url: 'https://paymaster.example.com' }, @@ -75,7 +76,6 @@ try { { type: 'onchainAddress' } ] } - // chainId defaults to Base mainnet (8453) if not specified }) return result; @@ -105,7 +105,7 @@ export const PAY_QUICK_TIPS = [ 'Amount is in USDC (e.g., "1" = $1 of USDC)', 'Only USDC on base and base sepolia are supported', 'Use payerInfo to request user information.', - 'Need other ERC20s? Use base.payWithToken with a token and paymaster configuration (amounts are specified in wei). chainId defaults to Base if not specified.', + 'Need other ERC20s? Use base.payWithToken with a token and paymaster configuration (amounts are specified in wei). testnet parameter supports Base or Base Sepolia only.', ]; export const GET_PAYMENT_STATUS_QUICK_TIPS = [ @@ -121,10 +121,10 @@ export const PAY_WITH_TOKEN_QUICK_TIPS = [ 'For USDC (6 decimals), 1 USDC = 1000000', 'For tokens with 18 decimals, 1 token = 1000000000000000000', 'Token can be a contract address or a supported symbol (e.g., "USDC", "WETH")', - 'chainId is optional and defaults to Base mainnet (8453). Specify chainId for other networks.', + 'testnet parameter toggles between Base mainnet (false) and Base Sepolia (true)', 'paymaster.url is required - configure your paymaster service', 'Use payerInfo to request user information.', - 'Supported tokens vary by chain - check token registry for available options', + 'Only Base and Base Sepolia are supported', ]; // Preset configurations for payWithToken @@ -145,10 +145,10 @@ try { amount: '1000000', // 1 USDC (6 decimals) to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', token: 'USDC', + testnet: false, // Base mainnet paymaster: { url: 'https://paymaster.example.com' } - // chainId defaults to Base mainnet (8453) }) return result; @@ -160,146 +160,14 @@ try { { name: 'USDC on Base Sepolia', description: 'Send 1 USDC on Base Sepolia testnet', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDC (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0x14a34', // Base Sepolia (84532) - token: 'USDC', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDC on Optimism Sepolia', - description: 'Send 1 USDC on Optimism Sepolia testnet', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDC (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0xaa37dc', // Optimism Sepolia (11155420) - token: 'USDC', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDC on Arbitrum Sepolia', - description: 'Send 1 USDC on Arbitrum Sepolia testnet', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDC (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0x66eee', // Arbitrum Sepolia (421614) - token: 'USDC', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDC on Polygon Amoy', - description: 'Send 1 USDC on Polygon Amoy testnet', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDC (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0x13882', // Polygon Amoy (80002) - token: 'USDC', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDC on Avalanche Fuji', - description: 'Send 1 USDC on Avalanche Fuji testnet', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDC (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0xa869', // Avalanche Fuji (43113) - token: 'USDC', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDC on BSC Testnet', - description: 'Send 1 USDC on BSC testnet', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDC (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0x61', // BSC Testnet (97) - token: 'USDC', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDC on Ethereum Sepolia', - description: 'Send 1 USDC on Ethereum Sepolia testnet', - code: `import { base } from '@base-org/account' + code: `import { base} from '@base-org/account' try { const result = await base.payWithToken({ amount: '1000000', // 1 USDC (6 decimals) to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0xaa36a7', // Ethereum Sepolia (11155111) token: 'USDC', + testnet: true, // Base Sepolia paymaster: { url: 'https://paymaster.example.com' } @@ -321,10 +189,10 @@ try { amount: '1000000', // 1 USDT (6 decimals) to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', token: 'USDT', + testnet: false, // Base mainnet paymaster: { url: 'https://paymaster.example.com' } - // chainId defaults to Base mainnet (8453) }) return result; @@ -343,403 +211,7 @@ try { amount: '1000000000000000000', // 1 DAI (18 decimals) to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', token: 'DAI', - paymaster: { - url: 'https://paymaster.example.com' - } - // chainId defaults to Base mainnet (8453) - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDC on Optimism', - description: 'Send 1 USDC on Optimism', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDC (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0xa', // Optimism (10) - token: 'USDC', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDC on Arbitrum', - description: 'Send 1 USDC on Arbitrum', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDC (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0xa4b1', // Arbitrum (42161) - token: 'USDC', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDT on Optimism', - description: 'Send 1 USDT on Optimism', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDT (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0xa', // Optimism (10) - token: 'USDT', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDT on Arbitrum', - description: 'Send 1 USDT on Arbitrum', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDT (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0xa4b1', // Arbitrum (42161) - token: 'USDT', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'DAI on Optimism', - description: 'Send 1 DAI on Optimism', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000000000000000', // 1 DAI (18 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0xa', // Optimism (10) - token: 'DAI', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'DAI on Arbitrum', - description: 'Send 1 DAI on Arbitrum', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000000000000000', // 1 DAI (18 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0xa4b1', // Arbitrum (42161) - token: 'DAI', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDC on Ethereum', - description: 'Send 1 USDC on Ethereum mainnet', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDC (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0x1', // Ethereum mainnet (1) - token: 'USDC', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDT on Ethereum', - description: 'Send 1 USDT on Ethereum mainnet', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDT (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0x1', // Ethereum mainnet (1) - token: 'USDT', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'DAI on Ethereum', - description: 'Send 1 DAI on Ethereum mainnet', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000000000000000', // 1 DAI (18 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0x1', // Ethereum mainnet (1) - token: 'DAI', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDC on Polygon', - description: 'Send 1 USDC on Polygon', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDC (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0x89', // Polygon (137) - token: 'USDC', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDT on Polygon', - description: 'Send 1 USDT on Polygon', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDT (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0x89', // Polygon (137) - token: 'USDT', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'DAI on Polygon', - description: 'Send 1 DAI on Polygon', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000000000000000', // 1 DAI (18 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0x89', // Polygon (137) - token: 'DAI', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDC on Avalanche', - description: 'Send 1 USDC on Avalanche C-Chain', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDC (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0xa86a', // Avalanche C-Chain (43114) - token: 'USDC', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDT on Avalanche', - description: 'Send 1 USDT on Avalanche C-Chain', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDT (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0xa86a', // Avalanche C-Chain (43114) - token: 'USDT', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'DAI on Avalanche', - description: 'Send 1 DAI on Avalanche C-Chain', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000000000000000', // 1 DAI (18 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0xa86a', // Avalanche C-Chain (43114) - token: 'DAI', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDC on BSC', - description: 'Send 1 USDC on Binance Smart Chain', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDC (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0x38', // BSC (56) - token: 'USDC', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDT on BSC', - description: 'Send 1 USDT on Binance Smart Chain', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDT (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0x38', // BSC (56) - token: 'USDT', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'DAI on BSC', - description: 'Send 1 DAI on Binance Smart Chain', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000000000000000', // 1 DAI (18 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0x38', // BSC (56) - token: 'DAI', + testnet: false, // Base mainnet paymaster: { url: 'https://paymaster.example.com' } @@ -752,8 +224,8 @@ try { }`, }, { - name: 'Custom Token Address', - description: 'Send tokens using a custom contract address', + name: 'Custom Token on Base', + description: 'Send tokens using a custom contract address on Base', code: `import { base } from '@base-org/account' try { @@ -761,10 +233,10 @@ try { amount: '1000000000000000000', // Amount in token's smallest unit to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', token: '0xYourTokenContractAddressHere', // Custom token address + testnet: false, // Base mainnet paymaster: { url: 'https://paymaster.example.com' } - // chainId defaults to Base mainnet (8453), specify a different one if needed }) return result; diff --git a/packages/account-sdk/src/interface/payment/README.md b/packages/account-sdk/src/interface/payment/README.md index 6b6e24fc6..a25dd891c 100644 --- a/packages/account-sdk/src/interface/payment/README.md +++ b/packages/account-sdk/src/interface/payment/README.md @@ -23,7 +23,7 @@ if (payment.success) { ## Token Payments (`payWithToken`) -Use `payWithToken` to send any ERC20 token (or native ETH via the `0xEeee…` placeholder) by specifying the chain, token, and paymaster configuration. +Use `payWithToken` to send any ERC20 token on Base or Base Sepolia by specifying the token and paymaster configuration. ```typescript import { payWithToken } from '@base-org/account'; @@ -31,7 +31,7 @@ import { payWithToken } from '@base-org/account'; const payment = await payWithToken({ amount: '1000000', // base units (wei) token: 'USDC', // symbol or contract address - chainId: '0x2105', // Base mainnet + testnet: false, // Use Base mainnet (false) or Base Sepolia (true) to: '0xFe21034794A5a574B94fE4fDfD16e005F1C96e51', paymaster: { url: 'https://paymaster.example.com', @@ -41,15 +41,16 @@ const payment = await payWithToken({ }, }); -console.log(`Token payment sent! Chain-specific ID: ${payment.id}`); +console.log(`Token payment sent! Transaction ID: ${payment.id}`); ``` **Token payment notes** -- `amount` must be provided in the token’s smallest unit (e.g., wei). -- `token` can be an address or a supported symbol (USDC, USDT, DAI). Symbols are validated against the `chainId` you provide. +- `amount` must be provided in the token's smallest unit (e.g., wei). +- `token` can be an address or a supported symbol (USDC, USDT, DAI). +- `testnet` toggles between Base mainnet (false) and Base Sepolia (true). Only Base and Base Sepolia are supported. - `paymaster` is required. Provide either a `url` for a paymaster service or a precomputed `paymasterAndData`. -- The returned `payment.id` uses the [ERC-3770](https://eips.ethereum.org/EIPS/eip-3770) `shortName:hash` format (for example, `base:0x1234…`). Pass this ID directly to `getPaymentStatus`—it already contains the chain context, so you do **not** need to supply `testnet`. +- The returned `payment.id` is a transaction hash, just like the `pay()` function. Pass this ID to `getPaymentStatus` along with the same `testnet` value. ## Checking Payment Status @@ -60,9 +61,10 @@ import { getPaymentStatus } from '@base/account-sdk'; // Assume tokenPayment/usdcPayment are the results from the examples above. -// Token payments (ERC-3770 encoded IDs). No testnet flag needed. +// Token payments - use the same testnet value as the original payment const tokenStatus = await getPaymentStatus({ - id: tokenPayment.id, // e.g., "base:0x1234..." + id: tokenPayment.id, // e.g., "0x1234..." + testnet: false, // Same testnet value used in payWithToken }); // USDC payments via pay() still require a testnet flag. @@ -186,8 +188,8 @@ The payment result is always a successful payment (errors are thrown as exceptio ### `getPaymentStatus(options: PaymentStatusOptions): Promise` -- `id: string` - Payment ID to check. For `payWithToken()` this is an ERC-3770 value (`shortName:0x…`). For `pay()` this is a plain transaction hash. -- `testnet?: boolean` - Only used for plain hashes returned by `pay()`. Ignored when the ID already encodes the chain (ERC-3770 format). +- `id: string` - Payment ID to check (transaction hash returned from `pay()` or `payWithToken()`). +- `testnet?: boolean` - Whether the payment was on testnet (Base Sepolia). Must match the value used in the original payment. - `telemetry?: boolean` - Whether to enable telemetry logging (default: true) #### PaymentStatus diff --git a/packages/account-sdk/src/interface/payment/payWithToken.test.ts b/packages/account-sdk/src/interface/payment/payWithToken.test.ts index 5d2d78240..8c6d9a8ad 100644 --- a/packages/account-sdk/src/interface/payment/payWithToken.test.ts +++ b/packages/account-sdk/src/interface/payment/payWithToken.test.ts @@ -10,7 +10,6 @@ vi.mock(':core/telemetry/events/payment.js', () => ({ vi.mock('./utils/validation.js', () => ({ normalizeAddress: vi.fn((address: string) => address), - normalizeChainId: vi.fn(() => 8453), validateBaseUnitAmount: vi.fn(() => BigInt(1000)), })); @@ -33,7 +32,7 @@ vi.mock('./utils/translateTokenPayment.js', () => ({ })); vi.mock('./utils/sdkManager.js', () => ({ - executePaymentOnChain: vi.fn(async () => ({ + executePaymentWithSDK: vi.fn(async () => ({ transactionHash: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', payerInfoResponses: { email: 'test@example.com' }, })), @@ -52,17 +51,16 @@ describe('payWithToken', () => { amount: '1000', to: '0xFe21034794A5a574B94fE4fDfD16e005F1C96e51', token: 'USDC', - chainId: '0x2105', + testnet: false, paymaster: { url: 'https://paymaster.example.com' }, }); expect(result).toEqual({ success: true, - id: 'base:0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', + id: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', token: 'USDC', tokenAddress: '0x0000000000000000000000000000000000000001', tokenAmount: '1000', - chainId: 8453, to: '0xFe21034794A5a574B94fE4fDfD16e005F1C96e51', payerInfoResponses: { email: 'test@example.com' }, }); @@ -79,13 +77,13 @@ describe('payWithToken', () => { }); it('should merge walletUrl into sdkConfig and pass it to the executor', async () => { - const { executePaymentOnChain } = await import('./utils/sdkManager.js'); + const { executePaymentWithSDK } = await import('./utils/sdkManager.js'); await payWithToken({ amount: '500', to: '0x0A7c6899cdCb379E284fbFd045462e751dA4C7cE', token: 'USDT', - chainId: 8453, + testnet: false, paymaster: { paymasterAndData: '0xdeadbeef' as `0x${string}` }, walletUrl: 'https://wallet.example.com', sdkConfig: { @@ -95,9 +93,9 @@ describe('payWithToken', () => { }, }); - expect(executePaymentOnChain).toHaveBeenCalledWith( + expect(executePaymentWithSDK).toHaveBeenCalledWith( expect.any(Object), - 8453, + false, true, expect.objectContaining({ preference: expect.objectContaining({ @@ -109,17 +107,17 @@ describe('payWithToken', () => { }); it('should propagate errors and log telemetry when execution fails', async () => { - const { executePaymentOnChain } = await import('./utils/sdkManager.js'); + const { executePaymentWithSDK } = await import('./utils/sdkManager.js'); const { logPayWithTokenError } = await import(':core/telemetry/events/payment.js'); - vi.mocked(executePaymentOnChain).mockRejectedValueOnce(new Error('execution reverted')); + vi.mocked(executePaymentWithSDK).mockRejectedValueOnce(new Error('execution reverted')); await expect( payWithToken({ amount: '1', to: '0x000000000000000000000000000000000000dead', token: 'USDC', - chainId: 8453, + testnet: false, paymaster: { url: 'https://paymaster.example.com' }, }) ).rejects.toThrow('execution reverted'); diff --git a/packages/account-sdk/src/interface/payment/payWithToken.ts b/packages/account-sdk/src/interface/payment/payWithToken.ts index e8ad80917..06666c991 100644 --- a/packages/account-sdk/src/interface/payment/payWithToken.ts +++ b/packages/account-sdk/src/interface/payment/payWithToken.ts @@ -5,11 +5,10 @@ import { } from ':core/telemetry/events/payment.js'; import { CHAIN_IDS } from './constants.js'; import type { PayWithTokenOptions, PayWithTokenResult, PaymentSDKConfig } from './types.js'; -import { encodePaymentId } from './utils/erc3770.js'; -import { executePaymentOnChain } from './utils/sdkManager.js'; +import { executePaymentWithSDK } from './utils/sdkManager.js'; import { resolveTokenAddress } from './utils/tokenRegistry.js'; import { buildTokenPaymentRequest } from './utils/translateTokenPayment.js'; -import { normalizeAddress, normalizeChainId, validateBaseUnitAmount } from './utils/validation.js'; +import { normalizeAddress, validateBaseUnitAmount } from './utils/validation.js'; function mergeSdkConfig( sdkConfig: PaymentSDKConfig | undefined, @@ -39,7 +38,7 @@ export async function payWithToken(options: PayWithTokenOptions): Promise Date: Mon, 24 Nov 2025 22:17:38 -0700 Subject: [PATCH 06/12] Remove ERC-3770 and limit to Base chains only --- .../src/interface/payment/constants.ts | 44 +--------- .../payment/getPaymentStatus.test.ts | 6 +- .../src/interface/payment/getPaymentStatus.ts | 81 ++++--------------- 3 files changed, 22 insertions(+), 109 deletions(-) diff --git a/packages/account-sdk/src/interface/payment/constants.ts b/packages/account-sdk/src/interface/payment/constants.ts index def1bf16b..657bea093 100644 --- a/packages/account-sdk/src/interface/payment/constants.ts +++ b/packages/account-sdk/src/interface/payment/constants.ts @@ -1,23 +1,11 @@ import type { Address } from 'viem'; /** - * Chain IDs for supported networks + * Chain IDs for supported networks (Base only) */ export const CHAIN_IDS = { base: 8453, baseSepolia: 84532, - ethereum: 1, - sepolia: 11155111, - optimism: 10, - optimismSepolia: 11155420, - arbitrum: 42161, - polygon: 137, - 'avalanche-c-chain': 43114, - avalanche: 43114, - baseMainnet: 8453, // alias - zora: 7777777, - BSC: 56, - bsc: 56, } as const; /** @@ -40,64 +28,36 @@ export const ETH_PLACEHOLDER_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEE /** * Registry of whitelisted stablecoins that can be referenced by symbol - * when calling token-aware payment APIs. - * - * NOTE: Not every token is available on every supported chain. Any missing - * addresses will force callers to provide an explicit token contract address. + * when calling token-aware payment APIs (Base and Base Sepolia only). */ export const STABLECOIN_WHITELIST = { USDC: { symbol: 'USDC', decimals: 6, addresses: { - [CHAIN_IDS.ethereum]: '0xA0b86991c6218b36c1d19D4a2e9eb0ce3606eb48', - [CHAIN_IDS.optimism]: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', - [CHAIN_IDS.optimismSepolia]: '0x5fd84259d66cD46123540766Be93DfE6d43130d7', - [CHAIN_IDS.arbitrum]: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', - [CHAIN_IDS.polygon]: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', - [CHAIN_IDS['avalanche-c-chain']]: '0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E', [CHAIN_IDS.base]: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', [CHAIN_IDS.baseSepolia]: '0x036CbD53842c5426634e7929541eC2318f3dCF7e', - [CHAIN_IDS.BSC]: '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d', } satisfies Partial>, }, USDT: { symbol: 'USDT', decimals: 6, addresses: { - [CHAIN_IDS.ethereum]: '0xdAC17F958D2ee523a2206206994597C13D831ec7', - [CHAIN_IDS.optimism]: '0x94b008aa00579c1307b0ef2c499ad98a8ce58e58', - [CHAIN_IDS.arbitrum]: '0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9', - [CHAIN_IDS.polygon]: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F', - [CHAIN_IDS['avalanche-c-chain']]: '0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7', [CHAIN_IDS.base]: '0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2', - [CHAIN_IDS.BSC]: '0x55d398326f99059fF775485246999027B3197955', } satisfies Partial>, }, DAI: { symbol: 'DAI', decimals: 18, addresses: { - [CHAIN_IDS.ethereum]: '0x6B175474E89094C44Da98b954EedeAC495271d0F', - [CHAIN_IDS.optimism]: '0xda10009cbd5d07dd0cecc66161fc93d7c9000da1', - [CHAIN_IDS.arbitrum]: '0xda10009cbd5d07dd0cecc66161fc93d7c9000da1', - [CHAIN_IDS.polygon]: '0x8f3cf7ad23cd3cadbd9735aff958023239c6a063', - [CHAIN_IDS['avalanche-c-chain']]: '0xd586E7F844cEa2F87f50152665BCbc2C279D8d70', [CHAIN_IDS.base]: '0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb', - [CHAIN_IDS.BSC]: '0x1AF3F329e8BE154074D8769D1FFa4eE058B1DBc3', } satisfies Partial>, }, EURC: { symbol: 'EURC', decimals: 6, addresses: { - [CHAIN_IDS.ethereum]: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c', - [CHAIN_IDS.optimism]: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c', - [CHAIN_IDS.arbitrum]: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c', - [CHAIN_IDS.polygon]: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c', - [CHAIN_IDS['avalanche-c-chain']]: '0xC891EB4cbdEFf6e178eE3d4314284F79b81Bd4C7', [CHAIN_IDS.base]: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c', - [CHAIN_IDS.BSC]: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c', } satisfies Partial>, }, } as const; diff --git a/packages/account-sdk/src/interface/payment/getPaymentStatus.test.ts b/packages/account-sdk/src/interface/payment/getPaymentStatus.test.ts index ac01ac929..1d226ed7b 100644 --- a/packages/account-sdk/src/interface/payment/getPaymentStatus.test.ts +++ b/packages/account-sdk/src/interface/payment/getPaymentStatus.test.ts @@ -84,7 +84,7 @@ describe('getPaymentStatus', () => { ); }); - it('should decode ERC-3770 encoded IDs and ignore the legacy testnet flag', async () => { + it('should use testnet flag to determine network (Base Sepolia when testnet=true)', async () => { const transactionHash = '0xabc1230000000000000000000000000000000000000000000000000000000000'; const mockReceipt = { jsonrpc: '2.0', @@ -104,13 +104,13 @@ describe('getPaymentStatus', () => { } as Response); const status = await getPaymentStatus({ - id: `base:${transactionHash}`, + id: transactionHash, testnet: true, }); expect(status.id).toBe(transactionHash); expect(fetch).toHaveBeenCalledWith( - 'https://api.developer.coinbase.com/rpc/v1/base/S-fOd2n2Oi4fl4e1Crm83XeDXZ7tkg8O', + 'https://api.developer.coinbase.com/rpc/v1/base-sepolia/S-fOd2n2Oi4fl4e1Crm83XeDXZ7tkg8O', expect.objectContaining({ body: JSON.stringify({ jsonrpc: '2.0', diff --git a/packages/account-sdk/src/interface/payment/getPaymentStatus.ts b/packages/account-sdk/src/interface/payment/getPaymentStatus.ts index 3cc535fba..0a133ce43 100644 --- a/packages/account-sdk/src/interface/payment/getPaymentStatus.ts +++ b/packages/account-sdk/src/interface/payment/getPaymentStatus.ts @@ -6,27 +6,25 @@ import { logPaymentStatusCheckError, logPaymentStatusCheckStarted, } from ':core/telemetry/events/payment.js'; -import { CHAIN_IDS, ERC20_TRANSFER_ABI } from './constants.js'; +import { ERC20_TRANSFER_ABI } from './constants.js'; import type { PaymentStatus, PaymentStatusOptions } from './types.js'; -import { decodePaymentId, getBundlerUrl } from './utils/erc3770.js'; import { getStablecoinMetadataByAddress } from './utils/tokenRegistry.js'; /** * Check the status of a payment transaction using its transaction ID (userOp hash) * * @param options - Payment status check options - * @param options.id - Payment ID. For `payWithToken()`, this is ERC-3770 encoded (e.g., "base:0x1234...5678"). - * For `pay()`, this is a plain transaction hash (e.g., "0x1234...5678"). - * @param options.testnet - Whether to use testnet (only used for legacy format from `pay()`) + * @param options.id - Transaction hash from pay() or payWithToken() + * @param options.testnet - Whether to use testnet (Base Sepolia). Defaults to false (Base mainnet) * @returns Promise - Status information about the payment * @throws Error if unable to connect to the RPC endpoint or if the RPC request fails * * @example * ```typescript - * // ERC-3770 encoded ID from payWithToken() * try { * const status = await getPaymentStatus({ - * id: "base:0x1234...5678" + * id: "0x1234...5678", + * testnet: true * }) * * if (status.status === 'failed') { @@ -36,22 +34,6 @@ import { getStablecoinMetadataByAddress } from './utils/tokenRegistry.js'; * console.error('Unable to check payment status:', error.message) * } * ``` - * - * @example - * ```typescript - * // Legacy format from pay() - requires testnet flag - * try { - * const status = await getPaymentStatus({ - * id: "0x1234...5678", - * testnet: true - * }) - * } catch (error) { - * console.error('Unable to check payment status:', error.message) - * } - * ``` - * - * @note For `payWithToken()`, the ID is automatically encoded with chain information using ERC-3770 format. - * For `pay()`, the ID is a plain hash and requires the `testnet` flag to determine the chain. */ export async function getPaymentStatus(options: PaymentStatusOptions): Promise { const { id, testnet = false, telemetry = true } = options; @@ -59,44 +41,15 @@ export async function getPaymentStatus(options: PaymentStatusOptions): Promise

Date: Mon, 24 Nov 2025 22:18:02 -0700 Subject: [PATCH 07/12] Revert playground changes - moved to separate branch --- .../pay-playground/constants/playground.ts | 554 +++++++++++++++++- 1 file changed, 541 insertions(+), 13 deletions(-) diff --git a/examples/testapp/src/pages/pay-playground/constants/playground.ts b/examples/testapp/src/pages/pay-playground/constants/playground.ts index fe3fa96b6..66a4b9f37 100644 --- a/examples/testapp/src/pages/pay-playground/constants/playground.ts +++ b/examples/testapp/src/pages/pay-playground/constants/playground.ts @@ -44,10 +44,10 @@ try { amount: '1000000', // 1 token (in smallest unit, e.g., 1 USDC = 1000000 for 6 decimals) to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', token: 'USDC', - testnet: true, // Use Base Sepolia for testing. Set to false for Base mainnet paymaster: { url: 'https://paymaster.example.com' } + // chainId defaults to Base mainnet (8453) if not specified }) return result; @@ -63,7 +63,6 @@ try { amount: '1000000', // 1 token (in smallest unit, e.g., 1 USDC = 1000000 for 6 decimals) to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', token: 'USDC', - testnet: true, // Use Base Sepolia for testing. Set to false for Base mainnet paymaster: { url: 'https://paymaster.example.com' }, @@ -76,6 +75,7 @@ try { { type: 'onchainAddress' } ] } + // chainId defaults to Base mainnet (8453) if not specified }) return result; @@ -105,7 +105,7 @@ export const PAY_QUICK_TIPS = [ 'Amount is in USDC (e.g., "1" = $1 of USDC)', 'Only USDC on base and base sepolia are supported', 'Use payerInfo to request user information.', - 'Need other ERC20s? Use base.payWithToken with a token and paymaster configuration (amounts are specified in wei). testnet parameter supports Base or Base Sepolia only.', + 'Need other ERC20s? Use base.payWithToken with a token and paymaster configuration (amounts are specified in wei). chainId defaults to Base if not specified.', ]; export const GET_PAYMENT_STATUS_QUICK_TIPS = [ @@ -121,10 +121,10 @@ export const PAY_WITH_TOKEN_QUICK_TIPS = [ 'For USDC (6 decimals), 1 USDC = 1000000', 'For tokens with 18 decimals, 1 token = 1000000000000000000', 'Token can be a contract address or a supported symbol (e.g., "USDC", "WETH")', - 'testnet parameter toggles between Base mainnet (false) and Base Sepolia (true)', + 'chainId is optional and defaults to Base mainnet (8453). Specify chainId for other networks.', 'paymaster.url is required - configure your paymaster service', 'Use payerInfo to request user information.', - 'Only Base and Base Sepolia are supported', + 'Supported tokens vary by chain - check token registry for available options', ]; // Preset configurations for payWithToken @@ -145,10 +145,10 @@ try { amount: '1000000', // 1 USDC (6 decimals) to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', token: 'USDC', - testnet: false, // Base mainnet paymaster: { url: 'https://paymaster.example.com' } + // chainId defaults to Base mainnet (8453) }) return result; @@ -160,14 +160,146 @@ try { { name: 'USDC on Base Sepolia', description: 'Send 1 USDC on Base Sepolia testnet', - code: `import { base} from '@base-org/account' + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDC (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0x14a34', // Base Sepolia (84532) + token: 'USDC', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDC on Optimism Sepolia', + description: 'Send 1 USDC on Optimism Sepolia testnet', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDC (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0xaa37dc', // Optimism Sepolia (11155420) + token: 'USDC', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDC on Arbitrum Sepolia', + description: 'Send 1 USDC on Arbitrum Sepolia testnet', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDC (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0x66eee', // Arbitrum Sepolia (421614) + token: 'USDC', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDC on Polygon Amoy', + description: 'Send 1 USDC on Polygon Amoy testnet', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDC (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0x13882', // Polygon Amoy (80002) + token: 'USDC', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDC on Avalanche Fuji', + description: 'Send 1 USDC on Avalanche Fuji testnet', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDC (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0xa869', // Avalanche Fuji (43113) + token: 'USDC', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDC on BSC Testnet', + description: 'Send 1 USDC on BSC testnet', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDC (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0x61', // BSC Testnet (97) + token: 'USDC', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDC on Ethereum Sepolia', + description: 'Send 1 USDC on Ethereum Sepolia testnet', + code: `import { base } from '@base-org/account' try { const result = await base.payWithToken({ amount: '1000000', // 1 USDC (6 decimals) to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0xaa36a7', // Ethereum Sepolia (11155111) token: 'USDC', - testnet: true, // Base Sepolia paymaster: { url: 'https://paymaster.example.com' } @@ -189,10 +321,10 @@ try { amount: '1000000', // 1 USDT (6 decimals) to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', token: 'USDT', - testnet: false, // Base mainnet paymaster: { url: 'https://paymaster.example.com' } + // chainId defaults to Base mainnet (8453) }) return result; @@ -211,7 +343,403 @@ try { amount: '1000000000000000000', // 1 DAI (18 decimals) to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', token: 'DAI', - testnet: false, // Base mainnet + paymaster: { + url: 'https://paymaster.example.com' + } + // chainId defaults to Base mainnet (8453) + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDC on Optimism', + description: 'Send 1 USDC on Optimism', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDC (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0xa', // Optimism (10) + token: 'USDC', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDC on Arbitrum', + description: 'Send 1 USDC on Arbitrum', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDC (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0xa4b1', // Arbitrum (42161) + token: 'USDC', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDT on Optimism', + description: 'Send 1 USDT on Optimism', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDT (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0xa', // Optimism (10) + token: 'USDT', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDT on Arbitrum', + description: 'Send 1 USDT on Arbitrum', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDT (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0xa4b1', // Arbitrum (42161) + token: 'USDT', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'DAI on Optimism', + description: 'Send 1 DAI on Optimism', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000000000000000', // 1 DAI (18 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0xa', // Optimism (10) + token: 'DAI', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'DAI on Arbitrum', + description: 'Send 1 DAI on Arbitrum', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000000000000000', // 1 DAI (18 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0xa4b1', // Arbitrum (42161) + token: 'DAI', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDC on Ethereum', + description: 'Send 1 USDC on Ethereum mainnet', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDC (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0x1', // Ethereum mainnet (1) + token: 'USDC', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDT on Ethereum', + description: 'Send 1 USDT on Ethereum mainnet', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDT (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0x1', // Ethereum mainnet (1) + token: 'USDT', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'DAI on Ethereum', + description: 'Send 1 DAI on Ethereum mainnet', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000000000000000', // 1 DAI (18 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0x1', // Ethereum mainnet (1) + token: 'DAI', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDC on Polygon', + description: 'Send 1 USDC on Polygon', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDC (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0x89', // Polygon (137) + token: 'USDC', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDT on Polygon', + description: 'Send 1 USDT on Polygon', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDT (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0x89', // Polygon (137) + token: 'USDT', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'DAI on Polygon', + description: 'Send 1 DAI on Polygon', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000000000000000', // 1 DAI (18 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0x89', // Polygon (137) + token: 'DAI', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDC on Avalanche', + description: 'Send 1 USDC on Avalanche C-Chain', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDC (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0xa86a', // Avalanche C-Chain (43114) + token: 'USDC', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDT on Avalanche', + description: 'Send 1 USDT on Avalanche C-Chain', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDT (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0xa86a', // Avalanche C-Chain (43114) + token: 'USDT', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'DAI on Avalanche', + description: 'Send 1 DAI on Avalanche C-Chain', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000000000000000', // 1 DAI (18 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0xa86a', // Avalanche C-Chain (43114) + token: 'DAI', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDC on BSC', + description: 'Send 1 USDC on Binance Smart Chain', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDC (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0x38', // BSC (56) + token: 'USDC', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'USDT on BSC', + description: 'Send 1 USDT on Binance Smart Chain', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000', // 1 USDT (6 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0x38', // BSC (56) + token: 'USDT', + paymaster: { + url: 'https://paymaster.example.com' + } + }) + + return result; +} catch (error) { + console.error('Token payment failed:', error.message); + throw error; +}`, + }, + { + name: 'DAI on BSC', + description: 'Send 1 DAI on Binance Smart Chain', + code: `import { base } from '@base-org/account' + +try { + const result = await base.payWithToken({ + amount: '1000000000000000000', // 1 DAI (18 decimals) + to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + chainId: '0x38', // BSC (56) + token: 'DAI', paymaster: { url: 'https://paymaster.example.com' } @@ -224,8 +752,8 @@ try { }`, }, { - name: 'Custom Token on Base', - description: 'Send tokens using a custom contract address on Base', + name: 'Custom Token Address', + description: 'Send tokens using a custom contract address', code: `import { base } from '@base-org/account' try { @@ -233,10 +761,10 @@ try { amount: '1000000000000000000', // Amount in token's smallest unit to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', token: '0xYourTokenContractAddressHere', // Custom token address - testnet: false, // Base mainnet paymaster: { url: 'https://paymaster.example.com' } + // chainId defaults to Base mainnet (8453), specify a different one if needed }) return result; From 19c1ec2002ac23d6c58525ee64a19cb57ee008d3 Mon Sep 17 00:00:00 2001 From: Spencer Stock Date: Mon, 24 Nov 2025 22:22:39 -0700 Subject: [PATCH 08/12] Move all playground changes to separate branch - SDK changes only --- .../components/CodeEditor.module.css | 52 -- .../pay-playground/components/CodeEditor.tsx | 113 +-- .../pay-playground/components/Output.tsx | 194 +---- .../pages/pay-playground/constants/index.ts | 5 - .../pay-playground/constants/playground.ts | 707 ------------------ .../pay-playground/hooks/useCodeExecution.ts | 23 +- .../pay-playground/utils/codeSanitizer.ts | 12 +- .../src/pages/pay-playground/utils/index.ts | 2 - .../utils/payerInfoTransform.ts | 89 --- .../utils/paymasterTransform.ts | 71 -- 10 files changed, 12 insertions(+), 1256 deletions(-) delete mode 100644 examples/testapp/src/pages/pay-playground/utils/payerInfoTransform.ts delete mode 100644 examples/testapp/src/pages/pay-playground/utils/paymasterTransform.ts diff --git a/examples/testapp/src/pages/pay-playground/components/CodeEditor.module.css b/examples/testapp/src/pages/pay-playground/components/CodeEditor.module.css index b0e4eec19..013d1fef6 100644 --- a/examples/testapp/src/pages/pay-playground/components/CodeEditor.module.css +++ b/examples/testapp/src/pages/pay-playground/components/CodeEditor.module.css @@ -32,58 +32,6 @@ color: #64748b; } -.presetContainer { - padding: 1rem 1.5rem; - border-bottom: 1px solid #e2e8f0; - background: #f8fafc; - display: flex; - align-items: center; - gap: 0.75rem; -} - -.presetLabel { - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 0.875rem; - font-weight: 500; - color: #475569; - white-space: nowrap; -} - -.presetIcon { - width: 16px; - height: 16px; - color: #64748b; -} - -.presetSelect { - flex: 1; - padding: 0.5rem 0.75rem; - font-size: 0.875rem; - color: #0f172a; - background: white; - border: 1px solid #cbd5e1; - border-radius: 6px; - cursor: pointer; - transition: all 0.2s; -} - -.presetSelect:hover:not(:disabled) { - border-color: #94a3b8; -} - -.presetSelect:focus { - outline: none; - border-color: #0052ff; - box-shadow: 0 0 0 3px rgba(0, 82, 255, 0.1); -} - -.presetSelect:disabled { - opacity: 0.5; - cursor: not-allowed; -} - .checkboxContainer { padding: 1rem 1.5rem; border-bottom: 1px solid #e2e8f0; diff --git a/examples/testapp/src/pages/pay-playground/components/CodeEditor.tsx b/examples/testapp/src/pages/pay-playground/components/CodeEditor.tsx index 166c230e0..cf0a0c8d8 100644 --- a/examples/testapp/src/pages/pay-playground/components/CodeEditor.tsx +++ b/examples/testapp/src/pages/pay-playground/components/CodeEditor.tsx @@ -1,12 +1,5 @@ -import React from 'react'; import styles from './CodeEditor.module.css'; -export interface Preset { - name: string; - description: string; - code: string; -} - interface CodeEditorProps { code: string; onChange: (code: string) => void; @@ -16,12 +9,6 @@ interface CodeEditorProps { includePayerInfo: boolean; onPayerInfoToggle: (checked: boolean) => void; showPayerInfoToggle?: boolean; - presets?: Preset[]; - transformPresetCode?: (code: string, includePayerInfo: boolean) => string; - paymasterUrl?: string; - onPaymasterUrlChange?: (url: string) => void; - showPaymasterUrl?: boolean; - onPresetChange?: (code: string) => void; } export const CodeEditor = ({ @@ -33,42 +20,7 @@ export const CodeEditor = ({ includePayerInfo, onPayerInfoToggle, showPayerInfoToggle = true, - presets, - transformPresetCode, - paymasterUrl, - onPaymasterUrlChange, - showPaymasterUrl = false, - onPresetChange, }: CodeEditorProps) => { - const presetSelectRef = React.useRef(null); - - const handlePresetChange = (e: React.ChangeEvent) => { - const selectedPreset = presets?.find((p) => p.name === e.target.value); - if (selectedPreset) { - let presetCode = selectedPreset.code; - // Apply payerInfo transformation if toggle is enabled and transform function is provided - if (includePayerInfo && transformPresetCode) { - presetCode = transformPresetCode(presetCode, includePayerInfo); - } - // Use onPresetChange if provided, otherwise use onChange - // onPresetChange will apply the current paymasterUrl to the preset code - if (onPresetChange) { - onPresetChange(presetCode); - } else { - onChange(presetCode); - } - // Presets don't control payer info toggle - it's independent - // Paymaster URL stays intact when switching presets - } - }; - - const handleReset = () => { - if (presetSelectRef.current) { - presetSelectRef.current.value = ''; - } - onReset(); - }; - return (

@@ -88,7 +40,7 @@ export const CodeEditor = ({ Code Editor -
- {showPaymasterUrl && ( -
- - onPaymasterUrlChange?.(e.target.value)} - disabled={isLoading} - className={styles.presetSelect} - /> -
- )} - - {presets && presets.length > 0 && ( -
- - -
- )} - {showPayerInfoToggle && (
)} - {result && isTokenPaymentResult(result) && ( -
-
- {result.success ? ( - <> - - - - - Payment Successful! - - ) : ( - <> - - - - - - Payment Failed - - )} -
- -
-
- Amount - - {formatUnits( - BigInt(result.tokenAmount), - getTokenInfo(result.tokenAddress).decimals - )}{' '} - {result.token || getTokenInfo(result.tokenAddress).symbol} - -
-
- Recipient - {result.to} -
- {result.success && result.id && ( -
- Transaction ID - {stripChainPrefix(result.id)} -
- )} -
- - {result.success && result.payerInfoResponses && ( -
-
- - - - - User Info -
-
- {result.payerInfoResponses.name && ( -
- Name - - {(() => { - const name = result.payerInfoResponses.name as unknown as { - firstName: string; - familyName: string; - }; - return `${name.firstName} ${name.familyName}`; - })()} - -
- )} - {result.payerInfoResponses.email && ( -
- Email - - {result.payerInfoResponses.email} - -
- )} - {result.payerInfoResponses.phoneNumber && ( -
- Phone - - {result.payerInfoResponses.phoneNumber.number} ( - {result.payerInfoResponses.phoneNumber.country}) - -
- )} - {result.payerInfoResponses.physicalAddress && ( -
- Address - - {(() => { - const addr = result.payerInfoResponses.physicalAddress as unknown as { - address1: string; - address2?: string; - city: string; - state: string; - postalCode: string; - countryCode: string; - name?: { - firstName: string; - familyName: string; - }; - }; - const parts = [ - addr.name ? `${addr.name.firstName} ${addr.name.familyName}` : null, - addr.address1, - addr.address2, - `${addr.city}, ${addr.state} ${addr.postalCode}`, - addr.countryCode, - ].filter(Boolean); - return parts.join(', '); - })()} - -
- )} - {result.payerInfoResponses.onchainAddress && ( -
- On-chain Address - - {result.payerInfoResponses.onchainAddress} - -
- )} -
-
- )} -
- )} - {result && isPaymentStatus(result) && (
Transaction ID - {stripChainPrefix(result.id)} + {result.id}
Message diff --git a/examples/testapp/src/pages/pay-playground/constants/index.ts b/examples/testapp/src/pages/pay-playground/constants/index.ts index 7584ad2d4..9e42f982d 100644 --- a/examples/testapp/src/pages/pay-playground/constants/index.ts +++ b/examples/testapp/src/pages/pay-playground/constants/index.ts @@ -1,13 +1,8 @@ export { DEFAULT_GET_PAYMENT_STATUS_CODE, DEFAULT_PAY_CODE, - DEFAULT_PAY_WITH_TOKEN_CODE, GET_PAYMENT_STATUS_QUICK_TIPS, PAY_CODE_WITH_PAYER_INFO, PAY_QUICK_TIPS, - PAY_WITH_TOKEN_CODE_WITH_PAYER_INFO, - PAY_WITH_TOKEN_PRESETS, - PAY_WITH_TOKEN_QUICK_TIPS, QUICK_TIPS, } from './playground'; -export type { PayWithTokenPreset } from './playground'; diff --git a/examples/testapp/src/pages/pay-playground/constants/playground.ts b/examples/testapp/src/pages/pay-playground/constants/playground.ts index 66a4b9f37..6dc8e54a6 100644 --- a/examples/testapp/src/pages/pay-playground/constants/playground.ts +++ b/examples/testapp/src/pages/pay-playground/constants/playground.ts @@ -37,53 +37,6 @@ try { throw error; }`; -export const DEFAULT_PAY_WITH_TOKEN_CODE = `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 token (in smallest unit, e.g., 1 USDC = 1000000 for 6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - token: 'USDC', - paymaster: { - url: 'https://paymaster.example.com' - } - // chainId defaults to Base mainnet (8453) if not specified - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`; - -export const PAY_WITH_TOKEN_CODE_WITH_PAYER_INFO = `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 token (in smallest unit, e.g., 1 USDC = 1000000 for 6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - token: 'USDC', - paymaster: { - url: 'https://paymaster.example.com' - }, - payerInfo: { - requests: [ - { type: 'name'}, - { type: 'email' }, - { type: 'phoneNumber', optional: true }, - { type: 'physicalAddress', optional: true }, - { type: 'onchainAddress' } - ] - } - // chainId defaults to Base mainnet (8453) if not specified - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`; - export const DEFAULT_GET_PAYMENT_STATUS_CODE = `import { base } from '@base-org/account' try { @@ -105,7 +58,6 @@ export const PAY_QUICK_TIPS = [ 'Amount is in USDC (e.g., "1" = $1 of USDC)', 'Only USDC on base and base sepolia are supported', 'Use payerInfo to request user information.', - 'Need other ERC20s? Use base.payWithToken with a token and paymaster configuration (amounts are specified in wei). chainId defaults to Base if not specified.', ]; export const GET_PAYMENT_STATUS_QUICK_TIPS = [ @@ -116,663 +68,4 @@ export const GET_PAYMENT_STATUS_QUICK_TIPS = [ 'Make sure to use the same testnet setting as the original payment', ]; -export const PAY_WITH_TOKEN_QUICK_TIPS = [ - "Amount is specified in the token's smallest unit (e.g., wei for ETH, or smallest unit for ERC20 tokens)", - 'For USDC (6 decimals), 1 USDC = 1000000', - 'For tokens with 18 decimals, 1 token = 1000000000000000000', - 'Token can be a contract address or a supported symbol (e.g., "USDC", "WETH")', - 'chainId is optional and defaults to Base mainnet (8453). Specify chainId for other networks.', - 'paymaster.url is required - configure your paymaster service', - 'Use payerInfo to request user information.', - 'Supported tokens vary by chain - check token registry for available options', -]; - -// Preset configurations for payWithToken -export interface PayWithTokenPreset { - name: string; - description: string; - code: string; -} - -export const PAY_WITH_TOKEN_PRESETS: PayWithTokenPreset[] = [ - { - name: 'USDC on Base Mainnet', - description: 'Send 1 USDC on Base mainnet', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDC (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - token: 'USDC', - paymaster: { - url: 'https://paymaster.example.com' - } - // chainId defaults to Base mainnet (8453) - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDC on Base Sepolia', - description: 'Send 1 USDC on Base Sepolia testnet', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDC (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0x14a34', // Base Sepolia (84532) - token: 'USDC', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDC on Optimism Sepolia', - description: 'Send 1 USDC on Optimism Sepolia testnet', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDC (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0xaa37dc', // Optimism Sepolia (11155420) - token: 'USDC', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDC on Arbitrum Sepolia', - description: 'Send 1 USDC on Arbitrum Sepolia testnet', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDC (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0x66eee', // Arbitrum Sepolia (421614) - token: 'USDC', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDC on Polygon Amoy', - description: 'Send 1 USDC on Polygon Amoy testnet', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDC (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0x13882', // Polygon Amoy (80002) - token: 'USDC', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDC on Avalanche Fuji', - description: 'Send 1 USDC on Avalanche Fuji testnet', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDC (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0xa869', // Avalanche Fuji (43113) - token: 'USDC', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDC on BSC Testnet', - description: 'Send 1 USDC on BSC testnet', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDC (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0x61', // BSC Testnet (97) - token: 'USDC', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDC on Ethereum Sepolia', - description: 'Send 1 USDC on Ethereum Sepolia testnet', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDC (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0xaa36a7', // Ethereum Sepolia (11155111) - token: 'USDC', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDT on Base Mainnet', - description: 'Send 1 USDT on Base mainnet', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDT (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - token: 'USDT', - paymaster: { - url: 'https://paymaster.example.com' - } - // chainId defaults to Base mainnet (8453) - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'DAI on Base Mainnet', - description: 'Send 1 DAI on Base mainnet', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000000000000000', // 1 DAI (18 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - token: 'DAI', - paymaster: { - url: 'https://paymaster.example.com' - } - // chainId defaults to Base mainnet (8453) - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDC on Optimism', - description: 'Send 1 USDC on Optimism', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDC (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0xa', // Optimism (10) - token: 'USDC', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDC on Arbitrum', - description: 'Send 1 USDC on Arbitrum', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDC (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0xa4b1', // Arbitrum (42161) - token: 'USDC', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDT on Optimism', - description: 'Send 1 USDT on Optimism', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDT (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0xa', // Optimism (10) - token: 'USDT', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDT on Arbitrum', - description: 'Send 1 USDT on Arbitrum', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDT (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0xa4b1', // Arbitrum (42161) - token: 'USDT', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'DAI on Optimism', - description: 'Send 1 DAI on Optimism', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000000000000000', // 1 DAI (18 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0xa', // Optimism (10) - token: 'DAI', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'DAI on Arbitrum', - description: 'Send 1 DAI on Arbitrum', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000000000000000', // 1 DAI (18 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0xa4b1', // Arbitrum (42161) - token: 'DAI', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDC on Ethereum', - description: 'Send 1 USDC on Ethereum mainnet', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDC (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0x1', // Ethereum mainnet (1) - token: 'USDC', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDT on Ethereum', - description: 'Send 1 USDT on Ethereum mainnet', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDT (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0x1', // Ethereum mainnet (1) - token: 'USDT', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'DAI on Ethereum', - description: 'Send 1 DAI on Ethereum mainnet', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000000000000000', // 1 DAI (18 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0x1', // Ethereum mainnet (1) - token: 'DAI', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDC on Polygon', - description: 'Send 1 USDC on Polygon', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDC (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0x89', // Polygon (137) - token: 'USDC', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDT on Polygon', - description: 'Send 1 USDT on Polygon', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDT (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0x89', // Polygon (137) - token: 'USDT', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'DAI on Polygon', - description: 'Send 1 DAI on Polygon', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000000000000000', // 1 DAI (18 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0x89', // Polygon (137) - token: 'DAI', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDC on Avalanche', - description: 'Send 1 USDC on Avalanche C-Chain', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDC (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0xa86a', // Avalanche C-Chain (43114) - token: 'USDC', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDT on Avalanche', - description: 'Send 1 USDT on Avalanche C-Chain', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDT (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0xa86a', // Avalanche C-Chain (43114) - token: 'USDT', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'DAI on Avalanche', - description: 'Send 1 DAI on Avalanche C-Chain', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000000000000000', // 1 DAI (18 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0xa86a', // Avalanche C-Chain (43114) - token: 'DAI', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDC on BSC', - description: 'Send 1 USDC on Binance Smart Chain', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDC (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0x38', // BSC (56) - token: 'USDC', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'USDT on BSC', - description: 'Send 1 USDT on Binance Smart Chain', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000', // 1 USDT (6 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0x38', // BSC (56) - token: 'USDT', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'DAI on BSC', - description: 'Send 1 DAI on Binance Smart Chain', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000000000000000', // 1 DAI (18 decimals) - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - chainId: '0x38', // BSC (56) - token: 'DAI', - paymaster: { - url: 'https://paymaster.example.com' - } - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, - { - name: 'Custom Token Address', - description: 'Send tokens using a custom contract address', - code: `import { base } from '@base-org/account' - -try { - const result = await base.payWithToken({ - amount: '1000000000000000000', // Amount in token's smallest unit - to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', - token: '0xYourTokenContractAddressHere', // Custom token address - paymaster: { - url: 'https://paymaster.example.com' - } - // chainId defaults to Base mainnet (8453), specify a different one if needed - }) - - return result; -} catch (error) { - console.error('Token payment failed:', error.message); - throw error; -}`, - }, -]; - export const QUICK_TIPS = PAY_QUICK_TIPS; diff --git a/examples/testapp/src/pages/pay-playground/hooks/useCodeExecution.ts b/examples/testapp/src/pages/pay-playground/hooks/useCodeExecution.ts index 549c9171b..c21a8d505 100644 --- a/examples/testapp/src/pages/pay-playground/hooks/useCodeExecution.ts +++ b/examples/testapp/src/pages/pay-playground/hooks/useCodeExecution.ts @@ -1,18 +1,12 @@ -import type { PaymentResult, PaymentStatus, PayWithTokenResult } from '@base-org/account'; -import { getPaymentStatus, pay, payWithToken } from '@base-org/account'; -// @ts-ignore - trade module types -import type { SwapResult, SwapQuote, SwapStatus } from '@base-org/account/trade'; -// @ts-ignore - trade module -import { swap, getSwapQuote, getSwapStatus } from '@base-org/account/trade'; +import type { PaymentResult, PaymentStatus } from '@base-org/account'; +import { getPaymentStatus, pay } from '@base-org/account'; import { useCallback, useState } from 'react'; import { transformAndSanitizeCode } from '../utils/codeTransform'; import { useConsoleCapture } from './useConsoleCapture'; export const useCodeExecution = () => { const [isLoading, setIsLoading] = useState(false); - const [result, setResult] = useState< - PaymentResult | PaymentStatus | PayWithTokenResult | SwapResult | SwapQuote | SwapStatus | null - >(null); + const [result, setResult] = useState(null); const [error, setError] = useState(null); const [consoleOutput, setConsoleOutput] = useState([]); const { captureConsole } = useConsoleCapture(); @@ -55,21 +49,10 @@ export const useCodeExecution = () => { // Individual functions for direct access pay, getPaymentStatus, - payWithToken, - swap, - getSwapQuote, - getSwapStatus, // Namespaced access via base object base: { pay, getPaymentStatus, - payWithToken, - }, - // Namespaced access via trade object - trade: { - swap, - getSwapQuote, - getSwapStatus, }, }; diff --git a/examples/testapp/src/pages/pay-playground/utils/codeSanitizer.ts b/examples/testapp/src/pages/pay-playground/utils/codeSanitizer.ts index 0975c5826..8c909c0c6 100644 --- a/examples/testapp/src/pages/pay-playground/utils/codeSanitizer.ts +++ b/examples/testapp/src/pages/pay-playground/utils/codeSanitizer.ts @@ -3,19 +3,11 @@ import * as acorn from 'acorn'; // Define the whitelist of allowed operations export const WHITELIST = { // Allowed SDK functions - allowedFunctions: [ - 'pay', - 'getPaymentStatus', - 'payWithToken', - 'swap', - 'getSwapQuote', - 'getSwapStatus', - ], + allowedFunctions: ['pay', 'getPaymentStatus'], // Allowed object properties and methods allowedObjects: { - base: ['pay', 'getPaymentStatus', 'payWithToken'], - trade: ['swap', 'getSwapQuote', 'getSwapStatus'], + base: ['pay', 'getPaymentStatus'], console: ['log', 'error', 'warn', 'info'], Promise: ['resolve', 'reject', 'all', 'race'], Object: ['keys', 'values', 'entries', 'assign'], diff --git a/examples/testapp/src/pages/pay-playground/utils/index.ts b/examples/testapp/src/pages/pay-playground/utils/index.ts index 67cf6bff2..9816937d8 100644 --- a/examples/testapp/src/pages/pay-playground/utils/index.ts +++ b/examples/testapp/src/pages/pay-playground/utils/index.ts @@ -1,4 +1,2 @@ export { CodeSanitizer, WHITELIST, sanitizeCode } from './codeSanitizer'; export { safeStringify, transformAndSanitizeCode, transformImports } from './codeTransform'; -export { togglePayerInfoInCode } from './payerInfoTransform'; -export { extractPaymasterUrl, updatePaymasterUrl } from './paymasterTransform'; diff --git a/examples/testapp/src/pages/pay-playground/utils/payerInfoTransform.ts b/examples/testapp/src/pages/pay-playground/utils/payerInfoTransform.ts deleted file mode 100644 index 024c91506..000000000 --- a/examples/testapp/src/pages/pay-playground/utils/payerInfoTransform.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Adds or removes payerInfo from payWithToken code - * @param code - The current code string - * @param includePayerInfo - Whether to include payerInfo - * @returns The modified code with payerInfo added or removed - */ -export function togglePayerInfoInCode(code: string, includePayerInfo: boolean): string { - // Check if code already has payerInfo - const hasPayerInfo = /payerInfo\s*:\s*\{/.test(code); - - if (includePayerInfo && !hasPayerInfo) { - // Add payerInfo before the closing brace of the payWithToken options object - const payerInfoBlock = `, - payerInfo: { - requests: [ - { type: 'name'}, - { type: 'email' }, - { type: 'phoneNumber', optional: true }, - { type: 'physicalAddress', optional: true }, - { type: 'onchainAddress' } - ] - }`; - - // Find the payWithToken call and locate the closing brace of its options object - // Look for base.payWithToken({ ... }) - const payWithTokenMatch = code.match(/base\.payWithToken\s*\(\s*\{/); - if (!payWithTokenMatch) { - return code; // Can't find payWithToken call - } - - const startIndex = payWithTokenMatch.index! + payWithTokenMatch[0].length; - let braceDepth = 1; - let i = startIndex; - - // Find the closing brace of the payWithToken options object - while (i < code.length && braceDepth > 0) { - if (code[i] === '{') { - braceDepth++; - } else if (code[i] === '}') { - braceDepth--; - if (braceDepth === 0) { - // Found the closing brace - insert before it - const beforeBrace = code.substring(0, i).trimEnd(); - const afterBrace = code.substring(i); - - // Check if we need a comma before payerInfo - const needsComma = - !beforeBrace.endsWith(',') && !beforeBrace.endsWith('{') && beforeBrace.length > 0; - - return ( - beforeBrace + (needsComma ? payerInfoBlock : payerInfoBlock.substring(1)) + afterBrace - ); - } - } - i++; - } - } else if (!includePayerInfo && hasPayerInfo) { - // Remove payerInfo block using a regex that handles nested objects - // Match: comma, whitespace, payerInfo:, whitespace, {, nested content, } - // The nested content includes requests array with nested objects - let modifiedCode = code; - - // First try to match payerInfo with proper nested structure - // This regex matches from the comma before payerInfo to the closing brace - const payerInfoRegex = /,\s*payerInfo\s*:\s*\{[\s\S]*?requests\s*:\s*\[[\s\S]*?\][\s\S]*?\}/; - modifiedCode = modifiedCode.replace(payerInfoRegex, ''); - - // If that didn't work, try a simpler pattern - if (modifiedCode === code) { - // Fallback: match payerInfo property more broadly (using [\s\S] instead of . with s flag) - const fallbackRegex = /,\s*payerInfo\s*:\s*\{[^}]*\{[^}]*\}[^}]*\}/; - modifiedCode = modifiedCode.replace(fallbackRegex, ''); - } - - // Clean up formatting issues - // Remove double commas - modifiedCode = modifiedCode.replace(/,\s*,/g, ','); - // Remove trailing comma before closing brace - modifiedCode = modifiedCode.replace(/,\s*}/g, '}'); - // Remove leading comma after opening brace - modifiedCode = modifiedCode.replace(/{\s*,/g, '{'); - // Clean up extra whitespace - modifiedCode = modifiedCode.replace(/\n\s*\n\s*\n/g, '\n\n'); - - return modifiedCode; - } - - return code; -} diff --git a/examples/testapp/src/pages/pay-playground/utils/paymasterTransform.ts b/examples/testapp/src/pages/pay-playground/utils/paymasterTransform.ts deleted file mode 100644 index bb71844bc..000000000 --- a/examples/testapp/src/pages/pay-playground/utils/paymasterTransform.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Extracts the paymaster URL from payWithToken code - * @param code - The code string to extract from - * @returns The paymaster URL if found, undefined otherwise - */ -export function extractPaymasterUrl(code: string): string | undefined { - // Match paymaster.url in the code - handle various whitespace patterns - const paymasterMatch = code.match(/paymaster\s*:\s*\{[\s\S]*?url\s*:\s*['"]([^'"]+)['"]/); - return paymasterMatch ? paymasterMatch[1] : undefined; -} - -/** - * Updates the paymaster URL in payWithToken code - * @param code - The code string to update - * @param paymasterUrl - The new paymaster URL to use - * @returns The updated code with the new paymaster URL - */ -export function updatePaymasterUrl(code: string, paymasterUrl: string): string { - // Check if paymaster already exists in the code - const hasPaymaster = /paymaster\s*:\s*\{/.test(code); - - if (!hasPaymaster) { - // If no paymaster exists, we need to add it - // Find the payWithToken call and locate where to insert paymaster - const payWithTokenMatch = code.match(/base\.payWithToken\s*\(\s*\{/); - if (!payWithTokenMatch) { - return code; // Can't find payWithToken call - } - - const startIndex = payWithTokenMatch.index! + payWithTokenMatch[0].length; - let braceDepth = 1; - let i = startIndex; - - // Find the closing brace of the payWithToken options object - while (i < code.length && braceDepth > 0) { - if (code[i] === '{') { - braceDepth++; - } else if (code[i] === '}') { - braceDepth--; - if (braceDepth === 0) { - // Found the closing brace - insert before it - const beforeBrace = code.substring(0, i).trimEnd(); - const afterBrace = code.substring(i); - - // Check if we need a comma before paymaster - const needsComma = - !beforeBrace.endsWith(',') && !beforeBrace.endsWith('{') && beforeBrace.length > 0; - - const paymasterBlock = `, - paymaster: { - url: '${paymasterUrl}' - }`; - - return ( - beforeBrace + (needsComma ? paymasterBlock : paymasterBlock.substring(1)) + afterBrace - ); - } - } - i++; - } - } else { - // Update existing paymaster URL - handle various whitespace patterns - const updatedCode = code.replace( - /(paymaster\s*:\s*\{[\s\S]*?url\s*:\s*['"])[^'"]+(['"])/, - `$1${paymasterUrl}$2` - ); - return updatedCode; - } - - return code; -} From f74b6e04e0adfa4b570412a1529a365e3f1b4f48 Mon Sep 17 00:00:00 2001 From: Spencer Stock Date: Mon, 24 Nov 2025 22:42:02 -0700 Subject: [PATCH 09/12] comment --- packages/account-sdk/src/interface/payment/constants.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/account-sdk/src/interface/payment/constants.ts b/packages/account-sdk/src/interface/payment/constants.ts index 657bea093..c65edb6d4 100644 --- a/packages/account-sdk/src/interface/payment/constants.ts +++ b/packages/account-sdk/src/interface/payment/constants.ts @@ -9,7 +9,8 @@ export const CHAIN_IDS = { } as const; /** - * Token configuration for legacy USDC-only payment APIs + * Token configuration for USDC-only payment APIs (e.g., pay()). + * For other stables or arbitrary tokens, use payWithToken. */ export const TOKENS = { USDC: { From c77210b5b49e1474092fd7293e3b10e684cfd5dd Mon Sep 17 00:00:00 2001 From: Spencer Stock Date: Mon, 24 Nov 2025 22:52:33 -0700 Subject: [PATCH 10/12] just use walletUrl for now --- .../src/interface/payment/pay.test.ts | 51 ++--------------- .../account-sdk/src/interface/payment/pay.ts | 6 +- .../interface/payment/payWithToken.test.ts | 16 +----- .../src/interface/payment/payWithToken.ts | 26 +-------- .../src/interface/payment/types.ts | 38 +------------ .../src/interface/payment/utils/sdkManager.ts | 55 ++++++------------- 6 files changed, 31 insertions(+), 161 deletions(-) diff --git a/packages/account-sdk/src/interface/payment/pay.test.ts b/packages/account-sdk/src/interface/payment/pay.test.ts index c4da7a3d8..42212d25f 100644 --- a/packages/account-sdk/src/interface/payment/pay.test.ts +++ b/packages/account-sdk/src/interface/payment/pay.test.ts @@ -338,8 +338,8 @@ describe('pay', () => { expect(sdkManager.executePaymentWithSDK).toHaveBeenCalledWith( expect.any(Object), false, - false, - undefined + undefined, + false ); }); @@ -386,8 +386,8 @@ describe('pay', () => { }), }), true, - true, - undefined + undefined, + true ); }); @@ -550,47 +550,4 @@ describe('pay', () => { errorMessage: 'Unknown error occurred', }); }); - - it('should pass sdkConfig to executePaymentWithSDK', async () => { - // Setup mocks - vi.mocked(validation.validateStringAmount).mockReturnValue(undefined); - vi.mocked(translatePayment.translatePaymentToSendCalls).mockReturnValue({ - version: '2.0.0', - chainId: 8453, - calls: [ - { - to: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', - data: '0xabcdef', - value: '0x0', - }, - ], - capabilities: {}, - }); - vi.mocked(sdkManager.executePaymentWithSDK).mockResolvedValue({ - transactionHash: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', - }); - - const sdkConfig = { - preference: { - mode: 'embedded' as const, - attribution: { auto: true }, - }, - appName: 'Test App', - }; - - await pay({ - amount: '10.50', - to: '0xFe21034794A5a574B94fE4fDfD16e005F1C96e51', - testnet: false, - sdkConfig, - }); - - // Verify sdkConfig was passed to executePaymentWithSDK - expect(sdkManager.executePaymentWithSDK).toHaveBeenCalledWith( - expect.any(Object), - false, - true, - sdkConfig - ); - }); }); diff --git a/packages/account-sdk/src/interface/payment/pay.ts b/packages/account-sdk/src/interface/payment/pay.ts index 92caa5755..2008ac585 100644 --- a/packages/account-sdk/src/interface/payment/pay.ts +++ b/packages/account-sdk/src/interface/payment/pay.ts @@ -35,7 +35,7 @@ import { normalizeAddress, validateStringAmount } from './utils/validation.js'; * ``` */ export async function pay(options: PaymentOptions): Promise { - const { amount, to, testnet = false, payerInfo, telemetry = true, sdkConfig } = options; + const { amount, to, testnet = false, payerInfo, walletUrl, telemetry = true } = options; // Generate correlation ID for this payment request const correlationId = crypto.randomUUID(); @@ -61,8 +61,8 @@ export async function pay(options: PaymentOptions): Promise { const executionResult = await executePaymentWithSDK( requestParams, testnet, - telemetry, - sdkConfig + walletUrl, + telemetry ); // Log payment completed diff --git a/packages/account-sdk/src/interface/payment/payWithToken.test.ts b/packages/account-sdk/src/interface/payment/payWithToken.test.ts index 8c6d9a8ad..32b73cd77 100644 --- a/packages/account-sdk/src/interface/payment/payWithToken.test.ts +++ b/packages/account-sdk/src/interface/payment/payWithToken.test.ts @@ -76,7 +76,7 @@ describe('payWithToken', () => { ); }); - it('should merge walletUrl into sdkConfig and pass it to the executor', async () => { + it('should pass walletUrl to the executor', async () => { const { executePaymentWithSDK } = await import('./utils/sdkManager.js'); await payWithToken({ @@ -86,23 +86,13 @@ describe('payWithToken', () => { testnet: false, paymaster: { paymasterAndData: '0xdeadbeef' as `0x${string}` }, walletUrl: 'https://wallet.example.com', - sdkConfig: { - preference: { - telemetry: false, - }, - }, }); expect(executePaymentWithSDK).toHaveBeenCalledWith( expect.any(Object), false, - true, - expect.objectContaining({ - preference: expect.objectContaining({ - telemetry: false, - walletUrl: 'https://wallet.example.com', - }), - }) + 'https://wallet.example.com', + true ); }); diff --git a/packages/account-sdk/src/interface/payment/payWithToken.ts b/packages/account-sdk/src/interface/payment/payWithToken.ts index 06666c991..c6c73b723 100644 --- a/packages/account-sdk/src/interface/payment/payWithToken.ts +++ b/packages/account-sdk/src/interface/payment/payWithToken.ts @@ -4,29 +4,12 @@ import { logPayWithTokenStarted, } from ':core/telemetry/events/payment.js'; import { CHAIN_IDS } from './constants.js'; -import type { PayWithTokenOptions, PayWithTokenResult, PaymentSDKConfig } from './types.js'; +import type { PayWithTokenOptions, PayWithTokenResult } from './types.js'; import { executePaymentWithSDK } from './utils/sdkManager.js'; import { resolveTokenAddress } from './utils/tokenRegistry.js'; import { buildTokenPaymentRequest } from './utils/translateTokenPayment.js'; import { normalizeAddress, validateBaseUnitAmount } from './utils/validation.js'; -function mergeSdkConfig( - sdkConfig: PaymentSDKConfig | undefined, - walletUrl?: string -): PaymentSDKConfig | undefined { - if (!walletUrl) { - return sdkConfig; - } - - return { - ...sdkConfig, - preference: { - ...(sdkConfig?.preference ?? {}), - walletUrl, - }, - }; -} - /** * Pay a specified address with any ERC20 token using an ephemeral smart wallet. * @@ -43,7 +26,6 @@ export async function payWithToken(options: PayWithTokenOptions): Promise; -} - /** * Input supported for token parameters. Accepts either a contract address or a supported symbol. */ export type TokenInput = string; /** - * Options for making a USDC payment. + * Options for making a payment */ export interface PaymentOptions { /** Amount of USDC to send as a string (e.g., "10.50") */ @@ -101,10 +70,9 @@ export interface PaymentOptions { testnet?: boolean; /** Optional payer information configuration for data callbacks */ payerInfo?: PayerInfo; + walletUrl?: string; /** Whether to enable telemetry logging. Defaults to true */ telemetry?: boolean; - /** @internal Advanced SDK configuration (undocumented) */ - sdkConfig?: PaymentSDKConfig; } /** @@ -139,8 +107,6 @@ export interface PayWithTokenOptions { walletUrl?: string; /** Whether to enable telemetry logging. Defaults to true */ telemetry?: boolean; - /** @internal Advanced SDK configuration (undocumented) */ - sdkConfig?: PaymentSDKConfig; } /** diff --git a/packages/account-sdk/src/interface/payment/utils/sdkManager.ts b/packages/account-sdk/src/interface/payment/utils/sdkManager.ts index d645af583..803681247 100644 --- a/packages/account-sdk/src/interface/payment/utils/sdkManager.ts +++ b/packages/account-sdk/src/interface/payment/utils/sdkManager.ts @@ -1,7 +1,7 @@ import type { Hex } from 'viem'; import { createBaseAccountSDK } from '../../builder/core/createBaseAccountSDK.js'; import { CHAIN_IDS } from '../constants.js'; -import type { PayerInfoResponses, PaymentSDKConfig } from '../types.js'; +import type { PayerInfoResponses } from '../types.js'; /** * Type for wallet_sendCalls request parameters @@ -39,31 +39,20 @@ export interface PaymentExecutionResult { /** * Creates an ephemeral SDK instance configured for payments * @param chainId - The chain ID to use + * @param walletUrl - Optional wallet URL to use * @param telemetry - Whether to enable telemetry (defaults to true) - * @param sdkConfig - Optional advanced SDK configuration * @returns The configured SDK instance */ -export function createEphemeralSDK( - chainId: number, - telemetry: boolean = true, - sdkConfig?: PaymentSDKConfig -) { +export function createEphemeralSDK(chainId: number, walletUrl?: string, telemetry: boolean = true) { const appName = typeof window !== 'undefined' ? window.location.origin : 'Base Pay SDK'; - // Merge sdkConfig with default settings - // sdkConfig takes precedence over individual parameters const sdk = createBaseAccountSDK({ - appName: sdkConfig?.appName || appName, - appLogoUrl: sdkConfig?.appLogoUrl, - appChainIds: sdkConfig?.appChainIds || [chainId], + appName: appName, + appChainIds: [chainId], preference: { - telemetry: sdkConfig?.preference?.telemetry ?? telemetry, - walletUrl: sdkConfig?.preference?.walletUrl, - mode: sdkConfig?.preference?.mode, - attribution: sdkConfig?.preference?.attribution, - ...sdkConfig?.preference, + telemetry: telemetry, + walletUrl, }, - paymasterUrls: sdkConfig?.paymasterUrls, }); // Chain clients will be automatically created when needed by getClient @@ -127,17 +116,20 @@ export async function executePayment( * Manages the complete payment flow with SDK lifecycle * @param requestParams - The wallet_sendCalls request parameters * @param testnet - Whether to use testnet + * @param walletUrl - Optional wallet URL to use * @param telemetry - Whether to enable telemetry (defaults to true) - * @param sdkConfig - Optional advanced SDK configuration * @returns The payment execution result */ -export async function executePaymentOnChain( +export async function executePaymentWithSDK( requestParams: WalletSendCallsRequestParams, - chainId: number, - telemetry: boolean = true, - sdkConfig?: PaymentSDKConfig + testnet: boolean, + walletUrl?: string, + telemetry: boolean = true ): Promise { - const sdk = createEphemeralSDK(chainId, telemetry, sdkConfig); + const network = testnet ? 'baseSepolia' : 'base'; + const chainId = CHAIN_IDS[network]; + + const sdk = createEphemeralSDK(chainId, walletUrl, telemetry); const provider = sdk.getProvider(); try { @@ -148,18 +140,3 @@ export async function executePaymentOnChain( await provider.disconnect(); } } - -/** - * Legacy wrapper that derives the chain ID from the Base / Base Sepolia flag - */ -export async function executePaymentWithSDK( - requestParams: WalletSendCallsRequestParams, - testnet: boolean, - telemetry: boolean = true, - sdkConfig?: PaymentSDKConfig -): Promise { - const network = testnet ? 'baseSepolia' : 'base'; - const chainId = CHAIN_IDS[network]; - - return executePaymentOnChain(requestParams, chainId, telemetry, sdkConfig); -} From 1bebddf49e07e9dc9f3f383801444e61ba6d833a Mon Sep 17 00:00:00 2001 From: Spencer Stock Date: Mon, 24 Nov 2025 23:00:07 -0700 Subject: [PATCH 11/12] remove chain normalization function --- .../src/interface/payment/utils/validation.ts | 41 ------------------- 1 file changed, 41 deletions(-) diff --git a/packages/account-sdk/src/interface/payment/utils/validation.ts b/packages/account-sdk/src/interface/payment/utils/validation.ts index 6f273b453..833ccbf5d 100644 --- a/packages/account-sdk/src/interface/payment/utils/validation.ts +++ b/packages/account-sdk/src/interface/payment/utils/validation.ts @@ -77,44 +77,3 @@ export function validateBaseUnitAmount(amount: string): bigint { return parsed; } - -/** - * Normalizes a user-supplied chain ID (number, decimal string, or hex string) - * into a positive integer. - * @param chainId - Chain identifier - */ -export function normalizeChainId(chainId: number | string): number { - if (typeof chainId === 'number') { - if (!Number.isFinite(chainId) || !Number.isInteger(chainId) || chainId <= 0) { - throw new Error('Invalid chainId: must be a positive integer'); - } - return chainId; - } - - if (typeof chainId !== 'string') { - throw new Error('Invalid chainId: must be a number or a string'); - } - - const trimmed = chainId.trim(); - if (trimmed.length === 0) { - throw new Error('Invalid chainId: value is required'); - } - - let parsedValue: number; - - if (/^0x/i.test(trimmed)) { - try { - parsedValue = Number(BigInt(trimmed)); - } catch { - throw new Error('Invalid chainId: hex string could not be parsed'); - } - } else { - parsedValue = Number.parseInt(trimmed, 10); - } - - if (!Number.isFinite(parsedValue) || !Number.isInteger(parsedValue) || parsedValue <= 0) { - throw new Error('Invalid chainId: must resolve to a positive integer'); - } - - return parsedValue; -} From 4cf3c305b05857c8063030cc7653d423ba8100c5 Mon Sep 17 00:00:00 2001 From: Spencer Stock Date: Mon, 24 Nov 2025 23:39:07 -0700 Subject: [PATCH 12/12] remove 3770 --- .../interface/payment/utils/erc3770.test.ts | 62 ------- .../src/interface/payment/utils/erc3770.ts | 154 ------------------ 2 files changed, 216 deletions(-) delete mode 100644 packages/account-sdk/src/interface/payment/utils/erc3770.test.ts delete mode 100644 packages/account-sdk/src/interface/payment/utils/erc3770.ts diff --git a/packages/account-sdk/src/interface/payment/utils/erc3770.test.ts b/packages/account-sdk/src/interface/payment/utils/erc3770.test.ts deleted file mode 100644 index 7ef35b6b9..000000000 --- a/packages/account-sdk/src/interface/payment/utils/erc3770.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { CHAIN_IDS } from '../constants.js'; -import { decodePaymentId, encodePaymentId, getChainShortName, isERC3770Format } from './erc3770.js'; - -describe('ERC-3770 utilities', () => { - describe('getChainShortName', () => { - it('returns the short name for a supported chain', () => { - expect(getChainShortName(CHAIN_IDS.base)).toBe('base'); - }); - - it('returns null for unsupported chains', () => { - expect(getChainShortName(999999)).toBeNull(); - }); - }); - - describe('encodePaymentId', () => { - it('encodes chainId and transaction hash', () => { - const encoded = encodePaymentId(CHAIN_IDS.base, '0xabc123'); - expect(encoded).toBe('base:0xabc123'); - }); - - it('throws for unsupported chain IDs', () => { - expect(() => encodePaymentId(999999, '0xabc123')).toThrow('Unsupported chain ID'); - }); - - it('throws for invalid transaction hashes', () => { - expect(() => encodePaymentId(CHAIN_IDS.base, 'abc123')).toThrow('Invalid transaction hash'); - }); - }); - - describe('decodePaymentId', () => { - it('returns null for legacy IDs without a short name', () => { - expect(decodePaymentId('0xabc123')).toBeNull(); - }); - - it('decodes ERC-3770 formatted IDs', () => { - expect(decodePaymentId('base:0xabc123')).toEqual({ - chainId: CHAIN_IDS.base, - transactionHash: '0xabc123', - }); - }); - - it('throws when the short name is missing', () => { - expect(() => decodePaymentId(':0xabc123')).toThrow('Invalid ERC-3770 format'); - }); - - it('throws when the transaction hash is invalid', () => { - expect(() => decodePaymentId('base:not-a-hash')).toThrow('Invalid ERC-3770 format'); - }); - }); - - describe('isERC3770Format', () => { - it('detects ERC-3770 IDs', () => { - expect(isERC3770Format('base:0xabc123')).toBe(true); - }); - - it('detects legacy IDs', () => { - expect(isERC3770Format('0xabc123')).toBe(false); - }); - }); -}); diff --git a/packages/account-sdk/src/interface/payment/utils/erc3770.ts b/packages/account-sdk/src/interface/payment/utils/erc3770.ts deleted file mode 100644 index 72b190a87..000000000 --- a/packages/account-sdk/src/interface/payment/utils/erc3770.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { CHAIN_IDS } from '../constants.js'; - -/** - * Mapping of chain IDs to ERC-3770 short names - * Short names are from https://github.com/ethereum-lists/chains - */ -const CHAIN_SHORT_NAMES: Record = { - [CHAIN_IDS.ethereum]: 'eth', - [CHAIN_IDS.sepolia]: 'sep', - [CHAIN_IDS.base]: 'base', - [CHAIN_IDS.baseSepolia]: 'base-sepolia', - [CHAIN_IDS.optimism]: 'oeth', - [CHAIN_IDS.optimismSepolia]: 'oeth-sepolia', - [CHAIN_IDS.arbitrum]: 'arb1', - [CHAIN_IDS.polygon]: 'matic', - [CHAIN_IDS.avalanche]: 'avax', - [CHAIN_IDS.BSC]: 'bnb', - [CHAIN_IDS.zora]: 'zora', -} as const; - -type HexString = `0x${string}`; - -export type ChainShortName = (typeof CHAIN_SHORT_NAMES)[keyof typeof CHAIN_SHORT_NAMES]; -export type ERC3770PaymentId = `${ChainShortName}:${HexString}`; - -/** - * Mapping of chain IDs to bundler URLs - * Format: https://api.developer.coinbase.com/rpc/v1/{chain-name}/{api-key} - */ -const BUNDLER_URLS: Record = { - [CHAIN_IDS.base]: - 'https://api.developer.coinbase.com/rpc/v1/base/S-fOd2n2Oi4fl4e1Crm83XeDXZ7tkg8O', - [CHAIN_IDS.baseSepolia]: - 'https://api.developer.coinbase.com/rpc/v1/base-sepolia/S-fOd2n2Oi4fl4e1Crm83XeDXZ7tkg8O', - // Add more chains as bundler URLs become available -} as const; - -/** - * Get the ERC-3770 short name for a given chain ID - * @param chainId - The chain ID - * @returns The short name, or null if not found - */ -export function getChainShortName(chainId: number): string | null { - return CHAIN_SHORT_NAMES[chainId] ?? null; -} - -/** - * Get the bundler URL for a given chain ID - * @param chainId - The chain ID - * @returns The bundler URL, or null if not available - */ -export function getBundlerUrl(chainId: number): string | null { - return BUNDLER_URLS[chainId] ?? null; -} - -/** - * Get the chain ID from an ERC-3770 short name - * @param shortName - The ERC-3770 short name - * @returns The chain ID, or null if not found - */ -export function getChainIdFromShortName(shortName: string): number | null { - const entry = Object.entries(CHAIN_SHORT_NAMES).find( - ([, name]) => name === shortName.toLowerCase() - ); - return entry ? Number.parseInt(entry[0], 10) : null; -} - -/** - * Decoded payment ID result - */ -export interface DecodedPaymentId { - chainId: number; - transactionHash: HexString; -} - -/** - * Decode an ERC-3770 encoded payment ID - * @param id - The payment ID (either ERC-3770 encoded or legacy format) - * @returns Decoded result with chainId and transactionHash, or null if legacy format - * @throws Error if the format is invalid - */ -export function decodePaymentId(id: string): DecodedPaymentId | null { - if (!id || typeof id !== 'string') { - throw new Error('Invalid payment ID: must be a non-empty string'); - } - - // Check if it's ERC-3770 format (contains colon) - const colonIndex = id.indexOf(':'); - if (colonIndex === -1) { - // Legacy format - no chain ID encoded - return null; - } - - // Extract short name and transaction hash - const shortName = id.slice(0, colonIndex); - const transactionHash = id.slice(colonIndex + 1) as HexString; - - // Validate short name - if (!shortName || shortName.length === 0) { - throw new Error('Invalid ERC-3770 format: short name is required'); - } - - // Validate transaction hash format (should be hex string starting with 0x) - if (!transactionHash || !/^0x[a-fA-F0-9]+$/.test(transactionHash)) { - throw new Error( - 'Invalid ERC-3770 format: transaction hash must be a valid hex string starting with 0x' - ); - } - - // Get chain ID from short name - const chainId = getChainIdFromShortName(shortName); - if (chainId === null) { - throw new Error(`Unknown chain short name: ${shortName}`); - } - - return { - chainId, - transactionHash, - }; -} - -/** - * Encode a payment ID with chain ID using ERC-3770 format - * @param chainId - The chain ID where the payment was executed - * @param transactionHash - The transaction hash - * @returns ERC-3770 encoded payment ID (format: "shortName:transactionHash") - * @throws Error if chainId is not supported - */ -export function encodePaymentId(chainId: number, transactionHash: string): ERC3770PaymentId { - if (!transactionHash || typeof transactionHash !== 'string') { - throw new Error('Invalid transaction hash: must be a non-empty string'); - } - - // Validate transaction hash format - if (!/^0x[a-fA-F0-9]+$/.test(transactionHash)) { - throw new Error('Invalid transaction hash: must be a valid hex string starting with 0x'); - } - - const shortName = getChainShortName(chainId); - if (shortName === null) { - throw new Error(`Unsupported chain ID: ${chainId}. Cannot encode payment ID.`); - } - - return `${shortName}:${transactionHash}` as ERC3770PaymentId; -} - -/** - * Check if a payment ID is in ERC-3770 format - * @param id - The payment ID to check - * @returns True if the ID contains a colon (ERC-3770 format), false otherwise - */ -export function isERC3770Format(id: string): boolean { - return typeof id === 'string' && id.includes(':'); -}