@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.js
CHANGED
|
@@ -58,12 +58,29 @@ function createApiClient(options = {}) {
|
|
|
58
58
|
},
|
|
59
59
|
getQuotes: (req) => post("/quotes", req),
|
|
60
60
|
submitOrder: (req) => post("/orders", req),
|
|
61
|
-
getOrder: (id) => request(`/orders/${encodeURIComponent(id)}`)
|
|
61
|
+
getOrder: (id) => request(`/orders/${encodeURIComponent(id)}`),
|
|
62
|
+
// ── Fiat ramps (design §12.1) ──
|
|
63
|
+
rankFiatQuotes: (req) => post("/fiat/quotes", req),
|
|
64
|
+
openFiatTrade: (req) => post("/fiat/trades", req),
|
|
65
|
+
getFiatTrade: (id) => request(`/fiat/trades/${encodeURIComponent(id)}`),
|
|
66
|
+
/** Hand the signed lock to the solver's relay, which submits it and pays the gas. */
|
|
67
|
+
submitFiatLock: (id, signature) => post(`/fiat/trades/${encodeURIComponent(id)}/lock`, { signature }),
|
|
68
|
+
/** Report a lock the user submitted themselves; the aggregator reads it from the chain. */
|
|
69
|
+
reportFiatFunded: (id, txHash) => post(`/fiat/trades/${encodeURIComponent(id)}/funded`, txHash ? { txHash } : {}),
|
|
70
|
+
/** `signature` is over `fiatConfirmMessage(id)`. */
|
|
71
|
+
confirmFiatTrade: (id, signature) => post(`/fiat/trades/${encodeURIComponent(id)}/confirm`, { signature }),
|
|
72
|
+
/** `signature` is over `fiatDisputeMessage(id, reason)`. */
|
|
73
|
+
disputeFiatTrade: (id, reason, signature) => post(`/fiat/trades/${encodeURIComponent(id)}/dispute`, { reason, signature })
|
|
62
74
|
};
|
|
63
75
|
}
|
|
64
76
|
|
|
65
77
|
// src/parse.ts
|
|
66
|
-
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>";
|
|
78
|
+
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)";
|
|
79
|
+
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";
|
|
80
|
+
function looksLikeBankDetails(text) {
|
|
81
|
+
const rest = text.replace(/0x[0-9a-fA-F]+/g, " ").replace(/[.,]\d+/g, "");
|
|
82
|
+
return /(\d[\s-]?){10,}/.test(rest);
|
|
83
|
+
}
|
|
67
84
|
var HF_ROUTER_URL = "https://router.huggingface.co/v1/chat/completions";
|
|
68
85
|
var DEFAULT_INTENT_MODEL = "Qwen/Qwen2.5-Coder-32B-Instruct";
|
|
69
86
|
var SYSTEM_PROMPT = `You are an intent extraction engine for a cross-chain swap protocol.
|
|
@@ -72,7 +89,7 @@ If the user specifies multiple intents (e.g. "swap X and then send Y"), return m
|
|
|
72
89
|
The JSON objects must perfectly match the following TypeScript interface:
|
|
73
90
|
|
|
74
91
|
type AmountKind = "token" | "usd";
|
|
75
|
-
type IntentAction = "swap" | "send";
|
|
92
|
+
type IntentAction = "swap" | "send" | "offramp";
|
|
76
93
|
|
|
77
94
|
interface RawIntent {
|
|
78
95
|
action: IntentAction;
|
|
@@ -83,14 +100,19 @@ interface RawIntent {
|
|
|
83
100
|
tokenOut: string | null; // target token (optional for send)
|
|
84
101
|
chainOut: string | null; // target chain (optional for send)
|
|
85
102
|
recipient: string | null; // address/ens (required for 'send', optional for 'swap')
|
|
103
|
+
fiatCurrency?: string | null; // 'offramp' only: the local currency to receive, e.g. "NGN" or "naira"
|
|
86
104
|
}
|
|
87
105
|
|
|
106
|
+
Use "offramp" when the user wants to cash out, sell, or withdraw crypto to a bank account or local currency.
|
|
107
|
+
For "offramp", tokenIn/chainIn/amount describe the crypto they give; tokenOut, chainOut and recipient are null.
|
|
108
|
+
Never put bank names, account numbers or personal names in any field.
|
|
109
|
+
|
|
88
110
|
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.`;
|
|
89
111
|
var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
90
112
|
var readString = (v) => typeof v === "string" && v.trim().length > 0 ? v : null;
|
|
91
113
|
var readAmount = (v) => v === null || v === void 0 ? null : String(v);
|
|
92
114
|
var readAmountKind = (v) => v === "usd" ? "usd" : "token";
|
|
93
|
-
var readAction = (v) => v === "swap" || v === "send" ? v : null;
|
|
115
|
+
var readAction = (v) => v === "swap" || v === "send" || v === "offramp" ? v : null;
|
|
94
116
|
var offTemplate = (message = TEMPLATE_HINT) => ({ ok: false, offTemplate: true, message });
|
|
95
117
|
function extractIntents(content) {
|
|
96
118
|
let json = content.trim();
|
|
@@ -115,7 +137,7 @@ function extractIntents(content) {
|
|
|
115
137
|
for (const item of items) {
|
|
116
138
|
const action = readAction(item.action);
|
|
117
139
|
if (!action) return offTemplate();
|
|
118
|
-
|
|
140
|
+
const intent = {
|
|
119
141
|
action,
|
|
120
142
|
amount: readAmount(item.amount),
|
|
121
143
|
amountKind: readAmountKind(item.amountKind),
|
|
@@ -124,13 +146,16 @@ function extractIntents(content) {
|
|
|
124
146
|
tokenOut: readString(item.tokenOut),
|
|
125
147
|
chainOut: readString(item.chainOut),
|
|
126
148
|
recipient: readString(item.recipient)
|
|
127
|
-
}
|
|
149
|
+
};
|
|
150
|
+
if (action === "offramp") intent.fiatCurrency = readString(item.fiatCurrency);
|
|
151
|
+
intents.push(intent);
|
|
128
152
|
}
|
|
129
153
|
return { ok: true, intents };
|
|
130
154
|
}
|
|
131
155
|
async function parseIntent(text, options = {}) {
|
|
132
156
|
const norm = text.trim();
|
|
133
157
|
if (!norm) return offTemplate();
|
|
158
|
+
if (looksLikeBankDetails(norm)) return offTemplate(BANK_DETAILS_HINT);
|
|
134
159
|
const token = options.hfAccessToken?.trim();
|
|
135
160
|
if (!token) return offTemplate(`Natural-language parsing isn't set up here. ${TEMPLATE_HINT}`);
|
|
136
161
|
const doFetch = options.fetch ?? ((...args) => fetch(...args));
|
|
@@ -170,12 +195,15 @@ var CHAIN_NAMES = {
|
|
|
170
195
|
8453: "Base",
|
|
171
196
|
42161: "Arbitrum",
|
|
172
197
|
56: "BNB Chain",
|
|
198
|
+
204: "opBNB",
|
|
173
199
|
43114: "Avalanche",
|
|
174
200
|
11155111: "Ethereum Sepolia",
|
|
175
201
|
84532: "Base Sepolia",
|
|
176
202
|
11155420: "OP Sepolia",
|
|
177
203
|
421614: "Arbitrum Sepolia",
|
|
178
204
|
80002: "Polygon Amoy",
|
|
205
|
+
97: "BNB Chain Testnet",
|
|
206
|
+
5611: "opBNB Testnet",
|
|
179
207
|
9000000001: "Solana",
|
|
180
208
|
9000000002: "Solana Devnet"
|
|
181
209
|
};
|
|
@@ -186,12 +214,15 @@ var EXPLORERS = {
|
|
|
186
214
|
8453: "https://basescan.org",
|
|
187
215
|
42161: "https://arbiscan.io",
|
|
188
216
|
56: "https://bscscan.com",
|
|
217
|
+
204: "https://opbnb.bscscan.com",
|
|
189
218
|
43114: "https://snowtrace.io",
|
|
190
219
|
11155111: "https://sepolia.etherscan.io",
|
|
191
220
|
84532: "https://sepolia.basescan.org",
|
|
192
221
|
11155420: "https://sepolia-optimism.etherscan.io",
|
|
193
222
|
421614: "https://sepolia.arbiscan.io",
|
|
194
|
-
80002: "https://amoy.polygonscan.com"
|
|
223
|
+
80002: "https://amoy.polygonscan.com",
|
|
224
|
+
97: "https://testnet.bscscan.com",
|
|
225
|
+
5611: "https://testnet.opbnbscan.com"
|
|
195
226
|
};
|
|
196
227
|
var SOLANA_CHAIN_IDS = /* @__PURE__ */ new Set([9000000001, 9000000002]);
|
|
197
228
|
function chainName(chainId, info) {
|
|
@@ -251,9 +282,14 @@ var CHAIN_ALIASES = {
|
|
|
251
282
|
op: { id: 10, name: "Optimism" },
|
|
252
283
|
polygon: { id: 137, name: "Polygon" },
|
|
253
284
|
matic: { id: 137, name: "Polygon" },
|
|
285
|
+
bsc: { id: 56, name: "BNB Chain" },
|
|
286
|
+
bnb: { id: 56, name: "BNB Chain" },
|
|
287
|
+
"bnb chain": { id: 56, name: "BNB Chain" },
|
|
288
|
+
"binance smart chain": { id: 56, name: "BNB Chain" },
|
|
289
|
+
opbnb: { id: 204, name: "opBNB" },
|
|
254
290
|
solana: { id: 9000000002, name: "Solana Devnet" },
|
|
255
291
|
"sol network": { id: 9000000002, name: "Solana Devnet" },
|
|
256
|
-
// testnets — the solver settles across these
|
|
292
|
+
// testnets — the solver settles across these
|
|
257
293
|
"base sepolia": { id: 84532, name: "Base Sepolia" },
|
|
258
294
|
"op sepolia": { id: 11155420, name: "OP Sepolia" },
|
|
259
295
|
"optimism sepolia": { id: 11155420, name: "OP Sepolia" },
|
|
@@ -263,7 +299,9 @@ var CHAIN_ALIASES = {
|
|
|
263
299
|
"eth sepolia": { id: 11155111, name: "Ethereum Sepolia" },
|
|
264
300
|
sepolia: { id: 11155111, name: "Ethereum Sepolia" },
|
|
265
301
|
"polygon amoy": { id: 80002, name: "Polygon Amoy" },
|
|
266
|
-
amoy: { id: 80002, name: "Polygon Amoy" }
|
|
302
|
+
amoy: { id: 80002, name: "Polygon Amoy" },
|
|
303
|
+
"bsc testnet": { id: 97, name: "BNB Chain Testnet" },
|
|
304
|
+
"bnb testnet": { id: 97, name: "BNB Chain Testnet" }
|
|
267
305
|
};
|
|
268
306
|
function normalizeToken(raw) {
|
|
269
307
|
if (!raw) return null;
|
|
@@ -365,7 +403,8 @@ var LABEL = {
|
|
|
365
403
|
chainIn: "Source chain",
|
|
366
404
|
tokenOut: "Destination token",
|
|
367
405
|
chainOut: "Destination chain",
|
|
368
|
-
recipient: "Recipient"
|
|
406
|
+
recipient: "Recipient",
|
|
407
|
+
fiatCurrency: "Currency"
|
|
369
408
|
};
|
|
370
409
|
var EVM_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
371
410
|
var SOLANA_ADDRESS_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
@@ -634,6 +673,23 @@ var PERMIT2_TYPES = {
|
|
|
634
673
|
{ name: "amount", type: "uint256" }
|
|
635
674
|
]
|
|
636
675
|
};
|
|
676
|
+
var PERMIT2_ESCROW_LOCK_TYPES = {
|
|
677
|
+
PermitWitnessTransferFrom: [
|
|
678
|
+
{ name: "permitted", type: "TokenPermissions" },
|
|
679
|
+
{ name: "spender", type: "address" },
|
|
680
|
+
{ name: "nonce", type: "uint256" },
|
|
681
|
+
{ name: "deadline", type: "uint256" },
|
|
682
|
+
{ name: "witness", type: "EscrowLock" }
|
|
683
|
+
],
|
|
684
|
+
EscrowLock: [
|
|
685
|
+
{ name: "tradeId", type: "bytes32" },
|
|
686
|
+
{ name: "counterparty", type: "address" }
|
|
687
|
+
],
|
|
688
|
+
TokenPermissions: [
|
|
689
|
+
{ name: "token", type: "address" },
|
|
690
|
+
{ name: "amount", type: "uint256" }
|
|
691
|
+
]
|
|
692
|
+
};
|
|
637
693
|
var EIP3009_TYPES = {
|
|
638
694
|
TransferWithAuthorization: [
|
|
639
695
|
{ name: "from", type: "address" },
|
|
@@ -670,8 +726,11 @@ function extractDomain(payload) {
|
|
|
670
726
|
function getTypesForPayload(payload) {
|
|
671
727
|
switch (payload.primaryType) {
|
|
672
728
|
case "PermitBatchWitnessTransferFrom":
|
|
673
|
-
case "PermitWitnessTransferFrom":
|
|
674
729
|
return PERMIT2_TYPES;
|
|
730
|
+
// The batch types do not define this primary type at all, so mapping it
|
|
731
|
+
// to them made every single-token signature fail.
|
|
732
|
+
case "PermitWitnessTransferFrom":
|
|
733
|
+
return PERMIT2_ESCROW_LOCK_TYPES;
|
|
675
734
|
case "TransferWithAuthorization":
|
|
676
735
|
case "ReceiveWithAuthorization":
|
|
677
736
|
case "CancelAuthorization":
|
|
@@ -739,6 +798,105 @@ async function signQuote(quote, signTypedDataAsync) {
|
|
|
739
798
|
}
|
|
740
799
|
}
|
|
741
800
|
|
|
801
|
+
// src/fiat/intent.ts
|
|
802
|
+
import { getAddress as getAddress2, parseUnits as parseUnits2 } from "viem";
|
|
803
|
+
|
|
804
|
+
// src/fiat/corridors.ts
|
|
805
|
+
var NG_BANKS = [
|
|
806
|
+
{ code: "044", name: "Access Bank" },
|
|
807
|
+
{ code: "023", name: "Citibank Nigeria" },
|
|
808
|
+
{ code: "050", name: "Ecobank Nigeria" },
|
|
809
|
+
{ code: "070", name: "Fidelity Bank" },
|
|
810
|
+
{ code: "011", name: "First Bank of Nigeria" },
|
|
811
|
+
{ code: "214", name: "First City Monument Bank" },
|
|
812
|
+
{ code: "058", name: "Guaranty Trust Bank" },
|
|
813
|
+
{ code: "082", name: "Keystone Bank" },
|
|
814
|
+
{ code: "50211", name: "Kuda Bank" },
|
|
815
|
+
{ code: "50515", name: "Moniepoint MFB" },
|
|
816
|
+
{ code: "999992", name: "OPay" },
|
|
817
|
+
{ code: "999991", name: "PalmPay" },
|
|
818
|
+
{ code: "076", name: "Polaris Bank" },
|
|
819
|
+
{ code: "101", name: "Providus Bank" },
|
|
820
|
+
{ code: "221", name: "Stanbic IBTC Bank" },
|
|
821
|
+
{ code: "068", name: "Standard Chartered Bank" },
|
|
822
|
+
{ code: "232", name: "Sterling Bank" },
|
|
823
|
+
{ code: "032", name: "Union Bank of Nigeria" },
|
|
824
|
+
{ code: "033", name: "United Bank For Africa" },
|
|
825
|
+
{ code: "215", name: "Unity Bank" },
|
|
826
|
+
{ code: "035", name: "Wema Bank" },
|
|
827
|
+
{ code: "057", name: "Zenith Bank" }
|
|
828
|
+
];
|
|
829
|
+
var FIAT_CORRIDORS = {
|
|
830
|
+
NGN: {
|
|
831
|
+
currency: "NGN",
|
|
832
|
+
rail: "ng.nip",
|
|
833
|
+
decimals: 2,
|
|
834
|
+
symbol: "\u20A6",
|
|
835
|
+
accountLabel: "Account number (NUBAN)",
|
|
836
|
+
accountPattern: /^\d{10}$/,
|
|
837
|
+
banks: NG_BANKS
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
var FIAT_CURRENCY_ALIASES = {
|
|
841
|
+
ngn: "NGN",
|
|
842
|
+
naira: "NGN",
|
|
843
|
+
"\u20A6": "NGN",
|
|
844
|
+
nigeria: "NGN",
|
|
845
|
+
nigerian: "NGN"
|
|
846
|
+
};
|
|
847
|
+
function normalizeFiatCurrency(raw) {
|
|
848
|
+
if (!raw) return null;
|
|
849
|
+
const k = raw.trim().toLowerCase();
|
|
850
|
+
return FIAT_CURRENCY_ALIASES[k] ?? (FIAT_CORRIDORS[k.toUpperCase()] ? k.toUpperCase() : null);
|
|
851
|
+
}
|
|
852
|
+
function formatFiat(minor, corridor) {
|
|
853
|
+
const n = typeof minor === "bigint" ? minor : BigInt(minor);
|
|
854
|
+
const scale = 10n ** BigInt(corridor.decimals);
|
|
855
|
+
const whole = (n / scale).toLocaleString("en-US");
|
|
856
|
+
if (corridor.decimals === 0) return whole;
|
|
857
|
+
return `${whole}.${(n % scale).toString().padStart(corridor.decimals, "0")}`;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// src/fiat/intent.ts
|
|
861
|
+
function validateOfframp(raw, supported) {
|
|
862
|
+
const issues = [];
|
|
863
|
+
if (!raw.amount) issues.push({ field: "amount", kind: "missing", message: "Amount is not stated." });
|
|
864
|
+
else if (!(Number(raw.amount) > 0)) issues.push({ field: "amount", kind: "unknown", message: "Amount must be a positive number." });
|
|
865
|
+
const chain = normalizeChain(raw.chainIn);
|
|
866
|
+
if (!raw.chainIn) issues.push({ field: "chainIn", kind: "missing", message: "Source chain is not stated." });
|
|
867
|
+
else if (!chain) issues.push({ field: "chainIn", kind: "unknown", message: `Chain '${raw.chainIn}' isn't recognized.` });
|
|
868
|
+
const sym = normalizeToken(raw.tokenIn);
|
|
869
|
+
if (!raw.tokenIn) issues.push({ field: "tokenIn", kind: "missing", message: "Source token is not stated." });
|
|
870
|
+
else if (!sym) issues.push({ field: "tokenIn", kind: "unknown", message: `Token '${raw.tokenIn}' isn't recognized.` });
|
|
871
|
+
else if (chain && !findAsset(supported, chain.id, sym)) {
|
|
872
|
+
issues.push({ field: "tokenIn", kind: "unsupported", message: `${sym} isn't supported on ${chain.name} yet.` });
|
|
873
|
+
}
|
|
874
|
+
const currency = normalizeFiatCurrency(raw.fiatCurrency);
|
|
875
|
+
if (!raw.fiatCurrency) {
|
|
876
|
+
issues.push({ field: "fiatCurrency", kind: "missing", message: "Say which currency to receive, e.g. NGN." });
|
|
877
|
+
} else if (!currency) {
|
|
878
|
+
const known = Object.keys(FIAT_CORRIDORS).join(", ");
|
|
879
|
+
issues.push({ field: "fiatCurrency", kind: "unsupported", message: `Cash out to ${raw.fiatCurrency} isn't available yet (only ${known}).` });
|
|
880
|
+
}
|
|
881
|
+
return issues;
|
|
882
|
+
}
|
|
883
|
+
function resolveOfframp(raw, supported) {
|
|
884
|
+
const chain = normalizeChain(raw.chainIn);
|
|
885
|
+
const sym = normalizeToken(raw.tokenIn);
|
|
886
|
+
const currency = normalizeFiatCurrency(raw.fiatCurrency);
|
|
887
|
+
const asset = chain && sym ? findAsset(supported, chain.id, sym) : void 0;
|
|
888
|
+
if (!chain || !asset || !currency || !raw.amount) throw new Error("resolveOfframp called on an invalid intent");
|
|
889
|
+
return {
|
|
890
|
+
chainId: chain.id,
|
|
891
|
+
chainName: chain.name,
|
|
892
|
+
asset: asset.symbol.toUpperCase(),
|
|
893
|
+
token: getAddress2(asset.address),
|
|
894
|
+
decimals: asset.decimals,
|
|
895
|
+
amount: parseUnits2(raw.amount, asset.decimals),
|
|
896
|
+
corridor: FIAT_CORRIDORS[currency]
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
|
|
742
900
|
// src/session.ts
|
|
743
901
|
var ERC20_TRANSFER_ABI = [
|
|
744
902
|
{
|
|
@@ -790,7 +948,8 @@ var INITIAL = {
|
|
|
790
948
|
error: null,
|
|
791
949
|
isConnected: false,
|
|
792
950
|
queueIndex: 0,
|
|
793
|
-
queueTotal: 0
|
|
951
|
+
queueTotal: 0,
|
|
952
|
+
offramp: null
|
|
794
953
|
};
|
|
795
954
|
function createIntentSession(options = {}) {
|
|
796
955
|
const api = options.api ?? createApiClient(options);
|
|
@@ -835,6 +994,7 @@ function createIntentSession(options = {}) {
|
|
|
835
994
|
}
|
|
836
995
|
}
|
|
837
996
|
async function quoteFor(raw, gen) {
|
|
997
|
+
if (raw.action === "offramp") return;
|
|
838
998
|
if (!wallet) return set({ phase: "needsWallet" });
|
|
839
999
|
try {
|
|
840
1000
|
set({ phase: "quoting", error: null });
|
|
@@ -985,6 +1145,12 @@ function createIntentSession(options = {}) {
|
|
|
985
1145
|
const parsed = await parseIntent(text, { hfAccessToken, model: options.hfModel, fetch: api.fetch });
|
|
986
1146
|
if (gen !== generation) return;
|
|
987
1147
|
if (!parsed.ok) return set({ phase: "offTemplate", hint: parsed.message });
|
|
1148
|
+
const offramp = parsed.intents.find((i) => i.action === "offramp");
|
|
1149
|
+
if (offramp) {
|
|
1150
|
+
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);
|
|
1151
|
+
if (issues.length) return set({ phase: "invalid", issues });
|
|
1152
|
+
return set({ phase: "offramp", offramp: resolveOfframp(offramp, supported) });
|
|
1153
|
+
}
|
|
988
1154
|
for (const intent of parsed.intents) {
|
|
989
1155
|
const issues = validateIntent(intent, supported);
|
|
990
1156
|
if (issues.length) return set({ phase: "invalid", issues });
|
|
@@ -1054,7 +1220,8 @@ function viemWallet(client) {
|
|
|
1054
1220
|
functionName: req.functionName,
|
|
1055
1221
|
args: req.args
|
|
1056
1222
|
}),
|
|
1057
|
-
sendTransaction: (req) => client.sendTransaction({ account, chain: null, to: req.to, value: req.value })
|
|
1223
|
+
sendTransaction: (req) => client.sendTransaction({ account, chain: null, to: req.to, value: req.value }),
|
|
1224
|
+
signMessage: (message) => client.signMessage({ account, message })
|
|
1058
1225
|
};
|
|
1059
1226
|
}
|
|
1060
1227
|
function stripDomainType(types) {
|
|
@@ -1062,14 +1229,399 @@ function stripDomainType(types) {
|
|
|
1062
1229
|
for (const [name, fields] of Object.entries(types)) if (name !== "EIP712Domain") out[name] = fields;
|
|
1063
1230
|
return out;
|
|
1064
1231
|
}
|
|
1232
|
+
|
|
1233
|
+
// src/fiat/payee.ts
|
|
1234
|
+
import { gcm } from "@noble/ciphers/aes";
|
|
1235
|
+
import { secp256k1 } from "@noble/curves/secp256k1";
|
|
1236
|
+
import { hkdf } from "@noble/hashes/hkdf";
|
|
1237
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
1238
|
+
import { bytesToHex as bytesToHex2, concat, hexToBytes as hexToBytes2, keccak256, stringToBytes } from "viem";
|
|
1239
|
+
var ALG = "ECIES-secp256k1-AES256GCM";
|
|
1240
|
+
var ENVELOPE_VERSION = 1;
|
|
1241
|
+
var PAYLOAD_VERSION = 1;
|
|
1242
|
+
var GCM_TAG_BYTES = 16;
|
|
1243
|
+
var SALT_BYTES = 32;
|
|
1244
|
+
var MAX_NAME_LENGTH = 140;
|
|
1245
|
+
var HKDF_INFO = stringToBytes("recoilpay/fiat-payee/v1");
|
|
1246
|
+
var RAIL = /^[A-Za-z]{2}\.[A-Za-z0-9_]{1,30}$/;
|
|
1247
|
+
var BANK_CODE = /^[A-Za-z0-9]{0,16}$/;
|
|
1248
|
+
var ACCOUNT = /^\+?[A-Za-z0-9]{1,34}$/;
|
|
1249
|
+
var ASCII_SPACE = /[ \t\r\n]/g;
|
|
1250
|
+
function payeeIdentity(payee) {
|
|
1251
|
+
const rail = payee.rail.replace(ASCII_SPACE, "");
|
|
1252
|
+
const bankCode = payee.bankCode.replace(ASCII_SPACE, "");
|
|
1253
|
+
const account = payee.account.replace(ASCII_SPACE, "").replace(/-/g, "");
|
|
1254
|
+
if (!RAIL.test(rail)) throw new Error(`invalid rail: ${JSON.stringify(payee.rail)}`);
|
|
1255
|
+
if (!BANK_CODE.test(bankCode)) throw new Error("invalid bank code");
|
|
1256
|
+
if (!ACCOUNT.test(account)) throw new Error("invalid account number");
|
|
1257
|
+
return {
|
|
1258
|
+
rail: rail.toLowerCase(),
|
|
1259
|
+
bank_code: bankCode.toUpperCase(),
|
|
1260
|
+
account: account.toUpperCase()
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
function canonicalPayeeIdentity(identity) {
|
|
1264
|
+
const { account, bank_code, rail } = payeeIdentity({
|
|
1265
|
+
rail: identity.rail,
|
|
1266
|
+
bankCode: identity.bank_code,
|
|
1267
|
+
account: identity.account
|
|
1268
|
+
});
|
|
1269
|
+
return JSON.stringify({ account, bank_code, rail });
|
|
1270
|
+
}
|
|
1271
|
+
function payeeCommitment(identity, salt) {
|
|
1272
|
+
const saltBytes = hexToBytes2(salt);
|
|
1273
|
+
if (saltBytes.length !== SALT_BYTES) throw new Error(`salt must be ${SALT_BYTES} bytes`);
|
|
1274
|
+
return keccak256(concat([stringToBytes(canonicalPayeeIdentity(identity)), saltBytes]));
|
|
1275
|
+
}
|
|
1276
|
+
function normaliseName(name) {
|
|
1277
|
+
const n = name.normalize("NFC").trim().replace(/\s+/g, " ");
|
|
1278
|
+
if (n.length === 0) throw new Error("the account holder name is required");
|
|
1279
|
+
if (n.length > MAX_NAME_LENGTH) throw new Error("the account holder name is too long");
|
|
1280
|
+
return n;
|
|
1281
|
+
}
|
|
1282
|
+
function deriveKey(sharedX, ephemeralPub) {
|
|
1283
|
+
return hkdf(sha256, sharedX, ephemeralPub, HKDF_INFO, 32);
|
|
1284
|
+
}
|
|
1285
|
+
function sealPayee(payee, solverPubkey, salt) {
|
|
1286
|
+
const identity = payeeIdentity(payee);
|
|
1287
|
+
const name = normaliseName(payee.name);
|
|
1288
|
+
const saltHex = salt ?? bytesToHex2(crypto.getRandomValues(new Uint8Array(SALT_BYTES)));
|
|
1289
|
+
const commitment = payeeCommitment(identity, saltHex);
|
|
1290
|
+
const recipient = hexToBytes2(solverPubkey);
|
|
1291
|
+
if (recipient.length !== 65 || recipient[0] !== 4) {
|
|
1292
|
+
throw new Error("solver public key must be 65 bytes uncompressed (0x04\u2026)");
|
|
1293
|
+
}
|
|
1294
|
+
const plaintext = stringToBytes(
|
|
1295
|
+
JSON.stringify({ v: PAYLOAD_VERSION, ...identity, name, salt: saltHex })
|
|
1296
|
+
);
|
|
1297
|
+
const ephemeralPriv = secp256k1.utils.randomPrivateKey();
|
|
1298
|
+
const ephemeralPub = secp256k1.getPublicKey(ephemeralPriv, false);
|
|
1299
|
+
const shared = secp256k1.getSharedSecret(ephemeralPriv, recipient, true).slice(1);
|
|
1300
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
1301
|
+
const out = gcm(deriveKey(shared, ephemeralPub), iv).encrypt(plaintext);
|
|
1302
|
+
return {
|
|
1303
|
+
sealed: {
|
|
1304
|
+
v: ENVELOPE_VERSION,
|
|
1305
|
+
alg: ALG,
|
|
1306
|
+
epk: bytesToHex2(ephemeralPub),
|
|
1307
|
+
iv: bytesToHex2(iv),
|
|
1308
|
+
// @noble appends the tag; split so any standard GCM can read it.
|
|
1309
|
+
ct: bytesToHex2(out.slice(0, out.length - GCM_TAG_BYTES)),
|
|
1310
|
+
tag: bytesToHex2(out.slice(out.length - GCM_TAG_BYTES))
|
|
1311
|
+
},
|
|
1312
|
+
commitment,
|
|
1313
|
+
salt: saltHex
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
function openPayee(sealed, solverPrivkey, expectedCommitment) {
|
|
1317
|
+
if (sealed.alg !== ALG) throw new Error(`unsupported envelope algorithm: ${sealed.alg}`);
|
|
1318
|
+
if (sealed.v !== ENVELOPE_VERSION) throw new Error(`unsupported envelope version: ${sealed.v}`);
|
|
1319
|
+
const epk = hexToBytes2(sealed.epk);
|
|
1320
|
+
const shared = secp256k1.getSharedSecret(hexToBytes2(solverPrivkey), epk, true).slice(1);
|
|
1321
|
+
const plaintext = gcm(deriveKey(shared, epk), hexToBytes2(sealed.iv)).decrypt(
|
|
1322
|
+
concat([hexToBytes2(sealed.ct), hexToBytes2(sealed.tag)])
|
|
1323
|
+
);
|
|
1324
|
+
const body = JSON.parse(new TextDecoder().decode(plaintext));
|
|
1325
|
+
if (body.v !== PAYLOAD_VERSION) throw new Error(`unsupported payee payload version: ${String(body.v)}`);
|
|
1326
|
+
const identity = payeeIdentity({
|
|
1327
|
+
rail: String(body.rail ?? ""),
|
|
1328
|
+
bankCode: String(body.bank_code ?? ""),
|
|
1329
|
+
account: String(body.account ?? "")
|
|
1330
|
+
});
|
|
1331
|
+
const salt = String(body.salt ?? "");
|
|
1332
|
+
if (payeeCommitment(identity, salt).toLowerCase() !== expectedCommitment.toLowerCase()) {
|
|
1333
|
+
throw new Error("sealed payee details do not match the trade commitment");
|
|
1334
|
+
}
|
|
1335
|
+
return { identity, name: normaliseName(String(body.name ?? "")), salt };
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// src/fiat/offramp.ts
|
|
1339
|
+
import { formatUnits as formatUnits2, getAddress as getAddress3 } from "viem";
|
|
1340
|
+
|
|
1341
|
+
// src/fiat/actions.ts
|
|
1342
|
+
function fiatConfirmMessage(tradeId) {
|
|
1343
|
+
return `RecoilPay: I confirm I received the fiat payment for trade ${tradeId}.`;
|
|
1344
|
+
}
|
|
1345
|
+
function fiatDisputeMessage(tradeId, reason) {
|
|
1346
|
+
return `RecoilPay: I dispute trade ${tradeId}. Reason: ${reason.trim()}`;
|
|
1347
|
+
}
|
|
1348
|
+
function fiatPaymentSentMessage(tradeId, reference) {
|
|
1349
|
+
return `RecoilPay: I have sent the fiat payment for trade ${tradeId}. Reference: ${reference.trim()}`;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// src/fiat/offramp.ts
|
|
1353
|
+
var TERMINAL = ["settled_to_fiat_payer", "refunded_to_funder", "failed"];
|
|
1354
|
+
var PHASE_OF = {
|
|
1355
|
+
quoted: "locking",
|
|
1356
|
+
awaiting_fiat: "awaitingFiat",
|
|
1357
|
+
awaiting_confirmation: "awaitingConfirmation",
|
|
1358
|
+
disputed: "disputed",
|
|
1359
|
+
settled_to_fiat_payer: "settled",
|
|
1360
|
+
refunded_to_funder: "refunded",
|
|
1361
|
+
failed: "failed"
|
|
1362
|
+
};
|
|
1363
|
+
var PLAIN_TEXT = /^[\x20-\x7e]+$/;
|
|
1364
|
+
var MAX_REASON = 500;
|
|
1365
|
+
var errMessage2 = (e) => e instanceof Error ? e.message : typeof e === "string" ? e : "Something went wrong.";
|
|
1366
|
+
var same = (a, b) => typeof a === "string" && typeof b === "string" && a.toLowerCase() === b.toLowerCase();
|
|
1367
|
+
function fmtAsset(amount, decimals) {
|
|
1368
|
+
const n = Number(formatUnits2(amount, decimals));
|
|
1369
|
+
return n.toLocaleString("en-US", { maximumFractionDigits: 6 });
|
|
1370
|
+
}
|
|
1371
|
+
function checkLock(lock, trade, request, quote, user) {
|
|
1372
|
+
const m = lock.typedData.message;
|
|
1373
|
+
const d = lock.typedData.domain;
|
|
1374
|
+
const problems = [
|
|
1375
|
+
lock.typedData.primaryType !== "PermitWitnessTransferFrom" && "not a Permit2 witness transfer",
|
|
1376
|
+
!same(d.verifyingContract, PERMIT2_ADDRESS) && "not signed for Permit2",
|
|
1377
|
+
Number(d.chainId) !== request.chainId && "the wrong chain",
|
|
1378
|
+
!same(m.permitted?.token, request.token) && "the wrong token",
|
|
1379
|
+
m.permitted?.amount !== request.amount.toString() && "the wrong amount",
|
|
1380
|
+
!same(m.spender, lock.escrow) && "a spender other than the escrow",
|
|
1381
|
+
!same(m.witness?.tradeId, lock.tradeId) && "another trade",
|
|
1382
|
+
!same(m.witness?.counterparty, trade.solver_address) && "a counterparty other than the solver",
|
|
1383
|
+
trade.quote_id !== quote.quoteId && "another quote",
|
|
1384
|
+
trade.fiat_minor_units !== quote.fiatNet && "a payout other than the one quoted",
|
|
1385
|
+
!same(trade.user_address, user) && "another wallet"
|
|
1386
|
+
].filter(Boolean);
|
|
1387
|
+
if (problems.length) throw new Error(`Refusing to sign: the lock names ${problems.join(", ")}.`);
|
|
1388
|
+
}
|
|
1389
|
+
function fmtRate(rate) {
|
|
1390
|
+
const n = Number(rate);
|
|
1391
|
+
return Number.isFinite(n) ? n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 6 }) : rate;
|
|
1392
|
+
}
|
|
1393
|
+
var INITIAL2 = {
|
|
1394
|
+
phase: "idle",
|
|
1395
|
+
request: null,
|
|
1396
|
+
quote: null,
|
|
1397
|
+
preview: null,
|
|
1398
|
+
needsApproval: false,
|
|
1399
|
+
trade: null,
|
|
1400
|
+
payeeSalt: null,
|
|
1401
|
+
error: null
|
|
1402
|
+
};
|
|
1403
|
+
function createOfframpSession(options = {}) {
|
|
1404
|
+
const api = options.api ?? createApiClient(options);
|
|
1405
|
+
const pollMs = options.pollIntervalMs ?? 3e3;
|
|
1406
|
+
let chainsPromise = null;
|
|
1407
|
+
const chains = () => chainsPromise ??= api.getChains().catch((e) => (chainsPromise = null, Promise.reject(e)));
|
|
1408
|
+
const reader = options.chainReader ?? rpcReaders(chains);
|
|
1409
|
+
let wallet = options.wallet ?? null;
|
|
1410
|
+
let pollTimer;
|
|
1411
|
+
let destroyed = false;
|
|
1412
|
+
let generation = 0;
|
|
1413
|
+
let state = { ...INITIAL2 };
|
|
1414
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1415
|
+
function set(patch) {
|
|
1416
|
+
if (destroyed) return;
|
|
1417
|
+
state = { ...state, ...patch };
|
|
1418
|
+
for (const l of listeners) l(state);
|
|
1419
|
+
}
|
|
1420
|
+
const stopPolling = () => {
|
|
1421
|
+
if (pollTimer) clearTimeout(pollTimer);
|
|
1422
|
+
pollTimer = void 0;
|
|
1423
|
+
};
|
|
1424
|
+
function showTrade(trade, patch = {}) {
|
|
1425
|
+
set({ trade, phase: PHASE_OF[trade.state], ...patch });
|
|
1426
|
+
}
|
|
1427
|
+
function track(tradeId, gen) {
|
|
1428
|
+
stopPolling();
|
|
1429
|
+
const poll = async () => {
|
|
1430
|
+
try {
|
|
1431
|
+
const trade = await api.getFiatTrade(tradeId);
|
|
1432
|
+
if (gen !== generation) return;
|
|
1433
|
+
if (state.phase === "confirming" || state.phase === "disputing") set({ trade });
|
|
1434
|
+
else showTrade(trade);
|
|
1435
|
+
if (TERMINAL.includes(trade.state)) return;
|
|
1436
|
+
} catch {
|
|
1437
|
+
}
|
|
1438
|
+
if (gen !== generation || destroyed) return;
|
|
1439
|
+
const wait = state.trade?.state === "disputed" ? pollMs * 10 : pollMs;
|
|
1440
|
+
pollTimer = setTimeout(poll, wait);
|
|
1441
|
+
};
|
|
1442
|
+
void poll();
|
|
1443
|
+
}
|
|
1444
|
+
async function quote(request) {
|
|
1445
|
+
const gen = ++generation;
|
|
1446
|
+
stopPolling();
|
|
1447
|
+
set({ ...INITIAL2, request, phase: wallet ? "quoting" : "needsWallet" });
|
|
1448
|
+
if (!wallet) return;
|
|
1449
|
+
try {
|
|
1450
|
+
const res = await api.rankFiatQuotes({
|
|
1451
|
+
direction: "offramp",
|
|
1452
|
+
chain: `eip155:${request.chainId}`,
|
|
1453
|
+
asset: request.asset,
|
|
1454
|
+
fiatCurrency: request.corridor.currency,
|
|
1455
|
+
rail: request.corridor.rail,
|
|
1456
|
+
assetAmount: request.amount.toString()
|
|
1457
|
+
});
|
|
1458
|
+
const best = res.quotes?.[0];
|
|
1459
|
+
if (!best) throw new Error(`No solver is cashing out ${request.asset} on ${request.chainName} to ${request.corridor.currency} for this amount right now.`);
|
|
1460
|
+
const pc = await reader(request.chainId).catch(() => null);
|
|
1461
|
+
const needsApproval = pc ? !await hasPermit2Allowance(pc, request.token, wallet.address, request.amount).catch(() => false) : true;
|
|
1462
|
+
if (gen !== generation) return;
|
|
1463
|
+
const c = request.corridor;
|
|
1464
|
+
set({
|
|
1465
|
+
phase: "quoted",
|
|
1466
|
+
quote: best,
|
|
1467
|
+
needsApproval,
|
|
1468
|
+
preview: {
|
|
1469
|
+
payAmount: fmtAsset(request.amount, request.decimals),
|
|
1470
|
+
paySymbol: request.asset,
|
|
1471
|
+
chainName: request.chainName,
|
|
1472
|
+
receiveAmount: formatFiat(best.fiatNet, c),
|
|
1473
|
+
currency: c.currency,
|
|
1474
|
+
currencySymbol: c.symbol,
|
|
1475
|
+
fee: formatFiat(best.fiatFee, c),
|
|
1476
|
+
rate: fmtRate(best.rate),
|
|
1477
|
+
paymentWindowSecs: best.paymentWindowSecs,
|
|
1478
|
+
quotesConsidered: res.totalConsidered
|
|
1479
|
+
}
|
|
1480
|
+
});
|
|
1481
|
+
} catch (e) {
|
|
1482
|
+
if (gen === generation) set({ phase: "error", error: errMessage2(e) });
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
async function confirm(payee) {
|
|
1486
|
+
const { request, quote: chosen } = state;
|
|
1487
|
+
const w = wallet;
|
|
1488
|
+
if (!request || !chosen || !w || state.phase !== "quoted") return;
|
|
1489
|
+
const gen = generation;
|
|
1490
|
+
try {
|
|
1491
|
+
set({ error: null });
|
|
1492
|
+
const details = { ...payee, rail: request.corridor.rail };
|
|
1493
|
+
payeeIdentity(details);
|
|
1494
|
+
if (!request.corridor.accountPattern.test(details.account.replace(/[\s-]/g, ""))) {
|
|
1495
|
+
throw new Error(`That doesn't look like a valid ${request.corridor.accountLabel.toLowerCase()}.`);
|
|
1496
|
+
}
|
|
1497
|
+
if (!details.name.trim()) throw new Error("Enter the account holder's name.");
|
|
1498
|
+
if (await w.getChainId() !== request.chainId) {
|
|
1499
|
+
set({ phase: "switchingChain" });
|
|
1500
|
+
await w.switchChain(request.chainId);
|
|
1501
|
+
}
|
|
1502
|
+
const pc = await reader(request.chainId).catch(() => null);
|
|
1503
|
+
const needsApproval = pc ? !await hasPermit2Allowance(pc, request.token, w.address, request.amount) : state.needsApproval;
|
|
1504
|
+
if (needsApproval) {
|
|
1505
|
+
set({ phase: "approving" });
|
|
1506
|
+
const approve = buildPermit2ApproveRequest(request.token);
|
|
1507
|
+
const hash = await w.writeContract({ chainId: request.chainId, ...approve, abi: approve.abi });
|
|
1508
|
+
if (pc) await pc.waitForTransactionReceipt({ hash });
|
|
1509
|
+
set({ needsApproval: false });
|
|
1510
|
+
}
|
|
1511
|
+
set({ phase: "opening" });
|
|
1512
|
+
const { sealed, commitment, salt } = sealPayee(details, chosen.payeeKey);
|
|
1513
|
+
const opened = await api.openFiatTrade({
|
|
1514
|
+
quoteId: chosen.quoteId,
|
|
1515
|
+
assetAmount: request.amount.toString(),
|
|
1516
|
+
userAddress: getAddress3(w.address),
|
|
1517
|
+
payee: { sealed, commitment }
|
|
1518
|
+
});
|
|
1519
|
+
if (gen !== generation) return;
|
|
1520
|
+
set({ trade: opened.trade, payeeSalt: salt });
|
|
1521
|
+
checkLock(opened.lock, opened.trade, request, chosen, w.address);
|
|
1522
|
+
if (!same(opened.trade.payee_commitment, commitment)) throw new Error("Refusing to sign: the trade is not for the bank details you entered.");
|
|
1523
|
+
set({ phase: "signing" });
|
|
1524
|
+
const { domain, types, primaryType, message } = opened.lock.typedData;
|
|
1525
|
+
const signature = await w.signTypedData({ domain, types, primaryType, message });
|
|
1526
|
+
set({ phase: "locking" });
|
|
1527
|
+
const trade = await api.submitFiatLock(opened.trade.id, signature);
|
|
1528
|
+
if (gen !== generation) return;
|
|
1529
|
+
showTrade(trade);
|
|
1530
|
+
track(trade.id, gen);
|
|
1531
|
+
} catch (e) {
|
|
1532
|
+
if (gen !== generation) return;
|
|
1533
|
+
if (state.trade) {
|
|
1534
|
+
set({ error: errMessage2(e) });
|
|
1535
|
+
track(state.trade.id, gen);
|
|
1536
|
+
} else set({ phase: "error", error: errMessage2(e) });
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
async function act(phase, message, send) {
|
|
1540
|
+
const trade = state.trade;
|
|
1541
|
+
const w = wallet;
|
|
1542
|
+
if (!trade || trade.state !== "awaiting_confirmation") return;
|
|
1543
|
+
const gen = generation;
|
|
1544
|
+
const before = state.phase;
|
|
1545
|
+
try {
|
|
1546
|
+
if (!w) throw new Error("Connect the wallet that opened this trade.");
|
|
1547
|
+
if (!same(w.address, trade.user_address)) throw new Error("Switch to the wallet that opened this trade.");
|
|
1548
|
+
if (!w.signMessage) throw new Error("This wallet can't sign messages, so it can't confirm or dispute.");
|
|
1549
|
+
set({ phase, error: null });
|
|
1550
|
+
const signature = await w.signMessage(message(trade.id));
|
|
1551
|
+
const next = await send(trade.id, signature);
|
|
1552
|
+
if (gen !== generation) return;
|
|
1553
|
+
showTrade(next);
|
|
1554
|
+
track(next.id, gen);
|
|
1555
|
+
} catch (e) {
|
|
1556
|
+
if (gen === generation) set({ phase: before, error: errMessage2(e) });
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
async function dispute(reason) {
|
|
1560
|
+
const r = reason.trim();
|
|
1561
|
+
if (!r || r.length > MAX_REASON || !PLAIN_TEXT.test(r)) {
|
|
1562
|
+
set({ error: `Describe the problem in 1 to ${MAX_REASON} plain characters (letters, digits, punctuation).` });
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
await act("disputing", (id) => fiatDisputeMessage(id, r), (id, sig) => api.disputeFiatTrade(id, r, sig));
|
|
1566
|
+
}
|
|
1567
|
+
async function resume(tradeId) {
|
|
1568
|
+
const gen = ++generation;
|
|
1569
|
+
stopPolling();
|
|
1570
|
+
set({ ...INITIAL2, phase: "quoting" });
|
|
1571
|
+
try {
|
|
1572
|
+
const trade = await api.getFiatTrade(tradeId);
|
|
1573
|
+
if (gen !== generation) return;
|
|
1574
|
+
showTrade(trade);
|
|
1575
|
+
track(trade.id, gen);
|
|
1576
|
+
} catch (e) {
|
|
1577
|
+
if (gen === generation) set({ phase: "error", error: errMessage2(e) });
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
function reset() {
|
|
1581
|
+
generation++;
|
|
1582
|
+
stopPolling();
|
|
1583
|
+
set({ ...INITIAL2 });
|
|
1584
|
+
}
|
|
1585
|
+
function setWallet(next) {
|
|
1586
|
+
const changed = wallet?.address !== next?.address;
|
|
1587
|
+
wallet = next;
|
|
1588
|
+
if (next && state.request && !state.trade && (state.phase === "needsWallet" || changed && state.phase === "quoted")) {
|
|
1589
|
+
void quote(state.request);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
return {
|
|
1593
|
+
getState: () => state,
|
|
1594
|
+
subscribe(listener) {
|
|
1595
|
+
listeners.add(listener);
|
|
1596
|
+
return () => listeners.delete(listener);
|
|
1597
|
+
},
|
|
1598
|
+
quote,
|
|
1599
|
+
confirm,
|
|
1600
|
+
confirmReceived: () => act("confirming", fiatConfirmMessage, (id, sig) => api.confirmFiatTrade(id, sig)),
|
|
1601
|
+
dispute,
|
|
1602
|
+
resume,
|
|
1603
|
+
reset,
|
|
1604
|
+
setWallet,
|
|
1605
|
+
destroy() {
|
|
1606
|
+
generation++;
|
|
1607
|
+
stopPolling();
|
|
1608
|
+
destroyed = true;
|
|
1609
|
+
listeners.clear();
|
|
1610
|
+
}
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1065
1613
|
export {
|
|
1066
1614
|
ApiError,
|
|
1615
|
+
BANK_DETAILS_HINT,
|
|
1067
1616
|
CHAIN_ALIASES,
|
|
1068
1617
|
DEFAULT_API_URL,
|
|
1069
1618
|
DEFAULT_INTENT_MODEL,
|
|
1070
1619
|
EMPTY_SUPPORTED,
|
|
1620
|
+
FIAT_CORRIDORS,
|
|
1621
|
+
FIAT_CURRENCY_ALIASES,
|
|
1071
1622
|
HF_ROUTER_URL,
|
|
1072
1623
|
MAX_UINT256,
|
|
1624
|
+
NG_BANKS,
|
|
1073
1625
|
PERMIT2_ADDRESS,
|
|
1074
1626
|
SOLANA_CHAIN_IDS,
|
|
1075
1627
|
TEMPLATE_HINT,
|
|
@@ -1078,23 +1630,38 @@ export {
|
|
|
1078
1630
|
buildPermit2ApproveRequest,
|
|
1079
1631
|
buildQuoteRequest,
|
|
1080
1632
|
buildSupportedSet,
|
|
1633
|
+
canonicalPayeeIdentity,
|
|
1081
1634
|
chainName,
|
|
1635
|
+
checkLock,
|
|
1082
1636
|
createApiClient,
|
|
1083
1637
|
createIntentSession,
|
|
1638
|
+
createOfframpSession,
|
|
1084
1639
|
explorerUrl,
|
|
1085
1640
|
extractIntents,
|
|
1641
|
+
fiatConfirmMessage,
|
|
1642
|
+
fiatDisputeMessage,
|
|
1643
|
+
fiatPaymentSentMessage,
|
|
1086
1644
|
findAsset,
|
|
1645
|
+
formatFiat,
|
|
1087
1646
|
getPermit2Allowance,
|
|
1088
1647
|
hasPermit2Allowance,
|
|
1089
1648
|
interopAddress,
|
|
1090
1649
|
isTerminalStatus,
|
|
1650
|
+
looksLikeBankDetails,
|
|
1091
1651
|
normalizeChain,
|
|
1652
|
+
normalizeFiatCurrency,
|
|
1092
1653
|
normalizeToken,
|
|
1654
|
+
openPayee,
|
|
1093
1655
|
parseIntent,
|
|
1656
|
+
payeeCommitment,
|
|
1657
|
+
payeeIdentity,
|
|
1094
1658
|
resolveIntent,
|
|
1659
|
+
resolveOfframp,
|
|
1095
1660
|
rpcReaders,
|
|
1661
|
+
sealPayee,
|
|
1096
1662
|
signQuote,
|
|
1097
1663
|
stripDomainType,
|
|
1098
1664
|
validateIntent,
|
|
1665
|
+
validateOfframp,
|
|
1099
1666
|
viemWallet
|
|
1100
1667
|
};
|