diff --git a/packages/util/package.json b/packages/util/package.json index 29026f612..16a3673e8 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -9,7 +9,9 @@ "@cerc-io/peer": "^0.2.61", "@cerc-io/solidity-mapper": "^0.2.61", "@cerc-io/ts-channel": "1.0.3-ts-nitro-0.1.1", + "@ethersproject/properties": "^5.7.0", "@ethersproject/providers": "^5.4.4", + "@ethersproject/web": "^5.7.1", "@graphql-tools/schema": "^9.0.10", "@graphql-tools/utils": "^9.1.1", "@ipld/dag-cbor": "^6.0.12", diff --git a/packages/util/src/config.ts b/packages/util/src/config.ts index e2bacaec9..14c5c52e8 100644 --- a/packages/util/src/config.ts +++ b/packages/util/src/config.ts @@ -225,15 +225,19 @@ export interface ServerConfig { } export interface FundingAmountsConfig { - directFund: string - virtualFund: string + directFund: string; + virtualFund: string; } export interface NitroPeerConfig { address: string; multiAddr: string; - amount: string; + fundingAmounts: FundingAmountsConfig; +} + +export interface EthServerPaymentsConfig { + nitro: NitroPeerConfig; paidRPCMethods: string[]; - fundingAmounts: FundingAmountsConfig + amount: string; } export interface UpstreamConfig { @@ -243,7 +247,7 @@ export interface UpstreamConfig { rpcProviderEndpoint: string; rpcProviderMutationEndpoint: string; rpcClient: boolean; - nitro: NitroPeerConfig; + payments: EthServerPaymentsConfig; } traceProviderEndpoint: string; } diff --git a/packages/util/src/payments.ts b/packages/util/src/payments.ts index dcf32d1e8..0e2fbe0fa 100644 --- a/packages/util/src/payments.ts +++ b/packages/util/src/payments.ts @@ -4,6 +4,7 @@ import debug from 'debug'; import assert from 'assert'; +import { providers } from 'ethers'; import { LRUCache } from 'lru-cache'; import { FieldNode } from 'graphql'; import { ApolloServerPlugin, GraphQLResponse, GraphQLRequestContext } from 'apollo-server-plugin-base'; @@ -13,6 +14,8 @@ import Channel from '@cerc-io/ts-channel'; import type { ReadWriteChannel } from '@cerc-io/ts-channel'; import type { Voucher } from '@cerc-io/nitro-node'; import { utils as nitroUtils, ChannelStatus, Destination } from '@cerc-io/nitro-node'; +import { deepCopy } from '@ethersproject/properties'; +import { fetchJson } from '@ethersproject/web'; import { BaseRatesConfig, NitroPeerConfig, PaymentsConfig } from './config'; @@ -70,8 +73,6 @@ export class PaymentsManager { private stopSubscriptionLoop: ReadWriteChannel; private paymentListeners: ReadWriteChannel[] = []; - private upstreamNodePaymentChannel?: string; - constructor (nitro: nitroUtils.Nitro, config: PaymentsConfig, baseRatesConfig: BaseRatesConfig) { this.nitro = nitro; this.config = config; @@ -240,41 +241,37 @@ export class PaymentsManager { } } - async setupUpstreamPaymentChannel (nitro: NitroPeerConfig): Promise { - log(`Adding upstream Nitro node: ${nitro.address}`); - await this.nitro.addPeerByMultiaddr(nitro.address, nitro.multiAddr); + async setupPaymentChannel (nodeConfig: NitroPeerConfig): Promise { + log(`Adding Nitro node: ${nodeConfig.address}`); + await this.nitro.addPeerByMultiaddr(nodeConfig.address, nodeConfig.multiAddr); - // Create a payment channel with upstream Nitro node + // Create a payment channel with the given Nitro node // if it doesn't already exist - const existingPaymentChannel = await this.getPaymentChannelWithPeer(nitro.address); + const existingPaymentChannel = await this.getPaymentChannelWithPeer(nodeConfig.address); if (existingPaymentChannel) { - this.upstreamNodePaymentChannel = existingPaymentChannel; - log(`Using existing payment channel ${existingPaymentChannel} with upstream Nitro node`); - - return; + log(`Using existing payment channel ${existingPaymentChannel} with Nitro node ${nodeConfig.address}`); + return existingPaymentChannel; } await this.nitro.directFund( - nitro.address, - Number(nitro.fundingAmounts.directFund) + nodeConfig.address, + Number(nodeConfig.fundingAmounts?.directFund || 0) ); - this.upstreamNodePaymentChannel = await this.nitro.virtualFund( - nitro.address, - Number(nitro.fundingAmounts.virtualFund) + return this.nitro.virtualFund( + nodeConfig.address, + Number(nodeConfig.fundingAmounts?.virtualFund || 0) ); // TODO: Handle closures } - async sendUpstreamPayment (amount: string): Promise<{ + async sendPayment (destChannelId: string, amount: string): Promise<{ channelId: string, amount: string, signature: string }> { - assert(this.upstreamNodePaymentChannel); - - const dest = new Destination(this.upstreamNodePaymentChannel); + const dest = new Destination(destChannelId); const voucher = await this.nitro.node.createVoucher(dest, BigInt(amount ?? 0)); assert(voucher.amount); @@ -429,3 +426,90 @@ export const paymentsPlugin = (paymentsManager?: PaymentsManager): ApolloServerP } }; }; + +// Helper method to modify a given JsonRpcProvider to make payment for required methods +// and attach the voucher details in reqeust URL +export const setupProviderWithPayments = ( + provider: providers.JsonRpcProvider, + paymentsManager: PaymentsManager, + paymentChannelId: string, + paidRPCMethods: string[], + paymentAmount: string +): void => { + // https://github.com/ethers-io/ethers.js/blob/v5.7.2/packages/providers/src.ts/json-rpc-provider.ts#L502 + provider.send = async (method: string, params: Array): Promise => { + log(`Making RPC call: ${method}`); + + const request = { + method: method, + params: params, + id: (provider._nextId++), + jsonrpc: '2.0' + }; + + provider.emit('debug', { + action: 'request', + request: deepCopy(request), + provider: provider + }); + + // We can expand this in the future to any call, but for now these + // are the biggest wins and do not require any serializing parameters. + const cache = (['eth_chainId', 'eth_blockNumber'].indexOf(method) >= 0); + // @ts-expect-error copied code + if (cache && provider._cache[method]) { + return provider._cache[method]; + } + + // Send a payment to upstream Nitro node and add details to the request URL + let updatedURL = `${provider.connection.url}?method=${method}`; + if (paidRPCMethods.includes(method)) { + const voucher = await paymentsManager.sendPayment(paymentChannelId, paymentAmount); + updatedURL = `${updatedURL}&channelId=${voucher.channelId}&amount=${voucher.amount}&signature=${voucher.signature}`; + } + + const result = fetchJson({ ...provider.connection, url: updatedURL }, JSON.stringify(request), getResult).then((result) => { + provider.emit('debug', { + action: 'response', + request: request, + response: result, + provider: provider + }); + + return result; + }, (error) => { + provider.emit('debug', { + action: 'response', + error: error, + request: request, + provider: provider + }); + + throw error; + }); + + // Cache the fetch, but clear it on the next event loop + if (cache) { + provider._cache[method] = result; + setTimeout(() => { + // @ts-expect-error copied code + provider._cache[method] = null; + }, 0); + } + + return result; + }; +}; + +// https://github.com/ethers-io/ethers.js/blob/v5.7.2/packages/providers/src.ts/json-rpc-provider.ts#L139 +function getResult (payload: { error?: { code?: number, data?: any, message?: string }, result?: any }): any { + if (payload.error) { + // @TODO: not any + const error: any = new Error(payload.error.message); + error.code = payload.error.code; + error.data = payload.error.data; + throw error; + } + + return payload.result; +} diff --git a/yarn.lock b/yarn.lock index 688784d79..595a89686 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1451,7 +1451,7 @@ "@ethersproject/properties" "^5.6.0" "@ethersproject/strings" "^5.6.1" -"@ethersproject/web@5.7.1", "@ethersproject/web@^5.6.1", "@ethersproject/web@^5.7.0": +"@ethersproject/web@5.7.1", "@ethersproject/web@^5.6.1", "@ethersproject/web@^5.7.0", "@ethersproject/web@^5.7.1": version "5.7.1" resolved "https://registry.npmjs.org/@ethersproject/web/-/web-5.7.1.tgz" integrity sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w==