Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions packages/svm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# @x402r/svm

AuthCapture payment scheme for x402 on **Solana**.

> **Pilot. Unaudited.** Mainnet usage is at users' own risk.

Direct port of `base/commerce-payments`'s authCapture primitives to SVM:

| `base/commerce-payments` (EVM) | This scheme (SVM) |
| :--- | :--- |
| `AuthCaptureEscrow` | `auth-capture-escrow` Anchor program |
| `EIP3009TokenCollector` / `Permit2TokenCollector` | `spl-token-collector` Anchor program (and any future `ITokenCollector`) |

Higher-level patterns (operator factories, plugin slots, condition / hook programs, multisig operators) are x402r-specific extensions and live outside this package — see `x402r-contracts-svm/programs/payment-operator/` if you want one.

## Layout

```
authCapture/client → AuthCaptureSvmScheme (createPaymentPayload)
authCapture/server → AuthCaptureSvmServerScheme
authCapture/facilitator → AuthCaptureSvmFacilitatorScheme (verify + settle)
authCapture/shared → types, constants, Borsh encoder/decoder, PDA helpers
```

## Toolchain

- `@solana/kit` (no `@solana/web3.js` v1)
- Codama-generated client (consumed by tests AND this SDK)
- Vitest, tsup, TypeScript 5.7+

## Versioning

Starts at `0.2.0` to match `@x402r/evm`. Both packages move in lockstep on the x402r release line.

## Status

Until the Anchor IDL is generated and Codama-generated clients land at `src/codama-generated/`, a few helpers throw `stub:` errors at runtime (see `shared/pda.ts`). Run `pnpm codama:generate` from `x402r-contracts-svm/` after `anchor build` to wire those in.
90 changes: 90 additions & 0 deletions packages/svm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
{
"name": "@x402r/svm",
"version": "0.2.0",
"description": "AuthCapture payment scheme for x402 on Solana (pilot, unaudited)",
"license": "MIT",
"author": "x402r",
"repository": {
"type": "git",
"url": "https://github.com/BackTrackCo/x402r-scheme.git",
"directory": "packages/svm"
},
"exports": {
".": {
"import": {
"types": "./dist/esm/index.d.mts",
"default": "./dist/esm/index.mjs"
},
"require": {
"types": "./dist/cjs/index.d.ts",
"default": "./dist/cjs/index.js"
}
},
"./authCapture/client": {
"import": {
"types": "./dist/esm/authCapture/client/index.d.mts",
"default": "./dist/esm/authCapture/client/index.mjs"
},
"require": {
"types": "./dist/cjs/authCapture/client/index.d.ts",
"default": "./dist/cjs/authCapture/client/index.js"
}
},
"./authCapture/server": {
"import": {
"types": "./dist/esm/authCapture/server/index.d.mts",
"default": "./dist/esm/authCapture/server/index.mjs"
},
"require": {
"types": "./dist/cjs/authCapture/server/index.d.ts",
"default": "./dist/cjs/authCapture/server/index.js"
}
},
"./authCapture/facilitator": {
"import": {
"types": "./dist/esm/authCapture/facilitator/index.d.mts",
"default": "./dist/esm/authCapture/facilitator/index.mjs"
},
"require": {
"types": "./dist/cjs/authCapture/facilitator/index.d.ts",
"default": "./dist/cjs/authCapture/facilitator/index.js"
}
}
},
"publishConfig": {
"access": "public"
},
"files": [
"dist",
"src",
"README.md"
],
"scripts": {
"build": "tsup",
"clean": "rm -rf dist",
"test": "vitest run",
"test:integration": "vitest run --config vitest.integration.config.ts",
"test:watch": "vitest",
"typecheck": "tsc --noEmit",
"lint": "eslint . --fix",
"lint:check": "eslint .",
"format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"",
"format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\""
},
"peerDependencies": {
"@x402/core": "^2.4.0",
"@x402/svm": "^2.5.0",
"@solana/kit": ">=5.1.0"
},
"dependencies": {
"@noble/hashes": "^1.5.0",
"@solana-program/compute-budget": "^0.11.0",
"@solana-program/token": "^0.9.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"tsup": "^8.4.0",
"typescript": "^5.7.0",
"vitest": "^4.0.18"
}
}
183 changes: 183 additions & 0 deletions packages/svm/src/authCapture/client/encoder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/**
* Hand-rolled instruction encoders for `auth_capture_escrow.{authorize,charge}`.
*
* Replace with Codama-generated builders once `pnpm codama:generate` runs.
*
* Inner ixs call escrow directly (mirrors the EVM scheme: client signs an
* authorization, facilitator submits to escrow). The escrow internally CPIs
* into the configured `ITokenCollector` for the actual SPL transfer. No
* intermediate operator program — `paymentInfo.operator` is whatever pubkey
* the merchant chose, signed-as via the partial-tx by either the
* captureAuthorizer themselves (typical: facilitator-as-captureAuthorizer)
* or by an external program (out-of-scope x402r extension).
*/

import type { Address, Instruction } from "@solana/kit";
import { getAddressEncoder } from "@solana/kit";
import { TOKEN_PROGRAM_ADDRESS } from "@solana-program/token";

import { ESCROW_IX_DISC } from "../shared/constants";
import { encodePaymentInfo } from "../shared/nonce";
import type { AuthCaptureSvmExtra, PaymentInfoSvm, SplitEntry } from "../shared/types";

const SYSTEM_PROGRAM_ID = "11111111111111111111111111111111" as Address;
const ASSOC_TOKEN_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" as Address;

interface AuthorizeAccs {
operator: Address;
paymentStatePda: Address;
vaultAta: Address;
payerAta: Address;
mint: Address;
payer: Address;
rentPayer: Address;
}

interface ChargeAccs extends AuthorizeAccs {
receiverAta: Address;
receiver: Address;
protocolFeeReceiverAta: Address;
protocolFeeReceiver: Address;
operatorFeeReceiverAta: Address;
operatorFeeReceiver: Address;
protocolFeeConfigPda: Address;
}

/** Encode `auth_capture_escrow::authorize`. */
export function encodeEscrowAuthorizeIx(args: {
paymentInfo: PaymentInfoSvm;
amount: bigint;
collectorData: Uint8Array;
extra: AuthCaptureSvmExtra;
accounts: AuthorizeAccs;
}): Instruction {
const data = concat([
ESCROW_IX_DISC.authorize,
encodePaymentInfo(args.paymentInfo),
u64Le(args.amount),
vecLen(args.collectorData.length),
args.collectorData,
]);

const a = args.accounts;
// Escrow `Authorize` named accounts; collector accounts come after as
// remaining_accounts (the escrow forwards them unchanged to the collector).
const accounts = [
meta(a.operator, false, true),
meta(a.paymentStatePda, true, false),
meta(a.vaultAta, true, false),
meta(a.mint, false, false),
meta(a.rentPayer, true, true),
meta(args.extra.collectorProgramId, false, false),
meta(TOKEN_PROGRAM_ADDRESS, false, false),
meta(ASSOC_TOKEN_PROGRAM_ID, false, false),
meta(SYSTEM_PROGRAM_ID, false, false),
// --- collector accounts (CollectAuthorize) ---
meta(a.payerAta, true, false),
meta(a.vaultAta, true, false),
meta(a.payer, false, true),
meta(a.mint, false, false),
meta(TOKEN_PROGRAM_ADDRESS, false, false),
];

return {
programAddress: args.extra.escrowProgramId,
accounts,
data,
};
}

/** Encode `auth_capture_escrow::charge`. */
export function encodeEscrowChargeIx(args: {
paymentInfo: PaymentInfoSvm;
amount: bigint;
splits: SplitEntry[];
collectorData: Uint8Array;
extra: AuthCaptureSvmExtra;
accounts: ChargeAccs;
}): Instruction {
const data = concat([
ESCROW_IX_DISC.charge,
encodePaymentInfo(args.paymentInfo),
u64Le(args.amount),
encodeSplits(args.splits),
vecLen(args.collectorData.length),
args.collectorData,
]);

const a = args.accounts;
const accounts = [
meta(a.operator, false, true),
meta(a.paymentStatePda, true, false),
meta(a.vaultAta, true, false),
meta(a.receiverAta, true, false),
meta(a.receiver, false, false),
meta(a.protocolFeeReceiverAta, true, false),
meta(a.protocolFeeReceiver, false, false),
meta(a.operatorFeeReceiverAta, true, false),
meta(a.operatorFeeReceiver, false, false),
meta(a.protocolFeeConfigPda, false, false),
meta(a.mint, false, false),
meta(a.rentPayer, true, true),
meta(args.extra.collectorProgramId, false, false),
meta(TOKEN_PROGRAM_ADDRESS, false, false),
meta(ASSOC_TOKEN_PROGRAM_ID, false, false),
meta(SYSTEM_PROGRAM_ID, false, false),
// --- collector accounts ---
meta(a.payerAta, true, false),
meta(a.vaultAta, true, false),
meta(a.payer, false, true),
meta(a.mint, false, false),
meta(TOKEN_PROGRAM_ADDRESS, false, false),
];

return {
programAddress: args.extra.escrowProgramId,
accounts,
data,
};
}

function meta(address: Address, isWritable: boolean, isSigner: boolean) {
return { address, role: roleFor(isWritable, isSigner) };
}

function roleFor(writable: boolean, signer: boolean): 0 | 1 | 2 | 3 {
if (writable && signer) return 3;
if (writable) return 2;
if (signer) return 1;
return 0;
}

function encodeSplits(splits: SplitEntry[]): Uint8Array {
const enc = getAddressEncoder();
const parts: Uint8Array[] = [vecLen(splits.length)];
for (const e of splits) {
parts.push(new Uint8Array(enc.encode(e.recipient)));
parts.push(u64Le(e.amount));
}
return concat(parts);
}

function u64Le(value: bigint): Uint8Array {
const buf = new Uint8Array(8);
new DataView(buf.buffer).setBigUint64(0, BigInt(value), true);
return buf;
}

function vecLen(n: number): Uint8Array {
const buf = new Uint8Array(4);
new DataView(buf.buffer).setUint32(0, n, true);
return buf;
}

function concat(parts: Uint8Array[]): Uint8Array {
const total = parts.reduce((s, p) => s + p.length, 0);
const out = new Uint8Array(total);
let off = 0;
for (const p of parts) {
out.set(p, off);
off += p.length;
}
return out;
}
5 changes: 5 additions & 0 deletions packages/svm/src/authCapture/client/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { AuthCaptureSvmScheme } from "./scheme";
export type { AuthCaptureSvmClientOptions } from "./scheme";
export { encodeEscrowAuthorizeIx, encodeEscrowChargeIx } from "./encoder";
export { registerAuthCaptureSvmScheme } from "./register";
export type { SvmClientConfig } from "./register";
40 changes: 40 additions & 0 deletions packages/svm/src/authCapture/client/register.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { Network } from "@x402/core/types";
import { x402Client } from "@x402/core/client";
import type { ClientSvmSigner, ClientSvmConfig } from "@x402/svm";
import { AuthCaptureSvmScheme } from "./scheme";

export interface SvmClientConfig {
signer: ClientSvmSigner;
config?: ClientSvmConfig;
defaultChargeOperatorBps?: number;
networks?: Network | Network[];
}

/**
* Register the SVM authCapture client scheme with x402Client.
*
* @example
* ```ts
* const client = new x402Client();
* registerAuthCaptureSvmScheme(client, { signer, networks: SVM_DEVNET });
* ```
*/
export function registerAuthCaptureSvmScheme(
client: x402Client,
config: SvmClientConfig,
): x402Client {
const scheme = new AuthCaptureSvmScheme({
signer: config.signer,
config: config.config,
defaultChargeOperatorBps: config.defaultChargeOperatorBps,
});
const networks = config.networks
? Array.isArray(config.networks)
? config.networks
: [config.networks]
: (["solana:*" as Network]);
for (const network of networks) {
client.register(network, scheme);
}
return client;
}
Loading
Loading