From c1580f569222cc1875f7de64a5ab7e3c1d484702 Mon Sep 17 00:00:00 2001 From: Roxane Letourneau Date: Fri, 1 Nov 2024 12:16:12 -0400 Subject: [PATCH 1/2] feat(wallet connect): sign payload using wc in test-dapp Added support to sign payload in the test-dapp using wallet connect, fix getPk and sign in the wallet connect package --- .../src/lib/TestContainer.svelte | 8 + apps/taquito-test-dapp/src/lib/Wallet.svelte | 9 +- apps/taquito-test-dapp/src/tests.ts | 99 ++++-- package.json | 2 +- .../taquito-wallet-connect-2/src/errors.ts | 8 + .../src/taquito-wallet-connect-2.ts | 77 ++--- .../taquito-wallet-connect-2/src/types.ts | 6 + .../test/taquito-wallet-connect-2.spec.ts | 309 +++++++++--------- 8 files changed, 282 insertions(+), 236 deletions(-) diff --git a/apps/taquito-test-dapp/src/lib/TestContainer.svelte b/apps/taquito-test-dapp/src/lib/TestContainer.svelte index 69d46d805c..2896d5bf33 100644 --- a/apps/taquito-test-dapp/src/lib/TestContainer.svelte +++ b/apps/taquito-test-dapp/src/lib/TestContainer.svelte @@ -80,6 +80,14 @@ title: "Confirmations through observable", body: result.confirmationObsOutput, }; + } else if (test.id === "show-public-key") { + testResult = { + id: test.id, + title: "Public Key", + body: { + output: result.output + } + } } } else { error = result.error; diff --git a/apps/taquito-test-dapp/src/lib/Wallet.svelte b/apps/taquito-test-dapp/src/lib/Wallet.svelte index 48983e1803..9806f80ba5 100644 --- a/apps/taquito-test-dapp/src/lib/Wallet.svelte +++ b/apps/taquito-test-dapp/src/lib/Wallet.svelte @@ -55,15 +55,18 @@ }); wallet.signClient.on("session_ping", ({ id, topic }) => { console.log("session_ping in test dapp", id, topic); + store.addEvent("session_ping"); }); wallet.signClient.on("session_delete", ({ topic }) => { - console.log("EVEN: session_delete", topic); + console.log("EVENT: session_delete", topic); + store.addEvent("session_delete"); if (!wallet.isActiveSession()) { resetApp(); } }); wallet.signClient.on("session_update", async ({ topic }) => { - console.log("EVEN: session_update", topic); + console.log("EVENT: session_update", topic); + store.addEvent("session_update"); const allAccounts = wallet.getAccounts(); await updateStore(wallet, allAccounts); }); @@ -75,7 +78,7 @@ permissionScope: { networks: [$store.networkType as NetworkTypeWc2], events: [], - methods: [PermissionScopeMethods.TEZOS_SEND, PermissionScopeMethods.TEZOS_SIGN], + methods: [PermissionScopeMethods.TEZOS_SEND, PermissionScopeMethods.TEZOS_SIGN, PermissionScopeMethods.TEZOS_GET_ACCOUNTS], }, pairingTopic, registryUrl: "https://www.tezos.help/wcdata/" diff --git a/apps/taquito-test-dapp/src/tests.ts b/apps/taquito-test-dapp/src/tests.ts index 1c967493cb..ad4d92513a 100644 --- a/apps/taquito-test-dapp/src/tests.ts +++ b/apps/taquito-test-dapp/src/tests.ts @@ -7,7 +7,7 @@ import { UnitValue } from "@taquito/taquito"; import type { ContractProvider } from "@taquito/taquito"; -import type { BeaconWallet } from "@taquito/beacon-wallet"; +import { BeaconWallet } from "@taquito/beacon-wallet"; import { stringToBytes, verifySignature } from "@taquito/utils"; import { SigningType, type RequestSignPayloadInput } from "@airgap/beacon-types"; import { get } from "svelte/store"; @@ -56,6 +56,16 @@ const sendTezToEtherlink = async ( } }; +const showPublicKey = async (Tezos: TezosToolkit): Promise => { + try { + const publicKey = await Tezos.wallet.pk(); + return { success: true, opHash: "", output: publicKey }; + } catch (error) { + console.log(error); + return { success: false, opHash: "", output: JSON.stringify(error) }; + } +} + const sendTez = async (Tezos: TezosToolkit): Promise => { let opHash = ""; try { @@ -348,19 +358,30 @@ const batchApiContractCallsTest = async ( const signPayload = async ( input: string, - wallet: BeaconWallet + wallet: BeaconWallet | WalletConnect2 ): Promise => { const userAddress = await wallet.getPKH(); const { payload, formattedInput } = preparePayloadToSign(input, userAddress); try { - const signedPayload = await wallet.client.requestSignPayload(payload); - return { - success: true, - opHash: "", - output: signedPayload.signature, - sigDetails: { input, formattedInput, bytes: payload.payload } - }; - } catch (error) { + if (wallet instanceof BeaconWallet) { + const signedPayload = await wallet.client.requestSignPayload(payload); + return { + success: true, + opHash: "", + output: signedPayload.signature, + sigDetails: { input, formattedInput, bytes: payload.payload } + }; + } else { + const signature = await wallet.sign(payload.payload); + return { + success: true, + opHash: "", + output: signature, + sigDetails: { input, formattedInput, bytes: payload.payload } + }; + } + } + catch (error) { console.log(error); return { success: false, opHash: "", output: JSON.stringify(error) }; } @@ -368,29 +389,42 @@ const signPayload = async ( const signPayloadAndSend = async ( input: string, - wallet: BeaconWallet, - contract: ContractAbstraction | ContractAbstraction + wallet: BeaconWallet | WalletConnect2, + contract: ContractAbstraction ): Promise => { if (!input) throw "No input provided"; const userAddress = await wallet.getPKH(); const { payload, formattedInput } = preparePayloadToSign(input, userAddress); try { - const signedPayload = await wallet.client.requestSignPayload(payload); - // gets user's public key - const activeAccount = await wallet.client.getActiveAccount(); - const publicKey = activeAccount.publicKey; - // sends transaction to contract - const op = await contract.methodsObject - .check_signature({ 0: publicKey, 1: signedPayload.signature, 2: payload.payload }) - .send(); - await op.confirmation(); - return { - success: true, - opHash: op.hasOwnProperty("opHash") ? op["opHash"] : op["hash"], - output: signedPayload.signature, - sigDetails: { input, formattedInput, bytes: payload.payload } - }; + if (wallet instanceof BeaconWallet) { + const signedPayload = await wallet.client.requestSignPayload(payload); + // gets user's public key + const activeAccount = await wallet.client.getActiveAccount(); + const publicKey = activeAccount.publicKey; + // sends transaction to contract + const op = await contract.methodsObject + .check_signature({ 0: publicKey, 1: signedPayload.signature, 2: payload.payload }) + .send(); + await op.confirmation(); + return { + success: true, + opHash: op.opHash, + output: signedPayload.signature, + sigDetails: { input, formattedInput, bytes: payload.payload } + }; + } else { + const signature = await wallet.sign(payload.payload); + const publicKey = await wallet.getPK(); + const op = await contract.methodsObject.check_signature({ 0: publicKey, 1: signature, 2: payload.payload }).send(); + await op.confirmation(); + return { + success: true, + opHash: op.opHash, + output: signature, + sigDetails: { input, formattedInput, bytes: payload.payload } + } + } } catch (error) { return { success: false, opHash: "", output: JSON.stringify(error) }; } @@ -656,6 +690,17 @@ export const init = ( contract: ContractAbstraction | ContractAbstraction, wallet: BeaconWallet | undefined ): TestSettings[] => [ + { + id: "show-public-key", + name: "Show public key", + description: "This test shows your public key.", + documentation: '', + keyword: 'publicKey', + run: () => showPublicKey(Tezos), + showExecutionTime: false, + inputRequired: false, + lastResult: { option: "none", val: false } + }, { id: "send-tez", name: "Send tez", diff --git a/package.json b/package.json index 4901540463..a5d877e5f5 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "^@taquito/tzip12$": "/packages/taquito-tzip12/src/taquito-tzip12.ts", "^@taquito/tzip16$": "/packages/taquito-tzip16/src/taquito-tzip16.ts", "^@taquito/utils$": "/packages/taquito-utils/src/taquito-utils.ts", - "^@taquito/wallet-connect-2$": "/packages/taquito-wallet-connect-2/src/wallet-connect-2.ts" + "^@taquito/wallet-connect-2$": "/packages/taquito-wallet-connect-2/src/taquito-wallet-connect-2.ts" }, "coveragePathIgnorePatterns": [ "/node_modules/", diff --git a/packages/taquito-wallet-connect-2/src/errors.ts b/packages/taquito-wallet-connect-2/src/errors.ts index 4ee21a1e80..d307f53940 100644 --- a/packages/taquito-wallet-connect-2/src/errors.ts +++ b/packages/taquito-wallet-connect-2/src/errors.ts @@ -147,3 +147,11 @@ export class InvalidSession extends Error { super(message); } } + +export class PublicKeyRetrievalError extends Error { + name = 'PublicKeyRetrievalError'; + + constructor() { + super(`Unable to retrieve public key`); + } +} diff --git a/packages/taquito-wallet-connect-2/src/taquito-wallet-connect-2.ts b/packages/taquito-wallet-connect-2/src/taquito-wallet-connect-2.ts index e9d12ea4aa..9655fd229a 100644 --- a/packages/taquito-wallet-connect-2/src/taquito-wallet-connect-2.ts +++ b/packages/taquito-wallet-connect-2/src/taquito-wallet-connect-2.ts @@ -35,7 +35,7 @@ import { OperationParams, PermissionScopeMethods, PermissionScopeParam, - SigningType, + TezosAccount, } from './types'; import { ActiveAccountUnspecified, @@ -49,6 +49,7 @@ import { InvalidSessionKey, MissingRequiredScope, NotConnected, + PublicKeyRetrievalError, } from './errors'; export { SignClientTypes, PairingTypes }; @@ -219,7 +220,7 @@ export class WalletConnect2 implements WalletProvider { const network = this.getActiveNetwork(); const account = await this.getPKH(); this.validateNetworkAndAccount(network, account); - const { transactionHash } = await this.signClient.request<{ transactionHash: string }>({ + const { operationHash } = await this.signClient.request<{ operationHash: string }>({ topic: session.topic, chainId: `${TEZOS_PLACEHOLDER}:${network}`, request: { @@ -230,12 +231,14 @@ export class WalletConnect2 implements WalletProvider { }, }, }); - return transactionHash; + return operationHash; } - // TODO need unit and test-dapp test + /** + * @description Once the session is establish, send payload to be signed by the wallet. + * @error MissingRequiredScope is thrown if permission to sign payload was not granted + */ async sign(bytes: string, watermark?: Uint8Array): Promise { - console.log(bytes, watermark); const session = this.getSession(); if (!this.getPermittedMethods().includes(PermissionScopeMethods.TEZOS_SIGN)) { throw new MissingRequiredScope(PermissionScopeMethods.TEZOS_SIGN); @@ -248,52 +251,20 @@ export class WalletConnect2 implements WalletProvider { expression = Array.from(watermark, (byte) => byte.toString(16).padStart(2, '0')).join('') + expression; } - const signature = await this.signClient.request({ + const { signature } = await this.signClient.request<{ signature: string }>({ topic: session.topic, chainId: `${TEZOS_PLACEHOLDER}:${network}`, request: { method: PermissionScopeMethods.TEZOS_SIGN, params: { - expression, - signingType: SigningType.RAW, + account: await this.getPKH(), + payload: expression, }, }, }); return signature; } - /** - * @description Once the session is establish, send payload to be approved and signed by the wallet. - * @error MissingRequiredScope is thrown if permission to sign payload was not granted - */ - async signPayload(params: { - signingType?: SigningType; - payload: string; - sourceAddress?: string; - }) { - const session = this.getSession(); - if (!this.getPermittedMethods().includes(PermissionScopeMethods.TEZOS_SIGN)) { - throw new MissingRequiredScope(PermissionScopeMethods.TEZOS_SIGN); - } - const network = this.getActiveNetwork(); - const account = await this.getPKH(); - this.validateNetworkAndAccount(network, account); - const signature = await this.signClient.request({ - topic: session.topic, - chainId: `${TEZOS_PLACEHOLDER}:${network}`, - request: { - method: PermissionScopeMethods.TEZOS_SIGN, - params: { - account: params.sourceAddress ?? account, - expression: params.payload, - signingType: params.signingType, - }, - }, - }); - console.log(signature); - return signature; - } - /** * @description Return all connected accounts from the active session * @error NotConnected if no active session @@ -331,16 +302,30 @@ export class WalletConnect2 implements WalletProvider { /** * @description Access the public key of the active account * @error ActiveAccountUnspecified thrown when there are multiple Tezos account in the session and none is set as the active one + * @error MissingRequiredScope is thrown if permission to get accounts was not granted + * @error PublicKeyRetrievalError is thrown if the public key is not found */ async getPK() { - if (!this.activeAccount) { - this.getSession(); - throw new ActiveAccountUnspecified(); + const session = this.getSession(); + if (!this.getPermittedMethods().includes(PermissionScopeMethods.TEZOS_GET_ACCOUNTS)) { + throw new MissingRequiredScope(PermissionScopeMethods.TEZOS_GET_ACCOUNTS); } - if (!this.getSession().sessionProperties) { - throw new InvalidSession('Session properties are not defined'); + const network = this.getActiveNetwork(); + const account = await this.getPKH(); + const accounts = await this.signClient.request({ + topic: session.topic, + chainId: `${TEZOS_PLACEHOLDER}:${network}`, + request: { + method: PermissionScopeMethods.TEZOS_GET_ACCOUNTS, + params: {}, + }, + }); + + const selectedAccount = accounts.find((acc) => acc.address === account); + if (!selectedAccount) { + throw new PublicKeyRetrievalError(); } - return this.getSession().sessionProperties!.publicKey; + return selectedAccount.pubkey; } /** diff --git a/packages/taquito-wallet-connect-2/src/types.ts b/packages/taquito-wallet-connect-2/src/types.ts index 2cd435a828..29870c47bd 100644 --- a/packages/taquito-wallet-connect-2/src/types.ts +++ b/packages/taquito-wallet-connect-2/src/types.ts @@ -47,6 +47,12 @@ export enum SigningType { MICHELINE = 'micheline', } +export interface TezosAccount { + algo: string; + address: string; + pubkey: string; +} + type WalletDefinedFields = 'source' | 'gas_limit' | 'storage_limit' | 'fee'; interface WalletOptionalFields { gas_limit?: string; diff --git a/packages/taquito-wallet-connect-2/test/taquito-wallet-connect-2.spec.ts b/packages/taquito-wallet-connect-2/test/taquito-wallet-connect-2.spec.ts index 05486fbb7b..7e0b513835 100644 --- a/packages/taquito-wallet-connect-2/test/taquito-wallet-connect-2.spec.ts +++ b/packages/taquito-wallet-connect-2/test/taquito-wallet-connect-2.spec.ts @@ -296,28 +296,48 @@ describe('Wallet connect 2 tests', () => { }); describe('test public key', () => { - it('should return public key when session namespace only have one', async () => { - mockSignClient.connect.mockReturnValue({ approval: async () => sessionExample }); - - await walletConnect.requestPermissions({ - permissionScope: { - methods: [PermissionScopeMethods.TEZOS_SEND], - networks: [NetworkType.GHOSTNET], + it('should throw an error when no active account is set and session namespace has multiple accounts', async () => { + mockSignClient.connect.mockReturnValue({ + approval: async () => { + return { + ...sessionExample, + namespaces: { + tezos: { + accounts: [ + 'tezos:ghostnet:tz2AJ8DYxeRSUWr8zS5DcFfJYzTSNYzALxSh', + 'tezos:ghostnet:tz2NBV8BNmMfR5VEscEvyT5y8knp37uPMctn', + ], + methods: [PermissionScopeMethods.TEZOS_GET_ACCOUNTS], + events: [], + }, + }, + requiredNamespaces: { + tezos: { + methods: [PermissionScopeMethods.TEZOS_GET_ACCOUNTS], + chains: ['tezos:ghostnet'], + events: [], + }, + }, + }; }, }); - - expect(await walletConnect.getPK()).toEqual( - '01b5630403234ba9745073c9ad081c7b812786b2bcfa8cfe1ff28d800b989f29' - ); - }); - - it('should throw an error when no active account is set and session namespace has multiple accounts', async () => { - mockSignClient.connect.mockReturnValue({ approval: async () => sessionMultipleChains }); - + const mockRequestResponse = [ + { + pubkey: 'sppk7aJNCPedN4NSP3h9mRNTecJMJugFBhrdTxn3jNGpTW22MGFAsvS', + algo: 'secp256k1', + address: 'tz2AJ8DYxeRSUWr8zS5DcFfJYzTSNYzALxSh', + }, + { + pubkey: 'sppk7aJNCgterggr4mRNTecJMJugFBhrdTxn3jNGpTW22MGFAsvS', + algo: 'secp256k1', + address: 'tz2NBV8BNmMfR5VEscEvyT5y8knp37uPMctn', + }, + ]; + mockSignClient.request.mockResolvedValue(mockRequestResponse); await walletConnect.requestPermissions({ permissionScope: { - methods: [PermissionScopeMethods.TEZOS_SIGN], - networks: [NetworkType.GHOSTNET, NetworkType.PARISNET], + methods: [PermissionScopeMethods.TEZOS_GET_ACCOUNTS], + networks: [NetworkType.GHOSTNET], }, }); @@ -326,49 +346,115 @@ describe('Wallet connect 2 tests', () => { ); }); - it('should set the active account successfully', async () => { - mockSignClient.connect.mockReturnValue({ approval: async () => sessionMultipleChains }); - + it('should return public key of active account when session namespace has multiple accounts', async () => { + mockSignClient.connect.mockReturnValue({ + approval: async () => { + return { + ...sessionExample, + namespaces: { + tezos: { + accounts: [ + 'tezos:ghostnet:tz2AJ8DYxeRSUWr8zS5DcFfJYzTSNYzALxSh', + 'tezos:ghostnet:tz2NBV8BNmMfR5VEscEvyT5y8knp37uPMctn', + ], + methods: [PermissionScopeMethods.TEZOS_GET_ACCOUNTS], + events: [], + }, + }, + requiredNamespaces: { + tezos: { + methods: [PermissionScopeMethods.TEZOS_GET_ACCOUNTS], + chains: ['tezos:ghostnet'], + events: [], + }, + }, + }; + }, + }); + const mockRequestResponse = [ + { + pubkey: 'sppk7aJNCPedN4NSP3h9mRNTecJMJugFBhrdTxn3jNGpTW22MGFAsvS', + algo: 'secp256k1', + address: 'tz2AJ8DYxeRSUWr8zS5DcFfJYzTSNYzALxSh', + }, + { + pubkey: 'sppk7aJNCgterggr4mRNTecJMJugFBhrdTxn3jNGpTW22MGFAsvS', + algo: 'secp256k1', + address: 'tz2NBV8BNmMfR5VEscEvyT5y8knp37uPMctn', + }, + ]; + mockSignClient.request.mockResolvedValue(mockRequestResponse); await walletConnect.requestPermissions({ permissionScope: { - methods: [PermissionScopeMethods.TEZOS_SIGN], - networks: [NetworkType.GHOSTNET, NetworkType.PARISNET], + methods: [PermissionScopeMethods.TEZOS_GET_ACCOUNTS], + networks: [NetworkType.GHOSTNET], }, }); - walletConnect.setActiveAccount('tz2AJ8DYxeRSUWr8zS5DcFfJYzTSNYzALxSh'); - expect(await walletConnect.getPK()).toEqual( - '72dfcd018c5a636c311a0214c19f24e1e52a0f38082e31e3971af9b0296f4767' - ); - }); + walletConnect.setActiveAccount('tz2NBV8BNmMfR5VEscEvyT5y8knp37uPMctn'); - it('should fail to set the active account when it is not part of the session namespace', async () => { - mockSignClient.connect.mockReturnValue({ approval: async () => sessionMultipleChains }); + const pk = await walletConnect.getPK(); - await walletConnect.requestPermissions({ - permissionScope: { - methods: [PermissionScopeMethods.TEZOS_SIGN], - networks: [NetworkType.GHOSTNET, NetworkType.PARISNET], + expect(mockSignClient.request).toHaveBeenCalledWith({ + topic: sessionExample.topic, + chainId: `tezos:ghostnet`, + request: { + method: PermissionScopeMethods.TEZOS_GET_ACCOUNTS, + params: {}, }, }); - expect(() => walletConnect.setActiveAccount('test')).toThrow('Invalid pkh "test"'); + expect(pk).toEqual('sppk7aJNCgterggr4mRNTecJMJugFBhrdTxn3jNGpTW22MGFAsvS'); }); - it('should delete active account when calling disconnect', async () => { + it('should throw an error if no accounts match the active account', async () => { + mockSignClient.connect.mockReturnValue({ + approval: async () => { + return { + ...sessionExample, + namespaces: { + tezos: { + accounts: [ + 'tezos:ghostnet:tz2AJ8DYxeRSUWr8zS5DcFfJYzTSNYzALxSh', + 'tezos:ghostnet:tz2NBV8BNmMfR5VEscEvyT5y8knp37uPMctn', + ], + methods: [PermissionScopeMethods.TEZOS_GET_ACCOUNTS], + events: [], + }, + }, + requiredNamespaces: { + tezos: { + methods: [PermissionScopeMethods.TEZOS_GET_ACCOUNTS], + chains: ['tezos:ghostnet'], + events: [], + }, + }, + }; + }, + }); + const mockRequestResponse = [ + { + pubkey: 'sppk7aJNCPedN4NSP3h9mRNTecJMJugFBhrdTxn3jNGpTW22MGFAsvS', + algo: 'secp256k1', + address: 'tz2AJ8DYxeRSUWr8zS5DcFfJYzTSNYzALxSh', + }, + { + pubkey: 'sppk7aJNCgterggr4mRNTecJMJugFBhrdTxn3jNGpTW22MGFAsvS', + algo: 'secp256k1', + address: 'tz2BxqkU3UvZrqA22vbEaSGyjR9bEQwc4k2G', + }, + ]; + mockSignClient.request.mockResolvedValue(mockRequestResponse); await walletConnect.requestPermissions({ permissionScope: { - methods: [PermissionScopeMethods.TEZOS_SEND], + methods: [PermissionScopeMethods.TEZOS_GET_ACCOUNTS], networks: [NetworkType.GHOSTNET], }, }); - expect(await walletConnect.getPK()).toEqual( - '01b5630403234ba9745073c9ad081c7b812786b2bcfa8cfe1ff28d800b989f29' - ); - await walletConnect.disconnect(); - expect(mockSignClient.disconnect).toHaveBeenCalledTimes(1); - await expect(walletConnect.getPK()).rejects.toThrow('Not connected, no active session'); + walletConnect.setActiveAccount('tz2NBV8BNmMfR5VEscEvyT5y8knp37uPMctn'); + + await expect(walletConnect.getPK()).rejects.toThrow('Unable to retrieve public key'); }); }); @@ -746,7 +832,7 @@ describe('Wallet connect 2 tests', () => { describe('test sendOperations', () => { it('should send transaction operation successfully', async () => { const mockRequestResponse = { - transactionHash: 'onoNdgS5qcpuxyQVUEerSGCZQdyA3aGbC3nKoQmHJGic5AH9kQf', + operationHash: 'onoNdgS5qcpuxyQVUEerSGCZQdyA3aGbC3nKoQmHJGic5AH9kQf', }; mockSignClient.request.mockResolvedValue(mockRequestResponse); await walletConnect.requestPermissions({ @@ -778,12 +864,12 @@ describe('Wallet connect 2 tests', () => { }, }); - expect(opHash).toEqual(mockRequestResponse.transactionHash); + expect(opHash).toEqual(mockRequestResponse.operationHash); }); it('should send transaction operation with defined limits successfully', async () => { const mockRequestResponse = { - transactionHash: 'onoNdgS5qcpuxyQVUEerSGCZQdyA3aGbC3nKoQmHJGic5AH9kQf', + operationHash: 'onoNdgS5qcpuxyQVUEerSGCZQdyA3aGbC3nKoQmHJGic5AH9kQf', }; mockSignClient.request.mockResolvedValue(mockRequestResponse); await walletConnect.requestPermissions({ @@ -824,7 +910,7 @@ describe('Wallet connect 2 tests', () => { }, }); - expect(opHash).toEqual(mockRequestResponse.transactionHash); + expect(opHash).toEqual(mockRequestResponse.operationHash); }); it('should fail to send transaction operation if permission is not granted', async () => { @@ -920,7 +1006,7 @@ describe('Wallet connect 2 tests', () => { }); }); - describe('test sign payload', () => { + describe('test sign', () => { it('should sign payload successfully', async () => { mockSignClient.connect.mockReturnValue({ approval: async () => { @@ -944,66 +1030,10 @@ describe('Wallet connect 2 tests', () => { }, }); - const mockedSignature = - 'edsigtpDN7L5LfzvYWM22fvYA4dPVr9wXaYje7z4nmBrT6ZxkGnHS6u3UuvD9TQv3BmNRSUgnMH1dKsAaLWhfuXXj63myo2m3De'; - mockSignClient.request.mockResolvedValue(mockedSignature); - await walletConnect.requestPermissions({ - permissionScope: { - methods: [PermissionScopeMethods.TEZOS_SIGN], - networks: [NetworkType.GHOSTNET], - }, - }); - - const params = { - signingType: SigningType.MICHELINE, - payload: - '05010031363454657a6f73205369676e6564204d6573736167653a207461717569746f2d746573742d646170702e6e65746c6966792e6170702f20323032322d31322d31335432333a30353a30372e3938375a2074657374', - sourceAddress: 'tz1hWt34L3dnwrpBeG9RWJPQVTgTTAmH1b1p', + const mockedSignature = { + signature: + 'edsigtpDN7L5LfzvYWM22fvYA4dPVr9wXaYje7z4nmBrT6ZxkGnHS6u3UuvD9TQv3BmNRSUgnMH1dKsAaLWhfuXXj63myo2m3De', }; - - const sig = await walletConnect.signPayload(params); - - expect(mockSignClient.request).toHaveBeenCalledWith({ - topic: sessionExample.topic, - chainId: `tezos:ghostnet`, - request: { - method: PermissionScopeMethods.TEZOS_SIGN, - params: { - account: 'tz1hWt34L3dnwrpBeG9RWJPQVTgTTAmH1b1p', - expression: params.payload, - signingType: params.signingType, - }, - }, - }); - - expect(sig).toEqual(mockedSignature); - }); - - it('should sign payload successfully when params.sourceAddress is undefined', async () => { - mockSignClient.connect.mockReturnValue({ - approval: async () => { - return { - ...sessionExample, - namespaces: { - tezos: { - accounts: ['tezos:ghostnet:tz1hWt34L3dnwrpBeG9RWJPQVTgTTAmH1b1p'], - methods: [PermissionScopeMethods.TEZOS_SIGN], - events: [], - }, - }, - requiredNamespaces: { - tezos: { - methods: [PermissionScopeMethods.TEZOS_SIGN], - chains: ['tezos:ghostnet'], - events: [], - }, - }, - }; - }, - }); - - const mockedSignature = - 'edsigtpDN7L5LfzvYWM22fvYA4dPVr9wXaYje7z4nmBrT6ZxkGnHS6u3UuvD9TQv3BmNRSUgnMH1dKsAaLWhfuXXj63myo2m3De'; mockSignClient.request.mockResolvedValue(mockedSignature); await walletConnect.requestPermissions({ permissionScope: { @@ -1018,7 +1048,7 @@ describe('Wallet connect 2 tests', () => { '05010031363454657a6f73205369676e6564204d6573736167653a207461717569746f2d746573742d646170702e6e65746c6966792e6170702f20323032322d31322d31335432333a30353a30372e3938375a2074657374', }; - const sig = await walletConnect.signPayload(params); + const sig = await walletConnect.sign(params.payload); expect(mockSignClient.request).toHaveBeenCalledWith({ topic: sessionExample.topic, @@ -1026,14 +1056,13 @@ describe('Wallet connect 2 tests', () => { request: { method: PermissionScopeMethods.TEZOS_SIGN, params: { - account: 'tz1hWt34L3dnwrpBeG9RWJPQVTgTTAmH1b1p', - expression: params.payload, - signingType: params.signingType, + account: 'tz2AJ8DYxeRSUWr8zS5DcFfJYzTSNYzALxSh', + payload: params.payload, }, }, }); - expect(sig).toEqual(mockedSignature); + expect(sig).toEqual(mockedSignature.signature); }); it('should fail to sign payload if permission is not granted', async () => { @@ -1072,14 +1101,12 @@ describe('Wallet connect 2 tests', () => { '05010031363454657a6f73205369676e6564204d6573736167653a207461717569746f2d746573742d646170702e6e65746c6966792e6170702f20323032322d31322d31335432333a30353a30372e3938375a2074657374', }; - await expect(walletConnect.signPayload(params)).rejects.toThrow( + await expect(walletConnect.sign(params.payload)).rejects.toThrow( 'Required permission scope were not granted for "tezos_sign"' ); }); - }); - describe('test sign', () => { - it('should sign successfully', async () => { + it('should sign successfully and add watermark', async () => { mockSignClient.connect.mockReturnValue({ approval: async () => { return { @@ -1102,8 +1129,10 @@ describe('Wallet connect 2 tests', () => { }, }); - const mockedSignature = - 'edsigtpDN7L5LfzvYWM22fvYA4dPVr9wXaYje7z4nmBrT6ZxkGnHS6u3UuvD9TQv3BmNRSUgnMH1dKsAaLWhfuXXj63myo2m3De'; + const mockedSignature = { + signature: + 'edsigtpDN7L5LfzvYWM22fvYA4dPVr9wXaYje7z4nmBrT6ZxkGnHS6u3UuvD9TQv3BmNRSUgnMH1dKsAaLWhfuXXj63myo2m3De', + }; mockSignClient.request.mockResolvedValue(mockedSignature); await walletConnect.requestPermissions({ permissionScope: { @@ -1123,52 +1152,14 @@ describe('Wallet connect 2 tests', () => { request: { method: PermissionScopeMethods.TEZOS_SIGN, params: { - expression: + payload: '05010031363454657a6f73205369676e6564204d6573736167653a207461717569746f2d746573742d646170702e6e65746c6966792e6170702f20323032322d31322d31335432333a30353a30372e3938375a2074657374', - signingType: SigningType.RAW, + account: 'tz2AJ8DYxeRSUWr8zS5DcFfJYzTSNYzALxSh', }, }, }); - expect(sig).toEqual(mockedSignature); - }); - - it('should fail to sign payload if permission is not granted', async () => { - mockSignClient.connect.mockReturnValue({ - approval: async () => { - return { - ...sessionExample, - namespaces: { - tezos: { - accounts: ['tezos:ghostnet:tz2AJ8DYxeRSUWr8zS5DcFfJYzTSNYzALxSh'], - methods: [PermissionScopeMethods.TEZOS_SEND], - events: [], - }, - }, - requiredNamespaces: { - tezos: { - methods: [PermissionScopeMethods.TEZOS_SEND], - chains: ['tezos:ghostnet'], - events: [], - }, - }, - }; - }, - }); - - await walletConnect.requestPermissions({ - permissionScope: { - methods: [PermissionScopeMethods.TEZOS_SEND], - networks: [NetworkType.GHOSTNET], - }, - }); - - await expect( - walletConnect.sign( - '010031363454657a6f73205369676e6564204d6573736167653a207461717569746f2d746573742d646170702e6e65746c6966792e6170702f20323032322d31322d31335432333a30353a30372e3938375a2074657374', - new Uint8Array([5]) - ) - ).rejects.toThrow('Required permission scope were not granted for "tezos_sign"'); + expect(sig).toEqual(mockedSignature.signature); }); }); From ced7349f0448d2861bc538d96d1fe703cc098c2a Mon Sep 17 00:00:00 2001 From: Roxane Letourneau Date: Tue, 5 Nov 2024 08:52:58 -0500 Subject: [PATCH 2/2] Updated docs --- apps/taquito-test-dapp/src/lib/Wallet.svelte | 3 +- docs/wallet_connect_2.md | 14 ++--- packages/taquito-wallet-connect-2/README.md | 58 +++++++++++++++++++ .../src/taquito-wallet-connect-2.ts | 8 ++- 4 files changed, 73 insertions(+), 10 deletions(-) create mode 100644 packages/taquito-wallet-connect-2/README.md diff --git a/apps/taquito-test-dapp/src/lib/Wallet.svelte b/apps/taquito-test-dapp/src/lib/Wallet.svelte index 9806f80ba5..bff2c1b65e 100644 --- a/apps/taquito-test-dapp/src/lib/Wallet.svelte +++ b/apps/taquito-test-dapp/src/lib/Wallet.svelte @@ -80,8 +80,7 @@ events: [], methods: [PermissionScopeMethods.TEZOS_SEND, PermissionScopeMethods.TEZOS_SIGN, PermissionScopeMethods.TEZOS_GET_ACCOUNTS], }, - pairingTopic, - registryUrl: "https://www.tezos.help/wcdata/" + pairingTopic }); const allAccounts = wallet.getAccounts(); await updateStore(wallet, allAccounts); diff --git a/docs/wallet_connect_2.md b/docs/wallet_connect_2.md index fba5d0b377..10d8c069f4 100644 --- a/docs/wallet_connect_2.md +++ b/docs/wallet_connect_2.md @@ -13,7 +13,7 @@ The first step is to instantiate `WalletConnect2` by passing your dapp details a import { WalletConnect2 } from "@taquito/wallet-connect-2"; const walletConnect = await WalletConnect2.init({ - projectId: "YOUR_PROJECT_ID", // Your Project ID gives you access to WalletConnect Cloud + projectId: "YOUR_PROJECT_ID", // Your Project ID gives you access to Reown Cloud metadata: { name: "YOUR_DAPP_NAME", description: "YOUR_DAPP_DESCRIPTION", @@ -22,7 +22,7 @@ const walletConnect = await WalletConnect2.init({ }, }); ``` -`YOUR_PROJECT_ID` can be obtained from [WalletConnect Cloud](https://cloud.walletconnect.com/sign-in) +`YOUR_PROJECT_ID` can be obtained from [Reown Cloud](https://cloud.reown.com) The second step is to establish a connection to a wallet using the `requestPermissions` method: @@ -33,10 +33,11 @@ await walletConnect.requestPermissions({ permissionScope: { networks: [NetworkType.GHOSTNET], methods: [ - PermissionScopeMethods.TEZOS_SEND, PermissionScopeMethods.TEZOS_SIGN + PermissionScopeMethods.TEZOS_SEND, + PermissionScopeMethods.TEZOS_SIGN, + PermissionScopeMethods.TEZOS_GET_ACCOUNTS ], - }, - // registryUrl: "https://www.tezos.help/wcdata/" + } }); ``` @@ -80,7 +81,6 @@ WalletConnect2.init({ networks: [NetworkType.GHOSTNET], methods: [PermissionScopeMethods.TEZOS_SEND], }, - // registryUrl: 'https://www.tezos.help/wcdata/', }) .then(() => { Tezos.setWalletProvider(walletConnect); @@ -99,7 +99,7 @@ WalletConnect2.init({ ## Sign payload with `WalletConnect2` -The `signPayload` method of `WalletConnect2` can be called to sign a payload. The response will be a string representing the signature. The permission `PermissionScopeMethods.TEZOS_SIGN` must have been granted, or the error `MissingRequiredScope` will be thrown. +The `sign` method of `WalletConnect2` can be called to sign a payload. The response will be a string representing the signature. The permission `PermissionScopeMethods.TEZOS_SIGN` must have been granted, or the error `MissingRequiredScope` will be thrown. ## Events handling diff --git a/packages/taquito-wallet-connect-2/README.md b/packages/taquito-wallet-connect-2/README.md new file mode 100644 index 0000000000..a1f809ee1c --- /dev/null +++ b/packages/taquito-wallet-connect-2/README.md @@ -0,0 +1,58 @@ +# Taquito Wallet Connect 2 / Reown package + +_Documentation can be found [here](https://taquito.io/docs/wallet_connect_2)_ + +## General Information + +`@taquito/wallet-connect-2` is an npm package that provides developers a way to connect a dapp built with Taquito to a wallet giving the freedom to the users of the dapp to choose the wallet via the WalletConnect/Reown protocol. The `WalletConnect2` class implements the `WalletProvider` interface, providing an alternative to `BeaconWallet`. +Note: Currently, a QR code is displayed to establish a connection with a wallet. As more Tezos wallets integrate with WalletConnect, we plan showing a list of available wallets alongside the QR code. + +## Install + +Install the package as follows + +``` +npm install @taquito/wallet-connect-2 +``` + +## Usage + +Create a wallet instance with defined option parameters and set the wallet provider using `setWalletProvider` to the `TezosToolkit` instance + +```ts +import { TezosToolkit } from '@taquito/taquito'; +import { WalletConnect2 } from '@taquito/wallet-connect-2'; + +const wallet = await WalletConnect2.init({ + projectId: "861613623da99d7285aaad8279a87ee9", // Your Project ID gives you access to WalletConnect Cloud. + metadata: { + name: "Taquito Test Dapp", + description: "Test Taquito with WalletConnect2", + icons: [], + url: "", + }, +}); + +await wallet.requestPermissions({ + permissionScope: { + networks: [NetworkType.GHOSTNET], + events: [], + methods: [ + PermissionScopeMethods.TEZOS_SEND, + PermissionScopeMethods.TEZOS_SIGN, + PermissionScopeMethods.TEZOS_GET_ACCOUNTS + ], + } +}); + +const Tezos = new TezosToolkit('https://YOUR_PREFERRED_RPC_URL'); +Tezos.setWalletProvider(wallet); +``` + +## Additional Info + +See the top-level [https://github.com/ecadlabs/taquito](https://github.com/ecadlabs/taquito) file for details on reporting issues, contributing and versioning. + +## Disclaimer + +THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/taquito-wallet-connect-2/src/taquito-wallet-connect-2.ts b/packages/taquito-wallet-connect-2/src/taquito-wallet-connect-2.ts index 9655fd229a..7a7e051dd2 100644 --- a/packages/taquito-wallet-connect-2/src/taquito-wallet-connect-2.ts +++ b/packages/taquito-wallet-connect-2/src/taquito-wallet-connect-2.ts @@ -58,6 +58,13 @@ export * from './types'; const TEZOS_PLACEHOLDER = 'tezos'; +/** + * @description The `WalletConnect2` class implements the `WalletProvider` interface, providing an alternative to `BeaconWallet`. + * This package enables dapps built with Taquito to connect to wallets via the WalletConnect/Reown protocol. + * + * @note Currently, a QR code is displayed to establish a connection with a wallet. As more Tezos wallets integrate with WalletConnect, + * we plan showing a list of available wallets alongside the QR code. + */ export class WalletConnect2 implements WalletProvider { public signClient: Client; private session: SessionTypes.Struct | undefined; @@ -298,7 +305,6 @@ export class WalletConnect2 implements WalletProvider { return this.activeAccount; } - // TODO need test-dapp test /** * @description Access the public key of the active account * @error ActiveAccountUnspecified thrown when there are multiple Tezos account in the session and none is set as the active one