@oasisprotocol/privana-sdk 0.4.0 → 0.4.2

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/dist/on-ramp.js CHANGED
@@ -1,8 +1,9 @@
1
1
  "use client";
2
- import { usePrivanaContext, usePrivateReadRequest, useDepositVerification, getTransactionReceipt, Button } from './chunk-ZVNC4FTJ.js';
2
+ import { usePrivanaContext, usePrivateReadRequest, useDepositVerification, waitForTransactionReceipt, Skeleton, Button } from './chunk-ZXFQJ5MG.js';
3
3
  import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
4
4
  import { parseAbiItem, zeroAddress, parseUnits, decodeEventLog, formatUnits } from 'viem';
5
5
  import { useConfig, useAccount } from 'wagmi';
6
+ import { Loader2 } from 'lucide-react';
6
7
  import { MoonPayBuyWidget } from '@moonpay/moonpay-react';
7
8
  import { jsxs, jsx } from 'react/jsx-runtime';
8
9
 
@@ -28,6 +29,7 @@ function useFiatOnRamp(options) {
28
29
  const [depositAddress, setDepositAddress] = useState();
29
30
  const [minDepositBaseUnits, setMinDepositBaseUnits] = useState();
30
31
  const [activeIntentId, setActiveIntentId] = useState(null);
32
+ const [finalityProgress, setFinalityProgress] = useState({});
31
33
  const onCreditedRef = useRef(onCredited);
32
34
  const onErrorRef = useRef(onError);
33
35
  const onDebugEventRef = useRef(onDebugEvent);
@@ -37,6 +39,7 @@ function useFiatOnRamp(options) {
37
39
  const activeVerificationKeyRef = useRef(null);
38
40
  const triggeredVerificationKeysRef = useRef(/* @__PURE__ */ new Set());
39
41
  const closeReconcilePromiseRef = useRef(null);
42
+ const purchaseInitiatedRef = useRef(false);
40
43
  useEffect(() => {
41
44
  onCreditedRef.current = onCredited;
42
45
  onErrorRef.current = onError;
@@ -132,13 +135,32 @@ function useFiatOnRamp(options) {
132
135
  pollTimeout: verificationTimeout,
133
136
  pollInterval: options.verificationPollInterval,
134
137
  finalityRetryInterval,
138
+ onCheckRetry: (message) => {
139
+ const record = activeVerificationRecordRef.current;
140
+ if (!record) return;
141
+ emitDebug("verification:check-retry", {
142
+ message,
143
+ record: summariseOnRampRecord(record)
144
+ });
145
+ setFinalityProgress((prev) => ({ ...prev, [record.transaction_id]: message }));
146
+ },
135
147
  onCredited: (depositTxHash) => {
136
148
  const record = activeVerificationRecordRef.current;
137
149
  emitDebug("verification:credited", {
138
150
  depositTxHash,
139
151
  record: record ? summariseOnRampRecord(record) : null
140
152
  });
141
- setStatus("credited");
153
+ if (record && activeIntentIdRef.current === record.transaction_id) {
154
+ setStatus("credited");
155
+ }
156
+ if (record) {
157
+ setFinalityProgress((prev) => {
158
+ if (!(record.transaction_id in prev)) return prev;
159
+ const next = { ...prev };
160
+ delete next[record.transaction_id];
161
+ return next;
162
+ });
163
+ }
142
164
  void (async () => {
143
165
  try {
144
166
  if (record && depositTxHash.startsWith("0x")) {
@@ -170,21 +192,27 @@ function useFiatOnRamp(options) {
170
192
  onCreditedRef.current?.(depositTxHash);
171
193
  },
172
194
  onCheckTimeout: (depositTxHash) => {
195
+ const record = activeVerificationRecordRef.current;
173
196
  const err = new Error(
174
197
  "Privana verification is still pending. Retry from the pending on-ramp list if it does not complete."
175
198
  );
176
199
  emitDebug("verification:timeout", { depositTxHash, message: err.message });
177
200
  clearActiveVerification();
178
- setStatus("failed");
179
- setError(err);
201
+ if (!record || activeIntentIdRef.current === record.transaction_id) {
202
+ setStatus("failed");
203
+ setError(err);
204
+ }
180
205
  void refreshPending();
181
206
  onErrorRef.current?.(err);
182
207
  },
183
208
  onError: (err) => {
209
+ const record = activeVerificationRecordRef.current;
184
210
  emitDebug("verification:error", errorPayload(err));
185
211
  clearActiveVerification();
186
- setStatus("failed");
187
- setError(err);
212
+ if (!record || activeIntentIdRef.current === record.transaction_id) {
213
+ setStatus("failed");
214
+ setError(err);
215
+ }
188
216
  onErrorRef.current?.(err);
189
217
  }
190
218
  });
@@ -196,6 +224,7 @@ function useFiatOnRamp(options) {
196
224
  }) => {
197
225
  try {
198
226
  setError(null);
227
+ purchaseInitiatedRef.current = false;
199
228
  const token = enabledTokens.find((t) => t.id.toLowerCase() === tokenId.toLowerCase());
200
229
  if (!token) throw new Error(`Unknown token: ${tokenId}`);
201
230
  if (!depositAddress) throw new Error("Privana deposit address is not ready");
@@ -278,6 +307,7 @@ function useFiatOnRamp(options) {
278
307
  const handleTransactionCreated = useCallback(
279
308
  async (props) => {
280
309
  emitDebug("moonpay:onTransactionCreated", summariseMoonPayEventProps(props));
310
+ purchaseInitiatedRef.current = true;
281
311
  await registerOnRampTokenMapping(props.id);
282
312
  },
283
313
  [emitDebug, registerOnRampTokenMapping]
@@ -356,6 +386,12 @@ function useFiatOnRamp(options) {
356
386
  triggeredVerificationKeysRef.current.add(verificationKey);
357
387
  activeVerificationKeyRef.current = verificationKey;
358
388
  activeVerificationRecordRef.current = record;
389
+ setFinalityProgress((prev) => {
390
+ if (!(record.transaction_id in prev)) return prev;
391
+ const next = { ...prev };
392
+ delete next[record.transaction_id];
393
+ return next;
394
+ });
359
395
  emitDebug("verification:start", {
360
396
  verificationKey,
361
397
  record: summariseOnRampRecord(record)
@@ -396,7 +432,9 @@ function useFiatOnRamp(options) {
396
432
  `Delivered amount (${record.quote_currency_amount}) is below the minimum deposit.`
397
433
  );
398
434
  }
399
- setStatus("verifying");
435
+ if (activeIntentIdRef.current === record.transaction_id) {
436
+ setStatus("verifying");
437
+ }
400
438
  emitDebug("verification:check-deposit-request", {
401
439
  hash: record.on_chain_tx_hash,
402
440
  chainId: record.chain_id,
@@ -464,6 +502,17 @@ function useFiatOnRamp(options) {
464
502
  await refreshPending();
465
503
  return;
466
504
  }
505
+ if (!purchaseInitiatedRef.current) {
506
+ emitDebug("moonpay:widget-closed-without-purchase", {
507
+ previousStatus,
508
+ transactionId
509
+ });
510
+ await refreshPending();
511
+ if (previousStatus === "awaiting-purchase" || previousStatus === "awaiting-delivery") {
512
+ setStatus("idle");
513
+ }
514
+ return;
515
+ }
467
516
  try {
468
517
  setStatus("awaiting-delivery");
469
518
  const record = await waitForOnChainHash(transactionId);
@@ -502,15 +551,39 @@ function useFiatOnRamp(options) {
502
551
  setStatus("failed");
503
552
  setError(e);
504
553
  onErrorRef.current?.(e);
554
+ throw e;
505
555
  }
506
556
  },
507
557
  [emitDebug, triggerVerification]
508
558
  );
559
+ const triggerVerificationRef = useRef(triggerVerification);
560
+ useEffect(() => {
561
+ triggerVerificationRef.current = triggerVerification;
562
+ }, [triggerVerification]);
563
+ useEffect(() => {
564
+ let cancelled = false;
565
+ void (async () => {
566
+ for (const record of pending) {
567
+ if (cancelled) break;
568
+ if (!record.on_chain_tx_hash || !record.quote_currency_amount) continue;
569
+ const key = getOnRampVerificationKey(record);
570
+ if (triggeredVerificationKeysRef.current.has(key)) continue;
571
+ try {
572
+ await triggerVerificationRef.current(record);
573
+ } catch {
574
+ }
575
+ }
576
+ })();
577
+ return () => {
578
+ cancelled = true;
579
+ };
580
+ }, [pending]);
509
581
  return {
510
582
  status,
511
583
  activeIntentId,
512
584
  pending,
513
585
  error,
586
+ finalityProgress,
514
587
  depositAddress,
515
588
  minDepositBaseUnits,
516
589
  selectedToken,
@@ -562,9 +635,11 @@ async function resolveDeliveredAmount({
562
635
  }
563
636
  let receiptError;
564
637
  try {
565
- const receipt = await getTransactionReceipt(wagmiConfig, {
638
+ const receipt = await waitForTransactionReceipt(wagmiConfig, {
566
639
  hash: onChainTxHash,
567
- chainId
640
+ chainId,
641
+ timeout: 6e4,
642
+ pollingInterval: 4e3
568
643
  });
569
644
  let delivered = 0n;
570
645
  for (const log of receipt.logs) {
@@ -662,6 +737,12 @@ function FiatOnRampForm({
662
737
  baseCurrencyCode = "usd",
663
738
  defaultBaseCurrencyAmount = "100",
664
739
  tokenSymbol,
740
+ theme,
741
+ themeId,
742
+ colorCode,
743
+ variant = "overlay",
744
+ lockAmount,
745
+ paymentMethod,
665
746
  onCredited,
666
747
  onError,
667
748
  onDebugEvent
@@ -669,11 +750,13 @@ function FiatOnRampForm({
669
750
  const { address } = useAccount();
670
751
  const [visible, setVisible] = useState(false);
671
752
  const [isPreparing, setIsPreparing] = useState(false);
753
+ const [rowError, setRowError] = useState(null);
672
754
  const {
673
755
  status,
674
756
  activeIntentId,
675
757
  pending,
676
758
  error,
759
+ finalityProgress,
677
760
  depositAddress,
678
761
  minDepositBaseUnits,
679
762
  selectedToken,
@@ -686,6 +769,10 @@ function FiatOnRampForm({
686
769
  } = useFiatOnRamp({ tokenId, onCredited, onError, onDebugEvent });
687
770
  const decimals = selectedToken?.decimals;
688
771
  const displaySymbol = tokenSymbol ?? selectedToken?.symbol ?? currencyCode.toUpperCase();
772
+ const overlayNode = useMemo(
773
+ () => variant === "overlay" ? buildOverlayNode() : void 0,
774
+ [variant]
775
+ );
689
776
  const emitFormDebug = useCallback(
690
777
  (event, payload) => {
691
778
  onDebugEvent?.({
@@ -700,15 +787,17 @@ function FiatOnRampForm({
700
787
  );
701
788
  const minFiatGate = minDepositBaseUnits !== void 0 && decimals !== void 0 ? Number(formatUnits(minDepositBaseUnits, decimals)) * 1.05 : void 0;
702
789
  const isBelowMin = minFiatGate !== void 0 && Number(defaultBaseCurrencyAmount) < minFiatGate;
703
- const isBusy = isPreparing || status === "awaiting-purchase" || status === "awaiting-delivery" || status === "verifying";
790
+ const isBusy = isPreparing || status === "awaiting-purchase";
791
+ const isInitializing = !!address && !depositAddress;
704
792
  const blockReasons = useMemo(
705
793
  () => [
706
794
  !address ? "wallet-not-connected" : null,
707
795
  !depositAddress ? "deposit-address-not-loaded" : null,
708
796
  isBusy ? `busy:${isPreparing ? "preparing" : status}` : null,
797
+ visible ? "widget-open" : null,
709
798
  isBelowMin ? "below-minimum" : null
710
799
  ].filter((reason) => Boolean(reason)),
711
- [address, depositAddress, isBelowMin, isBusy, isPreparing, status]
800
+ [address, depositAddress, isBelowMin, isBusy, isPreparing, status, visible]
712
801
  );
713
802
  const canBuy = blockReasons.length === 0;
714
803
  const handleOpen = useCallback(async () => {
@@ -783,11 +872,10 @@ function FiatOnRampForm({
783
872
  const handleReady = useCallback(async () => {
784
873
  emitFormDebug("moonpay:onReady");
785
874
  }, [emitFormDebug]);
786
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4", children: [
787
- /* @__PURE__ */ jsx(Button, { type: "button", onClick: handleOpen, children: isPreparing ? "Preparing MoonPay\u2026" : isBusy ? statusLabel(status) : `Buy ${displaySymbol}` }),
788
- !canBuy && !isBusy && /* @__PURE__ */ jsxs("p", { className: "text-muted-foreground text-xs", children: [
789
- "Button blocked: ",
790
- blockReasons.join(", ")
875
+ return /* @__PURE__ */ jsxs("div", { "data-privana": true, className: "flex flex-col gap-4", children: [
876
+ isInitializing ? /* @__PURE__ */ jsx(Skeleton, { className: "h-9 w-full rounded-md" }) : /* @__PURE__ */ jsxs(Button, { type: "button", onClick: handleOpen, disabled: !canBuy, children: [
877
+ (isBusy || visible) && /* @__PURE__ */ jsx(Loader2, { className: "animate-spin", "aria-hidden": true }),
878
+ "Buy"
791
879
  ] }),
792
880
  error && /* @__PURE__ */ jsx("p", { className: "text-destructive text-sm", role: "alert", children: error.message }),
793
881
  isBelowMin && minFiatGate !== void 0 && /* @__PURE__ */ jsxs("p", { className: "text-destructive text-sm", role: "alert", children: [
@@ -796,32 +884,68 @@ function FiatOnRampForm({
796
884
  "."
797
885
  ] }),
798
886
  pending.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 rounded-md border p-3", children: [
799
- /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: "Finish verification" }),
800
- /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-xs", children: "MoonPay delivered your purchase, but Privana hasn't verified it yet. Resume verification now to credit your balance." }),
801
- pending.map((record) => /* @__PURE__ */ jsxs(
802
- Button,
803
- {
804
- type: "button",
805
- variant: "outline",
806
- size: "sm",
807
- disabled: isBusy,
808
- onClick: () => finishPendingVerification(record),
809
- children: [
810
- record.quote_currency_amount ?? "?",
811
- " ",
812
- displaySymbol
813
- ]
814
- },
815
- record.transaction_id
816
- ))
887
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: "Pending payments" }),
888
+ pending.map((record) => {
889
+ const progress = parseFinalityProgress(finalityProgress[record.transaction_id]);
890
+ const hasProgress = !!finalityProgress[record.transaction_id];
891
+ const isStalled = !hasProgress && Date.now() / 1e3 - (record.updated_at ?? 0) > 60;
892
+ const showRetry = rowError?.id === record.transaction_id || isStalled;
893
+ return /* @__PURE__ */ jsxs(
894
+ "div",
895
+ {
896
+ className: "border-border flex flex-col gap-1 rounded-md border p-2",
897
+ children: [
898
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
899
+ /* @__PURE__ */ jsxs("p", { className: "text-muted-foreground flex items-center gap-1 text-xs", children: [
900
+ /* @__PURE__ */ jsx(Loader2, { className: "size-3 animate-spin", "aria-hidden": true }),
901
+ progress ?? "Verifying\u2026"
902
+ ] }),
903
+ /* @__PURE__ */ jsxs("p", { className: "text-muted-foreground text-xs", children: [
904
+ record.quote_currency_amount ?? "?",
905
+ " ",
906
+ displaySymbol
907
+ ] })
908
+ ] }),
909
+ rowError?.id === record.transaction_id && /* @__PURE__ */ jsx("p", { className: "text-destructive text-xs", children: rowError.message }),
910
+ showRetry && /* @__PURE__ */ jsx(
911
+ Button,
912
+ {
913
+ type: "button",
914
+ variant: "outline",
915
+ size: "sm",
916
+ onClick: async () => {
917
+ setRowError(null);
918
+ try {
919
+ await finishPendingVerification(record);
920
+ } catch (err) {
921
+ setRowError({
922
+ id: record.transaction_id,
923
+ message: err instanceof Error ? err.message : "Verification failed"
924
+ });
925
+ }
926
+ },
927
+ children: "Retry"
928
+ }
929
+ )
930
+ ]
931
+ },
932
+ record.transaction_id
933
+ );
934
+ })
817
935
  ] }),
818
936
  visible && depositAddress && activeIntentId && /* @__PURE__ */ jsx(
819
937
  MoonPayBuyWidget,
820
938
  {
821
- variant: "overlay",
939
+ variant,
822
940
  visible: true,
941
+ theme,
942
+ themeId,
943
+ colorCode,
944
+ overlayNode,
823
945
  baseCurrencyCode,
824
946
  baseCurrencyAmount: defaultBaseCurrencyAmount,
947
+ lockAmount: lockAmount ? "true" : void 0,
948
+ paymentMethod,
825
949
  currencyCode,
826
950
  walletAddress: depositAddress,
827
951
  externalCustomerId: address?.toLowerCase(),
@@ -836,21 +960,17 @@ function FiatOnRampForm({
836
960
  )
837
961
  ] });
838
962
  }
839
- function statusLabel(status) {
840
- switch (status) {
841
- case "awaiting-purchase":
842
- return "Complete your purchase\u2026";
843
- case "awaiting-delivery":
844
- return "Waiting for on-chain delivery\u2026";
845
- case "verifying":
846
- return "Verifying with Privana\u2026";
847
- case "credited":
848
- return "Credited";
849
- case "failed":
850
- return "Failed";
851
- default:
852
- return "";
853
- }
963
+ function parseFinalityProgress(message) {
964
+ if (!message) return null;
965
+ const match = message.match(/(\d+\/\d+)\s+confirmations/i);
966
+ return match ? `${match[1]} confirmations` : null;
967
+ }
968
+ function buildOverlayNode() {
969
+ if (typeof document === "undefined") return void 0;
970
+ const wrap = document.createElement("div");
971
+ wrap.style.cssText = "display:flex;flex-direction:column;align-items:center;gap:12px";
972
+ wrap.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="139" height="20.61" viewBox="0 0 139 20.61"><path d="M15.08,20.61L10.49,20.61L10.49,0.00L17.96,0.00L18.81,0.02L19.62,0.10L20.38,0.22L21.11,0.39L21.79,0.60L22.43,0.87L23.03,1.18L23.58,1.53L24.08,1.92L24.53,2.35L24.93,2.82L25.29,3.33L25.59,3.88L25.84,4.47L26.03,5.09L26.17,5.75L26.25,6.44L26.28,7.17L26.28,7.17L26.28,7.62L26.25,8.33L26.17,9.01L26.03,9.66L25.84,10.27L25.59,10.86L25.29,11.41L24.93,11.93L24.53,12.40L24.08,12.83L23.58,13.23L23.03,13.58L22.43,13.89L21.79,14.15L21.11,14.37L20.38,14.54L19.62,14.66L18.81,14.73L17.96,14.76L17.96,14.76L15.08,14.76L15.08,20.61ZM18.19,3.98L15.08,3.98L15.08,10.78L18.19,10.78L18.53,10.77L18.87,10.73L19.18,10.68L19.48,10.59L19.76,10.49L20.02,10.36L20.27,10.21L20.50,10.04L20.70,9.85L20.89,9.64L21.06,9.42L21.21,9.17L21.34,8.91L21.45,8.63L21.53,8.34L21.59,8.04L21.62,7.72L21.63,7.39L21.63,7.39L21.62,7.05L21.59,6.72L21.53,6.40L21.45,6.10L21.34,5.82L21.21,5.56L21.06,5.31L20.89,5.09L20.70,4.88L20.50,4.69L20.27,4.53L20.02,4.38L19.76,4.26L19.48,4.16L19.18,4.08L18.87,4.02L18.53,3.99L18.19,3.98L18.19,3.98ZM31.76,20.61L27.16,20.61L27.16,0.00L35.20,0.00L36.03,0.02L36.82,0.09L37.58,0.19L38.30,0.34L38.98,0.53L39.62,0.77L40.23,1.05L40.78,1.37L41.28,1.73L41.74,2.13L42.15,2.57L42.51,3.05L42.82,3.58L43.07,4.15L43.26,4.76L43.40,5.42L43.49,6.12L43.52,6.86L43.52,6.86L43.52,7.31L43.49,8.04L43.40,8.73L43.26,9.38L43.07,9.98L42.82,10.54L42.51,11.06L42.51,11.06L42.15,11.53L41.75,11.97L41.30,12.36L40.80,12.72L40.26,13.03L39.68,13.30L39.68,13.30L44.89,20.61L39.57,20.61L35.12,14.06L31.76,14.06L31.76,20.61ZM35.56,3.89L31.76,3.89L31.76,10.44L35.56,10.44L35.89,10.43L36.21,10.40L36.51,10.34L36.79,10.26L37.06,10.16L37.31,10.04L37.55,9.89L37.77,9.73L37.97,9.55L38.15,9.35L38.32,9.13L38.46,8.89L38.59,8.64L38.69,8.37L38.77,8.09L38.82,7.79L38.86,7.49L38.87,7.17L38.86,6.85L38.82,6.54L38.77,6.25L38.69,5.97L38.59,5.70L38.46,5.45L38.32,5.21L38.15,4.99L37.97,4.79L37.77,4.61L37.55,4.44L37.31,4.30L37.06,4.17L36.79,4.07L36.51,3.99L36.21,3.94L35.89,3.90L35.56,3.89L35.56,3.89ZM50.25,20.61L45.66,20.61L45.66,0.17L50.25,0.17L50.25,20.61ZM64.88,20.61L57.41,20.61L51.19,0.17L55.92,0.17L60.71,16.88L61.66,16.88L66.09,0.17L70.68,0.17L64.88,20.61ZM71.44,20.61L66.85,20.61L73.60,0.17L81.02,0.17L88.02,20.61L83.26,20.61L81.61,15.54L73.07,15.54L71.44,20.61ZM74.27,11.73L80.35,11.73L77.80,3.92L76.76,3.92L74.27,11.73ZM92.94,20.61L88.68,20.61L88.68,0.17L96.21,0.17L103.91,16.88L104.30,16.88L104.30,0.17L108.62,0.17L108.62,20.61L101.03,20.61L93.33,3.89L92.94,3.89L92.94,20.61ZM113.87,20.61L109.28,20.61L116.02,0.17L123.44,0.17L130.44,20.61L125.68,20.61L124.03,15.54L115.49,15.54L113.87,20.61ZM116.70,11.73L122.77,11.73L120.22,3.92L119.19,3.92L116.70,11.73Z" fill="#ffffff"/></svg><span>Your secure checkout is loading</span>`;
973
+ return wrap;
854
974
  }
855
975
 
856
976
  export { FiatOnRampForm, useFiatOnRamp };