@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.d.mts +762 -23
- package/dist/index.d.ts +762 -23
- package/dist/index.js +1192 -35
- package/dist/index.mjs +1182 -35
- package/package.json +1 -1
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.
|
|
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
|
|
971
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
2391
|
+
DepositSessionEventType.EXECUTION_SUCCEEDED,
|
|
2238
2392
|
(event) => settle(() => resolve(event.data.object))
|
|
2239
2393
|
),
|
|
2240
|
-
this.on(
|
|
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(
|
|
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(
|
|
2691
|
+
this.emitExecutionEvent(DepositSessionEventType.EXECUTION_DETECTED, execution, eventCreated);
|
|
2538
2692
|
} else {
|
|
2539
2693
|
this.emitExecutionEvent(
|
|
2540
|
-
|
|
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(
|
|
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(
|
|
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/
|
|
2652
|
-
var
|
|
2653
|
-
|
|
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
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
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 (!
|
|
2660
|
-
|
|
2909
|
+
if (!config.externalUserId) {
|
|
2910
|
+
throw new Error("OnrampSession: externalUserId is required");
|
|
2661
2911
|
}
|
|
2662
|
-
|
|
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
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
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,
|