@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.js CHANGED
@@ -3,6 +3,7 @@ var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
6
7
  var __export = (target, all) => {
7
8
  for (var name in all)
8
9
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -16,18 +17,28 @@ var __copyProps = (to, from, except, desc) => {
16
17
  return to;
17
18
  };
18
19
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
19
21
 
20
22
  // src/index.ts
21
23
  var index_exports = {};
22
24
  __export(index_exports, {
23
25
  ActionType: () => ActionType,
24
26
  CheckoutEventType: () => CheckoutEventType,
27
+ DETECTION_ARM_DELAY_MS: () => DETECTION_ARM_DELAY_MS,
28
+ DETECTION_POLL_INTERVAL_MS: () => DETECTION_POLL_INTERVAL_MS,
29
+ DepositAddressValidationError: () => DepositAddressValidationError,
25
30
  DepositEventType: () => DepositEventType,
31
+ DepositSession: () => DepositSession,
32
+ DepositSessionEventType: () => DepositSessionEventType,
33
+ DepositSessionWaitError: () => DepositSessionWaitError,
26
34
  ExecutionStatus: () => ExecutionStatus,
27
35
  IneligibilityReason: () => IneligibilityReason,
28
36
  IntegrationProvider: () => IntegrationProvider,
37
+ LOOKBACK_MS: () => LOOKBACK_MS,
38
+ SCAN_NUDGE_INTERVAL_MS: () => SCAN_NUDGE_INTERVAL_MS,
29
39
  SOLANA_USDC_ADDRESS: () => SOLANA_USDC_ADDRESS,
30
40
  StripeApiResponseError: () => StripeApiResponseError,
41
+ UnifoldClient: () => UnifoldClient,
31
42
  WithdrawEventType: () => WithdrawEventType,
32
43
  authenticateIntegrationOAuth: () => authenticateIntegrationOAuth,
33
44
  buildHypercoreTransaction: () => buildHypercoreTransaction,
@@ -41,6 +52,7 @@ __export(index_exports, {
41
52
  createIntegrationTransfer: () => createIntegrationTransfer,
42
53
  createOnrampSession: () => createOnrampSession,
43
54
  createOnrampVerificationSession: () => createOnrampVerificationSession,
55
+ createUnifoldClient: () => createUnifoldClient,
44
56
  exchangeOnrampVerificationToken: () => exchangeOnrampVerificationToken,
45
57
  formatStablecoinAmount: () => formatStablecoinAmount,
46
58
  generateKSUID: () => generateKSUID,
@@ -63,6 +75,7 @@ __export(index_exports, {
63
75
  getExchanges: () => getExchanges,
64
76
  getExternalWallets: () => getExternalWallets,
65
77
  getFiatCurrencies: () => getFiatCurrencies,
78
+ getFiatExchangeRates: () => getFiatExchangeRates,
66
79
  getIconUrl: () => getIconUrl,
67
80
  getIconUrlWithCdn: () => getIconUrlWithCdn,
68
81
  getIntegrationExchanges: () => getIntegrationExchanges,
@@ -74,6 +87,7 @@ __export(index_exports, {
74
87
  getOnrampVerificationSession: () => getOnrampVerificationSession,
75
88
  getPreferredIconUrl: () => getPreferredIconUrl,
76
89
  getProjectConfig: () => getProjectConfig,
90
+ getPublicIncident: () => getPublicIncident,
77
91
  getSupportedDepositTokens: () => getSupportedDepositTokens,
78
92
  getSupportedDestinationTokens: () => getSupportedDestinationTokens,
79
93
  getTokenChains: () => getTokenChains,
@@ -82,7 +96,10 @@ __export(index_exports, {
82
96
  getWalletMobileDeepLink: () => getWalletMobileDeepLink,
83
97
  i18n: () => i18n,
84
98
  isApplePayLimitReached: () => isApplePayLimitReached,
99
+ isDepositAddressValidationError: () => isDepositAddressValidationError,
85
100
  listPaymentIntentExecutions: () => listPaymentIntentExecutions,
101
+ mapDirectExecution: () => mapDirectExecution,
102
+ mapWalletToDepositAddress: () => mapWalletToDepositAddress,
86
103
  pollDirectExecutions: () => pollDirectExecutions,
87
104
  queryExecutions: () => queryExecutions,
88
105
  refreshIntegrationToken: () => refreshIntegrationToken,
@@ -210,6 +227,16 @@ var ActionType = /* @__PURE__ */ ((ActionType2) => {
210
227
  ActionType2["Withdraw"] = "withdraw";
211
228
  return ActionType2;
212
229
  })(ActionType || {});
230
+ var DepositAddressValidationError = class extends Error {
231
+ constructor(message) {
232
+ super(message);
233
+ __publicField(this, "isDepositAddressValidationError", true);
234
+ this.name = "DepositAddressValidationError";
235
+ }
236
+ };
237
+ function isDepositAddressValidationError(error) {
238
+ return error instanceof Error && error.isDepositAddressValidationError === true;
239
+ }
213
240
  async function createDepositAddress(overrides, publishableKey) {
214
241
  if (!overrides?.external_user_id) {
215
242
  throw new Error("external_user_id is required");
@@ -237,6 +264,13 @@ async function createDepositAddress(overrides, publishableKey) {
237
264
  body: JSON.stringify(payload)
238
265
  });
239
266
  if (!response.ok) {
267
+ if (response.status === 400) {
268
+ const body = await response.json().catch(() => null);
269
+ if (body?.error_type === "validation_error") {
270
+ const firstError = Array.isArray(body.details?.errors) ? body.details?.errors[0] : void 0;
271
+ throw new DepositAddressValidationError(firstError ?? "Invalid recipient address");
272
+ }
273
+ }
240
274
  throw new Error(`Failed to create EOA: ${response.statusText}`);
241
275
  }
242
276
  return response.json();
@@ -407,6 +441,27 @@ async function getFiatCurrencies(publishableKey) {
407
441
  }
408
442
  return response.json();
409
443
  }
444
+ async function getFiatExchangeRates(options = {}, publishableKey) {
445
+ const pk = publishableKey || DEFAULT_PUBLISHABLE_KEY;
446
+ validatePublishableKey(pk);
447
+ const params = new URLSearchParams();
448
+ if (options.currencies && options.currencies.length > 0) {
449
+ params.set("currencies", options.currencies.join(","));
450
+ }
451
+ const queryString = params.toString();
452
+ const url = `${API_BASE_URL}/v1/public/exchange_rates/fiat_currencies${queryString ? `?${queryString}` : ""}`;
453
+ const response = await fetch(url, {
454
+ method: "GET",
455
+ headers: {
456
+ accept: "application/json",
457
+ "x-publishable-key": pk
458
+ }
459
+ });
460
+ if (!response.ok) {
461
+ throw new Error(`Failed to fetch fiat exchange rates: ${response.statusText}`);
462
+ }
463
+ return response.json();
464
+ }
410
465
  async function getOnrampQuotes(request, publishableKey) {
411
466
  const pk = publishableKey || DEFAULT_PUBLISHABLE_KEY;
412
467
  validatePublishableKey(pk);
@@ -572,6 +627,21 @@ async function getProjectConfig(publishableKey, options) {
572
627
  const data = await response.json();
573
628
  return data;
574
629
  }
630
+ async function getPublicIncident(publishableKey) {
631
+ const pk = publishableKey || DEFAULT_PUBLISHABLE_KEY;
632
+ validatePublishableKey(pk);
633
+ const response = await fetch(`${API_BASE_URL}/v1/public/projects/incident`, {
634
+ method: "GET",
635
+ headers: {
636
+ accept: "application/json",
637
+ "x-publishable-key": pk
638
+ }
639
+ });
640
+ if (!response.ok) {
641
+ throw new Error(`Failed to fetch public incident: ${response.statusText}`);
642
+ }
643
+ return response.json();
644
+ }
575
645
  async function getIpAddress() {
576
646
  const response = await fetch(`${API_BASE_URL}/v1/public/ip_address`, {
577
647
  method: "GET",
@@ -627,7 +697,7 @@ async function getExternalWallets(publishableKey) {
627
697
  const data = await response.json();
628
698
  return data;
629
699
  }
630
- async function getWalletMobileDeepLink(wallet, depositAddresses, publishableKey) {
700
+ async function getWalletMobileDeepLink(wallet, depositAddresses, publishableKey, amountUsd) {
631
701
  const pk = publishableKey || DEFAULT_PUBLISHABLE_KEY;
632
702
  validatePublishableKey(pk);
633
703
  const response = await fetch(`${API_BASE_URL}/v1/public/external_wallets/mobile_deeplink`, {
@@ -637,7 +707,11 @@ async function getWalletMobileDeepLink(wallet, depositAddresses, publishableKey)
637
707
  accept: "application/json",
638
708
  "x-publishable-key": pk
639
709
  },
640
- body: JSON.stringify({ wallet, deposit_addresses: depositAddresses })
710
+ body: JSON.stringify({
711
+ wallet,
712
+ deposit_addresses: depositAddresses,
713
+ ...amountUsd ? { amount_usd: amountUsd } : {}
714
+ })
641
715
  });
642
716
  if (!response.ok) {
643
717
  throw new Error(`Failed to generate wallet deep link: ${response.statusText}`);
@@ -682,6 +756,15 @@ async function verifyRecipientAddress(request, publishableKey) {
682
756
  body: JSON.stringify(request)
683
757
  });
684
758
  if (!response.ok) {
759
+ const body = await response.json().catch(() => null);
760
+ if (response.status === 400 && body?.error_type === "validation_error") {
761
+ const firstError = Array.isArray(body.details?.errors) ? body.details?.errors[0] : void 0;
762
+ return {
763
+ valid: false,
764
+ failure_code: "validation_error",
765
+ message: firstError ?? "Invalid recipient address"
766
+ };
767
+ }
685
768
  throw new Error(`Failed to verify recipient address: ${response.statusText}`);
686
769
  }
687
770
  return response.json();
@@ -1105,6 +1188,7 @@ async function stripeGetDefaultToken(params, publishableKey) {
1105
1188
  chain_type: params.chainType
1106
1189
  });
1107
1190
  if (params.countryCode) query.append("country_code", params.countryCode);
1191
+ if (params.subdivisionCode) query.append("subdivision_code", params.subdivisionCode);
1108
1192
  const response = await fetch(
1109
1193
  `${API_BASE_URL}${HEADLESS_STRIPE_BASE}/default_token?${query.toString()}`,
1110
1194
  {
@@ -1630,6 +1714,806 @@ var CheckoutEventType = /* @__PURE__ */ ((CheckoutEventType2) => {
1630
1714
  return CheckoutEventType2;
1631
1715
  })(CheckoutEventType || {});
1632
1716
 
1717
+ // src/lib/emitter.ts
1718
+ var TypedEmitter = class {
1719
+ constructor() {
1720
+ __publicField(this, "handlers", /* @__PURE__ */ new Map());
1721
+ }
1722
+ on(type, handler) {
1723
+ let set = this.handlers.get(type);
1724
+ if (!set) {
1725
+ set = /* @__PURE__ */ new Set();
1726
+ this.handlers.set(type, set);
1727
+ }
1728
+ set.add(handler);
1729
+ return () => {
1730
+ set.delete(handler);
1731
+ };
1732
+ }
1733
+ emit(type, event) {
1734
+ const dispatch = (handler) => {
1735
+ try {
1736
+ handler(event);
1737
+ } catch (error) {
1738
+ console.error("[unifold] event handler threw", error);
1739
+ }
1740
+ };
1741
+ this.handlers.get(type)?.forEach(dispatch);
1742
+ this.handlers.get("*")?.forEach(dispatch);
1743
+ }
1744
+ removeAllListeners() {
1745
+ this.handlers.clear();
1746
+ }
1747
+ };
1748
+
1749
+ // src/lib/mappers.ts
1750
+ function mapWalletToDepositAddress(wallet) {
1751
+ return {
1752
+ id: wallet.id,
1753
+ chainType: wallet.chain_type,
1754
+ addressType: wallet.address_type,
1755
+ address: wallet.address,
1756
+ destinationChainType: wallet.destination_chain_type,
1757
+ destinationChainId: wallet.destination_chain_id,
1758
+ destinationTokenAddress: wallet.destination_token_address,
1759
+ recipientAddress: wallet.recipient_address,
1760
+ isPrimary: wallet.is_primary
1761
+ };
1762
+ }
1763
+ function mapDirectExecution(execution) {
1764
+ return {
1765
+ id: execution.id,
1766
+ transactionHash: execution.transaction_hash,
1767
+ recipientAddress: execution.recipient_address,
1768
+ depositAddress: execution.deposit_wallet?.address,
1769
+ sourceChainType: execution.source_chain_type,
1770
+ sourceChainId: execution.source_chain_id,
1771
+ sourceTokenAddress: execution.source_token_address,
1772
+ destinationChainType: execution.destination_chain_type,
1773
+ destinationChainId: execution.destination_chain_id,
1774
+ destinationTokenAddress: execution.destination_token_address,
1775
+ sourceAmountBaseUnit: execution.source_amount_base_unit,
1776
+ sourceAmountUsd: execution.source_amount_usd,
1777
+ destinationAmountBaseUnit: execution.destination_amount_base_unit,
1778
+ destinationAmountUsd: execution.destination_amount_usd,
1779
+ destinationTransactionHashes: execution.destination_transaction_hashes,
1780
+ status: execution.status,
1781
+ failureReason: execution.failure_reason,
1782
+ createdAt: execution.created_at,
1783
+ updatedAt: execution.updated_at,
1784
+ explorerUrl: execution.explorer_url,
1785
+ destinationExplorerUrl: execution.destination_explorer_url,
1786
+ sourceTokenMetadata: execution.source_token_metadata ? {
1787
+ iconUrl: execution.source_token_metadata.icon_url,
1788
+ iconUrls: execution.source_token_metadata.icon_urls,
1789
+ decimals: execution.source_token_metadata.decimals
1790
+ } : void 0,
1791
+ destinationTokenMetadata: execution.destination_token_metadata ? {
1792
+ iconUrl: execution.destination_token_metadata.icon_url,
1793
+ iconUrls: execution.destination_token_metadata.icon_urls,
1794
+ decimals: execution.destination_token_metadata.decimals
1795
+ } : void 0
1796
+ };
1797
+ }
1798
+
1799
+ // src/lib/deposit-session.ts
1800
+ var DETECTION_POLL_INTERVAL_MS = 2500;
1801
+ var SCAN_NUDGE_INTERVAL_MS = 5e3;
1802
+ var DETECTION_ARM_DELAY_MS = 5e3;
1803
+ var LOOKBACK_MS = 6e4;
1804
+ var ADDRESS_CREATE_MAX_ATTEMPTS = 4;
1805
+ var DepositSessionEventType = /* @__PURE__ */ ((DepositSessionEventType2) => {
1806
+ DepositSessionEventType2["SESSION_STARTED"] = "deposit_session.started";
1807
+ DepositSessionEventType2["ADDRESSES_CREATED"] = "deposit_session.addresses_created";
1808
+ DepositSessionEventType2["CONFIRMATION_STARTED"] = "deposit_session.confirmation_started";
1809
+ DepositSessionEventType2["SESSION_STOPPED"] = "deposit_session.stopped";
1810
+ DepositSessionEventType2["SESSION_ERRORED"] = "deposit_session.errored";
1811
+ DepositSessionEventType2["EXECUTION_DETECTED"] = "direct_execution.detected";
1812
+ DepositSessionEventType2["EXECUTION_UPDATED"] = "direct_execution.updated";
1813
+ DepositSessionEventType2["EXECUTION_SUCCEEDED"] = "direct_execution.succeeded";
1814
+ DepositSessionEventType2["EXECUTION_FAILED"] = "direct_execution.failed";
1815
+ return DepositSessionEventType2;
1816
+ })(DepositSessionEventType || {});
1817
+ var IN_PROGRESS_STATUSES = [
1818
+ "pending" /* PENDING */,
1819
+ "waiting" /* WAITING */,
1820
+ "delayed" /* DELAYED */
1821
+ ];
1822
+ var FAILURE_STATUSES = ["failed" /* FAILED */, "refunded" /* REFUNDED */];
1823
+ var DepositSessionWaitError = class extends Error {
1824
+ constructor(code, message, cause) {
1825
+ super(message);
1826
+ __publicField(this, "code");
1827
+ /** Failed execution (`DEPOSIT_FAILED`) or fatal {@link DepositSessionError} (`SESSION_ERROR`). */
1828
+ __publicField(this, "cause");
1829
+ this.name = "DepositSessionWaitError";
1830
+ this.code = code;
1831
+ this.cause = cause;
1832
+ }
1833
+ };
1834
+ var SessionCheckError = class extends Error {
1835
+ constructor(code, message) {
1836
+ super(message);
1837
+ this.code = code;
1838
+ }
1839
+ };
1840
+ var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1841
+ var DepositSession = class {
1842
+ constructor(config) {
1843
+ /** Immutable id for correlation, `dsess_<ksuid>`. Client-generated. */
1844
+ __publicField(this, "id");
1845
+ __publicField(this, "emitter", new TypedEmitter());
1846
+ __publicField(this, "listeners", /* @__PURE__ */ new Set());
1847
+ __publicField(this, "publishableKey");
1848
+ __publicField(this, "externalUserId");
1849
+ __publicField(this, "destination");
1850
+ __publicField(this, "confirmationMode");
1851
+ __publicField(this, "method");
1852
+ // Run state. runToken invalidates in-flight async work across stop()/restart.
1853
+ __publicField(this, "runToken", 0);
1854
+ __publicField(this, "startPromise", null);
1855
+ __publicField(this, "destroyed", false);
1856
+ /** Pending waiter rejections, invoked by destroy() so waiters never hang. */
1857
+ __publicField(this, "waiterDestroyCallbacks", /* @__PURE__ */ new Set());
1858
+ __publicField(this, "baselineMs", 0);
1859
+ __publicField(this, "tracked", /* @__PURE__ */ new Map());
1860
+ __publicField(this, "pollErrorLatched", false);
1861
+ __publicField(this, "detectionTimer", null);
1862
+ __publicField(this, "nudgeTimer", null);
1863
+ __publicField(this, "armTimer", null);
1864
+ // Snapshot state
1865
+ __publicField(this, "status", "idle");
1866
+ __publicField(this, "addresses", []);
1867
+ __publicField(this, "addressIds", []);
1868
+ __publicField(this, "executions", []);
1869
+ __publicField(this, "checkingDeposit", false);
1870
+ __publicField(this, "error", null);
1871
+ __publicField(this, "snapshot");
1872
+ if (!config.publishableKey || config.publishableKey.trim() === "") {
1873
+ throw new Error("DepositSession: publishableKey is required");
1874
+ }
1875
+ if (!config.externalUserId) {
1876
+ throw new Error("DepositSession: externalUserId is required");
1877
+ }
1878
+ this.id = generatePrefixedKSUID("dsess");
1879
+ this.publishableKey = config.publishableKey;
1880
+ this.externalUserId = config.externalUserId;
1881
+ this.destination = config.destination;
1882
+ this.confirmationMode = config.confirmationMode ?? "auto";
1883
+ this.method = config.method ?? "transfer";
1884
+ this.snapshot = this.buildSnapshot();
1885
+ }
1886
+ // -- Public surface -------------------------------------------------------
1887
+ /** Synchronous snapshot; the reference is stable until state changes. */
1888
+ getSnapshot() {
1889
+ return this.snapshot;
1890
+ }
1891
+ /**
1892
+ * Subscribe to snapshot changes (external-store contract; drives
1893
+ * `useSyncExternalStore` in the React binding). Returns an unsubscribe fn.
1894
+ */
1895
+ subscribe(listener) {
1896
+ this.listeners.add(listener);
1897
+ return () => {
1898
+ this.listeners.delete(listener);
1899
+ };
1900
+ }
1901
+ on(type, handler) {
1902
+ return this.emitter.on(type, handler);
1903
+ }
1904
+ /**
1905
+ * Creates/fetches addresses (with a fail-fast recipient check) and starts
1906
+ * detection polling. Idempotent while running; callable again after stop()
1907
+ * or a fatal error (fresh baseline).
1908
+ */
1909
+ start() {
1910
+ if (this.destroyed) {
1911
+ return Promise.reject(new Error("DepositSession has been destroyed"));
1912
+ }
1913
+ if (this.startPromise) return this.startPromise;
1914
+ this.startPromise = this.run();
1915
+ return this.startPromise;
1916
+ }
1917
+ /** Arms the backend scan nudge in 'manual' mode. No-op if already armed. */
1918
+ confirmFundsSent() {
1919
+ if (this.destroyed || !this.startPromise) return;
1920
+ this.armConfirmation("manual");
1921
+ }
1922
+ /**
1923
+ * Stops all polling. The session can be restarted with start(), which
1924
+ * resets the baseline and tracked executions (fresh run).
1925
+ */
1926
+ stop() {
1927
+ const wasActive = this.startPromise !== null;
1928
+ this.runToken += 1;
1929
+ this.clearTimers();
1930
+ this.startPromise = null;
1931
+ if (this.checkingDeposit) {
1932
+ this.checkingDeposit = false;
1933
+ }
1934
+ if (wasActive && !this.destroyed) {
1935
+ this.commit();
1936
+ this.emitSessionEvent("deposit_session.stopped" /* SESSION_STOPPED */, {
1937
+ sessionId: this.id
1938
+ });
1939
+ this.notify();
1940
+ }
1941
+ }
1942
+ /** stop() + release all listeners. Terminal — start() rejects afterwards. */
1943
+ destroy() {
1944
+ this.stop();
1945
+ this.destroyed = true;
1946
+ Array.from(this.waiterDestroyCallbacks).forEach((callback) => callback());
1947
+ this.waiterDestroyCallbacks.clear();
1948
+ this.emitter.removeAllListeners();
1949
+ this.listeners.clear();
1950
+ }
1951
+ // -- Promise waiters (subscription sugar over the event stream) ------------
1952
+ /**
1953
+ * Resolve when the session reaches one of the given statuses (immediately
1954
+ * if it's already there). Generic primitive over the status state machine:
1955
+ * `waitForStatus('processing')` awaits detection; `waitForStatus(['succeeded',
1956
+ * 'failed'])` awaits a terminal outcome.
1957
+ *
1958
+ * Rejects with {@link DepositSessionWaitError} on abort or destroy().
1959
+ * Does not start or stop the session — it only listens.
1960
+ */
1961
+ waitForStatus(status, options = {}) {
1962
+ const statuses = Array.isArray(status) ? status : [status];
1963
+ return new Promise((resolve, reject) => {
1964
+ this.installWaiter({
1965
+ options,
1966
+ reject,
1967
+ subscribe: (settle) => {
1968
+ const check = () => {
1969
+ if (statuses.includes(this.snapshot.status)) settle(() => resolve(this.snapshot));
1970
+ };
1971
+ check();
1972
+ return this.subscribe(check);
1973
+ }
1974
+ });
1975
+ });
1976
+ }
1977
+ /**
1978
+ * Resolve with the **first** succeeded {@link DirectExecution} observed by
1979
+ * this session — the one-liner for the 90% case. Mirrors `beginDeposit()`'s
1980
+ * promise contract: resolve on success, reject on failure.
1981
+ *
1982
+ * Multi-execution semantics (unlike quote-scoped models such as Privy's,
1983
+ * one session can observe many executions — a user may send twice, or on
1984
+ * two chains): this waiter is one-shot "first completion" detection. If an
1985
+ * execution has already succeeded, it resolves immediately with that
1986
+ * execution. The session keeps polling after success — to react to every
1987
+ * settlement, subscribe to `direct_execution.succeeded` events or read
1988
+ * `snapshot.executions` instead.
1989
+ *
1990
+ * Rejects with {@link DepositSessionWaitError}:
1991
+ * - `DEPOSIT_FAILED` (cause: the failed execution) when a deposit fails
1992
+ * before any succeeds,
1993
+ * - `SESSION_ERROR` (cause: the fatal {@link DepositSessionError}) on fatal
1994
+ * session errors (e.g. address creation failed),
1995
+ * - `ABORTED` / `DESTROYED` per the wait options and session lifecycle.
1996
+ */
1997
+ waitForSuccess(options = {}) {
1998
+ return new Promise((resolve, reject) => {
1999
+ this.installWaiter({
2000
+ options,
2001
+ reject,
2002
+ subscribe: (settle) => {
2003
+ const { executions, error } = this.snapshot;
2004
+ const alreadySucceeded = executions.find(
2005
+ (execution) => execution.status === "succeeded" /* SUCCEEDED */
2006
+ );
2007
+ if (alreadySucceeded) {
2008
+ settle(() => resolve(alreadySucceeded));
2009
+ return () => {
2010
+ };
2011
+ }
2012
+ const alreadyFailed = executions.find(
2013
+ (execution) => FAILURE_STATUSES.includes(execution.status)
2014
+ );
2015
+ if (alreadyFailed) {
2016
+ settle(
2017
+ () => reject(
2018
+ new DepositSessionWaitError("DEPOSIT_FAILED", "Deposit failed", alreadyFailed)
2019
+ )
2020
+ );
2021
+ return () => {
2022
+ };
2023
+ }
2024
+ if (error?.fatal) {
2025
+ settle(() => reject(new DepositSessionWaitError("SESSION_ERROR", error.message, error)));
2026
+ return () => {
2027
+ };
2028
+ }
2029
+ const offs = [
2030
+ this.on(
2031
+ "direct_execution.succeeded" /* EXECUTION_SUCCEEDED */,
2032
+ (event) => settle(() => resolve(event.data.object))
2033
+ ),
2034
+ this.on(
2035
+ "direct_execution.failed" /* EXECUTION_FAILED */,
2036
+ (event) => settle(
2037
+ () => reject(
2038
+ new DepositSessionWaitError("DEPOSIT_FAILED", "Deposit failed", event.data.object)
2039
+ )
2040
+ )
2041
+ ),
2042
+ this.on("deposit_session.errored" /* SESSION_ERRORED */, (event) => {
2043
+ if (!event.data.object.fatal) return;
2044
+ settle(
2045
+ () => reject(
2046
+ new DepositSessionWaitError(
2047
+ "SESSION_ERROR",
2048
+ event.data.object.message,
2049
+ event.data.object
2050
+ )
2051
+ )
2052
+ );
2053
+ })
2054
+ ];
2055
+ return () => offs.forEach((off) => off());
2056
+ }
2057
+ });
2058
+ });
2059
+ }
2060
+ /**
2061
+ * Shared waiter plumbing: AbortSignal and destroy() rejection, with
2062
+ * single-settlement and cleanup. `subscribe` installs the wait condition
2063
+ * and returns its unsubscribe fn; it settles via `settle(fn)`.
2064
+ */
2065
+ installWaiter({
2066
+ options,
2067
+ reject,
2068
+ subscribe
2069
+ }) {
2070
+ if (this.destroyed) {
2071
+ reject(new DepositSessionWaitError("DESTROYED", "DepositSession has been destroyed"));
2072
+ return;
2073
+ }
2074
+ if (options.signal?.aborted) {
2075
+ reject(new DepositSessionWaitError("ABORTED", "Wait aborted", options.signal.reason));
2076
+ return;
2077
+ }
2078
+ let settled = false;
2079
+ let unsubscribe = null;
2080
+ const cleanup = () => {
2081
+ unsubscribe?.();
2082
+ options.signal?.removeEventListener("abort", onAbort);
2083
+ this.waiterDestroyCallbacks.delete(onDestroy);
2084
+ };
2085
+ const settle = (finish) => {
2086
+ if (settled) return;
2087
+ settled = true;
2088
+ cleanup();
2089
+ finish();
2090
+ };
2091
+ const onAbort = () => settle(
2092
+ () => reject(new DepositSessionWaitError("ABORTED", "Wait aborted", options.signal?.reason))
2093
+ );
2094
+ const onDestroy = () => settle(
2095
+ () => reject(new DepositSessionWaitError("DESTROYED", "DepositSession was destroyed while waiting"))
2096
+ );
2097
+ this.waiterDestroyCallbacks.add(onDestroy);
2098
+ options.signal?.addEventListener("abort", onAbort, { once: true });
2099
+ unsubscribe = subscribe(settle);
2100
+ if (settled) cleanup();
2101
+ }
2102
+ // -- Run lifecycle ---------------------------------------------------------
2103
+ async run() {
2104
+ const token = ++this.runToken;
2105
+ this.baselineMs = Date.now();
2106
+ this.tracked.clear();
2107
+ this.pollErrorLatched = false;
2108
+ this.executions = [];
2109
+ this.error = null;
2110
+ this.checkingDeposit = false;
2111
+ this.setStatus("creating_addresses");
2112
+ this.emitSessionEvent("deposit_session.started" /* SESSION_STARTED */, {
2113
+ sessionId: this.id
2114
+ });
2115
+ this.notify();
2116
+ let wallets;
2117
+ try {
2118
+ [wallets] = await Promise.all([
2119
+ this.createAddressesWithRetry(token),
2120
+ this.runStartChecks()
2121
+ ]);
2122
+ } catch (cause) {
2123
+ if (token !== this.runToken) return;
2124
+ const isCheck = cause instanceof SessionCheckError;
2125
+ this.failFatally(
2126
+ isCheck ? cause.code : "ADDRESS_CREATION_FAILED",
2127
+ isCheck ? cause.message : "Failed to create deposit addresses",
2128
+ cause
2129
+ );
2130
+ return;
2131
+ }
2132
+ if (token !== this.runToken) return;
2133
+ this.addresses = wallets.map(mapWalletToDepositAddress);
2134
+ this.addressIds = wallets.map((w) => w.id).filter(Boolean);
2135
+ this.setStatus("awaiting_funds");
2136
+ this.commit();
2137
+ this.emitSessionEvent("deposit_session.addresses_created" /* ADDRESSES_CREATED */, {
2138
+ sessionId: this.id,
2139
+ addresses: this.addresses
2140
+ });
2141
+ this.notify();
2142
+ this.startDetectionLoop(token);
2143
+ if (this.confirmationMode === "auto") {
2144
+ this.armTimer = setTimeout(() => {
2145
+ if (token === this.runToken) this.armConfirmation("auto");
2146
+ }, DETECTION_ARM_DELAY_MS);
2147
+ }
2148
+ }
2149
+ failFatally(code, message, cause) {
2150
+ this.clearTimers();
2151
+ this.startPromise = null;
2152
+ this.error = { code, message, fatal: true, cause };
2153
+ this.setStatus("error");
2154
+ this.commit();
2155
+ this.emitSessionEvent("deposit_session.errored" /* SESSION_ERRORED */, {
2156
+ sessionId: this.id,
2157
+ code,
2158
+ message,
2159
+ fatal: true
2160
+ });
2161
+ this.notify();
2162
+ }
2163
+ async createAddressesWithRetry(token) {
2164
+ let lastError;
2165
+ for (let attempt = 0; attempt < ADDRESS_CREATE_MAX_ATTEMPTS; attempt++) {
2166
+ if (attempt > 0) {
2167
+ await delay(Math.min(1e3 * 2 ** (attempt - 1), 1e4));
2168
+ if (token !== this.runToken) throw new Error("DepositSession stopped");
2169
+ }
2170
+ try {
2171
+ const response = await createDepositAddress(
2172
+ {
2173
+ external_user_id: this.externalUserId,
2174
+ destination_chain_type: this.destination.chainType,
2175
+ destination_chain_id: this.destination.chainId,
2176
+ destination_token_address: this.destination.tokenAddress,
2177
+ recipient_address: this.destination.recipientAddress,
2178
+ contract_calls: this.destination.contractCalls
2179
+ },
2180
+ this.publishableKey
2181
+ );
2182
+ return response.data;
2183
+ } catch (error) {
2184
+ lastError = error;
2185
+ }
2186
+ }
2187
+ throw lastError;
2188
+ }
2189
+ /**
2190
+ * Fail-fast recipient validation (e.g. Algorand asset opt-in). Fails open
2191
+ * on network errors — the backend still enforces at execution time — but a
2192
+ * definitive negative result is fatal.
2193
+ *
2194
+ * Deliberately NOT IP/geo-aware: generating deposit addresses headless
2195
+ * carries no region gate. Hosts that want the modal's geo behavior render
2196
+ * against the opt-in `useAllowedCountry` hook instead.
2197
+ */
2198
+ async runStartChecks() {
2199
+ const recipientValid = await verifyRecipientAddress(
2200
+ {
2201
+ chain_type: this.destination.chainType,
2202
+ chain_id: this.destination.chainId,
2203
+ token_address: this.destination.tokenAddress,
2204
+ recipient_address: this.destination.recipientAddress
2205
+ },
2206
+ this.publishableKey
2207
+ ).then((result) => result.valid).catch(() => null);
2208
+ if (recipientValid === false) {
2209
+ throw new SessionCheckError(
2210
+ "INVALID_RECIPIENT",
2211
+ "Recipient address cannot receive funds for this destination"
2212
+ );
2213
+ }
2214
+ }
2215
+ // -- Detection polling (port of useDepositPolling Effect 2) ----------------
2216
+ startDetectionLoop(token) {
2217
+ const poll = () => void this.pollExecutions(token);
2218
+ poll();
2219
+ this.detectionTimer = setInterval(poll, DETECTION_POLL_INTERVAL_MS);
2220
+ }
2221
+ async pollExecutions(token) {
2222
+ try {
2223
+ const response = await queryExecutions(
2224
+ this.externalUserId,
2225
+ this.publishableKey,
2226
+ "deposit" /* Deposit */
2227
+ );
2228
+ if (token !== this.runToken) return;
2229
+ if (this.pollErrorLatched) {
2230
+ this.pollErrorLatched = false;
2231
+ if (this.error && !this.error.fatal) {
2232
+ this.error = null;
2233
+ this.commit();
2234
+ this.notify();
2235
+ }
2236
+ }
2237
+ const cutoffMs = this.baselineMs - LOOKBACK_MS;
2238
+ const sorted = [...response.data].sort((a, b) => {
2239
+ const timeA = a.created_at ? new Date(a.created_at).getTime() : 0;
2240
+ const timeB = b.created_at ? new Date(b.created_at).getTime() : 0;
2241
+ return timeB - timeA;
2242
+ });
2243
+ let candidate = null;
2244
+ for (const execution of sorted) {
2245
+ const createdMs = execution.created_at ? new Date(execution.created_at).getTime() : NaN;
2246
+ if (!Number.isFinite(createdMs) || createdMs < cutoffMs) continue;
2247
+ const trackedStatus = this.tracked.get(execution.id);
2248
+ const isTerminal = execution.status === "succeeded" /* SUCCEEDED */ || FAILURE_STATUSES.includes(execution.status);
2249
+ if (trackedStatus === void 0 && createdMs < this.baselineMs && isTerminal) {
2250
+ continue;
2251
+ }
2252
+ if (trackedStatus === void 0 || trackedStatus !== execution.status) {
2253
+ candidate = execution;
2254
+ break;
2255
+ }
2256
+ }
2257
+ if (!candidate) return;
2258
+ this.processExecutionChange(candidate);
2259
+ } catch (error) {
2260
+ if (token !== this.runToken) return;
2261
+ console.error("[unifold] failed to fetch executions:", error);
2262
+ if (!this.pollErrorLatched) {
2263
+ this.pollErrorLatched = true;
2264
+ this.error = {
2265
+ code: "POLLING_ERROR",
2266
+ message: "Failed to fetch deposit status",
2267
+ fatal: false,
2268
+ cause: error
2269
+ };
2270
+ this.commit();
2271
+ this.emitSessionEvent("deposit_session.errored" /* SESSION_ERRORED */, {
2272
+ sessionId: this.id,
2273
+ code: "POLLING_ERROR",
2274
+ message: "Failed to fetch deposit status",
2275
+ fatal: false
2276
+ });
2277
+ this.notify();
2278
+ }
2279
+ }
2280
+ }
2281
+ processExecutionChange(wire) {
2282
+ const previousStatus = this.tracked.get(wire.id) ?? null;
2283
+ this.tracked.set(wire.id, wire.status);
2284
+ const execution = mapDirectExecution(wire);
2285
+ const existingIndex = this.executions.findIndex((e) => e.id === execution.id);
2286
+ if (existingIndex >= 0) {
2287
+ this.executions = this.executions.map(
2288
+ (e, i) => i === existingIndex ? execution : e
2289
+ );
2290
+ } else {
2291
+ this.executions = [...this.executions, execution].sort((a, b) => {
2292
+ const timeA = a.createdAt ? new Date(a.createdAt).getTime() : 0;
2293
+ const timeB = b.createdAt ? new Date(b.createdAt).getTime() : 0;
2294
+ return timeB - timeA;
2295
+ });
2296
+ }
2297
+ if (wire.status === "succeeded" /* SUCCEEDED */) {
2298
+ this.setStatus("succeeded");
2299
+ } else if (FAILURE_STATUSES.includes(wire.status)) {
2300
+ this.setStatus("failed");
2301
+ } else if (IN_PROGRESS_STATUSES.includes(wire.status)) {
2302
+ this.setStatus("processing");
2303
+ }
2304
+ this.commit();
2305
+ const eventCreated = this.executionEventTimestamp(wire);
2306
+ if (previousStatus === null) {
2307
+ this.emitExecutionEvent(
2308
+ "direct_execution.detected" /* EXECUTION_DETECTED */,
2309
+ execution,
2310
+ eventCreated
2311
+ );
2312
+ } else {
2313
+ this.emitExecutionEvent(
2314
+ "direct_execution.updated" /* EXECUTION_UPDATED */,
2315
+ { ...execution, previousStatus },
2316
+ eventCreated
2317
+ );
2318
+ }
2319
+ const wasInProgressOrNew = previousStatus === null || IN_PROGRESS_STATUSES.includes(previousStatus);
2320
+ if (wire.status === "succeeded" /* SUCCEEDED */ && wasInProgressOrNew) {
2321
+ this.emitExecutionEvent(
2322
+ "direct_execution.succeeded" /* EXECUTION_SUCCEEDED */,
2323
+ execution,
2324
+ eventCreated
2325
+ );
2326
+ } else if (FAILURE_STATUSES.includes(wire.status) && (previousStatus === null || !FAILURE_STATUSES.includes(previousStatus))) {
2327
+ this.emitExecutionEvent(
2328
+ "direct_execution.failed" /* EXECUTION_FAILED */,
2329
+ execution,
2330
+ eventCreated
2331
+ );
2332
+ }
2333
+ this.notify();
2334
+ }
2335
+ // -- Scan nudge (port of useDepositPolling Effects 1 + 3) ------------------
2336
+ armConfirmation(trigger) {
2337
+ if (this.checkingDeposit || this.destroyed) return;
2338
+ if (this.armTimer) {
2339
+ clearTimeout(this.armTimer);
2340
+ this.armTimer = null;
2341
+ }
2342
+ this.checkingDeposit = true;
2343
+ this.commit();
2344
+ this.emitSessionEvent("deposit_session.confirmation_started" /* CONFIRMATION_STARTED */, {
2345
+ sessionId: this.id,
2346
+ trigger
2347
+ });
2348
+ this.notify();
2349
+ const token = this.runToken;
2350
+ const nudge = () => {
2351
+ if (token !== this.runToken) return;
2352
+ void Promise.all(
2353
+ this.addressIds.map(
2354
+ (id) => pollDirectExecutions({ deposit_wallet_id: id }, this.publishableKey).catch(
2355
+ () => {
2356
+ }
2357
+ )
2358
+ )
2359
+ );
2360
+ };
2361
+ nudge();
2362
+ this.nudgeTimer = setInterval(nudge, SCAN_NUDGE_INTERVAL_MS);
2363
+ }
2364
+ // -- Internals --------------------------------------------------------------
2365
+ setStatus(status) {
2366
+ this.status = status;
2367
+ }
2368
+ clearTimers() {
2369
+ if (this.detectionTimer) {
2370
+ clearInterval(this.detectionTimer);
2371
+ this.detectionTimer = null;
2372
+ }
2373
+ if (this.nudgeTimer) {
2374
+ clearInterval(this.nudgeTimer);
2375
+ this.nudgeTimer = null;
2376
+ }
2377
+ if (this.armTimer) {
2378
+ clearTimeout(this.armTimer);
2379
+ this.armTimer = null;
2380
+ }
2381
+ }
2382
+ buildSnapshot() {
2383
+ return {
2384
+ status: this.status,
2385
+ addresses: this.addresses,
2386
+ executions: this.executions,
2387
+ latestExecution: this.executions[0] ?? null,
2388
+ isCheckingDeposit: this.checkingDeposit,
2389
+ error: this.error
2390
+ };
2391
+ }
2392
+ /** Rebuild the snapshot so getSnapshot() reflects current state. */
2393
+ commit() {
2394
+ this.snapshot = this.buildSnapshot();
2395
+ }
2396
+ notify() {
2397
+ this.listeners.forEach((listener) => {
2398
+ try {
2399
+ listener();
2400
+ } catch (error) {
2401
+ console.error("[unifold] snapshot listener threw", error);
2402
+ }
2403
+ });
2404
+ }
2405
+ executionEventTimestamp(wire) {
2406
+ if (wire.updated_at) return Math.floor(new Date(wire.updated_at).getTime() / 1e3);
2407
+ if (wire.created_at) return Math.floor(new Date(wire.created_at).getTime() / 1e3);
2408
+ return Math.floor(Date.now() / 1e3);
2409
+ }
2410
+ emitSessionEvent(type, object) {
2411
+ this.emitter.emit(type, {
2412
+ id: generatePrefixedKSUID("sevt"),
2413
+ type,
2414
+ created: Math.floor(Date.now() / 1e3),
2415
+ method: this.method,
2416
+ data: { object }
2417
+ });
2418
+ }
2419
+ emitExecutionEvent(type, object, created) {
2420
+ this.emitter.emit(type, {
2421
+ id: generatePrefixedKSUID("sevt"),
2422
+ type,
2423
+ created,
2424
+ method: this.method,
2425
+ data: { object }
2426
+ });
2427
+ }
2428
+ };
2429
+
2430
+ // src/lib/client.ts
2431
+ var UnifoldClient = class {
2432
+ constructor(options) {
2433
+ __publicField(this, "publishableKey");
2434
+ const { publishableKey } = options;
2435
+ if (!publishableKey || publishableKey.trim() === "") {
2436
+ throw new Error("Unifold: publishableKey is required");
2437
+ }
2438
+ if (!publishableKey.startsWith("pk_test_") && !publishableKey.startsWith("pk_live_")) {
2439
+ console.warn(
2440
+ 'Unifold: publishableKey should start with "pk_test_" or "pk_live_".'
2441
+ );
2442
+ }
2443
+ this.publishableKey = publishableKey;
2444
+ }
2445
+ /** Create a headless deposit-session flow controller. */
2446
+ createDepositSession(params) {
2447
+ return new DepositSession({ ...params, publishableKey: this.publishableKey });
2448
+ }
2449
+ /**
2450
+ * Create (idempotently) and return the user's deposit addresses for a
2451
+ * destination — `POST /v1/public/deposit_addresses`.
2452
+ */
2453
+ async getDepositAddresses(params) {
2454
+ const response = await createDepositAddress(
2455
+ {
2456
+ external_user_id: params.externalUserId,
2457
+ destination_chain_type: params.destination.chainType,
2458
+ destination_chain_id: params.destination.chainId,
2459
+ destination_token_address: params.destination.tokenAddress,
2460
+ recipient_address: params.destination.recipientAddress,
2461
+ contract_calls: params.destination.contractCalls
2462
+ },
2463
+ this.publishableKey
2464
+ );
2465
+ return response.data.map(mapWalletToDepositAddress);
2466
+ }
2467
+ /** List the user's executions — `POST /v1/public/direct_executions/query`. */
2468
+ async listExecutions(params) {
2469
+ const response = await queryExecutions(
2470
+ params.externalUserId,
2471
+ this.publishableKey,
2472
+ params.actionType ?? "deposit" /* Deposit */
2473
+ );
2474
+ return response.data.map(mapDirectExecution);
2475
+ }
2476
+ /** Source tokens/chains a user can deposit from for a destination. */
2477
+ async getSupportedDepositTokens(params) {
2478
+ const response = await getSupportedDepositTokens(
2479
+ this.publishableKey,
2480
+ params?.destination || params?.productType ? {
2481
+ ...params.destination ? {
2482
+ destination_chain_type: params.destination.chainType,
2483
+ destination_chain_id: params.destination.chainId,
2484
+ destination_token_address: params.destination.tokenAddress
2485
+ } : {},
2486
+ ...params.productType ? { product_type: params.productType } : {}
2487
+ } : void 0
2488
+ );
2489
+ return response.data;
2490
+ }
2491
+ /** Validate a recipient address for a destination (e.g. Algorand opt-in). */
2492
+ async verifyAddress(params) {
2493
+ const response = await verifyRecipientAddress(
2494
+ {
2495
+ chain_type: params.chainType,
2496
+ chain_id: params.chainId,
2497
+ token_address: params.tokenAddress,
2498
+ recipient_address: params.recipientAddress
2499
+ },
2500
+ this.publishableKey
2501
+ );
2502
+ return {
2503
+ valid: response.valid,
2504
+ failureCode: response.failure_code ?? null,
2505
+ metadata: response.metadata ?? null
2506
+ };
2507
+ }
2508
+ /** Project-level configuration (feature flags, blocked countries, ...). */
2509
+ getProjectConfig(options) {
2510
+ return getProjectConfig(this.publishableKey, options);
2511
+ }
2512
+ };
2513
+ function createUnifoldClient(options) {
2514
+ return new UnifoldClient(options);
2515
+ }
2516
+
1633
2517
  // src/hooks/use-user-ip.ts
1634
2518
  var import_react_query = require("@tanstack/react-query");
1635
2519
  function useUserIp() {
@@ -1765,12 +2649,21 @@ var i18n = en_default;
1765
2649
  0 && (module.exports = {
1766
2650
  ActionType,
1767
2651
  CheckoutEventType,
2652
+ DETECTION_ARM_DELAY_MS,
2653
+ DETECTION_POLL_INTERVAL_MS,
2654
+ DepositAddressValidationError,
1768
2655
  DepositEventType,
2656
+ DepositSession,
2657
+ DepositSessionEventType,
2658
+ DepositSessionWaitError,
1769
2659
  ExecutionStatus,
1770
2660
  IneligibilityReason,
1771
2661
  IntegrationProvider,
2662
+ LOOKBACK_MS,
2663
+ SCAN_NUDGE_INTERVAL_MS,
1772
2664
  SOLANA_USDC_ADDRESS,
1773
2665
  StripeApiResponseError,
2666
+ UnifoldClient,
1774
2667
  WithdrawEventType,
1775
2668
  authenticateIntegrationOAuth,
1776
2669
  buildHypercoreTransaction,
@@ -1784,6 +2677,7 @@ var i18n = en_default;
1784
2677
  createIntegrationTransfer,
1785
2678
  createOnrampSession,
1786
2679
  createOnrampVerificationSession,
2680
+ createUnifoldClient,
1787
2681
  exchangeOnrampVerificationToken,
1788
2682
  formatStablecoinAmount,
1789
2683
  generateKSUID,
@@ -1806,6 +2700,7 @@ var i18n = en_default;
1806
2700
  getExchanges,
1807
2701
  getExternalWallets,
1808
2702
  getFiatCurrencies,
2703
+ getFiatExchangeRates,
1809
2704
  getIconUrl,
1810
2705
  getIconUrlWithCdn,
1811
2706
  getIntegrationExchanges,
@@ -1817,6 +2712,7 @@ var i18n = en_default;
1817
2712
  getOnrampVerificationSession,
1818
2713
  getPreferredIconUrl,
1819
2714
  getProjectConfig,
2715
+ getPublicIncident,
1820
2716
  getSupportedDepositTokens,
1821
2717
  getSupportedDestinationTokens,
1822
2718
  getTokenChains,
@@ -1825,7 +2721,10 @@ var i18n = en_default;
1825
2721
  getWalletMobileDeepLink,
1826
2722
  i18n,
1827
2723
  isApplePayLimitReached,
2724
+ isDepositAddressValidationError,
1828
2725
  listPaymentIntentExecutions,
2726
+ mapDirectExecution,
2727
+ mapWalletToDepositAddress,
1829
2728
  pollDirectExecutions,
1830
2729
  queryExecutions,
1831
2730
  refreshIntegrationToken,