@unifold/core 0.1.74 → 0.1.76

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
@@ -31,11 +31,16 @@ __export(index_exports, {
31
31
  DepositSession: () => DepositSession,
32
32
  DepositSessionEventType: () => DepositSessionEventType,
33
33
  DepositSessionWaitError: () => DepositSessionWaitError,
34
+ DirectExecutionEventType: () => DirectExecutionEventType,
34
35
  ExecutionStatus: () => ExecutionStatus,
35
36
  IneligibilityReason: () => IneligibilityReason,
36
37
  IntegrationProvider: () => IntegrationProvider,
37
38
  IntegrationTransferError: () => IntegrationTransferError,
38
39
  LOOKBACK_MS: () => LOOKBACK_MS,
40
+ OnrampSession: () => OnrampSession,
41
+ OnrampSessionEventType: () => OnrampSessionEventType,
42
+ OnrampSessionWaitError: () => OnrampSessionWaitError,
43
+ QUOTE_REFRESH_INTERVAL_MS: () => QUOTE_REFRESH_INTERVAL_MS,
39
44
  SCAN_NUDGE_INTERVAL_MS: () => SCAN_NUDGE_INTERVAL_MS,
40
45
  SOLANA_USDC_ADDRESS: () => SOLANA_USDC_ADDRESS,
41
46
  StripeApiResponseError: () => StripeApiResponseError,
@@ -52,6 +57,7 @@ __export(index_exports, {
52
57
  createCoinbaseWalletPaySession: () => createCoinbaseWalletPaySession,
53
58
  createDepositAddress: () => createDepositAddress,
54
59
  createExchangeSession: () => createExchangeSession,
60
+ createIntegrationExchangeSession: () => createIntegrationExchangeSession,
55
61
  createIntegrationTransfer: () => createIntegrationTransfer,
56
62
  createOnrampSession: () => createOnrampSession,
57
63
  createOnrampVerificationSession: () => createOnrampVerificationSession,
@@ -84,12 +90,14 @@ __export(index_exports, {
84
90
  getGooglePayProviders: () => getGooglePayProviders,
85
91
  getIconUrl: () => getIconUrl,
86
92
  getIconUrlWithCdn: () => getIconUrlWithCdn,
93
+ getIntegrationExchangeSessionStartUrl: () => getIntegrationExchangeSessionStartUrl,
87
94
  getIntegrationExchanges: () => getIntegrationExchanges,
88
95
  getIntegrationHoldings: () => getIntegrationHoldings,
89
96
  getIntegrationTransferDefaultToken: () => getIntegrationTransferDefaultToken,
90
97
  getIpAddress: () => getIpAddress,
91
98
  getOnrampQuotes: () => getOnrampQuotes,
92
99
  getOnrampSessionStartUrl: () => getOnrampSessionStartUrl,
100
+ getOnrampSessionStatus: () => getOnrampSessionStatus,
93
101
  getOnrampVerificationSession: () => getOnrampVerificationSession,
94
102
  getPreferredIconUrl: () => getPreferredIconUrl,
95
103
  getProjectConfig: () => getProjectConfig,
@@ -108,7 +116,9 @@ __export(index_exports, {
108
116
  isGooglePayLimitReached: () => isGooglePayLimitReached,
109
117
  isWalletPayLimitReached: () => isWalletPayLimitReached,
110
118
  listPaymentIntentExecutions: () => listPaymentIntentExecutions,
119
+ mapDefaultOnrampToken: () => mapDefaultOnrampToken,
111
120
  mapDirectExecution: () => mapDirectExecution,
121
+ mapOnrampQuote: () => mapOnrampQuote,
112
122
  mapWalletToDepositAddress: () => mapWalletToDepositAddress,
113
123
  pollDirectExecutions: () => pollDirectExecutions,
114
124
  queryExecutions: () => queryExecutions,
@@ -181,7 +191,7 @@ function generatePrefixedKSUID(prefix) {
181
191
  }
182
192
 
183
193
  // src/lib/client-headers.ts
184
- var SDK_VERSION = true ? "0.1.74" : "0.0.0-dev";
194
+ var SDK_VERSION = true ? "0.1.76" : "0.0.0-dev";
185
195
  var CLIENT_VERSION_HEADER = "x-unifold-client-version";
186
196
  var CLIENT_USER_AGENT_HEADER = "x-unifold-client-user-agent";
187
197
  function detectRuntime() {
@@ -679,6 +689,7 @@ async function getDefaultOnrampToken(params, publishableKey) {
679
689
  const queryParams = new URLSearchParams();
680
690
  if (params.country_code) queryParams.append("country_code", params.country_code);
681
691
  if (params.subdivision_code) queryParams.append("subdivision_code", params.subdivision_code);
692
+ if (params.service_provider) queryParams.append("service_provider", params.service_provider);
682
693
  queryParams.append("token_address", params.token_address);
683
694
  queryParams.append("chain_id", params.chain_id);
684
695
  queryParams.append("chain_type", params.chain_type);
@@ -943,6 +954,25 @@ async function createExchangeSession(request, publishableKey) {
943
954
  }
944
955
  return response.json();
945
956
  }
957
+ async function getOnrampSessionStatus(externalId, publishableKey, signal) {
958
+ const pk = publishableKey || DEFAULT_PUBLISHABLE_KEY;
959
+ validatePublishableKey(pk);
960
+ const response = await apiFetch(`${API_BASE_URL}/v1/public/onramps/sessions/status`, {
961
+ method: "POST",
962
+ headers: {
963
+ accept: "application/json",
964
+ "x-publishable-key": pk,
965
+ "Content-Type": "application/json"
966
+ },
967
+ body: JSON.stringify({ external_id: externalId }),
968
+ signal
969
+ });
970
+ if (!response.ok) {
971
+ const error = await response.json().catch(() => ({ message: response.statusText }));
972
+ throw new Error(`Failed to get session status: ${error.message || response.statusText}`);
973
+ }
974
+ return response.json();
975
+ }
946
976
  function getExchangeSessionStartUrl(request, publishableKey) {
947
977
  const params = new URLSearchParams();
948
978
  params.append("publishable_key", publishableKey);
@@ -961,24 +991,95 @@ function getExchangeSessionStartUrl(request, publishableKey) {
961
991
  if (request.source_amount) {
962
992
  params.append("source_amount", request.source_amount);
963
993
  }
994
+ if (request.country_code) {
995
+ params.append("country_code", request.country_code);
996
+ }
997
+ if (request.subdivision_code) {
998
+ params.append("subdivision_code", request.subdivision_code);
999
+ }
964
1000
  params.append("external_id", request.external_id ?? generatePrefixedKSUID("exc"));
965
1001
  return `${API_BASE_URL}/v1/public/onramps/exchanges/sessions/start?${params.toString()}`;
966
1002
  }
967
- async function getIntegrationExchanges(publishableKey) {
1003
+ async function getIntegrationExchanges(publishableKey, query) {
968
1004
  const pk = publishableKey || DEFAULT_PUBLISHABLE_KEY;
969
1005
  validatePublishableKey(pk);
970
- const response = await apiFetch(`${API_BASE_URL}/v1/public/integrations/oauth/exchanges`, {
971
- method: "GET",
1006
+ const params = new URLSearchParams();
1007
+ if (query?.country_code) params.append("country_code", query.country_code);
1008
+ if (query?.subdivision_code) params.append("subdivision_code", query.subdivision_code);
1009
+ const queryString = params.toString() ? `?${params.toString()}` : "";
1010
+ const headers = {
1011
+ accept: "application/json",
1012
+ "x-publishable-key": pk
1013
+ };
1014
+ const response = await apiFetch(
1015
+ `${API_BASE_URL}/v1/public/integrations/exchanges${queryString}`,
1016
+ {
1017
+ method: "GET",
1018
+ headers
1019
+ }
1020
+ );
1021
+ if (response.ok) {
1022
+ return response.json();
1023
+ }
1024
+ if (response.status === 404) {
1025
+ const legacyResponse = await apiFetch(
1026
+ `${API_BASE_URL}/v1/public/integrations/oauth/exchanges${queryString}`,
1027
+ {
1028
+ method: "GET",
1029
+ headers
1030
+ }
1031
+ );
1032
+ if (legacyResponse.ok) {
1033
+ return legacyResponse.json();
1034
+ }
1035
+ throw new Error(`Failed to fetch integration exchanges: ${legacyResponse.statusText}`);
1036
+ }
1037
+ throw new Error(`Failed to fetch integration exchanges: ${response.statusText}`);
1038
+ }
1039
+ async function createIntegrationExchangeSession(request, publishableKey) {
1040
+ const pk = publishableKey || DEFAULT_PUBLISHABLE_KEY;
1041
+ validatePublishableKey(pk);
1042
+ const response = await apiFetch(`${API_BASE_URL}/v1/public/integrations/exchanges/sessions`, {
1043
+ method: "POST",
972
1044
  headers: {
973
1045
  accept: "application/json",
974
- "x-publishable-key": pk
975
- }
1046
+ "x-publishable-key": pk,
1047
+ "Content-Type": "application/json"
1048
+ },
1049
+ body: JSON.stringify(request)
976
1050
  });
977
1051
  if (!response.ok) {
978
- throw new Error(`Failed to fetch integration exchanges: ${response.statusText}`);
1052
+ throw new Error(`Failed to create integration exchange session: ${response.statusText}`);
979
1053
  }
980
1054
  return response.json();
981
1055
  }
1056
+ function getIntegrationExchangeSessionStartUrl(request, publishableKey) {
1057
+ const params = new URLSearchParams();
1058
+ params.append("publishable_key", publishableKey);
1059
+ params.append("service_provider", request.service_provider);
1060
+ params.append("chain_type", request.chain_type);
1061
+ params.append("address", request.address);
1062
+ if (request.preferred_destination_currency) {
1063
+ params.append("preferred_destination_currency", request.preferred_destination_currency);
1064
+ }
1065
+ if (request.preferred_destination_network) {
1066
+ params.append("preferred_destination_network", request.preferred_destination_network);
1067
+ }
1068
+ if (request.source_currency) {
1069
+ params.append("source_currency", request.source_currency);
1070
+ }
1071
+ if (request.source_amount) {
1072
+ params.append("source_amount", request.source_amount);
1073
+ }
1074
+ if (request.country_code) {
1075
+ params.append("country_code", request.country_code);
1076
+ }
1077
+ if (request.subdivision_code) {
1078
+ params.append("subdivision_code", request.subdivision_code);
1079
+ }
1080
+ params.append("external_id", request.external_id ?? generatePrefixedKSUID("exc"));
1081
+ return `${API_BASE_URL}/v1/public/integrations/exchanges/sessions/start?${params.toString()}`;
1082
+ }
982
1083
  async function startIntegrationOAuth(publishableKey) {
983
1084
  const pk = publishableKey || DEFAULT_PUBLISHABLE_KEY;
984
1085
  validatePublishableKey(pk);
@@ -1872,10 +1973,17 @@ function createCoinbaseGooglePaySession(request, onrampToken, publishableKey, si
1872
1973
  }
1873
1974
 
1874
1975
  // src/lib/events.ts
1976
+ var DirectExecutionEventType = /* @__PURE__ */ ((DirectExecutionEventType2) => {
1977
+ DirectExecutionEventType2["DETECTED"] = "direct_execution.detected";
1978
+ DirectExecutionEventType2["UPDATED"] = "direct_execution.updated";
1979
+ DirectExecutionEventType2["SUCCEEDED"] = "direct_execution.succeeded";
1980
+ DirectExecutionEventType2["FAILED"] = "direct_execution.failed";
1981
+ return DirectExecutionEventType2;
1982
+ })(DirectExecutionEventType || {});
1875
1983
  var DepositEventType = /* @__PURE__ */ ((DepositEventType2) => {
1876
1984
  DepositEventType2["ONRAMP_SESSION_CREATED"] = "onramp_session.created";
1877
- DepositEventType2["DIRECT_EXECUTION_SUCCEEDED"] = "direct_execution.succeeded";
1878
- DepositEventType2["DIRECT_EXECUTION_FAILED"] = "direct_execution.failed";
1985
+ DepositEventType2["DIRECT_EXECUTION_SUCCEEDED"] = "direct_execution.succeeded" /* SUCCEEDED */;
1986
+ DepositEventType2["DIRECT_EXECUTION_FAILED"] = "direct_execution.failed" /* FAILED */;
1879
1987
  DepositEventType2["METHOD_SELECTED"] = "deposit.method_selected";
1880
1988
  DepositEventType2["TOKEN_SELECTED"] = "deposit.token_selected";
1881
1989
  DepositEventType2["WALLET_SELECTED"] = "deposit.wallet_selected";
@@ -1895,8 +2003,8 @@ var DepositEventType = /* @__PURE__ */ ((DepositEventType2) => {
1895
2003
  return DepositEventType2;
1896
2004
  })(DepositEventType || {});
1897
2005
  var WithdrawEventType = /* @__PURE__ */ ((WithdrawEventType2) => {
1898
- WithdrawEventType2["DIRECT_EXECUTION_SUCCEEDED"] = "direct_execution.succeeded";
1899
- WithdrawEventType2["DIRECT_EXECUTION_FAILED"] = "direct_execution.failed";
2006
+ WithdrawEventType2["DIRECT_EXECUTION_SUCCEEDED"] = "direct_execution.succeeded" /* SUCCEEDED */;
2007
+ WithdrawEventType2["DIRECT_EXECUTION_FAILED"] = "direct_execution.failed" /* FAILED */;
1900
2008
  WithdrawEventType2["TOKEN_SELECTED"] = "withdraw.token_selected";
1901
2009
  WithdrawEventType2["FLOW_STARTED"] = "withdraw.flow_started";
1902
2010
  return WithdrawEventType2;
@@ -1995,6 +2103,52 @@ function mapDirectExecution(execution) {
1995
2103
  } : void 0
1996
2104
  };
1997
2105
  }
2106
+ function mapOnrampQuote(quote) {
2107
+ return {
2108
+ serviceProvider: quote.service_provider,
2109
+ serviceProviderDisplayName: quote.service_provider_display_name,
2110
+ paymentMethodType: quote.payment_method_type,
2111
+ sourceCurrency: quote.source_currency,
2112
+ countryCode: quote.country_code,
2113
+ sourceAmount: quote.source_amount,
2114
+ sourceAmountWithoutFees: quote.source_amount_without_fees,
2115
+ totalFee: quote.total_fee,
2116
+ networkFee: quote.network_fee,
2117
+ transactionFee: quote.transaction_fee,
2118
+ partnerFee: quote.partner_fee,
2119
+ destinationAmount: quote.destination_amount,
2120
+ destinationAmountWithoutFees: quote.destination_amount_without_fees,
2121
+ destinationCurrency: quote.destination_currency,
2122
+ destinationNetwork: quote.destination_network,
2123
+ exchangeRate: quote.exchange_rate,
2124
+ customerScore: quote.customer_score,
2125
+ lowKyc: quote.low_kyc,
2126
+ iconUrl: quote.icon_url,
2127
+ iconUrls: quote.icon_urls,
2128
+ institutionName: quote.institution_name
2129
+ };
2130
+ }
2131
+ function mapDefaultOnrampToken(response) {
2132
+ const metadata = response.destination_token_metadata;
2133
+ return {
2134
+ network: response.destination_network,
2135
+ currency: response.destination_currency,
2136
+ isStablecoin: response.is_stablecoin ?? false,
2137
+ token: {
2138
+ symbol: metadata.symbol,
2139
+ name: metadata.name,
2140
+ tokenAddress: metadata.token_address,
2141
+ decimals: metadata.decimals,
2142
+ chainId: metadata.chain_id,
2143
+ chainType: metadata.chain_type,
2144
+ chainName: metadata.chain_name,
2145
+ iconUrl: metadata.icon_url,
2146
+ iconUrls: metadata.icon_urls,
2147
+ chainIconUrl: metadata.chain?.icon_url ?? ""
2148
+ },
2149
+ estimatedProcessingTimeSeconds: response.estimated_processing_time
2150
+ };
2151
+ }
1998
2152
 
1999
2153
  // src/lib/deposit-session.ts
2000
2154
  var DETECTION_POLL_INTERVAL_MS = 2500;
@@ -2002,16 +2156,16 @@ var SCAN_NUDGE_INTERVAL_MS = 5e3;
2002
2156
  var DETECTION_ARM_DELAY_MS = 5e3;
2003
2157
  var LOOKBACK_MS = 6e4;
2004
2158
  var ADDRESS_CREATE_MAX_ATTEMPTS = 4;
2005
- var DepositSessionEventType = /* @__PURE__ */ ((DepositSessionEventType2) => {
2159
+ var DepositSessionEventType = ((DepositSessionEventType2) => {
2006
2160
  DepositSessionEventType2["SESSION_STARTED"] = "deposit_session.started";
2007
2161
  DepositSessionEventType2["ADDRESSES_CREATED"] = "deposit_session.addresses_created";
2008
2162
  DepositSessionEventType2["CONFIRMATION_STARTED"] = "deposit_session.confirmation_started";
2009
2163
  DepositSessionEventType2["SESSION_STOPPED"] = "deposit_session.stopped";
2010
2164
  DepositSessionEventType2["SESSION_ERRORED"] = "deposit_session.errored";
2011
- DepositSessionEventType2["EXECUTION_DETECTED"] = "direct_execution.detected";
2012
- DepositSessionEventType2["EXECUTION_UPDATED"] = "direct_execution.updated";
2013
- DepositSessionEventType2["EXECUTION_SUCCEEDED"] = "direct_execution.succeeded";
2014
- DepositSessionEventType2["EXECUTION_FAILED"] = "direct_execution.failed";
2165
+ DepositSessionEventType2[DepositSessionEventType2["EXECUTION_DETECTED"] = "direct_execution.detected" /* DETECTED */] = "EXECUTION_DETECTED";
2166
+ DepositSessionEventType2[DepositSessionEventType2["EXECUTION_UPDATED"] = "direct_execution.updated" /* UPDATED */] = "EXECUTION_UPDATED";
2167
+ DepositSessionEventType2[DepositSessionEventType2["EXECUTION_SUCCEEDED"] = "direct_execution.succeeded" /* SUCCEEDED */] = "EXECUTION_SUCCEEDED";
2168
+ DepositSessionEventType2[DepositSessionEventType2["EXECUTION_FAILED"] = "direct_execution.failed" /* FAILED */] = "EXECUTION_FAILED";
2015
2169
  return DepositSessionEventType2;
2016
2170
  })(DepositSessionEventType || {});
2017
2171
  var IN_PROGRESS_STATUSES = [
@@ -2234,17 +2388,17 @@ var DepositSession = class {
2234
2388
  }
2235
2389
  const offs = [
2236
2390
  this.on(
2237
- "direct_execution.succeeded" /* EXECUTION_SUCCEEDED */,
2391
+ DepositSessionEventType.EXECUTION_SUCCEEDED,
2238
2392
  (event) => settle(() => resolve(event.data.object))
2239
2393
  ),
2240
- this.on("direct_execution.failed" /* EXECUTION_FAILED */, (event) => {
2394
+ this.on(DepositSessionEventType.EXECUTION_FAILED, (event) => {
2241
2395
  if (this.anyExecutionInFlight()) return;
2242
2396
  rejectFailure(event.data.object);
2243
2397
  }),
2244
2398
  // A previously-failed wait condition can become settleable when
2245
2399
  // the last in-flight execution also fails (updated → failed is
2246
2400
  // covered above; updated → refunded transitions re-check here).
2247
- this.on("direct_execution.updated" /* EXECUTION_UPDATED */, () => {
2401
+ this.on(DepositSessionEventType.EXECUTION_UPDATED, () => {
2248
2402
  if (this.anyExecutionInFlight() || this.firstSuccess) return;
2249
2403
  const failed = this.snapshot.executions.find(
2250
2404
  (execution) => FAILURE_STATUSES.includes(execution.status)
@@ -2534,18 +2688,18 @@ var DepositSession = class {
2534
2688
  this.commit();
2535
2689
  const eventCreated = this.executionEventTimestamp(wire);
2536
2690
  if (previousStatus === null) {
2537
- this.emitExecutionEvent("direct_execution.detected" /* EXECUTION_DETECTED */, execution, eventCreated);
2691
+ this.emitExecutionEvent(DepositSessionEventType.EXECUTION_DETECTED, execution, eventCreated);
2538
2692
  } else {
2539
2693
  this.emitExecutionEvent(
2540
- "direct_execution.updated" /* EXECUTION_UPDATED */,
2694
+ DepositSessionEventType.EXECUTION_UPDATED,
2541
2695
  { ...execution, previousStatus },
2542
2696
  eventCreated
2543
2697
  );
2544
2698
  }
2545
2699
  if (wire.status === "succeeded" /* SUCCEEDED */ && wasInProgressOrNew) {
2546
- this.emitExecutionEvent("direct_execution.succeeded" /* EXECUTION_SUCCEEDED */, execution, eventCreated);
2700
+ this.emitExecutionEvent(DepositSessionEventType.EXECUTION_SUCCEEDED, execution, eventCreated);
2547
2701
  } else if (FAILURE_STATUSES.includes(wire.status) && (previousStatus === null || !FAILURE_STATUSES.includes(previousStatus))) {
2548
- this.emitExecutionEvent("direct_execution.failed" /* EXECUTION_FAILED */, execution, eventCreated);
2702
+ this.emitExecutionEvent(DepositSessionEventType.EXECUTION_FAILED, execution, eventCreated);
2549
2703
  }
2550
2704
  this.notify();
2551
2705
  }
@@ -2648,22 +2802,1015 @@ var DepositSession = class {
2648
2802
  }
2649
2803
  };
2650
2804
 
2651
- // src/lib/client.ts
2652
- var UnifoldClient = class {
2653
- constructor(options) {
2805
+ // src/lib/onramp-session.ts
2806
+ var QUOTE_REFRESH_INTERVAL_MS = 6e4;
2807
+ var ADDRESS_CREATE_MAX_ATTEMPTS2 = 4;
2808
+ var OnrampSessionEventType = ((OnrampSessionEventType2) => {
2809
+ OnrampSessionEventType2["SESSION_STARTED"] = "onramp_session.started";
2810
+ OnrampSessionEventType2["ADDRESSES_CREATED"] = "onramp_session.addresses_created";
2811
+ OnrampSessionEventType2["QUOTES_UPDATED"] = "onramp_session.quotes_updated";
2812
+ OnrampSessionEventType2[OnrampSessionEventType2["CHECKOUT_CREATED"] = "onramp_session.created" /* ONRAMP_SESSION_CREATED */] = "CHECKOUT_CREATED";
2813
+ OnrampSessionEventType2["SESSION_STOPPED"] = "onramp_session.stopped";
2814
+ OnrampSessionEventType2["SESSION_ERRORED"] = "onramp_session.errored";
2815
+ OnrampSessionEventType2[OnrampSessionEventType2["EXECUTION_DETECTED"] = "direct_execution.detected" /* DETECTED */] = "EXECUTION_DETECTED";
2816
+ OnrampSessionEventType2[OnrampSessionEventType2["EXECUTION_UPDATED"] = "direct_execution.updated" /* UPDATED */] = "EXECUTION_UPDATED";
2817
+ OnrampSessionEventType2[OnrampSessionEventType2["EXECUTION_SUCCEEDED"] = "direct_execution.succeeded" /* SUCCEEDED */] = "EXECUTION_SUCCEEDED";
2818
+ OnrampSessionEventType2[OnrampSessionEventType2["EXECUTION_FAILED"] = "direct_execution.failed" /* FAILED */] = "EXECUTION_FAILED";
2819
+ return OnrampSessionEventType2;
2820
+ })(OnrampSessionEventType || {});
2821
+ var FAILURE_STATUSES2 = ["failed" /* FAILED */, "refunded" /* REFUNDED */];
2822
+ var IN_PROGRESS_STATUSES2 = [
2823
+ "pending" /* PENDING */,
2824
+ "waiting" /* WAITING */,
2825
+ "delayed" /* DELAYED */
2826
+ ];
2827
+ var OnrampSessionWaitError = class extends Error {
2828
+ constructor(code, message, cause) {
2829
+ super(message);
2830
+ __publicField(this, "code");
2831
+ /** Failed execution (`DEPOSIT_FAILED`) or fatal {@link OnrampSessionError} (`SESSION_ERROR`). */
2832
+ __publicField(this, "cause");
2833
+ this.name = "OnrampSessionWaitError";
2834
+ this.code = code;
2835
+ this.cause = cause;
2836
+ }
2837
+ };
2838
+ var SessionCheckError2 = class extends Error {
2839
+ constructor(code, message) {
2840
+ super(message);
2841
+ this.code = code;
2842
+ }
2843
+ };
2844
+ var delay2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2845
+ var OnrampSession = class {
2846
+ constructor(config) {
2847
+ /** Immutable id for correlation, `osess_<ksuid>`. Client-generated. */
2848
+ __publicField(this, "id");
2849
+ __publicField(this, "emitter", new TypedEmitter());
2850
+ __publicField(this, "listeners", /* @__PURE__ */ new Set());
2654
2851
  __publicField(this, "publishableKey");
2655
- const { publishableKey } = options;
2656
- if (!publishableKey || publishableKey.trim() === "") {
2657
- throw new Error("Unifold: publishableKey is required");
2852
+ __publicField(this, "externalUserId");
2853
+ __publicField(this, "destination");
2854
+ __publicField(this, "paymentMethodType");
2855
+ __publicField(this, "email");
2856
+ __publicField(this, "quoteRefreshIntervalMs");
2857
+ __publicField(this, "method");
2858
+ // Mutable quote request (updateQuoteRequest). Exactly one of
2859
+ // sourceAmount / destinationAmount is truthy at a time (amount mode).
2860
+ // Country/subdivision: host-supplied values win; when absent they are
2861
+ // IP-detected during start() (see resolveCountry).
2862
+ __publicField(this, "explicitCountryCode");
2863
+ __publicField(this, "explicitSubdivisionCode");
2864
+ __publicField(this, "detectedCountryCode", null);
2865
+ __publicField(this, "detectedSubdivisionCode", null);
2866
+ __publicField(this, "sourceAmount");
2867
+ __publicField(this, "destinationAmount");
2868
+ __publicField(this, "sourceCurrency");
2869
+ // Run state. runToken invalidates in-flight async work across stop()/restart.
2870
+ __publicField(this, "runToken", 0);
2871
+ __publicField(this, "startPromise", null);
2872
+ __publicField(this, "destroyed", false);
2873
+ /** Pending waiter rejections, invoked by destroy() so waiters never hang. */
2874
+ __publicField(this, "waiterDestroyCallbacks", /* @__PURE__ */ new Set());
2875
+ /** In-flight destination-token + quotes pipeline (see syncQuoteInputs). */
2876
+ __publicField(this, "syncPromise", null);
2877
+ /** A sync was asked for mid-pipeline; the loop owes it another pass. */
2878
+ __publicField(this, "syncRequested", false);
2879
+ /** Code of the non-fatal error already reported, so a streak emits once. */
2880
+ __publicField(this, "nonFatalErrorLatch", null);
2881
+ /** True once run() got past preparing — gates live quote-input syncing. */
2882
+ __publicField(this, "prepared", false);
2883
+ __publicField(this, "refreshTimer", null);
2884
+ /** service_provider chosen via selectQuote(); sticky across quote refreshes. */
2885
+ __publicField(this, "manualSelection", null);
2886
+ /** First execution to succeed this run — waitForSuccess's one-shot answer. */
2887
+ __publicField(this, "firstSuccess", null);
2888
+ // Settlement watcher (composed DepositSession), created at createCheckout().
2889
+ __publicField(this, "watcher", null);
2890
+ __publicField(this, "watcherOffs", []);
2891
+ // Snapshot state
2892
+ __publicField(this, "status", "idle");
2893
+ __publicField(this, "addresses", []);
2894
+ __publicField(this, "destinationToken", null);
2895
+ /** Geo `destinationToken` was resolved for; null while unresolved. */
2896
+ __publicField(this, "destinationTokenGeoKey", null);
2897
+ __publicField(this, "quotes", []);
2898
+ __publicField(this, "selectedQuote", null);
2899
+ __publicField(this, "isRefreshingQuotes", false);
2900
+ __publicField(this, "quotesUpdatedAt", null);
2901
+ __publicField(this, "checkout", null);
2902
+ __publicField(this, "executions", []);
2903
+ __publicField(this, "checkingDeposit", false);
2904
+ __publicField(this, "error", null);
2905
+ __publicField(this, "snapshot");
2906
+ if (!config.publishableKey || config.publishableKey.trim() === "") {
2907
+ throw new Error("OnrampSession: publishableKey is required");
2658
2908
  }
2659
- if (!publishableKey.startsWith("pk_test_") && !publishableKey.startsWith("pk_live_")) {
2660
- console.warn('Unifold: publishableKey should start with "pk_test_" or "pk_live_".');
2909
+ if (!config.externalUserId) {
2910
+ throw new Error("OnrampSession: externalUserId is required");
2661
2911
  }
2662
- this.publishableKey = publishableKey;
2912
+ if (config.quoteRequest.sourceAmount && config.quoteRequest.destinationAmount) {
2913
+ throw new Error(
2914
+ "OnrampSession: provide exactly one of quoteRequest.sourceAmount / destinationAmount"
2915
+ );
2916
+ }
2917
+ this.id = generatePrefixedKSUID("osess");
2918
+ this.publishableKey = config.publishableKey;
2919
+ this.externalUserId = config.externalUserId;
2920
+ this.destination = config.destination;
2921
+ this.paymentMethodType = config.paymentMethodType ?? "card";
2922
+ this.email = config.email;
2923
+ this.quoteRefreshIntervalMs = config.quoteRefreshIntervalMs ?? QUOTE_REFRESH_INTERVAL_MS;
2924
+ this.method = this.paymentMethodType === "apple_pay" ? "apple_pay" : "card";
2925
+ this.explicitCountryCode = config.quoteRequest.countryCode;
2926
+ this.explicitSubdivisionCode = config.quoteRequest.subdivisionCode;
2927
+ this.sourceAmount = config.quoteRequest.sourceAmount;
2928
+ this.destinationAmount = config.quoteRequest.destinationAmount;
2929
+ this.sourceCurrency = config.quoteRequest.sourceCurrency ?? "usd";
2930
+ this.snapshot = this.buildSnapshot();
2663
2931
  }
2664
- /** Create a headless deposit-session flow controller. */
2665
- createDepositSession(params) {
2666
- return new DepositSession({ ...params, publishableKey: this.publishableKey });
2932
+ // -- Public surface -------------------------------------------------------
2933
+ /** Synchronous snapshot; the reference is stable until state changes. */
2934
+ getSnapshot() {
2935
+ return this.snapshot;
2936
+ }
2937
+ /**
2938
+ * Subscribe to snapshot changes (external-store contract; drives
2939
+ * `useSyncExternalStore` in the React binding). Returns an unsubscribe fn.
2940
+ */
2941
+ subscribe(listener) {
2942
+ this.listeners.add(listener);
2943
+ return () => {
2944
+ this.listeners.delete(listener);
2945
+ };
2946
+ }
2947
+ on(type, handler) {
2948
+ return this.emitter.on(type, handler);
2949
+ }
2950
+ /**
2951
+ * Creates/fetches addresses, resolves the onramp destination token (both
2952
+ * with fail-fast recipient validation), and fetches the first quotes.
2953
+ * Idempotent while running; callable again after stop() or a fatal error.
2954
+ */
2955
+ start() {
2956
+ if (this.destroyed) {
2957
+ return Promise.reject(new Error("OnrampSession has been destroyed"));
2958
+ }
2959
+ if (this.startPromise) return this.startPromise;
2960
+ this.startPromise = this.run();
2961
+ return this.startPromise;
2962
+ }
2963
+ /**
2964
+ * Update the quote request (amount / currency / country) and refetch
2965
+ * quotes. Debounce keystrokes host-side — every call that changes
2966
+ * something hits the quotes API. No-op after createCheckout().
2967
+ *
2968
+ * Amount-mode switching: patching `sourceAmount` while in destination mode
2969
+ * (or `destinationAmount` while in source mode) switches modes — the other
2970
+ * amount is cleared. Patching BOTH to truthy values in one call throws
2971
+ * (exactly one drives quoting).
2972
+ */
2973
+ updateQuoteRequest(patch) {
2974
+ if (this.destroyed) return;
2975
+ let nextSourceAmount = "sourceAmount" in patch ? patch.sourceAmount : this.sourceAmount;
2976
+ let nextDestinationAmount = "destinationAmount" in patch ? patch.destinationAmount : this.destinationAmount;
2977
+ if (nextSourceAmount && nextDestinationAmount) {
2978
+ const sourcePatched = !!patch.sourceAmount;
2979
+ const destinationPatched = !!patch.destinationAmount;
2980
+ if (sourcePatched && destinationPatched) {
2981
+ throw new Error(
2982
+ "OnrampSession.updateQuoteRequest: provide exactly one of sourceAmount / destinationAmount"
2983
+ );
2984
+ }
2985
+ if (destinationPatched) nextSourceAmount = void 0;
2986
+ else nextDestinationAmount = void 0;
2987
+ }
2988
+ const next = {
2989
+ countryCode: patch.countryCode ?? this.explicitCountryCode,
2990
+ subdivisionCode: "subdivisionCode" in patch ? patch.subdivisionCode : this.explicitSubdivisionCode,
2991
+ sourceAmount: nextSourceAmount,
2992
+ destinationAmount: nextDestinationAmount,
2993
+ sourceCurrency: patch.sourceCurrency ?? this.sourceCurrency
2994
+ };
2995
+ const changed = next.countryCode !== this.explicitCountryCode || next.subdivisionCode !== this.explicitSubdivisionCode || next.sourceAmount !== this.sourceAmount || next.destinationAmount !== this.destinationAmount || next.sourceCurrency !== this.sourceCurrency;
2996
+ this.explicitCountryCode = next.countryCode;
2997
+ this.explicitSubdivisionCode = next.subdivisionCode;
2998
+ this.sourceAmount = next.sourceAmount;
2999
+ this.destinationAmount = next.destinationAmount;
3000
+ this.sourceCurrency = next.sourceCurrency;
3001
+ if (!changed || this.checkout || !this.startPromise) return;
3002
+ if (this.prepared) {
3003
+ void this.syncQuoteInputs(this.runToken);
3004
+ }
3005
+ }
3006
+ /** Refetch quotes with the current request. Resolves when the fetch settles. */
3007
+ refreshQuotes() {
3008
+ if (this.destroyed || !this.startPromise || this.checkout || !this.prepared) {
3009
+ return Promise.resolve();
3010
+ }
3011
+ return this.syncQuoteInputs(this.runToken);
3012
+ }
3013
+ /**
3014
+ * Pick a provider quote by `serviceProvider`. The selection is sticky
3015
+ * across refreshes: while the provider keeps quoting it stays selected
3016
+ * (with fresh pricing); if it drops out, selection falls back to the
3017
+ * backend's top quote and auto-selection resumes.
3018
+ *
3019
+ * Returns the selected quote, or null when no quote matches.
3020
+ */
3021
+ selectQuote(serviceProvider) {
3022
+ if (this.destroyed) return null;
3023
+ const match = this.quotes.find((quote) => quote.serviceProvider === serviceProvider);
3024
+ if (!match) return null;
3025
+ this.manualSelection = serviceProvider;
3026
+ this.selectedQuote = match;
3027
+ this.commit();
3028
+ this.notify();
3029
+ return match;
3030
+ }
3031
+ /**
3032
+ * Build the provider-hosted checkout and start watching the deposit
3033
+ * addresses for the provider's on-chain settlement.
3034
+ *
3035
+ * Uses `snapshot.selectedQuote` (backend's top quote unless the host called
3036
+ * selectQuote()). Hosts that manage their own selection UI can instead pass
3037
+ * `options.serviceProvider` for a one-shot choice without mutating the
3038
+ * sticky selection.
3039
+ *
3040
+ * Synchronous by design: the URL is assembled locally (single-use token
3041
+ * exchange happens when it is opened), so hosts can `window.open()` the
3042
+ * result inside the click handler without tripping popup blockers.
3043
+ *
3044
+ * Throws when the session is not ready (no matching quote / addresses or
3045
+ * destination token missing) — gate your button on `status === 'ready'`
3046
+ * and `selectedQuote`.
3047
+ */
3048
+ createCheckout(options = {}) {
3049
+ if (this.destroyed) throw new Error("OnrampSession has been destroyed");
3050
+ const quote = options.serviceProvider ? this.quotes.find((q) => q.serviceProvider === options.serviceProvider) : this.selectedQuote;
3051
+ if (!quote) {
3052
+ throw new Error(
3053
+ options.serviceProvider ? `OnrampSession.createCheckout: no quote from '${options.serviceProvider}'` : "OnrampSession.createCheckout: no quote selected"
3054
+ );
3055
+ }
3056
+ if (!this.destinationToken) {
3057
+ throw new Error("OnrampSession.createCheckout: destination token not resolved yet");
3058
+ }
3059
+ const wallet = this.addresses.find(
3060
+ (address) => address.chainType === this.destinationToken?.token.chainType
3061
+ );
3062
+ if (!wallet) {
3063
+ throw new Error("OnrampSession.createCheckout: no deposit address for the onramp chain type");
3064
+ }
3065
+ const externalId = options.externalId ?? generatePrefixedKSUID("orsext");
3066
+ const request = {
3067
+ service_provider: quote.serviceProvider,
3068
+ country_code: (quote.countryCode || this.effectiveCountryCode()).toUpperCase(),
3069
+ source_currency: quote.sourceCurrency,
3070
+ // Exactly one of source_amount / destination_amount — the session's
3071
+ // current amount mode (quotes were fetched in the same mode, so the
3072
+ // quoted providers all support it).
3073
+ ...this.sourceAmount ? { source_amount: this.sourceAmount } : { destination_amount: this.destinationAmount },
3074
+ destination_currency: quote.destinationCurrency,
3075
+ destination_network: quote.destinationNetwork,
3076
+ wallet_address: wallet.address,
3077
+ subdivision_code: this.effectiveSubdivisionCode(),
3078
+ external_id: externalId,
3079
+ email: options.email ?? this.email,
3080
+ payment_method_type: quote.paymentMethodType ?? this.paymentMethodType
3081
+ };
3082
+ const checkout = {
3083
+ url: getOnrampSessionStartUrl(request, this.publishableKey),
3084
+ externalId,
3085
+ serviceProvider: quote.serviceProvider,
3086
+ quote,
3087
+ sourceAmount: this.sourceAmount ?? null,
3088
+ destinationAmount: this.sourceAmount ? null : this.destinationAmount ?? null,
3089
+ sourceCurrency: quote.sourceCurrency,
3090
+ createdAt: Date.now()
3091
+ };
3092
+ this.checkout = checkout;
3093
+ this.clearRefreshTimer();
3094
+ this.setStatus("awaiting_payment");
3095
+ this.commit();
3096
+ this.emitSessionEvent(OnrampSessionEventType.CHECKOUT_CREATED, {
3097
+ externalId,
3098
+ sessionId: this.id,
3099
+ url: checkout.url,
3100
+ serviceProvider: quote.serviceProvider
3101
+ });
3102
+ this.notify();
3103
+ this.startWatcher();
3104
+ return checkout;
3105
+ }
3106
+ /**
3107
+ * Stops quote refresh and settlement watching. The session can be
3108
+ * restarted with start(), which resets quotes/checkout/executions
3109
+ * (fresh run).
3110
+ */
3111
+ stop() {
3112
+ const wasActive = this.startPromise !== null;
3113
+ this.runToken += 1;
3114
+ this.clearRefreshTimer();
3115
+ this.teardownWatcher();
3116
+ this.startPromise = null;
3117
+ this.syncPromise = null;
3118
+ this.isRefreshingQuotes = false;
3119
+ this.checkingDeposit = false;
3120
+ if (this.status !== "idle" && this.status !== "error") {
3121
+ this.setStatus("idle");
3122
+ }
3123
+ if (wasActive && !this.destroyed) {
3124
+ this.commit();
3125
+ this.emitSessionEvent("onramp_session.stopped" /* SESSION_STOPPED */, { sessionId: this.id });
3126
+ this.notify();
3127
+ }
3128
+ }
3129
+ /** stop() + release all listeners. Terminal — start() rejects afterwards. */
3130
+ destroy() {
3131
+ this.stop();
3132
+ this.destroyed = true;
3133
+ Array.from(this.waiterDestroyCallbacks).forEach((callback) => callback());
3134
+ this.waiterDestroyCallbacks.clear();
3135
+ this.emitter.removeAllListeners();
3136
+ this.listeners.clear();
3137
+ }
3138
+ // -- Promise waiters (subscription sugar over the event stream) ------------
3139
+ /**
3140
+ * Resolve when the session reaches one of the given statuses (immediately
3141
+ * if it's already there) — e.g. `waitForStatus('ready')` awaits quotes,
3142
+ * `waitForStatus('processing')` awaits detection of the settlement.
3143
+ * Statuses carry no outcomes; await those with {@link waitForSuccess}.
3144
+ *
3145
+ * Rejects with {@link OnrampSessionWaitError} on abort or destroy().
3146
+ * Does not start or stop the session — it only listens.
3147
+ */
3148
+ waitForStatus(status, options = {}) {
3149
+ const statuses = Array.isArray(status) ? status : [status];
3150
+ return new Promise((resolve, reject) => {
3151
+ this.installWaiter({
3152
+ options,
3153
+ reject,
3154
+ subscribe: (settle) => {
3155
+ const check = () => {
3156
+ if (statuses.includes(this.snapshot.status)) settle(() => resolve(this.snapshot));
3157
+ };
3158
+ check();
3159
+ return this.subscribe(check);
3160
+ }
3161
+ });
3162
+ });
3163
+ }
3164
+ /**
3165
+ * Resolve with the **first** succeeded {@link DirectExecution} observed by
3166
+ * this session — same one-shot first-completion contract as
3167
+ * {@link DepositSession.waitForSuccess}. A failure only rejects
3168
+ * (`DEPOSIT_FAILED`) when no other observed execution is still in flight;
3169
+ * fatal session errors reject with `SESSION_ERROR`. The session keeps
3170
+ * watching after success — subscribe to `direct_execution.succeeded` to
3171
+ * react to every settlement.
3172
+ */
3173
+ waitForSuccess(options = {}) {
3174
+ return new Promise((resolve, reject) => {
3175
+ this.installWaiter({
3176
+ options,
3177
+ reject,
3178
+ subscribe: (settle) => {
3179
+ const rejectFailure = (failed) => settle(
3180
+ () => reject(new OnrampSessionWaitError("DEPOSIT_FAILED", "Deposit failed", failed))
3181
+ );
3182
+ if (this.firstSuccess) {
3183
+ const first = this.firstSuccess;
3184
+ settle(() => resolve(first));
3185
+ return () => {
3186
+ };
3187
+ }
3188
+ const alreadyFailed = this.executions.find(
3189
+ (execution) => FAILURE_STATUSES2.includes(execution.status)
3190
+ );
3191
+ if (alreadyFailed && !this.anyExecutionInFlight()) {
3192
+ rejectFailure(alreadyFailed);
3193
+ return () => {
3194
+ };
3195
+ }
3196
+ if (this.error?.fatal) {
3197
+ const fatal = this.error;
3198
+ settle(() => reject(new OnrampSessionWaitError("SESSION_ERROR", fatal.message, fatal)));
3199
+ return () => {
3200
+ };
3201
+ }
3202
+ const offs = [
3203
+ this.on(
3204
+ OnrampSessionEventType.EXECUTION_SUCCEEDED,
3205
+ (event) => settle(() => resolve(event.data.object))
3206
+ ),
3207
+ this.on(OnrampSessionEventType.EXECUTION_FAILED, (event) => {
3208
+ if (this.anyExecutionInFlight()) return;
3209
+ rejectFailure(event.data.object);
3210
+ }),
3211
+ this.on(OnrampSessionEventType.EXECUTION_UPDATED, () => {
3212
+ if (this.anyExecutionInFlight() || this.firstSuccess) return;
3213
+ const failed = this.executions.find(
3214
+ (execution) => FAILURE_STATUSES2.includes(execution.status)
3215
+ );
3216
+ if (failed) rejectFailure(failed);
3217
+ }),
3218
+ this.on("onramp_session.errored" /* SESSION_ERRORED */, (event) => {
3219
+ if (!event.data.object.fatal) return;
3220
+ settle(
3221
+ () => reject(
3222
+ new OnrampSessionWaitError(
3223
+ "SESSION_ERROR",
3224
+ event.data.object.message,
3225
+ event.data.object
3226
+ )
3227
+ )
3228
+ );
3229
+ })
3230
+ ];
3231
+ return () => offs.forEach((off) => off());
3232
+ }
3233
+ });
3234
+ });
3235
+ }
3236
+ /** Shared waiter plumbing — AbortSignal / destroy() rejection with single settlement. */
3237
+ installWaiter({
3238
+ options,
3239
+ reject,
3240
+ subscribe
3241
+ }) {
3242
+ if (this.destroyed) {
3243
+ reject(new OnrampSessionWaitError("DESTROYED", "OnrampSession has been destroyed"));
3244
+ return;
3245
+ }
3246
+ if (options.signal?.aborted) {
3247
+ reject(new OnrampSessionWaitError("ABORTED", "Wait aborted", options.signal.reason));
3248
+ return;
3249
+ }
3250
+ let settled = false;
3251
+ let unsubscribe = null;
3252
+ const cleanup = () => {
3253
+ unsubscribe?.();
3254
+ options.signal?.removeEventListener("abort", onAbort);
3255
+ this.waiterDestroyCallbacks.delete(onDestroy);
3256
+ };
3257
+ const settle = (finish) => {
3258
+ if (settled) return;
3259
+ settled = true;
3260
+ cleanup();
3261
+ finish();
3262
+ };
3263
+ const onAbort = () => settle(
3264
+ () => reject(new OnrampSessionWaitError("ABORTED", "Wait aborted", options.signal?.reason))
3265
+ );
3266
+ const onDestroy = () => settle(
3267
+ () => reject(
3268
+ new OnrampSessionWaitError("DESTROYED", "OnrampSession was destroyed while waiting")
3269
+ )
3270
+ );
3271
+ this.waiterDestroyCallbacks.add(onDestroy);
3272
+ options.signal?.addEventListener("abort", onAbort, { once: true });
3273
+ unsubscribe = subscribe(settle);
3274
+ if (settled) cleanup();
3275
+ }
3276
+ // -- Run lifecycle ---------------------------------------------------------
3277
+ async run() {
3278
+ const token = ++this.runToken;
3279
+ this.teardownWatcher();
3280
+ this.prepared = false;
3281
+ this.addresses = [];
3282
+ this.destinationToken = null;
3283
+ this.destinationTokenGeoKey = null;
3284
+ this.quotes = [];
3285
+ this.selectedQuote = null;
3286
+ this.manualSelection = null;
3287
+ this.quotesUpdatedAt = null;
3288
+ this.nonFatalErrorLatch = null;
3289
+ this.checkout = null;
3290
+ this.executions = [];
3291
+ this.firstSuccess = null;
3292
+ this.checkingDeposit = false;
3293
+ this.error = null;
3294
+ this.setStatus("preparing");
3295
+ this.commit();
3296
+ this.emitSessionEvent("onramp_session.started" /* SESSION_STARTED */, { sessionId: this.id });
3297
+ this.notify();
3298
+ let wallets;
3299
+ let destinationToken;
3300
+ try {
3301
+ [wallets, destinationToken] = await Promise.all([
3302
+ this.createAddressesWithRetry(token),
3303
+ this.resolveCountry().then(() => this.resolveDestinationToken()),
3304
+ this.runStartChecks()
3305
+ ]);
3306
+ } catch (cause) {
3307
+ if (token !== this.runToken) return;
3308
+ const isCheck = cause instanceof SessionCheckError2;
3309
+ this.failFatally(
3310
+ isCheck ? cause.code : "ADDRESS_CREATION_FAILED",
3311
+ isCheck ? cause.message : "Failed to prepare onramp session",
3312
+ cause
3313
+ );
3314
+ return;
3315
+ }
3316
+ if (token !== this.runToken) return;
3317
+ this.addresses = wallets.map(mapWalletToDepositAddress);
3318
+ this.destinationToken = destinationToken.token;
3319
+ this.destinationTokenGeoKey = destinationToken.geoKey;
3320
+ this.prepared = true;
3321
+ this.setStatus("quoting");
3322
+ this.commit();
3323
+ this.emitSessionEvent("onramp_session.addresses_created" /* ADDRESSES_CREATED */, {
3324
+ sessionId: this.id,
3325
+ addresses: this.addresses
3326
+ });
3327
+ this.notify();
3328
+ if (token !== this.runToken) return;
3329
+ await this.syncQuoteInputs(token);
3330
+ if (token !== this.runToken) return;
3331
+ if (this.quoteRefreshIntervalMs > 0) {
3332
+ this.refreshTimer = setInterval(() => {
3333
+ if (token !== this.runToken || this.checkout) return;
3334
+ void this.syncQuoteInputs(token);
3335
+ }, this.quoteRefreshIntervalMs);
3336
+ }
3337
+ }
3338
+ failFatally(code, message, cause) {
3339
+ this.clearRefreshTimer();
3340
+ this.startPromise = null;
3341
+ this.error = { code, message, fatal: true, cause };
3342
+ this.setStatus("error");
3343
+ this.commit();
3344
+ this.emitSessionEvent("onramp_session.errored" /* SESSION_ERRORED */, {
3345
+ sessionId: this.id,
3346
+ code,
3347
+ message,
3348
+ fatal: true
3349
+ });
3350
+ this.notify();
3351
+ }
3352
+ async createAddressesWithRetry(token) {
3353
+ let lastError;
3354
+ for (let attempt = 0; attempt < ADDRESS_CREATE_MAX_ATTEMPTS2; attempt++) {
3355
+ if (attempt > 0) {
3356
+ await delay2(Math.min(1e3 * 2 ** (attempt - 1), 1e4));
3357
+ if (token !== this.runToken) throw new Error("OnrampSession stopped");
3358
+ }
3359
+ try {
3360
+ const response = await createDepositAddress(
3361
+ {
3362
+ external_user_id: this.externalUserId,
3363
+ destination_chain_type: this.destination.chainType,
3364
+ destination_chain_id: this.destination.chainId,
3365
+ destination_token_address: this.destination.tokenAddress,
3366
+ recipient_address: this.destination.recipientAddress,
3367
+ contract_calls: this.destination.contractCalls
3368
+ },
3369
+ this.publishableKey
3370
+ );
3371
+ return response.data;
3372
+ } catch (error) {
3373
+ lastError = error;
3374
+ }
3375
+ }
3376
+ throw lastError;
3377
+ }
3378
+ /** Host-supplied geo wins; otherwise the IP-detected value; 'US' as a last resort. */
3379
+ effectiveCountryCode() {
3380
+ return this.explicitCountryCode || this.detectedCountryCode || "US";
3381
+ }
3382
+ /**
3383
+ * Explicit subdivision wins. A detected subdivision applies only while the
3384
+ * country is also detected — mixing a detected subdivision into an
3385
+ * explicitly-set country would pin quotes to a region of the wrong country.
3386
+ */
3387
+ effectiveSubdivisionCode() {
3388
+ if (this.explicitSubdivisionCode !== void 0) return this.explicitSubdivisionCode;
3389
+ if (!this.explicitCountryCode && this.detectedCountryCode) {
3390
+ return this.detectedSubdivisionCode ?? void 0;
3391
+ }
3392
+ return void 0;
3393
+ }
3394
+ /**
3395
+ * Auto-detect the payer's country from their IP when the host didn't
3396
+ * supply one — so integrators don't need to build geo plumbing to render
3397
+ * a buy screen. Never fatal: detection failure falls back to 'US' (modal
3398
+ * parity — BuyWithCard quotes with `userIpInfo?.alpha2 || 'US'`). The
3399
+ * detection result is cached for the session's lifetime (restarts reuse
3400
+ * it); an explicit countryCode — at construction or via
3401
+ * updateQuoteRequest — always overrides.
3402
+ */
3403
+ async resolveCountry() {
3404
+ if (this.explicitCountryCode || this.detectedCountryCode) return;
3405
+ try {
3406
+ const info = await getIpAddress();
3407
+ this.detectedCountryCode = info.alpha2 || "US";
3408
+ this.detectedSubdivisionCode = info.subdivision_code ?? null;
3409
+ } catch {
3410
+ this.detectedCountryCode = "US";
3411
+ this.detectedSubdivisionCode = null;
3412
+ }
3413
+ }
3414
+ /** Identity of the geo a destination-token resolution was made for. */
3415
+ geoKey() {
3416
+ return `${this.effectiveCountryCode()}|${this.effectiveSubdivisionCode() ?? ""}`;
3417
+ }
3418
+ /**
3419
+ * Resolve the provider-side network/currency the destination maps to. The
3420
+ * geo it was resolved for travels with the result: routing is
3421
+ * geo-dependent, so the caller has to know when a later country change
3422
+ * invalidates it.
3423
+ */
3424
+ async resolveDestinationToken() {
3425
+ const geoKey = this.geoKey();
3426
+ try {
3427
+ const response = await getDefaultOnrampToken(
3428
+ {
3429
+ country_code: this.effectiveCountryCode(),
3430
+ subdivision_code: this.effectiveSubdivisionCode(),
3431
+ token_address: this.destination.tokenAddress,
3432
+ chain_id: this.destination.chainId,
3433
+ chain_type: this.destination.chainType
3434
+ },
3435
+ this.publishableKey
3436
+ );
3437
+ return { token: mapDefaultOnrampToken(response), geoKey };
3438
+ } catch {
3439
+ throw new SessionCheckError2(
3440
+ "DESTINATION_TOKEN_FAILED",
3441
+ "No onramp route available for this destination token"
3442
+ );
3443
+ }
3444
+ }
3445
+ /**
3446
+ * Fail-fast recipient validation — parity with DepositSession. Fails open
3447
+ * on network errors (the backend still enforces at execution time), but a
3448
+ * definitive negative result is fatal.
3449
+ */
3450
+ async runStartChecks() {
3451
+ const recipientValid = await verifyRecipientAddress(
3452
+ {
3453
+ chain_type: this.destination.chainType,
3454
+ chain_id: this.destination.chainId,
3455
+ token_address: this.destination.tokenAddress,
3456
+ recipient_address: this.destination.recipientAddress
3457
+ },
3458
+ this.publishableKey
3459
+ ).then((result) => result.valid).catch(() => null);
3460
+ if (recipientValid === false) {
3461
+ throw new SessionCheckError2(
3462
+ "INVALID_RECIPIENT",
3463
+ "Recipient address cannot receive funds for this destination"
3464
+ );
3465
+ }
3466
+ }
3467
+ // -- Quotes ----------------------------------------------------------------
3468
+ /**
3469
+ * Bring the quote inputs and the destination token back in sync, then
3470
+ * fetch quotes. Destination-token routing is geo-dependent (the modal
3471
+ * re-runs its default-token effect whenever `userIpInfo` changes), so a
3472
+ * payer-country change has to re-resolve it first — quoting on the network
3473
+ * and currency routed for the previous geo would also carry into the
3474
+ * checkout built from those quotes.
3475
+ */
3476
+ syncQuoteInputs(token) {
3477
+ if (this.syncPromise) {
3478
+ this.syncRequested = true;
3479
+ return this.syncPromise;
3480
+ }
3481
+ this.syncPromise = Promise.resolve().then(() => this.syncQuoteInputsUntilFresh(token)).finally(() => {
3482
+ this.syncPromise = null;
3483
+ });
3484
+ return this.syncPromise;
3485
+ }
3486
+ /** Loops until the session state matches the inputs it was built from. */
3487
+ async syncQuoteInputsUntilFresh(token) {
3488
+ while (token === this.runToken && !this.checkout) {
3489
+ this.syncRequested = false;
3490
+ if (!this.destinationToken || this.destinationTokenGeoKey !== this.geoKey()) {
3491
+ if (!await this.resolveDestinationTokenForGeo(token) && !this.syncRequested) return;
3492
+ continue;
3493
+ }
3494
+ if (!await this.fetchQuotesOnce(token) && !this.syncRequested) return;
3495
+ }
3496
+ }
3497
+ /**
3498
+ * Re-resolve the destination token for the current geo. Returns false when
3499
+ * the run ended or the lookup failed.
3500
+ */
3501
+ async resolveDestinationTokenForGeo(token) {
3502
+ this.destinationToken = null;
3503
+ this.destinationTokenGeoKey = null;
3504
+ this.quotes = [];
3505
+ this.selectedQuote = null;
3506
+ this.isRefreshingQuotes = true;
3507
+ this.commit();
3508
+ this.notify();
3509
+ let resolved;
3510
+ try {
3511
+ resolved = await this.resolveDestinationToken();
3512
+ } catch (cause) {
3513
+ if (token !== this.runToken) return false;
3514
+ this.isRefreshingQuotes = false;
3515
+ this.raiseNonFatal(
3516
+ "DESTINATION_TOKEN_FAILED",
3517
+ "No onramp route available for this destination token",
3518
+ cause
3519
+ );
3520
+ return false;
3521
+ }
3522
+ if (token !== this.runToken) return false;
3523
+ this.destinationToken = resolved.token;
3524
+ this.destinationTokenGeoKey = resolved.geoKey;
3525
+ this.isRefreshingQuotes = false;
3526
+ this.commit();
3527
+ this.notify();
3528
+ return true;
3529
+ }
3530
+ /** Returns true when the request inputs moved and quotes must be refetched. */
3531
+ async fetchQuotesOnce(token) {
3532
+ const destinationToken = this.destinationToken;
3533
+ if (!destinationToken) return false;
3534
+ if (this.destinationAmount && !destinationToken.isStablecoin) {
3535
+ this.quotes = [];
3536
+ this.selectedQuote = null;
3537
+ if (this.status === "quoting") this.setStatus("ready");
3538
+ this.raiseNonFatal(
3539
+ "DESTINATION_AMOUNT_UNSUPPORTED",
3540
+ "Buying a fixed destination amount is only supported for stablecoin destinations"
3541
+ );
3542
+ return false;
3543
+ }
3544
+ const activeAmount = this.sourceAmount || this.destinationAmount || "";
3545
+ const amount = parseFloat(activeAmount);
3546
+ if (!Number.isFinite(amount) || amount <= 0) {
3547
+ this.quotes = [];
3548
+ this.selectedQuote = null;
3549
+ if (this.status === "quoting") this.setStatus("ready");
3550
+ this.commit();
3551
+ this.notify();
3552
+ return false;
3553
+ }
3554
+ const requestSourceAmount = this.sourceAmount;
3555
+ const requestDestinationAmount = this.destinationAmount;
3556
+ const requestCurrency = this.sourceCurrency;
3557
+ const requestCountry = this.effectiveCountryCode();
3558
+ const requestSubdivision = this.effectiveSubdivisionCode();
3559
+ this.isRefreshingQuotes = true;
3560
+ this.commit();
3561
+ this.notify();
3562
+ try {
3563
+ const response = await getOnrampQuotes(
3564
+ {
3565
+ country_code: requestCountry,
3566
+ // Exactly one of source_amount / destination_amount (amount mode).
3567
+ ...requestSourceAmount ? { source_amount: requestSourceAmount } : { destination_amount: requestDestinationAmount },
3568
+ source_currency: requestCurrency.toLowerCase(),
3569
+ destination_currency: destinationToken.currency,
3570
+ destination_network: destinationToken.network,
3571
+ subdivision_code: requestSubdivision
3572
+ },
3573
+ this.publishableKey
3574
+ );
3575
+ if (token !== this.runToken || this.checkout) return false;
3576
+ const stale = requestSourceAmount !== this.sourceAmount || requestDestinationAmount !== this.destinationAmount || requestCurrency !== this.sourceCurrency || requestCountry !== this.effectiveCountryCode() || requestSubdivision !== this.effectiveSubdivisionCode();
3577
+ if (!stale) {
3578
+ this.quotes = response.data.map(mapOnrampQuote);
3579
+ this.reconcileSelection();
3580
+ this.quotesUpdatedAt = Date.now();
3581
+ if (this.nonFatalErrorLatch) {
3582
+ this.nonFatalErrorLatch = null;
3583
+ if (this.error && !this.error.fatal) this.error = null;
3584
+ }
3585
+ this.commit();
3586
+ this.emitSessionEvent("onramp_session.quotes_updated" /* QUOTES_UPDATED */, {
3587
+ sessionId: this.id,
3588
+ quotes: this.quotes,
3589
+ selectedQuote: this.selectedQuote
3590
+ });
3591
+ }
3592
+ } catch (cause) {
3593
+ if (token !== this.runToken || this.checkout) return false;
3594
+ console.error("[unifold] failed to fetch onramp quotes:", cause);
3595
+ this.quotes = [];
3596
+ this.selectedQuote = null;
3597
+ this.raiseNonFatal("QUOTES_FAILED", "Failed to fetch onramp quotes", cause);
3598
+ } finally {
3599
+ if (token === this.runToken) {
3600
+ this.isRefreshingQuotes = false;
3601
+ if (this.status === "quoting") this.setStatus("ready");
3602
+ this.commit();
3603
+ this.notify();
3604
+ }
3605
+ }
3606
+ return token === this.runToken && !this.checkout && this.startPromise !== null && (requestSourceAmount !== this.sourceAmount || requestDestinationAmount !== this.destinationAmount || requestCurrency !== this.sourceCurrency || requestCountry !== this.effectiveCountryCode() || requestSubdivision !== this.effectiveSubdivisionCode());
3607
+ }
3608
+ /**
3609
+ * Record a non-fatal error and announce it once per streak — auto-refresh
3610
+ * keeps retrying, and repeating the event every 60s would be noise.
3611
+ */
3612
+ raiseNonFatal(code, message, cause) {
3613
+ const alreadyReported = this.nonFatalErrorLatch === code;
3614
+ if (!alreadyReported) {
3615
+ this.nonFatalErrorLatch = code;
3616
+ this.error = { code, message, fatal: false, cause };
3617
+ }
3618
+ this.commit();
3619
+ if (!alreadyReported) {
3620
+ this.emitSessionEvent("onramp_session.errored" /* SESSION_ERRORED */, {
3621
+ sessionId: this.id,
3622
+ code,
3623
+ message,
3624
+ fatal: false
3625
+ });
3626
+ }
3627
+ this.notify();
3628
+ }
3629
+ /** Sticky manual selection: keep the host's provider while it still quotes. */
3630
+ reconcileSelection() {
3631
+ if (this.manualSelection) {
3632
+ const match = this.quotes.find((quote) => quote.serviceProvider === this.manualSelection);
3633
+ if (match) {
3634
+ this.selectedQuote = match;
3635
+ return;
3636
+ }
3637
+ this.manualSelection = null;
3638
+ }
3639
+ this.selectedQuote = this.quotes[0] ?? null;
3640
+ }
3641
+ // -- Settlement watcher (composed DepositSession) ---------------------------
3642
+ /**
3643
+ * Start (once) the composed {@link DepositSession} that watches the deposit
3644
+ * addresses for the provider's on-chain settlement. Its baseline starts at
3645
+ * checkout time — correct for card rails, where funds can only arrive after
3646
+ * the user pays at the provider. Detection polling, the backend scan nudge
3647
+ * (auto-armed), the lookback window, and `direct_execution.*` semantics are
3648
+ * all inherited rather than reimplemented.
3649
+ */
3650
+ startWatcher() {
3651
+ if (this.watcher) return;
3652
+ const watcher = new DepositSession({
3653
+ publishableKey: this.publishableKey,
3654
+ externalUserId: this.externalUserId,
3655
+ destination: this.destination,
3656
+ confirmationMode: "auto",
3657
+ method: this.method
3658
+ });
3659
+ this.watcher = watcher;
3660
+ const offEvents = watcher.on("*", (event) => {
3661
+ switch (event.type) {
3662
+ case DepositSessionEventType.EXECUTION_DETECTED:
3663
+ case DepositSessionEventType.EXECUTION_UPDATED:
3664
+ case DepositSessionEventType.EXECUTION_SUCCEEDED:
3665
+ case DepositSessionEventType.EXECUTION_FAILED:
3666
+ this.syncFromWatcher(watcher);
3667
+ if (event.type === DepositSessionEventType.EXECUTION_SUCCEEDED && !this.firstSuccess) {
3668
+ this.firstSuccess = event.data.object;
3669
+ }
3670
+ this.forwardExecutionEvent(event.type, event);
3671
+ break;
3672
+ case "deposit_session.errored" /* SESSION_ERRORED */: {
3673
+ const { code, message, fatal } = event.data.object;
3674
+ this.syncFromWatcher(watcher);
3675
+ if (fatal) {
3676
+ this.error = { code, message, fatal: true };
3677
+ this.setStatus("error");
3678
+ this.commit();
3679
+ }
3680
+ this.emitSessionEvent("onramp_session.errored" /* SESSION_ERRORED */, {
3681
+ sessionId: this.id,
3682
+ code,
3683
+ message,
3684
+ fatal
3685
+ });
3686
+ break;
3687
+ }
3688
+ default:
3689
+ break;
3690
+ }
3691
+ });
3692
+ const offSnapshot = watcher.subscribe(() => {
3693
+ this.syncFromWatcher(watcher);
3694
+ this.notify();
3695
+ });
3696
+ this.watcherOffs = [offEvents, offSnapshot];
3697
+ void watcher.start().catch(() => {
3698
+ });
3699
+ }
3700
+ syncFromWatcher(watcher) {
3701
+ if (this.destroyed || this.watcher !== watcher) return;
3702
+ const inner = watcher.getSnapshot();
3703
+ this.executions = inner.executions;
3704
+ this.checkingDeposit = inner.isCheckingDeposit;
3705
+ if (this.status !== "error") {
3706
+ if (inner.error && !inner.error.fatal) {
3707
+ this.error = {
3708
+ code: inner.error.code,
3709
+ message: inner.error.message,
3710
+ fatal: false,
3711
+ cause: inner.error.cause
3712
+ };
3713
+ } else if (!inner.error && this.error && !this.error.fatal) {
3714
+ this.error = null;
3715
+ }
3716
+ this.setStatus(inner.status === "processing" ? "processing" : "awaiting_payment");
3717
+ }
3718
+ this.commit();
3719
+ }
3720
+ forwardExecutionEvent(type, event) {
3721
+ this.emitter.emit(
3722
+ type,
3723
+ {
3724
+ id: event.id,
3725
+ type,
3726
+ created: event.created,
3727
+ method: this.method,
3728
+ data: event.data
3729
+ }
3730
+ );
3731
+ }
3732
+ teardownWatcher() {
3733
+ this.watcherOffs.forEach((off) => off());
3734
+ this.watcherOffs = [];
3735
+ this.watcher?.destroy();
3736
+ this.watcher = null;
3737
+ }
3738
+ // -- Internals --------------------------------------------------------------
3739
+ anyExecutionInFlight() {
3740
+ return this.executions.some((execution) => IN_PROGRESS_STATUSES2.includes(execution.status));
3741
+ }
3742
+ setStatus(status) {
3743
+ this.status = status;
3744
+ }
3745
+ clearRefreshTimer() {
3746
+ if (this.refreshTimer) {
3747
+ clearInterval(this.refreshTimer);
3748
+ this.refreshTimer = null;
3749
+ }
3750
+ }
3751
+ buildSnapshot() {
3752
+ return {
3753
+ status: this.status,
3754
+ countryCode: this.explicitCountryCode || this.detectedCountryCode || null,
3755
+ addresses: this.addresses,
3756
+ destinationToken: this.destinationToken,
3757
+ quotes: this.quotes,
3758
+ selectedQuote: this.selectedQuote,
3759
+ isQuoteAutoSelected: this.manualSelection === null,
3760
+ canSelectProvider: this.quotes.length > 1,
3761
+ isRefreshingQuotes: this.isRefreshingQuotes,
3762
+ quotesUpdatedAt: this.quotesUpdatedAt,
3763
+ checkout: this.checkout,
3764
+ executions: this.executions,
3765
+ latestExecution: this.executions[0] ?? null,
3766
+ isCheckingDeposit: this.checkingDeposit,
3767
+ error: this.error
3768
+ };
3769
+ }
3770
+ /** Rebuild the snapshot so getSnapshot() reflects current state. */
3771
+ commit() {
3772
+ this.snapshot = this.buildSnapshot();
3773
+ }
3774
+ notify() {
3775
+ this.listeners.forEach((listener) => {
3776
+ try {
3777
+ listener();
3778
+ } catch (error) {
3779
+ console.error("[unifold] snapshot listener threw", error);
3780
+ }
3781
+ });
3782
+ }
3783
+ emitSessionEvent(type, object) {
3784
+ this.emitter.emit(type, {
3785
+ id: generatePrefixedKSUID("sevt"),
3786
+ type,
3787
+ created: Math.floor(Date.now() / 1e3),
3788
+ method: this.method,
3789
+ data: { object }
3790
+ });
3791
+ }
3792
+ };
3793
+
3794
+ // src/lib/client.ts
3795
+ var UnifoldClient = class {
3796
+ constructor(options) {
3797
+ __publicField(this, "publishableKey");
3798
+ const { publishableKey } = options;
3799
+ if (!publishableKey || publishableKey.trim() === "") {
3800
+ throw new Error("Unifold: publishableKey is required");
3801
+ }
3802
+ if (!publishableKey.startsWith("pk_test_") && !publishableKey.startsWith("pk_live_")) {
3803
+ console.warn('Unifold: publishableKey should start with "pk_test_" or "pk_live_".');
3804
+ }
3805
+ this.publishableKey = publishableKey;
3806
+ }
3807
+ /** Create a headless deposit-session flow controller. */
3808
+ createDepositSession(params) {
3809
+ return new DepositSession({ ...params, publishableKey: this.publishableKey });
3810
+ }
3811
+ /** Create a headless fiat-onramp flow controller (buy with card by default). */
3812
+ createOnrampSession(params) {
3813
+ return new OnrampSession({ ...params, publishableKey: this.publishableKey });
2667
3814
  }
2668
3815
  /**
2669
3816
  * Create (idempotently) and return the user's deposit addresses for a
@@ -2875,11 +4022,16 @@ var i18n = en_default;
2875
4022
  DepositSession,
2876
4023
  DepositSessionEventType,
2877
4024
  DepositSessionWaitError,
4025
+ DirectExecutionEventType,
2878
4026
  ExecutionStatus,
2879
4027
  IneligibilityReason,
2880
4028
  IntegrationProvider,
2881
4029
  IntegrationTransferError,
2882
4030
  LOOKBACK_MS,
4031
+ OnrampSession,
4032
+ OnrampSessionEventType,
4033
+ OnrampSessionWaitError,
4034
+ QUOTE_REFRESH_INTERVAL_MS,
2883
4035
  SCAN_NUDGE_INTERVAL_MS,
2884
4036
  SOLANA_USDC_ADDRESS,
2885
4037
  StripeApiResponseError,
@@ -2896,6 +4048,7 @@ var i18n = en_default;
2896
4048
  createCoinbaseWalletPaySession,
2897
4049
  createDepositAddress,
2898
4050
  createExchangeSession,
4051
+ createIntegrationExchangeSession,
2899
4052
  createIntegrationTransfer,
2900
4053
  createOnrampSession,
2901
4054
  createOnrampVerificationSession,
@@ -2928,12 +4081,14 @@ var i18n = en_default;
2928
4081
  getGooglePayProviders,
2929
4082
  getIconUrl,
2930
4083
  getIconUrlWithCdn,
4084
+ getIntegrationExchangeSessionStartUrl,
2931
4085
  getIntegrationExchanges,
2932
4086
  getIntegrationHoldings,
2933
4087
  getIntegrationTransferDefaultToken,
2934
4088
  getIpAddress,
2935
4089
  getOnrampQuotes,
2936
4090
  getOnrampSessionStartUrl,
4091
+ getOnrampSessionStatus,
2937
4092
  getOnrampVerificationSession,
2938
4093
  getPreferredIconUrl,
2939
4094
  getProjectConfig,
@@ -2952,7 +4107,9 @@ var i18n = en_default;
2952
4107
  isGooglePayLimitReached,
2953
4108
  isWalletPayLimitReached,
2954
4109
  listPaymentIntentExecutions,
4110
+ mapDefaultOnrampToken,
2955
4111
  mapDirectExecution,
4112
+ mapOnrampQuote,
2956
4113
  mapWalletToDepositAddress,
2957
4114
  pollDirectExecutions,
2958
4115
  queryExecutions,