@oasisprotocol/privana-sdk 0.4.0 → 0.4.1

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) {
@@ -669,11 +744,13 @@ function FiatOnRampForm({
669
744
  const { address } = useAccount();
670
745
  const [visible, setVisible] = useState(false);
671
746
  const [isPreparing, setIsPreparing] = useState(false);
747
+ const [rowError, setRowError] = useState(null);
672
748
  const {
673
749
  status,
674
750
  activeIntentId,
675
751
  pending,
676
752
  error,
753
+ finalityProgress,
677
754
  depositAddress,
678
755
  minDepositBaseUnits,
679
756
  selectedToken,
@@ -700,15 +777,17 @@ function FiatOnRampForm({
700
777
  );
701
778
  const minFiatGate = minDepositBaseUnits !== void 0 && decimals !== void 0 ? Number(formatUnits(minDepositBaseUnits, decimals)) * 1.05 : void 0;
702
779
  const isBelowMin = minFiatGate !== void 0 && Number(defaultBaseCurrencyAmount) < minFiatGate;
703
- const isBusy = isPreparing || status === "awaiting-purchase" || status === "awaiting-delivery" || status === "verifying";
780
+ const isBusy = isPreparing || status === "awaiting-purchase";
781
+ const isInitializing = !!address && !depositAddress;
704
782
  const blockReasons = useMemo(
705
783
  () => [
706
784
  !address ? "wallet-not-connected" : null,
707
785
  !depositAddress ? "deposit-address-not-loaded" : null,
708
786
  isBusy ? `busy:${isPreparing ? "preparing" : status}` : null,
787
+ visible ? "widget-open" : null,
709
788
  isBelowMin ? "below-minimum" : null
710
789
  ].filter((reason) => Boolean(reason)),
711
- [address, depositAddress, isBelowMin, isBusy, isPreparing, status]
790
+ [address, depositAddress, isBelowMin, isBusy, isPreparing, status, visible]
712
791
  );
713
792
  const canBuy = blockReasons.length === 0;
714
793
  const handleOpen = useCallback(async () => {
@@ -783,11 +862,10 @@ function FiatOnRampForm({
783
862
  const handleReady = useCallback(async () => {
784
863
  emitFormDebug("moonpay:onReady");
785
864
  }, [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(", ")
865
+ return /* @__PURE__ */ jsxs("div", { "data-privana": true, className: "flex flex-col gap-4", children: [
866
+ isInitializing ? /* @__PURE__ */ jsx(Skeleton, { className: "h-9 w-full rounded-md" }) : /* @__PURE__ */ jsxs(Button, { type: "button", onClick: handleOpen, disabled: !canBuy, children: [
867
+ (isBusy || visible) && /* @__PURE__ */ jsx(Loader2, { className: "animate-spin", "aria-hidden": true }),
868
+ "Buy"
791
869
  ] }),
792
870
  error && /* @__PURE__ */ jsx("p", { className: "text-destructive text-sm", role: "alert", children: error.message }),
793
871
  isBelowMin && minFiatGate !== void 0 && /* @__PURE__ */ jsxs("p", { className: "text-destructive text-sm", role: "alert", children: [
@@ -796,24 +874,54 @@ function FiatOnRampForm({
796
874
  "."
797
875
  ] }),
798
876
  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
- ))
877
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: "Pending payments" }),
878
+ pending.map((record) => {
879
+ const progress = parseFinalityProgress(finalityProgress[record.transaction_id]);
880
+ const hasProgress = !!finalityProgress[record.transaction_id];
881
+ const isStalled = !hasProgress && Date.now() / 1e3 - (record.updated_at ?? 0) > 60;
882
+ const showRetry = rowError?.id === record.transaction_id || isStalled;
883
+ return /* @__PURE__ */ jsxs(
884
+ "div",
885
+ {
886
+ className: "border-border flex flex-col gap-1 rounded-md border p-2",
887
+ children: [
888
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
889
+ /* @__PURE__ */ jsxs("p", { className: "text-muted-foreground flex items-center gap-1 text-xs", children: [
890
+ /* @__PURE__ */ jsx(Loader2, { className: "size-3 animate-spin", "aria-hidden": true }),
891
+ progress ?? "Verifying\u2026"
892
+ ] }),
893
+ /* @__PURE__ */ jsxs("p", { className: "text-muted-foreground text-xs", children: [
894
+ record.quote_currency_amount ?? "?",
895
+ " ",
896
+ displaySymbol
897
+ ] })
898
+ ] }),
899
+ rowError?.id === record.transaction_id && /* @__PURE__ */ jsx("p", { className: "text-destructive text-xs", children: rowError.message }),
900
+ showRetry && /* @__PURE__ */ jsx(
901
+ Button,
902
+ {
903
+ type: "button",
904
+ variant: "outline",
905
+ size: "sm",
906
+ onClick: async () => {
907
+ setRowError(null);
908
+ try {
909
+ await finishPendingVerification(record);
910
+ } catch (err) {
911
+ setRowError({
912
+ id: record.transaction_id,
913
+ message: err instanceof Error ? err.message : "Verification failed"
914
+ });
915
+ }
916
+ },
917
+ children: "Retry"
918
+ }
919
+ )
920
+ ]
921
+ },
922
+ record.transaction_id
923
+ );
924
+ })
817
925
  ] }),
818
926
  visible && depositAddress && activeIntentId && /* @__PURE__ */ jsx(
819
927
  MoonPayBuyWidget,
@@ -836,21 +944,10 @@ function FiatOnRampForm({
836
944
  )
837
945
  ] });
838
946
  }
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
- }
947
+ function parseFinalityProgress(message) {
948
+ if (!message) return null;
949
+ const match = message.match(/(\d+\/\d+)\s+confirmations/i);
950
+ return match ? `${match[1]} confirmations` : null;
854
951
  }
855
952
 
856
953
  export { FiatOnRampForm, useFiatOnRamp };