@selat-ai/router-client 0.1.0

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 ADDED
@@ -0,0 +1,126 @@
1
+ # @selat-ai/router-client
2
+
3
+ TypeScript SDK for selat-router that offers a fetch-like API to pay any endpoints simply with Circle Nanopayments.
4
+
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npm install @selat-ai/router-client
10
+ ```
11
+ ## Quick Start
12
+
13
+ Supported signers include Private Keys, the Circle Agent Wallet, and custom Remote Signers. The recommended approach is to use the Circle Agent Wallet, ensuring that sensitive private keys remain entirely outside the SDK environment.
14
+
15
+ ### Starting with Private Key
16
+
17
+ ```ts
18
+ import { RouterClient, createViemSigner } from "@selat-ai/router-client";
19
+
20
+ const signer = createViemSigner(process.env.X402_CLIENT_PRIVATE_KEY as `0x${string}`);
21
+
22
+ const client = new RouterClient({
23
+ chain: "base",
24
+ signer
25
+ });
26
+
27
+ const response = await client.fetch("https://upstream.example.com/v1/data", {
28
+ method: "GET"
29
+ });
30
+ ```
31
+
32
+ ### Using Circle Agent Wallet (RECOMMENDED)
33
+
34
+ Before using this path, install and set up your Circle Agent Wallet here:
35
+
36
+ https://developers.circle.com/agent-stack/agent-wallets/quickstart
37
+
38
+ You should already have:
39
+ 1. Circle CLI installed and authenticated
40
+ 2. A funded agent wallet on the target chain
41
+
42
+ ```ts
43
+ import { RouterClient, createCircleAgentWalletSigner } from "@selat-ai/router-client";
44
+
45
+ const signer = createCircleAgentWalletSigner({
46
+ address: process.env.SELAT_SIGNER_ADDRESS as `0x${string}`,
47
+ chain: "base"
48
+ });
49
+
50
+ const client = new RouterClient({
51
+ chain: "base",
52
+ signer
53
+ });
54
+
55
+ const response = await client.fetch(
56
+ "https://pro-api.coinmarketcap.com/x402/v3/cryptocurrency/quotes/latest?symbol=eth",
57
+ { method: "GET" }
58
+ );
59
+ ```
60
+
61
+
62
+ ### Custom Signer
63
+
64
+ Use this when signing happens outside the SDK, for example in a wallet service, HSM, or KMS.
65
+
66
+ ```ts
67
+ import { RouterClient, createRemoteSigner } from "@selat-ai/router-client";
68
+
69
+ const signer = createRemoteSigner(
70
+ "0x1111111111111111111111111111111111111111",
71
+ async ({ address, typedData }) => {
72
+ // Delegate to your wallet/HSM/KMS signer service.
73
+ const response = await fetch("https://signer.example.com/sign-typed-data", {
74
+ method: "POST",
75
+ headers: { "content-type": "application/json" },
76
+ body: JSON.stringify({ address, typedData })
77
+ });
78
+ const data = await response.json() as { signature: `0x${string}` };
79
+ return data.signature;
80
+ }
81
+ );
82
+
83
+ const client = new RouterClient({
84
+ chain: "base",
85
+ signer
86
+ });
87
+ ```
88
+
89
+ ## Run the Example
90
+
91
+ You can run the included example script to call a real upstream endpoint through router.
92
+
93
+ 1. Copy `.env.example` to `.env` and fill values.
94
+ 2. Run:
95
+
96
+ With Private key:
97
+
98
+ ```bash
99
+ export SELAT_CHAIN=base
100
+ export SELAT_TARGET_URL="https://pro-api.coinmarketcap.com/x402/v3/cryptocurrency/quotes/latest?symbol=eth"
101
+ export X402_CLIENT_PRIVATE_KEY=0x<your-private-key>
102
+ pnpm run example
103
+ ```
104
+
105
+ Using Remote signer:
106
+
107
+ ```bash
108
+ export SELAT_SIGNER_MODE=remote
109
+ export SELAT_CHAIN=base
110
+ export SELAT_TARGET_URL="https://pro-api.coinmarketcap.com/x402/v3/cryptocurrency/quotes/latest?symbol=eth"
111
+ export SELAT_SIGNER_ADDRESS=0x<your-signer-address>
112
+ export SELAT_SIGNER_API_URL="https://signer.example.com/sign-typed-data"
113
+ pnpm run example
114
+ ```
115
+
116
+ Using Circle Agent Wallet:
117
+
118
+ ```bash
119
+ export SELAT_SIGNER_MODE=circle-agent-wallet
120
+ export SELAT_CHAIN=base
121
+ export SELAT_TARGET_URL="https://pro-api.coinmarketcap.com/x402/v3/cryptocurrency/quotes/latest?symbol=eth"
122
+ export SELAT_SIGNER_ADDRESS=0x<your-agent-wallet-address>
123
+ pnpm run example
124
+ ```
125
+
126
+
package/dist/index.cjs ADDED
@@ -0,0 +1,589 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ CircleNanopaymentPayloadBuilder: () => CircleNanopaymentPayloadBuilder,
24
+ QuoteParseError: () => QuoteParseError,
25
+ RouterClient: () => RouterClient,
26
+ RouterClientConfigError: () => RouterClientConfigError,
27
+ RouterSdkError: () => RouterSdkError,
28
+ createCircleAgentWalletSigner: () => createCircleAgentWalletSigner,
29
+ createRemoteSigner: () => createRemoteSigner,
30
+ createViemSigner: () => createViemSigner
31
+ });
32
+ module.exports = __toCommonJS(index_exports);
33
+
34
+ // src/errors/index.ts
35
+ var RouterSdkError = class extends Error {
36
+ constructor(message) {
37
+ super(message);
38
+ this.name = "RouterSdkError";
39
+ }
40
+ };
41
+ var QuoteParseError = class extends RouterSdkError {
42
+ constructor(message) {
43
+ super(message);
44
+ this.name = "QuoteParseError";
45
+ }
46
+ };
47
+ var RouterClientConfigError = class extends RouterSdkError {
48
+ constructor(message) {
49
+ super(message);
50
+ this.name = "RouterClientConfigError";
51
+ }
52
+ };
53
+
54
+ // src/protocols/circleNanopaymentPayloadBuilder.ts
55
+ var import_client = require("@circle-fin/x402-batching/client");
56
+ function selectGatewayWalletBatchedOption(quote, chain) {
57
+ const expectedNetwork = `eip155:${import_client.CHAIN_CONFIGS[chain].chain.id}`;
58
+ const selected = quote.accepts.find((accept) => {
59
+ const extraName = typeof accept.extra?.name === "string" ? accept.extra.name : void 0;
60
+ return accept.scheme === "exact" && accept.network === expectedNetwork && extraName === "GatewayWalletBatched";
61
+ });
62
+ if (!selected) {
63
+ throw new QuoteParseError(
64
+ `no GatewayWalletBatched accept for ${expectedNetwork}; available networks: ${quote.accepts.map((item) => item.network).join(",")}`
65
+ );
66
+ }
67
+ return selected;
68
+ }
69
+ var CircleNanopaymentPayloadBuilder = class {
70
+ constructor(options) {
71
+ this.options = options;
72
+ }
73
+ options;
74
+ async build(challenge) {
75
+ const selected = selectGatewayWalletBatchedOption(challenge, this.options.chain);
76
+ if (typeof this.options.signer.circleAgentWalletProcessor === "function") {
77
+ const verifyingContract = typeof selected.extra?.verifyingContract === "string" ? selected.extra.verifyingContract : void 0;
78
+ const version = typeof selected.extra?.version === "string" ? selected.extra.version : void 0;
79
+ await this.options.signer.circleAgentWalletProcessor({
80
+ chainId: import_client.CHAIN_CONFIGS[this.options.chain].chain.id,
81
+ ...verifyingContract ? { verifyingContract } : {},
82
+ ...version ? { version } : {}
83
+ });
84
+ }
85
+ const batchScheme = new import_client.BatchEvmScheme({
86
+ address: this.options.signer.address,
87
+ signTypedData: async (params) => this.options.signer.signTypedData(params)
88
+ });
89
+ const paymentPayload = await batchScheme.createPaymentPayload(challenge.x402Version, {
90
+ scheme: selected.scheme,
91
+ network: selected.network,
92
+ asset: selected.asset,
93
+ amount: selected.amount,
94
+ payTo: selected.payTo,
95
+ maxTimeoutSeconds: selected.maxTimeoutSeconds,
96
+ ...selected.extra ? { extra: selected.extra } : {}
97
+ });
98
+ const paymentSignature = Buffer.from(
99
+ JSON.stringify({
100
+ ...paymentPayload,
101
+ ...challenge.resource ? { resource: challenge.resource } : {},
102
+ accepted: selected
103
+ })
104
+ ).toString("base64");
105
+ return { paymentSignature };
106
+ }
107
+ };
108
+
109
+ // src/quote/QuoteParser.ts
110
+ var ROUTER_QUOTE_TTL_SECONDS = 60;
111
+ var QUOTE_TTL_SAFETY_SKEW_SECONDS = 5;
112
+ var LOCAL_QUOTE_TTL_MS = (ROUTER_QUOTE_TTL_SECONDS - QUOTE_TTL_SAFETY_SKEW_SECONDS) * 1e3;
113
+ function parsePaymentRequiredHeader(headerValue) {
114
+ let decoded;
115
+ try {
116
+ decoded = Buffer.from(headerValue, "base64").toString("utf8");
117
+ } catch {
118
+ throw new QuoteParseError("failed to base64 decode payment-required header");
119
+ }
120
+ let payload;
121
+ try {
122
+ payload = JSON.parse(decoded);
123
+ } catch {
124
+ throw new QuoteParseError("failed to parse payment-required header as JSON");
125
+ }
126
+ const rawVersion = payload.x402Version;
127
+ const x402Version = typeof rawVersion === "number" && Number.isFinite(rawVersion) ? rawVersion : 2;
128
+ const resource = typeof payload.resource === "object" && payload.resource !== null ? payload.resource : void 0;
129
+ const rawAccepts = Array.isArray(payload.accepts) ? payload.accepts : [];
130
+ const accepts = rawAccepts.map((entry) => {
131
+ if (typeof entry !== "object" || entry === null) {
132
+ return void 0;
133
+ }
134
+ const record = entry;
135
+ const scheme = typeof record.scheme === "string" ? record.scheme : void 0;
136
+ const network = typeof record.network === "string" ? record.network : void 0;
137
+ const asset = typeof record.asset === "string" ? record.asset : void 0;
138
+ const amount = typeof record.amount === "string" ? record.amount : void 0;
139
+ const payTo = typeof record.payTo === "string" ? record.payTo : void 0;
140
+ const maxTimeoutSeconds = typeof record.maxTimeoutSeconds === "number" ? record.maxTimeoutSeconds : void 0;
141
+ const extra = typeof record.extra === "object" && record.extra !== null ? record.extra : void 0;
142
+ if (!scheme || !network || !asset || !amount || !payTo || maxTimeoutSeconds === void 0) {
143
+ return void 0;
144
+ }
145
+ return {
146
+ scheme,
147
+ network,
148
+ asset,
149
+ amount,
150
+ payTo,
151
+ maxTimeoutSeconds,
152
+ ...extra ? { extra } : {}
153
+ };
154
+ }).filter((item) => item !== void 0);
155
+ if (accepts.length === 0) {
156
+ throw new QuoteParseError("payment-required header has no valid accepts");
157
+ }
158
+ return resource ? {
159
+ x402Version,
160
+ resource,
161
+ accepts
162
+ } : {
163
+ x402Version,
164
+ accepts
165
+ };
166
+ }
167
+ var QuoteParser = class {
168
+ async parse(response) {
169
+ if (response.status !== 402) {
170
+ return null;
171
+ }
172
+ const quoteId = response.headers.get("x-selat-quote-id");
173
+ if (!quoteId) {
174
+ throw new QuoteParseError("missing x-selat-quote-id in 402 response");
175
+ }
176
+ const paymentRequiredHeader = response.headers.get("payment-required");
177
+ if (!paymentRequiredHeader) {
178
+ throw new QuoteParseError("missing payment-required header in 402 response");
179
+ }
180
+ const parsed = parsePaymentRequiredHeader(paymentRequiredHeader);
181
+ const expiresAt = Date.now() + LOCAL_QUOTE_TTL_MS;
182
+ return {
183
+ quoteId,
184
+ expiresAt,
185
+ x402Version: parsed.x402Version,
186
+ ...parsed.resource ? { resource: parsed.resource } : {},
187
+ accepts: parsed.accepts
188
+ };
189
+ }
190
+ };
191
+
192
+ // src/signers/createViemSigner.ts
193
+ var import_accounts = require("viem/accounts");
194
+ function createViemSigner(privateKey) {
195
+ const account = (0, import_accounts.privateKeyToAccount)(privateKey);
196
+ return {
197
+ address: account.address,
198
+ signTypedData: async (params) => account.signTypedData(params)
199
+ };
200
+ }
201
+
202
+ // src/client/RouterClient.ts
203
+ var DEFAULT_ROUTER_URL = "https://router.selat.ai";
204
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
205
+ function toHeaders(base, override) {
206
+ const headers = new Headers(base);
207
+ if (override) {
208
+ new Headers(override).forEach((value, key) => headers.set(key, value));
209
+ }
210
+ return headers;
211
+ }
212
+ function toRouterProxyUrl(routerUrl, targetUrl) {
213
+ const proxyUrl = new URL("/proxy", routerUrl);
214
+ proxyUrl.searchParams.set("target", targetUrl);
215
+ return proxyUrl.toString();
216
+ }
217
+ function withSignal(init, signal) {
218
+ if (!signal) {
219
+ return init;
220
+ }
221
+ return {
222
+ ...init,
223
+ signal
224
+ };
225
+ }
226
+ function createRequestSignal(callerSignal, requestTimeoutMs) {
227
+ const timeoutController = new AbortController();
228
+ const timeoutId = setTimeout(() => timeoutController.abort(), requestTimeoutMs);
229
+ const timeoutSignal = timeoutController.signal;
230
+ if (!callerSignal) {
231
+ return {
232
+ signal: timeoutSignal,
233
+ cleanup: () => clearTimeout(timeoutId)
234
+ };
235
+ }
236
+ const controller = new AbortController();
237
+ const abort = () => controller.abort();
238
+ const cleanup = () => {
239
+ clearTimeout(timeoutId);
240
+ callerSignal.removeEventListener("abort", abort);
241
+ timeoutSignal.removeEventListener("abort", abort);
242
+ };
243
+ if (callerSignal.aborted || timeoutSignal.aborted) {
244
+ cleanup();
245
+ controller.abort();
246
+ return {
247
+ signal: controller.signal,
248
+ cleanup
249
+ };
250
+ }
251
+ callerSignal.addEventListener("abort", abort);
252
+ timeoutSignal.addEventListener("abort", abort);
253
+ return {
254
+ signal: controller.signal,
255
+ cleanup
256
+ };
257
+ }
258
+ var RouterClient = class {
259
+ constructor(options) {
260
+ this.options = options;
261
+ this.routerUrl = options.routerUrl ?? DEFAULT_ROUTER_URL;
262
+ this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
263
+ if (!options.chain) {
264
+ throw new RouterClientConfigError(
265
+ "chain is required"
266
+ );
267
+ }
268
+ const signer = options.signer ?? (() => {
269
+ if (!options.privateKey) {
270
+ return void 0;
271
+ }
272
+ return createViemSigner(options.privateKey);
273
+ })();
274
+ if (!signer) {
275
+ throw new RouterClientConfigError(
276
+ "Provide either signer, or privateKey (with chain) for Circle nanopayments"
277
+ );
278
+ }
279
+ this.paymentPayloadBuilder = new CircleNanopaymentPayloadBuilder({
280
+ chain: options.chain,
281
+ signer
282
+ });
283
+ }
284
+ options;
285
+ quoteParser = new QuoteParser();
286
+ routerUrl;
287
+ requestTimeoutMs;
288
+ paymentPayloadBuilder;
289
+ createFetch(config) {
290
+ return (path, init) => this.fetch(new URL(path, config.baseUrl).toString(), init);
291
+ }
292
+ async fetch(input, init) {
293
+ const { preferProtocol, ...requestInit } = init ?? {};
294
+ const proxyUrl = toRouterProxyUrl(this.routerUrl, input);
295
+ const firstHeaders = toHeaders(this.options.defaultHeaders ?? {}, requestInit.headers);
296
+ firstHeaders.set("x-selat-prefer-protocol", preferProtocol ?? "mpp");
297
+ const firstRequest = createRequestSignal(requestInit.signal ?? void 0, this.requestTimeoutMs);
298
+ let firstResponse;
299
+ try {
300
+ firstResponse = await fetch(proxyUrl, withSignal({
301
+ ...requestInit,
302
+ headers: firstHeaders
303
+ }, firstRequest.signal));
304
+ } finally {
305
+ firstRequest.cleanup();
306
+ }
307
+ const challenge = await this.quoteParser.parse(firstResponse.clone());
308
+ if (!challenge) {
309
+ return firstResponse;
310
+ }
311
+ const payment = await this.paymentPayloadBuilder.build(challenge);
312
+ const paidHeaders = firstHeaders;
313
+ paidHeaders.set("PAYMENT-SIGNATURE", payment.paymentSignature);
314
+ paidHeaders.set("x-selat-quote-id", challenge.quoteId);
315
+ const paidRequest = createRequestSignal(requestInit.signal ?? void 0, this.requestTimeoutMs);
316
+ try {
317
+ return await fetch(proxyUrl, withSignal({
318
+ ...requestInit,
319
+ headers: paidHeaders
320
+ }, paidRequest.signal));
321
+ } finally {
322
+ paidRequest.cleanup();
323
+ }
324
+ }
325
+ };
326
+
327
+ // src/signers/createRemoteSigner.ts
328
+ function createRemoteSigner(address, requester) {
329
+ return {
330
+ address,
331
+ signTypedData: async (params) => requester({ address, typedData: params })
332
+ };
333
+ }
334
+
335
+ // src/signers/createCircleAgentWalletSigner.ts
336
+ var import_node_child_process = require("child_process");
337
+ var import_viem = require("viem");
338
+ var DEFAULT_CIRCLE_CLI_COMMAND = "circle";
339
+ var DEFAULT_CIRCLE_CLI_TIMEOUT_MS = 3e4;
340
+ function toCliChain(chain) {
341
+ switch (chain) {
342
+ case "base":
343
+ return "BASE";
344
+ case "ethereum":
345
+ return "ETHEREUM";
346
+ case "avalanche":
347
+ return "AVALANCHE";
348
+ case "optimism":
349
+ return "OPTIMISM";
350
+ case "polygon":
351
+ return "POLYGON";
352
+ default:
353
+ return chain.toUpperCase();
354
+ }
355
+ }
356
+ function stringifySafeJson(value) {
357
+ return JSON.stringify(value, (_key, item) => {
358
+ if (typeof item !== "bigint") {
359
+ return item;
360
+ }
361
+ if (item <= BigInt(Number.MAX_SAFE_INTEGER) && item >= BigInt(Number.MIN_SAFE_INTEGER)) {
362
+ return Number(item);
363
+ }
364
+ return item.toString();
365
+ });
366
+ }
367
+ function ensureEip712DomainTypeForCircleCli(typedData) {
368
+ if (!typedData || typeof typedData !== "object") {
369
+ return typedData;
370
+ }
371
+ const root = typedData;
372
+ const domain = root.domain;
373
+ const types = root.types;
374
+ if (!domain || typeof domain !== "object" || !types || typeof types !== "object") {
375
+ return typedData;
376
+ }
377
+ const typesRecord = types;
378
+ if (Array.isArray(typesRecord.EIP712Domain)) {
379
+ return typedData;
380
+ }
381
+ const domainRecord = domain;
382
+ const eip712Domain = [];
383
+ if (typeof domainRecord.name === "string") {
384
+ eip712Domain.push({ name: "name", type: "string" });
385
+ }
386
+ if (typeof domainRecord.version === "string") {
387
+ eip712Domain.push({ name: "version", type: "string" });
388
+ }
389
+ if (typeof domainRecord.chainId === "number" || typeof domainRecord.chainId === "bigint" || typeof domainRecord.chainId === "string") {
390
+ eip712Domain.push({ name: "chainId", type: "uint256" });
391
+ }
392
+ if (typeof domainRecord.verifyingContract === "string") {
393
+ eip712Domain.push({ name: "verifyingContract", type: "address" });
394
+ }
395
+ if (typeof domainRecord.salt === "string") {
396
+ eip712Domain.push({ name: "salt", type: "bytes32" });
397
+ }
398
+ return {
399
+ ...root,
400
+ types: {
401
+ EIP712Domain: eip712Domain,
402
+ ...typesRecord
403
+ }
404
+ };
405
+ }
406
+ function normalizeAddress(address) {
407
+ return address.toLowerCase();
408
+ }
409
+ function extractHexSignature(text) {
410
+ const matches = text.match(/0x[0-9a-fA-F]{130}/g);
411
+ if (!matches || matches.length === 0) {
412
+ return null;
413
+ }
414
+ return matches[matches.length - 1];
415
+ }
416
+ async function ensureSignatureMatchesExpectedAddress(typedData, signature, fallbackExpectedAddress) {
417
+ try {
418
+ const typedDataRecord = typedData;
419
+ const message = typedDataRecord.message;
420
+ const expectedAddress = typeof message?.from === "string" ? normalizeAddress(message.from) : fallbackExpectedAddress;
421
+ const recoveredAddress = await (0, import_viem.recoverTypedDataAddress)({
422
+ ...typedDataRecord,
423
+ signature
424
+ });
425
+ if (normalizeAddress(recoveredAddress) !== normalizeAddress(expectedAddress)) {
426
+ throw new Error(
427
+ `Circle CLI produced a signature for ${recoveredAddress}, but expected ${expectedAddress}. This commonly happens when an outdated Circle CLI is used or when the selected wallet signs as a different key model. Upgrade @circle-fin/cli and verify the wallet address with \`circle wallet list --chain <CHAIN>\` before running the SDK.`
428
+ );
429
+ }
430
+ } catch (error) {
431
+ if (error instanceof Error && error.message.startsWith("Circle CLI produced a signature")) {
432
+ throw error;
433
+ }
434
+ }
435
+ }
436
+ async function signTypedDataWithCircleCli(command, timeoutMs, address, chain, typedData, options) {
437
+ const normalizedTypedData = ensureEip712DomainTypeForCircleCli(typedData);
438
+ const typedDataJson = stringifySafeJson(normalizedTypedData);
439
+ const args = [
440
+ "wallet",
441
+ "sign",
442
+ "typed-data",
443
+ typedDataJson,
444
+ "--address",
445
+ address,
446
+ "--chain",
447
+ toCliChain(chain)
448
+ ];
449
+ return new Promise((resolve, reject) => {
450
+ const child = (0, import_node_child_process.spawn)(command, args, {
451
+ shell: false,
452
+ windowsHide: true
453
+ });
454
+ let stdout = "";
455
+ let stderr = "";
456
+ const timeout = setTimeout(() => {
457
+ child.kill();
458
+ reject(new Error(`Circle CLI signing timed out after ${timeoutMs}ms`));
459
+ }, timeoutMs);
460
+ child.stdout.on("data", (chunk) => {
461
+ stdout += chunk.toString();
462
+ });
463
+ child.stderr.on("data", (chunk) => {
464
+ stderr += chunk.toString();
465
+ });
466
+ child.on("error", (error) => {
467
+ clearTimeout(timeout);
468
+ const message = error instanceof Error ? error.message : String(error);
469
+ reject(
470
+ new Error(
471
+ `Failed to execute Circle CLI ("${command}"): ${message}. Ensure @circle-fin/cli is installed and authenticated.`
472
+ )
473
+ );
474
+ });
475
+ child.on("close", (code) => {
476
+ clearTimeout(timeout);
477
+ if (code !== 0) {
478
+ const details = stderr.trim() || stdout.trim() || `exit code ${code}`;
479
+ reject(new Error(`Circle CLI signing failed: ${details}`));
480
+ return;
481
+ }
482
+ const signature = extractHexSignature(stdout);
483
+ if (!signature) {
484
+ reject(new Error(`Circle CLI output did not contain a valid signature: ${stdout.trim()}`));
485
+ return;
486
+ }
487
+ if (options?.skipSignatureAddressCheck) {
488
+ resolve(signature);
489
+ return;
490
+ }
491
+ ensureSignatureMatchesExpectedAddress(normalizedTypedData, signature, address).then(() => resolve(signature)).catch((error) => reject(error instanceof Error ? error : new Error(String(error))));
492
+ });
493
+ });
494
+ }
495
+ var GATEWAY_AUTH_TYPES = {
496
+ TransferWithAuthorization: [
497
+ { name: "from", type: "address" },
498
+ { name: "to", type: "address" },
499
+ { name: "value", type: "uint256" },
500
+ { name: "validAfter", type: "uint256" },
501
+ { name: "validBefore", type: "uint256" },
502
+ { name: "nonce", type: "bytes32" }
503
+ ]
504
+ };
505
+ async function resolveGatewaySignerAddress(command, timeoutMs, walletAddress, chain, input) {
506
+ if (!input.verifyingContract) {
507
+ return walletAddress;
508
+ }
509
+ const typedData = {
510
+ domain: {
511
+ name: "GatewayWalletBatched",
512
+ version: input.version ?? "1",
513
+ chainId: input.chainId,
514
+ verifyingContract: input.verifyingContract
515
+ },
516
+ primaryType: "TransferWithAuthorization",
517
+ types: GATEWAY_AUTH_TYPES,
518
+ message: {
519
+ from: walletAddress,
520
+ to: "0x0000000000000000000000000000000000000000",
521
+ value: 0,
522
+ validAfter: 0,
523
+ validBefore: 0,
524
+ nonce: "0x" + "0".repeat(64)
525
+ }
526
+ };
527
+ const probeSignature = await signTypedDataWithCircleCli(
528
+ command,
529
+ timeoutMs,
530
+ walletAddress,
531
+ chain,
532
+ typedData,
533
+ { skipSignatureAddressCheck: true }
534
+ );
535
+ const owner = await (0, import_viem.recoverTypedDataAddress)({
536
+ ...typedData,
537
+ signature: probeSignature
538
+ });
539
+ return normalizeAddress(owner);
540
+ }
541
+ function createCircleAgentWalletSigner(options) {
542
+ const command = options.cliCommand ?? DEFAULT_CIRCLE_CLI_COMMAND;
543
+ const timeoutMs = options.timeoutMs ?? DEFAULT_CIRCLE_CLI_TIMEOUT_MS;
544
+ const walletAddress = normalizeAddress(options.address);
545
+ let effectiveAddress = walletAddress;
546
+ let resolvedOwnerAddress = null;
547
+ let resolveOwnerAddressInFlight = null;
548
+ const resolveOwnerAddress = async (input) => {
549
+ if (!input.verifyingContract) {
550
+ return walletAddress;
551
+ }
552
+ if (resolvedOwnerAddress) {
553
+ return resolvedOwnerAddress;
554
+ }
555
+ if (!resolveOwnerAddressInFlight) {
556
+ resolveOwnerAddressInFlight = resolveGatewaySignerAddress(command, timeoutMs, walletAddress, options.chain, input).then((owner2) => {
557
+ resolvedOwnerAddress = owner2;
558
+ effectiveAddress = owner2;
559
+ return owner2;
560
+ }).finally(() => {
561
+ resolveOwnerAddressInFlight = null;
562
+ });
563
+ }
564
+ const owner = await resolveOwnerAddressInFlight;
565
+ resolvedOwnerAddress = owner;
566
+ effectiveAddress = owner;
567
+ return owner;
568
+ };
569
+ return {
570
+ get address() {
571
+ return effectiveAddress;
572
+ },
573
+ circleAgentWalletProcessor: async (input) => {
574
+ await resolveOwnerAddress(input);
575
+ },
576
+ signTypedData: async (typedData) => signTypedDataWithCircleCli(command, timeoutMs, walletAddress, options.chain, typedData)
577
+ };
578
+ }
579
+ // Annotate the CommonJS export names for ESM import in node:
580
+ 0 && (module.exports = {
581
+ CircleNanopaymentPayloadBuilder,
582
+ QuoteParseError,
583
+ RouterClient,
584
+ RouterClientConfigError,
585
+ RouterSdkError,
586
+ createCircleAgentWalletSigner,
587
+ createRemoteSigner,
588
+ createViemSigner
589
+ });