@unifold/core 0.1.69 → 0.1.70

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/index.mjs CHANGED
@@ -1,3 +1,7 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+
1
5
  // src/lib/utils.ts
2
6
  function formatStablecoinAmount(baseUnits, decimals) {
3
7
  const raw = Number(baseUnits) / 10 ** decimals;
@@ -94,6 +98,16 @@ var ActionType = /* @__PURE__ */ ((ActionType2) => {
94
98
  ActionType2["Withdraw"] = "withdraw";
95
99
  return ActionType2;
96
100
  })(ActionType || {});
101
+ var DepositAddressValidationError = class extends Error {
102
+ constructor(message) {
103
+ super(message);
104
+ __publicField(this, "isDepositAddressValidationError", true);
105
+ this.name = "DepositAddressValidationError";
106
+ }
107
+ };
108
+ function isDepositAddressValidationError(error) {
109
+ return error instanceof Error && error.isDepositAddressValidationError === true;
110
+ }
97
111
  async function createDepositAddress(overrides, publishableKey) {
98
112
  if (!overrides?.external_user_id) {
99
113
  throw new Error("external_user_id is required");
@@ -121,6 +135,13 @@ async function createDepositAddress(overrides, publishableKey) {
121
135
  body: JSON.stringify(payload)
122
136
  });
123
137
  if (!response.ok) {
138
+ if (response.status === 400) {
139
+ const body = await response.json().catch(() => null);
140
+ if (body?.error_type === "validation_error") {
141
+ const firstError = Array.isArray(body.details?.errors) ? body.details?.errors[0] : void 0;
142
+ throw new DepositAddressValidationError(firstError ?? "Invalid recipient address");
143
+ }
144
+ }
124
145
  throw new Error(`Failed to create EOA: ${response.statusText}`);
125
146
  }
126
147
  return response.json();
@@ -400,6 +421,9 @@ function getOnrampSessionStartUrl(request, publishableKey) {
400
421
  if (request.email) {
401
422
  params.append("email", request.email);
402
423
  }
424
+ if (request.payment_method_type) {
425
+ params.append("payment_method_type", request.payment_method_type);
426
+ }
403
427
  if (request.payment_method) {
404
428
  params.append("payment_method", request.payment_method);
405
429
  }
@@ -477,6 +501,21 @@ async function getProjectConfig(publishableKey, options) {
477
501
  const data = await response.json();
478
502
  return data;
479
503
  }
504
+ async function getPublicIncident(publishableKey) {
505
+ const pk = publishableKey || DEFAULT_PUBLISHABLE_KEY;
506
+ validatePublishableKey(pk);
507
+ const response = await fetch(`${API_BASE_URL}/v1/public/projects/incident`, {
508
+ method: "GET",
509
+ headers: {
510
+ accept: "application/json",
511
+ "x-publishable-key": pk
512
+ }
513
+ });
514
+ if (!response.ok) {
515
+ throw new Error(`Failed to fetch public incident: ${response.statusText}`);
516
+ }
517
+ return response.json();
518
+ }
480
519
  async function getIpAddress() {
481
520
  const response = await fetch(`${API_BASE_URL}/v1/public/ip_address`, {
482
521
  method: "GET",
@@ -532,7 +571,7 @@ async function getExternalWallets(publishableKey) {
532
571
  const data = await response.json();
533
572
  return data;
534
573
  }
535
- async function getWalletMobileDeepLink(wallet, depositAddresses, publishableKey) {
574
+ async function getWalletMobileDeepLink(wallet, depositAddresses, publishableKey, amountUsd) {
536
575
  const pk = publishableKey || DEFAULT_PUBLISHABLE_KEY;
537
576
  validatePublishableKey(pk);
538
577
  const response = await fetch(`${API_BASE_URL}/v1/public/external_wallets/mobile_deeplink`, {
@@ -542,7 +581,11 @@ async function getWalletMobileDeepLink(wallet, depositAddresses, publishableKey)
542
581
  accept: "application/json",
543
582
  "x-publishable-key": pk
544
583
  },
545
- body: JSON.stringify({ wallet, deposit_addresses: depositAddresses })
584
+ body: JSON.stringify({
585
+ wallet,
586
+ deposit_addresses: depositAddresses,
587
+ ...amountUsd ? { amount_usd: amountUsd } : {}
588
+ })
546
589
  });
547
590
  if (!response.ok) {
548
591
  throw new Error(`Failed to generate wallet deep link: ${response.statusText}`);
@@ -587,6 +630,15 @@ async function verifyRecipientAddress(request, publishableKey) {
587
630
  body: JSON.stringify(request)
588
631
  });
589
632
  if (!response.ok) {
633
+ const body = await response.json().catch(() => null);
634
+ if (response.status === 400 && body?.error_type === "validation_error") {
635
+ const firstError = Array.isArray(body.details?.errors) ? body.details?.errors[0] : void 0;
636
+ return {
637
+ valid: false,
638
+ failure_code: "validation_error",
639
+ message: firstError ?? "Invalid recipient address"
640
+ };
641
+ }
590
642
  throw new Error(`Failed to verify recipient address: ${response.statusText}`);
591
643
  }
592
644
  return response.json();
@@ -1029,11 +1081,12 @@ async function stripeGetDefaultToken(params, publishableKey) {
1029
1081
  }
1030
1082
  var HEADLESS_STRIPE_BASE = "/v1/public/onramps/headless/stripe";
1031
1083
  var StripeApiResponseError = class extends Error {
1032
- constructor(message, statusCode, stripeCode, errorType) {
1084
+ constructor(message, statusCode, stripeCode, errorType, stripeMessage) {
1033
1085
  super(message);
1034
1086
  this.statusCode = statusCode;
1035
1087
  this.stripeCode = stripeCode;
1036
1088
  this.errorType = errorType;
1089
+ this.stripeMessage = stripeMessage;
1037
1090
  this.name = "StripeApiResponseError";
1038
1091
  }
1039
1092
  };
@@ -1044,7 +1097,8 @@ function throwStripeError(prefix, response, error) {
1044
1097
  `${prefix}: ${detailMessage}`,
1045
1098
  response.status,
1046
1099
  stripeError?.code,
1047
- error.error_type
1100
+ error.error_type,
1101
+ stripeError?.message
1048
1102
  );
1049
1103
  }
1050
1104
  async function stripeGetConfig(publishableKey) {
@@ -1536,6 +1590,825 @@ var CheckoutEventType = /* @__PURE__ */ ((CheckoutEventType2) => {
1536
1590
  return CheckoutEventType2;
1537
1591
  })(CheckoutEventType || {});
1538
1592
 
1593
+ // src/lib/emitter.ts
1594
+ var TypedEmitter = class {
1595
+ constructor() {
1596
+ __publicField(this, "handlers", /* @__PURE__ */ new Map());
1597
+ }
1598
+ on(type, handler) {
1599
+ let set = this.handlers.get(type);
1600
+ if (!set) {
1601
+ set = /* @__PURE__ */ new Set();
1602
+ this.handlers.set(type, set);
1603
+ }
1604
+ set.add(handler);
1605
+ return () => {
1606
+ set.delete(handler);
1607
+ };
1608
+ }
1609
+ emit(type, event) {
1610
+ const dispatch = (handler) => {
1611
+ try {
1612
+ handler(event);
1613
+ } catch (error) {
1614
+ console.error("[unifold] event handler threw", error);
1615
+ }
1616
+ };
1617
+ this.handlers.get(type)?.forEach(dispatch);
1618
+ this.handlers.get("*")?.forEach(dispatch);
1619
+ }
1620
+ removeAllListeners() {
1621
+ this.handlers.clear();
1622
+ }
1623
+ };
1624
+
1625
+ // src/lib/mappers.ts
1626
+ function mapWalletToDepositAddress(wallet) {
1627
+ return {
1628
+ id: wallet.id,
1629
+ chainType: wallet.chain_type,
1630
+ addressType: wallet.address_type,
1631
+ address: wallet.address,
1632
+ destinationChainType: wallet.destination_chain_type,
1633
+ destinationChainId: wallet.destination_chain_id,
1634
+ destinationTokenAddress: wallet.destination_token_address,
1635
+ recipientAddress: wallet.recipient_address,
1636
+ isPrimary: wallet.is_primary
1637
+ };
1638
+ }
1639
+ function mapDirectExecution(execution) {
1640
+ return {
1641
+ id: execution.id,
1642
+ transactionHash: execution.transaction_hash,
1643
+ recipientAddress: execution.recipient_address,
1644
+ depositAddress: execution.deposit_wallet?.address,
1645
+ sourceChainType: execution.source_chain_type,
1646
+ sourceChainId: execution.source_chain_id,
1647
+ sourceTokenAddress: execution.source_token_address,
1648
+ destinationChainType: execution.destination_chain_type,
1649
+ destinationChainId: execution.destination_chain_id,
1650
+ destinationTokenAddress: execution.destination_token_address,
1651
+ sourceAmountBaseUnit: execution.source_amount_base_unit,
1652
+ sourceAmountUsd: execution.source_amount_usd,
1653
+ destinationAmountBaseUnit: execution.destination_amount_base_unit,
1654
+ destinationAmountUsd: execution.destination_amount_usd,
1655
+ destinationTransactionHashes: execution.destination_transaction_hashes,
1656
+ status: execution.status,
1657
+ failureReason: execution.failure_reason,
1658
+ createdAt: execution.created_at,
1659
+ updatedAt: execution.updated_at,
1660
+ explorerUrl: execution.explorer_url,
1661
+ destinationExplorerUrl: execution.destination_explorer_url,
1662
+ sourceTokenMetadata: execution.source_token_metadata ? {
1663
+ iconUrl: execution.source_token_metadata.icon_url,
1664
+ iconUrls: execution.source_token_metadata.icon_urls,
1665
+ decimals: execution.source_token_metadata.decimals
1666
+ } : void 0,
1667
+ destinationTokenMetadata: execution.destination_token_metadata ? {
1668
+ iconUrl: execution.destination_token_metadata.icon_url,
1669
+ iconUrls: execution.destination_token_metadata.icon_urls,
1670
+ decimals: execution.destination_token_metadata.decimals
1671
+ } : void 0
1672
+ };
1673
+ }
1674
+
1675
+ // src/lib/deposit-session.ts
1676
+ var DETECTION_POLL_INTERVAL_MS = 2500;
1677
+ var SCAN_NUDGE_INTERVAL_MS = 5e3;
1678
+ var DETECTION_ARM_DELAY_MS = 5e3;
1679
+ var LOOKBACK_MS = 6e4;
1680
+ var ADDRESS_CREATE_MAX_ATTEMPTS = 4;
1681
+ var DepositSessionEventType = /* @__PURE__ */ ((DepositSessionEventType2) => {
1682
+ DepositSessionEventType2["SESSION_STARTED"] = "deposit_session.started";
1683
+ DepositSessionEventType2["ADDRESSES_CREATED"] = "deposit_session.addresses_created";
1684
+ DepositSessionEventType2["CONFIRMATION_STARTED"] = "deposit_session.confirmation_started";
1685
+ DepositSessionEventType2["SESSION_STOPPED"] = "deposit_session.stopped";
1686
+ DepositSessionEventType2["SESSION_ERRORED"] = "deposit_session.errored";
1687
+ DepositSessionEventType2["EXECUTION_DETECTED"] = "direct_execution.detected";
1688
+ DepositSessionEventType2["EXECUTION_UPDATED"] = "direct_execution.updated";
1689
+ DepositSessionEventType2["EXECUTION_SUCCEEDED"] = "direct_execution.succeeded";
1690
+ DepositSessionEventType2["EXECUTION_FAILED"] = "direct_execution.failed";
1691
+ return DepositSessionEventType2;
1692
+ })(DepositSessionEventType || {});
1693
+ var IN_PROGRESS_STATUSES = [
1694
+ "pending" /* PENDING */,
1695
+ "waiting" /* WAITING */,
1696
+ "delayed" /* DELAYED */
1697
+ ];
1698
+ var FAILURE_STATUSES = ["failed" /* FAILED */, "refunded" /* REFUNDED */];
1699
+ var DepositSessionWaitError = class extends Error {
1700
+ constructor(code, message, cause) {
1701
+ super(message);
1702
+ __publicField(this, "code");
1703
+ /** Failed execution (`DEPOSIT_FAILED`) or fatal {@link DepositSessionError} (`SESSION_ERROR`). */
1704
+ __publicField(this, "cause");
1705
+ this.name = "DepositSessionWaitError";
1706
+ this.code = code;
1707
+ this.cause = cause;
1708
+ }
1709
+ };
1710
+ var SessionCheckError = class extends Error {
1711
+ constructor(code, message) {
1712
+ super(message);
1713
+ this.code = code;
1714
+ }
1715
+ };
1716
+ var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1717
+ var DepositSession = class {
1718
+ constructor(config) {
1719
+ /** Immutable id for correlation, `dsess_<ksuid>`. Client-generated. */
1720
+ __publicField(this, "id");
1721
+ __publicField(this, "emitter", new TypedEmitter());
1722
+ __publicField(this, "listeners", /* @__PURE__ */ new Set());
1723
+ __publicField(this, "publishableKey");
1724
+ __publicField(this, "externalUserId");
1725
+ __publicField(this, "destination");
1726
+ __publicField(this, "confirmationMode");
1727
+ __publicField(this, "method");
1728
+ // Run state. runToken invalidates in-flight async work across stop()/restart.
1729
+ __publicField(this, "runToken", 0);
1730
+ __publicField(this, "startPromise", null);
1731
+ __publicField(this, "destroyed", false);
1732
+ /** Pending waiter rejections, invoked by destroy() so waiters never hang. */
1733
+ __publicField(this, "waiterDestroyCallbacks", /* @__PURE__ */ new Set());
1734
+ __publicField(this, "baselineMs", 0);
1735
+ __publicField(this, "tracked", /* @__PURE__ */ new Map());
1736
+ __publicField(this, "pollErrorLatched", false);
1737
+ __publicField(this, "pollInFlight", false);
1738
+ /** First execution to succeed this run — waitForSuccess's one-shot answer. */
1739
+ __publicField(this, "firstSuccess", null);
1740
+ __publicField(this, "detectionTimer", null);
1741
+ __publicField(this, "nudgeTimer", null);
1742
+ __publicField(this, "armTimer", null);
1743
+ // Snapshot state
1744
+ __publicField(this, "status", "idle");
1745
+ __publicField(this, "addresses", []);
1746
+ __publicField(this, "addressIds", []);
1747
+ __publicField(this, "executions", []);
1748
+ __publicField(this, "checkingDeposit", false);
1749
+ __publicField(this, "error", null);
1750
+ __publicField(this, "snapshot");
1751
+ if (!config.publishableKey || config.publishableKey.trim() === "") {
1752
+ throw new Error("DepositSession: publishableKey is required");
1753
+ }
1754
+ if (!config.externalUserId) {
1755
+ throw new Error("DepositSession: externalUserId is required");
1756
+ }
1757
+ this.id = generatePrefixedKSUID("dsess");
1758
+ this.publishableKey = config.publishableKey;
1759
+ this.externalUserId = config.externalUserId;
1760
+ this.destination = config.destination;
1761
+ this.confirmationMode = config.confirmationMode ?? "auto";
1762
+ this.method = config.method ?? "transfer";
1763
+ this.snapshot = this.buildSnapshot();
1764
+ }
1765
+ // -- Public surface -------------------------------------------------------
1766
+ /** Synchronous snapshot; the reference is stable until state changes. */
1767
+ getSnapshot() {
1768
+ return this.snapshot;
1769
+ }
1770
+ /**
1771
+ * Subscribe to snapshot changes (external-store contract; drives
1772
+ * `useSyncExternalStore` in the React binding). Returns an unsubscribe fn.
1773
+ */
1774
+ subscribe(listener) {
1775
+ this.listeners.add(listener);
1776
+ return () => {
1777
+ this.listeners.delete(listener);
1778
+ };
1779
+ }
1780
+ on(type, handler) {
1781
+ return this.emitter.on(type, handler);
1782
+ }
1783
+ /**
1784
+ * Creates/fetches addresses (with a fail-fast recipient check) and starts
1785
+ * detection polling. Idempotent while running; callable again after stop()
1786
+ * or a fatal error (fresh baseline).
1787
+ */
1788
+ start() {
1789
+ if (this.destroyed) {
1790
+ return Promise.reject(new Error("DepositSession has been destroyed"));
1791
+ }
1792
+ if (this.startPromise) return this.startPromise;
1793
+ this.startPromise = this.run();
1794
+ return this.startPromise;
1795
+ }
1796
+ /** Arms the backend scan nudge in 'manual' mode. No-op if already armed. */
1797
+ confirmFundsSent() {
1798
+ if (this.destroyed || !this.startPromise) return;
1799
+ this.armConfirmation("manual");
1800
+ }
1801
+ /**
1802
+ * Stops all polling. The session can be restarted with start(), which
1803
+ * resets the baseline and tracked executions (fresh run).
1804
+ */
1805
+ stop() {
1806
+ const wasActive = this.startPromise !== null;
1807
+ this.runToken += 1;
1808
+ this.clearTimers();
1809
+ this.startPromise = null;
1810
+ this.checkingDeposit = false;
1811
+ if (this.status !== "idle" && this.status !== "error") {
1812
+ this.setStatus("idle");
1813
+ }
1814
+ if (wasActive && !this.destroyed) {
1815
+ this.commit();
1816
+ this.emitSessionEvent("deposit_session.stopped" /* SESSION_STOPPED */, {
1817
+ sessionId: this.id
1818
+ });
1819
+ this.notify();
1820
+ }
1821
+ }
1822
+ /** stop() + release all listeners. Terminal — start() rejects afterwards. */
1823
+ destroy() {
1824
+ this.stop();
1825
+ this.destroyed = true;
1826
+ Array.from(this.waiterDestroyCallbacks).forEach((callback) => callback());
1827
+ this.waiterDestroyCallbacks.clear();
1828
+ this.emitter.removeAllListeners();
1829
+ this.listeners.clear();
1830
+ }
1831
+ // -- Promise waiters (subscription sugar over the event stream) ------------
1832
+ /**
1833
+ * Resolve when the session reaches one of the given statuses (immediately
1834
+ * if it's already there). Generic primitive over the lifecycle state
1835
+ * machine — e.g. `waitForStatus('processing')` awaits detection of live
1836
+ * activity, `waitForStatus('ready')` awaits readiness. Statuses
1837
+ * carry no outcomes; await those with {@link waitForSuccess} or the
1838
+ * `direct_execution.*` events.
1839
+ *
1840
+ * Rejects with {@link DepositSessionWaitError} on abort or destroy().
1841
+ * Does not start or stop the session — it only listens.
1842
+ */
1843
+ waitForStatus(status, options = {}) {
1844
+ const statuses = Array.isArray(status) ? status : [status];
1845
+ return new Promise((resolve, reject) => {
1846
+ this.installWaiter({
1847
+ options,
1848
+ reject,
1849
+ subscribe: (settle) => {
1850
+ const check = () => {
1851
+ if (statuses.includes(this.snapshot.status)) settle(() => resolve(this.snapshot));
1852
+ };
1853
+ check();
1854
+ return this.subscribe(check);
1855
+ }
1856
+ });
1857
+ });
1858
+ }
1859
+ /**
1860
+ * Resolve with the **first** succeeded {@link DirectExecution} observed by
1861
+ * this session — the one-liner for the 90% case. Mirrors `beginDeposit()`'s
1862
+ * promise contract: resolve on success, reject on failure.
1863
+ *
1864
+ * Multi-execution semantics (unlike quote-scoped models such as Privy's,
1865
+ * one session can observe many executions — a user may send twice, or on
1866
+ * two chains): this waiter is one-shot "first completion" detection. If an
1867
+ * execution has already succeeded this run, it resolves immediately with
1868
+ * the FIRST one that did (not the newest). The session keeps polling after
1869
+ * success — to react to every settlement, subscribe to
1870
+ * `direct_execution.succeeded` events or read `snapshot.executions`.
1871
+ *
1872
+ * Rejects with {@link DepositSessionWaitError}:
1873
+ * - `DEPOSIT_FAILED` (cause: the failed execution) when a deposit fails and
1874
+ * NO other observed execution is still in flight — a failure while
1875
+ * another deposit is pending keeps waiting (that one may still succeed),
1876
+ * - `SESSION_ERROR` (cause: the fatal {@link DepositSessionError}) on fatal
1877
+ * session errors (e.g. address creation failed),
1878
+ * - `ABORTED` / `DESTROYED` per the wait options and session lifecycle.
1879
+ */
1880
+ waitForSuccess(options = {}) {
1881
+ return new Promise((resolve, reject) => {
1882
+ this.installWaiter({
1883
+ options,
1884
+ reject,
1885
+ subscribe: (settle) => {
1886
+ const rejectFailure = (failed) => settle(
1887
+ () => reject(new DepositSessionWaitError("DEPOSIT_FAILED", "Deposit failed", failed))
1888
+ );
1889
+ const { executions, error } = this.snapshot;
1890
+ if (this.firstSuccess) {
1891
+ const first = this.firstSuccess;
1892
+ settle(() => resolve(first));
1893
+ return () => {
1894
+ };
1895
+ }
1896
+ const alreadyFailed = executions.find(
1897
+ (execution) => FAILURE_STATUSES.includes(execution.status)
1898
+ );
1899
+ if (alreadyFailed && !this.anyExecutionInFlight()) {
1900
+ rejectFailure(alreadyFailed);
1901
+ return () => {
1902
+ };
1903
+ }
1904
+ if (error?.fatal) {
1905
+ settle(
1906
+ () => reject(new DepositSessionWaitError("SESSION_ERROR", error.message, error))
1907
+ );
1908
+ return () => {
1909
+ };
1910
+ }
1911
+ const offs = [
1912
+ this.on(
1913
+ "direct_execution.succeeded" /* EXECUTION_SUCCEEDED */,
1914
+ (event) => settle(() => resolve(event.data.object))
1915
+ ),
1916
+ this.on("direct_execution.failed" /* EXECUTION_FAILED */, (event) => {
1917
+ if (this.anyExecutionInFlight()) return;
1918
+ rejectFailure(event.data.object);
1919
+ }),
1920
+ // A previously-failed wait condition can become settleable when
1921
+ // the last in-flight execution also fails (updated → failed is
1922
+ // covered above; updated → refunded transitions re-check here).
1923
+ this.on("direct_execution.updated" /* EXECUTION_UPDATED */, () => {
1924
+ if (this.anyExecutionInFlight() || this.firstSuccess) return;
1925
+ const failed = this.snapshot.executions.find(
1926
+ (execution) => FAILURE_STATUSES.includes(execution.status)
1927
+ );
1928
+ if (failed) rejectFailure(failed);
1929
+ }),
1930
+ this.on("deposit_session.errored" /* SESSION_ERRORED */, (event) => {
1931
+ if (!event.data.object.fatal) return;
1932
+ settle(
1933
+ () => reject(
1934
+ new DepositSessionWaitError(
1935
+ "SESSION_ERROR",
1936
+ event.data.object.message,
1937
+ event.data.object
1938
+ )
1939
+ )
1940
+ );
1941
+ })
1942
+ ];
1943
+ return () => offs.forEach((off) => off());
1944
+ }
1945
+ });
1946
+ });
1947
+ }
1948
+ /**
1949
+ * Shared waiter plumbing: AbortSignal and destroy() rejection, with
1950
+ * single-settlement and cleanup. `subscribe` installs the wait condition
1951
+ * and returns its unsubscribe fn; it settles via `settle(fn)`.
1952
+ */
1953
+ installWaiter({
1954
+ options,
1955
+ reject,
1956
+ subscribe
1957
+ }) {
1958
+ if (this.destroyed) {
1959
+ reject(new DepositSessionWaitError("DESTROYED", "DepositSession has been destroyed"));
1960
+ return;
1961
+ }
1962
+ if (options.signal?.aborted) {
1963
+ reject(new DepositSessionWaitError("ABORTED", "Wait aborted", options.signal.reason));
1964
+ return;
1965
+ }
1966
+ let settled = false;
1967
+ let unsubscribe = null;
1968
+ const cleanup = () => {
1969
+ unsubscribe?.();
1970
+ options.signal?.removeEventListener("abort", onAbort);
1971
+ this.waiterDestroyCallbacks.delete(onDestroy);
1972
+ };
1973
+ const settle = (finish) => {
1974
+ if (settled) return;
1975
+ settled = true;
1976
+ cleanup();
1977
+ finish();
1978
+ };
1979
+ const onAbort = () => settle(
1980
+ () => reject(new DepositSessionWaitError("ABORTED", "Wait aborted", options.signal?.reason))
1981
+ );
1982
+ const onDestroy = () => settle(
1983
+ () => reject(
1984
+ new DepositSessionWaitError("DESTROYED", "DepositSession was destroyed while waiting")
1985
+ )
1986
+ );
1987
+ this.waiterDestroyCallbacks.add(onDestroy);
1988
+ options.signal?.addEventListener("abort", onAbort, { once: true });
1989
+ unsubscribe = subscribe(settle);
1990
+ if (settled) cleanup();
1991
+ }
1992
+ // -- Run lifecycle ---------------------------------------------------------
1993
+ async run() {
1994
+ const token = ++this.runToken;
1995
+ this.baselineMs = Date.now();
1996
+ this.tracked.clear();
1997
+ this.pollErrorLatched = false;
1998
+ this.executions = [];
1999
+ this.firstSuccess = null;
2000
+ this.error = null;
2001
+ this.checkingDeposit = false;
2002
+ this.addresses = [];
2003
+ this.addressIds = [];
2004
+ this.setStatus("creating_addresses");
2005
+ this.commit();
2006
+ this.emitSessionEvent("deposit_session.started" /* SESSION_STARTED */, {
2007
+ sessionId: this.id
2008
+ });
2009
+ this.notify();
2010
+ let wallets;
2011
+ try {
2012
+ [wallets] = await Promise.all([this.createAddressesWithRetry(token), this.runStartChecks()]);
2013
+ } catch (cause) {
2014
+ if (token !== this.runToken) return;
2015
+ const isCheck = cause instanceof SessionCheckError;
2016
+ this.failFatally(
2017
+ isCheck ? cause.code : "ADDRESS_CREATION_FAILED",
2018
+ isCheck ? cause.message : "Failed to create deposit addresses",
2019
+ cause
2020
+ );
2021
+ return;
2022
+ }
2023
+ if (token !== this.runToken) return;
2024
+ this.addresses = wallets.map(mapWalletToDepositAddress);
2025
+ this.addressIds = wallets.map((w) => w.id).filter(Boolean);
2026
+ this.setStatus("ready");
2027
+ this.commit();
2028
+ this.emitSessionEvent("deposit_session.addresses_created" /* ADDRESSES_CREATED */, {
2029
+ sessionId: this.id,
2030
+ addresses: this.addresses
2031
+ });
2032
+ this.notify();
2033
+ if (token !== this.runToken) return;
2034
+ this.startDetectionLoop(token);
2035
+ if (this.confirmationMode === "auto") {
2036
+ this.armTimer = setTimeout(() => {
2037
+ if (token === this.runToken) this.armConfirmation("auto");
2038
+ }, DETECTION_ARM_DELAY_MS);
2039
+ }
2040
+ }
2041
+ failFatally(code, message, cause) {
2042
+ this.clearTimers();
2043
+ this.startPromise = null;
2044
+ this.error = { code, message, fatal: true, cause };
2045
+ this.setStatus("error");
2046
+ this.commit();
2047
+ this.emitSessionEvent("deposit_session.errored" /* SESSION_ERRORED */, {
2048
+ sessionId: this.id,
2049
+ code,
2050
+ message,
2051
+ fatal: true
2052
+ });
2053
+ this.notify();
2054
+ }
2055
+ async createAddressesWithRetry(token) {
2056
+ let lastError;
2057
+ for (let attempt = 0; attempt < ADDRESS_CREATE_MAX_ATTEMPTS; attempt++) {
2058
+ if (attempt > 0) {
2059
+ await delay(Math.min(1e3 * 2 ** (attempt - 1), 1e4));
2060
+ if (token !== this.runToken) throw new Error("DepositSession stopped");
2061
+ }
2062
+ try {
2063
+ const response = await createDepositAddress(
2064
+ {
2065
+ external_user_id: this.externalUserId,
2066
+ destination_chain_type: this.destination.chainType,
2067
+ destination_chain_id: this.destination.chainId,
2068
+ destination_token_address: this.destination.tokenAddress,
2069
+ recipient_address: this.destination.recipientAddress,
2070
+ contract_calls: this.destination.contractCalls
2071
+ },
2072
+ this.publishableKey
2073
+ );
2074
+ return response.data;
2075
+ } catch (error) {
2076
+ lastError = error;
2077
+ }
2078
+ }
2079
+ throw lastError;
2080
+ }
2081
+ /**
2082
+ * Fail-fast recipient validation (e.g. Algorand asset opt-in). Fails open
2083
+ * on network errors — the backend still enforces at execution time — but a
2084
+ * definitive negative result is fatal.
2085
+ *
2086
+ * Deliberately NOT IP/geo-aware: generating deposit addresses headless
2087
+ * carries no region gate. Hosts that want the modal's geo behavior render
2088
+ * against the opt-in `useAllowedCountry` hook instead.
2089
+ */
2090
+ async runStartChecks() {
2091
+ const recipientValid = await verifyRecipientAddress(
2092
+ {
2093
+ chain_type: this.destination.chainType,
2094
+ chain_id: this.destination.chainId,
2095
+ token_address: this.destination.tokenAddress,
2096
+ recipient_address: this.destination.recipientAddress
2097
+ },
2098
+ this.publishableKey
2099
+ ).then((result) => result.valid).catch(() => null);
2100
+ if (recipientValid === false) {
2101
+ throw new SessionCheckError(
2102
+ "INVALID_RECIPIENT",
2103
+ "Recipient address cannot receive funds for this destination"
2104
+ );
2105
+ }
2106
+ }
2107
+ // -- Detection polling (port of useDepositPolling Effect 2) ----------------
2108
+ startDetectionLoop(token) {
2109
+ const poll = () => {
2110
+ if (token !== this.runToken) {
2111
+ if (this.detectionTimer) {
2112
+ clearInterval(this.detectionTimer);
2113
+ this.detectionTimer = null;
2114
+ }
2115
+ return;
2116
+ }
2117
+ void this.pollExecutions(token);
2118
+ };
2119
+ poll();
2120
+ this.detectionTimer = setInterval(poll, DETECTION_POLL_INTERVAL_MS);
2121
+ }
2122
+ async pollExecutions(token) {
2123
+ if (this.pollInFlight) return;
2124
+ this.pollInFlight = true;
2125
+ try {
2126
+ await this.pollExecutionsOnce(token);
2127
+ } finally {
2128
+ this.pollInFlight = false;
2129
+ }
2130
+ }
2131
+ async pollExecutionsOnce(token) {
2132
+ try {
2133
+ const response = await queryExecutions(
2134
+ this.externalUserId,
2135
+ this.publishableKey,
2136
+ "deposit" /* Deposit */
2137
+ );
2138
+ if (token !== this.runToken) return;
2139
+ if (this.pollErrorLatched) {
2140
+ this.pollErrorLatched = false;
2141
+ if (this.error && !this.error.fatal) {
2142
+ this.error = null;
2143
+ this.commit();
2144
+ this.notify();
2145
+ }
2146
+ }
2147
+ const cutoffMs = this.baselineMs - LOOKBACK_MS;
2148
+ const sorted = [...response.data].sort((a, b) => {
2149
+ const timeA = a.created_at ? new Date(a.created_at).getTime() : 0;
2150
+ const timeB = b.created_at ? new Date(b.created_at).getTime() : 0;
2151
+ return timeB - timeA;
2152
+ });
2153
+ let candidate = null;
2154
+ for (const execution of sorted) {
2155
+ const createdMs = execution.created_at ? new Date(execution.created_at).getTime() : NaN;
2156
+ if (!Number.isFinite(createdMs) || createdMs < cutoffMs) continue;
2157
+ const trackedStatus = this.tracked.get(execution.id);
2158
+ const isTerminal = execution.status === "succeeded" /* SUCCEEDED */ || FAILURE_STATUSES.includes(execution.status);
2159
+ if (trackedStatus === void 0 && createdMs < this.baselineMs && isTerminal) {
2160
+ continue;
2161
+ }
2162
+ if (trackedStatus === void 0 || trackedStatus !== execution.status) {
2163
+ candidate = execution;
2164
+ break;
2165
+ }
2166
+ }
2167
+ if (!candidate) return;
2168
+ this.processExecutionChange(candidate);
2169
+ } catch (error) {
2170
+ if (token !== this.runToken) return;
2171
+ console.error("[unifold] failed to fetch executions:", error);
2172
+ if (!this.pollErrorLatched) {
2173
+ this.pollErrorLatched = true;
2174
+ this.error = {
2175
+ code: "POLLING_ERROR",
2176
+ message: "Failed to fetch deposit status",
2177
+ fatal: false,
2178
+ cause: error
2179
+ };
2180
+ this.commit();
2181
+ this.emitSessionEvent("deposit_session.errored" /* SESSION_ERRORED */, {
2182
+ sessionId: this.id,
2183
+ code: "POLLING_ERROR",
2184
+ message: "Failed to fetch deposit status",
2185
+ fatal: false
2186
+ });
2187
+ this.notify();
2188
+ }
2189
+ }
2190
+ }
2191
+ processExecutionChange(wire) {
2192
+ const previousStatus = this.tracked.get(wire.id) ?? null;
2193
+ this.tracked.set(wire.id, wire.status);
2194
+ const execution = mapDirectExecution(wire);
2195
+ const existingIndex = this.executions.findIndex((e) => e.id === execution.id);
2196
+ if (existingIndex >= 0) {
2197
+ this.executions = this.executions.map((e, i) => i === existingIndex ? execution : e);
2198
+ } else {
2199
+ this.executions = [...this.executions, execution].sort((a, b) => {
2200
+ const timeA = a.createdAt ? new Date(a.createdAt).getTime() : 0;
2201
+ const timeB = b.createdAt ? new Date(b.createdAt).getTime() : 0;
2202
+ return timeB - timeA;
2203
+ });
2204
+ }
2205
+ this.setStatus(this.anyExecutionInFlight() ? "processing" : "ready");
2206
+ const wasInProgressOrNew = previousStatus === null || IN_PROGRESS_STATUSES.includes(previousStatus);
2207
+ if (wire.status === "succeeded" /* SUCCEEDED */ && wasInProgressOrNew && !this.firstSuccess) {
2208
+ this.firstSuccess = execution;
2209
+ }
2210
+ this.commit();
2211
+ const eventCreated = this.executionEventTimestamp(wire);
2212
+ if (previousStatus === null) {
2213
+ this.emitExecutionEvent("direct_execution.detected" /* EXECUTION_DETECTED */, execution, eventCreated);
2214
+ } else {
2215
+ this.emitExecutionEvent(
2216
+ "direct_execution.updated" /* EXECUTION_UPDATED */,
2217
+ { ...execution, previousStatus },
2218
+ eventCreated
2219
+ );
2220
+ }
2221
+ if (wire.status === "succeeded" /* SUCCEEDED */ && wasInProgressOrNew) {
2222
+ this.emitExecutionEvent("direct_execution.succeeded" /* EXECUTION_SUCCEEDED */, execution, eventCreated);
2223
+ } else if (FAILURE_STATUSES.includes(wire.status) && (previousStatus === null || !FAILURE_STATUSES.includes(previousStatus))) {
2224
+ this.emitExecutionEvent("direct_execution.failed" /* EXECUTION_FAILED */, execution, eventCreated);
2225
+ }
2226
+ this.notify();
2227
+ }
2228
+ // -- Scan nudge (port of useDepositPolling Effects 1 + 3) ------------------
2229
+ armConfirmation(trigger) {
2230
+ if (this.checkingDeposit || this.destroyed) return;
2231
+ if (this.addressIds.length === 0) return;
2232
+ if (this.armTimer) {
2233
+ clearTimeout(this.armTimer);
2234
+ this.armTimer = null;
2235
+ }
2236
+ this.checkingDeposit = true;
2237
+ this.commit();
2238
+ this.emitSessionEvent("deposit_session.confirmation_started" /* CONFIRMATION_STARTED */, {
2239
+ sessionId: this.id,
2240
+ trigger
2241
+ });
2242
+ this.notify();
2243
+ const token = this.runToken;
2244
+ const nudge = () => {
2245
+ if (token !== this.runToken) return;
2246
+ void Promise.all(
2247
+ this.addressIds.map(
2248
+ (id) => pollDirectExecutions({ deposit_wallet_id: id }, this.publishableKey).catch(() => {
2249
+ })
2250
+ )
2251
+ );
2252
+ };
2253
+ nudge();
2254
+ this.nudgeTimer = setInterval(nudge, SCAN_NUDGE_INTERVAL_MS);
2255
+ }
2256
+ // -- Internals --------------------------------------------------------------
2257
+ anyExecutionInFlight() {
2258
+ return Array.from(this.tracked.values()).some(
2259
+ (status) => IN_PROGRESS_STATUSES.includes(status)
2260
+ );
2261
+ }
2262
+ setStatus(status) {
2263
+ this.status = status;
2264
+ }
2265
+ clearTimers() {
2266
+ if (this.detectionTimer) {
2267
+ clearInterval(this.detectionTimer);
2268
+ this.detectionTimer = null;
2269
+ }
2270
+ if (this.nudgeTimer) {
2271
+ clearInterval(this.nudgeTimer);
2272
+ this.nudgeTimer = null;
2273
+ }
2274
+ if (this.armTimer) {
2275
+ clearTimeout(this.armTimer);
2276
+ this.armTimer = null;
2277
+ }
2278
+ }
2279
+ buildSnapshot() {
2280
+ return {
2281
+ status: this.status,
2282
+ addresses: this.addresses,
2283
+ executions: this.executions,
2284
+ latestExecution: this.executions[0] ?? null,
2285
+ isCheckingDeposit: this.checkingDeposit,
2286
+ error: this.error
2287
+ };
2288
+ }
2289
+ /** Rebuild the snapshot so getSnapshot() reflects current state. */
2290
+ commit() {
2291
+ this.snapshot = this.buildSnapshot();
2292
+ }
2293
+ notify() {
2294
+ this.listeners.forEach((listener) => {
2295
+ try {
2296
+ listener();
2297
+ } catch (error) {
2298
+ console.error("[unifold] snapshot listener threw", error);
2299
+ }
2300
+ });
2301
+ }
2302
+ executionEventTimestamp(wire) {
2303
+ if (wire.updated_at) return Math.floor(new Date(wire.updated_at).getTime() / 1e3);
2304
+ if (wire.created_at) return Math.floor(new Date(wire.created_at).getTime() / 1e3);
2305
+ return Math.floor(Date.now() / 1e3);
2306
+ }
2307
+ emitSessionEvent(type, object) {
2308
+ this.emitter.emit(type, {
2309
+ id: generatePrefixedKSUID("sevt"),
2310
+ type,
2311
+ created: Math.floor(Date.now() / 1e3),
2312
+ method: this.method,
2313
+ data: { object }
2314
+ });
2315
+ }
2316
+ emitExecutionEvent(type, object, created) {
2317
+ this.emitter.emit(type, {
2318
+ id: generatePrefixedKSUID("sevt"),
2319
+ type,
2320
+ created,
2321
+ method: this.method,
2322
+ data: { object }
2323
+ });
2324
+ }
2325
+ };
2326
+
2327
+ // src/lib/client.ts
2328
+ var UnifoldClient = class {
2329
+ constructor(options) {
2330
+ __publicField(this, "publishableKey");
2331
+ const { publishableKey } = options;
2332
+ if (!publishableKey || publishableKey.trim() === "") {
2333
+ throw new Error("Unifold: publishableKey is required");
2334
+ }
2335
+ if (!publishableKey.startsWith("pk_test_") && !publishableKey.startsWith("pk_live_")) {
2336
+ console.warn('Unifold: publishableKey should start with "pk_test_" or "pk_live_".');
2337
+ }
2338
+ this.publishableKey = publishableKey;
2339
+ }
2340
+ /** Create a headless deposit-session flow controller. */
2341
+ createDepositSession(params) {
2342
+ return new DepositSession({ ...params, publishableKey: this.publishableKey });
2343
+ }
2344
+ /**
2345
+ * Create (idempotently) and return the user's deposit addresses for a
2346
+ * destination — `POST /v1/public/deposit_addresses`.
2347
+ */
2348
+ async getDepositAddresses(params) {
2349
+ const response = await createDepositAddress(
2350
+ {
2351
+ external_user_id: params.externalUserId,
2352
+ destination_chain_type: params.destination.chainType,
2353
+ destination_chain_id: params.destination.chainId,
2354
+ destination_token_address: params.destination.tokenAddress,
2355
+ recipient_address: params.destination.recipientAddress,
2356
+ contract_calls: params.destination.contractCalls
2357
+ },
2358
+ this.publishableKey
2359
+ );
2360
+ return response.data.map(mapWalletToDepositAddress);
2361
+ }
2362
+ /** List the user's executions — `POST /v1/public/direct_executions/query`. */
2363
+ async listExecutions(params) {
2364
+ const response = await queryExecutions(
2365
+ params.externalUserId,
2366
+ this.publishableKey,
2367
+ params.actionType ?? "deposit" /* Deposit */
2368
+ );
2369
+ return response.data.map(mapDirectExecution);
2370
+ }
2371
+ /** Source tokens/chains a user can deposit from for a destination. */
2372
+ async getSupportedDepositTokens(params) {
2373
+ const response = await getSupportedDepositTokens(
2374
+ this.publishableKey,
2375
+ params?.destination || params?.productType ? {
2376
+ ...params.destination ? {
2377
+ destination_chain_type: params.destination.chainType,
2378
+ destination_chain_id: params.destination.chainId,
2379
+ destination_token_address: params.destination.tokenAddress
2380
+ } : {},
2381
+ ...params.productType ? { product_type: params.productType } : {}
2382
+ } : void 0
2383
+ );
2384
+ return response.data;
2385
+ }
2386
+ /** Validate a recipient address for a destination (e.g. Algorand opt-in). */
2387
+ async verifyAddress(params) {
2388
+ const response = await verifyRecipientAddress(
2389
+ {
2390
+ chain_type: params.chainType,
2391
+ chain_id: params.chainId,
2392
+ token_address: params.tokenAddress,
2393
+ recipient_address: params.recipientAddress
2394
+ },
2395
+ this.publishableKey
2396
+ );
2397
+ return {
2398
+ valid: response.valid,
2399
+ failureCode: response.failure_code ?? null,
2400
+ metadata: response.metadata ?? null
2401
+ };
2402
+ }
2403
+ /** Project-level configuration (feature flags, blocked countries, ...). */
2404
+ getProjectConfig(options) {
2405
+ return getProjectConfig(this.publishableKey, options);
2406
+ }
2407
+ };
2408
+ function createUnifoldClient(options) {
2409
+ return new UnifoldClient(options);
2410
+ }
2411
+
1539
2412
  // src/hooks/use-user-ip.ts
1540
2413
  import { useQuery } from "@tanstack/react-query";
1541
2414
  function useUserIp() {
@@ -1670,12 +2543,21 @@ var i18n = en_default;
1670
2543
  export {
1671
2544
  ActionType,
1672
2545
  CheckoutEventType,
2546
+ DETECTION_ARM_DELAY_MS,
2547
+ DETECTION_POLL_INTERVAL_MS,
2548
+ DepositAddressValidationError,
1673
2549
  DepositEventType,
2550
+ DepositSession,
2551
+ DepositSessionEventType,
2552
+ DepositSessionWaitError,
1674
2553
  ExecutionStatus,
1675
2554
  IneligibilityReason,
1676
2555
  IntegrationProvider,
2556
+ LOOKBACK_MS,
2557
+ SCAN_NUDGE_INTERVAL_MS,
1677
2558
  SOLANA_USDC_ADDRESS,
1678
2559
  StripeApiResponseError,
2560
+ UnifoldClient,
1679
2561
  WithdrawEventType,
1680
2562
  authenticateIntegrationOAuth,
1681
2563
  buildHypercoreTransaction,
@@ -1689,6 +2571,7 @@ export {
1689
2571
  createIntegrationTransfer,
1690
2572
  createOnrampSession,
1691
2573
  createOnrampVerificationSession,
2574
+ createUnifoldClient,
1692
2575
  exchangeOnrampVerificationToken,
1693
2576
  formatStablecoinAmount,
1694
2577
  generateKSUID,
@@ -1723,6 +2606,7 @@ export {
1723
2606
  getOnrampVerificationSession,
1724
2607
  getPreferredIconUrl,
1725
2608
  getProjectConfig,
2609
+ getPublicIncident,
1726
2610
  getSupportedDepositTokens,
1727
2611
  getSupportedDestinationTokens,
1728
2612
  getTokenChains,
@@ -1731,7 +2615,10 @@ export {
1731
2615
  getWalletMobileDeepLink,
1732
2616
  i18n,
1733
2617
  isApplePayLimitReached,
2618
+ isDepositAddressValidationError,
1734
2619
  listPaymentIntentExecutions,
2620
+ mapDirectExecution,
2621
+ mapWalletToDepositAddress,
1735
2622
  pollDirectExecutions,
1736
2623
  queryExecutions,
1737
2624
  refreshIntegrationToken,