@usdctofiat/offramp 3.0.2 → 3.0.3

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/CHANGELOG.md CHANGED
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Fixed
11
+
12
+ - Reject shortened or non-PayPal URLs in the PayPal.me username normalizer so
13
+ inputs like `t.co/...` cannot be converted into a fake PayPal username.
14
+
15
+ ### Changed
16
+
17
+ - Bump `@zkp2p/sdk` to `0.4.1`.
18
+
19
+ ## [3.0.3] - 2026-05-20
20
+
21
+ ### Fixed
22
+
23
+ - Return canonical OTC share links from `getOtcLink()` and `enableOtc()` using
24
+ `https://otc.usdctofiat.xyz/d/<escrow>/<depositId>`.
25
+
10
26
  ## [3.0.2] - 2026-05-19
11
27
 
12
28
  ### Fixed
package/README.md CHANGED
@@ -102,7 +102,7 @@ const { depositId, otcLink } = await offramp(walletClient, {
102
102
  identifier: "alice",
103
103
  otcTaker: "0xBuyerAddress",
104
104
  });
105
- // otcLink = "https://usdctofiat.xyz/deposit/0x.../362"
105
+ // otcLink = "https://otc.usdctofiat.xyz/d/0x.../362"
106
106
  // Share otcLink with the buyer — they open it, connect their wallet, and fill the order.
107
107
  ```
108
108
 
@@ -1,6 +1,6 @@
1
1
  // src/config.ts
2
2
  import { getContracts, getGatingServiceAddress } from "@zkp2p/sdk";
3
- var SDK_VERSION = "3.0.2";
3
+ var SDK_VERSION = "3.0.3";
4
4
  var BASE_CHAIN_ID = 8453;
5
5
  var RUNTIME_ENV = "production";
6
6
  var API_BASE_URL = "https://api.zkp2p.xyz";
@@ -102,7 +102,8 @@ var FALLBACK_CURRENCIES = {
102
102
  zelle: ["USD"],
103
103
  paypal: ["USD", "EUR", "GBP", "SGD", "NZD", "AUD", "CAD"],
104
104
  monzo: ["GBP"],
105
- n26: ["EUR"]
105
+ n26: ["EUR"],
106
+ luxon: ["USD", "EUR", "GBP"]
106
107
  };
107
108
  function gatherCatalogHashes(platform) {
108
109
  if (platform === "zelle") {
@@ -126,13 +127,39 @@ function resolveSupportedCurrencies(platform) {
126
127
  function normalizePaypalMeUsername(value) {
127
128
  const trimmed = value.trim();
128
129
  if (!trimmed) return "";
129
- const withoutProtocol = trimmed.replace(/^https?:\/\//i, "").replace(/^www\./i, "");
130
- if (/^paypal\.me(?:[?#].*)?$/i.test(withoutProtocol)) return "";
131
- const withoutDomain = withoutProtocol.replace(/^paypal\.me\//i, "");
132
- const [pathWithoutQuery] = withoutDomain.split(/[?#]/, 1);
130
+ const withoutProtocol = trimmed.replace(/^https?:\/\//i, "");
131
+ const [pathWithoutQuery] = withoutProtocol.split(/[?#]/, 1);
133
132
  const sanitizedPath = pathWithoutQuery.replace(/^\/+/, "");
134
- const [username] = sanitizedPath.split("/", 1);
135
- return username.replace(/^@+/, "").trim().toLowerCase();
133
+ const [host = "", ...pathSegments] = sanitizedPath.split("/");
134
+ const normalizedHost = host.toLowerCase().replace(/^www\./i, "");
135
+ if (normalizedHost === "paypal.me") {
136
+ const [username = ""] = pathSegments;
137
+ return username.replace(/^@+/, "").trim().toLowerCase();
138
+ }
139
+ if (normalizedHost === "paypal.com") {
140
+ const [paypalMePath = "", username = ""] = pathSegments;
141
+ if (paypalMePath.toLowerCase() !== "paypalme") return "";
142
+ return username.replace(/^@+/, "").trim().toLowerCase();
143
+ }
144
+ if (/^https?:\/\//i.test(trimmed) || trimmed.includes("/")) {
145
+ return trimmed;
146
+ }
147
+ return trimmed.replace(/^@+/, "").toLowerCase();
148
+ }
149
+ function isValidIBAN(iban) {
150
+ const upper = iban.toUpperCase();
151
+ if (!/^[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$/.test(upper)) return false;
152
+ const rearranged = upper.slice(4) + upper.slice(0, 4);
153
+ const numeric = rearranged.split("").map((ch) => {
154
+ const code = ch.charCodeAt(0);
155
+ return code >= 65 && code <= 90 ? (code - 55).toString() : ch;
156
+ }).join("");
157
+ let remainder = numeric;
158
+ while (remainder.length > 2) {
159
+ const block = remainder.slice(0, 9);
160
+ remainder = (parseInt(block, 10) % 97).toString() + remainder.slice(block.length);
161
+ }
162
+ return parseInt(remainder, 10) % 97 === 1;
136
163
  }
137
164
  var BLUEPRINTS = {
138
165
  venmo: {
@@ -184,9 +211,9 @@ var BLUEPRINTS = {
184
211
  transform: (v) => v.replace(/^@+/, "").trim(),
185
212
  extensionRegistration: {
186
213
  providerId: "wise",
187
- requiredPrompt: "This Wisetag is not registered yet. Verify it in the Peer extension, then refresh and retry with the same Wisetag.",
188
- ctaLabel: "Install Peer Extension",
189
- minExtensionVersion: "0.4.12"
214
+ requiredPrompt: "This Wisetag is not registered yet. Register it with PeerAuth, then retry with the same Wisetag.",
215
+ ctaLabel: "Register Wisetag",
216
+ minExtensionVersion: "0.5.0"
190
217
  }
191
218
  },
192
219
  mercadopago: {
@@ -203,7 +230,11 @@ var BLUEPRINTS = {
203
230
  identifierLabel: "Email",
204
231
  placeholder: "email",
205
232
  helperText: "Registered Zelle email",
206
- validation: z.string().email()
233
+ // Curator's ZelleProcessor rejects any non-lowercase email outright, so
234
+ // canonicalize to lowercase here rather than letting /v2/makers/create
235
+ // reject after the maker has already approved USDC + paid gas.
236
+ validation: z.string().email(),
237
+ transform: (v) => v.trim().toLowerCase()
207
238
  },
208
239
  paypal: {
209
240
  id: "paypal",
@@ -211,14 +242,17 @@ var BLUEPRINTS = {
211
242
  identifierLabel: "PayPal.me Username",
212
243
  placeholder: "paypal.me/your-username",
213
244
  helperText: "Your PayPal.me username, not your email",
214
- validation: z.string().min(1, "PayPal.me username is required").regex(/^[a-z0-9._-]+$/i, "Use your PayPal.me username (letters, numbers, . _ -)"),
245
+ validation: z.string().min(1, "PayPal.me username is required").regex(
246
+ /^[a-z0-9._-]+$/i,
247
+ "Use your PayPal.me username or paypal.me link, not a shortened URL."
248
+ ),
215
249
  transform: normalizePaypalMeUsername,
216
250
  extensionRegistration: {
217
251
  providerId: "paypal",
218
- requiredPrompt: "This PayPal username is not registered yet. Verify it in the Peer extension, then refresh and retry with the same PayPal username.",
219
- ctaLabel: "Install Peer Extension",
252
+ requiredPrompt: "This PayPal username is not registered yet. Register it with PeerAuth, then retry with the same PayPal username.",
253
+ ctaLabel: "Register PayPal Username",
220
254
  ctaSubtext: "PayPal Business accounts are not supported at this time.",
221
- minExtensionVersion: "0.4.14"
255
+ minExtensionVersion: "0.5.0"
222
256
  }
223
257
  },
224
258
  monzo: {
@@ -235,8 +269,20 @@ var BLUEPRINTS = {
235
269
  identifierLabel: "IBAN",
236
270
  placeholder: "IBAN (e.g. DE89...)",
237
271
  helperText: "Your IBAN (spaces will be removed)",
238
- validation: z.string().min(15).max(34).regex(/^[A-Z]{2}[0-9]{2}[A-Z0-9]+$/i),
272
+ // transform runs before validation, so the schema sees a spaceless,
273
+ // uppercased IBAN. The MOD-97 refine mirrors curator's N26Processor so a
274
+ // typo'd-but-well-formed IBAN fails here, not after USDC approval + gas.
275
+ validation: z.string().regex(/^[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$/, "Enter a valid IBAN").refine(isValidIBAN, "That IBAN looks wrong \u2014 its checksum doesn't match"),
239
276
  transform: (v) => v.replace(/\s/g, "").toUpperCase()
277
+ },
278
+ luxon: {
279
+ id: "luxon",
280
+ name: "Luxon",
281
+ identifierLabel: "Email",
282
+ placeholder: "your-luxon-email@example.com",
283
+ helperText: "Email address registered with Luxon",
284
+ validation: z.string().email(),
285
+ transform: (v) => v.trim()
240
286
  }
241
287
  };
242
288
  function buildPlatformEntry(bp) {
@@ -275,8 +321,18 @@ var PLATFORMS = {
275
321
  ZELLE: buildPlatformEntry(BLUEPRINTS.zelle),
276
322
  PAYPAL: buildPlatformEntry(BLUEPRINTS.paypal),
277
323
  MONZO: buildPlatformEntry(BLUEPRINTS.monzo),
278
- N26: buildPlatformEntry(BLUEPRINTS.n26)
324
+ N26: buildPlatformEntry(BLUEPRINTS.n26),
325
+ LUXON: buildPlatformEntry(BLUEPRINTS.luxon)
326
+ };
327
+ var PLATFORM_LIMITS = {
328
+ venmo: { platformCapUsd: 5e3 },
329
+ n26: { platformCapUsd: 1e4 },
330
+ paypal: { minTier: "PLUS" }
279
331
  };
332
+ function getPlatformLimits(platform) {
333
+ const id = platform.trim().toLowerCase();
334
+ return PLATFORM_LIMITS[id] ?? null;
335
+ }
280
336
  function getPlatformById(id) {
281
337
  return Object.values(PLATFORMS).find((p) => p.id === id) ?? null;
282
338
  }
@@ -385,349 +441,7 @@ function isUserCancellation(error) {
385
441
  // src/otc.ts
386
442
  import { createPublicClient, http, isAddress, zeroAddress } from "viem";
387
443
  import { base } from "viem/chains";
388
-
389
- // ../../node_modules/@zkp2p/contracts-v2/abis/base/WhitelistPreIntentHook.json
390
- var WhitelistPreIntentHook_default = [
391
- {
392
- inputs: [
393
- {
394
- internalType: "address",
395
- name: "_orchestratorRegistry",
396
- type: "address"
397
- }
398
- ],
399
- stateMutability: "nonpayable",
400
- type: "constructor"
401
- },
402
- {
403
- inputs: [],
404
- name: "EmptyArray",
405
- type: "error"
406
- },
407
- {
408
- inputs: [
409
- {
410
- internalType: "address",
411
- name: "taker",
412
- type: "address"
413
- },
414
- {
415
- internalType: "address",
416
- name: "escrow",
417
- type: "address"
418
- },
419
- {
420
- internalType: "uint256",
421
- name: "depositId",
422
- type: "uint256"
423
- }
424
- ],
425
- name: "TakerNotInWhitelist",
426
- type: "error"
427
- },
428
- {
429
- inputs: [
430
- {
431
- internalType: "address",
432
- name: "taker",
433
- type: "address"
434
- },
435
- {
436
- internalType: "address",
437
- name: "escrow",
438
- type: "address"
439
- },
440
- {
441
- internalType: "uint256",
442
- name: "depositId",
443
- type: "uint256"
444
- }
445
- ],
446
- name: "TakerNotWhitelisted",
447
- type: "error"
448
- },
449
- {
450
- inputs: [
451
- {
452
- internalType: "address",
453
- name: "caller",
454
- type: "address"
455
- },
456
- {
457
- internalType: "address",
458
- name: "owner",
459
- type: "address"
460
- },
461
- {
462
- internalType: "address",
463
- name: "delegate",
464
- type: "address"
465
- }
466
- ],
467
- name: "UnauthorizedCallerOrDelegate",
468
- type: "error"
469
- },
470
- {
471
- inputs: [
472
- {
473
- internalType: "address",
474
- name: "caller",
475
- type: "address"
476
- }
477
- ],
478
- name: "UnauthorizedOrchestratorCaller",
479
- type: "error"
480
- },
481
- {
482
- inputs: [],
483
- name: "ZeroAddress",
484
- type: "error"
485
- },
486
- {
487
- anonymous: false,
488
- inputs: [
489
- {
490
- indexed: true,
491
- internalType: "address",
492
- name: "escrow",
493
- type: "address"
494
- },
495
- {
496
- indexed: true,
497
- internalType: "uint256",
498
- name: "depositId",
499
- type: "uint256"
500
- },
501
- {
502
- indexed: true,
503
- internalType: "address",
504
- name: "taker",
505
- type: "address"
506
- }
507
- ],
508
- name: "TakerRemovedFromWhitelist",
509
- type: "event"
510
- },
511
- {
512
- anonymous: false,
513
- inputs: [
514
- {
515
- indexed: true,
516
- internalType: "address",
517
- name: "escrow",
518
- type: "address"
519
- },
520
- {
521
- indexed: true,
522
- internalType: "uint256",
523
- name: "depositId",
524
- type: "uint256"
525
- },
526
- {
527
- indexed: true,
528
- internalType: "address",
529
- name: "taker",
530
- type: "address"
531
- }
532
- ],
533
- name: "TakerWhitelisted",
534
- type: "event"
535
- },
536
- {
537
- inputs: [
538
- {
539
- internalType: "address",
540
- name: "_escrow",
541
- type: "address"
542
- },
543
- {
544
- internalType: "uint256",
545
- name: "_depositId",
546
- type: "uint256"
547
- },
548
- {
549
- internalType: "address[]",
550
- name: "_takers",
551
- type: "address[]"
552
- }
553
- ],
554
- name: "addToWhitelist",
555
- outputs: [],
556
- stateMutability: "nonpayable",
557
- type: "function"
558
- },
559
- {
560
- inputs: [
561
- {
562
- internalType: "address",
563
- name: "_escrow",
564
- type: "address"
565
- },
566
- {
567
- internalType: "uint256",
568
- name: "_depositId",
569
- type: "uint256"
570
- },
571
- {
572
- internalType: "address",
573
- name: "_taker",
574
- type: "address"
575
- }
576
- ],
577
- name: "isWhitelisted",
578
- outputs: [
579
- {
580
- internalType: "bool",
581
- name: "",
582
- type: "bool"
583
- }
584
- ],
585
- stateMutability: "view",
586
- type: "function"
587
- },
588
- {
589
- inputs: [],
590
- name: "orchestratorRegistry",
591
- outputs: [
592
- {
593
- internalType: "contract IOrchestratorRegistry",
594
- name: "",
595
- type: "address"
596
- }
597
- ],
598
- stateMutability: "view",
599
- type: "function"
600
- },
601
- {
602
- inputs: [
603
- {
604
- internalType: "address",
605
- name: "_escrow",
606
- type: "address"
607
- },
608
- {
609
- internalType: "uint256",
610
- name: "_depositId",
611
- type: "uint256"
612
- },
613
- {
614
- internalType: "address[]",
615
- name: "_takers",
616
- type: "address[]"
617
- }
618
- ],
619
- name: "removeFromWhitelist",
620
- outputs: [],
621
- stateMutability: "nonpayable",
622
- type: "function"
623
- },
624
- {
625
- inputs: [
626
- {
627
- components: [
628
- {
629
- internalType: "address",
630
- name: "taker",
631
- type: "address"
632
- },
633
- {
634
- internalType: "address",
635
- name: "escrow",
636
- type: "address"
637
- },
638
- {
639
- internalType: "uint256",
640
- name: "depositId",
641
- type: "uint256"
642
- },
643
- {
644
- internalType: "uint256",
645
- name: "amount",
646
- type: "uint256"
647
- },
648
- {
649
- internalType: "address",
650
- name: "to",
651
- type: "address"
652
- },
653
- {
654
- internalType: "bytes32",
655
- name: "paymentMethod",
656
- type: "bytes32"
657
- },
658
- {
659
- internalType: "bytes32",
660
- name: "fiatCurrency",
661
- type: "bytes32"
662
- },
663
- {
664
- internalType: "uint256",
665
- name: "conversionRate",
666
- type: "uint256"
667
- },
668
- {
669
- components: [
670
- {
671
- internalType: "address",
672
- name: "recipient",
673
- type: "address"
674
- },
675
- {
676
- internalType: "uint256",
677
- name: "fee",
678
- type: "uint256"
679
- }
680
- ],
681
- internalType: "struct IReferralFee.ReferralFee[]",
682
- name: "referralFees",
683
- type: "tuple[]"
684
- },
685
- {
686
- internalType: "bytes",
687
- name: "preIntentHookData",
688
- type: "bytes"
689
- }
690
- ],
691
- internalType: "struct IPreIntentHook.PreIntentContext",
692
- name: "_ctx",
693
- type: "tuple"
694
- }
695
- ],
696
- name: "validateSignalIntent",
697
- outputs: [],
698
- stateMutability: "view",
699
- type: "function"
700
- },
701
- {
702
- inputs: [
703
- {
704
- internalType: "address",
705
- name: "",
706
- type: "address"
707
- },
708
- {
709
- internalType: "uint256",
710
- name: "",
711
- type: "uint256"
712
- },
713
- {
714
- internalType: "address",
715
- name: "",
716
- type: "address"
717
- }
718
- ],
719
- name: "whitelist",
720
- outputs: [
721
- {
722
- internalType: "bool",
723
- name: "",
724
- type: "bool"
725
- }
726
- ],
727
- stateMutability: "view",
728
- type: "function"
729
- }
730
- ];
444
+ import WhitelistPreIntentHookABI from "@zkp2p/contracts-v2/abis/base/WhitelistPreIntentHook.json";
731
445
 
732
446
  // src/sdk-client.ts
733
447
  import { OfframpClient } from "@zkp2p/sdk";
@@ -743,7 +457,7 @@ function createSdkClient(walletClient) {
743
457
 
744
458
  // src/otc.ts
745
459
  var WHITELIST_HOOK_ADDRESS = "0xda023Ea0d789A41BcF5866F7B6BBd2CaDF9b79B8";
746
- var OTC_BASE_URL = "https://usdctofiat.xyz";
460
+ var OTC_BASE_URL = "https://otc.usdctofiat.xyz";
747
461
  async function enableOtc(walletClient, depositId, takerAddress, escrowAddress) {
748
462
  if (!isAddress(takerAddress)) {
749
463
  throw new OfframpError("Invalid taker address", "VALIDATION");
@@ -815,7 +529,7 @@ async function disableOtc(walletClient, depositId, escrowAddress) {
815
529
  }
816
530
  function getOtcLink(depositId, escrowAddress) {
817
531
  const escrow = escrowAddress || ESCROW_ADDRESS;
818
- return `${OTC_BASE_URL}/deposit/${escrow}/${depositId}`;
532
+ return `${OTC_BASE_URL}/d/${escrow}/${depositId}`;
819
533
  }
820
534
  async function writeContract(walletClient, functionName, args) {
821
535
  const wc = walletClient;
@@ -824,7 +538,7 @@ async function writeContract(walletClient, functionName, args) {
824
538
  }
825
539
  await wc.writeContract({
826
540
  address: WHITELIST_HOOK_ADDRESS,
827
- abi: WhitelistPreIntentHook_default,
541
+ abi: WhitelistPreIntentHookABI,
828
542
  functionName,
829
543
  args
830
544
  });
@@ -833,7 +547,7 @@ async function readIsWhitelisted(escrow, depositId, taker) {
833
547
  const publicClient = createPublicClient({ chain: base, transport: http(BASE_RPC_URL) });
834
548
  return publicClient.readContract({
835
549
  address: WHITELIST_HOOK_ADDRESS,
836
- abi: WhitelistPreIntentHook_default,
550
+ abi: WhitelistPreIntentHookABI,
837
551
  functionName: "isWhitelisted",
838
552
  args: [escrow, depositId, taker]
839
553
  });
@@ -1200,6 +914,28 @@ async function registerPayeeDetails(processorName, payload) {
1200
914
  clearTimeout(timeout);
1201
915
  }
1202
916
  }
917
+ async function validatePayeeDetails(processorName, offchainId) {
918
+ const controller = new AbortController();
919
+ const timeout = setTimeout(() => controller.abort(), PAYEE_REGISTRATION_TIMEOUT_MS);
920
+ try {
921
+ const res = await fetch(`${API_BASE_URL}/v2/makers/validate`, {
922
+ method: "POST",
923
+ headers: { "Content-Type": "application/json" },
924
+ body: JSON.stringify({ processorName, offchainId }),
925
+ signal: controller.signal
926
+ });
927
+ if (!res.ok) return "unknown";
928
+ const json = await res.json();
929
+ if (typeof json.responseObject === "boolean") {
930
+ return json.responseObject ? "valid" : "invalid";
931
+ }
932
+ return "unknown";
933
+ } catch {
934
+ return "unknown";
935
+ } finally {
936
+ clearTimeout(timeout);
937
+ }
938
+ }
1203
939
  function attachOracleConfig(entries, conversionRates) {
1204
940
  return entries.map(
1205
941
  (group, gi) => group.map((entry, ci) => {
@@ -1371,6 +1107,17 @@ async function offramp(walletClient, params, onProgress, telemetryContext) {
1371
1107
  } catch (err) {
1372
1108
  if (err instanceof OfframpError) throw err;
1373
1109
  }
1110
+ const canonicalName = platformId.startsWith("zelle") ? "zelle" : platformId;
1111
+ if (!getPeerExtensionRegistrationInfo(platformId)) {
1112
+ const outcome = await validatePayeeDetails(canonicalName, normalizedIdentifier);
1113
+ if (outcome === "invalid") {
1114
+ throw new OfframpError(
1115
+ `Couldn't verify that ${platform.name} ${platform.identifier.label.toLowerCase()}. Double-check it and try again.`,
1116
+ "VALIDATION",
1117
+ "registering"
1118
+ );
1119
+ }
1120
+ }
1374
1121
  const client = createSdkClient(walletClient);
1375
1122
  const txOverrides = { referrer: [REFERRER] };
1376
1123
  emitProgress({ step: "approving" });
@@ -1395,7 +1142,6 @@ async function offramp(walletClient, params, onProgress, telemetryContext) {
1395
1142
  }
1396
1143
  emitProgress({ step: "registering" });
1397
1144
  let hashedOnchainId;
1398
- const canonicalName = platformId.startsWith("zelle") ? "zelle" : platformId;
1399
1145
  try {
1400
1146
  const payeePayload = buildDepositData(canonicalName, normalizedIdentifier);
1401
1147
  hashedOnchainId = await registerPayeeDetails(canonicalName, payeePayload);
@@ -1759,7 +1505,10 @@ import {
1759
1505
  export {
1760
1506
  ESCROW_ADDRESS,
1761
1507
  normalizePaypalMeUsername,
1508
+ isValidIBAN,
1762
1509
  PLATFORMS,
1510
+ PLATFORM_LIMITS,
1511
+ getPlatformLimits,
1763
1512
  getPeerExtensionRegistrationInfo,
1764
1513
  isPeerExtensionRegistrationError,
1765
1514
  MakersCreateError,
@@ -1786,4 +1535,4 @@ export {
1786
1535
  openPeerExtensionInstallPage,
1787
1536
  getPeerExtensionState
1788
1537
  };
1789
- //# sourceMappingURL=chunk-F7DVUBMT.js.map
1538
+ //# sourceMappingURL=chunk-4KKKDDES.js.map