@usdctofiat/offramp 1.1.4 → 2.0.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.
@@ -1,5 +1,6 @@
1
1
  // src/config.ts
2
2
  import { getContracts, getGatingServiceAddress } from "@zkp2p/sdk";
3
+ var SDK_VERSION = "1.2.0";
3
4
  var BASE_CHAIN_ID = 8453;
4
5
  var RUNTIME_ENV = "production";
5
6
  var API_BASE_URL = "https://api.zkp2p.xyz";
@@ -15,6 +16,7 @@ var GATING_SERVICE_ADDRESS = getGatingServiceAddress(
15
16
  );
16
17
  var DELEGATE_RATE_MANAGER_ID = "0x8666d6fb0f6797c56e95339fd7ca82fdd348b9db200e10a4c4aa0a0b879fc41c";
17
18
  var RATE_MANAGER_REGISTRY_ADDRESS = "0xeed7db23e724ac4590d6db6f78fda6db203535f3";
19
+ var DELEGATE_MANAGER_FEE_BPS = 10;
18
20
  var REFERRER = "galleonlabs";
19
21
  var MIN_DEPOSIT_USDC = 1;
20
22
  var MIN_ORDER_USDC = 1;
@@ -120,6 +122,17 @@ function resolveSupportedCurrencies(platform) {
120
122
  }
121
123
  return Array.from(codes).sort();
122
124
  }
125
+ function normalizePaypalMeUsername(value) {
126
+ const trimmed = value.trim();
127
+ if (!trimmed) return "";
128
+ const withoutProtocol = trimmed.replace(/^https?:\/\//i, "").replace(/^www\./i, "");
129
+ if (/^paypal\.me(?:[?#].*)?$/i.test(withoutProtocol)) return "";
130
+ const withoutDomain = withoutProtocol.replace(/^paypal\.me\//i, "");
131
+ const [pathWithoutQuery] = withoutDomain.split(/[?#]/, 1);
132
+ const sanitizedPath = pathWithoutQuery.replace(/^\/+/, "");
133
+ const [username] = sanitizedPath.split("/", 1);
134
+ return username.replace(/^@+/, "").trim().toLowerCase();
135
+ }
123
136
  var BLUEPRINTS = {
124
137
  venmo: {
125
138
  id: "venmo",
@@ -167,7 +180,13 @@ var BLUEPRINTS = {
167
180
  placeholder: "wisetag (no @)",
168
181
  helperText: "Your Wise @wisetag (no @)",
169
182
  validation: z.string().min(1).regex(/^[a-zA-Z0-9_-]+$/),
170
- transform: (v) => v.replace(/^@+/, "").trim()
183
+ transform: (v) => v.replace(/^@+/, "").trim(),
184
+ extensionRegistration: {
185
+ providerId: "wise",
186
+ requiredPrompt: "This Wisetag is not registered yet. Verify it in the Peer extension, then refresh and retry with the same Wisetag.",
187
+ ctaLabel: "Install Peer Extension",
188
+ minExtensionVersion: "0.4.12"
189
+ }
171
190
  },
172
191
  mercadopago: {
173
192
  id: "mercadopago",
@@ -188,10 +207,18 @@ var BLUEPRINTS = {
188
207
  paypal: {
189
208
  id: "paypal",
190
209
  name: "PayPal",
191
- identifierLabel: "Email",
192
- placeholder: "email",
193
- helperText: "Email linked to PayPal account",
194
- validation: z.string().email()
210
+ identifierLabel: "PayPal.me Username",
211
+ placeholder: "paypal.me/your-username",
212
+ helperText: "Your PayPal.me username, not your email",
213
+ validation: z.string().min(1, "PayPal.me username is required").regex(/^[a-z0-9._-]+$/i, "Use your PayPal.me username (letters, numbers, . _ -)"),
214
+ transform: normalizePaypalMeUsername,
215
+ extensionRegistration: {
216
+ providerId: "paypal",
217
+ requiredPrompt: "This PayPal username is not registered yet. Verify it in the Peer extension, then refresh and retry with the same PayPal username.",
218
+ ctaLabel: "Install Peer Extension",
219
+ ctaSubtext: "PayPal Business accounts are not supported at this time.",
220
+ minExtensionVersion: "0.4.14"
221
+ }
195
222
  },
196
223
  monzo: {
197
224
  id: "monzo",
@@ -222,6 +249,7 @@ function buildPlatformEntry(bp) {
222
249
  placeholder: bp.placeholder,
223
250
  help: bp.helperText
224
251
  },
252
+ ...bp.extensionRegistration ? { extensionRegistration: bp.extensionRegistration } : {},
225
253
  validate(input) {
226
254
  const transformed = bp.transform ? bp.transform(input) : input;
227
255
  const result = bp.validation.safeParse(transformed);
@@ -248,6 +276,9 @@ var PLATFORMS = {
248
276
  MONZO: buildPlatformEntry(BLUEPRINTS.monzo),
249
277
  N26: buildPlatformEntry(BLUEPRINTS.n26)
250
278
  };
279
+ function getPlatformById(id) {
280
+ return Object.values(PLATFORMS).find((p) => p.id === id) ?? null;
281
+ }
251
282
  function normalizePaymentMethodLookupName(platform) {
252
283
  const normalized = platform.trim().toLowerCase();
253
284
  if (!normalized) return null;
@@ -291,34 +322,37 @@ function getPaymentMethodHashes(platform) {
291
322
  }
292
323
  return Array.from(hashes);
293
324
  }
294
- function buildDepositData(platform, identifier) {
295
- switch (platform) {
296
- case "venmo":
297
- return { venmoUsername: identifier, telegramUsername: "" };
298
- case "cashapp":
299
- return { cashtag: identifier, telegramUsername: "" };
300
- case "chime":
301
- return { chimesign: identifier.toLowerCase(), telegramUsername: "" };
302
- case "revolut":
303
- return { revolutUsername: identifier, telegramUsername: "" };
304
- case "wise":
305
- return { wisetag: identifier, telegramUsername: "" };
306
- case "mercadopago":
307
- return { cvu: identifier, telegramUsername: "" };
308
- case "zelle":
309
- return { zelleEmail: identifier, telegramUsername: "" };
310
- case "paypal":
311
- return { paypalEmail: identifier, telegramUsername: "" };
312
- case "monzo":
313
- return { monzoMeUsername: identifier, telegramUsername: "" };
314
- case "n26":
315
- return { iban: identifier, telegramUsername: "" };
316
- default:
317
- return { identifier, telegramUsername: "" };
318
- }
325
+ function buildDepositData(platform, identifier, telegramUsername = "") {
326
+ const offchainId = platform === "chime" ? identifier.toLowerCase() : identifier;
327
+ return telegramUsername ? { offchainId, telegramUsername } : { offchainId };
328
+ }
329
+ function getPeerExtensionRegistrationInfo(platform) {
330
+ const entry = getPlatformById(platform);
331
+ return entry?.extensionRegistration ?? null;
332
+ }
333
+ var EXTENSION_REGISTRATION_ERROR_PATTERNS = ["creating the maker", "create the maker"];
334
+ function isPeerExtensionRegistrationError(platform, message, statusCode) {
335
+ const info = getPeerExtensionRegistrationInfo(platform);
336
+ if (!info) return false;
337
+ const normalized = (message ?? "").trim();
338
+ if (normalized === info.requiredPrompt) return true;
339
+ if (statusCode === 400) return true;
340
+ if (!normalized) return false;
341
+ const lower = normalized.toLowerCase();
342
+ return EXTENSION_REGISTRATION_ERROR_PATTERNS.some((p) => lower.includes(p));
319
343
  }
320
344
 
321
345
  // src/errors.ts
346
+ var MakersCreateError = class extends Error {
347
+ status;
348
+ body;
349
+ constructor(message, status, body) {
350
+ super(message);
351
+ this.name = "MakersCreateError";
352
+ this.status = status;
353
+ this.body = body;
354
+ }
355
+ };
322
356
  var OfframpError = class extends Error {
323
357
  code;
324
358
  step;
@@ -967,6 +1001,108 @@ async function undelegate(walletClient, depositId, escrowAddress) {
967
1001
  return hash;
968
1002
  }
969
1003
 
1004
+ // src/telemetry.ts
1005
+ var SDK_EVENTS_ENDPOINT = "https://usdctofiat.xyz/api/sdk-events";
1006
+ var MAX_BATCH_SIZE = 50;
1007
+ var FLUSH_DELAY_MS = 180;
1008
+ var TELEMETRY_TIMEOUT_MS = 2e3;
1009
+ var queuedEvents = [];
1010
+ var flushTimer = null;
1011
+ var flushInFlight = false;
1012
+ function sanitizeAttributionId(raw) {
1013
+ if (typeof raw !== "string") return void 0;
1014
+ const stripped = raw.trim().replace(/[^a-zA-Z0-9_-]/g, "");
1015
+ if (!stripped) return void 0;
1016
+ if (stripped.length > 64) return void 0;
1017
+ return stripped;
1018
+ }
1019
+ function scheduleFlush() {
1020
+ if (flushTimer != null) return;
1021
+ flushTimer = setTimeout(() => {
1022
+ flushTimer = null;
1023
+ void flushQueue();
1024
+ }, FLUSH_DELAY_MS);
1025
+ }
1026
+ async function postBatch(batch) {
1027
+ if (!batch.length) return;
1028
+ const body = JSON.stringify(batch);
1029
+ try {
1030
+ if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
1031
+ const blob = new Blob([body], { type: "application/json" });
1032
+ const accepted = navigator.sendBeacon(SDK_EVENTS_ENDPOINT, blob);
1033
+ if (accepted) return;
1034
+ }
1035
+ } catch {
1036
+ }
1037
+ try {
1038
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
1039
+ const timeout = controller ? setTimeout(() => controller.abort(), TELEMETRY_TIMEOUT_MS) : null;
1040
+ await fetch(SDK_EVENTS_ENDPOINT, {
1041
+ method: "POST",
1042
+ headers: { "Content-Type": "application/json" },
1043
+ body,
1044
+ keepalive: true,
1045
+ signal: controller?.signal
1046
+ }).catch(() => {
1047
+ });
1048
+ if (timeout) clearTimeout(timeout);
1049
+ } catch {
1050
+ }
1051
+ }
1052
+ async function flushQueue() {
1053
+ if (flushInFlight) return;
1054
+ if (!queuedEvents.length) return;
1055
+ flushInFlight = true;
1056
+ try {
1057
+ while (queuedEvents.length > 0) {
1058
+ const batch = queuedEvents.slice(0, MAX_BATCH_SIZE);
1059
+ queuedEvents = queuedEvents.slice(batch.length);
1060
+ await postBatch(batch);
1061
+ }
1062
+ } finally {
1063
+ flushInFlight = false;
1064
+ }
1065
+ }
1066
+ function emitEvent(name, payload) {
1067
+ try {
1068
+ queuedEvents.push({
1069
+ name,
1070
+ payload,
1071
+ ts: Date.now()
1072
+ });
1073
+ if (queuedEvents.length >= MAX_BATCH_SIZE) {
1074
+ void flushQueue();
1075
+ return;
1076
+ }
1077
+ scheduleFlush();
1078
+ } catch {
1079
+ }
1080
+ }
1081
+ function sendTelemetryEvent(name, payload) {
1082
+ emitEvent(name, payload);
1083
+ }
1084
+ function createTelemetryContext(options) {
1085
+ const integratorId = sanitizeAttributionId(options.integratorId);
1086
+ const referralId = sanitizeAttributionId(options.referralId);
1087
+ const enabled = options.telemetry !== false;
1088
+ const sdkVersion = options.sdkVersion;
1089
+ return {
1090
+ enabled,
1091
+ sdkVersion,
1092
+ integratorId,
1093
+ referralId,
1094
+ emit(name, payload = {}) {
1095
+ if (!enabled) return;
1096
+ emitEvent(name, {
1097
+ sdkVersion,
1098
+ integratorId,
1099
+ referralId,
1100
+ ...payload
1101
+ });
1102
+ }
1103
+ };
1104
+ }
1105
+
970
1106
  // src/deposit.ts
971
1107
  import { createPublicClient as createPublicClient3, decodeEventLog, formatUnits as formatUnits2, http as http3, parseUnits } from "viem";
972
1108
  import { base as base3 } from "viem/chains";
@@ -1018,23 +1154,39 @@ function extractDepositIdFromLogs(logs) {
1018
1154
  }
1019
1155
  return null;
1020
1156
  }
1021
- async function registerPayeeDetails(processorName, depositData) {
1157
+ async function registerPayeeDetails(processorName, payload) {
1022
1158
  const controller = new AbortController();
1023
1159
  const timeout = setTimeout(() => controller.abort(), PAYEE_REGISTRATION_TIMEOUT_MS);
1024
1160
  try {
1025
- const res = await fetch(`${API_BASE_URL}/v1/makers/create`, {
1161
+ const res = await fetch(`${API_BASE_URL}/v2/makers/create`, {
1026
1162
  method: "POST",
1027
1163
  headers: { "Content-Type": "application/json" },
1028
- body: JSON.stringify({ processorName, depositData }),
1164
+ body: JSON.stringify({ processorName, ...payload }),
1029
1165
  signal: controller.signal
1030
1166
  });
1031
1167
  if (!res.ok) {
1032
1168
  const txt = await res.text().catch(() => "");
1033
- throw new Error(`makers/create failed (${res.status}): ${txt || res.statusText}`);
1169
+ let detail = txt || res.statusText;
1170
+ try {
1171
+ const parsed = JSON.parse(txt);
1172
+ if (typeof parsed.message === "string" && parsed.message.trim()) {
1173
+ detail = parsed.message;
1174
+ }
1175
+ } catch {
1176
+ }
1177
+ throw new MakersCreateError(
1178
+ `makers/create failed (${res.status}): ${detail}`,
1179
+ res.status,
1180
+ txt
1181
+ );
1034
1182
  }
1035
1183
  const json = await res.json();
1036
1184
  if (!json.success || !json.responseObject?.hashedOnchainId) {
1037
- throw new Error(json.message || "makers/create returned no hashedOnchainId");
1185
+ throw new MakersCreateError(
1186
+ json.message || "makers/create returned no hashedOnchainId",
1187
+ json.statusCode ?? null,
1188
+ ""
1189
+ );
1038
1190
  }
1039
1191
  return json.responseObject.hashedOnchainId;
1040
1192
  } finally {
@@ -1090,261 +1242,516 @@ async function findUndelegatedDeposit(walletAddress) {
1090
1242
  if (remaining === 0n) return false;
1091
1243
  return (d.rateManagerId ?? "").toLowerCase() !== delegateIdLower;
1092
1244
  });
1093
- undelegated.sort((a, b) => Number(BigInt(b.depositId) - BigInt(a.depositId)));
1245
+ undelegated.sort((a, b) => {
1246
+ const left = BigInt(a.depositId);
1247
+ const right = BigInt(b.depositId);
1248
+ if (left === right) return 0;
1249
+ return left > right ? -1 : 1;
1250
+ });
1094
1251
  return undelegated[0] ?? null;
1095
1252
  } catch {
1096
1253
  return null;
1097
1254
  }
1098
1255
  }
1099
- async function offramp(walletClient, params, onProgress) {
1256
+ async function offramp(walletClient, params, onProgress, telemetryContext) {
1100
1257
  const { amount, platform, currency, identifier } = params;
1101
1258
  const platformId = platform.id;
1102
1259
  const currencyCode = currency.code;
1103
- if (!walletClient.account?.address) {
1104
- throw new OfframpError("Wallet client has no account. Connect a wallet first.", "VALIDATION");
1105
- }
1106
- const walletAddress = walletClient.account.address;
1107
- const amt = parseFloat(amount);
1108
- if (!Number.isFinite(amt) || amt < MIN_DEPOSIT_USDC) {
1109
- throw new OfframpError(`Minimum deposit is ${MIN_DEPOSIT_USDC} USDC`, "VALIDATION");
1110
- }
1111
- if (!platform.currencies.includes(currencyCode)) {
1112
- throw new OfframpError(`${currencyCode} is not supported on ${platform.name}`, "UNSUPPORTED");
1113
- }
1114
- const validation = platform.validate(identifier);
1115
- if (!validation.valid) {
1116
- throw new OfframpError(validation.error || "Invalid identifier", "VALIDATION");
1117
- }
1118
- const normalizedIdentifier = validation.normalized;
1119
- const methodHash = getPaymentMethodHash(platformId);
1120
- if (!methodHash) {
1121
- throw new OfframpError(`${platform.name} is not currently supported`, "UNSUPPORTED");
1122
- }
1123
- const existing = await findUndelegatedDeposit(walletAddress);
1124
- if (existing) {
1125
- onProgress?.({ step: "resuming", depositId: existing.depositId });
1126
- const client2 = createSdkClient(walletClient);
1127
- const txOverrides2 = { referrer: [REFERRER] };
1260
+ const telemetry = telemetryContext ?? createTelemetryContext({
1261
+ sdkVersion: SDK_VERSION,
1262
+ telemetry: true,
1263
+ integratorId: params.integratorId,
1264
+ referralId: params.referralId
1265
+ });
1266
+ const flowStartMs = Date.now();
1267
+ const emitProgress = (progress) => {
1268
+ onProgress?.(progress);
1269
+ telemetry.emit("sdk.createDeposit.progress", {
1270
+ step: progress.step,
1271
+ txHash: progress.txHash
1272
+ });
1273
+ };
1274
+ try {
1275
+ if (!walletClient.account?.address) {
1276
+ throw new OfframpError("Wallet client has no account. Connect a wallet first.", "VALIDATION");
1277
+ }
1278
+ const walletAddress = walletClient.account.address.toLowerCase();
1279
+ telemetry.emit("sdk.createDeposit.start", {
1280
+ platform: platformId,
1281
+ currency: currencyCode,
1282
+ amount,
1283
+ wallet: walletAddress
1284
+ });
1285
+ const amt = parseFloat(amount);
1286
+ if (!Number.isFinite(amt) || amt < MIN_DEPOSIT_USDC) {
1287
+ throw new OfframpError(`Minimum deposit is ${MIN_DEPOSIT_USDC} USDC`, "VALIDATION");
1288
+ }
1289
+ if (!platform.currencies.includes(currencyCode)) {
1290
+ throw new OfframpError(`${currencyCode} is not supported on ${platform.name}`, "UNSUPPORTED");
1291
+ }
1292
+ const validation = platform.validate(identifier);
1293
+ if (!validation.valid) {
1294
+ throw new OfframpError(validation.error || "Invalid identifier", "VALIDATION");
1295
+ }
1296
+ const normalizedIdentifier = validation.normalized;
1297
+ const methodHash = getPaymentMethodHash(platformId);
1298
+ if (!methodHash) {
1299
+ throw new OfframpError(`${platform.name} is not currently supported`, "UNSUPPORTED");
1300
+ }
1301
+ const existing = await findUndelegatedDeposit(walletAddress);
1302
+ if (existing) {
1303
+ emitProgress({ step: "resuming", depositId: existing.depositId });
1304
+ const client2 = createSdkClient(walletClient);
1305
+ const txOverrides2 = { referrer: [REFERRER] };
1306
+ try {
1307
+ const result2 = await client2.setRateManager({
1308
+ depositId: BigInt(existing.depositId),
1309
+ rateManagerAddress: RATE_MANAGER_REGISTRY_ADDRESS,
1310
+ rateManagerId: DELEGATE_RATE_MANAGER_ID,
1311
+ escrowAddress: existing.escrowAddress,
1312
+ txOverrides: txOverrides2
1313
+ });
1314
+ const txHash = extractTxHash2(result2);
1315
+ emitProgress({ step: "done", txHash, depositId: existing.depositId });
1316
+ const resumedResult = { depositId: existing.depositId, txHash, resumed: true };
1317
+ telemetry.emit("sdk.createDeposit.success", {
1318
+ depositId: resumedResult.depositId,
1319
+ txHash: resumedResult.txHash,
1320
+ wallet: walletAddress,
1321
+ resumed: true,
1322
+ wallDurationMs: Date.now() - flowStartMs
1323
+ });
1324
+ return resumedResult;
1325
+ } catch (err) {
1326
+ if (isUserCancellation(err))
1327
+ throw new OfframpError("User cancelled", "USER_CANCELLED", "resuming", err);
1328
+ throw new OfframpError(
1329
+ "Found undelegated deposit but delegation failed. Try again.",
1330
+ "DELEGATION_FAILED",
1331
+ "resuming",
1332
+ err,
1333
+ { depositId: existing.depositId, txHash: existing.txHash }
1334
+ );
1335
+ }
1336
+ }
1337
+ const truncatedAmount = amt.toFixed(6).replace(/\.?0+$/, "");
1338
+ const amountUnits = usdcToUnits(truncatedAmount);
1339
+ const minUnits = usdcToUnits(String(MIN_ORDER_USDC));
1340
+ const maxUnits = usdcToUnits(String(Math.min(amt, 2500)));
1128
1341
  try {
1129
- const result = await client2.setRateManager({
1130
- depositId: BigInt(existing.depositId),
1131
- rateManagerAddress: RATE_MANAGER_REGISTRY_ADDRESS,
1132
- rateManagerId: DELEGATE_RATE_MANAGER_ID,
1133
- escrowAddress: existing.escrowAddress,
1134
- txOverrides: txOverrides2
1342
+ const publicClient = createPublicClient3({ chain: base3, transport: http3(BASE_RPC_URL) });
1343
+ const balance = await publicClient.readContract({
1344
+ address: USDC_ADDRESS,
1345
+ abi: [
1346
+ {
1347
+ name: "balanceOf",
1348
+ type: "function",
1349
+ stateMutability: "view",
1350
+ inputs: [{ name: "account", type: "address" }],
1351
+ outputs: [{ name: "", type: "uint256" }]
1352
+ }
1353
+ ],
1354
+ functionName: "balanceOf",
1355
+ args: [walletAddress]
1356
+ });
1357
+ if (balance < amountUnits) {
1358
+ const available = Number(formatUnits2(balance, 6));
1359
+ throw new OfframpError(
1360
+ `Insufficient USDC balance. Have ${available.toFixed(2)}, need ${truncatedAmount}.`,
1361
+ "VALIDATION"
1362
+ );
1363
+ }
1364
+ } catch (err) {
1365
+ if (err instanceof OfframpError) throw err;
1366
+ }
1367
+ const client = createSdkClient(walletClient);
1368
+ const txOverrides = { referrer: [REFERRER] };
1369
+ emitProgress({ step: "approving" });
1370
+ try {
1371
+ await client.ensureAllowance({
1372
+ token: USDC_ADDRESS,
1373
+ amount: amountUnits,
1374
+ escrowAddress: ESCROW_ADDRESS,
1375
+ maxApprove: false,
1376
+ txOverrides
1135
1377
  });
1136
- const txHash = extractTxHash2(result);
1137
- onProgress?.({ step: "done", txHash, depositId: existing.depositId });
1138
- return { depositId: existing.depositId, txHash, resumed: true };
1139
1378
  } catch (err) {
1140
1379
  if (isUserCancellation(err))
1141
- throw new OfframpError("User cancelled", "USER_CANCELLED", "resuming", err);
1380
+ throw new OfframpError("User cancelled", "USER_CANCELLED", "approving", err);
1381
+ const detail = err instanceof Error ? err.message : String(err);
1142
1382
  throw new OfframpError(
1143
- "Found undelegated deposit but delegation failed. Try again.",
1144
- "DELEGATION_FAILED",
1145
- "resuming",
1146
- err,
1147
- { depositId: existing.depositId, txHash: existing.txHash }
1383
+ `USDC approval failed: ${detail}`,
1384
+ "APPROVAL_FAILED",
1385
+ "approving",
1386
+ err
1148
1387
  );
1149
1388
  }
1150
- }
1151
- const truncatedAmount = amt.toFixed(6).replace(/\.?0+$/, "");
1152
- const amountUnits = usdcToUnits(truncatedAmount);
1153
- const minUnits = usdcToUnits(String(MIN_ORDER_USDC));
1154
- const maxUnits = usdcToUnits(String(Math.min(amt, 2500)));
1155
- try {
1156
- const publicClient = createPublicClient3({ chain: base3, transport: http3(BASE_RPC_URL) });
1157
- const balance = await publicClient.readContract({
1158
- address: USDC_ADDRESS,
1159
- abi: [
1160
- {
1161
- name: "balanceOf",
1162
- type: "function",
1163
- stateMutability: "view",
1164
- inputs: [{ name: "account", type: "address" }],
1165
- outputs: [{ name: "", type: "uint256" }]
1166
- }
1167
- ],
1168
- functionName: "balanceOf",
1169
- args: [walletAddress]
1170
- });
1171
- if (balance < amountUnits) {
1172
- const available = Number(formatUnits2(balance, 6));
1389
+ emitProgress({ step: "registering" });
1390
+ let hashedOnchainId;
1391
+ const canonicalName = platformId.startsWith("zelle") ? "zelle" : platformId;
1392
+ try {
1393
+ const payeePayload = buildDepositData(canonicalName, normalizedIdentifier);
1394
+ hashedOnchainId = await registerPayeeDetails(canonicalName, payeePayload);
1395
+ } catch (err) {
1396
+ const status = err instanceof MakersCreateError ? err.status : null;
1397
+ const message = err instanceof Error ? err.message : String(err);
1398
+ if (isPeerExtensionRegistrationError(canonicalName, message, status)) {
1399
+ throw new OfframpError(
1400
+ `${platform.name} maker is not yet registered in the Peer extension. Verify this handle in the extension, then retry.`,
1401
+ "EXTENSION_REGISTRATION_REQUIRED",
1402
+ "registering",
1403
+ err
1404
+ );
1405
+ }
1173
1406
  throw new OfframpError(
1174
- `Insufficient USDC balance. Have ${available.toFixed(2)}, need ${truncatedAmount}.`,
1175
- "VALIDATION"
1407
+ "Payee registration failed",
1408
+ "REGISTRATION_FAILED",
1409
+ "registering",
1410
+ err
1176
1411
  );
1177
1412
  }
1178
- } catch (err) {
1179
- if (err instanceof OfframpError) throw err;
1180
- }
1181
- const client = createSdkClient(walletClient);
1182
- const txOverrides = { referrer: [REFERRER] };
1183
- onProgress?.({ step: "approving" });
1184
- try {
1185
- await client.ensureAllowance({
1186
- token: USDC_ADDRESS,
1187
- amount: amountUnits,
1188
- escrowAddress: ESCROW_ADDRESS,
1189
- maxApprove: false,
1190
- txOverrides
1191
- });
1192
- } catch (err) {
1193
- if (isUserCancellation(err))
1194
- throw new OfframpError("User cancelled", "USER_CANCELLED", "approving", err);
1195
- const detail = err instanceof Error ? err.message : String(err);
1196
- throw new OfframpError(`USDC approval failed: ${detail}`, "APPROVAL_FAILED", "approving", err);
1197
- }
1198
- onProgress?.({ step: "registering" });
1199
- let hashedOnchainId;
1200
- try {
1201
- const canonicalName = platformId.startsWith("zelle") ? "zelle" : platformId;
1202
- const depositData = buildDepositData(platformId, normalizedIdentifier);
1203
- hashedOnchainId = await registerPayeeDetails(canonicalName, depositData);
1204
- } catch (err) {
1205
- throw new OfframpError("Payee registration failed", "REGISTRATION_FAILED", "registering", err);
1206
- }
1207
- onProgress?.({ step: "depositing" });
1208
- const conversionRates = [
1209
- [{ currency: currencyCode, conversionRate: "1" }]
1210
- ];
1211
- const baseCurrenciesOverride = mapConversionRatesToOnchainMinRate(conversionRates, 1);
1212
- const currenciesOverride = attachOracleConfig(baseCurrenciesOverride, conversionRates);
1213
- let hash;
1214
- try {
1215
- const result = await client.createDeposit({
1216
- token: USDC_ADDRESS,
1217
- amount: amountUnits,
1218
- retainOnEmpty: false,
1219
- intentAmountRange: { min: minUnits, max: maxUnits },
1220
- processorNames: [platformId],
1221
- conversionRates,
1222
- payeeDetailsHashes: [hashedOnchainId],
1223
- paymentMethodsOverride: [methodHash],
1224
- paymentMethodDataOverride: [
1225
- {
1226
- intentGatingService: GATING_SERVICE_ADDRESS,
1227
- payeeDetails: hashedOnchainId,
1228
- data: "0x"
1229
- }
1230
- ],
1231
- currenciesOverride,
1232
- escrowAddress: ESCROW_ADDRESS,
1233
- txOverrides
1234
- });
1235
- if (!result?.hash) throw new Error("No transaction hash returned");
1236
- hash = result.hash;
1237
- } catch (err) {
1238
- if (isUserCancellation(err))
1239
- throw new OfframpError("User cancelled", "USER_CANCELLED", "depositing", err);
1240
- const detail = err instanceof Error ? err.message : String(err);
1241
- throw new OfframpError(
1242
- `Deposit transaction failed: ${detail}`,
1243
- "DEPOSIT_FAILED",
1244
- "depositing",
1245
- err
1246
- );
1247
- }
1248
- onProgress?.({ step: "confirming", txHash: hash });
1249
- let depositId = "";
1250
- const receiptClient = client;
1251
- if (typeof receiptClient.waitForTransactionReceipt === "function") {
1413
+ emitProgress({ step: "depositing" });
1414
+ const conversionRates = [
1415
+ [{ currency: currencyCode, conversionRate: "1" }]
1416
+ ];
1417
+ const baseCurrenciesOverride = mapConversionRatesToOnchainMinRate(conversionRates, 1);
1418
+ const currenciesOverride = attachOracleConfig(baseCurrenciesOverride, conversionRates);
1419
+ let hash;
1252
1420
  try {
1253
- const receipt = await receiptClient.waitForTransactionReceipt({ hash, confirmations: 1 });
1254
- depositId = extractDepositIdFromLogs(receipt.logs) || "";
1255
- } catch {
1421
+ const result2 = await client.createDeposit({
1422
+ token: USDC_ADDRESS,
1423
+ amount: amountUnits,
1424
+ retainOnEmpty: false,
1425
+ intentAmountRange: { min: minUnits, max: maxUnits },
1426
+ processorNames: [platformId],
1427
+ conversionRates,
1428
+ payeeDetailsHashes: [hashedOnchainId],
1429
+ paymentMethodsOverride: [methodHash],
1430
+ paymentMethodDataOverride: [
1431
+ {
1432
+ intentGatingService: GATING_SERVICE_ADDRESS,
1433
+ payeeDetails: hashedOnchainId,
1434
+ data: "0x"
1435
+ }
1436
+ ],
1437
+ currenciesOverride,
1438
+ escrowAddress: ESCROW_ADDRESS,
1439
+ txOverrides
1440
+ });
1441
+ if (!result2?.hash) throw new Error("No transaction hash returned");
1442
+ hash = result2.hash;
1443
+ } catch (err) {
1444
+ if (isUserCancellation(err))
1445
+ throw new OfframpError("User cancelled", "USER_CANCELLED", "depositing", err);
1446
+ const detail = err instanceof Error ? err.message : String(err);
1447
+ throw new OfframpError(
1448
+ `Deposit transaction failed: ${detail}`,
1449
+ "DEPOSIT_FAILED",
1450
+ "depositing",
1451
+ err
1452
+ );
1256
1453
  }
1257
- }
1258
- if (!depositId) {
1259
- let delay = INDEXER_INITIAL_DELAY_MS;
1260
- for (let attempt = 0; attempt < INDEXER_MAX_ATTEMPTS && !depositId; attempt++) {
1454
+ emitProgress({ step: "confirming", txHash: hash });
1455
+ let depositId = "";
1456
+ const receiptClient = client;
1457
+ if (typeof receiptClient.waitForTransactionReceipt === "function") {
1261
1458
  try {
1262
- const deps = await client.indexer.getDepositsWithRelations(
1263
- { depositor: walletAddress },
1264
- { limit: 25 }
1265
- );
1266
- const hit = deps.find((d) => (d?.txHash || "").toLowerCase() === hash.toLowerCase());
1267
- if (hit) {
1268
- depositId = String(hit.depositId);
1269
- break;
1270
- }
1459
+ const receipt = await receiptClient.waitForTransactionReceipt({ hash, confirmations: 1 });
1460
+ depositId = extractDepositIdFromLogs(receipt.logs) || "";
1271
1461
  } catch {
1272
1462
  }
1273
- await new Promise((r) => setTimeout(r, delay));
1274
- delay = Math.min(INDEXER_MAX_DELAY_MS, Math.floor(delay * 1.7));
1275
1463
  }
1276
- }
1277
- if (!depositId) {
1278
- throw new OfframpError(
1279
- "Deposit created on-chain but could not confirm deposit ID. Your funds are safe. Call offramp() again to resume delegation.",
1280
- "CONFIRMATION_FAILED",
1281
- "confirming",
1282
- void 0,
1283
- { txHash: hash }
1284
- );
1285
- }
1286
- onProgress?.({ step: "delegating", txHash: hash, depositId });
1287
- try {
1288
- await client.setRateManager({
1289
- depositId: BigInt(depositId),
1290
- rateManagerAddress: RATE_MANAGER_REGISTRY_ADDRESS,
1291
- rateManagerId: DELEGATE_RATE_MANAGER_ID,
1292
- escrowAddress: ESCROW_ADDRESS,
1293
- txOverrides
1294
- });
1295
- } catch (delegationError) {
1296
- if (isUserCancellation(delegationError)) {
1464
+ if (!depositId) {
1465
+ let delay = INDEXER_INITIAL_DELAY_MS;
1466
+ for (let attempt = 0; attempt < INDEXER_MAX_ATTEMPTS && !depositId; attempt++) {
1467
+ try {
1468
+ const deps = await client.indexer.getDepositsWithRelations(
1469
+ { depositor: walletAddress },
1470
+ { limit: 25 }
1471
+ );
1472
+ const hit = deps.find((d) => (d?.txHash || "").toLowerCase() === hash.toLowerCase());
1473
+ if (hit) {
1474
+ depositId = String(hit.depositId);
1475
+ break;
1476
+ }
1477
+ } catch {
1478
+ }
1479
+ await new Promise((r) => setTimeout(r, delay));
1480
+ delay = Math.min(INDEXER_MAX_DELAY_MS, Math.floor(delay * 1.7));
1481
+ }
1482
+ }
1483
+ if (!depositId) {
1484
+ throw new OfframpError(
1485
+ "Deposit created on-chain but could not confirm deposit ID. Your funds are safe. Call offramp() again to resume delegation.",
1486
+ "CONFIRMATION_FAILED",
1487
+ "confirming",
1488
+ void 0,
1489
+ { txHash: hash }
1490
+ );
1491
+ }
1492
+ emitProgress({ step: "delegating", txHash: hash, depositId });
1493
+ try {
1494
+ await client.setRateManager({
1495
+ depositId: BigInt(depositId),
1496
+ rateManagerAddress: RATE_MANAGER_REGISTRY_ADDRESS,
1497
+ rateManagerId: DELEGATE_RATE_MANAGER_ID,
1498
+ escrowAddress: ESCROW_ADDRESS,
1499
+ txOverrides
1500
+ });
1501
+ } catch (delegationError) {
1502
+ if (isUserCancellation(delegationError)) {
1503
+ throw new OfframpError(
1504
+ "User cancelled delegation",
1505
+ "USER_CANCELLED",
1506
+ "delegating",
1507
+ delegationError,
1508
+ { txHash: hash, depositId }
1509
+ );
1510
+ }
1297
1511
  throw new OfframpError(
1298
- "User cancelled delegation",
1299
- "USER_CANCELLED",
1512
+ "Deposit created but delegation failed. Call offramp() again to resume delegation.",
1513
+ "DELEGATION_FAILED",
1300
1514
  "delegating",
1301
1515
  delegationError,
1302
1516
  { txHash: hash, depositId }
1303
1517
  );
1304
1518
  }
1305
- throw new OfframpError(
1306
- "Deposit created but delegation failed. Call offramp() again to resume delegation.",
1307
- "DELEGATION_FAILED",
1308
- "delegating",
1309
- delegationError,
1310
- { txHash: hash, depositId }
1311
- );
1312
- }
1313
- let otcLink;
1314
- if (params.otcTaker) {
1315
- onProgress?.({ step: "restricting", txHash: hash, depositId });
1316
- try {
1317
- const otcResult = await enableOtc(walletClient, depositId, params.otcTaker);
1318
- otcLink = otcResult.otcLink;
1319
- } catch (otcError) {
1320
- if (isUserCancellation(otcError)) {
1519
+ let otcLink;
1520
+ if (params.otcTaker) {
1521
+ emitProgress({ step: "restricting", txHash: hash, depositId });
1522
+ try {
1523
+ const otcResult = await enableOtc(walletClient, depositId, params.otcTaker);
1524
+ otcLink = otcResult.otcLink;
1525
+ } catch (otcError) {
1526
+ if (isUserCancellation(otcError)) {
1527
+ throw new OfframpError(
1528
+ "User cancelled OTC restriction",
1529
+ "USER_CANCELLED",
1530
+ "restricting",
1531
+ otcError,
1532
+ {
1533
+ txHash: hash,
1534
+ depositId
1535
+ }
1536
+ );
1537
+ }
1321
1538
  throw new OfframpError(
1322
- "User cancelled OTC restriction",
1323
- "USER_CANCELLED",
1539
+ "Deposit created and delegated but OTC restriction failed. Use enableOtc() to retry.",
1540
+ "DEPOSIT_FAILED",
1324
1541
  "restricting",
1325
1542
  otcError,
1326
- {
1327
- txHash: hash,
1328
- depositId
1329
- }
1543
+ { txHash: hash, depositId }
1330
1544
  );
1331
1545
  }
1546
+ }
1547
+ emitProgress({ step: "done", txHash: hash, depositId });
1548
+ const result = { depositId, txHash: hash, resumed: false, otcLink };
1549
+ telemetry.emit("sdk.createDeposit.success", {
1550
+ depositId: result.depositId,
1551
+ txHash: result.txHash,
1552
+ wallet: walletAddress,
1553
+ resumed: false,
1554
+ wallDurationMs: Date.now() - flowStartMs
1555
+ });
1556
+ return result;
1557
+ } catch (error) {
1558
+ const known = error instanceof OfframpError ? error : null;
1559
+ telemetry.emit("sdk.createDeposit.error", {
1560
+ code: known?.code ?? "DEPOSIT_FAILED",
1561
+ step: known?.step ?? "unknown"
1562
+ });
1563
+ throw error;
1564
+ }
1565
+ }
1566
+
1567
+ // src/currencies.ts
1568
+ import { currencyInfo as currencyInfo2 } from "@zkp2p/sdk";
1569
+ function buildCurrencies() {
1570
+ const entries = {};
1571
+ for (const [code, info] of Object.entries(
1572
+ currencyInfo2
1573
+ )) {
1574
+ entries[code] = {
1575
+ code: info.currencyCode ?? code,
1576
+ name: info.currencyName ?? code,
1577
+ symbol: info.currencySymbol ?? code,
1578
+ countryCode: info.countryCode ?? ""
1579
+ };
1580
+ }
1581
+ return entries;
1582
+ }
1583
+ var CURRENCIES = buildCurrencies();
1584
+
1585
+ // src/offramp.ts
1586
+ var IDEMPOTENCY_TTL_MS = 10 * 60 * 1e3;
1587
+ var IDEMPOTENCY_STORAGE_PREFIX = "offramp:idempotency:";
1588
+ function getWalletAddress(walletClient) {
1589
+ const address = walletClient.account?.address;
1590
+ if (!address) {
1591
+ throw new OfframpError("Wallet client has no account. Connect a wallet first.", "VALIDATION");
1592
+ }
1593
+ return address.toLowerCase();
1594
+ }
1595
+ function getIdempotencyStorageKey(walletAddress, idempotencyKey) {
1596
+ return `${IDEMPOTENCY_STORAGE_PREFIX}${walletAddress}:${idempotencyKey}`;
1597
+ }
1598
+ function readIdempotencyResult(storageKey) {
1599
+ if (typeof sessionStorage === "undefined") return null;
1600
+ try {
1601
+ const raw = sessionStorage.getItem(storageKey);
1602
+ if (!raw) return null;
1603
+ const parsed = JSON.parse(raw);
1604
+ const expiresAt = typeof parsed.expiresAt === "number" && Number.isFinite(parsed.expiresAt) ? parsed.expiresAt : 0;
1605
+ if (expiresAt <= Date.now()) {
1606
+ sessionStorage.removeItem(storageKey);
1607
+ return null;
1608
+ }
1609
+ const result = parsed.result;
1610
+ if (!result?.depositId || !result.txHash) {
1611
+ sessionStorage.removeItem(storageKey);
1612
+ return null;
1613
+ }
1614
+ return result;
1615
+ } catch {
1616
+ return null;
1617
+ }
1618
+ }
1619
+ function writeIdempotencyResult(storageKey, result) {
1620
+ if (typeof sessionStorage === "undefined") return;
1621
+ try {
1622
+ const record = {
1623
+ result,
1624
+ expiresAt: Date.now() + IDEMPOTENCY_TTL_MS
1625
+ };
1626
+ sessionStorage.setItem(storageKey, JSON.stringify(record));
1627
+ } catch {
1628
+ }
1629
+ }
1630
+ function createPerFlowTelemetry(base4, integratorId, referralId) {
1631
+ return createTelemetryContext({
1632
+ sdkVersion: base4.sdkVersion,
1633
+ telemetry: base4.enabled,
1634
+ integratorId: sanitizeAttributionId(integratorId) ?? base4.integratorId,
1635
+ referralId: sanitizeAttributionId(referralId) ?? base4.referralId
1636
+ });
1637
+ }
1638
+ function normalizeCurrencyEntry(code) {
1639
+ const fromCatalog = CURRENCIES[code];
1640
+ if (fromCatalog) return fromCatalog;
1641
+ return {
1642
+ code,
1643
+ name: code,
1644
+ symbol: code,
1645
+ countryCode: ""
1646
+ };
1647
+ }
1648
+ function buildCurrencyGroups() {
1649
+ const grouped = {};
1650
+ for (const platform of Object.values(PLATFORMS)) {
1651
+ grouped[platform.id] = platform.currencies.map(normalizeCurrencyEntry);
1652
+ }
1653
+ return grouped;
1654
+ }
1655
+ var Offramp = class {
1656
+ walletClient;
1657
+ telemetry;
1658
+ constructor(options) {
1659
+ this.walletClient = options.walletClient;
1660
+ this.telemetry = createTelemetryContext({
1661
+ sdkVersion: SDK_VERSION,
1662
+ telemetry: options.telemetry,
1663
+ integratorId: options.integratorId,
1664
+ referralId: options.referralId
1665
+ });
1666
+ this.telemetry.emit("sdk.init", {
1667
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : "unknown"
1668
+ });
1669
+ }
1670
+ async createDeposit(params, onProgress) {
1671
+ const flowTelemetry = createPerFlowTelemetry(
1672
+ this.telemetry,
1673
+ params.integratorId,
1674
+ params.referralId
1675
+ );
1676
+ const sanitizedParams = {
1677
+ ...params,
1678
+ integratorId: flowTelemetry.integratorId,
1679
+ referralId: flowTelemetry.referralId
1680
+ };
1681
+ const sanitizedKey = sanitizeAttributionId(params.idempotencyKey);
1682
+ const walletAddress = sanitizedKey ? getWalletAddress(this.walletClient) : null;
1683
+ const storageKey = walletAddress && sanitizedKey ? getIdempotencyStorageKey(walletAddress, sanitizedKey) : null;
1684
+ if (storageKey) {
1685
+ const existing = readIdempotencyResult(storageKey);
1686
+ if (existing) return existing;
1687
+ }
1688
+ const result = await offramp(
1689
+ this.walletClient,
1690
+ sanitizedParams,
1691
+ onProgress,
1692
+ flowTelemetry
1693
+ );
1694
+ if (storageKey) {
1695
+ writeIdempotencyResult(storageKey, result);
1696
+ }
1697
+ return result;
1698
+ }
1699
+ getQuote(input) {
1700
+ const amountUsdc = Number(input.amount);
1701
+ if (!Number.isFinite(amountUsdc) || amountUsdc <= 0) {
1702
+ throw new OfframpError("Amount must be a positive number", "VALIDATION");
1703
+ }
1704
+ if (!input.platform.currencies.includes(input.currency.code)) {
1332
1705
  throw new OfframpError(
1333
- "Deposit created and delegated but OTC restriction failed. Use enableOtc() to retry.",
1334
- "DEPOSIT_FAILED",
1335
- "restricting",
1336
- otcError,
1337
- { txHash: hash, depositId }
1706
+ `${input.currency.code} is not supported on ${input.platform.name}`,
1707
+ "UNSUPPORTED"
1338
1708
  );
1339
1709
  }
1710
+ const vaultSpreadBps = 0;
1711
+ const spreadFactor = Math.max(0, 1 - vaultSpreadBps / 1e4);
1712
+ const expectedFiat = amountUsdc * spreadFactor;
1713
+ const effectiveRate = amountUsdc === 0 ? 0 : expectedFiat / amountUsdc;
1714
+ return {
1715
+ amountUsdc,
1716
+ expectedFiat,
1717
+ effectiveRate,
1718
+ vaultSpreadBps
1719
+ };
1720
+ }
1721
+ getVaultStatus() {
1722
+ return {
1723
+ rateManagerId: DELEGATE_RATE_MANAGER_ID,
1724
+ rateManagerAddress: RATE_MANAGER_REGISTRY_ADDRESS,
1725
+ feeRateBps: DELEGATE_MANAGER_FEE_BPS,
1726
+ escrow: ESCROW_ADDRESS,
1727
+ isActive: true
1728
+ };
1340
1729
  }
1341
- onProgress?.({ step: "done", txHash: hash, depositId });
1342
- return { depositId, txHash: hash, resumed: false, otcLink };
1730
+ listCurrencies() {
1731
+ return buildCurrencyGroups();
1732
+ }
1733
+ };
1734
+ function createOfframp(options) {
1735
+ return new Offramp(options);
1343
1736
  }
1344
1737
 
1738
+ // src/extension.ts
1739
+ import {
1740
+ PEER_EXTENSION_CHROME_URL,
1741
+ peerExtensionSdk,
1742
+ createPeerExtensionSdk,
1743
+ isPeerExtensionAvailable,
1744
+ openPeerExtensionInstallPage,
1745
+ getPeerExtensionState
1746
+ } from "@zkp2p/sdk";
1747
+
1345
1748
  export {
1346
1749
  ESCROW_ADDRESS,
1750
+ normalizePaypalMeUsername,
1347
1751
  PLATFORMS,
1752
+ getPeerExtensionRegistrationInfo,
1753
+ isPeerExtensionRegistrationError,
1754
+ MakersCreateError,
1348
1755
  OfframpError,
1349
1756
  enableOtc,
1350
1757
  disableOtc,
@@ -1353,6 +1760,19 @@ export {
1353
1760
  close,
1354
1761
  delegate,
1355
1762
  undelegate,
1356
- offramp
1763
+ sanitizeAttributionId,
1764
+ emitEvent,
1765
+ sendTelemetryEvent,
1766
+ createTelemetryContext,
1767
+ offramp,
1768
+ CURRENCIES,
1769
+ Offramp,
1770
+ createOfframp,
1771
+ PEER_EXTENSION_CHROME_URL,
1772
+ peerExtensionSdk,
1773
+ createPeerExtensionSdk,
1774
+ isPeerExtensionAvailable,
1775
+ openPeerExtensionInstallPage,
1776
+ getPeerExtensionState
1357
1777
  };
1358
- //# sourceMappingURL=chunk-OJY6DE3I.js.map
1778
+ //# sourceMappingURL=chunk-HBR3Z6HX.js.map