diff --git a/packages/ethereum-storage/package.json b/packages/ethereum-storage/package.json index bea62766f3..4c7566c6bb 100644 --- a/packages/ethereum-storage/package.json +++ b/packages/ethereum-storage/package.json @@ -44,6 +44,7 @@ "@requestnetwork/smart-contracts": "0.48.0", "@requestnetwork/types": "0.54.0", "@requestnetwork/utils": "0.54.0", + "@thirdweb-dev/engine": "0.0.19", "ethers": "5.7.2", "form-data": "3.0.0", "qs": "6.11.2", diff --git a/packages/ethereum-storage/src/index.ts b/packages/ethereum-storage/src/index.ts index 40dbc32440..e223441dd4 100644 --- a/packages/ethereum-storage/src/index.ts +++ b/packages/ethereum-storage/src/index.ts @@ -6,3 +6,4 @@ export { EthereumStorage } from './ethereum-storage'; export { EthereumTransactionSubmitter } from './ethereum-tx-submitter'; export { GasFeeDefiner } from './gas-fee-definer'; export { IpfsStorage } from './ipfs-storage'; +export { ThirdwebTransactionSubmitter } from './thirdweb/thirdweb-tx-submitter'; diff --git a/packages/ethereum-storage/src/thirdweb/thirdweb-tx-submitter.ts b/packages/ethereum-storage/src/thirdweb/thirdweb-tx-submitter.ts new file mode 100644 index 0000000000..3989ed1782 --- /dev/null +++ b/packages/ethereum-storage/src/thirdweb/thirdweb-tx-submitter.ts @@ -0,0 +1,218 @@ +import { BigNumber, utils, providers } from 'ethers'; +import { StorageTypes, LogTypes } from '@requestnetwork/types'; +import { Engine } from '@thirdweb-dev/engine'; +import { SimpleLogger } from '@requestnetwork/utils'; +import { requestHashSubmitterArtifact } from '@requestnetwork/smart-contracts'; +import type { CurrencyTypes } from '@requestnetwork/types'; +import { getChainId } from './types'; + +export interface ThirdwebSubmitterOptions { + /** + * The URL of your Thirdweb Engine instance + */ + engineUrl: string; + + /** + * The access token for Thirdweb Engine + */ + accessToken: string; + + /** + * The address of the wallet configured in Thirdweb Engine + * This is the wallet that will sign and send transactions + */ + backendWalletAddress: string; + + /** + * Network name (e.g. 'gnosis', 'sepolia', etc.) + */ + network: CurrencyTypes.EvmChainName; + + /** + * Optional logger instance + */ + logger?: LogTypes.ILogger; + + /** + * Optional RPC URL for the network. If not provided, will use default public RPC. + */ + rpcUrl?: string; +} + +/** + * Handles the submission of IPFS CID hashes using Thirdweb Engine. + * Thirdweb Engine manages transaction signing, fee estimation, and automatically + * handles retries for failed transactions. + */ +export class ThirdwebTransactionSubmitter implements StorageTypes.ITransactionSubmitter { + private readonly logger: LogTypes.ILogger; + private readonly engine: Engine; + private readonly backendWalletAddress: string; + private readonly provider: providers.Provider; + + // Public variables instead of getters/setters + public network: CurrencyTypes.EvmChainName; + public hashSubmitterAddress: string; + + constructor({ + engineUrl, + accessToken, + backendWalletAddress, + network, + logger, + rpcUrl, + }: ThirdwebSubmitterOptions) { + this.logger = logger || new SimpleLogger(); + this.engine = new Engine({ + url: engineUrl, + accessToken: accessToken, + }); + this.backendWalletAddress = backendWalletAddress; + this.network = network; + // Get the hash submitter address for the specified network + this.hashSubmitterAddress = requestHashSubmitterArtifact.getAddress(network); + + // Initialize provider with RPC URL if provided, otherwise use default network name + this.provider = rpcUrl + ? new providers.JsonRpcProvider(rpcUrl) + : providers.getDefaultProvider(network); + } + + async initialize(): Promise { + const chainId = getChainId(this.network); + this.logger.info( + `Initializing ThirdwebTransactionSubmitter for network ${this.network} (chainId: ${chainId})`, + ); + + // Check Engine connection + try { + await this.engine.backendWallet.getBalance(chainId, this.backendWalletAddress); + this.logger.info('Successfully connected to Thirdweb Engine'); + } catch (error) { + throw new Error(`Failed to connect to Thirdweb Engine due to error: ${error}`); + } + } + + /** + * Submits an IPFS CID hash via Thirdweb Engine + * @param ipfsHash - The IPFS CID hash to submit + * @param ipfsSize - The size of the data on IPFS + * @returns A transaction object compatible with ethers.js TransactionResponse interface + */ + async submit(ipfsHash: string, ipfsSize: number): Promise { + this.logger.info(`Submitting IPFS CID ${ipfsHash} with size ${ipfsSize} via Thirdweb Engine`); + const preparedTx = await this.prepareSubmit(ipfsHash, ipfsSize); + const chainId = getChainId(this.network); + + try { + const result = await this.engine.backendWallet.sendTransaction( + chainId, + this.backendWalletAddress, + { + toAddress: preparedTx.to, + data: preparedTx.data, + value: preparedTx.value ? preparedTx.value.toString() : '0', + }, + ); + + this.logger.info(`Transaction submitted. Queue ID: ${result.result.queueId}`); + + // Return a complete ethers.js TransactionResponse-compatible object for an EIP-1559 transaction + return { + // Only available for mined transactions + blockHash: null, + blockNumber: null, + timestamp: Math.floor(Date.now() / 1000), + transactionIndex: null, + + // Transaction details + hash: '0x0000000000000000000000000000000000000000000000000000000000000000', + from: this.backendWalletAddress, + chainId, + to: preparedTx.to, + nonce: 0, // Not available from Thirdweb Engine + gasLimit: BigNumber.from(2000000), + gasPrice: null, // Not used in EIP-1559 transactions + data: preparedTx.data, + value: preparedTx.value || BigNumber.from(0), + confirmations: 1, + + // EIP-1559 fields + maxPriorityFeePerGas: BigNumber.from(2000000000), // 2 Gwei tip + maxFeePerGas: BigNumber.from(50000000000), // 50 Gwei max fee + + // Transaction type (2: EIP-1559) + type: 2, + + // Signature components (not available) + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + v: 0, + + /** + * Returns a promise that resolves to a transaction receipt. + * + * This implements a "fire and forget" pattern - we submit the transaction + * to Thirdweb Engine and immediately assume success without waiting for + * or confirming that the transaction is actually mined. + * + * We don't use polling or webhook notifications to track transaction status. + * Thirdweb Engine handles retries and gas price adjustments internally. + * + * @returns A simplified TransactionReceipt compatible with ethers.js + */ + wait: async () => { + return { + // TransactionReceipt properties + transactionHash: '0x0000000000000000000000000000000000000000000000000000000000000000', + blockNumber: 0, + blockHash: '0x0000000000000000000000000000000000000000000000000000000000000000', + confirmations: 1, + status: 1, // 1 = success + from: this.backendWalletAddress, + to: preparedTx.to, + contractAddress: null, + transactionIndex: 0, + gasUsed: BigNumber.from(0), + cumulativeGasUsed: BigNumber.from(0), + effectiveGasPrice: BigNumber.from(0), + logs: [], + logsBloom: + '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + type: 2, // EIP-1559 transaction type + }; + }, + }; + } catch (error) { + this.logger.error('Failed to submit transaction through Thirdweb Engine', error); + throw error; + } + } + + /** + * Prepares the transaction for submitting an IPFS CID hash + * @param ipfsHash - The IPFS CID hash to submit + * @param ipfsSize - The size of the data on IPFS + * @returns A transaction request object compatible with ethers.js + */ + async prepareSubmit(ipfsHash: string, ipfsSize: number): Promise { + // Create contract interface for the hash submitter + const iface = requestHashSubmitterArtifact.getInterface(); + + // Get the fee from the contract - now using provider + const hashSubmitter = requestHashSubmitterArtifact.connect(this.network, this.provider); + const fee = await hashSubmitter.getFeesAmount(ipfsSize); + + // Encode function data + const data = iface.encodeFunctionData('submitHash', [ + ipfsHash, + utils.hexZeroPad(utils.hexlify(ipfsSize), 32), + ]); + + return { + to: this.hashSubmitterAddress, + data: data, + value: fee, + }; + } +} diff --git a/packages/ethereum-storage/src/thirdweb/types.ts b/packages/ethereum-storage/src/thirdweb/types.ts new file mode 100644 index 0000000000..3b2a1a1928 --- /dev/null +++ b/packages/ethereum-storage/src/thirdweb/types.ts @@ -0,0 +1,49 @@ +/** + * Configuration for the Thirdweb Engine + */ +export interface ThirdwebEngineConfig { + /** + * The URL of your Thirdweb Engine instance + */ + engineUrl: string; + + /** + * The access token for Thirdweb Engine + */ + accessToken: string; + + /** + * The address of the wallet configured in Thirdweb Engine + */ + backendWalletAddress: string; + + /** + * Secret for verifying webhook signatures + */ + webhookSecret?: string; + + /** + * Whether to use Thirdweb Engine + */ + enabled: boolean; +} + +/** + * Chain ID mapping for common networks + */ +export const networkToChainId: Record = { + mainnet: '1', + goerli: '5', + sepolia: '11155111', + xdai: '100', + private: '1337', +}; + +/** + * Get the chain ID for a given network + * @param network Network name + * @returns Chain ID + */ +export function getChainId(network: string): string { + return networkToChainId[network] || '1'; +} diff --git a/packages/ethereum-storage/test/thirdweb-tx-submitter.test.ts b/packages/ethereum-storage/test/thirdweb-tx-submitter.test.ts new file mode 100644 index 0000000000..09683648e4 --- /dev/null +++ b/packages/ethereum-storage/test/thirdweb-tx-submitter.test.ts @@ -0,0 +1,109 @@ +import { ThirdwebTransactionSubmitter } from '../src/thirdweb/thirdweb-tx-submitter'; +import { Engine } from '@thirdweb-dev/engine'; +import { LogTypes } from '@requestnetwork/types'; +import { requestHashSubmitterArtifact } from '@requestnetwork/smart-contracts'; + +// Mock the Thirdweb Engine +jest.mock('@thirdweb-dev/engine', () => { + return { + Engine: jest.fn().mockImplementation(() => ({ + getWallets: jest.fn().mockResolvedValue([{ address: '0x123' }]), + sendTransaction: jest.fn().mockResolvedValue({ + transactionHash: '0xabcdef1234567890', + }), + })), + }; +}); + +// Mock the hash submitter artifact +jest.mock('@requestnetwork/smart-contracts', () => { + return { + requestHashSubmitterArtifact: { + getAddress: jest.fn().mockReturnValue('0xf25186b5081ff5ce73482ad761db0eb0d25abfbf'), + getInterface: jest.fn().mockReturnValue({ + encodeFunctionData: jest.fn().mockReturnValue('0x1234abcd'), + }), + }, + }; +}); + +describe('ThirdwebTransactionSubmitter', () => { + const mockLogger: LogTypes.ILogger = { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + + const submitterOptions = { + engineUrl: 'https://engine.thirdweb.com', + accessToken: 'test-token', + backendWalletAddress: '0xbackendWalletAddress', + network: 'mainnet', + logger: mockLogger, + }; + + let txSubmitter: ThirdwebTransactionSubmitter; + + beforeEach(() => { + jest.clearAllMocks(); + txSubmitter = new ThirdwebTransactionSubmitter(submitterOptions); + }); + + it('can initialize', async () => { + await txSubmitter.initialize(); + expect(mockLogger.info).toHaveBeenCalledWith( + 'Initializing ThirdwebTransactionSubmitter for network mainnet (chainId: 1)', + ); + expect(mockLogger.info).toHaveBeenCalledWith('Successfully connected to Thirdweb Engine'); + }); + + it('can prepare submit', async () => { + const result = await txSubmitter.prepareSubmit('ipfshash', 100); + + expect(result).toEqual({ + to: '0xf25186b5081ff5ce73482ad761db0eb0d25abfbf', + data: '0x1234abcd', + value: expect.any(Object), + }); + + expect(requestHashSubmitterArtifact.getInterface).toHaveBeenCalled(); + expect(requestHashSubmitterArtifact.getInterface().encodeFunctionData).toHaveBeenCalledWith( + 'submitHash', + ['ipfshash', expect.any(String)], + ); + }); + + it('can submit', async () => { + const result = await txSubmitter.submit('ipfshash', 100); + + expect(result).toEqual({ + hash: '0xabcdef1234567890', + wait: expect.any(Function), + }); + + expect(mockLogger.info).toHaveBeenCalledWith( + 'Submitting hash ipfshash with size 100 via Thirdweb Engine', + ); + + const engineInstance = (Engine as jest.Mock).mock.results[0].value; + expect(engineInstance.sendTransaction).toHaveBeenCalledWith({ + chainId: 1, + fromAddress: '0xbackendWalletAddress', + toAddress: '0xf25186b5081ff5ce73482ad761db0eb0d25abfbf', + data: '0x1234abcd', + value: '0', + }); + }); + + it('should handle errors during submission', async () => { + const engineInstance = (Engine as jest.Mock).mock.results[0].value; + engineInstance.sendTransaction.mockRejectedValueOnce(new Error('Transaction failed')); + + await expect(txSubmitter.submit('ipfshash', 100)).rejects.toThrow('Transaction failed'); + expect(mockLogger.error).toHaveBeenCalledWith( + 'Failed to submit transaction through Thirdweb Engine', + expect.any(Error), + ); + }); +}); diff --git a/packages/request-node/README.md b/packages/request-node/README.md index 56361f839a..1bb02d93eb 100644 --- a/packages/request-node/README.md +++ b/packages/request-node/README.md @@ -221,42 +221,25 @@ Default values correspond to the basic configuration used to run a server in a t #### Options: -- `--port` Port for the server to listen for API requests - - Default value: `3000` - - Environment variable name: `$PORT` -- `--networkId` Id of the Ethereum network used - - Default value: `0` - - Environment variable name: `$ETHEREUM_NETWORK_ID` -- `--providerUrl` URL of the web3 provider for Ethereum - - Default value: `http://localhost:8545` - - Environment variable name: `$WEB3_PROVIDER_URL` -- `--ipfsUrl` URL of the IPFS gateway - - Default value: `http://localhost:5001` - - Environment variable name: `$IPFS_URL` -- `--ipfsTimeout` Timeout threshold to connect to the IPFS gateway - - Default value: `10000` - - Environment variable name: `$IPFS_TIMEOUT` -- `--blockConfirmations` The number of block confirmations to consider a transaction successful - - Default value: `2` - - Environment variable name: `$BLOCK_CONFIRMATIONS` -- `--storageConcurrency` Maximum number of concurrent calls to Ethereum or IPFS - - Default value: `'200'` - - Environment variable name: `$STORAGE_MAX_CONCURRENCY` -- `--logLevel` The maximum level of messages we will log - - Environment variable name: `$LOG_LEVEL` - - Available levels: ERROR, WARN, INFO and DEBUG -- `--logMode` Defines the log format to use - - Environment variable name: `$LOG_MODE` - - Available modes: - - `human` is a more human readable log to display during development - - `machine` is better for parsing on CI or deployments -- `--persistTransactionTimeout` Defines the delay in seconds to wait before sending a timeout when creating or updating a request - - Default value: 600 - - Environment variable name: `$PERSIST_TRANSACTION_TIMEOUT` -- `--externalUrl` External url of the node (used to identified where the buffer data are stored before being broadcasted on ethereum) - - Environment variable name: `$EXTERNAL_URL` -- `--graphNodeUrl` External url of the Graph node, if any. If specified, this will replace the traditional data access with the Graph implementation. Default is undefined. See [TheGraph mode](#thegraph-mode). - - Environment variable name: `$GRAPH_NODE_URL` +| Option | Environment Variable | Description | Required | Default Value | +| -------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------ | ----------------------------------- | +| `--port` | `PORT` | Port for the server to listen for API requests | No | `3000` | +| `--networkId` | `ETHEREUM_NETWORK_ID` | Id of the Ethereum network used | No | `0` | +| `--providerUrl` | `WEB3_PROVIDER_URL` | URL of the web3 provider for Ethereum | No | `http://localhost:8545` | +| `--ipfsUrl` | `IPFS_URL` | URL of the IPFS gateway | No | `http://localhost:5001` | +| `--ipfsTimeout` | `IPFS_TIMEOUT` | Timeout threshold to connect to the IPFS gateway | No | `10000` | +| `--blockConfirmations` | `BLOCK_CONFIRMATIONS` | The number of block confirmations to consider a transaction successful | No | `2` | +| `--storageConcurrency` | `STORAGE_MAX_CONCURRENCY` | Maximum number of concurrent calls to Ethereum or IPFS | No | `200` | +| `--logLevel` | `LOG_LEVEL` | The maximum level of messages we will log (ERROR, WARN, INFO or DEBUG) | No | `INFO` | +| `--logMode` | `LOG_MODE` | The log format to use (human or machine) | No | `human` | +| `--persistTransactionTimeout` | `PERSIST_TRANSACTION_TIMEOUT` | Defines the delay in seconds to wait before sending a timeout when creating or updating a request | No | `600` | +| `--externalUrl` | `EXTERNAL_URL` | External url of the node (used to identify where the buffer data are stored) | No | - | +| `--graphNodeUrl` | `GRAPH_NODE_URL` | External url of the Graph node. See [TheGraph mode](#thegraph-mode) | No | - | +| `--thirdwebEngineUrl` | `THIRDWEB_ENGINE_URL` | URL of your Thirdweb Engine instance | **Yes** | - | +| `--thirdwebAccessToken` | `THIRDWEB_ACCESS_TOKEN` | Access token for Thirdweb Engine | **Yes** | - | +| `--thirdwebBackendWalletAddress` | `THIRDWEB_BACKEND_WALLET_ADDRESS` | Address of the wallet configured in Thirdweb Engine | **Yes** | - | +| `--thirdwebWebhookSecret` | `THIRDWEB_WEBHOOK_SECRET` | Secret for verifying webhook signatures | No | - | +| - | `MNEMONIC` | The mnemonic for generating the wallet private key | **Yes** (except on private networks) | `candy maple...` (only for testing) | #### Mnemonic @@ -336,6 +319,25 @@ yarn start Open a browser and navigate towards: http://localhost:3000/status You can see the details of your local Request & IPFS nodes. +## Thirdweb Engine Integration + +The Request Node uses Thirdweb Engine for transaction submission, which offers several advantages: + +- No need to manage private keys in the Request Node +- Better transaction management and monitoring +- Automated gas price optimization and retry mechanisms +- Webhook notifications for transaction status + +### Setting Up Thirdweb Engine + +1. Deploy Thirdweb Engine by following the [official documentation](https://portal.thirdweb.com/engine/getting-started) +2. Create a wallet in Thirdweb Engine for the Request Node +3. Ensure the wallet has sufficient funds for gas costs +4. Generate an access token with appropriate permissions +5. Configure the Request Node with the required environment variables (see Options table above) + +**Note:** The Request Node no longer supports transaction submission through local wallets. All transactions are processed through Thirdweb Engine. + ## Contributing Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. diff --git a/packages/request-node/src/config.ts b/packages/request-node/src/config.ts index 48640e077b..dc872c49eb 100644 --- a/packages/request-node/src/config.ts +++ b/packages/request-node/src/config.ts @@ -41,7 +41,6 @@ const defaultValues = { mode: LogMode.human, }, server: { - headers: '{}', port: 3000, }, wallet: { @@ -51,6 +50,12 @@ const defaultValues = { litProtocolRPC: 'https://yellowstone-rpc.litprotocol.com', litProtocolCapacityCreditsUsage: '1', litProtocolCapacityCreditsExpirationInSeconds: 10 * 60, // 10 minutes + thirdweb: { + engineUrl: '', + accessToken: '', + backendWalletAddress: '', + webhookSecret: '', + }, }; const getOption = ( @@ -83,6 +88,42 @@ export const getLitProtocolNetwork = makeOption( defaultValues.litProtocolNetwork, ); +/** + * Get Thirdweb Engine URL from command line argument, environment variables or default values + */ +export const getThirdwebEngineUrl = makeOption( + 'thirdwebEngineUrl', + 'THIRDWEB_ENGINE_URL', + defaultValues.thirdweb.engineUrl, +); + +/** + * Get Thirdweb Access Token from command line argument, environment variables or default values + */ +export const getThirdwebAccessToken = makeOption( + 'thirdwebAccessToken', + 'THIRDWEB_ACCESS_TOKEN', + defaultValues.thirdweb.accessToken, +); + +/** + * Get Thirdweb Backend Wallet Address from command line argument, environment variables or default values + */ +export const getThirdwebBackendWalletAddress = makeOption( + 'thirdwebBackendWalletAddress', + 'THIRDWEB_BACKEND_WALLET_ADDRESS', + defaultValues.thirdweb.backendWalletAddress, +); + +/** + * Get Thirdweb Webhook Secret from command line argument, environment variables or default values + */ +export const getThirdwebWebhookSecret = makeOption( + 'thirdwebWebhookSecret', + 'THIRDWEB_WEBHOOK_SECRET', + defaultValues.thirdweb.webhookSecret, +); + /** * Get the litProtocolNetwork from command line argument, environment variables or default values to send with the API responses */ @@ -240,9 +281,12 @@ export function getHelpMessage(): string { OPTIONS SERVER OPTIONS port (${defaultValues.server.port})\t\t\t\tPort for the server to listen for API requests - headers (${ - defaultValues.server.headers - })\t\t\t\tCustom headers to send with the API responses + + THIRDWEB ENGINE OPTIONS + thirdwebEngineUrl\t\t\t\tURL of your Thirdweb Engine instance (REQUIRED) + thirdwebAccessToken\t\t\t\tAccess token for Thirdweb Engine (REQUIRED) + thirdwebBackendWalletAddress\t\tAddress of the wallet configured in Thirdweb Engine (REQUIRED) + thirdwebWebhookSecret\t\t\tSecret for verifying webhook signatures (optional) THE GRAPH OPTIONS graphNodeUrl (${defaultValues.storage.thegraph.nodeUrl})\t\t\t\tURL of the Graph node @@ -280,13 +324,13 @@ export function getHelpMessage(): string { logMode (${defaultValues.log.mode})\t\t\tThe node log mode (human or machine) EXAMPLE - yarn start --port 5000 --networkId 1 + yarn start --port 5000 --networkId 1 --thirdwebEngineUrl=https://engine.thirdweb.io --thirdwebAccessToken=your_token --thirdwebBackendWalletAddress=0x123... - All options are optional, not specified options are read from environment variables - If the environment variable is not specified, default value is used + All options except Thirdweb Engine options are optional. Thirdweb Engine options are required and can be set via environment variables if not specified in command line. Default mnemonic is: ${defaultValues.wallet.mnemonic} + NOTE: This mnemonic should ONLY be used for testing on private networks. `; return message; @@ -306,5 +350,35 @@ export const getConfigDisplay = (): string => { Lit Protocol RPC: ${getLitProtocolRPC()} Lit Protocol Capacity Credits Uses: ${getLitProtocolCapacityCreditsUsage()} Lit Protocol Capacity Credits Expiration in seconds: ${getLitProtocolCapacityCreditsExpirationInSeconds()} + Thirdweb Engine URL: ${getThirdwebEngineUrl()} + Thirdweb Backend Wallet: ${getThirdwebBackendWalletAddress()} `; }; + +/** + * Check if all required Thirdweb configuration values are provided + * @throws Error if required configuration is missing + */ +export function validateThirdwebConfig(): void { + const engineUrl = getThirdwebEngineUrl(); + const accessToken = getThirdwebAccessToken(); + const backendWalletAddress = getThirdwebBackendWalletAddress(); + + if (!engineUrl) { + throw new Error( + 'Thirdweb Engine URL is required. Set THIRDWEB_ENGINE_URL environment variable or use --thirdwebEngineUrl option.', + ); + } + + if (!accessToken) { + throw new Error( + 'Thirdweb Access Token is required. Set THIRDWEB_ACCESS_TOKEN environment variable or use --thirdwebAccessToken option.', + ); + } + + if (!backendWalletAddress) { + throw new Error( + 'Thirdweb Backend Wallet Address is required. Set THIRDWEB_BACKEND_WALLET_ADDRESS environment variable or use --thirdwebBackendWalletAddress option.', + ); + } +} diff --git a/packages/request-node/src/dataAccess.ts b/packages/request-node/src/dataAccess.ts index 9f51609b41..c65a0bee9c 100644 --- a/packages/request-node/src/dataAccess.ts +++ b/packages/request-node/src/dataAccess.ts @@ -1,49 +1,56 @@ -import { providers, Wallet } from 'ethers'; -import { NonceManager } from '@ethersproject/experimental'; -import { CurrencyTypes, DataAccessTypes, LogTypes, StorageTypes } from '@requestnetwork/types'; - -import * as config from './config'; import { TheGraphDataAccess } from '@requestnetwork/thegraph-data-access'; +import { EthereumStorage, ThirdwebTransactionSubmitter } from '@requestnetwork/ethereum-storage'; import { PendingStore } from '@requestnetwork/data-access'; -import { EthereumStorage, EthereumTransactionSubmitter } from '@requestnetwork/ethereum-storage'; +import { CurrencyTypes, LogTypes, StorageTypes } from '@requestnetwork/types'; +import * as config from './config'; -export function getDataAccess( +/** + * Creates and returns a data access instance + * @param network The Ethereum network to use + * @param ipfsStorage The IPFS storage instance + * @param logger Logger instance + * @param graphNodeUrl Graph Node endpoint URL + * @param blockConfirmations Number of block confirmations to wait for + * @returns A data access instance + */ +export async function getDataAccess( network: CurrencyTypes.EvmChainName, ipfsStorage: StorageTypes.IIpfsStorage, logger: LogTypes.ILogger, -): DataAccessTypes.IDataAccess { - const graphNodeUrl = config.getGraphNodeUrl(); + graphNodeUrl: string, + blockConfirmations: number, +): Promise { + // Validate that all required Thirdweb config options are set + config.validateThirdwebConfig(); - const wallet = Wallet.fromMnemonic(config.getMnemonic()).connect( - new providers.StaticJsonRpcProvider(config.getStorageWeb3ProviderUrl()), - ); + logger.info('Using Thirdweb Engine for transaction submission'); - const signer = new NonceManager(wallet); - - const gasPriceMin = config.getGasPriceMin(); - const gasPriceMax = config.getGasPriceMax(); - const gasPriceMultiplier = config.getGasPriceMultiplier(); - const blockConfirmations = config.getBlockConfirmations(); - const txSubmitter = new EthereumTransactionSubmitter({ + // Create ThirdwebTransactionSubmitter + const txSubmitter = new ThirdwebTransactionSubmitter({ + engineUrl: config.getThirdwebEngineUrl(), + accessToken: config.getThirdwebAccessToken(), + backendWalletAddress: config.getThirdwebBackendWalletAddress(), network, logger, - gasPriceMin, - gasPriceMax, - gasPriceMultiplier, - signer, }); - const pendingStore = new PendingStore(); + + // Initialize the transaction submitter + await txSubmitter.initialize(); + + // Create Ethereum Storage with the transaction submitter const storage = new EthereumStorage({ ipfsStorage, txSubmitter, - logger, blockConfirmations, + logger, }); + + // Create and return TheGraphDataAccess return new TheGraphDataAccess({ graphql: { url: graphNodeUrl }, storage, network, logger, - pendingStore, + pendingStore: new PendingStore(), }); } diff --git a/packages/request-node/src/server.ts b/packages/request-node/src/server.ts index 2422492cfd..eaebecff15 100755 --- a/packages/request-node/src/server.ts +++ b/packages/request-node/src/server.ts @@ -8,6 +8,7 @@ import ConfirmedTransactionStore from './request/confirmedTransactionStore'; import { EvmChains } from '@requestnetwork/currency'; import { getEthereumStorageNetworkNameFromId } from '@requestnetwork/ethereum-storage'; import { SubgraphClient } from '@requestnetwork/thegraph-data-access'; +import { StorageTypes } from '@requestnetwork/types'; // Initialize the node logger const logger = new Logger(config.getLogLevel(), config.getLogMode()); @@ -21,10 +22,32 @@ const getNetwork = () => { return network; }; -export const getRequestNode = (): RequestNode => { +// Initialize the data access layer +async function initializeDataAccess(ipfsStorage: StorageTypes.IIpfsStorage) { + // Get configuration values const network = getNetwork(); - const storage = getDataStorage(logger); - const dataAccess = getDataAccess(network, storage, logger); + const graphqlUrl = config.getGraphNodeUrl(); + const blockConfirmations = config.getBlockConfirmations(); + + // Create data access with Thirdweb transaction submitter + const dataAccess = await getDataAccess( + network, + ipfsStorage, + logger, + graphqlUrl, + blockConfirmations, + ); + + return dataAccess; +} + +export const getRequestNode = async (): Promise => { + const network = getNetwork(); + const ipfsStorage = getDataStorage(logger); + await ipfsStorage.initialize(); + + // Use the initialized data access + const dataAccess = await initializeDataAccess(ipfsStorage); // we access the subgraph client directly, not through the data access, // because this feature is specific to RN use with Request Node. Without a node, @@ -34,12 +57,13 @@ export const getRequestNode = (): RequestNode => { network, ); - return new RequestNode(dataAccess, storage, confirmedTransactionStore, logger); + return new RequestNode(dataAccess, ipfsStorage, confirmedTransactionStore, logger); }; +// Main server setup export const startNode = async (): Promise => { const port = config.getServerPort(); - const requestNode = getRequestNode(); + const requestNode = await getRequestNode(); const server = withShutdown( requestNode.listen(port, () => { logger.info(`Listening on port ${port}`); diff --git a/yarn.lock b/yarn.lock index 445b3f98d6..d1f126975d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6345,6 +6345,11 @@ dependencies: defer-to-connect "^2.0.1" +"@thirdweb-dev/engine@0.0.19": + version "0.0.19" + resolved "https://registry.yarnpkg.com/@thirdweb-dev/engine/-/engine-0.0.19.tgz#d875faf82862d30aa9741abb8ac3d6c74354a304" + integrity sha512-Gseb0+enmAWnc6T2zX6TnpAy/HP29J5HHOilpKQJ26DI2O+ivQ/m1YrYrIKoye+MIhSAVB5ItQNKxbvuhizVVQ== + "@tootallnate/once@2": version "2.0.0" resolved "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz" @@ -23650,7 +23655,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -23668,15 +23673,6 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz" @@ -23811,7 +23807,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -23846,13 +23842,6 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" -strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" @@ -27046,7 +27035,7 @@ workerpool@6.2.1: resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -27081,15 +27070,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"