@rubicon-caliga/agent-sdk 0.1.1 → 0.1.3
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 +42 -25
- package/dist/agent-client.d.ts +20 -5
- package/dist/agent-client.d.ts.map +1 -1
- package/dist/agent-client.js +82 -4
- 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 +191 -0
- package/dist/agent-client.test.js.map +1 -0
- package/dist/circle-agent-wallet.d.ts +6 -6
- package/dist/circle-agent-wallet.d.ts.map +1 -1
- package/dist/circle-agent-wallet.js +8 -8
- package/dist/circle-agent-wallet.js.map +1 -1
- package/dist/circle-cli-gateway-payment.d.ts +51 -4
- package/dist/circle-cli-gateway-payment.d.ts.map +1 -1
- package/dist/circle-cli-gateway-payment.js +85 -14
- package/dist/circle-cli-gateway-payment.js.map +1 -1
- package/dist/circle-cli-gateway-payment.test.js +110 -1
- package/dist/circle-cli-gateway-payment.test.js.map +1 -1
- package/dist/payment-engine.d.ts +17 -5
- package/dist/payment-engine.d.ts.map +1 -1
- package/dist/payment-engine.js +30 -2
- package/dist/payment-engine.js.map +1 -1
- package/package.json +2 -2
- package/src/agent-client.test.ts +205 -0
- package/src/agent-client.ts +93 -4
- package/src/circle-agent-wallet.ts +10 -12
- package/src/circle-cli-gateway-payment.test.ts +129 -0
- package/src/circle-cli-gateway-payment.ts +114 -15
- package/src/payment-engine.ts +39 -5
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { RubiconClient } from "./agent-client.js";
|
|
4
|
+
import type { AgentPaymentEngine } from "./payment-engine.js";
|
|
5
|
+
|
|
6
|
+
const paymentEngine: AgentPaymentEngine = {
|
|
7
|
+
async createWordPayment() {
|
|
8
|
+
return { paymentPayload: { ok: true } };
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const chunkPaymentEngine: AgentPaymentEngine = {
|
|
13
|
+
async createWordPayment() {
|
|
14
|
+
throw new Error("word fallback should not be used");
|
|
15
|
+
},
|
|
16
|
+
async createChunkPayment(_session, input) {
|
|
17
|
+
return {
|
|
18
|
+
paymentPayload: { amountAtomic: `${input.maxWords}` },
|
|
19
|
+
maxWords: input.maxWords,
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
test("run receipt preserves Gateway settlement receipt fields", async () => {
|
|
25
|
+
const fetcher = (async (input: Parameters<typeof fetch>[0]) => {
|
|
26
|
+
const url = String(input);
|
|
27
|
+
if (url.endsWith("/v1/sessions")) {
|
|
28
|
+
return jsonResponse({
|
|
29
|
+
sessionId: "session_1",
|
|
30
|
+
state: "active",
|
|
31
|
+
article: article(),
|
|
32
|
+
navigation: navigation(),
|
|
33
|
+
pricePerWordAtomic: "1",
|
|
34
|
+
maxArticlePriceAtomic: "10",
|
|
35
|
+
conversationId: "conversation_1",
|
|
36
|
+
wordPaymentAtomic: "1",
|
|
37
|
+
gatewayFeeBps: 0,
|
|
38
|
+
paymentRequired: { scheme: "exact" },
|
|
39
|
+
expiresAt: "2026-06-18T12:00:00.000Z",
|
|
40
|
+
wordsPaid: 0,
|
|
41
|
+
wordsDelivered: 0,
|
|
42
|
+
paidAtomic: "0",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (url.endsWith("/v1/sessions/session_1/payments")) {
|
|
46
|
+
return jsonResponse({
|
|
47
|
+
accepted: true,
|
|
48
|
+
sequence: 0,
|
|
49
|
+
word: "Rubicon",
|
|
50
|
+
priceAtomic: "1",
|
|
51
|
+
wordsPaid: 1,
|
|
52
|
+
wordsDelivered: 1,
|
|
53
|
+
paidAtomic: "1",
|
|
54
|
+
completed: true,
|
|
55
|
+
transactionHashes: [],
|
|
56
|
+
settlementIds: ["settlement_1"],
|
|
57
|
+
payment: {
|
|
58
|
+
paymentId: "payment_1",
|
|
59
|
+
sessionId: "session_1",
|
|
60
|
+
articleId: "article_1",
|
|
61
|
+
sequence: 0,
|
|
62
|
+
meteringUnit: "word",
|
|
63
|
+
amountAtomic: "1",
|
|
64
|
+
currency: "USDC",
|
|
65
|
+
network: "eip155:5042002",
|
|
66
|
+
payTo: "0x3333333333333333333333333333333333333333",
|
|
67
|
+
transactionHashes: [],
|
|
68
|
+
settlementIds: ["settlement_1"],
|
|
69
|
+
buyerWalletAddress: "0x2222222222222222222222222222222222222222",
|
|
70
|
+
settledAt: "2026-06-18T12:00:00.000Z",
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
throw new Error(`Unexpected fetch: ${url}`);
|
|
75
|
+
}) as typeof fetch;
|
|
76
|
+
|
|
77
|
+
const client = new RubiconClient({
|
|
78
|
+
baseUrl: "http://rubicon.test",
|
|
79
|
+
paymentEngine,
|
|
80
|
+
fetch: fetcher,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const receipt = await client.run({
|
|
84
|
+
articleId: "article_1",
|
|
85
|
+
maxSpendAtomic: "10",
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
assert.deepEqual(receipt.transactionHashes, []);
|
|
89
|
+
assert.deepEqual(receipt.settlementIds, ["settlement_1"]);
|
|
90
|
+
assert.equal(receipt.buyerWalletAddress, "0x2222222222222222222222222222222222222222");
|
|
91
|
+
assert.equal(receipt.sellerPayTo, "0x3333333333333333333333333333333333333333");
|
|
92
|
+
assert.equal(receipt.network, "eip155:5042002");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("run can consume chunk stream while preserving per-word receipt fields", async () => {
|
|
96
|
+
const fetcher = (async (input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => {
|
|
97
|
+
const url = String(input);
|
|
98
|
+
if (url.endsWith("/v1/sessions")) {
|
|
99
|
+
return jsonResponse({
|
|
100
|
+
sessionId: "session_1",
|
|
101
|
+
state: "active",
|
|
102
|
+
article: article(),
|
|
103
|
+
navigation: navigation(),
|
|
104
|
+
pricePerWordAtomic: "1",
|
|
105
|
+
maxArticlePriceAtomic: "10",
|
|
106
|
+
conversationId: "conversation_1",
|
|
107
|
+
wordPaymentAtomic: "1",
|
|
108
|
+
gatewayFeeBps: 0,
|
|
109
|
+
paymentRequired: { accepts: [{ amount: "1" }] },
|
|
110
|
+
expiresAt: "2026-06-18T12:00:00.000Z",
|
|
111
|
+
wordsPaid: 0,
|
|
112
|
+
wordsDelivered: 0,
|
|
113
|
+
paidAtomic: "0",
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (url.endsWith("/v1/sessions/session_1/stream")) {
|
|
117
|
+
const body = JSON.parse(String(init?.body)) as { maxWords: number };
|
|
118
|
+
assert.equal(body.maxWords, 3);
|
|
119
|
+
return jsonResponse({
|
|
120
|
+
accepted: true,
|
|
121
|
+
words: [
|
|
122
|
+
{ sequence: 0, word: "Rubicon", priceAtomic: "1", payment: payment(0, "Rubicon") },
|
|
123
|
+
{ sequence: 1, word: "streams", priceAtomic: "1", payment: payment(1, "streams") },
|
|
124
|
+
{ sequence: 2, word: "chunks", priceAtomic: "1", payment: payment(2, "chunks") },
|
|
125
|
+
],
|
|
126
|
+
text: "Rubicon streams chunks",
|
|
127
|
+
wordsPaid: 3,
|
|
128
|
+
wordsDelivered: 3,
|
|
129
|
+
paidAtomic: "3",
|
|
130
|
+
completed: true,
|
|
131
|
+
settlementIds: ["settlement_chunk"],
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
throw new Error(`Unexpected fetch: ${url}`);
|
|
135
|
+
}) as typeof fetch;
|
|
136
|
+
|
|
137
|
+
const client = new RubiconClient({
|
|
138
|
+
baseUrl: "http://rubicon.test",
|
|
139
|
+
paymentEngine: chunkPaymentEngine,
|
|
140
|
+
fetch: fetcher,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const receipt = await client.run({
|
|
144
|
+
articleId: "article_1",
|
|
145
|
+
maxSpendAtomic: "10",
|
|
146
|
+
chunkWords: 3,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
assert.equal(receipt.text, "Rubicon streams chunks");
|
|
150
|
+
assert.equal(receipt.wordsRead, 3);
|
|
151
|
+
assert.equal(receipt.amountPaidAtomic, "3");
|
|
152
|
+
assert.equal(receipt.payments.length, 3);
|
|
153
|
+
assert.deepEqual(receipt.settlementIds, ["settlement_chunk"]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
function jsonResponse(body: unknown): Response {
|
|
157
|
+
return new Response(JSON.stringify(body), {
|
|
158
|
+
status: 200,
|
|
159
|
+
headers: { "content-type": "application/json" },
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function article() {
|
|
164
|
+
return {
|
|
165
|
+
articleId: "article_1",
|
|
166
|
+
creatorId: "creator_1",
|
|
167
|
+
creatorUsername: "creator",
|
|
168
|
+
title: "Title",
|
|
169
|
+
author: "Author",
|
|
170
|
+
state: "published",
|
|
171
|
+
totalWords: 1,
|
|
172
|
+
pricePerWordAtomic: "1",
|
|
173
|
+
maxArticlePriceAtomic: "1",
|
|
174
|
+
sections: [],
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function navigation() {
|
|
179
|
+
return {
|
|
180
|
+
articleId: "article_1",
|
|
181
|
+
sections: [],
|
|
182
|
+
sellerAgent: {
|
|
183
|
+
recommendedSectionId: "intro",
|
|
184
|
+
alternativeSectionIds: [],
|
|
185
|
+
rationale: "",
|
|
186
|
+
safeHints: [],
|
|
187
|
+
withheld: [],
|
|
188
|
+
},
|
|
189
|
+
stopConditions: [],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function payment(sequence: number, word: string) {
|
|
194
|
+
return {
|
|
195
|
+
paymentId: `payment_${sequence}`,
|
|
196
|
+
sessionId: "session_1",
|
|
197
|
+
articleId: "article_1",
|
|
198
|
+
sequence,
|
|
199
|
+
meteringUnit: "word",
|
|
200
|
+
amountAtomic: "1",
|
|
201
|
+
currency: "USDC",
|
|
202
|
+
settledAt: "2026-06-18T12:00:00.000Z",
|
|
203
|
+
word,
|
|
204
|
+
};
|
|
205
|
+
}
|
package/src/agent-client.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
StartConversationResponse,
|
|
8
8
|
StartSessionRequest,
|
|
9
9
|
StartSessionResponse,
|
|
10
|
+
StreamChunkResponse,
|
|
10
11
|
StreamPaymentRequest,
|
|
11
12
|
StreamPaymentResponse,
|
|
12
13
|
WordPaymentReceipt,
|
|
@@ -63,6 +64,14 @@ export type RubiconReadEvent =
|
|
|
63
64
|
payment?: WordPaymentReceipt;
|
|
64
65
|
text: string;
|
|
65
66
|
}
|
|
67
|
+
| {
|
|
68
|
+
type: "article.chunk";
|
|
69
|
+
words: Array<{ sequence: number; word: string; priceAtomic: `${bigint}`; payment?: WordPaymentReceipt }>;
|
|
70
|
+
text: string;
|
|
71
|
+
wordsRead: number;
|
|
72
|
+
amountPaidAtomic: `${bigint}`;
|
|
73
|
+
completed: boolean;
|
|
74
|
+
}
|
|
66
75
|
| { type: "article.usage"; wordsPaid: number; wordsDelivered: number; paidAtomic: `${bigint}` }
|
|
67
76
|
| { type: "article.completed"; receipt: ReadReceipt }
|
|
68
77
|
| { type: "article.error"; message: string };
|
|
@@ -76,6 +85,8 @@ export interface ReadOptions {
|
|
|
76
85
|
maxSpendAtomic?: `${bigint}`;
|
|
77
86
|
budget?: Budget;
|
|
78
87
|
maxWords?: number;
|
|
88
|
+
/** Number of words to authorize and deliver per gateway round trip when supported. */
|
|
89
|
+
chunkWords?: number;
|
|
79
90
|
/** Return true to stop reading once enough information has been collected. */
|
|
80
91
|
stopWhen?: (state: {
|
|
81
92
|
text: string;
|
|
@@ -92,8 +103,8 @@ export interface RunOptions extends ReadOptions {
|
|
|
92
103
|
|
|
93
104
|
/**
|
|
94
105
|
* High-level buyer-agent client for Rubicon. `read()` runs the entire
|
|
95
|
-
*
|
|
96
|
-
*
|
|
106
|
+
* authorize -> word -> usage loop until a stop condition is met, so application
|
|
107
|
+
* developers never drive a payment flow for every word themselves.
|
|
97
108
|
*/
|
|
98
109
|
export class RubiconClient {
|
|
99
110
|
private readonly fetcher: typeof fetch;
|
|
@@ -170,6 +181,18 @@ export class RubiconClient {
|
|
|
170
181
|
return response.json() as Promise<StreamPaymentResponse>;
|
|
171
182
|
}
|
|
172
183
|
|
|
184
|
+
async streamChunk(sessionId: string, payment: StreamPaymentRequest): Promise<StreamChunkResponse> {
|
|
185
|
+
const response = await this.fetcher(`${this.baseUrl}/v1/sessions/${sessionId}/stream`, {
|
|
186
|
+
method: "POST",
|
|
187
|
+
headers: this.headers({ "content-type": "application/json" }),
|
|
188
|
+
body: JSON.stringify(payment),
|
|
189
|
+
});
|
|
190
|
+
if (!response.ok) {
|
|
191
|
+
throw new Error(`Chunk stream rejected: ${response.status} ${await response.text()}`);
|
|
192
|
+
}
|
|
193
|
+
return response.json() as Promise<StreamChunkResponse>;
|
|
194
|
+
}
|
|
195
|
+
|
|
173
196
|
async abort(sessionId: string, reason = "agent_cancelled"): Promise<void> {
|
|
174
197
|
await this.fetcher(`${this.baseUrl}/v1/sessions/${sessionId}/abort`, {
|
|
175
198
|
method: "POST",
|
|
@@ -222,8 +245,8 @@ export class RubiconClient {
|
|
|
222
245
|
}
|
|
223
246
|
|
|
224
247
|
/**
|
|
225
|
-
* Read an article
|
|
226
|
-
* running usage, and a final completion event carrying the receipt.
|
|
248
|
+
* Read an article with word-level metering. Yields seller messages, paid
|
|
249
|
+
* words, running usage, and a final completion event carrying the receipt.
|
|
227
250
|
*/
|
|
228
251
|
async *read(options: ReadOptions): AsyncGenerator<RubiconReadEvent, ReadReceipt> {
|
|
229
252
|
const budget: Budget =
|
|
@@ -274,6 +297,8 @@ export class RubiconClient {
|
|
|
274
297
|
const transactionHashes: string[] = [];
|
|
275
298
|
const settlementIds: string[] = [];
|
|
276
299
|
const payments: WordPaymentReceipt[] = [];
|
|
300
|
+
const chunkWords = normalizeChunkWords(options.chunkWords);
|
|
301
|
+
const useChunks = chunkWords > 1 && typeof this.paymentEngine.createChunkPayment === "function";
|
|
277
302
|
let stopReason: ReadReceipt["stopReason"] = "article_completed";
|
|
278
303
|
let completed = false;
|
|
279
304
|
|
|
@@ -308,6 +333,64 @@ export class RubiconClient {
|
|
|
308
333
|
break;
|
|
309
334
|
}
|
|
310
335
|
|
|
336
|
+
if (useChunks) {
|
|
337
|
+
const remainingWords = options.maxWords === undefined ? chunkWords : Math.max(0, options.maxWords - wordsRead);
|
|
338
|
+
const affordableWords = Number((budgetAtomic - amountPaid) / wordPaymentAtomic);
|
|
339
|
+
const maxWords = Math.max(1, Math.min(chunkWords, remainingWords || chunkWords, affordableWords));
|
|
340
|
+
const payment = await this.paymentEngine.createChunkPayment!(session, {
|
|
341
|
+
nextSequence: wordsRead,
|
|
342
|
+
maxWords,
|
|
343
|
+
});
|
|
344
|
+
const idempotencyKey = `${session.sessionId}:${wordsRead}:${maxWords}`;
|
|
345
|
+
let result: StreamChunkResponse;
|
|
346
|
+
try {
|
|
347
|
+
result = await this.streamChunk(session.sessionId, { ...payment, idempotencyKey, maxWords });
|
|
348
|
+
} catch (error) {
|
|
349
|
+
yield { type: "article.error", message: error instanceof Error ? error.message : String(error) };
|
|
350
|
+
stopReason = "aborted";
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
if (result.words.length === 0 && result.completed) {
|
|
354
|
+
completed = true;
|
|
355
|
+
stopReason = "article_completed";
|
|
356
|
+
const receipt = makeReceipt();
|
|
357
|
+
yield { type: "article.completed", receipt };
|
|
358
|
+
return receipt;
|
|
359
|
+
}
|
|
360
|
+
for (const entry of result.words) {
|
|
361
|
+
if (entry.payment) {
|
|
362
|
+
payments.push(entry.payment);
|
|
363
|
+
}
|
|
364
|
+
text = text ? `${text} ${entry.word}` : entry.word;
|
|
365
|
+
}
|
|
366
|
+
wordsRead = result.wordsDelivered;
|
|
367
|
+
amountPaid = BigInt(result.paidAtomic);
|
|
368
|
+
transactionHashes.push(...(result.transactionHashes ?? (result.transactionHash ? [result.transactionHash] : [])));
|
|
369
|
+
settlementIds.push(...(result.settlementIds ?? (result.settlementId ? [result.settlementId] : [])));
|
|
370
|
+
yield {
|
|
371
|
+
type: "article.chunk",
|
|
372
|
+
words: result.words,
|
|
373
|
+
text,
|
|
374
|
+
wordsRead,
|
|
375
|
+
amountPaidAtomic: `${amountPaid}`,
|
|
376
|
+
completed: result.completed,
|
|
377
|
+
};
|
|
378
|
+
yield {
|
|
379
|
+
type: "article.usage",
|
|
380
|
+
wordsPaid: result.wordsPaid,
|
|
381
|
+
wordsDelivered: result.wordsDelivered,
|
|
382
|
+
paidAtomic: result.paidAtomic,
|
|
383
|
+
};
|
|
384
|
+
if (result.completed) {
|
|
385
|
+
completed = true;
|
|
386
|
+
stopReason = "article_completed";
|
|
387
|
+
const receipt = makeReceipt();
|
|
388
|
+
yield { type: "article.completed", receipt };
|
|
389
|
+
return receipt;
|
|
390
|
+
}
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
311
394
|
const payment = await this.paymentEngine.createWordPayment(session);
|
|
312
395
|
// Idempotency key ties this payment to the specific next word; safe retries.
|
|
313
396
|
const idempotencyKey = `${session.sessionId}:${wordsRead}`;
|
|
@@ -391,3 +474,9 @@ export class RubiconClient {
|
|
|
391
474
|
|
|
392
475
|
/** Backwards-compatible alias. */
|
|
393
476
|
export const AgentClient = RubiconClient;
|
|
477
|
+
|
|
478
|
+
function normalizeChunkWords(chunkWords: number | undefined): number {
|
|
479
|
+
if (chunkWords === undefined) return 1;
|
|
480
|
+
if (!Number.isInteger(chunkWords) || chunkWords < 1) return 1;
|
|
481
|
+
return Math.min(chunkWords, 256);
|
|
482
|
+
}
|
|
@@ -2,10 +2,8 @@ import type { StartSessionResponse, StreamPaymentRequest } from "@rubicon-caliga
|
|
|
2
2
|
import { x402Client } from "@x402/core/client";
|
|
3
3
|
import { registerBatchScheme } from "@circle-fin/x402-batching/client";
|
|
4
4
|
import { ExactEvmScheme } from "@x402/evm/exact/client";
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
type CircleDeveloperControlledWalletsClient,
|
|
8
|
-
} from "@circle-fin/developer-controlled-wallets";
|
|
5
|
+
import * as CircleWallets from "@circle-fin/developer-controlled-wallets";
|
|
6
|
+
import type { CircleDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets";
|
|
9
7
|
import type { AgentPaymentEngine } from "./payment-engine.js";
|
|
10
8
|
|
|
11
9
|
export interface CircleAgentWalletEngineOptions {
|
|
@@ -13,7 +11,7 @@ export interface CircleAgentWalletEngineOptions {
|
|
|
13
11
|
apiKey: string;
|
|
14
12
|
/** Entity secret registered for the Circle developer account. */
|
|
15
13
|
entitySecret: string;
|
|
16
|
-
/** The Agent Wallet that holds USDC and signs
|
|
14
|
+
/** The Agent Wallet that holds USDC and signs Circle / Arc authorizations. */
|
|
17
15
|
walletId: string;
|
|
18
16
|
/**
|
|
19
17
|
* The wallet's on-chain address. Optional — when omitted it is resolved once
|
|
@@ -80,7 +78,7 @@ class CircleAgentWalletSigner {
|
|
|
80
78
|
const res = await this.client.signTypedData({
|
|
81
79
|
walletId: this.walletId,
|
|
82
80
|
data: serializeTypedData(toEip712Payload(typed)),
|
|
83
|
-
memo: "Rubicon
|
|
81
|
+
memo: "Rubicon reading authorization",
|
|
84
82
|
});
|
|
85
83
|
const signature = res.data?.signature;
|
|
86
84
|
if (!signature) {
|
|
@@ -91,10 +89,10 @@ class CircleAgentWalletSigner {
|
|
|
91
89
|
}
|
|
92
90
|
|
|
93
91
|
/**
|
|
94
|
-
* Circle Agent Wallet engine. Signs
|
|
95
|
-
* custodial Circle Agent Wallet
|
|
96
|
-
*
|
|
97
|
-
*
|
|
92
|
+
* Circle Agent Wallet engine. Signs gateway authorization terms with a
|
|
93
|
+
* custodial Circle Agent Wallet so the buyer agent never handles a local
|
|
94
|
+
* signing key. Current gateways may still call the legacy one-word method as a
|
|
95
|
+
* chunk-compatibility path.
|
|
98
96
|
*/
|
|
99
97
|
export class CircleAgentWalletEngine implements AgentPaymentEngine {
|
|
100
98
|
private readonly x402 = new x402Client();
|
|
@@ -103,7 +101,7 @@ export class CircleAgentWalletEngine implements AgentPaymentEngine {
|
|
|
103
101
|
constructor(options: CircleAgentWalletEngineOptions) {
|
|
104
102
|
const client =
|
|
105
103
|
options.client ??
|
|
106
|
-
initiateDeveloperControlledWalletsClient({
|
|
104
|
+
CircleWallets.initiateDeveloperControlledWalletsClient({
|
|
107
105
|
apiKey: options.apiKey,
|
|
108
106
|
entitySecret: options.entitySecret,
|
|
109
107
|
...(options.baseUrl ? { baseUrl: options.baseUrl } : {}),
|
|
@@ -119,7 +117,7 @@ export class CircleAgentWalletEngine implements AgentPaymentEngine {
|
|
|
119
117
|
|
|
120
118
|
async createWordPayment(session: StartSessionResponse): Promise<StreamPaymentRequest> {
|
|
121
119
|
if (!session.paymentRequired) {
|
|
122
|
-
throw new Error("Session did not include
|
|
120
|
+
throw new Error("Session did not include a legacy x402 payment requirement");
|
|
123
121
|
}
|
|
124
122
|
// Resolve the wallet address up front so the synchronous `address` read
|
|
125
123
|
// inside createPaymentPayload sees a real value.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { test } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import {
|
|
4
|
+
CircleCliGatewaySigner,
|
|
5
|
+
parseCircleGatewayBackingEOA,
|
|
4
6
|
parseCircleCliSignature,
|
|
5
7
|
parseCircleCliWalletAddress,
|
|
6
8
|
} from "./circle-cli-gateway-payment.js";
|
|
@@ -37,6 +39,133 @@ test("parses a sole wallet address from Circle CLI list output", () => {
|
|
|
37
39
|
);
|
|
38
40
|
});
|
|
39
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
|
+
|
|
40
169
|
test("requires explicit wallet address when multiple Agent Wallets are present", () => {
|
|
41
170
|
assert.throws(
|
|
42
171
|
() =>
|