인터랙티브 플레이그라운드: 앱에 연동하기 전에 Base Pay SDK Playground에서 pay()와 getPaymentStatus() 함수를 직접 실험해보세요.
Browser (SDK)
import { pay, getPaymentStatus } from '@base-org/account';// Trigger a payment – user will see a popup from their wallet servicetry { const payment = await pay({ amount: '1.00', // USD amount (USDC used internally) to: '0xRecipient', // your address testnet: true // set false for Mainnet }); // Option 1: Poll until mined const { status } = await getPaymentStatus({ id: payment.id, testnet: true // MUST match the testnet setting used in pay() }); if (status === 'completed') console.log('🎉 payment settled');} catch (error) { console.error(`Payment failed: ${error.message}`);}
중요:getPaymentStatus()의 testnet 파라미터는 원래 pay() 호출에서 사용한 값과 일치해야 합니다. testnet: true로 테스트넷에서 결제를 시작했다면, 상태 확인 시에도 testnet: true를 전달해야 합니다.
주문을 이행하기 전에 getPaymentStatus()를 백엔드에서 사용하여 결제가 완료되었는지 확인하세요. 프론트엔드의 결제 확인만 믿지 마세요.
Backend (SDK)
import { getPaymentStatus } from '@base-org/account';export async function checkPayment(txId: string, testnet = false) { const status = await getPaymentStatus({ id: txId, testnet // Must match the testnet setting from the original pay() call }); if (status.status === 'completed') { // fulfill order }}
재생 공격 및 사칭 공격 방지
재생 공격: 악의적인 사용자가 동일한 유효한 트랜잭션 ID를 여러 번 제출할 수 있습니다. 항상 데이터베이스에서 처리된 트랜잭션 ID를 추적하세요.
사칭 공격: 악의적인 사용자가 자신의 주문을 이행하기 위해 다른 사람의 트랜잭션 ID를 제출할 수 있습니다. 항상 결제 발신자가 인증된 사용자와 일치하는지 확인하세요.
두 가지 공격 벡터를 모두 방지하는 예시:
Backend (with replay protection)
import { getPaymentStatus } from '@base-org/account';// Example using a database to track processed transactions// Replace with your actual database implementation (PostgreSQL, MongoDB, etc.)const processedTransactions = new Map<string, { orderId: string; sender: string; amount: string; timestamp: Date;}>(); // In production, use a persistent databaseexport async function verifyAndFulfillPayment( txId: string, orderId: string, payerAddress: string, // From authenticated user (SIWE, JWT, etc.) testnet = false) { // 1. Check if this transaction was already processed if (processedTransactions.has(txId)) { throw new Error('Transaction already processed'); } // 2. Verify the payment status on-chain const { status, sender, amount, recipient } = await getPaymentStatus({ id: txId, testnet }); if (status !== 'completed') { throw new Error(`Payment not completed. Status: ${status}`); } // 3. Verify the payment sender matches the authenticated user // This prevents a malicious user from claiming someone else's payment if (sender.toLowerCase() !== payerAddress.toLowerCase()) { throw new Error('Payment sender does not match authenticated user'); } // 4. Validate the payment details match your order // This ensures the user paid the correct amount to the correct address const expectedAmount = await getOrderAmount(orderId); const expectedRecipient = process.env.PAYMENT_ADDRESS; if (amount !== expectedAmount) { throw new Error('Payment amount mismatch'); } if (recipient.toLowerCase() !== expectedRecipient.toLowerCase()) { throw new Error('Payment recipient mismatch'); } // 5. Mark transaction as processed BEFORE fulfilling // Store sender for easy lookup (e.g., to query all payments from a user) // In production, use a database transaction to ensure atomicity processedTransactions.set(txId, { orderId, sender, amount, timestamp: new Date() }); // 6. Fulfill the order await fulfillOrder(orderId); return { success: true, orderId, sender };}