From b6655cd6b403b3719fe828b1e5b98d28706557bb Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Tue, 15 Apr 2025 14:45:30 -0700 Subject: [PATCH 1/2] Add EVM JSON-RPC plugin --- .vscode/settings.json | 3 + package.json | 3 +- src/plugins/allPlugins.ts | 89 ++++++++- src/plugins/evmRpc.ts | 129 +++++++++++++ test/plugins/evmRpc.test.ts | 352 ++++++++++++++++++++++++++++++++++++ yarn.lock | 106 ++++++++++- 6 files changed, 667 insertions(+), 15 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/plugins/evmRpc.ts create mode 100644 test/plugins/evmRpc.test.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..25fa621 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib" +} diff --git a/package.json b/package.json index 6669507..8acd5ee 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "node-fetch": "^2.6.0", "prom-client": "^15.1.0", "serverlet": "^0.1.1", + "viem": "^2.27.0", "ws": "^8.18.0", "yaob": "^0.3.12", "yavent": "^0.1.4" @@ -65,7 +66,7 @@ "rimraf": "^3.0.0", "sucrase": "^3.16.0", "ts-jest": "^29.2.6", - "typescript": "^4.6.2", + "typescript": "^5.8.3", "wait-for-expect": "^3.0.2" } } diff --git a/src/plugins/allPlugins.ts b/src/plugins/allPlugins.ts index dc28386..227b92b 100644 --- a/src/plugins/allPlugins.ts +++ b/src/plugins/allPlugins.ts @@ -1,6 +1,7 @@ import { serverConfig } from '../serverConfig' import { AddressPlugin } from '../types/addressPlugin' import { BlockbookOptions, makeBlockbook } from './blockbook' +import { makeEvmRpc } from './evmRpc' import { makeFakePlugin } from './fakePlugin' function makeNowNode(opts: BlockbookOptions): AddressPlugin { @@ -35,25 +36,93 @@ export const allPlugins = [ }), // Ethereum family: - makeNowNode({ + makeEvmRpc({ + pluginId: 'abstract', + url: 'https://api.mainnet.abs.xyz' + }), + makeEvmRpc({ + pluginId: 'amoy', + url: 'https://polygon-amoy-bor-rpc.publicnode.com' + }), + makeEvmRpc({ pluginId: 'arbitrum', - url: 'wss://arb-blockbook.nownodes.io/wss' + url: 'https://arbitrum-one-rpc.publicnode.com' }), - makeNowNode({ + makeEvmRpc({ pluginId: 'avalanche', - url: 'wss://avax-blockbook.nownodes.io/wss' + url: 'https://avalanche-c-chain-rpc.publicnode.com' }), - makeNowNode({ + makeEvmRpc({ pluginId: 'base', - url: 'wss://base-blockbook.nownodes.io/wss' + url: 'https://base-rpc.publicnode.com' }), - makeNowNode({ + makeEvmRpc({ + pluginId: 'binancesmartchain', + url: 'https://bsc-rpc.publicnode.com' + }), + makeEvmRpc({ + pluginId: 'bobevm', + url: 'https://bob.drpc.org' + }), + makeEvmRpc({ + pluginId: 'celo', + url: 'https://celo-rpc.publicnode.com' + }), + makeEvmRpc({ pluginId: 'ethereum', - url: 'wss://eth-blockbook.nownodes.io/wss' + url: 'https://ethereum-rpc.publicnode.com' }), - makeNowNode({ + makeEvmRpc({ + pluginId: 'ethereumclassic', + url: 'https://geth-at.etc-network.info' + }), + makeEvmRpc({ + pluginId: 'ethereumpow', + url: 'https://mainnet.ethereumpow.org' + }), + makeEvmRpc({ + pluginId: 'fantom', + url: 'https://rpc.fantom.network' + }), + makeEvmRpc({ + pluginId: 'filecoinfevm', + url: 'https://rpc.ankr.com/filecoin' + }), + makeEvmRpc({ + pluginId: 'filecoinfevmcalibration', + url: 'https://rpc.ankr.com/filecoin_testnet' + }), + makeEvmRpc({ + pluginId: 'holesky', + url: 'https://ethereum-holesky-rpc.publicnode.com' + }), + makeEvmRpc({ + pluginId: 'optimism', + url: 'https://optimism-rpc.publicnode.com' + }), + makeEvmRpc({ pluginId: 'polygon', - url: 'wss://maticbook.nownodes.io/wss' + url: 'https://polygon-bor-rpc.publicnode.com' + }), + makeEvmRpc({ + pluginId: 'pulsechain', + url: 'https://pulsechain-rpc.publicnode.com' + }), + makeEvmRpc({ + pluginId: 'rsk', + url: 'https://public-node.rsk.co' + }), + makeEvmRpc({ + pluginId: 'sepolia', + url: 'https://ethereum-sepolia-rpc.publicnode.com' + }), + makeEvmRpc({ + pluginId: 'sonic', + url: 'https://sonic.drpc.org' + }), + makeEvmRpc({ + pluginId: 'zksync', + url: 'https://1rpc.io/zksync2-era' }), // Testing: diff --git a/src/plugins/evmRpc.ts b/src/plugins/evmRpc.ts new file mode 100644 index 0000000..5d74144 --- /dev/null +++ b/src/plugins/evmRpc.ts @@ -0,0 +1,129 @@ +import { createPublicClient, http, parseAbiItem } from 'viem' +import { mainnet } from 'viem/chains' +import { makeEvents } from 'yavent' + +import { AddressPlugin, PluginEvents } from '../types/addressPlugin' + +export interface EvmRpcOptions { + pluginId: string + + /** A clean URL for logging */ + safeUrl?: string + + /** The actual wss connection URL */ + url: string +} + +const ERC20_TRANSFER_EVENT = parseAbiItem( + 'event Transfer(address indexed from, address indexed to, uint256 value)' +) + +export function makeEvmRpc(opts: EvmRpcOptions): AddressPlugin { + const { pluginId, safeUrl = opts.url, url } = opts + + const [on, emit] = makeEvents() + + const logPrefix = `${pluginId} (${safeUrl}):` + const logger = { + log: (...args: unknown[]): void => { + console.log(logPrefix, ...args) + }, + error: (...args: unknown[]): void => { + console.error(logPrefix, ...args) + }, + warn: (...args: unknown[]): void => { + console.warn(logPrefix, ...args) + } + } + + // Track subscribed addresses (normalized lowercase address -> original address) + const subscribedAddresses = new Map() + + const client = createPublicClient({ + chain: mainnet, + transport: http(url) + }) + + client.watchBlocks({ + includeTransactions: true, + emitMissed: true, + onError: error => { + logger.error('watchBlocks error', error) + }, + onBlock: async block => { + logger.log('onBlock', block.number) + // Skip processing if no subscriptions + if (subscribedAddresses.size === 0) return + + // Track which subscribed addresses have updates in this block + const addressesToUpdate = new Set() + + // Check regular transactions + block.transactions.forEach(tx => { + const normalizedFromAddress = tx.from.toLowerCase() + const normalizedToAddress = tx.to?.toLowerCase() + const matchingFromAddress = subscribedAddresses.get( + normalizedFromAddress + ) + const matchingToAddress = + normalizedToAddress !== undefined + ? subscribedAddresses.get(normalizedToAddress) + : undefined + if (matchingFromAddress != null) { + addressesToUpdate.add(matchingFromAddress) + } + if (matchingToAddress != null) { + addressesToUpdate.add(matchingToAddress) + } + }) + + // Check ERC20 transfers + const transferLogs = await client.getLogs({ + blockHash: block.hash, + event: ERC20_TRANSFER_EVENT + }) + transferLogs.forEach(log => { + const normalizedFromAddress = log.args.from?.toLowerCase() + const normalizedToAddress = log.args.to?.toLowerCase() + const matchingFromAddress = + normalizedFromAddress !== undefined + ? subscribedAddresses.get(normalizedFromAddress) + : undefined + const matchingToAddress = + normalizedToAddress !== undefined + ? subscribedAddresses.get(normalizedToAddress) + : undefined + if (matchingFromAddress != null) { + addressesToUpdate.add(matchingFromAddress) + } + if (matchingToAddress != null) { + addressesToUpdate.add(matchingToAddress) + } + }) + + // Emit update events for all affected subscribed addresses + for (const originalAddress of addressesToUpdate) { + emit('update', { + address: originalAddress, + checkpoint: block.number.toString() + }) + } + } + }) + + const plugin: AddressPlugin = { + pluginId, + subscribe: async address => { + const normalizedAddress = address.toLowerCase() + subscribedAddresses.set(normalizedAddress, address) + return true + }, + unsubscribe: async address => { + const normalizedAddress = address.toLowerCase() + return subscribedAddresses.delete(normalizedAddress) + }, + on + } + + return plugin +} diff --git a/test/plugins/evmRpc.test.ts b/test/plugins/evmRpc.test.ts new file mode 100644 index 0000000..fe4e5d6 --- /dev/null +++ b/test/plugins/evmRpc.test.ts @@ -0,0 +1,352 @@ +import { + afterEach, + beforeEach, + describe, + expect, + jest, + test +} from '@jest/globals' + +import { makeEvmRpc } from '../../src/plugins/evmRpc' +import { AddressPlugin } from '../../src/types/addressPlugin' + +// Mock viem +jest.mock('viem', () => { + // Mock functions and data + const mockWatchBlocks = jest.fn() + const mockGetLogs = jest.fn() + + const mockClient = { + watchBlocks: mockWatchBlocks, + getLogs: mockGetLogs + } + + return { + createPublicClient: jest.fn(() => mockClient), + http: jest.fn(url => ({ url })), + parseAbiItem: jest.fn(abiString => ({ abiString })) + } +}) + +jest.mock('viem/chains', () => ({ + mainnet: { id: 1, name: 'Mainnet' } +})) + +// Access the mocked client - Using any type to avoid TS errors with Jest mocks +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const mockViemLib: any = jest.requireMock('viem') +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const mockClient: any = mockViemLib.createPublicClient() + +describe('evmRpc plugin', function () { + const TEST_ETH_ADDRESS = '0x742d35Cc6634C0532925a3b844Bc454e4438f44e' + const TEST_ETH_ADDRESS_LOWERCASE = TEST_ETH_ADDRESS.toLowerCase() + const TEST_SECOND_ADDRESS = '0x6B175474E89094C44Da98b954EedeAC495271d0F' // DAI + const TEST_SECOND_ADDRESS_LOWERCASE = TEST_SECOND_ADDRESS.toLowerCase() + + const mockUrl = 'https://ethereum.example.com/rpc' + + const consoleSpy = { + log: jest.spyOn(console, 'log').mockImplementation(() => {}), + warn: jest.spyOn(console, 'warn').mockImplementation(() => {}), + error: jest.spyOn(console, 'error').mockImplementation(() => {}) + } + + let plugin: AddressPlugin + + beforeEach(() => { + jest.clearAllMocks() + + // Reset mock functions + mockClient.watchBlocks.mockImplementation( + ({ onBlock }: { onBlock: any }) => { + // Store the callback to trigger it later in tests + mockClient.watchBlocks.onBlock = onBlock + return { unwatch: jest.fn() } + } + ) + + mockClient.getLogs.mockResolvedValue([]) + + plugin = makeEvmRpc({ + pluginId: 'test-evm', + url: mockUrl + }) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + test('plugin instantiation', function () { + expect(plugin.pluginId).toBe('test-evm') + expect(mockViemLib.createPublicClient).toHaveBeenCalledWith({ + chain: expect.anything(), + transport: expect.anything() + }) + expect(mockClient.watchBlocks).toHaveBeenCalled() + }) + + test('subscribe should return true', async function () { + const result = await plugin.subscribe(TEST_ETH_ADDRESS) + expect(result).toBe(true) + }) + + test('address normalization during subscription', async function () { + await plugin.subscribe(TEST_ETH_ADDRESS) + + // Simulate a block with a transaction from our address (in different case) + const mockBlock = { + number: 123456n, + hash: '0xabc', + transactions: [ + { + from: TEST_ETH_ADDRESS_LOWERCASE, + to: '0xdef' + } + ] + } + + // Set up event handler + const updateHandler = jest.fn() + plugin.on('update', updateHandler) + + // Trigger block callback + await mockClient.watchBlocks.onBlock(mockBlock) + + // Check that our handler was called with the original address case + expect(updateHandler).toHaveBeenCalledWith({ + address: TEST_ETH_ADDRESS, + checkpoint: '123456' + }) + }) + + test('unsubscribe should remove address', async function () { + await plugin.subscribe(TEST_ETH_ADDRESS) + const result = await plugin.unsubscribe(TEST_ETH_ADDRESS) + expect(result).toBe(true) + + // Set up event handler + const updateHandler = jest.fn() + plugin.on('update', updateHandler) + + // Simulate a block with a transaction from our address + const mockBlock = { + number: 123456n, + hash: '0xabc', + transactions: [ + { + from: TEST_ETH_ADDRESS_LOWERCASE, + to: '0xdef' + } + ] + } + + // Trigger block callback + await mockClient.watchBlocks.onBlock(mockBlock) + + // Check that our handler was NOT called because we unsubscribed + expect(updateHandler).not.toHaveBeenCalled() + }) + + test('update event should fire for sender address', async function () { + await plugin.subscribe(TEST_ETH_ADDRESS) + + // Set up event handler + const updateHandler = jest.fn() + plugin.on('update', updateHandler) + + // Simulate a block with a transaction from our address + const mockBlock = { + number: 123456n, + hash: '0xabc', + transactions: [ + { + from: TEST_ETH_ADDRESS_LOWERCASE, + to: '0xdef' + } + ] + } + + // Trigger block callback + await mockClient.watchBlocks.onBlock(mockBlock) + + expect(updateHandler).toHaveBeenCalledWith({ + address: TEST_ETH_ADDRESS, + checkpoint: '123456' + }) + }) + + test('update event should fire for recipient address', async function () { + await plugin.subscribe(TEST_ETH_ADDRESS) + + // Set up event handler + const updateHandler = jest.fn() + plugin.on('update', updateHandler) + + // Simulate a block with a transaction to our address + const mockBlock = { + number: 123456n, + hash: '0xabc', + transactions: [ + { + from: '0xdef', + to: TEST_ETH_ADDRESS_LOWERCASE + } + ] + } + + // Trigger block callback + await mockClient.watchBlocks.onBlock(mockBlock) + + expect(updateHandler).toHaveBeenCalledWith({ + address: TEST_ETH_ADDRESS, + checkpoint: '123456' + }) + }) + + test('update event should fire for ERC20 transfer events', async function () { + await plugin.subscribe(TEST_ETH_ADDRESS) + + // Set up event handler + const updateHandler = jest.fn() + plugin.on('update', updateHandler) + + // Simulate a block with no direct transactions for our address + const mockBlock = { + number: 123456n, + hash: '0xabc', + transactions: [ + { + from: '0xaaa', + to: '0xbbb' + } + ] + } + + // Mock an ERC20 transfer log where our address is the sender + mockClient.getLogs.mockResolvedValueOnce([ + { + args: { + from: TEST_ETH_ADDRESS_LOWERCASE, + to: '0xccc', + value: 1000000000000000000n + } + } + ]) + + // Trigger block callback + await mockClient.watchBlocks.onBlock(mockBlock) + + expect(mockClient.getLogs).toHaveBeenCalledWith({ + blockHash: '0xabc', + event: expect.anything() + }) + + expect(updateHandler).toHaveBeenCalledWith({ + address: TEST_ETH_ADDRESS, + checkpoint: '123456' + }) + }) + + test('update event should fire for ERC20 token received', async function () { + await plugin.subscribe(TEST_ETH_ADDRESS) + + // Set up event handler + const updateHandler = jest.fn() + plugin.on('update', updateHandler) + + // Simulate a block with no direct transactions for our address + const mockBlock = { + number: 123456n, + hash: '0xabc', + transactions: [ + { + from: '0xaaa', + to: '0xbbb' + } + ] + } + + // Mock an ERC20 transfer log where our address is the recipient + mockClient.getLogs.mockResolvedValueOnce([ + { + args: { + from: '0xccc', + to: TEST_ETH_ADDRESS_LOWERCASE, + value: 1000000000000000000n + } + } + ]) + + // Trigger block callback + await mockClient.watchBlocks.onBlock(mockBlock) + + expect(updateHandler).toHaveBeenCalledWith({ + address: TEST_ETH_ADDRESS, + checkpoint: '123456' + }) + }) + + test('multiple subscribed addresses should all receive updates', async function () { + await plugin.subscribe(TEST_ETH_ADDRESS) + await plugin.subscribe(TEST_SECOND_ADDRESS) + + // Set up event handler + const updateHandler = jest.fn() + plugin.on('update', updateHandler) + + // Simulate a block with transactions for both addresses + const mockBlock = { + number: 123456n, + hash: '0xabc', + transactions: [ + { + from: TEST_ETH_ADDRESS_LOWERCASE, + to: TEST_SECOND_ADDRESS_LOWERCASE + } + ] + } + + // Trigger block callback + await mockClient.watchBlocks.onBlock(mockBlock) + + // Both addresses should receive updates + expect(updateHandler).toHaveBeenCalledTimes(2) + expect(updateHandler).toHaveBeenCalledWith({ + address: TEST_ETH_ADDRESS, + checkpoint: '123456' + }) + expect(updateHandler).toHaveBeenCalledWith({ + address: TEST_SECOND_ADDRESS, + checkpoint: '123456' + }) + }) + + test('scanAddress should return true', async function () { + if (plugin.scanAddress == null) { + return + } + + // Subscribe first to make sure the address is tracked + await plugin.subscribe(TEST_ETH_ADDRESS) + + const result = await plugin.scanAddress(TEST_ETH_ADDRESS) + expect(result).toBe(true) + }) + + test('watchBlocks error handler should log errors', async function () { + // Get the error handler that was passed to watchBlocks + const errorHandler = mockClient.watchBlocks.mock.calls[0][0].onError + + // Call the error handler + errorHandler(new Error('Test error')) + + // Check that the error was logged + expect(consoleSpy.error).toHaveBeenCalledWith( + 'test-evm (https://ethereum.example.com/rpc):', + 'watchBlocks error', + expect.any(Error) + ) + }) +}) diff --git a/yarn.lock b/yarn.lock index 533ca4a..e509f69 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@adraffy/ens-normalize@^1.10.1": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz#42cc67c5baa407ac25059fcd7d405cc5ecdb0c33" + integrity sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg== + "@ampproject/remapping@^2.2.0": version "2.3.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" @@ -550,6 +555,30 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@noble/curves@1.8.1": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.8.1.tgz#19bc3970e205c99e4bdb1c64a4785706bce497ff" + integrity sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ== + dependencies: + "@noble/hashes" "1.7.1" + +"@noble/curves@^1.6.0", "@noble/curves@~1.8.1": + version "1.8.2" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.8.2.tgz#8f24c037795e22b90ae29e222a856294c1d9ffc7" + integrity sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g== + dependencies: + "@noble/hashes" "1.7.2" + +"@noble/hashes@1.7.1": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.7.1.tgz#5738f6d765710921e7a751e00c20ae091ed8db0f" + integrity sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ== + +"@noble/hashes@1.7.2", "@noble/hashes@^1.5.0", "@noble/hashes@~1.7.1": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.7.2.tgz#d53c65a21658fb02f3303e7ee3ba89d6754c64b4" + integrity sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ== + "@nodelib/fs.scandir@2.1.3": version "2.1.3" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" @@ -576,6 +605,28 @@ resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.7.0.tgz#b139c81999c23e3c8d3c0a7234480e945920fc40" integrity sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw== +"@scure/base@~1.2.2", "@scure/base@~1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.2.4.tgz#002eb571a35d69bdb4c214d0995dff76a8dcd2a9" + integrity sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ== + +"@scure/bip32@1.6.2", "@scure/bip32@^1.5.0": + version "1.6.2" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.6.2.tgz#093caa94961619927659ed0e711a6e4bf35bffd0" + integrity sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw== + dependencies: + "@noble/curves" "~1.8.1" + "@noble/hashes" "~1.7.1" + "@scure/base" "~1.2.2" + +"@scure/bip39@1.5.4", "@scure/bip39@^1.4.0": + version "1.5.4" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.5.4.tgz#07fd920423aa671be4540d59bdd344cc1461db51" + integrity sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA== + dependencies: + "@noble/hashes" "~1.7.1" + "@scure/base" "~1.2.4" + "@sinclair/typebox@^0.27.8": version "0.27.8" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" @@ -777,6 +828,11 @@ "@typescript-eslint/types" "4.10.0" eslint-visitor-keys "^2.0.0" +abitype@1.0.8, abitype@^1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.8.tgz#3554f28b2e9d6e9f35eb59878193eabd1b9f46ba" + integrity sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg== + acorn-jsx@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" @@ -1737,6 +1793,11 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +eventemitter3@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" + integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== + execa@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" @@ -2297,6 +2358,11 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= +isows@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.6.tgz#0da29d706fa51551c663c627ace42769850f86e7" + integrity sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw== + istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" @@ -3171,6 +3237,19 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" +ox@0.6.9: + version "0.6.9" + resolved "https://registry.yarnpkg.com/ox/-/ox-0.6.9.tgz#da1ee04fa10de30c8d04c15bfb80fe58b1f554bd" + integrity sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug== + dependencies: + "@adraffy/ens-normalize" "^1.10.1" + "@noble/curves" "^1.6.0" + "@noble/hashes" "^1.5.0" + "@scure/bip32" "^1.5.0" + "@scure/bip39" "^1.4.0" + abitype "^1.0.6" + eventemitter3 "5.0.1" + p-limit@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" @@ -4063,10 +4142,10 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -typescript@^4.6.2: - version "4.6.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.2.tgz#fe12d2727b708f4eef40f51598b3398baa9611d4" - integrity sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg== +typescript@^5.8.3: + version "5.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e" + integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== undici-types@~5.25.1: version "5.25.3" @@ -4110,6 +4189,20 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +viem@^2.27.0: + version "2.27.0" + resolved "https://registry.yarnpkg.com/viem/-/viem-2.27.0.tgz#edfca8e107d96eecff70d6c4f049c5e43422f902" + integrity sha512-pKw2dcwDi6TaWlTzLHYazOgjO1GgbUpE1zdLsLNSiCjHNrMTpL/teL0wVHnJDLiB2tR5CL19LBqefYNtRUkH5Q== + dependencies: + "@noble/curves" "1.8.1" + "@noble/hashes" "1.7.1" + "@scure/bip32" "1.6.2" + "@scure/bip39" "1.5.4" + abitype "1.0.8" + isows "1.0.6" + ox "0.6.9" + ws "8.18.1" + wait-for-expect@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/wait-for-expect/-/wait-for-expect-3.0.2.tgz#d2f14b2f7b778c9b82144109c8fa89ceaadaa463" @@ -4172,6 +4265,11 @@ write-file-atomic@^4.0.2: imurmurhash "^0.1.4" signal-exit "^3.0.7" +ws@8.18.1: + version "8.18.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.1.tgz#ea131d3784e1dfdff91adb0a4a116b127515e3cb" + integrity sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w== + ws@^8.18.0: version "8.18.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" From dc0eacbb030dc09696888284b018e9422a5236f5 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Wed, 16 Apr 2025 12:32:33 -0700 Subject: [PATCH 2/2] Define error log file in pm2.json --- pm2.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pm2.json b/pm2.json index 1704b4f..df534f1 100644 --- a/pm2.json +++ b/pm2.json @@ -3,7 +3,8 @@ { "name": "changeServer", "script": "./lib/index.js", - "out_file": "/var/log/pm2/changeServer.log" + "out_file": "/var/log/pm2/changeServer.log", + "error_file": "/var/log/pm2/changeServer.err.log" } ] }