@unifold/core 0.1.68 → 0.1.70-beta.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/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();
@@ -291,6 +312,27 @@ async function getFiatCurrencies(publishableKey) {
291
312
  }
292
313
  return response.json();
293
314
  }
315
+ async function getFiatExchangeRates(options = {}, publishableKey) {
316
+ const pk = publishableKey || DEFAULT_PUBLISHABLE_KEY;
317
+ validatePublishableKey(pk);
318
+ const params = new URLSearchParams();
319
+ if (options.currencies && options.currencies.length > 0) {
320
+ params.set("currencies", options.currencies.join(","));
321
+ }
322
+ const queryString = params.toString();
323
+ const url = `${API_BASE_URL}/v1/public/exchange_rates/fiat_currencies${queryString ? `?${queryString}` : ""}`;
324
+ const response = await fetch(url, {
325
+ method: "GET",
326
+ headers: {
327
+ accept: "application/json",
328
+ "x-publishable-key": pk
329
+ }
330
+ });
331
+ if (!response.ok) {
332
+ throw new Error(`Failed to fetch fiat exchange rates: ${response.statusText}`);
333
+ }
334
+ return response.json();
335
+ }
294
336
  async function getOnrampQuotes(request, publishableKey) {
295
337
  const pk = publishableKey || DEFAULT_PUBLISHABLE_KEY;
296
338
  validatePublishableKey(pk);
@@ -456,6 +498,21 @@ async function getProjectConfig(publishableKey, options) {
456
498
  const data = await response.json();
457
499
  return data;
458
500
  }
501
+ async function getPublicIncident(publishableKey) {
502
+ const pk = publishableKey || DEFAULT_PUBLISHABLE_KEY;
503
+ validatePublishableKey(pk);
504
+ const response = await fetch(`${API_BASE_URL}/v1/public/projects/incident`, {
505
+ method: "GET",
506
+ headers: {
507
+ accept: "application/json",
508
+ "x-publishable-key": pk
509
+ }
510
+ });
511
+ if (!response.ok) {
512
+ throw new Error(`Failed to fetch public incident: ${response.statusText}`);
513
+ }
514
+ return response.json();
515
+ }
459
516
  async function getIpAddress() {
460
517
  const response = await fetch(`${API_BASE_URL}/v1/public/ip_address`, {
461
518
  method: "GET",
@@ -511,7 +568,7 @@ async function getExternalWallets(publishableKey) {
511
568
  const data = await response.json();
512
569
  return data;
513
570
  }
514
- async function getWalletMobileDeepLink(wallet, depositAddresses, publishableKey) {
571
+ async function getWalletMobileDeepLink(wallet, depositAddresses, publishableKey, amountUsd) {
515
572
  const pk = publishableKey || DEFAULT_PUBLISHABLE_KEY;
516
573
  validatePublishableKey(pk);
517
574
  const response = await fetch(`${API_BASE_URL}/v1/public/external_wallets/mobile_deeplink`, {
@@ -521,7 +578,11 @@ async function getWalletMobileDeepLink(wallet, depositAddresses, publishableKey)
521
578
  accept: "application/json",
522
579
  "x-publishable-key": pk
523
580
  },
524
- body: JSON.stringify({ wallet, deposit_addresses: depositAddresses })
581
+ body: JSON.stringify({
582
+ wallet,
583
+ deposit_addresses: depositAddresses,
584
+ ...amountUsd ? { amount_usd: amountUsd } : {}
585
+ })
525
586
  });
526
587
  if (!response.ok) {
527
588
  throw new Error(`Failed to generate wallet deep link: ${response.statusText}`);
@@ -566,6 +627,15 @@ async function verifyRecipientAddress(request, publishableKey) {
566
627
  body: JSON.stringify(request)
567
628
  });
568
629
  if (!response.ok) {
630
+ const body = await response.json().catch(() => null);
631
+ if (response.status === 400 && body?.error_type === "validation_error") {
632
+ const firstError = Array.isArray(body.details?.errors) ? body.details?.errors[0] : void 0;
633
+ return {
634
+ valid: false,
635
+ failure_code: "validation_error",
636
+ message: firstError ?? "Invalid recipient address"
637
+ };
638
+ }
569
639
  throw new Error(`Failed to verify recipient address: ${response.statusText}`);
570
640
  }
571
641
  return response.json();
@@ -989,6 +1059,7 @@ async function stripeGetDefaultToken(params, publishableKey) {
989
1059
  chain_type: params.chainType
990
1060
  });
991
1061
  if (params.countryCode) query.append("country_code", params.countryCode);
1062
+ if (params.subdivisionCode) query.append("subdivision_code", params.subdivisionCode);
992
1063
  const response = await fetch(
993
1064
  `${API_BASE_URL}${HEADLESS_STRIPE_BASE}/default_token?${query.toString()}`,
994
1065
  {
@@ -1514,6 +1585,806 @@ var CheckoutEventType = /* @__PURE__ */ ((CheckoutEventType2) => {
1514
1585
  return CheckoutEventType2;
1515
1586
  })(CheckoutEventType || {});
1516
1587
 
1588
+ // src/lib/emitter.ts
1589
+ var TypedEmitter = class {
1590
+ constructor() {
1591
+ __publicField(this, "handlers", /* @__PURE__ */ new Map());
1592
+ }
1593
+ on(type, handler) {
1594
+ let set = this.handlers.get(type);
1595
+ if (!set) {
1596
+ set = /* @__PURE__ */ new Set();
1597
+ this.handlers.set(type, set);
1598
+ }
1599
+ set.add(handler);
1600
+ return () => {
1601
+ set.delete(handler);
1602
+ };
1603
+ }
1604
+ emit(type, event) {
1605
+ const dispatch = (handler) => {
1606
+ try {
1607
+ handler(event);
1608
+ } catch (error) {
1609
+ console.error("[unifold] event handler threw", error);
1610
+ }
1611
+ };
1612
+ this.handlers.get(type)?.forEach(dispatch);
1613
+ this.handlers.get("*")?.forEach(dispatch);
1614
+ }
1615
+ removeAllListeners() {
1616
+ this.handlers.clear();
1617
+ }
1618
+ };
1619
+
1620
+ // src/lib/mappers.ts
1621
+ function mapWalletToDepositAddress(wallet) {
1622
+ return {
1623
+ id: wallet.id,
1624
+ chainType: wallet.chain_type,
1625
+ addressType: wallet.address_type,
1626
+ address: wallet.address,
1627
+ destinationChainType: wallet.destination_chain_type,
1628
+ destinationChainId: wallet.destination_chain_id,
1629
+ destinationTokenAddress: wallet.destination_token_address,
1630
+ recipientAddress: wallet.recipient_address,
1631
+ isPrimary: wallet.is_primary
1632
+ };
1633
+ }
1634
+ function mapDirectExecution(execution) {
1635
+ return {
1636
+ id: execution.id,
1637
+ transactionHash: execution.transaction_hash,
1638
+ recipientAddress: execution.recipient_address,
1639
+ depositAddress: execution.deposit_wallet?.address,
1640
+ sourceChainType: execution.source_chain_type,
1641
+ sourceChainId: execution.source_chain_id,
1642
+ sourceTokenAddress: execution.source_token_address,
1643
+ destinationChainType: execution.destination_chain_type,
1644
+ destinationChainId: execution.destination_chain_id,
1645
+ destinationTokenAddress: execution.destination_token_address,
1646
+ sourceAmountBaseUnit: execution.source_amount_base_unit,
1647
+ sourceAmountUsd: execution.source_amount_usd,
1648
+ destinationAmountBaseUnit: execution.destination_amount_base_unit,
1649
+ destinationAmountUsd: execution.destination_amount_usd,
1650
+ destinationTransactionHashes: execution.destination_transaction_hashes,
1651
+ status: execution.status,
1652
+ failureReason: execution.failure_reason,
1653
+ createdAt: execution.created_at,
1654
+ updatedAt: execution.updated_at,
1655
+ explorerUrl: execution.explorer_url,
1656
+ destinationExplorerUrl: execution.destination_explorer_url,
1657
+ sourceTokenMetadata: execution.source_token_metadata ? {
1658
+ iconUrl: execution.source_token_metadata.icon_url,
1659
+ iconUrls: execution.source_token_metadata.icon_urls,
1660
+ decimals: execution.source_token_metadata.decimals
1661
+ } : void 0,
1662
+ destinationTokenMetadata: execution.destination_token_metadata ? {
1663
+ iconUrl: execution.destination_token_metadata.icon_url,
1664
+ iconUrls: execution.destination_token_metadata.icon_urls,
1665
+ decimals: execution.destination_token_metadata.decimals
1666
+ } : void 0
1667
+ };
1668
+ }
1669
+
1670
+ // src/lib/deposit-session.ts
1671
+ var DETECTION_POLL_INTERVAL_MS = 2500;
1672
+ var SCAN_NUDGE_INTERVAL_MS = 5e3;
1673
+ var DETECTION_ARM_DELAY_MS = 5e3;
1674
+ var LOOKBACK_MS = 6e4;
1675
+ var ADDRESS_CREATE_MAX_ATTEMPTS = 4;
1676
+ var DepositSessionEventType = /* @__PURE__ */ ((DepositSessionEventType2) => {
1677
+ DepositSessionEventType2["SESSION_STARTED"] = "deposit_session.started";
1678
+ DepositSessionEventType2["ADDRESSES_CREATED"] = "deposit_session.addresses_created";
1679
+ DepositSessionEventType2["CONFIRMATION_STARTED"] = "deposit_session.confirmation_started";
1680
+ DepositSessionEventType2["SESSION_STOPPED"] = "deposit_session.stopped";
1681
+ DepositSessionEventType2["SESSION_ERRORED"] = "deposit_session.errored";
1682
+ DepositSessionEventType2["EXECUTION_DETECTED"] = "direct_execution.detected";
1683
+ DepositSessionEventType2["EXECUTION_UPDATED"] = "direct_execution.updated";
1684
+ DepositSessionEventType2["EXECUTION_SUCCEEDED"] = "direct_execution.succeeded";
1685
+ DepositSessionEventType2["EXECUTION_FAILED"] = "direct_execution.failed";
1686
+ return DepositSessionEventType2;
1687
+ })(DepositSessionEventType || {});
1688
+ var IN_PROGRESS_STATUSES = [
1689
+ "pending" /* PENDING */,
1690
+ "waiting" /* WAITING */,
1691
+ "delayed" /* DELAYED */
1692
+ ];
1693
+ var FAILURE_STATUSES = ["failed" /* FAILED */, "refunded" /* REFUNDED */];
1694
+ var DepositSessionWaitError = class extends Error {
1695
+ constructor(code, message, cause) {
1696
+ super(message);
1697
+ __publicField(this, "code");
1698
+ /** Failed execution (`DEPOSIT_FAILED`) or fatal {@link DepositSessionError} (`SESSION_ERROR`). */
1699
+ __publicField(this, "cause");
1700
+ this.name = "DepositSessionWaitError";
1701
+ this.code = code;
1702
+ this.cause = cause;
1703
+ }
1704
+ };
1705
+ var SessionCheckError = class extends Error {
1706
+ constructor(code, message) {
1707
+ super(message);
1708
+ this.code = code;
1709
+ }
1710
+ };
1711
+ var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1712
+ var DepositSession = class {
1713
+ constructor(config) {
1714
+ /** Immutable id for correlation, `dsess_<ksuid>`. Client-generated. */
1715
+ __publicField(this, "id");
1716
+ __publicField(this, "emitter", new TypedEmitter());
1717
+ __publicField(this, "listeners", /* @__PURE__ */ new Set());
1718
+ __publicField(this, "publishableKey");
1719
+ __publicField(this, "externalUserId");
1720
+ __publicField(this, "destination");
1721
+ __publicField(this, "confirmationMode");
1722
+ __publicField(this, "method");
1723
+ // Run state. runToken invalidates in-flight async work across stop()/restart.
1724
+ __publicField(this, "runToken", 0);
1725
+ __publicField(this, "startPromise", null);
1726
+ __publicField(this, "destroyed", false);
1727
+ /** Pending waiter rejections, invoked by destroy() so waiters never hang. */
1728
+ __publicField(this, "waiterDestroyCallbacks", /* @__PURE__ */ new Set());
1729
+ __publicField(this, "baselineMs", 0);
1730
+ __publicField(this, "tracked", /* @__PURE__ */ new Map());
1731
+ __publicField(this, "pollErrorLatched", false);
1732
+ __publicField(this, "detectionTimer", null);
1733
+ __publicField(this, "nudgeTimer", null);
1734
+ __publicField(this, "armTimer", null);
1735
+ // Snapshot state
1736
+ __publicField(this, "status", "idle");
1737
+ __publicField(this, "addresses", []);
1738
+ __publicField(this, "addressIds", []);
1739
+ __publicField(this, "executions", []);
1740
+ __publicField(this, "checkingDeposit", false);
1741
+ __publicField(this, "error", null);
1742
+ __publicField(this, "snapshot");
1743
+ if (!config.publishableKey || config.publishableKey.trim() === "") {
1744
+ throw new Error("DepositSession: publishableKey is required");
1745
+ }
1746
+ if (!config.externalUserId) {
1747
+ throw new Error("DepositSession: externalUserId is required");
1748
+ }
1749
+ this.id = generatePrefixedKSUID("dsess");
1750
+ this.publishableKey = config.publishableKey;
1751
+ this.externalUserId = config.externalUserId;
1752
+ this.destination = config.destination;
1753
+ this.confirmationMode = config.confirmationMode ?? "auto";
1754
+ this.method = config.method ?? "transfer";
1755
+ this.snapshot = this.buildSnapshot();
1756
+ }
1757
+ // -- Public surface -------------------------------------------------------
1758
+ /** Synchronous snapshot; the reference is stable until state changes. */
1759
+ getSnapshot() {
1760
+ return this.snapshot;
1761
+ }
1762
+ /**
1763
+ * Subscribe to snapshot changes (external-store contract; drives
1764
+ * `useSyncExternalStore` in the React binding). Returns an unsubscribe fn.
1765
+ */
1766
+ subscribe(listener) {
1767
+ this.listeners.add(listener);
1768
+ return () => {
1769
+ this.listeners.delete(listener);
1770
+ };
1771
+ }
1772
+ on(type, handler) {
1773
+ return this.emitter.on(type, handler);
1774
+ }
1775
+ /**
1776
+ * Creates/fetches addresses (with a fail-fast recipient check) and starts
1777
+ * detection polling. Idempotent while running; callable again after stop()
1778
+ * or a fatal error (fresh baseline).
1779
+ */
1780
+ start() {
1781
+ if (this.destroyed) {
1782
+ return Promise.reject(new Error("DepositSession has been destroyed"));
1783
+ }
1784
+ if (this.startPromise) return this.startPromise;
1785
+ this.startPromise = this.run();
1786
+ return this.startPromise;
1787
+ }
1788
+ /** Arms the backend scan nudge in 'manual' mode. No-op if already armed. */
1789
+ confirmFundsSent() {
1790
+ if (this.destroyed || !this.startPromise) return;
1791
+ this.armConfirmation("manual");
1792
+ }
1793
+ /**
1794
+ * Stops all polling. The session can be restarted with start(), which
1795
+ * resets the baseline and tracked executions (fresh run).
1796
+ */
1797
+ stop() {
1798
+ const wasActive = this.startPromise !== null;
1799
+ this.runToken += 1;
1800
+ this.clearTimers();
1801
+ this.startPromise = null;
1802
+ if (this.checkingDeposit) {
1803
+ this.checkingDeposit = false;
1804
+ }
1805
+ if (wasActive && !this.destroyed) {
1806
+ this.commit();
1807
+ this.emitSessionEvent("deposit_session.stopped" /* SESSION_STOPPED */, {
1808
+ sessionId: this.id
1809
+ });
1810
+ this.notify();
1811
+ }
1812
+ }
1813
+ /** stop() + release all listeners. Terminal — start() rejects afterwards. */
1814
+ destroy() {
1815
+ this.stop();
1816
+ this.destroyed = true;
1817
+ Array.from(this.waiterDestroyCallbacks).forEach((callback) => callback());
1818
+ this.waiterDestroyCallbacks.clear();
1819
+ this.emitter.removeAllListeners();
1820
+ this.listeners.clear();
1821
+ }
1822
+ // -- Promise waiters (subscription sugar over the event stream) ------------
1823
+ /**
1824
+ * Resolve when the session reaches one of the given statuses (immediately
1825
+ * if it's already there). Generic primitive over the status state machine:
1826
+ * `waitForStatus('processing')` awaits detection; `waitForStatus(['succeeded',
1827
+ * 'failed'])` awaits a terminal outcome.
1828
+ *
1829
+ * Rejects with {@link DepositSessionWaitError} on abort or destroy().
1830
+ * Does not start or stop the session — it only listens.
1831
+ */
1832
+ waitForStatus(status, options = {}) {
1833
+ const statuses = Array.isArray(status) ? status : [status];
1834
+ return new Promise((resolve, reject) => {
1835
+ this.installWaiter({
1836
+ options,
1837
+ reject,
1838
+ subscribe: (settle) => {
1839
+ const check = () => {
1840
+ if (statuses.includes(this.snapshot.status)) settle(() => resolve(this.snapshot));
1841
+ };
1842
+ check();
1843
+ return this.subscribe(check);
1844
+ }
1845
+ });
1846
+ });
1847
+ }
1848
+ /**
1849
+ * Resolve with the **first** succeeded {@link DirectExecution} observed by
1850
+ * this session — the one-liner for the 90% case. Mirrors `beginDeposit()`'s
1851
+ * promise contract: resolve on success, reject on failure.
1852
+ *
1853
+ * Multi-execution semantics (unlike quote-scoped models such as Privy's,
1854
+ * one session can observe many executions — a user may send twice, or on
1855
+ * two chains): this waiter is one-shot "first completion" detection. If an
1856
+ * execution has already succeeded, it resolves immediately with that
1857
+ * execution. The session keeps polling after success — to react to every
1858
+ * settlement, subscribe to `direct_execution.succeeded` events or read
1859
+ * `snapshot.executions` instead.
1860
+ *
1861
+ * Rejects with {@link DepositSessionWaitError}:
1862
+ * - `DEPOSIT_FAILED` (cause: the failed execution) when a deposit fails
1863
+ * before any succeeds,
1864
+ * - `SESSION_ERROR` (cause: the fatal {@link DepositSessionError}) on fatal
1865
+ * session errors (e.g. address creation failed),
1866
+ * - `ABORTED` / `DESTROYED` per the wait options and session lifecycle.
1867
+ */
1868
+ waitForSuccess(options = {}) {
1869
+ return new Promise((resolve, reject) => {
1870
+ this.installWaiter({
1871
+ options,
1872
+ reject,
1873
+ subscribe: (settle) => {
1874
+ const { executions, error } = this.snapshot;
1875
+ const alreadySucceeded = executions.find(
1876
+ (execution) => execution.status === "succeeded" /* SUCCEEDED */
1877
+ );
1878
+ if (alreadySucceeded) {
1879
+ settle(() => resolve(alreadySucceeded));
1880
+ return () => {
1881
+ };
1882
+ }
1883
+ const alreadyFailed = executions.find(
1884
+ (execution) => FAILURE_STATUSES.includes(execution.status)
1885
+ );
1886
+ if (alreadyFailed) {
1887
+ settle(
1888
+ () => reject(
1889
+ new DepositSessionWaitError("DEPOSIT_FAILED", "Deposit failed", alreadyFailed)
1890
+ )
1891
+ );
1892
+ return () => {
1893
+ };
1894
+ }
1895
+ if (error?.fatal) {
1896
+ settle(() => reject(new DepositSessionWaitError("SESSION_ERROR", error.message, error)));
1897
+ return () => {
1898
+ };
1899
+ }
1900
+ const offs = [
1901
+ this.on(
1902
+ "direct_execution.succeeded" /* EXECUTION_SUCCEEDED */,
1903
+ (event) => settle(() => resolve(event.data.object))
1904
+ ),
1905
+ this.on(
1906
+ "direct_execution.failed" /* EXECUTION_FAILED */,
1907
+ (event) => settle(
1908
+ () => reject(
1909
+ new DepositSessionWaitError("DEPOSIT_FAILED", "Deposit failed", event.data.object)
1910
+ )
1911
+ )
1912
+ ),
1913
+ this.on("deposit_session.errored" /* SESSION_ERRORED */, (event) => {
1914
+ if (!event.data.object.fatal) return;
1915
+ settle(
1916
+ () => reject(
1917
+ new DepositSessionWaitError(
1918
+ "SESSION_ERROR",
1919
+ event.data.object.message,
1920
+ event.data.object
1921
+ )
1922
+ )
1923
+ );
1924
+ })
1925
+ ];
1926
+ return () => offs.forEach((off) => off());
1927
+ }
1928
+ });
1929
+ });
1930
+ }
1931
+ /**
1932
+ * Shared waiter plumbing: AbortSignal and destroy() rejection, with
1933
+ * single-settlement and cleanup. `subscribe` installs the wait condition
1934
+ * and returns its unsubscribe fn; it settles via `settle(fn)`.
1935
+ */
1936
+ installWaiter({
1937
+ options,
1938
+ reject,
1939
+ subscribe
1940
+ }) {
1941
+ if (this.destroyed) {
1942
+ reject(new DepositSessionWaitError("DESTROYED", "DepositSession has been destroyed"));
1943
+ return;
1944
+ }
1945
+ if (options.signal?.aborted) {
1946
+ reject(new DepositSessionWaitError("ABORTED", "Wait aborted", options.signal.reason));
1947
+ return;
1948
+ }
1949
+ let settled = false;
1950
+ let unsubscribe = null;
1951
+ const cleanup = () => {
1952
+ unsubscribe?.();
1953
+ options.signal?.removeEventListener("abort", onAbort);
1954
+ this.waiterDestroyCallbacks.delete(onDestroy);
1955
+ };
1956
+ const settle = (finish) => {
1957
+ if (settled) return;
1958
+ settled = true;
1959
+ cleanup();
1960
+ finish();
1961
+ };
1962
+ const onAbort = () => settle(
1963
+ () => reject(new DepositSessionWaitError("ABORTED", "Wait aborted", options.signal?.reason))
1964
+ );
1965
+ const onDestroy = () => settle(
1966
+ () => reject(new DepositSessionWaitError("DESTROYED", "DepositSession was destroyed while waiting"))
1967
+ );
1968
+ this.waiterDestroyCallbacks.add(onDestroy);
1969
+ options.signal?.addEventListener("abort", onAbort, { once: true });
1970
+ unsubscribe = subscribe(settle);
1971
+ if (settled) cleanup();
1972
+ }
1973
+ // -- Run lifecycle ---------------------------------------------------------
1974
+ async run() {
1975
+ const token = ++this.runToken;
1976
+ this.baselineMs = Date.now();
1977
+ this.tracked.clear();
1978
+ this.pollErrorLatched = false;
1979
+ this.executions = [];
1980
+ this.error = null;
1981
+ this.checkingDeposit = false;
1982
+ this.setStatus("creating_addresses");
1983
+ this.emitSessionEvent("deposit_session.started" /* SESSION_STARTED */, {
1984
+ sessionId: this.id
1985
+ });
1986
+ this.notify();
1987
+ let wallets;
1988
+ try {
1989
+ [wallets] = await Promise.all([
1990
+ this.createAddressesWithRetry(token),
1991
+ this.runStartChecks()
1992
+ ]);
1993
+ } catch (cause) {
1994
+ if (token !== this.runToken) return;
1995
+ const isCheck = cause instanceof SessionCheckError;
1996
+ this.failFatally(
1997
+ isCheck ? cause.code : "ADDRESS_CREATION_FAILED",
1998
+ isCheck ? cause.message : "Failed to create deposit addresses",
1999
+ cause
2000
+ );
2001
+ return;
2002
+ }
2003
+ if (token !== this.runToken) return;
2004
+ this.addresses = wallets.map(mapWalletToDepositAddress);
2005
+ this.addressIds = wallets.map((w) => w.id).filter(Boolean);
2006
+ this.setStatus("awaiting_funds");
2007
+ this.commit();
2008
+ this.emitSessionEvent("deposit_session.addresses_created" /* ADDRESSES_CREATED */, {
2009
+ sessionId: this.id,
2010
+ addresses: this.addresses
2011
+ });
2012
+ this.notify();
2013
+ this.startDetectionLoop(token);
2014
+ if (this.confirmationMode === "auto") {
2015
+ this.armTimer = setTimeout(() => {
2016
+ if (token === this.runToken) this.armConfirmation("auto");
2017
+ }, DETECTION_ARM_DELAY_MS);
2018
+ }
2019
+ }
2020
+ failFatally(code, message, cause) {
2021
+ this.clearTimers();
2022
+ this.startPromise = null;
2023
+ this.error = { code, message, fatal: true, cause };
2024
+ this.setStatus("error");
2025
+ this.commit();
2026
+ this.emitSessionEvent("deposit_session.errored" /* SESSION_ERRORED */, {
2027
+ sessionId: this.id,
2028
+ code,
2029
+ message,
2030
+ fatal: true
2031
+ });
2032
+ this.notify();
2033
+ }
2034
+ async createAddressesWithRetry(token) {
2035
+ let lastError;
2036
+ for (let attempt = 0; attempt < ADDRESS_CREATE_MAX_ATTEMPTS; attempt++) {
2037
+ if (attempt > 0) {
2038
+ await delay(Math.min(1e3 * 2 ** (attempt - 1), 1e4));
2039
+ if (token !== this.runToken) throw new Error("DepositSession stopped");
2040
+ }
2041
+ try {
2042
+ const response = await createDepositAddress(
2043
+ {
2044
+ external_user_id: this.externalUserId,
2045
+ destination_chain_type: this.destination.chainType,
2046
+ destination_chain_id: this.destination.chainId,
2047
+ destination_token_address: this.destination.tokenAddress,
2048
+ recipient_address: this.destination.recipientAddress,
2049
+ contract_calls: this.destination.contractCalls
2050
+ },
2051
+ this.publishableKey
2052
+ );
2053
+ return response.data;
2054
+ } catch (error) {
2055
+ lastError = error;
2056
+ }
2057
+ }
2058
+ throw lastError;
2059
+ }
2060
+ /**
2061
+ * Fail-fast recipient validation (e.g. Algorand asset opt-in). Fails open
2062
+ * on network errors — the backend still enforces at execution time — but a
2063
+ * definitive negative result is fatal.
2064
+ *
2065
+ * Deliberately NOT IP/geo-aware: generating deposit addresses headless
2066
+ * carries no region gate. Hosts that want the modal's geo behavior render
2067
+ * against the opt-in `useAllowedCountry` hook instead.
2068
+ */
2069
+ async runStartChecks() {
2070
+ const recipientValid = await verifyRecipientAddress(
2071
+ {
2072
+ chain_type: this.destination.chainType,
2073
+ chain_id: this.destination.chainId,
2074
+ token_address: this.destination.tokenAddress,
2075
+ recipient_address: this.destination.recipientAddress
2076
+ },
2077
+ this.publishableKey
2078
+ ).then((result) => result.valid).catch(() => null);
2079
+ if (recipientValid === false) {
2080
+ throw new SessionCheckError(
2081
+ "INVALID_RECIPIENT",
2082
+ "Recipient address cannot receive funds for this destination"
2083
+ );
2084
+ }
2085
+ }
2086
+ // -- Detection polling (port of useDepositPolling Effect 2) ----------------
2087
+ startDetectionLoop(token) {
2088
+ const poll = () => void this.pollExecutions(token);
2089
+ poll();
2090
+ this.detectionTimer = setInterval(poll, DETECTION_POLL_INTERVAL_MS);
2091
+ }
2092
+ async pollExecutions(token) {
2093
+ try {
2094
+ const response = await queryExecutions(
2095
+ this.externalUserId,
2096
+ this.publishableKey,
2097
+ "deposit" /* Deposit */
2098
+ );
2099
+ if (token !== this.runToken) return;
2100
+ if (this.pollErrorLatched) {
2101
+ this.pollErrorLatched = false;
2102
+ if (this.error && !this.error.fatal) {
2103
+ this.error = null;
2104
+ this.commit();
2105
+ this.notify();
2106
+ }
2107
+ }
2108
+ const cutoffMs = this.baselineMs - LOOKBACK_MS;
2109
+ const sorted = [...response.data].sort((a, b) => {
2110
+ const timeA = a.created_at ? new Date(a.created_at).getTime() : 0;
2111
+ const timeB = b.created_at ? new Date(b.created_at).getTime() : 0;
2112
+ return timeB - timeA;
2113
+ });
2114
+ let candidate = null;
2115
+ for (const execution of sorted) {
2116
+ const createdMs = execution.created_at ? new Date(execution.created_at).getTime() : NaN;
2117
+ if (!Number.isFinite(createdMs) || createdMs < cutoffMs) continue;
2118
+ const trackedStatus = this.tracked.get(execution.id);
2119
+ const isTerminal = execution.status === "succeeded" /* SUCCEEDED */ || FAILURE_STATUSES.includes(execution.status);
2120
+ if (trackedStatus === void 0 && createdMs < this.baselineMs && isTerminal) {
2121
+ continue;
2122
+ }
2123
+ if (trackedStatus === void 0 || trackedStatus !== execution.status) {
2124
+ candidate = execution;
2125
+ break;
2126
+ }
2127
+ }
2128
+ if (!candidate) return;
2129
+ this.processExecutionChange(candidate);
2130
+ } catch (error) {
2131
+ if (token !== this.runToken) return;
2132
+ console.error("[unifold] failed to fetch executions:", error);
2133
+ if (!this.pollErrorLatched) {
2134
+ this.pollErrorLatched = true;
2135
+ this.error = {
2136
+ code: "POLLING_ERROR",
2137
+ message: "Failed to fetch deposit status",
2138
+ fatal: false,
2139
+ cause: error
2140
+ };
2141
+ this.commit();
2142
+ this.emitSessionEvent("deposit_session.errored" /* SESSION_ERRORED */, {
2143
+ sessionId: this.id,
2144
+ code: "POLLING_ERROR",
2145
+ message: "Failed to fetch deposit status",
2146
+ fatal: false
2147
+ });
2148
+ this.notify();
2149
+ }
2150
+ }
2151
+ }
2152
+ processExecutionChange(wire) {
2153
+ const previousStatus = this.tracked.get(wire.id) ?? null;
2154
+ this.tracked.set(wire.id, wire.status);
2155
+ const execution = mapDirectExecution(wire);
2156
+ const existingIndex = this.executions.findIndex((e) => e.id === execution.id);
2157
+ if (existingIndex >= 0) {
2158
+ this.executions = this.executions.map(
2159
+ (e, i) => i === existingIndex ? execution : e
2160
+ );
2161
+ } else {
2162
+ this.executions = [...this.executions, execution].sort((a, b) => {
2163
+ const timeA = a.createdAt ? new Date(a.createdAt).getTime() : 0;
2164
+ const timeB = b.createdAt ? new Date(b.createdAt).getTime() : 0;
2165
+ return timeB - timeA;
2166
+ });
2167
+ }
2168
+ if (wire.status === "succeeded" /* SUCCEEDED */) {
2169
+ this.setStatus("succeeded");
2170
+ } else if (FAILURE_STATUSES.includes(wire.status)) {
2171
+ this.setStatus("failed");
2172
+ } else if (IN_PROGRESS_STATUSES.includes(wire.status)) {
2173
+ this.setStatus("processing");
2174
+ }
2175
+ this.commit();
2176
+ const eventCreated = this.executionEventTimestamp(wire);
2177
+ if (previousStatus === null) {
2178
+ this.emitExecutionEvent(
2179
+ "direct_execution.detected" /* EXECUTION_DETECTED */,
2180
+ execution,
2181
+ eventCreated
2182
+ );
2183
+ } else {
2184
+ this.emitExecutionEvent(
2185
+ "direct_execution.updated" /* EXECUTION_UPDATED */,
2186
+ { ...execution, previousStatus },
2187
+ eventCreated
2188
+ );
2189
+ }
2190
+ const wasInProgressOrNew = previousStatus === null || IN_PROGRESS_STATUSES.includes(previousStatus);
2191
+ if (wire.status === "succeeded" /* SUCCEEDED */ && wasInProgressOrNew) {
2192
+ this.emitExecutionEvent(
2193
+ "direct_execution.succeeded" /* EXECUTION_SUCCEEDED */,
2194
+ execution,
2195
+ eventCreated
2196
+ );
2197
+ } else if (FAILURE_STATUSES.includes(wire.status) && (previousStatus === null || !FAILURE_STATUSES.includes(previousStatus))) {
2198
+ this.emitExecutionEvent(
2199
+ "direct_execution.failed" /* EXECUTION_FAILED */,
2200
+ execution,
2201
+ eventCreated
2202
+ );
2203
+ }
2204
+ this.notify();
2205
+ }
2206
+ // -- Scan nudge (port of useDepositPolling Effects 1 + 3) ------------------
2207
+ armConfirmation(trigger) {
2208
+ if (this.checkingDeposit || this.destroyed) return;
2209
+ if (this.armTimer) {
2210
+ clearTimeout(this.armTimer);
2211
+ this.armTimer = null;
2212
+ }
2213
+ this.checkingDeposit = true;
2214
+ this.commit();
2215
+ this.emitSessionEvent("deposit_session.confirmation_started" /* CONFIRMATION_STARTED */, {
2216
+ sessionId: this.id,
2217
+ trigger
2218
+ });
2219
+ this.notify();
2220
+ const token = this.runToken;
2221
+ const nudge = () => {
2222
+ if (token !== this.runToken) return;
2223
+ void Promise.all(
2224
+ this.addressIds.map(
2225
+ (id) => pollDirectExecutions({ deposit_wallet_id: id }, this.publishableKey).catch(
2226
+ () => {
2227
+ }
2228
+ )
2229
+ )
2230
+ );
2231
+ };
2232
+ nudge();
2233
+ this.nudgeTimer = setInterval(nudge, SCAN_NUDGE_INTERVAL_MS);
2234
+ }
2235
+ // -- Internals --------------------------------------------------------------
2236
+ setStatus(status) {
2237
+ this.status = status;
2238
+ }
2239
+ clearTimers() {
2240
+ if (this.detectionTimer) {
2241
+ clearInterval(this.detectionTimer);
2242
+ this.detectionTimer = null;
2243
+ }
2244
+ if (this.nudgeTimer) {
2245
+ clearInterval(this.nudgeTimer);
2246
+ this.nudgeTimer = null;
2247
+ }
2248
+ if (this.armTimer) {
2249
+ clearTimeout(this.armTimer);
2250
+ this.armTimer = null;
2251
+ }
2252
+ }
2253
+ buildSnapshot() {
2254
+ return {
2255
+ status: this.status,
2256
+ addresses: this.addresses,
2257
+ executions: this.executions,
2258
+ latestExecution: this.executions[0] ?? null,
2259
+ isCheckingDeposit: this.checkingDeposit,
2260
+ error: this.error
2261
+ };
2262
+ }
2263
+ /** Rebuild the snapshot so getSnapshot() reflects current state. */
2264
+ commit() {
2265
+ this.snapshot = this.buildSnapshot();
2266
+ }
2267
+ notify() {
2268
+ this.listeners.forEach((listener) => {
2269
+ try {
2270
+ listener();
2271
+ } catch (error) {
2272
+ console.error("[unifold] snapshot listener threw", error);
2273
+ }
2274
+ });
2275
+ }
2276
+ executionEventTimestamp(wire) {
2277
+ if (wire.updated_at) return Math.floor(new Date(wire.updated_at).getTime() / 1e3);
2278
+ if (wire.created_at) return Math.floor(new Date(wire.created_at).getTime() / 1e3);
2279
+ return Math.floor(Date.now() / 1e3);
2280
+ }
2281
+ emitSessionEvent(type, object) {
2282
+ this.emitter.emit(type, {
2283
+ id: generatePrefixedKSUID("sevt"),
2284
+ type,
2285
+ created: Math.floor(Date.now() / 1e3),
2286
+ method: this.method,
2287
+ data: { object }
2288
+ });
2289
+ }
2290
+ emitExecutionEvent(type, object, created) {
2291
+ this.emitter.emit(type, {
2292
+ id: generatePrefixedKSUID("sevt"),
2293
+ type,
2294
+ created,
2295
+ method: this.method,
2296
+ data: { object }
2297
+ });
2298
+ }
2299
+ };
2300
+
2301
+ // src/lib/client.ts
2302
+ var UnifoldClient = class {
2303
+ constructor(options) {
2304
+ __publicField(this, "publishableKey");
2305
+ const { publishableKey } = options;
2306
+ if (!publishableKey || publishableKey.trim() === "") {
2307
+ throw new Error("Unifold: publishableKey is required");
2308
+ }
2309
+ if (!publishableKey.startsWith("pk_test_") && !publishableKey.startsWith("pk_live_")) {
2310
+ console.warn(
2311
+ 'Unifold: publishableKey should start with "pk_test_" or "pk_live_".'
2312
+ );
2313
+ }
2314
+ this.publishableKey = publishableKey;
2315
+ }
2316
+ /** Create a headless deposit-session flow controller. */
2317
+ createDepositSession(params) {
2318
+ return new DepositSession({ ...params, publishableKey: this.publishableKey });
2319
+ }
2320
+ /**
2321
+ * Create (idempotently) and return the user's deposit addresses for a
2322
+ * destination — `POST /v1/public/deposit_addresses`.
2323
+ */
2324
+ async getDepositAddresses(params) {
2325
+ const response = await createDepositAddress(
2326
+ {
2327
+ external_user_id: params.externalUserId,
2328
+ destination_chain_type: params.destination.chainType,
2329
+ destination_chain_id: params.destination.chainId,
2330
+ destination_token_address: params.destination.tokenAddress,
2331
+ recipient_address: params.destination.recipientAddress,
2332
+ contract_calls: params.destination.contractCalls
2333
+ },
2334
+ this.publishableKey
2335
+ );
2336
+ return response.data.map(mapWalletToDepositAddress);
2337
+ }
2338
+ /** List the user's executions — `POST /v1/public/direct_executions/query`. */
2339
+ async listExecutions(params) {
2340
+ const response = await queryExecutions(
2341
+ params.externalUserId,
2342
+ this.publishableKey,
2343
+ params.actionType ?? "deposit" /* Deposit */
2344
+ );
2345
+ return response.data.map(mapDirectExecution);
2346
+ }
2347
+ /** Source tokens/chains a user can deposit from for a destination. */
2348
+ async getSupportedDepositTokens(params) {
2349
+ const response = await getSupportedDepositTokens(
2350
+ this.publishableKey,
2351
+ params?.destination || params?.productType ? {
2352
+ ...params.destination ? {
2353
+ destination_chain_type: params.destination.chainType,
2354
+ destination_chain_id: params.destination.chainId,
2355
+ destination_token_address: params.destination.tokenAddress
2356
+ } : {},
2357
+ ...params.productType ? { product_type: params.productType } : {}
2358
+ } : void 0
2359
+ );
2360
+ return response.data;
2361
+ }
2362
+ /** Validate a recipient address for a destination (e.g. Algorand opt-in). */
2363
+ async verifyAddress(params) {
2364
+ const response = await verifyRecipientAddress(
2365
+ {
2366
+ chain_type: params.chainType,
2367
+ chain_id: params.chainId,
2368
+ token_address: params.tokenAddress,
2369
+ recipient_address: params.recipientAddress
2370
+ },
2371
+ this.publishableKey
2372
+ );
2373
+ return {
2374
+ valid: response.valid,
2375
+ failureCode: response.failure_code ?? null,
2376
+ metadata: response.metadata ?? null
2377
+ };
2378
+ }
2379
+ /** Project-level configuration (feature flags, blocked countries, ...). */
2380
+ getProjectConfig(options) {
2381
+ return getProjectConfig(this.publishableKey, options);
2382
+ }
2383
+ };
2384
+ function createUnifoldClient(options) {
2385
+ return new UnifoldClient(options);
2386
+ }
2387
+
1517
2388
  // src/hooks/use-user-ip.ts
1518
2389
  import { useQuery } from "@tanstack/react-query";
1519
2390
  function useUserIp() {
@@ -1648,12 +2519,21 @@ var i18n = en_default;
1648
2519
  export {
1649
2520
  ActionType,
1650
2521
  CheckoutEventType,
2522
+ DETECTION_ARM_DELAY_MS,
2523
+ DETECTION_POLL_INTERVAL_MS,
2524
+ DepositAddressValidationError,
1651
2525
  DepositEventType,
2526
+ DepositSession,
2527
+ DepositSessionEventType,
2528
+ DepositSessionWaitError,
1652
2529
  ExecutionStatus,
1653
2530
  IneligibilityReason,
1654
2531
  IntegrationProvider,
2532
+ LOOKBACK_MS,
2533
+ SCAN_NUDGE_INTERVAL_MS,
1655
2534
  SOLANA_USDC_ADDRESS,
1656
2535
  StripeApiResponseError,
2536
+ UnifoldClient,
1657
2537
  WithdrawEventType,
1658
2538
  authenticateIntegrationOAuth,
1659
2539
  buildHypercoreTransaction,
@@ -1667,6 +2547,7 @@ export {
1667
2547
  createIntegrationTransfer,
1668
2548
  createOnrampSession,
1669
2549
  createOnrampVerificationSession,
2550
+ createUnifoldClient,
1670
2551
  exchangeOnrampVerificationToken,
1671
2552
  formatStablecoinAmount,
1672
2553
  generateKSUID,
@@ -1689,6 +2570,7 @@ export {
1689
2570
  getExchanges,
1690
2571
  getExternalWallets,
1691
2572
  getFiatCurrencies,
2573
+ getFiatExchangeRates,
1692
2574
  getIconUrl,
1693
2575
  getIconUrlWithCdn,
1694
2576
  getIntegrationExchanges,
@@ -1700,6 +2582,7 @@ export {
1700
2582
  getOnrampVerificationSession,
1701
2583
  getPreferredIconUrl,
1702
2584
  getProjectConfig,
2585
+ getPublicIncident,
1703
2586
  getSupportedDepositTokens,
1704
2587
  getSupportedDestinationTokens,
1705
2588
  getTokenChains,
@@ -1708,7 +2591,10 @@ export {
1708
2591
  getWalletMobileDeepLink,
1709
2592
  i18n,
1710
2593
  isApplePayLimitReached,
2594
+ isDepositAddressValidationError,
1711
2595
  listPaymentIntentExecutions,
2596
+ mapDirectExecution,
2597
+ mapWalletToDepositAddress,
1712
2598
  pollDirectExecutions,
1713
2599
  queryExecutions,
1714
2600
  refreshIntegrationToken,