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