diff --git a/.gitignore b/.gitignore index fe39e36..95525e3 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,15 @@ tsconfig.test.json /test/ /e2e_tests/ /non-public/ +/demo/cloudflare-demo/ +/adapters/ethers/.upstream/ +/adapters/viem/.upstream/viem/ +/docs/migration-0.3-to-0.4.md +/demo/UTC--2026-08-03T13-05-08.008Z--7f8880845d4f215594c6fcf07f884f635216d958 +/.codex/skills/architecture-boundary-review/SKILL.md +/demo/MyEtherWallet _ The Best Crypto Wallet For Web3.pdf +/docs/ml-llm-crash-course.md +/docs/job-application-tracker-extension-spec.md +/.codex/config.toml +/AGENTS.md +/.dockerignore diff --git a/CHANGELOG.md b/CHANGELOG.md index 9309409..0db9592 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,8 @@ All notable public changes to d402 are documented here. - Removed d402 core's runtime dependency on ethers and viem. - Standardized client and executor composition around four neutral capabilities: `rpcClient`, `codec`, `errorDecoder`, and `txSender`. -- Preserved direct custom integration by allowing applications to provide those - capabilities without constructing a provider-specific adapter object. +- Added the shared `D402Adapter` contract so applications can provide those + capabilities through one provider-neutral adapter object. - Retained custom payment executors for relayers, custodial wallets, and other integrations that do not use the standard dPayments execution path. @@ -26,12 +26,18 @@ All notable public changes to d402 are documented here. ### API cleanup +- Split payment configuration into `adapter` and `payment` sections. Routes, + `paymentActions()`, verification, settlement, and refund helpers now share + the same nested `{ adapter, payment }` composition API. +- Added `PaymentOptions` for confirmations, settlement timing, caching, + identifiers, logging, events, and multicall settings. - Removed client-side confirmation options. Transaction confirmation depth is now configured on the adapter transaction sender; server verification confirmations remain part of server payment configuration. - Simplified `Once()` to accept only the payment actions it consumes. -- Removed unused adapter types and provider-specific dependencies from shared - protocol code. +- Removed unused provider-specific adapter type aliases and provider-specific + dependencies from shared protocol code. +- Updated the public documentation and runnable examples for the new API. ## 0.3.3 - 2026-08-13 diff --git a/ELI5.md b/ELI5.md index d8193aa..750bcbd 100644 --- a/ELI5.md +++ b/ELI5.md @@ -22,7 +22,7 @@ Use `payable()` when d402 can own the complete route: ```ts const route = payable({ - paymentConfig, + ...paymentConfig, terms, handler, }); @@ -32,7 +32,7 @@ Use `PaymentAuthorizer` when your controller owns the surrounding work: ```ts const payment = new PaymentAuthorizer({ - paymentConfig, + ...paymentConfig, terms, }); diff --git a/README.md b/README.md index 5cf3d1e..f454169 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ the retried request from its proof. ## Install ```sh -npm install d402 ethers +npm install d402 @d402/ethers ethers ``` ## 1. Protect a complete route @@ -17,12 +17,14 @@ route: ```ts import { JsonRpcProvider, Wallet } from "ethers"; +import { createEthersAdapter } from "@d402/ethers"; import { payable } from "d402/server"; const provider = new JsonRpcProvider(process.env.RPC_URL); // Any ethers Signer works here, including a KMS or custody-backed signer. // Signer is only needed if the server performs an on-chain action during the request (for example, Once consumption or refunds). const payee = new Wallet(process.env.PAYEE_PRIVATE_KEY, provider); +const adapter = createEthersAdapter({ provider, signer: payee }); const terms = { chainId: 100, payeeAddress: payee.address, @@ -33,11 +35,8 @@ const terms = { }; export const GET = payable({ - paymentConfig: { - provider, - signer: payee, - settlementWindow: 3600, - }, + adapter, + payment: { settlementWindow: 3600 }, terms, handler: (_request, payment) => Response.json({ @@ -58,12 +57,8 @@ Use `PaymentAuthorizer` when your framework or application owns the controller: import { PaymentAuthorizer } from "d402/server"; const reportPayment = new PaymentAuthorizer({ - paymentConfig: { - provider, - // This may also be a KMS or custody-backed ethers Signer. - signer: payee, - settlementWindow: 3600, - }, + adapter, + payment: { settlementWindow: 3600 }, terms, }); @@ -97,17 +92,13 @@ most one protected operation: import { Once, payable, paymentActions } from "d402/server"; const actions = paymentActions({ - provider, - // Use the payee's Wallet, KMS, or custody-backed ethers Signer. - signer: payee, + adapter, + payment: {}, }); const download = payable({ - paymentConfig: { - provider, - signer: payee, - settlementWindow: 3600, - }, + adapter, + payment: { settlementWindow: 3600 }, terms, consumer: Once(actions), handler: async () => @@ -125,11 +116,8 @@ invoice. Put the order ID in the terms so retries reconstruct the same payment: ```ts const orderRoute = payable({ - paymentConfig: { - provider, - // This may also be a KMS or custody-backed ethers Signer. - signer: payee, - }, + adapter, + payment: {}, terms: (request) => { const orderId = new URL(request.url).pathname.split("/").at(-1); @@ -159,17 +147,13 @@ payment instead of reusing an order identity: import { Once, payable, paymentActions } from "d402/server"; const actions = paymentActions({ - provider, - // Use the payee's Wallet, KMS, or custody-backed ethers Signer. - signer: payee, + adapter, + payment: {}, }); const independentPaymentRoute = payable({ - paymentConfig: { - provider, - signer: payee, - identifier: "client", - }, + adapter, + payment: { identifier: "client" }, terms, consumer: Once(actions), handler, diff --git a/adapters/ethers/src/adapter.ts b/adapters/ethers/src/adapter.ts index 59cb0f1..8b75b49 100644 --- a/adapters/ethers/src/adapter.ts +++ b/adapters/ethers/src/adapter.ts @@ -1,19 +1,11 @@ -import { ABI } from "@rakelabs/dpayments-sdk"; -import type { AbiCodec } from "@rakelabs/dpayments-sdk"; -import type { - AbstractProvider, - Signer, -} from "ethers"; +import {ABI} from "@rakelabs/dpayments-sdk"; +import type {AbstractProvider, Signer,} from "ethers"; -import type { - D402ErrorDecoder, - D402RpcClient, - D402TxSender, -} from "d402/core"; -import { decodeEthersError } from "@rakelabs/ethers-adapter"; -import { createEthersAbiCodec } from "./codec.js"; -import { createEthersRpcClient } from "./rpc-client.js"; -import { createEthersTxSender } from "./tx-sender.js"; +import type {D402Adapter, D402ErrorDecoder,} from "d402/core"; +import {decodeEthersError} from "@rakelabs/ethers-adapter"; +import {createEthersAbiCodec} from "./codec.js"; +import {createEthersRpcClient} from "./rpc-client.js"; +import {createEthersTxSender} from "./tx-sender.js"; export interface EthersAdapterOptions { provider: AbstractProvider; @@ -21,16 +13,9 @@ export interface EthersAdapterOptions { confirmations?: number; } -export interface EthersAdapter { - readonly rpcClient: D402RpcClient; - readonly codec: AbiCodec; - readonly errorDecoder: D402ErrorDecoder; - readonly txSender?: D402TxSender; -} - export function createEthersAdapter( options: EthersAdapterOptions, -): EthersAdapter { +): D402Adapter & { readonly errorDecoder: D402ErrorDecoder } { const rpcClient = createEthersRpcClient(options.provider); const codec = createEthersAbiCodec(ABI); const errorDecoder: D402ErrorDecoder = (error) => @@ -46,20 +31,10 @@ export function createEthersAdapter( : { confirmations: options.confirmations }), }); - const components: { - rpcClient: D402RpcClient; - codec: AbiCodec; - errorDecoder: D402ErrorDecoder; - txSender?: D402TxSender; - } = { + return { rpcClient, codec, errorDecoder, - }; - - if (txSender !== undefined) { - components.txSender = txSender; - } - - return components; -} + ...(txSender === undefined ? {} : {txSender}), + } satisfies D402Adapter; +} \ No newline at end of file diff --git a/adapters/ethers/src/client.ts b/adapters/ethers/src/client.ts index 68920ea..f18f810 100644 --- a/adapters/ethers/src/client.ts +++ b/adapters/ethers/src/client.ts @@ -18,12 +18,12 @@ export interface EthersClientOptions extends Omit< } /** - * Ethers-backed compatibility constructor. + * Ethers-backed convenience constructor. * * The adapter accepts provider/signer construction inputs and supplies the * neutral components consumed by d402 core. */ -export function createClient( +export function createEthersClient( options: EthersClientOptions, ): Promise { const { diff --git a/adapters/ethers/src/index.ts b/adapters/ethers/src/index.ts index 7214381..5159aa5 100644 --- a/adapters/ethers/src/index.ts +++ b/adapters/ethers/src/index.ts @@ -1,11 +1,10 @@ export { - createClient, + createEthersClient, type EthersClientOptions, } from "./client.js"; export { createEthersAdapter, - type EthersAdapter, type EthersAdapterOptions, } from "./adapter.js"; diff --git a/adapters/ethers/test/adapter.test.ts b/adapters/ethers/test/adapter.test.ts index 02c6bb0..1486d66 100644 --- a/adapters/ethers/test/adapter.test.ts +++ b/adapters/ethers/test/adapter.test.ts @@ -3,6 +3,7 @@ import type { AbstractProvider, Signer } from "ethers"; import type { PreparedTx } from "@rakelabs/dpayments-sdk"; import { + createEthersClient, createEthersTxSender, } from "../src/index.js"; import { createEthersAdapter } from "../src/adapter.js"; @@ -27,6 +28,10 @@ const preparedTx: PreparedTx = { }; describe("@d402/ethers adapter", () => { + it("exports the named Ethers client constructor", () => { + expect(createEthersClient).toBeTypeOf("function"); + }); + it("creates a read-only adapter from a provider", () => { const adapter = createEthersAdapter({ provider }); diff --git a/adapters/viem/src/adapter.ts b/adapters/viem/src/adapter.ts index 4474a30..75e713a 100644 --- a/adapters/viem/src/adapter.ts +++ b/adapters/viem/src/adapter.ts @@ -1,16 +1,11 @@ -import { ABI } from "@rakelabs/dpayments-sdk"; -import type { AbiCodec } from "@rakelabs/dpayments-sdk"; -import type { PublicClient, WalletClient } from "viem"; +import {ABI} from "@rakelabs/dpayments-sdk"; +import type {PublicClient, WalletClient} from "viem"; -import type { - D402ErrorDecoder, - D402RpcClient, - D402TxSender, -} from "d402/core"; -import { decodeViemError } from "@rakelabs/viem-adapter"; -import { createViemAbiCodec } from "./codec.js"; -import { createViemRpcClient } from "./rpc-client.js"; -import { createViemTxSender } from "./tx-sender.js"; +import type {D402Adapter, D402ErrorDecoder,} from "d402/core"; +import {decodeViemError} from "@rakelabs/viem-adapter"; +import {createViemAbiCodec} from "./codec.js"; +import {createViemRpcClient} from "./rpc-client.js"; +import {createViemTxSender} from "./tx-sender.js"; export interface ViemAdapterOptions { publicClient: PublicClient; @@ -18,16 +13,9 @@ export interface ViemAdapterOptions { confirmations?: number; } -export interface ViemAdapter { - readonly rpcClient: D402RpcClient; - readonly codec: AbiCodec; - readonly errorDecoder: D402ErrorDecoder; - readonly txSender?: D402TxSender; -} - export function createViemAdapter( options: ViemAdapterOptions, -): ViemAdapter { +): D402Adapter { const rpcClient = createViemRpcClient(options.publicClient); const codec = createViemAbiCodec(ABI); const errorDecoder: D402ErrorDecoder = (error) => @@ -42,20 +30,10 @@ export function createViemAdapter( : { confirmations: options.confirmations }), }); - const components: { - rpcClient: D402RpcClient; - codec: AbiCodec; - errorDecoder: D402ErrorDecoder; - txSender?: D402TxSender; - } = { - rpcClient, - codec, - errorDecoder, - }; - - if (txSender !== undefined) { - components.txSender = txSender; - } - - return components; -} + return { + rpcClient, + codec, + errorDecoder, + ...(txSender === undefined ? {} : {txSender}), + } satisfies D402Adapter; +} \ No newline at end of file diff --git a/adapters/viem/src/client.ts b/adapters/viem/src/client.ts new file mode 100644 index 0000000..b0bf9e7 --- /dev/null +++ b/adapters/viem/src/client.ts @@ -0,0 +1,47 @@ +import { createD402Client } from "d402/client"; +import type { + CreateD402ClientOptions, + D402Client, +} from "d402/client"; +import type { D402TxSender } from "d402/core"; +import type { PublicClient, WalletClient } from "viem"; +import { createViemAdapter } from "./adapter.js"; + +export interface ViemClientOptions extends Omit< + CreateD402ClientOptions, + "rpcClient" | "codec" | "errorDecoder" | "txSender" +> { + publicClient: PublicClient; + walletClient?: WalletClient; + confirmations?: number; + txSender?: D402TxSender; +} + +export function createViemClient( + options: ViemClientOptions, +): Promise { + const { + publicClient, + walletClient, + confirmations, + txSender, + ...clientOptions + } = options; + const adapter = createViemAdapter({ + publicClient, + ...(walletClient === undefined ? {} : { walletClient }), + ...(confirmations === undefined ? {} : { confirmations }), + }); + + return createD402Client({ + ...clientOptions, + rpcClient: adapter.rpcClient, + codec: adapter.codec, + ...(adapter.errorDecoder === undefined + ? {} + : { errorDecoder: adapter.errorDecoder }), + ...(txSender !== undefined + ? { txSender } + : adapter.txSender === undefined ? {} : { txSender: adapter.txSender }), + }); +} diff --git a/adapters/viem/src/index.ts b/adapters/viem/src/index.ts index 0645d03..1eb8bc8 100644 --- a/adapters/viem/src/index.ts +++ b/adapters/viem/src/index.ts @@ -1,6 +1,10 @@ +export { + createViemClient, + type ViemClientOptions, +} from "./client.js"; + export { createViemAdapter, - type ViemAdapter, type ViemAdapterOptions, } from "./adapter.js"; diff --git a/docs/adapters.md b/docs/adapters.md new file mode 100644 index 0000000..ebec019 --- /dev/null +++ b/docs/adapters.md @@ -0,0 +1,134 @@ +# Ethers and Viem adapters + +d402 core is provider-neutral. It does not construct Ethers providers, +Viem clients, wallets, transaction requests, receipts, or provider-specific +errors. A provider integration supplies four capabilities: + +```ts +rpcClient // chain reads and normalized receipts +codec // ABI and event encoding/decoding +errorDecoder // provider error -> decoded contract error +txSender // transaction preparation, broadcast, retry, and confirmation +``` + +The official integrations are `@d402/ethers` and `@d402/viem`. + +## Install + +For Ethers: + +```sh +npm install d402 @d402/ethers ethers +``` + +For Viem: + +```sh +npm install d402 @d402/viem viem +``` + +## Ethers + +Create the adapter once and pass its capabilities to the d402 client: + +```ts +import { JsonRpcProvider, Wallet } from "ethers"; +import { createEthersAdapter } from "@d402/ethers"; +import { createD402Client } from "d402/client"; + +const provider = new JsonRpcProvider(process.env.RPC_URL); +const signer = new Wallet(process.env.PAYER_PRIVATE_KEY, provider); +const adapter = createEthersAdapter({ provider, signer, confirmations: 1 }); + +const client = await createD402Client({ + ...adapter, + policy: { allowedChains: [100] }, +}); +``` + +For a shorter provider-specific client constructor, use `createEthersClient()`: + +```ts +import { createEthersClient } from "@d402/ethers"; + +const client = await createEthersClient({ + provider, + signer, + confirmations: 1, + policy: { allowedChains: [100] }, +}); +``` + +For server actions, use the same adapter capabilities: + +```ts +import { JsonRpcProvider, Wallet } from "ethers"; +import { createEthersAdapter } from "@d402/ethers"; +import { Once, payable, paymentActions } from "d402/server"; + +const provider = new JsonRpcProvider(process.env.RPC_URL); +const signer = new Wallet(process.env.PAYEE_PRIVATE_KEY, provider); + +const adapter = createEthersAdapter({ provider, signer }); +const actions = paymentActions({ + adapter, + payment: { confirmations: 1 }, +}); + +export const route = payable({ + adapter, + payment: { confirmations: 1, settlementWindow: 3600 }, + terms, + consumer: Once(actions), + handler, +}); +``` + +## Viem + +Viem applications construct their own public and wallet clients, then pass +them to the adapter: + +```ts +import { createPublicClient, createWalletClient, http } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { mainnet } from "viem/chains"; +import { createViemClient } from "@d402/viem"; + +const account = privateKeyToAccount(process.env.PAYER_PRIVATE_KEY); +const publicClient = createPublicClient({ chain: mainnet, transport: http() }); +const walletClient = createWalletClient({ + account, + chain: mainnet, + transport: http(), +}); +const client = await createViemClient({ + publicClient, + walletClient, + confirmations: 1, +}); +``` + +The low-level `createViemAdapter()` remains available when the same neutral +capabilities must be shared by multiple d402 components. + +The same `adapter` object can be placed in the `PaymentConfig` passed to +`paymentActions()` and `payable()` for server-side lifecycle actions. A +read-only adapter omits +`walletClient` and therefore does not expose `txSender`; it is suitable for +verification-only integrations, not for payment creation or server actions. + +## Confirmation and error behavior + +Client transaction confirmation depth is configured when creating the adapter. +Server verification and action confirmation depth belongs in +`PaymentConfig.payment`. The adapter owns provider-specific nonce, gas, +receipt, confirmation, and retry behavior for the transactions it sends. + +Contract errors are decoded by the adapter's `errorDecoder` and normalized by +d402 into `D402PaymentExecutionError`. d402 core does not inspect Ethers or +Viem exception classes. + +For custom integrations, implement the same neutral capabilities directly and +pass them to `createD402Client`, or place them in the `adapter` property of the +`PaymentConfig` passed to `paymentActions` or `payable`. diff --git a/docs/advanced.md b/docs/advanced.md index 85ea089..059ab60 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -79,9 +79,8 @@ Use a string when the same payment terms protect one stable URL. ```ts const authorizationConfig = { - paymentConfig: { - provider, - }, + adapter, + payment: {}, terms: { ...terms, resource: "https://api.example.com/reports/monthly", @@ -98,9 +97,8 @@ than the literal request URL. ```ts const authorizationConfig = { - paymentConfig: { - provider, - }, + adapter, + payment: {}, terms: { chainId: 100, payeeAddress, @@ -122,7 +120,7 @@ The client defaults to matching the URL it retries. When the server uses a custom identifier, pass the same string or resolver as the client's `resource` option. Use a stable public URL pattern or opaque application identifier and constrain it with client policy. If each request needs a distinct payment -identity, use `paymentConfig.identifier: "client"` or put a stable request or +identity, use `payment.identifier: "client"` or put a stable request or order ID in `agreement.id`. ### Framework request metadata and request bodies @@ -133,7 +131,8 @@ Use resolver context when a framework extends the standard `Request` type: import type { NextRequest } from "next/server"; const authorizationConfig = { - paymentConfig: { provider }, + adapter, + payment: {}, terms: async ( _request, { originalRequest, bodyRequest }, @@ -172,7 +171,7 @@ metadata and the clone for body reads so the handler retains its body. Both public forms enter the same authorizer: ```text -payable({ authorization config, handler }) +payable({ adapter, payment, handler }) | v PaymentAuthorizer.authorize(request) @@ -253,7 +252,7 @@ to be available. import { FundedPayment } from "d402/server"; const authorizationConfig = { - paymentConfig, + ...paymentConfig, terms, verificationPolicy: FundedPayment, } satisfies PaymentAuthorizationConfig; @@ -286,7 +285,7 @@ authenticated payment request, proof, observed payment, and consumer result. ```ts const authorizationConfig = { - paymentConfig, + ...paymentConfig, terms, } satisfies PaymentAuthorizationConfig; @@ -329,14 +328,14 @@ the server payment actions once and inject them into the consumer: ```ts import { Once, paymentActions } from "d402/server"; -const actions = paymentActions({ provider, signer: payee }); +const paymentConfig = { + adapter, + payment: { identifier: "client" }, +}; +const actions = paymentActions(paymentConfig); const authorizationConfig = { - paymentConfig: { - provider, - signer: payee, - identifier: "client", - }, + ...paymentConfig, consumer: Once(actions), terms, } satisfies PaymentAuthorizationConfig; @@ -438,7 +437,7 @@ const recovery: PaymentRecovery = async ({ payment }) => { }; const authorizationConfig = { - paymentConfig, + ...paymentConfig, terms, recovery, consumer: Once(actions), @@ -594,7 +593,7 @@ function DatabaseOnce(chainId: number): PaymentConsumer { } const authorizationConfig = { - paymentConfig, + ...paymentConfig, consumer: DatabaseOnce(chainId), terms, } satisfies PaymentAuthorizationConfig; @@ -657,7 +656,7 @@ SKU, order state, agreement metadata, quotas, or server-side entitlements. import { None } from "d402/server"; const authorizationConfig = { - paymentConfig, + ...paymentConfig, terms, consumer: None, } satisfies PaymentAuthorizationConfig; @@ -676,7 +675,7 @@ Store a usage count when one payment buys a fixed number of uses. ```ts const authorizationConfig = { - paymentConfig, + ...paymentConfig, terms, } satisfies PaymentAuthorizationConfig; @@ -857,7 +856,10 @@ The d402 server action helper also exposes common server-side lifecycle actions for consumers, workers, and recovery flows: ```ts -const actions = paymentActions({ provider, signer }); +const actions = paymentActions({ + adapter, + payment: {}, +}); await actions.settlePayment(paymentAddress); await actions.refundPayment(paymentAddress); @@ -897,11 +899,14 @@ const logger: D402Logger = (record) => { applicationLogger[record.level](record.context, record.message); }; -const actions = paymentActions({ provider, signer, logger }); +const actions = paymentActions({ + adapter, + payment: { logger }, +}); ``` The same option is accepted by `createD402Client()` and `createDPaymentsExecutor()`. d402 isolates logging from payment behavior: thrown logger errors and rejected logger promises are ignored. Records include stable event names and shallow safe context, never signed transaction payloads, -credentials, evidence URIs, or arbitrary error properties. \ No newline at end of file +credentials, evidence URIs, or arbitrary error properties. diff --git a/docs/api.md b/docs/api.md index f6358bb..050df44 100644 --- a/docs/api.md +++ b/docs/api.md @@ -267,6 +267,15 @@ import { } from "d402/server"; ``` +Server configuration is shared across routes and actions: + +```ts +interface PaymentConfig { + adapter: D402Adapter; + payment: PaymentOptions; +} +``` + ### `payable(options)` Wraps a request handler and returns a function that either: @@ -276,15 +285,16 @@ Wraps a request handler and returns a function that either: Important options: -- `paymentConfig.provider`: ethers provider used for verification. -- `paymentConfig.confirmations`: required payment transaction confirmations. -- `paymentConfig.settlementWindow`: optional settlement window in seconds for +- `adapter`: provider-neutral chain capabilities used for verification and + optional server actions. +- `payment.confirmations`: required payment transaction confirmations. +- `payment.settlementWindow`: optional settlement window in seconds for dynamic relative settlement timing. -- `paymentConfig.settlementTimeUnixSec`: explicit settlement time. -- `paymentConfig.cache`: optional cache setting for settlement-window support. -- `paymentConfig.logger`: optional structured record sink for server payment +- `payment.settlementTimeUnixSec`: explicit settlement time. +- `payment.cache`: optional cache setting for settlement-window support. +- `payment.logger`: optional structured record sink for server payment actions. It has the same failure-isolated behavior as the client logger. -- `paymentConfig.multicall`: optional trusted Multicall3 configuration used by +- `payment.multicall`: optional trusted Multicall3 configuration used by canonical payment-state observation. - `terms`: static terms or a function of the request. Its optional `resource` may be a string or function of the request; it defaults to the incoming URL. @@ -299,7 +309,7 @@ Important options: observation. `FundedOrSettledPayment` is the default; the exported `FundedPayment` accepts only payments whose current state is `funded`. - `consumer`: optional payment-consumption policy. Use - `Once(actions)` with a shared `paymentActions({ provider, signer })` instance + `Once(actions)` with a shared `paymentActions({ adapter, payment })` instance to consume a verified payment before the protected handler runs, or `None` to state the reusable policy explicitly. Routes are reusable by default. `Once` is an at-most-once authorization @@ -342,7 +352,10 @@ behavior. Creates a `PaymentActions` object containing the server-side lifecycle methods: ```ts -const actions = paymentActions({ provider, signer }); +const actions = paymentActions({ + adapter, + payment: { confirmations: 1 }, +}); await actions.settlePayment(paymentAddress); await actions.refundPayment(paymentAddress); @@ -351,9 +364,10 @@ await actions.submitEvidence(paymentAddress, "ipfs://QmEvidence"); await actions.appealPayment(paymentAddress); ``` -The configuration requires `provider` and `signer`; `confirmations` is -optional. Reuse the returned object for payable consumers, lifecycle workers, -and recovery flows that use that configuration. +The configuration contains the provider-neutral `adapter` and the server +`payment` options. The adapter must expose `txSender` for broadcast actions. +Reuse the returned object for payable consumers, lifecycle workers, and recovery +flows that use that configuration. `paymentActions()` creates an independent action object for each call. Each object privately orders its own broadcasts, while nonce selection remains @@ -408,7 +422,10 @@ intentionally permits or rejects a particular current payment state. Use consumption: ```ts -const actions = paymentActions({ provider, signer }); +const actions = paymentActions({ + adapter, + payment: { confirmations: 1 }, +}); const consumer = Once(actions); ``` @@ -432,7 +449,7 @@ const databaseOnce: PaymentConsumer = { }; const route = payable({ - paymentConfig, + ...paymentConfig, consumer: databaseOnce, terms, handler, diff --git a/docs/disputes.md b/docs/disputes.md index 0ecfefa..a28393c 100644 --- a/docs/disputes.md +++ b/docs/disputes.md @@ -39,7 +39,7 @@ underlying resolution outcome is not yet modeled in d402. - Verify payment creation and live payment state. - Initiate a dispute through the client executor. - Submit an evidence URI through `actions.submitEvidence()`, where `actions` - is returned by `paymentActions({ provider, signer })`. + is returned by `paymentActions({ adapter, payment })`. - Provide server-side appeal and settlement/refund transaction helpers. - Reject access for disputed or resolved payments. @@ -73,7 +73,10 @@ refund when policy approves. See [Refunds](./refunds.md). ```ts import { paymentActions } from "d402/server"; -const actions = paymentActions({ provider, signer }); +const actions = paymentActions({ + adapter, + payment: {}, +}); const result = await evidencePublisher.publish({ title: `d402 evidence for ${paymentId}`, diff --git a/docs/http-integration.md b/docs/http-integration.md index d23f78d..e5761bf 100644 --- a/docs/http-integration.md +++ b/docs/http-integration.md @@ -89,7 +89,7 @@ import { } from "d402/server"; const route = payable({ - paymentConfig, + ...paymentConfig, terms, handler, diff --git a/docs/refunds.md b/docs/refunds.md index 87e4e37..a668816 100644 --- a/docs/refunds.md +++ b/docs/refunds.md @@ -30,10 +30,8 @@ import { } from "d402/server"; const routeConfig = { - paymentConfig: { - provider, - signer: payee, - }, + adapter, + payment: {}, terms, refunds: { url: "/refund", @@ -74,9 +72,9 @@ reference formats it supports. For cross-origin preflight, cookies, and bearer credentials on the refund request, see [HTTP and Framework Integration](./http-integration.md). -The original route configuration is reused for its provider, signer, and -payment-verification settings. The refund handler does not invoke the original -terms resolver, recovery hook, consumer, or protected handler. +The original route configuration is reused for its adapter and payment +verification settings. The refund handler does not invoke the original terms +resolver, recovery hook, consumer, or protected handler. ## Configure the client diff --git a/docs/scaling.md b/docs/scaling.md index 9ba316d..0268af6 100644 --- a/docs/scaling.md +++ b/docs/scaling.md @@ -111,14 +111,14 @@ Use `Once(actions)` when one payment should authorize at most one handler execution. `Once` claims the payment on-chain before entering the handler: ```ts -const actions = paymentActions({ provider, signer: payee }); +const actions = paymentActions({ + adapter, + payment: {}, +}); const route = payable({ - paymentConfig: { - provider, - signer: payee, - identifier: "client", - }, + adapter, + payment: { identifier: "client" }, consumer: Once(actions), terms, handler, @@ -273,4 +273,4 @@ status or recovery responses from shared storage. - Monitor provider latency, retryable verification failures, and transaction confirmation time. - Test challenge issuance, paid retry, restart recovery, and one-shot races - across different replicas. \ No newline at end of file + across different replicas. diff --git a/docs/schemes.md b/docs/schemes.md index f312015..c8d37a2 100644 --- a/docs/schemes.md +++ b/docs/schemes.md @@ -19,10 +19,8 @@ subscription, entitlement, or other business object: ```ts const route = payable({ - paymentConfig: { - provider, - identifier: "server", - }, + adapter, + payment: { identifier: "server" }, terms, handler, }); @@ -36,10 +34,8 @@ even if multiple clients receive identical terms: ```ts const route = payable({ - paymentConfig: { - provider, - identifier: "client", - }, + adapter, + payment: { identifier: "client" }, terms, handler, }); @@ -54,14 +50,14 @@ most one protected operation: ```ts import { Once, payable, paymentActions } from "d402/server"; -const actions = paymentActions({ provider, signer: payee }); +const actions = paymentActions({ + adapter, + payment: {}, +}); const route = payable({ - paymentConfig: { - provider, - signer: payee, - identifier: "client", - }, + adapter, + payment: { identifier: "client" }, consumer: Once(actions), terms, handler, @@ -102,10 +98,7 @@ server identity. ```ts const orderCheckout = payable({ - paymentConfig: { - provider, - resource: ({ url }) => `order:${new URL(url).searchParams.get("orderId")}`, - }, + adapter, terms: async (request) => { const order = await orders.loadRequired(request); @@ -118,6 +111,8 @@ const orderCheckout = payable({ agreement: { id: `order:${order.id}:v${order.priceVersion}`, }, + resource: ({ url }) => + `order:${new URL(url).searchParams.get("orderId")}`, expiresAtUnixSec: order.quoteExpiresAtUnixSec, }; }, @@ -189,10 +184,8 @@ payments: ```ts const paidSearch = payable({ - paymentConfig: { - provider, - identifier: "client", - }, + adapter, + payment: { identifier: "client" }, terms: searchTerms, handler: runSearch, }); @@ -240,7 +233,10 @@ lifecycle actions from an authorized server process: ```ts import { paymentActions } from "d402/server"; -const actions = paymentActions({ provider, signer: payee }); +const actions = paymentActions({ + adapter, + payment: {}, +}); await actions.refundPayment(paymentAddress); await actions.settlePayment(paymentAddress); @@ -310,4 +306,4 @@ The application answers: > returned or recovered? That boundary is what lets integrators scale storage, queues, caching, locking, -and fulfillment independently from the protocol. \ No newline at end of file +and fulfillment independently from the protocol. diff --git a/docs/signing.md b/docs/signing.md index 3859a55..9442e91 100644 --- a/docs/signing.md +++ b/docs/signing.md @@ -118,9 +118,8 @@ Servers can also send dPayment action transactions with their own signer. import { paymentActions } from "d402/server"; const actions = paymentActions({ - provider, - signer, - confirmations: 2, + adapter, + payment: { confirmations: 2 }, }); await actions.settlePayment(paymentAddress); diff --git a/examples/express-native/package-lock.json b/examples/express-native/package-lock.json index 03e676f..304c5aa 100644 --- a/examples/express-native/package-lock.json +++ b/examples/express-native/package-lock.json @@ -6,6 +6,7 @@ "": { "name": "@d402/example-express-native", "dependencies": { + "@d402/ethers": "file:../../adapters/ethers", "d402": "file:../..", "dotenv": "^16.4.7", "ethers": "^6.17.0", @@ -19,12 +20,12 @@ } }, "../..": { - "version": "0.3.0-rc.0", + "version": "0.4.0", "license": "Apache-2.0", "dependencies": { - "@rakelabs/dpayments-sdk": "^0.1.5", + "@noble/hashes": "^2.0.1", + "@rakelabs/dpayments-sdk": "^0.2.1", "canonicalize": "^3.0.0", - "ethers": "^6.17.0", "zod": "^4.4.3" }, "devDependencies": { @@ -33,13 +34,34 @@ "@eslint/js": "^10.0.1", "@types/node": "^26.0.1", "eslint": "^10.6.0", + "ethers": "^6.17.0", "tsx": "^4.22.4", "typescript": "^6.0.3", "typescript-eslint": "^8.62.0", + "viem": "^2.55.19", "vitest": "^4.1.9" }, + "engines": { + "node": ">=20.19.0" + } + }, + "../../adapters/ethers": { + "name": "@d402/ethers", + "version": "0.1.0", + "dependencies": { + "@rakelabs/dpayments-sdk": "^0.2.1", + "@rakelabs/ethers-adapter": "^0.1.0" + }, + "devDependencies": { + "d402": "file:../..", + "ethers": "^6.17.0" + }, "engines": { "node": ">=20" + }, + "peerDependencies": { + "d402": "^0.4.0", + "ethers": "^6.17.0" } }, "node_modules/@adraffy/ens-normalize": { @@ -48,6 +70,10 @@ "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", "license": "MIT" }, + "node_modules/@d402/ethers": { + "resolved": "../../adapters/ethers", + "link": true + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", diff --git a/examples/express-native/package.json b/examples/express-native/package.json index a9b9edb..c010cfc 100644 --- a/examples/express-native/package.json +++ b/examples/express-native/package.json @@ -10,6 +10,7 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { + "@d402/ethers": "file:../../adapters/ethers", "d402": "file:../..", "dotenv": "^16.4.7", "ethers": "^6.17.0", diff --git a/examples/express-native/src/shared.ts b/examples/express-native/src/shared.ts index 7dc339b..e6bbb47 100644 --- a/examples/express-native/src/shared.ts +++ b/examples/express-native/src/shared.ts @@ -2,6 +2,7 @@ import "dotenv/config"; import express from "express"; import { JsonRpcProvider } from "ethers"; +import { createEthersAdapter } from "@d402/ethers"; import type { PaymentAuthorizationConfig } from "d402/server"; @@ -10,10 +11,11 @@ export const port = Number(process.env.PORT ?? "3000"); const chainId = Number(requireEnv("CHAIN_ID")); const payeeAddress = requireEnv("PAYEE_ADDRESS") as `0x${string}`; const provider = new JsonRpcProvider(requireEnv("RPC_URL")); +const adapter = createEthersAdapter({ provider }); export const reportAuthorization = { - paymentConfig: { - provider, + adapter, + payment: { confirmations: 1, settlementWindow: 3600, }, diff --git a/examples/next-route-handler/app/api/reports/[id]/route.ts b/examples/next-route-handler/app/api/reports/[id]/route.ts index 258ea49..d4eeff9 100644 --- a/examples/next-route-handler/app/api/reports/[id]/route.ts +++ b/examples/next-route-handler/app/api/reports/[id]/route.ts @@ -1,14 +1,16 @@ import { JsonRpcProvider } from "ethers"; +import { createEthersAdapter } from "@d402/ethers"; import { payable } from "d402/server"; const provider = new JsonRpcProvider(requireEnv("RPC_URL")); +const adapter = createEthersAdapter({ provider }); const chainId = Number(requireEnv("CHAIN_ID")); const payeeAddress = requireEnv("PAYEE_ADDRESS") as `0x${string}`; const protectReport = payable({ - paymentConfig: { - provider, + adapter, + payment: { confirmations: 1, settlementWindow: 3600, }, diff --git a/examples/next-route-handler/package-lock.json b/examples/next-route-handler/package-lock.json index bf1c86c..296a666 100644 --- a/examples/next-route-handler/package-lock.json +++ b/examples/next-route-handler/package-lock.json @@ -6,6 +6,7 @@ "": { "name": "@d402/example-next-route-handler", "dependencies": { + "@d402/ethers": "file:../../adapters/ethers", "d402": "file:../..", "dotenv": "^16.4.7", "ethers": "^6.17.0", @@ -21,12 +22,12 @@ } }, "../..": { - "version": "0.3.0-rc.0", + "version": "0.4.0", "license": "Apache-2.0", "dependencies": { - "@rakelabs/dpayments-sdk": "^0.1.5", + "@noble/hashes": "^2.0.1", + "@rakelabs/dpayments-sdk": "^0.2.1", "canonicalize": "^3.0.0", - "ethers": "^6.17.0", "zod": "^4.4.3" }, "devDependencies": { @@ -35,13 +36,34 @@ "@eslint/js": "^10.0.1", "@types/node": "^26.0.1", "eslint": "^10.6.0", + "ethers": "^6.17.0", "tsx": "^4.22.4", "typescript": "^6.0.3", "typescript-eslint": "^8.62.0", + "viem": "^2.55.19", "vitest": "^4.1.9" }, + "engines": { + "node": ">=20.19.0" + } + }, + "../../adapters/ethers": { + "name": "@d402/ethers", + "version": "0.1.0", + "dependencies": { + "@rakelabs/dpayments-sdk": "^0.2.1", + "@rakelabs/ethers-adapter": "^0.1.0" + }, + "devDependencies": { + "d402": "file:../..", + "ethers": "^6.17.0" + }, "engines": { "node": ">=20" + }, + "peerDependencies": { + "d402": "^0.4.0", + "ethers": "^6.17.0" } }, "node_modules/@adraffy/ens-normalize": { @@ -50,6 +72,10 @@ "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", "license": "MIT" }, + "node_modules/@d402/ethers": { + "resolved": "../../adapters/ethers", + "link": true + }, "node_modules/@emnapi/runtime": { "version": "1.11.3", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", diff --git a/examples/next-route-handler/package.json b/examples/next-route-handler/package.json index 1f1c7b2..f5f05bb 100644 --- a/examples/next-route-handler/package.json +++ b/examples/next-route-handler/package.json @@ -8,6 +8,7 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { + "@d402/ethers": "file:../../adapters/ethers", "d402": "file:../..", "dotenv": "^16.4.7", "ethers": "^6.17.0", diff --git a/examples/one-shot-access/package-lock.json b/examples/one-shot-access/package-lock.json index 53344c2..f215d35 100644 --- a/examples/one-shot-access/package-lock.json +++ b/examples/one-shot-access/package-lock.json @@ -6,6 +6,7 @@ "": { "name": "@d402/example-one-shot-access", "dependencies": { + "@d402/ethers": "file:../../adapters/ethers", "d402": "file:../..", "dotenv": "^16.4.7", "ethers": "^6.17.0", @@ -19,12 +20,12 @@ } }, "../..": { - "version": "0.3.0-rc.0", + "version": "0.4.0", "license": "Apache-2.0", "dependencies": { - "@rakelabs/dpayments-sdk": "^0.1.5", + "@noble/hashes": "^2.0.1", + "@rakelabs/dpayments-sdk": "^0.2.1", "canonicalize": "^3.0.0", - "ethers": "^6.17.0", "zod": "^4.4.3" }, "devDependencies": { @@ -33,13 +34,34 @@ "@eslint/js": "^10.0.1", "@types/node": "^26.0.1", "eslint": "^10.6.0", + "ethers": "^6.17.0", "tsx": "^4.22.4", "typescript": "^6.0.3", "typescript-eslint": "^8.62.0", + "viem": "^2.55.19", "vitest": "^4.1.9" }, + "engines": { + "node": ">=20.19.0" + } + }, + "../../adapters/ethers": { + "name": "@d402/ethers", + "version": "0.1.0", + "dependencies": { + "@rakelabs/dpayments-sdk": "^0.2.1", + "@rakelabs/ethers-adapter": "^0.1.0" + }, + "devDependencies": { + "d402": "file:../..", + "ethers": "^6.17.0" + }, "engines": { "node": ">=20" + }, + "peerDependencies": { + "d402": "^0.4.0", + "ethers": "^6.17.0" } }, "node_modules/@adraffy/ens-normalize": { @@ -48,6 +70,10 @@ "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", "license": "MIT" }, + "node_modules/@d402/ethers": { + "resolved": "../../adapters/ethers", + "link": true + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", diff --git a/examples/one-shot-access/package.json b/examples/one-shot-access/package.json index d707cbd..fd71ffe 100644 --- a/examples/one-shot-access/package.json +++ b/examples/one-shot-access/package.json @@ -8,6 +8,7 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { + "@d402/ethers": "file:../../adapters/ethers", "d402": "file:../..", "dotenv": "^16.4.7", "ethers": "^6.17.0", diff --git a/examples/one-shot-access/src/server.ts b/examples/one-shot-access/src/server.ts index 0f96688..508e6ff 100644 --- a/examples/one-shot-access/src/server.ts +++ b/examples/one-shot-access/src/server.ts @@ -2,6 +2,7 @@ import "dotenv/config"; import express from "express"; import { JsonRpcProvider, Wallet } from "ethers"; +import { createEthersAdapter } from "@d402/ethers"; import { Once, payable, paymentActions } from "d402/server"; @@ -10,15 +11,15 @@ const chainId = Number(requireEnv("CHAIN_ID")); const provider = new JsonRpcProvider(requireEnv("RPC_URL")); const payee = new Wallet(requireEnv("PAYEE_PRIVATE_KEY"), provider); const payeeAddress = payee.address as `0x${string}`; +const adapter = createEthersAdapter({ provider, signer: payee }); const actions = paymentActions({ - provider, - signer: payee, + adapter, + payment: { confirmations: 1 }, }); const protectedDownload = payable({ - paymentConfig: { - provider, - signer: payee, + adapter, + payment: { confirmations: 1, identifier: "client", settlementWindow: 3600, diff --git a/src/core/adapter.ts b/src/core/adapter.ts index 55775ab..0c73acb 100644 --- a/src/core/adapter.ts +++ b/src/core/adapter.ts @@ -1,4 +1,5 @@ import type { + AbiCodec, DecodedError, EvmLog, PreparedTx, @@ -42,3 +43,10 @@ export interface D402TxSender { /** Broadcasts a prepared transaction without waiting for confirmation. */ broadcastTransaction(tx: PreparedTx): Promise; } + +export interface D402Adapter { + readonly rpcClient: D402RpcClient; + readonly codec: AbiCodec; + readonly errorDecoder?: D402ErrorDecoder; + readonly txSender?: D402TxSender; +} diff --git a/src/core/index.ts b/src/core/index.ts index 6446483..94a7924 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -22,6 +22,7 @@ export type { D402CanonicalSalt } from "./constants.js"; export type { D402BlockInfo, D402BroadcastedTx, + D402Adapter, D402ErrorDecoder, D402RpcClient, D402TxReceipt, diff --git a/src/server/cache.ts b/src/server/cache.ts index f53fdea..ec58933 100644 --- a/src/server/cache.ts +++ b/src/server/cache.ts @@ -42,7 +42,7 @@ export function resolveLatestBlockCacheTtlMs( if (!Number.isInteger(cache) || cache <= 0) { throw new Error( - `paymentConfig.cache must be a positive integer, got ${cache}`, + `payment.cache must be a positive integer, got ${cache}`, ); } diff --git a/src/server/index.ts b/src/server/index.ts index 05667d7..6122815 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -2,7 +2,6 @@ export { payable } from "./payable.js"; export { refunder } from "./refunder.js"; export { PaymentAuthorizer } from "./payment-authorizer.js"; export { createDPaymentsObserver } from "./payment-verifier.js"; -export type { DPaymentsObserverOptions } from "./payment-verifier.js"; export { decodeD402PaymentProof, } from "./payment-proof.js"; @@ -45,6 +44,7 @@ export type { AuthenticatedPayment, AuthenticatedPaymentContext, PaymentConfig, + PaymentOptions, PaymentIdentifier, PaymentAppealPeriod, PaymentAppealResult, diff --git a/src/server/payment-actions.ts b/src/server/payment-actions.ts index 3f54e89..db19163 100644 --- a/src/server/payment-actions.ts +++ b/src/server/payment-actions.ts @@ -1,6 +1,5 @@ import type { D402PaymentActionResult, - D402TxSender, PaymentAddress, } from "../core/index.js"; import { createPinnedDPayments } from "../runtime/dpayments.js"; @@ -24,37 +23,25 @@ import type { PaymentActions, PaymentConfig, } from "./types.js"; -import type { D402Logger } from "../runtime/logger.js"; - -type ResolvedPaymentConfig = PaymentConfig & { - txSender: D402TxSender; - logger: D402Logger; -}; - const ACTION_TRANSACTION_FAILURE_MESSAGE = "DPayments action transaction failed after broadcast or was not mined successfully."; export function paymentActions(config: PaymentConfig): PaymentActions { - if (config.txSender === undefined) { + if (config.adapter.txSender === undefined) { throw new Error( - "paymentConfig.txSender is required for payment actions so the adapter can broadcast, retry, and confirm settlement, refund, consumption, evidence, or appeal transactions.", + "adapter.txSender is required for payment actions so the adapter can broadcast, retry, and confirm settlement, refund, consumption, evidence, or appeal transactions.", ); } - const actionConfig: ResolvedPaymentConfig = { - ...config, - txSender: config.txSender, - logger: config.logger ?? NoopLogger, - }; const broadcastInQueue = createBroadcastQueue(); return { settlePayment(payment) { return executePaymentOperation( - actionConfig, + config, "settle", payment, () => sendPaymentAction( - actionConfig, + config, payment, "settle", broadcastInQueue, @@ -63,11 +50,11 @@ export function paymentActions(config: PaymentConfig): PaymentActions { }, refundPayment(payment) { return executePaymentOperation( - actionConfig, + config, "refund", payment, () => sendPaymentAction( - actionConfig, + config, payment, "refund", broadcastInQueue, @@ -76,11 +63,11 @@ export function paymentActions(config: PaymentConfig): PaymentActions { }, consumePayment(payment) { return executePaymentOperation( - actionConfig, + config, "consume", payment, () => sendPaymentAction( - actionConfig, + config, payment, "consume", broadcastInQueue, @@ -89,11 +76,11 @@ export function paymentActions(config: PaymentConfig): PaymentActions { }, submitEvidence(payment, evidenceUri) { return executePaymentOperation( - actionConfig, + config, "submit-evidence", payment, () => sendEvidenceAction( - actionConfig, + config, payment, evidenceUri, broadcastInQueue, @@ -102,11 +89,11 @@ export function paymentActions(config: PaymentConfig): PaymentActions { }, appealPayment(payment) { return executePaymentOperation( - actionConfig, + config, "appeal", payment, () => sendAppealAction( - actionConfig, + config, payment, broadcastInQueue, ), @@ -116,7 +103,7 @@ export function paymentActions(config: PaymentConfig): PaymentActions { } async function executePaymentOperation( - config: ResolvedPaymentConfig, + config: PaymentConfig, operation: D402PaymentOperation, paymentAddress: PaymentAddress, execute: () => Promise, @@ -124,15 +111,16 @@ async function executePaymentOperation( try { return await execute(); } catch (cause) { + const logger = config.payment.logger ?? NoopLogger; const error = normalizePaymentExecutionError({ operation, paymentAddress, - codec: config.codec, - errorDecoder: config.errorDecoder, - logger: config.logger, + codec: config.adapter.codec, + errorDecoder: config.adapter.errorDecoder, + logger, cause, }); - emitLog(config.logger, { + emitLog(logger, { level: "error", event: "payment.execution.failed", message: "Payment execution failed.", @@ -154,13 +142,15 @@ async function executePaymentOperation( } async function sendPaymentAction( - config: ResolvedPaymentConfig, + config: PaymentConfig, paymentAddress: PaymentAddress, action: "settle" | "refund" | "consume", broadcastInQueue: BroadcastQueue, ): Promise { - const walletAddress = await config.txSender.getAddress(); - emitLog(config.logger, { + const txSender = config.adapter.txSender!; + const logger = config.payment.logger ?? NoopLogger; + const walletAddress = await txSender.getAddress(); + emitLog(logger, { level: "debug", event: "payment.action.started", message: "Payment action started.", @@ -171,8 +161,8 @@ async function sendPaymentAction( }, }); const dpayments = await createPinnedDPayments({ - rpcClient: config.rpcClient, - codec: config.codec, + rpcClient: config.adapter.rpcClient, + codec: config.adapter.codec, walletAddress, }); const dPayment = dpayments.dPayment(paymentAddress); @@ -183,16 +173,16 @@ async function sendPaymentAction( : dPayment.consume(walletAddress); const response = await broadcastInQueue(() => broadcastPreparedTransaction({ - txSender: config.txSender, + txSender, tx, - onEvent: config.onEvent, + onEvent: config.payment.onEvent, }), ); const receipt = await waitForSuccessfulReceipt( response, ACTION_TRANSACTION_FAILURE_MESSAGE, ); - emitLog(config.logger, { + emitLog(logger, { level: "info", event: "payment.action.confirmed", message: "Payment action confirmed.", @@ -208,13 +198,15 @@ async function sendPaymentAction( } async function sendEvidenceAction( - config: ResolvedPaymentConfig, + config: PaymentConfig, paymentAddress: PaymentAddress, evidenceUri: string, broadcastInQueue: BroadcastQueue, ): Promise { - const walletAddress = await config.txSender.getAddress(); - emitLog(config.logger, { + const txSender = config.adapter.txSender!; + const logger = config.payment.logger ?? NoopLogger; + const walletAddress = await txSender.getAddress(); + emitLog(logger, { level: "debug", event: "payment.evidence.started", message: "Payment evidence submission started.", @@ -224,24 +216,24 @@ async function sendEvidenceAction( }, }); const dpayments = await createPinnedDPayments({ - rpcClient: config.rpcClient, - codec: config.codec, + rpcClient: config.adapter.rpcClient, + codec: config.adapter.codec, walletAddress, }); const dPayment = dpayments.dPayment(paymentAddress); const tx = dPayment.submitEvidence(evidenceUri, walletAddress); const response = await broadcastInQueue(() => broadcastPreparedTransaction({ - txSender: config.txSender, + txSender, tx, - onEvent: config.onEvent, + onEvent: config.payment.onEvent, }), ); const receipt = await waitForSuccessfulReceipt( response, ACTION_TRANSACTION_FAILURE_MESSAGE, ); - emitLog(config.logger, { + emitLog(logger, { level: "info", event: "payment.evidence.confirmed", message: "Payment evidence submission confirmed.", @@ -256,12 +248,14 @@ async function sendEvidenceAction( } async function sendAppealAction( - config: ResolvedPaymentConfig, + config: PaymentConfig, paymentAddress: PaymentAddress, broadcastInQueue: BroadcastQueue, ): Promise { - const walletAddress = await config.txSender.getAddress(); - emitLog(config.logger, { + const txSender = config.adapter.txSender!; + const logger = config.payment.logger ?? NoopLogger; + const walletAddress = await txSender.getAddress(); + emitLog(logger, { level: "debug", event: "payment.appeal.started", message: "Payment appeal started.", @@ -271,8 +265,8 @@ async function sendAppealAction( }, }); const dpayments = await createPinnedDPayments({ - rpcClient: config.rpcClient, - codec: config.codec, + rpcClient: config.adapter.rpcClient, + codec: config.adapter.codec, walletAddress, }); const dPayment = dpayments.dPayment(paymentAddress); @@ -282,16 +276,16 @@ async function sendAppealAction( ); const response = await broadcastInQueue(() => broadcastPreparedTransaction({ - txSender: config.txSender, + txSender, tx: prepared.tx, - onEvent: config.onEvent, + onEvent: config.payment.onEvent, }), ); const receipt = await waitForSuccessfulReceipt( response, ACTION_TRANSACTION_FAILURE_MESSAGE, ); - emitLog(config.logger, { + emitLog(logger, { level: "info", event: "payment.appeal.confirmed", message: "Payment appeal confirmed.", diff --git a/src/server/payment-authorizer.ts b/src/server/payment-authorizer.ts index f1317c7..fcdb534 100644 --- a/src/server/payment-authorizer.ts +++ b/src/server/payment-authorizer.ts @@ -53,9 +53,9 @@ export class PaymentAuthorizer< constructor(config: PaymentAuthorizationConfig) { this.#config = config; - this.#logger = config.paymentConfig.logger ?? NoopLogger; - this.#authenticator = createDPaymentsAuthenticator(config.paymentConfig); - this.#observer = createDPaymentsObserver(config.paymentConfig); + this.#logger = config.payment.logger ?? NoopLogger; + this.#authenticator = createDPaymentsAuthenticator(config); + this.#observer = createDPaymentsObserver(config); this.#verificationPolicy = config.verificationPolicy ?? FundedOrSettledPayment; this.#consumer = config.consumer ?? (None as PaymentConsumer); @@ -63,8 +63,8 @@ export class PaymentAuthorizer< ? undefined : refundsSchema.parse(config.refunds); - const cacheSetting = config.paymentConfig.cache - ?? (config.paymentConfig.settlementWindow !== undefined ? true : undefined); + const cacheSetting = config.payment.cache + ?? (config.payment.settlementWindow !== undefined ? true : undefined); const referenceCacheTtlMs = resolveLatestBlockCacheTtlMs(cacheSetting); this.#referenceCache = referenceCacheTtlMs === null ? null @@ -96,14 +96,14 @@ export class PaymentAuthorizer< message: "Resolving settlement timing for a payment challenge.", context: { resource: terms.resource, - settlementWindow: this.#config.paymentConfig.settlementWindow, + settlementWindow: this.#config.payment.settlementWindow, cacheEnabled: this.#referenceCache !== null, }, }); let challengeSettlement; try { challengeSettlement = await resolveChallengeSettlementTerms( - this.#config.paymentConfig, + this.#config, terms, this.#referenceCache, ); @@ -148,8 +148,8 @@ export class PaymentAuthorizer< const paymentRequest = buildServerPaymentRequest({ request, terms: challengeSettlement.terms, - ...(this.#config.paymentConfig.identifier !== undefined - ? { identifier: this.#config.paymentConfig.identifier } + ...(this.#config.payment.identifier !== undefined + ? { identifier: this.#config.payment.identifier } : {}), }); @@ -184,7 +184,7 @@ export class PaymentAuthorizer< } const settlement = resolveProofSettlementTerms( - this.#config.paymentConfig, + this.#config, terms, proof.settlementReference, ); @@ -200,8 +200,8 @@ export class PaymentAuthorizer< const paymentRequest = buildServerPaymentRequest({ request, terms: settlement.terms, - ...(this.#config.paymentConfig.identifier !== undefined - ? { identifier: this.#config.paymentConfig.identifier } + ...(this.#config.payment.identifier !== undefined + ? { identifier: this.#config.payment.identifier } : {}), }); const { dPaymentProof } = proof; @@ -216,7 +216,7 @@ export class PaymentAuthorizer< let authenticatedSettlementReference: D402BlockReference | undefined; if (settlement.mode === "window" && settlement.settlementReference !== undefined) { const resolvedReference = await resolveSettlementReference( - this.#config.paymentConfig.rpcClient, + this.#config.adapter.rpcClient, this.#referenceCache, settlement.settlementReference, ); diff --git a/src/server/payment-request.ts b/src/server/payment-request.ts index 9ba986b..6a86b88 100644 --- a/src/server/payment-request.ts +++ b/src/server/payment-request.ts @@ -2,7 +2,7 @@ import { D402_CANONICAL_SALT, D402_VERSION, } from "../core/constants.js"; -import { parsePaymentRequest } from "../core/payment-request.js"; +import { parsePaymentRequest } from "../core/index.js"; import type { D402PaymentRequest } from "../core/types.js"; import type { PaymentIdentifier, @@ -82,7 +82,7 @@ function completeTermsFromRequest( if (settlementTimeUnixSec === undefined) { throw new Error( - "settlementTimeUnixSec must be provided by paymentConfig.settlementWindow, paymentConfig.settlementTimeUnixSec, or terms.settlementTimeUnixSec", + "settlementTimeUnixSec must be provided by payment.settlementWindow, payment.settlementTimeUnixSec, or terms.settlementTimeUnixSec", ); } @@ -112,7 +112,7 @@ function assertPayableTermsDoNotSelectSalt( ): void { if (Object.prototype.hasOwnProperty.call(terms, "paymentSalt")) { throw new Error( - "paymentSalt cannot be configured through payable terms; use paymentConfig.identifier", + "paymentSalt cannot be configured through payable terms; use payment.identifier", ); } -} +} \ No newline at end of file diff --git a/src/server/payment-verifier.ts b/src/server/payment-verifier.ts index bc76474..171d878 100644 --- a/src/server/payment-verifier.ts +++ b/src/server/payment-verifier.ts @@ -7,8 +7,6 @@ import { } from "@rakelabs/dpayments-sdk"; import { requireAddress } from "@rakelabs/dpayments-sdk"; import type { - AbiCodec, - MulticallConfig, PaymentCreatedEvent, } from "@rakelabs/dpayments-sdk"; import { @@ -29,6 +27,7 @@ import type { PaymentState as D402PaymentState, AuthenticatedPayment, AuthenticatedPaymentContext, + PaymentConfig, PaymentAuthenticator, PaymentFailure, PaymentObserver, @@ -53,24 +52,15 @@ export function verifyPaymentSalt( return validatePaymentSalt(paymentRequest, dPaymentProof.paymentSalt); } -export interface DPaymentsAuthenticatorOptions { - rpcClient: D402RpcClient; - codec: AbiCodec; - confirmations?: number; - settlementWindow?: number; - /** Trusted private-network or test-chain Multicall3 deployment. */ - multicall?: MulticallConfig; -} - export function createDPaymentsAuthenticator( - options: DPaymentsAuthenticatorOptions, + config: PaymentConfig, ): PaymentAuthenticator { - const events = new PaymentEvents(options.codec); - const confirmations = options.confirmations ?? D402_DEFAULT_CONFIRMATIONS; + const events = new PaymentEvents(config.adapter.codec); + const confirmations = config.payment.confirmations ?? D402_DEFAULT_CONFIRMATIONS; let connectedChainId: Promise | undefined; function getVerifierChainId(): Promise { - connectedChainId ??= getConnectedChainId(options.rpcClient); + connectedChainId ??= getConnectedChainId(config.adapter.rpcClient); return connectedChainId; } @@ -86,7 +76,7 @@ export function createDPaymentsAuthenticator( } const receiptResult = await readTransactionReceipt( - options.rpcClient, + config.adapter.rpcClient, proof.txHash, ); if (!receiptResult.ok) return receiptResult; @@ -95,7 +85,7 @@ export function createDPaymentsAuthenticator( paymentRequest, dPaymentProof: proof, receipt: receiptResult.receipt, - rpcClient: options.rpcClient, + rpcClient: config.adapter.rpcClient, events, confirmations, }); @@ -109,9 +99,9 @@ export function createDPaymentsAuthenticator( ? { settlementReference: input.settlementReference } : {}), receipt: createdEventResult.receipt, - rpcClient: options.rpcClient, - ...(options.settlementWindow !== undefined - ? { settlementWindow: options.settlementWindow } + rpcClient: config.adapter.rpcClient, + ...(config.payment.settlementWindow !== undefined + ? { settlementWindow: config.payment.settlementWindow } : {}), }); if (!settlementResult.ok) return settlementResult; @@ -129,25 +119,18 @@ export function createDPaymentsAuthenticator( }; } -export interface DPaymentsObserverOptions { - rpcClient: D402RpcClient; - codec: AbiCodec; - /** Trusted private-network or test-chain Multicall3 deployment. */ - multicall?: MulticallConfig; -} - export function createDPaymentsObserver( - options: DPaymentsObserverOptions, + config: PaymentConfig, ): PaymentObserver { let reader: Promise | undefined; const inFlightPaymentStateReads = new Map>(); function getReader(): Promise { - reader ??= getConnectedChainId(options.rpcClient).then((chainId) => + reader ??= getConnectedChainId(config.adapter.rpcClient).then((chainId) => new PaymentReader( - options.rpcClient, - options.codec, - options.multicall ?? getDPaymentsMulticallConfig(chainId), + config.adapter.rpcClient, + config.adapter.codec, + config.payment.multicall ?? getDPaymentsMulticallConfig(chainId), ), ); return reader; diff --git a/src/server/refunder.ts b/src/server/refunder.ts index 370313c..7adb2f9 100644 --- a/src/server/refunder.ts +++ b/src/server/refunder.ts @@ -39,19 +39,17 @@ export function refunder< } refundsSchema.parse(routeConfig.refunds); - const txSender = routeConfig.paymentConfig.txSender; + const txSender = routeConfig.adapter.txSender; if (txSender === undefined) { throw new Error( - "paymentConfig.txSender is required for refunder so the payee can broadcast refunds.", + "adapter.txSender is required for refunder so the payee can broadcast refunds.", ); } const signerAddress = txSender.getAddress(); - const authenticator = createDPaymentsAuthenticator( - routeConfig.paymentConfig, - ); - const observer = createDPaymentsObserver(routeConfig.paymentConfig); - const actions = paymentActions(routeConfig.paymentConfig); + const authenticator = createDPaymentsAuthenticator(routeConfig); + const observer = createDPaymentsObserver(routeConfig); + const actions = paymentActions(routeConfig); return async function handleRefundRequest(request: Req): Promise { const parsed = await parseRefundRequest(request); diff --git a/src/server/settlement.ts b/src/server/settlement.ts index afe8c54..257c898 100644 --- a/src/server/settlement.ts +++ b/src/server/settlement.ts @@ -1,19 +1,14 @@ import type { D402BlockReference, D402PaymentRequest, - D402RpcClient, } from "../core/index.js"; -import type { PayableTerms, ResolvedPayableTerms } from "./types.js"; +import type { + PayableTerms, + PaymentConfig, + ResolvedPayableTerms, +} from "./types.js"; import type { BlockReferenceCache } from "./cache.js"; import { readBlockReference } from "./block-reference.js"; -import type { D402Logger } from "../runtime/logger.js"; - -export interface SettlementConfig { - rpcClient: D402RpcClient; - settlementWindow?: number; - settlementTimeUnixSec?: number; - logger?: D402Logger; -} export class SettlementTimingConfigurationError extends Error { constructor(message: string) { @@ -27,22 +22,22 @@ export type ResolvedSettlementTerms = ResolvedPayableTerms & { }; export async function resolveChallengeSettlementTerms( - paymentConfig: SettlementConfig, + config: PaymentConfig, terms: ResolvedPayableTerms, referenceCache: BlockReferenceCache | null, ): Promise<{ terms: ResolvedSettlementTerms; settlementReference?: D402BlockReference; }> { - validateSettlementTimingConfiguration(paymentConfig, terms); + validateSettlementTimingConfiguration(config, terms); - if (paymentConfig.settlementWindow !== undefined) { + if (config.payment.settlementWindow !== undefined) { const lookup = referenceCache - ? await referenceCache.getLatest(paymentConfig.rpcClient) + ? await referenceCache.getLatest(config.adapter.rpcClient) : await readBlockReference( - paymentConfig.rpcClient, + config.adapter.rpcClient, "latest", - paymentConfig.logger, + config.payment.logger, ); if (!lookup.ok) { throw lookup.cause instanceof Error @@ -52,12 +47,12 @@ export async function resolveChallengeSettlementTerms( const resolvedTerms = withSettlementTime( terms, - addWindow(lookup.reference.blockTimestampUnixSec, paymentConfig.settlementWindow), + addWindow(lookup.reference.blockTimestampUnixSec, config.payment.settlementWindow), ); return { terms: resolvedTerms, settlementReference: lookup.reference }; } - return { terms: withSettlementTime(terms, fixedSettlementTime(paymentConfig, terms)) }; + return { terms: withSettlementTime(terms, fixedSettlementTime(config, terms)) }; } @@ -71,13 +66,13 @@ export type ProofSettlementResult = | { ok: false; reason: "missing-settlement-reference" }; export function resolveProofSettlementTerms( - paymentConfig: SettlementConfig, + config: PaymentConfig, terms: ResolvedPayableTerms, suppliedReference?: D402BlockReference, ): ProofSettlementResult { - validateSettlementTimingConfiguration(paymentConfig, terms); + validateSettlementTimingConfiguration(config, terms); - if (paymentConfig.settlementWindow !== undefined) { + if (config.payment.settlementWindow !== undefined) { if (suppliedReference === undefined) { return { ok: false, reason: "missing-settlement-reference" }; } @@ -86,7 +81,7 @@ export function resolveProofSettlementTerms( mode: "window", terms: withSettlementTime( terms, - addWindow(suppliedReference.blockTimestampUnixSec, paymentConfig.settlementWindow), + addWindow(suppliedReference.blockTimestampUnixSec, config.payment.settlementWindow), ), settlementReference: suppliedReference, }; @@ -95,20 +90,20 @@ export function resolveProofSettlementTerms( return { ok: true, mode: "fixed", - terms: withSettlementTime(terms, fixedSettlementTime(paymentConfig, terms)), + terms: withSettlementTime(terms, fixedSettlementTime(config, terms)), }; } function fixedSettlementTime( - config: SettlementConfig, + config: PaymentConfig, terms: ResolvedPayableTerms, ): D402PaymentRequest["settlementTimeUnixSec"] { - if (config.settlementTimeUnixSec !== undefined) return String(config.settlementTimeUnixSec) as `${bigint}`; + if (config.payment.settlementTimeUnixSec !== undefined) return String(config.payment.settlementTimeUnixSec) as `${bigint}`; const termTime = (terms as Partial>) .settlementTimeUnixSec; if (termTime !== undefined) return termTime; throw new SettlementTimingConfigurationError( - "settlementTimeUnixSec must be provided by paymentConfig.settlementWindow, paymentConfig.settlementTimeUnixSec, or terms.settlementTimeUnixSec", + "settlementTimeUnixSec must be provided by payment.settlementWindow, payment.settlementTimeUnixSec, or terms.settlementTimeUnixSec", ); } @@ -124,29 +119,29 @@ function addWindow(timestamp: string, window: number): D402PaymentRequest["settl } export function validateSettlementTimingConfiguration( - config: SettlementConfig, + config: PaymentConfig, terms: PayableTerms, ): void { const termTime = (terms as Partial>) .settlementTimeUnixSec; - if (config.settlementWindow !== undefined && config.settlementTimeUnixSec !== undefined) { + if (config.payment.settlementWindow !== undefined && config.payment.settlementTimeUnixSec !== undefined) { throw new SettlementTimingConfigurationError( - "paymentConfig.settlementWindow and paymentConfig.settlementTimeUnixSec cannot both be set; choose one source of settlement timing", + "payment.settlementWindow and payment.settlementTimeUnixSec cannot both be set; choose one source of settlement timing", ); } - if (config.settlementWindow !== undefined && termTime !== undefined) { + if (config.payment.settlementWindow !== undefined && termTime !== undefined) { throw new SettlementTimingConfigurationError( - "paymentConfig.settlementWindow and terms.settlementTimeUnixSec cannot both be set; choose one source of settlement timing", + "payment.settlementWindow and terms.settlementTimeUnixSec cannot both be set; choose one source of settlement timing", ); } - if (config.settlementTimeUnixSec !== undefined && termTime !== undefined) { + if (config.payment.settlementTimeUnixSec !== undefined && termTime !== undefined) { throw new SettlementTimingConfigurationError( - "paymentConfig.settlementTimeUnixSec and terms.settlementTimeUnixSec cannot both be set; choose one source of settlement timing", + "payment.settlementTimeUnixSec and terms.settlementTimeUnixSec cannot both be set; choose one source of settlement timing", ); } - if (config.settlementWindow !== undefined && (!Number.isInteger(config.settlementWindow) || config.settlementWindow < 0)) { + if (config.payment.settlementWindow !== undefined && (!Number.isInteger(config.payment.settlementWindow) || config.payment.settlementWindow < 0)) { throw new SettlementTimingConfigurationError( - "paymentConfig.settlementWindow must be a non-negative integer", + "payment.settlementWindow must be a non-negative integer", ); } } diff --git a/src/server/types.ts b/src/server/types.ts index 8953a51..c460923 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -1,6 +1,6 @@ import type { Address, - D402ErrorDecoder, + D402Adapter, D402PaymentActionResult, D402PaymentChallenge, D402RefundRoute, @@ -8,12 +8,9 @@ import type { D402BlockReference, D402EventHandler, D402PaymentRequest, - D402RpcClient, - D402TxSender, Hex32, PaymentAddress, } from "../core/index.js"; -import type { AbiCodec } from "@rakelabs/dpayments-sdk"; import type { MulticallConfig } from "@rakelabs/dpayments-sdk"; import type { D402Logger } from "../runtime/logger.js"; import type { PaymentConsumer } from "./payment-consumer.js"; @@ -47,11 +44,7 @@ export type PaymentIdentifier = | "server" | "client"; -export interface PaymentConfig { - rpcClient: D402RpcClient; - codec: AbiCodec; - errorDecoder?: D402ErrorDecoder; - txSender?: D402TxSender; +export interface PaymentOptions { confirmations?: number; settlementWindow?: number; settlementTimeUnixSec?: number; @@ -63,6 +56,11 @@ export interface PaymentConfig { multicall?: MulticallConfig; } +export interface PaymentConfig { + adapter: D402Adapter; + payment: PaymentOptions; +} + export type PayableTerms< Req extends Request = Request, > = Pick< @@ -272,8 +270,7 @@ export interface PayableRouteConfig< Req extends Request = Request, Result = void, Res = Response, -> { - paymentConfig: PaymentConfig; +> extends PaymentConfig { terms: PayableTermsResolver; handler: PayableHandler; verificationPolicy?: VerificationPolicy;