@recoilpay/intent-core 0.2.0 → 0.4.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 +600 -12
- package/dist/index.d.cts +464 -29
- package/dist/index.d.ts +464 -29
- package/dist/index.js +576 -10
- 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));
|
|
@@ -378,7 +403,8 @@ var LABEL = {
|
|
|
378
403
|
chainIn: "Source chain",
|
|
379
404
|
tokenOut: "Destination token",
|
|
380
405
|
chainOut: "Destination chain",
|
|
381
|
-
recipient: "Recipient"
|
|
406
|
+
recipient: "Recipient",
|
|
407
|
+
fiatCurrency: "Currency"
|
|
382
408
|
};
|
|
383
409
|
var EVM_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
384
410
|
var SOLANA_ADDRESS_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
@@ -647,6 +673,23 @@ var PERMIT2_TYPES = {
|
|
|
647
673
|
{ name: "amount", type: "uint256" }
|
|
648
674
|
]
|
|
649
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
|
+
};
|
|
650
693
|
var EIP3009_TYPES = {
|
|
651
694
|
TransferWithAuthorization: [
|
|
652
695
|
{ name: "from", type: "address" },
|
|
@@ -683,8 +726,11 @@ function extractDomain(payload) {
|
|
|
683
726
|
function getTypesForPayload(payload) {
|
|
684
727
|
switch (payload.primaryType) {
|
|
685
728
|
case "PermitBatchWitnessTransferFrom":
|
|
686
|
-
case "PermitWitnessTransferFrom":
|
|
687
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;
|
|
688
734
|
case "TransferWithAuthorization":
|
|
689
735
|
case "ReceiveWithAuthorization":
|
|
690
736
|
case "CancelAuthorization":
|
|
@@ -752,6 +798,105 @@ async function signQuote(quote, signTypedDataAsync) {
|
|
|
752
798
|
}
|
|
753
799
|
}
|
|
754
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
|
+
|
|
755
900
|
// src/session.ts
|
|
756
901
|
var ERC20_TRANSFER_ABI = [
|
|
757
902
|
{
|
|
@@ -803,7 +948,8 @@ var INITIAL = {
|
|
|
803
948
|
error: null,
|
|
804
949
|
isConnected: false,
|
|
805
950
|
queueIndex: 0,
|
|
806
|
-
queueTotal: 0
|
|
951
|
+
queueTotal: 0,
|
|
952
|
+
offramp: null
|
|
807
953
|
};
|
|
808
954
|
function createIntentSession(options = {}) {
|
|
809
955
|
const api = options.api ?? createApiClient(options);
|
|
@@ -848,6 +994,7 @@ function createIntentSession(options = {}) {
|
|
|
848
994
|
}
|
|
849
995
|
}
|
|
850
996
|
async function quoteFor(raw, gen) {
|
|
997
|
+
if (raw.action === "offramp") return;
|
|
851
998
|
if (!wallet) return set({ phase: "needsWallet" });
|
|
852
999
|
try {
|
|
853
1000
|
set({ phase: "quoting", error: null });
|
|
@@ -998,6 +1145,12 @@ function createIntentSession(options = {}) {
|
|
|
998
1145
|
const parsed = await parseIntent(text, { hfAccessToken, model: options.hfModel, fetch: api.fetch });
|
|
999
1146
|
if (gen !== generation) return;
|
|
1000
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
|
+
}
|
|
1001
1154
|
for (const intent of parsed.intents) {
|
|
1002
1155
|
const issues = validateIntent(intent, supported);
|
|
1003
1156
|
if (issues.length) return set({ phase: "invalid", issues });
|
|
@@ -1067,7 +1220,8 @@ function viemWallet(client) {
|
|
|
1067
1220
|
functionName: req.functionName,
|
|
1068
1221
|
args: req.args
|
|
1069
1222
|
}),
|
|
1070
|
-
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 })
|
|
1071
1225
|
};
|
|
1072
1226
|
}
|
|
1073
1227
|
function stripDomainType(types) {
|
|
@@ -1075,14 +1229,408 @@ function stripDomainType(types) {
|
|
|
1075
1229
|
for (const [name, fields] of Object.entries(types)) if (name !== "EIP712Domain") out[name] = fields;
|
|
1076
1230
|
return out;
|
|
1077
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
|
+
function fiatClaimMessage(tradeId, reason) {
|
|
1352
|
+
return `RecoilPay: I claim the fiat payment for trade ${tradeId} never arrived. Reason: ${reason.trim()}`;
|
|
1353
|
+
}
|
|
1354
|
+
function fiatRevealMessage(tradeId) {
|
|
1355
|
+
return `RecoilPay: I reveal the bank details I gave for trade ${tradeId}.`;
|
|
1356
|
+
}
|
|
1357
|
+
function fiatStatementMessage(tradeId, text) {
|
|
1358
|
+
return `RecoilPay: My statement on trade ${tradeId}: ${text.trim()}`;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// src/fiat/offramp.ts
|
|
1362
|
+
var TERMINAL = ["settled_to_fiat_payer", "refunded_to_funder", "failed"];
|
|
1363
|
+
var PHASE_OF = {
|
|
1364
|
+
quoted: "locking",
|
|
1365
|
+
awaiting_fiat: "awaitingFiat",
|
|
1366
|
+
awaiting_confirmation: "awaitingConfirmation",
|
|
1367
|
+
disputed: "disputed",
|
|
1368
|
+
settled_to_fiat_payer: "settled",
|
|
1369
|
+
refunded_to_funder: "refunded",
|
|
1370
|
+
failed: "failed"
|
|
1371
|
+
};
|
|
1372
|
+
var PLAIN_TEXT = /^[\x20-\x7e]+$/;
|
|
1373
|
+
var MAX_REASON = 500;
|
|
1374
|
+
var errMessage2 = (e) => e instanceof Error ? e.message : typeof e === "string" ? e : "Something went wrong.";
|
|
1375
|
+
var same = (a, b) => typeof a === "string" && typeof b === "string" && a.toLowerCase() === b.toLowerCase();
|
|
1376
|
+
function fmtAsset(amount, decimals) {
|
|
1377
|
+
const n = Number(formatUnits2(amount, decimals));
|
|
1378
|
+
return n.toLocaleString("en-US", { maximumFractionDigits: 6 });
|
|
1379
|
+
}
|
|
1380
|
+
function checkLock(lock, trade, request, quote, user) {
|
|
1381
|
+
const m = lock.typedData.message;
|
|
1382
|
+
const d = lock.typedData.domain;
|
|
1383
|
+
const problems = [
|
|
1384
|
+
lock.typedData.primaryType !== "PermitWitnessTransferFrom" && "not a Permit2 witness transfer",
|
|
1385
|
+
!same(d.verifyingContract, PERMIT2_ADDRESS) && "not signed for Permit2",
|
|
1386
|
+
Number(d.chainId) !== request.chainId && "the wrong chain",
|
|
1387
|
+
!same(m.permitted?.token, request.token) && "the wrong token",
|
|
1388
|
+
m.permitted?.amount !== request.amount.toString() && "the wrong amount",
|
|
1389
|
+
!same(m.spender, lock.escrow) && "a spender other than the escrow",
|
|
1390
|
+
!same(m.witness?.tradeId, lock.tradeId) && "another trade",
|
|
1391
|
+
!same(m.witness?.counterparty, trade.solver_address) && "a counterparty other than the solver",
|
|
1392
|
+
trade.quote_id !== quote.quoteId && "another quote",
|
|
1393
|
+
trade.fiat_minor_units !== quote.fiatNet && "a payout other than the one quoted",
|
|
1394
|
+
!same(trade.user_address, user) && "another wallet"
|
|
1395
|
+
].filter(Boolean);
|
|
1396
|
+
if (problems.length) throw new Error(`Refusing to sign: the lock names ${problems.join(", ")}.`);
|
|
1397
|
+
}
|
|
1398
|
+
function fmtRate(rate) {
|
|
1399
|
+
const n = Number(rate);
|
|
1400
|
+
return Number.isFinite(n) ? n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 6 }) : rate;
|
|
1401
|
+
}
|
|
1402
|
+
var INITIAL2 = {
|
|
1403
|
+
phase: "idle",
|
|
1404
|
+
request: null,
|
|
1405
|
+
quote: null,
|
|
1406
|
+
preview: null,
|
|
1407
|
+
needsApproval: false,
|
|
1408
|
+
trade: null,
|
|
1409
|
+
payeeSalt: null,
|
|
1410
|
+
error: null
|
|
1411
|
+
};
|
|
1412
|
+
function createOfframpSession(options = {}) {
|
|
1413
|
+
const api = options.api ?? createApiClient(options);
|
|
1414
|
+
const pollMs = options.pollIntervalMs ?? 3e3;
|
|
1415
|
+
let chainsPromise = null;
|
|
1416
|
+
const chains = () => chainsPromise ??= api.getChains().catch((e) => (chainsPromise = null, Promise.reject(e)));
|
|
1417
|
+
const reader = options.chainReader ?? rpcReaders(chains);
|
|
1418
|
+
let wallet = options.wallet ?? null;
|
|
1419
|
+
let pollTimer;
|
|
1420
|
+
let destroyed = false;
|
|
1421
|
+
let generation = 0;
|
|
1422
|
+
let state = { ...INITIAL2 };
|
|
1423
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1424
|
+
function set(patch) {
|
|
1425
|
+
if (destroyed) return;
|
|
1426
|
+
state = { ...state, ...patch };
|
|
1427
|
+
for (const l of listeners) l(state);
|
|
1428
|
+
}
|
|
1429
|
+
const stopPolling = () => {
|
|
1430
|
+
if (pollTimer) clearTimeout(pollTimer);
|
|
1431
|
+
pollTimer = void 0;
|
|
1432
|
+
};
|
|
1433
|
+
function showTrade(trade, patch = {}) {
|
|
1434
|
+
set({ trade, phase: PHASE_OF[trade.state], ...patch });
|
|
1435
|
+
}
|
|
1436
|
+
function track(tradeId, gen) {
|
|
1437
|
+
stopPolling();
|
|
1438
|
+
const poll = async () => {
|
|
1439
|
+
try {
|
|
1440
|
+
const trade = await api.getFiatTrade(tradeId);
|
|
1441
|
+
if (gen !== generation) return;
|
|
1442
|
+
if (state.phase === "confirming" || state.phase === "disputing") set({ trade });
|
|
1443
|
+
else showTrade(trade);
|
|
1444
|
+
if (TERMINAL.includes(trade.state)) return;
|
|
1445
|
+
} catch {
|
|
1446
|
+
}
|
|
1447
|
+
if (gen !== generation || destroyed) return;
|
|
1448
|
+
const wait = state.trade?.state === "disputed" ? pollMs * 10 : pollMs;
|
|
1449
|
+
pollTimer = setTimeout(poll, wait);
|
|
1450
|
+
};
|
|
1451
|
+
void poll();
|
|
1452
|
+
}
|
|
1453
|
+
async function quote(request) {
|
|
1454
|
+
const gen = ++generation;
|
|
1455
|
+
stopPolling();
|
|
1456
|
+
set({ ...INITIAL2, request, phase: wallet ? "quoting" : "needsWallet" });
|
|
1457
|
+
if (!wallet) return;
|
|
1458
|
+
try {
|
|
1459
|
+
const res = await api.rankFiatQuotes({
|
|
1460
|
+
direction: "offramp",
|
|
1461
|
+
chain: `eip155:${request.chainId}`,
|
|
1462
|
+
asset: request.asset,
|
|
1463
|
+
fiatCurrency: request.corridor.currency,
|
|
1464
|
+
rail: request.corridor.rail,
|
|
1465
|
+
assetAmount: request.amount.toString()
|
|
1466
|
+
});
|
|
1467
|
+
const best = res.quotes?.[0];
|
|
1468
|
+
if (!best) throw new Error(`No solver is cashing out ${request.asset} on ${request.chainName} to ${request.corridor.currency} for this amount right now.`);
|
|
1469
|
+
const pc = await reader(request.chainId).catch(() => null);
|
|
1470
|
+
const needsApproval = pc ? !await hasPermit2Allowance(pc, request.token, wallet.address, request.amount).catch(() => false) : true;
|
|
1471
|
+
if (gen !== generation) return;
|
|
1472
|
+
const c = request.corridor;
|
|
1473
|
+
set({
|
|
1474
|
+
phase: "quoted",
|
|
1475
|
+
quote: best,
|
|
1476
|
+
needsApproval,
|
|
1477
|
+
preview: {
|
|
1478
|
+
payAmount: fmtAsset(request.amount, request.decimals),
|
|
1479
|
+
paySymbol: request.asset,
|
|
1480
|
+
chainName: request.chainName,
|
|
1481
|
+
receiveAmount: formatFiat(best.fiatNet, c),
|
|
1482
|
+
currency: c.currency,
|
|
1483
|
+
currencySymbol: c.symbol,
|
|
1484
|
+
fee: formatFiat(best.fiatFee, c),
|
|
1485
|
+
rate: fmtRate(best.rate),
|
|
1486
|
+
paymentWindowSecs: best.paymentWindowSecs,
|
|
1487
|
+
quotesConsidered: res.totalConsidered
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1490
|
+
} catch (e) {
|
|
1491
|
+
if (gen === generation) set({ phase: "error", error: errMessage2(e) });
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
async function confirm(payee) {
|
|
1495
|
+
const { request, quote: chosen } = state;
|
|
1496
|
+
const w = wallet;
|
|
1497
|
+
if (!request || !chosen || !w || state.phase !== "quoted") return;
|
|
1498
|
+
const gen = generation;
|
|
1499
|
+
try {
|
|
1500
|
+
set({ error: null });
|
|
1501
|
+
const details = { ...payee, rail: request.corridor.rail };
|
|
1502
|
+
payeeIdentity(details);
|
|
1503
|
+
if (!request.corridor.accountPattern.test(details.account.replace(/[\s-]/g, ""))) {
|
|
1504
|
+
throw new Error(`That doesn't look like a valid ${request.corridor.accountLabel.toLowerCase()}.`);
|
|
1505
|
+
}
|
|
1506
|
+
if (!details.name.trim()) throw new Error("Enter the account holder's name.");
|
|
1507
|
+
if (await w.getChainId() !== request.chainId) {
|
|
1508
|
+
set({ phase: "switchingChain" });
|
|
1509
|
+
await w.switchChain(request.chainId);
|
|
1510
|
+
}
|
|
1511
|
+
const pc = await reader(request.chainId).catch(() => null);
|
|
1512
|
+
const needsApproval = pc ? !await hasPermit2Allowance(pc, request.token, w.address, request.amount) : state.needsApproval;
|
|
1513
|
+
if (needsApproval) {
|
|
1514
|
+
set({ phase: "approving" });
|
|
1515
|
+
const approve = buildPermit2ApproveRequest(request.token);
|
|
1516
|
+
const hash = await w.writeContract({ chainId: request.chainId, ...approve, abi: approve.abi });
|
|
1517
|
+
if (pc) await pc.waitForTransactionReceipt({ hash });
|
|
1518
|
+
set({ needsApproval: false });
|
|
1519
|
+
}
|
|
1520
|
+
set({ phase: "opening" });
|
|
1521
|
+
const { sealed, commitment, salt } = sealPayee(details, chosen.payeeKey);
|
|
1522
|
+
const opened = await api.openFiatTrade({
|
|
1523
|
+
quoteId: chosen.quoteId,
|
|
1524
|
+
assetAmount: request.amount.toString(),
|
|
1525
|
+
userAddress: getAddress3(w.address),
|
|
1526
|
+
payee: { sealed, commitment }
|
|
1527
|
+
});
|
|
1528
|
+
if (gen !== generation) return;
|
|
1529
|
+
set({ trade: opened.trade, payeeSalt: salt });
|
|
1530
|
+
checkLock(opened.lock, opened.trade, request, chosen, w.address);
|
|
1531
|
+
if (!same(opened.trade.payee_commitment, commitment)) throw new Error("Refusing to sign: the trade is not for the bank details you entered.");
|
|
1532
|
+
set({ phase: "signing" });
|
|
1533
|
+
const { domain, types, primaryType, message } = opened.lock.typedData;
|
|
1534
|
+
const signature = await w.signTypedData({ domain, types, primaryType, message });
|
|
1535
|
+
set({ phase: "locking" });
|
|
1536
|
+
const trade = await api.submitFiatLock(opened.trade.id, signature);
|
|
1537
|
+
if (gen !== generation) return;
|
|
1538
|
+
showTrade(trade);
|
|
1539
|
+
track(trade.id, gen);
|
|
1540
|
+
} catch (e) {
|
|
1541
|
+
if (gen !== generation) return;
|
|
1542
|
+
if (state.trade) {
|
|
1543
|
+
set({ error: errMessage2(e) });
|
|
1544
|
+
track(state.trade.id, gen);
|
|
1545
|
+
} else set({ phase: "error", error: errMessage2(e) });
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
async function act(phase, message, send) {
|
|
1549
|
+
const trade = state.trade;
|
|
1550
|
+
const w = wallet;
|
|
1551
|
+
if (!trade || trade.state !== "awaiting_confirmation") return;
|
|
1552
|
+
const gen = generation;
|
|
1553
|
+
const before = state.phase;
|
|
1554
|
+
try {
|
|
1555
|
+
if (!w) throw new Error("Connect the wallet that opened this trade.");
|
|
1556
|
+
if (!same(w.address, trade.user_address)) throw new Error("Switch to the wallet that opened this trade.");
|
|
1557
|
+
if (!w.signMessage) throw new Error("This wallet can't sign messages, so it can't confirm or dispute.");
|
|
1558
|
+
set({ phase, error: null });
|
|
1559
|
+
const signature = await w.signMessage(message(trade.id));
|
|
1560
|
+
const next = await send(trade.id, signature);
|
|
1561
|
+
if (gen !== generation) return;
|
|
1562
|
+
showTrade(next);
|
|
1563
|
+
track(next.id, gen);
|
|
1564
|
+
} catch (e) {
|
|
1565
|
+
if (gen === generation) set({ phase: before, error: errMessage2(e) });
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
async function dispute(reason) {
|
|
1569
|
+
const r = reason.trim();
|
|
1570
|
+
if (!r || r.length > MAX_REASON || !PLAIN_TEXT.test(r)) {
|
|
1571
|
+
set({ error: `Describe the problem in 1 to ${MAX_REASON} plain characters (letters, digits, punctuation).` });
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
await act("disputing", (id) => fiatDisputeMessage(id, r), (id, sig) => api.disputeFiatTrade(id, r, sig));
|
|
1575
|
+
}
|
|
1576
|
+
async function resume(tradeId) {
|
|
1577
|
+
const gen = ++generation;
|
|
1578
|
+
stopPolling();
|
|
1579
|
+
set({ ...INITIAL2, phase: "quoting" });
|
|
1580
|
+
try {
|
|
1581
|
+
const trade = await api.getFiatTrade(tradeId);
|
|
1582
|
+
if (gen !== generation) return;
|
|
1583
|
+
showTrade(trade);
|
|
1584
|
+
track(trade.id, gen);
|
|
1585
|
+
} catch (e) {
|
|
1586
|
+
if (gen === generation) set({ phase: "error", error: errMessage2(e) });
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
function reset() {
|
|
1590
|
+
generation++;
|
|
1591
|
+
stopPolling();
|
|
1592
|
+
set({ ...INITIAL2 });
|
|
1593
|
+
}
|
|
1594
|
+
function setWallet(next) {
|
|
1595
|
+
const changed = wallet?.address !== next?.address;
|
|
1596
|
+
wallet = next;
|
|
1597
|
+
if (next && state.request && !state.trade && (state.phase === "needsWallet" || changed && state.phase === "quoted")) {
|
|
1598
|
+
void quote(state.request);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
return {
|
|
1602
|
+
getState: () => state,
|
|
1603
|
+
subscribe(listener) {
|
|
1604
|
+
listeners.add(listener);
|
|
1605
|
+
return () => listeners.delete(listener);
|
|
1606
|
+
},
|
|
1607
|
+
quote,
|
|
1608
|
+
confirm,
|
|
1609
|
+
confirmReceived: () => act("confirming", fiatConfirmMessage, (id, sig) => api.confirmFiatTrade(id, sig)),
|
|
1610
|
+
dispute,
|
|
1611
|
+
resume,
|
|
1612
|
+
reset,
|
|
1613
|
+
setWallet,
|
|
1614
|
+
destroy() {
|
|
1615
|
+
generation++;
|
|
1616
|
+
stopPolling();
|
|
1617
|
+
destroyed = true;
|
|
1618
|
+
listeners.clear();
|
|
1619
|
+
}
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1078
1622
|
export {
|
|
1079
1623
|
ApiError,
|
|
1624
|
+
BANK_DETAILS_HINT,
|
|
1080
1625
|
CHAIN_ALIASES,
|
|
1081
1626
|
DEFAULT_API_URL,
|
|
1082
1627
|
DEFAULT_INTENT_MODEL,
|
|
1083
1628
|
EMPTY_SUPPORTED,
|
|
1629
|
+
FIAT_CORRIDORS,
|
|
1630
|
+
FIAT_CURRENCY_ALIASES,
|
|
1084
1631
|
HF_ROUTER_URL,
|
|
1085
1632
|
MAX_UINT256,
|
|
1633
|
+
NG_BANKS,
|
|
1086
1634
|
PERMIT2_ADDRESS,
|
|
1087
1635
|
SOLANA_CHAIN_IDS,
|
|
1088
1636
|
TEMPLATE_HINT,
|
|
@@ -1091,23 +1639,41 @@ export {
|
|
|
1091
1639
|
buildPermit2ApproveRequest,
|
|
1092
1640
|
buildQuoteRequest,
|
|
1093
1641
|
buildSupportedSet,
|
|
1642
|
+
canonicalPayeeIdentity,
|
|
1094
1643
|
chainName,
|
|
1644
|
+
checkLock,
|
|
1095
1645
|
createApiClient,
|
|
1096
1646
|
createIntentSession,
|
|
1647
|
+
createOfframpSession,
|
|
1097
1648
|
explorerUrl,
|
|
1098
1649
|
extractIntents,
|
|
1650
|
+
fiatClaimMessage,
|
|
1651
|
+
fiatConfirmMessage,
|
|
1652
|
+
fiatDisputeMessage,
|
|
1653
|
+
fiatPaymentSentMessage,
|
|
1654
|
+
fiatRevealMessage,
|
|
1655
|
+
fiatStatementMessage,
|
|
1099
1656
|
findAsset,
|
|
1657
|
+
formatFiat,
|
|
1100
1658
|
getPermit2Allowance,
|
|
1101
1659
|
hasPermit2Allowance,
|
|
1102
1660
|
interopAddress,
|
|
1103
1661
|
isTerminalStatus,
|
|
1662
|
+
looksLikeBankDetails,
|
|
1104
1663
|
normalizeChain,
|
|
1664
|
+
normalizeFiatCurrency,
|
|
1105
1665
|
normalizeToken,
|
|
1666
|
+
openPayee,
|
|
1106
1667
|
parseIntent,
|
|
1668
|
+
payeeCommitment,
|
|
1669
|
+
payeeIdentity,
|
|
1107
1670
|
resolveIntent,
|
|
1671
|
+
resolveOfframp,
|
|
1108
1672
|
rpcReaders,
|
|
1673
|
+
sealPayee,
|
|
1109
1674
|
signQuote,
|
|
1110
1675
|
stripDomainType,
|
|
1111
1676
|
validateIntent,
|
|
1677
|
+
validateOfframp,
|
|
1112
1678
|
viemWallet
|
|
1113
1679
|
};
|