@rubicon-caliga/agent-sdk 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -7
- package/dist/agent-client.d.ts +4 -0
- package/dist/agent-client.d.ts.map +1 -1
- package/dist/agent-client.js +6 -0
- package/dist/agent-client.js.map +1 -1
- package/dist/agent-client.test.d.ts +2 -0
- package/dist/agent-client.test.d.ts.map +1 -0
- package/dist/agent-client.test.js +110 -0
- package/dist/agent-client.test.js.map +1 -0
- package/dist/circle-agent-wallet.d.ts +65 -0
- package/dist/circle-agent-wallet.d.ts.map +1 -0
- package/dist/circle-agent-wallet.js +156 -0
- package/dist/circle-agent-wallet.js.map +1 -0
- package/dist/circle-agent-wallet.test.d.ts +2 -0
- package/dist/circle-agent-wallet.test.d.ts.map +1 -0
- package/dist/circle-agent-wallet.test.js +106 -0
- package/dist/circle-agent-wallet.test.js.map +1 -0
- package/dist/circle-cli-gateway-payment.d.ts +74 -0
- package/dist/circle-cli-gateway-payment.d.ts.map +1 -0
- package/dist/circle-cli-gateway-payment.js +218 -0
- package/dist/circle-cli-gateway-payment.js.map +1 -0
- package/dist/circle-cli-gateway-payment.test.d.ts +2 -0
- package/dist/circle-cli-gateway-payment.test.d.ts.map +1 -0
- package/dist/circle-cli-gateway-payment.test.js +141 -0
- package/dist/circle-cli-gateway-payment.test.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/payment-engine.d.ts +0 -14
- package/dist/payment-engine.d.ts.map +1 -1
- package/dist/payment-engine.js +0 -35
- package/dist/payment-engine.js.map +1 -1
- package/dist/payment-engine.test.d.ts.map +1 -0
- package/package.json +11 -11
- package/src/agent-client.test.ts +118 -0
- package/src/agent-client.ts +10 -0
- package/src/circle-agent-wallet.test.ts +118 -0
- package/src/circle-agent-wallet.ts +210 -0
- package/src/circle-cli-gateway-payment.test.ts +182 -0
- package/src/circle-cli-gateway-payment.ts +284 -0
- package/src/index.ts +2 -0
- package/src/payment-engine.ts +0 -38
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
CircleCliGatewaySigner,
|
|
5
|
+
parseCircleGatewayBackingEOA,
|
|
6
|
+
parseCircleCliSignature,
|
|
7
|
+
parseCircleCliWalletAddress,
|
|
8
|
+
} from "./circle-cli-gateway-payment.js";
|
|
9
|
+
|
|
10
|
+
test("parses quiet Circle CLI signatures", () => {
|
|
11
|
+
assert.equal(parseCircleCliSignature("0xabc123\n"), "0xabc123");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("parses JSON Circle CLI signatures", () => {
|
|
15
|
+
assert.equal(
|
|
16
|
+
parseCircleCliSignature(JSON.stringify({ data: { signature: "0xdef456" } })),
|
|
17
|
+
"0xdef456",
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("rejects non-signature Circle CLI output", () => {
|
|
22
|
+
assert.throws(() => parseCircleCliSignature("signed"), /did not return a hex EIP-712 signature/);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("parses a sole wallet address from Circle CLI list output", () => {
|
|
26
|
+
assert.equal(
|
|
27
|
+
parseCircleCliWalletAddress(
|
|
28
|
+
JSON.stringify({
|
|
29
|
+
data: {
|
|
30
|
+
wallets: [
|
|
31
|
+
{
|
|
32
|
+
address: "0x1111111111111111111111111111111111111111",
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
),
|
|
38
|
+
"0x1111111111111111111111111111111111111111",
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("parses backing EOA from Circle Gateway balance output", () => {
|
|
43
|
+
assert.equal(
|
|
44
|
+
parseCircleGatewayBackingEOA(
|
|
45
|
+
JSON.stringify({
|
|
46
|
+
data: {
|
|
47
|
+
backingEOA: "0x2222222222222222222222222222222222222222",
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
),
|
|
51
|
+
"0x2222222222222222222222222222222222222222",
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("separates Circle CLI Agent Wallet address from x402 backing EOA", async () => {
|
|
56
|
+
const calls: string[][] = [];
|
|
57
|
+
const signer = new CircleCliGatewaySigner({
|
|
58
|
+
agentWalletAddress: "0x1111111111111111111111111111111111111111",
|
|
59
|
+
chain: "ARC-TESTNET",
|
|
60
|
+
command: "circle",
|
|
61
|
+
runner: async (_command, args) => {
|
|
62
|
+
calls.push(args);
|
|
63
|
+
assert.deepEqual(args, [
|
|
64
|
+
"gateway",
|
|
65
|
+
"balance",
|
|
66
|
+
"--address",
|
|
67
|
+
"0x1111111111111111111111111111111111111111",
|
|
68
|
+
"--chain",
|
|
69
|
+
"ARC-TESTNET",
|
|
70
|
+
"--output",
|
|
71
|
+
"json",
|
|
72
|
+
]);
|
|
73
|
+
return JSON.stringify({
|
|
74
|
+
data: {
|
|
75
|
+
backingEOA: "0x2222222222222222222222222222222222222222",
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await signer.ensureAddress();
|
|
82
|
+
|
|
83
|
+
assert.equal(signer.agentWalletAddress, "0x1111111111111111111111111111111111111111");
|
|
84
|
+
assert.equal(signer.address, "0x2222222222222222222222222222222222222222");
|
|
85
|
+
assert.equal(calls.length, 1);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("discovers sole Agent Wallet and then its Gateway backing EOA", async () => {
|
|
89
|
+
const calls: string[][] = [];
|
|
90
|
+
const signer = new CircleCliGatewaySigner({
|
|
91
|
+
chain: "ARC-TESTNET",
|
|
92
|
+
command: "circle",
|
|
93
|
+
runner: async (_command, args) => {
|
|
94
|
+
calls.push(args);
|
|
95
|
+
if (args[0] === "wallet") {
|
|
96
|
+
return JSON.stringify({
|
|
97
|
+
data: {
|
|
98
|
+
wallets: [{ address: "0x1111111111111111111111111111111111111111" }],
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return JSON.stringify({
|
|
103
|
+
data: {
|
|
104
|
+
backingEOA: "0x2222222222222222222222222222222222222222",
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await signer.ensureAddress();
|
|
111
|
+
|
|
112
|
+
assert.equal(signer.agentWalletAddress, "0x1111111111111111111111111111111111111111");
|
|
113
|
+
assert.equal(signer.address, "0x2222222222222222222222222222222222222222");
|
|
114
|
+
assert.deepEqual(calls, [
|
|
115
|
+
["wallet", "list", "--chain", "ARC-TESTNET", "--type", "agent", "--output", "json"],
|
|
116
|
+
[
|
|
117
|
+
"gateway",
|
|
118
|
+
"balance",
|
|
119
|
+
"--address",
|
|
120
|
+
"0x1111111111111111111111111111111111111111",
|
|
121
|
+
"--chain",
|
|
122
|
+
"ARC-TESTNET",
|
|
123
|
+
"--output",
|
|
124
|
+
"json",
|
|
125
|
+
],
|
|
126
|
+
]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("typed data message.from uses backing EOA while CLI signs with Agent Wallet", async () => {
|
|
130
|
+
let signedPayload: Record<string, unknown> | undefined;
|
|
131
|
+
const signer = new CircleCliGatewaySigner({
|
|
132
|
+
agentWalletAddress: "0x1111111111111111111111111111111111111111",
|
|
133
|
+
buyerWalletAddress: "0x2222222222222222222222222222222222222222",
|
|
134
|
+
chain: "ARC-TESTNET",
|
|
135
|
+
command: "circle",
|
|
136
|
+
runner: async (_command, args) => {
|
|
137
|
+
const addressFlag = args.indexOf("--address");
|
|
138
|
+
assert.equal(args[addressFlag + 1], "0x1111111111111111111111111111111111111111");
|
|
139
|
+
signedPayload = JSON.parse(args[3] ?? "{}") as Record<string, unknown>;
|
|
140
|
+
return "0xabc123";
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
await signer.ensureAddress();
|
|
144
|
+
assert.equal(signer.address, "0x2222222222222222222222222222222222222222");
|
|
145
|
+
|
|
146
|
+
await signer.signTypedData({
|
|
147
|
+
domain: { name: "USD Coin", version: "2", chainId: 5042002 },
|
|
148
|
+
types: {
|
|
149
|
+
TransferWithAuthorization: [
|
|
150
|
+
{ name: "from", type: "address" },
|
|
151
|
+
{ name: "to", type: "address" },
|
|
152
|
+
{ name: "value", type: "uint256" },
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
primaryType: "TransferWithAuthorization",
|
|
156
|
+
message: {
|
|
157
|
+
from: signer.address,
|
|
158
|
+
to: "0x3333333333333333333333333333333333333333",
|
|
159
|
+
value: 1n,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
assert.equal(
|
|
164
|
+
(signedPayload?.message as Record<string, unknown>).from,
|
|
165
|
+
"0x2222222222222222222222222222222222222222",
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("requires explicit wallet address when multiple Agent Wallets are present", () => {
|
|
170
|
+
assert.throws(
|
|
171
|
+
() =>
|
|
172
|
+
parseCircleCliWalletAddress(
|
|
173
|
+
JSON.stringify({
|
|
174
|
+
data: [
|
|
175
|
+
{ address: "0x1111111111111111111111111111111111111111" },
|
|
176
|
+
{ address: "0x2222222222222222222222222222222222222222" },
|
|
177
|
+
],
|
|
178
|
+
}),
|
|
179
|
+
),
|
|
180
|
+
/Multiple Circle Agent Wallets found/,
|
|
181
|
+
);
|
|
182
|
+
});
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import type { StartSessionResponse, StreamPaymentRequest } from "@rubicon-caliga/core";
|
|
4
|
+
import { registerBatchScheme } from "@circle-fin/x402-batching/client";
|
|
5
|
+
import { x402Client } from "@x402/core/client";
|
|
6
|
+
import { ExactEvmScheme } from "@x402/evm/exact/client";
|
|
7
|
+
import type { AgentPaymentEngine } from "./payment-engine.js";
|
|
8
|
+
import { serializeTypedData, toEip712Payload } from "./circle-agent-wallet.js";
|
|
9
|
+
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
|
|
12
|
+
export type CircleCliRunner = (command: string, args: string[]) => Promise<string>;
|
|
13
|
+
|
|
14
|
+
export interface CircleCliGatewayPaymentEngineOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Agent Wallet address controlled by Circle CLI. When omitted, the engine
|
|
17
|
+
* resolves the sole agent wallet returned by `circle wallet list`.
|
|
18
|
+
*
|
|
19
|
+
* @deprecated Use `agentWalletAddress`. This alias is kept for existing SDK
|
|
20
|
+
* callers and is treated as the Circle CLI signing wallet, not the x402
|
|
21
|
+
* authorization `from` address.
|
|
22
|
+
*/
|
|
23
|
+
walletAddress?: `0x${string}`;
|
|
24
|
+
/**
|
|
25
|
+
* Agent Wallet address controlled by Circle CLI. This is passed to
|
|
26
|
+
* `circle wallet sign typed-data --address`.
|
|
27
|
+
*/
|
|
28
|
+
agentWalletAddress?: `0x${string}`;
|
|
29
|
+
/**
|
|
30
|
+
* Gateway backing EOA used as the x402 authorization `from` address. When
|
|
31
|
+
* omitted, the engine resolves it from `circle gateway balance`.
|
|
32
|
+
*/
|
|
33
|
+
buyerWalletAddress?: `0x${string}`;
|
|
34
|
+
/** Alias for `buyerWalletAddress`. */
|
|
35
|
+
backingEOA?: `0x${string}`;
|
|
36
|
+
/** Circle CLI chain name. Rubicon real reads settle on Arc Testnet by default. */
|
|
37
|
+
chain?: string;
|
|
38
|
+
/** Circle CLI binary name or path. */
|
|
39
|
+
command?: string;
|
|
40
|
+
/** Command runner injection point for tests or hosted agent sandboxes. */
|
|
41
|
+
runner?: CircleCliRunner;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface TypedDataRequest {
|
|
45
|
+
domain: Record<string, unknown>;
|
|
46
|
+
types: Record<string, unknown>;
|
|
47
|
+
primaryType: string;
|
|
48
|
+
message: Record<string, unknown>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Circle CLI / Agent Wallet payment engine. It creates the one-word x402
|
|
53
|
+
* payment payload for Rubicon's session-first flow and delegates EIP-712
|
|
54
|
+
* signing to `circle wallet sign typed-data`, so agents never need raw private
|
|
55
|
+
* keys or hand-built x402 payloads.
|
|
56
|
+
*/
|
|
57
|
+
export class CircleCliGatewayPaymentEngine implements AgentPaymentEngine {
|
|
58
|
+
private readonly x402 = new x402Client();
|
|
59
|
+
private readonly signer: CircleCliGatewaySigner;
|
|
60
|
+
|
|
61
|
+
constructor(options: CircleCliGatewayPaymentEngineOptions = {}) {
|
|
62
|
+
this.signer = new CircleCliGatewaySigner({
|
|
63
|
+
agentWalletAddress: options.agentWalletAddress ?? options.walletAddress,
|
|
64
|
+
buyerWalletAddress: options.buyerWalletAddress ?? options.backingEOA,
|
|
65
|
+
chain: options.chain ?? "ARC-TESTNET",
|
|
66
|
+
command: options.command ?? "circle",
|
|
67
|
+
runner: options.runner ?? runCircleCli,
|
|
68
|
+
});
|
|
69
|
+
registerBatchScheme(this.x402, {
|
|
70
|
+
signer: this.signer,
|
|
71
|
+
fallbackScheme: new ExactEvmScheme(this.signer),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async createWordPayment(session: StartSessionResponse): Promise<StreamPaymentRequest> {
|
|
76
|
+
if (!session.paymentRequired) {
|
|
77
|
+
throw new Error("Session did not include an x402 one-word payment requirement");
|
|
78
|
+
}
|
|
79
|
+
await this.signer.ensureAddress();
|
|
80
|
+
return {
|
|
81
|
+
paymentPayload: await this.x402.createPaymentPayload(session.paymentRequired as never),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export class CircleCliGatewaySigner {
|
|
87
|
+
agentWalletAddress: `0x${string}` = "0x0000000000000000000000000000000000000000";
|
|
88
|
+
address: `0x${string}` = "0x0000000000000000000000000000000000000000";
|
|
89
|
+
private resolved = false;
|
|
90
|
+
private resolving?: Promise<void>;
|
|
91
|
+
|
|
92
|
+
constructor(
|
|
93
|
+
private readonly options: {
|
|
94
|
+
agentWalletAddress?: `0x${string}`;
|
|
95
|
+
buyerWalletAddress?: `0x${string}`;
|
|
96
|
+
chain: string;
|
|
97
|
+
command: string;
|
|
98
|
+
runner: CircleCliRunner;
|
|
99
|
+
},
|
|
100
|
+
) {
|
|
101
|
+
if (options.agentWalletAddress) {
|
|
102
|
+
this.agentWalletAddress = options.agentWalletAddress;
|
|
103
|
+
}
|
|
104
|
+
if (options.buyerWalletAddress) {
|
|
105
|
+
this.address = options.buyerWalletAddress;
|
|
106
|
+
}
|
|
107
|
+
if (options.agentWalletAddress && options.buyerWalletAddress) {
|
|
108
|
+
this.resolved = true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async ensureAddress(): Promise<void> {
|
|
113
|
+
if (this.resolved) return;
|
|
114
|
+
if (!this.resolving) {
|
|
115
|
+
this.resolving = this.resolveAddress();
|
|
116
|
+
}
|
|
117
|
+
return this.resolving;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async signTypedData(typed: TypedDataRequest): Promise<`0x${string}`> {
|
|
121
|
+
await this.ensureAddress();
|
|
122
|
+
const signature = await this.options.runner(this.options.command, [
|
|
123
|
+
"wallet",
|
|
124
|
+
"sign",
|
|
125
|
+
"typed-data",
|
|
126
|
+
serializeTypedData(toEip712Payload(typed)),
|
|
127
|
+
"--address",
|
|
128
|
+
this.agentWalletAddress,
|
|
129
|
+
"--chain",
|
|
130
|
+
this.options.chain,
|
|
131
|
+
"--quiet",
|
|
132
|
+
]);
|
|
133
|
+
return parseCircleCliSignature(signature);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private async resolveAddress(): Promise<void> {
|
|
137
|
+
const agentWalletAddress =
|
|
138
|
+
this.options.agentWalletAddress ?? (await this.resolveAgentWalletAddress());
|
|
139
|
+
this.agentWalletAddress = agentWalletAddress;
|
|
140
|
+
this.address =
|
|
141
|
+
this.options.buyerWalletAddress ?? (await this.resolveBackingEOA(agentWalletAddress));
|
|
142
|
+
this.resolved = true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private async resolveAgentWalletAddress(): Promise<`0x${string}`> {
|
|
146
|
+
const output = await this.options.runner(this.options.command, [
|
|
147
|
+
"wallet",
|
|
148
|
+
"list",
|
|
149
|
+
"--chain",
|
|
150
|
+
this.options.chain,
|
|
151
|
+
"--type",
|
|
152
|
+
"agent",
|
|
153
|
+
"--output",
|
|
154
|
+
"json",
|
|
155
|
+
]);
|
|
156
|
+
return parseCircleCliWalletAddress(output);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private async resolveBackingEOA(agentWalletAddress: `0x${string}`): Promise<`0x${string}`> {
|
|
160
|
+
const output = await this.options.runner(this.options.command, [
|
|
161
|
+
"gateway",
|
|
162
|
+
"balance",
|
|
163
|
+
"--address",
|
|
164
|
+
agentWalletAddress,
|
|
165
|
+
"--chain",
|
|
166
|
+
this.options.chain,
|
|
167
|
+
"--output",
|
|
168
|
+
"json",
|
|
169
|
+
]);
|
|
170
|
+
return parseCircleGatewayBackingEOA(output);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function runCircleCli(command: string, args: string[]): Promise<string> {
|
|
175
|
+
try {
|
|
176
|
+
const { stdout } = await execFileAsync(command, args, {
|
|
177
|
+
maxBuffer: 1024 * 1024,
|
|
178
|
+
});
|
|
179
|
+
return stdout;
|
|
180
|
+
} catch (error) {
|
|
181
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
182
|
+
throw new Error(
|
|
183
|
+
`Circle CLI command failed. Ensure Circle CLI is installed, logged in, and has an Agent Wallet on the selected chain. ${message}`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function parseCircleCliSignature(output: string): `0x${string}` {
|
|
189
|
+
const trimmed = output.trim();
|
|
190
|
+
if (isHexSignature(trimmed)) {
|
|
191
|
+
return trimmed;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let parsed: unknown;
|
|
195
|
+
try {
|
|
196
|
+
parsed = parseJson(trimmed);
|
|
197
|
+
} catch {
|
|
198
|
+
throw new Error("Circle CLI did not return a hex EIP-712 signature");
|
|
199
|
+
}
|
|
200
|
+
const signature = findString(parsed, ["signature", "signedData", "data.signature"]);
|
|
201
|
+
if (signature && isHexSignature(signature)) {
|
|
202
|
+
return signature;
|
|
203
|
+
}
|
|
204
|
+
throw new Error("Circle CLI did not return a hex EIP-712 signature");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function parseCircleCliWalletAddress(output: string): `0x${string}` {
|
|
208
|
+
const parsed = parseJson(output);
|
|
209
|
+
const wallets = collectWalletCandidates(parsed);
|
|
210
|
+
const addresses = wallets
|
|
211
|
+
.map((wallet) => findString(wallet, ["address", "walletAddress", "blockchainAddress"]))
|
|
212
|
+
.filter((address): address is `0x${string}` => Boolean(address && isAddress(address)));
|
|
213
|
+
|
|
214
|
+
const unique = [...new Set(addresses.map((address) => address.toLowerCase()))];
|
|
215
|
+
if (unique.length === 1) {
|
|
216
|
+
return addresses.find((address) => address.toLowerCase() === unique[0])!;
|
|
217
|
+
}
|
|
218
|
+
if (unique.length === 0) {
|
|
219
|
+
throw new Error("Circle CLI did not return an Agent Wallet address");
|
|
220
|
+
}
|
|
221
|
+
throw new Error("Multiple Circle Agent Wallets found; pass agentWalletAddress explicitly");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function parseCircleGatewayBackingEOA(output: string): `0x${string}` {
|
|
225
|
+
const parsed = parseJson(output);
|
|
226
|
+
const backingEOA = findString(parsed, ["data.backingEOA", "backingEOA"]);
|
|
227
|
+
if (backingEOA && isAddress(backingEOA)) {
|
|
228
|
+
return backingEOA;
|
|
229
|
+
}
|
|
230
|
+
throw new Error("Circle Gateway balance did not return a backingEOA address");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function collectWalletCandidates(value: unknown): Record<string, unknown>[] {
|
|
234
|
+
if (Array.isArray(value)) {
|
|
235
|
+
return value.filter(isRecord);
|
|
236
|
+
}
|
|
237
|
+
if (!isRecord(value)) {
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
for (const key of ["wallets", "items", "data"]) {
|
|
241
|
+
const nested = value[key];
|
|
242
|
+
if (Array.isArray(nested)) {
|
|
243
|
+
return nested.filter(isRecord);
|
|
244
|
+
}
|
|
245
|
+
if (isRecord(nested)) {
|
|
246
|
+
const deeper = collectWalletCandidates(nested);
|
|
247
|
+
if (deeper.length > 0) return deeper;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return [value];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function findString(value: unknown, keys: string[]): string | undefined {
|
|
254
|
+
for (const key of keys) {
|
|
255
|
+
const found = key.split(".").reduce<unknown>((current, part) => {
|
|
256
|
+
if (!isRecord(current)) return undefined;
|
|
257
|
+
return current[part];
|
|
258
|
+
}, value);
|
|
259
|
+
if (typeof found === "string") {
|
|
260
|
+
return found;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function parseJson(value: string): unknown {
|
|
267
|
+
try {
|
|
268
|
+
return JSON.parse(value);
|
|
269
|
+
} catch {
|
|
270
|
+
throw new Error("Circle CLI returned non-JSON output");
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function isHexSignature(value: string): value is `0x${string}` {
|
|
275
|
+
return /^0x[0-9a-fA-F]+$/.test(value);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function isAddress(value: string): value is `0x${string}` {
|
|
279
|
+
return /^0x[0-9a-fA-F]{40}$/.test(value);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
283
|
+
return typeof value === "object" && value !== null;
|
|
284
|
+
}
|
package/src/index.ts
CHANGED
package/src/payment-engine.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import type { StartSessionResponse, StreamPaymentRequest } from "@rubicon-caliga/core";
|
|
2
|
-
import { x402Client } from "@x402/core/client";
|
|
3
|
-
import { registerBatchScheme, type GatewayClientConfig } from "@circle-fin/x402-batching/client";
|
|
4
|
-
import { ExactEvmScheme } from "@x402/evm/exact/client";
|
|
5
|
-
import { privateKeyToAccount } from "viem/accounts";
|
|
6
2
|
|
|
7
3
|
/**
|
|
8
4
|
* Produces the payment payload for exactly one word. Called once per word by the
|
|
@@ -31,37 +27,3 @@ export class StaticPaymentEngine implements AgentPaymentEngine {
|
|
|
31
27
|
};
|
|
32
28
|
}
|
|
33
29
|
}
|
|
34
|
-
|
|
35
|
-
export type CircleGatewayPaymentEngineOptions = GatewayClientConfig;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Circle/x402 engine. Signs the gateway's one-word `paymentRequired` terms.
|
|
39
|
-
* Circle may batch settlement internally, but each signed payload corresponds to
|
|
40
|
-
* exactly one word.
|
|
41
|
-
*/
|
|
42
|
-
export class CircleGatewayPaymentEngine implements AgentPaymentEngine {
|
|
43
|
-
private readonly client = new x402Client();
|
|
44
|
-
private readonly account: ReturnType<typeof privateKeyToAccount>;
|
|
45
|
-
|
|
46
|
-
constructor(private readonly options: CircleGatewayPaymentEngineOptions) {
|
|
47
|
-
this.account = privateKeyToAccount(this.options.privateKey);
|
|
48
|
-
// Recommended buyer integration (Circle x402 buyer how-to): register the
|
|
49
|
-
// gasless batched scheme with an `exact` fallback. `registerBatchScheme`
|
|
50
|
-
// wires a CompositeEvmScheme that uses Gateway batching when the seller
|
|
51
|
-
// supports it and falls back to a standard EIP-3009 `exact` payment
|
|
52
|
-
// otherwise — no per-request routing logic needed.
|
|
53
|
-
registerBatchScheme(this.client, {
|
|
54
|
-
signer: this.account,
|
|
55
|
-
fallbackScheme: new ExactEvmScheme(this.account),
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async createWordPayment(session: StartSessionResponse): Promise<StreamPaymentRequest> {
|
|
60
|
-
if (!session.paymentRequired) {
|
|
61
|
-
throw new Error("Session did not include an x402 one-word payment requirement");
|
|
62
|
-
}
|
|
63
|
-
return {
|
|
64
|
-
paymentPayload: await this.client.createPaymentPayload(session.paymentRequired as never),
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
}
|