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