UNPKG

@ledgerhq/live-common

Version:
1,062 lines (985 loc) • 31.3 kB
import invariant from "invariant"; import { log } from "@ledgerhq/logs"; import { createSpeculosDevice, findLatestAppCandidate, listAppCandidates, releaseSpeculosDevice, SpeculosTransport, } from "../load/speculos"; import { createSpeculosDeviceCI, releaseSpeculosDeviceCI } from "./speculosCI"; import type { AppCandidate } from "@ledgerhq/ledger-wallet-framework/bot/types"; import { DeviceModelId } from "@ledgerhq/devices"; import { CryptoCurrency } from "@ledgerhq/types-cryptoassets"; import axios, { AxiosError, AxiosResponse } from "axios"; import { getEnv } from "@ledgerhq/live-env"; import { getCryptoCurrencyById } from "../currencies"; import { DeviceLabels } from "./enum/DeviceLabels"; import { Account } from "./enum/Account"; import { Currency } from "./enum/Currency"; import expect from "expect"; import { sendBTC, sendBTCBasedCoin } from "./families/bitcoin"; import { sendEVM } from "./families/evm"; import { sendPolkadot } from "./families/polkadot"; import { sendAlgorand } from "./families/algorand"; import { sendTron } from "./families/tron"; import { sendStellar } from "./families/stellar"; import { delegateCardano, sendCardano } from "./families/cardano"; import { sendXRP } from "./families/xrp"; import { delegateAptos, sendAptos } from "./families/aptos"; import { sendHedera } from "./families/hedera"; import { delegateNear } from "./families/near"; import { delegateCosmos, sendCosmos } from "./families/cosmos"; import { sendKaspa } from "./families/kaspa"; import { delegateSolana, sendSolana } from "./families/solana"; import { delegateTezos } from "./families/tezos"; import { delegateCelo } from "./families/celo"; import { delegateMultiversX } from "./families/multiversX"; import { Transaction } from "./models/Transaction"; import { Delegate } from "./models/Delegate"; import { Swap } from "./models/Swap"; import { delegateOsmosis } from "./families/osmosis"; import { AppInfos } from "./enum/AppInfos"; import { DEVICE_LABELS_CONFIG } from "./data/deviceLabelsData"; import { sendSui } from "./families/sui"; import { getAppVersionFromCatalog, getSpeculosModel, isTouchDevice } from "./speculosAppVersion"; import { pressAndRelease, longPressAndRelease, swipeRight, } from "./deviceInteraction/TouchDeviceSimulator"; import { withDeviceController } from "./deviceInteraction/DeviceController"; import { sanitizeError } from "."; import { sendVechain } from "./families/vechain"; import { getDeviceCoordinates } from "./deviceCoordinates"; import { sendInternetComputer } from "./families/internet_computer"; const isSpeculosRemote = process.env.REMOTE_SPECULOS === "true"; export type Spec = { currency?: CryptoCurrency; appQuery: { model: DeviceModelId; appName: string; appVersion?: string; }; dependencies?: Dependency[]; onSpeculosDeviceCreated?: (device: Device) => Promise<void>; }; export type Dependency = { name: string; appVersion?: string }; export type SpeculosDevice = { id: string; port: number; appName?: string; appVersion?: string; dependencies?: Dependency[]; }; export function setExchangeDependencies(dependencies: Dependency[]) { const map = new Map<string, Dependency>(); for (const dep of dependencies) { if (!map.has(dep.name)) { map.set(dep.name, dep); } } specs["Exchange"].dependencies = Array.from(map.values()); } type Specs = { [key: string]: Spec; }; export type Device = { transport: SpeculosTransport; id: string; appPath: string; }; export const specs: Specs = { Bitcoin: { currency: getCryptoCurrencyById("bitcoin"), appQuery: { model: getSpeculosModel(), appName: "Bitcoin", }, dependencies: [], }, Aptos: { currency: getCryptoCurrencyById("aptos"), appQuery: { model: getSpeculosModel(), appName: "Aptos", }, dependencies: [], }, Exchange: { appQuery: { model: getSpeculosModel(), appName: "Exchange", }, dependencies: [], }, LedgerSync: { appQuery: { model: getSpeculosModel(), appName: "Ledger Sync", }, dependencies: [], }, Dogecoin: { currency: getCryptoCurrencyById("dogecoin"), appQuery: { model: getSpeculosModel(), appName: "Dogecoin", }, dependencies: [], }, Ethereum: { currency: getCryptoCurrencyById("ethereum"), appQuery: { model: getSpeculosModel(), appName: "Ethereum", }, dependencies: [], }, Ethereum_Sepolia: { currency: getCryptoCurrencyById("ethereum_sepolia"), appQuery: { model: getSpeculosModel(), appName: "Ethereum", }, dependencies: [], }, Ethereum_Classic: { currency: getCryptoCurrencyById("ethereum_classic"), appQuery: { model: getSpeculosModel(), appName: "Ethereum Classic", }, dependencies: [{ name: "Ethereum" }], }, Bitcoin_Testnet: { currency: getCryptoCurrencyById("bitcoin_testnet"), appQuery: { model: getSpeculosModel(), appName: "Bitcoin Test", }, dependencies: [], }, Solana: { currency: getCryptoCurrencyById("solana"), appQuery: { model: getSpeculosModel(), appName: "Solana", }, dependencies: [], }, Cardano: { currency: getCryptoCurrencyById("cardano"), appQuery: { model: getSpeculosModel(), appName: "CardanoADA", }, dependencies: [], }, Polkadot: { currency: getCryptoCurrencyById("polkadot"), appQuery: { model: getSpeculosModel(), appName: "Polkadot", }, dependencies: [], }, Tron: { currency: getCryptoCurrencyById("tron"), appQuery: { model: getSpeculosModel(), appName: "Tron", }, dependencies: [], }, XRP: { currency: getCryptoCurrencyById("ripple"), appQuery: { model: getSpeculosModel(), appName: "XRP", }, dependencies: [], }, Stellar: { currency: getCryptoCurrencyById("stellar"), appQuery: { model: getSpeculosModel(), appName: "Stellar", }, dependencies: [], }, Bitcoin_Cash: { currency: getCryptoCurrencyById("bitcoin_cash"), appQuery: { model: getSpeculosModel(), appName: "Bitcoin Cash", }, dependencies: [], }, Algorand: { currency: getCryptoCurrencyById("algorand"), appQuery: { model: getSpeculosModel(), appName: "Algorand", }, dependencies: [], }, Cosmos: { currency: getCryptoCurrencyById("cosmos"), appQuery: { model: getSpeculosModel(), appName: "Cosmos", }, dependencies: [], }, Tezos: { currency: getCryptoCurrencyById("tezos"), appQuery: { model: getSpeculosModel(), appName: "TezosWallet", }, dependencies: [], }, Polygon: { currency: getCryptoCurrencyById("polygon"), appQuery: { model: getSpeculosModel(), appName: "Ethereum", }, dependencies: [], }, BNB_Chain: { currency: getCryptoCurrencyById("bsc"), appQuery: { model: getSpeculosModel(), appName: "Ethereum", }, dependencies: [], }, Ton: { currency: getCryptoCurrencyById("ton"), appQuery: { model: getSpeculosModel(), appName: "TON", }, dependencies: [], }, Near: { currency: getCryptoCurrencyById("near"), appQuery: { model: getSpeculosModel(), appName: "NEAR", }, dependencies: [], }, Multivers_X: { currency: getCryptoCurrencyById("elrond"), appQuery: { model: getSpeculosModel(), appName: "MultiversX", }, dependencies: [], }, Osmosis: { currency: getCryptoCurrencyById("osmo"), appQuery: { model: getSpeculosModel(), appName: "Cosmos", }, dependencies: [], }, Injective: { currency: getCryptoCurrencyById("injective"), appQuery: { model: getSpeculosModel(), appName: "Cosmos", }, dependencies: [], }, Celo: { currency: getCryptoCurrencyById("celo"), appQuery: { model: getSpeculosModel(), appName: "Celo", }, dependencies: [], }, Litecoin: { currency: getCryptoCurrencyById("litecoin"), appQuery: { model: getSpeculosModel(), appName: "Litecoin", }, dependencies: [], }, Kaspa: { currency: getCryptoCurrencyById("kaspa"), appQuery: { model: getSpeculosModel(), appName: "Kaspa", }, dependencies: [], }, Hedera: { currency: getCryptoCurrencyById("hedera"), appQuery: { model: getSpeculosModel(), appName: "Hedera", }, dependencies: [], }, Sui: { currency: getCryptoCurrencyById("sui"), appQuery: { model: getSpeculosModel(), appName: "Sui", }, dependencies: [], }, Base: { currency: getCryptoCurrencyById("base"), appQuery: { model: getSpeculosModel(), appName: "Ethereum", }, dependencies: [], }, Vechain: { currency: getCryptoCurrencyById("vechain"), appQuery: { model: getSpeculosModel(), appName: "Vechain", }, dependencies: [], }, Zcash: { currency: getCryptoCurrencyById("zcash"), appQuery: { model: getSpeculosModel(), appName: "Zcash", }, dependencies: [], }, Aleo: { currency: getCryptoCurrencyById("aleo"), appQuery: { model: getSpeculosModel(), appName: "Aleo", }, dependencies: [], }, Internet_Computer: { currency: getCryptoCurrencyById("internet_computer"), appQuery: { model: getSpeculosModel(), appName: "InternetComputer", }, dependencies: [], }, }; export async function startSpeculos( testName: string, spec: Specs[keyof Specs], ): Promise<SpeculosDevice | undefined> { log("engine", `test ${testName}`); const { SEED, COINAPPS } = process.env; const seed = SEED; invariant(seed, "SEED is not set"); const coinapps = COINAPPS; invariant(coinapps, "COINAPPS is not set"); const appCandidates = await listAppCandidates(coinapps); const nanoAppCatalogPath = getEnv("E2E_NANO_APP_VERSION_PATH"); const { appQuery, onSpeculosDeviceCreated } = spec; try { const displayName = spec.currency?.managerAppName || appQuery.appName; const catalogVersion = await getAppVersionFromCatalog(displayName, nanoAppCatalogPath); if (catalogVersion) { appQuery.appVersion = catalogVersion; } } catch (e) { console.warn("[speculos] Unable to fetch app version from catalog", e); } const appCandidate = findLatestAppCandidate(appCandidates, appQuery); const { model } = appQuery; const { dependencies } = spec; const newAppQuery = dependencies?.map(dep => { return findLatestAppCandidate(appCandidates, { model, appName: dep.name, firmware: appCandidate?.firmware, }); }); const appVersionMap = new Map(newAppQuery?.map(app => [app?.appName, app?.appVersion])); dependencies?.forEach(dependency => { dependency.appVersion = appVersionMap.get(dependency.name) || "1.0.0"; }); if (!appCandidate) { console.warn("no app found for " + testName); console.warn(appQuery); } invariant( appCandidate, "%s: no app found. Are you sure your COINAPPS is up to date?", testName, coinapps, ); log( "engine", `test ${testName} will use ${appCandidate.appName} ${appCandidate.appVersion} on ${appCandidate.model} ${appCandidate.firmware}`, ); const deviceParams = { ...(appCandidate as AppCandidate), appName: spec.currency ? spec.currency.managerAppName : spec.appQuery.appName, seed, dependencies, coinapps, onSpeculosDeviceCreated, }; try { return isSpeculosRemote ? await createSpeculosDeviceCI(deviceParams) : await createSpeculosDevice(deviceParams).then(device => { invariant(device.ports.apiPort, "[E2E] Speculos apiPort is not defined"); return { id: device.id, port: device.ports.apiPort, appName: deviceParams.appName, appVersion: deviceParams.appVersion, dependencies: deviceParams.dependencies, }; }); } catch (e: unknown) { console.error(sanitizeError(e)); log("engine", `test ${testName} failed with ${String(e)}`); } } export async function stopSpeculos(deviceId: string | undefined) { if (deviceId) { log("engine", `test ${deviceId} finished`); isSpeculosRemote ? await releaseSpeculosDeviceCI(deviceId) : await releaseSpeculosDevice(deviceId); } } interface Event { text: string; x: number; y: number; } interface ResponseData { events: Event[]; } export function getSpeculosAddress(): string { const speculosAddress = process.env.SPECULOS_ADDRESS; return speculosAddress || "http://127.0.0.1"; } export async function retryAxiosRequest<T>( requestFn: () => Promise<AxiosResponse<T>>, maxRetries: number = 5, baseDelay: number = 1000, retryableStatusCodes: number[] = [500, 502, 503, 504], ): Promise<AxiosResponse<T>> { let lastError: AxiosError | Error; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await requestFn(); } catch (error) { lastError = error as AxiosError | Error; const isRetryable = axios.isAxiosError(error) && error.response && retryableStatusCodes.includes(error.response.status); const isNetworkError = axios.isAxiosError(error) && !error.response; if ((isRetryable || isNetworkError) && attempt < maxRetries) { const delay = baseDelay * (attempt + 1); console.warn( `Axios request failed (attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${delay}ms...`, { status: axios.isAxiosError(error) ? error.response?.status : "network error", message: error.message, }, ); await new Promise(resolve => setTimeout(resolve, delay)); continue; } throw lastError; } } throw lastError!; } export async function waitFor(text: string, maxAttempts = 60): Promise<string> { const port = getEnv("SPECULOS_API_PORT"); let texts = ""; for (let attempt = 0; attempt < maxAttempts; attempt++) { texts = await fetchCurrentScreenTexts(port); if (texts.toLowerCase().includes(text.toLowerCase())) { return texts; } await waitForTimeOut(500); } throw new Error( `Text "${text}" not found on device screen after ${maxAttempts} attempts. Last screen text: "${texts}"`, ); } export async function fetchCurrentScreenTexts(speculosApiPort: number): Promise<string> { const speculosAddress = getSpeculosAddress(); const response = await retryAxiosRequest(() => axios.get<ResponseData>( `${speculosAddress}:${speculosApiPort}/events?stream=false&currentscreenonly=true`, ), ); return response.data.events.map(event => event.text).join(" "); } export async function getDeviceLabelCoordinates( label: string, speculosApiPort: number, ): Promise<{ x: number; y: number }> { const speculosAddress = getSpeculosAddress(); const response = await retryAxiosRequest(() => axios.get<ResponseData>( `${speculosAddress}:${speculosApiPort}/events?stream=false&currentscreenonly=true`, ), ); const event = response.data.events.find(e => e.text === label); if (!event) { throw new Error(`Label "${label}" not found in screen events`); } return { x: event.x, y: event.y }; } export async function fetchAllEvents(speculosApiPort: number): Promise<string[]> { const speculosAddress = getSpeculosAddress(); const response = await retryAxiosRequest(() => axios.get<ResponseData>( `${speculosAddress}:${speculosApiPort}/events?stream=false&currentscreenonly=false`, ), ); return response.data.events.map(event => event.text); } export const pressUntilTextFound = withDeviceController( ({ getButtonsController }) => async (targetText: string, strictMatch: boolean = false): Promise<string[]> => { const maxAttempts = 18; const speculosApiPort = getEnv("SPECULOS_API_PORT"); const buttons = getButtonsController(); for (let attempts = 0; attempts < maxAttempts; attempts++) { const texts = await fetchCurrentScreenTexts(speculosApiPort); if ( strictMatch ? texts === targetText : texts.toLowerCase().includes(targetText.toLowerCase()) ) { return await fetchAllEvents(speculosApiPort); } if (isTouchDevice()) { await swipeRight(); } else { await buttons.right(); } await waitForTimeOut(200); } throw new Error( `ElementNotFoundException: Element with text "${targetText}" not found on speculos screen`, ); }, ); export function containsSubstringInEvent(targetString: string, events: string[]): boolean { const concatenatedEvents = events.join(""); let result = concatenatedEvents.includes(targetString); if (!result) { const regexPattern = targetString.split("").join(".*?"); const regex = new RegExp(regexPattern, "s"); result = regex.test(concatenatedEvents); } return result; } export async function takeScreenshot(port?: number): Promise<Buffer | undefined> { const speculosAddress = getSpeculosAddress(); const speculosApiPort = port ?? getEnv("SPECULOS_API_PORT"); try { const response = await retryAxiosRequest(() => axios.get(`${speculosAddress}:${speculosApiPort}/screenshot`, { responseType: "arraybuffer", }), ); return response.data; } catch (error) { console.error("Error downloading speculos screenshot:", sanitizeError(error)); } } export async function waitForTimeOut(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); } export const removeMemberLedgerSync = withDeviceController( ({ getButtonsController }) => async () => { const buttons = getButtonsController(); await waitFor(DeviceLabels.CONNECT_TO); if (isTouchDevice()) { await pressAndRelease(DeviceLabels.CONNECT); await waitFor(DeviceLabels.REMOVE_FROM_LEDGER_SYNC); await pressAndRelease(DeviceLabels.REMOVE); await waitFor(DeviceLabels.CONFIRM_CHANGE); await pressAndRelease(DeviceLabels.TAP_TO_CONTINUE); await waitFor(DeviceLabels.TURN_ON_SYNC); await pressUntilTextFound(DeviceLabels.LEDGER_WALLET_WILL_BE); await pressUntilTextFound(DeviceLabels.TURN_ON_SYNC); const turnOnSyncCoordinates = getDeviceCoordinates("turnOnSync"); await pressAndRelease( DeviceLabels.TURN_ON_SYNC, turnOnSyncCoordinates.x, turnOnSyncCoordinates.y, ); } else { await pressUntilTextFound(DeviceLabels.CONNECT, true); await buttons.both(); await waitFor(DeviceLabels.REMOVE_FROM_LEDGER_SYNC); await pressUntilTextFound(DeviceLabels.REMOVE, true); await buttons.both(); await waitFor(DeviceLabels.TURN_ON_SYNC); await pressUntilTextFound(DeviceLabels.LEDGER_WALLET_WILL_BE); await pressUntilTextFound(DeviceLabels.TURN_ON_SYNC); await buttons.both(); } }, ); export const activateLedgerSync = withDeviceController(({ getButtonsController }) => async () => { const buttons = getButtonsController(); await waitFor(DeviceLabels.CONNECT_TO); if (isTouchDevice()) { await pressAndRelease(DeviceLabels.CONNECT); } else { await pressUntilTextFound(DeviceLabels.CONNECT_TO_LEDGER_SYNC); await buttons.right(); await buttons.both(); } await waitFor(DeviceLabels.TURN_ON_SYNC); if (isTouchDevice()) { const turnOnSyncCoordinates = getDeviceCoordinates("turnOnSync"); await pressAndRelease( DeviceLabels.TURN_ON_SYNC, turnOnSyncCoordinates.x, turnOnSyncCoordinates.y, ); } else { await pressUntilTextFound(DeviceLabels.LEDGER_WALLET_WILL_BE); await pressUntilTextFound(DeviceLabels.TURN_ON_SYNC); await buttons.both(); } }); export const activateExpertMode = withDeviceController(({ getButtonsController }) => async () => { const buttons = getButtonsController(); if (isTouchDevice()) { await goToSettings(); const settingsToggle1Coords = getDeviceCoordinates("settingsToggle1"); await pressAndRelease( DeviceLabels.SETTINGS_TOGGLE_1, settingsToggle1Coords.x, settingsToggle1Coords.y, ); } else { await pressUntilTextFound(DeviceLabels.EXPERT_MODE); await buttons.both(); } }); export const activateContractData = withDeviceController(({ getButtonsController }) => async () => { const buttons = getButtonsController(); await pressUntilTextFound(DeviceLabels.SETTINGS); await buttons.both(); await waitFor(DeviceLabels.CONTRACT_DATA); await buttons.both(); }); export const goToSettings = withDeviceController(({ getButtonsController }) => async () => { const buttons = getButtonsController(); if (isTouchDevice()) { const settingsCogwheelCoords = getDeviceCoordinates("settingsCogwheel"); await pressAndRelease( DeviceLabels.SETTINGS, settingsCogwheelCoords.x, settingsCogwheelCoords.y, ); } else { await pressUntilTextFound(DeviceLabels.SETTINGS); await buttons.both(); } }); export const providePublicKey = withDeviceController(({ getButtonsController }) => async () => { const buttons = getButtonsController(); await buttons.right(); }); type DeviceLabelsReturn = { delegateConfirmLabel: string; delegateVerifyLabel: string; receiveConfirmLabel: string; receiveVerifyLabel: string; sendVerifyLabel: string; sendConfirmLabel: string; }; export function getDeviceLabels(appInfo: AppInfos): DeviceLabelsReturn { const deviceModel = getSpeculosModel(); const deviceConfig = DEVICE_LABELS_CONFIG[deviceModel] ?? DEVICE_LABELS_CONFIG.default; if (!deviceConfig) { throw new Error(`No device configuration found for ${deviceModel}`); } const receiveVerifyLabel = deviceConfig.receiveVerify[appInfo.name] ?? deviceConfig.receiveVerify.default; const receiveConfirmLabel = deviceConfig.receiveConfirm[appInfo.name] ?? deviceConfig.receiveConfirm.default; const delegateVerifyLabel = deviceConfig.delegateVerify[appInfo.name] ?? deviceConfig.delegateVerify.default; const delegateConfirmLabel = deviceConfig.delegateConfirm[appInfo.name] ?? deviceConfig.delegateConfirm.default; const sendVerifyLabel = deviceConfig.sendVerify[appInfo.name] ?? deviceConfig.sendVerify.default; const sendConfirmLabel = deviceConfig.sendConfirm[appInfo.name] ?? deviceConfig.sendConfirm.default; return { receiveVerifyLabel, receiveConfirmLabel, delegateVerifyLabel, delegateConfirmLabel, sendVerifyLabel, sendConfirmLabel, }; } export const expectValidAddressDevice = withDeviceController( ({ getButtonsController }) => async (account: Account, addressDisplayed: string) => { const buttons = getButtonsController(); if (account.currency === Currency.SUI_USDC) { await providePublicKey(); } const { receiveVerifyLabel, receiveConfirmLabel } = getDeviceLabels( account.currency.speculosApp, ); await waitFor(receiveVerifyLabel); if (isTouchDevice()) { const events = await pressUntilTextFound(receiveConfirmLabel); const isAddressCorrect = containsSubstringInEvent(addressDisplayed, events); expect(isAddressCorrect).toBeTruthy(); await pressAndRelease(DeviceLabels.CONFIRM); } else { const events = await pressUntilTextFound(receiveConfirmLabel); const isAddressCorrect = containsSubstringInEvent(addressDisplayed, events); expect(isAddressCorrect).toBeTruthy(); await buttons.both(); } }, ); export async function signSendTransaction(tx: Transaction) { const currencyId = tx.accountToDebit.currency.id; switch (currencyId) { case Currency.sepETH.id: case Currency.BASE.id: case Currency.POL.id: case Currency.ETH.id: case Currency.ETH_USDT.id: await sendEVM(tx); break; case Currency.BTC.id: await sendBTC(tx); break; case Currency.DOGE.id: case Currency.BCH.id: case Currency.ZEC.id: await sendBTCBasedCoin(tx); break; case Currency.DOT.id: await sendPolkadot(tx); break; case Currency.ALGO.id: await sendAlgorand(tx); break; case Currency.SOL.id: case Currency.SOL_GIGA.id: await sendSolana(tx); break; case Currency.TRX.id: await sendTron(tx); break; case Currency.XLM.id: await sendStellar(tx); break; case Currency.ATOM.id: await sendCosmos(tx); break; case Currency.ADA.id: await sendCardano(tx); break; case Currency.XRP.id: await sendXRP(tx); break; case Currency.APT.id: await sendAptos(tx); break; case Currency.KAS.id: await sendKaspa(tx); break; case Currency.HBAR.id: await sendHedera(); break; case Currency.SUI.id: case Currency.SUI_USDC.id: await sendSui(tx); break; case Currency.VET.id: await sendVechain(tx); break; case Currency.ICP.id: await sendInternetComputer(tx); break; default: throw new Error(`Unsupported currency: ${tx.accountToDebit.currency.ticker}`); } } export async function getSendEvents(tx: Transaction): Promise<string[]> { const { sendVerifyLabel, sendConfirmLabel } = getDeviceLabels( tx.accountToDebit.currency.speculosApp, ); await waitFor(sendVerifyLabel); return await pressUntilTextFound(sendConfirmLabel); } export async function signDelegationTransaction(delegatingAccount: Delegate) { const currencyName = delegatingAccount.account.currency.name; switch (currencyName) { case Account.SOL_1.currency.name: await delegateSolana(delegatingAccount); break; case Account.NEAR_1.currency.name: await delegateNear(delegatingAccount); break; case Account.ATOM_1.currency.name: case Account.INJ_1.currency.name: await delegateCosmos(delegatingAccount); break; case Account.OSMO_1.currency.name: await delegateOsmosis(delegatingAccount); break; case Account.MULTIVERS_X_1.currency.name: await delegateMultiversX(delegatingAccount); break; case Account.ADA_1.currency.name: await delegateCardano(); break; case Account.XTZ_1.currency.name: await delegateTezos(delegatingAccount); break; case Account.CELO_1.currency.name: await delegateCelo(delegatingAccount); break; case Account.APTOS_1.currency.name: await delegateAptos(delegatingAccount); break; default: throw new Error(`Unsupported currency: ${currencyName}`); } } export async function getDelegateEvents(delegatingAccount: Delegate): Promise<string[]> { const { delegateVerifyLabel, delegateConfirmLabel } = getDeviceLabels( delegatingAccount.account.currency.speculosApp, ); await waitFor(delegateVerifyLabel); return await pressUntilTextFound(delegateConfirmLabel); } export const verifyAmountsAndAcceptSwap = withDeviceController( ({ getButtonsController }) => async (swap: Swap, amount: string) => { const buttons = getButtonsController(); await waitFor(DeviceLabels.REVIEW_TRANSACTION); const events = getSpeculosModel() === DeviceModelId.nanoS ? await pressUntilTextFound(DeviceLabels.ACCEPT_AND_SEND) : await pressUntilTextFound(DeviceLabels.SIGN_TRANSACTION); verifySwapData(swap, events, amount); if (isTouchDevice()) { await longPressAndRelease(DeviceLabels.HOLD_TO_SIGN, 3); } else { await buttons.both(); } }, ); export const verifyAmountsAndAcceptSwapForDifferentSeed = withDeviceController( ({ getButtonsController }) => async (swap: Swap, amount: string, errorMessage: string | null) => { const buttons = getButtonsController(); if (errorMessage === null) { if (isTouchDevice()) { await waitFor(DeviceLabels.RECEIVE_ADDRESS_DOES_NOT_BELONG); await pressAndRelease(DeviceLabels.CONTINUE_ANYWAY); } else { await waitFor(DeviceLabels.REVIEW_TRANSACTION); await pressUntilTextFound(DeviceLabels.RECEIVE_ADDRESS_DOES_NOT_BELONG); await buttons.both(); } } else { await waitFor(DeviceLabels.REVIEW_TRANSACTION); } const events = await pressUntilTextFound(DeviceLabels.SIGN_TRANSACTION); verifySwapData(swap, events, amount); if (isTouchDevice()) { await longPressAndRelease(DeviceLabels.HOLD_TO_SIGN, 3); } else { await buttons.both(); } }, ); export const verifyAmountsAndRejectSwap = withDeviceController( ({ getButtonsController }) => async (swap: Swap, amount: string) => { const buttons = getButtonsController(); await waitFor(DeviceLabels.REVIEW_TRANSACTION); let events: string[] = []; if (isTouchDevice()) { events = await pressUntilTextFound(DeviceLabels.HOLD_TO_SIGN); } else { events = await pressUntilTextFound(DeviceLabels.REJECT); } verifySwapData(swap, events, amount); if (isTouchDevice()) { await pressAndRelease(DeviceLabels.REJECT); await waitFor(DeviceLabels.YES_REJECT); await pressAndRelease(DeviceLabels.YES_REJECT); } else { await buttons.both(); } }, ); function verifySwapData(swap: Swap, events: string[], amount: string) { const swapPair = `swap ${swap.getAccountToDebit.currency.ticker} to ${swap.getAccountToCredit.currency.ticker}`; if (getSpeculosModel() !== DeviceModelId.nanoS) { expectDeviceScreenContains(swapPair, events, "Swap pair not found on the device screen"); } expectDeviceScreenContains(amount, events, `Amount ${amount} not found on the device screen`); } function expectDeviceScreenContains(substring: string, events: string[], message: string) { const found = containsSubstringInEvent(substring, events); if (!found) { throw new Error( `${message}. Expected events to contain "${substring}". Got: ${JSON.stringify(events)}`, ); } } export const exportUfvk = withDeviceController( ({ getButtonsController }) => async (account: Account) => { const buttons = getButtonsController(); const { receiveVerifyLabel, receiveConfirmLabel } = getDeviceLabels( account.currency.speculosApp, ); await waitFor(receiveVerifyLabel); if (isTouchDevice()) { await pressUntilTextFound(receiveConfirmLabel); await pressAndRelease(DeviceLabels.CONFIRM); } else { await pressUntilTextFound(receiveConfirmLabel); await buttons.both(); } }, ); export const shareViewKey = withDeviceController(({ getButtonsController }) => async () => { const buttons = getButtonsController(); await pressUntilTextFound(DeviceLabels.CONFIRM); if (isTouchDevice()) { await pressAndRelease(DeviceLabels.CONFIRM); } else { await buttons.both(); } });