@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
1,067 lines • 52 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ExchangeType = void 0;
exports.safeGetRefValue = safeGetRefValue;
exports.useSetWalletAPIAccounts = useSetWalletAPIAccounts;
exports.useDAppManifestCurrencyIds = useDAppManifestCurrencyIds;
exports.usePermission = usePermission;
exports.useConfig = useConfig;
exports.useWalletAPIServer = useWalletAPIServer;
exports.useCategories = useCategories;
exports.useLocalLiveApp = useLocalLiveApp;
exports.useCacheBustedLiveApps = useCacheBustedLiveApps;
exports.useRecentlyUsed = useRecentlyUsed;
exports.useDisclaimerRaw = useDisclaimerRaw;
const react_1 = require("react");
const react_redux_1 = require("react-redux");
const semver_1 = __importDefault(require("semver"));
const date_fns_1 = require("date-fns");
const wallet_api_server_1 = require("@ledgerhq/wallet-api-server");
const operators_1 = require("rxjs/operators");
const live_env_1 = require("@ledgerhq/live-env");
const errors_1 = require("@ledgerhq/errors");
const api_1 = require("@ledgerhq/cryptoassets/cal-client/state-manager/api");
const FeatureFlagsContext_1 = require("../featureFlags/FeatureFlagsContext");
const converters_1 = require("./converters");
const helpers_1 = require("./helpers");
const account_1 = require("../account");
const currencies_1 = require("../currencies");
const state_1 = require("@ledgerhq/cryptoassets/state");
const logic_1 = require("./logic");
const FeatureFlags_1 = require("./FeatureFlags");
const bridge_1 = require("../bridge");
const openTransportAsSubject_1 = __importDefault(require("../hw/openTransportAsSubject"));
const constants_1 = require("./constants");
const useCurrenciesUnderFeatureFlag_1 = require("../modularDrawer/hooks/useCurrenciesUnderFeatureFlag");
function safeGetRefValue(ref) {
if (!ref.current) {
throw new Error("Ref objects doesn't have a current value");
}
return ref.current;
}
function useSetWalletAPIAccounts(accounts) {
(0, react_1.useEffect)(() => {
accounts.forEach(account => {
(0, converters_1.setWalletApiIdForAccountId)(account.id);
});
}, [accounts]);
}
function useDAppManifestCurrencyIds(manifest) {
return (0, react_1.useMemo)(() => {
return (manifest.dapp?.networks.map(network => {
return network.currency;
}) ?? []);
}, [manifest.dapp?.networks]);
}
function usePermission(manifest) {
return (0, react_1.useMemo)(() => ({
methodIds: manifest.permissions,
}), [manifest]);
}
function useTransport(postMessage) {
return (0, react_1.useMemo)(() => {
return {
onMessage: undefined,
send: postMessage,
};
}, [postMessage]);
}
function useConfig({ appId, userId, tracking, wallet, mevProtected, }) {
return (0, react_1.useMemo)(() => ({
appId,
userId,
tracking,
wallet,
mevProtected,
}), [appId, mevProtected, tracking, userId, wallet]);
}
function useDeviceTransport({ manifest, tracking }) {
const ref = (0, react_1.useRef)(undefined);
const subscribe = (0, react_1.useCallback)((deviceId) => {
ref.current = (0, openTransportAsSubject_1.default)({ deviceId });
ref.current.subscribe({
complete: () => {
ref.current = undefined;
},
});
}, []);
const close = (0, react_1.useCallback)(() => {
ref.current?.complete();
}, []);
const exchange = (0, react_1.useCallback)(({ apduHex }) => {
const subject$ = ref.current;
return new Promise((resolve, reject) => {
if (!subject$) {
reject(new Error("No device transport"));
return;
}
subject$.pipe((0, operators_1.first)(e => e.type === "device-response" || e.type === "error")).subscribe({
next: e => {
if (e.type === "device-response") {
tracking.deviceExchangeSuccess(manifest);
resolve(e.data);
return;
}
if (e.type === "error") {
tracking.deviceExchangeFail(manifest);
reject(e.error || new Error("deviceExchange: unknown error"));
}
},
error: error => {
tracking.deviceExchangeFail(manifest);
reject(error);
},
});
subject$.next({ type: "input-frame", apduHex });
});
}, [manifest, tracking]);
(0, react_1.useEffect)(() => {
return () => {
ref.current?.complete();
};
}, []);
return (0, react_1.useMemo)(() => ({ ref, subscribe, close, exchange }), [close, exchange, subscribe]);
}
function useWalletAPIServer({ walletState, manifest, accounts, tracking, config, webviewHook, uiHook: { "account.request": uiAccountRequest, "account.receive": uiAccountReceive, "message.sign": uiMessageSign, "storage.get": uiStorageGet, "storage.set": uiStorageSet, "transaction.sign": uiTxSign, "transaction.signRaw": uiTxSignRaw, "transaction.broadcast": uiTxBroadcast, "device.transport": uiDeviceTransport, "device.select": uiDeviceSelect, "exchange.start": uiExchangeStart, "exchange.complete": uiExchangeComplete, }, customHandlers, }) {
// Enables the proper typing on dispatch with RTK
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const dispatch = (0, react_redux_1.useDispatch)();
const { deactivatedCurrencyIds } = (0, useCurrenciesUnderFeatureFlag_1.useCurrenciesUnderFeatureFlag)();
const { getFeature } = (0, FeatureFlagsContext_1.useFeatureFlags)();
const permission = usePermission(manifest);
const transport = useTransport(webviewHook.postMessage);
const [widgetLoaded, setWidgetLoaded] = (0, react_1.useState)(false);
// We need to set the wallet API account IDs mapping upfront
// If we don't want the map to be empty when requesting an account
useSetWalletAPIAccounts(accounts);
// Merge featureFlags handler with customHandlers
const mergedCustomHandlers = (0, react_1.useMemo)(() => {
const featureFlagsHandlersInstance = (0, FeatureFlags_1.handlers)({ manifest, getFeature });
return {
...featureFlagsHandlersInstance,
...customHandlers,
};
}, [manifest, customHandlers, getFeature]);
const serverRef = (0, react_1.useRef)(undefined);
// Lazily initialize WalletAPIServer once to avoid re-creation on re-renders
// https://react.dev/reference/react/useRef#avoiding-recreating-the-ref-contents
if (serverRef.current === undefined) {
serverRef.current = new wallet_api_server_1.WalletAPIServer(transport, config, undefined, mergedCustomHandlers);
}
const server = serverRef.current;
(0, react_1.useEffect)(() => {
if (mergedCustomHandlers) {
server.setCustomHandlers(mergedCustomHandlers);
}
}, [mergedCustomHandlers, server]);
(0, react_1.useEffect)(() => {
server.setConfig(config);
}, [config, server]);
(0, react_1.useEffect)(() => {
server.setPermissions(permission);
}, [permission, server]);
const onMessage = (0, react_1.useCallback)((event) => {
transport.onMessage?.(event);
}, [transport]);
(0, react_1.useEffect)(() => {
tracking.load(manifest);
}, [tracking, manifest]);
// TODO: refactor each handler into its own logic function for clarity
(0, react_1.useEffect)(() => {
server.setHandler("currency.list", async ({ currencyIds }) => {
// 1. Parse manifest currency patterns to determine what to include
const manifestCurrencyIds = manifest.currencies === "*" ? ["**"] : manifest.currencies;
// 2. Apply query filter early - intersect with manifest patterns
const queryCurrencyIdsSet = currencyIds ? new Set(currencyIds) : undefined;
let effectiveCurrencyIds = manifestCurrencyIds;
if (queryCurrencyIdsSet) {
// If we have a query filter, narrow down what we need to fetch
effectiveCurrencyIds = manifestCurrencyIds.flatMap(manifestId => {
if (manifestId === "**") {
// Query can ask for anything, so use the query list
return [...queryCurrencyIdsSet];
}
else if (manifestId.endsWith("/**")) {
// Pattern like "ethereum/**" - keep tokens from query that match this family
const family = manifestId.slice(0, -3);
return [...queryCurrencyIdsSet].filter(qId => qId.startsWith(`${family}/`));
}
else if (queryCurrencyIdsSet.has(manifestId)) {
// Specific currency/token that's in the query
return [manifestId];
}
// Not in query, skip it
return [];
});
}
// 3. Parse effective currency IDs to determine what to fetch
const includeAllCurrencies = effectiveCurrencyIds.includes("**");
const specificCurrencies = new Set();
const tokenFamilies = new Set();
const specificTokenIds = new Set();
for (const id of effectiveCurrencyIds) {
if (id === "**") {
// Already handled above
continue;
}
else if (id.endsWith("/**")) {
// Pattern like "ethereum/**" or "solana/**" - include tokens for this family
const family = id.slice(0, -3);
tokenFamilies.add(family);
// Additionally include the parent currency itself
specificCurrencies.add(family);
}
else if (id.includes("/")) {
// Specific token ID like "ethereum/erc20/usd__coin"
specificTokenIds.add(id);
}
else {
// Specific currency like "bitcoin" or "ethereum"
specificCurrencies.add(id);
}
}
// 4. Gather all supported parent currencies
const allCurrencies = (0, currencies_1.listSupportedCurrencies)().reduce((acc, c) => {
if ((0, helpers_1.isWalletAPISupportedCurrency)(c) && !deactivatedCurrencyIds.has(c.id))
acc.push((0, converters_1.currencyToWalletAPICurrency)(c));
return acc;
}, []);
// 5. Determine which currencies to include based on patterns
let includedCurrencies = [];
if (includeAllCurrencies) {
includedCurrencies = allCurrencies;
}
else {
includedCurrencies = allCurrencies.filter(c => specificCurrencies.has(c.id));
}
// 6. Fetch specific tokens by ID if any
const specificTokens = [];
if (specificTokenIds.size > 0) {
const tokenPromises = [...specificTokenIds].map(async (tokenId) => {
const token = await (0, state_1.getCryptoAssetsStore)().findTokenById(tokenId);
return token ? (0, converters_1.currencyToWalletAPICurrency)(token) : null;
});
const resolvedTokens = await Promise.all(tokenPromises);
specificTokens.push(...resolvedTokens.filter((t) => t !== null));
}
// 7. Determine which token families to fetch (only if not already fetched as specific tokens)
const familiesToFetch = new Set();
if (includeAllCurrencies) {
// Fetch tokens for all currency families
allCurrencies.forEach(c => {
if (c.type === "CryptoCurrency")
familiesToFetch.add(c.family);
});
}
else if (tokenFamilies.size > 0) {
// Only fetch tokens for families explicitly marked with /**
tokenFamilies.forEach(family => familiesToFetch.add(family));
}
// 8. Fetch tokens for relevant families
const fetchAllPagesForFamily = async (family) => {
const args = { networkFamily: family, pageSize: 1000 };
let hasNextPage = true;
let data;
while (hasNextPage) {
const querySub = dispatch(api_1.endpoints.getTokensData.initiate(args, data ? { direction: "forward" } : undefined));
try {
const result = await querySub;
data = result.data;
hasNextPage = result.hasNextPage;
if (result.error)
throw result.error;
}
finally {
querySub.unsubscribe();
}
}
return (data?.pages ?? []).flatMap(p => p.tokens);
};
const tokensByFamily = await Promise.all([...familiesToFetch].map(f => fetchAllPagesForFamily(f)));
// 9. Combine all results (no additional filter needed since we pre-filtered)
const result = tokensByFamily.reduce((acc, tokens) => [...acc, ...tokens.map(t => (0, converters_1.currencyToWalletAPICurrency)(t))], [...includedCurrencies, ...specificTokens]);
return result;
});
}, [walletState, manifest, server, tracking, dispatch, deactivatedCurrencyIds]);
(0, react_1.useEffect)(() => {
server.setHandler("account.list", ({ currencyIds }) => {
// 1. Parse manifest currency patterns to determine what to include
const manifestCurrencyIds = manifest.currencies === "*" ? ["**"] : manifest.currencies;
// 2. Apply query filter early - intersect with manifest patterns
const queryCurrencyIdsSet = currencyIds ? new Set(currencyIds) : undefined;
let effectiveCurrencyIds = manifestCurrencyIds;
if (queryCurrencyIdsSet) {
// If we have a query filter, narrow down what we need to check
effectiveCurrencyIds = manifestCurrencyIds.flatMap(manifestId => {
if (manifestId === "**") {
// Query can ask for anything, so use the query list
return [...queryCurrencyIdsSet];
}
else if (manifestId.endsWith("/**")) {
// Pattern like "ethereum/**" - keep tokens from query that match this family
const family = manifestId.slice(0, -3);
return [...queryCurrencyIdsSet].filter(qId => qId.startsWith(`${family}/`));
}
else if (queryCurrencyIdsSet.has(manifestId)) {
// Specific currency/token that's in the query
return [manifestId];
}
// Not in query, skip it
return [];
});
}
// 3. Build a set of allowed currency IDs based on effective patterns
const allowedCurrencyIds = new Set();
const includeAllCurrencies = effectiveCurrencyIds.includes("**");
const tokenFamilyPrefixes = new Set();
for (const id of effectiveCurrencyIds) {
if (id === "**") {
// Will match all currencies
continue;
}
else if (id.endsWith("/**")) {
// Pattern like "ethereum/**" - store prefix for matching
const family = id.slice(0, -3);
tokenFamilyPrefixes.add(family);
}
else {
// Specific currency/token ID
allowedCurrencyIds.add(id);
}
}
// 4. Filter accounts based on effective currency IDs
const wapiAccounts = accounts.reduce((acc, account) => {
const parentAccount = (0, account_1.getParentAccount)(account, accounts);
const accountCurrencyId = account.type === "TokenAccount" ? account.token.id : account.currency.id;
const parentCurrencyId = account.type === "TokenAccount" ? account.token.parentCurrency.id : account.currency.id;
// Check if account currency ID matches the effective patterns
const isAllowed = includeAllCurrencies ||
allowedCurrencyIds.has(accountCurrencyId) ||
tokenFamilyPrefixes.has(parentCurrencyId);
if (isAllowed) {
acc.push((0, converters_1.accountToWalletAPIAccount)(walletState, account, parentAccount));
}
return acc;
}, []);
return wapiAccounts;
});
}, [walletState, manifest, server, accounts]);
(0, react_1.useEffect)(() => {
if (!uiAccountRequest)
return;
server.setHandler("account.request", async ({ currencyIds, drawerConfiguration, areCurrenciesFiltered, useCase, uiUseCase }) => {
tracking.requestAccountRequested(manifest);
return new Promise((resolve, reject) => {
let done = false;
try {
uiAccountRequest({
currencyIds,
drawerConfiguration,
areCurrenciesFiltered,
useCase,
uiUseCase,
onSuccess: (account, parentAccount) => {
if (done)
return;
done = true;
tracking.requestAccountSuccess(manifest);
resolve((0, converters_1.accountToWalletAPIAccount)(walletState, account, parentAccount));
},
onCancel: () => {
if (done)
return;
done = true;
tracking.requestAccountFail(manifest);
reject(new Error("Canceled by user"));
},
});
}
catch (error) {
tracking.requestAccountFail(manifest);
reject(error);
}
});
});
}, [walletState, manifest, server, tracking, uiAccountRequest]);
(0, react_1.useEffect)(() => {
if (!uiAccountReceive)
return;
server.setHandler("account.receive", ({ accountId, tokenCurrency }) => (0, logic_1.receiveOnAccountLogic)(walletState, { manifest, accounts, tracking }, accountId, (account, parentAccount, accountAddress) => new Promise((resolve, reject) => {
let done = false;
return uiAccountReceive({
account,
parentAccount,
accountAddress,
onSuccess: accountAddress => {
if (done)
return;
done = true;
tracking.receiveSuccess(manifest);
resolve(accountAddress);
},
onCancel: () => {
if (done)
return;
done = true;
tracking.receiveFail(manifest);
reject(new Error("User cancelled"));
},
onError: error => {
if (done)
return;
done = true;
tracking.receiveFail(manifest);
reject(error);
},
});
}), tokenCurrency));
}, [walletState, accounts, manifest, server, tracking, uiAccountReceive]);
(0, react_1.useEffect)(() => {
if (!uiMessageSign)
return;
server.setHandler("message.sign", ({ accountId, message, options }) => (0, logic_1.signMessageLogic)({ manifest, accounts, tracking }, accountId, message.toString("hex"), (account, message) => new Promise((resolve, reject) => {
let done = false;
return uiMessageSign({
account,
message,
options,
onSuccess: signature => {
if (done)
return;
done = true;
tracking.signMessageSuccess(manifest);
resolve(signature.startsWith("0x")
? Buffer.from(signature.replace("0x", ""), "hex")
: Buffer.from(signature));
},
onCancel: () => {
if (done)
return;
done = true;
tracking.signMessageFail(manifest);
reject(new errors_1.UserRefusedOnDevice());
},
onError: error => {
if (done)
return;
done = true;
tracking.signMessageFail(manifest);
reject(error);
},
});
})));
}, [accounts, manifest, server, tracking, uiMessageSign]);
(0, react_1.useEffect)(() => {
if (!uiStorageGet)
return;
server.setHandler("storage.get", (0, logic_1.protectStorageLogic)(manifest, uiStorageGet));
}, [manifest, server, uiStorageGet]);
(0, react_1.useEffect)(() => {
if (!uiStorageSet)
return;
server.setHandler("storage.set", (0, logic_1.protectStorageLogic)(manifest, uiStorageSet));
}, [manifest, server, uiStorageSet]);
(0, react_1.useEffect)(() => {
if (!uiTxSignRaw)
return;
server.setHandler("bitcoin.signPsbt", async ({ accountId, psbt, broadcast }) => {
const signedOperation = await (0, logic_1.signRawTransactionLogic)({ manifest, accounts, tracking }, accountId, psbt, (account, parentAccount, tx) => new Promise((resolve, reject) => {
let done = false;
return uiTxSignRaw({
account,
parentAccount,
transaction: tx,
broadcast,
options: undefined,
onSuccess: signedOperation => {
if (done)
return;
done = true;
tracking.signRawTransactionSuccess(manifest);
resolve(signedOperation);
},
onError: error => {
if (done)
return;
done = true;
tracking.signRawTransactionFail(manifest);
reject(error);
},
});
}));
const rawData = signedOperation.rawData;
if (!rawData || typeof rawData.psbtSigned !== "string") {
throw new Error("Missing psbtSigned in signed operation rawData");
}
const psbtSigned = rawData.psbtSigned;
if (broadcast) {
const txHash = await (0, logic_1.broadcastTransactionLogic)({ manifest, accounts, tracking }, accountId, signedOperation, async (account, parentAccount, signedOperation) => {
const bridge = (0, bridge_1.getAccountBridge)(account, parentAccount);
const mainAccount = (0, account_1.getMainAccount)(account, parentAccount);
let optimisticOperation = signedOperation.operation;
const networkId = account.type === "TokenAccount"
? account.token.parentCurrency.id
: account.currency.id;
const broadcastTrackingData = {
sourceCurrency: account.type === "TokenAccount" ? account.token.name : account.currency.name,
network: networkId,
};
if (!(0, live_env_1.getEnv)("DISABLE_TRANSACTION_BROADCAST")) {
try {
optimisticOperation = await bridge.broadcast({
account: mainAccount,
signedOperation,
});
tracking.broadcastSuccess(manifest, broadcastTrackingData);
}
catch (error) {
tracking.broadcastFail(manifest, broadcastTrackingData);
throw error;
}
}
if (uiTxBroadcast) {
uiTxBroadcast(account, parentAccount, mainAccount, optimisticOperation);
}
return optimisticOperation.hash;
});
return { psbtSigned, txHash };
}
return { psbtSigned };
});
}, [accounts, config.mevProtected, manifest, server, tracking, uiTxBroadcast, uiTxSignRaw]);
(0, react_1.useEffect)(() => {
if (!uiTxSign)
return;
server.setHandler("transaction.sign", async ({ accountId, tokenCurrency, transaction, options }) => {
let currency;
const signedOperation = await (0, logic_1.signTransactionLogic)({ manifest, accounts, tracking }, accountId, transaction, (account, parentAccount, signFlowInfos) => {
currency =
account.type === "TokenAccount"
? account.token.parentCurrency.id
: account.currency.id;
return new Promise((resolve, reject) => {
let done = false;
return uiTxSign({
account,
parentAccount,
signFlowInfos,
options,
onSuccess: signedOperation => {
if (done)
return;
done = true;
tracking.signTransactionSuccess(manifest);
resolve(signedOperation);
},
onError: error => {
if (done)
return;
done = true;
tracking.signTransactionFail(manifest);
reject(error);
},
});
});
}, tokenCurrency);
return currency === "solana"
? Buffer.from(signedOperation.signature, "hex")
: Buffer.from(signedOperation.signature);
});
}, [accounts, manifest, server, tracking, uiTxSign]);
(0, react_1.useEffect)(() => {
if (!uiTxSignRaw)
return;
server.setHandler("transaction.signRaw", async ({ accountId, transaction, broadcast, options }) => {
const signedOperation = await (0, logic_1.signRawTransactionLogic)({ manifest, accounts, tracking }, accountId, transaction, (account, parentAccount, tx) => new Promise((resolve, reject) => {
let done = false;
return uiTxSignRaw({
account,
parentAccount,
transaction: tx,
broadcast,
options,
onSuccess: signedOperation => {
if (done)
return;
done = true;
tracking.signRawTransactionSuccess(manifest);
resolve(signedOperation);
},
onError: error => {
if (done)
return;
done = true;
tracking.signRawTransactionFail(manifest);
reject(error);
},
});
}));
let hash;
if (broadcast) {
hash = await (0, logic_1.broadcastTransactionLogic)({ manifest, accounts, tracking }, accountId, signedOperation, async (account, parentAccount, signedOperation) => {
const bridge = (0, bridge_1.getAccountBridge)(account, parentAccount);
const mainAccount = (0, account_1.getMainAccount)(account, parentAccount);
const networkId = account.type === "TokenAccount"
? account.token.parentCurrency.id
: account.currency.id;
const broadcastTrackingData = {
sourceCurrency: account.type === "TokenAccount" ? account.token.name : account.currency.name,
network: networkId,
};
let optimisticOperation = signedOperation.operation;
if (!(0, live_env_1.getEnv)("DISABLE_TRANSACTION_BROADCAST")) {
try {
optimisticOperation = await bridge.broadcast({
account: mainAccount,
signedOperation,
broadcastConfig: {
mevProtected: !!config.mevProtected,
source: { type: "live-app", name: manifest.id },
},
});
tracking.broadcastSuccess(manifest, broadcastTrackingData);
}
catch (error) {
tracking.broadcastFail(manifest, broadcastTrackingData);
throw error;
}
}
uiTxBroadcast &&
uiTxBroadcast(account, parentAccount, mainAccount, optimisticOperation);
return optimisticOperation.hash;
});
}
return {
signedTransactionHex: signedOperation.signature,
transactionHash: hash,
};
});
}, [accounts, config.mevProtected, manifest, server, tracking, uiTxBroadcast, uiTxSignRaw]);
(0, react_1.useEffect)(() => {
if (!uiTxSign)
return;
server.setHandler("transaction.signAndBroadcast", async ({ accountId, tokenCurrency, transaction, options, meta }) => {
const sponsored = transaction.family === "ethereum" && transaction.sponsored;
// isEmbedded and partner are passed via meta (not transaction) as they're tracking params, not tx properties
const isEmbeddedSwap = meta?.isEmbedded;
const partner = meta?.partner;
const signedTransaction = await (0, logic_1.signTransactionLogic)({ manifest, accounts, tracking }, accountId, transaction, (account, parentAccount, signFlowInfos) => new Promise((resolve, reject) => {
let done = false;
return uiTxSign({
account,
parentAccount,
signFlowInfos,
options,
onSuccess: signedOperation => {
if (done)
return;
done = true;
tracking.signTransactionSuccess(manifest, isEmbeddedSwap, partner);
resolve(signedOperation);
},
onError: error => {
if (done)
return;
done = true;
tracking.signTransactionFail(manifest, isEmbeddedSwap, partner);
reject(error);
},
});
}), tokenCurrency, isEmbeddedSwap, partner);
return (0, logic_1.broadcastTransactionLogic)({ manifest, accounts, tracking }, accountId, signedTransaction, async (account, parentAccount, signedOperation) => {
const bridge = (0, bridge_1.getAccountBridge)(account, parentAccount);
const mainAccount = (0, account_1.getMainAccount)(account, parentAccount);
const networkId = account.type === "TokenAccount"
? account.token.parentCurrency.id
: account.currency.id;
const broadcastTrackingData = {
isEmbeddedSwap,
partner,
sourceCurrency: account.type === "TokenAccount" ? account.token.name : account.currency.name,
network: networkId,
};
let optimisticOperation = signedOperation.operation;
if (!(0, live_env_1.getEnv)("DISABLE_TRANSACTION_BROADCAST")) {
try {
optimisticOperation = await bridge.broadcast({
account: mainAccount,
signedOperation,
broadcastConfig: {
mevProtected: !!config.mevProtected,
sponsored,
source: { type: "live-app", name: manifest.id },
},
});
tracking.broadcastSuccess(manifest, broadcastTrackingData);
}
catch (error) {
tracking.broadcastFail(manifest, broadcastTrackingData);
throw error;
}
}
uiTxBroadcast &&
uiTxBroadcast(account, parentAccount, mainAccount, optimisticOperation);
return optimisticOperation.hash;
}, tokenCurrency);
});
}, [accounts, config.mevProtected, manifest, server, tracking, uiTxBroadcast, uiTxSign]);
const onLoad = (0, react_1.useCallback)(() => {
tracking.loadSuccess(manifest);
setWidgetLoaded(true);
}, [manifest, tracking]);
const onReload = (0, react_1.useCallback)(() => {
tracking.reload(manifest);
setWidgetLoaded(false);
webviewHook.reload();
}, [manifest, tracking, webviewHook]);
const onLoadError = (0, react_1.useCallback)(() => {
tracking.loadFail(manifest);
}, [manifest, tracking]);
const device = useDeviceTransport({ manifest, tracking });
(0, react_1.useEffect)(() => {
if (!uiDeviceTransport)
return;
server.setHandler("device.transport", ({ appName, appVersionRange, devices }) => new Promise((resolve, reject) => {
if (device.ref.current) {
return reject(new Error("Device already opened"));
}
tracking.deviceTransportRequested(manifest);
let done = false;
return uiDeviceTransport({
appName,
onSuccess: ({ device: deviceParam, appAndVersion }) => {
if (done)
return;
done = true;
tracking.deviceTransportSuccess(manifest);
if (!deviceParam) {
reject(new Error("No device"));
return;
}
if (devices && !devices.includes(deviceParam.modelId)) {
reject(new Error("Device not in the devices list"));
return;
}
if (appVersionRange &&
appAndVersion &&
semver_1.default.satisfies(appAndVersion.version, appVersionRange)) {
reject(new Error("App version doesn't satisfies the range"));
return;
}
// TODO handle appFirmwareRange & seeded params
device.subscribe(deviceParam.deviceId);
resolve("1");
},
onCancel: () => {
if (done)
return;
done = true;
tracking.deviceTransportFail(manifest);
reject(new Error("User cancelled"));
},
});
}));
}, [device, manifest, server, tracking, uiDeviceTransport]);
(0, react_1.useEffect)(() => {
if (!uiDeviceSelect)
return;
server.setHandler("device.select", ({ appName, appVersionRange, devices }) => new Promise((resolve, reject) => {
if (device.ref.current) {
return reject(new Error("Device already opened"));
}
tracking.deviceSelectRequested(manifest);
let done = false;
return uiDeviceSelect({
appName,
onSuccess: ({ device: deviceParam, appAndVersion }) => {
if (done)
return;
done = true;
tracking.deviceSelectSuccess(manifest);
if (!deviceParam) {
reject(new Error("No device"));
return;
}
if (devices && !devices.includes(deviceParam.modelId)) {
reject(new Error("Device not in the devices list"));
return;
}
if (appVersionRange &&
appAndVersion &&
semver_1.default.satisfies(appAndVersion.version, appVersionRange)) {
reject(new Error("App version doesn't satisfies the range"));
return;
}
resolve(deviceParam.deviceId);
},
onCancel: () => {
if (done)
return;
done = true;
tracking.deviceSelectFail(manifest);
reject(new Error("User cancelled"));
},
});
}));
}, [device.ref, manifest, server, tracking, uiDeviceSelect]);
(0, react_1.useEffect)(() => {
server.setHandler("device.open", params => {
if (device.ref.current) {
return Promise.reject(new Error("Device already opened"));
}
tracking.deviceOpenRequested(manifest);
device.subscribe(params.deviceId);
return "1";
});
}, [device, manifest, server, tracking]);
(0, react_1.useEffect)(() => {
server.setHandler("device.exchange", params => {
if (!device.ref.current) {
return Promise.reject(new Error("No device opened"));
}
tracking.deviceExchangeRequested(manifest);
return device.exchange(params);
});
}, [device, manifest, server, tracking]);
(0, react_1.useEffect)(() => {
server.setHandler("device.close", ({ transportId }) => {
if (!device.ref.current) {
return Promise.reject(new Error("No device opened"));
}
tracking.deviceCloseRequested(manifest);
device.close();
tracking.deviceCloseSuccess(manifest);
return Promise.resolve(transportId);
});
}, [device, manifest, server, tracking]);
(0, react_1.useEffect)(() => {
server.setHandler("bitcoin.getAddress", ({ accountId, derivationPath }) => {
return (0, logic_1.bitcoinFamilyAccountGetAddressLogic)({ manifest, accounts, tracking }, accountId, derivationPath);
});
}, [accounts, manifest, server, tracking]);
(0, react_1.useEffect)(() => {
server.setHandler("bitcoin.getAddresses", ({ accountId, intentions }) => {
return (0, logic_1.bitcoinFamilyAccountGetAddressesLogic)({ manifest, accounts, tracking }, accountId, intentions);
});
}, [accounts, manifest, server, tracking]);
(0, react_1.useEffect)(() => {
server.setHandler("bitcoin.getPublicKey", ({ accountId, derivationPath }) => {
return (0, logic_1.bitcoinFamilyAccountGetPublicKeyLogic)({ manifest, accounts, tracking }, accountId, derivationPath);
});
}, [accounts, manifest, server, tracking]);
(0, react_1.useEffect)(() => {
server.setHandler("bitcoin.getXPub", ({ accountId }) => {
return (0, logic_1.bitcoinFamilyAccountGetXPubLogic)({ manifest, accounts, tracking }, accountId);
});
}, [accounts, manifest, server, tracking]);
(0, react_1.useEffect)(() => {
if (!uiExchangeStart) {
return;
}
server.setHandler("exchange.start", ({ exchangeType }) => {
return (0, logic_1.startExchangeLogic)({ manifest, accounts, tracking }, exchangeType, exchangeType => new Promise((resolve, reject) => {
let done = false;
return uiExchangeStart({
exchangeType,
onSuccess: (nonce) => {
if (done)
return;
done = true;
tracking.startExchangeSuccess(manifest);
resolve(nonce);
},
onCancel: error => {
if (done)
return;
done = true;
tracking.completeExchangeFail(manifest);
reject(error);
},
});
}));
});
}, [uiExchangeStart, accounts, manifest, server, tracking]);
(0, react_1.useEffect)(() => {
if (!uiExchangeComplete) {
return;
}
server.setHandler("exchange.complete", params => {
// retrofit of the exchange params to fit the old platform spec
const request = {
provider: params.provider,
fromAccountId: params.fromAccountId,
toAccountId: params.exchangeType === "SWAP" ? params.toAccountId : undefined,
transaction: params.transaction,
binaryPayload: params.binaryPayload.toString("hex"),
signature: params.signature.toString("hex"),
feesStrategy: params.feeStrategy,
exchangeType: ExchangeType[params.exchangeType],
swapId: params.exchangeType === "SWAP" ? params.swapId : undefined,
rate: params.exchangeType === "SWAP" ? params.rate : undefined,
tokenCurrency: params.exchangeType !== "SELL" ? params.tokenCurrency : undefined,
};
return (0, logic_1.completeExchangeLogic)({ manifest, accounts, tracking }, request, request => new Promise((resolve, reject) => {
let done = false;
return uiExchangeComplete({
exchangeParams: request,
onSuccess: (hash) => {
if (done)
return;
done = true;
tracking.completeExchangeSuccess(manifest);
resolve(hash);
},
onCancel: error => {
if (done)
return;
done = true;
tracking.completeExchangeFail(manifest);
reject(error);
},
});
}));
});
}, [uiExchangeComplete, accounts, manifest, server, tracking]);
return {
widgetLoaded: widgetLoaded,
onMessage,
onLoad,
onReload,
onLoadError,
server,
};
}
var ExchangeType;
(function (ExchangeType) {
ExchangeType[ExchangeType["SWAP"] = 0] = "SWAP";
ExchangeType[ExchangeType["SELL"] = 1] = "SELL";
ExchangeType[ExchangeType["FUND"] = 2] = "FUND";
ExchangeType[ExchangeType["SWAP_NG"] = 3] = "SWAP_NG";
ExchangeType[ExchangeType["SELL_NG"] = 4] = "SELL_NG";
ExchangeType[ExchangeType["FUND_NG"] = 5] = "FUND_NG";
})(ExchangeType || (exports.ExchangeType = ExchangeType = {}));
function useCategories(manifests, initialCategory) {
const [selected, setSelected] = (0, react_1.useState)(initialCategory || constants_1.DISCOVER_INITIAL_CATEGORY);
const reset = (0, react_1.useCallback)(() => {
setSelected(constants_1.DISCOVER_INITIAL_CATEGORY);
}, []);
const manifestsByCategories = (0, react_1.useMemo)(() => {
const res = manifests.reduce((res, manifest) => {
manifest.categories.forEach(category => {
const list = res.has(category) ? [...res.get(category), manifest] : [manifest];
res.set(category, list);
});
return res;
}, new Map().set("all", manifests));
return res;
}, [manifests]);
const categories = (0, react_1.useMemo)(() => [...manifestsByCategories.keys()], [manifestsByCategories]);
return (0, react_1.useMemo)(() => ({
categories,
manifestsByCategories,
selected,
setSelected,
reset,
}), [categories, manifestsByCategories, selected, reset]);
}
function useLocalLiveApp([LocalLiveAppDb, setState]) {
(0, react_1.useEffect)(() => {
if (LocalLiveAppDb === undefined) {
setState(discoverDB => {
return { ...discoverDB, localLiveApp: constants_1.INITIAL_PLATFORM_STATE.localLiveApp };
});
}
}, [LocalLiveAppDb, setState]);
const addLocalManifest = (0, react_1.useCallback)((newLocalManifest) => {
setState(discoverDB => {
const newLocalLiveAppList = discoverDB.localLiveApp?.filter(manifest => manifest.id !== newLocalManifest.id);
newLocalLiveAppList.push(newLocalManifest);
return { ...discoverDB, localLiveApp: newLocalLiveAppList };
});
}, [setState]);
const removeLocalManifestById = (0, react_1.useCallback)((manifestId) => {
setState(discoverDB => {
const newLocalLiveAppList = discoverDB.localLiveApp.filter(manifest => manifest.id !== manifestId);
return { ...discoverDB, localLiveApp: newLocalLiveAppList };
});
}, [setState]);
const getLocalLiveAppManifestById = (0, react_1.useCallback)((manifestId) => {
return LocalLiveAppDb.find(manifest => manifest.id === manifestId);
}, [LocalLiveAppDb]);
return {
state: LocalLiveAppDb,
addLocalManifest,
removeLocalManifestById,
getLocalLiveAppManifestById,
};
}
function calculateTimeDiff(usedAt) {
const start = new Date();
const end = new Date(usedAt);
const interval = (0, date_fns_1.intervalToDuration)({ start, end });
const units = [
"years",
"months",
"weeks",
"days",
"hours",
"minutes",
"seconds",
];
let timeDiff = { unit: undefined, diff: 0 };
for (const unit of units) {
if (interval[unit] > 0) {
timeDiff = { unit, diff: interval[unit] };
break;
}
}
return timeDiff;
}
function useCacheBustedLiveApps([cacheBustedLiveAppsDb, setState]) {
const getLatest = (0, react_1.useCallback)((manifestId) => {
return cacheBustedLiveAppsDb?.[manifestId];
}, [cacheBustedLiveAppsDb]);
const edit = (0, react_1.useCallback)((manifestId, cacheBustingId) => {
const _cacheBustedLiveAppsDb = {
...cacheBustedLiveAppsDb,
[manifestId]: cacheBustingId,
init: 1,
};
setState(state => {
const newstate = { ...state, cacheBustedLiveApps: _cacheBustedLiveAppsDb };
return newstate;
});
}, [setState, cacheBustedLiveAppsDb]);
return { getLatest, edit };
}
function useRecentlyUsed(manifests, [recentlyUsedManifestsDb, setState]) {
const data = (0, react_1.useMemo)(() => recentlyUsedManifestsDb
.map(recentlyUsed => {
const res = manifests.find(manifest => manifest.id === recentlyUsed.id);
return res
? {
...res,
usedAt: calculateTimeDiff(recentlyUsed.usedAt),
}
: undefined;
})
.filter((manifest) => manifest !== undefined), [recentlyUsedManifestsDb, manifests]);
const append = (0, react_1.useCallback)((manifest) => {
setState(state => {
const index = state.recentlyUsed.findIndex(({ id }) => id === manifest.id);
// Manifest already in first position
if (index === 0) {
return {
...state,
recentlyUsed: [
{ ...state.recentlyUsed[0], usedAt: new Date().toISOString() },
...state.recentlyUsed.slice(1),
],
};
}
// Manifest present we move it to the first position
// No need to check for MAX_LENGTH as we only move it
i