Skip to content
Draft
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
2 changes: 2 additions & 0 deletions .changeset/gold-vans-look.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
46 changes: 31 additions & 15 deletions examples/x402-buyer-fetch/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dotenv/config';
import { createInflowClient } from '@inflowpayai/x402-buyer';
import { sellerProbe } from '@inflowpayai/x402-buyer/probe';
import { x402HTTPClient } from '@x402/core/client';
import { decodePaymentResponseHeader } from '@x402/core/http';

const apiKey = process.env.INFLOW_API_KEY;
if (apiKey === undefined || apiKey === '') {
Expand Down Expand Up @@ -41,20 +42,35 @@ const paymentHeaders = http.encodePaymentSignatureHeader(paymentPayload);

const paid = await fetch(target, { headers: paymentHeaders });

// processResponse parses the response body once and returns a
// discriminated outcome. `'success'` carries the parsed body and the
// settle response; other branches surface the failure mode.
const result = await http.processResponse(paid);
switch (result.kind) {
case 'success':
console.log(` status: ${result.response.status.toString()}`);
console.log(` body: ${JSON.stringify(result.body)}`);
console.log(` paid via ${result.settleResponse.network}: ${result.settleResponse.transaction}`);
break;
case 'settle_failed':
console.error(` settle failed: ${result.settleResponse.errorReason ?? 'unknown'}`);
process.exit(1);
default:
console.error(` unexpected outcome: kind=${result.kind}`);
const body = await readResponseBody(paid);
console.log(` status: ${paid.status.toString()}`);
console.log(` body: ${JSON.stringify(body)}`);

const paymentResponseHeader = paid.headers.get('payment-response') ?? paid.headers.get('x-payment-response');
if (paymentResponseHeader !== null && paymentResponseHeader !== '') {
const settled = decodePaymentResponseHeader(paymentResponseHeader);
if (settled.success) {
console.log(` paid via ${settled.network}: ${settled.transaction}`);
} else {
console.error(` settle failed: ${settled.errorReason ?? settled.errorMessage ?? 'unknown'}`);
process.exit(1);
}
}

if (!paid.ok) {
process.exit(1);
}

async function readResponseBody(response: Response): Promise<unknown> {
const text = await response.text();
if (text === '') {
return undefined;
}

const contentType = response.headers.get('content-type') ?? '';
if (contentType.includes('application/json')) {
return JSON.parse(text) as unknown;
}

return text;
}
23 changes: 21 additions & 2 deletions packages/x402-seller/test/fixtures/config-response.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import type { X402ConfigResponse, X402FacilitatorSupportedResponse } from '@inflowpayai/x402';

/**
* Representative `X402ConfigResponse` shape. One EVM chain (Base) with USDC + USDT, one Solana chain with USDC, and an
* InFlow balance payment method. All addresses are deterministic fakes.
* Representative `X402ConfigResponse` shape. Base with USDC + USDT, Solana with USDC, Tempo with USDC, and an InFlow
* balance payment method. All addresses are deterministic fakes.
*/
export const SAMPLE_CONFIG: X402ConfigResponse = {
sellerId: '00000000-0000-0000-0000-000000000001',
supported: [
{ network: 'eip155:8453', scheme: 'exact', x402Version: 2 },
{ network: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', scheme: 'exact', x402Version: 2 },
{ network: 'eip155:4217', scheme: 'exact', x402Version: 2 },
{ network: 'inflow:1', scheme: 'balance', x402Version: 2 },
],
wallets: [
Expand All @@ -23,6 +24,11 @@ export const SAMPLE_CONFIG: X402ConfigResponse = {
network: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
feePayer: 'SoLaNaFeePayer0000000000000000000000000004',
},
{
address: '0x0000000000000000000000000000000000004217',
blockchain: 'TEMPO',
network: 'eip155:4217',
},
],
assets: [
{
Expand Down Expand Up @@ -58,6 +64,18 @@ export const SAMPLE_CONFIG: X402ConfigResponse = {
decimals: 6,
network: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
},
{
assetTransferMethod: 'permit2',
assetId: '0x20c000000000000000000000b9537d11c60e8b50',
assetName: 'USDC',
blockchain: 'TEMPO',
currency: 'USDC',
decimals: 6,
network: 'eip155:4217',
permit2Proxy: '0x402085c248EeA27D92E8b30b2C58ed07f9E20001',
tokenName: 'USDC.e',
tokenVersion: '2',
},
],
paymentMethods: [
{
Expand All @@ -74,6 +92,7 @@ export const SAMPLE_SUPPORTED: X402FacilitatorSupportedResponse = {
extensions: ['payment-identifier'],
signers: {
'eip155:8453': ['0xSigner1', '0xSigner2'],
'eip155:4217': ['0xTempoSigner'],
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': ['SoLaNaSigner'],
},
};
26 changes: 26 additions & 0 deletions packages/x402-seller/test/property/inflow-accepts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,32 @@ describe('inflowAccepts property invariants', () => {
);
});

it('on-chain entries carry extra.assetName verbatim from the asset config', async () => {
await fc.assert(
fc.asyncProperty(configArb, async (config) => {
server.use(
http.get(`${PROD_BASE}/v1/x402/config`, () => HttpResponse.json(config)),
http.get(`${PROD_BASE}/v1/x402/supported`, () => HttpResponse.json(SAMPLE_SUPPORTED)),
);
const client = await createInflowSellerClient({ environment: 'production', apiKey: 'sk_test' });
const out = await inflowAccepts(client, { price: '$0.01' });
const exact = out.filter((o) => o.scheme === 'exact');
for (const entry of exact) {
const price = entry.price;
expect(typeof price).toBe('object');
expect(price).not.toBeNull();
const priceObj = price as { asset?: unknown };
expect(typeof priceObj.asset).toBe('string');
const asset = config.assets.find((a) => a.network === entry.network && a.assetId === priceObj.asset);
expect(asset).toBeDefined();
expect((entry.extra as { assetName?: string }).assetName).toBe(asset?.assetName);
}
server.resetHandlers();
}),
{ numRuns: 30 },
);
});

it('every entry carries an AssetAmount price (asset + amount strings)', async () => {
await fc.assert(
fc.asyncProperty(configArb, async (config) => {
Expand Down
33 changes: 19 additions & 14 deletions packages/x402-seller/test/unit/inflow-accepts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,14 @@ describe('inflowAccepts', () => {

// SAMPLE_CONFIG (under USD wildcard):
// On-chain: USDC/Base (1 entry), USDT/Base (1 entry),
// USDC/Solana (1 entry) = 3 exact entries. The SDK takes
// `assetTransferMethod` from the server verbatim — no
// implicit EIP-3009/Permit2 fanout.
// USDC/Solana (1 entry), USDC/Tempo (1 entry) = 4 exact entries.
// The SDK takes `assetTransferMethod` from the server verbatim.
// Balance: 1 paymentMethod × 2 distinct asset currencies (USDC,
// USDT) = 2 balance entries.
// Total: 3 + 2 = 5.
expect(out).toHaveLength(5);
// Total: 4 + 2 = 6.
expect(out).toHaveLength(6);
const schemes = out.map((o) => o.scheme);
expect(schemes.filter((s) => s === 'exact')).toHaveLength(3);
expect(schemes.filter((s) => s === 'exact')).toHaveLength(4);
expect(schemes.filter((s) => s === 'balance')).toHaveLength(2);
});

Expand Down Expand Up @@ -112,6 +111,13 @@ describe('inflowAccepts', () => {
expect(extra.permit2Proxy).toBe('0x402085c248EeA27D92E8b30b2C58ed07f9E20001');
});

it('on-chain entries advertise the server assetName in extras', async () => {
const client = await makeClient();
const out = await inflowAccepts(client, { price: '0.01 USDC', networks: ['eip155:4217'] });
expect(out).toHaveLength(1);
expect((out[0]!.extra as { assetName: string }).assetName).toBe('USDC');
});

it('emits feePayer on Solana entries', async () => {
const client = await makeClient();
const out = await inflowAccepts(client, { price: '$0.01' });
Expand Down Expand Up @@ -230,10 +236,8 @@ describe('inflowAccepts', () => {
const client = await makeClient();
const out = await inflowAccepts(client, { price: '$0.01', schemes: ['exact'] });
expect(out.every((o) => o.scheme === 'exact')).toBe(true);
// SAMPLE_CONFIG has three on-chain assets: USDC/Base, USDT/Base,
// USDC/Solana. One entry per (wallet, asset) pair — no implicit
// EIP-3009/Permit2 fanout — so 3 exact entries total.
expect(out).toHaveLength(3);
// SAMPLE_CONFIG has four on-chain assets: USDC/Base, USDT/Base, USDC/Solana, and USDC/Tempo.
expect(out).toHaveLength(4);
});

it('filters by schemes (balance only)', async () => {
Expand Down Expand Up @@ -332,20 +336,21 @@ describe('inflowAccepts', () => {
const client = await makeClient();
const out = await inflowAccepts(client, { price: '$0.01' });
const assets = new Set(out.filter((o) => o.scheme === 'exact').map((o) => (o.price as { asset: string }).asset));
// Both USDC (Base + Solana) and USDT (Base) — three distinct assets.
expect(assets.size).toBe(3);
// USDC on Base, Solana, and Tempo plus USDT on Base.
expect(assets.size).toBe(4);
});

it('ordering: on-chain entries in wallet declaration order, then payment methods', async () => {
const client = await makeClient();
const out = await inflowAccepts(client, { price: '$0.01' });
// Wallet declaration order: Base, Solana. One entry per (wallet, asset).
// Wallet declaration order: Base, Solana, Tempo. One entry per (wallet, asset).
// Then payment methods (one per distinct currency under USD wildcard).
expect(out[0]!.network).toBe('eip155:8453'); // USDC eip3009
expect(out[1]!.network).toBe('eip155:8453'); // USDT permit2
expect(out[2]!.network).toBe('solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'); // USDC solana
expect(out[3]!.network).toBe('inflow:1');
expect(out[3]!.network).toBe('eip155:4217'); // USDC Tempo permit2
expect(out[4]!.network).toBe('inflow:1');
expect(out[5]!.network).toBe('inflow:1');
});

it('within an asset, the server-published assetTransferMethod is emitted verbatim', async () => {
Expand Down
6 changes: 4 additions & 2 deletions packages/x402-seller/test/unit/scheme-registrations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,12 @@ describe('inflowSchemeRegistrations', () => {
// SAMPLE_CONFIG has:
// - assets on eip155:8453 (USDC + USDT) → dedupes to one (exact, eip155:8453)
// - asset on solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp → (exact, solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp)
// - asset on eip155:4217 → (exact, eip155:4217)
// - paymentMethods: balance / inflow:1 → (balance, inflow:1)
expect(pairs(registrations)).toEqual([
['exact', 'eip155:8453'],
['exact', 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'],
['exact', 'eip155:4217'],
['balance', 'inflow:1'],
]);
});
Expand Down Expand Up @@ -71,11 +73,11 @@ describe('inflowSchemeRegistrations', () => {
],
});
const registrations = await inflowSchemeRegistrations(client);
// Still exactly three: dedupe collapsed the duplicate.
expect(registrations).toHaveLength(3);
expect(registrations).toHaveLength(4);
expect(pairs(registrations)).toEqual([
['exact', 'eip155:8453'],
['exact', 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'],
['exact', 'eip155:4217'],
['balance', 'inflow:1'],
]);
});
Expand Down
Loading