@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.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,48 @@ __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
|
+
fiatClaimMessage: () => fiatClaimMessage,
|
|
51
|
+
fiatConfirmMessage: () => fiatConfirmMessage,
|
|
52
|
+
fiatDisputeMessage: () => fiatDisputeMessage,
|
|
53
|
+
fiatPaymentSentMessage: () => fiatPaymentSentMessage,
|
|
54
|
+
fiatRevealMessage: () => fiatRevealMessage,
|
|
55
|
+
fiatStatementMessage: () => fiatStatementMessage,
|
|
43
56
|
findAsset: () => findAsset,
|
|
57
|
+
formatFiat: () => formatFiat,
|
|
44
58
|
getPermit2Allowance: () => getPermit2Allowance,
|
|
45
59
|
hasPermit2Allowance: () => hasPermit2Allowance,
|
|
46
60
|
interopAddress: () => interopAddress,
|
|
47
61
|
isTerminalStatus: () => isTerminalStatus,
|
|
62
|
+
looksLikeBankDetails: () => looksLikeBankDetails,
|
|
48
63
|
normalizeChain: () => normalizeChain,
|
|
64
|
+
normalizeFiatCurrency: () => normalizeFiatCurrency,
|
|
49
65
|
normalizeToken: () => normalizeToken,
|
|
66
|
+
openPayee: () => openPayee,
|
|
50
67
|
parseIntent: () => parseIntent,
|
|
68
|
+
payeeCommitment: () => payeeCommitment,
|
|
69
|
+
payeeIdentity: () => payeeIdentity,
|
|
51
70
|
resolveIntent: () => resolveIntent,
|
|
71
|
+
resolveOfframp: () => resolveOfframp,
|
|
52
72
|
rpcReaders: () => rpcReaders,
|
|
73
|
+
sealPayee: () => sealPayee,
|
|
53
74
|
signQuote: () => signQuote,
|
|
54
75
|
stripDomainType: () => stripDomainType,
|
|
55
76
|
validateIntent: () => validateIntent,
|
|
77
|
+
validateOfframp: () => validateOfframp,
|
|
56
78
|
viemWallet: () => viemWallet
|
|
57
79
|
});
|
|
58
80
|
module.exports = __toCommonJS(index_exports);
|
|
59
81
|
|
|
60
82
|
// src/session.ts
|
|
61
|
-
var
|
|
83
|
+
var import_viem5 = require("viem");
|
|
62
84
|
|
|
63
85
|
// src/api.ts
|
|
64
86
|
var DEFAULT_API_URL = "https://api.recoilpay.com";
|
|
@@ -117,12 +139,29 @@ function createApiClient(options = {}) {
|
|
|
117
139
|
},
|
|
118
140
|
getQuotes: (req) => post("/quotes", req),
|
|
119
141
|
submitOrder: (req) => post("/orders", req),
|
|
120
|
-
getOrder: (id) => request(`/orders/${encodeURIComponent(id)}`)
|
|
142
|
+
getOrder: (id) => request(`/orders/${encodeURIComponent(id)}`),
|
|
143
|
+
// ── Fiat ramps (design §12.1) ──
|
|
144
|
+
rankFiatQuotes: (req) => post("/fiat/quotes", req),
|
|
145
|
+
openFiatTrade: (req) => post("/fiat/trades", req),
|
|
146
|
+
getFiatTrade: (id) => request(`/fiat/trades/${encodeURIComponent(id)}`),
|
|
147
|
+
/** Hand the signed lock to the solver's relay, which submits it and pays the gas. */
|
|
148
|
+
submitFiatLock: (id, signature) => post(`/fiat/trades/${encodeURIComponent(id)}/lock`, { signature }),
|
|
149
|
+
/** Report a lock the user submitted themselves; the aggregator reads it from the chain. */
|
|
150
|
+
reportFiatFunded: (id, txHash) => post(`/fiat/trades/${encodeURIComponent(id)}/funded`, txHash ? { txHash } : {}),
|
|
151
|
+
/** `signature` is over `fiatConfirmMessage(id)`. */
|
|
152
|
+
confirmFiatTrade: (id, signature) => post(`/fiat/trades/${encodeURIComponent(id)}/confirm`, { signature }),
|
|
153
|
+
/** `signature` is over `fiatDisputeMessage(id, reason)`. */
|
|
154
|
+
disputeFiatTrade: (id, reason, signature) => post(`/fiat/trades/${encodeURIComponent(id)}/dispute`, { reason, signature })
|
|
121
155
|
};
|
|
122
156
|
}
|
|
123
157
|
|
|
124
158
|
// 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>";
|
|
159
|
+
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)";
|
|
160
|
+
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";
|
|
161
|
+
function looksLikeBankDetails(text) {
|
|
162
|
+
const rest = text.replace(/0x[0-9a-fA-F]+/g, " ").replace(/[.,]\d+/g, "");
|
|
163
|
+
return /(\d[\s-]?){10,}/.test(rest);
|
|
164
|
+
}
|
|
126
165
|
var HF_ROUTER_URL = "https://router.huggingface.co/v1/chat/completions";
|
|
127
166
|
var DEFAULT_INTENT_MODEL = "Qwen/Qwen2.5-Coder-32B-Instruct";
|
|
128
167
|
var SYSTEM_PROMPT = `You are an intent extraction engine for a cross-chain swap protocol.
|
|
@@ -131,7 +170,7 @@ If the user specifies multiple intents (e.g. "swap X and then send Y"), return m
|
|
|
131
170
|
The JSON objects must perfectly match the following TypeScript interface:
|
|
132
171
|
|
|
133
172
|
type AmountKind = "token" | "usd";
|
|
134
|
-
type IntentAction = "swap" | "send";
|
|
173
|
+
type IntentAction = "swap" | "send" | "offramp";
|
|
135
174
|
|
|
136
175
|
interface RawIntent {
|
|
137
176
|
action: IntentAction;
|
|
@@ -142,14 +181,19 @@ interface RawIntent {
|
|
|
142
181
|
tokenOut: string | null; // target token (optional for send)
|
|
143
182
|
chainOut: string | null; // target chain (optional for send)
|
|
144
183
|
recipient: string | null; // address/ens (required for 'send', optional for 'swap')
|
|
184
|
+
fiatCurrency?: string | null; // 'offramp' only: the local currency to receive, e.g. "NGN" or "naira"
|
|
145
185
|
}
|
|
146
186
|
|
|
187
|
+
Use "offramp" when the user wants to cash out, sell, or withdraw crypto to a bank account or local currency.
|
|
188
|
+
For "offramp", tokenIn/chainIn/amount describe the crypto they give; tokenOut, chainOut and recipient are null.
|
|
189
|
+
Never put bank names, account numbers or personal names in any field.
|
|
190
|
+
|
|
147
191
|
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
192
|
var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
149
193
|
var readString = (v) => typeof v === "string" && v.trim().length > 0 ? v : null;
|
|
150
194
|
var readAmount = (v) => v === null || v === void 0 ? null : String(v);
|
|
151
195
|
var readAmountKind = (v) => v === "usd" ? "usd" : "token";
|
|
152
|
-
var readAction = (v) => v === "swap" || v === "send" ? v : null;
|
|
196
|
+
var readAction = (v) => v === "swap" || v === "send" || v === "offramp" ? v : null;
|
|
153
197
|
var offTemplate = (message = TEMPLATE_HINT) => ({ ok: false, offTemplate: true, message });
|
|
154
198
|
function extractIntents(content) {
|
|
155
199
|
let json = content.trim();
|
|
@@ -174,7 +218,7 @@ function extractIntents(content) {
|
|
|
174
218
|
for (const item of items) {
|
|
175
219
|
const action = readAction(item.action);
|
|
176
220
|
if (!action) return offTemplate();
|
|
177
|
-
|
|
221
|
+
const intent = {
|
|
178
222
|
action,
|
|
179
223
|
amount: readAmount(item.amount),
|
|
180
224
|
amountKind: readAmountKind(item.amountKind),
|
|
@@ -183,13 +227,16 @@ function extractIntents(content) {
|
|
|
183
227
|
tokenOut: readString(item.tokenOut),
|
|
184
228
|
chainOut: readString(item.chainOut),
|
|
185
229
|
recipient: readString(item.recipient)
|
|
186
|
-
}
|
|
230
|
+
};
|
|
231
|
+
if (action === "offramp") intent.fiatCurrency = readString(item.fiatCurrency);
|
|
232
|
+
intents.push(intent);
|
|
187
233
|
}
|
|
188
234
|
return { ok: true, intents };
|
|
189
235
|
}
|
|
190
236
|
async function parseIntent(text, options = {}) {
|
|
191
237
|
const norm = text.trim();
|
|
192
238
|
if (!norm) return offTemplate();
|
|
239
|
+
if (looksLikeBankDetails(norm)) return offTemplate(BANK_DETAILS_HINT);
|
|
193
240
|
const token = options.hfAccessToken?.trim();
|
|
194
241
|
if (!token) return offTemplate(`Natural-language parsing isn't set up here. ${TEMPLATE_HINT}`);
|
|
195
242
|
const doFetch = options.fetch ?? ((...args) => fetch(...args));
|
|
@@ -437,7 +484,8 @@ var LABEL = {
|
|
|
437
484
|
chainIn: "Source chain",
|
|
438
485
|
tokenOut: "Destination token",
|
|
439
486
|
chainOut: "Destination chain",
|
|
440
|
-
recipient: "Recipient"
|
|
487
|
+
recipient: "Recipient",
|
|
488
|
+
fiatCurrency: "Currency"
|
|
441
489
|
};
|
|
442
490
|
var EVM_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
443
491
|
var SOLANA_ADDRESS_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
@@ -706,6 +754,23 @@ var PERMIT2_TYPES = {
|
|
|
706
754
|
{ name: "amount", type: "uint256" }
|
|
707
755
|
]
|
|
708
756
|
};
|
|
757
|
+
var PERMIT2_ESCROW_LOCK_TYPES = {
|
|
758
|
+
PermitWitnessTransferFrom: [
|
|
759
|
+
{ name: "permitted", type: "TokenPermissions" },
|
|
760
|
+
{ name: "spender", type: "address" },
|
|
761
|
+
{ name: "nonce", type: "uint256" },
|
|
762
|
+
{ name: "deadline", type: "uint256" },
|
|
763
|
+
{ name: "witness", type: "EscrowLock" }
|
|
764
|
+
],
|
|
765
|
+
EscrowLock: [
|
|
766
|
+
{ name: "tradeId", type: "bytes32" },
|
|
767
|
+
{ name: "counterparty", type: "address" }
|
|
768
|
+
],
|
|
769
|
+
TokenPermissions: [
|
|
770
|
+
{ name: "token", type: "address" },
|
|
771
|
+
{ name: "amount", type: "uint256" }
|
|
772
|
+
]
|
|
773
|
+
};
|
|
709
774
|
var EIP3009_TYPES = {
|
|
710
775
|
TransferWithAuthorization: [
|
|
711
776
|
{ name: "from", type: "address" },
|
|
@@ -742,8 +807,11 @@ function extractDomain(payload) {
|
|
|
742
807
|
function getTypesForPayload(payload) {
|
|
743
808
|
switch (payload.primaryType) {
|
|
744
809
|
case "PermitBatchWitnessTransferFrom":
|
|
745
|
-
case "PermitWitnessTransferFrom":
|
|
746
810
|
return PERMIT2_TYPES;
|
|
811
|
+
// The batch types do not define this primary type at all, so mapping it
|
|
812
|
+
// to them made every single-token signature fail.
|
|
813
|
+
case "PermitWitnessTransferFrom":
|
|
814
|
+
return PERMIT2_ESCROW_LOCK_TYPES;
|
|
747
815
|
case "TransferWithAuthorization":
|
|
748
816
|
case "ReceiveWithAuthorization":
|
|
749
817
|
case "CancelAuthorization":
|
|
@@ -811,6 +879,105 @@ async function signQuote(quote, signTypedDataAsync) {
|
|
|
811
879
|
}
|
|
812
880
|
}
|
|
813
881
|
|
|
882
|
+
// src/fiat/intent.ts
|
|
883
|
+
var import_viem4 = require("viem");
|
|
884
|
+
|
|
885
|
+
// src/fiat/corridors.ts
|
|
886
|
+
var NG_BANKS = [
|
|
887
|
+
{ code: "044", name: "Access Bank" },
|
|
888
|
+
{ code: "023", name: "Citibank Nigeria" },
|
|
889
|
+
{ code: "050", name: "Ecobank Nigeria" },
|
|
890
|
+
{ code: "070", name: "Fidelity Bank" },
|
|
891
|
+
{ code: "011", name: "First Bank of Nigeria" },
|
|
892
|
+
{ code: "214", name: "First City Monument Bank" },
|
|
893
|
+
{ code: "058", name: "Guaranty Trust Bank" },
|
|
894
|
+
{ code: "082", name: "Keystone Bank" },
|
|
895
|
+
{ code: "50211", name: "Kuda Bank" },
|
|
896
|
+
{ code: "50515", name: "Moniepoint MFB" },
|
|
897
|
+
{ code: "999992", name: "OPay" },
|
|
898
|
+
{ code: "999991", name: "PalmPay" },
|
|
899
|
+
{ code: "076", name: "Polaris Bank" },
|
|
900
|
+
{ code: "101", name: "Providus Bank" },
|
|
901
|
+
{ code: "221", name: "Stanbic IBTC Bank" },
|
|
902
|
+
{ code: "068", name: "Standard Chartered Bank" },
|
|
903
|
+
{ code: "232", name: "Sterling Bank" },
|
|
904
|
+
{ code: "032", name: "Union Bank of Nigeria" },
|
|
905
|
+
{ code: "033", name: "United Bank For Africa" },
|
|
906
|
+
{ code: "215", name: "Unity Bank" },
|
|
907
|
+
{ code: "035", name: "Wema Bank" },
|
|
908
|
+
{ code: "057", name: "Zenith Bank" }
|
|
909
|
+
];
|
|
910
|
+
var FIAT_CORRIDORS = {
|
|
911
|
+
NGN: {
|
|
912
|
+
currency: "NGN",
|
|
913
|
+
rail: "ng.nip",
|
|
914
|
+
decimals: 2,
|
|
915
|
+
symbol: "\u20A6",
|
|
916
|
+
accountLabel: "Account number (NUBAN)",
|
|
917
|
+
accountPattern: /^\d{10}$/,
|
|
918
|
+
banks: NG_BANKS
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
var FIAT_CURRENCY_ALIASES = {
|
|
922
|
+
ngn: "NGN",
|
|
923
|
+
naira: "NGN",
|
|
924
|
+
"\u20A6": "NGN",
|
|
925
|
+
nigeria: "NGN",
|
|
926
|
+
nigerian: "NGN"
|
|
927
|
+
};
|
|
928
|
+
function normalizeFiatCurrency(raw) {
|
|
929
|
+
if (!raw) return null;
|
|
930
|
+
const k = raw.trim().toLowerCase();
|
|
931
|
+
return FIAT_CURRENCY_ALIASES[k] ?? (FIAT_CORRIDORS[k.toUpperCase()] ? k.toUpperCase() : null);
|
|
932
|
+
}
|
|
933
|
+
function formatFiat(minor, corridor) {
|
|
934
|
+
const n = typeof minor === "bigint" ? minor : BigInt(minor);
|
|
935
|
+
const scale = 10n ** BigInt(corridor.decimals);
|
|
936
|
+
const whole = (n / scale).toLocaleString("en-US");
|
|
937
|
+
if (corridor.decimals === 0) return whole;
|
|
938
|
+
return `${whole}.${(n % scale).toString().padStart(corridor.decimals, "0")}`;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// src/fiat/intent.ts
|
|
942
|
+
function validateOfframp(raw, supported) {
|
|
943
|
+
const issues = [];
|
|
944
|
+
if (!raw.amount) issues.push({ field: "amount", kind: "missing", message: "Amount is not stated." });
|
|
945
|
+
else if (!(Number(raw.amount) > 0)) issues.push({ field: "amount", kind: "unknown", message: "Amount must be a positive number." });
|
|
946
|
+
const chain = normalizeChain(raw.chainIn);
|
|
947
|
+
if (!raw.chainIn) issues.push({ field: "chainIn", kind: "missing", message: "Source chain is not stated." });
|
|
948
|
+
else if (!chain) issues.push({ field: "chainIn", kind: "unknown", message: `Chain '${raw.chainIn}' isn't recognized.` });
|
|
949
|
+
const sym = normalizeToken(raw.tokenIn);
|
|
950
|
+
if (!raw.tokenIn) issues.push({ field: "tokenIn", kind: "missing", message: "Source token is not stated." });
|
|
951
|
+
else if (!sym) issues.push({ field: "tokenIn", kind: "unknown", message: `Token '${raw.tokenIn}' isn't recognized.` });
|
|
952
|
+
else if (chain && !findAsset(supported, chain.id, sym)) {
|
|
953
|
+
issues.push({ field: "tokenIn", kind: "unsupported", message: `${sym} isn't supported on ${chain.name} yet.` });
|
|
954
|
+
}
|
|
955
|
+
const currency = normalizeFiatCurrency(raw.fiatCurrency);
|
|
956
|
+
if (!raw.fiatCurrency) {
|
|
957
|
+
issues.push({ field: "fiatCurrency", kind: "missing", message: "Say which currency to receive, e.g. NGN." });
|
|
958
|
+
} else if (!currency) {
|
|
959
|
+
const known = Object.keys(FIAT_CORRIDORS).join(", ");
|
|
960
|
+
issues.push({ field: "fiatCurrency", kind: "unsupported", message: `Cash out to ${raw.fiatCurrency} isn't available yet (only ${known}).` });
|
|
961
|
+
}
|
|
962
|
+
return issues;
|
|
963
|
+
}
|
|
964
|
+
function resolveOfframp(raw, supported) {
|
|
965
|
+
const chain = normalizeChain(raw.chainIn);
|
|
966
|
+
const sym = normalizeToken(raw.tokenIn);
|
|
967
|
+
const currency = normalizeFiatCurrency(raw.fiatCurrency);
|
|
968
|
+
const asset = chain && sym ? findAsset(supported, chain.id, sym) : void 0;
|
|
969
|
+
if (!chain || !asset || !currency || !raw.amount) throw new Error("resolveOfframp called on an invalid intent");
|
|
970
|
+
return {
|
|
971
|
+
chainId: chain.id,
|
|
972
|
+
chainName: chain.name,
|
|
973
|
+
asset: asset.symbol.toUpperCase(),
|
|
974
|
+
token: (0, import_viem4.getAddress)(asset.address),
|
|
975
|
+
decimals: asset.decimals,
|
|
976
|
+
amount: (0, import_viem4.parseUnits)(raw.amount, asset.decimals),
|
|
977
|
+
corridor: FIAT_CORRIDORS[currency]
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
|
|
814
981
|
// src/session.ts
|
|
815
982
|
var ERC20_TRANSFER_ABI = [
|
|
816
983
|
{
|
|
@@ -827,7 +994,7 @@ var ERC20_TRANSFER_ABI = [
|
|
|
827
994
|
var errMessage = (e) => e instanceof Error ? e.message : typeof e === "string" ? e : "Something went wrong.";
|
|
828
995
|
function fmtUnits(amount, decimals) {
|
|
829
996
|
if (amount === void 0) return "";
|
|
830
|
-
const human = (0,
|
|
997
|
+
const human = (0, import_viem5.formatUnits)(typeof amount === "bigint" ? amount : BigInt(amount), decimals);
|
|
831
998
|
const n = Number(human);
|
|
832
999
|
return Number.isFinite(n) ? n.toLocaleString("en-US", { maximumFractionDigits: 6 }) : human;
|
|
833
1000
|
}
|
|
@@ -862,7 +1029,8 @@ var INITIAL = {
|
|
|
862
1029
|
error: null,
|
|
863
1030
|
isConnected: false,
|
|
864
1031
|
queueIndex: 0,
|
|
865
|
-
queueTotal: 0
|
|
1032
|
+
queueTotal: 0,
|
|
1033
|
+
offramp: null
|
|
866
1034
|
};
|
|
867
1035
|
function createIntentSession(options = {}) {
|
|
868
1036
|
const api = options.api ?? createApiClient(options);
|
|
@@ -907,6 +1075,7 @@ function createIntentSession(options = {}) {
|
|
|
907
1075
|
}
|
|
908
1076
|
}
|
|
909
1077
|
async function quoteFor(raw, gen) {
|
|
1078
|
+
if (raw.action === "offramp") return;
|
|
910
1079
|
if (!wallet) return set({ phase: "needsWallet" });
|
|
911
1080
|
try {
|
|
912
1081
|
set({ phase: "quoting", error: null });
|
|
@@ -1057,6 +1226,12 @@ function createIntentSession(options = {}) {
|
|
|
1057
1226
|
const parsed = await parseIntent(text, { hfAccessToken, model: options.hfModel, fetch: api.fetch });
|
|
1058
1227
|
if (gen !== generation) return;
|
|
1059
1228
|
if (!parsed.ok) return set({ phase: "offTemplate", hint: parsed.message });
|
|
1229
|
+
const offramp = parsed.intents.find((i) => i.action === "offramp");
|
|
1230
|
+
if (offramp) {
|
|
1231
|
+
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);
|
|
1232
|
+
if (issues.length) return set({ phase: "invalid", issues });
|
|
1233
|
+
return set({ phase: "offramp", offramp: resolveOfframp(offramp, supported) });
|
|
1234
|
+
}
|
|
1060
1235
|
for (const intent of parsed.intents) {
|
|
1061
1236
|
const issues = validateIntent(intent, supported);
|
|
1062
1237
|
if (issues.length) return set({ phase: "invalid", issues });
|
|
@@ -1126,7 +1301,8 @@ function viemWallet(client) {
|
|
|
1126
1301
|
functionName: req.functionName,
|
|
1127
1302
|
args: req.args
|
|
1128
1303
|
}),
|
|
1129
|
-
sendTransaction: (req) => client.sendTransaction({ account, chain: null, to: req.to, value: req.value })
|
|
1304
|
+
sendTransaction: (req) => client.sendTransaction({ account, chain: null, to: req.to, value: req.value }),
|
|
1305
|
+
signMessage: (message) => client.signMessage({ account, message })
|
|
1130
1306
|
};
|
|
1131
1307
|
}
|
|
1132
1308
|
function stripDomainType(types) {
|
|
@@ -1134,15 +1310,409 @@ function stripDomainType(types) {
|
|
|
1134
1310
|
for (const [name, fields] of Object.entries(types)) if (name !== "EIP712Domain") out[name] = fields;
|
|
1135
1311
|
return out;
|
|
1136
1312
|
}
|
|
1313
|
+
|
|
1314
|
+
// src/fiat/payee.ts
|
|
1315
|
+
var import_aes = require("@noble/ciphers/aes");
|
|
1316
|
+
var import_secp256k1 = require("@noble/curves/secp256k1");
|
|
1317
|
+
var import_hkdf = require("@noble/hashes/hkdf");
|
|
1318
|
+
var import_sha256 = require("@noble/hashes/sha256");
|
|
1319
|
+
var import_viem6 = require("viem");
|
|
1320
|
+
var ALG = "ECIES-secp256k1-AES256GCM";
|
|
1321
|
+
var ENVELOPE_VERSION = 1;
|
|
1322
|
+
var PAYLOAD_VERSION = 1;
|
|
1323
|
+
var GCM_TAG_BYTES = 16;
|
|
1324
|
+
var SALT_BYTES = 32;
|
|
1325
|
+
var MAX_NAME_LENGTH = 140;
|
|
1326
|
+
var HKDF_INFO = (0, import_viem6.stringToBytes)("recoilpay/fiat-payee/v1");
|
|
1327
|
+
var RAIL = /^[A-Za-z]{2}\.[A-Za-z0-9_]{1,30}$/;
|
|
1328
|
+
var BANK_CODE = /^[A-Za-z0-9]{0,16}$/;
|
|
1329
|
+
var ACCOUNT = /^\+?[A-Za-z0-9]{1,34}$/;
|
|
1330
|
+
var ASCII_SPACE = /[ \t\r\n]/g;
|
|
1331
|
+
function payeeIdentity(payee) {
|
|
1332
|
+
const rail = payee.rail.replace(ASCII_SPACE, "");
|
|
1333
|
+
const bankCode = payee.bankCode.replace(ASCII_SPACE, "");
|
|
1334
|
+
const account = payee.account.replace(ASCII_SPACE, "").replace(/-/g, "");
|
|
1335
|
+
if (!RAIL.test(rail)) throw new Error(`invalid rail: ${JSON.stringify(payee.rail)}`);
|
|
1336
|
+
if (!BANK_CODE.test(bankCode)) throw new Error("invalid bank code");
|
|
1337
|
+
if (!ACCOUNT.test(account)) throw new Error("invalid account number");
|
|
1338
|
+
return {
|
|
1339
|
+
rail: rail.toLowerCase(),
|
|
1340
|
+
bank_code: bankCode.toUpperCase(),
|
|
1341
|
+
account: account.toUpperCase()
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
function canonicalPayeeIdentity(identity) {
|
|
1345
|
+
const { account, bank_code, rail } = payeeIdentity({
|
|
1346
|
+
rail: identity.rail,
|
|
1347
|
+
bankCode: identity.bank_code,
|
|
1348
|
+
account: identity.account
|
|
1349
|
+
});
|
|
1350
|
+
return JSON.stringify({ account, bank_code, rail });
|
|
1351
|
+
}
|
|
1352
|
+
function payeeCommitment(identity, salt) {
|
|
1353
|
+
const saltBytes = (0, import_viem6.hexToBytes)(salt);
|
|
1354
|
+
if (saltBytes.length !== SALT_BYTES) throw new Error(`salt must be ${SALT_BYTES} bytes`);
|
|
1355
|
+
return (0, import_viem6.keccak256)((0, import_viem6.concat)([(0, import_viem6.stringToBytes)(canonicalPayeeIdentity(identity)), saltBytes]));
|
|
1356
|
+
}
|
|
1357
|
+
function normaliseName(name) {
|
|
1358
|
+
const n = name.normalize("NFC").trim().replace(/\s+/g, " ");
|
|
1359
|
+
if (n.length === 0) throw new Error("the account holder name is required");
|
|
1360
|
+
if (n.length > MAX_NAME_LENGTH) throw new Error("the account holder name is too long");
|
|
1361
|
+
return n;
|
|
1362
|
+
}
|
|
1363
|
+
function deriveKey(sharedX, ephemeralPub) {
|
|
1364
|
+
return (0, import_hkdf.hkdf)(import_sha256.sha256, sharedX, ephemeralPub, HKDF_INFO, 32);
|
|
1365
|
+
}
|
|
1366
|
+
function sealPayee(payee, solverPubkey, salt) {
|
|
1367
|
+
const identity = payeeIdentity(payee);
|
|
1368
|
+
const name = normaliseName(payee.name);
|
|
1369
|
+
const saltHex = salt ?? (0, import_viem6.bytesToHex)(crypto.getRandomValues(new Uint8Array(SALT_BYTES)));
|
|
1370
|
+
const commitment = payeeCommitment(identity, saltHex);
|
|
1371
|
+
const recipient = (0, import_viem6.hexToBytes)(solverPubkey);
|
|
1372
|
+
if (recipient.length !== 65 || recipient[0] !== 4) {
|
|
1373
|
+
throw new Error("solver public key must be 65 bytes uncompressed (0x04\u2026)");
|
|
1374
|
+
}
|
|
1375
|
+
const plaintext = (0, import_viem6.stringToBytes)(
|
|
1376
|
+
JSON.stringify({ v: PAYLOAD_VERSION, ...identity, name, salt: saltHex })
|
|
1377
|
+
);
|
|
1378
|
+
const ephemeralPriv = import_secp256k1.secp256k1.utils.randomPrivateKey();
|
|
1379
|
+
const ephemeralPub = import_secp256k1.secp256k1.getPublicKey(ephemeralPriv, false);
|
|
1380
|
+
const shared = import_secp256k1.secp256k1.getSharedSecret(ephemeralPriv, recipient, true).slice(1);
|
|
1381
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
1382
|
+
const out = (0, import_aes.gcm)(deriveKey(shared, ephemeralPub), iv).encrypt(plaintext);
|
|
1383
|
+
return {
|
|
1384
|
+
sealed: {
|
|
1385
|
+
v: ENVELOPE_VERSION,
|
|
1386
|
+
alg: ALG,
|
|
1387
|
+
epk: (0, import_viem6.bytesToHex)(ephemeralPub),
|
|
1388
|
+
iv: (0, import_viem6.bytesToHex)(iv),
|
|
1389
|
+
// @noble appends the tag; split so any standard GCM can read it.
|
|
1390
|
+
ct: (0, import_viem6.bytesToHex)(out.slice(0, out.length - GCM_TAG_BYTES)),
|
|
1391
|
+
tag: (0, import_viem6.bytesToHex)(out.slice(out.length - GCM_TAG_BYTES))
|
|
1392
|
+
},
|
|
1393
|
+
commitment,
|
|
1394
|
+
salt: saltHex
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
function openPayee(sealed, solverPrivkey, expectedCommitment) {
|
|
1398
|
+
if (sealed.alg !== ALG) throw new Error(`unsupported envelope algorithm: ${sealed.alg}`);
|
|
1399
|
+
if (sealed.v !== ENVELOPE_VERSION) throw new Error(`unsupported envelope version: ${sealed.v}`);
|
|
1400
|
+
const epk = (0, import_viem6.hexToBytes)(sealed.epk);
|
|
1401
|
+
const shared = import_secp256k1.secp256k1.getSharedSecret((0, import_viem6.hexToBytes)(solverPrivkey), epk, true).slice(1);
|
|
1402
|
+
const plaintext = (0, import_aes.gcm)(deriveKey(shared, epk), (0, import_viem6.hexToBytes)(sealed.iv)).decrypt(
|
|
1403
|
+
(0, import_viem6.concat)([(0, import_viem6.hexToBytes)(sealed.ct), (0, import_viem6.hexToBytes)(sealed.tag)])
|
|
1404
|
+
);
|
|
1405
|
+
const body = JSON.parse(new TextDecoder().decode(plaintext));
|
|
1406
|
+
if (body.v !== PAYLOAD_VERSION) throw new Error(`unsupported payee payload version: ${String(body.v)}`);
|
|
1407
|
+
const identity = payeeIdentity({
|
|
1408
|
+
rail: String(body.rail ?? ""),
|
|
1409
|
+
bankCode: String(body.bank_code ?? ""),
|
|
1410
|
+
account: String(body.account ?? "")
|
|
1411
|
+
});
|
|
1412
|
+
const salt = String(body.salt ?? "");
|
|
1413
|
+
if (payeeCommitment(identity, salt).toLowerCase() !== expectedCommitment.toLowerCase()) {
|
|
1414
|
+
throw new Error("sealed payee details do not match the trade commitment");
|
|
1415
|
+
}
|
|
1416
|
+
return { identity, name: normaliseName(String(body.name ?? "")), salt };
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// src/fiat/offramp.ts
|
|
1420
|
+
var import_viem7 = require("viem");
|
|
1421
|
+
|
|
1422
|
+
// src/fiat/actions.ts
|
|
1423
|
+
function fiatConfirmMessage(tradeId) {
|
|
1424
|
+
return `RecoilPay: I confirm I received the fiat payment for trade ${tradeId}.`;
|
|
1425
|
+
}
|
|
1426
|
+
function fiatDisputeMessage(tradeId, reason) {
|
|
1427
|
+
return `RecoilPay: I dispute trade ${tradeId}. Reason: ${reason.trim()}`;
|
|
1428
|
+
}
|
|
1429
|
+
function fiatPaymentSentMessage(tradeId, reference) {
|
|
1430
|
+
return `RecoilPay: I have sent the fiat payment for trade ${tradeId}. Reference: ${reference.trim()}`;
|
|
1431
|
+
}
|
|
1432
|
+
function fiatClaimMessage(tradeId, reason) {
|
|
1433
|
+
return `RecoilPay: I claim the fiat payment for trade ${tradeId} never arrived. Reason: ${reason.trim()}`;
|
|
1434
|
+
}
|
|
1435
|
+
function fiatRevealMessage(tradeId) {
|
|
1436
|
+
return `RecoilPay: I reveal the bank details I gave for trade ${tradeId}.`;
|
|
1437
|
+
}
|
|
1438
|
+
function fiatStatementMessage(tradeId, text) {
|
|
1439
|
+
return `RecoilPay: My statement on trade ${tradeId}: ${text.trim()}`;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// src/fiat/offramp.ts
|
|
1443
|
+
var TERMINAL = ["settled_to_fiat_payer", "refunded_to_funder", "failed"];
|
|
1444
|
+
var PHASE_OF = {
|
|
1445
|
+
quoted: "locking",
|
|
1446
|
+
awaiting_fiat: "awaitingFiat",
|
|
1447
|
+
awaiting_confirmation: "awaitingConfirmation",
|
|
1448
|
+
disputed: "disputed",
|
|
1449
|
+
settled_to_fiat_payer: "settled",
|
|
1450
|
+
refunded_to_funder: "refunded",
|
|
1451
|
+
failed: "failed"
|
|
1452
|
+
};
|
|
1453
|
+
var PLAIN_TEXT = /^[\x20-\x7e]+$/;
|
|
1454
|
+
var MAX_REASON = 500;
|
|
1455
|
+
var errMessage2 = (e) => e instanceof Error ? e.message : typeof e === "string" ? e : "Something went wrong.";
|
|
1456
|
+
var same = (a, b) => typeof a === "string" && typeof b === "string" && a.toLowerCase() === b.toLowerCase();
|
|
1457
|
+
function fmtAsset(amount, decimals) {
|
|
1458
|
+
const n = Number((0, import_viem7.formatUnits)(amount, decimals));
|
|
1459
|
+
return n.toLocaleString("en-US", { maximumFractionDigits: 6 });
|
|
1460
|
+
}
|
|
1461
|
+
function checkLock(lock, trade, request, quote, user) {
|
|
1462
|
+
const m = lock.typedData.message;
|
|
1463
|
+
const d = lock.typedData.domain;
|
|
1464
|
+
const problems = [
|
|
1465
|
+
lock.typedData.primaryType !== "PermitWitnessTransferFrom" && "not a Permit2 witness transfer",
|
|
1466
|
+
!same(d.verifyingContract, PERMIT2_ADDRESS) && "not signed for Permit2",
|
|
1467
|
+
Number(d.chainId) !== request.chainId && "the wrong chain",
|
|
1468
|
+
!same(m.permitted?.token, request.token) && "the wrong token",
|
|
1469
|
+
m.permitted?.amount !== request.amount.toString() && "the wrong amount",
|
|
1470
|
+
!same(m.spender, lock.escrow) && "a spender other than the escrow",
|
|
1471
|
+
!same(m.witness?.tradeId, lock.tradeId) && "another trade",
|
|
1472
|
+
!same(m.witness?.counterparty, trade.solver_address) && "a counterparty other than the solver",
|
|
1473
|
+
trade.quote_id !== quote.quoteId && "another quote",
|
|
1474
|
+
trade.fiat_minor_units !== quote.fiatNet && "a payout other than the one quoted",
|
|
1475
|
+
!same(trade.user_address, user) && "another wallet"
|
|
1476
|
+
].filter(Boolean);
|
|
1477
|
+
if (problems.length) throw new Error(`Refusing to sign: the lock names ${problems.join(", ")}.`);
|
|
1478
|
+
}
|
|
1479
|
+
function fmtRate(rate) {
|
|
1480
|
+
const n = Number(rate);
|
|
1481
|
+
return Number.isFinite(n) ? n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 6 }) : rate;
|
|
1482
|
+
}
|
|
1483
|
+
var INITIAL2 = {
|
|
1484
|
+
phase: "idle",
|
|
1485
|
+
request: null,
|
|
1486
|
+
quote: null,
|
|
1487
|
+
preview: null,
|
|
1488
|
+
needsApproval: false,
|
|
1489
|
+
trade: null,
|
|
1490
|
+
payeeSalt: null,
|
|
1491
|
+
error: null
|
|
1492
|
+
};
|
|
1493
|
+
function createOfframpSession(options = {}) {
|
|
1494
|
+
const api = options.api ?? createApiClient(options);
|
|
1495
|
+
const pollMs = options.pollIntervalMs ?? 3e3;
|
|
1496
|
+
let chainsPromise = null;
|
|
1497
|
+
const chains = () => chainsPromise ??= api.getChains().catch((e) => (chainsPromise = null, Promise.reject(e)));
|
|
1498
|
+
const reader = options.chainReader ?? rpcReaders(chains);
|
|
1499
|
+
let wallet = options.wallet ?? null;
|
|
1500
|
+
let pollTimer;
|
|
1501
|
+
let destroyed = false;
|
|
1502
|
+
let generation = 0;
|
|
1503
|
+
let state = { ...INITIAL2 };
|
|
1504
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1505
|
+
function set(patch) {
|
|
1506
|
+
if (destroyed) return;
|
|
1507
|
+
state = { ...state, ...patch };
|
|
1508
|
+
for (const l of listeners) l(state);
|
|
1509
|
+
}
|
|
1510
|
+
const stopPolling = () => {
|
|
1511
|
+
if (pollTimer) clearTimeout(pollTimer);
|
|
1512
|
+
pollTimer = void 0;
|
|
1513
|
+
};
|
|
1514
|
+
function showTrade(trade, patch = {}) {
|
|
1515
|
+
set({ trade, phase: PHASE_OF[trade.state], ...patch });
|
|
1516
|
+
}
|
|
1517
|
+
function track(tradeId, gen) {
|
|
1518
|
+
stopPolling();
|
|
1519
|
+
const poll = async () => {
|
|
1520
|
+
try {
|
|
1521
|
+
const trade = await api.getFiatTrade(tradeId);
|
|
1522
|
+
if (gen !== generation) return;
|
|
1523
|
+
if (state.phase === "confirming" || state.phase === "disputing") set({ trade });
|
|
1524
|
+
else showTrade(trade);
|
|
1525
|
+
if (TERMINAL.includes(trade.state)) return;
|
|
1526
|
+
} catch {
|
|
1527
|
+
}
|
|
1528
|
+
if (gen !== generation || destroyed) return;
|
|
1529
|
+
const wait = state.trade?.state === "disputed" ? pollMs * 10 : pollMs;
|
|
1530
|
+
pollTimer = setTimeout(poll, wait);
|
|
1531
|
+
};
|
|
1532
|
+
void poll();
|
|
1533
|
+
}
|
|
1534
|
+
async function quote(request) {
|
|
1535
|
+
const gen = ++generation;
|
|
1536
|
+
stopPolling();
|
|
1537
|
+
set({ ...INITIAL2, request, phase: wallet ? "quoting" : "needsWallet" });
|
|
1538
|
+
if (!wallet) return;
|
|
1539
|
+
try {
|
|
1540
|
+
const res = await api.rankFiatQuotes({
|
|
1541
|
+
direction: "offramp",
|
|
1542
|
+
chain: `eip155:${request.chainId}`,
|
|
1543
|
+
asset: request.asset,
|
|
1544
|
+
fiatCurrency: request.corridor.currency,
|
|
1545
|
+
rail: request.corridor.rail,
|
|
1546
|
+
assetAmount: request.amount.toString()
|
|
1547
|
+
});
|
|
1548
|
+
const best = res.quotes?.[0];
|
|
1549
|
+
if (!best) throw new Error(`No solver is cashing out ${request.asset} on ${request.chainName} to ${request.corridor.currency} for this amount right now.`);
|
|
1550
|
+
const pc = await reader(request.chainId).catch(() => null);
|
|
1551
|
+
const needsApproval = pc ? !await hasPermit2Allowance(pc, request.token, wallet.address, request.amount).catch(() => false) : true;
|
|
1552
|
+
if (gen !== generation) return;
|
|
1553
|
+
const c = request.corridor;
|
|
1554
|
+
set({
|
|
1555
|
+
phase: "quoted",
|
|
1556
|
+
quote: best,
|
|
1557
|
+
needsApproval,
|
|
1558
|
+
preview: {
|
|
1559
|
+
payAmount: fmtAsset(request.amount, request.decimals),
|
|
1560
|
+
paySymbol: request.asset,
|
|
1561
|
+
chainName: request.chainName,
|
|
1562
|
+
receiveAmount: formatFiat(best.fiatNet, c),
|
|
1563
|
+
currency: c.currency,
|
|
1564
|
+
currencySymbol: c.symbol,
|
|
1565
|
+
fee: formatFiat(best.fiatFee, c),
|
|
1566
|
+
rate: fmtRate(best.rate),
|
|
1567
|
+
paymentWindowSecs: best.paymentWindowSecs,
|
|
1568
|
+
quotesConsidered: res.totalConsidered
|
|
1569
|
+
}
|
|
1570
|
+
});
|
|
1571
|
+
} catch (e) {
|
|
1572
|
+
if (gen === generation) set({ phase: "error", error: errMessage2(e) });
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
async function confirm(payee) {
|
|
1576
|
+
const { request, quote: chosen } = state;
|
|
1577
|
+
const w = wallet;
|
|
1578
|
+
if (!request || !chosen || !w || state.phase !== "quoted") return;
|
|
1579
|
+
const gen = generation;
|
|
1580
|
+
try {
|
|
1581
|
+
set({ error: null });
|
|
1582
|
+
const details = { ...payee, rail: request.corridor.rail };
|
|
1583
|
+
payeeIdentity(details);
|
|
1584
|
+
if (!request.corridor.accountPattern.test(details.account.replace(/[\s-]/g, ""))) {
|
|
1585
|
+
throw new Error(`That doesn't look like a valid ${request.corridor.accountLabel.toLowerCase()}.`);
|
|
1586
|
+
}
|
|
1587
|
+
if (!details.name.trim()) throw new Error("Enter the account holder's name.");
|
|
1588
|
+
if (await w.getChainId() !== request.chainId) {
|
|
1589
|
+
set({ phase: "switchingChain" });
|
|
1590
|
+
await w.switchChain(request.chainId);
|
|
1591
|
+
}
|
|
1592
|
+
const pc = await reader(request.chainId).catch(() => null);
|
|
1593
|
+
const needsApproval = pc ? !await hasPermit2Allowance(pc, request.token, w.address, request.amount) : state.needsApproval;
|
|
1594
|
+
if (needsApproval) {
|
|
1595
|
+
set({ phase: "approving" });
|
|
1596
|
+
const approve = buildPermit2ApproveRequest(request.token);
|
|
1597
|
+
const hash = await w.writeContract({ chainId: request.chainId, ...approve, abi: approve.abi });
|
|
1598
|
+
if (pc) await pc.waitForTransactionReceipt({ hash });
|
|
1599
|
+
set({ needsApproval: false });
|
|
1600
|
+
}
|
|
1601
|
+
set({ phase: "opening" });
|
|
1602
|
+
const { sealed, commitment, salt } = sealPayee(details, chosen.payeeKey);
|
|
1603
|
+
const opened = await api.openFiatTrade({
|
|
1604
|
+
quoteId: chosen.quoteId,
|
|
1605
|
+
assetAmount: request.amount.toString(),
|
|
1606
|
+
userAddress: (0, import_viem7.getAddress)(w.address),
|
|
1607
|
+
payee: { sealed, commitment }
|
|
1608
|
+
});
|
|
1609
|
+
if (gen !== generation) return;
|
|
1610
|
+
set({ trade: opened.trade, payeeSalt: salt });
|
|
1611
|
+
checkLock(opened.lock, opened.trade, request, chosen, w.address);
|
|
1612
|
+
if (!same(opened.trade.payee_commitment, commitment)) throw new Error("Refusing to sign: the trade is not for the bank details you entered.");
|
|
1613
|
+
set({ phase: "signing" });
|
|
1614
|
+
const { domain, types, primaryType, message } = opened.lock.typedData;
|
|
1615
|
+
const signature = await w.signTypedData({ domain, types, primaryType, message });
|
|
1616
|
+
set({ phase: "locking" });
|
|
1617
|
+
const trade = await api.submitFiatLock(opened.trade.id, signature);
|
|
1618
|
+
if (gen !== generation) return;
|
|
1619
|
+
showTrade(trade);
|
|
1620
|
+
track(trade.id, gen);
|
|
1621
|
+
} catch (e) {
|
|
1622
|
+
if (gen !== generation) return;
|
|
1623
|
+
if (state.trade) {
|
|
1624
|
+
set({ error: errMessage2(e) });
|
|
1625
|
+
track(state.trade.id, gen);
|
|
1626
|
+
} else set({ phase: "error", error: errMessage2(e) });
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
async function act(phase, message, send) {
|
|
1630
|
+
const trade = state.trade;
|
|
1631
|
+
const w = wallet;
|
|
1632
|
+
if (!trade || trade.state !== "awaiting_confirmation") return;
|
|
1633
|
+
const gen = generation;
|
|
1634
|
+
const before = state.phase;
|
|
1635
|
+
try {
|
|
1636
|
+
if (!w) throw new Error("Connect the wallet that opened this trade.");
|
|
1637
|
+
if (!same(w.address, trade.user_address)) throw new Error("Switch to the wallet that opened this trade.");
|
|
1638
|
+
if (!w.signMessage) throw new Error("This wallet can't sign messages, so it can't confirm or dispute.");
|
|
1639
|
+
set({ phase, error: null });
|
|
1640
|
+
const signature = await w.signMessage(message(trade.id));
|
|
1641
|
+
const next = await send(trade.id, signature);
|
|
1642
|
+
if (gen !== generation) return;
|
|
1643
|
+
showTrade(next);
|
|
1644
|
+
track(next.id, gen);
|
|
1645
|
+
} catch (e) {
|
|
1646
|
+
if (gen === generation) set({ phase: before, error: errMessage2(e) });
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
async function dispute(reason) {
|
|
1650
|
+
const r = reason.trim();
|
|
1651
|
+
if (!r || r.length > MAX_REASON || !PLAIN_TEXT.test(r)) {
|
|
1652
|
+
set({ error: `Describe the problem in 1 to ${MAX_REASON} plain characters (letters, digits, punctuation).` });
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
await act("disputing", (id) => fiatDisputeMessage(id, r), (id, sig) => api.disputeFiatTrade(id, r, sig));
|
|
1656
|
+
}
|
|
1657
|
+
async function resume(tradeId) {
|
|
1658
|
+
const gen = ++generation;
|
|
1659
|
+
stopPolling();
|
|
1660
|
+
set({ ...INITIAL2, phase: "quoting" });
|
|
1661
|
+
try {
|
|
1662
|
+
const trade = await api.getFiatTrade(tradeId);
|
|
1663
|
+
if (gen !== generation) return;
|
|
1664
|
+
showTrade(trade);
|
|
1665
|
+
track(trade.id, gen);
|
|
1666
|
+
} catch (e) {
|
|
1667
|
+
if (gen === generation) set({ phase: "error", error: errMessage2(e) });
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
function reset() {
|
|
1671
|
+
generation++;
|
|
1672
|
+
stopPolling();
|
|
1673
|
+
set({ ...INITIAL2 });
|
|
1674
|
+
}
|
|
1675
|
+
function setWallet(next) {
|
|
1676
|
+
const changed = wallet?.address !== next?.address;
|
|
1677
|
+
wallet = next;
|
|
1678
|
+
if (next && state.request && !state.trade && (state.phase === "needsWallet" || changed && state.phase === "quoted")) {
|
|
1679
|
+
void quote(state.request);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
return {
|
|
1683
|
+
getState: () => state,
|
|
1684
|
+
subscribe(listener) {
|
|
1685
|
+
listeners.add(listener);
|
|
1686
|
+
return () => listeners.delete(listener);
|
|
1687
|
+
},
|
|
1688
|
+
quote,
|
|
1689
|
+
confirm,
|
|
1690
|
+
confirmReceived: () => act("confirming", fiatConfirmMessage, (id, sig) => api.confirmFiatTrade(id, sig)),
|
|
1691
|
+
dispute,
|
|
1692
|
+
resume,
|
|
1693
|
+
reset,
|
|
1694
|
+
setWallet,
|
|
1695
|
+
destroy() {
|
|
1696
|
+
generation++;
|
|
1697
|
+
stopPolling();
|
|
1698
|
+
destroyed = true;
|
|
1699
|
+
listeners.clear();
|
|
1700
|
+
}
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1137
1703
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1138
1704
|
0 && (module.exports = {
|
|
1139
1705
|
ApiError,
|
|
1706
|
+
BANK_DETAILS_HINT,
|
|
1140
1707
|
CHAIN_ALIASES,
|
|
1141
1708
|
DEFAULT_API_URL,
|
|
1142
1709
|
DEFAULT_INTENT_MODEL,
|
|
1143
1710
|
EMPTY_SUPPORTED,
|
|
1711
|
+
FIAT_CORRIDORS,
|
|
1712
|
+
FIAT_CURRENCY_ALIASES,
|
|
1144
1713
|
HF_ROUTER_URL,
|
|
1145
1714
|
MAX_UINT256,
|
|
1715
|
+
NG_BANKS,
|
|
1146
1716
|
PERMIT2_ADDRESS,
|
|
1147
1717
|
SOLANA_CHAIN_IDS,
|
|
1148
1718
|
TEMPLATE_HINT,
|
|
@@ -1151,23 +1721,41 @@ function stripDomainType(types) {
|
|
|
1151
1721
|
buildPermit2ApproveRequest,
|
|
1152
1722
|
buildQuoteRequest,
|
|
1153
1723
|
buildSupportedSet,
|
|
1724
|
+
canonicalPayeeIdentity,
|
|
1154
1725
|
chainName,
|
|
1726
|
+
checkLock,
|
|
1155
1727
|
createApiClient,
|
|
1156
1728
|
createIntentSession,
|
|
1729
|
+
createOfframpSession,
|
|
1157
1730
|
explorerUrl,
|
|
1158
1731
|
extractIntents,
|
|
1732
|
+
fiatClaimMessage,
|
|
1733
|
+
fiatConfirmMessage,
|
|
1734
|
+
fiatDisputeMessage,
|
|
1735
|
+
fiatPaymentSentMessage,
|
|
1736
|
+
fiatRevealMessage,
|
|
1737
|
+
fiatStatementMessage,
|
|
1159
1738
|
findAsset,
|
|
1739
|
+
formatFiat,
|
|
1160
1740
|
getPermit2Allowance,
|
|
1161
1741
|
hasPermit2Allowance,
|
|
1162
1742
|
interopAddress,
|
|
1163
1743
|
isTerminalStatus,
|
|
1744
|
+
looksLikeBankDetails,
|
|
1164
1745
|
normalizeChain,
|
|
1746
|
+
normalizeFiatCurrency,
|
|
1165
1747
|
normalizeToken,
|
|
1748
|
+
openPayee,
|
|
1166
1749
|
parseIntent,
|
|
1750
|
+
payeeCommitment,
|
|
1751
|
+
payeeIdentity,
|
|
1167
1752
|
resolveIntent,
|
|
1753
|
+
resolveOfframp,
|
|
1168
1754
|
rpcReaders,
|
|
1755
|
+
sealPayee,
|
|
1169
1756
|
signQuote,
|
|
1170
1757
|
stripDomainType,
|
|
1171
1758
|
validateIntent,
|
|
1759
|
+
validateOfframp,
|
|
1172
1760
|
viemWallet
|
|
1173
1761
|
});
|