From 3c222a51846015078b5a97ef2797385a913d1672 Mon Sep 17 00:00:00 2001 From: Emil Ibatullin Date: Tue, 19 Sep 2023 09:08:08 +0200 Subject: [PATCH] Set up Prettier and prettify the code (#15) * Add prettier config to package.json * Prettify source * Check prettier on CI * Prettify yml and md files --- .github/workflows/lint.yml | 36 +++++++++++++++++++ .github/workflows/playwright.yml | 62 ++++++++++++++++---------------- .prettierrc | 5 +++ README.md | 46 +++++++++++++++--------- src/Web3ProviderBackend.ts | 54 ++++++++++++++++++++-------- src/factory.ts | 10 +++--- tests/e2e/metamask.spec.ts | 27 +++++++------- 7 files changed, 162 insertions(+), 78 deletions(-) create mode 100644 .github/workflows/lint.yml create mode 100644 .prettierrc diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..f07e5c3 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,36 @@ +name: Lint +on: + push: + branches: [main] + pull_request: + branches: [main] +jobs: + test: + timeout-minutes: 10 + + strategy: + matrix: + os: [ubuntu-latest] + node-version: [18] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.head_ref }} + # This is important to fetch the changes to the previous commit + fetch-depth: 0 + + - uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + cache: 'yarn' + + - name: Install dependencies + run: yarn install + + - name: Prettify code + uses: actionsx/prettier@v2 + with: + args: --check . diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 8997d73..a27ee47 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -1,9 +1,9 @@ name: Playwright Tests on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] jobs: test: timeout-minutes: 60 @@ -16,32 +16,32 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node-version }} - cache: 'yarn' - - - name: Install dependencies - run: yarn install - - - name: Build library - run: yarn build - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly - - - name: Install Playwright Browsers - run: npx playwright install --with-deps - - - name: Run Playwright tests - run: yarn playwright test - - - uses: actions/upload-artifact@v2 - if: always() - with: - name: playwright-report - path: playwright-report/ - retention-days: 30 + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + cache: 'yarn' + + - name: Install dependencies + run: yarn install + + - name: Build library + run: yarn build + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Install Playwright Browsers + run: npx playwright install --with-deps + + - name: Run Playwright tests + run: yarn playwright test + + - uses: actions/upload-artifact@v2 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..12bd349 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "printWidth": 80, + "semi": false, + "singleQuote": true +} diff --git a/README.md b/README.md index f834d67..cd092c3 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,13 @@ npm i -D headless-web3-provider ``` ## About + The `headless-web3-provider` library emulates a Web3 wallet similar to Metamask and provides programmatic control over various operations, such as switching networks, connecting a wallet, and sending transactions, making it useful for end-to-end testing of Ethereum-based applications. It allows to programmatically accept or decline operations, making it handy for test automation. #### Supported methods | Method | Confirmable | -|----------------------------|-------------| +| -------------------------- | ----------- | | eth_requestAccounts | Yes | | eth_accounts | Yes | | eth_sendTransaction | Yes | @@ -37,14 +38,14 @@ The `headless-web3-provider` library emulates a Web3 wallet similar to Metamask | eth_chainId | No | | net_version | No | - ## Examples - ### Playwright + Below given a simple example. More complex scenarios you can find in [tests/e2e](./tests/e2e) folder. Setup (add a fixture): + ```js // tests/fixtures.js import { test as base } from '@playwright/test' @@ -56,19 +57,20 @@ export const test = base.extend({ // injectWeb3Provider - function that injects web3 provider instance into the page injectWeb3Provider: async ({ signers }, use) => { - await use((page, privateKeys = signers) => ( + await use((page, privateKeys = signers) => injectHeadlessWeb3Provider( page, - privateKeys, // Private keys that you want to use in tests - 31337, // Chain ID - 31337 is common testnet id + privateKeys, // Private keys that you want to use in tests + 31337, // Chain ID - 31337 is common testnet id 'http://localhost:8545' // Ethereum client's JSON-RPC URL ) - )) + ) }, }) ``` Usage: + ```js // tests/e2e/example.spec.js import { test } from '../fixtures' @@ -87,31 +89,42 @@ test('connect the wallet', async ({ page, injectWeb3Provider }) => { // Verify if the wallet is really connected await test.expect(page.locator('text=Connected')).toBeVisible() - await test.expect(page.locator('text=0x8b3a08b22d25c60e4b2bfd984e331568eca4c299')).toBeVisible() + await test + .expect(page.locator('text=0x8b3a08b22d25c60e4b2bfd984e331568eca4c299')) + .toBeVisible() }) ``` ### Jest + Add a helper script for injecting the ethereum provider instance. + ```ts // tests/web3-helper.ts import { Wallet } from 'ethers' -import { makeHeadlessWeb3Provider, Web3ProviderBackend } from 'headless-web3-provider' +import { + makeHeadlessWeb3Provider, + Web3ProviderBackend, +} from 'headless-web3-provider' /** * injectWeb3Provider - Function to create and inject web3 provider instance into the global window object * * @returns {Array} An array containing the wallets and the web3Provider instance */ -export function injectWeb3Provider(): [[Wallet, ...Wallet[]], Web3ProviderBackend] { - +export function injectWeb3Provider(): [ + [Wallet, ...Wallet[]], + Web3ProviderBackend +] { // Create 2 random instances of Wallet class - const wallets = Array(2).fill(0).map(() => Wallet.createRandom()) as [Wallet, Wallet] + const wallets = Array(2) + .fill(0) + .map(() => Wallet.createRandom()) as [Wallet, Wallet] // Create an instance of the Web3ProviderBackend class let web3Manager: Web3ProviderBackend = makeHeadlessWeb3Provider( wallets.map((wallet) => wallet.privateKey), - 31337, // Chain ID - 31337 or is a common testnet id + 31337, // Chain ID - 31337 or is a common testnet id 'http://localhost:8545' // Ethereum client's JSON-RPC URL ) @@ -146,8 +159,9 @@ describe('', () => { render() // Request connecting the wallet - await userEvent.click(screen.getByRole('button', { name: /connect wallet/i })) - + await userEvent.click( + screen.getByRole('button', { name: /connect wallet/i }) + ) // Verify if the wallet is NOT yet connected expect(screen.queryByText(wallets[0].address)).not.toBeInTheDocument() @@ -163,14 +177,12 @@ describe('', () => { }) ``` - ## Additional Tools Enhance your testing environment with these complementary tools that integrate seamlessly with `headless-web3-provider`: - [Foundry Anvil](https://book.getfoundry.sh/anvil/) - a dev chain platform ideal for testing your applications against. - ## Resources - [Metamask Test DApp](https://metamask.github.io/test-dapp/) diff --git a/src/Web3ProviderBackend.ts b/src/Web3ProviderBackend.ts index 0ce2dea..3ac6adb 100644 --- a/src/Web3ProviderBackend.ts +++ b/src/Web3ProviderBackend.ts @@ -1,3 +1,4 @@ +import assert from 'node:assert/strict' import { filter, firstValueFrom, @@ -9,7 +10,6 @@ import { } from 'rxjs' import { ethers, Wallet } from 'ethers' import { signTypedData, SignTypedDataVersion } from '@metamask/eth-sig-util' -import assert from 'assert/strict' import { Web3RequestKind } from './utils' import { @@ -111,7 +111,9 @@ export class Web3ProviderBackend extends EventEmitter implements IWeb3Provider { { method, params }, async () => { const accounts = await Promise.all( - this.#wallets.map(async (wallet) => (await wallet.getAddress()).toLowerCase()) + this.#wallets.map(async (wallet) => + (await wallet.getAddress()).toLowerCase() + ) ) this.emit('accountsChanged', accounts) return accounts @@ -123,7 +125,9 @@ export class Web3ProviderBackend extends EventEmitter implements IWeb3Provider { case 'eth_accounts': { if (this._authorizedRequests['eth_requestAccounts']) { return await Promise.all( - this.#wallets.map(async (wallet) => (await wallet.getAddress()).toLowerCase()) + this.#wallets.map(async (wallet) => + (await wallet.getAddress()).toLowerCase() + ) ) } return [] @@ -178,7 +182,9 @@ export class Web3ProviderBackend extends EventEmitter implements IWeb3Provider { return this.waitAuthorization({ method, params }, async () => { const accounts = await Promise.all( - this.#wallets.map(async (wallet) => (await wallet.getAddress()).toLowerCase()) + this.#wallets.map(async (wallet) => + (await wallet.getAddress()).toLowerCase() + ) ) this.emit('accountsChanged', accounts) return [{ parentCapability: 'eth_accounts' }] @@ -259,7 +265,7 @@ export class Web3ProviderBackend extends EventEmitter implements IWeb3Provider { waitAuthorization( requestInfo: PendingRequest['requestInfo'], task: () => Promise, - permanentPermission = false, + permanentPermission = false ) { if (this._authorizedRequests[requestInfo.method]) { return task() @@ -349,7 +355,11 @@ export class Web3ProviderBackend extends EventEmitter implements IWeb3Provider { this.#wallets = privateKeys.map((key) => new ethers.Wallet(key)) this.emit( 'accountsChanged', - await Promise.all(this.#wallets.map(async (wallet) => (await wallet.getAddress()).toLowerCase())) + await Promise.all( + this.#wallets.map(async (wallet) => + (await wallet.getAddress()).toLowerCase() + ) + ) ) } @@ -409,31 +419,47 @@ function without(list: T[], item: T): T[] { // Allowed keys for a JSON-RPC transaction as defined in: // https://ethereum.github.io/execution-apis/api-documentation/ -const allowedTransactionKeys = ['accessList', 'chainId', 'data', 'from', 'gas', 'gasPrice', 'maxFeePerGas', - 'maxPriorityFeePerGas', 'nonce', 'to', 'type', 'value'] +const allowedTransactionKeys = [ + 'accessList', + 'chainId', + 'data', + 'from', + 'gas', + 'gasPrice', + 'maxFeePerGas', + 'maxPriorityFeePerGas', + 'nonce', + 'to', + 'type', + 'value', +] // Convert a JSON-RPC transaction to an ethers.js transaction. // The reverse of this function can be found in the ethers.js library: // https://github.com/ethers-io/ethers.js/blob/v5.7.2/packages/providers/src.ts/json-rpc-provider.ts#L701 -function convertJsonRpcTxToEthersTxRequest(tx: { [key: string]: any }): ethers.providers.TransactionRequest { +function convertJsonRpcTxToEthersTxRequest(tx: { + [key: string]: any +}): ethers.providers.TransactionRequest { const result: any = {} allowedTransactionKeys.forEach((key) => { - if (tx[key] == null) { return } + if (tx[key] == null) { + return + } switch (key) { // gasLimit is referred to as "gas" in JSON-RPC - case "gas": + case 'gas': result['gasLimit'] = tx[key] return // ethers.js expects `chainId` and `type` to be a number - case "chainId": - case "type": + case 'chainId': + case 'type': result[key] = Number(tx[key]) return default: result[key] = tx[key] } - }); + }) return result } diff --git a/src/factory.ts b/src/factory.ts index c3e2c2a..b4c68d9 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -13,12 +13,14 @@ export function makeHeadlessWeb3Provider( method: T, ...args: IWeb3Provider[T] extends Fn ? Parameters : [] ) => Promise = async () => {}, - config?: Web3ProviderConfig + config?: Web3ProviderConfig ) { const chainRpc = new ethers.providers.JsonRpcProvider(rpcUrl, chainId) - const web3Provider = new Web3ProviderBackend(privateKeys, [ - { chainId, rpcUrl }, - ], config) + const web3Provider = new Web3ProviderBackend( + privateKeys, + [{ chainId, rpcUrl }], + config + ) relayEvents(web3Provider, evaluate) diff --git a/tests/e2e/metamask.spec.ts b/tests/e2e/metamask.spec.ts index e4808e9..7f7c866 100644 --- a/tests/e2e/metamask.spec.ts +++ b/tests/e2e/metamask.spec.ts @@ -16,9 +16,9 @@ test.beforeEach(async ({ page, injectWeb3Provider }) => { }) test('connect the wallet', async ({ page, accounts }) => { - // Until the wallet is connected, the accounts should be empty - let ethAccounts = await page.evaluate(() => - window.ethereum.request({ method: 'eth_accounts', params: [] }) + // Until the wallet is connected, the accounts should be empty + let ethAccounts = await page.evaluate(() => + window.ethereum.request({ method: 'eth_accounts', params: [] }) ) expect(ethAccounts).toEqual([]) @@ -118,12 +118,16 @@ test('deploy a token', async ({ page }) => { await expect(page.locator('#tokenAddress')).toContainText(/0x.+/) }) -const getTransactionCount = async (page: Page, account: string): Promise => { - const res = await page.evaluate(addr => - window.ethereum.request({ - method: 'eth_getTransactionCount', - params: [addr, 'latest'] - }), +const getTransactionCount = async ( + page: Page, + account: string +): Promise => { + const res = await page.evaluate( + (addr) => + window.ethereum.request({ + method: 'eth_getTransactionCount', + params: [addr, 'latest'], + }), account ) return Number(res) @@ -138,10 +142,9 @@ test('send legacy transaction', async ({ page, accounts }) => { await wallet.authorize(Web3RequestKind.SendTransaction) const nonceAfter = await getTransactionCount(page, accounts[0]) - expect(nonceAfter).toEqual(nonceBefore+1) + expect(nonceAfter).toEqual(nonceBefore + 1) }) - test('send EIP-1559 transaction', async ({ page, accounts }) => { await page.locator('text=Connect').click() await wallet.authorize(Web3RequestKind.RequestAccounts) @@ -151,7 +154,7 @@ test('send EIP-1559 transaction', async ({ page, accounts }) => { await wallet.authorize(Web3RequestKind.SendTransaction) const nonceAfter = await getTransactionCount(page, accounts[0]) - expect(nonceAfter).toEqual(nonceBefore+1) + expect(nonceAfter).toEqual(nonceBefore + 1) }) /**