@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 +126 -0
- package/dist/index.cjs +589 -0
- package/dist/index.d.cts +94 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.js +555 -0
- package/package.json +58 -0
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
|
+
});
|