diff --git a/examples/react-apps/src/pages/metamask-connection/__tests__/connect-wallet.test.tsx b/examples/react-apps/src/pages/metamask-connection/__tests__/connect-wallet.test.tsx index 171e80d..8e61815 100644 --- a/examples/react-apps/src/pages/metamask-connection/__tests__/connect-wallet.test.tsx +++ b/examples/react-apps/src/pages/metamask-connection/__tests__/connect-wallet.test.tsx @@ -2,13 +2,15 @@ import { act, render, screen, + waitFor, waitForElementToBeRemoved, } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { generateTestingUtils } from "eth-testing"; +import { ethers } from "ethers"; import WalletConnection from ".."; -describe("Connect wallet", () => { +describe("Connect wallet [Metamask]", () => { let originalEthereum: any; const testingUtils = generateTestingUtils({ providerType: "MetaMask", @@ -113,4 +115,35 @@ describe("Connect wallet", () => { await screen.findByText(/chain id: 0x3/i); expect(screen.getByText(/balance: 5.00/i)).toBeInTheDocument(); }); + test("User should be able to sign a transaction", async () => { + const bobsWallet = ethers.Wallet.createRandom(); + // Start with a connected bobsWallet + testingUtils.mockConnectedWallet([bobsWallet.address]); + // mock out a function to call when we request personal sign + let signedMessages: string[] = []; + testingUtils.lowLevel.mockRequest("personal_sign", async (params: any) => { + let statement = (params as string[])[0]; + const signedMessage = await bobsWallet.signMessage(statement); + signedMessages.push(signedMessage); + return signedMessage; + }); + // render the wallet + render(); + // connect the wallet + const connectButton = screen.getByRole("button", { name: /connect/i }); + userEvent.click(connectButton); + await screen.findByText(`account: ${bobsWallet.address}`, { exact: false }); + // get the div for the signature result + const signatureResult = screen.getByTestId("signatureResult"); + expect(signatureResult).toHaveTextContent(""); + // sign the transaction + const signButton = screen.getByRole("button", { name: /sign/i }); + userEvent.click(signButton); + // wait for the result to appear + await waitFor(() => { + expect(signatureResult).not.toHaveTextContent(""); + }); + // expect it to be the same as the signed message + expect(signatureResult).toHaveTextContent(signedMessages[0]); + }); }); diff --git a/examples/react-apps/src/pages/metamask-connection/wallet.tsx b/examples/react-apps/src/pages/metamask-connection/wallet.tsx index f41f078..bb90a21 100644 --- a/examples/react-apps/src/pages/metamask-connection/wallet.tsx +++ b/examples/react-apps/src/pages/metamask-connection/wallet.tsx @@ -149,12 +149,24 @@ function useWallet() { function Wallet() { const { account, chainId, balance } = useWallet(); + const [signatureResult, setSignatureResult] = React.useState(""); + const personalSign = React.useCallback(async () => { + const ethereum = window.ethereum; + const message = "Hello, world!"; + const result = await ethereum.request({ + method: "personal_sign", + params: [message, account, ""], + }); + setSignatureResult(result); + }, [account]); return (
Account: {account}
Chain ID: {chainId}
Balance: {(Number(balance) / 10 ** 18).toFixed(2)}
+
{signatureResult}
+
); } diff --git a/examples/react-apps/src/pages/wallet-connect-connection/__test__/connect-wallet.test.tsx b/examples/react-apps/src/pages/wallet-connect-connection/__test__/connect-wallet.test.tsx index 70da182..6727eed 100644 --- a/examples/react-apps/src/pages/wallet-connect-connection/__test__/connect-wallet.test.tsx +++ b/examples/react-apps/src/pages/wallet-connect-connection/__test__/connect-wallet.test.tsx @@ -1,108 +1,144 @@ import { - act, - render, - screen, - waitForElementToBeRemoved, - } from "@testing-library/react"; - import userEvent from "@testing-library/user-event"; - import { generateTestingUtils } from "eth-testing"; - import WalletConnection from ".."; - import * as walletConnectProvider from '../provider'; - - describe("Connect wallet", () => { - const testingUtils = generateTestingUtils({ - providerType: "WalletConnect", + act, + render, + screen, + waitFor, + waitForElementToBeRemoved, +} from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { generateTestingUtils } from "eth-testing"; +import { ethers } from "ethers"; +import WalletConnection from ".."; +import * as walletConnectProvider from "../provider"; + +describe("Connect wallet [Wallet Connect]", () => { + const testingUtils = generateTestingUtils({ + providerType: "WalletConnect", + }); + + beforeEach(() => { + jest + .spyOn(walletConnectProvider, "getWalletConnectProvider") + .mockReturnValue(testingUtils.getProvider() as unknown as any); + }); + + afterEach(() => { + testingUtils.clearAllMocks(); + }); + + test("User should be able to connect using Wallet Connect", async () => { + // Start with not connected wallet + testingUtils.mockNotConnectedWallet(); + + testingUtils.mockAccounts(["0xf61B443A155b07D2b2cAeA2d99715dC84E839EEf"]); + testingUtils.mockChainId(1); + testingUtils.mockBlockNumber(1); + testingUtils.mockBalance( + "0xf61B443A155b07D2b2cAeA2d99715dC84E839EEf", + "0x1bc16d674ec80000" + ); + + render(); + + const connectButton = screen.getByRole("button", { name: /connect/i }); + userEvent.click(connectButton); + + await waitForElementToBeRemoved(connectButton); + + // Wait for sync + await screen.findByText( + /account: 0xf61B443A155b07D2b2cAeA2d99715dC84E839EEf/i + ); + // Rest of the state should be present + expect(screen.getByText(/chain id: 0x1/i)).toBeInTheDocument(); + expect(screen.getByText(/balance: 2.00/i)).toBeInTheDocument(); + }); + + test("User should be able to see a changed account or network", async () => { + // Start with a connected wallet + testingUtils.mockConnectedWallet( + ["0xf61B443A155b07D2b2cAeA2d99715dC84E839EEf"], + { + balance: "0x1bc16d674ec80000", + } + ); + + render(); + + const connectButton = screen.getByRole("button", { name: /connect/i }); + userEvent.click(connectButton); + + await waitForElementToBeRemoved(connectButton); + + // Wait for sync + await screen.findByText( + /account: 0xf61B443A155b07D2b2cAeA2d99715dC84E839EEf/i + ); + + // Mock the balance of the new account + testingUtils.mockBalance( + "0x138071e4e810f34265bd833be9c5dd96f01bd8a5", + "0xde0b6b3a7640000" + ); + + // Simulate a change of account + act(() => { + testingUtils.mockAccountsChanged([ + "0x138071e4e810f34265bd833be9c5dd96f01bd8a5", + ]); }); - - beforeEach(() => { - jest.spyOn(walletConnectProvider, "getWalletConnectProvider").mockReturnValue(testingUtils.getProvider() as unknown as any) - }) - - afterEach(() => { - testingUtils.clearAllMocks(); + + // Wait for sync + await screen.findByText( + /account: 0x138071e4e810f34265bd833be9c5dd96f01bd8a5/i + ); + expect(screen.getByText(/balance: 1.00/i)).toBeInTheDocument(); + + // Mock account balance on the new chain + testingUtils.mockBalance( + "0x138071e4e810f34265bd833be9c5dd96f01bd8a5", + "0x4563918244f40000" + ); + + // Simulate a change of chain + act(() => { + testingUtils.mockChainChanged("0x3"); }); - - test("User should be able to connect using Wallet Connect", async () => { - // Start with not connected wallet - testingUtils.mockNotConnectedWallet(); - - testingUtils.mockAccounts( - ["0xf61B443A155b07D2b2cAeA2d99715dC84E839EEf"] - ); - testingUtils.mockChainId(1); - testingUtils.mockBlockNumber(1); - testingUtils.mockBalance("0xf61B443A155b07D2b2cAeA2d99715dC84E839EEf", "0x1bc16d674ec80000"); - - render(); - - const connectButton = screen.getByRole("button", { name: /connect/i }); - userEvent.click(connectButton); - - await waitForElementToBeRemoved(connectButton); - - // Wait for sync - await screen.findByText( - /account: 0xf61B443A155b07D2b2cAeA2d99715dC84E839EEf/i - ); - // Rest of the state should be present - expect(screen.getByText(/chain id: 0x1/i)).toBeInTheDocument(); - expect(screen.getByText(/balance: 2.00/i)).toBeInTheDocument(); + + // Wait for sync + await screen.findByText(/chain id: 0x3/i); + expect(screen.getByText(/balance: 5.00/i)).toBeInTheDocument(); + }); + + test("User should be able to sign a transaction", async () => { + const bobsWallet = ethers.Wallet.createRandom(); + // Start with a connected bobsWallet + testingUtils.mockConnectedWallet([bobsWallet.address]); + // mock out a function to call when we request personal sign + let signedMessages: string[] = []; + testingUtils.lowLevel.mockRequest("personal_sign", async (params: any) => { + let statement = (params as string[])[0]; + const signedMessage = await bobsWallet.signMessage(statement); + signedMessages.push(signedMessage); + return signedMessage; }); - - test("User should be able to see a changed account or network", async () => { - // Start with a connected wallet - testingUtils.mockConnectedWallet( - ["0xf61B443A155b07D2b2cAeA2d99715dC84E839EEf"], - { - balance: "0x1bc16d674ec80000", - } - ); - - render(); - - const connectButton = screen.getByRole("button", { name: /connect/i }); - userEvent.click(connectButton); - - await waitForElementToBeRemoved(connectButton); - - // Wait for sync - await screen.findByText( - /account: 0xf61B443A155b07D2b2cAeA2d99715dC84E839EEf/i - ); - - // Mock the balance of the new account - testingUtils.mockBalance( - "0x138071e4e810f34265bd833be9c5dd96f01bd8a5", - "0xde0b6b3a7640000" - ); - - // Simulate a change of account - act(() => { - testingUtils.mockAccountsChanged([ - "0x138071e4e810f34265bd833be9c5dd96f01bd8a5", - ]); - }); - - // Wait for sync - await screen.findByText( - /account: 0x138071e4e810f34265bd833be9c5dd96f01bd8a5/i - ); - expect(screen.getByText(/balance: 1.00/i)).toBeInTheDocument(); - - // Mock account balance on the new chain - testingUtils.mockBalance( - "0x138071e4e810f34265bd833be9c5dd96f01bd8a5", - "0x4563918244f40000" - ); - - // Simulate a change of chain - act(() => { - testingUtils.mockChainChanged("0x3"); - }); - - // Wait for sync - await screen.findByText(/chain id: 0x3/i); - expect(screen.getByText(/balance: 5.00/i)).toBeInTheDocument(); + // render the wallet + render(); + // connect the wallet + const connectButton = screen.getByRole("button", { name: /connect/i }); + userEvent.click(connectButton); + await screen.findByText(`account: ${bobsWallet.address}`, { exact: false }); + // get the div for the signature result + const signatureResult = screen.getByTestId("signatureResult"); + expect(signatureResult).toHaveTextContent(""); + // sign the transaction + const signButton = screen.getByRole("button", { name: /sign/i }); + userEvent.click(signButton); + // wait for the result to appear + await waitFor(() => { + expect(signatureResult).not.toHaveTextContent(""); }); + // expect it to be the same as the signed message + expect(signatureResult).toHaveTextContent(signedMessages[0]); }); - \ No newline at end of file +}); diff --git a/examples/react-apps/src/pages/wallet-connect-connection/wallet.tsx b/examples/react-apps/src/pages/wallet-connect-connection/wallet.tsx index d56b3e8..babe186 100644 --- a/examples/react-apps/src/pages/wallet-connect-connection/wallet.tsx +++ b/examples/react-apps/src/pages/wallet-connect-connection/wallet.tsx @@ -150,12 +150,25 @@ function useWallet() { function Wallet() { const { account, chainId, balance } = useWallet(); + const [signatureResult, setSignatureResult] = React.useState(""); + + const personalSign = React.useCallback(async () => { + const provider = getWalletConnectProvider(); + const message = "Hello, world!"; + const result = await provider.request({ + method: "personal_sign", + params: [message, account, ""], + }); + setSignatureResult(result); + }, [account]); return (
Account: {account}
Chain ID: {chainId}
Balance: {(Number(balance) / 10 ** 18).toFixed(2)}
+
{signatureResult}
+
); } diff --git a/src/mock-manager.ts b/src/mock-manager.ts index 4769843..d500117 100644 --- a/src/mock-manager.ts +++ b/src/mock-manager.ts @@ -23,18 +23,24 @@ export class MockManager { /** * Mock a JSON-RPC request * @param method JSON-RPC method name - * @param data Data to be resolved, or error to be thrown in case of throw + * @param data Data to be resolved, or function to be called, or error to be thrown in case of throw * @param mockOptions Options of the mock * @example ```ts * // Mock one "eth_accounts" request * mockManager.mockRequest("eth_accounts", ["0x..."]); * // Persistently mock "eth_chainId" request * mockManager.mockRequest("eth_chainId", "0x1", { persistent: true }); + * // Mock with a dynamical value based on params + * // "personal_sign" in this case + * mockManager.mockRequest("personal_sign", async (params: any) => { + * let statement = (params as string[])[0]; + * return await bobsWallet.signMessage(statement); + * }); * ``` */ public mockRequest( method: string, - data: unknown, + data: unknown | ((params: unknown[]) => unknown), mockOptions: MockOptions = {} ) { const { condition, persistent } = mockOptions; diff --git a/src/providers/provider.ts b/src/providers/provider.ts index 4c4d21e..e87098b 100644 --- a/src/providers/provider.ts +++ b/src/providers/provider.ts @@ -44,15 +44,24 @@ export class Provider extends EventEmitter { if (mock.shouldThrow) { reject(mock.data); } else { - resolve({ data: mock.data, callback: mock.triggerCallback }); + resolve({ + data: mock.data, + callback: mock.triggerCallback, + }); } }, mock.timeout || 0); }); const { data, callback } = await promise; + let returnValue: unknown; + if (typeof data === "function") { + returnValue = await data(params); + } else { + returnValue = data; + } if (callback) { - callback(data, params); + callback(returnValue, params); } - return data; + return returnValue; } private findMock(method: string, params: unknown[]) { diff --git a/src/types.ts b/src/types.ts index 1b085d7..4189412 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,5 +10,5 @@ export type MockOptions = { export type MockRequest = { method: string; - data: unknown; + data: unknown | ((params: unknown[]) => unknown); } & MockOptions;