diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index f887e7d7..b4caf7d0 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -15,3 +15,4 @@ export * from './job-runner'; export * from './index-block'; export * from './fill'; export * from './peer'; +export * from './utils'; diff --git a/packages/erc20-watcher/src/indexer.ts b/packages/erc20-watcher/src/indexer.ts index 90dccab4..4861a143 100644 --- a/packages/erc20-watcher/src/indexer.ts +++ b/packages/erc20-watcher/src/indexer.ts @@ -158,7 +158,7 @@ export class Indexer implements IndexerInterface { if (this._serverMode === ETH_CALL_MODE) { const contract = new ethers.Contract(token, this._abi, this._ethProvider); - // eth_call doesnt support calling method by blockHash https://eth.wiki/json-rpc/API#the-default-block-parameter + // eth_call doesn't support calling method by blockHash https://eth.wiki/json-rpc/API#the-default-block-parameter const value = await contract.balanceOf(owner, { blockTag: blockHash }); result = { diff --git a/packages/mobymask-v2-watcher/package.json b/packages/mobymask-v2-watcher/package.json index 8e36db20..a61c2aba 100644 --- a/packages/mobymask-v2-watcher/package.json +++ b/packages/mobymask-v2-watcher/package.json @@ -24,7 +24,8 @@ "import-state:dev": "DEBUG=vulcanize:* ts-node src/cli/import-state.ts", "inspect-cid": "DEBUG=vulcanize:* ts-node src/cli/inspect-cid.ts", "index-block": "DEBUG=vulcanize:* ts-node src/cli/index-block.ts", - "peer": "DEBUG='vulcanize:*, laconic:*' node --enable-source-maps dist/cli/peer.js" + "peer": "DEBUG='vulcanize:*, laconic:*' node --enable-source-maps dist/cli/peer.js", + "peer-listener": "DEBUG='vulcanize:*, laconic:*' node --enable-source-maps dist/peer-listener.js" }, "repository": { "type": "git", @@ -42,6 +43,7 @@ "@cerc-io/ipld-eth-client": "^0.2.30", "@cerc-io/solidity-mapper": "^0.2.30", "@cerc-io/util": "^0.2.30", + "@cerc-io/peer": "^0.2.30", "@ethersproject/providers": "^5.4.4", "apollo-type-bigint": "^0.1.3", "debug": "^4.3.1", diff --git a/packages/mobymask-v2-watcher/src/indexer.ts b/packages/mobymask-v2-watcher/src/indexer.ts index a4428ad7..2de8b8c5 100644 --- a/packages/mobymask-v2-watcher/src/indexer.ts +++ b/packages/mobymask-v2-watcher/src/indexer.ts @@ -11,7 +11,7 @@ import { ethers } from 'ethers'; import { JsonFragment } from '@ethersproject/abi'; import { JsonRpcProvider } from '@ethersproject/providers'; import { EthClient } from '@cerc-io/ipld-eth-client'; -import { MappingKey, StorageLayout } from '@cerc-io/solidity-mapper'; +import { MappingKey, StorageLayout, getStorageValue } from '@cerc-io/solidity-mapper'; import { Indexer as BaseIndexer, IndexerInterface, @@ -270,11 +270,14 @@ export class Indexer implements IndexerInterface { defaultValue: any ): Promise { const [{ number }, syncStatus] = await Promise.all([ - this._ethProvider.send('eth_getHeaderByHash', [blockHash]), + // Laconicd doesn't support eth_getHeaderByHash + // this._ethProvider.send('eth_getHeaderByHash', [blockHash]), + this._ethProvider.getBlock(blockHash), this.getSyncStatus() ]); - const blockNumber = ethers.BigNumber.from(number).toNumber(); + // const blockNumber = ethers.BigNumber.from(number).toNumber(); + const blockNumber = number; let result: ValueResult = { value: defaultValue @@ -294,7 +297,17 @@ export class Indexer implements IndexerInterface { const storageLayout = this._storageLayoutMap.get(KIND_PHISHERREGISTRY); assert(storageLayout); - result = await this._baseIndexer.getStorageValue( + // Get storage value using ipld-eth-server + // result = await this._baseIndexer.getStorageValue( + // storageLayout, + // blockHash, + // contractAddress, + // storageVariableName, + // ...Object.values(mappingKeys) + // ); + + // Get storage value using ETH RPC endpoint + result = await this._getStorageValueRPC( storageLayout, blockHash, contractAddress, @@ -313,6 +326,30 @@ export class Indexer implements IndexerInterface { } as any; } + async _getStorageValueRPC (storageLayout: StorageLayout, blockHash: string, contractAddress: string, variable: string, ...mappingKeys: MappingKey[]): Promise { + const getStorageAt = async (params: { blockHash: string, contract: string, slot: string }) => { + const { blockHash, contract, slot } = params; + const value = await this._ethProvider.getStorageAt(contract, slot, blockHash); + + return { + value, + proof: { + // Returning null value as proof, since ethers library getStorageAt method doesn't return proof. + data: JSON.stringify(null) + } + }; + }; + + return getStorageValue( + storageLayout, + getStorageAt, + blockHash, + contractAddress, + variable, + ...mappingKeys + ); + } + async getStorageValue (storageLayout: StorageLayout, blockHash: string, contractAddress: string, variable: string, ...mappingKeys: MappingKey[]): Promise { return this._baseIndexer.getStorageValue( storageLayout, @@ -608,11 +645,19 @@ export class Indexer implements IndexerInterface { return this._baseIndexer.getAncestorAtDepth(blockHash, depth); } - // Get latest block using eth client. + // Get latest block using eth provider. async getLatestBlock (): Promise { - const { block } = await this._ethClient.getBlockByHash(); + // Use ipld-eth-server + // const { block } = await this._ethClient.getBlockByHash(); - return block; + // Use ETH RPC endpoint + const number = await this._ethProvider.getBlockNumber(); + const { hash } = await this._ethProvider.getBlock(number); + + return { + number, + hash + }; } // Get full transaction data. diff --git a/packages/mobymask-v2-watcher/src/libp2p-utils.ts b/packages/mobymask-v2-watcher/src/libp2p-utils.ts index a2c5f16f..8d951882 100644 --- a/packages/mobymask-v2-watcher/src/libp2p-utils.ts +++ b/packages/mobymask-v2-watcher/src/libp2p-utils.ts @@ -3,10 +3,13 @@ // import debug from 'debug'; -import { ethers } from 'ethers'; +import { ethers, Signer } from 'ethers'; +import { TransactionReceipt, TransactionResponse } from '@ethersproject/providers'; import { abi as PhisherRegistryABI } from './artifacts/PhisherRegistry.json'; +const log = debug('laconic:libp2p-utils'); + const contractInterface = new ethers.utils.Interface(PhisherRegistryABI); const MESSAGE_KINDS = { @@ -14,6 +17,71 @@ const MESSAGE_KINDS = { REVOKE: 'revoke' }; +export async function sendMessageToL2 ( + signer: Signer, + { contractAddress, gasLimit }: { + contractAddress: string, + gasLimit: number + }, + data: any +): Promise { + const { kind, message } = data; + const contract = new ethers.Contract(contractAddress, PhisherRegistryABI, signer); + let receipt: TransactionReceipt | undefined; + + try { + switch (kind) { + case MESSAGE_KINDS.INVOKE: { + const signedInvocations = message; + + const transaction: TransactionResponse = await contract.invoke( + signedInvocations, + // Setting gasLimit as eth_estimateGas call takes too long in L2 chain + { gasLimit } + ); + + receipt = await transaction.wait(); + + break; + } + + case MESSAGE_KINDS.REVOKE: { + const { signedDelegation, signedIntendedRevocation } = message; + + const transaction: TransactionResponse = await contract.revokeDelegation( + signedDelegation, + signedIntendedRevocation, + // Setting gasLimit as eth_estimateGas call takes too long in L2 chain + { gasLimit } + ); + + receipt = await transaction.wait(); + + break; + } + + default: { + log(`Handler for libp2p message kind ${kind} not implemented`); + log(JSON.stringify(message, null, 2)); + break; + } + } + + if (receipt) { + log(`Transaction receipt for ${kind} message`, { + to: receipt.to, + blockNumber: receipt.blockNumber, + blockHash: receipt.blockHash, + transactionHash: receipt.transactionHash, + effectiveGasPrice: receipt.effectiveGasPrice.toString(), + gasUsed: receipt.gasUsed.toString() + }); + } + } catch (error) { + log(error); + } +} + export function parseLibp2pMessage (log: debug.Debugger, peerId: string, data: any): void { log('Received a message on mobymask P2P network from peer:', peerId); const { kind, message } = data; @@ -57,17 +125,5 @@ function _parseRevocation (log: debug.Debugger, msg: any): void { log('Signed delegation:'); log(JSON.stringify(signedDelegation, null, 2)); log('Signed intention to revoke:'); - const stringifiedSignedIntendedRevocation = JSON.stringify( - signedIntendedRevocation, - (key, value) => { - if (key === 'delegationHash' && value.type === 'Buffer') { - // Show hex value for delegationHash instead of Buffer - return ethers.utils.hexlify(Buffer.from(value)); - } - - return value; - }, - 2 - ); - log(stringifiedSignedIntendedRevocation); + log(JSON.stringify(signedIntendedRevocation, null, 2)); } diff --git a/packages/mobymask-v2-watcher/src/peer-listener.ts b/packages/mobymask-v2-watcher/src/peer-listener.ts new file mode 100644 index 00000000..ff715361 --- /dev/null +++ b/packages/mobymask-v2-watcher/src/peer-listener.ts @@ -0,0 +1,105 @@ +import debug from 'debug'; +import { hideBin } from 'yargs/helpers'; +import yargs from 'yargs'; +import assert from 'assert'; + +import { Config, DEFAULT_CONFIG_PATH, getConfig, initClients } from '@cerc-io/util'; +import { + PeerInitConfig, + PeerIdObj + // @ts-expect-error https://github.com/microsoft/TypeScript/issues/49721#issuecomment-1319854183 +} from '@cerc-io/peer'; + +import { sendMessageToL2 } from './libp2p-utils'; +import { readPeerId } from '@cerc-io/cli'; +import { ethers } from 'ethers'; + +const log = debug('vulcanize:peer-listener'); + +const DEFAULT_GAS_LIMIT = 500000; + +interface Arguments { + configFile: string; + privateKey: string; + contractAddress: string; + gasLimit: number; +} + +export const main = async (): Promise => { + const argv = _getArgv(); + const config: Config = await getConfig(argv.configFile); + const { ethProvider } = await initClients(config); + + const p2pConfig = config.server.p2p; + const peerConfig = p2pConfig.peer; + assert(peerConfig, 'Peer config not set'); + + const { Peer } = await import('@cerc-io/peer'); + + let peerIdObj: PeerIdObj | undefined; + if (peerConfig.peerIdFile) { + peerIdObj = readPeerId(peerConfig.peerIdFile); + } + + const peer = new Peer(peerConfig.relayMultiaddr, true); + + const peerNodeInit: PeerInitConfig = { + pingInterval: peerConfig.pingInterval, + pingTimeout: peerConfig.pingTimeout, + maxRelayConnections: peerConfig.maxRelayConnections, + relayRedialInterval: peerConfig.relayRedialInterval, + maxConnections: peerConfig.maxConnections, + dialTimeout: peerConfig.dialTimeout, + enableDebugInfo: peerConfig.enableDebugInfo + }; + + await peer.init(peerNodeInit, peerIdObj); + const wallet = new ethers.Wallet(argv.privateKey, ethProvider); + + peer.subscribeTopic(peerConfig.pubSubTopic, (peerId, data) => { + log('Received a message on mobymask P2P network from peer:', peerId); + + // TODO: throttle message handler + sendMessageToL2(wallet, argv, data); + }); + + log(`Peer ID: ${peer.peerId?.toString()}`); +}; + +const _getArgv = (): Arguments => { + return yargs(hideBin(process.argv)).parserConfiguration({ + 'parse-numbers': false + }).options({ + configFile: { + alias: 'config-file', + describe: 'configuration file path (toml)', + type: 'string', + default: DEFAULT_CONFIG_PATH + }, + privateKey: { + alias: 'private-key', + demandOption: true, + describe: 'Private key of the account to use for eth_call', + type: 'string' + }, + contractAddress: { + alias: 'contract', + demandOption: true, + describe: 'Address of MobyMask contract', + type: 'string' + }, + gasLimit: { + alias: 'gas-limit', + describe: 'Gas limit for eth txs', + type: 'number', + default: DEFAULT_GAS_LIMIT + } + // https://github.com/yargs/yargs/blob/main/docs/typescript.md?plain=1#L83 + }).parseSync(); +}; + +main().then(() => { + log('Starting peer...'); +}).catch(err => { + log(err); +}); diff --git a/packages/mobymask-v2-watcher/tsconfig.json b/packages/mobymask-v2-watcher/tsconfig.json index 4e55bfd1..55bb2c33 100644 --- a/packages/mobymask-v2-watcher/tsconfig.json +++ b/packages/mobymask-v2-watcher/tsconfig.json @@ -5,8 +5,8 @@ /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */ - "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ - "lib": ["es2019"], /* Specify library files to be included in the compilation. */ + "module": "CommonJS", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + "lib": [ "ES5", "ES6", "ES2020" ], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ @@ -44,7 +44,7 @@ // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ /* Module Resolution Options */ - // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + "moduleResolution": "Node16", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ diff --git a/packages/solidity-mapper/test/utils.ts b/packages/solidity-mapper/test/utils.ts index 4cf96774..a8a5170b 100644 --- a/packages/solidity-mapper/test/utils.ts +++ b/packages/solidity-mapper/test/utils.ts @@ -13,7 +13,7 @@ import isArray from 'lodash/isArray'; import { StorageLayout, GetStorageAt } from '../src'; -// storageLayout doesnt exist in type CompilerOutput doesnt. +// storageLayout doesn't exist in type CompilerOutput doesn't. // Extending CompilerOutput type to include storageLayout property. interface StorageCompilerOutput extends CompilerOutput { contracts: { @@ -82,7 +82,7 @@ export const getStorageAt: GetStorageAt = async ({ blockHash, contract, slot }) return { value, proof: { - // Returning null value as proof, since ethers library getStorageAt method doesnt return proof. + // Returning null value as proof, since ethers library getStorageAt method doesn't return proof. // This function is used in tests to mock the getStorageAt method of ipld-eth-client which returns proof along with value. data: JSON.stringify(null) }