@recoilpay/intent-core 0.1.0 → 0.3.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 +42 -1
- package/dist/index.cjs +601 -15
- package/dist/index.d.cts +454 -29
- package/dist/index.d.ts +454 -29
- package/dist/index.js +580 -13
- package/package.json +6 -1
package/dist/index.cjs
CHANGED
|
@@ -21,12 +21,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
ApiError: () => ApiError,
|
|
24
|
+
BANK_DETAILS_HINT: () => BANK_DETAILS_HINT,
|
|
24
25
|
CHAIN_ALIASES: () => CHAIN_ALIASES,
|
|
25
26
|
DEFAULT_API_URL: () => DEFAULT_API_URL,
|
|
26
27
|
DEFAULT_INTENT_MODEL: () => DEFAULT_INTENT_MODEL,
|
|
27
28
|
EMPTY_SUPPORTED: () => EMPTY_SUPPORTED,
|
|
29
|
+
FIAT_CORRIDORS: () => FIAT_CORRIDORS,
|
|
30
|
+
FIAT_CURRENCY_ALIASES: () => FIAT_CURRENCY_ALIASES,
|
|
28
31
|
HF_ROUTER_URL: () => HF_ROUTER_URL,
|
|
29
32
|
MAX_UINT256: () => MAX_UINT256,
|
|
33
|
+
NG_BANKS: () => NG_BANKS,
|
|
30
34
|
PERMIT2_ADDRESS: () => PERMIT2_ADDRESS,
|
|
31
35
|
SOLANA_CHAIN_IDS: () => SOLANA_CHAIN_IDS,
|
|
32
36
|
TEMPLATE_HINT: () => TEMPLATE_HINT,
|
|
@@ -35,30 +39,45 @@ __export(index_exports, {
|
|
|
35
39
|
buildPermit2ApproveRequest: () => buildPermit2ApproveRequest,
|
|
36
40
|
buildQuoteRequest: () => buildQuoteRequest,
|
|
37
41
|
buildSupportedSet: () => buildSupportedSet,
|
|
42
|
+
canonicalPayeeIdentity: () => canonicalPayeeIdentity,
|
|
38
43
|
chainName: () => chainName,
|
|
44
|
+
checkLock: () => checkLock,
|
|
39
45
|
createApiClient: () => createApiClient,
|
|
40
46
|
createIntentSession: () => createIntentSession,
|
|
47
|
+
createOfframpSession: () => createOfframpSession,
|
|
41
48
|
explorerUrl: () => explorerUrl,
|
|
42
49
|
extractIntents: () => extractIntents,
|
|
50
|
+
fiatConfirmMessage: () => fiatConfirmMessage,
|
|
51
|
+
fiatDisputeMessage: () => fiatDisputeMessage,
|
|
52
|
+
fiatPaymentSentMessage: () => fiatPaymentSentMessage,
|
|
43
53
|
findAsset: () => findAsset,
|
|
54
|
+
formatFiat: () => formatFiat,
|
|
44
55
|
getPermit2Allowance: () => getPermit2Allowance,
|
|
45
56
|
hasPermit2Allowance: () => hasPermit2Allowance,
|
|
46
57
|
interopAddress: () => interopAddress,
|
|
47
58
|
isTerminalStatus: () => isTerminalStatus,
|
|
59
|
+
looksLikeBankDetails: () => looksLikeBankDetails,
|
|
48
60
|
normalizeChain: () => normalizeChain,
|
|
61
|
+
normalizeFiatCurrency: () => normalizeFiatCurrency,
|
|
49
62
|
normalizeToken: () => normalizeToken,
|
|
63
|
+
openPayee: () => openPayee,
|
|
50
64
|
parseIntent: () => parseIntent,
|
|
65
|
+
payeeCommitment: () => payeeCommitment,
|
|
66
|
+
payeeIdentity: () => payeeIdentity,
|
|
51
67
|
resolveIntent: () => resolveIntent,
|
|
68
|
+
resolveOfframp: () => resolveOfframp,
|
|
52
69
|
rpcReaders: () => rpcReaders,
|
|
70
|
+
sealPayee: () => sealPayee,
|
|
53
71
|
signQuote: () => signQuote,
|
|
54
72
|
stripDomainType: () => stripDomainType,
|
|
55
73
|
validateIntent: () => validateIntent,
|
|
74
|
+
validateOfframp: () => validateOfframp,
|
|
56
75
|
viemWallet: () => viemWallet
|
|
57
76
|
});
|
|
58
77
|
module.exports = __toCommonJS(index_exports);
|
|
59
78
|
|
|
60
79
|
// src/session.ts
|
|
61
|
-
var
|
|
80
|
+
var import_viem5 = require("viem");
|
|
62
81
|
|
|
63
82
|
// src/api.ts
|
|
64
83
|
var DEFAULT_API_URL = "https://api.recoilpay.com";
|
|
@@ -117,12 +136,29 @@ function createApiClient(options = {}) {
|
|
|
117
136
|
},
|
|
118
137
|
getQuotes: (req) => post("/quotes", req),
|
|
119
138
|
submitOrder: (req) => post("/orders", req),
|
|
120
|
-
getOrder: (id) => request(`/orders/${encodeURIComponent(id)}`)
|
|
139
|
+
getOrder: (id) => request(`/orders/${encodeURIComponent(id)}`),
|
|
140
|
+
// ── Fiat ramps (design §12.1) ──
|
|
141
|
+
rankFiatQuotes: (req) => post("/fiat/quotes", req),
|
|
142
|
+
openFiatTrade: (req) => post("/fiat/trades", req),
|
|
143
|
+
getFiatTrade: (id) => request(`/fiat/trades/${encodeURIComponent(id)}`),
|
|
144
|
+
/** Hand the signed lock to the solver's relay, which submits it and pays the gas. */
|
|
145
|
+
submitFiatLock: (id, signature) => post(`/fiat/trades/${encodeURIComponent(id)}/lock`, { signature }),
|
|
146
|
+
/** Report a lock the user submitted themselves; the aggregator reads it from the chain. */
|
|
147
|
+
reportFiatFunded: (id, txHash) => post(`/fiat/trades/${encodeURIComponent(id)}/funded`, txHash ? { txHash } : {}),
|
|
148
|
+
/** `signature` is over `fiatConfirmMessage(id)`. */
|
|
149
|
+
confirmFiatTrade: (id, signature) => post(`/fiat/trades/${encodeURIComponent(id)}/confirm`, { signature }),
|
|
150
|
+
/** `signature` is over `fiatDisputeMessage(id, reason)`. */
|
|
151
|
+
disputeFiatTrade: (id, reason, signature) => post(`/fiat/trades/${encodeURIComponent(id)}/dispute`, { reason, signature })
|
|
121
152
|
};
|
|
122
153
|
}
|
|
123
154
|
|
|
124
155
|
// 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>";
|
|
156
|
+
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> OR cash out <amount> <token> on <chain> to <currency> (e.g. cash out 50 USDC on Base to NGN)";
|
|
157
|
+
var BANK_DETAILS_HINT = "Leave bank details out of the sentence \u2014 you'll enter them privately on the next step. Try: cash out 50 USDC on Base to NGN";
|
|
158
|
+
function looksLikeBankDetails(text) {
|
|
159
|
+
const rest = text.replace(/0x[0-9a-fA-F]+/g, " ").replace(/[.,]\d+/g, "");
|
|
160
|
+
return /(\d[\s-]?){10,}/.test(rest);
|
|
161
|
+
}
|
|
126
162
|
var HF_ROUTER_URL = "https://router.huggingface.co/v1/chat/completions";
|
|
127
163
|
var DEFAULT_INTENT_MODEL = "Qwen/Qwen2.5-Coder-32B-Instruct";
|
|
128
164
|
var SYSTEM_PROMPT = `You are an intent extraction engine for a cross-chain swap protocol.
|
|
@@ -131,7 +167,7 @@ If the user specifies multiple intents (e.g. "swap X and then send Y"), return m
|
|
|
131
167
|
The JSON objects must perfectly match the following TypeScript interface:
|
|
132
168
|
|
|
133
169
|
type AmountKind = "token" | "usd";
|
|
134
|
-
type IntentAction = "swap" | "send";
|
|
170
|
+
type IntentAction = "swap" | "send" | "offramp";
|
|
135
171
|
|
|
136
172
|
interface RawIntent {
|
|
137
173
|
action: IntentAction;
|
|
@@ -142,14 +178,19 @@ interface RawIntent {
|
|
|
142
178
|
tokenOut: string | null; // target token (optional for send)
|
|
143
179
|
chainOut: string | null; // target chain (optional for send)
|
|
144
180
|
recipient: string | null; // address/ens (required for 'send', optional for 'swap')
|
|
181
|
+
fiatCurrency?: string | null; // 'offramp' only: the local currency to receive, e.g. "NGN" or "naira"
|
|
145
182
|
}
|
|
146
183
|
|
|
184
|
+
Use "offramp" when the user wants to cash out, sell, or withdraw crypto to a bank account or local currency.
|
|
185
|
+
For "offramp", tokenIn/chainIn/amount describe the crypto they give; tokenOut, chainOut and recipient are null.
|
|
186
|
+
Never put bank names, account numbers or personal names in any field.
|
|
187
|
+
|
|
147
188
|
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
189
|
var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
149
190
|
var readString = (v) => typeof v === "string" && v.trim().length > 0 ? v : null;
|
|
150
191
|
var readAmount = (v) => v === null || v === void 0 ? null : String(v);
|
|
151
192
|
var readAmountKind = (v) => v === "usd" ? "usd" : "token";
|
|
152
|
-
var readAction = (v) => v === "swap" || v === "send" ? v : null;
|
|
193
|
+
var readAction = (v) => v === "swap" || v === "send" || v === "offramp" ? v : null;
|
|
153
194
|
var offTemplate = (message = TEMPLATE_HINT) => ({ ok: false, offTemplate: true, message });
|
|
154
195
|
function extractIntents(content) {
|
|
155
196
|
let json = content.trim();
|
|
@@ -174,7 +215,7 @@ function extractIntents(content) {
|
|
|
174
215
|
for (const item of items) {
|
|
175
216
|
const action = readAction(item.action);
|
|
176
217
|
if (!action) return offTemplate();
|
|
177
|
-
|
|
218
|
+
const intent = {
|
|
178
219
|
action,
|
|
179
220
|
amount: readAmount(item.amount),
|
|
180
221
|
amountKind: readAmountKind(item.amountKind),
|
|
@@ -183,13 +224,16 @@ function extractIntents(content) {
|
|
|
183
224
|
tokenOut: readString(item.tokenOut),
|
|
184
225
|
chainOut: readString(item.chainOut),
|
|
185
226
|
recipient: readString(item.recipient)
|
|
186
|
-
}
|
|
227
|
+
};
|
|
228
|
+
if (action === "offramp") intent.fiatCurrency = readString(item.fiatCurrency);
|
|
229
|
+
intents.push(intent);
|
|
187
230
|
}
|
|
188
231
|
return { ok: true, intents };
|
|
189
232
|
}
|
|
190
233
|
async function parseIntent(text, options = {}) {
|
|
191
234
|
const norm = text.trim();
|
|
192
235
|
if (!norm) return offTemplate();
|
|
236
|
+
if (looksLikeBankDetails(norm)) return offTemplate(BANK_DETAILS_HINT);
|
|
193
237
|
const token = options.hfAccessToken?.trim();
|
|
194
238
|
if (!token) return offTemplate(`Natural-language parsing isn't set up here. ${TEMPLATE_HINT}`);
|
|
195
239
|
const doFetch = options.fetch ?? ((...args) => fetch(...args));
|
|
@@ -229,12 +273,15 @@ var CHAIN_NAMES = {
|
|
|
229
273
|
8453: "Base",
|
|
230
274
|
42161: "Arbitrum",
|
|
231
275
|
56: "BNB Chain",
|
|
276
|
+
204: "opBNB",
|
|
232
277
|
43114: "Avalanche",
|
|
233
278
|
11155111: "Ethereum Sepolia",
|
|
234
279
|
84532: "Base Sepolia",
|
|
235
280
|
11155420: "OP Sepolia",
|
|
236
281
|
421614: "Arbitrum Sepolia",
|
|
237
282
|
80002: "Polygon Amoy",
|
|
283
|
+
97: "BNB Chain Testnet",
|
|
284
|
+
5611: "opBNB Testnet",
|
|
238
285
|
9000000001: "Solana",
|
|
239
286
|
9000000002: "Solana Devnet"
|
|
240
287
|
};
|
|
@@ -245,12 +292,15 @@ var EXPLORERS = {
|
|
|
245
292
|
8453: "https://basescan.org",
|
|
246
293
|
42161: "https://arbiscan.io",
|
|
247
294
|
56: "https://bscscan.com",
|
|
295
|
+
204: "https://opbnb.bscscan.com",
|
|
248
296
|
43114: "https://snowtrace.io",
|
|
249
297
|
11155111: "https://sepolia.etherscan.io",
|
|
250
298
|
84532: "https://sepolia.basescan.org",
|
|
251
299
|
11155420: "https://sepolia-optimism.etherscan.io",
|
|
252
300
|
421614: "https://sepolia.arbiscan.io",
|
|
253
|
-
80002: "https://amoy.polygonscan.com"
|
|
301
|
+
80002: "https://amoy.polygonscan.com",
|
|
302
|
+
97: "https://testnet.bscscan.com",
|
|
303
|
+
5611: "https://testnet.opbnbscan.com"
|
|
254
304
|
};
|
|
255
305
|
var SOLANA_CHAIN_IDS = /* @__PURE__ */ new Set([9000000001, 9000000002]);
|
|
256
306
|
function chainName(chainId, info) {
|
|
@@ -310,9 +360,14 @@ var CHAIN_ALIASES = {
|
|
|
310
360
|
op: { id: 10, name: "Optimism" },
|
|
311
361
|
polygon: { id: 137, name: "Polygon" },
|
|
312
362
|
matic: { id: 137, name: "Polygon" },
|
|
363
|
+
bsc: { id: 56, name: "BNB Chain" },
|
|
364
|
+
bnb: { id: 56, name: "BNB Chain" },
|
|
365
|
+
"bnb chain": { id: 56, name: "BNB Chain" },
|
|
366
|
+
"binance smart chain": { id: 56, name: "BNB Chain" },
|
|
367
|
+
opbnb: { id: 204, name: "opBNB" },
|
|
313
368
|
solana: { id: 9000000002, name: "Solana Devnet" },
|
|
314
369
|
"sol network": { id: 9000000002, name: "Solana Devnet" },
|
|
315
|
-
// testnets — the solver settles across these
|
|
370
|
+
// testnets — the solver settles across these
|
|
316
371
|
"base sepolia": { id: 84532, name: "Base Sepolia" },
|
|
317
372
|
"op sepolia": { id: 11155420, name: "OP Sepolia" },
|
|
318
373
|
"optimism sepolia": { id: 11155420, name: "OP Sepolia" },
|
|
@@ -322,7 +377,9 @@ var CHAIN_ALIASES = {
|
|
|
322
377
|
"eth sepolia": { id: 11155111, name: "Ethereum Sepolia" },
|
|
323
378
|
sepolia: { id: 11155111, name: "Ethereum Sepolia" },
|
|
324
379
|
"polygon amoy": { id: 80002, name: "Polygon Amoy" },
|
|
325
|
-
amoy: { id: 80002, name: "Polygon Amoy" }
|
|
380
|
+
amoy: { id: 80002, name: "Polygon Amoy" },
|
|
381
|
+
"bsc testnet": { id: 97, name: "BNB Chain Testnet" },
|
|
382
|
+
"bnb testnet": { id: 97, name: "BNB Chain Testnet" }
|
|
326
383
|
};
|
|
327
384
|
function normalizeToken(raw) {
|
|
328
385
|
if (!raw) return null;
|
|
@@ -424,7 +481,8 @@ var LABEL = {
|
|
|
424
481
|
chainIn: "Source chain",
|
|
425
482
|
tokenOut: "Destination token",
|
|
426
483
|
chainOut: "Destination chain",
|
|
427
|
-
recipient: "Recipient"
|
|
484
|
+
recipient: "Recipient",
|
|
485
|
+
fiatCurrency: "Currency"
|
|
428
486
|
};
|
|
429
487
|
var EVM_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
430
488
|
var SOLANA_ADDRESS_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
@@ -693,6 +751,23 @@ var PERMIT2_TYPES = {
|
|
|
693
751
|
{ name: "amount", type: "uint256" }
|
|
694
752
|
]
|
|
695
753
|
};
|
|
754
|
+
var PERMIT2_ESCROW_LOCK_TYPES = {
|
|
755
|
+
PermitWitnessTransferFrom: [
|
|
756
|
+
{ name: "permitted", type: "TokenPermissions" },
|
|
757
|
+
{ name: "spender", type: "address" },
|
|
758
|
+
{ name: "nonce", type: "uint256" },
|
|
759
|
+
{ name: "deadline", type: "uint256" },
|
|
760
|
+
{ name: "witness", type: "EscrowLock" }
|
|
761
|
+
],
|
|
762
|
+
EscrowLock: [
|
|
763
|
+
{ name: "tradeId", type: "bytes32" },
|
|
764
|
+
{ name: "counterparty", type: "address" }
|
|
765
|
+
],
|
|
766
|
+
TokenPermissions: [
|
|
767
|
+
{ name: "token", type: "address" },
|
|
768
|
+
{ name: "amount", type: "uint256" }
|
|
769
|
+
]
|
|
770
|
+
};
|
|
696
771
|
var EIP3009_TYPES = {
|
|
697
772
|
TransferWithAuthorization: [
|
|
698
773
|
{ name: "from", type: "address" },
|
|
@@ -729,8 +804,11 @@ function extractDomain(payload) {
|
|
|
729
804
|
function getTypesForPayload(payload) {
|
|
730
805
|
switch (payload.primaryType) {
|
|
731
806
|
case "PermitBatchWitnessTransferFrom":
|
|
732
|
-
case "PermitWitnessTransferFrom":
|
|
733
807
|
return PERMIT2_TYPES;
|
|
808
|
+
// The batch types do not define this primary type at all, so mapping it
|
|
809
|
+
// to them made every single-token signature fail.
|
|
810
|
+
case "PermitWitnessTransferFrom":
|
|
811
|
+
return PERMIT2_ESCROW_LOCK_TYPES;
|
|
734
812
|
case "TransferWithAuthorization":
|
|
735
813
|
case "ReceiveWithAuthorization":
|
|
736
814
|
case "CancelAuthorization":
|
|
@@ -798,6 +876,105 @@ async function signQuote(quote, signTypedDataAsync) {
|
|
|
798
876
|
}
|
|
799
877
|
}
|
|
800
878
|
|
|
879
|
+
// src/fiat/intent.ts
|
|
880
|
+
var import_viem4 = require("viem");
|
|
881
|
+
|
|
882
|
+
// src/fiat/corridors.ts
|
|
883
|
+
var NG_BANKS = [
|
|
884
|
+
{ code: "044", name: "Access Bank" },
|
|
885
|
+
{ code: "023", name: "Citibank Nigeria" },
|
|
886
|
+
{ code: "050", name: "Ecobank Nigeria" },
|
|
887
|
+
{ code: "070", name: "Fidelity Bank" },
|
|
888
|
+
{ code: "011", name: "First Bank of Nigeria" },
|
|
889
|
+
{ code: "214", name: "First City Monument Bank" },
|
|
890
|
+
{ code: "058", name: "Guaranty Trust Bank" },
|
|
891
|
+
{ code: "082", name: "Keystone Bank" },
|
|
892
|
+
{ code: "50211", name: "Kuda Bank" },
|
|
893
|
+
{ code: "50515", name: "Moniepoint MFB" },
|
|
894
|
+
{ code: "999992", name: "OPay" },
|
|
895
|
+
{ code: "999991", name: "PalmPay" },
|
|
896
|
+
{ code: "076", name: "Polaris Bank" },
|
|
897
|
+
{ code: "101", name: "Providus Bank" },
|
|
898
|
+
{ code: "221", name: "Stanbic IBTC Bank" },
|
|
899
|
+
{ code: "068", name: "Standard Chartered Bank" },
|
|
900
|
+
{ code: "232", name: "Sterling Bank" },
|
|
901
|
+
{ code: "032", name: "Union Bank of Nigeria" },
|
|
902
|
+
{ code: "033", name: "United Bank For Africa" },
|
|
903
|
+
{ code: "215", name: "Unity Bank" },
|
|
904
|
+
{ code: "035", name: "Wema Bank" },
|
|
905
|
+
{ code: "057", name: "Zenith Bank" }
|
|
906
|
+
];
|
|
907
|
+
var FIAT_CORRIDORS = {
|
|
908
|
+
NGN: {
|
|
909
|
+
currency: "NGN",
|
|
910
|
+
rail: "ng.nip",
|
|
911
|
+
decimals: 2,
|
|
912
|
+
symbol: "\u20A6",
|
|
913
|
+
accountLabel: "Account number (NUBAN)",
|
|
914
|
+
accountPattern: /^\d{10}$/,
|
|
915
|
+
banks: NG_BANKS
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
var FIAT_CURRENCY_ALIASES = {
|
|
919
|
+
ngn: "NGN",
|
|
920
|
+
naira: "NGN",
|
|
921
|
+
"\u20A6": "NGN",
|
|
922
|
+
nigeria: "NGN",
|
|
923
|
+
nigerian: "NGN"
|
|
924
|
+
};
|
|
925
|
+
function normalizeFiatCurrency(raw) {
|
|
926
|
+
if (!raw) return null;
|
|
927
|
+
const k = raw.trim().toLowerCase();
|
|
928
|
+
return FIAT_CURRENCY_ALIASES[k] ?? (FIAT_CORRIDORS[k.toUpperCase()] ? k.toUpperCase() : null);
|
|
929
|
+
}
|
|
930
|
+
function formatFiat(minor, corridor) {
|
|
931
|
+
const n = typeof minor === "bigint" ? minor : BigInt(minor);
|
|
932
|
+
const scale = 10n ** BigInt(corridor.decimals);
|
|
933
|
+
const whole = (n / scale).toLocaleString("en-US");
|
|
934
|
+
if (corridor.decimals === 0) return whole;
|
|
935
|
+
return `${whole}.${(n % scale).toString().padStart(corridor.decimals, "0")}`;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// src/fiat/intent.ts
|
|
939
|
+
function validateOfframp(raw, supported) {
|
|
940
|
+
const issues = [];
|
|
941
|
+
if (!raw.amount) issues.push({ field: "amount", kind: "missing", message: "Amount is not stated." });
|
|
942
|
+
else if (!(Number(raw.amount) > 0)) issues.push({ field: "amount", kind: "unknown", message: "Amount must be a positive number." });
|
|
943
|
+
const chain = normalizeChain(raw.chainIn);
|
|
944
|
+
if (!raw.chainIn) issues.push({ field: "chainIn", kind: "missing", message: "Source chain is not stated." });
|
|
945
|
+
else if (!chain) issues.push({ field: "chainIn", kind: "unknown", message: `Chain '${raw.chainIn}' isn't recognized.` });
|
|
946
|
+
const sym = normalizeToken(raw.tokenIn);
|
|
947
|
+
if (!raw.tokenIn) issues.push({ field: "tokenIn", kind: "missing", message: "Source token is not stated." });
|
|
948
|
+
else if (!sym) issues.push({ field: "tokenIn", kind: "unknown", message: `Token '${raw.tokenIn}' isn't recognized.` });
|
|
949
|
+
else if (chain && !findAsset(supported, chain.id, sym)) {
|
|
950
|
+
issues.push({ field: "tokenIn", kind: "unsupported", message: `${sym} isn't supported on ${chain.name} yet.` });
|
|
951
|
+
}
|
|
952
|
+
const currency = normalizeFiatCurrency(raw.fiatCurrency);
|
|
953
|
+
if (!raw.fiatCurrency) {
|
|
954
|
+
issues.push({ field: "fiatCurrency", kind: "missing", message: "Say which currency to receive, e.g. NGN." });
|
|
955
|
+
} else if (!currency) {
|
|
956
|
+
const known = Object.keys(FIAT_CORRIDORS).join(", ");
|
|
957
|
+
issues.push({ field: "fiatCurrency", kind: "unsupported", message: `Cash out to ${raw.fiatCurrency} isn't available yet (only ${known}).` });
|
|
958
|
+
}
|
|
959
|
+
return issues;
|
|
960
|
+
}
|
|
961
|
+
function resolveOfframp(raw, supported) {
|
|
962
|
+
const chain = normalizeChain(raw.chainIn);
|
|
963
|
+
const sym = normalizeToken(raw.tokenIn);
|
|
964
|
+
const currency = normalizeFiatCurrency(raw.fiatCurrency);
|
|
965
|
+
const asset = chain && sym ? findAsset(supported, chain.id, sym) : void 0;
|
|
966
|
+
if (!chain || !asset || !currency || !raw.amount) throw new Error("resolveOfframp called on an invalid intent");
|
|
967
|
+
return {
|
|
968
|
+
chainId: chain.id,
|
|
969
|
+
chainName: chain.name,
|
|
970
|
+
asset: asset.symbol.toUpperCase(),
|
|
971
|
+
token: (0, import_viem4.getAddress)(asset.address),
|
|
972
|
+
decimals: asset.decimals,
|
|
973
|
+
amount: (0, import_viem4.parseUnits)(raw.amount, asset.decimals),
|
|
974
|
+
corridor: FIAT_CORRIDORS[currency]
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
|
|
801
978
|
// src/session.ts
|
|
802
979
|
var ERC20_TRANSFER_ABI = [
|
|
803
980
|
{
|
|
@@ -814,7 +991,7 @@ var ERC20_TRANSFER_ABI = [
|
|
|
814
991
|
var errMessage = (e) => e instanceof Error ? e.message : typeof e === "string" ? e : "Something went wrong.";
|
|
815
992
|
function fmtUnits(amount, decimals) {
|
|
816
993
|
if (amount === void 0) return "";
|
|
817
|
-
const human = (0,
|
|
994
|
+
const human = (0, import_viem5.formatUnits)(typeof amount === "bigint" ? amount : BigInt(amount), decimals);
|
|
818
995
|
const n = Number(human);
|
|
819
996
|
return Number.isFinite(n) ? n.toLocaleString("en-US", { maximumFractionDigits: 6 }) : human;
|
|
820
997
|
}
|
|
@@ -849,7 +1026,8 @@ var INITIAL = {
|
|
|
849
1026
|
error: null,
|
|
850
1027
|
isConnected: false,
|
|
851
1028
|
queueIndex: 0,
|
|
852
|
-
queueTotal: 0
|
|
1029
|
+
queueTotal: 0,
|
|
1030
|
+
offramp: null
|
|
853
1031
|
};
|
|
854
1032
|
function createIntentSession(options = {}) {
|
|
855
1033
|
const api = options.api ?? createApiClient(options);
|
|
@@ -894,6 +1072,7 @@ function createIntentSession(options = {}) {
|
|
|
894
1072
|
}
|
|
895
1073
|
}
|
|
896
1074
|
async function quoteFor(raw, gen) {
|
|
1075
|
+
if (raw.action === "offramp") return;
|
|
897
1076
|
if (!wallet) return set({ phase: "needsWallet" });
|
|
898
1077
|
try {
|
|
899
1078
|
set({ phase: "quoting", error: null });
|
|
@@ -1044,6 +1223,12 @@ function createIntentSession(options = {}) {
|
|
|
1044
1223
|
const parsed = await parseIntent(text, { hfAccessToken, model: options.hfModel, fetch: api.fetch });
|
|
1045
1224
|
if (gen !== generation) return;
|
|
1046
1225
|
if (!parsed.ok) return set({ phase: "offTemplate", hint: parsed.message });
|
|
1226
|
+
const offramp = parsed.intents.find((i) => i.action === "offramp");
|
|
1227
|
+
if (offramp) {
|
|
1228
|
+
const issues = parsed.intents.length > 1 ? [{ field: "fiatCurrency", kind: "unsupported", message: "Cash out on its own, not in the same sentence as other actions." }] : validateOfframp(offramp, supported);
|
|
1229
|
+
if (issues.length) return set({ phase: "invalid", issues });
|
|
1230
|
+
return set({ phase: "offramp", offramp: resolveOfframp(offramp, supported) });
|
|
1231
|
+
}
|
|
1047
1232
|
for (const intent of parsed.intents) {
|
|
1048
1233
|
const issues = validateIntent(intent, supported);
|
|
1049
1234
|
if (issues.length) return set({ phase: "invalid", issues });
|
|
@@ -1113,7 +1298,8 @@ function viemWallet(client) {
|
|
|
1113
1298
|
functionName: req.functionName,
|
|
1114
1299
|
args: req.args
|
|
1115
1300
|
}),
|
|
1116
|
-
sendTransaction: (req) => client.sendTransaction({ account, chain: null, to: req.to, value: req.value })
|
|
1301
|
+
sendTransaction: (req) => client.sendTransaction({ account, chain: null, to: req.to, value: req.value }),
|
|
1302
|
+
signMessage: (message) => client.signMessage({ account, message })
|
|
1117
1303
|
};
|
|
1118
1304
|
}
|
|
1119
1305
|
function stripDomainType(types) {
|
|
@@ -1121,15 +1307,400 @@ function stripDomainType(types) {
|
|
|
1121
1307
|
for (const [name, fields] of Object.entries(types)) if (name !== "EIP712Domain") out[name] = fields;
|
|
1122
1308
|
return out;
|
|
1123
1309
|
}
|
|
1310
|
+
|
|
1311
|
+
// src/fiat/payee.ts
|
|
1312
|
+
var import_aes = require("@noble/ciphers/aes");
|
|
1313
|
+
var import_secp256k1 = require("@noble/curves/secp256k1");
|
|
1314
|
+
var import_hkdf = require("@noble/hashes/hkdf");
|
|
1315
|
+
var import_sha256 = require("@noble/hashes/sha256");
|
|
1316
|
+
var import_viem6 = require("viem");
|
|
1317
|
+
var ALG = "ECIES-secp256k1-AES256GCM";
|
|
1318
|
+
var ENVELOPE_VERSION = 1;
|
|
1319
|
+
var PAYLOAD_VERSION = 1;
|
|
1320
|
+
var GCM_TAG_BYTES = 16;
|
|
1321
|
+
var SALT_BYTES = 32;
|
|
1322
|
+
var MAX_NAME_LENGTH = 140;
|
|
1323
|
+
var HKDF_INFO = (0, import_viem6.stringToBytes)("recoilpay/fiat-payee/v1");
|
|
1324
|
+
var RAIL = /^[A-Za-z]{2}\.[A-Za-z0-9_]{1,30}$/;
|
|
1325
|
+
var BANK_CODE = /^[A-Za-z0-9]{0,16}$/;
|
|
1326
|
+
var ACCOUNT = /^\+?[A-Za-z0-9]{1,34}$/;
|
|
1327
|
+
var ASCII_SPACE = /[ \t\r\n]/g;
|
|
1328
|
+
function payeeIdentity(payee) {
|
|
1329
|
+
const rail = payee.rail.replace(ASCII_SPACE, "");
|
|
1330
|
+
const bankCode = payee.bankCode.replace(ASCII_SPACE, "");
|
|
1331
|
+
const account = payee.account.replace(ASCII_SPACE, "").replace(/-/g, "");
|
|
1332
|
+
if (!RAIL.test(rail)) throw new Error(`invalid rail: ${JSON.stringify(payee.rail)}`);
|
|
1333
|
+
if (!BANK_CODE.test(bankCode)) throw new Error("invalid bank code");
|
|
1334
|
+
if (!ACCOUNT.test(account)) throw new Error("invalid account number");
|
|
1335
|
+
return {
|
|
1336
|
+
rail: rail.toLowerCase(),
|
|
1337
|
+
bank_code: bankCode.toUpperCase(),
|
|
1338
|
+
account: account.toUpperCase()
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
function canonicalPayeeIdentity(identity) {
|
|
1342
|
+
const { account, bank_code, rail } = payeeIdentity({
|
|
1343
|
+
rail: identity.rail,
|
|
1344
|
+
bankCode: identity.bank_code,
|
|
1345
|
+
account: identity.account
|
|
1346
|
+
});
|
|
1347
|
+
return JSON.stringify({ account, bank_code, rail });
|
|
1348
|
+
}
|
|
1349
|
+
function payeeCommitment(identity, salt) {
|
|
1350
|
+
const saltBytes = (0, import_viem6.hexToBytes)(salt);
|
|
1351
|
+
if (saltBytes.length !== SALT_BYTES) throw new Error(`salt must be ${SALT_BYTES} bytes`);
|
|
1352
|
+
return (0, import_viem6.keccak256)((0, import_viem6.concat)([(0, import_viem6.stringToBytes)(canonicalPayeeIdentity(identity)), saltBytes]));
|
|
1353
|
+
}
|
|
1354
|
+
function normaliseName(name) {
|
|
1355
|
+
const n = name.normalize("NFC").trim().replace(/\s+/g, " ");
|
|
1356
|
+
if (n.length === 0) throw new Error("the account holder name is required");
|
|
1357
|
+
if (n.length > MAX_NAME_LENGTH) throw new Error("the account holder name is too long");
|
|
1358
|
+
return n;
|
|
1359
|
+
}
|
|
1360
|
+
function deriveKey(sharedX, ephemeralPub) {
|
|
1361
|
+
return (0, import_hkdf.hkdf)(import_sha256.sha256, sharedX, ephemeralPub, HKDF_INFO, 32);
|
|
1362
|
+
}
|
|
1363
|
+
function sealPayee(payee, solverPubkey, salt) {
|
|
1364
|
+
const identity = payeeIdentity(payee);
|
|
1365
|
+
const name = normaliseName(payee.name);
|
|
1366
|
+
const saltHex = salt ?? (0, import_viem6.bytesToHex)(crypto.getRandomValues(new Uint8Array(SALT_BYTES)));
|
|
1367
|
+
const commitment = payeeCommitment(identity, saltHex);
|
|
1368
|
+
const recipient = (0, import_viem6.hexToBytes)(solverPubkey);
|
|
1369
|
+
if (recipient.length !== 65 || recipient[0] !== 4) {
|
|
1370
|
+
throw new Error("solver public key must be 65 bytes uncompressed (0x04\u2026)");
|
|
1371
|
+
}
|
|
1372
|
+
const plaintext = (0, import_viem6.stringToBytes)(
|
|
1373
|
+
JSON.stringify({ v: PAYLOAD_VERSION, ...identity, name, salt: saltHex })
|
|
1374
|
+
);
|
|
1375
|
+
const ephemeralPriv = import_secp256k1.secp256k1.utils.randomPrivateKey();
|
|
1376
|
+
const ephemeralPub = import_secp256k1.secp256k1.getPublicKey(ephemeralPriv, false);
|
|
1377
|
+
const shared = import_secp256k1.secp256k1.getSharedSecret(ephemeralPriv, recipient, true).slice(1);
|
|
1378
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
1379
|
+
const out = (0, import_aes.gcm)(deriveKey(shared, ephemeralPub), iv).encrypt(plaintext);
|
|
1380
|
+
return {
|
|
1381
|
+
sealed: {
|
|
1382
|
+
v: ENVELOPE_VERSION,
|
|
1383
|
+
alg: ALG,
|
|
1384
|
+
epk: (0, import_viem6.bytesToHex)(ephemeralPub),
|
|
1385
|
+
iv: (0, import_viem6.bytesToHex)(iv),
|
|
1386
|
+
// @noble appends the tag; split so any standard GCM can read it.
|
|
1387
|
+
ct: (0, import_viem6.bytesToHex)(out.slice(0, out.length - GCM_TAG_BYTES)),
|
|
1388
|
+
tag: (0, import_viem6.bytesToHex)(out.slice(out.length - GCM_TAG_BYTES))
|
|
1389
|
+
},
|
|
1390
|
+
commitment,
|
|
1391
|
+
salt: saltHex
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
function openPayee(sealed, solverPrivkey, expectedCommitment) {
|
|
1395
|
+
if (sealed.alg !== ALG) throw new Error(`unsupported envelope algorithm: ${sealed.alg}`);
|
|
1396
|
+
if (sealed.v !== ENVELOPE_VERSION) throw new Error(`unsupported envelope version: ${sealed.v}`);
|
|
1397
|
+
const epk = (0, import_viem6.hexToBytes)(sealed.epk);
|
|
1398
|
+
const shared = import_secp256k1.secp256k1.getSharedSecret((0, import_viem6.hexToBytes)(solverPrivkey), epk, true).slice(1);
|
|
1399
|
+
const plaintext = (0, import_aes.gcm)(deriveKey(shared, epk), (0, import_viem6.hexToBytes)(sealed.iv)).decrypt(
|
|
1400
|
+
(0, import_viem6.concat)([(0, import_viem6.hexToBytes)(sealed.ct), (0, import_viem6.hexToBytes)(sealed.tag)])
|
|
1401
|
+
);
|
|
1402
|
+
const body = JSON.parse(new TextDecoder().decode(plaintext));
|
|
1403
|
+
if (body.v !== PAYLOAD_VERSION) throw new Error(`unsupported payee payload version: ${String(body.v)}`);
|
|
1404
|
+
const identity = payeeIdentity({
|
|
1405
|
+
rail: String(body.rail ?? ""),
|
|
1406
|
+
bankCode: String(body.bank_code ?? ""),
|
|
1407
|
+
account: String(body.account ?? "")
|
|
1408
|
+
});
|
|
1409
|
+
const salt = String(body.salt ?? "");
|
|
1410
|
+
if (payeeCommitment(identity, salt).toLowerCase() !== expectedCommitment.toLowerCase()) {
|
|
1411
|
+
throw new Error("sealed payee details do not match the trade commitment");
|
|
1412
|
+
}
|
|
1413
|
+
return { identity, name: normaliseName(String(body.name ?? "")), salt };
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// src/fiat/offramp.ts
|
|
1417
|
+
var import_viem7 = require("viem");
|
|
1418
|
+
|
|
1419
|
+
// src/fiat/actions.ts
|
|
1420
|
+
function fiatConfirmMessage(tradeId) {
|
|
1421
|
+
return `RecoilPay: I confirm I received the fiat payment for trade ${tradeId}.`;
|
|
1422
|
+
}
|
|
1423
|
+
function fiatDisputeMessage(tradeId, reason) {
|
|
1424
|
+
return `RecoilPay: I dispute trade ${tradeId}. Reason: ${reason.trim()}`;
|
|
1425
|
+
}
|
|
1426
|
+
function fiatPaymentSentMessage(tradeId, reference) {
|
|
1427
|
+
return `RecoilPay: I have sent the fiat payment for trade ${tradeId}. Reference: ${reference.trim()}`;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// src/fiat/offramp.ts
|
|
1431
|
+
var TERMINAL = ["settled_to_fiat_payer", "refunded_to_funder", "failed"];
|
|
1432
|
+
var PHASE_OF = {
|
|
1433
|
+
quoted: "locking",
|
|
1434
|
+
awaiting_fiat: "awaitingFiat",
|
|
1435
|
+
awaiting_confirmation: "awaitingConfirmation",
|
|
1436
|
+
disputed: "disputed",
|
|
1437
|
+
settled_to_fiat_payer: "settled",
|
|
1438
|
+
refunded_to_funder: "refunded",
|
|
1439
|
+
failed: "failed"
|
|
1440
|
+
};
|
|
1441
|
+
var PLAIN_TEXT = /^[\x20-\x7e]+$/;
|
|
1442
|
+
var MAX_REASON = 500;
|
|
1443
|
+
var errMessage2 = (e) => e instanceof Error ? e.message : typeof e === "string" ? e : "Something went wrong.";
|
|
1444
|
+
var same = (a, b) => typeof a === "string" && typeof b === "string" && a.toLowerCase() === b.toLowerCase();
|
|
1445
|
+
function fmtAsset(amount, decimals) {
|
|
1446
|
+
const n = Number((0, import_viem7.formatUnits)(amount, decimals));
|
|
1447
|
+
return n.toLocaleString("en-US", { maximumFractionDigits: 6 });
|
|
1448
|
+
}
|
|
1449
|
+
function checkLock(lock, trade, request, quote, user) {
|
|
1450
|
+
const m = lock.typedData.message;
|
|
1451
|
+
const d = lock.typedData.domain;
|
|
1452
|
+
const problems = [
|
|
1453
|
+
lock.typedData.primaryType !== "PermitWitnessTransferFrom" && "not a Permit2 witness transfer",
|
|
1454
|
+
!same(d.verifyingContract, PERMIT2_ADDRESS) && "not signed for Permit2",
|
|
1455
|
+
Number(d.chainId) !== request.chainId && "the wrong chain",
|
|
1456
|
+
!same(m.permitted?.token, request.token) && "the wrong token",
|
|
1457
|
+
m.permitted?.amount !== request.amount.toString() && "the wrong amount",
|
|
1458
|
+
!same(m.spender, lock.escrow) && "a spender other than the escrow",
|
|
1459
|
+
!same(m.witness?.tradeId, lock.tradeId) && "another trade",
|
|
1460
|
+
!same(m.witness?.counterparty, trade.solver_address) && "a counterparty other than the solver",
|
|
1461
|
+
trade.quote_id !== quote.quoteId && "another quote",
|
|
1462
|
+
trade.fiat_minor_units !== quote.fiatNet && "a payout other than the one quoted",
|
|
1463
|
+
!same(trade.user_address, user) && "another wallet"
|
|
1464
|
+
].filter(Boolean);
|
|
1465
|
+
if (problems.length) throw new Error(`Refusing to sign: the lock names ${problems.join(", ")}.`);
|
|
1466
|
+
}
|
|
1467
|
+
function fmtRate(rate) {
|
|
1468
|
+
const n = Number(rate);
|
|
1469
|
+
return Number.isFinite(n) ? n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 6 }) : rate;
|
|
1470
|
+
}
|
|
1471
|
+
var INITIAL2 = {
|
|
1472
|
+
phase: "idle",
|
|
1473
|
+
request: null,
|
|
1474
|
+
quote: null,
|
|
1475
|
+
preview: null,
|
|
1476
|
+
needsApproval: false,
|
|
1477
|
+
trade: null,
|
|
1478
|
+
payeeSalt: null,
|
|
1479
|
+
error: null
|
|
1480
|
+
};
|
|
1481
|
+
function createOfframpSession(options = {}) {
|
|
1482
|
+
const api = options.api ?? createApiClient(options);
|
|
1483
|
+
const pollMs = options.pollIntervalMs ?? 3e3;
|
|
1484
|
+
let chainsPromise = null;
|
|
1485
|
+
const chains = () => chainsPromise ??= api.getChains().catch((e) => (chainsPromise = null, Promise.reject(e)));
|
|
1486
|
+
const reader = options.chainReader ?? rpcReaders(chains);
|
|
1487
|
+
let wallet = options.wallet ?? null;
|
|
1488
|
+
let pollTimer;
|
|
1489
|
+
let destroyed = false;
|
|
1490
|
+
let generation = 0;
|
|
1491
|
+
let state = { ...INITIAL2 };
|
|
1492
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1493
|
+
function set(patch) {
|
|
1494
|
+
if (destroyed) return;
|
|
1495
|
+
state = { ...state, ...patch };
|
|
1496
|
+
for (const l of listeners) l(state);
|
|
1497
|
+
}
|
|
1498
|
+
const stopPolling = () => {
|
|
1499
|
+
if (pollTimer) clearTimeout(pollTimer);
|
|
1500
|
+
pollTimer = void 0;
|
|
1501
|
+
};
|
|
1502
|
+
function showTrade(trade, patch = {}) {
|
|
1503
|
+
set({ trade, phase: PHASE_OF[trade.state], ...patch });
|
|
1504
|
+
}
|
|
1505
|
+
function track(tradeId, gen) {
|
|
1506
|
+
stopPolling();
|
|
1507
|
+
const poll = async () => {
|
|
1508
|
+
try {
|
|
1509
|
+
const trade = await api.getFiatTrade(tradeId);
|
|
1510
|
+
if (gen !== generation) return;
|
|
1511
|
+
if (state.phase === "confirming" || state.phase === "disputing") set({ trade });
|
|
1512
|
+
else showTrade(trade);
|
|
1513
|
+
if (TERMINAL.includes(trade.state)) return;
|
|
1514
|
+
} catch {
|
|
1515
|
+
}
|
|
1516
|
+
if (gen !== generation || destroyed) return;
|
|
1517
|
+
const wait = state.trade?.state === "disputed" ? pollMs * 10 : pollMs;
|
|
1518
|
+
pollTimer = setTimeout(poll, wait);
|
|
1519
|
+
};
|
|
1520
|
+
void poll();
|
|
1521
|
+
}
|
|
1522
|
+
async function quote(request) {
|
|
1523
|
+
const gen = ++generation;
|
|
1524
|
+
stopPolling();
|
|
1525
|
+
set({ ...INITIAL2, request, phase: wallet ? "quoting" : "needsWallet" });
|
|
1526
|
+
if (!wallet) return;
|
|
1527
|
+
try {
|
|
1528
|
+
const res = await api.rankFiatQuotes({
|
|
1529
|
+
direction: "offramp",
|
|
1530
|
+
chain: `eip155:${request.chainId}`,
|
|
1531
|
+
asset: request.asset,
|
|
1532
|
+
fiatCurrency: request.corridor.currency,
|
|
1533
|
+
rail: request.corridor.rail,
|
|
1534
|
+
assetAmount: request.amount.toString()
|
|
1535
|
+
});
|
|
1536
|
+
const best = res.quotes?.[0];
|
|
1537
|
+
if (!best) throw new Error(`No solver is cashing out ${request.asset} on ${request.chainName} to ${request.corridor.currency} for this amount right now.`);
|
|
1538
|
+
const pc = await reader(request.chainId).catch(() => null);
|
|
1539
|
+
const needsApproval = pc ? !await hasPermit2Allowance(pc, request.token, wallet.address, request.amount).catch(() => false) : true;
|
|
1540
|
+
if (gen !== generation) return;
|
|
1541
|
+
const c = request.corridor;
|
|
1542
|
+
set({
|
|
1543
|
+
phase: "quoted",
|
|
1544
|
+
quote: best,
|
|
1545
|
+
needsApproval,
|
|
1546
|
+
preview: {
|
|
1547
|
+
payAmount: fmtAsset(request.amount, request.decimals),
|
|
1548
|
+
paySymbol: request.asset,
|
|
1549
|
+
chainName: request.chainName,
|
|
1550
|
+
receiveAmount: formatFiat(best.fiatNet, c),
|
|
1551
|
+
currency: c.currency,
|
|
1552
|
+
currencySymbol: c.symbol,
|
|
1553
|
+
fee: formatFiat(best.fiatFee, c),
|
|
1554
|
+
rate: fmtRate(best.rate),
|
|
1555
|
+
paymentWindowSecs: best.paymentWindowSecs,
|
|
1556
|
+
quotesConsidered: res.totalConsidered
|
|
1557
|
+
}
|
|
1558
|
+
});
|
|
1559
|
+
} catch (e) {
|
|
1560
|
+
if (gen === generation) set({ phase: "error", error: errMessage2(e) });
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
async function confirm(payee) {
|
|
1564
|
+
const { request, quote: chosen } = state;
|
|
1565
|
+
const w = wallet;
|
|
1566
|
+
if (!request || !chosen || !w || state.phase !== "quoted") return;
|
|
1567
|
+
const gen = generation;
|
|
1568
|
+
try {
|
|
1569
|
+
set({ error: null });
|
|
1570
|
+
const details = { ...payee, rail: request.corridor.rail };
|
|
1571
|
+
payeeIdentity(details);
|
|
1572
|
+
if (!request.corridor.accountPattern.test(details.account.replace(/[\s-]/g, ""))) {
|
|
1573
|
+
throw new Error(`That doesn't look like a valid ${request.corridor.accountLabel.toLowerCase()}.`);
|
|
1574
|
+
}
|
|
1575
|
+
if (!details.name.trim()) throw new Error("Enter the account holder's name.");
|
|
1576
|
+
if (await w.getChainId() !== request.chainId) {
|
|
1577
|
+
set({ phase: "switchingChain" });
|
|
1578
|
+
await w.switchChain(request.chainId);
|
|
1579
|
+
}
|
|
1580
|
+
const pc = await reader(request.chainId).catch(() => null);
|
|
1581
|
+
const needsApproval = pc ? !await hasPermit2Allowance(pc, request.token, w.address, request.amount) : state.needsApproval;
|
|
1582
|
+
if (needsApproval) {
|
|
1583
|
+
set({ phase: "approving" });
|
|
1584
|
+
const approve = buildPermit2ApproveRequest(request.token);
|
|
1585
|
+
const hash = await w.writeContract({ chainId: request.chainId, ...approve, abi: approve.abi });
|
|
1586
|
+
if (pc) await pc.waitForTransactionReceipt({ hash });
|
|
1587
|
+
set({ needsApproval: false });
|
|
1588
|
+
}
|
|
1589
|
+
set({ phase: "opening" });
|
|
1590
|
+
const { sealed, commitment, salt } = sealPayee(details, chosen.payeeKey);
|
|
1591
|
+
const opened = await api.openFiatTrade({
|
|
1592
|
+
quoteId: chosen.quoteId,
|
|
1593
|
+
assetAmount: request.amount.toString(),
|
|
1594
|
+
userAddress: (0, import_viem7.getAddress)(w.address),
|
|
1595
|
+
payee: { sealed, commitment }
|
|
1596
|
+
});
|
|
1597
|
+
if (gen !== generation) return;
|
|
1598
|
+
set({ trade: opened.trade, payeeSalt: salt });
|
|
1599
|
+
checkLock(opened.lock, opened.trade, request, chosen, w.address);
|
|
1600
|
+
if (!same(opened.trade.payee_commitment, commitment)) throw new Error("Refusing to sign: the trade is not for the bank details you entered.");
|
|
1601
|
+
set({ phase: "signing" });
|
|
1602
|
+
const { domain, types, primaryType, message } = opened.lock.typedData;
|
|
1603
|
+
const signature = await w.signTypedData({ domain, types, primaryType, message });
|
|
1604
|
+
set({ phase: "locking" });
|
|
1605
|
+
const trade = await api.submitFiatLock(opened.trade.id, signature);
|
|
1606
|
+
if (gen !== generation) return;
|
|
1607
|
+
showTrade(trade);
|
|
1608
|
+
track(trade.id, gen);
|
|
1609
|
+
} catch (e) {
|
|
1610
|
+
if (gen !== generation) return;
|
|
1611
|
+
if (state.trade) {
|
|
1612
|
+
set({ error: errMessage2(e) });
|
|
1613
|
+
track(state.trade.id, gen);
|
|
1614
|
+
} else set({ phase: "error", error: errMessage2(e) });
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
async function act(phase, message, send) {
|
|
1618
|
+
const trade = state.trade;
|
|
1619
|
+
const w = wallet;
|
|
1620
|
+
if (!trade || trade.state !== "awaiting_confirmation") return;
|
|
1621
|
+
const gen = generation;
|
|
1622
|
+
const before = state.phase;
|
|
1623
|
+
try {
|
|
1624
|
+
if (!w) throw new Error("Connect the wallet that opened this trade.");
|
|
1625
|
+
if (!same(w.address, trade.user_address)) throw new Error("Switch to the wallet that opened this trade.");
|
|
1626
|
+
if (!w.signMessage) throw new Error("This wallet can't sign messages, so it can't confirm or dispute.");
|
|
1627
|
+
set({ phase, error: null });
|
|
1628
|
+
const signature = await w.signMessage(message(trade.id));
|
|
1629
|
+
const next = await send(trade.id, signature);
|
|
1630
|
+
if (gen !== generation) return;
|
|
1631
|
+
showTrade(next);
|
|
1632
|
+
track(next.id, gen);
|
|
1633
|
+
} catch (e) {
|
|
1634
|
+
if (gen === generation) set({ phase: before, error: errMessage2(e) });
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
async function dispute(reason) {
|
|
1638
|
+
const r = reason.trim();
|
|
1639
|
+
if (!r || r.length > MAX_REASON || !PLAIN_TEXT.test(r)) {
|
|
1640
|
+
set({ error: `Describe the problem in 1 to ${MAX_REASON} plain characters (letters, digits, punctuation).` });
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
await act("disputing", (id) => fiatDisputeMessage(id, r), (id, sig) => api.disputeFiatTrade(id, r, sig));
|
|
1644
|
+
}
|
|
1645
|
+
async function resume(tradeId) {
|
|
1646
|
+
const gen = ++generation;
|
|
1647
|
+
stopPolling();
|
|
1648
|
+
set({ ...INITIAL2, phase: "quoting" });
|
|
1649
|
+
try {
|
|
1650
|
+
const trade = await api.getFiatTrade(tradeId);
|
|
1651
|
+
if (gen !== generation) return;
|
|
1652
|
+
showTrade(trade);
|
|
1653
|
+
track(trade.id, gen);
|
|
1654
|
+
} catch (e) {
|
|
1655
|
+
if (gen === generation) set({ phase: "error", error: errMessage2(e) });
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
function reset() {
|
|
1659
|
+
generation++;
|
|
1660
|
+
stopPolling();
|
|
1661
|
+
set({ ...INITIAL2 });
|
|
1662
|
+
}
|
|
1663
|
+
function setWallet(next) {
|
|
1664
|
+
const changed = wallet?.address !== next?.address;
|
|
1665
|
+
wallet = next;
|
|
1666
|
+
if (next && state.request && !state.trade && (state.phase === "needsWallet" || changed && state.phase === "quoted")) {
|
|
1667
|
+
void quote(state.request);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
return {
|
|
1671
|
+
getState: () => state,
|
|
1672
|
+
subscribe(listener) {
|
|
1673
|
+
listeners.add(listener);
|
|
1674
|
+
return () => listeners.delete(listener);
|
|
1675
|
+
},
|
|
1676
|
+
quote,
|
|
1677
|
+
confirm,
|
|
1678
|
+
confirmReceived: () => act("confirming", fiatConfirmMessage, (id, sig) => api.confirmFiatTrade(id, sig)),
|
|
1679
|
+
dispute,
|
|
1680
|
+
resume,
|
|
1681
|
+
reset,
|
|
1682
|
+
setWallet,
|
|
1683
|
+
destroy() {
|
|
1684
|
+
generation++;
|
|
1685
|
+
stopPolling();
|
|
1686
|
+
destroyed = true;
|
|
1687
|
+
listeners.clear();
|
|
1688
|
+
}
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1124
1691
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1125
1692
|
0 && (module.exports = {
|
|
1126
1693
|
ApiError,
|
|
1694
|
+
BANK_DETAILS_HINT,
|
|
1127
1695
|
CHAIN_ALIASES,
|
|
1128
1696
|
DEFAULT_API_URL,
|
|
1129
1697
|
DEFAULT_INTENT_MODEL,
|
|
1130
1698
|
EMPTY_SUPPORTED,
|
|
1699
|
+
FIAT_CORRIDORS,
|
|
1700
|
+
FIAT_CURRENCY_ALIASES,
|
|
1131
1701
|
HF_ROUTER_URL,
|
|
1132
1702
|
MAX_UINT256,
|
|
1703
|
+
NG_BANKS,
|
|
1133
1704
|
PERMIT2_ADDRESS,
|
|
1134
1705
|
SOLANA_CHAIN_IDS,
|
|
1135
1706
|
TEMPLATE_HINT,
|
|
@@ -1138,23 +1709,38 @@ function stripDomainType(types) {
|
|
|
1138
1709
|
buildPermit2ApproveRequest,
|
|
1139
1710
|
buildQuoteRequest,
|
|
1140
1711
|
buildSupportedSet,
|
|
1712
|
+
canonicalPayeeIdentity,
|
|
1141
1713
|
chainName,
|
|
1714
|
+
checkLock,
|
|
1142
1715
|
createApiClient,
|
|
1143
1716
|
createIntentSession,
|
|
1717
|
+
createOfframpSession,
|
|
1144
1718
|
explorerUrl,
|
|
1145
1719
|
extractIntents,
|
|
1720
|
+
fiatConfirmMessage,
|
|
1721
|
+
fiatDisputeMessage,
|
|
1722
|
+
fiatPaymentSentMessage,
|
|
1146
1723
|
findAsset,
|
|
1724
|
+
formatFiat,
|
|
1147
1725
|
getPermit2Allowance,
|
|
1148
1726
|
hasPermit2Allowance,
|
|
1149
1727
|
interopAddress,
|
|
1150
1728
|
isTerminalStatus,
|
|
1729
|
+
looksLikeBankDetails,
|
|
1151
1730
|
normalizeChain,
|
|
1731
|
+
normalizeFiatCurrency,
|
|
1152
1732
|
normalizeToken,
|
|
1733
|
+
openPayee,
|
|
1153
1734
|
parseIntent,
|
|
1735
|
+
payeeCommitment,
|
|
1736
|
+
payeeIdentity,
|
|
1154
1737
|
resolveIntent,
|
|
1738
|
+
resolveOfframp,
|
|
1155
1739
|
rpcReaders,
|
|
1740
|
+
sealPayee,
|
|
1156
1741
|
signQuote,
|
|
1157
1742
|
stripDomainType,
|
|
1158
1743
|
validateIntent,
|
|
1744
|
+
validateOfframp,
|
|
1159
1745
|
viemWallet
|
|
1160
1746
|
});
|