From 4e3a2d437564920722e92fd7b7d6ff620105166b Mon Sep 17 00:00:00 2001 From: Gregor Gilchrist Date: Wed, 5 Jul 2023 08:57:30 +0200 Subject: [PATCH 01/27] test: added framework and test for wallet api/live app detox test --- .../tests/utils/dummy-wallet-app/README.md | 120 ++++++- .../utils/dummy-wallet-app/public/index.html | 26 +- .../tests/utils/dummy-wallet-app/src/App.tsx | 72 +--- .../tests/utils/dummy-wallet-app/src/hooks.ts | 51 +++ .../dummy-wallet-app/src/react-app-env.d.ts | 3 + apps/ledger-live-mobile/.env.mock | 5 +- .../debug/res/xml/network_security_config.xml | 4 + apps/ledger-live-mobile/detox.config.js | 12 +- apps/ledger-live-mobile/e2e/bridge/client.ts | 34 +- apps/ledger-live-mobile/e2e/bridge/server.ts | 13 +- .../e2e/models/liveApps/cryptoDrawer.ts | 11 + .../e2e/models/liveApps/liveAppWebview.ts | 30 ++ .../e2e/specs/wallet-api.spec.ts | 101 ++++++ .../Web3AppWebview/WalletAPIWebview.tsx | 330 +---------------- .../src/components/Web3AppWebview/helpers.ts | 332 +++++++++++++++++- .../src/components/Web3AppWebview/types.ts | 6 +- .../react-native-hw-transport-ble/makeMock.ts | 6 +- 17 files changed, 716 insertions(+), 440 deletions(-) create mode 100644 apps/ledger-live-desktop/tests/utils/dummy-wallet-app/src/hooks.ts create mode 100644 apps/ledger-live-mobile/e2e/models/liveApps/cryptoDrawer.ts create mode 100644 apps/ledger-live-mobile/e2e/models/liveApps/liveAppWebview.ts create mode 100644 apps/ledger-live-mobile/e2e/specs/wallet-api.spec.ts diff --git a/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/README.md b/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/README.md index 438d6b901400..9f76b0bfc9a8 100644 --- a/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/README.md +++ b/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/README.md @@ -1,20 +1,120 @@ -# Ledger Live Dummy Wallet App +# Dummy App -The purpose of this app is to allow automated front end testing of Ledger Live Wallet apps, and verify that Ledger Live correctly: +The purpose of this app is to allow automated front end testing of Ledger Live's Wallet API server implementation, and verify that Ledger Live correctly: - handles the rendering of external Live Apps -- handles calls of the Live SDK from external Live apps +- handles calls of the Wallet API client from external Live apps -The app is a simple [Create React App](https://github.com/facebook/create-react-app) which uses the [Ledger Live App SDK](https://www.npmjs.com/package/@ledgerhq/live-app-sdk). It has some buttons that have hardcoded responses that can be triggered from the playwright tests, thus allowing us to check the UI. This means the app isn't suitable for manual testing or full E2E testing since it is not dynamic, and does not make calls to external services or the Nano itself. +The app is a simple [Create React App](https://github.com/facebook/create-react-app) which uses the [Wallet API](https://www.npmjs.com/package/@ledgerhq/wallet-api).## How to run locally for development -## How to run locally for development +## Run dummy app locally -Run `pnpm --filter="dummy-wallet-app" start`. +```sh +pnpm i -## Quick script to build the app from scratch +# Start development server +pnpm --filter="dummy-wallet-app" start -To use the Dummy app in the Playwright tests, you must install and build the dependencies and source code for the dummy app. To do this run the following from the root folder of this monorepo: +# Create production build +pnpm --filter="dummy-wallet-app" build -`pnpm clean && pnpm --filter="dummy-wallet-app" i && pnpm --filter="dummy-wallet-app" build` +# Serve production build +pnpm --filter="dummy-wallet-app" serve +``` -Then run `pnpm --filter="dummy-wallet-app" serve` +## Run E2E test locally + +```sh +### Setup + +pnpm i +pnpm --filter="dummy-wallet-app" build + +### Desktop + +pnpm desktop build:testing +pnpm desktop test:playwright wallet-api.spec.ts + +### iOS + +pnpm mobile e2e:build -c ios.sim.debug +pnpm mobile e2e:test -c ios.sim.debug apps/ledger-live-mobile/e2e/specs/wallet-api.spec.ts + +### Android + +pnpm mobile e2e:build -c android.emu.debug +pnpm mobile e2e:test -c android.emu.debug apps/ledger-live-mobile/e2e/specs/wallet-api.spec.ts +``` + +## [WIP] How it works? + +### Desktop + +#### Loading dummy app in discover catalog + +Add manifest json string to `MOCK_REMOTE_LIVE_MANIFEST`. + +```typescript +import { getMockAppManifest } from "PATH/TO/utils/serve-dummy-app"; + +test.beforeAll(async () => { + process.env.MOCK_REMOTE_LIVE_MANIFEST = JSON.stringify( + // getMockAppManifest accepts object to override mock values + getMockAppManifest({ + id: "dummy-app", + url: "localhost:3000", + name: "Dummy App", + apiVersion: "2.0.0", + content: { + shortDescription: { + en: "App to test the Wallet API", + }, + description: { + en: "App to test the Wallet API with Playwright", + }, + }, + }), + ); +}); +``` + +```mermaid +sequenceDiagram + participant T as Test (Playwright) + participant A as App (Electron) + participant W as WebView + T->>A: await LiveApp.send + activate T + A->>W: await window.ledger.e2e.walletApi.send(request) via webview.executeJavaScript + activate A + W-->>A: wallet-api request + A-->>A: UI Hook (e.g. Side drawer to select account etc.) + A-->>W: wallet-api response + W->>A: Promise + deactivate A + A->>T: Promise + deactivate T +``` + +- `LiveApp.send`: This method turns JSON-RPC request object into JSON. Then, it calls injected method via [`webview.executeJavaScript`](https://www.electronjs.org/docs/latest/api/webview-tag#webviewexecutejavascriptcode-usergesture). It allows testing engineers to simply await for a response from Live's wallet server and `expect` against the response. +- `window.ledger.e2e.walletApi.send`: This is an injected method in Dummy App to send request to wallet api server. It returns `Promise`. + +### Mobile + +```mermaid +sequenceDiagram + participant T as Test (Jest) + participant A as App (React Native) + participant W as WebView (react-native-webview) + T->>A: await LiveApp.send + activate T + A->>W: webview.injectJavScript + activate A + W-->>A: wallet-api request + A-->>A: UI Hook (e.g. Side drawer to select account etc.) + A-->>W: wallet-api response + W->>A: postMessage(response) + deactivate A + A->>T: Promise + deactivate T +``` diff --git a/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/public/index.html b/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/public/index.html index fc371124727b..949dca9a1edf 100644 --- a/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/public/index.html +++ b/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/public/index.html @@ -1,18 +1,16 @@ + + + + + + + Dummy Wallet API App + - - - - - - - Ledger Live App - - - - -
- - + + +
+ diff --git a/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/src/App.tsx b/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/src/App.tsx index e54c6b5bdacc..1e90312e9163 100644 --- a/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/src/App.tsx +++ b/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/src/App.tsx @@ -1,62 +1,14 @@ -import React, { useCallback, useEffect, useRef } from "react"; -import { WindowMessageTransport, RpcResponse } from "@ledgerhq/wallet-api-client"; +import React, { useMemo } from "react"; +import { useE2EInjection } from "./hooks"; import "./App.css"; -type PendingRequests = Record void>; - -function useE2EInjection() { - const queue = useRef({}); - const transport = useRef(new WindowMessageTransport()); - - const send = useCallback(jsonStr => { - const { id } = JSON.parse(jsonStr); - - return new Promise(resolve => { - queue.current[id] = resolve; - transport.current.send(jsonStr); - }); - }, []); - - useEffect(() => { - transport.current.connect(); - transport.current.onMessage = (msgStr: string) => { - const msg = JSON.parse(msgStr); - if (!msg.id) return; - - const resolve = queue.current[msg.id]; - if (!resolve) return; - - resolve(msg); - delete queue.current[msg.id]; - }; - - window.ledger = { - // to avoid overriding other fields - ...window.ledger, - e2e: { - ...window.ledger?.e2e, - walletApi: { - send, - }, - }, - }; - }, [send]); -} - -function getQueryParams(params: URLSearchParams) { - const paramList = []; - - for (const [key, value] of params.entries()) { - paramList.push(
  • {`${key}: ${value}`}
  • ); - } - - return paramList; -} - -const App = () => { +export default function App() { useE2EInjection(); - const searchParams = new URLSearchParams(window.location.search); + const params = useMemo( + () => Array.from(new URLSearchParams(window.location.search).entries()), + [], + ); return (
    @@ -69,11 +21,13 @@ const App = () => {

    App for testing the Ledger Live Wallet API manually and in Automated tests

    Query Params for web app: -
      {searchParams && getQueryParams(searchParams)}
    +
      + {params.map(([key, value]) => ( +
    1. {`${key}: ${value}`}
    2. + ))} +
    ); -}; - -export default App; +} diff --git a/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/src/hooks.ts b/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/src/hooks.ts new file mode 100644 index 000000000000..4884fe561559 --- /dev/null +++ b/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/src/hooks.ts @@ -0,0 +1,51 @@ +import { useCallback, useEffect, useRef } from "react"; +import { WindowMessageTransport, RpcResponse } from "@ledgerhq/wallet-api-client"; + +type PendingRequests = Record void>; + +export function useE2EInjection() { + const queue = useRef({}); + const transport = useRef(new WindowMessageTransport()); + + const send = useCallback(async jsonStr => { + const { id } = JSON.parse(jsonStr); + + const promise = new Promise(resolve => { + queue.current[id] = resolve; + transport.current.send(jsonStr); + }); + + // if mobile + if (window.ReactNativeWebview) { + const response = await promise; + window.ReactNativeWebview.postMessage(JSON.stringify({ type: "e2eTest", payload: response })); + } + + return promise; + }, []); + + useEffect(() => { + transport.current.connect(); + transport.current.onMessage = (msgStr: string) => { + const msg = JSON.parse(msgStr); + if (!msg.id) return; + + const resolve = queue.current[msg.id]; + if (!resolve) return; + + resolve(msg); + delete queue.current[msg.id]; + }; + + window.ledger = { + // to avoid overriding other fields + ...window.ledger, + e2e: { + ...window.ledger?.e2e, + walletApi: { + send, + }, + }, + }; + }, [send]); +} diff --git a/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/src/react-app-env.d.ts b/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/src/react-app-env.d.ts index 0db73384d20e..dea0ca3b214d 100644 --- a/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/src/react-app-env.d.ts +++ b/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/src/react-app-env.d.ts @@ -8,4 +8,7 @@ interface Window { }; }; }; + ReactNativeWebview?: { + postMessage: (value: unknown) => void; + }; } diff --git a/apps/ledger-live-mobile/.env.mock b/apps/ledger-live-mobile/.env.mock index eab20879f8cc..71e8829d2035 100644 --- a/apps/ledger-live-mobile/.env.mock +++ b/apps/ledger-live-mobile/.env.mock @@ -8,6 +8,7 @@ ADJUST_APP_TOKEN=cbxft2ch7wn4 BRAZE_ANDROID_API_KEY="be5e1bc8-43f1-4864-b097-076a3c693a43" BRAZE_IOS_API_KEY="e0a7dfaf-fc30-48f6-b998-01dbebbb73a4" BRAZE_CUSTOM_ENDPOINT="sdk.fra-02.braze.eu" -FEATURE_FLAGS={"syncOnboarding":{"enabled":false},"llmNewDeviceSelection":{"enabled":false},"protectServicesMobile":{"enabled":false},"brazePushNotifications":{"enabled":false}} +FEATURE_FLAGS={"syncOnboarding":{"enabled":false},"llmNewDeviceSelection":{"enabled":false},"protectServicesMobile":{"enabled":false},"brazePushNotifications":{"enabled":false},"discover":{"enabled":true,"params":{"version":"2"}}} # Fix random iOS app crash https://github.com/wix/Detox/pull/3135 -SIMCTL_CHILD_NSZombieEnabled=YES \ No newline at end of file +SIMCTL_CHILD_NSZombieEnabled=YES +MOCK_REMOTE_LIVE_MANIFEST=[{"name":"Dummy Wallet API Live App","homepageUrl":"https://developers.ledger.com/","icon":"","platforms":["ios","android","desktop"],"apiVersion":"2.0.0","manifestVersion":"1","branch":"stable","categories":["tools"],"currencies":"*","content":{"shortDescription":{"en":"App to test the Wallet API"},"description":{"en":"App to test the Wallet API with Playwright"}},"permissions":["account.list","account.receive","account.request","currency.list","device.close","device.exchange","device.transport","message.sign","transaction.sign","transaction.signAndBroadcast","storage.set","storage.get","bitcoin.getXPub","wallet.capabilities","wallet.userId","wallet.info"],"domains":["http://*"],"visibility":"complete","id":"dummy-live-app","url":"http://localhost:52619"}] \ No newline at end of file diff --git a/apps/ledger-live-mobile/android/app/src/debug/res/xml/network_security_config.xml b/apps/ledger-live-mobile/android/app/src/debug/res/xml/network_security_config.xml index 2439f15c2cb7..99bee9f07783 100644 --- a/apps/ledger-live-mobile/android/app/src/debug/res/xml/network_security_config.xml +++ b/apps/ledger-live-mobile/android/app/src/debug/res/xml/network_security_config.xml @@ -1,4 +1,8 @@ + + 10.0.2.2 + localhost + diff --git a/apps/ledger-live-mobile/detox.config.js b/apps/ledger-live-mobile/detox.config.js index 2d5a0bc6e114..690abc01fec7 100644 --- a/apps/ledger-live-mobile/detox.config.js +++ b/apps/ledger-live-mobile/detox.config.js @@ -30,28 +30,24 @@ module.exports = { "ios.debug": { type: "ios.app", build: `export ENVFILE=.env.mock && xcodebuild ARCHS=${iosArch} ONLY_ACTIVE_ARCH=no -workspace ios/ledgerlivemobile.xcworkspace -scheme ledgerlivemobile -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build`, - binaryPath: - "ios/build/Build/Products/Debug-iphonesimulator/ledgerlivemobile.app", + binaryPath: "ios/build/Build/Products/Debug-iphonesimulator/ledgerlivemobile.app", }, "ios.staging": { type: "ios.app", build: `export ENVFILE=.env.mock && xcodebuild ARCHS=${iosArch} ONLY_ACTIVE_ARCH=no -workspace ios/ledgerlivemobile.xcworkspace -scheme ledgerlivemobile -configuration Staging -sdk iphonesimulator -derivedDataPath ios/build`, - binaryPath: - "ios/build/Build/Products/Staging-iphonesimulator/ledgerlivemobile.app", + binaryPath: "ios/build/Build/Products/Staging-iphonesimulator/ledgerlivemobile.app", }, "ios.release": { type: "ios.app", build: `export ENVFILE=.env.mock && xcodebuild ARCHS=${iosArch} ONLY_ACTIVE_ARCH=no -workspace ios/ledgerlivemobile.xcworkspace -scheme ledgerlivemobile -configuration Release -sdk iphonesimulator -derivedDataPath ios/build`, - binaryPath: - "ios/build/Build/Products/Release-iphonesimulator/ledgerlivemobile.app", + binaryPath: "ios/build/Build/Products/Release-iphonesimulator/ledgerlivemobile.app", }, "android.debug": { type: "android.apk", build: "cd android && ENVFILE=.env.mock ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug && cd ..", binaryPath: `android/app/build/outputs/apk/debug/app-${androidArch}-debug.apk`, - testBinaryPath: - "android/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk", + testBinaryPath: "android/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk", }, "android.release": { type: "android.apk", diff --git a/apps/ledger-live-mobile/e2e/bridge/client.ts b/apps/ledger-live-mobile/e2e/bridge/client.ts index 86e216ab4896..a06ebe963237 100644 --- a/apps/ledger-live-mobile/e2e/bridge/client.ts +++ b/apps/ledger-live-mobile/e2e/bridge/client.ts @@ -9,6 +9,15 @@ import { acceptGeneralTermsLastVersion } from "../../src/logic/terms"; import accountModel from "../../src/logic/accountModel"; import { navigate } from "../../src/rootnavigation"; +type ClientData = + | { + type: DescriptorEventType; + payload: { id: string; name: string; serviceUUID: string }; + } + | { type: "open" }; + +export const e2eBridgeClient = new Subject(); + let ws: WebSocket; export function init(port = 8099) { @@ -26,11 +35,8 @@ export function init(port = 8099) { ws.onmessage = onMessage; } -async function onMessage(event: { data: unknown }) { - invariant( - typeof event.data === "string", - "[E2E Bridge Client]: Message data must be string", - ); +function onMessage(event: { data: unknown }) { + invariant(typeof event.data === "string", "[E2E Bridge Client]: Message data must be string"); const msg = JSON.parse(event.data); invariant(msg.type, "[E2E Bridge Client]: type is missing"); @@ -40,7 +46,7 @@ async function onMessage(event: { data: unknown }) { switch (msg.type) { case "add": case "open": - e2eBridgeSubject.next(msg); + e2eBridgeClient.next(msg); break; case "setGlobals": Object.entries(msg.payload).forEach(([k, v]) => { @@ -67,14 +73,14 @@ async function onMessage(event: { data: unknown }) { } } -type SubjectData = - | { - type: DescriptorEventType; - payload: { id: string; name: string; serviceUUID: string }; - } - | { type: "open" }; - -export const e2eBridgeSubject = new Subject(); +export function sendWalletAPIResponse(payload: Record) { + ws.send( + JSON.stringify({ + type: "walletAPIResponse", + payload, + }), + ); +} function log(message: string) { // eslint-disable-next-line no-console diff --git a/apps/ledger-live-mobile/e2e/bridge/server.ts b/apps/ledger-live-mobile/e2e/bridge/server.ts index ea52acb4a917..5136909be7cb 100644 --- a/apps/ledger-live-mobile/e2e/bridge/server.ts +++ b/apps/ledger-live-mobile/e2e/bridge/server.ts @@ -2,6 +2,14 @@ import { Server } from "ws"; import path from "path"; import fs from "fs"; import { NavigatorName } from "../../src/const"; +import { Subject } from "rxjs"; + +type ServerData = { + type: "walletAPIResponse"; + payload: string; +}; + +export const e2eBridgeServer = new Subject(); let wss: Server; @@ -70,10 +78,13 @@ export function open() { } function onMessage(messageStr: string) { - const msg = JSON.parse(messageStr); + const msg: ServerData = JSON.parse(messageStr); log(`Message\n${JSON.stringify(msg, null, 2)}`); switch (msg.type) { + case "walletAPIResponse": + e2eBridgeServer.next(msg); + break; default: break; } diff --git a/apps/ledger-live-mobile/e2e/models/liveApps/cryptoDrawer.ts b/apps/ledger-live-mobile/e2e/models/liveApps/cryptoDrawer.ts new file mode 100644 index 000000000000..e9e78d5e9ec5 --- /dev/null +++ b/apps/ledger-live-mobile/e2e/models/liveApps/cryptoDrawer.ts @@ -0,0 +1,11 @@ +import { tapByText } from "../../helpers"; + +export default class CryptoDrawer { + selectCurrencyFromDrawer(currencyName: string) { + return tapByText(currencyName); + } + + selectAccountFromDrawer(accountName: string) { + return tapByText(accountName); + } +} diff --git a/apps/ledger-live-mobile/e2e/models/liveApps/liveAppWebview.ts b/apps/ledger-live-mobile/e2e/models/liveApps/liveAppWebview.ts new file mode 100644 index 000000000000..0a58b463c6bd --- /dev/null +++ b/apps/ledger-live-mobile/e2e/models/liveApps/liveAppWebview.ts @@ -0,0 +1,30 @@ +import { randomUUID } from "crypto"; +import { web, by } from "detox"; +import { e2eBridgeServer } from "../../bridge/server"; +import { first, filter, map } from "rxjs/operators"; + +export default class LiveAppWebview { + async send(params: Record) { + const webview = web.element(by.web.id("root")); + const id = randomUUID(); + const json = JSON.stringify({ + id, + jsonrpc: "2.0", + ...params, + }); + + await webview.runScript(`function foo(element) { + window.ledger.e2e.walletApi.send('${json}'); + }`); + + const response = e2eBridgeServer + .pipe( + filter(msg => msg.type === "walletAPIResponse"), + first(), + map(msg => msg.payload), + ) + .toPromise(); + + return { id, response }; + } +} diff --git a/apps/ledger-live-mobile/e2e/specs/wallet-api.spec.ts b/apps/ledger-live-mobile/e2e/specs/wallet-api.spec.ts new file mode 100644 index 000000000000..34924338fef4 --- /dev/null +++ b/apps/ledger-live-mobile/e2e/specs/wallet-api.spec.ts @@ -0,0 +1,101 @@ +import * as detox from "detox"; // this is because we need to use both the jest expect and the detox.expect version, which has some different assertions +import { loadConfig } from "../bridge/server"; +import { isAndroid } from "../helpers"; +import * as server from "../../../ledger-live-desktop/tests/utils/serve-dummy-app"; +import PortfolioPage from "../models/wallet/portfolioPage"; +import DiscoveryPage from "../models/discovery/discoveryPage"; +import LiveAppWebview from "../models/liveApps/liveAppWebview"; +import CryptoDrawer from "../models/liveApps/cryptoDrawer"; + +let portfolioPage: PortfolioPage; +let discoverPage: DiscoveryPage; +let liveAppWebview: LiveAppWebview; +let cryptoDrawer: CryptoDrawer; + +let continueTest: boolean; + +describe("Wallet API methods", () => { + beforeAll(async () => { + // Check that dummy app in tests/utils/dummy-app-build has been started successfully + try { + const port = await server.start( + "../../../ledger-live-desktop/tests/utils/dummy-wallet-app/build", + 52619, + ); + + await detox.device.reverseTcpPort(52619); // To allow the android emulator to access the dummy app + + const url = `http://localhost:${port}`; + const response = await fetch(url); + if (response.ok) { + continueTest = true; + + // eslint-disable-next-line no-console + console.info( + `========> Dummy Wallet API app successfully running on port ${port}! <=========`, + ); + } else { + throw new Error("Ping response != 200, got: " + response.status); + } + } catch (error) { + console.warn(`========> Dummy test app not running! <=========`); + console.error(error); + + continueTest = false; + } + + if (!continueTest || !isAndroid()) { + return; // need to make this a proper ignore/jest warning + } + + // start navigation + portfolioPage = new PortfolioPage(); + discoverPage = new DiscoveryPage(); + liveAppWebview = new LiveAppWebview(); + cryptoDrawer = new CryptoDrawer(); + + loadConfig("1AccountBTC1AccountETHReadOnlyFalse", true); + + await portfolioPage.waitForPortfolioPageToLoad(); + await discoverPage.openViaDeeplink("dummy-live-app"); + + const title = await detox.web.element(detox.by.web.id("image-container")).getTitle(); + expect(title).toBe("Dummy Wallet API App"); + + const url = await detox.web.element(detox.by.web.id("param-container")).getCurrentUrl(); + expect(url).toBe("http://localhost:52619/?theme=light&lang=en&name=Dummy+Wallet+API+Live+App"); + }); + + afterAll(() => { + server.stop(); + }); + + it("account.request", async () => { + const { id, response } = await liveAppWebview.send({ + method: "account.request", + params: { + currencyIds: ["ethereum", "bitcoin"], + }, + }); + + await cryptoDrawer.selectCurrencyFromDrawer("Bitcoin"); + await cryptoDrawer.selectAccountFromDrawer("Bitcoin 1 (legacy)"); + + await expect(response).resolves.toMatchObject({ + jsonrpc: "2.0", + id, + result: { + rawAccount: { + id: "2d23ca2a-069e-579f-b13d-05bc706c7583", + address: "1xeyL26EKAAR3pStd7wEveajk4MQcrYezeJ", + balance: "35688397", + blockHeight: 194870, + currency: "bitcoin", + // lastSyncDate: "2020-03-14T13:34:42.000Z", + name: "Bitcoin 1 (legacy)", + spendableBalance: "35688397", + }, + }, + }); + }); +}); diff --git a/apps/ledger-live-mobile/src/components/Web3AppWebview/WalletAPIWebview.tsx b/apps/ledger-live-mobile/src/components/Web3AppWebview/WalletAPIWebview.tsx index d3d32d927eaa..ad2f893412a9 100644 --- a/apps/ledger-live-mobile/src/components/Web3AppWebview/WalletAPIWebview.tsx +++ b/apps/ledger-live-mobile/src/components/Web3AppWebview/WalletAPIWebview.tsx @@ -1,330 +1,19 @@ -import React, { useState, useCallback, useEffect, useMemo, RefObject, forwardRef } from "react"; -import { useSelector } from "react-redux"; +import React, { forwardRef } from "react"; import { ActivityIndicator, StyleSheet, View } from "react-native"; -import VersionNumber from "react-native-version-number"; import { WebView as RNWebView } from "react-native-webview"; -import { useNavigation } from "@react-navigation/native"; -import { Operation, SignedOperation } from "@ledgerhq/types-live"; -import type { Transaction } from "@ledgerhq/live-common/generated/types"; -import { - safeGetRefValue, - ExchangeType, - UiHook, - useConfig, - useWalletAPIServer, -} from "@ledgerhq/live-common/wallet-api/react"; -import trackingWrapper from "@ledgerhq/live-common/wallet-api/tracking"; -import type { Device } from "@ledgerhq/live-common/hw/actions/types"; -import BigNumber from "bignumber.js"; -import { AppManifest } from "@ledgerhq/live-common/wallet-api/types"; -import { NavigatorName, ScreenName } from "../../const"; -import { flattenAccountsSelector } from "../../reducers/accounts"; -import { track } from "../../analytics/segment"; -import prepareSignTransaction from "./liveSDKLogic"; -import { StackNavigatorNavigation } from "../RootNavigator/types/helpers"; -import { BaseNavigatorStackParamList } from "../RootNavigator/types/BaseNavigator"; -import { analyticsEnabledSelector } from "../../reducers/settings"; -import getOrCreateUser from "../../user"; import { WebviewAPI, WebviewProps } from "./types"; -import { useWebviewState } from "./helpers"; -import deviceStorage from "../../logic/storeWrapper"; +import { useWebView } from "./helpers"; import { NetworkError } from "./NetworkError"; -const wallet = { - name: "ledger-live-mobile", - version: VersionNumber.appVersion, -}; -const tracking = trackingWrapper(track); - -function useUiHook(): Partial { - const navigation = useNavigation(); - const [device, setDevice] = useState(); - - return useMemo( - () => ({ - "account.request": ({ accounts$, currencies, onSuccess, onError }) => { - if (currencies.length === 1) { - navigation.navigate(NavigatorName.RequestAccount, { - screen: ScreenName.RequestAccountsSelectAccount, - params: { - accounts$, - currency: currencies[0], - allowAddAccount: true, - onSuccess, - onError, - }, - }); - } else { - navigation.navigate(NavigatorName.RequestAccount, { - screen: ScreenName.RequestAccountsSelectCrypto, - params: { - accounts$, - currencies, - allowAddAccount: true, - onSuccess, - onError, - }, - }); - } - }, - "account.receive": ({ - account, - parentAccount, - accountAddress, - onSuccess, - onCancel, - onError, - }) => { - navigation.navigate(ScreenName.VerifyAccount, { - account, - parentId: parentAccount ? parentAccount.id : undefined, - onSuccess: () => onSuccess(accountAddress), - onClose: onCancel, - onError, - }); - }, - "message.sign": ({ account, message, onSuccess, onError, onCancel }) => { - navigation.navigate(NavigatorName.SignMessage, { - screen: ScreenName.SignSummary, - params: { - message, - accountId: account.id, - onConfirmationHandler: onSuccess, - onFailHandler: onError, - }, - onClose: onCancel, - }); - }, - "storage.get": async ({ key, storeId }) => { - return (await deviceStorage.get(`${storeId}-${key}`)) as string; - }, - "storage.set": ({ key, value, storeId }) => { - deviceStorage.save(`${storeId}-${key}`, value); - }, - "transaction.sign": ({ - account, - parentAccount, - signFlowInfos: { liveTx }, - options, - onSuccess, - onError, - }) => { - const tx = prepareSignTransaction( - account, - parentAccount, - liveTx as Partial, - ); - - navigation.navigate(NavigatorName.SignTransaction, { - screen: ScreenName.SignTransactionSummary, - params: { - currentNavigation: ScreenName.SignTransactionSummary, - nextNavigation: ScreenName.SignTransactionSelectDevice, - transaction: tx as Transaction, - accountId: account.id, - parentId: parentAccount ? parentAccount.id : undefined, - appName: options?.hwAppId, - onSuccess: ({ - signedOperation, - transactionSignError, - }: { - signedOperation: SignedOperation; - transactionSignError: Error; - }) => { - if (transactionSignError) { - onError(transactionSignError); - } else { - onSuccess(signedOperation); - - const n = - navigation.getParent>() || - navigation; - n.pop(); - } - }, - onError, - }, - }); - }, - "device.transport": ({ appName, onSuccess, onCancel }) => { - navigation.navigate(ScreenName.DeviceConnect, { - appName, - onSuccess, - onClose: onCancel, - }); - }, - "exchange.start": ({ exchangeType, onSuccess, onCancel }) => { - navigation.navigate(NavigatorName.PlatformExchange, { - screen: ScreenName.PlatformStartExchange, - params: { - request: { - exchangeType: ExchangeType[exchangeType], - }, - onResult: (result: { - startExchangeResult?: string; - startExchangeError?: Error; - device?: Device; - }) => { - if (result.startExchangeError) { - onCancel(result.startExchangeError); - } - - if (result.startExchangeResult) { - setDevice(result.device); - onSuccess(result.startExchangeResult); - } - - const n = - navigation.getParent>() || - navigation; - n.pop(); - }, - }, - }); - }, - "exchange.complete": ({ exchangeParams, onSuccess, onCancel }) => { - navigation.navigate(NavigatorName.PlatformExchange, { - screen: ScreenName.PlatformCompleteExchange, - params: { - request: { - exchangeType: exchangeParams.exchangeType, - provider: exchangeParams.provider, - exchange: exchangeParams.exchange, - transaction: exchangeParams.transaction as Transaction, - binaryPayload: exchangeParams.binaryPayload, - signature: exchangeParams.signature, - feesStrategy: exchangeParams.feesStrategy, - }, - device, - onResult: (result: { operation?: Operation; error?: Error }) => { - if (result.error) { - onCancel(result.error); - } - if (result.operation) { - onSuccess(result.operation.id); - } - setDevice(undefined); - const n = - navigation.getParent>() || - navigation; - n.pop(); - }, - }, - }); - }, - }), - [navigation, device], - ); -} - -const useGetUserId = () => { - const [userId, setUserId] = useState(""); - - useEffect(() => { - let mounted = true; - getOrCreateUser().then(({ user }) => { - if (mounted) setUserId(user.id); - }); - return () => { - mounted = false; - }; - }, []); - - return userId; -}; - -function useWebView( - { manifest }: Pick, - webviewRef: RefObject, -) { - const accounts = useSelector(flattenAccountsSelector); - - const uiHook = useUiHook(); - const analyticsEnabled = useSelector(analyticsEnabledSelector); - const userId = useGetUserId(); - const config = useConfig({ - appId: manifest.id, - userId, - tracking: analyticsEnabled, - wallet, - }); - - const webviewHook = useMemo(() => { - return { - reload: () => { - const webview = safeGetRefValue(webviewRef); - - webview.reload(); - }, - // TODO: wallet-api-server lifecycle is not perfect and will try to send messages before a ref is available. Some additional thinkering is needed here. - postMessage: (message: string) => { - try { - const webview = safeGetRefValue(webviewRef); - - webview.postMessage(message); - } catch (error) { - console.warn( - "wallet-api-server tried to send a message while the webview was not yet initialized.", - ); - } - }, - }; - }, [webviewRef]); - - const { onMessage: onMessageRaw, onLoadError } = useWalletAPIServer({ - manifest: manifest as AppManifest, - accounts, - tracking, - config, - webviewHook, - uiHook, - }); - - const onMessage = useCallback( - e => { - if (e.nativeEvent?.data) { - onMessageRaw(e.nativeEvent.data); - } - }, - [onMessageRaw], - ); - - return { - onLoadError, - onMessage, - }; -} - -function renderLoading() { - return ( - - - - ); -} - export const WalletAPIWebview = forwardRef( ({ manifest, inputs = {}, onStateChange, allowsBackForwardNavigationGestures = true }, ref) => { - const { webviewProps, webviewState, webviewRef } = useWebviewState( - { - manifest: manifest as AppManifest, - inputs, - }, - ref, - ); - - useEffect(() => { - if (onStateChange) { - onStateChange(webviewState); - } - }, [webviewState, onStateChange]); - - const { onMessage, onLoadError } = useWebView( + const { onMessage, onLoadError, webviewProps, webviewRef } = useWebView( { manifest, inputs, }, - webviewRef, + ref, + onStateChange, ); return ( @@ -346,6 +35,7 @@ export const WalletAPIWebview = forwardRef( scrollEnabled={true} style={styles.webview} renderError={() => webviewRef.current?.reload()} />} + testID="wallet-api-webview" {...webviewProps} /> ); @@ -354,6 +44,14 @@ export const WalletAPIWebview = forwardRef( WalletAPIWebview.displayName = "WalletAPIWebview"; +function renderLoading() { + return ( + + + + ); +} + const styles = StyleSheet.create({ root: { flex: 1, diff --git a/apps/ledger-live-mobile/src/components/Web3AppWebview/helpers.ts b/apps/ledger-live-mobile/src/components/Web3AppWebview/helpers.ts index a6ffdc6137bd..84e8c29a3387 100644 --- a/apps/ledger-live-mobile/src/components/Web3AppWebview/helpers.ts +++ b/apps/ledger-live-mobile/src/components/Web3AppWebview/helpers.ts @@ -1,10 +1,119 @@ import { AppManifest } from "@ledgerhq/live-common/wallet-api/types"; import { addParamsToURL, getClientHeaders } from "@ledgerhq/live-common/wallet-api/helpers"; -import { safeGetRefValue } from "@ledgerhq/live-common/wallet-api/react"; +import { + safeGetRefValue, + ExchangeType, + UiHook, + useConfig, + useWalletAPIServer, +} from "@ledgerhq/live-common/wallet-api/react"; +import { Operation, SignedOperation } from "@ledgerhq/types-live"; +import type { Transaction } from "@ledgerhq/live-common/generated/types"; +import trackingWrapper from "@ledgerhq/live-common/wallet-api/tracking"; +import type { Device } from "@ledgerhq/live-common/hw/actions/types"; +import BigNumber from "bignumber.js"; +import { useSelector } from "react-redux"; import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react"; import { WebViewProps, WebView } from "react-native-webview"; +import VersionNumber from "react-native-version-number"; import { useTheme } from "styled-components/native"; -import { WebviewAPI, WebviewState } from "./types"; +import { useNavigation } from "@react-navigation/native"; +import { NavigatorName, ScreenName } from "../../const"; +import { flattenAccountsSelector } from "../../reducers/accounts"; +import { WebviewAPI, WebviewProps, WebviewState } from "./types"; +import prepareSignTransaction from "./liveSDKLogic"; +import { StackNavigatorNavigation } from "../RootNavigator/types/helpers"; +import { BaseNavigatorStackParamList } from "../RootNavigator/types/BaseNavigator"; +import { analyticsEnabledSelector } from "../../reducers/settings"; +import deviceStorage from "../../logic/storeWrapper"; +import { track } from "../../analytics/segment"; +import getOrCreateUser from "../../user"; +import * as bridge from "../../../e2e/bridge/client"; +import Config from "react-native-config"; + +export function useWebView( + { manifest, inputs }: Pick, + ref: React.ForwardedRef, + onStateChange: WebviewProps["onStateChange"], +) { + const { webviewProps, webviewRef } = useWebviewState( + { + manifest: manifest as AppManifest, + inputs, + }, + ref, + onStateChange, + ); + + const accounts = useSelector(flattenAccountsSelector); + + const uiHook = useUiHook(); + const analyticsEnabled = useSelector(analyticsEnabledSelector); + const userId = useGetUserId(); + const config = useConfig({ + appId: manifest.id, + userId, + tracking: analyticsEnabled, + wallet, + }); + + const webviewHook = useMemo(() => { + return { + reload: () => { + const webview = safeGetRefValue(webviewRef); + + webview.reload(); + }, + // TODO: wallet-api-server lifecycle is not perfect and will try to send messages before a ref is available. Some additional thinkering is needed here. + postMessage: (message: string) => { + try { + const webview = safeGetRefValue(webviewRef); + + webview.postMessage(message); + } catch (error) { + console.warn( + "wallet-api-server tried to send a message while the webview was not yet initialized.", + ); + } + }, + }; + }, [webviewRef]); + + const { onMessage: onMessageRaw, onLoadError } = useWalletAPIServer({ + manifest: manifest as AppManifest, + accounts, + tracking, + config, + webviewHook, + uiHook, + }); + + const onMessage = useCallback( + e => { + if (e.nativeEvent?.data) { + try { + const msg = JSON.parse(e.nativeEvent.data); + + if (Config.MOCK && msg.type === "e2eTest") { + bridge.sendWalletAPIResponse(msg.payload); + } else { + onMessageRaw(e.nativeEvent.data); + } + } catch { + onMessageRaw(e.nativeEvent.data); + } + } + }, + [onMessageRaw], + ); + + return { + onLoadError, + onMessage, + webviewProps, + webviewRef, + }; +} export const initialWebviewState: WebviewState = { url: "", @@ -14,14 +123,10 @@ export const initialWebviewState: WebviewState = { loading: false, }; -type useWebviewStateParams = { - manifest: AppManifest; - inputs?: Record; -}; - export function useWebviewState( - params: useWebviewStateParams, + params: Pick, WebviewAPIRef: React.ForwardedRef, + onStateChange: WebviewProps["onStateChange"], ) { const webviewRef = useRef(null); const { manifest, inputs } = params; @@ -126,6 +231,12 @@ export function useWebviewState( [], ); + useEffect(() => { + if (onStateChange) { + onStateChange(state); + } + }, [state, onStateChange]); + const props: Partial = useMemo( () => ({ onLoad, @@ -138,8 +249,211 @@ export function useWebviewState( ); return { - webviewState: state, webviewProps: props, webviewRef, }; } + +function useUiHook(): Partial { + const navigation = useNavigation(); + const [device, setDevice] = useState(); + + return useMemo( + () => ({ + "account.request": ({ accounts$, currencies, onSuccess, onError }) => { + if (currencies.length === 1) { + navigation.navigate(NavigatorName.RequestAccount, { + screen: ScreenName.RequestAccountsSelectAccount, + params: { + accounts$, + currency: currencies[0], + allowAddAccount: true, + onSuccess, + onError, + }, + }); + } else { + navigation.navigate(NavigatorName.RequestAccount, { + screen: ScreenName.RequestAccountsSelectCrypto, + params: { + accounts$, + currencies, + allowAddAccount: true, + onSuccess, + onError, + }, + }); + } + }, + "account.receive": ({ + account, + parentAccount, + accountAddress, + onSuccess, + onCancel, + onError, + }) => { + navigation.navigate(ScreenName.VerifyAccount, { + account, + parentId: parentAccount ? parentAccount.id : undefined, + onSuccess: () => onSuccess(accountAddress), + onClose: onCancel, + onError, + }); + }, + "message.sign": ({ account, message, onSuccess, onError, onCancel }) => { + navigation.navigate(NavigatorName.SignMessage, { + screen: ScreenName.SignSummary, + params: { + message, + accountId: account.id, + onConfirmationHandler: onSuccess, + onFailHandler: onError, + }, + onClose: onCancel, + }); + }, + "storage.get": async ({ key, storeId }) => { + return (await deviceStorage.get(`${storeId}-${key}`)) as string; + }, + "storage.set": ({ key, value, storeId }) => { + deviceStorage.save(`${storeId}-${key}`, value); + }, + "transaction.sign": ({ + account, + parentAccount, + signFlowInfos: { liveTx }, + options, + onSuccess, + onError, + }) => { + const tx = prepareSignTransaction( + account, + parentAccount, + liveTx as Partial, + ); + + navigation.navigate(NavigatorName.SignTransaction, { + screen: ScreenName.SignTransactionSummary, + params: { + currentNavigation: ScreenName.SignTransactionSummary, + nextNavigation: ScreenName.SignTransactionSelectDevice, + transaction: tx as Transaction, + accountId: account.id, + parentId: parentAccount ? parentAccount.id : undefined, + appName: options?.hwAppId, + onSuccess: ({ + signedOperation, + transactionSignError, + }: { + signedOperation: SignedOperation; + transactionSignError: Error; + }) => { + if (transactionSignError) { + onError(transactionSignError); + } else { + onSuccess(signedOperation); + + const n = + navigation.getParent>() || + navigation; + n.pop(); + } + }, + onError, + }, + }); + }, + "device.transport": ({ appName, onSuccess, onCancel }) => { + navigation.navigate(ScreenName.DeviceConnect, { + appName, + onSuccess, + onClose: onCancel, + }); + }, + "exchange.start": ({ exchangeType, onSuccess, onCancel }) => { + navigation.navigate(NavigatorName.PlatformExchange, { + screen: ScreenName.PlatformStartExchange, + params: { + request: { + exchangeType: ExchangeType[exchangeType], + }, + onResult: (result: { + startExchangeResult?: string; + startExchangeError?: Error; + device?: Device; + }) => { + if (result.startExchangeError) { + onCancel(result.startExchangeError); + } + + if (result.startExchangeResult) { + setDevice(result.device); + onSuccess(result.startExchangeResult); + } + + const n = + navigation.getParent>() || + navigation; + n.pop(); + }, + }, + }); + }, + "exchange.complete": ({ exchangeParams, onSuccess, onCancel }) => { + navigation.navigate(NavigatorName.PlatformExchange, { + screen: ScreenName.PlatformCompleteExchange, + params: { + request: { + exchangeType: exchangeParams.exchangeType, + provider: exchangeParams.provider, + exchange: exchangeParams.exchange, + transaction: exchangeParams.transaction as Transaction, + binaryPayload: exchangeParams.binaryPayload, + signature: exchangeParams.signature, + feesStrategy: exchangeParams.feesStrategy, + }, + device, + onResult: (result: { operation?: Operation; error?: Error }) => { + if (result.error) { + onCancel(result.error); + } + if (result.operation) { + onSuccess(result.operation.id); + } + setDevice(undefined); + const n = + navigation.getParent>() || + navigation; + n.pop(); + }, + }, + }); + }, + }), + [navigation, device], + ); +} + +const wallet = { + name: "ledger-live-mobile", + version: VersionNumber.appVersion, +}; + +const tracking = trackingWrapper(track); + +function useGetUserId() { + const [userId, setUserId] = useState(""); + + useEffect(() => { + let mounted = true; + getOrCreateUser().then(({ user }) => { + if (mounted) setUserId(user.id); + }); + return () => { + mounted = false; + }; + }, []); + + return userId; +} diff --git a/apps/ledger-live-mobile/src/components/Web3AppWebview/types.ts b/apps/ledger-live-mobile/src/components/Web3AppWebview/types.ts index a2bac7d9715f..fc8757e48d9e 100644 --- a/apps/ledger-live-mobile/src/components/Web3AppWebview/types.ts +++ b/apps/ledger-live-mobile/src/components/Web3AppWebview/types.ts @@ -1,4 +1,5 @@ import { LiveAppManifest } from "@ledgerhq/live-common/platform/types"; +import WebView from "react-native-webview"; export type WebviewProps = { manifest: LiveAppManifest; @@ -15,9 +16,6 @@ export type WebviewState = { loading: boolean; }; -export type WebviewAPI = { - reload: () => void; - goBack: () => void; - goForward: () => void; +export type WebviewAPI = Pick & { loadURL: (url: string) => void; }; diff --git a/apps/ledger-live-mobile/src/react-native-hw-transport-ble/makeMock.ts b/apps/ledger-live-mobile/src/react-native-hw-transport-ble/makeMock.ts index d5a641cbb22d..56f1f3c294ea 100644 --- a/apps/ledger-live-mobile/src/react-native-hw-transport-ble/makeMock.ts +++ b/apps/ledger-live-mobile/src/react-native-hw-transport-ble/makeMock.ts @@ -6,7 +6,7 @@ import type { Observer as TransportObserver, DescriptorEvent } from "@ledgerhq/h import { HwTransportError } from "@ledgerhq/errors"; import type { ApduMock } from "../logic/createAPDUMock"; import { hookRejections } from "../logic/debugReject"; -import { e2eBridgeSubject } from "../../e2e/bridge/client"; +import { e2eBridgeClient } from "../../e2e/bridge/client"; export type DeviceMock = { id: string; @@ -39,7 +39,7 @@ export default (opts: Opts) => { static setLogLevel = (_param: string) => {}; static listen(observer: TransportObserver, HwTransportError>) { - return e2eBridgeSubject + return e2eBridgeClient .pipe( filter(msg => msg.type === "add"), take(3), @@ -59,7 +59,7 @@ export default (opts: Opts) => { } static async open(device: string | Device) { - await e2eBridgeSubject + await e2eBridgeClient .pipe( filter(msg => msg.type === "open"), first(), From 42704d2859f49029126f7f7cd125f9b4a48248bb Mon Sep 17 00:00:00 2001 From: Gregor Gilchrist Date: Wed, 5 Jul 2023 11:28:54 +0200 Subject: [PATCH 02/27] test: made server.close function wait for server onclose event to fire to prevent open handles --- .../tests/specs/services/buy.smoke.spec.ts | 4 ++-- .../tests/specs/services/discover.smoke.spec.ts | 4 ++-- .../tests/specs/services/wallet-api.smoke.spec.ts | 4 ++-- .../tests/utils/serve-dummy-app.ts | 9 ++++++++- apps/ledger-live-mobile/e2e/helpers.ts | 2 +- apps/ledger-live-mobile/e2e/specs/wallet-api.spec.ts | 11 +++++++---- 6 files changed, 22 insertions(+), 12 deletions(-) diff --git a/apps/ledger-live-desktop/tests/specs/services/buy.smoke.spec.ts b/apps/ledger-live-desktop/tests/specs/services/buy.smoke.spec.ts index 65fc15db4402..b928b9edc9ce 100644 --- a/apps/ledger-live-desktop/tests/specs/services/buy.smoke.spec.ts +++ b/apps/ledger-live-desktop/tests/specs/services/buy.smoke.spec.ts @@ -64,8 +64,8 @@ test.beforeAll(async ({ request }) => { } }); -test.afterAll(() => { - server.stop(); +test.afterAll(async () => { + await server.stop(); console.info(`========> Dummy test app stopped <=========`); delete process.env.MOCK_REMOTE_LIVE_MANIFEST; }); diff --git a/apps/ledger-live-desktop/tests/specs/services/discover.smoke.spec.ts b/apps/ledger-live-desktop/tests/specs/services/discover.smoke.spec.ts index e62833046bd8..8c4c6d27b8df 100644 --- a/apps/ledger-live-desktop/tests/specs/services/discover.smoke.spec.ts +++ b/apps/ledger-live-desktop/tests/specs/services/discover.smoke.spec.ts @@ -44,8 +44,8 @@ test.beforeAll(async ({ request }) => { } }); -test.afterAll(() => { - server.stop(); +test.afterAll(async () => { + await server.stop(); console.info(`========> Dummy test app stopped <=========`); delete process.env.MOCK_REMOTE_LIVE_MANIFEST; }); diff --git a/apps/ledger-live-desktop/tests/specs/services/wallet-api.smoke.spec.ts b/apps/ledger-live-desktop/tests/specs/services/wallet-api.smoke.spec.ts index 7871d96b4faa..f0dbdef4b0bc 100644 --- a/apps/ledger-live-desktop/tests/specs/services/wallet-api.smoke.spec.ts +++ b/apps/ledger-live-desktop/tests/specs/services/wallet-api.smoke.spec.ts @@ -48,8 +48,8 @@ test.beforeAll(async ({ request }) => { } }); -test.afterAll(() => { - server.stop(); +test.afterAll(async () => { + await server.stop(); console.info(`========> Dummy Wallet API app stopped <=========`); delete process.env.MOCK_REMOTE_LIVE_MANIFEST; }); diff --git a/apps/ledger-live-desktop/tests/utils/serve-dummy-app.ts b/apps/ledger-live-desktop/tests/utils/serve-dummy-app.ts index 86ffe60d15c4..9d6dae8cd38b 100644 --- a/apps/ledger-live-desktop/tests/utils/serve-dummy-app.ts +++ b/apps/ledger-live-desktop/tests/utils/serve-dummy-app.ts @@ -76,4 +76,11 @@ export const liveAppManifest = (params: Partial & Pick server.close(); +export const stop = (): Promise => { + server.close(); + return new Promise(resolve => { + server.on("close", () => { + resolve("Server Closed"); + }); + }); +}; diff --git a/apps/ledger-live-mobile/e2e/helpers.ts b/apps/ledger-live-mobile/e2e/helpers.ts index 18123155e171..e9474b02c7db 100644 --- a/apps/ledger-live-mobile/e2e/helpers.ts +++ b/apps/ledger-live-mobile/e2e/helpers.ts @@ -96,6 +96,6 @@ export async function openDeeplink(link?: string) { await device.openURL({ url: BASE_DEEPLINK + link }); } -export async function isAndroid() { +export function isAndroid() { return device.getPlatform() === "android"; } diff --git a/apps/ledger-live-mobile/e2e/specs/wallet-api.spec.ts b/apps/ledger-live-mobile/e2e/specs/wallet-api.spec.ts index 34924338fef4..2a92eac9466a 100644 --- a/apps/ledger-live-mobile/e2e/specs/wallet-api.spec.ts +++ b/apps/ledger-live-mobile/e2e/specs/wallet-api.spec.ts @@ -35,13 +35,12 @@ describe("Wallet API methods", () => { `========> Dummy Wallet API app successfully running on port ${port}! <=========`, ); } else { + continueTest = false; throw new Error("Ping response != 200, got: " + response.status); } } catch (error) { console.warn(`========> Dummy test app not running! <=========`); console.error(error); - - continueTest = false; } if (!continueTest || !isAndroid()) { @@ -66,11 +65,15 @@ describe("Wallet API methods", () => { expect(url).toBe("http://localhost:52619/?theme=light&lang=en&name=Dummy+Wallet+API+Live+App"); }); - afterAll(() => { - server.stop(); + afterAll(async () => { + await server.stop(); }); it("account.request", async () => { + if (!continueTest || !isAndroid()) { + return; // need to make this a proper ignore/jest warning + } + const { id, response } = await liveAppWebview.send({ method: "account.request", params: { From 8d5cc75dceda720c6d29ede7c20ee836045807ab Mon Sep 17 00:00:00 2001 From: Gregor Gilchrist Date: Wed, 5 Jul 2023 12:23:28 +0200 Subject: [PATCH 03/27] test: improved non-android console warnings --- apps/ledger-live-mobile/e2e/specs/deeplinks.spec.ts | 2 +- apps/ledger-live-mobile/e2e/specs/wallet-api.spec.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/ledger-live-mobile/e2e/specs/deeplinks.spec.ts b/apps/ledger-live-mobile/e2e/specs/deeplinks.spec.ts index 21aa22b05d6d..c3d1e6b325cb 100644 --- a/apps/ledger-live-mobile/e2e/specs/deeplinks.spec.ts +++ b/apps/ledger-live-mobile/e2e/specs/deeplinks.spec.ts @@ -97,7 +97,7 @@ describe("DeepLinks Tests", () => { }); it("should open Discover page and Mercuryo", async () => { - if (!(await isAndroid())) return; + if (!isAndroid()) return; await discoveryPage.openViaDeeplink(); await discoveryPage.expectDiscoveryPage(); await discoveryPage.openViaDeeplink(mercuryoDL.name); diff --git a/apps/ledger-live-mobile/e2e/specs/wallet-api.spec.ts b/apps/ledger-live-mobile/e2e/specs/wallet-api.spec.ts index 2a92eac9466a..bc7327db40f0 100644 --- a/apps/ledger-live-mobile/e2e/specs/wallet-api.spec.ts +++ b/apps/ledger-live-mobile/e2e/specs/wallet-api.spec.ts @@ -44,6 +44,7 @@ describe("Wallet API methods", () => { } if (!continueTest || !isAndroid()) { + console.warn("Stopping Wallet API test setup"); return; // need to make this a proper ignore/jest warning } @@ -71,6 +72,7 @@ describe("Wallet API methods", () => { it("account.request", async () => { if (!continueTest || !isAndroid()) { + console.warn("Stopping Wallet API test"); return; // need to make this a proper ignore/jest warning } From b522a2d477b337ef967400fe3b47f84f98c3b9b9 Mon Sep 17 00:00:00 2001 From: Gregor Gilchrist Date: Wed, 5 Jul 2023 14:50:39 +0200 Subject: [PATCH 04/27] test: fixed ReactNativeWebView typing --- .../tests/utils/dummy-wallet-app/src/hooks.ts | 4 ++-- .../utils/dummy-wallet-app/src/react-app-env.d.ts | 4 ++-- .../tests/utils/serve-dummy-app.ts | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/src/hooks.ts b/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/src/hooks.ts index 4884fe561559..47be31ef0c9f 100644 --- a/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/src/hooks.ts +++ b/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/src/hooks.ts @@ -16,9 +16,9 @@ export function useE2EInjection() { }); // if mobile - if (window.ReactNativeWebview) { + if (window.ReactNativeWebView) { const response = await promise; - window.ReactNativeWebview.postMessage(JSON.stringify({ type: "e2eTest", payload: response })); + window.ReactNativeWebView.postMessage(JSON.stringify({ type: "e2eTest", payload: response })); } return promise; diff --git a/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/src/react-app-env.d.ts b/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/src/react-app-env.d.ts index dea0ca3b214d..5b6e0126b658 100644 --- a/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/src/react-app-env.d.ts +++ b/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/src/react-app-env.d.ts @@ -4,11 +4,11 @@ interface Window { ledger: { e2e: { walletApi: { - send: (params: any) => void; + send: (params: unknown) => void; }; }; }; - ReactNativeWebview?: { + ReactNativeWebView?: { postMessage: (value: unknown) => void; }; } diff --git a/apps/ledger-live-desktop/tests/utils/serve-dummy-app.ts b/apps/ledger-live-desktop/tests/utils/serve-dummy-app.ts index 9d6dae8cd38b..3760e222fdbe 100644 --- a/apps/ledger-live-desktop/tests/utils/serve-dummy-app.ts +++ b/apps/ledger-live-desktop/tests/utils/serve-dummy-app.ts @@ -13,7 +13,7 @@ export const server = http.createServer((request, response) => { }); }); -export const start = (appPath: string, port = 0): Promise => { +export function start(appPath: string, port = 0): Promise { dummyAppPath = appPath; return new Promise((resolve, reject) => { @@ -27,9 +27,9 @@ export const start = (appPath: string, port = 0): Promise => { reject(error); }); }); -}; +} -export const liveAppManifest = (params: Partial & Pick) => { +export function liveAppManifest(params: Partial & Pick) { const manifest = [ { name: "Generic Live App", @@ -74,13 +74,13 @@ export const liveAppManifest = (params: Partial & Pick => { +export function stop(): Promise { server.close(); return new Promise(resolve => { server.on("close", () => { resolve("Server Closed"); }); }); -}; +} From b23aa469957df55ca474aaa165ef04424aea6254 Mon Sep 17 00:00:00 2001 From: JunichiSugiura Date: Wed, 5 Jul 2023 17:08:40 +0200 Subject: [PATCH 05/27] docs: mention useE2EInjection hook in README --- .../tests/utils/dummy-wallet-app/README.md | 35 ++++++++++++++----- .../e2e/specs/wallet-api.spec.ts | 2 ++ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/README.md b/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/README.md index 9f76b0bfc9a8..28412cb4f02d 100644 --- a/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/README.md +++ b/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/README.md @@ -5,9 +5,11 @@ The purpose of this app is to allow automated front end testing of Ledger Live's - handles the rendering of external Live Apps - handles calls of the Wallet API client from external Live apps -The app is a simple [Create React App](https://github.com/facebook/create-react-app) which uses the [Wallet API](https://www.npmjs.com/package/@ledgerhq/wallet-api).## How to run locally for development +The app is a simple [Create React App](https://github.com/facebook/create-react-app) which uses the [Wallet API](https://www.npmjs.com/package/@ledgerhq/wallet-api). -## Run dummy app locally +## How to run locally for development + +### Run dummy app locally ```sh pnpm i @@ -22,7 +24,7 @@ pnpm --filter="dummy-wallet-app" build pnpm --filter="dummy-wallet-app" serve ``` -## Run E2E test locally +### Run E2E test locally ```sh ### Setup @@ -46,7 +48,7 @@ pnpm mobile e2e:build -c android.emu.debug pnpm mobile e2e:test -c android.emu.debug apps/ledger-live-mobile/e2e/specs/wallet-api.spec.ts ``` -## [WIP] How it works? +## How it works? ### Desktop @@ -83,13 +85,16 @@ sequenceDiagram participant T as Test (Playwright) participant A as App (Electron) participant W as WebView + T->>A: await LiveApp.send activate T - A->>W: await window.ledger.e2e.walletApi.send(request) via webview.executeJavaScript + A->>W: await window.ledger.e2e.walletApi.send(request) activate A W-->>A: wallet-api request + activate W A-->>A: UI Hook (e.g. Side drawer to select account etc.) A-->>W: wallet-api response + deactivate W W->>A: Promise deactivate A A->>T: Promise @@ -97,7 +102,7 @@ sequenceDiagram ``` - `LiveApp.send`: This method turns JSON-RPC request object into JSON. Then, it calls injected method via [`webview.executeJavaScript`](https://www.electronjs.org/docs/latest/api/webview-tag#webviewexecutejavascriptcode-usergesture). It allows testing engineers to simply await for a response from Live's wallet server and `expect` against the response. -- `window.ledger.e2e.walletApi.send`: This is an injected method in Dummy App to send request to wallet api server. It returns `Promise`. +- `window.ledger.e2e.walletApi.send`: This is an injected method in Dummy App to send request to wallet api server. It returns `Promise` on desktop. ### Mobile @@ -106,15 +111,27 @@ sequenceDiagram participant T as Test (Jest) participant A as App (React Native) participant W as WebView (react-native-webview) + T->>A: await LiveApp.send activate T - A->>W: webview.injectJavScript + A->>W: window.ledger.e2e.walletApi.send(request) activate A W-->>A: wallet-api request + activate W A-->>A: UI Hook (e.g. Side drawer to select account etc.) A-->>W: wallet-api response - W->>A: postMessage(response) + deactivate W + W->>A: window.ReactNativeWebview.postMessage(response) deactivate A - A->>T: Promise + Note over A: onMessage + A->>T: bridge.sendWalletAPIResponse(response) deactivate T ``` + +- `LiveApp.send`: The same as desktop but uses [`webview.injectJavaScript`](https://github.com/react-native-webview/react-native-webview/blob/master/docs/Reference.md#injectjavascriptstr) instead. +- `window.ledger.e2e.walletApi.send`: This global method behaves differently on mobile. If dummy app finds `window.ReactNativeWebView` which injected by `react-native-webview` module, it does not return a promise but calls `window.ReactNativeWebView.postMessage` method to send response back to App runtime. This message will be catched on `onMessage` handler passed to the webview component. Then, the handler sends the request to Test runtime via E2E bridge(ws) + `e2eBridgeServer` subject. + + +### Dummy App + +`useE2EInjection` injects `window.ledger.e2e.walletApi.send` method. diff --git a/apps/ledger-live-mobile/e2e/specs/wallet-api.spec.ts b/apps/ledger-live-mobile/e2e/specs/wallet-api.spec.ts index bc7327db40f0..feebd2cb5cc3 100644 --- a/apps/ledger-live-mobile/e2e/specs/wallet-api.spec.ts +++ b/apps/ledger-live-mobile/e2e/specs/wallet-api.spec.ts @@ -1,6 +1,7 @@ import * as detox from "detox"; // this is because we need to use both the jest expect and the detox.expect version, which has some different assertions import { loadConfig } from "../bridge/server"; import { isAndroid } from "../helpers"; +// TODO: move it to common or make it as independent workspace import * as server from "../../../ledger-live-desktop/tests/utils/serve-dummy-app"; import PortfolioPage from "../models/wallet/portfolioPage"; import DiscoveryPage from "../models/discovery/discoveryPage"; @@ -16,6 +17,7 @@ let continueTest: boolean; describe("Wallet API methods", () => { beforeAll(async () => { + // TODO: Move this to LiveAppWebview // Check that dummy app in tests/utils/dummy-app-build has been started successfully try { const port = await server.start( From 7d9e35a652a1f0bcd2b3e99a4d516152f16b11a3 Mon Sep 17 00:00:00 2001 From: Gregor Gilchrist Date: Wed, 5 Jul 2023 19:17:04 +0200 Subject: [PATCH 06/27] chore: updating desktop docs --- .../tests/utils/dummy-wallet-app/README.md | 146 ++++++++++++------ 1 file changed, 97 insertions(+), 49 deletions(-) diff --git a/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/README.md b/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/README.md index 28412cb4f02d..9ec02da5dd70 100644 --- a/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/README.md +++ b/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/README.md @@ -1,4 +1,4 @@ -# Dummy App +# Dummy Wallet API App The purpose of this app is to allow automated front end testing of Ledger Live's Wallet API server implementation, and verify that Ledger Live correctly: @@ -7,24 +7,14 @@ The purpose of this app is to allow automated front end testing of Ledger Live's The app is a simple [Create React App](https://github.com/facebook/create-react-app) which uses the [Wallet API](https://www.npmjs.com/package/@ledgerhq/wallet-api). -## How to run locally for development - -### Run dummy app locally +## Local Dummy App setup steps ```sh pnpm i - -# Start development server -pnpm --filter="dummy-wallet-app" start - -# Create production build -pnpm --filter="dummy-wallet-app" build - -# Serve production build -pnpm --filter="dummy-wallet-app" serve +pnpm --filter="dummy-wallet-app" start # Start development server ``` -### Run E2E test locally +## Serve production build ```sh ### Setup @@ -33,52 +23,27 @@ pnpm i pnpm --filter="dummy-wallet-app" build ### Desktop - +pnpm build:lld:deps pnpm desktop build:testing pnpm desktop test:playwright wallet-api.spec.ts -### iOS +### Mobie +pnpm mobile start +### iOS +pnpm build:llm:deps pnpm mobile e2e:build -c ios.sim.debug pnpm mobile e2e:test -c ios.sim.debug apps/ledger-live-mobile/e2e/specs/wallet-api.spec.ts ### Android - +pnpm build:llm:deps pnpm mobile e2e:build -c android.emu.debug pnpm mobile e2e:test -c android.emu.debug apps/ledger-live-mobile/e2e/specs/wallet-api.spec.ts ``` -## How it works? +# How does it work? -### Desktop - -#### Loading dummy app in discover catalog - -Add manifest json string to `MOCK_REMOTE_LIVE_MANIFEST`. - -```typescript -import { getMockAppManifest } from "PATH/TO/utils/serve-dummy-app"; - -test.beforeAll(async () => { - process.env.MOCK_REMOTE_LIVE_MANIFEST = JSON.stringify( - // getMockAppManifest accepts object to override mock values - getMockAppManifest({ - id: "dummy-app", - url: "localhost:3000", - name: "Dummy App", - apiVersion: "2.0.0", - content: { - shortDescription: { - en: "App to test the Wallet API", - }, - description: { - en: "App to test the Wallet API with Playwright", - }, - }, - }), - ); -}); -``` +## Desktop ```mermaid sequenceDiagram @@ -101,9 +66,93 @@ sequenceDiagram deactivate T ``` -- `LiveApp.send`: This method turns JSON-RPC request object into JSON. Then, it calls injected method via [`webview.executeJavaScript`](https://www.electronjs.org/docs/latest/api/webview-tag#webviewexecutejavascriptcode-usergesture). It allows testing engineers to simply await for a response from Live's wallet server and `expect` against the response. +- `LiveApp.send`: This method turns a JSON-RPC request object into JSON. Then, it calls injected method via [`webview.executeJavaScript`](https://www.electronjs.org/docs/latest/api/webview-tag#webviewexecutejavascriptcode-usergesture). It allows testing engineers to simply await for a response from Live's wallet server and `expect` against the response. - `window.ledger.e2e.walletApi.send`: This is an injected method in Dummy App to send request to wallet api server. It returns `Promise` on desktop. +### Steps to write a test + +First start the local Dummy app at port 3000 by default and check that it can be accessed. Then we load a manifest which corresponds to this live app into the `MOCK_REMOTE_LIVE_MANIFEST` environmental variable (make sure the URL of the live app matches that of the live app, typically `http://localhost:3000`). + +```typescript +import { getMockAppManifest } from "PATH/TO/utils/serve-dummy-app"; + +test.beforeAll(async () => { + try { + const port = await server.start("dummy-wallet-app/build"); + const url = `http://localhost:${port}`; + const response = await request.get(url); + if (response.ok()) { + continueTest = true; + console.info( + `========> Dummy Wallet API app successfully running on port ${port}! <=========`, + ); + process.env.MOCK_REMOTE_LIVE_MANIFEST = JSON.stringify( + server.liveAppManifest({ + id: "dummy-live-app", + url, + name: "Dummy Wallet API Live App", + apiVersion: "2.0.0", + content: { + shortDescription: { + en: "App to test the Wallet API", + }, + description: { + en: "App to test the Wallet API with Playwright", + }, + }, + }), + ); + } else { + throw new Error("Ping response != 200, got: " + response.status); + } + } catch (error) { + console.warn(`========> Dummy test app not running! <=========`); + console.error(error); + } +}); +``` + +Then in the test we first send the Wallet API method that we want from the live app, perform any actions in Ledger Live, receive the response in the Live app, and verify the response by fulfilling the promise we initiated in the first step: + +```typescript +const discoverPage = new DiscoverPage(page); + +await test.step("account.request", async () => { + // previous test steps.... + + // generate a random id + const id = randomUUID(); + +// send the account.request method and save the promise for later + const resPromise = discoverPage.send({ + jsonrpc: "2.0", + id, + method: "account.request", + params: { + currencyIds: ["ethereum", "bitcoin"], + }, + }); + +// perform actions in Ledger Live to get the wallet API response + await drawer.selectCurrency("bitcoin"); + await drawer.selectAccount("bitcoin"); + +// get the response value that the live app received + const res = await resPromise; + + // verify the response is as expected + expect(res).toStrictEqual({ + jsonrpc: "2.0", + id, + result: { + rawAccount: { + id: "2d23ca2a-069e-579f-b13d-05bc706c7583", + // etc.. + } + } + }) +``` + ### Mobile ```mermaid @@ -131,7 +180,6 @@ sequenceDiagram - `LiveApp.send`: The same as desktop but uses [`webview.injectJavaScript`](https://github.com/react-native-webview/react-native-webview/blob/master/docs/Reference.md#injectjavascriptstr) instead. - `window.ledger.e2e.walletApi.send`: This global method behaves differently on mobile. If dummy app finds `window.ReactNativeWebView` which injected by `react-native-webview` module, it does not return a promise but calls `window.ReactNativeWebView.postMessage` method to send response back to App runtime. This message will be catched on `onMessage` handler passed to the webview component. Then, the handler sends the request to Test runtime via E2E bridge(ws) + `e2eBridgeServer` subject. - ### Dummy App `useE2EInjection` injects `window.ledger.e2e.walletApi.send` method. From cb69a367d73b497b6eef4d6e56994d57460e3976 Mon Sep 17 00:00:00 2001 From: Gregor Gilchrist Date: Thu, 6 Jul 2023 11:53:42 +0200 Subject: [PATCH 07/27] test: updated docs --- .../tests/utils/dummy-wallet-app/README.md | 177 ++++++++++++++---- .../tests/utils/serve-dummy-app.ts | 4 +- .../e2e/models/discovery/discoveryPage.ts | 4 +- .../e2e/models/liveApps/liveAppWebview.ts | 2 +- 4 files changed, 144 insertions(+), 43 deletions(-) diff --git a/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/README.md b/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/README.md index 9ec02da5dd70..67ece6e05cf6 100644 --- a/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/README.md +++ b/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/README.md @@ -67,11 +67,13 @@ sequenceDiagram ``` - `LiveApp.send`: This method turns a JSON-RPC request object into JSON. Then, it calls injected method via [`webview.executeJavaScript`](https://www.electronjs.org/docs/latest/api/webview-tag#webviewexecutejavascriptcode-usergesture). It allows testing engineers to simply await for a response from Live's wallet server and `expect` against the response. -- `window.ledger.e2e.walletApi.send`: This is an injected method in Dummy App to send request to wallet api server. It returns `Promise` on desktop. +- `window.ledger.e2e.walletApi.send`: This is an injected method in Dummy App to send a request to the Wallet API server. It returns `Promise` which is saved in a queue on the Dummy app side. +- Ledger Live Desktop either responds immediately with a response to the Wallet API request from the Live app, or after we've done some actions on the native LLD side (for example select an account, or mock a nano action). With this response the queued promise on the Live side is fulfilled with the result of the Wallet API request. +- The `webview.executeJavascript` promise on the Playwright test side is then subsequently fulfilled with the result of the Wallet API request, and we can use this to assert the expected result. ### Steps to write a test -First start the local Dummy app at port 3000 by default and check that it can be accessed. Then we load a manifest which corresponds to this live app into the `MOCK_REMOTE_LIVE_MANIFEST` environmental variable (make sure the URL of the live app matches that of the live app, typically `http://localhost:3000`). +First we start the local Dummy app at port 3000 by default and check that it can be accessed. Then we load a manifest which corresponds to this live app into the `MOCK_REMOTE_LIVE_MANIFEST` environmental variable (make sure the URL of the live app matches that of the live app, typically `http://localhost:3000`). ```typescript import { getMockAppManifest } from "PATH/TO/utils/serve-dummy-app"; @@ -118,42 +120,43 @@ Then in the test we first send the Wallet API method that we want from the live const discoverPage = new DiscoverPage(page); await test.step("account.request", async () => { - // previous test steps.... + // previous test steps.... // generate a random id - const id = randomUUID(); - -// send the account.request method and save the promise for later - const resPromise = discoverPage.send({ - jsonrpc: "2.0", - id, - method: "account.request", - params: { - currencyIds: ["ethereum", "bitcoin"], - }, - }); - -// perform actions in Ledger Live to get the wallet API response - await drawer.selectCurrency("bitcoin"); - await drawer.selectAccount("bitcoin"); - -// get the response value that the live app received - const res = await resPromise; - - // verify the response is as expected - expect(res).toStrictEqual({ - jsonrpc: "2.0", - id, - result: { - rawAccount: { - id: "2d23ca2a-069e-579f-b13d-05bc706c7583", - // etc.. - } - } - }) + const id = randomUUID(); + + // send the account.request method and save the promise for later + const resPromise = discoverPage.send({ + jsonrpc: "2.0", + id, + method: "account.request", + params: { + currencyIds: ["ethereum", "bitcoin"], + }, + }); + + // perform actions in Ledger Live to get the wallet API response + await drawer.selectCurrency("bitcoin"); + await drawer.selectAccount("bitcoin"); + + // get the response value that the live app received + const res = await resPromise; + + // verify the response is as expected + expect(res).toStrictEqual({ + jsonrpc: "2.0", + id, + result: { + rawAccount: { + id: "2d23ca2a-069e-579f-b13d-05bc706c7583", + // etc.. + }, + }, + }); +}); ``` -### Mobile +## Mobile ```mermaid sequenceDiagram @@ -177,9 +180,107 @@ sequenceDiagram deactivate T ``` -- `LiveApp.send`: The same as desktop but uses [`webview.injectJavaScript`](https://github.com/react-native-webview/react-native-webview/blob/master/docs/Reference.md#injectjavascriptstr) instead. -- `window.ledger.e2e.walletApi.send`: This global method behaves differently on mobile. If dummy app finds `window.ReactNativeWebView` which injected by `react-native-webview` module, it does not return a promise but calls `window.ReactNativeWebView.postMessage` method to send response back to App runtime. This message will be catched on `onMessage` handler passed to the webview component. Then, the handler sends the request to Test runtime via E2E bridge(ws) + `e2eBridgeServer` subject. +The mobile side works similarly to desktop but with a few key differences: + +- `LiveAppWebview.send`: The same as desktop but uses [`webview.runScript`](https://wix.github.io/Detox/docs/api/webviews/#runscriptscript) from detox instead. +- `window.ledger.e2e.walletApi.send`: This global method in the dummy app behaves differently on mobile. In desktop/playwright the JS injection `send` method in the live app returns the response to the test process, whereas the mobile/detox method doesn't return anything. To get around this the live app looks for `window.ReactNativeWebView` which is injected by the `react-native-webview` module, and then calls `window.ReactNativeWebView.postMessage` method to send the Wallet API response back to the LLM App runtime. This message will be caught with the `onMessage` handler passed to the LLM webview component. Then, the handler sends the request to the Test runtime via the E2E websocket bridge server. Finally the bridge server sends the response to the test via `e2eBridgeServer` RxJS subject. + +### Steps to write a test + +First we start the local Dummy app at port 52619 (hardcoded for now) which matches the live app manifest in `.env.mock`. In future we can make this dynamic by passing a manifest from the test to LLM Live app Provider via the E2E Bridge, but this way is simpler for initial tests. + +Then in the `beforeAll` we check that the server is running and then navigate to the live app. + +```typescript +import { getMockAppManifest } from "PATH/TO/utils/serve-dummy-app"; + +test.beforeAll(async () => { + try { + const port = await server.start("dummy-wallet-app/build"); + const url = `http://localhost:${port}`; + const response = await request.get(url); + if (response.ok()) { + continueTest = true; + console.info( + `========> Dummy Wallet API app successfully running on port ${port}! <=========`, + ); + process.env.MOCK_REMOTE_LIVE_MANIFEST = JSON.stringify( + server.liveAppManifest({ + id: "dummy-live-app", + url, + name: "Dummy Wallet API Live App", + apiVersion: "2.0.0", + content: { + shortDescription: { + en: "App to test the Wallet API", + }, + description: { + en: "App to test the Wallet API with Playwright", + }, + }, + }), + ); + } else { + throw new Error("Ping response != 200, got: " + response.status); + } + } catch (error) { + console.warn(`========> Dummy test app not running! <=========`); + console.error(error); + } + + if (!continueTest || !isAndroid()) { + console.warn("Stopping Wallet API test setup"); + return; // need to make this a proper ignore/jest warning + } + + // start navigation + portfolioPage = new PortfolioPage(); + discoverPage = new DiscoveryPage(); + liveAppWebview = new LiveAppWebview(); + cryptoDrawer = new CryptoDrawer(); + + loadConfig("1AccountBTC1AccountETHReadOnlyFalse", true); + + await portfolioPage.waitForPortfolioPageToLoad(); + await discoverPage.openViaDeeplink("dummy-live-app"); + + const title = await detox.web.element(detox.by.web.id("image-container")).getTitle(); + expect(title).toBe("Dummy Wallet API App"); +}); +``` + +Then in the test we first send the Wallet API method that we want from the live app, perform any actions in Ledger Live, receive the response in the Live app, and verify the response by fulfilling the promise we initiated in the first step: + +```typescript +it("account.request", async () => { + // send wallet API request for the webview to send to LLM + const { id, response } = await liveAppWebview.send({ + method: "account.request", + params: { + currencyIds: ["ethereum", "bitcoin"], + }, + }); + + // perform any required actions in LLM for the test + await cryptoDrawer.selectCurrencyFromDrawer("Bitcoin"); + await cryptoDrawer.selectAccountFromDrawer("Bitcoin 1 (legacy)"); + + // verify the response after the Wallet API response is received and all the test steps are finished + await expect(response).resolves.toMatchObject({ + jsonrpc: "2.0", + id, + result: { + rawAccount: { + id: "2d23ca2a-069e-579f-b13d-05bc706c7583", + address: "1xeyL26EKAAR3pStd7wEveajk4MQcrYezeJ", + }, + }, + }); +}); +``` + +## Dummy App -### Dummy App +The most important part of the dummy app is handled by `useE2EInjection()`, which injects the `window.ledger.e2e.walletApi.send()` method. This is the method that is used by the tests to simulate a user doing an action that requires a Wallet API response from Ledger Live. -`useE2EInjection` injects `window.ledger.e2e.walletApi.send` method. +`useE2EInjection()` also initialises the Wallet API transports (like any live app needs to communicate with Ledger Live), and also has a 'pending request' queue that keeps track of the requests it has sent to the Wallet API, so we can send responses back to the test for assertion. diff --git a/apps/ledger-live-desktop/tests/utils/serve-dummy-app.ts b/apps/ledger-live-desktop/tests/utils/serve-dummy-app.ts index 3760e222fdbe..b13e3c294d9a 100644 --- a/apps/ledger-live-desktop/tests/utils/serve-dummy-app.ts +++ b/apps/ledger-live-desktop/tests/utils/serve-dummy-app.ts @@ -76,11 +76,11 @@ export function liveAppManifest(params: Partial & Pick { +export function stop(): Promise { server.close(); return new Promise(resolve => { server.on("close", () => { - resolve("Server Closed"); + resolve(); }); }); } diff --git a/apps/ledger-live-mobile/e2e/models/discovery/discoveryPage.ts b/apps/ledger-live-mobile/e2e/models/discovery/discoveryPage.ts index 5824396cc395..5bb2ed93677a 100644 --- a/apps/ledger-live-mobile/e2e/models/discovery/discoveryPage.ts +++ b/apps/ledger-live-mobile/e2e/models/discovery/discoveryPage.ts @@ -4,7 +4,7 @@ import { expect } from "detox"; const baseLink = "discover/"; export default class DiscoveryPage { - getDicoveryBanner = () => getElementById("discover-banner"); + getDiscoveryBanner = () => getElementById("discover-banner"); waitForSelectCrypto = () => waitForElementByText("Select crypto"); async openViaDeeplink(discoverApps = "") { @@ -12,6 +12,6 @@ export default class DiscoveryPage { } async expectDiscoveryPage() { - await expect(this.getDicoveryBanner()).toBeVisible(); + await expect(this.getDiscoveryBanner()).toBeVisible(); } } diff --git a/apps/ledger-live-mobile/e2e/models/liveApps/liveAppWebview.ts b/apps/ledger-live-mobile/e2e/models/liveApps/liveAppWebview.ts index 0a58b463c6bd..6d831f6dbb01 100644 --- a/apps/ledger-live-mobile/e2e/models/liveApps/liveAppWebview.ts +++ b/apps/ledger-live-mobile/e2e/models/liveApps/liveAppWebview.ts @@ -13,7 +13,7 @@ export default class LiveAppWebview { ...params, }); - await webview.runScript(`function foo(element) { + await webview.runScript(`function sendWalletAPIRequestFromLiveApp(webviewElement) { window.ledger.e2e.walletApi.send('${json}'); }`); From 044cd1535efd57c00eaf324fec19bb91b1e76e4b Mon Sep 17 00:00:00 2001 From: JunichiSugiura Date: Thu, 6 Jul 2023 14:27:32 +0200 Subject: [PATCH 08/27] fix: remove dup logic --- .../src/components/Web3AppWebview/PlatformAPIWebview.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/apps/ledger-live-mobile/src/components/Web3AppWebview/PlatformAPIWebview.tsx b/apps/ledger-live-mobile/src/components/Web3AppWebview/PlatformAPIWebview.tsx index 69a633e13466..c30255d56d88 100644 --- a/apps/ledger-live-mobile/src/components/Web3AppWebview/PlatformAPIWebview.tsx +++ b/apps/ledger-live-mobile/src/components/Web3AppWebview/PlatformAPIWebview.tsx @@ -57,20 +57,15 @@ function renderLoading() { } export const PlatformAPIWebview = forwardRef( ({ manifest, inputs = {}, onStateChange }, ref) => { - const { webviewProps, webviewState, webviewRef } = useWebviewState( + const { webviewProps, webviewRef } = useWebviewState( { manifest, inputs, }, ref, + onStateChange, ); - useEffect(() => { - if (onStateChange) { - onStateChange(webviewState); - } - }, [webviewState, onStateChange]); - const accounts = useSelector(flattenAccountsSelector); const navigation = useNavigation< From 19f7a70a335d18fc60a4b28aee81f2c29d972f8c Mon Sep 17 00:00:00 2001 From: Gregor Gilchrist Date: Fri, 7 Jul 2023 13:51:12 +0200 Subject: [PATCH 09/27] chore: update readme and skipping test --- .../tests/utils/dummy-wallet-app/README.md | 46 +++---------------- .../e2e/specs/wallet-api.spec.ts | 2 +- 2 files changed, 8 insertions(+), 40 deletions(-) diff --git a/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/README.md b/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/README.md index 67ece6e05cf6..9700f555c331 100644 --- a/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/README.md +++ b/apps/ledger-live-desktop/tests/utils/dummy-wallet-app/README.md @@ -78,39 +78,10 @@ First we start the local Dummy app at port 3000 by default and check that it can ```typescript import { getMockAppManifest } from "PATH/TO/utils/serve-dummy-app"; +let continueTest = false; + test.beforeAll(async () => { - try { - const port = await server.start("dummy-wallet-app/build"); - const url = `http://localhost:${port}`; - const response = await request.get(url); - if (response.ok()) { - continueTest = true; - console.info( - `========> Dummy Wallet API app successfully running on port ${port}! <=========`, - ); - process.env.MOCK_REMOTE_LIVE_MANIFEST = JSON.stringify( - server.liveAppManifest({ - id: "dummy-live-app", - url, - name: "Dummy Wallet API Live App", - apiVersion: "2.0.0", - content: { - shortDescription: { - en: "App to test the Wallet API", - }, - description: { - en: "App to test the Wallet API with Playwright", - }, - }, - }), - ); - } else { - throw new Error("Ping response != 200, got: " + response.status); - } - } catch (error) { - console.warn(`========> Dummy test app not running! <=========`); - console.error(error); - } + continueTest = await LiveApp.start(request); }); ``` @@ -125,8 +96,8 @@ await test.step("account.request", async () => { // generate a random id const id = randomUUID(); - // send the account.request method and save the promise for later - const resPromise = discoverPage.send({ + // send the account.request method. This send method gives us the Wallet API message id and a promise from the 'inject javascript' step that we will resolve later + const { id, response } = discoverPage.send({ jsonrpc: "2.0", id, method: "account.request", @@ -139,11 +110,8 @@ await test.step("account.request", async () => { await drawer.selectCurrency("bitcoin"); await drawer.selectAccount("bitcoin"); - // get the response value that the live app received - const res = await resPromise; - - // verify the response is as expected - expect(res).toStrictEqual({ + // verify the response is as expected. Be careful to only resolve this one all the required user actions are finished + await expect(res).resolves.toStrictEqual({ jsonrpc: "2.0", id, result: { diff --git a/apps/ledger-live-mobile/e2e/specs/wallet-api.spec.ts b/apps/ledger-live-mobile/e2e/specs/wallet-api.spec.ts index feebd2cb5cc3..1f1844a78afa 100644 --- a/apps/ledger-live-mobile/e2e/specs/wallet-api.spec.ts +++ b/apps/ledger-live-mobile/e2e/specs/wallet-api.spec.ts @@ -15,7 +15,7 @@ let cryptoDrawer: CryptoDrawer; let continueTest: boolean; -describe("Wallet API methods", () => { +describe.skip("Wallet API methods", () => { beforeAll(async () => { // TODO: Move this to LiveAppWebview // Check that dummy app in tests/utils/dummy-app-build has been started successfully From 98011010bb99b78d12df2d288b2e17c63ef80e75 Mon Sep 17 00:00:00 2001 From: Gregor Gilchrist Date: Fri, 7 Jul 2023 15:14:28 +0200 Subject: [PATCH 10/27] feat: created new test-utils directory to contain dummy live apps --- .../dummy-live-app/build/asset-manifest.json | 13 ++++++ .../dummy-live-app/build/index.html | 1 + .../build/static/css/main.27dca9f4.css | 2 + .../build/static/css/main.27dca9f4.css.map | 1 + .../build/static/js/main.ee6f3787.js | 3 ++ .../static/js/main.ee6f3787.js.LICENSE.txt | 43 ++++++++++++++++++ .../build/static/js/main.ee6f3787.js.map | 1 + .../build/asset-manifest.json | 13 ++++++ .../dummy-wallet-app/build/dummy-icon.png | Bin 0 -> 3115 bytes .../dummy-wallet-app/build/index.html | 1 + .../dummy-wallet-app/build/ledger-logo.png | Bin 0 -> 2372 bytes .../build/static/css/main.b9e6a4f6.css | 2 + .../build/static/css/main.b9e6a4f6.css.map | 1 + .../build/static/js/main.f841cea8.js | 3 ++ .../static/js/main.f841cea8.js.LICENSE.txt | 43 ++++++++++++++++++ .../build/static/js/main.f841cea8.js.map | 1 + 16 files changed, 128 insertions(+) create mode 100644 libs/test-utils/dummy-live-app/build/asset-manifest.json create mode 100644 libs/test-utils/dummy-live-app/build/index.html create mode 100644 libs/test-utils/dummy-live-app/build/static/css/main.27dca9f4.css create mode 100644 libs/test-utils/dummy-live-app/build/static/css/main.27dca9f4.css.map create mode 100644 libs/test-utils/dummy-live-app/build/static/js/main.ee6f3787.js create mode 100644 libs/test-utils/dummy-live-app/build/static/js/main.ee6f3787.js.LICENSE.txt create mode 100644 libs/test-utils/dummy-live-app/build/static/js/main.ee6f3787.js.map create mode 100644 libs/test-utils/dummy-wallet-app/build/asset-manifest.json create mode 100644 libs/test-utils/dummy-wallet-app/build/dummy-icon.png create mode 100644 libs/test-utils/dummy-wallet-app/build/index.html create mode 100644 libs/test-utils/dummy-wallet-app/build/ledger-logo.png create mode 100644 libs/test-utils/dummy-wallet-app/build/static/css/main.b9e6a4f6.css create mode 100644 libs/test-utils/dummy-wallet-app/build/static/css/main.b9e6a4f6.css.map create mode 100644 libs/test-utils/dummy-wallet-app/build/static/js/main.f841cea8.js create mode 100644 libs/test-utils/dummy-wallet-app/build/static/js/main.f841cea8.js.LICENSE.txt create mode 100644 libs/test-utils/dummy-wallet-app/build/static/js/main.f841cea8.js.map diff --git a/libs/test-utils/dummy-live-app/build/asset-manifest.json b/libs/test-utils/dummy-live-app/build/asset-manifest.json new file mode 100644 index 000000000000..f9e88271ae46 --- /dev/null +++ b/libs/test-utils/dummy-live-app/build/asset-manifest.json @@ -0,0 +1,13 @@ +{ + "files": { + "main.css": "/static/css/main.27dca9f4.css", + "main.js": "/static/js/main.ee6f3787.js", + "index.html": "/index.html", + "main.27dca9f4.css.map": "/static/css/main.27dca9f4.css.map", + "main.ee6f3787.js.map": "/static/js/main.ee6f3787.js.map" + }, + "entrypoints": [ + "static/css/main.27dca9f4.css", + "static/js/main.ee6f3787.js" + ] +} \ No newline at end of file diff --git a/libs/test-utils/dummy-live-app/build/index.html b/libs/test-utils/dummy-live-app/build/index.html new file mode 100644 index 000000000000..1f8a5ae80740 --- /dev/null +++ b/libs/test-utils/dummy-live-app/build/index.html @@ -0,0 +1 @@ +Ledger Live App
    \ No newline at end of file diff --git a/libs/test-utils/dummy-live-app/build/static/css/main.27dca9f4.css b/libs/test-utils/dummy-live-app/build/static/css/main.27dca9f4.css new file mode 100644 index 000000000000..90fa50ca5dd7 --- /dev/null +++ b/libs/test-utils/dummy-live-app/build/static/css/main.27dca9f4.css @@ -0,0 +1,2 @@ +body{font-family:system-ui,Segoe UI,Roboto,sans-serif;margin:0}::-webkit-scrollbar{display:none}#root{height:100%;width:100%}.App{text-align:center}.App-logo{height:10vmin}.App-header{align-items:center;background-color:#282c34;color:#fff;display:flex;flex-direction:column;font-size:calc(10px + 2vmin);min-height:100vh}.button-container{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:center;width:100%}.output-container{background-color:#000;font-size:12px;overflow:scroll;text-align:start;width:90%} +/*# sourceMappingURL=main.27dca9f4.css.map*/ \ No newline at end of file diff --git a/libs/test-utils/dummy-live-app/build/static/css/main.27dca9f4.css.map b/libs/test-utils/dummy-live-app/build/static/css/main.27dca9f4.css.map new file mode 100644 index 000000000000..b253f9ce5ce3 --- /dev/null +++ b/libs/test-utils/dummy-live-app/build/static/css/main.27dca9f4.css.map @@ -0,0 +1 @@ +{"version":3,"file":"static/css/main.27dca9f4.css","mappings":"AAAA,KAEE,gDAAwD,CADxD,QAEF,CAEA,oBACE,YACF,CAEA,MAEE,WAAY,CADZ,UAEF,CCZA,KACE,iBACF,CAEA,UACE,aACF,CAEA,YAKE,kBAAmB,CAJnB,wBAAyB,CAMzB,UAAY,CAJZ,YAAa,CACb,qBAAsB,CAEtB,4BAA6B,CAJ7B,gBAMF,CAEA,kBACE,YAAa,CACb,kBAAmB,CACnB,cAAe,CACf,sBAAuB,CACvB,UACF,CAEA,kBACE,qBAAuB,CAIvB,cAAe,CAFf,eAAgB,CADhB,gBAAiB,CAIjB,SACF","sources":["index.css","App.css"],"sourcesContent":["body {\n margin: 0;\n font-family: system-ui, \"Segoe UI\", \"Roboto\", sans-serif;\n}\n\n*::-webkit-scrollbar {\n display: none;\n}\n\n#root {\n width: 100%;\n height: 100%;\n}\n",".App {\n text-align: center;\n}\n\n.App-logo {\n height: 10vmin;\n}\n\n.App-header {\n background-color: #282c34;\n min-height: 100vh;\n display: flex;\n flex-direction: column;\n align-items: center;\n font-size: calc(10px + 2vmin);\n color: white;\n}\n\n.button-container {\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n justify-content: center;\n width: 100%;\n}\n\n.output-container {\n background-color: black;\n text-align: start;\n overflow: scroll;\n /* margin: 10; */\n font-size: 12px;\n width: 90%;\n}\n"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/libs/test-utils/dummy-live-app/build/static/js/main.ee6f3787.js b/libs/test-utils/dummy-live-app/build/static/js/main.ee6f3787.js new file mode 100644 index 000000000000..d3ec563abf54 --- /dev/null +++ b/libs/test-utils/dummy-live-app/build/static/js/main.ee6f3787.js @@ -0,0 +1,3 @@ +/*! For license information please see main.ee6f3787.js.LICENSE.txt */ +!function(){var e={757:function(e,t,n){"use strict";var r=n(1108).default,o=n(294).default,a=n(8527).default,i=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))((function(o,a){function i(e){try{u(r.next(e))}catch(t){a(t)}}function l(e){try{u(r.throw(e))}catch(t){a(t)}}function u(e){var t;e.done?o(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(i,l)}u((r=r.apply(e,t||[])).next())}))},l=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});var u=n(7656),c=l(n(5223)),s=n(9345),f=n(4033),d=new c.default("LL-PlatformSDK"),p=function(){function e(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:d;o(this,e),this.transport=t,this.logger=n}return a(e,[{key:"_request",value:function(e,t){return i(this,void 0,void 0,r().mark((function n(){var o;return r().wrap((function(n){for(;;)switch(n.prev=n.next){case 0:if(this.serverAndClient){n.next=3;break}throw this.logger.error("not connected - ".concat(e)),new Error("Ledger Live API not connected");case 3:return this.logger.log("request - ".concat(e),t),n.prev=4,n.next=7,this.serverAndClient.request(e,t);case 7:return o=n.sent,this.logger.log("response - ".concat(e),t),n.abrupt("return",o);case 12:throw n.prev=12,n.t0=n.catch(4),this.logger.warn("error - ".concat(e),t),n.t0;case 16:case"end":return n.stop()}}),n,this,[[4,12]])})))}},{key:"connect",value:function(){var e=this,t=new u.JSONRPCServerAndClient(new u.JSONRPCServer,new u.JSONRPCClient((function(t){return e.transport.send(t)})));this.transport.onMessage=function(e){return t.receiveAndSend(e)},this.transport.connect(),this.serverAndClient=t,this.logger.log("connected",this.transport)}},{key:"disconnect",value:function(){delete this.serverAndClient,this.transport.disconnect(),this.logger.log("disconnected",this.transport)}},{key:"bridgeApp",value:function(e){return i(this,void 0,void 0,r().mark((function e(){return r().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:throw new Error("Function is not implemented yet");case 1:case"end":return e.stop()}}),e)})))}},{key:"bridgeDashboard",value:function(){return i(this,void 0,void 0,r().mark((function e(){return r().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:throw new Error("Function is not implemented yet");case 1:case"end":return e.stop()}}),e)})))}},{key:"startExchange",value:function(e){var t=e.exchangeType;return i(this,void 0,void 0,r().mark((function e(){return r().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",this._request("exchange.start",{exchangeType:t}));case 1:case"end":return e.stop()}}),e,this)})))}},{key:"completeExchange",value:function(e){var t=e.provider,n=e.fromAccountId,o=e.toAccountId,a=e.transaction,l=e.binaryPayload,u=e.signature,c=e.feesStrategy,s=e.exchangeType;return i(this,void 0,void 0,r().mark((function e(){return r().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(s!==f.ExchangeType.SWAP||o){e.next=2;break}throw new Error("Missing parameter 'toAccountId' for a swap operation");case 2:return e.abrupt("return",this._request("exchange.complete",{provider:t,fromAccountId:n,toAccountId:o,transaction:a,binaryPayload:l,signature:u,feesStrategy:c,exchangeType:s}));case 3:case"end":return e.stop()}}),e,this)})))}},{key:"signTransaction",value:function(e,t,n){return i(this,void 0,void 0,r().mark((function o(){return r().wrap((function(r){for(;;)switch(r.prev=r.next){case 0:return r.abrupt("return",this._request("transaction.sign",{accountId:e,transaction:(0,s.serializeTransaction)(t),params:n||{}}));case 1:case"end":return r.stop()}}),o,this)})))}},{key:"broadcastSignedTransaction",value:function(e,t){return i(this,void 0,void 0,r().mark((function n(){return r().wrap((function(n){for(;;)switch(n.prev=n.next){case 0:return n.abrupt("return",this._request("transaction.broadcast",{accountId:e,signedTransaction:t}));case 1:case"end":return n.stop()}}),n,this)})))}},{key:"listAccounts",value:function(){return i(this,void 0,void 0,r().mark((function e(){var t;return r().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,this._request("account.list");case 2:return t=e.sent,e.abrupt("return",t.map(s.deserializeAccount));case 4:case"end":return e.stop()}}),e,this)})))}},{key:"requestAccount",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return i(this,void 0,void 0,r().mark((function t(){var n;return r().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,this._request("account.request",e);case 2:return n=t.sent,t.abrupt("return",(0,s.deserializeAccount)(n));case 4:case"end":return t.stop()}}),t,this)})))}},{key:"receive",value:function(e){return i(this,void 0,void 0,r().mark((function t(){return r().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.abrupt("return",this._request("account.receive",{accountId:e}));case 1:case"end":return t.stop()}}),t,this)})))}},{key:"synchronizeAccount",value:function(e){return i(this,void 0,void 0,r().mark((function e(){return r().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:throw new Error("Function is not implemented yet");case 1:case"end":return e.stop()}}),e)})))}},{key:"listCurrencies",value:function(e){return i(this,void 0,void 0,r().mark((function t(){return r().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.abrupt("return",this._request("currency.list",e||{}));case 1:case"end":return t.stop()}}),t,this)})))}},{key:"getLastConnectedDeviceInfo",value:function(){return i(this,void 0,void 0,r().mark((function e(){return r().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:throw new Error("Function is not implemented yet");case 1:case"end":return e.stop()}}),e)})))}},{key:"listApps",value:function(){return i(this,void 0,void 0,r().mark((function e(){return r().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:throw new Error("Function is not implemented yet");case 1:case"end":return e.stop()}}),e)})))}}]),e}();t.default=p},5311:function(e,t,n){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.deserializeAlgorandTransaction=t.serializeAlgorandTransaction=void 0;var o=r(n(2083));t.serializeAlgorandTransaction=function(e){var t=e.family,n=e.mode,r=e.fees,o=e.assetId,a=e.memo,i=e.amount,l=e.recipient;return{family:t,amount:i.toString(),recipient:l,fees:r?r.toString():void 0,memo:a,mode:n,assetId:o}};t.deserializeAlgorandTransaction=function(e){var t=e.family,n=e.mode,r=e.fees,a=e.assetId,i=e.memo,l=e.amount,u=e.recipient;return{family:t,amount:new o.default(l),recipient:u,fees:r?new o.default(r):void 0,memo:i,mode:n,assetId:a}}},8906:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0})},3167:function(e,t,n){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.deserializeBitcoinTransaction=t.serializeBitcoinTransaction=void 0;var o=r(n(2083));t.serializeBitcoinTransaction=function(e){var t=e.family,n=e.amount,r=e.recipient,o=e.feePerByte;return{family:t,amount:n.toString(),recipient:r,feePerByte:o?o.toString():void 0}},t.deserializeBitcoinTransaction=function(e){var t=e.family,n=e.amount,r=e.recipient,a=e.feePerByte;return{family:t,amount:new o.default(n),recipient:r,feePerByte:a?new o.default(a):void 0}}},1978:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0})},993:function(e,t,n){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.deserializeCosmosTransaction=t.serializeCosmosTransaction=void 0;var o=r(n(2083));t.serializeCosmosTransaction=function(e){var t=e.amount,n=e.recipient,r=e.family,o=e.mode,a=e.fees,i=e.gas,l=e.memo;return{amount:t.toString(),recipient:n,family:r,mode:o,fees:a?a.toString():void 0,gas:i?i.toString():void 0,memo:l}};t.deserializeCosmosTransaction=function(e){var t=e.amount,n=e.recipient,r=e.family,a=e.mode,i=e.fees,l=e.gas,u=e.memo;return{amount:new o.default(t),recipient:n,family:r,mode:a,fees:i?new o.default(i):void 0,gas:l?new o.default(l):void 0,memo:u}}},8502:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0})},3498:function(e,t,n){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.deserializeCryptoOrgTransaction=t.serializeCryptoOrgTransaction=void 0;var o=r(n(2083));t.serializeCryptoOrgTransaction=function(e){var t=e.family,n=e.mode,r=e.fees,o=e.amount,a=e.recipient;return{family:t,amount:o.toString(),recipient:a,fees:r?r.toString():void 0,mode:n}};t.deserializeCryptoOrgTransaction=function(e){var t=e.family,n=e.mode,r=e.fees,a=e.amount,i=e.recipient;return{family:t,amount:new o.default(a),recipient:i,fees:r?new o.default(r):void 0,mode:n}}},2148:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0})},3878:function(e,t,n){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.deserializeEthereumTransaction=t.serializeEthereumTransaction=void 0;var o=r(n(2083));t.serializeEthereumTransaction=function(e){var t=e.family,n=e.amount,r=e.recipient,o=e.nonce,a=e.data,i=e.gasPrice,l=e.gasLimit;return{family:t,amount:n.toString(),recipient:r,nonce:o,data:a?a.toString("hex"):void 0,gasPrice:i?i.toString():void 0,gasLimit:l?l.toString():void 0}},t.deserializeEthereumTransaction=function(e){var t=e.family,n=e.amount,r=e.recipient,a=e.nonce,i=e.data,l=e.gasPrice,u=e.gasLimit;return{family:t,amount:new o.default(n),recipient:r,nonce:a,data:i?Buffer.from(i,"hex"):void 0,gasPrice:l?new o.default(l):void 0,gasLimit:u?new o.default(u):void 0}}},6829:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0})},777:function(e,t,n){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.deserializePolkadotTransaction=t.serializePolkadotTransaction=void 0;var o=r(n(2083));t.serializePolkadotTransaction=function(e){var t=e.amount,n=e.recipient,r=e.family,o=e.mode,a=e.fee,i=e.era;return{amount:t.toString(),recipient:n,family:r,mode:o,fee:a?a.toString():void 0,era:i}};t.deserializePolkadotTransaction=function(e){var t=e.amount,n=e.recipient,r=e.family,a=e.mode,i=e.fee,l=e.era;return{amount:new o.default(t),recipient:n,family:r,mode:a,fee:i?new o.default(i):void 0,era:l}}},7411:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0})},8113:function(e,t,n){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.deserializeRippleTransaction=t.serializeRippleTransaction=void 0;var o=r(n(2083));t.serializeRippleTransaction=function(e){var t=e.family,n=e.fee,r=e.tag,o=e.amount,a=e.recipient;return{family:t,amount:o.toString(),recipient:a,fee:n?n.toString():void 0,tag:r}};t.deserializeRippleTransaction=function(e){var t=e.family,n=e.fee,r=e.tag,a=e.amount,i=e.recipient;return{family:t,amount:new o.default(a),recipient:i,fee:n?new o.default(n):void 0,tag:r}}},6540:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0})},5237:function(e,t,n){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.deserializeStellarTransaction=t.serializeStellarTransaction=void 0;var o=r(n(2083));t.serializeStellarTransaction=function(e){var t=e.amount,n=e.recipient,r=e.family,o=e.fees,a=e.memoType,i=e.memoValue;return{amount:t.toString(),recipient:n,family:r,fees:o?o.toString():void 0,memoType:a,memoValue:i}};t.deserializeStellarTransaction=function(e){var t=e.amount,n=e.recipient,r=e.family,a=e.fees,i=e.memoType,l=e.memoValue;return{amount:new o.default(t),recipient:n,family:r,fees:a?new o.default(a):void 0,memoType:i,memoValue:l}}},4762:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0})},2690:function(e,t,n){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.deserializeTezosTransaction=t.serializeTezosTransaction=void 0;var o=r(n(2083));t.serializeTezosTransaction=function(e){var t=e.amount,n=e.recipient,r=e.family,o=e.mode,a=e.fees,i=e.gasLimit;return{amount:t.toString(),recipient:n,family:r,mode:o,fees:a?a.toString():void 0,gasLimit:i?i.toString():void 0}};t.deserializeTezosTransaction=function(e){var t=e.amount,n=e.recipient,r=e.family,a=e.mode,i=e.fees,l=e.gasLimit;return{amount:new o.default(t),recipient:n,family:r,mode:a,fees:i?new o.default(i):void 0,gasLimit:l?new o.default(l):void 0}}},4919:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0})},1346:function(e,t,n){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.deserializeTronTransaction=t.serializeTronTransaction=void 0;var o=r(n(2083));t.serializeTronTransaction=function(e){var t=e.amount,n=e.recipient,r=e.family,o=e.mode,a=e.resource,i=e.duration;return{amount:t.toString(),recipient:n,family:r,mode:o,resource:a,duration:i}};t.deserializeTronTransaction=function(e){var t=e.amount,n=e.recipient,r=e.family,a=e.mode,i=e.resource,l=e.duration;return{amount:new o.default(t),recipient:n,family:r,mode:a,resource:i,duration:l}}},4682:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0})},3077:function(e,t){"use strict";var n;Object.defineProperty(t,"__esModule",{value:!0}),function(e){e.BITCOIN="bitcoin",e.ETHEREUM="ethereum",e.ALGORAND="algorand",e.CRYPTO_ORG="crypto_org",e.RIPPLE="ripple",e.COSMOS="cosmos",e.TEZOS="tezos",e.POLKADOT="polkadot",e.STELLAR="stellar",e.TRON="tron"}(n||(n={})),t.default=n},803:function(e,t,n){"use strict";var r=this&&this.__createBinding||(Object.create?function(e,t,n,r){void 0===r&&(r=n),Object.defineProperty(e,r,{enumerable:!0,get:function(){return t[n]}})}:function(e,t,n,r){void 0===r&&(r=n),e[r]=t[n]}),o=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||r(t,e,n)},a=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.Mock=t.FAMILIES=void 0,o(n(4033),t),o(n(7099),t),o(n(6829),t),o(n(1978),t),o(n(8906),t),o(n(8502),t),o(n(2148),t),o(n(7411),t),o(n(6540),t),o(n(4762),t),o(n(4919),t),o(n(4682),t);var i=n(3077);Object.defineProperty(t,"FAMILIES",{enumerable:!0,get:function(){return a(i).default}});var l=n(5280);Object.defineProperty(t,"Mock",{enumerable:!0,get:function(){return a(l).default}}),o(n(6793),t),o(n(9345),t);var u=n(757);Object.defineProperty(t,"default",{enumerable:!0,get:function(){return a(u).default}})},5223:function(e,t,n){"use strict";var r=n(294).default,o=n(8527).default;Object.defineProperty(t,"__esModule",{value:!0});var a=function(){function e(t){r(this,e),this.prefix="",this.prefix=t?"[".concat(t,"] "):""}return o(e,[{key:"log",value:function(e){for(var t,n=arguments.length,r=new Array(n>1?n-1:0),o=1;o1?n-1:0),o=1;o1?n-1:0),o=1;o1?n-1:0),o=1;o0&&void 0!==arguments[0]?arguments[0]:window,o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:i;r(this,e),this.connect=function(){var e;t.target.addEventListener("message",t._onMessageEvent,!1),null===(e=t.target.document)||void 0===e||e.addEventListener("message",t._onMessageEvent,!1),t.logger.debug("event listeners registered")},this.disconnect=function(){var e;t.target.removeEventListener("message",t._onMessageEvent),null===(e=t.target.document)||void 0===e||e.removeEventListener("message",t._onMessageEvent,!1),t.logger.debug("event listeners unregistered")},this._onMessageEvent=function(e){if(t._onMessage)if(t.logger.debug("received message event",e),e.origin!==t.target.location.origin&&e.data&&"string"===typeof e.data)try{var n=JSON.parse(e.data.toString());n.jsonrpc?(t.logger.log("received message",n),t._onMessage(n)):t.logger.debug("not a jsonrpc message")}catch(r){t.logger.warn("parse error"),t._onMessage(r)}else t.logger.debug("ignoring message same origin");else t.logger.debug("no handler registered")},this.send=function(e){try{return t.target.ReactNativeWebView?(t.logger.log("sending message (ReactNativeWebview)",e),t.target.ReactNativeWebView.postMessage(JSON.stringify(e))):t.target.ElectronWebview?(t.logger.log("sending message (ElectronWebview)",e),t.target.ElectronWebview.postMessage(JSON.stringify(e))):(t.logger.log("sending message",e),t.target.top.postMessage(JSON.stringify(e),"*")),Promise.resolve()}catch(n){return t.logger.error("unexpected error on send",n),Promise.reject(n)}},this.target=n,this.logger=o}return o(e,[{key:"onMessage",get:function(){return this._onMessage},set:function(e){this._onMessage=e}}]),e}();t.default=l},4033:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.ExchangeType=t.DeviceModel=t.FeesLevel=void 0,function(e){e.Slow="slow",e.Medium="medium",e.Fast="fast"}(t.FeesLevel||(t.FeesLevel={})),function(e){e.Blue="blue",e.NanoS="nanoS",e.NanoX="nanoX"}(t.DeviceModel||(t.DeviceModel={})),function(e){e[e.SWAP=0]="SWAP",e[e.SELL=1]="SELL",e[e.FUND=2]="FUND"}(t.ExchangeType||(t.ExchangeType={}))},2083:function(e,t,n){var r;!function(o){"use strict";var a,i=/^-?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i,l=Math.ceil,u=Math.floor,c="[BigNumber Error] ",s=c+"Number primitive has more than 15 significant digits: ",f=1e14,d=14,p=9007199254740991,h=[1,10,100,1e3,1e4,1e5,1e6,1e7,1e8,1e9,1e10,1e11,1e12,1e13],v=1e7,m=1e9;function g(e){var t=0|e;return e>0||e===t?t:t-1}function y(e){for(var t,n,r=1,o=e.length,a=e[0]+"";rc^n?1:-1;for(l=(u=o.length)<(c=a.length)?u:c,i=0;ia[i]^n?1:-1;return u==c?0:u>c^n?1:-1}function w(e,t,n,r){if(en||e!==u(e))throw Error(c+(r||"Argument")+("number"==typeof e?en?" out of range: ":" not an integer: ":" not a primitive number: ")+String(e))}function k(e){var t=e.c.length-1;return g(e.e/d)==t&&e.c[t]%2!=0}function E(e,t){return(e.length>1?e.charAt(0)+"."+e.slice(1):e)+(t<0?"e":"e+")+t}function S(e,t,n){var r,o;if(t<0){for(o=n+".";++t;o+=n);e=o+e}else if(++t>(r=e.length)){for(o=n,t-=r;--t;o+=n);e+=o}else tN?g.c=g.e=null:e.e=10;f/=10,c++);return void(c>N?g.c=g.e=null:(g.e=c,g.c=[e]))}m=String(e)}else{if(!i.test(m=String(e)))return o(g,m,h);g.s=45==m.charCodeAt(0)?(m=m.slice(1),-1):1}(c=m.indexOf("."))>-1&&(m=m.replace(".","")),(f=m.search(/e/i))>0?(c<0&&(c=f),c+=+m.slice(f+1),m=m.substring(0,f)):c<0&&(c=m.length)}else{if(w(t,2,I.length,"Base"),10==t&&z)return U(g=new D(e),x+g.e+1,C);if(m=String(e),h="number"==typeof e){if(0*e!=0)return o(g,m,h,t);if(g.s=1/e<0?(m=m.slice(1),-1):1,D.DEBUG&&m.replace(/^0\.0*|\./,"").length>15)throw Error(s+e)}else g.s=45===m.charCodeAt(0)?(m=m.slice(1),-1):1;for(n=I.slice(0,t),c=f=0,v=m.length;fc){c=v;continue}}else if(!l&&(m==m.toUpperCase()&&(m=m.toLowerCase())||m==m.toLowerCase()&&(m=m.toUpperCase()))){l=!0,f=-1,c=0;continue}return o(g,String(e),h,t)}h=!1,(c=(m=r(m,t,10,g.s)).indexOf("."))>-1?m=m.replace(".",""):c=m.length}for(f=0;48===m.charCodeAt(f);f++);for(v=m.length;48===m.charCodeAt(--v););if(m=m.slice(f,++v)){if(v-=f,h&&D.DEBUG&&v>15&&(e>p||e!==u(e)))throw Error(s+g.s*e);if((c=c-f-1)>N)g.c=g.e=null;else if(c=A)?E(u,i):S(u,i,"0");else if(a=(e=U(new D(e),t,n)).e,l=(u=y(e.c)).length,1==r||2==r&&(t<=a||a<=P)){for(;ll){if(--t>0)for(u+=".";t--;u+="0");}else if((t+=a-l)>0)for(a+1==l&&(u+=".");t--;u+="0");return e.s<0&&o?"-"+u:u}function F(e,t){for(var n,r=1,o=new D(e[0]);r=10;o/=10,r++);return(n=r+n*d-1)>N?e.c=e.e=null:n=10;c/=10,o++);if((a=t-o)<0)a+=d,i=t,v=(s=m[p=0])/g[o-i-1]%10|0;else if((p=l((a+1)/d))>=m.length){if(!r)break e;for(;m.length<=p;m.push(0));s=v=0,o=1,i=(a%=d)-d+1}else{for(s=c=m[p],o=1;c>=10;c/=10,o++);v=(i=(a%=d)-d+o)<0?0:s/g[o-i-1]%10|0}if(r=r||t<0||null!=m[p+1]||(i<0?s:s%g[o-i-1]),r=n<4?(v||r)&&(0==n||n==(e.s<0?3:2)):v>5||5==v&&(4==n||r||6==n&&(a>0?i>0?s/g[o-i]:0:m[p-1])%10&1||n==(e.s<0?8:7)),t<1||!m[0])return m.length=0,r?(t-=e.e+1,m[0]=g[(d-t%d)%d],e.e=-t||0):m[0]=e.e=0,e;if(0==a?(m.length=p,c=1,p--):(m.length=p+1,c=g[d-a],m[p]=i>0?u(s/g[o-i]%g[i])*c:0),r)for(;;){if(0==p){for(a=1,i=m[0];i>=10;i/=10,a++);for(i=m[0]+=c,c=1;i>=10;i/=10,c++);a!=c&&(e.e++,m[0]==f&&(m[0]=1));break}if(m[p]+=c,m[p]!=f)break;m[p--]=0,c=1}for(a=m.length;0===m[--a];m.pop());}e.e>N?e.c=e.e=null:e.e=A?E(t,n):S(t,n,"0"),e.s<0?"-"+t:t)}return D.clone=e,D.ROUND_UP=0,D.ROUND_DOWN=1,D.ROUND_CEIL=2,D.ROUND_FLOOR=3,D.ROUND_HALF_UP=4,D.ROUND_HALF_DOWN=5,D.ROUND_HALF_EVEN=6,D.ROUND_HALF_CEIL=7,D.ROUND_HALF_FLOOR=8,D.EUCLID=9,D.config=D.set=function(e){var t,n;if(null!=e){if("object"!=typeof e)throw Error(c+"Object expected: "+e);if(e.hasOwnProperty(t="DECIMAL_PLACES")&&(w(n=e[t],0,m,t),x=n),e.hasOwnProperty(t="ROUNDING_MODE")&&(w(n=e[t],0,8,t),C=n),e.hasOwnProperty(t="EXPONENTIAL_AT")&&((n=e[t])&&n.pop?(w(n[0],-m,0,t),w(n[1],0,m,t),P=n[0],A=n[1]):(w(n,-m,m,t),P=-(A=n<0?-n:n))),e.hasOwnProperty(t="RANGE"))if((n=e[t])&&n.pop)w(n[0],-m,-1,t),w(n[1],1,m,t),O=n[0],N=n[1];else{if(w(n,-m,m,t),!n)throw Error(c+t+" cannot be zero: "+n);O=-(N=n<0?-n:n)}if(e.hasOwnProperty(t="CRYPTO")){if((n=e[t])!==!!n)throw Error(c+t+" not true or false: "+n);if(n){if("undefined"==typeof crypto||!crypto||!crypto.getRandomValues&&!crypto.randomBytes)throw T=!n,Error(c+"crypto unavailable");T=n}else T=n}if(e.hasOwnProperty(t="MODULO_MODE")&&(w(n=e[t],0,9,t),L=n),e.hasOwnProperty(t="POW_PRECISION")&&(w(n=e[t],0,m,t),M=n),e.hasOwnProperty(t="FORMAT")){if("object"!=typeof(n=e[t]))throw Error(c+t+" not an object: "+n);R=n}if(e.hasOwnProperty(t="ALPHABET")){if("string"!=typeof(n=e[t])||/^.?$|[+\-.\s]|(.).*\1/.test(n))throw Error(c+t+" invalid: "+n);z="0123456789"==n.slice(0,10),I=n}}return{DECIMAL_PLACES:x,ROUNDING_MODE:C,EXPONENTIAL_AT:[P,A],RANGE:[O,N],CRYPTO:T,MODULO_MODE:L,POW_PRECISION:M,FORMAT:R,ALPHABET:I}},D.isBigNumber=function(e){if(!e||!0!==e._isBigNumber)return!1;if(!D.DEBUG)return!0;var t,n,r=e.c,o=e.e,a=e.s;e:if("[object Array]"=={}.toString.call(r)){if((1===a||-1===a)&&o>=-m&&o<=m&&o===u(o)){if(0===r[0]){if(0===o&&1===r.length)return!0;break e}if((t=(o+1)%d)<1&&(t+=d),String(r[0]).length==t){for(t=0;t=f||n!==u(n))break e;if(0!==n)return!0}}}else if(null===r&&null===o&&(null===a||1===a||-1===a))return!0;throw Error(c+"Invalid BigNumber: "+e)},D.maximum=D.max=function(){return F(arguments,a.lt)},D.minimum=D.min=function(){return F(arguments,a.gt)},D.random=function(){var e=9007199254740992,t=Math.random()*e&2097151?function(){return u(Math.random()*e)}:function(){return 8388608*(1073741824*Math.random()|0)+(8388608*Math.random()|0)};return function(e){var n,r,o,a,i,s=0,f=[],p=new D(_);if(null==e?e=x:w(e,0,m),a=l(e/d),T)if(crypto.getRandomValues){for(n=crypto.getRandomValues(new Uint32Array(a*=2));s>>11))>=9e15?(r=crypto.getRandomValues(new Uint32Array(2)),n[s]=r[0],n[s+1]=r[1]):(f.push(i%1e14),s+=2);s=a/2}else{if(!crypto.randomBytes)throw T=!1,Error(c+"crypto unavailable");for(n=crypto.randomBytes(a*=7);s=9e15?crypto.randomBytes(7).copy(n,s):(f.push(i%1e14),s+=7);s=a/7}if(!T)for(;s=10;i/=10,s++);sn-1&&(null==i[o+1]&&(i[o+1]=0),i[o+1]+=i[o]/n|0,i[o]%=n)}return i.reverse()}return function(r,o,a,i,l){var u,c,s,f,d,p,h,v,m=r.indexOf("."),g=x,b=C;for(m>=0&&(f=M,M=0,r=r.replace(".",""),p=(v=new D(o)).pow(r.length-m),M=f,v.c=t(S(y(p.c),p.e,"0"),10,a,e),v.e=v.c.length),s=f=(h=t(r,o,a,l?(u=I,e):(u=e,I))).length;0==h[--f];h.pop());if(!h[0])return u.charAt(0);if(m<0?--s:(p.c=h,p.e=s,p.s=i,h=(p=n(p,v,g,b,a)).c,d=p.r,s=p.e),m=h[c=s+g+1],f=a/2,d=d||c<0||null!=h[c+1],d=b<4?(null!=m||d)&&(0==b||b==(p.s<0?3:2)):m>f||m==f&&(4==b||d||6==b&&1&h[c-1]||b==(p.s<0?8:7)),c<1||!h[0])r=d?S(u.charAt(1),-g,u.charAt(0)):u.charAt(0);else{if(h.length=c,d)for(--a;++h[--c]>a;)h[c]=0,c||(++s,h=[1].concat(h));for(f=h.length;!h[--f];);for(m=0,r="";m<=f;r+=u.charAt(h[m++]));r=S(r,s,u.charAt(0))}return r}}(),n=function(){function e(e,t,n){var r,o,a,i,l=0,u=e.length,c=t%v,s=t/v|0;for(e=e.slice();u--;)l=((o=c*(a=e[u]%v)+(r=s*a+(i=e[u]/v|0)*c)%v*v+l)/n|0)+(r/v|0)+s*i,e[u]=o%n;return l&&(e=[l].concat(e)),e}function t(e,t,n,r){var o,a;if(n!=r)a=n>r?1:-1;else for(o=a=0;ot[o]?1:-1;break}return a}function n(e,t,n,r){for(var o=0;n--;)e[n]-=o,o=e[n]1;e.splice(0,1));}return function(r,o,a,i,l){var c,s,p,h,v,m,y,b,w,k,E,S,_,x,C,P,A,O=r.s==o.s?1:-1,N=r.c,T=o.c;if(!N||!N[0]||!T||!T[0])return new D(r.s&&o.s&&(N?!T||N[0]!=T[0]:T)?N&&0==N[0]||!T?0*O:O/0:NaN);for(w=(b=new D(O)).c=[],O=a+(s=r.e-o.e)+1,l||(l=f,s=g(r.e/d)-g(o.e/d),O=O/d|0),p=0;T[p]==(N[p]||0);p++);if(T[p]>(N[p]||0)&&s--,O<0)w.push(1),h=!0;else{for(x=N.length,P=T.length,p=0,O+=2,(v=u(l/(T[0]+1)))>1&&(T=e(T,v,l),N=e(N,v,l),P=T.length,x=N.length),_=P,E=(k=N.slice(0,P)).length;E=l/2&&C++;do{if(v=0,(c=t(T,k,P,E))<0){if(S=k[0],P!=E&&(S=S*l+(k[1]||0)),(v=u(S/C))>1)for(v>=l&&(v=l-1),y=(m=e(T,v,l)).length,E=k.length;1==t(m,k,y,E);)v--,n(m,P=10;O/=10,p++);U(b,a+(b.e=p+s*d-1)+1,i,h)}else b.e=s,b.r=+h;return b}}(),o=function(){var e=/^(-?)0([xbo])(?=\w[\w.]*$)/i,t=/^([^.]+)\.$/,n=/^\.([^.]+)$/,r=/^-?(Infinity|NaN)$/,o=/^\s*\+(?=[\w.])|^\s+|\s+$/g;return function(a,i,l,u){var s,f=l?i:i.replace(o,"");if(r.test(f))a.s=isNaN(f)?null:f<0?-1:1;else{if(!l&&(f=f.replace(e,(function(e,t,n){return s="x"==(n=n.toLowerCase())?16:"b"==n?2:8,u&&u!=s?e:t})),u&&(s=u,f=f.replace(t,"$1").replace(n,"0.$1")),i!=f))return new D(f,s);if(D.DEBUG)throw Error(c+"Not a"+(u?" base "+u:"")+" number: "+i);a.s=null}a.c=a.e=null}}(),a.absoluteValue=a.abs=function(){var e=new D(this);return e.s<0&&(e.s=1),e},a.comparedTo=function(e,t){return b(this,new D(e,t))},a.decimalPlaces=a.dp=function(e,t){var n,r,o,a=this;if(null!=e)return w(e,0,m),null==t?t=C:w(t,0,8),U(new D(a),e+a.e+1,t);if(!(n=a.c))return null;if(r=((o=n.length-1)-g(this.e/d))*d,o=n[o])for(;o%10==0;o/=10,r--);return r<0&&(r=0),r},a.dividedBy=a.div=function(e,t){return n(this,new D(e,t),x,C)},a.dividedToIntegerBy=a.idiv=function(e,t){return n(this,new D(e,t),0,1)},a.exponentiatedBy=a.pow=function(e,t){var n,r,o,a,i,s,f,p,h=this;if((e=new D(e)).c&&!e.isInteger())throw Error(c+"Exponent not an integer: "+W(e));if(null!=t&&(t=new D(t)),i=e.e>14,!h.c||!h.c[0]||1==h.c[0]&&!h.e&&1==h.c.length||!e.c||!e.c[0])return p=new D(Math.pow(+W(h),i?2-k(e):+W(e))),t?p.mod(t):p;if(s=e.s<0,t){if(t.c?!t.c[0]:!t.s)return new D(NaN);(r=!s&&h.isInteger()&&t.isInteger())&&(h=h.mod(t))}else{if(e.e>9&&(h.e>0||h.e<-1||(0==h.e?h.c[0]>1||i&&h.c[1]>=24e7:h.c[0]<8e13||i&&h.c[0]<=9999975e7)))return a=h.s<0&&k(e)?-0:0,h.e>-1&&(a=1/a),new D(s?1/a:a);M&&(a=l(M/d+2))}for(i?(n=new D(.5),s&&(e.s=1),f=k(e)):f=(o=Math.abs(+W(e)))%2,p=new D(_);;){if(f){if(!(p=p.times(h)).c)break;a?p.c.length>a&&(p.c.length=a):r&&(p=p.mod(t))}if(o){if(0===(o=u(o/2)))break;f=o%2}else if(U(e=e.times(n),e.e+1,1),e.e>14)f=k(e);else{if(0===(o=+W(e)))break;f=o%2}h=h.times(h),a?h.c&&h.c.length>a&&(h.c.length=a):r&&(h=h.mod(t))}return r?p:(s&&(p=_.div(p)),t?p.mod(t):a?U(p,M,C,undefined):p)},a.integerValue=function(e){var t=new D(this);return null==e?e=C:w(e,0,8),U(t,t.e+1,e)},a.isEqualTo=a.eq=function(e,t){return 0===b(this,new D(e,t))},a.isFinite=function(){return!!this.c},a.isGreaterThan=a.gt=function(e,t){return b(this,new D(e,t))>0},a.isGreaterThanOrEqualTo=a.gte=function(e,t){return 1===(t=b(this,new D(e,t)))||0===t},a.isInteger=function(){return!!this.c&&g(this.e/d)>this.c.length-2},a.isLessThan=a.lt=function(e,t){return b(this,new D(e,t))<0},a.isLessThanOrEqualTo=a.lte=function(e,t){return-1===(t=b(this,new D(e,t)))||0===t},a.isNaN=function(){return!this.s},a.isNegative=function(){return this.s<0},a.isPositive=function(){return this.s>0},a.isZero=function(){return!!this.c&&0==this.c[0]},a.minus=function(e,t){var n,r,o,a,i=this,l=i.s;if(t=(e=new D(e,t)).s,!l||!t)return new D(NaN);if(l!=t)return e.s=-t,i.plus(e);var u=i.e/d,c=e.e/d,s=i.c,p=e.c;if(!u||!c){if(!s||!p)return s?(e.s=-t,e):new D(p?i:NaN);if(!s[0]||!p[0])return p[0]?(e.s=-t,e):new D(s[0]?i:3==C?-0:0)}if(u=g(u),c=g(c),s=s.slice(),l=u-c){for((a=l<0)?(l=-l,o=s):(c=u,o=p),o.reverse(),t=l;t--;o.push(0));o.reverse()}else for(r=(a=(l=s.length)<(t=p.length))?l:t,l=t=0;t0)for(;t--;s[n++]=0);for(t=f-1;r>l;){if(s[--r]=0;){for(n=0,h=S[o]%w,m=S[o]/w|0,a=o+(i=u);a>o;)n=((c=h*(c=E[--i]%w)+(l=m*c+(s=E[i]/w|0)*h)%w*w+y[a]+n)/b|0)+(l/w|0)+m*s,y[a--]=c%b;y[a]=n}return n?++r:y.splice(0,1),B(e,y,r)},a.negated=function(){var e=new D(this);return e.s=-e.s||null,e},a.plus=function(e,t){var n,r=this,o=r.s;if(t=(e=new D(e,t)).s,!o||!t)return new D(NaN);if(o!=t)return e.s=-t,r.minus(e);var a=r.e/d,i=e.e/d,l=r.c,u=e.c;if(!a||!i){if(!l||!u)return new D(o/0);if(!l[0]||!u[0])return u[0]?e:new D(l[0]?r:0*o)}if(a=g(a),i=g(i),l=l.slice(),o=a-i){for(o>0?(i=a,n=u):(o=-o,n=l),n.reverse();o--;n.push(0));n.reverse()}for((o=l.length)-(t=u.length)<0&&(n=u,u=l,l=n,t=o),o=0;t;)o=(l[--t]=l[t]+u[t]+o)/f|0,l[t]=f===l[t]?0:l[t]%f;return o&&(l=[o].concat(l),++i),B(e,l,i)},a.precision=a.sd=function(e,t){var n,r,o,a=this;if(null!=e&&e!==!!e)return w(e,1,m),null==t?t=C:w(t,0,8),U(new D(a),e,t);if(!(n=a.c))return null;if(r=(o=n.length-1)*d+1,o=n[o]){for(;o%10==0;o/=10,r--);for(o=n[0];o>=10;o/=10,r++);}return e&&a.e+1>r&&(r=a.e+1),r},a.shiftedBy=function(e){return w(e,-9007199254740991,p),this.times("1e"+e)},a.squareRoot=a.sqrt=function(){var e,t,r,o,a,i=this,l=i.c,u=i.s,c=i.e,s=x+4,f=new D("0.5");if(1!==u||!l||!l[0])return new D(!u||u<0&&(!l||l[0])?NaN:l?i:1/0);if(0==(u=Math.sqrt(+W(i)))||u==1/0?(((t=y(l)).length+c)%2==0&&(t+="0"),u=Math.sqrt(+t),c=g((c+1)/2)-(c<0||c%2),r=new D(t=u==1/0?"5e"+c:(t=u.toExponential()).slice(0,t.indexOf("e")+1)+c)):r=new D(u+""),r.c[0])for((u=(c=r.e)+s)<3&&(u=0);;)if(a=r,r=f.times(a.plus(n(i,a,s,1))),y(a.c).slice(0,u)===(t=y(r.c)).slice(0,u)){if(r.e0&&v>0){for(a=v%l||l,f=h.substr(0,a);a0&&(f+=s+h.slice(a)),p&&(f="-"+f)}r=d?f+(n.decimalSeparator||"")+((u=+n.fractionGroupSize)?d.replace(new RegExp("\\d{"+u+"}\\B","g"),"$&"+(n.fractionGroupSeparator||"")):d):f}return(n.prefix||"")+r+(n.suffix||"")},a.toFraction=function(e){var t,r,o,a,i,l,u,s,f,p,v,m,g=this,b=g.c;if(null!=e&&(!(u=new D(e)).isInteger()&&(u.c||1!==u.s)||u.lt(_)))throw Error(c+"Argument "+(u.isInteger()?"out of range: ":"not an integer: ")+W(u));if(!b)return new D(g);for(t=new D(_),f=r=new D(_),o=s=new D(_),m=y(b),i=t.e=m.length-g.e-1,t.c[0]=h[(l=i%d)<0?d+l:l],e=!e||u.comparedTo(t)>0?i>0?t:f:u,l=N,N=1/0,u=new D(m),s.c[0]=0;p=n(u,t,0,1),1!=(a=r.plus(p.times(o))).comparedTo(e);)r=o,o=a,f=s.plus(p.times(a=f)),s=a,t=u.minus(p.times(a=t)),u=a;return a=n(e.minus(r),o,0,1),s=s.plus(a.times(f)),r=r.plus(a.times(o)),s.s=f.s=g.s,v=n(f,o,i*=2,C).minus(g).abs().comparedTo(n(s,r,i,C).minus(g).abs())<1?[f,o]:[s,r],N=l,v},a.toNumber=function(){return+W(this)},a.toPrecision=function(e,t){return null!=e&&w(e,1,m),j(this,e,t,2)},a.toString=function(e){var t,n=this,o=n.s,a=n.e;return null===a?o?(t="Infinity",o<0&&(t="-"+t)):t="NaN":(null==e?t=a<=P||a>=A?E(y(n.c),a):S(y(n.c),a,"0"):10===e&&z?t=S(y((n=U(new D(n),x+a+1,C)).c),n.e,"0"):(w(e,2,I.length,"Base"),t=r(S(y(n.c),a,"0"),10,e,o,!0)),o<0&&n.c[0]&&(t="-"+t)),t},a.valueOf=a.toJSON=function(){return W(this)},a._isBigNumber=!0,null!=t&&D.set(t),D}(),a.default=a.BigNumber=a,void 0===(r=function(){return a}.call(t,n,t,e))||(e.exports=r)}()},6518:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.JSONRPCClient=void 0;var r=n(5707),o=n(6450),a=o.createLogDeprecationWarning("Using a higher order function on JSONRPCClient send method is deprecated.\nInstead of this: new JSONRPCClient((jsonRPCClient) => (clientParams) => /* no change here */)\nDo this: new JSONRPCClient((jsonRPCClient, clientParams) => /* no change here */)\nThe old way still works, but we will drop the support in the future."),i=function(){function e(e,t){this._send=e,this.createID=t,this.idToResolveMap=new Map,this.id=0}return e.prototype._createID=function(){return this.createID?this.createID():++this.id},e.prototype.timeout=function(e,t){var n=this;void 0===t&&(t=function(e){return r.createJSONRPCErrorResponse(e,o.DefaultErrorCode,"Request timeout")});var a=function(r,o){var a=setTimeout((function(){var e=n.idToResolveMap.get(r);e&&(n.idToResolveMap.delete(r),e(t(r)))}),e);return o().then((function(e){return clearTimeout(a),e}),(function(e){return clearTimeout(a),Promise.reject(e)}))};return{request:function(e,t,r){var o=n._createID();return a(o,(function(){return n.requestWithID(e,t,r,o)}))},requestAdvanced:function(e,t){return a(e.id,(function(){return n.requestAdvanced(e,t)}))}}},e.prototype.request=function(e,t,n){return this.requestWithID(e,t,n,this._createID())},e.prototype.requestWithID=function(e,t,n,o){var a={jsonrpc:r.JSONRPC,method:e,params:t,id:o};return this.requestAdvanced(a,n).then((function(e){return void 0===e.result||e.error?void 0===e.result&&e.error?Promise.reject(new Error(e.error.message)):Promise.reject(new Error("An unexpected error occurred")):e.result}))},e.prototype.requestAdvanced=function(e,t){var n=this,a=new Promise((function(t){return n.idToResolveMap.set(e.id,t)}));return this.send(e,t).then((function(){return a}),(function(t){return n.receive(r.createJSONRPCErrorResponse(e.id,o.DefaultErrorCode,t&&t.message||"Failed to send a request")),a}))},e.prototype.notify=function(e,t,n){this.send({jsonrpc:r.JSONRPC,method:e,params:t},n).then(void 0,(function(){}))},e.prototype.send=function(e,t){var n=this._send(e,t);return"function"===typeof n&&(a(),n=n(t)),n},e.prototype.rejectAllPendingRequests=function(e){this.idToResolveMap.forEach((function(t,n){return t(r.createJSONRPCErrorResponse(n,o.DefaultErrorCode,e))})),this.idToResolveMap.clear()},e.prototype.receive=function(e){var t=this.idToResolveMap.get(e.id);t&&(this.idToResolveMap.delete(e.id),t(e))},e}();t.JSONRPCClient=i},7656:function(e,t,n){"use strict";var r=this&&this.__createBinding||(Object.create?function(e,t,n,r){void 0===r&&(r=n),Object.defineProperty(e,r,{enumerable:!0,get:function(){return t[n]}})}:function(e,t,n,r){void 0===r&&(r=n),e[r]=t[n]}),o=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||t.hasOwnProperty(n)||r(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),o(n(6518),t),o(n(5707),t),o(n(4317),t),o(n(5774),t)},6450:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.DefaultErrorCode=t.createLogDeprecationWarning=void 0,t.createLogDeprecationWarning=function(e){return function(){0}},t.DefaultErrorCode=0},5707:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.createJSONRPCErrorResponse=t.JSONRPCErrorCode=t.isJSONRPCResponse=t.isJSONRPCRequest=t.isJSONRPCID=t.JSONRPC=void 0,t.JSONRPC="2.0",t.isJSONRPCID=function(e){return"string"===typeof e||"number"===typeof e||null===e},t.isJSONRPCRequest=function(e){return e.jsonrpc===t.JSONRPC&&void 0!==e.method&&void 0===e.result&&void 0===e.error},t.isJSONRPCResponse=function(e){return e.jsonrpc===t.JSONRPC&&void 0!==e.id&&(void 0!==e.result||void 0!==e.error)},function(e){e[e.ParseError=-32700]="ParseError",e[e.InvalidRequest=-32600]="InvalidRequest",e[e.MethodNotFound=-32601]="MethodNotFound",e[e.InvalidParams=-32602]="InvalidParams",e[e.InternalError=-32603]="InternalError"}(t.JSONRPCErrorCode||(t.JSONRPCErrorCode={})),t.createJSONRPCErrorResponse=function(e,n,r,o){var a={code:n,message:r};return o&&(a.data=o),{jsonrpc:t.JSONRPC,id:e,error:a}}},5774:function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))((function(o,a){function i(e){try{u(r.next(e))}catch(t){a(t)}}function l(e){try{u(r.throw(e))}catch(t){a(t)}}function u(e){var t;e.done?o(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(i,l)}u((r=r.apply(e,t||[])).next())}))},o=this&&this.__generator||function(e,t){var n,r,o,a,i={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return a={next:l(0),throw:l(1),return:l(2)},"function"===typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function l(a){return function(l){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;i;)try{if(n=1,r&&(o=2&a[0]?r.return:a[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,a[1])).done)return o;switch(r=0,o&&(a=[2&a[0],o.value]),a[0]){case 0:case 1:o=a;break;case 4:return i.label++,{value:a[1],done:!1};case 5:i.label++,r=a[1],a=[0];continue;case 7:a=i.ops.pop(),i.trys.pop();continue;default:if(!(o=(o=i.trys).length>0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1] (serverParams) => /* no change here */)\nDo this: jsonRPCServer.addMethod(methodName, (params, serverParams) => /* no change here */)\nThe old way still works, but we will drop the support in the future."),u=function(){function e(){this.mapErrorToJSONRPCErrorResponse=f,this.nameToMethodDictionary={},this.middleware=null}return e.prototype.addMethod=function(e,t){this.addMethodAdvanced(e,this.toJSONRPCMethod(t))},e.prototype.toJSONRPCMethod=function(e){var t=this;return function(n,r){var o=e(n.params,r);return"function"===typeof o&&(l(),o=o(r)),Promise.resolve(o).then((function(e){return s(n.id,e)}),(function(e){return console.warn("JSON-RPC method "+n.method+" responded an error",e),t.mapErrorToJSONRPCErrorResponseIfNecessary(n.id,e)}))}},e.prototype.addMethodAdvanced=function(e,t){var n;this.nameToMethodDictionary=r(r({},this.nameToMethodDictionary),((n={})[e]=t,n))},e.prototype.receiveJSON=function(e,t){var n=this.tryParseRequestJSON(e);return n?this.receive(n,t):Promise.resolve(a.createJSONRPCErrorResponse(null,a.JSONRPCErrorCode.ParseError,"Parse error"))},e.prototype.tryParseRequestJSON=function(e){try{return JSON.parse(e)}catch(t){return null}},e.prototype.receive=function(e,t){var n,r=this.nameToMethodDictionary[e.method];return a.isJSONRPCRequest(e)?r?this.callMethod(r,e,t).then((function(t){return d(e,t)})):void 0!==e.id?Promise.resolve((n=e.id,a.createJSONRPCErrorResponse(n,a.JSONRPCErrorCode.MethodNotFound,"Method not found"))):Promise.resolve(null):Promise.resolve(function(e){return a.createJSONRPCErrorResponse(a.isJSONRPCID(e.id)?e.id:null,a.JSONRPCErrorCode.InvalidRequest,"Invalid Request")}(e))},e.prototype.applyMiddleware=function(){for(var e=[],t=0;t