diff --git a/sdk/arbiter/ai-integration.mdx b/sdk/arbiter/ai-integration.mdx index 333b400..773c9d4 100644 --- a/sdk/arbiter/ai-integration.mdx +++ b/sdk/arbiter/ai-integration.mdx @@ -23,9 +23,6 @@ interface CaseEvaluationContext { /** The payment information struct */ paymentInfo: PaymentInfo; - /** The record index (nonce) identifying which charge */ - nonce: bigint; - /** Current payment state */ paymentState: PaymentState; @@ -89,15 +86,15 @@ const handler = createWebhookHandler({ ``` -Setting `autoSubmitDecision: true` calls `approveRefundRequest` or `denyRefundRequest` on-chain automatically. This submits the decision only -- executing the actual refund transfer via `executeRefundInEscrow` is a separate step you handle after approval. +Setting `autoSubmitDecision: true` calls `refundInEscrow` (for approvals) or `denyRefundRequest` on-chain automatically. In v3, `refundInEscrow()` both executes the refund and auto-approves the pending request in one step. ## Webhook Handler Configuration ```typescript interface WebhookHandlerConfig { - /** X402rArbiter instance */ - arbiter: X402rArbiter; + /** Arbiter client instance */ + arbiter: ReturnType; /** Your evaluation function */ evaluationHook: ArbiterHook; @@ -127,8 +124,7 @@ interface WebhookResult extends DecisionResult { The most common pattern combines `watchNewCases` with the webhook handler to automatically evaluate incoming refund requests: ```typescript -import { X402rArbiter, createWebhookHandler } from '@x402r/arbiter'; -import type { CaseEvaluationContext, DecisionResult, RefundRequestEventLog } from '@x402r/arbiter'; +import { createArbiterClient } from '@x402r/sdk'; import { PaymentState, RequestStatus } from '@x402r/core'; import type { PaymentInfo } from '@x402r/core'; @@ -143,23 +139,21 @@ const handler = createWebhookHandler({ }); // Step 2: Watch for new cases and feed them to the handler -const { unsubscribe } = arbiter.watchNewCases(async (event: RefundRequestEventLog) => { - const paymentInfoHash = event.args.paymentInfoHash!; - const nonce = event.args.nonce ?? 0n; +const unsubscribe = arbiter.watch.onRefundRequest(async (event: any) => { + const paymentInfoHash = event.args?.paymentInfoHash; + if (!paymentInfoHash) return; - console.log(`[NEW CASE] ${paymentInfoHash} (nonce: ${nonce})`); + console.log(`[NEW CASE] ${paymentInfoHash}`); - // Build the evaluation context - // NOTE: You need to reconstruct the full PaymentInfo from your database or event logs - const paymentInfo = await lookupPaymentInfo(paymentInfoHash); + // Retrieve stored PaymentInfo from RefundRequest contract + const paymentInfo = await arbiter.refund.getStoredPaymentInfo(paymentInfoHash); const context: CaseEvaluationContext = { paymentInfo, - nonce, paymentState: PaymentState.InEscrow, refundStatus: RequestStatus.Pending, paymentInfoHash, - refundAmount: event.args.amount, + refundAmount: event.args?.amount, }; // Evaluate and optionally auto-submit @@ -170,15 +164,7 @@ const { unsubscribe } = arbiter.watchNewCases(async (event: RefundRequestEventLo if (result.executed) { console.log(`[ON-CHAIN] Decision submitted: ${result.txHash}`); - - // If approved, execute the refund transfer - if (result.decision === 'approve') { - const { txHash } = await arbiter.executeRefundInEscrow( - paymentInfo, - result.refundAmount // partial refund if specified - ); - console.log(`[REFUND] Executed: ${txHash}`); - } + // In v3, refundInEscrow already executes the refund and auto-approves } else { console.log('[SKIPPED] Confidence below threshold, requires manual review'); } diff --git a/sdk/arbiter/batch-operations.mdx b/sdk/arbiter/batch-operations.mdx index 7f32132..10dbce7 100644 --- a/sdk/arbiter/batch-operations.mdx +++ b/sdk/arbiter/batch-operations.mdx @@ -1,122 +1,92 @@ --- -title: "Batch Operations" +title: "Batch operations" description: "Process multiple refund decisions efficiently with the Arbiter SDK" icon: "layer-group" --- -The Arbiter SDK provides batch operations for processing multiple refund requests in a single call. Both `batchApprove` and `batchDeny` accept an array of `{ paymentInfo, nonce }` objects. +You can process multiple refund decisions by iterating over requests and calling individual methods. In v3, there is no separate approve step — calling `payment.refundInEscrow()` executes the refund and auto-approves the pending request in one transaction. -Batch items are processed **sequentially**, not atomically. If one item fails mid-batch, all previously processed items will **not** be rolled back. Design your error handling accordingly. +Each decision is a separate on-chain transaction. If one fails mid-batch, previously processed items will **not** be rolled back. Design your error handling accordingly. -## Batch Approve +## Batch refund -Approve multiple refund requests in one call: +Execute refunds for multiple pending requests: ```typescript -const items = [ - { paymentInfo: paymentInfo1, nonce: 0n }, - { paymentInfo: paymentInfo2, nonce: 0n }, - { paymentInfo: paymentInfo3, nonce: 1n }, -]; +const items = [paymentInfo1, paymentInfo2, paymentInfo3]; -const results = await arbiter.batchApprove(items); - -for (const { txHash } of results) { - console.log('Approved:', txHash); +for (const paymentInfo of items) { + const request = await arbiter.refund.get(paymentInfo); + const txHash = await arbiter.payment.refundInEscrow(paymentInfo, request.amount); + console.log('Refund executed:', txHash); } ``` -## Batch Deny +## Batch deny -Deny multiple refund requests in one call: +Deny multiple refund requests: ```typescript -const items = [ - { paymentInfo: paymentInfo4, nonce: 0n }, - { paymentInfo: paymentInfo5, nonce: 0n }, -]; - -const results = await arbiter.batchDeny(items); +const items = [paymentInfo4, paymentInfo5]; -for (const { txHash } of results) { +for (const paymentInfo of items) { + const txHash = await arbiter.refund.deny(paymentInfo); console.log('Denied:', txHash); } ``` -## Empty Batch Handling - -Both batch methods safely handle empty arrays and return an empty results array: - -```typescript -const results = await arbiter.batchApprove([]); -console.log(results.length); // 0 -``` - -## Item Format - -Each item in the batch array must include both the `paymentInfo` struct and the `nonce`: - -```typescript -interface BatchItem { - /** The full PaymentInfo struct identifying the payment */ - paymentInfo: PaymentInfo; - /** The record index (nonce) from PaymentIndexRecorder */ - nonce: bigint; -} -``` - - -The `nonce` identifies which specific charge record the refund request targets. For most single-charge payments, this is `0n`. - - -## Example: Triage and Batch Process Pending Cases +## Example: triage and batch process pending cases -Fetch all pending cases, evaluate each one, then batch approve and deny: +Fetch all pending cases, evaluate each one, then process: ```typescript -import { X402rArbiter } from '@x402r/arbiter'; +import { createArbiterClient } from '@x402r/sdk'; import { RequestStatus } from '@x402r/core'; import type { PaymentInfo, RefundRequestData } from '@x402r/core'; async function triageAndProcess( - arbiter: X402rArbiter, - receiverAddress: `0x${string}`, - lookupPaymentInfo: (hash: `0x${string}`) => Promise + arbiter: ReturnType, + operatorAddress: `0x${string}` ) { - // Step 1: Fetch pending cases - const { keys, total } = await arbiter.getPendingRefundRequests(0n, 100n, receiverAddress); - console.log(`Processing ${total} pending cases`); + // Step 1: Fetch cases for this operator + const { keys, total } = await arbiter.refund.getOperatorRequests( + operatorAddress, 0n, 100n + ); + console.log(`Processing ${total} cases`); - const toApprove: Array<{ paymentInfo: PaymentInfo; nonce: bigint }> = []; - const toDeny: Array<{ paymentInfo: PaymentInfo; nonce: bigint }> = []; + const toRefund: PaymentInfo[] = []; + const toDeny: PaymentInfo[] = []; // Step 2: Evaluate each case for (const key of keys) { - const request = await arbiter.getRefundRequestByKey(key); + const request = await arbiter.refund.getByKey(key); if (request.status !== RequestStatus.Pending) { continue; } - const paymentInfo = await lookupPaymentInfo(request.paymentInfoHash); - const item = { paymentInfo, nonce: request.nonce }; + const paymentInfo = await arbiter.refund.getStoredPaymentInfo(request.paymentInfoHash); if (shouldApprove(request)) { - toApprove.push(item); + toRefund.push(paymentInfo); } else { - toDeny.push(item); + toDeny.push(paymentInfo); } } - // Step 3: Batch process decisions - const approveResults = await arbiter.batchApprove(toApprove); - const denyResults = await arbiter.batchDeny(toDeny); + // Step 3: Process decisions + for (const paymentInfo of toRefund) { + const request = await arbiter.refund.get(paymentInfo); + await arbiter.payment.refundInEscrow(paymentInfo, request.amount); + } - console.log(`Approved: ${approveResults.length}, Denied: ${denyResults.length}`); + for (const paymentInfo of toDeny) { + await arbiter.refund.deny(paymentInfo); + } - return { approved: approveResults, denied: denyResults }; + console.log(`Refunded: ${toRefund.length}, Denied: ${toDeny.length}`); } function shouldApprove(request: RefundRequestData): boolean { @@ -125,56 +95,20 @@ function shouldApprove(request: RefundRequestData): boolean { } ``` -## Example: Batch Approve with Refund Execution - -After batch approving, execute refunds individually for each approved payment: - -```typescript -import { X402rArbiter } from '@x402r/arbiter'; -import type { PaymentInfo } from '@x402r/core'; - -async function batchApproveAndExecute( - arbiter: X402rArbiter, - items: Array<{ paymentInfo: PaymentInfo; nonce: bigint }> -) { - // Step 1: Batch approve all items - const approveResults = await arbiter.batchApprove(items); - console.log(`Approved ${approveResults.length} refund requests`); - - // Step 2: Execute refunds individually - const executeResults: Array<{ txHash: `0x${string}` }> = []; - - for (const { paymentInfo } of items) { - try { - const { txHash } = await arbiter.executeRefundInEscrow(paymentInfo); - executeResults.push({ txHash }); - console.log('Refund executed:', txHash); - } catch (error) { - console.error(`Failed to execute refund for ${paymentInfo.payer}:`, error); - } - } - - return { - approved: approveResults, - executed: executeResults, - }; -} -``` - -## Performance Considerations +## Performance considerations -Each item in a batch results in a separate on-chain transaction. Gas costs scale linearly with the number of items. Plan batch sizes around your RPC provider's rate limits. +Each item results in a separate on-chain transaction. Gas costs scale linearly with the number of items. Plan batch sizes around your RPC provider's rate limits. | Factor | Detail | |--------|--------| | **Transaction ordering** | Items are processed sequentially to ensure correct nonce ordering. | -| **Gas costs** | Each item is a separate transaction. Batch methods save on SDK overhead, not gas. | +| **Gas costs** | Each item is a separate transaction. | | **Partial failures** | If one transaction fails, previous ones remain on-chain. Handle partial failures in your logic. | | **Rate limiting** | Large batches may hit RPC rate limits. Consider adding delays for 50+ item batches. | -## Next Steps +## Next steps @@ -184,7 +118,7 @@ Each item in a batch results in a separate on-chain transaction. Gas costs scale Watch for new cases in real-time. - Individual approve/deny methods and executeRefundInEscrow. + Individual deny methods and refundInEscrow. Review the complete arbiter setup guide. diff --git a/sdk/arbiter/decision-submission.mdx b/sdk/arbiter/decision-submission.mdx index 696479e..b75ee3f 100644 --- a/sdk/arbiter/decision-submission.mdx +++ b/sdk/arbiter/decision-submission.mdx @@ -1,58 +1,53 @@ --- -title: "Decision Submission" +title: "Decision submission" description: "Submit decisions on refund requests and execute refunds with the Arbiter SDK" icon: "gavel" --- -The Arbiter SDK provides methods for reviewing refund requests, making decisions, and executing refunds for disputed payments. +The arbiter client provides methods for reviewing refund requests, making decisions, and executing refunds for disputed payments. In v3, there is no separate approve step — calling `payment.refundInEscrow()` both executes the refund and auto-approves the pending request. -## Approve a Refund Request +## Execute refund in escrow -Approve a pending refund request. This updates the on-chain status but does not transfer funds. +Execute a refund to transfer funds back to the payer. This also auto-approves any pending refund request via the IRecorder plugin. ```typescript -const { txHash } = await arbiter.approveRefundRequest(paymentInfo, 0n); -console.log('Refund approved:', txHash); -``` +// Full refund +const txHash = await arbiter.payment.refundInEscrow( + paymentInfo, + paymentInfo.maxAmount +); +console.log('Full refund executed:', txHash); - -Approving a refund request updates the request status to `Approved` but does not transfer funds. You must call `executeRefundInEscrow()` separately to move funds back to the payer. - +// Partial refund +const partialAmount = BigInt('500000'); // 0.5 USDC +const partialTx = await arbiter.payment.refundInEscrow(paymentInfo, partialAmount); +console.log('Partial refund executed:', partialTx); +``` -## Deny a Refund Request +## Deny a refund request Deny a pending refund request: ```typescript -const { txHash } = await arbiter.denyRefundRequest(paymentInfo, 0n); +const txHash = await arbiter.refund.deny(paymentInfo); console.log('Refund denied:', txHash); ``` -## Execute Refund in Escrow +## Refuse a refund request -After approving a refund request, execute the actual fund transfer back to the payer: +Refuse a pending refund request (similar to deny, but signals a different intent): ```typescript -// Full refund (defaults to paymentInfo.maxAmount) -const { txHash } = await arbiter.executeRefundInEscrow(paymentInfo); -console.log('Full refund executed:', txHash); - -// Partial refund -const partialAmount = BigInt('500000'); // 0.5 USDC -const { txHash: partialTx } = await arbiter.executeRefundInEscrow(paymentInfo, partialAmount); -console.log('Partial refund executed:', partialTx); +const txHash = await arbiter.refund.refuse(paymentInfo); +console.log('Refund refused:', txHash); ``` - -When no `amount` is provided, `executeRefundInEscrow` defaults to `paymentInfo.maxAmount`, issuing a full refund. - - -## Check If a Refund Request Exists +## Check if a refund request exists -Verify whether a refund request has been submitted for a given payment and nonce: +Verify whether a refund request has been submitted for a given payment: ```typescript -const hasRequest = await arbiter.hasRefundRequest(paymentInfo, 0n); +const hasRequest = await arbiter.refund.has(paymentInfo); if (!hasRequest) { console.log('No refund request found for this payment'); @@ -60,47 +55,35 @@ if (!hasRequest) { } ``` -## Get Refund Request Data +## Get refund request data Retrieve the full refund request data, including amount and status: ```typescript import { RequestStatus } from '@x402r/core'; -const request = await arbiter.getRefundRequest(paymentInfo, 0n); +const request = await arbiter.refund.get(paymentInfo); console.log('Payment hash:', request.paymentInfoHash); -console.log('Nonce:', request.nonce); console.log('Refund amount:', request.amount); console.log('Status:', RequestStatus[request.status]); ``` -The `RefundRequestData` type contains: - -```typescript -interface RefundRequestData { - paymentInfoHash: `0x${string}`; - nonce: bigint; - amount: bigint; - status: RequestStatus; -} -``` - -## Get Refund Request Status +## Get refund request status Query the current status of a specific refund request: ```typescript import { RequestStatus } from '@x402r/core'; -const status = await arbiter.getRefundStatus(paymentInfo, 0n); +const status = await arbiter.refund.getStatus(paymentInfo); switch (status) { case RequestStatus.Pending: console.log('Awaiting decision'); break; case RequestStatus.Approved: - console.log('Already approved'); + console.log('Already approved (via refundInEscrow)'); break; case RequestStatus.Denied: console.log('Already denied'); @@ -111,58 +94,41 @@ switch (status) { } ``` -## Get Pending Refund Requests (Paginated) +## Get refund requests by operator (paginated) -Retrieve a paginated list of refund request keys for a receiver. You can optionally filter by receiver address: +Retrieve a paginated list of refund request keys for an operator: ```typescript -// Get the first 10 pending requests for a specific receiver -const { keys, total } = await arbiter.getPendingRefundRequests( +const { keys, total } = await arbiter.refund.getOperatorRequests( + operatorAddress, 0n, // offset - 10n, // count - '0xReceiverAddress...' // optional: defaults to the arbiter's wallet address + 10n // count ); console.log(`${total} total cases, showing first ${keys.length}`); -// Look up each request by its composite key for (const key of keys) { - const request = await arbiter.getRefundRequestByKey(key); + const request = await arbiter.refund.getByKey(key); console.log(`Amount: ${request.amount}, Status: ${RequestStatus[request.status]}`); } ``` -## Get Refund Request Count +## Get refund request by key -Get the total number of refund requests for a receiver: +Look up a specific refund request using its `paymentInfoHash`: ```typescript -const count = await arbiter.getRefundRequestCount('0xReceiverAddress...'); -console.log(`Total refund requests: ${count}`); - -// Defaults to the arbiter's wallet address if not specified -const myCount = await arbiter.getRefundRequestCount(); -console.log(`My refund requests: ${myCount}`); -``` - -## Get Refund Request by Composite Key - -Look up a specific refund request using its `keccak256(paymentInfoHash, nonce)` composite key: - -```typescript -const request = await arbiter.getRefundRequestByKey(compositeKey); +const request = await arbiter.refund.getByKey(paymentInfoHash); console.log('Payment hash:', request.paymentInfoHash); console.log('Amount:', request.amount); console.log('Status:', RequestStatus[request.status]); ``` -## Check If a Payment Is Frozen - -Verify whether a payment is currently frozen by a Freeze condition contract: +## Check if a payment is frozen ```typescript -const frozen = await arbiter.isFrozen(paymentInfo, freezeAddress); +const frozen = await arbiter.freeze.isFrozen(paymentInfo); if (frozen) { console.log('Payment is frozen - dispute in progress'); @@ -171,23 +137,28 @@ if (frozen) { } ``` -## Complete Decision Workflow +## Complete decision workflow This example shows the full arbiter workflow: fetching pending cases, reviewing them, making a decision, and executing the refund. ```typescript -import { X402rArbiter } from '@x402r/arbiter'; +import { createArbiterClient } from '@x402r/sdk'; import { RequestStatus } from '@x402r/core'; import type { PaymentInfo } from '@x402r/core'; -async function processAllPendingCases(arbiter: X402rArbiter, receiverAddress: `0x${string}`) { - // Step 1: Get all pending refund requests - const { keys, total } = await arbiter.getPendingRefundRequests(0n, 50n, receiverAddress); - console.log(`Found ${total} pending cases`); +async function processAllPendingCases( + arbiter: ReturnType, + operatorAddress: `0x${string}` +) { + // Step 1: Get all refund requests for this operator + const { keys, total } = await arbiter.refund.getOperatorRequests( + operatorAddress, 0n, 50n + ); + console.log(`Found ${total} cases`); for (const key of keys) { // Step 2: Retrieve request details - const request = await arbiter.getRefundRequestByKey(key); + const request = await arbiter.refund.getByKey(key); // Step 3: Skip if already decided if (request.status !== RequestStatus.Pending) { @@ -195,48 +166,41 @@ async function processAllPendingCases(arbiter: X402rArbiter, receiverAddress: `0 continue; } - // Step 4: Apply your decision logic - const shouldApprove = await evaluateCase(request); - - if (shouldApprove) { - // Step 5a: Approve and execute the refund - // NOTE: You need the full PaymentInfo struct to call these methods. - // Retrieve it from your application's database or event logs. - const paymentInfo = await lookupPaymentInfo(request.paymentInfoHash); + // Step 4: Retrieve the stored PaymentInfo + const paymentInfo = await arbiter.refund.getStoredPaymentInfo(request.paymentInfoHash); - const { txHash: approveTx } = await arbiter.approveRefundRequest(paymentInfo, request.nonce); - console.log(`Approved: ${approveTx}`); + // Step 5: Apply your decision logic + const shouldRefund = await evaluateCase(request); - const { txHash: executeTx } = await arbiter.executeRefundInEscrow(paymentInfo); - console.log(`Refund executed: ${executeTx}`); + if (shouldRefund) { + // Execute refund (auto-approves the request) + const txHash = await arbiter.payment.refundInEscrow(paymentInfo, request.amount); + console.log(`Refund executed: ${txHash}`); } else { - // Step 5b: Deny the refund - const paymentInfo = await lookupPaymentInfo(request.paymentInfoHash); - - const { txHash } = await arbiter.denyRefundRequest(paymentInfo, request.nonce); + // Deny the refund + const txHash = await arbiter.refund.deny(paymentInfo); console.log(`Denied: ${txHash}`); } } } ``` -## Decision Flow Diagram +## Decision flow diagram ```mermaid flowchart TD - A[Get Pending Requests] --> B[getRefundRequestByKey] + A[Get Operator Requests] --> B[refund.getByKey] B --> C{Status Pending?} C -->|No| D[Skip - Already Decided] C -->|Yes| E[Evaluate Case] E --> F{Decision} - F -->|Approve| G[approveRefundRequest] - F -->|Deny| H[denyRefundRequest] - G --> I[executeRefundInEscrow] - I --> J[Funds Returned to Payer] + F -->|Refund| G[payment.refundInEscrow] + F -->|Deny| H[refund.deny] + G --> I[Funds Returned + Request Auto-Approved] H --> K[Merchant Keeps Funds] ``` -## Next Steps +## Next steps diff --git a/sdk/arbiter/quickstart.mdx b/sdk/arbiter/quickstart.mdx index dc24b32..1024d7e 100644 --- a/sdk/arbiter/quickstart.mdx +++ b/sdk/arbiter/quickstart.mdx @@ -8,49 +8,53 @@ icon: "rocket" The Arbiter SDK is experimental. The dispute resolution system design is actively evolving. -The `@x402r/arbiter` package provides methods for arbiters to resolve disputes: reviewing refund requests, approving or denying them, and executing refunds. +The `@x402r/sdk` package provides methods for arbiters to resolve disputes: reviewing refund requests, denying them, and executing refunds via `refundInEscrow()`. ## Setup ```typescript -import { X402rArbiter } from '@x402r/arbiter'; -import { getNetworkConfig } from '@x402r/core'; +import { createArbiterClient } from '@x402r/sdk'; -const config = getNetworkConfig('eip155:84532')!; - -const arbiter = new X402rArbiter({ +const arbiter = createArbiterClient({ publicClient, walletClient, operatorAddress: '0x...', - refundRequestAddress: config.refundRequest, - arbiterRegistryAddress: config.arbiterRegistry, + // Optional: enable escrow and freeze features + escrowPeriodAddress: '0x...', + freezeAddress: '0x...', }); ``` -## Available Methods +Refund request and evidence addresses are auto-resolved from the chain config. + +## Available methods -The Arbiter SDK currently supports: +The arbiter client currently supports: -- **`approveRefundRequest()`** — Approve a pending refund request -- **`denyRefundRequest()`** — Deny a pending refund request -- **`executeRefundInEscrow()`** — Execute an approved refund to transfer funds back -- **`getPendingRefundRequests()`** — List refund requests awaiting decision -- **`getRefundRequestByKey()`** — Get details of a specific refund request -- **`registerArbiter()`** — Register in the on-chain ArbiterRegistry -- **`isArbiterRegistered()`** — Check registration status -- **`submitEvidence()`** — Attach evidence (IPFS CID) to a refund request -- **`getEvidence()`** — Retrieve a single evidence entry by index -- **`getAllEvidence()`** — Retrieve all evidence for a refund request +- **`payment.refundInEscrow()`** — Execute a refund (auto-approves pending request) +- **`refund.deny()`** — Deny a pending refund request +- **`refund.refuse()`** — Refuse a pending refund request +- **`refund.get()`** — Get full refund request data +- **`refund.getByKey()`** — Look up by paymentInfoHash +- **`refund.getStatus()`** — Check request status +- **`refund.has()`** — Check if a request exists +- **`refund.getOperatorRequests()`** — List requests by operator +- **`freeze.isFrozen()`** — Check if a payment is frozen +- **`freeze.unfreeze()`** — Unfreeze a frozen payment +- **`evidence.submit()`** — Attach evidence (IPFS CID) to a refund request +- **`evidence.get()`** — Retrieve a single evidence entry by index +- **`evidence.getBatch()`** — Retrieve multiple evidence entries +- **`operator.distributeFees()`** — Distribute accumulated fees -## Try It Now +## Try it now -The easiest way to try arbiter features is with the **arbiter-cli** example, which provides a command-line interface for all arbiter operations: +The easiest way to try arbiter features is with the **arbiter-cli** example: CLI tool for arbiters to review cases, make decisions, and manage registry. -## Next Steps +## Next steps diff --git a/sdk/arbiter/registry.mdx b/sdk/arbiter/registry.mdx index 1855c8d..1f88866 100644 --- a/sdk/arbiter/registry.mdx +++ b/sdk/arbiter/registry.mdx @@ -11,20 +11,22 @@ The ArbiterRegistry is an on-chain contract that allows arbiters to register the To use registry methods, provide the `arbiterRegistryAddress` when creating the arbiter instance: ```typescript -import { X402rArbiter } from '@x402r/arbiter'; -import { getNetworkConfig } from '@x402r/core'; +import { createArbiterClient } from '@x402r/sdk'; +import { getChainConfig } from '@x402r/core'; -const config = getNetworkConfig('eip155:84532')!; +const config = getChainConfig(84532); -const arbiter = new X402rArbiter({ +const arbiter = createArbiterClient({ publicClient, walletClient, operatorAddress: '0x...', - refundRequestAddress: config.refundRequest, - arbiterRegistryAddress: config.arbiterRegistry, }); ``` + +The arbiter registry address is resolved automatically from the chain config. Registry methods are not currently exposed on the preset client — use `createX402r()` for direct access if needed. + + ## Register as an Arbiter Register your address in the on-chain registry with a URI pointing to your metadata or API endpoint: @@ -120,17 +122,16 @@ interface ArbiterList { ## Complete Example ```typescript -import { X402rArbiter } from '@x402r/arbiter'; -import { getNetworkConfig } from '@x402r/core'; +import { createArbiterClient } from '@x402r/sdk'; +import { getChainConfig } from '@x402r/core'; async function main() { - const config = getNetworkConfig('eip155:84532')!; + const config = getChainConfig(84532); - const arbiter = new X402rArbiter({ + const arbiter = createArbiterClient({ publicClient, walletClient, operatorAddress: '0x...', - arbiterRegistryAddress: config.arbiterRegistry, }); // Register diff --git a/sdk/arbiter/subscriptions.mdx b/sdk/arbiter/subscriptions.mdx index e2436ae..062a152 100644 --- a/sdk/arbiter/subscriptions.mdx +++ b/sdk/arbiter/subscriptions.mdx @@ -1,120 +1,74 @@ --- -title: "Arbiter Events" +title: "Arbiter events" description: "Subscribe to dispute events and build real-time arbiter dashboards" icon: "bell" --- -The Arbiter SDK provides three subscription methods for monitoring dispute activity in real-time. Each returns an object with an `unsubscribe` function you call to stop watching. +The arbiter client provides watch methods for monitoring dispute activity in real-time. Each returns an unsubscribe function you call to stop watching. -## Watch New Cases +## Watch refund requests -Subscribe to `RefundRequested` events -- these are new refund requests that need your attention: +Subscribe to RefundRequest contract events — these include new requests, status updates, and cancellations: ```typescript -import type { RefundRequestEventLog } from '@x402r/core'; - -const { unsubscribe } = arbiter.watchNewCases((event: RefundRequestEventLog) => { - console.log('New refund request!'); - console.log('Payment hash:', event.args.paymentInfoHash); - console.log('Payer:', event.args.payer); - console.log('Receiver:', event.args.receiver); - console.log('Amount:', event.args.amount); - console.log('Nonce:', event.args.nonce); - console.log('Block:', event.blockNumber); +const unsubscribe = arbiter.watch.onRefundRequest((event) => { + console.log('Refund request event:', event); }); // Later: stop watching unsubscribe(); ``` -## Watch Decisions +## Watch refund execution -Subscribe to `RefundRequestStatusUpdated` events -- these fire when a refund request is approved or denied: +Subscribe to `RefundInEscrowExecuted` and `RefundPostEscrowExecuted` events on the operator: ```typescript -import type { RefundRequestEventLog } from '@x402r/core'; - -const { unsubscribe } = arbiter.watchDecisions((event: RefundRequestEventLog) => { - console.log('Decision made!'); - console.log('Payment hash:', event.args.paymentInfoHash); - console.log('New status:', event.args.status); - console.log('Transaction:', event.transactionHash); +const unsubscribe = arbiter.watch.onRefundExecuted((event) => { + console.log('Refund executed:', event); }); + +unsubscribe(); ``` -## Watch Freeze Events +## Watch payment events -Subscribe to `PaymentFrozen` and `PaymentUnfrozen` events from a Freeze condition contract: +Subscribe to `AuthorizationCreated`, `ChargeExecuted`, and `ReleaseExecuted` events: ```typescript -import type { FreezeEventLog } from '@x402r/core'; - -const freezeAddress = '0xFreezeContractAddress...' as `0x${string}`; - -const { unsubscribe } = arbiter.watchFreezeEvents( - freezeAddress, - (event: FreezeEventLog) => { - if (event.eventName === 'PaymentFrozen') { - console.log('Payment frozen:', event.args.paymentInfoHash); - console.log('Frozen by:', event.args.caller); - } else if (event.eventName === 'PaymentUnfrozen') { - console.log('Payment unfrozen:', event.args.paymentInfoHash); - } - } -); -``` - -## Event Type Reference +const unsubscribe = arbiter.watch.onPayment((event) => { + console.log('Payment event:', event); +}); -| Method | Contract Event | Fires When | -|--------|---------------|------------| -| `watchNewCases` | `RefundRequested` | A payer submits a new refund request | -| `watchDecisions` | `RefundRequestStatusUpdated` | An arbiter or receiver approves/denies a request | -| `watchFreezeEvents` | `PaymentFrozen` / `PaymentUnfrozen` | A payment is frozen or unfrozen | +unsubscribe(); +``` -## Event Log Types +## Watch fee distribution -Both `watchNewCases` and `watchDecisions` emit `RefundRequestEventLog` events: +Subscribe to `FeesDistributed` events: ```typescript -interface RefundRequestEventLog { - eventName: 'RefundRequested' | 'RefundRequestStatusUpdated' | 'RefundRequestCancelled'; - args: { - paymentInfoHash?: `0x${string}`; - payer?: `0x${string}`; - receiver?: `0x${string}`; - amount?: bigint; - nonce?: bigint; - status?: number; - }; - address: `0x${string}`; - blockNumber: bigint; - transactionHash: `0x${string}`; - logIndex: number; -} +const unsubscribe = arbiter.watch.onFeeDistribution((event) => { + console.log('Fees distributed:', event); +}); + +unsubscribe(); ``` -The `watchFreezeEvents` method emits `FreezeEventLog` events: +## Event type reference -```typescript -interface FreezeEventLog { - eventName: 'PaymentFrozen' | 'PaymentUnfrozen'; - args: { - paymentInfoHash?: `0x${string}`; - caller?: `0x${string}`; - }; - address: `0x${string}`; - blockNumber: bigint; - transactionHash: `0x${string}`; - logIndex: number; -} -``` +| Method | Events Watched | Contract | Fires When | +|--------|---------------|----------|------------| +| `watch.onRefundRequest` | All RefundRequest events | RefundRequest | Request created, status updated, or cancelled | +| `watch.onRefundExecuted` | `RefundInEscrowExecuted`, `RefundPostEscrowExecuted` | PaymentOperator | A refund is executed | +| `watch.onPayment` | `AuthorizationCreated`, `ChargeExecuted`, `ReleaseExecuted` | PaymentOperator | Payment lifecycle events | +| `watch.onFeeDistribution` | `FeesDistributed` | PaymentOperator | Fees are distributed | -All subscription methods use viem's `watchContractEvent` under the hood. For reliable real-time delivery, configure your `publicClient` with a [WebSocket transport](https://viem.sh/docs/clients/transports/websocket). +For reliable real-time delivery, configure your `publicClient` with a [WebSocket transport](https://viem.sh/docs/clients/transports/websocket). -## Next Steps +## Next steps diff --git a/sdk/client/escrow-management.mdx b/sdk/client/escrow-management.mdx index 5f86fd3..40e7ec3 100644 --- a/sdk/client/escrow-management.mdx +++ b/sdk/client/escrow-management.mdx @@ -1,33 +1,28 @@ --- -title: "Escrow Management" +title: "Escrow management" description: "Manage escrow periods and freeze payments with the Client SDK" icon: "lock" --- -The Client SDK provides methods to interact with the escrow system, including freezing payments during disputes and querying escrow period timing. All methods documented on this page are **fully functional**. +The payer client provides methods to interact with the escrow system, including freezing payments during disputes and querying escrow period timing. -## Freeze Operations +## Freeze operations -Freezing a payment pauses the escrow timer, preventing the merchant from releasing funds while a dispute is being resolved. Freeze operations interact with the `Freeze` contract. +Freezing a payment pauses the escrow timer, preventing the merchant from releasing funds while a dispute is being resolved. -### freezePayment +### freeze Freeze a payment to pause the escrow timer. Only the payer can freeze a payment. ```typescript -const freezeAddress = '0x...'; // Freeze contract address - -const { txHash } = await client.freezePayment(paymentInfo, freezeAddress); +const txHash = await client.freeze.freeze(paymentInfo); console.log(`Payment frozen: ${txHash}`); ``` #### Signature ```typescript -freezePayment( - paymentInfo: PaymentInfo, - freezeAddress: `0x${string}` -): Promise<{ txHash: `0x${string}` }> +freeze(paymentInfo: PaymentInfo, data?: Hex): Promise ``` @@ -37,22 +32,19 @@ Freezing is useful when: - The merchant is unresponsive to your refund request -### unfreezePayment +### unfreeze Unfreeze a previously frozen payment. The receiver (merchant) or arbiter can unfreeze a payment once the dispute is resolved. ```typescript -const { txHash } = await client.unfreezePayment(paymentInfo, freezeAddress); +const txHash = await client.freeze.unfreeze(paymentInfo); console.log(`Payment unfrozen: ${txHash}`); ``` #### Signature ```typescript -unfreezePayment( - paymentInfo: PaymentInfo, - freezeAddress: `0x${string}` -): Promise<{ txHash: `0x${string}` }> +unfreeze(paymentInfo: PaymentInfo, data?: Hex): Promise ``` ### isFrozen @@ -60,7 +52,7 @@ unfreezePayment( Check whether a payment is currently frozen. ```typescript -const frozen = await client.isFrozen(paymentInfo, freezeAddress); +const frozen = await client.freeze.isFrozen(paymentInfo); if (frozen) { console.log('Payment is frozen - escrow timer paused'); @@ -72,24 +64,19 @@ if (frozen) { #### Signature ```typescript -isFrozen( - paymentInfo: PaymentInfo, - freezeAddress: `0x${string}` -): Promise +isFrozen(paymentInfo: PaymentInfo): Promise ``` -## Escrow Period Operations +## Escrow period operations -These methods interact with the `EscrowPeriod` contract to query timing information about a payment's escrow window. +These methods interact with the `EscrowPeriod` contract to query timing information about a payment's escrow window. You must provide an `escrowPeriodAddress` in your client config to use these methods. ### getAuthorizationTime Get the timestamp (in seconds) when a payment was authorized on-chain. This is the starting point of the escrow period. ```typescript -const escrowPeriodAddress = '0x...'; // EscrowPeriod contract address - -const authTime = await client.getAuthorizationTime(paymentInfo, escrowPeriodAddress); +const authTime = await client.escrow.getAuthorizationTime(paymentInfo); const authDate = new Date(Number(authTime) * 1000); console.log(`Payment authorized at: ${authDate.toISOString()}`); @@ -98,21 +85,15 @@ console.log(`Payment authorized at: ${authDate.toISOString()}`); #### Signature ```typescript -getAuthorizationTime( - paymentInfo: PaymentInfo, - escrowPeriodAddress: `0x${string}` -): Promise +getAuthorizationTime(paymentInfo: PaymentInfo): Promise ``` -### isDuringEscrowPeriod +### isDuringEscrow Check whether a payment is still within its escrow period. Returns `true` if the escrow period has not yet passed, meaning the payment can still be refunded. ```typescript -const duringEscrow = await client.isDuringEscrowPeriod( - paymentInfo, - escrowPeriodAddress -); +const duringEscrow = await client.escrow.isDuringEscrow(paymentInfo); if (duringEscrow) { console.log('Still in escrow period - refund is possible'); @@ -124,17 +105,25 @@ if (duringEscrow) { #### Signature ```typescript -isDuringEscrowPeriod( - paymentInfo: PaymentInfo, - escrowPeriodAddress: `0x${string}` -): Promise +isDuringEscrow(paymentInfo: PaymentInfo): Promise +``` + +### getDuration + +Get the configured escrow period duration in seconds. + +```typescript +const duration = await client.escrow.getDuration(); +console.log(`Escrow period: ${Number(duration) / 86400} days`); ``` - -The method name is `isDuringEscrowPeriod`, **not** `isEscrowPeriodPassed`. It returns `true` when the escrow period is still **active** (refund window is open), and `false` when it has passed. - +#### Signature + +```typescript +getDuration(): Promise +``` -## Understanding Escrow Timing +## Understanding escrow timing | Condition | Escrow Timer | Can Request Refund | Can Release | |-----------|--------------|-------------------|-------------| @@ -143,63 +132,47 @@ The method name is `isDuringEscrowPeriod`, **not** `isEscrowPeriodPassed`. It re | Escrow period passed (unfrozen) | Stopped | No | Full amount | -The escrow period length is configured at the contract level when the `EscrowPeriod` condition is deployed. Common values are 7 days, 14 days, or 30 days. You can calculate the remaining time from `getAuthorizationTime` and the configured period. +The escrow period length is configured at the contract level when the `EscrowPeriod` condition is deployed. Common values are 7 days, 14 days, or 30 days. You can use `getDuration()` to retrieve the configured period. -## Example: Freeze and Request Refund +## Example: freeze and request refund A common pattern is to freeze a payment before submitting a refund request, ensuring the merchant cannot release funds while the request is pending. ```typescript -import { X402rClient } from '@x402r/client'; +import { createPayerClient } from '@x402r/sdk'; import type { PaymentInfo } from '@x402r/core'; async function freezeAndRequestRefund( - client: X402rClient, + client: ReturnType, paymentInfo: PaymentInfo, - freezeAddress: `0x${string}`, refundAmount: bigint ) { // Step 1: Check if already frozen - const alreadyFrozen = await client.isFrozen(paymentInfo, freezeAddress); + const alreadyFrozen = await client.freeze.isFrozen(paymentInfo); if (!alreadyFrozen) { - // Freeze the payment first - const { txHash: freezeTx } = await client.freezePayment( - paymentInfo, - freezeAddress - ); + const freezeTx = await client.freeze.freeze(paymentInfo); console.log(`Payment frozen: ${freezeTx}`); } // Step 2: Check if refund request already exists - const nonce = 0n; - const hasRequest = await client.hasRefundRequest(paymentInfo, nonce); + const hasRequest = await client.refund.has(paymentInfo); if (!hasRequest) { - // Submit the refund request - const { txHash: refundTx } = await client.requestRefund( - paymentInfo, - refundAmount, - nonce - ); + const refundTx = await client.refund.request(paymentInfo, refundAmount); console.log(`Refund requested: ${refundTx}`); } // Step 3: Watch for resolution - const { unsubscribe } = client.watchFreezeEvents( - freezeAddress, - (event) => { - if (event.eventName === 'PaymentUnfrozen') { - console.log('Payment was unfrozen - dispute may be resolved'); - unsubscribe(); - } - } - ); + const unsubscribe = client.watch.onPayment((event) => { + console.log('Payment event received'); + unsubscribe(); + }); } ``` -## Freeze / Unfreeze Flow +## Freeze / unfreeze flow ```mermaid sequenceDiagram @@ -208,19 +181,19 @@ sequenceDiagram participant M as Merchant / Arbiter Note over F: Escrow timer running - P->>F: freezePayment() + P->>F: freeze() Note over F: Timer paused alt Dispute resolved favorably - M->>F: unfreezePayment() + M->>F: unfreeze() Note over F: Timer resumes else Payer cancels dispute - P->>F: unfreezePayment() + P->>F: unfreeze() Note over F: Timer resumes end ``` -## Next Steps +## Next steps @@ -230,7 +203,7 @@ sequenceDiagram Request refunds while payment is in escrow. - Planned query methods and current workarounds. + Query payment state and details. Full setup guide for the Client SDK. diff --git a/sdk/client/payment-queries.mdx b/sdk/client/payment-queries.mdx index 38d3103..ac84d15 100644 --- a/sdk/client/payment-queries.mdx +++ b/sdk/client/payment-queries.mdx @@ -1,108 +1,68 @@ --- -title: "Payment Queries" +title: "Payment queries" description: "Query payment states and details with the Client SDK" icon: "magnifying-glass" --- -The Client SDK provides five methods for querying payment state and history. These read directly from the escrow contract and on-chain event logs. +The payer client provides methods for querying payment state and retrieving payment information. -## getPaymentState +## getState Derive the lifecycle state of a payment from the escrow contract (amounts and expiry). ```typescript -import { PaymentState } from '@x402r/core'; +const [hasCollected, capturableAmount, refundableAmount] = await client.payment.getState(paymentInfo); -const state = await client.getPaymentState(paymentInfo); - -// PaymentState enum: -// 0 = NonExistent - Payment has never been authorized -// 1 = InEscrow - Funds locked, capturableAmount > 0 -// 2 = Released - Funds released to receiver, may still be refundable -// 3 = Settled - No funds in escrow or refundable -// 4 = Expired - Authorization expired, payer can reclaim -``` - -```typescript -getPaymentState(paymentInfo: PaymentInfo): Promise -``` - -## paymentExists - -Check whether a payment has been collected by reading the escrow's `hasCollectedPayment` flag. - -```typescript -const exists = await client.paymentExists(paymentInfoHash); -if (exists) { - console.log('Payment found'); -} -``` - -```typescript -paymentExists(paymentInfoHash: `0x${string}`): Promise -``` - -## isInEscrow - -Check if a payment currently has capturable funds in escrow. - -```typescript -const inEscrow = await client.isInEscrow(paymentInfoHash); -if (inEscrow) { +if (capturableAmount > 0n) { console.log('Payment has funds in escrow'); } ``` ```typescript -isInEscrow(paymentInfoHash: `0x${string}`): Promise +getState(paymentInfo: PaymentInfo): Promise ``` -## getPaymentDetails +## getAmounts -Retrieve the full `PaymentInfo` struct by scanning `AuthorizationCreated` events for the given hash. +Get the capturable and refundable amounts for a payment. ```typescript -const details = await client.getPaymentDetails(paymentInfoHash); +const amounts = await client.payment.getAmounts(paymentInfo); -console.log('Payer:', details.payer); -console.log('Receiver:', details.receiver); -console.log('Amount:', details.maxAmount); +console.log('Capturable:', amounts.capturableAmount); +console.log('Refundable:', amounts.refundableAmount); ``` ```typescript -getPaymentDetails( - paymentInfoHash: `0x${string}`, - fromBlock?: bigint -): Promise +getAmounts(paymentInfo: PaymentInfo): Promise ``` - -This method scans event logs. Pass `fromBlock` to limit the scan range if your RPC limits `eth_getLogs` responses (Base Sepolia typically caps at 10,000 blocks). - - -## getPayerPayments +## Query payments -List all payments where the connected wallet is the payer, by scanning `AuthorizationCreated` events. +If you configured a `paymentIndexRecorderAddress` or `paymentStore`, you can use the query action group to look up payments. ```typescript -const payments = await client.getPayerPayments(); +import { createPayerClient, queryActions } from '@x402r/sdk'; -for (const { hash, paymentInfo } of payments) { - console.log(`Payment ${hash}: ${paymentInfo.maxAmount}`); -} -``` +const client = createPayerClient({ + publicClient, + walletClient, + operatorAddress: '0x...', + paymentIndexRecorderAddress: '0x...', +}).extend(queryActions('0xRecorderAddress...')); -```typescript -getPayerPayments( - fromBlock?: bigint -): Promise> +// Get all payments where you are the payer +const payments = await client.query.getPayerPayments(payerAddress); + +// Get a specific payment by hash +const payment = await client.query.getPayment(paymentInfoHash); ``` -Like `getPaymentDetails`, this scans event logs. Pass `fromBlock` to limit the range for large histories. +The query plugin uses a tiered provider stack: in-memory store, on-chain recorder, then event logs (if `eventFromBlock` is configured). The first provider returning results wins. -## Next Steps +## Next steps diff --git a/sdk/client/quickstart.mdx b/sdk/client/quickstart.mdx index f451a5f..69d3955 100644 --- a/sdk/client/quickstart.mdx +++ b/sdk/client/quickstart.mdx @@ -8,43 +8,47 @@ icon: "rocket" The Client SDK is experimental. APIs will change as the refund and dispute system design evolves. -The `@x402r/client` package provides payer-side methods for interacting with X402r payments: requesting refunds, freezing payments, and querying escrow state. +The `@x402r/sdk` package provides payer-side methods for interacting with X402r payments: requesting refunds, freezing payments, and querying escrow state. ## Setup ```typescript -import { X402rClient } from '@x402r/client'; -import { getNetworkConfig } from '@x402r/core'; +import { createPayerClient } from '@x402r/sdk'; -const networkConfig = getNetworkConfig('eip155:84532')!; - -const client = new X402rClient({ +const client = createPayerClient({ publicClient, walletClient, operatorAddress: '0x...', // Your PaymentOperator address - refundRequestAddress: networkConfig.refundRequest, - escrowAddress: networkConfig.authCaptureEscrow, + // Optional: enable escrow, freeze, and refund features + escrowPeriodAddress: '0x...', + freezeAddress: '0x...', }); ``` -## Available Methods +Refund and evidence addresses are resolved automatically from the chain config. + +## Available methods -The Client SDK currently supports: +The payer client exposes action groups: -- **`requestRefund()`** — Submit a refund request for a payment in escrow -- **`getRefundStatus()`** — Check the status of a refund request -- **`freezePayment()`** — Freeze a payment to prevent release during a dispute -- **`isFrozen()`** — Check if a payment is frozen -- **`isDuringEscrowPeriod()`** — Check if a payment is still in its escrow window -- **`getAuthorizationTime()`** — Get when a payment was authorized -- **`getPaymentState()`** — Derive the lifecycle state of a payment -- **`paymentExists()`** — Check if a payment has been authorized -- **`isInEscrow()`** — Check if a payment has capturable funds -- **`getPaymentDetails()`** — Retrieve full PaymentInfo from event logs -- **`getPayerPayments()`** — List all payments for the connected wallet -- **`submitEvidence()`** — Attach evidence (IPFS CID) to a refund request -- **`getEvidence()`** — Retrieve a single evidence entry by index -- **`getAllEvidence()`** — Retrieve all evidence for a refund request +- **`client.payment.getState()`** — Derive the lifecycle state of a payment +- **`client.payment.getAmounts()`** — Get capturable and refundable amounts +- **`client.refund.request()`** — Submit a refund request for a payment +- **`client.refund.cancel()`** — Cancel a pending refund request +- **`client.refund.getStatus()`** — Check the status of a refund request +- **`client.refund.has()`** — Check if a refund request exists +- **`client.refund.get()`** — Get full refund request data +- **`client.refund.getByKey()`** — Look up by paymentInfoHash +- **`client.refund.getStoredPaymentInfo()`** — Retrieve stored PaymentInfo +- **`client.refund.getPayerRequests()`** — List refund requests by payer +- **`client.freeze.freeze()`** — Freeze a payment to prevent release +- **`client.freeze.isFrozen()`** — Check if a payment is frozen +- **`client.escrow.isDuringEscrow()`** — Check if still in escrow window +- **`client.escrow.getAuthorizationTime()`** — Get when payment was authorized +- **`client.escrow.getDuration()`** — Get the escrow period duration +- **`client.evidence.submit()`** — Attach evidence (IPFS CID) to a refund +- **`client.evidence.get()`** — Retrieve a single evidence entry by index +- **`client.evidence.getBatch()`** — Retrieve multiple evidence entries ## Try It Now diff --git a/sdk/client/refund-operations.mdx b/sdk/client/refund-operations.mdx index 7acd7f3..0fe2936 100644 --- a/sdk/client/refund-operations.mdx +++ b/sdk/client/refund-operations.mdx @@ -1,106 +1,115 @@ --- -title: "Refund Operations" +title: "Refund operations" description: "Request and manage refunds with the Client SDK - submit, cancel, and track refund requests" icon: "rotate-left" --- -The Client SDK provides complete refund management capabilities for payers. All refund methods interact directly with the `RefundRequest` contract on-chain. +The payer client provides complete refund management capabilities. All refund methods interact with the `RefundRequest` contract on-chain. Each payment supports one refund request, keyed by its `paymentInfoHash`. -**About the `nonce` parameter:** Every refund method requires a `nonce: bigint` parameter. This is the record index from the `PaymentIndexRecorder` and identifies which charge within a payment you are requesting a refund for. For the first (and most common) charge, use `0n`. +In v3, RefundRequest is wired as an IRecorder plugin. Refund approval happens automatically when the merchant calls `refundInEscrow()` — there is no separate approve step. -## requestRefund +## request Submit a refund request for a payment that is in escrow. The request goes on-chain and is visible to the merchant and any assigned arbiter. ```typescript -const { txHash } = await client.requestRefund( +const txHash = await client.refund.request( paymentInfo, - BigInt('1000000'), // amount to refund (e.g., 1 USDC with 6 decimals) - 0n // nonce: first charge + BigInt('1000000') // amount to refund (e.g., 1 USDC with 6 decimals) ); console.log(`Refund requested: ${txHash}`); ``` -## cancelRefundRequest +## cancel Cancel a pending refund request that you submitted. Only the original requester (payer) can cancel, and only while the request status is `Pending`. ```typescript -const { txHash } = await client.cancelRefundRequest(paymentInfo, 0n); +const txHash = await client.refund.cancel(paymentInfo); console.log(`Refund request cancelled: ${txHash}`); ``` -## Query Refund State +## Query refund state -These methods read on-chain state for refund requests. None require a wallet client. +These methods read on-chain state for refund requests. ### Check existence and status ```typescript // Check if a refund request exists -const hasRequest = await client.hasRefundRequest(paymentInfo, 0n); +const hasRequest = await client.refund.has(paymentInfo); // Get just the status -const status = await client.getRefundStatus(paymentInfo, 0n); +const status = await client.refund.getStatus(paymentInfo); // Returns: RequestStatus.Pending | Approved | Denied | Cancelled ``` ### Get full refund request data ```typescript -// By paymentInfo + nonce -const request = await client.getRefundRequest(paymentInfo, 0n); +// By paymentInfo +const request = await client.refund.get(paymentInfo); console.log(request.amount, request.status); -// By composite key (from getMyRefundRequests) -const request2 = await client.getRefundRequestByKey(compositeKey); +// By paymentInfoHash +const request2 = await client.refund.getByKey(paymentInfoHash); ``` ### List your refund requests ```typescript -// Get total count -const count = await client.getMyRefundRequestCount(); - -// Get paginated keys, then fetch details -const { keys, total } = await client.getMyRefundRequests(0n, 10n); +// Get paginated refund request keys for a payer address +const { keys, total } = await client.refund.getPayerRequests( + payerAddress, + 0n, // offset + 10n // count +); for (const key of keys) { - const request = await client.getRefundRequestByKey(key); + const request = await client.refund.getByKey(key); console.log(`Amount: ${request.amount}, Status: ${request.status}`); } ``` -## Refund Request Lifecycle +### Retrieve stored payment info + +```typescript +// Get PaymentInfo stored by RefundRequest for a given hash +const storedInfo = await client.refund.getStoredPaymentInfo(paymentInfoHash); +``` + +## Refund request lifecycle ```mermaid stateDiagram-v2 - [*] --> Pending: requestRefund() - Pending --> Approved: Merchant/Arbiter approves + [*] --> Pending: refund.request() + Pending --> Approved: Merchant calls refundInEscrow() Pending --> Denied: Merchant/Arbiter denies - Pending --> Cancelled: cancelRefundRequest() + Pending --> Cancelled: refund.cancel() Approved --> [*]: Funds returned Denied --> [*] Cancelled --> [*] ``` -## Method Reference +## Method reference | Method | Parameters | Returns | |--------|-----------|---------| -| `requestRefund` | `paymentInfo, amount, nonce` | `{ txHash }` | -| `cancelRefundRequest` | `paymentInfo, nonce` | `{ txHash }` | -| `hasRefundRequest` | `paymentInfo, nonce` | `boolean` | -| `getRefundStatus` | `paymentInfo, nonce` | `RequestStatus` | -| `getRefundRequest` | `paymentInfo, nonce` | `RefundRequestData` | -| `getRefundRequestByKey` | `compositeKey` | `RefundRequestData` | -| `getMyRefundRequests` | `offset, count` | `{ keys, total }` | -| `getMyRefundRequestCount` | none | `bigint` | - -## Next Steps +| `refund.request` | `paymentInfo, amount` | `Hash` | +| `refund.cancel` | `paymentInfo` | `Hash` | +| `refund.has` | `paymentInfo` | `boolean` | +| `refund.getStatus` | `paymentInfo` | `RefundRequestStatus` | +| `refund.get` | `paymentInfo` | `RefundRequestData` | +| `refund.getByKey` | `paymentInfoHash` | `RefundRequestData` | +| `refund.getStoredPaymentInfo` | `paymentInfoHash` | `PaymentInfo` | +| `refund.getPayerRequests` | `payer, offset, count` | `{ keys, total }` | +| `refund.getCancelCount` | `paymentInfo` | `bigint` | +| `refund.getCancelledAmount` | `paymentInfo, cancelIndex` | `bigint` | + +## Next steps diff --git a/sdk/client/subscriptions.mdx b/sdk/client/subscriptions.mdx index cac10ce..52dff2f 100644 --- a/sdk/client/subscriptions.mdx +++ b/sdk/client/subscriptions.mdx @@ -1,35 +1,19 @@ --- -title: "Client Events" +title: "Client events" description: "Subscribe to real-time payment, refund, and freeze events as a payer" icon: "bell" --- -The Client SDK provides methods to subscribe to blockchain events in real-time using viem's `watchContractEvent` under the hood. All subscription methods return an object with an `unsubscribe` function that you should call when you no longer need the watcher. +The SDK provides methods to subscribe to blockchain events in real-time using viem's `watchContractEvent` under the hood. All watch methods return an unsubscribe function. -## watchPaymentState +## onPayment -Watch for state changes on a specific payment. This subscribes to `ReleaseExecuted`, `RefundInEscrowExecuted`, and `RefundPostEscrowExecuted` events on the `PaymentOperator` contract. +Watch for payment lifecycle events on the operator contract. This subscribes to `AuthorizationCreated`, `ChargeExecuted`, and `ReleaseExecuted` events. ```typescript -const { unsubscribe } = client.watchPaymentState( - paymentInfoHash, - (event) => { - console.log(`Payment event: ${event.eventName}`); - - switch (event.eventName) { - case 'ReleaseExecuted': - console.log('Funds released to merchant'); - console.log('Amount:', event.args.amount); - break; - case 'RefundInEscrowExecuted': - console.log('Funds refunded from escrow'); - break; - case 'RefundPostEscrowExecuted': - console.log('Funds refunded after escrow period'); - break; - } - } -); +const unsubscribe = client.watch.onPayment((event) => { + console.log('Payment event received:', event); +}); // Stop watching when done unsubscribe(); @@ -38,55 +22,16 @@ unsubscribe(); ### Signature ```typescript -watchPaymentState( - paymentInfoHash: `0x${string}`, - callback: (event: PaymentOperatorEventLog) => void -): { unsubscribe: () => void } -``` - -### PaymentOperatorEventLog Type - -```typescript -interface PaymentOperatorEventLog { - eventName: - | 'ReleaseExecuted' - | 'RefundInEscrowExecuted' - | 'RefundPostEscrowExecuted' - | 'AuthorizationCreated' - | 'ChargeExecuted'; - args: { - paymentInfoHash?: `0x${string}`; - payer?: `0x${string}`; - receiver?: `0x${string}`; - amount?: bigint; - }; - address: `0x${string}`; - blockNumber: bigint; - transactionHash: `0x${string}`; - logIndex: number; -} +onPayment(callback: (log: unknown) => void): () => void ``` -## watchRefundRequests +## onRefundRequest -Watch for refund request lifecycle events. This subscribes to `RefundRequested`, `RefundRequestStatusUpdated`, and `RefundRequestCancelled` events on the `RefundRequest` contract. +Watch for refund request lifecycle events on the RefundRequest contract. ```typescript -const { unsubscribe } = client.watchRefundRequests((event) => { - switch (event.eventName) { - case 'RefundRequested': - console.log('New refund request submitted'); - console.log('Payment:', event.args.paymentInfoHash); - console.log('Amount:', event.args.amount); - break; - case 'RefundRequestStatusUpdated': - console.log('Refund status changed:', event.args.status); - // 1 = Approved, 2 = Denied - break; - case 'RefundRequestCancelled': - console.log('Refund request cancelled'); - break; - } +const unsubscribe = client.watch.onRefundRequest((event) => { + console.log('Refund request event:', event); }); // Stop watching when done @@ -96,130 +41,63 @@ unsubscribe(); ### Signature ```typescript -watchRefundRequests( - callback: (event: RefundRequestEventLog) => void -): { unsubscribe: () => void } +onRefundRequest(callback: (log: unknown) => void): () => void ``` -Requires `refundRequestAddress` to be configured on the client. Throws an error if not set. +This is a no-op if no `refundRequestAddress` was resolved. The address is auto-resolved from the chain config. -### RefundRequestEventLog Type +## onRefundExecuted -```typescript -interface RefundRequestEventLog { - eventName: - | 'RefundRequested' - | 'RefundRequestStatusUpdated' - | 'RefundRequestCancelled'; - args: { - paymentInfoHash?: `0x${string}`; - payer?: `0x${string}`; - receiver?: `0x${string}`; - amount?: bigint; - nonce?: bigint; - status?: number; - }; - address: `0x${string}`; - blockNumber: bigint; - transactionHash: `0x${string}`; - logIndex: number; -} -``` - -## watchMyPayments - -Watch for new payment authorizations where the connected wallet is the payer. This subscribes to `AuthorizationCreated` events on the `PaymentOperator` contract, filtered by the wallet's address. +Watch for refund execution events (`RefundInEscrowExecuted`, `RefundPostEscrowExecuted`) on the operator contract. ```typescript -const { unsubscribe } = client.watchMyPayments((event) => { - console.log('New payment authorized!'); - console.log('Event:', event.eventName); // 'AuthorizationCreated' - console.log('Hash:', event.args.paymentInfoHash); - console.log('Receiver:', event.args.receiver); - console.log('Amount:', event.args.amount); +const unsubscribe = client.watch.onRefundExecuted((event) => { + console.log('Refund executed:', event); }); -// Stop watching when done unsubscribe(); ``` ### Signature ```typescript -watchMyPayments( - callback: (event: PaymentOperatorEventLog) => void -): { unsubscribe: () => void } +onRefundExecuted(callback: (log: unknown) => void): () => void ``` - -Requires a `walletClient` with an account to be configured, since the events are filtered by the payer address. - - -## watchFreezeEvents +## onFeeDistribution -Watch for freeze and unfreeze events on a specific `Freeze` contract. This subscribes to `PaymentFrozen` and `PaymentUnfrozen` events. +Watch for `FeesDistributed` events on the operator contract. ```typescript -const freezeAddress = '0x...'; // Freeze contract address - -const { unsubscribe } = client.watchFreezeEvents( - freezeAddress, - (event) => { - if (event.eventName === 'PaymentFrozen') { - console.log('Payment frozen:', event.args.paymentInfoHash); - console.log('Frozen by:', event.args.caller); - } else if (event.eventName === 'PaymentUnfrozen') { - console.log('Payment unfrozen:', event.args.paymentInfoHash); - console.log('Unfrozen by:', event.args.caller); - } - } -); +const unsubscribe = client.watch.onFeeDistribution((event) => { + console.log('Fees distributed:', event); +}); -// Stop watching when done unsubscribe(); ``` ### Signature ```typescript -watchFreezeEvents( - freezeAddress: `0x${string}`, - callback: (event: FreezeEventLog) => void -): { unsubscribe: () => void } -``` - -### FreezeEventLog Type - -```typescript -interface FreezeEventLog { - eventName: 'PaymentFrozen' | 'PaymentUnfrozen'; - args: { - paymentInfoHash?: `0x${string}`; - caller?: `0x${string}`; - }; - address: `0x${string}`; - blockNumber: bigint; - transactionHash: `0x${string}`; - logIndex: number; -} +onFeeDistribution(callback: (log: unknown) => void): () => void ``` -## Event Types Reference +## Event types reference | Method | Events Watched | Contract | Use Case | |--------|---------------|----------|----------| -| `watchPaymentState` | `ReleaseExecuted`, `RefundInEscrowExecuted`, `RefundPostEscrowExecuted` | PaymentOperator | Track a single payment's lifecycle | -| `watchRefundRequests` | `RefundRequested`, `RefundRequestStatusUpdated`, `RefundRequestCancelled` | RefundRequest | Monitor refund request workflow | -| `watchMyPayments` | `AuthorizationCreated` (filtered by payer) | PaymentOperator | Track new payments for your wallet | -| `watchFreezeEvents` | `PaymentFrozen`, `PaymentUnfrozen` | Freeze | Monitor dispute freeze activity | +| `onPayment` | `AuthorizationCreated`, `ChargeExecuted`, `ReleaseExecuted` | PaymentOperator | Track payment lifecycle | +| `onRefundRequest` | All RefundRequest events | RefundRequest | Monitor refund request workflow | +| `onRefundExecuted` | `RefundInEscrowExecuted`, `RefundPostEscrowExecuted` | PaymentOperator | Track refund execution | +| `onFeeDistribution` | `FeesDistributed` | PaymentOperator | Monitor fee distribution | -All subscription methods use viem's `watchContractEvent` under the hood. For reliable real-time delivery, configure your `publicClient` with a [WebSocket transport](https://viem.sh/docs/clients/transports/websocket). +For reliable real-time delivery, configure your `publicClient` with a [WebSocket transport](https://viem.sh/docs/clients/transports/websocket). -## Next Steps +## Next steps diff --git a/sdk/concepts.mdx b/sdk/concepts.mdx index 3b66c61..2a49212 100644 --- a/sdk/concepts.mdx +++ b/sdk/concepts.mdx @@ -64,65 +64,66 @@ The **EscrowPeriod** contract tracks when a payment was authorized and enforces - **After the period** — merchants can release funds to themselves ```typescript -import { X402rClient } from '@x402r/client'; +import { createPayerClient } from '@x402r/sdk'; -// Check when payment was authorized -const authTime = await client.getAuthorizationTime(paymentInfo, escrowPeriodAddress); +// Check when payment was authorized (requires escrowPeriodAddress in config) +const authTime = await client.escrow.getAuthorizationTime(paymentInfo); // Check if still within escrow period -const inEscrow = await client.isDuringEscrowPeriod(paymentInfo, escrowPeriodAddress); +const inEscrow = await client.escrow.isDuringEscrow(paymentInfo); if (!inEscrow) { console.log('Escrow period has passed - funds can be released'); } ``` -## Refund Requests +## Refund requests -When a payer wants a refund, they create a refund request that goes through approval: +When a payer wants a refund, they create a refund request. Each payment supports one refund request, keyed by its `paymentInfoHash`. ```typescript import { RequestStatus } from '@x402r/core'; // RequestStatus values: RequestStatus.Pending // 0 - Awaiting decision -RequestStatus.Approved // 1 - Approved by merchant/arbiter +RequestStatus.Approved // 1 - Auto-approved via refundInEscrow RequestStatus.Denied // 2 - Denied by merchant/arbiter RequestStatus.Cancelled // 3 - Cancelled by payer ``` -### Refund Flow +### Refund flow + +In v3, RefundRequest is wired as an IRecorder plugin. Refund approval happens automatically when the merchant or arbiter calls `refundInEscrow()` on the operator — no separate approve step is needed. ```mermaid sequenceDiagram participant P as Payer - participant R as RefundRequest Contract + participant R as RefundRequest (IRecorder) participant M as Merchant + participant O as PaymentOperator participant A as Arbiter - P->>R: requestRefund(paymentInfo, amount, nonce) + P->>R: requestRefund(paymentInfo, amount) R-->>M: RefundRequested event - alt Merchant approves - M->>R: approveRefundRequest(paymentInfo, nonce) - M->>R: refundInEscrow(paymentInfo, amount) + alt Merchant refunds + M->>O: refundInEscrow(paymentInfo, amount) + O->>R: record() auto-approves pending request + O->>P: Funds returned else Merchant denies - M->>R: denyRefundRequest(paymentInfo, nonce) + M->>R: denyRefundRequest(paymentInfo) else Escalate to arbiter - A->>R: approveRefundRequest(paymentInfo, nonce) - A->>R: executeRefundInEscrow(paymentInfo, amount) + A->>O: refundInEscrow(paymentInfo, amount) + O->>R: record() auto-approves pending request + O->>P: Funds returned end ``` -### The Nonce Parameter - -All refund methods require a `nonce` parameter. This is the record index from the PaymentIndexRecorder that identifies which charge the refund request applies to. For the first charge, use `0n`. - ```typescript -// Request refund for the first charge -await client.requestRefund(paymentInfo, amount, 0n); +// Request a refund (one per payment) +await client.refund.request(paymentInfo, amount); -// Check status for the first charge -const status = await client.getRefundStatus(paymentInfo, 0n); +// Check status +const status = await client.refund.getStatus(paymentInfo); ``` ## Freeze / Unfreeze @@ -130,14 +131,14 @@ const status = await client.getRefundStatus(paymentInfo, 0n); The **Freeze** contract allows payers to freeze a payment during the escrow period, preventing release until the freeze expires or is lifted: ```typescript -// Payer freezes payment (requires payer authorization) -await client.freezePayment(paymentInfo, freezeAddress); +// Payer freezes payment (requires freezeAddress in config) +await client.freeze.freeze(paymentInfo); -// Merchant unfreezes payment (requires receiver authorization) -await merchant.unfreezePayment(paymentInfo, freezeAddress); +// Merchant unfreezes payment +await merchant.freeze.unfreeze(paymentInfo); // Check frozen status -const frozen = await client.isFrozen(paymentInfo, freezeAddress); +const frozen = await client.freeze.isFrozen(paymentInfo); ``` @@ -149,8 +150,8 @@ Freezing a payment does not automatically escalate to an arbiter. It pauses the | Role | Can Do | |------|--------| | **Payer** | Request refunds, freeze payments, cancel requests, query escrow state | -| **Merchant** | Release payments, charge, approve/deny refunds, unfreeze payments | -| **Arbiter** | Approve/deny disputed refunds, execute refunds, batch operations, registry | +| **Merchant** | Release payments, charge, refund in escrow (auto-approves requests), deny refunds | +| **Arbiter** | Deny/refuse disputed refunds, refund in escrow, review evidence | ## Contract Architecture @@ -167,9 +168,10 @@ flowchart TB subgraph Components[Supporting Contracts] direction LR - subgraph RR[RefundRequest] + subgraph RR[RefundRequest IRecorder] rr1[Request/Cancel] - rr2[Approve/Deny] + rr2[Deny/Refuse] + rr3[Auto-approve on refund] end subgraph EP[EscrowPeriod] ep1[Track auth time] diff --git a/sdk/deploy-operator.mdx b/sdk/deploy-operator.mdx index a97990b..4280541 100644 --- a/sdk/deploy-operator.mdx +++ b/sdk/deploy-operator.mdx @@ -16,12 +16,12 @@ A complete marketplace operator deployment includes: 1. **EscrowPeriod** — Records authorization time, enforces waiting period before release 2. **Freeze** — Allows payer to freeze payment during escrow, receiver to unfreeze -3. **StaticAddressCondition** — Restricts refund approval to the designated arbiter -4. **OrCondition** — Allows either the receiver OR the arbiter to approve in-escrow refunds +3. **ReceiverCondition** — Gates in-escrow refunds to the merchant (receiver) +4. **RefundRequest (IRecorder)** — Wired as `refundInEscrowRecorder`, auto-approves pending refund requests during `refundInEscrow()` 5. **StaticFeeCalculator** — Optional operator fee (basis points) 6. **PaymentOperator** — The main contract tying everything together -All contracts are deployed via factories using CREATE2, so identical configurations produce identical addresses across deployments. +All contracts are deployed via factories using CREATE3, so identical configurations produce identical addresses across all supported chains. ## Deploy Your Operator @@ -143,8 +143,8 @@ interface MarketplaceOperatorDeployment { operatorAddress: Address; // The PaymentOperator escrowPeriodAddress: Address; // EscrowPeriod recorder/condition freezeAddress: Address; // Freeze condition - arbiterConditionAddress: Address; // StaticAddressCondition for arbiter - refundInEscrowCondition: Address; // OR(Receiver, Arbiter) + refundInEscrowCondition: Address; // ReceiverCondition (merchant-gated) + refundInEscrowRecorder: Address; // RefundRequest (auto-approve on refund) feeCalculatorAddress: Address | null; // null if no fee txHashes: Hash[]; // All deployment tx hashes summary: { @@ -155,7 +155,7 @@ interface MarketplaceOperatorDeployment { ``` -Because all contracts use CREATE2, redeploying with the same parameters is idempotent — it will detect existing contracts and skip them. The `summary` tells you what was new vs reused. +Because all contracts use CREATE3, redeploying with the same parameters is idempotent — it will detect existing contracts and skip them. The `summary` tells you what was new vs reused. ## Preview Addresses (No Deploy) @@ -189,7 +189,8 @@ The deployed operator has the following slot configuration: | `AUTHORIZE_RECORDER` | EscrowPeriod | Records authorization timestamp | | `CHARGE_CONDITION` | (none) | No restrictions on charge | | `RELEASE_CONDITION` | EscrowPeriod | Blocks release during escrow period | -| `REFUND_IN_ESCROW_CONDITION` | OR(Receiver, Arbiter) | Receiver or arbiter can approve | +| `REFUND_IN_ESCROW_CONDITION` | ReceiverCondition | Only the receiver (merchant) can call | +| `REFUND_IN_ESCROW_RECORDER` | RefundRequest | Auto-approves pending requests during refund | | `REFUND_POST_ESCROW_CONDITION` | Receiver | Only receiver after escrow | | `FEE_CALCULATOR` | StaticFeeCalculator | Fixed percentage fee | | `FEE_RECIPIENT` | Your address | Receives fees | @@ -201,16 +202,18 @@ Deployment is supported on all configured networks: | Network | Chain ID | EIP-155 ID | |---------|----------|------------| | Base Sepolia | 84532 | `eip155:84532` | -| Base Mainnet | 8453 | `eip155:8453` | +| Base | 8453 | `eip155:8453` | | Ethereum | 1 | `eip155:1` | | Ethereum Sepolia | 11155111 | `eip155:11155111` | -| Arbitrum Sepolia | 421614 | `eip155:421614` | -| Polygon | 137 | `eip155:137` | | Arbitrum | 42161 | `eip155:42161` | +| Arbitrum Sepolia | 421614 | `eip155:421614` | | Optimism | 10 | `eip155:10` | -| Avalanche | 43114 | `eip155:43114` | +| Polygon | 137 | `eip155:137` | | Celo | 42220 | `eip155:42220` | +| Avalanche | 43114 | `eip155:43114` | +| Linea | 59144 | `eip155:59144` | | Monad | 143 | `eip155:143` | +| SKALE Base | 1187947933 | `eip155:1187947933` | Deployment requires gas fees. Ensure your wallet has ETH on the target network. On Base Sepolia, you can get testnet ETH from [Base network faucets](https://docs.base.org/base-chain/tools/network-faucets). diff --git a/sdk/helpers/refundable.mdx b/sdk/helpers/refundable.mdx index 1d5fbfe..b2d8d77 100644 --- a/sdk/helpers/refundable.mdx +++ b/sdk/helpers/refundable.mdx @@ -121,7 +121,7 @@ const option = refundable({ ## Supported Networks -The function resolves addresses from the network config for all supported networks. See `getNetworkConfig()` for the full list (Base Sepolia, Base, Ethereum, Ethereum Sepolia, Arbitrum Sepolia, Polygon, Arbitrum, Optimism, Avalanche, Celo, Monad). +The function resolves addresses from the chain config for all supported networks. See `getChainConfig()` for the full list (Base Sepolia, Base, Ethereum, Ethereum Sepolia, Arbitrum, Arbitrum Sepolia, Optimism, Polygon, Celo, Avalanche, Linea, Monad, SKALE Base). ```typescript refundable({ network: 'eip155:84532', ... }, '0x...'); // Base Sepolia diff --git a/sdk/installation.mdx b/sdk/installation.mdx index 7485080..a54373b 100644 --- a/sdk/installation.mdx +++ b/sdk/installation.mdx @@ -9,19 +9,9 @@ icon: "download" Install only the packages you need for your use case: - + ```bash - npm install @x402r/client @x402r/core viem - ``` - - - ```bash - npm install @x402r/merchant @x402r/helpers @x402r/core viem - ``` - - - ```bash - npm install @x402r/arbiter @x402r/core viem + npm install @x402r/sdk @x402r/core viem ``` @@ -31,27 +21,29 @@ Install only the packages you need for your use case: -## Setup viem Clients +The `@x402r/sdk` package includes factory functions for all roles (payer, merchant, arbiter). + +## Setup viem clients -Create `publicClient` and `walletClient` using [viem](https://viem.sh/docs/clients/public). All SDK classes require these as constructor arguments. +Create `publicClient` and `walletClient` using [viem](https://viem.sh/docs/clients/public). All SDK factory functions require these as config arguments. -## Contract Addresses +## Contract addresses -Get the deployed contract addresses from the network config: +Get the deployed contract addresses from the chain config: ```typescript -import { getNetworkConfig } from '@x402r/core'; +import { getChainConfig } from '@x402r/core'; -const config = getNetworkConfig('eip155:84532'); // Base Sepolia +const config = getChainConfig(84532); // Base Sepolia console.log(config.authCaptureEscrow); // Escrow contract -console.log(config.refundRequest); // RefundRequest contract console.log(config.arbiterRegistry); // ArbiterRegistry contract -console.log(config.usdc); // USDC token address +console.log(config.usdc); // USDC token address +console.log(config.factories); // Factory addresses ``` -Network identifiers use the [EIP-155](https://eips.ethereum.org/EIPS/eip-155) format: `eip155:`. For Base Sepolia, use `'eip155:84532'`. For Base Mainnet, use `'eip155:8453'`. +`getChainConfig()` accepts a numeric chain ID (e.g., `84532` for Base Sepolia). All v3 protocol addresses are identical across every supported chain thanks to CREATE3 deployment. diff --git a/sdk/limitations.mdx b/sdk/limitations.mdx index 7e3a23c..ae1904b 100644 --- a/sdk/limitations.mdx +++ b/sdk/limitations.mdx @@ -8,38 +8,43 @@ The SDK provides full coverage of core payment flows including authorization, re ## API Constraints -### EIP-155 Network Identifiers +### Chain ID lookups -Network configuration requires EIP-155 format strings, not chain ID numbers: +Network configuration uses numeric chain IDs: ```typescript // Correct -const config = getNetworkConfig('eip155:84532'); +const config = getChainConfig(84532); -// Incorrect - will return undefined -const config = getNetworkConfig(84532); +// Incorrect - EIP-155 strings are no longer used for chain config +// const config = getNetworkConfig('eip155:84532'); ``` -### PaymentInfo Must Be Complete +Use `toNetworkId(chainId)` and `fromNetworkId(networkId)` to convert between numeric chain IDs and EIP-155 format strings when needed. -All SDK methods require a complete `PaymentInfo` object. You cannot query by hash alone: +### PaymentInfo must be complete + +Most SDK methods require a complete `PaymentInfo` object. You can use `refund.getStoredPaymentInfo(paymentInfoHash)` to retrieve stored PaymentInfo from the RefundRequest contract, or use the query plugin to look up payments by hash: ```typescript -// Works - full PaymentInfo -const status = await client.getRefundStatus(paymentInfo, 0n); +// Retrieve stored PaymentInfo from RefundRequest contract +const paymentInfo = await client.refund.getStoredPaymentInfo(paymentInfoHash); -// Not supported - hash-only queries require the full struct -// const state = await client.getPaymentStateByHash(hash); +// Or use the query plugin +const paymentInfo = await client.query.getPayment(paymentInfoHash); ``` ### Event Log Scanning Limits -`getPayerPayments()`, `getReceiverPayments()`, and `getPaymentDetails()` scan `AuthorizationCreated` events using `eth_getLogs`. Base Sepolia RPCs typically limit responses to 10,000 blocks. Pass a `fromBlock` parameter for large ranges: +The event-based query provider scans `AuthorizationCreated` and `ChargeExecuted` events using `eth_getLogs`. Base Sepolia RPCs typically limit responses to 10,000 blocks. Configure `eventFromBlock` in your client config to set the scan start: ```typescript -// Scan only recent blocks to avoid RPC limits -const payments = await client.getPayerPayments(recentBlockNumber); -const details = await client.getPaymentDetails(hash, recentBlockNumber); +const client = createPayerClient({ + publicClient, + walletClient, + operatorAddress: '0x...', + eventFromBlock: recentBlockNumber, // Required to enable event fallback provider +}); ``` ### No Express/Hono Middleware diff --git a/sdk/merchant/payment-operations.mdx b/sdk/merchant/payment-operations.mdx index e5bdd20..b6aa703 100644 --- a/sdk/merchant/payment-operations.mdx +++ b/sdk/merchant/payment-operations.mdx @@ -1,66 +1,64 @@ --- -title: "Payment Operations" +title: "Payment operations" description: "Release funds, charge payments, process refunds, and query escrow state with the Merchant SDK" icon: "coins" --- -The `X402rMerchant` class provides methods for managing the full payment lifecycle: releasing escrowed funds, charging directly for subscriptions, processing refunds, and querying operator configuration. +The merchant client provides methods for managing the full payment lifecycle: releasing escrowed funds, charging directly for subscriptions, processing refunds, and querying operator configuration. -## Payment Operations +## Payment operations -### Release Funds from Escrow +### Release funds from escrow -Use `release()` to transfer escrowed funds to the receiver (merchant). The `amount` parameter is **required** and specifies the exact amount to release in token units. +Use `payment.release()` to transfer escrowed funds to the receiver (merchant). The `amount` parameter is **required**. ```typescript -import { X402rMerchant } from '@x402r/merchant'; - // Release 10 USDC (6 decimals) from escrow -const { txHash } = await merchant.release(paymentInfo, BigInt('10000000')); +const txHash = await merchant.payment.release(paymentInfo, BigInt('10000000')); console.log('Released:', txHash); ``` -For partial releases, specify a smaller amount. The remaining funds stay in escrow and can be released or refunded later. +For partial releases, specify a smaller amount. The remaining funds stay in escrow. ```typescript // Release 3 USDC of a 10 USDC escrow -const { txHash } = await merchant.release(paymentInfo, BigInt('3000000')); +const txHash = await merchant.payment.release(paymentInfo, BigInt('3000000')); console.log('Partial release:', txHash); // Check what remains -const { capturableAmount } = await merchant.getPaymentAmounts(paymentInfo); -console.log('Remaining in escrow:', capturableAmount); // 7000000n +const amounts = await merchant.payment.getAmounts(paymentInfo); +console.log('Remaining in escrow:', amounts.capturableAmount); // 7000000n ``` -The `amount` parameter is always required. There is no default "release all" behavior. Always query `getPaymentAmounts()` first to determine the available capturable amount. +The `amount` parameter is always required. There is no default "release all" behavior. Always query `payment.getAmounts()` first to determine the available capturable amount. -### Refund While in Escrow +### Refund while in escrow -Use `refundInEscrow()` to return escrowed funds to the payer before release. The `amount` parameter is **required**. +Use `payment.refundInEscrow()` to return escrowed funds to the payer. This also auto-approves any pending RefundRequest. ```typescript // Full refund of 10 USDC -const { txHash } = await merchant.refundInEscrow(paymentInfo, BigInt('10000000')); +const txHash = await merchant.payment.refundInEscrow(paymentInfo, BigInt('10000000')); console.log('Refunded from escrow:', txHash); ``` ```typescript // Partial refund: return 2 USDC, keep 8 USDC in escrow -const { txHash } = await merchant.refundInEscrow(paymentInfo, BigInt('2000000')); +const txHash = await merchant.payment.refundInEscrow(paymentInfo, BigInt('2000000')); console.log('Partial refund:', txHash); ``` -### Charge Directly +### Charge directly -Use `charge()` for non-escrow flows such as subscriptions or session-based payments. This pulls funds directly from the payer via a token collector (e.g., ERC-3009 `transferWithAuthorization`). +Use `payment.charge()` for non-escrow flows such as subscriptions or session-based payments. ```typescript const tokenCollectorAddress: `0x${string}` = '0xTokenCollector...'; const collectorData: `0x${string}` = '0xSignatureOrCalldata...'; -const { txHash } = await merchant.charge( +const txHash = await merchant.payment.charge( paymentInfo, BigInt('5000000'), // 5 USDC tokenCollectorAddress, // token collector contract @@ -73,15 +71,15 @@ console.log('Charged:', txHash); The `charge()` method is designed for recurring payments and session-based billing where funds are not pre-escrowed. The token collector contract handles the actual token transfer. -### Refund After Release (Post-Escrow) +### Refund after release (post-escrow) -Use `refundPostEscrow()` to refund funds that have already been released to the receiver. This requires a token collector to source the refund from the merchant's balance. +Use `payment.refundPostEscrow()` to refund funds that have already been released to the receiver. ```typescript const tokenCollectorAddress: `0x${string}` = '0xTokenCollector...'; const collectorData: `0x${string}` = '0xSignatureOrCalldata...'; -const { txHash } = await merchant.refundPostEscrow( +const txHash = await merchant.payment.refundPostEscrow( paymentInfo, BigInt('5000000'), // 5 USDC to refund tokenCollectorAddress, // token collector that sources the refund @@ -94,167 +92,80 @@ console.log('Post-escrow refund:', txHash); Post-escrow refunds require the merchant to have sufficient token balance. The token collector pulls funds from the merchant to return to the payer. -## Query Methods - -### Get Payment Amounts +## Query methods -Use `getPaymentAmounts()` to query the current capturable and refundable amounts for a payment. This method reads directly from the escrow contract. +### Get payment amounts ```typescript -const { capturableAmount, refundableAmount } = await merchant.getPaymentAmounts(paymentInfo); +const amounts = await merchant.payment.getAmounts(paymentInfo); -console.log('Capturable:', capturableAmount); // Funds available to release -console.log('Refundable:', refundableAmount); // Funds available to refund +console.log('Capturable:', amounts.capturableAmount); // Funds available to release +console.log('Refundable:', amounts.refundableAmount); // Funds available to refund -if (capturableAmount > 0n) { - // Release available funds - const { txHash } = await merchant.release(paymentInfo, capturableAmount); +if (amounts.capturableAmount > 0n) { + const txHash = await merchant.payment.release(paymentInfo, amounts.capturableAmount); console.log('Released all capturable funds:', txHash); } ``` - -`getPaymentAmounts()` requires the `escrowAddress` to be configured when creating the `X402rMerchant` instance. - - -### Get Operator Configuration +### Get operator configuration -Use `getOperatorConfig()` to retrieve all 14 immutable slot addresses from the PaymentOperator contract. This includes the escrow address, fee configuration, all 5 condition slots, and all 5 recorder slots. +Use `operator.getConfig()` to retrieve all immutable slot addresses from the PaymentOperator contract, including conditions and recorders. ```typescript -const config = await merchant.getOperatorConfig(); +const config = await merchant.operator.getConfig(); // Core state -console.log('Escrow:', config.escrow); console.log('Fee recipient:', config.feeRecipient); console.log('Fee calculator:', config.feeCalculator); -console.log('Protocol fee config:', config.protocolFeeConfig); // Condition slots (address(0) = always allow) -console.log('Authorize condition:', config.authorizeCondition); -console.log('Charge condition:', config.chargeCondition); console.log('Release condition:', config.releaseCondition); console.log('Refund in-escrow condition:', config.refundInEscrowCondition); -console.log('Refund post-escrow condition:', config.refundPostEscrowCondition); // Recorder slots (address(0) = no-op) -console.log('Authorize recorder:', config.authorizeRecorder); -console.log('Charge recorder:', config.chargeRecorder); -console.log('Release recorder:', config.releaseRecorder); console.log('Refund in-escrow recorder:', config.refundInEscrowRecorder); -console.log('Refund post-escrow recorder:', config.refundPostEscrowRecorder); ``` -### Get Fee Structure +### Get fee addresses -Use `getFeeStructure()` to retrieve the fee-related addresses for the operator. This is a lighter alternative to `getOperatorConfig()` when you only need fee information. +Use `operator.getFeeAddresses()` for the fee-related addresses. This is a lighter alternative to `operator.getConfig()`. ```typescript -const fees = await merchant.getFeeStructure(); +const fees = await merchant.operator.getFeeAddresses(); console.log('Fee calculator:', fees.feeCalculator); console.log('Protocol fee config:', fees.protocolFeeConfig); console.log('Fee recipient:', fees.feeRecipient); ``` -The returned `FeeStructure` contains three fields: - -| Field | Type | Description | -|-------|------|-------------| -| `feeCalculator` | `0x${string}` | Contract that computes fee amounts | -| `protocolFeeConfig` | `0x${string}` | Protocol-level fee configuration | -| `feeRecipient` | `0x${string}` | Address that receives the operator's fee share | - -### Get Release Conditions - -Use `getReleaseConditions()` to check which condition contract governs release operations. A zero address means releases are always allowed. - -```typescript -const releaseCondition = await merchant.getReleaseConditions(); - -const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; -if (releaseCondition === ZERO_ADDRESS) { - console.log('No release conditions configured - releases always allowed'); -} else { - console.log('Release condition contract:', releaseCondition); -} -``` - -### Get Payment State - -Use `getPaymentState()` to derive the lifecycle state of a payment from the escrow contract. - -```typescript -import { PaymentState } from '@x402r/core'; - -const state = await merchant.getPaymentState(paymentInfo); -// PaymentState: NonExistent, InEscrow, Released, Settled, or Expired -``` - -```typescript -getPaymentState(paymentInfo: PaymentInfo): Promise -``` - -### Get Receiver Payments - -Use `getReceiverPayments()` to list all payments where the connected wallet is the receiver, by scanning `AuthorizationCreated` events. - -```typescript -const payments = await merchant.getReceiverPayments(); - -for (const { hash, paymentInfo } of payments) { - console.log(`Payment ${hash}: ${paymentInfo.maxAmount}`); -} -``` - -```typescript -getReceiverPayments( - fromBlock?: bigint -): Promise> -``` - - -This method scans event logs. Pass `fromBlock` to limit the scan range if your RPC limits `eth_getLogs` responses (Base Sepolia typically caps at 10,000 blocks). - - -### Get Payment Details - -Use `getPaymentDetails()` to retrieve the full `PaymentInfo` struct by scanning `AuthorizationCreated` events for a given hash. - -```typescript -const details = await merchant.getPaymentDetails(paymentInfoHash); -console.log('Payer:', details.payer); -console.log('Amount:', details.maxAmount); -``` +### Get payment state ```typescript -getPaymentDetails( - paymentInfoHash: `0x${string}`, - fromBlock?: bigint -): Promise +const [hasCollected, capturableAmount, refundableAmount] = await merchant.payment.getState(paymentInfo); ``` -## Release vs Refund Decision Flow +## Release vs refund decision flow ```mermaid flowchart TD - A[Payment in Escrow] --> B{Check getPaymentAmounts} + A[Payment in Escrow] --> B{Check payment.getAmounts} B --> C{capturableAmount > 0?} C -->|Yes| D{Has refund request?} C -->|No| E[Nothing to release] D -->|No| F[Safe to release] D -->|Yes| G{Approve refund?} - F --> H["release(paymentInfo, amount)"] - G -->|Yes| I["refundInEscrow(paymentInfo, amount)"] - G -->|No| J[Deny request, then release] + F --> H["payment.release(paymentInfo, amount)"] + G -->|Yes| I["payment.refundInEscrow(paymentInfo, amount)"] + G -->|No| J["refund.deny(paymentInfo), then release"] J --> H ``` -## Next Steps +## Next steps - Process incoming refund requests with approve/deny workflows. + Process incoming refund requests with deny workflows. Watch for real-time payment and refund events. diff --git a/sdk/merchant/quickstart.mdx b/sdk/merchant/quickstart.mdx index 4929d3f..dae3664 100644 --- a/sdk/merchant/quickstart.mdx +++ b/sdk/merchant/quickstart.mdx @@ -1,19 +1,19 @@ --- title: "Merchant SDK" -description: "Release funds, charge payments, process refunds, and query escrow state with @x402r/merchant" +description: "Release funds, charge payments, process refunds, and query escrow state with @x402r/sdk" icon: "rocket" --- -The `@x402r/merchant` package provides everything merchants need for the post-payment lifecycle: releasing escrowed funds, charging directly, processing refunds, and querying operator state. +The `@x402r/sdk` package provides everything merchants need for the post-payment lifecycle: releasing escrowed funds, charging directly, processing refunds, and querying operator state. -**Looking for server setup?** The [Merchant Server Quickstart](/sdk/merchant/getting-started) shows how to accept escrow payments via Express middleware. This page covers the `X402rMerchant` class for managing payments after they arrive. +**Looking for server setup?** The [Merchant Server Quickstart](/sdk/merchant/getting-started) shows how to accept escrow payments via Express middleware. This page covers the merchant client for managing payments after they arrive. ## Installation ```bash -npm install @x402r/merchant @x402r/helpers @x402r/core viem +npm install @x402r/sdk @x402r/helpers @x402r/core viem ``` ## Setup @@ -21,27 +21,27 @@ npm install @x402r/merchant @x402r/helpers @x402r/core viem Create viem clients as described in [Installation](/sdk/installation), then: ```typescript -import { X402rMerchant } from '@x402r/merchant'; -import { getNetworkConfig } from '@x402r/core'; +import { createMerchantClient } from '@x402r/sdk'; -const config = getNetworkConfig('eip155:84532')!; - -const merchant = new X402rMerchant({ +const merchant = createMerchantClient({ publicClient, walletClient, operatorAddress: '0x...', // Your PaymentOperator address - escrowAddress: config.authCaptureEscrow, - refundRequestAddress: config.refundRequest, + // Optional: enable escrow and freeze features + escrowPeriodAddress: '0x...', + freezeAddress: '0x...', }); ``` -## Release Funds from Escrow +Refund request and evidence addresses are auto-resolved from the chain config. + +## Release funds from escrow -Use `release()` to transfer escrowed funds to the receiver (merchant). The `amount` parameter is **required** and specifies the exact amount to release in token units. +Use `payment.release()` to transfer escrowed funds to the receiver (merchant). The `amount` parameter is **required** and specifies the exact amount to release in token units. ```typescript // Release 10 USDC (6 decimals) from escrow -const { txHash } = await merchant.release(paymentInfo, BigInt('10000000')); +const txHash = await merchant.payment.release(paymentInfo, BigInt('10000000')); console.log('Released:', txHash); ``` @@ -49,43 +49,47 @@ For partial releases, specify a smaller amount. The remaining funds stay in escr ```typescript // Release 3 USDC of a 10 USDC escrow -const { txHash } = await merchant.release(paymentInfo, BigInt('3000000')); +const txHash = await merchant.payment.release(paymentInfo, BigInt('3000000')); console.log('Partial release:', txHash); // Check what remains -const { capturableAmount } = await merchant.getPaymentAmounts(paymentInfo); -console.log('Remaining in escrow:', capturableAmount); // 7000000n +const amounts = await merchant.payment.getAmounts(paymentInfo); +console.log('Remaining in escrow:', amounts.capturableAmount); // 7000000n ``` -The `amount` parameter is always required. There is no default "release all" behavior. Always query `getPaymentAmounts()` first to determine the available capturable amount. +The `amount` parameter is always required. There is no default "release all" behavior. Always query `payment.getAmounts()` first to determine the available capturable amount. -## Refund While in Escrow +## Refund while in escrow -Use `refundInEscrow()` to return escrowed funds to the payer before release. The `amount` parameter is **required**. +Use `payment.refundInEscrow()` to return escrowed funds to the payer before release. This also auto-approves any pending RefundRequest for this payment. ```typescript // Full refund of 10 USDC -const { txHash } = await merchant.refundInEscrow(paymentInfo, BigInt('10000000')); +const txHash = await merchant.payment.refundInEscrow(paymentInfo, BigInt('10000000')); console.log('Refunded from escrow:', txHash); ``` ```typescript // Partial refund: return 2 USDC, keep 8 USDC in escrow -const { txHash } = await merchant.refundInEscrow(paymentInfo, BigInt('2000000')); +const txHash = await merchant.payment.refundInEscrow(paymentInfo, BigInt('2000000')); console.log('Partial refund:', txHash); ``` -## Charge Directly + +In v3, calling `refundInEscrow()` automatically approves any pending RefundRequest for this payment via the IRecorder plugin. You do not need a separate approve step. + + +## Charge directly -Use `charge()` for non-escrow flows such as subscriptions or session-based payments. This pulls funds directly from the payer via a token collector (e.g., ERC-3009 `transferWithAuthorization`). +Use `payment.charge()` for non-escrow flows such as subscriptions or session-based payments. This pulls funds directly from the payer via a token collector (e.g., ERC-3009 `transferWithAuthorization`). ```typescript const tokenCollectorAddress: `0x${string}` = '0xTokenCollector...'; const collectorData: `0x${string}` = '0xSignatureOrCalldata...'; -const { txHash } = await merchant.charge( +const txHash = await merchant.payment.charge( paymentInfo, BigInt('5000000'), // 5 USDC tokenCollectorAddress, // token collector contract @@ -98,15 +102,15 @@ console.log('Charged:', txHash); The `charge()` method is designed for recurring payments and session-based billing where funds are not pre-escrowed. The token collector contract handles the actual token transfer. -## Refund After Release (Post-Escrow) +## Refund after release (post-escrow) -Use `refundPostEscrow()` to refund funds that have already been released to the receiver. This requires a token collector to source the refund from the merchant's balance. +Use `payment.refundPostEscrow()` to refund funds that have already been released to the receiver. This requires a token collector to source the refund from the merchant's balance. ```typescript const tokenCollectorAddress: `0x${string}` = '0xTokenCollector...'; const collectorData: `0x${string}` = '0xSignatureOrCalldata...'; -const { txHash } = await merchant.refundPostEscrow( +const txHash = await merchant.payment.refundPostEscrow( paymentInfo, BigInt('5000000'), // 5 USDC to refund tokenCollectorAddress, // token collector that sources the refund @@ -119,118 +123,73 @@ console.log('Post-escrow refund:', txHash); Post-escrow refunds require the merchant to have sufficient token balance. The token collector pulls funds from the merchant to return to the payer. -## Query Methods - -### Get Payment Amounts +## Query methods -Use `getPaymentAmounts()` to query the current capturable and refundable amounts for a payment. +### Get payment amounts ```typescript -const { capturableAmount, refundableAmount } = await merchant.getPaymentAmounts(paymentInfo); +const amounts = await merchant.payment.getAmounts(paymentInfo); -console.log('Capturable:', capturableAmount); // Funds available to release -console.log('Refundable:', refundableAmount); // Funds available to refund +console.log('Capturable:', amounts.capturableAmount); // Funds available to release +console.log('Refundable:', amounts.refundableAmount); // Funds available to refund -if (capturableAmount > 0n) { - const { txHash } = await merchant.release(paymentInfo, capturableAmount); +if (amounts.capturableAmount > 0n) { + const txHash = await merchant.payment.release(paymentInfo, amounts.capturableAmount); console.log('Released all capturable funds:', txHash); } ``` - -`getPaymentAmounts()` requires the `escrowAddress` to be configured when creating the `X402rMerchant` instance. - - -### Get Payment State - -Use `getPaymentState()` to derive the lifecycle state of a payment from the escrow contract. - -```typescript -import { PaymentState } from '@x402r/core'; - -const state = await merchant.getPaymentState(paymentInfo); -// PaymentState: NonExistent, InEscrow, Released, Settled, or Expired -``` - -### Get Receiver Payments - -Use `getReceiverPayments()` to list all payments where the connected wallet is the receiver. - -```typescript -const payments = await merchant.getReceiverPayments(); - -for (const { hash, paymentInfo } of payments) { - console.log(`Payment ${hash}: ${paymentInfo.maxAmount}`); -} -``` - - -This method scans event logs. Pass `fromBlock` to limit the scan range if your RPC limits `eth_getLogs` responses (Base Sepolia typically caps at 10,000 blocks). - - -### Get Payment Details - -Use `getPaymentDetails()` to retrieve the full `PaymentInfo` struct by scanning `AuthorizationCreated` events for a given hash. +### Get payment state ```typescript -const details = await merchant.getPaymentDetails(paymentInfoHash); -console.log('Payer:', details.payer); -console.log('Amount:', details.maxAmount); +const [hasCollected, capturableAmount, refundableAmount] = await merchant.payment.getState(paymentInfo); ``` -### Get Operator Configuration +### Get operator configuration -Use `getOperatorConfig()` to retrieve all 14 immutable slot addresses from the PaymentOperator contract. +Use `operator.getConfig()` to retrieve all immutable slot addresses from the PaymentOperator contract. ```typescript -const config = await merchant.getOperatorConfig(); +const config = await merchant.operator.getConfig(); -console.log('Escrow:', config.escrow); console.log('Fee recipient:', config.feeRecipient); console.log('Fee calculator:', config.feeCalculator); console.log('Release condition:', config.releaseCondition); ``` -### Get Fee Structure +### Get fee structure -Use `getFeeStructure()` for just the fee-related addresses — a lighter alternative to `getOperatorConfig()`. +Use `operator.getFeeAddresses()` for just the fee-related addresses. ```typescript -const fees = await merchant.getFeeStructure(); +const fees = await merchant.operator.getFeeAddresses(); console.log('Fee calculator:', fees.feeCalculator); console.log('Protocol fee config:', fees.protocolFeeConfig); console.log('Fee recipient:', fees.feeRecipient); ``` -### Get Release Conditions - -```typescript -const releaseCondition = await merchant.getReleaseConditions(); -// address(0) means releases are always allowed -``` - -## Release vs Refund Decision Flow +## Release vs refund decision flow ```mermaid flowchart TD - A[Payment in Escrow] --> B{Check getPaymentAmounts} + A[Payment in Escrow] --> B{Check payment.getAmounts} B --> C{capturableAmount > 0?} C -->|Yes| D{Has refund request?} C -->|No| E[Nothing to release] D -->|No| F[Safe to release] D -->|Yes| G{Approve refund?} - F --> H["release(paymentInfo, amount)"] - G -->|Yes| I["refundInEscrow(paymentInfo, amount)"] - G -->|No| J[Deny request, then release] + F --> H["payment.release(paymentInfo, amount)"] + G -->|Yes| I["payment.refundInEscrow(paymentInfo, amount)"] + G -->|No| J["refund.deny(paymentInfo), then release"] J --> H ``` -## Next Steps +## Next steps - Process incoming refund requests with approve/deny workflows. + Process incoming refund requests with deny workflows. Mark payment options as refundable with your operator. diff --git a/sdk/merchant/refund-handling.mdx b/sdk/merchant/refund-handling.mdx index 02e8b31..bc298c6 100644 --- a/sdk/merchant/refund-handling.mdx +++ b/sdk/merchant/refund-handling.mdx @@ -1,23 +1,21 @@ --- -title: "Refund Handling" -description: "Process, approve, deny, and manage refund requests with the Merchant SDK" +title: "Refund handling" +description: "Process, deny, and manage refund requests with the Merchant SDK" icon: "rotate-left" --- -The `X402rMerchant` class provides a complete set of methods for handling refund requests from payers. Every refund-related method requires a `nonce: bigint` parameter that identifies which specific charge the refund targets. +The merchant client provides methods for handling refund requests from payers. In v3, refund approval happens automatically when you call `payment.refundInEscrow()` — the RefundRequest IRecorder plugin auto-approves any pending request during execution. -The `nonce` parameter corresponds to the record index from the `PaymentIndexRecorder`. For the first charge against a payment, the nonce is `0n`. Each subsequent charge increments the nonce. +Each payment supports one refund request, keyed by `paymentInfoHash`. There is no nonce parameter — one request per payment. -## Refund Request Queries +## Refund request queries -### Check If a Refund Request Exists - -Use `hasRefundRequest()` to check whether a payer has submitted a refund request for a specific payment and nonce. +### Check if a refund request exists ```typescript -const hasRequest = await merchant.hasRefundRequest(paymentInfo, 0n); +const hasRequest = await merchant.refund.has(paymentInfo); if (hasRequest) { console.log('Refund request exists for this payment'); @@ -26,21 +24,19 @@ if (hasRequest) { } ``` -### Get Refund Request Status - -Use `getRefundStatus()` to retrieve the current status of a refund request. Returns a `RequestStatus` enum value. +### Get refund request status ```typescript import { RequestStatus } from '@x402r/core'; -const status = await merchant.getRefundStatus(paymentInfo, 0n); +const status = await merchant.refund.getStatus(paymentInfo); switch (status) { case RequestStatus.Pending: console.log('Awaiting your decision'); break; case RequestStatus.Approved: - console.log('You approved this refund'); + console.log('Auto-approved via refundInEscrow'); break; case RequestStatus.Denied: console.log('You denied this refund'); @@ -51,118 +47,59 @@ switch (status) { } ``` -### Get Full Refund Request Data - -Use `getRefundRequest()` to retrieve the complete refund request data, including the amount and status. +### Get full refund request data ```typescript -import type { RefundRequestData } from '@x402r/core'; - -const request: RefundRequestData = await merchant.getRefundRequest(paymentInfo, 0n); +const request = await merchant.refund.get(paymentInfo); console.log('Payment hash:', request.paymentInfoHash); -console.log('Nonce:', request.nonce); console.log('Requested amount:', request.amount); console.log('Status:', request.status); ``` -The `RefundRequestData` type contains: - -| Field | Type | Description | -|-------|------|-------------| -| `paymentInfoHash` | `0x${string}` | Hash of the PaymentInfo struct | -| `nonce` | `bigint` | Record index this refund targets | -| `amount` | `bigint` | Amount requested for refund (uint120) | -| `status` | `RequestStatus` | Current status (Pending, Approved, Denied, Cancelled) | - -### Get Refund Request by Composite Key +### Get refund request by key -Use `getRefundRequestByKey()` to look up a refund request directly by its composite key (the `keccak256(paymentInfoHash, nonce)` value returned from paginated queries). +Use `getByKey()` to look up a refund request directly by its `paymentInfoHash` (returned from paginated queries). ```typescript -const request = await merchant.getRefundRequestByKey(compositeKey); +const request = await merchant.refund.getByKey(paymentInfoHash); console.log('Amount:', request.amount); console.log('Status:', request.status); ``` -## Paginated Refund Request Listing +## Paginated refund request listing -### Get Pending Refund Requests +### Get refund requests for a receiver -Use `getPendingRefundRequests()` to retrieve paginated refund request keys for the current receiver address. This method uses the wallet address associated with your `X402rMerchant` instance. +Use `getReceiverRequests()` to retrieve paginated refund request keys for a receiver address. ```typescript +const receiverAddress = walletClient.account.address; + // Get the first 10 refund request keys -const { keys, total } = await merchant.getPendingRefundRequests(0n, 10n); +const { keys, total } = await merchant.refund.getReceiverRequests( + receiverAddress, + 0n, // offset + 10n // count +); console.log(`Showing ${keys.length} of ${total} total refund requests`); -// Look up each request by its composite key for (const key of keys) { - const request = await merchant.getRefundRequestByKey(key); - console.log(`Key: ${key}`); - console.log(` Amount: ${request.amount}`); - console.log(` Status: ${request.status}`); -} -``` - -For pagination, adjust the `offset` and `count` parameters: - -```typescript -// Page through all refund requests, 20 at a time -const pageSize = 20n; -let offset = 0n; -let hasMore = true; - -while (hasMore) { - const { keys, total } = await merchant.getPendingRefundRequests(offset, pageSize); - - for (const key of keys) { - const request = await merchant.getRefundRequestByKey(key); - // Process each request... - } - - offset += pageSize; - hasMore = offset < total; -} -``` - -### Get Refund Request Count - -Use `getRefundRequestCount()` to get the total number of refund requests targeting the current receiver. - -```typescript -const count = await merchant.getRefundRequestCount(); -console.log(`Total refund requests: ${count}`); - -if (count > 0n) { - const { keys } = await merchant.getPendingRefundRequests(0n, count); - console.log(`Retrieved all ${keys.length} request keys`); + const request = await merchant.refund.getByKey(key); + console.log(`Key: ${key}, Amount: ${request.amount}, Status: ${request.status}`); } ``` -## Refund Request Actions +## Refund request actions -### Approve a Refund Request +### Deny a refund request -Use `approveRefundRequest()` to approve a pending refund request. This changes the request status to `Approved`. +Use `refund.deny()` to deny a pending refund request. This changes the request status to `Denied`. ```typescript -const { txHash } = await merchant.approveRefundRequest(paymentInfo, 0n); -console.log('Refund approved:', txHash); -``` - - -Approving a refund request changes its status but does **not** transfer funds. You must also call `refundInEscrow()` or `refundPostEscrow()` to execute the actual token transfer. - - -### Deny a Refund Request - -Use `denyRefundRequest()` to deny a pending refund request. This changes the request status to `Denied`. - -```typescript -const { txHash } = await merchant.denyRefundRequest(paymentInfo, 0n); +const txHash = await merchant.refund.deny(paymentInfo); console.log('Refund denied:', txHash); ``` @@ -170,60 +107,37 @@ console.log('Refund denied:', txHash); If you deny a request, the payer may escalate to an arbiter for dispute resolution. Consider providing a reason off-chain to reduce escalation risk. -## Freeze Management - -### Check If a Payment Is Frozen - -Use `isFrozen()` to check whether a payment has been frozen by the payer or an arbiter. Frozen payments cannot be released until unfrozen. - -```typescript -const freezeAddress: `0x${string}` = '0xFreezeContract...'; - -const frozen = await merchant.isFrozen(paymentInfo, freezeAddress); +### Refund in escrow (auto-approves) -if (frozen) { - console.log('Payment is frozen - cannot release until unfrozen'); -} else { - console.log('Payment is not frozen'); -} -``` - -### Unfreeze a Payment - -Use `unfreezePayment()` to remove a freeze on a payment. Only the receiver (merchant) or an authorized party can unfreeze. +Instead of a separate approve step, you call `payment.refundInEscrow()` directly. The RefundRequest recorder plugin auto-approves any pending request during execution. ```typescript -const freezeAddress: `0x${string}` = '0xFreezeContract...'; - -const { txHash } = await merchant.unfreezePayment(paymentInfo, freezeAddress); -console.log('Payment unfrozen:', txHash); +const txHash = await merchant.payment.refundInEscrow(paymentInfo, request.amount); +console.log('Refund executed (request auto-approved):', txHash); ``` -## Complete Refund Workflow +## Complete refund workflow Here is a full workflow showing how to detect a refund request, review it, make a decision, and execute the refund if approved. ```typescript -import { createPublicClient, createWalletClient, http } from 'viem'; -import { baseSepolia } from 'viem/chains'; -import { privateKeyToAccount } from 'viem/accounts'; -import { X402rMerchant } from '@x402r/merchant'; -import { getNetworkConfig, RequestStatus } from '@x402r/core'; +import { createMerchantClient } from '@x402r/sdk'; +import { RequestStatus } from '@x402r/core'; +import type { PaymentInfo } from '@x402r/core'; async function handleRefundWorkflow( - merchant: X402rMerchant, - paymentInfo: PaymentInfo, - nonce: bigint + merchant: ReturnType, + paymentInfo: PaymentInfo ) { // Step 1: Check if a refund request exists - const hasRequest = await merchant.hasRefundRequest(paymentInfo, nonce); + const hasRequest = await merchant.refund.has(paymentInfo); if (!hasRequest) { - console.log('No refund request for this payment/nonce'); + console.log('No refund request for this payment'); return; } // Step 2: Get the full request data - const request = await merchant.getRefundRequest(paymentInfo, nonce); + const request = await merchant.refund.get(paymentInfo); console.log(`Refund request: ${request.amount} tokens, status: ${request.status}`); // Step 3: Only process pending requests @@ -232,82 +146,68 @@ async function handleRefundWorkflow( return; } - // Step 4: Check if the payment is frozen - const freezeAddress: `0x${string}` = '0xFreezeContract...'; - const frozen = await merchant.isFrozen(paymentInfo, freezeAddress); - if (frozen) { - console.log('Payment is frozen - resolve dispute before processing refund'); - return; - } - - // Step 5: Check available amounts - const { capturableAmount, refundableAmount } = await merchant.getPaymentAmounts(paymentInfo); - console.log(`Available to refund: ${refundableAmount}`); - - // Step 6: Make a decision - const shouldApprove = request.amount <= refundableAmount; + // Step 4: Check available amounts + const amounts = await merchant.payment.getAmounts(paymentInfo); + console.log(`Available to refund: ${amounts.refundableAmount}`); - if (shouldApprove) { - // Approve the request - const { txHash: approveTx } = await merchant.approveRefundRequest(paymentInfo, nonce); - console.log('Approved:', approveTx); + // Step 5: Make a decision + const shouldRefund = request.amount <= amounts.refundableAmount; - // Execute the refund from escrow - const { txHash: refundTx } = await merchant.refundInEscrow(paymentInfo, request.amount); - console.log('Refund executed:', refundTx); + if (shouldRefund) { + // Execute the refund (auto-approves the request) + const txHash = await merchant.payment.refundInEscrow(paymentInfo, request.amount); + console.log('Refund executed:', txHash); } else { // Deny the request - const { txHash: denyTx } = await merchant.denyRefundRequest(paymentInfo, nonce); - console.log('Denied:', denyTx); + const txHash = await merchant.refund.deny(paymentInfo); + console.log('Denied:', txHash); } } ``` -## Refund Request Lifecycle +## Refund request lifecycle ```mermaid sequenceDiagram - participant P as Payer (Client SDK) - participant R as RefundRequest Contract - participant M as Merchant (Merchant SDK) + participant P as Payer + participant R as RefundRequest (IRecorder) + participant M as Merchant participant O as PaymentOperator - P->>R: requestRefund(paymentInfo, amount, nonce) + P->>R: refund.request(paymentInfo, amount) R-->>M: RefundRequested event - M->>R: hasRefundRequest(paymentInfo, nonce) + M->>R: refund.has(paymentInfo) R-->>M: true - M->>R: getRefundRequest(paymentInfo, nonce) + M->>R: refund.get(paymentInfo) R-->>M: RefundRequestData M->>M: Review request (policy check) - alt Approve - M->>R: approveRefundRequest(paymentInfo, nonce) - M->>O: refundInEscrow(paymentInfo, amount) + alt Refund + M->>O: payment.refundInEscrow(paymentInfo, amount) + O->>R: record() auto-approves pending request O->>P: Funds returned to payer else Deny - M->>R: denyRefundRequest(paymentInfo, nonce) + M->>R: refund.deny(paymentInfo) Note over P: Payer may escalate to arbiter end ``` -## Method Reference +## Method reference | Method | Parameters | Returns | Description | |--------|-----------|---------|-------------| -| `hasRefundRequest` | `paymentInfo, nonce: bigint` | `Promise` | Check if refund request exists | -| `getRefundStatus` | `paymentInfo, nonce: bigint` | `Promise` | Get request status | -| `getRefundRequest` | `paymentInfo, nonce: bigint` | `Promise` | Get full request data | -| `approveRefundRequest` | `paymentInfo, nonce: bigint` | `Promise<{ txHash }>` | Approve a pending request | -| `denyRefundRequest` | `paymentInfo, nonce: bigint` | `Promise<{ txHash }>` | Deny a pending request | -| `getPendingRefundRequests` | `offset: bigint, count: bigint` | `Promise<{ keys, total }>` | Paginated request keys | -| `getRefundRequestCount` | _(none)_ | `Promise` | Total requests for receiver | -| `getRefundRequestByKey` | `compositeKey: hex` | `Promise` | Look up by composite key | -| `unfreezePayment` | `paymentInfo, freezeAddress: hex` | `Promise<{ txHash }>` | Remove payment freeze | -| `isFrozen` | `paymentInfo, freezeAddress: hex` | `Promise` | Check if payment is frozen | - -## Next Steps +| `refund.has` | `paymentInfo` | `Promise` | Check if refund request exists | +| `refund.getStatus` | `paymentInfo` | `Promise` | Get request status | +| `refund.get` | `paymentInfo` | `Promise` | Get full request data | +| `refund.deny` | `paymentInfo` | `Promise` | Deny a pending request | +| `refund.getReceiverRequests` | `receiver, offset, count` | `Promise<{ keys, total }>` | Paginated request keys | +| `refund.getByKey` | `paymentInfoHash` | `Promise` | Look up by key | +| `payment.refundInEscrow` | `paymentInfo, amount, data?` | `Promise` | Refund and auto-approve | +| `freeze.isFrozen` | `paymentInfo` | `Promise` | Check if payment is frozen | + +## Next steps diff --git a/sdk/merchant/subscriptions.mdx b/sdk/merchant/subscriptions.mdx index 813b31d..0d1dba5 100644 --- a/sdk/merchant/subscriptions.mdx +++ b/sdk/merchant/subscriptions.mdx @@ -1,190 +1,115 @@ --- -title: "Merchant Events" +title: "Merchant events" description: "Subscribe to real-time refund, release, and freeze events with the Merchant SDK" icon: "bell" --- -The `X402rMerchant` class provides three subscription methods for watching blockchain events in real-time. Each returns an object with an `unsubscribe` function for cleanup. +The merchant client provides watch methods for subscribing to blockchain events in real-time. Each returns an unsubscribe function for cleanup. -## Watch Refund Requests +## Watch refund requests -Use `watchRefundRequests()` to subscribe to `RefundRequested` events emitted by the RefundRequest contract. The callback receives a `RefundRequestEventLog` object for each event. +Use `watch.onRefundRequest()` to subscribe to events emitted by the RefundRequest contract. ```typescript -const { unsubscribe } = merchant.watchRefundRequests((event) => { - console.log('Event:', event.eventName); - console.log('Payment hash:', event.args.paymentInfoHash); - console.log('Payer:', event.args.payer); - console.log('Receiver:', event.args.receiver); - console.log('Amount:', event.args.amount); - console.log('Nonce:', event.args.nonce); - console.log('Block:', event.blockNumber); - console.log('Tx hash:', event.transactionHash); +const unsubscribe = merchant.watch.onRefundRequest((event) => { + console.log('Refund request event:', event); }); // Later: stop watching unsubscribe(); ``` -The `RefundRequestEventLog` type has the following shape: - -```typescript -interface RefundRequestEventLog { - eventName: 'RefundRequested' | 'RefundRequestStatusUpdated' | 'RefundRequestCancelled'; - args: { - paymentInfoHash?: `0x${string}`; - payer?: `0x${string}`; - receiver?: `0x${string}`; - amount?: bigint; - nonce?: bigint; - status?: number; - }; - address: `0x${string}`; - blockNumber: bigint; - transactionHash: `0x${string}`; - logIndex: number; -} -``` - -`watchRefundRequests()` requires the `refundRequestAddress` to be configured when creating the `X402rMerchant` instance. +This is a no-op if no `refundRequestAddress` was resolved. The address is auto-resolved from the chain config. -### Example: Auto-respond to Small Refund Requests +### Example: auto-respond to small refund requests ```typescript -import { X402rMerchant } from '@x402r/merchant'; -import { RequestStatus } from '@x402r/core'; +import { createMerchantClient } from '@x402r/sdk'; const AUTO_APPROVE_THRESHOLD = BigInt('5000000'); // 5 USDC -const { unsubscribe } = merchant.watchRefundRequests(async (event) => { - const amount = event.args.amount; - const paymentHash = event.args.paymentInfoHash; +const unsubscribe = merchant.watch.onRefundRequest(async (event: any) => { + const amount = event.args?.amount; - console.log(`New refund request: ${paymentHash}, amount: ${amount}`); + console.log('New refund request event'); if (amount && amount < AUTO_APPROVE_THRESHOLD) { - console.log('Auto-approving small refund request'); - // You would look up the paymentInfo from your database - // and call merchant.approveRefundRequest(paymentInfo, nonce) + console.log('Auto-refunding small request'); + // Look up paymentInfo from your database, then: + // await merchant.payment.refundInEscrow(paymentInfo, amount) } else { console.log('Queuing for manual review'); } }); ``` -## Watch Releases +## Watch payment events -Use `watchReleases()` to subscribe to `ReleaseExecuted` events emitted by the PaymentOperator contract. The callback receives a `PaymentOperatorEventLog` object for each event. +Use `watch.onPayment()` to subscribe to `AuthorizationCreated`, `ChargeExecuted`, and `ReleaseExecuted` events on the PaymentOperator contract. ```typescript -const { unsubscribe } = merchant.watchReleases((event) => { - console.log('Release executed!'); - console.log('Payment hash:', event.args.paymentInfoHash); - console.log('Amount:', event.args.amount); - console.log('Payer:', event.args.payer); - console.log('Receiver:', event.args.receiver); - console.log('Block:', event.blockNumber); - console.log('Tx hash:', event.transactionHash); +const unsubscribe = merchant.watch.onPayment((event) => { + console.log('Payment event:', event); }); // Later: stop watching unsubscribe(); ``` -The `PaymentOperatorEventLog` type has the following shape: - -```typescript -interface PaymentOperatorEventLog { - eventName: 'ReleaseExecuted' | 'RefundInEscrowExecuted' | 'RefundPostEscrowExecuted' - | 'AuthorizationCreated' | 'ChargeExecuted'; - args: { - paymentInfoHash?: `0x${string}`; - payer?: `0x${string}`; - receiver?: `0x${string}`; - amount?: bigint; - }; - address: `0x${string}`; - blockNumber: bigint; - transactionHash: `0x${string}`; - logIndex: number; -} -``` - -### Example: Revenue Tracking +### Example: revenue tracking ```typescript let totalReleased = 0n; -const { unsubscribe } = merchant.watchReleases((event) => { - const amount = event.args.amount ?? 0n; - totalReleased += amount; - - console.log(`Release: +${amount} tokens`); - console.log(`Total released: ${totalReleased}`); +const unsubscribe = merchant.watch.onPayment((event: any) => { + if (event.eventName === 'ReleaseExecuted') { + const amount = event.args?.amount ?? 0n; + totalReleased += amount; + console.log(`Release: +${amount} tokens, Total: ${totalReleased}`); + } }); ``` -## Watch Freeze Events +## Watch refund execution -Use `watchFreezeEvents()` to subscribe to `PaymentFrozen` and `PaymentUnfrozen` events from a specific Freeze contract. You must provide the Freeze contract address as the first argument. +Use `watch.onRefundExecuted()` to subscribe to `RefundInEscrowExecuted` and `RefundPostEscrowExecuted` events. ```typescript -const freezeAddress: `0x${string}` = '0xFreezeContract...'; - -const { unsubscribe } = merchant.watchFreezeEvents( - freezeAddress, - (event) => { - if (event.eventName === 'PaymentFrozen') { - console.log('Payment FROZEN:', event.args.paymentInfoHash); - console.log('Frozen by:', event.args.caller); - // Alert: a dispute may be in progress - } else if (event.eventName === 'PaymentUnfrozen') { - console.log('Payment UNFROZEN:', event.args.paymentInfoHash); - console.log('Unfrozen by:', event.args.caller); - // The payment can now be released - } - } -); +const unsubscribe = merchant.watch.onRefundExecuted((event) => { + console.log('Refund executed:', event); +}); -// Later: stop watching unsubscribe(); ``` -The `FreezeEventLog` type has the following shape: +## Watch fee distribution + +Use `watch.onFeeDistribution()` to subscribe to `FeesDistributed` events. ```typescript -interface FreezeEventLog { - eventName: 'PaymentFrozen' | 'PaymentUnfrozen'; - args: { - paymentInfoHash?: `0x${string}`; - caller?: `0x${string}`; - }; - address: `0x${string}`; - blockNumber: bigint; - transactionHash: `0x${string}`; - logIndex: number; -} -``` +const unsubscribe = merchant.watch.onFeeDistribution((event) => { + console.log('Fees distributed:', event); +}); - -The `freezeAddress` parameter is the address of the Freeze condition contract, not the PaymentOperator. You can retrieve it from your operator config via `merchant.getOperatorConfig()`. - +unsubscribe(); +``` -## Event Types Reference +## Event types reference -| Method | Event Name | Callback Type | Use Case | -|--------|-----------|---------------|----------| -| `watchRefundRequests` | `RefundRequested` | `RefundRequestEventLog` | Detect incoming refund requests | -| `watchReleases` | `ReleaseExecuted` | `PaymentOperatorEventLog` | Track revenue and release confirmations | -| `watchFreezeEvents` | `PaymentFrozen` / `PaymentUnfrozen` | `FreezeEventLog` | Monitor dispute-related freezes | +| Method | Events Watched | Contract | Use Case | +|--------|---------------|----------|----------| +| `watch.onPayment` | `AuthorizationCreated`, `ChargeExecuted`, `ReleaseExecuted` | PaymentOperator | Track payment lifecycle | +| `watch.onRefundRequest` | All RefundRequest events | RefundRequest | Detect incoming refund requests | +| `watch.onRefundExecuted` | `RefundInEscrowExecuted`, `RefundPostEscrowExecuted` | PaymentOperator | Track refund execution | +| `watch.onFeeDistribution` | `FeesDistributed` | PaymentOperator | Monitor fee distribution | -All subscription methods use viem's `watchContractEvent` under the hood. For reliable real-time delivery, configure your `publicClient` with a [WebSocket transport](https://viem.sh/docs/clients/transports/websocket). +For reliable real-time delivery, configure your `publicClient` with a [WebSocket transport](https://viem.sh/docs/clients/transports/websocket). -## Next Steps +## Next steps @@ -194,7 +119,7 @@ All subscription methods use viem's `watchContractEvent` under the hood. For rel Learn about dispute resolution from the arbiter perspective. - Process refund requests with approve/deny workflows. + Process refund requests with deny workflows. See how clients subscribe to the same events. diff --git a/sdk/overview.mdx b/sdk/overview.mdx index c0e9ceb..ffe2c55 100644 --- a/sdk/overview.mdx +++ b/sdk/overview.mdx @@ -5,7 +5,7 @@ icon: "cube" --- -The X402r SDK is in active development (v0.0.2). APIs may change between releases. Always test on Base Sepolia before using real funds on mainnet. +The X402r SDK is in active development. APIs may change between releases. Always test on Base Sepolia before using real funds on mainnet. The X402r SDK provides a complete TypeScript implementation for integrating with the X402r refundable payments protocol. It enables clients, merchants, and arbiters to interact with smart contracts for payment authorization, escrow management, and dispute resolution. @@ -32,18 +32,22 @@ The SDK is organized into packages designed for specific roles in the payment ec -## Network Support +## Network support + +All v3 contracts are deployed to the same address on every chain via CREATE3. | Network | Chain ID | Status | |---------|----------|--------| | Base Sepolia | 84532 | Tested | -| Base Mainnet | 8453 | Deployed, not yet tested | -| Ethereum | 1 | Deployed, not yet tested | -| Ethereum Sepolia | 11155111 | Deployed, not yet tested | -| Arbitrum Sepolia | 421614 | Deployed, not yet tested | -| Polygon | 137 | Deployed, not yet tested | -| Arbitrum | 42161 | Deployed, not yet tested | -| Optimism | 10 | Deployed, not yet tested | -| Avalanche | 43114 | Deployed, not yet tested | -| Celo | 42220 | Deployed, not yet tested | -| Monad | 143 | Deployed, not yet tested | +| Base | 8453 | Deployed | +| Ethereum | 1 | Deployed | +| Ethereum Sepolia | 11155111 | Deployed | +| Arbitrum | 42161 | Deployed | +| Arbitrum Sepolia | 421614 | Deployed | +| Optimism | 10 | Deployed | +| Polygon | 137 | Deployed | +| Celo | 42220 | Deployed | +| Avalanche | 43114 | Deployed | +| Linea | 59144 | Deployed | +| Monad | 143 | Deployed | +| SKALE Base | 1187947933 | Deployed (Shanghai EVM) |