From a11ec594ac6f25fc01253395cd1a2c7be1f3f0a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Mi=C3=B1o?= Date: Wed, 6 Mar 2024 15:07:32 -0300 Subject: [PATCH] Tests handle provider request (#7) --- .github/workflows/test.yaml | 12 +- .gitignore | 4 +- package.json | 12 +- scripts/tests.sh | 29 ++ src/handleProviderRequest.test.ts | 539 ++++++++++++++++++++++++++++++ src/handleProviderRequest.ts | 55 +-- src/references/messengers.ts | 4 +- src/utils/tests.ts | 94 ++++++ tsconfig.json | 1 + yarn.lock | 9 +- 10 files changed, 721 insertions(+), 38 deletions(-) create mode 100755 scripts/tests.sh create mode 100644 src/handleProviderRequest.test.ts create mode 100644 src/utils/tests.ts diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index fed2bbd..9d2cd9b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -45,8 +45,18 @@ jobs: name: node_modules.tar.gz - name: Unzip node_modules run: tar xzf node_modules.tar.gz + - name: Append GitHub Access Token to .env + run: echo "ETH_MAINNET_RPC=${{ secrets.ETH_MAINNET_RPC }}" >> .env + - name: Install Anvil + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly - name: Run tests - run: yarn test + uses: nick-fields/retry@v2 + with: + timeout_minutes: 5 + max_attempts: 3 + command: yarn test # LINT, TYPECHECK, AUDIT ci-checks: diff --git a/.gitignore b/.gitignore index 5711a55..65d5b2c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .DS_Store dist/ node_modules/ -yarn-error.log \ No newline at end of file +yarn-error.log + +.env \ No newline at end of file diff --git a/package.json b/package.json index a5b7422..ae4950e 100644 --- a/package.json +++ b/package.json @@ -8,25 +8,27 @@ ], "scripts": { "build": "tsc", - "test": "vitest", + "test": "./scripts/tests.sh", "lint": "eslint --cache --max-warnings 0", "typecheck": "tsc --noEmit", "check-lockfile": "./scripts/check-lockfile.sh", - "audit:ci": "yarn audit-ci --moderate --config audit-ci.jsonc" + "audit:ci": "yarn audit-ci --moderate --config audit-ci.jsonc", + "anvil": "ETH_MAINNET_RPC=$(grep ETH_MAINNET_RPC .env | cut -d '=' -f2) && anvil --fork-url $ETH_MAINNET_RPC", + "anvil:kill": "lsof -i :8545|tail -n +2|awk '{print $2}'|xargs -r kill -s SIGINT" }, - "type": "module", "module": "dist/index.js", "devDependencies": { "@typescript-eslint/eslint-plugin": "6.20.0", "@typescript-eslint/parser": "6.20.0", + "anvil": "0.0.6", "audit-ci": "6.6.1", "eslint": "8.56.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-prettier": "5.1.3", - "jsdom": "^24.0.0", + "jsdom": "24.0.0", "prettier": "3.2.4", "typescript": "5.3.3", - "vitest": "^1.3.1" + "vitest": "1.3.1" }, "dependencies": { "@ethersproject/abstract-provider": "5.7.0", diff --git a/scripts/tests.sh b/scripts/tests.sh new file mode 100755 index 0000000..df3ed63 --- /dev/null +++ b/scripts/tests.sh @@ -0,0 +1,29 @@ +#!/bin/bash +ANVIL_PORT=8545 + +# Launch anvil in the bg +yarn anvil:kill +yarn anvil --chain-id 1 & +echo "Launching Anvil..." + +# Give it some time to boot +interval=5 +until nc -z localhost $ANVIL_PORT; do + sleep $interval + interval=$((interval * 2)) +done +echo "Anvil Launched..." + +# Run the tests and store the result +echo "Running Tests..." +yarn vitest --reporter=verbose --bail 1 + +# Store exit code +TEST_RESULT=$? + +# kill anvil +echo "Cleaning Up..." +yarn anvil:kill + +# return the result of the tests +exit "$TEST_RESULT" diff --git a/src/handleProviderRequest.test.ts b/src/handleProviderRequest.test.ts new file mode 100644 index 0000000..286006a --- /dev/null +++ b/src/handleProviderRequest.test.ts @@ -0,0 +1,539 @@ +import { describe, it, vi, expect, beforeAll, Mock } from 'vitest'; +import { handleProviderRequest } from './handleProviderRequest'; +import { Messenger, createTransport } from './utils/tests'; +import { + ProviderRequestPayload, + RequestResponse, +} from './references/messengers'; +import { Address, isHex, toHex } from 'viem'; +import { mainnet, optimism } from 'viem/chains'; +import { StaticJsonRpcProvider } from '@ethersproject/providers'; + +const TESTMAR27_ETH_ADDRESS: Address = + '0x5e087b61aad29559e31565079fcdabe384b44614'; +const RAINBOWWALLET_ETH_ADDRESS: Address = + '0x7a3d05c70581bd345fe117c06e45f9669205384f'; +const RAINBOWWALLET_ETH_TX_HASH = + '0xfc621a4577ba3398adc0800400b2ba2c408ab76cdc1521dadbfc802dc93a8b37'; +const TX_HASH = + '0x43cfbb52ec99192e96f34a42b37354cfabd6845403e9473e921030da9751d12d'; +const SIGN_SIGNATURE = '0x123456789'; + +const TYPED_MESSAGE = { + domain: { + chainId: 1, + name: 'Ether Mail', + verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + version: '1', + }, + types: { + Mail: [ + { name: 'from', type: 'Person' }, + { name: 'to', type: 'Person' }, + { name: 'contents', type: 'string' }, + ], + Person: [ + { name: 'name', type: 'string' }, + { name: 'wallet', type: 'address' }, + ], + }, + value: { + contents: 'Hello, Bob!', + from: { + name: 'Cow', + wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', + }, + to: { + name: 'Bob', + wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + }, + }, +}; + +describe('handleProviderRequest', () => { + const getFeatureFlagsMock = vi.fn(() => ({ custom_rpc: true })); + const checkRateLimitMock: Mock = vi.fn(() => Promise.resolve(undefined)); + const isSupportedChainMock = vi.fn(() => true); + const getActiveSessionMock = vi.fn(({ host }: { host: string }) => { + switch (host) { + case 'dapp1.com': { + return { + address: RAINBOWWALLET_ETH_ADDRESS, + chainId: mainnet.id, + }; + } + case 'dapp2.com': + default: { + return { + address: TESTMAR27_ETH_ADDRESS, + chainId: optimism.id, + }; + } + } + }); + const getChainMock = vi.fn((chainId: number) => { + switch (chainId) { + case 1: + default: + return mainnet; + } + }); + const getProviderMock = vi.fn(({ chainId }: { chainId?: number }) => { + switch (chainId) { + case 1: + default: + return new StaticJsonRpcProvider('http://127.0.0.1:8545'); + } + }); + const messengerProviderRequestMock = vi.fn((payload) => { + // This is just passing messages so we can test the responses + switch (payload.method) { + case 'eth_requestAccounts': { + return Promise.resolve(RAINBOWWALLET_ETH_ADDRESS); + } + case 'eth_sendTransaction': { + return Promise.resolve(TX_HASH); + } + case 'personal_sign': { + return Promise.resolve( + SIGN_SIGNATURE + 'personal_sign' + payload.params[0], + ); + } + case 'eth_signTypedData': { + return Promise.resolve( + SIGN_SIGNATURE + 'eth_signTypedData' + payload.params[0], + ); + } + case 'eth_signTypedData_v3': { + return Promise.resolve( + SIGN_SIGNATURE + 'eth_signTypedData_v3' + payload.params[0], + ); + } + case 'eth_signTypedData_v4': { + return Promise.resolve( + SIGN_SIGNATURE + 'eth_signTypedData_v4' + payload.params[0], + ); + } + case 'wallet_addEthereumChain': { + return Promise.resolve(true); + } + case 'wallet_switchEthereumChain': { + return Promise.resolve(true); + } + case 'wallet_watchAsset': { + return Promise.resolve(true); + } + default: { + return Promise.resolve({}); + } + } + }); + const onAddEthereumChainMock = vi.fn(() => ({ chainAlreadyAdded: false })); + const onSwitchEthereumChainNotSupportedMock = vi.fn(() => null); + const onSwitchEthereumChainSupportedMock = vi.fn(() => null); + + const messenger = new Messenger('test'); + const transport = createTransport({ + messenger, + topic: 'providerRequest', + }); + + beforeAll(() => { + handleProviderRequest({ + providerRequestTransport: transport, + getFeatureFlags: getFeatureFlagsMock, + checkRateLimit: checkRateLimitMock, + isSupportedChain: isSupportedChainMock, + getActiveSession: getActiveSessionMock, + getChain: getChainMock, + getProvider: getProviderMock, + messengerProviderRequest: messengerProviderRequestMock, + onAddEthereumChain: onAddEthereumChainMock, + onSwitchEthereumChainNotSupported: onSwitchEthereumChainNotSupportedMock, + onSwitchEthereumChainSupported: onSwitchEthereumChainSupportedMock, + }); + }); + + it('should rate limit requests', async () => { + checkRateLimitMock.mockImplementationOnce(() => + Promise.resolve({ id: 1, error: new Error('Rate Limit Exceeded') }), + ); + const response = await transport.send( + { id: 1, method: 'eth_requestAccounts' }, + { id: 1 }, + ); + expect(response).toEqual({ + id: 1, + error: new Error('Rate Limit Exceeded'), + }); + }); + + it('should call eth_chainId correctly', async () => { + const response = await transport.send( + { + id: 1, + method: 'eth_chainId', + meta: { + sender: { url: 'https://dapp1.com' }, + topic: 'providerRequest', + }, + }, + { id: 1 }, + ); + expect(Number(response.result)).toEqual(mainnet.id); + }); + + it('should call eth_coinbase correctly', async () => { + const response1 = await transport.send( + { + id: 1, + method: 'eth_coinbase', + meta: { + sender: { url: 'https://dapp1.com' }, + topic: 'providerRequest', + }, + }, + { id: 1 }, + ); + expect(response1.result).toEqual(RAINBOWWALLET_ETH_ADDRESS); + const response2 = await transport.send( + { + id: 2, + method: 'eth_coinbase', + meta: { + sender: { url: 'https://dapp2.com' }, + topic: 'providerRequest', + }, + }, + { id: 2 }, + ); + expect(response2.result).toEqual(TESTMAR27_ETH_ADDRESS); + }); + + it('should call eth_accounts correctly', async () => { + const response1 = await transport.send( + { + id: 1, + method: 'eth_accounts', + meta: { + sender: { url: 'https://dapp1.com' }, + topic: 'providerRequest', + }, + }, + { id: 1 }, + ); + expect(response1.result).toEqual([RAINBOWWALLET_ETH_ADDRESS]); + const response2 = await transport.send( + { + id: 2, + method: 'eth_accounts', + meta: { + sender: { url: 'https://dapp2.com' }, + topic: 'providerRequest', + }, + }, + { id: 2 }, + ); + expect(response2.result).toEqual([TESTMAR27_ETH_ADDRESS]); + }); + + it('should call eth_blockNumber correctly', async () => { + const response = await transport.send( + { + id: 1, + method: 'eth_blockNumber', + meta: { + sender: { url: 'https://dapp1.com' }, + topic: 'providerRequest', + }, + }, + { id: 1 }, + ); + expect(isHex(response.result)).toBeTruthy(); + }); + + it('should call eth_getBalance correctly', async () => { + const response = await transport.send( + { + id: 1, + method: 'eth_getBalance', + params: [RAINBOWWALLET_ETH_ADDRESS], + meta: { + sender: { url: 'https://dapp1.com' }, + topic: 'providerRequest', + }, + }, + { id: 1 }, + ); + expect(isHex(response.result)).toBeTruthy(); + }); + + it('should call eth_getTransactionByHash correctly', async () => { + const response = await transport.send( + { + id: 1, + method: 'eth_getTransactionByHash', + params: [RAINBOWWALLET_ETH_TX_HASH], + meta: { + sender: { url: 'https://dapp1.com' }, + topic: 'providerRequest', + }, + }, + { id: 1 }, + ); + expect(isHex(response.result.data)).toBeTruthy(); + }); + + it('should call eth_call correctly', async () => { + const response = await transport.send( + { + id: 1, + method: 'eth_call', + params: [ + { + from: null, + to: '0x6b175474e89094c44da98b954eedeac495271d0f', + data: '0x70a082310000000000000000000000006E0d01A76C3Cf4288372a29124A26D4353EE51BE', + }, + 'latest', + ], + meta: { + sender: { url: 'https://dapp1.com' }, + topic: 'providerRequest', + }, + }, + { id: 1 }, + ); + expect(isHex(response.result)).toBeTruthy(); + }); + + it('should call eth_estimateGas correctly', async () => { + const response = await transport.send( + { + id: 1, + method: 'eth_estimateGas', + params: [ + { + from: '0x8D97689C9818892B700e27F316cc3E41e17fBeb9', + to: '0xd3CdA913deB6f67967B99D67aCDFa1712C293601', + value: '0x186a0', + }, + ], + meta: { + sender: { url: 'https://dapp1.com' }, + topic: 'providerRequest', + }, + }, + { id: 1 }, + ); + expect(isHex(response.result)).toBeTruthy(); + }); + + it('should call eth_gasPrice correctly', async () => { + const response = await transport.send( + { + id: 1, + method: 'eth_gasPrice', + params: [], + meta: { + sender: { url: 'https://dapp1.com' }, + topic: 'providerRequest', + }, + }, + { id: 1 }, + ); + expect(isHex(response.result)).toBeTruthy(); + }); + + it('should call eth_getCode correctly', async () => { + const response = await transport.send( + { + id: 1, + method: 'eth_getCode', + params: ['0x5B56438000bAc5ed2c6E0c1EcFF4354aBfFaf889', 'latest'], + meta: { + sender: { url: 'https://dapp1.com' }, + topic: 'providerRequest', + }, + }, + { id: 1 }, + ); + expect(isHex(response.result)).toBeTruthy(); + }); + + it('should call eth_requestAccounts correctly', async () => { + const response = await transport.send( + { + id: 1, + method: 'eth_requestAccounts', + params: [], + meta: { + sender: { url: 'https://dapp1.com' }, + topic: 'providerRequest', + }, + }, + { id: 1 }, + ); + expect(response.result[0]).toBe(RAINBOWWALLET_ETH_ADDRESS); + }); + + it('should call eth_sendTransaction correctly', async () => { + const response = await transport.send( + { + id: 1, + method: 'eth_sendTransaction', + params: [ + { + from: '0x5B570F0F8E2a29B7bCBbfC000f9C7b78D45b7C35', + gas: '0x5208', + to: '0x5B570F0F8E2a29B7bCBbfC000f9C7b78D45b7C35', + value: '0x9184e72a000', + }, + ], + meta: { + sender: { url: 'https://dapp1.com' }, + topic: 'providerRequest', + }, + }, + { id: 1 }, + ); + expect(response.result).toBe(TX_HASH); + }); + + it('should call personal_sign correctly', async () => { + const response = await transport.send( + { + id: 1, + method: 'personal_sign', + params: ['personal_sign_message'], + meta: { + sender: { url: 'https://dapp1.com' }, + topic: 'providerRequest', + }, + }, + { id: 1 }, + ); + expect(response.result).toBe( + SIGN_SIGNATURE + 'personal_sign' + 'personal_sign_message', + ); + }); + + it('should call eth_signTypedData correctly', async () => { + const response = await transport.send( + { + id: 1, + method: 'eth_signTypedData', + params: ['eth_signTypedData_message'], + meta: { + sender: { url: 'https://dapp1.com' }, + topic: 'providerRequest', + }, + }, + { id: 1 }, + ); + expect(response.result).toBe( + SIGN_SIGNATURE + 'eth_signTypedData' + 'eth_signTypedData_message', + ); + }); + + it('should call eth_signTypedData_v3 correctly', async () => { + const response = await transport.send( + { + id: 1, + method: 'eth_signTypedData_v3', + params: ['eth_signTypedData_v3_message'], + meta: { + sender: { url: 'https://dapp1.com' }, + topic: 'providerRequest', + }, + }, + { id: 1 }, + ); + expect(response.result).toBe( + SIGN_SIGNATURE + 'eth_signTypedData_v3' + 'eth_signTypedData_v3_message', + ); + }); + + it('should call eth_signTypedData_v4 correctly', async () => { + const response = await transport.send( + { + id: 1, + method: 'eth_signTypedData_v4', + params: [TYPED_MESSAGE], + meta: { + sender: { url: 'https://dapp1.com' }, + topic: 'providerRequest', + }, + }, + { id: 1 }, + ); + expect(response.result).toBe( + SIGN_SIGNATURE + 'eth_signTypedData_v4' + TYPED_MESSAGE, + ); + }); + + it('should call wallet_addEthereumChain correctly', async () => { + const response = await transport.send( + { + id: 1, + method: 'wallet_addEthereumChain', + params: [ + { + blockExplorerUrls: [mainnet.blockExplorers.default.url], + chainId: toHex(mainnet.id), + chainName: mainnet.network, + nativeCurrency: mainnet.nativeCurrency, + rpcUrls: [mainnet.rpcUrls.default.http], + }, + RAINBOWWALLET_ETH_ADDRESS, + ], + meta: { + sender: { url: 'https://dapp1.com' }, + topic: 'providerRequest', + }, + }, + { id: 1 }, + ); + expect(response.result).toBeNull(); + }); + + it('should call wallet_switchEthereumChain correctly', async () => { + const response = await transport.send( + { + id: 1, + method: 'wallet_switchEthereumChain', + params: [{ chainId: toHex(mainnet.id) }], + meta: { + sender: { url: 'https://dapp1.com' }, + topic: 'providerRequest', + }, + }, + { id: 1 }, + ); + expect(response.result).toBeNull(); + }); + + it('should call wallet_watchAsset correctly', async () => { + const response = await transport.send( + { + id: 1, + method: 'wallet_watchAsset', + params: { + type: 'ERC20', + options: { + address: '0xb60e8dd61c5d32be8058bb8eb970870f07233155', + symbol: 'FOO', + decimals: 18, + image: 'https://foo.io/token-image.svg', + }, + }, + meta: { + sender: { url: 'https://dapp1.com' }, + topic: 'providerRequest', + }, + }, + { id: 1 }, + ); + expect(response.result).toBeTruthy(); + }); +}); diff --git a/src/handleProviderRequest.ts b/src/handleProviderRequest.ts index 30b9c7f..e9bef5d 100644 --- a/src/handleProviderRequest.ts +++ b/src/handleProviderRequest.ts @@ -72,16 +72,15 @@ export const handleProviderRequest = ({ }) => providerRequestTransport?.reply(async ({ method, id, params }, meta) => { try { - const url = meta?.sender?.url || ''; - const host = (isValidUrl(url) && getDappHost(url)) || ''; - const activeSession = getActiveSession({ host }); - const rateLimited = await checkRateLimit({ id, meta, method }); - if (rateLimited) { return { id, error: new Error('Rate Limit Exceeded') }; } + const url = meta?.sender?.url || ''; + const host = (isValidUrl(url) && getDappHost(url)) || ''; + const activeSession = getActiveSession({ host }); + let response = null; switch (method) { @@ -106,16 +105,16 @@ export const handleProviderRequest = ({ break; } case 'eth_getBalance': { + const p = params as Array; const provider = getProvider({ chainId: activeSession?.chainId }); - const balance = await provider.getBalance(params?.[0] as string); + const balance = await provider.getBalance(p?.[0] as string); response = toHex(balance); break; } case 'eth_getTransactionByHash': { + const p = params as Array; const provider = getProvider({ chainId: activeSession?.chainId }); - const transaction = await provider.getTransaction( - params?.[0] as string, - ); + const transaction = await provider.getTransaction(p?.[0] as string); const normalizedTransaction = normalizeTransactionResponsePayload(transaction); const { @@ -138,15 +137,15 @@ export const handleProviderRequest = ({ break; } case 'eth_call': { + const p = params as Array; const provider = getProvider({ chainId: activeSession?.chainId }); - response = await provider.call(params?.[0] as TransactionRequest); + response = await provider.call(p?.[0] as TransactionRequest); break; } case 'eth_estimateGas': { + const p = params as Array; const provider = getProvider({ chainId: activeSession?.chainId }); - const gas = await provider.estimateGas( - params?.[0] as TransactionRequest, - ); + const gas = await provider.estimateGas(p?.[0] as TransactionRequest); response = toHex(gas); break; } @@ -157,11 +156,9 @@ export const handleProviderRequest = ({ break; } case 'eth_getCode': { + const p = params as Array; const provider = getProvider({ chainId: activeSession?.chainId }); - response = await provider.getCode( - params?.[0] as string, - params?.[1] as string, - ); + response = await provider.getCode(p?.[0] as string, p?.[1] as string); break; } case 'eth_sendTransaction': @@ -171,11 +168,12 @@ export const handleProviderRequest = ({ case 'eth_signTypedData_v3': case 'eth_signTypedData_v4': { // If we need to validate the input before showing the UI, it should go here. + const p = params as Array; if (method === 'eth_signTypedData_v4') { // we don't trust the params order - let dataParam = params?.[1]; - if (!isAddress(params?.[0] as Address)) { - dataParam = params?.[0]; + let dataParam = p?.[1]; + if (!isAddress(p?.[0] as Address)) { + dataParam = p?.[0]; } const data = @@ -202,7 +200,8 @@ export const handleProviderRequest = ({ break; } case 'wallet_addEthereumChain': { - const proposedChain = params?.[0] as AddEthereumChainProposedChain; + const p = params as Array; + const proposedChain = p?.[0] as AddEthereumChainProposedChain; const proposedChainId = Number(proposedChain.chainId); const featureFlags = getFeatureFlags(); if (!featureFlags.custom_rpc) { @@ -280,13 +279,14 @@ export const handleProviderRequest = ({ if (!response) { throw new Error('User rejected the request.'); } else { - response = null + response = null; } } break; } case 'wallet_switchEthereumChain': { - const proposedChain = params?.[0] as AddEthereumChainProposedChain; + const p = params as Array; + const proposedChain = p?.[0] as AddEthereumChainProposedChain; const supportedChainId = isSupportedChain?.( Number(proposedChain.chainId), ); @@ -322,7 +322,6 @@ export const handleProviderRequest = ({ decimals?: number; }; }; - if (type !== 'ERC20') { throw new Error('Method supported only for ERC20'); } @@ -370,12 +369,14 @@ export const handleProviderRequest = ({ response = [address?.toLowerCase()]; break; } - case 'personal_ecRecover': + case 'personal_ecRecover': { + const p = params as Array; response = recoverPersonalSignature({ - data: params?.[0] as string, - signature: params?.[1] as string, + data: p?.[0] as string, + signature: p?.[1] as string, }); break; + } default: { try { if (method?.substring(0, 7) === 'wallet_') { diff --git a/src/references/messengers.ts b/src/references/messengers.ts index f4d8348..a629e0f 100644 --- a/src/references/messengers.ts +++ b/src/references/messengers.ts @@ -3,7 +3,7 @@ import { RPCMethod } from './ethereum'; export type RequestArguments = { id?: number; method: RPCMethod; - params?: Array; + params?: Array | object; }; export type RequestResponse = @@ -61,7 +61,7 @@ export type ProviderRequestPayload = RequestArguments & { meta?: CallbackOptions; }; -type ProviderResponse = RequestResponse; +export type ProviderResponse = RequestResponse; export interface IProviderRequestTransport { send( diff --git a/src/utils/tests.ts b/src/utils/tests.ts new file mode 100644 index 0000000..954584c --- /dev/null +++ b/src/utils/tests.ts @@ -0,0 +1,94 @@ +import EventEmitter from 'eventemitter3'; +import { + CallbackOptions, + IMessageSender, + IMessenger, +} from '../references/messengers'; + +// Placeholder for callback type, adjust according to your actual callback function signature +type CallbackFunction = ( + payload: TPayload, + options: { id?: number | string; sender: IMessageSender; topic: string }, +) => Promise; + +export function createTransport({ + messenger, + topic, +}: { + messenger: IMessenger; + topic: string; +}) { + if (!messenger.available) { + console.error( + `Messenger "${messenger.name}" is not available in this context.`, + ); + } + return { + async send(payload: TPayload, { id }: { id: number }) { + return messenger.send(topic, payload, { id }); + }, + async reply( + callback: ( + payload: TPayload, + callbackOptions: CallbackOptions, + ) => Promise, + ) { + messenger.reply(topic, callback); + }, + }; +} + +export class Messenger extends EventEmitter implements IMessenger { + available: boolean; + name: string; + + constructor(name: string) { + super(); + this.available = true; + this.name = name; + } + + async send( + topic: string, + payload: TPayload, + options?: { id?: string | number }, + ): Promise { + return new Promise((resolve, reject) => { + const scopedTopic = `${topic}:${options?.id || 'global'}`; + this.once(scopedTopic, (response: TResponse | Error) => { + if (response instanceof Error) { + reject(response); + } else { + resolve(response); + } + }); + + this.emit(topic, payload, options); + }); + } + + reply( + topic: string, + callback: CallbackFunction, + ) { + const listener = async (payload: TPayload, options: { id?: string }) => { + try { + const response = await callback(payload, { + id: options.id, + topic: topic, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sender: { url: (payload as any).meta?.sender?.url || '' }, + }); + const scopedTopic = `${topic}:${options.id || 'global'}`; + this.emit(scopedTopic, response); + } catch (error) { + const scopedTopic = `${topic}:${options.id || 'global'}`; + this.emit(scopedTopic, error); + } + }; + + this.on(topic, listener); + + return () => this.off(topic, listener); + } +} diff --git a/tsconfig.json b/tsconfig.json index 2183d12..e80fb90 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,7 @@ "src/**/*" ], "exclude": [ + "**/*.test.ts", "node_modules" ] } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index d6527f9..e9cf50a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -852,6 +852,11 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== +anvil@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/anvil/-/anvil-0.0.6.tgz#f572691462fd76c087fc6b76d77e628a8db7a7aa" + integrity sha512-k9GcppcaE1cruJj1Gi141q/5TlPO2HIitPz99K0tWWh5ImtYG3Lq83lIWQXqrNRA+hX7DN/EjS7J9tCI/wDofQ== + argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" @@ -1621,7 +1626,7 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -jsdom@^24.0.0: +jsdom@24.0.0: version "24.0.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-24.0.0.tgz#e2dc04e4c79da368481659818ee2b0cd7c39007c" integrity sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A== @@ -2381,7 +2386,7 @@ vite@^5.0.0: optionalDependencies: fsevents "~2.3.3" -vitest@^1.3.1: +vitest@1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/vitest/-/vitest-1.3.1.tgz#2d7e9861f030d88a4669392a4aecb40569d90937" integrity sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==