From a9713007cf37a8cdd1e9f3a96240a4185fb55a59 Mon Sep 17 00:00:00 2001 From: Elad Date: Tue, 13 Dec 2022 17:29:48 +0100 Subject: [PATCH 001/187] initial setup Very basic signing support for eth_sendTransaction and eth_signTransaction Will be expended upon in upcoming commits --- background/package.json | 1 + .../wallet-connect/eip155-request-utils.ts | 34 +++ background/services/wallet-connect/index.ts | 244 +++++++++++++++--- yarn.lock | 20 ++ 4 files changed, 261 insertions(+), 38 deletions(-) create mode 100644 background/services/wallet-connect/eip155-request-utils.ts diff --git a/background/package.json b/background/package.json index cb6fefb5c5..9836dba69e 100644 --- a/background/package.json +++ b/background/package.json @@ -33,6 +33,7 @@ "@ethersproject/providers": "^5.5.3", "@ethersproject/transactions": "^5.5.0", "@ethersproject/web": "^5.5.1", + "@json-rpc-tools/utils": "^1.7.6", "@ledgerhq/devices": "^6.20.0", "@ledgerhq/errors": "^6.12.0", "@ledgerhq/hw-app-eth": "^6.30.0", diff --git a/background/services/wallet-connect/eip155-request-utils.ts b/background/services/wallet-connect/eip155-request-utils.ts new file mode 100644 index 0000000000..a5805b5401 --- /dev/null +++ b/background/services/wallet-connect/eip155-request-utils.ts @@ -0,0 +1,34 @@ +import { + formatJsonRpcError, + formatJsonRpcResult, + JsonRpcError, + JsonRpcResult, +} from "@json-rpc-tools/utils" +import { SignClientTypes } from "@walletconnect/types" + +export function approveEIP155Request( + requestEvent: SignClientTypes.EventArguments["session_request"], + signedMessage: string +): JsonRpcResult { + const { params, id } = requestEvent + const { request } = params + + switch (request.method) { + case "eth_sign": + case "personal_sign": + case "eth_signTransaction": + case "eth_sendTransaction": + return formatJsonRpcResult(id, signedMessage) + + default: + throw new Error("UNKNOWN_JSONRPC_METHOD") + } +} + +export function rejectEIP155Request( + request: SignClientTypes.EventArguments["session_request"] +): JsonRpcError { + const { id } = request + + return formatJsonRpcError(id, "JSONRPC_REQUEST_METHOD_REJECTED") +} diff --git a/background/services/wallet-connect/index.ts b/background/services/wallet-connect/index.ts index e7ca3adfa4..2085a47c30 100644 --- a/background/services/wallet-connect/index.ts +++ b/background/services/wallet-connect/index.ts @@ -1,6 +1,12 @@ import SignClient from "@walletconnect/sign-client" import { parseUri } from "@walletconnect/utils" import { SignClientTypes, SessionTypes } from "@walletconnect/types" +import { + EIP1193Error, + EIP1193_ERROR_CODES, + isEIP1193Error, + RPCRequest, +} from "@tallyho/provider-bridge-shared" import { ServiceCreatorFunction, ServiceLifecycleEvents } from "../types" @@ -8,11 +14,24 @@ import BaseService from "../base" import PreferenceService from "../preferences" import ProviderBridgeService from "../provider-bridge" import InternalEthereumProviderService from "../internal-ethereum-provider" +import { browser } from "../.." +import { + approveEIP155Request, + rejectEIP155Request, +} from "./eip155-request-utils" interface Events extends ServiceLifecycleEvents { placeHolderEventForTypingPurposes: string } +interface TranslatedRequestParams { + topic?: string + method: string + params: RPCRequest["params"] +} + +const temporaryDAppUri = "https://react-dapp-v2-with-ethers.vercel.app" // TODO: this constant should be removed and replaced with a dynamic value + /* * The walletconnect service is responsible for encapsulating the wallet connect * implementation details, maintaining the websocket connection, handling the protocol @@ -77,8 +96,12 @@ export default class WalletConnectService extends BaseService { // TODO: remove this, inject uri // simulate connection attempt - const wcUri = "wc:710f..." - await this.performConnection(wcUri) + const wcUri = + "wc:70e3ca637cd88494fd6e68d348c0f3940a73911bb272e409b28c28faaa9d89f5@2?relay-protocol=irn&symKey=8bcd2d3018dda379dd836b19a56ee4b70506ba20615f6c1b70e9b622c7fce73c" + + setTimeout(() => { + this.performConnection(wcUri) + }, 2000) } private static createSignClient(): Promise { @@ -97,47 +120,18 @@ export default class WalletConnectService extends BaseService { } private defineEventHandlers(): void { - const address = "0xcd6b1f2080bde01d56023c9b50cd91ff09fefd73" // TODO: remove this, replace with real address - - this.signClient?.on( - "session_proposal", - async (proposal: SignClientTypes.EventArguments["session_proposal"]) => { - // TODO: in case of a new connection, this callback should perform request processing AFTER wallet selection/confirmation dialog - const { id, params } = proposal - const { requiredNamespaces, relays } = params - - // TODO: expand this section to be able to match requiredNamespaces to actual wallet - const key = "eip155" - const accounts = [`eip155:1:${address}`] - const namespaces: SessionTypes.Namespaces = {} - namespaces[key] = { - accounts, - methods: requiredNamespaces[key].methods, - events: requiredNamespaces[key].events, - } - - if (this.signClient !== undefined && relays.length > 0) { - const { acknowledged } = await this.signClient.approve({ - id, - relayProtocol: relays[0].protocol, - namespaces, - }) - await acknowledged() - } else { - // TODO: how to handle this case? - } - } + this.signClient?.on("session_proposal", (proposal) => + this.sessionProposalListener(proposal) ) - this.signClient?.on( - "session_request", - // eslint-disable-next-line @typescript-eslint/no-unused-vars - (event: SignClientTypes.EventArguments["session_request"]) => {} + this.signClient?.on("session_request", (event) => + this.sessionRequestListener(event) ) } async performConnection(uri: string): Promise { if (this.signClient === undefined) { + WalletConnectService.tempFeatureLog("signClient undefined") return } @@ -147,12 +141,186 @@ export default class WalletConnectService extends BaseService { // Route the provided URI to the v1 SignClient if URI version indicates it, else use v2. if (version === 1) { // createLegacySignClient({ uri }) + WalletConnectService.tempFeatureLog( + "TODO: unsupported legacy", + parseUri(uri) + ) } else if (version === 2) { await this.signClient.pair({ uri }) + WalletConnectService.tempFeatureLog("pairing request sent") } else { // TODO: decide how to handle this + WalletConnectService.tempFeatureLog("unhandled uri") + } + } catch (err: unknown) { + WalletConnectService.tempFeatureLog("TODO: handle error", err) + } + } + + private async acknowledgeProposal( + proposal: SignClientTypes.EventArguments["session_proposal"], + address: string + ) { + // TODO: in case of a new connection, this callback should perform request processing AFTER wallet selection/confirmation dialog + const { id, params } = proposal + const { requiredNamespaces, relays } = params + + WalletConnectService.tempFeatureLog( + "requiredNamespaces", + requiredNamespaces + ) + // TODO: expand this section to be able to match requiredNamespaces to actual wallet + const key = "eip155" + const accounts = [`eip155:1:${address}`] + const namespaces: SessionTypes.Namespaces = {} + namespaces[key] = { + accounts, + methods: requiredNamespaces[key].methods, + events: requiredNamespaces[key].events, + } + + if (this.signClient !== undefined && relays.length > 0) { + const { acknowledged } = await this.signClient.approve({ + id, + relayProtocol: relays[0].protocol, + namespaces, + }) + + await acknowledged() + WalletConnectService.tempFeatureLog("connection acknowledged", namespaces) + } else { + // TODO: how to handle this case? + } + } + + private async postApprovalResponse( + event: SignClientTypes.EventArguments["session_request"], + payload: any + ) { + const { topic } = event + const response = approveEIP155Request(event, payload) + await this.signClient?.respond({ + topic, + response, + }) + } + + private async postRejectionResponse( + event: SignClientTypes.EventArguments["session_request"] + ) { + const { topic } = event + const response = rejectEIP155Request(event) + await this.signClient?.respond({ + topic, + response, + }) + } + + private static getMetaPort( + name: string, + postMessage: (message: any) => void + ): Required { + const port: browser.Runtime.Port = browser.runtime.connect({ + name, + }) + port.sender = { + url: temporaryDAppUri, + } + port.postMessage = postMessage + return port as unknown as Required + } + + private static processRequestParams( + event: SignClientTypes.EventArguments["session_request"] + ): TranslatedRequestParams { + // TODO: figure out if this method is needed + const { params: eventParams } = event + // TODO: handle chain id + const { request } = eventParams + + switch (request.method) { + case "eth_signTypedData": + case "eth_signTypedData_v1": + case "eth_signTypedData_v3": + case "eth_signTypedData_v4": + return { + method: request.method, + params: request.params, + } + case "eth_sign": // --- important wallet methods --- + case "personal_sign": + case "eth_sendTransaction": + return { + method: request.method, + params: request.params, + } + case "eth_signTransaction": + return { + method: request.method, + params: request.params, + } + default: + throw new EIP1193Error(EIP1193_ERROR_CODES.unsupportedMethod) + } + } + + async sessionRequestListener( + event: SignClientTypes.EventArguments["session_request"] + ): Promise { + WalletConnectService.tempFeatureLog("in sessionRequestListener", event) + const { method, params } = WalletConnectService.processRequestParams(event) + + const port = WalletConnectService.getMetaPort( + "sessionRequestListenerPort", + async (message) => { + WalletConnectService.tempFeatureLog( + "sessionRequestListenerPort message:", + message + ) + + // TODO: make this check more elaborate + if (isEIP1193Error(message.result)) { + await this.postRejectionResponse(event) + } else { + await this.postApprovalResponse(event, message.result) + } } - // eslint-disable-next-line no-empty - } catch (err: unknown) {} + ) + + await this.providerBridgeService.onMessageListener(port, { + id: "1400", + request: { + method, + params, + }, + }) + } + + async sessionProposalListener( + proposal: SignClientTypes.EventArguments["session_proposal"] + ): Promise { + WalletConnectService.tempFeatureLog("in sessionProposalListener") + + const port = WalletConnectService.getMetaPort( + "sessionProposalListenerPort", + async (message) => { + if (Array.isArray(message.result) && message.result.length > 0) { + await this.acknowledgeProposal(proposal, message.result[0]) + WalletConnectService.tempFeatureLog("pairing request acknowledged") + } + } + ) + + await this.providerBridgeService.onMessageListener(port, { + id: "1300", + request: { + method: "eth_requestAccounts", + params: [], + }, + }) + } + + private static tempFeatureLog(message?: any, ...optionalParams: any[]): void { + console.log(`[WalletConnect Demo] - ${message || ""}`, optionalParams) } } diff --git a/yarn.lock b/yarn.lock index bf881af529..af4784ddf3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2472,6 +2472,21 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz#b6461fb0c2964356c469e115f504c95ad97ab88c" integrity sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w== +"@json-rpc-tools/types@^1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@json-rpc-tools/types/-/types-1.7.6.tgz#5abd5fde01364a130c46093b501715bcce5bdc0e" + integrity sha512-nDSqmyRNEqEK9TZHtM15uNnDljczhCUdBmRhpNZ95bIPKEDQ+nTDmGMFd2lLin3upc5h2VVVd9tkTDdbXUhDIQ== + dependencies: + keyvaluestorage-interface "^1.0.0" + +"@json-rpc-tools/utils@^1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@json-rpc-tools/utils/-/utils-1.7.6.tgz#67f04987dbaa2e7adb6adff1575367b75a9a9ba1" + integrity sha512-HjA8x/U/Q78HRRe19yh8HVKoZ+Iaoo3YZjakJYxR+rw52NHo6jM+VE9b8+7ygkCFXl/EHID5wh/MkXaE/jGyYw== + dependencies: + "@json-rpc-tools/types" "^1.7.6" + "@pedrouid/environment" "^1.0.1" + "@ledgerhq/cryptoassets@^6.37.0": version "6.37.0" resolved "https://registry.yarnpkg.com/@ledgerhq/cryptoassets/-/cryptoassets-6.37.0.tgz#302833777bcd210809ca7820afb82cff8da5c296" @@ -2758,6 +2773,11 @@ node-gyp "^7.1.0" read-package-json-fast "^2.0.1" +"@pedrouid/environment@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@pedrouid/environment/-/environment-1.0.1.tgz#858f0f8a057340e0b250398b75ead77d6f4342ec" + integrity sha512-HaW78NszGzRZd9SeoI3JD11JqY+lubnaOx7Pewj5pfjqWXOEATpeKIFb9Z4t2WBUK2iryiXX3lzWwmYWgUL0Ug== + "@playwright/test@^1.26.1": version "1.28.1" resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.28.1.tgz#e5be297e024a3256610cac2baaa9347fd57c7860" From 037cc67031b0d873560708fe33d938f3102c4ae4 Mon Sep 17 00:00:00 2001 From: Elad Date: Tue, 13 Dec 2022 17:33:44 +0100 Subject: [PATCH 002/187] moved @walletconnect/types devDependencies ts types should reside under devDependencies --- background/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/package.json b/background/package.json index 9836dba69e..dda6bc5d47 100644 --- a/background/package.json +++ b/background/package.json @@ -46,7 +46,6 @@ "@types/w3c-web-usb": "^1.0.5", "@uniswap/token-lists": "^1.0.0-beta.30", "@walletconnect/sign-client": "^2.1.4", - "@walletconnect/types": "^2.1.4", "@walletconnect/utils": "^2.1.4", "ajv": "^8.6.2", "ajv-formats": "^2.1.0", @@ -70,6 +69,7 @@ "@types/sinon": "^10.0.12", "@types/uuid": "^8.3.4", "@types/webextension-polyfill": "^0.8.0", + "@walletconnect/types": "^2.1.4", "crypto-browserify": "^3.12.0", "fake-indexeddb": "^4.0.0", "jest-webextension-mock": "^3.7.22", From b627e21b178095b54212e280cfbedd6319d0ac12 Mon Sep 17 00:00:00 2001 From: Elad Date: Fri, 16 Dec 2022 12:03:33 +0100 Subject: [PATCH 003/187] initial WalletConnect v1 setup Despite the planned deprecation, most of the current dApps have not yet migrated to V2. Allowing V1 support would enable us to interface with any WalletConnect-enabled dApp that is available --- background/package.json | 2 + .../wallet-connect/eip155-request-utils.ts | 40 +++- background/services/wallet-connect/index.ts | 186 ++++++++---------- .../legacy-sign-client-helper.ts | 144 ++++++++++++++ .../wallet-connect/sign-client-helper.ts | 16 ++ background/services/wallet-connect/types.ts | 8 + background/services/wallet-connect/utils.ts | 17 ++ yarn.lock | 160 ++++++++++++++- 8 files changed, 457 insertions(+), 116 deletions(-) create mode 100644 background/services/wallet-connect/legacy-sign-client-helper.ts create mode 100644 background/services/wallet-connect/sign-client-helper.ts create mode 100644 background/services/wallet-connect/types.ts create mode 100644 background/services/wallet-connect/utils.ts diff --git a/background/package.json b/background/package.json index dda6bc5d47..95b989a9d8 100644 --- a/background/package.json +++ b/background/package.json @@ -45,6 +45,7 @@ "@tallyho/window-provider": "0.0.1", "@types/w3c-web-usb": "^1.0.5", "@uniswap/token-lists": "^1.0.0-beta.30", + "@walletconnect/client": "^1.8.0", "@walletconnect/sign-client": "^2.1.4", "@walletconnect/utils": "^2.1.4", "ajv": "^8.6.2", @@ -69,6 +70,7 @@ "@types/sinon": "^10.0.12", "@types/uuid": "^8.3.4", "@types/webextension-polyfill": "^0.8.0", + "@walletconnect/legacy-types": "^2.0.0-rc.0", "@walletconnect/types": "^2.1.4", "crypto-browserify": "^3.12.0", "fake-indexeddb": "^4.0.0", diff --git a/background/services/wallet-connect/eip155-request-utils.ts b/background/services/wallet-connect/eip155-request-utils.ts index a5805b5401..5d3398b444 100644 --- a/background/services/wallet-connect/eip155-request-utils.ts +++ b/background/services/wallet-connect/eip155-request-utils.ts @@ -1,19 +1,23 @@ +import { SignClientTypes } from "@walletconnect/types" +import { + EIP1193Error, + EIP1193_ERROR_CODES, +} from "@tallyho/provider-bridge-shared" import { formatJsonRpcError, formatJsonRpcResult, JsonRpcError, JsonRpcResult, } from "@json-rpc-tools/utils" -import { SignClientTypes } from "@walletconnect/types" +import { TranslatedRequestParams } from "./types" export function approveEIP155Request( - requestEvent: SignClientTypes.EventArguments["session_request"], + request: TranslatedRequestParams, signedMessage: string ): JsonRpcResult { - const { params, id } = requestEvent - const { request } = params + const { id, method } = request - switch (request.method) { + switch (method) { case "eth_sign": case "personal_sign": case "eth_signTransaction": @@ -26,9 +30,33 @@ export function approveEIP155Request( } export function rejectEIP155Request( - request: SignClientTypes.EventArguments["session_request"] + request: TranslatedRequestParams ): JsonRpcError { const { id } = request return formatJsonRpcError(id, "JSONRPC_REQUEST_METHOD_REJECTED") } + +export function processRequestParams( + event: SignClientTypes.EventArguments["session_request"] +): TranslatedRequestParams { + // TODO: figure out if this method is needed + const { id, params: eventParams, topic } = event + // TODO: handle chain id + const { request } = eventParams + + switch (request.method) { + case "eth_sign": + case "personal_sign": + case "eth_sendTransaction": + case "eth_signTransaction": + return { + id, + topic, + method: request.method, + params: request.params, + } + default: + throw new EIP1193Error(EIP1193_ERROR_CODES.unsupportedMethod) + } +} diff --git a/background/services/wallet-connect/index.ts b/background/services/wallet-connect/index.ts index 2085a47c30..05997c328e 100644 --- a/background/services/wallet-connect/index.ts +++ b/background/services/wallet-connect/index.ts @@ -1,12 +1,7 @@ -import SignClient from "@walletconnect/sign-client" import { parseUri } from "@walletconnect/utils" import { SignClientTypes, SessionTypes } from "@walletconnect/types" -import { - EIP1193Error, - EIP1193_ERROR_CODES, - isEIP1193Error, - RPCRequest, -} from "@tallyho/provider-bridge-shared" +import SignClient from "@walletconnect/sign-client" +import { isEIP1193Error } from "@tallyho/provider-bridge-shared" import { ServiceCreatorFunction, ServiceLifecycleEvents } from "../types" @@ -14,24 +9,28 @@ import BaseService from "../base" import PreferenceService from "../preferences" import ProviderBridgeService from "../provider-bridge" import InternalEthereumProviderService from "../internal-ethereum-provider" -import { browser } from "../.." import { approveEIP155Request, + processRequestParams, rejectEIP155Request, } from "./eip155-request-utils" +import createSignClient from "./sign-client-helper" +import { + acknowledgeLegacyProposal, + createLegacySignClient, + LegacyEventData, + postLegacyApprovalResponse, + postLegacyRejectionResponse, + processLegacyRequestParams, +} from "./legacy-sign-client-helper" +import { getMetaPort } from "./utils" +import { TranslatedRequestParams } from "./types" + interface Events extends ServiceLifecycleEvents { placeHolderEventForTypingPurposes: string } -interface TranslatedRequestParams { - topic?: string - method: string - params: RPCRequest["params"] -} - -const temporaryDAppUri = "https://react-dapp-v2-with-ethers.vercel.app" // TODO: this constant should be removed and replaced with a dynamic value - /* * The walletconnect service is responsible for encapsulating the wallet connect * implementation details, maintaining the websocket connection, handling the protocol @@ -44,6 +43,8 @@ const temporaryDAppUri = "https://react-dapp-v2-with-ethers.vercel.app" // TODO: export default class WalletConnectService extends BaseService { signClient: SignClient | undefined + senderUrl = "" + /* * Create a new WalletConnectService. The service isn't initialized until * startService() is called and resolved. @@ -89,9 +90,8 @@ export default class WalletConnectService extends BaseService { await super.internalStopService() } - // eslint-disable-next-line class-methods-use-this private async initializeWalletConnect() { - this.signClient = await WalletConnectService.createSignClient() + this.signClient = await createSignClient() this.defineEventHandlers() // TODO: remove this, inject uri @@ -104,28 +104,13 @@ export default class WalletConnectService extends BaseService { }, 2000) } - private static createSignClient(): Promise { - return SignClient.init({ - logger: "debug", // TODO: set from .env - projectId: "9ab2e13df08600b06ac588e1292d6512", // TODO: set from .env - relayUrl: "wss://relay.walletconnect.com", - metadata: { - // TODO: customize this metadata - name: "Tally Ho Wallet", - description: "WalletConnect for Tally Ho wallet", - url: "https://walletconnect.com/", - icons: ["https://avatars.githubusercontent.com/u/37784886"], - }, - }) - } - private defineEventHandlers(): void { this.signClient?.on("session_proposal", (proposal) => - this.sessionProposalListener(proposal) + this.sessionProposalListener(false, proposal) ) this.signClient?.on("session_request", (event) => - this.sessionRequestListener(event) + this.sessionRequestListener(false, event) ) } @@ -140,10 +125,11 @@ export default class WalletConnectService extends BaseService { // Route the provided URI to the v1 SignClient if URI version indicates it, else use v2. if (version === 1) { - // createLegacySignClient({ uri }) - WalletConnectService.tempFeatureLog( - "TODO: unsupported legacy", - parseUri(uri) + WalletConnectService.tempFeatureLog("legacy pairing", parseUri(uri)) + createLegacySignClient( + uri, + (payload) => this.sessionProposalListener(true, payload), + (payload) => this.sessionRequestListener(true, undefined, payload) ) } else if (version === 2) { await this.signClient.pair({ uri }) @@ -165,6 +151,7 @@ export default class WalletConnectService extends BaseService { const { id, params } = proposal const { requiredNamespaces, relays } = params + WalletConnectService.tempFeatureLog("proposal", proposal) WalletConnectService.tempFeatureLog( "requiredNamespaces", requiredNamespaces @@ -194,7 +181,7 @@ export default class WalletConnectService extends BaseService { } private async postApprovalResponse( - event: SignClientTypes.EventArguments["session_request"], + event: TranslatedRequestParams, payload: any ) { const { topic } = event @@ -205,9 +192,7 @@ export default class WalletConnectService extends BaseService { }) } - private async postRejectionResponse( - event: SignClientTypes.EventArguments["session_request"] - ) { + private async postRejectionResponse(event: TranslatedRequestParams) { const { topic } = event const response = rejectEIP155Request(event) await this.signClient?.respond({ @@ -216,96 +201,85 @@ export default class WalletConnectService extends BaseService { }) } - private static getMetaPort( - name: string, - postMessage: (message: any) => void - ): Required { - const port: browser.Runtime.Port = browser.runtime.connect({ - name, - }) - port.sender = { - url: temporaryDAppUri, - } - port.postMessage = postMessage - return port as unknown as Required - } - - private static processRequestParams( - event: SignClientTypes.EventArguments["session_request"] - ): TranslatedRequestParams { - // TODO: figure out if this method is needed - const { params: eventParams } = event - // TODO: handle chain id - const { request } = eventParams - - switch (request.method) { - case "eth_signTypedData": - case "eth_signTypedData_v1": - case "eth_signTypedData_v3": - case "eth_signTypedData_v4": - return { - method: request.method, - params: request.params, - } - case "eth_sign": // --- important wallet methods --- - case "personal_sign": - case "eth_sendTransaction": - return { - method: request.method, - params: request.params, - } - case "eth_signTransaction": - return { - method: request.method, - params: request.params, - } - default: - throw new EIP1193Error(EIP1193_ERROR_CODES.unsupportedMethod) - } - } - async sessionRequestListener( - event: SignClientTypes.EventArguments["session_request"] + isLegacy: boolean, // TODO: this along with @legacyEvent should be removed when we fully migrate to v2, @event should become non optional + event?: SignClientTypes.EventArguments["session_request"], + legacyEvent?: LegacyEventData ): Promise { WalletConnectService.tempFeatureLog("in sessionRequestListener", event) - const { method, params } = WalletConnectService.processRequestParams(event) - const port = WalletConnectService.getMetaPort( + let request: TranslatedRequestParams | undefined + if (isLegacy && legacyEvent) { + request = processLegacyRequestParams(legacyEvent) + } else if (event) { + request = processRequestParams(event) + } + + const port = getMetaPort( "sessionRequestListenerPort", + this.senderUrl, async (message) => { WalletConnectService.tempFeatureLog( "sessionRequestListenerPort message:", message ) - // TODO: make this check more elaborate + if (!request) { + return + } + if (isEIP1193Error(message.result)) { - await this.postRejectionResponse(event) + if (isLegacy) { + postLegacyRejectionResponse(request) + } else { + await this.postRejectionResponse(request) + } + } else if (isLegacy) { + postLegacyApprovalResponse(request, message.result) } else { - await this.postApprovalResponse(event, message.result) + await this.postApprovalResponse(request, message.result) } } ) + if (!request) { + return + } + await this.providerBridgeService.onMessageListener(port, { id: "1400", - request: { - method, - params, - }, + request, }) } async sessionProposalListener( + isLegacy: boolean, proposal: SignClientTypes.EventArguments["session_proposal"] ): Promise { - WalletConnectService.tempFeatureLog("in sessionProposalListener") + WalletConnectService.tempFeatureLog("in sessionProposalListener", proposal) + + const { params } = proposal + + if (isLegacy && Array.isArray(params) && params.length > 0) { + this.senderUrl = params[0].peerMeta?.url || "" + } else if (params) { + this.senderUrl = params.proposer.metadata.url // we can also extract information such as icons and description + } + + if (!this.senderUrl) { + return + } - const port = WalletConnectService.getMetaPort( + const port = getMetaPort( "sessionProposalListenerPort", + this.senderUrl, async (message) => { if (Array.isArray(message.result) && message.result.length > 0) { - await this.acknowledgeProposal(proposal, message.result[0]) + if (isLegacy) { + acknowledgeLegacyProposal(message.result) + } else if (proposal) { + await this.acknowledgeProposal(proposal, message.result[0]) + } WalletConnectService.tempFeatureLog("pairing request acknowledged") } } @@ -320,7 +294,9 @@ export default class WalletConnectService extends BaseService { }) } + /* eslint-disable */ private static tempFeatureLog(message?: any, ...optionalParams: any[]): void { - console.log(`[WalletConnect Demo] - ${message || ""}`, optionalParams) + console.log(`[WalletConnect Demo] - ${message || ""}`, ...optionalParams) } + /* eslint-enable */ } diff --git a/background/services/wallet-connect/legacy-sign-client-helper.ts b/background/services/wallet-connect/legacy-sign-client-helper.ts new file mode 100644 index 0000000000..69a7c375b6 --- /dev/null +++ b/background/services/wallet-connect/legacy-sign-client-helper.ts @@ -0,0 +1,144 @@ +import { IWalletConnectSession } from "@walletconnect/legacy-types" +import LegacySignClient from "@walletconnect/client" +import { SignClientTypes } from "@walletconnect/types" + +import { TranslatedRequestParams } from "./types" +import { + approveEIP155Request, + rejectEIP155Request, +} from "./eip155-request-utils" + +type SessionProposalListener = ( + payload: SignClientTypes.EventArguments["session_proposal"] +) => void +type SessionRequestListener = (payload: any) => void + +export type LegacyEventData = { + id: number + topic: string + method: string + params: any[] +} + +let legacySignClient: LegacySignClient | undefined + +function deleteCachedLegacySession(): void { + if (typeof window === "undefined") return + window.localStorage.removeItem("walletconnect") +} + +function getCachedLegacySession(): IWalletConnectSession | null { + if (typeof window === "undefined") return null + + const local = window.localStorage + ? window.localStorage.getItem("walletconnect") + : null + + let session = null + if (local) { + session = JSON.parse(local) + } + return session +} + +export function createLegacySignClient( + uri?: string, + sessionProposalListener?: SessionProposalListener, + sessionRequestListener?: SessionRequestListener +): void { + // If URI is passed always create a new session, + // otherwise fall back to cached session if client isn't already instantiated. + if (uri) { + deleteCachedLegacySession() + legacySignClient = new LegacySignClient({ uri }) + } else if (!legacySignClient && getCachedLegacySession()) { + const session = getCachedLegacySession() + if (session != null) { + legacySignClient = new LegacySignClient({ session }) + } + } else { + return + } + + legacySignClient?.on("session_request", (error, payload) => { + if (error) { + throw new Error(`legacySignClient > session_request failed: ${error}`) + } + console.log("LegacySessionProposalModal", { legacyProposal: payload }) + sessionProposalListener?.(payload) + }) + + legacySignClient?.on("connect", () => { + console.log("legacySignClient > connect") + }) + + legacySignClient?.on("error", (error) => { + throw new Error(`legacySignClient > on error: ${error}`) + }) + + legacySignClient?.on("call_request", (error, payload) => { + if (error) { + throw new Error(`legacySignClient > call_request failed: ${error}`) + } + // onCallRequest(payload) + sessionRequestListener?.(payload) + }) + + legacySignClient?.on("disconnect", async () => { + deleteCachedLegacySession() + }) +} + +export function acknowledgeLegacyProposal(accounts: [string]): void { + legacySignClient?.approveSession({ + accounts, + chainId: 1, + }) +} + +export function processLegacyRequestParams( + payload: LegacyEventData +): TranslatedRequestParams | undefined { + // TODO: figure out if this method is needed + const { method } = payload + // TODO: handle chain id + + switch (method) { + case "eth_signTypedData": + case "personal_sign": + case "eth_sendTransaction": + case "eth_signTransaction": + return payload + default: + return undefined + } +} + +export async function postLegacyApprovalResponse( + event: TranslatedRequestParams, + payload: any +): Promise { + const { id } = event + const { result } = approveEIP155Request(event, payload) + legacySignClient?.approveRequest({ + id, + result, + }) +} + +export async function postLegacyRejectionResponse( + event: TranslatedRequestParams +): Promise { + const { id } = event + const { error } = rejectEIP155Request(event) + legacySignClient?.rejectRequest({ + id, + error, + }) +} + +/* eslint-disable */ +function tempFeatureLog(message?: any, ...optionalParams: any[]): void { + console.log(`[WalletConnect Demo V1] - ${message || ""}`, optionalParams) +} +/* eslint-enable */ diff --git a/background/services/wallet-connect/sign-client-helper.ts b/background/services/wallet-connect/sign-client-helper.ts new file mode 100644 index 0000000000..32a7856a5b --- /dev/null +++ b/background/services/wallet-connect/sign-client-helper.ts @@ -0,0 +1,16 @@ +import SignClient from "@walletconnect/sign-client" + +export default function createSignClient(): Promise { + return SignClient.init({ + logger: "debug", // TODO: set from .env + projectId: "9ab2e13df08600b06ac588e1292d6512", // TODO: set from .env + relayUrl: "wss://relay.walletconnect.com", + metadata: { + // TODO: customize this metadata + name: "Tally Ho Wallet", + description: "WalletConnect for Tally Ho wallet", + url: "https://walletconnect.com/", + icons: ["https://avatars.githubusercontent.com/u/37784886"], + }, + }) +} diff --git a/background/services/wallet-connect/types.ts b/background/services/wallet-connect/types.ts new file mode 100644 index 0000000000..1f3ff2f013 --- /dev/null +++ b/background/services/wallet-connect/types.ts @@ -0,0 +1,8 @@ +import { RPCRequest } from "@tallyho/provider-bridge-shared" + +export interface TranslatedRequestParams { + id: number + topic: string + method: string + params: RPCRequest["params"] +} diff --git a/background/services/wallet-connect/utils.ts b/background/services/wallet-connect/utils.ts new file mode 100644 index 0000000000..5a3b2c1a53 --- /dev/null +++ b/background/services/wallet-connect/utils.ts @@ -0,0 +1,17 @@ +import { browser } from "../.." + +/* eslint-disable import/prefer-default-export */ +export const getMetaPort = ( + name: string, + senderUrl: string, + postMessage: (message: any) => void +): Required => { + const port: browser.Runtime.Port = browser.runtime.connect({ + name, + }) + port.sender = { + url: senderUrl, + } + port.postMessage = postMessage + return port as unknown as Required +} diff --git a/yarn.lock b/yarn.lock index af4784ddf3..d7625f8961 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4076,6 +4076,27 @@ resolved "https://registry.yarnpkg.com/@uniswap/token-lists/-/token-lists-1.0.0-beta.30.tgz#2103ca23b8007c59ec71718d34cdc97861c409e5" integrity sha512-HwY2VvkQ8lNR6ks5NqQfAtg+4IZqz3KV1T8d2DlI8emIn9uMmaoFbIOg0nzjqAVKKnZSbMTRRtUoAh6mmjRvog== +"@walletconnect/browser-utils@^1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@walletconnect/browser-utils/-/browser-utils-1.8.0.tgz#33c10e777aa6be86c713095b5206d63d32df0951" + integrity sha512-Wcqqx+wjxIo9fv6eBUFHPsW1y/bGWWRboni5dfD8PtOmrihrEpOCmvRJe4rfl7xgJW8Ea9UqKEaq0bIRLHlK4A== + dependencies: + "@walletconnect/safe-json" "1.0.0" + "@walletconnect/types" "^1.8.0" + "@walletconnect/window-getters" "1.0.0" + "@walletconnect/window-metadata" "1.0.0" + detect-browser "5.2.0" + +"@walletconnect/client@^1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@walletconnect/client/-/client-1.8.0.tgz#6f46b5499c7c861c651ff1ebe5da5b66225ca696" + integrity sha512-svyBQ14NHx6Cs2j4TpkQaBI/2AF4+LXz64FojTjMtV4VMMhl81jSO1vNeg+yYhQzvjcGH/GpSwixjyCW0xFBOQ== + dependencies: + "@walletconnect/core" "^1.8.0" + "@walletconnect/iso-crypto" "^1.8.0" + "@walletconnect/types" "^1.8.0" + "@walletconnect/utils" "^1.8.0" + "@walletconnect/core@2.1.4": version "2.1.4" resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.1.4.tgz#871a1e6b80b14fc3a755763b2cdce0c334e41536" @@ -4098,6 +4119,36 @@ pino "7.11.0" uint8arrays "3.1.0" +"@walletconnect/core@^1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-1.8.0.tgz#6b2748b90c999d9d6a70e52e26a8d5e8bfeaa81e" + integrity sha512-aFTHvEEbXcZ8XdWBw6rpQDte41Rxwnuk3SgTD8/iKGSRTni50gI9S3YEzMj05jozSiOBxQci4pJDMVhIUMtarw== + dependencies: + "@walletconnect/socket-transport" "^1.8.0" + "@walletconnect/types" "^1.8.0" + "@walletconnect/utils" "^1.8.0" + +"@walletconnect/crypto@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@walletconnect/crypto/-/crypto-1.0.3.tgz#7b8dd4d7e2884fe3543c7c07aea425eef5ef9dd4" + integrity sha512-+2jdORD7XQs76I2Odgr3wwrtyuLUXD/kprNVsjWRhhhdO9Mt6WqVzOPu0/t7OHSmgal8k7SoBQzUc5hu/8zL/g== + dependencies: + "@walletconnect/encoding" "^1.0.2" + "@walletconnect/environment" "^1.0.1" + "@walletconnect/randombytes" "^1.0.3" + aes-js "^3.1.2" + hash.js "^1.1.7" + tslib "1.14.1" + +"@walletconnect/encoding@^1.0.1", "@walletconnect/encoding@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@walletconnect/encoding/-/encoding-1.0.2.tgz#cb3942ad038d6a6bf01158f66773062dd25724da" + integrity sha512-CrwSBrjqJ7rpGQcTL3kU+Ief+Bcuu9PH6JLOb+wM6NITX1GTxR/MfNwnQfhLKK6xpRAyj2/nM04OOH6wS8Imag== + dependencies: + is-typedarray "1.0.0" + tslib "1.14.1" + typedarray-to-buffer "3.1.5" + "@walletconnect/environment@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/environment/-/environment-1.0.1.tgz#1d7f82f0009ab821a2ba5ad5e5a7b8ae3b214cd7" @@ -4122,6 +4173,15 @@ "@walletconnect/time" "^1.0.2" tslib "1.14.1" +"@walletconnect/iso-crypto@^1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@walletconnect/iso-crypto/-/iso-crypto-1.8.0.tgz#44ddf337c4f02837c062dbe33fa7ab36789df451" + integrity sha512-pWy19KCyitpfXb70hA73r9FcvklS+FvO9QUIttp3c2mfW8frxgYeRXfxLRCIQTkaYueRKvdqPjbyhPLam508XQ== + dependencies: + "@walletconnect/crypto" "^1.0.2" + "@walletconnect/types" "^1.8.0" + "@walletconnect/utils" "^1.8.0" + "@walletconnect/jsonrpc-provider@^1.0.6": version "1.0.6" resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-provider/-/jsonrpc-provider-1.0.6.tgz#e91321ef523f1904e6634e7866a0f3c6f056d2cd" @@ -4131,7 +4191,7 @@ "@walletconnect/safe-json" "^1.0.1" tslib "1.14.1" -"@walletconnect/jsonrpc-types@^1.0.2": +"@walletconnect/jsonrpc-types@^1.0.0", "@walletconnect/jsonrpc-types@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-types/-/jsonrpc-types-1.0.2.tgz#b79519f679cd6a5fa4a1bea888f27c1916689a20" integrity sha512-CZe8tjJX73OWdHjrBHy7HtAapJ2tT0Q3TYhPBhRxi3643lwPIQWC9En45ldY14TZwgSewkbZ0FtGBZK0G7Bbyg== @@ -4139,7 +4199,7 @@ keyvaluestorage-interface "^1.0.0" tslib "1.14.1" -"@walletconnect/jsonrpc-utils@^1.0.4": +"@walletconnect/jsonrpc-utils@^1.0.3", "@walletconnect/jsonrpc-utils@^1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-utils/-/jsonrpc-utils-1.0.4.tgz#2009ba3907b02516f2caacd2fb871ff0d472b2cb" integrity sha512-y0+tDxcTZ9BHBBKBJbjZxLUXb+zQZCylf7y/jTvDPNx76J0hYYc+F9zHzyqBLeorSKepLTk6yI8hw3NXbAQB3g== @@ -4166,6 +4226,13 @@ safe-json-utils "^1.1.1" tslib "1.14.1" +"@walletconnect/legacy-types@^2.0.0-rc.0": + version "2.0.0-rc.0" + resolved "https://registry.yarnpkg.com/@walletconnect/legacy-types/-/legacy-types-2.0.0-rc.0.tgz#a4ee908501b943f5ceae19b3ed13e400c3a09aa0" + integrity sha512-9dDfZ+jTOycOSm4+HmcDj5T8qotqY96BaEU/LdQ6H2M/mBoYLkPLMlaNLoqIpSm+ebOj9CF0T6B251ZAZoo7/Q== + dependencies: + "@walletconnect/jsonrpc-types" "^1.0.0" + "@walletconnect/logger@^2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/logger/-/logger-2.0.1.tgz#7f489b96e9a1ff6bf3e58f0fbd6d69718bf844a8" @@ -4174,6 +4241,16 @@ pino "7.11.0" tslib "1.14.1" +"@walletconnect/randombytes@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@walletconnect/randombytes/-/randombytes-1.0.3.tgz#e795e4918367fd1e6a2215e075e64ab93e23985b" + integrity sha512-35lpzxcHFbTN3ABefC9W+uBpNZl1GC4Wpx0ed30gibfO/y9oLdy1NznbV96HARQKSBV9J9M/rrtIvf6a23jfYw== + dependencies: + "@walletconnect/encoding" "^1.0.2" + "@walletconnect/environment" "^1.0.1" + randombytes "^2.1.0" + tslib "1.14.1" + "@walletconnect/relay-api@^1.0.7": version "1.0.7" resolved "https://registry.yarnpkg.com/@walletconnect/relay-api/-/relay-api-1.0.7.tgz#e7aed03cbaff99ecdf2c8d32280c0b5d673bb419" @@ -4194,6 +4271,11 @@ tslib "1.14.1" uint8arrays "^3.0.0" +"@walletconnect/safe-json@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@walletconnect/safe-json/-/safe-json-1.0.0.tgz#12eeb11d43795199c045fafde97e3c91646683b2" + integrity sha512-QJzp/S/86sUAgWY6eh5MKYmSfZaRpIlmCJdi5uG4DJlKkZrHEF7ye7gA+VtbVzvTtpM/gRwO2plQuiooIeXjfg== + "@walletconnect/safe-json@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/safe-json/-/safe-json-1.0.1.tgz#9813fa0a7a544b16468730c2d7bed046ed160957" @@ -4218,6 +4300,15 @@ events "^3.3.0" pino "7.11.0" +"@walletconnect/socket-transport@^1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@walletconnect/socket-transport/-/socket-transport-1.8.0.tgz#9a1128a249628a0be11a0979b522fe82b44afa1b" + integrity sha512-5DyIyWrzHXTcVp0Vd93zJ5XMW61iDM6bcWT4p8DTRfFsOtW46JquruMhxOLeCOieM4D73kcr3U7WtyR4JUsGuQ== + dependencies: + "@walletconnect/types" "^1.8.0" + "@walletconnect/utils" "^1.8.0" + ws "7.5.3" + "@walletconnect/time@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@walletconnect/time/-/time-1.0.2.tgz#6c5888b835750ecb4299d28eecc5e72c6d336523" @@ -4237,6 +4328,11 @@ "@walletconnect/logger" "^2.0.1" events "^3.3.0" +"@walletconnect/types@^1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-1.8.0.tgz#3f5e85b2d6b149337f727ab8a71b8471d8d9a195" + integrity sha512-Cn+3I0V0vT9ghMuzh1KzZvCkiAxTq+1TR2eSqw5E5AVWfmCtECFkVZBP6uUJZ8YjwLqXheI+rnjqPy7sVM4Fyg== + "@walletconnect/utils@2.1.4", "@walletconnect/utils@^2.1.4": version "2.1.4" resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.1.4.tgz#992442d175f71cf7a685c5568d12bf25d719600e" @@ -4258,13 +4354,38 @@ query-string "7.1.1" uint8arrays "3.1.0" -"@walletconnect/window-getters@^1.0.1": +"@walletconnect/utils@^1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-1.8.0.tgz#2591a197c1fa7429941fe428876088fda6632060" + integrity sha512-zExzp8Mj1YiAIBfKNm5u622oNw44WOESzo6hj+Q3apSMIb0Jph9X3GDIdbZmvVZsNPxWDL7uodKgZcCInZv2vA== + dependencies: + "@walletconnect/browser-utils" "^1.8.0" + "@walletconnect/encoding" "^1.0.1" + "@walletconnect/jsonrpc-utils" "^1.0.3" + "@walletconnect/types" "^1.8.0" + bn.js "4.11.8" + js-sha3 "0.8.0" + query-string "6.13.5" + +"@walletconnect/window-getters@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@walletconnect/window-getters/-/window-getters-1.0.0.tgz#1053224f77e725dfd611c83931b5f6c98c32bfc8" + integrity sha512-xB0SQsLaleIYIkSsl43vm8EwETpBzJ2gnzk7e0wMF3ktqiTGS6TFHxcprMl5R44KKh4tCcHCJwolMCaDSwtAaA== + +"@walletconnect/window-getters@^1.0.0", "@walletconnect/window-getters@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/window-getters/-/window-getters-1.0.1.tgz#f36d1c72558a7f6b87ecc4451fc8bd44f63cbbdc" integrity sha512-vHp+HqzGxORPAN8gY03qnbTMnhqIwjeRJNOMOAzePRg4xVEEE2WvYsI9G2NMjOknA8hnuYbU3/hwLcKbjhc8+Q== dependencies: tslib "1.14.1" +"@walletconnect/window-metadata@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@walletconnect/window-metadata/-/window-metadata-1.0.0.tgz#93b1cc685e6b9b202f29c26be550fde97800c4e5" + integrity sha512-9eFvmJxIKCC3YWOL97SgRkKhlyGXkrHwamfechmqszbypFspaSk+t2jQXAEU7YClHF6Qjw5eYOmy1//zFi9/GA== + dependencies: + "@walletconnect/window-getters" "^1.0.0" + "@walletconnect/window-metadata@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/window-metadata/-/window-metadata-1.0.1.tgz#2124f75447b7e989e4e4e1581d55d25bc75f7be5" @@ -4492,6 +4613,11 @@ aes-js@3.0.0: resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.0.0.tgz#e21df10ad6c2053295bcbb8dab40b09dbea87e4d" integrity sha1-4h3xCtbCBTKVvLuNq0Cwnb6ofk0= +aes-js@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.1.2.tgz#db9aabde85d5caabbfc0d4f2a4446960f627146a" + integrity sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ== + agent-base@6, agent-base@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -5253,6 +5379,11 @@ blakejs@^1.1.0: resolved "https://registry.yarnpkg.com/blakejs/-/blakejs-1.1.1.tgz#bf313053978b2cd4c444a48795710be05c785702" integrity sha512-bLG6PHOCZJKNshTjGRBvET0vTciwQE6zFKOKKXPDJfwFBd4Ac0yBfPZqcGvGJap50l7ktvlpFqc2jGVaUgbJgg== +bn.js@4.11.8: + version "4.11.8" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" + integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== + bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.1, bn.js@^4.11.9: version "4.12.0" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" @@ -6395,6 +6526,11 @@ destroy@1.2.0: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== +detect-browser@5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/detect-browser/-/detect-browser-5.2.0.tgz#c9cd5afa96a6a19fda0bbe9e9be48a6b6e1e9c97" + integrity sha512-tr7XntDAu50BVENgQfajMLzacmSe34D+qZc4zjnniz0ZVuw/TZcLcyxHQjYpJTM36sGEkZZlYLnIM1hH7alTMA== + detect-browser@5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/detect-browser/-/detect-browser-5.3.0.tgz#9705ef2bddf46072d0f7265a1fe300e36fe7ceca" @@ -8608,7 +8744,7 @@ is-typed-array@^1.1.3, is-typed-array@^1.1.7: foreach "^2.0.5" has-tostringtag "^1.0.0" -is-typedarray@^1.0.0, is-typedarray@~1.0.0: +is-typedarray@1.0.0, is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= @@ -11501,6 +11637,15 @@ qs@~6.5.2: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== +query-string@6.13.5: + version "6.13.5" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.13.5.tgz#99e95e2fb7021db90a6f373f990c0c814b3812d8" + integrity sha512-svk3xg9qHR39P3JlHuD7g3nRnyay5mHbrPctEBDUxUkHRifPHXJDhBUycdCC0NBjXoDf44Gb+IsOZL1Uwn8M/Q== + dependencies: + decode-uri-component "^0.2.0" + split-on-first "^1.0.0" + strict-uri-encode "^2.0.0" + query-string@7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.1.tgz#754620669db978625a90f635f12617c271a088e1" @@ -13399,7 +13544,7 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -typedarray-to-buffer@^3.1.5: +typedarray-to-buffer@3.1.5, typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== @@ -14013,6 +14158,11 @@ ws@7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== +ws@7.5.3: + version "7.5.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74" + integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg== + ws@^7.4.6: version "7.5.5" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881" From c6d3d840dbfa0b65690c0961507425cef664bca3 Mon Sep 17 00:00:00 2001 From: Elad Date: Fri, 16 Dec 2022 12:06:09 +0100 Subject: [PATCH 004/187] logging fix resolving linting issue --- .../wallet-connect/legacy-sign-client-helper.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/background/services/wallet-connect/legacy-sign-client-helper.ts b/background/services/wallet-connect/legacy-sign-client-helper.ts index 69a7c375b6..2cbf693d58 100644 --- a/background/services/wallet-connect/legacy-sign-client-helper.ts +++ b/background/services/wallet-connect/legacy-sign-client-helper.ts @@ -22,6 +22,12 @@ export type LegacyEventData = { let legacySignClient: LegacySignClient | undefined +/* eslint-disable */ +function tempFeatureLog(message?: any, ...optionalParams: any[]): void { + console.log(`[WalletConnect Demo V1] - ${message || ""}`, optionalParams) +} +/* eslint-enable */ + function deleteCachedLegacySession(): void { if (typeof window === "undefined") return window.localStorage.removeItem("walletconnect") @@ -64,12 +70,12 @@ export function createLegacySignClient( if (error) { throw new Error(`legacySignClient > session_request failed: ${error}`) } - console.log("LegacySessionProposalModal", { legacyProposal: payload }) + tempFeatureLog("LegacySessionProposalModal", { legacyProposal: payload }) sessionProposalListener?.(payload) }) legacySignClient?.on("connect", () => { - console.log("legacySignClient > connect") + tempFeatureLog("legacySignClient > connect") }) legacySignClient?.on("error", (error) => { @@ -136,9 +142,3 @@ export async function postLegacyRejectionResponse( error, }) } - -/* eslint-disable */ -function tempFeatureLog(message?: any, ...optionalParams: any[]): void { - console.log(`[WalletConnect Demo V1] - ${message || ""}`, optionalParams) -} -/* eslint-enable */ From e994a980b252a75f4fca6ed2f0bbc1c0469f0f47 Mon Sep 17 00:00:00 2001 From: Gergo Nagy Date: Fri, 16 Dec 2022 15:46:58 +0100 Subject: [PATCH 005/187] implement `tally_walletConnectInit` flow --- background/services/provider-bridge/index.ts | 40 ++++++++++++++++---- background/services/wallet-connect/index.ts | 24 +++++------- background/services/wallet-connect/utils.ts | 2 +- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/background/services/provider-bridge/index.ts b/background/services/provider-bridge/index.ts index a360f6012b..a35600a91a 100644 --- a/background/services/provider-bridge/index.ts +++ b/background/services/provider-bridge/index.ts @@ -33,6 +33,7 @@ type Events = ServiceLifecycleEvents & { requestPermission: PermissionRequest initializeAllowedPages: PermissionMap setClaimReferrer: string + walletConnectInit: string } /** @@ -147,15 +148,40 @@ export default class ProviderBridgeService extends BaseService { defaultWallet: await this.preferenceService.getDefaultWallet(), chainId: toHexChainID(network.chainID), } - } else if (event.request.method === "tally_setClaimReferrer") { - const referrer = event.request.params[0] - if (origin !== WEBSITE_ORIGIN || typeof referrer !== "string") { - logger.warn(`invalid 'setClaimReferrer' request`) - return + } else if (event.request.method.startsWith("tally_")) { + switch (event.request.method) { + case "tally_setClaimReferrer": + if (origin !== WEBSITE_ORIGIN) { + logger.warn( + `invalid WEBSITE_ORIGIN ${WEBSITE_ORIGIN} when using a custom 'tally_...' method` + ) + return + } + + if (typeof event.request.params[0] !== "string") { + logger.warn(`invalid 'tally_setClaimReferrer' request`) + return + } + + this.emitter.emit("setClaimReferrer", String(event.request.params[0])) + break + case "tally_walletConnectInit": + if (typeof event.request.params[0] !== "string") { + logger.warn(`invalid 'tally_walletConnectInit' request`) + return + } + + await this.emitter.emit( + "walletConnectInit", + String(event.request.params[0]) + ) + break + default: + logger.debug( + `Unknown method ${event.request.method} in 'ProviderBridgeService'` + ) } - this.emitter.emit("setClaimReferrer", String(referrer)) - response.result = null } else if ( event.request.method === "eth_chainId" || diff --git a/background/services/wallet-connect/index.ts b/background/services/wallet-connect/index.ts index 05997c328e..ee32391898 100644 --- a/background/services/wallet-connect/index.ts +++ b/background/services/wallet-connect/index.ts @@ -80,7 +80,15 @@ export default class WalletConnectService extends BaseService { protected override async internalStartService(): Promise { await super.internalStartService() - await this.initializeWalletConnect() + this.signClient = await createSignClient() + this.defineEventHandlers() + + this.providerBridgeService.emitter.on( + "walletConnectInit", + async (wcUri: string) => { + this.performConnection(wcUri) + } + ) } protected override async internalStopService(): Promise { @@ -90,20 +98,6 @@ export default class WalletConnectService extends BaseService { await super.internalStopService() } - private async initializeWalletConnect() { - this.signClient = await createSignClient() - this.defineEventHandlers() - - // TODO: remove this, inject uri - // simulate connection attempt - const wcUri = - "wc:70e3ca637cd88494fd6e68d348c0f3940a73911bb272e409b28c28faaa9d89f5@2?relay-protocol=irn&symKey=8bcd2d3018dda379dd836b19a56ee4b70506ba20615f6c1b70e9b622c7fce73c" - - setTimeout(() => { - this.performConnection(wcUri) - }, 2000) - } - private defineEventHandlers(): void { this.signClient?.on("session_proposal", (proposal) => this.sessionProposalListener(false, proposal) diff --git a/background/services/wallet-connect/utils.ts b/background/services/wallet-connect/utils.ts index 5a3b2c1a53..c5a394c8b7 100644 --- a/background/services/wallet-connect/utils.ts +++ b/background/services/wallet-connect/utils.ts @@ -1,4 +1,4 @@ -import { browser } from "../.." +import browser from "webextension-polyfill" /* eslint-disable import/prefer-default-export */ export const getMetaPort = ( From 146d6fc14b9033984a81cc2fdb4400764e2c6dcf Mon Sep 17 00:00:00 2001 From: Gergo Nagy Date: Fri, 16 Dec 2022 19:18:32 +0100 Subject: [PATCH 006/187] implement tally button inject on WC modal --- env.d.ts | 1 + window-provider/wallet-connection-handlers.ts | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/env.d.ts b/env.d.ts index 67c7e82ef8..8188315307 100644 --- a/env.d.ts +++ b/env.d.ts @@ -38,6 +38,7 @@ type WalletProvider = { type TallyProvider = WalletProvider & { isTally: true + send: (method: string, params: unknown[]) => Promise } type WindowEthereum = WalletProvider & { diff --git a/window-provider/wallet-connection-handlers.ts b/window-provider/wallet-connection-handlers.ts index bd88a1280e..99fd958e4b 100644 --- a/window-provider/wallet-connection-handlers.ts +++ b/window-provider/wallet-connection-handlers.ts @@ -446,6 +446,33 @@ function findAndReplaceAlpacaFinanceMetamaskOption(addedNode: Node): void { } } +function addTallyButtonForWalletConnectModal(addedNode: Node): void { + // For some reason this fails with children but works with childNodes. + // childNodes is a NodeList and Node elements don't have className in their types. + // In reality it's there and works well. + if ( + (addedNode?.childNodes[1] as unknown as HTMLElement)?.className !== + "walletconnect-search__input" + ) { + return + } + + const walletButtonsWrapper = (addedNode as unknown as HTMLElement) + .children[2] as HTMLElement + + const aWalletButton = walletButtonsWrapper.children[2] as HTMLAnchorElement + const aUrl = new URL(aWalletButton.href) + + const wcUri = aUrl.searchParams.get("uri") + + const tallyButton = document.createElement("button") + tallyButton.innerHTML = "Here be doggos" + tallyButton.onclick = () => { + window?.tally?.send("tally_walletConnectInit", [wcUri]) + } + walletButtonsWrapper.before(tallyButton) +} + const hostnameToHandler = { "uniswap.org": findAndReplaceUniswapInjectedOption, "gmx.io": findAndReplaceGMXMetamaskOption, @@ -469,4 +496,6 @@ export default function monitorForWalletConnectionPrompts(): void { observeMutations(hostnameToHandler[hostname]) } }) + + observeMutations(addTallyButtonForWalletConnectModal) } From 647ba95657339ce4ffdb1f31e9ced9be2cbbea6b Mon Sep 17 00:00:00 2001 From: Gergo Nagy Date: Fri, 16 Dec 2022 19:40:35 +0100 Subject: [PATCH 007/187] remove injection until release --- window-provider/wallet-connection-handlers.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/window-provider/wallet-connection-handlers.ts b/window-provider/wallet-connection-handlers.ts index 99fd958e4b..d47df8edb8 100644 --- a/window-provider/wallet-connection-handlers.ts +++ b/window-provider/wallet-connection-handlers.ts @@ -446,6 +446,7 @@ function findAndReplaceAlpacaFinanceMetamaskOption(addedNode: Node): void { } } +// eslint-disable-next-line @typescript-eslint/no-unused-vars function addTallyButtonForWalletConnectModal(addedNode: Node): void { // For some reason this fails with children but works with childNodes. // childNodes is a NodeList and Node elements don't have className in their types. @@ -497,5 +498,7 @@ export default function monitorForWalletConnectionPrompts(): void { } }) - observeMutations(addTallyButtonForWalletConnectModal) + // Commenting this out for now, because we don't have a way to put this behind a feature flag + // SUPPORT_WALLET_CONNECT flag + // observeMutations(addTallyButtonForWalletConnectModal) } From 6616bef3b6b5cfb88e6fd5fe191bab93052348a7 Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Mon, 19 Dec 2022 12:31:31 +0100 Subject: [PATCH 008/187] Displaying an error message from an RPC request --- background/services/provider-bridge/index.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/background/services/provider-bridge/index.ts b/background/services/provider-bridge/index.ts index a360f6012b..43c87d4ce5 100644 --- a/background/services/provider-bridge/index.ts +++ b/background/services/provider-bridge/index.ts @@ -452,7 +452,18 @@ export default class ProviderBridgeService extends BaseService { } } } catch (error) { - logger.log("error processing request", error) + if (typeof error === "object" && error !== null && "body" in error) { + logger.log("error processing request", error) + const parsedError = JSON.parse((error as { body: string }).body)?.error + if (parsedError) { + return { + ...parsedError, + message: + parsedError.message[0].toUpperCase() + + parsedError.message.slice(1), + } + } + } return new EIP1193Error(EIP1193_ERROR_CODES.userRejectedRequest).toJSON() } } From 4e73b4cf25e7774ef8870d93df1f9d73126e228a Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Tue, 20 Dec 2022 10:51:50 +0100 Subject: [PATCH 009/187] Improve displaying RPC errors for dapps --- background/services/provider-bridge/index.ts | 17 +++++++-------- background/services/provider-bridge/utils.ts | 22 +++++++++++++++++++- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/background/services/provider-bridge/index.ts b/background/services/provider-bridge/index.ts index 43c87d4ce5..3df9c32a05 100644 --- a/background/services/provider-bridge/index.ts +++ b/background/services/provider-bridge/index.ts @@ -25,7 +25,7 @@ import { import showExtensionPopup from "./show-popup" import { HexString } from "../../types" import { WEBSITE_ORIGIN } from "../../constants/website" -import { PermissionMap } from "./utils" +import { getRPCErrorResponser, PermissionMap } from "./utils" import { toHexChainID } from "../../networks" import { TALLY_INTERNAL_ORIGIN } from "../internal-ethereum-provider/constants" @@ -452,15 +452,14 @@ export default class ProviderBridgeService extends BaseService { } } } catch (error) { - if (typeof error === "object" && error !== null && "body" in error) { + if (typeof error === "object" && error !== null) { logger.log("error processing request", error) - const parsedError = JSON.parse((error as { body: string }).body)?.error - if (parsedError) { - return { - ...parsedError, - message: - parsedError.message[0].toUpperCase() + - parsedError.message.slice(1), + // eslint-disable-next-line default-case + switch (true) { + case "body" in error: + return getRPCErrorResponser(error) + case "error" in error: { + return getRPCErrorResponser((error as { error: string }).error) } } } diff --git a/background/services/provider-bridge/utils.ts b/background/services/provider-bridge/utils.ts index c88db9fb41..610c27a123 100644 --- a/background/services/provider-bridge/utils.ts +++ b/background/services/provider-bridge/utils.ts @@ -1,4 +1,7 @@ -import { PermissionRequest } from "@tallyho/provider-bridge-shared" +import { + EIP1193_ERROR_CODES, + PermissionRequest, +} from "@tallyho/provider-bridge-shared" export type PermissionMap = { evm: { @@ -23,3 +26,20 @@ export const keyPermissionsByChainIdAddressOrigin = ( }) return map } + +export function getRPCErrorResponser(error: unknown): { + code: number + message: string +} { + const parsedError = JSON.parse((error as { body: string }).body)?.error + return { + /** + * The code should be the same as for user rejected requests because otherwise it will not be displayed. + */ + code: 4001, + message: + "message" in parsedError && parsedError.message + ? parsedError.message[0].toUpperCase() + parsedError.message.slice(1) + : EIP1193_ERROR_CODES.userRejectedRequest.message, + } +} From db2f4c80f05d226d10bb41879058635d4c7893e9 Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Thu, 22 Dec 2022 09:06:02 +0100 Subject: [PATCH 010/187] Use the if structure to extend error messages --- background/services/provider-bridge/index.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/background/services/provider-bridge/index.ts b/background/services/provider-bridge/index.ts index 3df9c32a05..e9a6db21ef 100644 --- a/background/services/provider-bridge/index.ts +++ b/background/services/provider-bridge/index.ts @@ -454,13 +454,11 @@ export default class ProviderBridgeService extends BaseService { } catch (error) { if (typeof error === "object" && error !== null) { logger.log("error processing request", error) - // eslint-disable-next-line default-case - switch (true) { - case "body" in error: - return getRPCErrorResponser(error) - case "error" in error: { - return getRPCErrorResponser((error as { error: string }).error) - } + if ("body" in error) { + return getRPCErrorResponser(error) + } + if ("error" in error) { + return getRPCErrorResponser((error as { error: string }).error) } } return new EIP1193Error(EIP1193_ERROR_CODES.userRejectedRequest).toJSON() From 487f15a013c0229e19353d3ffad1f03db5cd68e9 Mon Sep 17 00:00:00 2001 From: Elad Date: Tue, 27 Dec 2022 19:00:05 +0100 Subject: [PATCH 011/187] connection acknowledgement improvements acknowledgment was expanded to support v1 and various rejection scenarios --- background/main.ts | 3 +- background/services/wallet-connect/index.ts | 77 +++++++++++++------ .../legacy-sign-client-helper.ts | 29 +++++-- 3 files changed, 78 insertions(+), 31 deletions(-) diff --git a/background/main.ts b/background/main.ts index 6469254594..5896236c7a 100644 --- a/background/main.ts +++ b/background/main.ts @@ -305,7 +305,8 @@ export default class Main extends BaseService { ? WalletConnectService.create( providerBridgeService, internalEthereumProviderService, - preferenceService + preferenceService, + chainService ) : getNoopService() diff --git a/background/services/wallet-connect/index.ts b/background/services/wallet-connect/index.ts index ee32391898..74d3a79dcf 100644 --- a/background/services/wallet-connect/index.ts +++ b/background/services/wallet-connect/index.ts @@ -1,4 +1,4 @@ -import { parseUri } from "@walletconnect/utils" +import { parseUri, getSdkError } from "@walletconnect/utils" import { SignClientTypes, SessionTypes } from "@walletconnect/types" import SignClient from "@walletconnect/sign-client" import { isEIP1193Error } from "@tallyho/provider-bridge-shared" @@ -20,12 +20,15 @@ import { acknowledgeLegacyProposal, createLegacySignClient, LegacyEventData, + LegacyProposal, postLegacyApprovalResponse, postLegacyRejectionResponse, processLegacyRequestParams, + rejectLegacyProposal, } from "./legacy-sign-client-helper" import { getMetaPort } from "./utils" import { TranslatedRequestParams } from "./types" +import ChainService from "../chain" interface Events extends ServiceLifecycleEvents { placeHolderEventForTypingPurposes: string @@ -55,24 +58,28 @@ export default class WalletConnectService extends BaseService { [ Promise, Promise, - Promise + Promise, + Promise ] > = async ( providerBridgeService, internalEthereumProviderService, - preferenceService + preferenceService, + chainService ) => { return new this( await providerBridgeService, await internalEthereumProviderService, - await preferenceService + await preferenceService, + await chainService ) } private constructor( private providerBridgeService: ProviderBridgeService, private internalEthereumProviderService: InternalEthereumProviderService, - private preferenceService: PreferenceService + private preferenceService: PreferenceService, + private chainService: ChainService ) { super() } @@ -122,7 +129,7 @@ export default class WalletConnectService extends BaseService { WalletConnectService.tempFeatureLog("legacy pairing", parseUri(uri)) createLegacySignClient( uri, - (payload) => this.sessionProposalListener(true, payload), + (payload) => this.sessionProposalListener(true, undefined, payload), (payload) => this.sessionRequestListener(true, undefined, payload) ) } else if (version === 2) { @@ -139,7 +146,7 @@ export default class WalletConnectService extends BaseService { private async acknowledgeProposal( proposal: SignClientTypes.EventArguments["session_proposal"], - address: string + selectedAccounts: [string] ) { // TODO: in case of a new connection, this callback should perform request processing AFTER wallet selection/confirmation dialog const { id, params } = proposal @@ -150,14 +157,23 @@ export default class WalletConnectService extends BaseService { "requiredNamespaces", requiredNamespaces ) - // TODO: expand this section to be able to match requiredNamespaces to actual wallet - const key = "eip155" - const accounts = [`eip155:1:${address}`] + + const ethNamespaceKey = "eip155" + const ethNamespace = requiredNamespaces[ethNamespaceKey] + if (!ethNamespace) { + await this.rejectProposal(id) + return + } + const namespaces: SessionTypes.Namespaces = {} - namespaces[key] = { + const accounts: string[] = [] + ethNamespace.chains.forEach((chain) => { + selectedAccounts.map((acc) => accounts.push(`${chain}:${acc}`)) + }) + namespaces[ethNamespaceKey] = { accounts, - methods: requiredNamespaces[key].methods, - events: requiredNamespaces[key].events, + methods: requiredNamespaces[ethNamespaceKey].methods, + events: requiredNamespaces[ethNamespaceKey].events, } if (this.signClient !== undefined && relays.length > 0) { @@ -171,9 +187,17 @@ export default class WalletConnectService extends BaseService { WalletConnectService.tempFeatureLog("connection acknowledged", namespaces) } else { // TODO: how to handle this case? + await this.rejectProposal(id) } } + private async rejectProposal(id: number) { + await this.signClient?.reject({ + id, + reason: getSdkError("USER_REJECTED_METHODS"), + }) + } + private async postApprovalResponse( event: TranslatedRequestParams, payload: any @@ -248,15 +272,18 @@ export default class WalletConnectService extends BaseService { async sessionProposalListener( isLegacy: boolean, - proposal: SignClientTypes.EventArguments["session_proposal"] + proposal?: SignClientTypes.EventArguments["session_proposal"], + legacyProposal?: LegacyProposal ): Promise { WalletConnectService.tempFeatureLog("in sessionProposalListener", proposal) - const { params } = proposal - - if (isLegacy && Array.isArray(params) && params.length > 0) { - this.senderUrl = params[0].peerMeta?.url || "" - } else if (params) { + if (isLegacy && legacyProposal) { + const { params } = legacyProposal + if (Array.isArray(params) && params.length > 0) { + this.senderUrl = params[0].peerMeta?.url || "" + } + } else if (proposal) { + const { params } = proposal this.senderUrl = params.proposer.metadata.url // we can also extract information such as icons and description } @@ -269,12 +296,18 @@ export default class WalletConnectService extends BaseService { this.senderUrl, async (message) => { if (Array.isArray(message.result) && message.result.length > 0) { - if (isLegacy) { - acknowledgeLegacyProposal(message.result) + if (isLegacy && legacyProposal) { + acknowledgeLegacyProposal(legacyProposal, message.result) } else if (proposal) { - await this.acknowledgeProposal(proposal, message.result[0]) + await this.acknowledgeProposal(proposal, message.result) } WalletConnectService.tempFeatureLog("pairing request acknowledged") + } else if (isEIP1193Error(message.result)) { + if (isLegacy) { + rejectLegacyProposal() + } else if (proposal) { + await this.rejectProposal(proposal?.id) + } } } ) diff --git a/background/services/wallet-connect/legacy-sign-client-helper.ts b/background/services/wallet-connect/legacy-sign-client-helper.ts index 2cbf693d58..3f1fc32cc6 100644 --- a/background/services/wallet-connect/legacy-sign-client-helper.ts +++ b/background/services/wallet-connect/legacy-sign-client-helper.ts @@ -1,17 +1,17 @@ -import { IWalletConnectSession } from "@walletconnect/legacy-types" +import { IClientMeta, IWalletConnectSession } from "@walletconnect/legacy-types" import LegacySignClient from "@walletconnect/client" -import { SignClientTypes } from "@walletconnect/types" +import { getSdkError } from "@walletconnect/utils" import { TranslatedRequestParams } from "./types" import { approveEIP155Request, rejectEIP155Request, } from "./eip155-request-utils" -type SessionProposalListener = ( - payload: SignClientTypes.EventArguments["session_proposal"] -) => void -type SessionRequestListener = (payload: any) => void +export type LegacyProposal = { + id: number + params: [{ chainId: number; peerId: string; peerMeta: IClientMeta }] +} export type LegacyEventData = { id: number @@ -20,6 +20,9 @@ export type LegacyEventData = { params: any[] } +type SessionProposalListener = (payload: LegacyProposal) => void +type SessionRequestListener = (payload: any) => void + let legacySignClient: LegacySignClient | undefined /* eslint-disable */ @@ -95,13 +98,23 @@ export function createLegacySignClient( }) } -export function acknowledgeLegacyProposal(accounts: [string]): void { +export function acknowledgeLegacyProposal( + proposal: LegacyProposal, + accounts: string[] +): void { + const { params } = proposal + const [{ chainId }] = params + legacySignClient?.approveSession({ accounts, - chainId: 1, + chainId: chainId ?? 1, }) } +export function rejectLegacyProposal(): void { + legacySignClient?.rejectSession(getSdkError("USER_REJECTED_METHODS")) +} + export function processLegacyRequestParams( payload: LegacyEventData ): TranslatedRequestParams | undefined { From 1d98fe8f3697599fc3bd321fb6ea97fac3245b6b Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Wed, 28 Dec 2022 12:31:01 +0100 Subject: [PATCH 012/187] Add first test for provider-bridge --- .../provider-bridge/tests/index.unit.test.ts | 105 ++++++++++++++++++ background/tests/factories.ts | 33 ++++++ setupJest.ts | 8 ++ 3 files changed, 146 insertions(+) create mode 100644 background/services/provider-bridge/tests/index.unit.test.ts diff --git a/background/services/provider-bridge/tests/index.unit.test.ts b/background/services/provider-bridge/tests/index.unit.test.ts new file mode 100644 index 0000000000..3c0ed92f4a --- /dev/null +++ b/background/services/provider-bridge/tests/index.unit.test.ts @@ -0,0 +1,105 @@ +import { + EIP1193_ERROR_CODES, + PermissionRequest, +} from "@tallyho/provider-bridge-shared" +import sinon from "sinon" +import { createProviderBridgeService } from "../../../tests/factories" +import ProviderBridgeService from "../index" + +const WINDOW = { + focused: true, + incognito: false, + alwaysOnTop: true, +} + +const chainID = "1" +const accountAddress = "0x0000000000000000000000000000000000000000" + +const BASE_DATA = { + enablingPermission: { + key: `https://app.test_${"0x0000000000000000000000000000000000000000"}_${chainID}`, + origin: "https://app.test", + faviconUrl: "https://app.test/favicon.png", + title: "Test", + state: "allow", + accountAddress, + chainID, + } as PermissionRequest, + origin: "https://app.test", +} + +const PARAMS = { + eth_accounts: ["Test", "https://app.test/favicon.png"], + eth_sendTransaction: [ + { + from: accountAddress, + data: Date.now().toString(), + gasPrice: "0xf4240", + to: "0x1111111111111111111111111111111111111111", + }, + ], +} +describe("Provider bridge", () => { + let providerBridgeService: ProviderBridgeService + const sandbox = sinon.createSandbox() + + beforeEach(async () => { + browser.windows.getCurrent = jest.fn(() => Promise.resolve(WINDOW)) + providerBridgeService = await createProviderBridgeService() + await providerBridgeService.startService() + sandbox.restore() + }) + + afterEach(async () => { + await providerBridgeService.stopService() + }) + + describe("routeContentScriptRPCRequest", () => { + it("should return the account address owned by the client", async () => { + const { enablingPermission, origin } = BASE_DATA + const method = "eth_accounts" + const params = PARAMS[method] + + const response = await providerBridgeService.routeContentScriptRPCRequest( + enablingPermission, + method, + params, + origin + ) + expect(response).toEqual([enablingPermission.accountAddress]) + }) + + it("should call routeSafeRequest user has permission to sign", async () => { + const { enablingPermission, origin } = BASE_DATA + const method = "eth_sendTransaction" + const params = PARAMS[method] + const stub = sandbox.stub(providerBridgeService, "routeSafeRequest") + + await providerBridgeService.routeContentScriptRPCRequest( + enablingPermission, + method, + params, + origin + ) + + expect(stub.called).toBe(true) + }) + + it("should not call routeSafeRequest user has not permission to sign", async () => { + const { enablingPermission, origin } = BASE_DATA + const method = "eth_sendTransaction" + const params = PARAMS[method] + const stub = sandbox.stub(providerBridgeService, "routeSafeRequest") + + const response = await providerBridgeService.routeContentScriptRPCRequest( + { ...enablingPermission, state: "deny" }, + method, + params, + origin + ) + + expect(stub.called).toBe(false) + expect(response).toBe(EIP1193_ERROR_CODES.userRejectedRequest) + }) + }) +}) diff --git a/background/tests/factories.ts b/background/tests/factories.ts index 5939dca038..ce00a12df4 100644 --- a/background/tests/factories.ts +++ b/background/tests/factories.ts @@ -33,10 +33,12 @@ import { import { ChainService, IndexingService, + InternalEthereumProviderService, KeyringService, LedgerService, NameService, PreferenceService, + ProviderBridgeService, SigningService, } from "../services" import { QueuedTxToRetrieve } from "../services/chain" @@ -104,6 +106,16 @@ type CreateSigningServiceOverrides = { chainService?: Promise } +type CreateProviderBridgeServiceOverrides = { + internalEthereumProviderService?: Promise + preferenceService?: Promise +} + +type CreateInternalEthereumProviderServiceOverrides = { + chainService?: Promise + preferenceService?: Promise +} + export const createSigningService = async ( overrides: CreateSigningServiceOverrides = {} ): Promise => { @@ -114,6 +126,27 @@ export const createSigningService = async ( ) } +export const createInternalEthereumProviderService = async ( + overrides: CreateInternalEthereumProviderServiceOverrides = {} +): Promise => { + return InternalEthereumProviderService.create( + overrides.chainService ?? createChainService(), + overrides.preferenceService ?? createPreferenceService() + ) +} + +export const createProviderBridgeService = async ( + overrides: CreateProviderBridgeServiceOverrides = {} +): Promise => { + const preferenceService = + overrides?.preferenceService ?? createPreferenceService() + return ProviderBridgeService.create( + overrides.internalEthereumProviderService ?? + createInternalEthereumProviderService({ preferenceService }), + preferenceService + ) +} + // Copied from a legacy Optimism transaction generated with our test wallet. export const createLegacyTransactionRequest = ( overrides: Partial = {} diff --git a/setupJest.ts b/setupJest.ts index 07f6f0ccc3..df0c5aec3c 100644 --- a/setupJest.ts +++ b/setupJest.ts @@ -33,6 +33,14 @@ Object.defineProperty(browser, "alarms", { }, }) +Object.defineProperty(browser, "windows", { + writable: true, + value: { + getCurrent: () => {}, + create: () => {}, + }, +}) + // Mock top-level logger calls. browser.extension.getBackgroundPage = jest.fn() browser.tabs.getCurrent = jest.fn(() => From 475792a54a5012d123dfe75bdb39815bc3c29d64 Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Wed, 28 Dec 2022 14:36:31 +0100 Subject: [PATCH 013/187] Update description for provider bridge tests --- background/services/provider-bridge/tests/index.unit.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/background/services/provider-bridge/tests/index.unit.test.ts b/background/services/provider-bridge/tests/index.unit.test.ts index 3c0ed92f4a..e2333f848c 100644 --- a/background/services/provider-bridge/tests/index.unit.test.ts +++ b/background/services/provider-bridge/tests/index.unit.test.ts @@ -69,7 +69,7 @@ describe("Provider bridge", () => { expect(response).toEqual([enablingPermission.accountAddress]) }) - it("should call routeSafeRequest user has permission to sign", async () => { + it("should call routeSafeRequest when user has permission to sign", async () => { const { enablingPermission, origin } = BASE_DATA const method = "eth_sendTransaction" const params = PARAMS[method] @@ -85,7 +85,7 @@ describe("Provider bridge", () => { expect(stub.called).toBe(true) }) - it("should not call routeSafeRequest user has not permission to sign", async () => { + it("should not call routeSafeRequest when user has not permission to sign", async () => { const { enablingPermission, origin } = BASE_DATA const method = "eth_sendTransaction" const params = PARAMS[method] From 34eb459ea096f011bca18f1a1d56c78be1251e5f Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Wed, 4 Jan 2023 15:19:40 +0100 Subject: [PATCH 014/187] Handle correctly instances of EIP1193Error --- background/services/provider-bridge/index.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/background/services/provider-bridge/index.ts b/background/services/provider-bridge/index.ts index e9a6db21ef..dd40b69167 100644 --- a/background/services/provider-bridge/index.ts +++ b/background/services/provider-bridge/index.ts @@ -9,6 +9,8 @@ import { RPCRequest, EIP1193_ERROR_CODES, isTallyConfigPayload, + isEIP1193Error, + EIP1193ErrorPayload, } from "@tallyho/provider-bridge-shared" import { TransactionRequest as EthersTransactionRequest } from "@ethersproject/abstract-provider" import BaseService from "../base" @@ -452,8 +454,16 @@ export default class ProviderBridgeService extends BaseService { } } } catch (error) { + logger.log("error processing request", error) if (typeof error === "object" && error !== null) { - logger.log("error processing request", error) + if ( + "eip1193Error" in error && + isEIP1193Error( + (error as { eip1193Error: EIP1193ErrorPayload }).eip1193Error + ) + ) { + return error + } if ("body" in error) { return getRPCErrorResponser(error) } From 0fcf944cb3e92e83ebc7445351752b5731921595 Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Wed, 4 Jan 2023 15:34:56 +0100 Subject: [PATCH 015/187] Fix issue for provider-bridge tests --- background/services/provider-bridge/index.ts | 14 +++++++------- .../provider-bridge/tests/index.unit.test.ts | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/background/services/provider-bridge/index.ts b/background/services/provider-bridge/index.ts index dd40b69167..85a7d483d4 100644 --- a/background/services/provider-bridge/index.ts +++ b/background/services/provider-bridge/index.ts @@ -456,13 +456,13 @@ export default class ProviderBridgeService extends BaseService { } catch (error) { logger.log("error processing request", error) if (typeof error === "object" && error !== null) { - if ( - "eip1193Error" in error && - isEIP1193Error( - (error as { eip1193Error: EIP1193ErrorPayload }).eip1193Error - ) - ) { - return error + if ("eip1193Error" in error) { + const { eip1193Error } = error as { + eip1193Error: EIP1193ErrorPayload + } + if (isEIP1193Error(eip1193Error)) { + return eip1193Error + } } if ("body" in error) { return getRPCErrorResponser(error) diff --git a/background/services/provider-bridge/tests/index.unit.test.ts b/background/services/provider-bridge/tests/index.unit.test.ts index e2333f848c..12c5fb9941 100644 --- a/background/services/provider-bridge/tests/index.unit.test.ts +++ b/background/services/provider-bridge/tests/index.unit.test.ts @@ -99,7 +99,7 @@ describe("Provider bridge", () => { ) expect(stub.called).toBe(false) - expect(response).toBe(EIP1193_ERROR_CODES.userRejectedRequest) + expect(response).toBe(EIP1193_ERROR_CODES.unauthorized) }) }) }) From 9fd7fd6d39dd5189317d93966644c484c6c61e9d Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Thu, 5 Jan 2023 10:17:25 +0100 Subject: [PATCH 016/187] Fix issue with provider bridge tests --- __mocks__/webextension-polyfill.ts | 7 +++++++ .../services/provider-bridge/tests/index.unit.test.ts | 2 ++ setupJest.ts | 8 -------- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/__mocks__/webextension-polyfill.ts b/__mocks__/webextension-polyfill.ts index 479ed2ee9c..a6a6da7521 100644 --- a/__mocks__/webextension-polyfill.ts +++ b/__mocks__/webextension-polyfill.ts @@ -26,6 +26,13 @@ module.exports = { Promise.resolve(undefined as unknown as Tabs.Tab) ), }, + windows: { + writable: true, + value: { + getCurrent: () => {}, + create: () => {}, + }, + }, runtime: { ...browserMock.runtime, setUninstallURL: jest.fn(), diff --git a/background/services/provider-bridge/tests/index.unit.test.ts b/background/services/provider-bridge/tests/index.unit.test.ts index 12c5fb9941..a3fec6a001 100644 --- a/background/services/provider-bridge/tests/index.unit.test.ts +++ b/background/services/provider-bridge/tests/index.unit.test.ts @@ -3,6 +3,7 @@ import { PermissionRequest, } from "@tallyho/provider-bridge-shared" import sinon from "sinon" +import browser from "webextension-polyfill" import { createProviderBridgeService } from "../../../tests/factories" import ProviderBridgeService from "../index" @@ -45,6 +46,7 @@ describe("Provider bridge", () => { beforeEach(async () => { browser.windows.getCurrent = jest.fn(() => Promise.resolve(WINDOW)) + browser.windows.create = jest.fn(() => Promise.resolve(WINDOW)) providerBridgeService = await createProviderBridgeService() await providerBridgeService.startService() sandbox.restore() diff --git a/setupJest.ts b/setupJest.ts index 7e40735938..e47bde1951 100644 --- a/setupJest.ts +++ b/setupJest.ts @@ -20,14 +20,6 @@ Object.defineProperty(window.navigator, "usb", { }, }) -Object.defineProperty(browser, "windows", { - writable: true, - value: { - getCurrent: () => {}, - create: () => {}, - }, -}) - // Prevent Dexie from caching indexedDB global so fake-indexeddb // can reset properly. Object.defineProperty(Dexie.dependencies, "indexedDB", { From 66918acc8b87e881e12faf1726261f71b465cc02 Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Tue, 17 Jan 2023 11:30:04 +0100 Subject: [PATCH 017/187] Updates for ability removal window --- ui/_locales/en/messages.json | 10 +++ ui/pages/Abilities/AbilityRemovalConfirm.tsx | 78 +++++++++++--------- 2 files changed, 52 insertions(+), 36 deletions(-) diff --git a/ui/_locales/en/messages.json b/ui/_locales/en/messages.json index e07d04faf3..b46d5414f7 100644 --- a/ui/_locales/en/messages.json +++ b/ui/_locales/en/messages.json @@ -757,6 +757,16 @@ "toggleTitle": "Use Tally Ho as default wallet" } }, + "abilities": { + "deleteSlideUpMenu": { + "title": "Delete ability?", + "desc": "If you delete an ability you won't be able to see it anymore", + "spamPrompt": "Is this ability a spam?", + "reportSpamBtn": "Yes, report spam", + "submitBtn": "Yes, delete", + "snackbar": "Ability deleted" + } + }, "globalError": { "title": "Ups, nothing to see here", "desc": "Looks like you encountered an empty bowl, try refreshing the page to see if something appears", diff --git a/ui/pages/Abilities/AbilityRemovalConfirm.tsx b/ui/pages/Abilities/AbilityRemovalConfirm.tsx index 4e143d53c6..dfcd031f99 100644 --- a/ui/pages/Abilities/AbilityRemovalConfirm.tsx +++ b/ui/pages/Abilities/AbilityRemovalConfirm.tsx @@ -21,33 +21,37 @@ export default function AbilityRemovalConfirm({ return (
-
Delete ability?
-
- If you delete an ability you won't be able to see it anymore +
+
{t("abilities.deleteSlideUpMenu.title")}
+
+ {t("abilities.deleteSlideUpMenu.desc")} +
+
    +
  • +
  • + {t("abilities.deleteSlideUpMenu.spamPrompt")} +
  • + { + e.stopPropagation() + // @TODO Actually report spam + dispatch( + removeAbility({ + address: ability.address, + abilityId: ability.abilityId, + }) + ) + close() + }} + > + {t("abilities.deleteSlideUpMenu.reportSpamBtn")} + + +
-
    -
  • -
  • Is this ability spam?
  • - { - e.stopPropagation() - // @TODO Actually report spam - dispatch( - removeAbility({ - address: ability.address, - abilityId: ability.abilityId, - }) - ) - close() - }} - > - Yes, report spam - - -
- Yes, delete + {t("abilities.deleteSlideUpMenu.submitBtn")}
From bf9af2fa8f52f48d38007ea49b1a9fa2c5f45d38 Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Wed, 18 Jan 2023 16:04:47 +0100 Subject: [PATCH 018/187] Add notification dot for tab bar element --- ui/components/TabBar/TabBar.tsx | 21 ++++++++++++++++-- ui/components/TabBar/TabBarIconButton.tsx | 26 ++++++++++++++++++++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/ui/components/TabBar/TabBar.tsx b/ui/components/TabBar/TabBar.tsx index 6b90b55caa..7d256f7e44 100644 --- a/ui/components/TabBar/TabBar.tsx +++ b/ui/components/TabBar/TabBar.tsx @@ -1,7 +1,10 @@ -import React, { ReactElement } from "react" +import React, { ReactElement, useCallback } from "react" import { matchPath, useHistory, useLocation } from "react-router-dom" -import { selectCurrentNetwork } from "@tallyho/tally-background/redux-slices/selectors" +import { + selectAbilityCount, + selectCurrentNetwork, +} from "@tallyho/tally-background/redux-slices/selectors" import { NETWORKS_SUPPORTING_SWAPS } from "@tallyho/tally-background/constants/networks" import { EVMNetwork } from "@tallyho/tally-background/networks" import { useTranslation } from "react-i18next" @@ -21,6 +24,7 @@ const isTabSupportedByNetwork = (tab: TabInfo, network: EVMNetwork) => { export default function TabBar(): ReactElement { const location = useLocation() const selectedNetwork = useBackgroundSelector(selectCurrentNetwork) + const abilityCount = useBackgroundSelector(selectAbilityCount) const history = useHistory() const { t } = useTranslation() @@ -33,6 +37,18 @@ export default function TabBar(): ReactElement { matchPath(location.pathname, { path, exact: false }) ) ?? defaultTab + const hasNotifications = useCallback( + (path: string): boolean => { + switch (path) { + case "/portfolio": + return abilityCount > 0 + default: + return false + } + }, + [abilityCount] + ) + return (