UNPKG

@ledgerhq/live-common

Version:
1,092 lines • 50.3 kB
import { useMemo, useState, useEffect, useRef, useCallback } from "react"; import { useDispatch } from "react-redux"; import semver from "semver"; import { intervalToDuration } from "date-fns"; import { WalletAPIServer } from "@ledgerhq/wallet-api-server"; import { first } from "rxjs/operators"; import { getEnv } from "@ledgerhq/live-env"; import { UserRefusedOnDevice } from "@ledgerhq/errors"; import { endpoints as calEndpoints } from "@ledgerhq/cryptoassets/cal-client/state-manager/api"; import { useFeatureFlags } from "../featureFlags/FeatureFlagsContext"; import { accountToWalletAPIAccount, currencyToWalletAPICurrency, setWalletApiIdForAccountId, } from "./converters"; import { isWalletAPISupportedCurrency } from "./helpers"; import { getMainAccount, getParentAccount } from "../account"; import { listSupportedCurrencies } from "../currencies"; import { getCryptoAssetsStore } from "@ledgerhq/cryptoassets/state"; import { bitcoinFamilyAccountGetXPubLogic, broadcastTransactionLogic, startExchangeLogic, completeExchangeLogic, receiveOnAccountLogic, signMessageLogic, signTransactionLogic, bitcoinFamilyAccountGetAddressLogic, bitcoinFamilyAccountGetAddressesLogic, bitcoinFamilyAccountGetPublicKeyLogic, signRawTransactionLogic, protectStorageLogic, } from "./logic"; import { handlers as featureFlagsHandlers } from "./FeatureFlags"; import { getAccountBridge } from "../bridge"; import openTransportAsSubject from "../hw/openTransportAsSubject"; import { DISCOVER_INITIAL_CATEGORY, INITIAL_PLATFORM_STATE, MAX_RECENTLY_USED_LENGTH, } from "./constants"; import { useCurrenciesUnderFeatureFlag } from "../modularDrawer/hooks/useCurrenciesUnderFeatureFlag"; export function safeGetRefValue(ref) { if (!ref.current) { throw new Error("Ref objects doesn't have a current value"); } return ref.current; } export function useSetWalletAPIAccounts(accounts) { useEffect(() => { accounts.forEach(account => { setWalletApiIdForAccountId(account.id); }); }, [accounts]); } export function useDAppManifestCurrencyIds(manifest) { return useMemo(() => { return (manifest.dapp?.networks.map(network => { return network.currency; }) ?? []); }, [manifest.dapp?.networks]); } export function usePermission(manifest) { return useMemo(() => ({ methodIds: manifest.permissions, }), [manifest]); } function useTransport(postMessage) { return useMemo(() => { return { onMessage: undefined, send: postMessage, }; }, [postMessage]); } export function useConfig({ appId, userId, tracking, wallet, mevProtected, }) { return useMemo(() => ({ appId, userId, tracking, wallet, mevProtected, }), [appId, mevProtected, tracking, userId, wallet]); } function useDeviceTransport({ manifest, tracking }) { const ref = useRef(undefined); const subscribe = useCallback((deviceId) => { ref.current = openTransportAsSubject({ deviceId }); ref.current.subscribe({ complete: () => { ref.current = undefined; }, }); }, []); const close = useCallback(() => { ref.current?.complete(); }, []); const exchange = useCallback(({ apduHex }) => { const subject$ = ref.current; return new Promise((resolve, reject) => { if (!subject$) { reject(new Error("No device transport")); return; } subject$.pipe(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]); useEffect(() => { return () => { ref.current?.complete(); }; }, []); return useMemo(() => ({ ref, subscribe, close, exchange }), [close, exchange, subscribe]); } export 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 = useDispatch(); const { deactivatedCurrencyIds } = useCurrenciesUnderFeatureFlag(); const { getFeature } = useFeatureFlags(); const permission = usePermission(manifest); const transport = useTransport(webviewHook.postMessage); const [widgetLoaded, setWidgetLoaded] = 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 = useMemo(() => { const featureFlagsHandlersInstance = featureFlagsHandlers({ manifest, getFeature }); return { ...featureFlagsHandlersInstance, ...customHandlers, }; }, [manifest, customHandlers, getFeature]); const serverRef = 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 WalletAPIServer(transport, config, undefined, mergedCustomHandlers); } const server = serverRef.current; useEffect(() => { if (mergedCustomHandlers) { server.setCustomHandlers(mergedCustomHandlers); } }, [mergedCustomHandlers, server]); useEffect(() => { server.setConfig(config); }, [config, server]); useEffect(() => { server.setPermissions(permission); }, [permission, server]); const onMessage = useCallback((event) => { transport.onMessage?.(event); }, [transport]); useEffect(() => { tracking.load(manifest); }, [tracking, manifest]); // TODO: refactor each handler into its own logic function for clarity 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 = listSupportedCurrencies().reduce((acc, c) => { if (isWalletAPISupportedCurrency(c) && !deactivatedCurrencyIds.has(c.id)) acc.push(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 getCryptoAssetsStore().findTokenById(tokenId); return token ? 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(calEndpoints.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 => currencyToWalletAPICurrency(t))], [...includedCurrencies, ...specificTokens]); return result; }); }, [walletState, manifest, server, tracking, dispatch, deactivatedCurrencyIds]); 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 = 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(accountToWalletAPIAccount(walletState, account, parentAccount)); } return acc; }, []); return wapiAccounts; }); }, [walletState, manifest, server, accounts]); 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(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]); useEffect(() => { if (!uiAccountReceive) return; server.setHandler("account.receive", ({ accountId, tokenCurrency }) => 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]); useEffect(() => { if (!uiMessageSign) return; server.setHandler("message.sign", ({ accountId, message, options }) => 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 UserRefusedOnDevice()); }, onError: error => { if (done) return; done = true; tracking.signMessageFail(manifest); reject(error); }, }); }))); }, [accounts, manifest, server, tracking, uiMessageSign]); useEffect(() => { if (!uiStorageGet) return; server.setHandler("storage.get", protectStorageLogic(manifest, uiStorageGet)); }, [manifest, server, uiStorageGet]); useEffect(() => { if (!uiStorageSet) return; server.setHandler("storage.set", protectStorageLogic(manifest, uiStorageSet)); }, [manifest, server, uiStorageSet]); useEffect(() => { if (!uiTxSignRaw) return; server.setHandler("bitcoin.signPsbt", async ({ accountId, psbt, broadcast }) => { const signedOperation = await 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 broadcastTransactionLogic({ manifest, accounts, tracking }, accountId, signedOperation, async (account, parentAccount, signedOperation) => { const bridge = getAccountBridge(account, parentAccount); const mainAccount = 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 (!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]); useEffect(() => { if (!uiTxSign) return; server.setHandler("transaction.sign", async ({ accountId, tokenCurrency, transaction, options }) => { let currency; const signedOperation = await 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]); useEffect(() => { if (!uiTxSignRaw) return; server.setHandler("transaction.signRaw", async ({ accountId, transaction, broadcast, options }) => { const signedOperation = await 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 broadcastTransactionLogic({ manifest, accounts, tracking }, accountId, signedOperation, async (account, parentAccount, signedOperation) => { const bridge = getAccountBridge(account, parentAccount); const mainAccount = 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 (!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]); 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 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 broadcastTransactionLogic({ manifest, accounts, tracking }, accountId, signedTransaction, async (account, parentAccount, signedOperation) => { const bridge = getAccountBridge(account, parentAccount); const mainAccount = 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 (!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 = useCallback(() => { tracking.loadSuccess(manifest); setWidgetLoaded(true); }, [manifest, tracking]); const onReload = useCallback(() => { tracking.reload(manifest); setWidgetLoaded(false); webviewHook.reload(); }, [manifest, tracking, webviewHook]); const onLoadError = useCallback(() => { tracking.loadFail(manifest); }, [manifest, tracking]); const device = useDeviceTransport({ manifest, tracking }); 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.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]); 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.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]); 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]); 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]); 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]); useEffect(() => { server.setHandler("bitcoin.getAddress", ({ accountId, derivationPath }) => { return bitcoinFamilyAccountGetAddressLogic({ manifest, accounts, tracking }, accountId, derivationPath); }); }, [accounts, manifest, server, tracking]); useEffect(() => { server.setHandler("bitcoin.getAddresses", ({ accountId, intentions }) => { return bitcoinFamilyAccountGetAddressesLogic({ manifest, accounts, tracking }, accountId, intentions); }); }, [accounts, manifest, server, tracking]); useEffect(() => { server.setHandler("bitcoin.getPublicKey", ({ accountId, derivationPath }) => { return bitcoinFamilyAccountGetPublicKeyLogic({ manifest, accounts, tracking }, accountId, derivationPath); }); }, [accounts, manifest, server, tracking]); useEffect(() => { server.setHandler("bitcoin.getXPub", ({ accountId }) => { return bitcoinFamilyAccountGetXPubLogic({ manifest, accounts, tracking }, accountId); }); }, [accounts, manifest, server, tracking]); useEffect(() => { if (!uiExchangeStart) { return; } server.setHandler("exchange.start", ({ exchangeType }) => { return 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]); 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 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, }; } export 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 || (ExchangeType = {})); export function useCategories(manifests, initialCategory) { const [selected, setSelected] = useState(initialCategory || DISCOVER_INITIAL_CATEGORY); const reset = useCallback(() => { setSelected(DISCOVER_INITIAL_CATEGORY); }, []); const manifestsByCategories = 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 = useMemo(() => [...manifestsByCategories.keys()], [manifestsByCategories]); return useMemo(() => ({ categories, manifestsByCategories, selected, setSelected, reset, }), [categories, manifestsByCategories, selected, reset]); } export function useLocalLiveApp([LocalLiveAppDb, setState]) { useEffect(() => { if (LocalLiveAppDb === undefined) { setState(discoverDB => { return { ...discoverDB, localLiveApp: INITIAL_PLATFORM_STATE.localLiveApp }; }); } }, [LocalLiveAppDb, setState]); const addLocalManifest = useCallback((newLocalManifest) => { setState(discoverDB => { const newLocalLiveAppList = discoverDB.localLiveApp?.filter(manifest => manifest.id !== newLocalManifest.id); newLocalLiveAppList.push(newLocalManifest); return { ...discoverDB, localLiveApp: newLocalLiveAppList }; }); }, [setState]); const removeLocalManifestById = useCallback((manifestId) => { setState(discoverDB => { const newLocalLiveAppList = discoverDB.localLiveApp.filter(manifest => manifest.id !== manifestId); return { ...discoverDB, localLiveApp: newLocalLiveAppList }; }); }, [setState]); const getLocalLiveAppManifestById = 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 = 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; } export function useCacheBustedLiveApps([cacheBustedLiveAppsDb, setState]) { const getLatest = useCallback((manifestId) => { return cacheBustedLiveAppsDb?.[manifestId]; }, [cacheBustedLiveAppsDb]); const edit = useCallback((manifestId, cacheBustingId) => { const _cacheBustedLiveAppsDb = { ...cacheBustedLiveAppsDb, [manifestId]: cacheBustingId, init: 1, }; setState(state => { const newstate = { ...state, cacheBustedLiveApps: _cacheBustedLiveAppsDb }; return newstate; }); }, [setState, cacheBustedLiveAppsDb]); return { getLatest, edit }; } export function useRecentlyUsed(manifests, [recentlyUsedManifestsDb, setState]) { const data = 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 = 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 if (index !== -1) { return { ...state, recentlyUsed: [ { id: manifest.id, usedAt: new Date().toISOString() }, ...state.recentlyUsed.slice(0, index), ...state.recentlyUsed.slice(index + 1), ], }; } // Manifest not preset we simply append and check for the length return { ...state, recentlyUsed: state.recentlyUsed.length >= MAX_RECENTLY_USED_LENGTH ? [ { id: manifest.id, usedAt: new Date().toISOString() }, ...state.recentlyUsed.slice(0, -1), ] : [{ id: manifest.id, usedAt: new Date().toISOString() }, ...state.recentlyUsed], }; }); }, [setState]); const clear = useCallback(() => { setState(state => ({ ...state, recentlyUsed: [] })); }, [setState]); return { data, append, clear }; } export function useDisclaimerRaw({ isReadOnly = false, isDismissed, uiHook, appendRecentlyUsed, }) { const onConfirm = useCallback((manifest, isChecked) => { if (!manifest) return; if (isChecked) { uiHook.dismiss(); } uiHook.close(); appendRecentlyUsed(manifest); uiHook.openApp(manifest); }, [uiHook, appendRecentlyUsed]); const onSelect = useCallback((manifest) => { if (manifest.branch === "soon") { return; } if (!isDismissed && !isReadOnly && manifest.author !== "l