@recoilpay/intent-core 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/LICENSE +21 -0
- package/README.md +88 -0
- package/dist/index.cjs +1160 -0
- package/dist/index.d.cts +623 -0
- package/dist/index.d.ts +623 -0
- package/dist/index.js +1100 -0
- package/package.json +62 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1160 @@
|
|
|
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
|
+
ApiError: () => ApiError,
|
|
24
|
+
CHAIN_ALIASES: () => CHAIN_ALIASES,
|
|
25
|
+
DEFAULT_API_URL: () => DEFAULT_API_URL,
|
|
26
|
+
DEFAULT_INTENT_MODEL: () => DEFAULT_INTENT_MODEL,
|
|
27
|
+
EMPTY_SUPPORTED: () => EMPTY_SUPPORTED,
|
|
28
|
+
HF_ROUTER_URL: () => HF_ROUTER_URL,
|
|
29
|
+
MAX_UINT256: () => MAX_UINT256,
|
|
30
|
+
PERMIT2_ADDRESS: () => PERMIT2_ADDRESS,
|
|
31
|
+
SOLANA_CHAIN_IDS: () => SOLANA_CHAIN_IDS,
|
|
32
|
+
TEMPLATE_HINT: () => TEMPLATE_HINT,
|
|
33
|
+
TOKEN_ALIASES: () => TOKEN_ALIASES,
|
|
34
|
+
buildOrderRequest: () => buildOrderRequest,
|
|
35
|
+
buildPermit2ApproveRequest: () => buildPermit2ApproveRequest,
|
|
36
|
+
buildQuoteRequest: () => buildQuoteRequest,
|
|
37
|
+
buildSupportedSet: () => buildSupportedSet,
|
|
38
|
+
chainName: () => chainName,
|
|
39
|
+
createApiClient: () => createApiClient,
|
|
40
|
+
createIntentSession: () => createIntentSession,
|
|
41
|
+
explorerUrl: () => explorerUrl,
|
|
42
|
+
extractIntents: () => extractIntents,
|
|
43
|
+
findAsset: () => findAsset,
|
|
44
|
+
getPermit2Allowance: () => getPermit2Allowance,
|
|
45
|
+
hasPermit2Allowance: () => hasPermit2Allowance,
|
|
46
|
+
interopAddress: () => interopAddress,
|
|
47
|
+
isTerminalStatus: () => isTerminalStatus,
|
|
48
|
+
normalizeChain: () => normalizeChain,
|
|
49
|
+
normalizeToken: () => normalizeToken,
|
|
50
|
+
parseIntent: () => parseIntent,
|
|
51
|
+
resolveIntent: () => resolveIntent,
|
|
52
|
+
rpcReaders: () => rpcReaders,
|
|
53
|
+
signQuote: () => signQuote,
|
|
54
|
+
stripDomainType: () => stripDomainType,
|
|
55
|
+
validateIntent: () => validateIntent,
|
|
56
|
+
viemWallet: () => viemWallet
|
|
57
|
+
});
|
|
58
|
+
module.exports = __toCommonJS(index_exports);
|
|
59
|
+
|
|
60
|
+
// src/session.ts
|
|
61
|
+
var import_viem4 = require("viem");
|
|
62
|
+
|
|
63
|
+
// src/api.ts
|
|
64
|
+
var DEFAULT_API_URL = "https://api.recoilpay.com";
|
|
65
|
+
var ApiError = class extends Error {
|
|
66
|
+
constructor(status, code, message) {
|
|
67
|
+
super(message);
|
|
68
|
+
this.status = status;
|
|
69
|
+
this.code = code;
|
|
70
|
+
this.name = "ApiError";
|
|
71
|
+
}
|
|
72
|
+
status;
|
|
73
|
+
code;
|
|
74
|
+
};
|
|
75
|
+
function createApiClient(options = {}) {
|
|
76
|
+
const base = `${(options.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "")}/api/v1`;
|
|
77
|
+
const doFetch = options.fetch ?? ((...args) => fetch(...args));
|
|
78
|
+
async function request(path, init) {
|
|
79
|
+
const res = await doFetch(`${base}${path}`, {
|
|
80
|
+
...init,
|
|
81
|
+
headers: {
|
|
82
|
+
Accept: "application/json",
|
|
83
|
+
...init?.body ? { "Content-Type": "application/json" } : {},
|
|
84
|
+
...init?.headers
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
if (!res.ok) {
|
|
88
|
+
const body = await res.json().catch(() => null);
|
|
89
|
+
throw new ApiError(res.status, body?.error ?? "HTTP_ERROR", body?.message ?? `${path} \u2192 ${res.status} ${res.statusText}`);
|
|
90
|
+
}
|
|
91
|
+
return await res.json();
|
|
92
|
+
}
|
|
93
|
+
const post = (path, body) => request(path, { method: "POST", body: JSON.stringify(body) });
|
|
94
|
+
return {
|
|
95
|
+
/** The fetch this client uses (shared with the session's parser). */
|
|
96
|
+
fetch: doFetch,
|
|
97
|
+
/** Supported chains with RPC endpoints and settlement contracts. */
|
|
98
|
+
async getChains() {
|
|
99
|
+
return (await request("/chains")).data;
|
|
100
|
+
},
|
|
101
|
+
getSolvers: () => request("/solvers"),
|
|
102
|
+
/**
|
|
103
|
+
* Every asset an active solver can fill, de-duplicated — the set that
|
|
104
|
+
* decides whether an intent is executable right now.
|
|
105
|
+
*/
|
|
106
|
+
async getSupportedAssets() {
|
|
107
|
+
const { solvers } = await request("/solvers");
|
|
108
|
+
const seen = /* @__PURE__ */ new Map();
|
|
109
|
+
for (const solver of solvers) {
|
|
110
|
+
if (solver.status !== "active") continue;
|
|
111
|
+
for (const asset of solver.supportedAssets.assets ?? []) {
|
|
112
|
+
const key = `${asset.chainId}:${asset.address.toLowerCase()}`;
|
|
113
|
+
if (!seen.has(key)) seen.set(key, { ...asset, address: asset.address.toLowerCase() });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return [...seen.values()];
|
|
117
|
+
},
|
|
118
|
+
getQuotes: (req) => post("/quotes", req),
|
|
119
|
+
submitOrder: (req) => post("/orders", req),
|
|
120
|
+
getOrder: (id) => request(`/orders/${encodeURIComponent(id)}`)
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/parse.ts
|
|
125
|
+
var TEMPLATE_HINT = "Try: swap <amount> <token> on <chain> for <token> on <chain> [to <address>] (e.g. swap 10 USDC on Base for ETH on Arbitrum) OR send <amount> <token> on <chain> to <address>";
|
|
126
|
+
var HF_ROUTER_URL = "https://router.huggingface.co/v1/chat/completions";
|
|
127
|
+
var DEFAULT_INTENT_MODEL = "Qwen/Qwen2.5-Coder-32B-Instruct";
|
|
128
|
+
var SYSTEM_PROMPT = `You are an intent extraction engine for a cross-chain swap protocol.
|
|
129
|
+
Your job is to parse the user's natural language request into a strict JSON array of objects representing their intents.
|
|
130
|
+
If the user specifies multiple intents (e.g. "swap X and then send Y"), return multiple objects in the array.
|
|
131
|
+
The JSON objects must perfectly match the following TypeScript interface:
|
|
132
|
+
|
|
133
|
+
type AmountKind = "token" | "usd";
|
|
134
|
+
type IntentAction = "swap" | "send";
|
|
135
|
+
|
|
136
|
+
interface RawIntent {
|
|
137
|
+
action: IntentAction;
|
|
138
|
+
amount: string | null; // numeric string, no leading '$'. e.g. "100" (null if not stated)
|
|
139
|
+
amountKind: AmountKind; // "usd" if they mention dollars/bucks, "token" otherwise
|
|
140
|
+
tokenIn: string | null; // raw token symbol/alias as typed, e.g. "USDC"
|
|
141
|
+
chainIn: string | null; // raw chain alias as typed, e.g. "Base"
|
|
142
|
+
tokenOut: string | null; // target token (optional for send)
|
|
143
|
+
chainOut: string | null; // target chain (optional for send)
|
|
144
|
+
recipient: string | null; // address/ens (required for 'send', optional for 'swap')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
Return ONLY the JSON array, nothing else. If you absolutely cannot determine any intents, return {"error": "unrecognized"}. Do not make up information that is missing; use null.`;
|
|
148
|
+
var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
149
|
+
var readString = (v) => typeof v === "string" && v.trim().length > 0 ? v : null;
|
|
150
|
+
var readAmount = (v) => v === null || v === void 0 ? null : String(v);
|
|
151
|
+
var readAmountKind = (v) => v === "usd" ? "usd" : "token";
|
|
152
|
+
var readAction = (v) => v === "swap" || v === "send" ? v : null;
|
|
153
|
+
var offTemplate = (message = TEMPLATE_HINT) => ({ ok: false, offTemplate: true, message });
|
|
154
|
+
function extractIntents(content) {
|
|
155
|
+
let json = content.trim();
|
|
156
|
+
const fenced = json.match(/```json\n([\s\S]*?)\n```/) || json.match(/```\n([\s\S]*?)\n```/);
|
|
157
|
+
if (fenced) {
|
|
158
|
+
json = fenced[1];
|
|
159
|
+
} else {
|
|
160
|
+
const first = Math.min(...["{", "["].map((c) => json.indexOf(c) === -1 ? Infinity : json.indexOf(c)));
|
|
161
|
+
const last = Math.max(json.lastIndexOf("}"), json.lastIndexOf("]"));
|
|
162
|
+
if (first !== Infinity && last !== -1) json = json.slice(first, last + 1);
|
|
163
|
+
}
|
|
164
|
+
let parsed;
|
|
165
|
+
try {
|
|
166
|
+
parsed = JSON.parse(json);
|
|
167
|
+
} catch {
|
|
168
|
+
return offTemplate();
|
|
169
|
+
}
|
|
170
|
+
if (isRecord(parsed) && parsed.error || !Array.isArray(parsed) && !isRecord(parsed)) return offTemplate();
|
|
171
|
+
const items = Array.isArray(parsed) ? parsed : [parsed];
|
|
172
|
+
if (items.length === 0 || !items.every(isRecord)) return offTemplate();
|
|
173
|
+
const intents = [];
|
|
174
|
+
for (const item of items) {
|
|
175
|
+
const action = readAction(item.action);
|
|
176
|
+
if (!action) return offTemplate();
|
|
177
|
+
intents.push({
|
|
178
|
+
action,
|
|
179
|
+
amount: readAmount(item.amount),
|
|
180
|
+
amountKind: readAmountKind(item.amountKind),
|
|
181
|
+
tokenIn: readString(item.tokenIn),
|
|
182
|
+
chainIn: readString(item.chainIn),
|
|
183
|
+
tokenOut: readString(item.tokenOut),
|
|
184
|
+
chainOut: readString(item.chainOut),
|
|
185
|
+
recipient: readString(item.recipient)
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return { ok: true, intents };
|
|
189
|
+
}
|
|
190
|
+
async function parseIntent(text, options = {}) {
|
|
191
|
+
const norm = text.trim();
|
|
192
|
+
if (!norm) return offTemplate();
|
|
193
|
+
const token = options.hfAccessToken?.trim();
|
|
194
|
+
if (!token) return offTemplate(`Natural-language parsing isn't set up here. ${TEMPLATE_HINT}`);
|
|
195
|
+
const doFetch = options.fetch ?? ((...args) => fetch(...args));
|
|
196
|
+
let res;
|
|
197
|
+
try {
|
|
198
|
+
res = await doFetch(HF_ROUTER_URL, {
|
|
199
|
+
method: "POST",
|
|
200
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
201
|
+
body: JSON.stringify({
|
|
202
|
+
model: options.model ?? DEFAULT_INTENT_MODEL,
|
|
203
|
+
messages: [
|
|
204
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
205
|
+
{ role: "user", content: norm }
|
|
206
|
+
],
|
|
207
|
+
max_tokens: 250,
|
|
208
|
+
temperature: 0.1
|
|
209
|
+
})
|
|
210
|
+
});
|
|
211
|
+
} catch {
|
|
212
|
+
return offTemplate(`Could not reach the intent parser. Check your connection and try again.`);
|
|
213
|
+
}
|
|
214
|
+
if (!res.ok) {
|
|
215
|
+
if (res.status === 429) return offTemplate("Too many requests \u2014 wait a moment and try again.");
|
|
216
|
+
return offTemplate(`Failed to understand that. ${TEMPLATE_HINT}`);
|
|
217
|
+
}
|
|
218
|
+
const body = await res.json().catch(() => null);
|
|
219
|
+
return extractIntents(body?.choices?.[0]?.message?.content ?? "");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/chains.ts
|
|
223
|
+
var import_viem = require("viem");
|
|
224
|
+
var import_actions = require("viem/actions");
|
|
225
|
+
var CHAIN_NAMES = {
|
|
226
|
+
1: "Ethereum",
|
|
227
|
+
10: "Optimism",
|
|
228
|
+
137: "Polygon",
|
|
229
|
+
8453: "Base",
|
|
230
|
+
42161: "Arbitrum",
|
|
231
|
+
56: "BNB Chain",
|
|
232
|
+
43114: "Avalanche",
|
|
233
|
+
11155111: "Ethereum Sepolia",
|
|
234
|
+
84532: "Base Sepolia",
|
|
235
|
+
11155420: "OP Sepolia",
|
|
236
|
+
421614: "Arbitrum Sepolia",
|
|
237
|
+
80002: "Polygon Amoy",
|
|
238
|
+
9000000001: "Solana",
|
|
239
|
+
9000000002: "Solana Devnet"
|
|
240
|
+
};
|
|
241
|
+
var EXPLORERS = {
|
|
242
|
+
1: "https://etherscan.io",
|
|
243
|
+
10: "https://optimistic.etherscan.io",
|
|
244
|
+
137: "https://polygonscan.com",
|
|
245
|
+
8453: "https://basescan.org",
|
|
246
|
+
42161: "https://arbiscan.io",
|
|
247
|
+
56: "https://bscscan.com",
|
|
248
|
+
43114: "https://snowtrace.io",
|
|
249
|
+
11155111: "https://sepolia.etherscan.io",
|
|
250
|
+
84532: "https://sepolia.basescan.org",
|
|
251
|
+
11155420: "https://sepolia-optimism.etherscan.io",
|
|
252
|
+
421614: "https://sepolia.arbiscan.io",
|
|
253
|
+
80002: "https://amoy.polygonscan.com"
|
|
254
|
+
};
|
|
255
|
+
var SOLANA_CHAIN_IDS = /* @__PURE__ */ new Set([9000000001, 9000000002]);
|
|
256
|
+
function chainName(chainId, info) {
|
|
257
|
+
return CHAIN_NAMES[chainId] ?? info?.name ?? `Chain ${chainId}`;
|
|
258
|
+
}
|
|
259
|
+
function explorerUrl(chainId) {
|
|
260
|
+
return EXPLORERS[chainId];
|
|
261
|
+
}
|
|
262
|
+
function rpcReaders(chains) {
|
|
263
|
+
const cache = /* @__PURE__ */ new Map();
|
|
264
|
+
return async (chainId) => {
|
|
265
|
+
const hit = cache.get(chainId);
|
|
266
|
+
if (hit) return hit;
|
|
267
|
+
const info = (await chains()).find((c) => c.chain_id === chainId);
|
|
268
|
+
if (!info?.rpc_url) return null;
|
|
269
|
+
const client = (0, import_viem.createClient)({ transport: (0, import_viem.http)(info.rpc_url) });
|
|
270
|
+
const reader = {
|
|
271
|
+
readContract: (args) => (0, import_actions.readContract)(client, args),
|
|
272
|
+
waitForTransactionReceipt: (args) => (0, import_actions.waitForTransactionReceipt)(client, args)
|
|
273
|
+
};
|
|
274
|
+
cache.set(chainId, reader);
|
|
275
|
+
return reader;
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/intent/registry.ts
|
|
280
|
+
var TOKEN_ALIASES = {
|
|
281
|
+
usdc: "USDC",
|
|
282
|
+
usdt: "USDT",
|
|
283
|
+
tether: "USDT",
|
|
284
|
+
dai: "DAI",
|
|
285
|
+
eth: "ETH",
|
|
286
|
+
ether: "ETH",
|
|
287
|
+
ethereum: "ETH",
|
|
288
|
+
weth: "WETH",
|
|
289
|
+
wbtc: "WBTC",
|
|
290
|
+
btc: "WBTC",
|
|
291
|
+
bitcoin: "WBTC",
|
|
292
|
+
sol: "SOL",
|
|
293
|
+
solana: "SOL",
|
|
294
|
+
arb: "ARB",
|
|
295
|
+
arbitrum: "ARB",
|
|
296
|
+
op: "OP",
|
|
297
|
+
optimism: "OP",
|
|
298
|
+
matic: "MATIC",
|
|
299
|
+
pol: "MATIC",
|
|
300
|
+
polygon: "MATIC"
|
|
301
|
+
};
|
|
302
|
+
var CHAIN_ALIASES = {
|
|
303
|
+
ethereum: { id: 1, name: "Ethereum" },
|
|
304
|
+
eth: { id: 1, name: "Ethereum" },
|
|
305
|
+
mainnet: { id: 1, name: "Ethereum" },
|
|
306
|
+
base: { id: 8453, name: "Base" },
|
|
307
|
+
arbitrum: { id: 42161, name: "Arbitrum" },
|
|
308
|
+
arb: { id: 42161, name: "Arbitrum" },
|
|
309
|
+
optimism: { id: 10, name: "Optimism" },
|
|
310
|
+
op: { id: 10, name: "Optimism" },
|
|
311
|
+
polygon: { id: 137, name: "Polygon" },
|
|
312
|
+
matic: { id: 137, name: "Polygon" },
|
|
313
|
+
solana: { id: 9000000002, name: "Solana Devnet" },
|
|
314
|
+
"sol network": { id: 9000000002, name: "Solana Devnet" },
|
|
315
|
+
// testnets — the solver settles across these five
|
|
316
|
+
"base sepolia": { id: 84532, name: "Base Sepolia" },
|
|
317
|
+
"op sepolia": { id: 11155420, name: "OP Sepolia" },
|
|
318
|
+
"optimism sepolia": { id: 11155420, name: "OP Sepolia" },
|
|
319
|
+
"arbitrum sepolia": { id: 421614, name: "Arbitrum Sepolia" },
|
|
320
|
+
"arb sepolia": { id: 421614, name: "Arbitrum Sepolia" },
|
|
321
|
+
"ethereum sepolia": { id: 11155111, name: "Ethereum Sepolia" },
|
|
322
|
+
"eth sepolia": { id: 11155111, name: "Ethereum Sepolia" },
|
|
323
|
+
sepolia: { id: 11155111, name: "Ethereum Sepolia" },
|
|
324
|
+
"polygon amoy": { id: 80002, name: "Polygon Amoy" },
|
|
325
|
+
amoy: { id: 80002, name: "Polygon Amoy" }
|
|
326
|
+
};
|
|
327
|
+
function normalizeToken(raw) {
|
|
328
|
+
if (!raw) return null;
|
|
329
|
+
return TOKEN_ALIASES[raw.trim().toLowerCase()] ?? null;
|
|
330
|
+
}
|
|
331
|
+
function normalizeChain(raw) {
|
|
332
|
+
if (!raw) return null;
|
|
333
|
+
return CHAIN_ALIASES[raw.trim().toLowerCase()] ?? null;
|
|
334
|
+
}
|
|
335
|
+
var EMPTY_SUPPORTED = { chains: /* @__PURE__ */ new Set(), assets: /* @__PURE__ */ new Map() };
|
|
336
|
+
function buildSupportedSet(assets) {
|
|
337
|
+
const chains = /* @__PURE__ */ new Set();
|
|
338
|
+
const map = /* @__PURE__ */ new Map();
|
|
339
|
+
for (const a of assets) {
|
|
340
|
+
chains.add(a.chainId);
|
|
341
|
+
map.set(`${a.chainId}:${a.symbol.toUpperCase()}`, a);
|
|
342
|
+
}
|
|
343
|
+
return { chains, assets: map };
|
|
344
|
+
}
|
|
345
|
+
function findAsset(set, chainId, symbol) {
|
|
346
|
+
return set.assets.get(`${chainId}:${symbol.toUpperCase()}`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/intent/resolve.ts
|
|
350
|
+
var import_viem2 = require("viem");
|
|
351
|
+
function resolveIntent(raw, supported, user, solanaUser) {
|
|
352
|
+
const chainIn = normalizeChain(raw.chainIn);
|
|
353
|
+
const symIn = normalizeToken(raw.tokenIn);
|
|
354
|
+
if (!chainIn || typeof chainIn.id !== "number" || !symIn || !raw.amount) {
|
|
355
|
+
throw new Error("resolveIntent called on an unvalidated/invalid intent");
|
|
356
|
+
}
|
|
357
|
+
const srcChainId = chainIn.id;
|
|
358
|
+
const inAsset = findAsset(supported, srcChainId, symIn);
|
|
359
|
+
if (!inAsset) throw new Error(`No supported asset for ${symIn} on ${chainIn.name}`);
|
|
360
|
+
const inputToken = isSolana(srcChainId) ? inAsset.address : (0, import_viem2.getAddress)(inAsset.address);
|
|
361
|
+
const inputAmount = (0, import_viem2.parseUnits)(raw.amount, inAsset.decimals);
|
|
362
|
+
let dstChainId;
|
|
363
|
+
let outputToken;
|
|
364
|
+
let recipient;
|
|
365
|
+
if (raw.action === "swap") {
|
|
366
|
+
const chainOut = normalizeChain(raw.chainOut);
|
|
367
|
+
const symOut = normalizeToken(raw.tokenOut);
|
|
368
|
+
if (!chainOut || typeof chainOut.id !== "number" || !symOut) {
|
|
369
|
+
throw new Error("resolveIntent: unresolved destination on a swap");
|
|
370
|
+
}
|
|
371
|
+
dstChainId = chainOut.id;
|
|
372
|
+
const outAsset = findAsset(supported, dstChainId, symOut);
|
|
373
|
+
if (!outAsset) throw new Error(`No supported asset for ${symOut} on ${chainOut.name}`);
|
|
374
|
+
outputToken = isSolana(dstChainId) ? outAsset.address : (0, import_viem2.getAddress)(outAsset.address);
|
|
375
|
+
recipient = raw.recipient ? isSolana(dstChainId) ? raw.recipient : (0, import_viem2.getAddress)(raw.recipient) : isSolana(dstChainId) ? solanaUser ?? user : user;
|
|
376
|
+
} else {
|
|
377
|
+
if (!raw.recipient) throw new Error("resolveIntent: send without recipient");
|
|
378
|
+
if (raw.chainOut) {
|
|
379
|
+
const chainOut = normalizeChain(raw.chainOut);
|
|
380
|
+
if (!chainOut || typeof chainOut.id !== "number") {
|
|
381
|
+
throw new Error("resolveIntent: unresolved destination chain on a send");
|
|
382
|
+
}
|
|
383
|
+
dstChainId = chainOut.id;
|
|
384
|
+
} else {
|
|
385
|
+
dstChainId = srcChainId;
|
|
386
|
+
}
|
|
387
|
+
if (raw.tokenOut) {
|
|
388
|
+
const symOut = normalizeToken(raw.tokenOut);
|
|
389
|
+
if (!symOut) throw new Error("resolveIntent: unresolved destination token on a send");
|
|
390
|
+
const outAsset = findAsset(supported, dstChainId, symOut);
|
|
391
|
+
if (!outAsset) throw new Error(`No supported asset for ${symOut} on destination chain`);
|
|
392
|
+
outputToken = isSolana(dstChainId) ? outAsset.address : (0, import_viem2.getAddress)(outAsset.address);
|
|
393
|
+
} else {
|
|
394
|
+
if (dstChainId !== srcChainId) {
|
|
395
|
+
const outAsset = findAsset(supported, dstChainId, symIn);
|
|
396
|
+
if (!outAsset) throw new Error(`No supported asset for ${symIn} on destination chain`);
|
|
397
|
+
outputToken = isSolana(dstChainId) ? outAsset.address : (0, import_viem2.getAddress)(outAsset.address);
|
|
398
|
+
} else {
|
|
399
|
+
outputToken = inputToken;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
recipient = isSolana(dstChainId) ? raw.recipient : (0, import_viem2.getAddress)(raw.recipient);
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
action: raw.action,
|
|
406
|
+
user,
|
|
407
|
+
srcChainId,
|
|
408
|
+
dstChainId,
|
|
409
|
+
inputToken,
|
|
410
|
+
inputDecimals: inAsset.decimals,
|
|
411
|
+
inputAmount,
|
|
412
|
+
outputToken,
|
|
413
|
+
recipient
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function isSolana(chainId) {
|
|
417
|
+
return chainId === 9000000001 || chainId === 9000000002;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/intent/validate.ts
|
|
421
|
+
var LABEL = {
|
|
422
|
+
amount: "Amount",
|
|
423
|
+
tokenIn: "Source token",
|
|
424
|
+
chainIn: "Source chain",
|
|
425
|
+
tokenOut: "Destination token",
|
|
426
|
+
chainOut: "Destination chain",
|
|
427
|
+
recipient: "Recipient"
|
|
428
|
+
};
|
|
429
|
+
var EVM_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
430
|
+
var SOLANA_ADDRESS_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
431
|
+
function checkChain(field, raw, supported, issues) {
|
|
432
|
+
if (!raw) {
|
|
433
|
+
issues.push({ field, kind: "missing", message: `${LABEL[field]} is not stated.` });
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
const c = normalizeChain(raw);
|
|
437
|
+
if (!c) {
|
|
438
|
+
issues.push({ field, kind: "unknown", message: `Chain '${raw}' isn't recognized.` });
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
if (typeof c.id !== "number" || !supported.chains.has(c.id)) {
|
|
442
|
+
issues.push({ field, kind: "unsupported", message: `${c.name} isn't supported yet.` });
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
return c;
|
|
446
|
+
}
|
|
447
|
+
function checkToken(field, raw, chain, supported, issues) {
|
|
448
|
+
if (!raw) {
|
|
449
|
+
issues.push({ field, kind: "missing", message: `${LABEL[field]} is not stated.` });
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const sym = normalizeToken(raw);
|
|
453
|
+
if (!sym) {
|
|
454
|
+
issues.push({ field, kind: "unknown", message: `Token '${raw}' isn't recognized.` });
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (chain && typeof chain.id === "number") {
|
|
458
|
+
if (!findAsset(supported, chain.id, sym)) {
|
|
459
|
+
issues.push({
|
|
460
|
+
field,
|
|
461
|
+
kind: "unsupported",
|
|
462
|
+
message: `${sym} isn't supported on ${chain.name} yet.`
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function checkRecipient(raw, chainOutId, issues) {
|
|
468
|
+
if (!raw) {
|
|
469
|
+
issues.push({ field: "recipient", kind: "missing", message: "Recipient is not stated." });
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (chainOutId === 9000000001 || chainOutId === 9000000002) {
|
|
473
|
+
if (SOLANA_ADDRESS_RE.test(raw)) return;
|
|
474
|
+
issues.push({
|
|
475
|
+
field: "recipient",
|
|
476
|
+
kind: "unknown",
|
|
477
|
+
message: `Recipient '${raw}' isn\u2019t a valid Solana address.`
|
|
478
|
+
});
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (EVM_ADDRESS_RE.test(raw)) return;
|
|
482
|
+
if (raw.endsWith(".eth")) {
|
|
483
|
+
issues.push({
|
|
484
|
+
field: "recipient",
|
|
485
|
+
kind: "unsupported",
|
|
486
|
+
message: "ENS recipients aren\u2019t supported yet \u2014 use a 0x address."
|
|
487
|
+
});
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
issues.push({
|
|
491
|
+
field: "recipient",
|
|
492
|
+
kind: "unknown",
|
|
493
|
+
message: `Recipient '${raw}' isn\u2019t a valid 0x address.`
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
function validateIntent(raw, supported) {
|
|
497
|
+
const issues = [];
|
|
498
|
+
if (!raw.amount) {
|
|
499
|
+
issues.push({ field: "amount", kind: "missing", message: "Amount is not stated." });
|
|
500
|
+
} else if (!(Number(raw.amount) > 0)) {
|
|
501
|
+
issues.push({ field: "amount", kind: "unknown", message: "Amount must be a positive number." });
|
|
502
|
+
}
|
|
503
|
+
const chainIn = checkChain("chainIn", raw.chainIn, supported, issues);
|
|
504
|
+
checkToken("tokenIn", raw.tokenIn, chainIn, supported, issues);
|
|
505
|
+
if (raw.action === "swap") {
|
|
506
|
+
const chainOut = checkChain("chainOut", raw.chainOut, supported, issues);
|
|
507
|
+
checkToken("tokenOut", raw.tokenOut, chainOut, supported, issues);
|
|
508
|
+
if (raw.recipient) {
|
|
509
|
+
checkRecipient(raw.recipient, chainOut?.id, issues);
|
|
510
|
+
}
|
|
511
|
+
} else {
|
|
512
|
+
let chainOut = chainIn;
|
|
513
|
+
if (raw.chainOut) {
|
|
514
|
+
chainOut = checkChain("chainOut", raw.chainOut, supported, issues);
|
|
515
|
+
}
|
|
516
|
+
checkRecipient(raw.recipient, chainOut?.id, issues);
|
|
517
|
+
if (raw.tokenOut) {
|
|
518
|
+
checkToken("tokenOut", raw.tokenOut, chainOut, supported, issues);
|
|
519
|
+
} else if (chainOut && chainOut.id !== chainIn?.id) {
|
|
520
|
+
checkToken("tokenOut", raw.tokenIn, chainOut, supported, issues);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return issues;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// src/oif/interop.ts
|
|
527
|
+
function hexToBytes(hex) {
|
|
528
|
+
const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
529
|
+
const bytes = [];
|
|
530
|
+
for (let i = 0; i < cleanHex.length; i += 2) {
|
|
531
|
+
bytes.push(parseInt(cleanHex.slice(i, i + 2), 16));
|
|
532
|
+
}
|
|
533
|
+
return bytes;
|
|
534
|
+
}
|
|
535
|
+
function bytesToHex(bytes) {
|
|
536
|
+
return "0x" + bytes.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
537
|
+
}
|
|
538
|
+
function chainIdToBytes(chainId) {
|
|
539
|
+
const bytes = [];
|
|
540
|
+
let value = chainId;
|
|
541
|
+
while (value > 0) {
|
|
542
|
+
bytes.unshift(value & 255);
|
|
543
|
+
value >>= 8;
|
|
544
|
+
}
|
|
545
|
+
if (bytes.length === 0) bytes.push(0);
|
|
546
|
+
return bytes;
|
|
547
|
+
}
|
|
548
|
+
function toInteropAddress(address, chainId) {
|
|
549
|
+
const version = "0001";
|
|
550
|
+
const chainType = "0000";
|
|
551
|
+
const chainRefBytes = chainIdToBytes(chainId);
|
|
552
|
+
const chainRefHex = bytesToHex(chainRefBytes).slice(2);
|
|
553
|
+
const chainRefLength = chainRefBytes.length.toString(16).padStart(2, "0");
|
|
554
|
+
const addressBytes = hexToBytes(address);
|
|
555
|
+
const addressHex = bytesToHex(addressBytes).slice(2);
|
|
556
|
+
const addressLength = addressBytes.length.toString(16).padStart(2, "0");
|
|
557
|
+
return `0x${version}${chainType}${chainRefLength}${chainRefHex}${addressLength}${addressHex}`;
|
|
558
|
+
}
|
|
559
|
+
function interopAddress(chainId, address) {
|
|
560
|
+
return toInteropAddress(address, chainId);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/oif/buildQuoteRequest.ts
|
|
564
|
+
function buildQuoteRequest(resolved) {
|
|
565
|
+
const {
|
|
566
|
+
user,
|
|
567
|
+
recipient,
|
|
568
|
+
srcChainId,
|
|
569
|
+
dstChainId,
|
|
570
|
+
inputToken,
|
|
571
|
+
outputToken,
|
|
572
|
+
inputAmount
|
|
573
|
+
} = resolved;
|
|
574
|
+
const userInterop = interopAddress(srcChainId, user);
|
|
575
|
+
return {
|
|
576
|
+
user: userInterop,
|
|
577
|
+
intent: {
|
|
578
|
+
intentType: "oif-swap",
|
|
579
|
+
inputs: [
|
|
580
|
+
{
|
|
581
|
+
user: userInterop,
|
|
582
|
+
asset: interopAddress(srcChainId, inputToken),
|
|
583
|
+
amount: inputAmount.toString()
|
|
584
|
+
}
|
|
585
|
+
],
|
|
586
|
+
outputs: [
|
|
587
|
+
{
|
|
588
|
+
receiver: interopAddress(dstChainId, recipient),
|
|
589
|
+
asset: interopAddress(dstChainId, outputToken)
|
|
590
|
+
}
|
|
591
|
+
],
|
|
592
|
+
swapType: "exact-input",
|
|
593
|
+
// NOTE: do NOT send minValidUntil. This deployment copies it verbatim into
|
|
594
|
+
// the 3009 authorization's `validBefore`, so a relative value like 600 makes
|
|
595
|
+
// the order "expired since 1970". Omitting it lets the solver set a proper
|
|
596
|
+
// absolute deadline (~now+300s), matching the aggregator demo.
|
|
597
|
+
originSubmission: {
|
|
598
|
+
mode: "user",
|
|
599
|
+
schemes: ["permit2"]
|
|
600
|
+
}
|
|
601
|
+
},
|
|
602
|
+
supportedTypes: ["oif-escrow-v0"],
|
|
603
|
+
solverOptions: {
|
|
604
|
+
timeout: 8e3,
|
|
605
|
+
solverTimeout: 5e3,
|
|
606
|
+
minQuotes: 1
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
function buildOrderRequest(quote, signature) {
|
|
611
|
+
return {
|
|
612
|
+
quoteResponse: quote,
|
|
613
|
+
signature
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// src/oif/permit2.ts
|
|
618
|
+
var PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
|
|
619
|
+
var MAX_UINT256 = (1n << 256n) - 1n;
|
|
620
|
+
var ERC20_ALLOWANCE_ABI = [
|
|
621
|
+
{
|
|
622
|
+
name: "allowance",
|
|
623
|
+
type: "function",
|
|
624
|
+
stateMutability: "view",
|
|
625
|
+
inputs: [
|
|
626
|
+
{ name: "owner", type: "address" },
|
|
627
|
+
{ name: "spender", type: "address" }
|
|
628
|
+
],
|
|
629
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
630
|
+
}
|
|
631
|
+
];
|
|
632
|
+
var ERC20_APPROVE_ABI = [
|
|
633
|
+
{
|
|
634
|
+
name: "approve",
|
|
635
|
+
type: "function",
|
|
636
|
+
stateMutability: "nonpayable",
|
|
637
|
+
inputs: [
|
|
638
|
+
{ name: "spender", type: "address" },
|
|
639
|
+
{ name: "amount", type: "uint256" }
|
|
640
|
+
],
|
|
641
|
+
outputs: [{ name: "", type: "bool" }]
|
|
642
|
+
}
|
|
643
|
+
];
|
|
644
|
+
function getPermit2Allowance(publicClient, token, owner) {
|
|
645
|
+
return publicClient.readContract({
|
|
646
|
+
address: token,
|
|
647
|
+
abi: ERC20_ALLOWANCE_ABI,
|
|
648
|
+
functionName: "allowance",
|
|
649
|
+
args: [owner, PERMIT2_ADDRESS]
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
async function hasPermit2Allowance(publicClient, token, owner, required) {
|
|
653
|
+
const allowance = await getPermit2Allowance(publicClient, token, owner);
|
|
654
|
+
return allowance >= required;
|
|
655
|
+
}
|
|
656
|
+
function buildPermit2ApproveRequest(token, amount = MAX_UINT256) {
|
|
657
|
+
return {
|
|
658
|
+
address: token,
|
|
659
|
+
abi: ERC20_APPROVE_ABI,
|
|
660
|
+
functionName: "approve",
|
|
661
|
+
args: [PERMIT2_ADDRESS, amount]
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// src/oif/sign.ts
|
|
666
|
+
var import_viem3 = require("viem");
|
|
667
|
+
var PERMIT2_TYPES = {
|
|
668
|
+
PermitBatchWitnessTransferFrom: [
|
|
669
|
+
{ name: "permitted", type: "TokenPermissions[]" },
|
|
670
|
+
{ name: "spender", type: "address" },
|
|
671
|
+
{ name: "nonce", type: "uint256" },
|
|
672
|
+
{ name: "deadline", type: "uint256" },
|
|
673
|
+
{ name: "witness", type: "Permit2Witness" }
|
|
674
|
+
],
|
|
675
|
+
MandateOutput: [
|
|
676
|
+
{ name: "oracle", type: "bytes32" },
|
|
677
|
+
{ name: "settler", type: "bytes32" },
|
|
678
|
+
{ name: "chainId", type: "uint256" },
|
|
679
|
+
{ name: "token", type: "bytes32" },
|
|
680
|
+
{ name: "amount", type: "uint256" },
|
|
681
|
+
{ name: "recipient", type: "bytes32" },
|
|
682
|
+
{ name: "callbackData", type: "bytes" },
|
|
683
|
+
{ name: "context", type: "bytes" }
|
|
684
|
+
],
|
|
685
|
+
Permit2Witness: [
|
|
686
|
+
{ name: "user", type: "address" },
|
|
687
|
+
{ name: "expires", type: "uint32" },
|
|
688
|
+
{ name: "inputOracle", type: "address" },
|
|
689
|
+
{ name: "outputs", type: "MandateOutput[]" }
|
|
690
|
+
],
|
|
691
|
+
TokenPermissions: [
|
|
692
|
+
{ name: "token", type: "address" },
|
|
693
|
+
{ name: "amount", type: "uint256" }
|
|
694
|
+
]
|
|
695
|
+
};
|
|
696
|
+
var EIP3009_TYPES = {
|
|
697
|
+
TransferWithAuthorization: [
|
|
698
|
+
{ name: "from", type: "address" },
|
|
699
|
+
{ name: "to", type: "address" },
|
|
700
|
+
{ name: "value", type: "uint256" },
|
|
701
|
+
{ name: "validAfter", type: "uint256" },
|
|
702
|
+
{ name: "validBefore", type: "uint256" },
|
|
703
|
+
{ name: "nonce", type: "bytes32" }
|
|
704
|
+
],
|
|
705
|
+
ReceiveWithAuthorization: [
|
|
706
|
+
{ name: "from", type: "address" },
|
|
707
|
+
{ name: "to", type: "address" },
|
|
708
|
+
{ name: "value", type: "uint256" },
|
|
709
|
+
{ name: "validAfter", type: "uint256" },
|
|
710
|
+
{ name: "validBefore", type: "uint256" },
|
|
711
|
+
{ name: "nonce", type: "bytes32" }
|
|
712
|
+
],
|
|
713
|
+
CancelAuthorization: [
|
|
714
|
+
{ name: "authorizer", type: "address" },
|
|
715
|
+
{ name: "nonce", type: "bytes32" }
|
|
716
|
+
]
|
|
717
|
+
};
|
|
718
|
+
function extractDomain(payload) {
|
|
719
|
+
const domain = {
|
|
720
|
+
name: payload.domain.name,
|
|
721
|
+
chainId: BigInt(payload.domain.chainId),
|
|
722
|
+
verifyingContract: payload.domain.verifyingContract
|
|
723
|
+
};
|
|
724
|
+
if (payload.domain.version !== void 0 && payload.domain.version !== null) {
|
|
725
|
+
domain.version = payload.domain.version;
|
|
726
|
+
}
|
|
727
|
+
return domain;
|
|
728
|
+
}
|
|
729
|
+
function getTypesForPayload(payload) {
|
|
730
|
+
switch (payload.primaryType) {
|
|
731
|
+
case "PermitBatchWitnessTransferFrom":
|
|
732
|
+
case "PermitWitnessTransferFrom":
|
|
733
|
+
return PERMIT2_TYPES;
|
|
734
|
+
case "TransferWithAuthorization":
|
|
735
|
+
case "ReceiveWithAuthorization":
|
|
736
|
+
case "CancelAuthorization":
|
|
737
|
+
return { [payload.primaryType]: EIP3009_TYPES[payload.primaryType] };
|
|
738
|
+
default: {
|
|
739
|
+
if (payload.types) {
|
|
740
|
+
const filtered = {};
|
|
741
|
+
for (const [typeName, typeFields] of Object.entries(payload.types)) {
|
|
742
|
+
if (typeName !== "EIP712Domain") filtered[typeName] = typeFields;
|
|
743
|
+
}
|
|
744
|
+
return filtered;
|
|
745
|
+
}
|
|
746
|
+
throw new Error(
|
|
747
|
+
`Unknown primary type: ${payload.primaryType}. Please provide types in payload.`
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
async function signPermit2(payload, signer) {
|
|
753
|
+
const domain = extractDomain(payload);
|
|
754
|
+
const types = getTypesForPayload(payload);
|
|
755
|
+
const signature = await signer({
|
|
756
|
+
domain,
|
|
757
|
+
types,
|
|
758
|
+
primaryType: payload.primaryType,
|
|
759
|
+
message: payload.message
|
|
760
|
+
});
|
|
761
|
+
return `0x00${signature.slice(2)}`;
|
|
762
|
+
}
|
|
763
|
+
async function signEip3009(payload, metadata, signer) {
|
|
764
|
+
const domain = extractDomain(payload);
|
|
765
|
+
if (domain.version === void 0 || domain.version === null) {
|
|
766
|
+
domain.version = "1";
|
|
767
|
+
}
|
|
768
|
+
const types = getTypesForPayload(payload);
|
|
769
|
+
const signature = await signer({
|
|
770
|
+
domain,
|
|
771
|
+
types,
|
|
772
|
+
primaryType: payload.primaryType,
|
|
773
|
+
message: payload.message
|
|
774
|
+
});
|
|
775
|
+
const prefixed = `0x01${signature.slice(2)}`;
|
|
776
|
+
const meta = metadata;
|
|
777
|
+
const msg = payload.message;
|
|
778
|
+
const inputs = meta?.inputs ?? msg.inputs ?? [];
|
|
779
|
+
if (Array.isArray(inputs) && inputs.length > 1) {
|
|
780
|
+
return (0, import_viem3.encodeAbiParameters)([{ type: "bytes[]" }], [[prefixed]]);
|
|
781
|
+
}
|
|
782
|
+
return prefixed;
|
|
783
|
+
}
|
|
784
|
+
async function signQuote(quote, signTypedDataAsync) {
|
|
785
|
+
const order = quote.order;
|
|
786
|
+
switch (order.type) {
|
|
787
|
+
case "oif-escrow-v0":
|
|
788
|
+
return signPermit2(order.payload, signTypedDataAsync);
|
|
789
|
+
case "oif-3009-v0":
|
|
790
|
+
return signEip3009(order.payload, order.metadata, signTypedDataAsync);
|
|
791
|
+
case "oif-resource-lock-v0":
|
|
792
|
+
case "oif-generic-v0":
|
|
793
|
+
throw new Error(
|
|
794
|
+
`Unsupported order type for wallet signing: ${order.type} (only oif-escrow-v0 and oif-3009-v0 are implemented)`
|
|
795
|
+
);
|
|
796
|
+
default:
|
|
797
|
+
throw new Error(`Unsupported order type: ${order.type}`);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// src/session.ts
|
|
802
|
+
var ERC20_TRANSFER_ABI = [
|
|
803
|
+
{
|
|
804
|
+
name: "transfer",
|
|
805
|
+
type: "function",
|
|
806
|
+
stateMutability: "nonpayable",
|
|
807
|
+
inputs: [
|
|
808
|
+
{ name: "to", type: "address" },
|
|
809
|
+
{ name: "amount", type: "uint256" }
|
|
810
|
+
],
|
|
811
|
+
outputs: [{ name: "", type: "bool" }]
|
|
812
|
+
}
|
|
813
|
+
];
|
|
814
|
+
var errMessage = (e) => e instanceof Error ? e.message : typeof e === "string" ? e : "Something went wrong.";
|
|
815
|
+
function fmtUnits(amount, decimals) {
|
|
816
|
+
if (amount === void 0) return "";
|
|
817
|
+
const human = (0, import_viem4.formatUnits)(typeof amount === "bigint" ? amount : BigInt(amount), decimals);
|
|
818
|
+
const n = Number(human);
|
|
819
|
+
return Number.isFinite(n) ? n.toLocaleString("en-US", { maximumFractionDigits: 6 }) : human;
|
|
820
|
+
}
|
|
821
|
+
function isTerminalStatus(status) {
|
|
822
|
+
if (typeof status === "object") return "failed" in status;
|
|
823
|
+
return status === "finalized" || status === "refunded";
|
|
824
|
+
}
|
|
825
|
+
function fillTxHash(order) {
|
|
826
|
+
const f = order?.fillTransaction;
|
|
827
|
+
if (!f || typeof f !== "object") return null;
|
|
828
|
+
const rec = f;
|
|
829
|
+
const h = rec.hash ?? rec.transactionHash ?? rec.txHash;
|
|
830
|
+
return typeof h === "string" && /^0x[0-9a-fA-F]+$/.test(h) ? h : null;
|
|
831
|
+
}
|
|
832
|
+
var isNativeToken = (token) => !token || /^0x0{40}$/i.test(token) || /^0xe{40}$/i.test(token);
|
|
833
|
+
var INITIAL = {
|
|
834
|
+
phase: "idle",
|
|
835
|
+
assetsLoading: true,
|
|
836
|
+
assetsError: false,
|
|
837
|
+
ready: false,
|
|
838
|
+
hint: null,
|
|
839
|
+
issues: [],
|
|
840
|
+
resolved: null,
|
|
841
|
+
route: null,
|
|
842
|
+
quote: null,
|
|
843
|
+
preview: null,
|
|
844
|
+
needsApproval: false,
|
|
845
|
+
orderId: null,
|
|
846
|
+
status: null,
|
|
847
|
+
order: null,
|
|
848
|
+
explorerUrl: null,
|
|
849
|
+
error: null,
|
|
850
|
+
isConnected: false,
|
|
851
|
+
queueIndex: 0,
|
|
852
|
+
queueTotal: 0
|
|
853
|
+
};
|
|
854
|
+
function createIntentSession(options = {}) {
|
|
855
|
+
const api = options.api ?? createApiClient(options);
|
|
856
|
+
const pollMs = options.pollIntervalMs ?? 2500;
|
|
857
|
+
let chainsPromise = null;
|
|
858
|
+
const chains = () => chainsPromise ??= api.getChains().catch((e) => (chainsPromise = null, Promise.reject(e)));
|
|
859
|
+
const reader = options.chainReader ?? rpcReaders(chains);
|
|
860
|
+
let wallet = options.wallet ?? null;
|
|
861
|
+
let hfAccessToken = options.hfAccessToken ?? null;
|
|
862
|
+
let supported = EMPTY_SUPPORTED;
|
|
863
|
+
let queue = [];
|
|
864
|
+
let pollTimer;
|
|
865
|
+
let destroyed = false;
|
|
866
|
+
let generation = 0;
|
|
867
|
+
let state = { ...INITIAL, isConnected: Boolean(wallet) };
|
|
868
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
869
|
+
function set(patch) {
|
|
870
|
+
if (destroyed) return;
|
|
871
|
+
const next = { ...state, ...patch };
|
|
872
|
+
next.explorerUrl = buildExplorerUrl(next);
|
|
873
|
+
state = next;
|
|
874
|
+
for (const l of listeners) l(state);
|
|
875
|
+
}
|
|
876
|
+
function buildExplorerUrl(s) {
|
|
877
|
+
if (!s.resolved) return null;
|
|
878
|
+
const base = explorerUrl(s.resolved.dstChainId);
|
|
879
|
+
if (!base) return null;
|
|
880
|
+
const hash = s.route === "direct" ? s.orderId : fillTxHash(s.order);
|
|
881
|
+
return hash ? `${base}/tx/${hash}` : `${base}/address/${s.resolved.recipient}`;
|
|
882
|
+
}
|
|
883
|
+
const stopPolling = () => {
|
|
884
|
+
if (pollTimer) clearTimeout(pollTimer);
|
|
885
|
+
pollTimer = void 0;
|
|
886
|
+
};
|
|
887
|
+
async function refreshAssets() {
|
|
888
|
+
set({ assetsLoading: true, assetsError: false });
|
|
889
|
+
try {
|
|
890
|
+
supported = buildSupportedSet(await api.getSupportedAssets());
|
|
891
|
+
set({ assetsLoading: false, ready: true });
|
|
892
|
+
} catch {
|
|
893
|
+
set({ assetsLoading: false, assetsError: true });
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
async function quoteFor(raw, gen) {
|
|
897
|
+
if (!wallet) return set({ phase: "needsWallet" });
|
|
898
|
+
try {
|
|
899
|
+
set({ phase: "quoting", error: null });
|
|
900
|
+
const resolved = resolveIntent(raw, supported, wallet.address, wallet.solanaAddress);
|
|
901
|
+
const inAsset = findAsset(supported, resolved.srcChainId, normalizeToken(raw.tokenIn) ?? "");
|
|
902
|
+
const outSymbol = raw.action === "swap" ? normalizeToken(raw.tokenOut) ?? "" : inAsset?.symbol ?? "";
|
|
903
|
+
const outAsset = findAsset(supported, resolved.dstChainId, outSymbol);
|
|
904
|
+
const infos = await chains().catch(() => []);
|
|
905
|
+
const name = (id) => chainName(id, infos.find((c) => c.chain_id === id));
|
|
906
|
+
const paySymbol = inAsset?.symbol ?? (outSymbol || "?");
|
|
907
|
+
if (resolved.action === "send" && resolved.srcChainId === resolved.dstChainId) {
|
|
908
|
+
if (gen !== generation) return;
|
|
909
|
+
const amount = fmtUnits(resolved.inputAmount, resolved.inputDecimals);
|
|
910
|
+
return set({
|
|
911
|
+
phase: "quoted",
|
|
912
|
+
resolved,
|
|
913
|
+
route: "direct",
|
|
914
|
+
quote: null,
|
|
915
|
+
needsApproval: false,
|
|
916
|
+
preview: {
|
|
917
|
+
action: raw.action,
|
|
918
|
+
payAmount: amount,
|
|
919
|
+
paySymbol,
|
|
920
|
+
srcChainName: name(resolved.srcChainId),
|
|
921
|
+
receiveAmount: amount,
|
|
922
|
+
receiveSymbol: paySymbol,
|
|
923
|
+
dstChainName: name(resolved.dstChainId),
|
|
924
|
+
recipient: resolved.recipient,
|
|
925
|
+
etaSeconds: 15,
|
|
926
|
+
solverCount: 1
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
const res = await api.getQuotes(buildQuoteRequest(resolved));
|
|
931
|
+
const best = res.quotes?.[0];
|
|
932
|
+
if (!best) throw new Error("No solver returned a quote for this route yet.");
|
|
933
|
+
let needsApproval = false;
|
|
934
|
+
if (best.order.type === "oif-escrow-v0") {
|
|
935
|
+
const pc = await reader(resolved.srcChainId).catch(() => null);
|
|
936
|
+
needsApproval = pc ? !await hasPermit2Allowance(pc, resolved.inputToken, resolved.user, resolved.inputAmount).catch(() => false) : true;
|
|
937
|
+
}
|
|
938
|
+
if (gen !== generation) return;
|
|
939
|
+
set({
|
|
940
|
+
phase: "quoted",
|
|
941
|
+
resolved,
|
|
942
|
+
route: "solver",
|
|
943
|
+
quote: best,
|
|
944
|
+
needsApproval,
|
|
945
|
+
preview: {
|
|
946
|
+
action: raw.action,
|
|
947
|
+
payAmount: fmtUnits(resolved.inputAmount, resolved.inputDecimals),
|
|
948
|
+
paySymbol,
|
|
949
|
+
srcChainName: name(resolved.srcChainId),
|
|
950
|
+
receiveAmount: fmtUnits(best.preview.outputs?.[0]?.amount, outAsset?.decimals ?? resolved.inputDecimals),
|
|
951
|
+
receiveSymbol: outAsset?.symbol ?? (outSymbol || inAsset?.symbol || "?"),
|
|
952
|
+
dstChainName: name(resolved.dstChainId),
|
|
953
|
+
recipient: raw.action === "send" ? resolved.recipient : void 0,
|
|
954
|
+
etaSeconds: best.eta,
|
|
955
|
+
solverCount: res.totalQuotes || res.quotes.length || 1,
|
|
956
|
+
solversQueried: res.metadata?.solversQueried,
|
|
957
|
+
raceMs: res.metadata?.totalDurationMs
|
|
958
|
+
}
|
|
959
|
+
});
|
|
960
|
+
} catch (e) {
|
|
961
|
+
if (gen === generation) set({ phase: "error", error: errMessage(e) });
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
function advanceQueue(gen) {
|
|
965
|
+
const next = state.queueIndex + 1;
|
|
966
|
+
if (next >= queue.length) return false;
|
|
967
|
+
set({ queueIndex: next, route: null, quote: null, preview: null, orderId: null, status: null, order: null });
|
|
968
|
+
void quoteFor(queue[next], gen);
|
|
969
|
+
return true;
|
|
970
|
+
}
|
|
971
|
+
function track(orderId, gen) {
|
|
972
|
+
stopPolling();
|
|
973
|
+
const poll = async () => {
|
|
974
|
+
try {
|
|
975
|
+
const order = await api.getOrder(orderId);
|
|
976
|
+
if (gen !== generation) return;
|
|
977
|
+
set({ status: order.status, order });
|
|
978
|
+
if (isTerminalStatus(order.status)) {
|
|
979
|
+
if (!advanceQueue(gen)) set({ phase: "done" });
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
} catch {
|
|
983
|
+
}
|
|
984
|
+
if (gen === generation && !destroyed) pollTimer = setTimeout(poll, pollMs);
|
|
985
|
+
};
|
|
986
|
+
void poll();
|
|
987
|
+
}
|
|
988
|
+
async function confirm() {
|
|
989
|
+
const { resolved, route } = state;
|
|
990
|
+
const w = wallet;
|
|
991
|
+
if (!resolved || !route || !w) return;
|
|
992
|
+
const gen = generation;
|
|
993
|
+
try {
|
|
994
|
+
set({ error: null });
|
|
995
|
+
if (SOLANA_CHAIN_IDS.has(resolved.srcChainId)) {
|
|
996
|
+
throw new Error("Swaps from Solana are not supported yet.");
|
|
997
|
+
}
|
|
998
|
+
if (await w.getChainId() !== resolved.srcChainId) {
|
|
999
|
+
set({ phase: "switchingChain" });
|
|
1000
|
+
await w.switchChain(resolved.srcChainId);
|
|
1001
|
+
}
|
|
1002
|
+
if (route === "direct") {
|
|
1003
|
+
set({ phase: "signing" });
|
|
1004
|
+
const hash = isNativeToken(resolved.inputToken) ? await w.sendTransaction({ chainId: resolved.srcChainId, to: resolved.recipient, value: resolved.inputAmount }) : await w.writeContract({
|
|
1005
|
+
chainId: resolved.srcChainId,
|
|
1006
|
+
address: resolved.inputToken,
|
|
1007
|
+
abi: ERC20_TRANSFER_ABI,
|
|
1008
|
+
functionName: "transfer",
|
|
1009
|
+
args: [resolved.recipient, resolved.inputAmount]
|
|
1010
|
+
});
|
|
1011
|
+
if (gen !== generation) return;
|
|
1012
|
+
set({ orderId: hash, status: "finalized", order: null });
|
|
1013
|
+
if (!advanceQueue(gen)) set({ phase: "done" });
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
if (state.quote?.order.type === "oif-escrow-v0") {
|
|
1017
|
+
const pc = await reader(resolved.srcChainId).catch(() => null);
|
|
1018
|
+
const stillNeeds = pc ? !await hasPermit2Allowance(pc, resolved.inputToken, resolved.user, resolved.inputAmount) : state.needsApproval;
|
|
1019
|
+
if (stillNeeds) {
|
|
1020
|
+
set({ phase: "approving" });
|
|
1021
|
+
const approve = buildPermit2ApproveRequest(resolved.inputToken);
|
|
1022
|
+
const hash = await w.writeContract({ chainId: resolved.srcChainId, ...approve, abi: approve.abi });
|
|
1023
|
+
if (pc) await pc.waitForTransactionReceipt({ hash });
|
|
1024
|
+
set({ needsApproval: false });
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
set({ phase: "signing" });
|
|
1028
|
+
const fresh = (await api.getQuotes(buildQuoteRequest(resolved))).quotes?.[0];
|
|
1029
|
+
if (!fresh) throw new Error("The quote expired and no fresh quote is available \u2014 try again.");
|
|
1030
|
+
const signature = await signQuote(fresh, w.signTypedData);
|
|
1031
|
+
set({ phase: "submitting" });
|
|
1032
|
+
const order = await api.submitOrder(buildOrderRequest(fresh, signature));
|
|
1033
|
+
if (gen !== generation) return;
|
|
1034
|
+
set({ phase: "tracking", quote: fresh, orderId: order.orderId, status: order.status, order });
|
|
1035
|
+
track(order.orderId, gen);
|
|
1036
|
+
} catch (e) {
|
|
1037
|
+
if (gen === generation) set({ phase: "error", error: errMessage(e) });
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
async function run(text) {
|
|
1041
|
+
const gen = ++generation;
|
|
1042
|
+
stopPolling();
|
|
1043
|
+
set({ ...INITIAL, assetsLoading: state.assetsLoading, assetsError: state.assetsError, ready: state.ready, isConnected: Boolean(wallet), phase: "parsing" });
|
|
1044
|
+
const parsed = await parseIntent(text, { hfAccessToken, model: options.hfModel, fetch: api.fetch });
|
|
1045
|
+
if (gen !== generation) return;
|
|
1046
|
+
if (!parsed.ok) return set({ phase: "offTemplate", hint: parsed.message });
|
|
1047
|
+
for (const intent of parsed.intents) {
|
|
1048
|
+
const issues = validateIntent(intent, supported);
|
|
1049
|
+
if (issues.length) return set({ phase: "invalid", issues });
|
|
1050
|
+
}
|
|
1051
|
+
queue = parsed.intents;
|
|
1052
|
+
set({ queueIndex: 0, queueTotal: queue.length });
|
|
1053
|
+
await quoteFor(queue[0], gen);
|
|
1054
|
+
}
|
|
1055
|
+
function reset() {
|
|
1056
|
+
generation++;
|
|
1057
|
+
stopPolling();
|
|
1058
|
+
queue = [];
|
|
1059
|
+
set({ ...INITIAL, assetsLoading: state.assetsLoading, assetsError: state.assetsError, ready: state.ready, isConnected: Boolean(wallet) });
|
|
1060
|
+
}
|
|
1061
|
+
function setWallet(next) {
|
|
1062
|
+
const changedAccount = wallet?.address !== next?.address;
|
|
1063
|
+
wallet = next;
|
|
1064
|
+
set({ isConnected: Boolean(next) });
|
|
1065
|
+
if (next && (state.phase === "needsWallet" || changedAccount && state.phase === "quoted") && queue.length) {
|
|
1066
|
+
void quoteFor(queue[state.queueIndex], generation);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
void refreshAssets();
|
|
1070
|
+
return {
|
|
1071
|
+
getState: () => state,
|
|
1072
|
+
subscribe(listener) {
|
|
1073
|
+
listeners.add(listener);
|
|
1074
|
+
return () => listeners.delete(listener);
|
|
1075
|
+
},
|
|
1076
|
+
run,
|
|
1077
|
+
confirm,
|
|
1078
|
+
reset,
|
|
1079
|
+
setWallet,
|
|
1080
|
+
refreshAssets,
|
|
1081
|
+
setHfAccessToken(token) {
|
|
1082
|
+
hfAccessToken = token;
|
|
1083
|
+
},
|
|
1084
|
+
destroy() {
|
|
1085
|
+
generation++;
|
|
1086
|
+
stopPolling();
|
|
1087
|
+
destroyed = true;
|
|
1088
|
+
listeners.clear();
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// src/wallet.ts
|
|
1094
|
+
function viemWallet(client) {
|
|
1095
|
+
const account = client.account;
|
|
1096
|
+
if (!account) throw new Error("viemWallet: the WalletClient has no account attached");
|
|
1097
|
+
return {
|
|
1098
|
+
address: account.address,
|
|
1099
|
+
getChainId: () => client.getChainId(),
|
|
1100
|
+
switchChain: (id) => client.switchChain({ id }),
|
|
1101
|
+
signTypedData: (args) => client.signTypedData({
|
|
1102
|
+
account,
|
|
1103
|
+
domain: args.domain,
|
|
1104
|
+
types: stripDomainType(args.types),
|
|
1105
|
+
primaryType: args.primaryType,
|
|
1106
|
+
message: args.message
|
|
1107
|
+
}),
|
|
1108
|
+
writeContract: (req) => client.writeContract({
|
|
1109
|
+
account,
|
|
1110
|
+
chain: null,
|
|
1111
|
+
address: req.address,
|
|
1112
|
+
abi: req.abi,
|
|
1113
|
+
functionName: req.functionName,
|
|
1114
|
+
args: req.args
|
|
1115
|
+
}),
|
|
1116
|
+
sendTransaction: (req) => client.sendTransaction({ account, chain: null, to: req.to, value: req.value })
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
function stripDomainType(types) {
|
|
1120
|
+
const out = {};
|
|
1121
|
+
for (const [name, fields] of Object.entries(types)) if (name !== "EIP712Domain") out[name] = fields;
|
|
1122
|
+
return out;
|
|
1123
|
+
}
|
|
1124
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1125
|
+
0 && (module.exports = {
|
|
1126
|
+
ApiError,
|
|
1127
|
+
CHAIN_ALIASES,
|
|
1128
|
+
DEFAULT_API_URL,
|
|
1129
|
+
DEFAULT_INTENT_MODEL,
|
|
1130
|
+
EMPTY_SUPPORTED,
|
|
1131
|
+
HF_ROUTER_URL,
|
|
1132
|
+
MAX_UINT256,
|
|
1133
|
+
PERMIT2_ADDRESS,
|
|
1134
|
+
SOLANA_CHAIN_IDS,
|
|
1135
|
+
TEMPLATE_HINT,
|
|
1136
|
+
TOKEN_ALIASES,
|
|
1137
|
+
buildOrderRequest,
|
|
1138
|
+
buildPermit2ApproveRequest,
|
|
1139
|
+
buildQuoteRequest,
|
|
1140
|
+
buildSupportedSet,
|
|
1141
|
+
chainName,
|
|
1142
|
+
createApiClient,
|
|
1143
|
+
createIntentSession,
|
|
1144
|
+
explorerUrl,
|
|
1145
|
+
extractIntents,
|
|
1146
|
+
findAsset,
|
|
1147
|
+
getPermit2Allowance,
|
|
1148
|
+
hasPermit2Allowance,
|
|
1149
|
+
interopAddress,
|
|
1150
|
+
isTerminalStatus,
|
|
1151
|
+
normalizeChain,
|
|
1152
|
+
normalizeToken,
|
|
1153
|
+
parseIntent,
|
|
1154
|
+
resolveIntent,
|
|
1155
|
+
rpcReaders,
|
|
1156
|
+
signQuote,
|
|
1157
|
+
stripDomainType,
|
|
1158
|
+
validateIntent,
|
|
1159
|
+
viemWallet
|
|
1160
|
+
});
|