Add payments config and charge according to configured rates (#395)

* Track amount paid on payment channels and charge as per query cost

* Add payments manager config

* Fix received payment checks

* Separate out payment not received and amount insufficient errors

* Serve a query for free if its rate is not configured

* Load open payment channels from the stored state

* Take list of free queries from config

* Take mutation rates from config

* Rename query rates config fields

* Take cache settings and request timeout from config

* Upgrade ts-nitro packages

* Upgrade package versions
This commit is contained in:
prathamesh0 2023-07-27 15:52:36 +05:30 committed by GitHub
parent aa10a2dc05
commit 198d3e65db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 208 additions and 111 deletions

View File

@ -2,7 +2,7 @@
"packages": [ "packages": [
"packages/*" "packages/*"
], ],
"version": "0.2.49", "version": "0.2.50",
"npmClient": "yarn", "npmClient": "yarn",
"useWorkspaces": true, "useWorkspaces": true,
"command": { "command": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@cerc-io/cache", "name": "@cerc-io/cache",
"version": "0.2.49", "version": "0.2.50",
"description": "Generic object cache", "description": "Generic object cache",
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@cerc-io/cli", "name": "@cerc-io/cli",
"version": "0.2.49", "version": "0.2.50",
"main": "dist/index.js", "main": "dist/index.js",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
@ -11,8 +11,8 @@
"chat": "DEBUG='vulcanize:*, laconic:*' node dist/chat.js" "chat": "DEBUG='vulcanize:*, laconic:*' node dist/chat.js"
}, },
"dependencies": { "dependencies": {
"@cerc-io/peer": "^0.2.49", "@cerc-io/peer": "^0.2.50",
"@cerc-io/util": "^0.2.49", "@cerc-io/util": "^0.2.50",
"@ethersproject/providers": "^5.4.4", "@ethersproject/providers": "^5.4.4",
"@graphql-tools/utils": "^9.1.1", "@graphql-tools/utils": "^9.1.1",
"@ipld/dag-cbor": "^8.0.0", "@ipld/dag-cbor": "^8.0.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "@cerc-io/codegen", "name": "@cerc-io/codegen",
"version": "0.2.49", "version": "0.2.50",
"description": "Code generator", "description": "Code generator",
"private": true, "private": true,
"main": "index.js", "main": "index.js",
@ -20,7 +20,7 @@
}, },
"homepage": "https://github.com/cerc-io/watcher-ts#readme", "homepage": "https://github.com/cerc-io/watcher-ts#readme",
"dependencies": { "dependencies": {
"@cerc-io/util": "^0.2.49", "@cerc-io/util": "^0.2.50",
"@graphql-tools/load-files": "^6.5.2", "@graphql-tools/load-files": "^6.5.2",
"@poanet/solidity-flattener": "https://github.com/vulcanize/solidity-flattener.git", "@poanet/solidity-flattener": "https://github.com/vulcanize/solidity-flattener.git",
"@solidity-parser/parser": "^0.13.2", "@solidity-parser/parser": "^0.13.2",

View File

@ -41,12 +41,12 @@
"homepage": "https://github.com/cerc-io/watcher-ts#readme", "homepage": "https://github.com/cerc-io/watcher-ts#readme",
"dependencies": { "dependencies": {
"@apollo/client": "^3.3.19", "@apollo/client": "^3.3.19",
"@cerc-io/cli": "^0.2.49", "@cerc-io/cli": "^0.2.50",
"@cerc-io/ipld-eth-client": "^0.2.49", "@cerc-io/ipld-eth-client": "^0.2.50",
"@cerc-io/solidity-mapper": "^0.2.49", "@cerc-io/solidity-mapper": "^0.2.50",
"@cerc-io/util": "^0.2.49", "@cerc-io/util": "^0.2.50",
{{#if (subgraphPath)}} {{#if (subgraphPath)}}
"@cerc-io/graph-node": "^0.2.49", "@cerc-io/graph-node": "^0.2.50",
{{/if}} {{/if}}
"@ethersproject/providers": "^5.4.4", "@ethersproject/providers": "^5.4.4",
"apollo-type-bigint": "^0.1.3", "apollo-type-bigint": "^0.1.3",

View File

@ -1,10 +1,10 @@
{ {
"name": "@cerc-io/graph-node", "name": "@cerc-io/graph-node",
"version": "0.2.49", "version": "0.2.50",
"main": "dist/index.js", "main": "dist/index.js",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"devDependencies": { "devDependencies": {
"@cerc-io/solidity-mapper": "^0.2.49", "@cerc-io/solidity-mapper": "^0.2.50",
"@ethersproject/providers": "^5.4.4", "@ethersproject/providers": "^5.4.4",
"@graphprotocol/graph-ts": "^0.22.0", "@graphprotocol/graph-ts": "^0.22.0",
"@nomiclabs/hardhat-ethers": "^2.0.2", "@nomiclabs/hardhat-ethers": "^2.0.2",
@ -51,9 +51,9 @@
"dependencies": { "dependencies": {
"@apollo/client": "^3.3.19", "@apollo/client": "^3.3.19",
"@cerc-io/assemblyscript": "0.19.10-watcher-ts-0.1.2", "@cerc-io/assemblyscript": "0.19.10-watcher-ts-0.1.2",
"@cerc-io/cache": "^0.2.49", "@cerc-io/cache": "^0.2.50",
"@cerc-io/ipld-eth-client": "^0.2.49", "@cerc-io/ipld-eth-client": "^0.2.50",
"@cerc-io/util": "^0.2.49", "@cerc-io/util": "^0.2.50",
"@types/json-diff": "^0.5.2", "@types/json-diff": "^0.5.2",
"@types/yargs": "^17.0.0", "@types/yargs": "^17.0.0",
"bn.js": "^4.11.9", "bn.js": "^4.11.9",

View File

@ -1,6 +1,6 @@
{ {
"name": "@cerc-io/ipld-eth-client", "name": "@cerc-io/ipld-eth-client",
"version": "0.2.49", "version": "0.2.50",
"description": "IPLD ETH Client", "description": "IPLD ETH Client",
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {
@ -20,7 +20,7 @@
"homepage": "https://github.com/cerc-io/watcher-ts#readme", "homepage": "https://github.com/cerc-io/watcher-ts#readme",
"dependencies": { "dependencies": {
"@apollo/client": "^3.7.1", "@apollo/client": "^3.7.1",
"@cerc-io/cache": "^0.2.49", "@cerc-io/cache": "^0.2.50",
"cross-fetch": "^3.1.4", "cross-fetch": "^3.1.4",
"debug": "^4.3.1", "debug": "^4.3.1",
"ethers": "^5.4.4", "ethers": "^5.4.4",

View File

@ -1,6 +1,6 @@
{ {
"name": "@cerc-io/peer", "name": "@cerc-io/peer",
"version": "0.2.49", "version": "0.2.50",
"description": "libp2p module", "description": "libp2p module",
"main": "dist/index.js", "main": "dist/index.js",
"exports": "./dist/index.js", "exports": "./dist/index.js",

View File

@ -1,10 +1,10 @@
{ {
"name": "@cerc-io/solidity-mapper", "name": "@cerc-io/solidity-mapper",
"version": "0.2.49", "version": "0.2.50",
"main": "dist/index.js", "main": "dist/index.js",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"devDependencies": { "devDependencies": {
"@cerc-io/ipld-eth-client": "^0.2.49", "@cerc-io/ipld-eth-client": "^0.2.50",
"@ethersproject/abi": "^5.3.0", "@ethersproject/abi": "^5.3.0",
"@nomiclabs/hardhat-ethers": "^2.0.2", "@nomiclabs/hardhat-ethers": "^2.0.2",
"@nomiclabs/hardhat-waffle": "^2.0.1", "@nomiclabs/hardhat-waffle": "^2.0.1",

View File

@ -1,6 +1,6 @@
{ {
"name": "@cerc-io/test", "name": "@cerc-io/test",
"version": "0.2.49", "version": "0.2.50",
"main": "dist/index.js", "main": "dist/index.js",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"private": true, "private": true,

View File

@ -1,6 +1,6 @@
{ {
"name": "@cerc-io/tracing-client", "name": "@cerc-io/tracing-client",
"version": "0.2.49", "version": "0.2.50",
"description": "ETH VM tracing client", "description": "ETH VM tracing client",
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {

View File

@ -1,12 +1,12 @@
{ {
"name": "@cerc-io/util", "name": "@cerc-io/util",
"version": "0.2.49", "version": "0.2.50",
"main": "dist/index.js", "main": "dist/index.js",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@apollo/utils.keyvaluecache": "^1.0.1", "@apollo/utils.keyvaluecache": "^1.0.1",
"@cerc-io/nitro-client": "^0.1.4", "@cerc-io/nitro-client": "^0.1.5",
"@cerc-io/solidity-mapper": "^0.2.49", "@cerc-io/solidity-mapper": "^0.2.50",
"@cerc-io/ts-channel": "1.0.3-ts-nitro-0.1.1", "@cerc-io/ts-channel": "1.0.3-ts-nitro-0.1.1",
"@ethersproject/providers": "^5.4.4", "@ethersproject/providers": "^5.4.4",
"@graphql-tools/schema": "^9.0.10", "@graphql-tools/schema": "^9.0.10",
@ -40,8 +40,8 @@
"yargs": "^17.0.1" "yargs": "^17.0.1"
}, },
"devDependencies": { "devDependencies": {
"@cerc-io/cache": "^0.2.49", "@cerc-io/cache": "^0.2.50",
"@cerc-io/ipld-eth-client": "^0.2.49", "@cerc-io/ipld-eth-client": "^0.2.50",
"@nomiclabs/hardhat-waffle": "^2.0.1", "@nomiclabs/hardhat-waffle": "^2.0.1",
"@types/express": "^4.17.14", "@types/express": "^4.17.14",
"@types/fs-extra": "^9.0.11", "@types/fs-extra": "^9.0.11",

View File

@ -81,7 +81,7 @@ interface L2TxsConfig {
privateKey: string; privateKey: string;
// Gas limit for tx // Gas limit for tx
gasLimit?: number gasLimit?: number;
} }
// Peer config // Peer config
@ -126,11 +126,37 @@ export interface PeerConfig {
l2TxsConfig?: L2TxsConfig; l2TxsConfig?: L2TxsConfig;
} }
export interface BaseRatesConfig {
freeQueriesLimit: number;
freeQueriesList: string[];
queries: { [key: string]: string };
mutations: { [key: string]: string };
}
export interface PaymentsCacheConfig {
maxAccounts: number;
accountTTLInSecs: number;
maxVouchersPerAccount: number;
voucherTTLInSecs: number;
maxPaymentChannels: number;
paymentChannelTTLInSecs: number;
}
// Payments manager config
export interface PaymentsConfig {
ratesFile: string;
requestTimeoutInSecs: number;
cache: PaymentsCacheConfig;
}
// ts-nitro config // ts-nitro config
export interface NitroConfig { export interface NitroConfig {
store: string store: string;
privateKey: string
chainPrivateKey: string privateKey: string;
chainPrivateKey: string;
payments: PaymentsConfig;
} }
// P2P config // P2P config
@ -179,7 +205,7 @@ export interface ServerConfig {
// Flag to specify whether RPC endpoint supports block hash as block tag parameter // Flag to specify whether RPC endpoint supports block hash as block tag parameter
// https://ethereum.org/en/developers/docs/apis/json-rpc/#default-block // https://ethereum.org/en/developers/docs/apis/json-rpc/#default-block
rpcSupportsBlockHashParam: boolean rpcSupportsBlockHashParam: boolean;
} }
export interface UpstreamConfig { export interface UpstreamConfig {

View File

@ -7,7 +7,9 @@ import { Response as HTTPResponse } from 'apollo-server-env';
import Channel from '@cerc-io/ts-channel'; import Channel from '@cerc-io/ts-channel';
import type { ReadWriteChannel } from '@cerc-io/ts-channel'; import type { ReadWriteChannel } from '@cerc-io/ts-channel';
import type { Client, Voucher } from '@cerc-io/nitro-client'; import type { Client, Voucher } from '@cerc-io/nitro-client';
import { utils as nitroUtils } from '@cerc-io/nitro-client'; import { utils as nitroUtils, ChannelStatus } from '@cerc-io/nitro-client';
import { BaseRatesConfig, PaymentsConfig } from './config';
const log = debug('laconic:payments'); const log = debug('laconic:payments');
@ -17,7 +19,8 @@ const PAYMENT_HEADER_REGEX = /vhash:(.*),vsig:(.*)/;
const ERR_FREE_QUOTA_EXHUASTED = 'Free quota exhausted'; const ERR_FREE_QUOTA_EXHUASTED = 'Free quota exhausted';
const ERR_PAYMENT_NOT_RECEIVED = 'Payment not received'; const ERR_PAYMENT_NOT_RECEIVED = 'Payment not received';
const HTTP_CODE_PAYMENT_NOT_RECEIVED = 402; // Payment required const ERR_AMOUNT_INSUFFICIENT = 'Payment amount insufficient';
const HTTP_CODE_PAYMENT_REQUIRED = 402; // Payment required
const ERR_HEADER_MISSING = 'Payment header x-payment not set'; const ERR_HEADER_MISSING = 'Payment header x-payment not set';
const ERR_INVALID_PAYMENT_HEADER = 'Invalid payment header format'; const ERR_INVALID_PAYMENT_HEADER = 'Invalid payment header format';
@ -25,41 +28,72 @@ const HTTP_CODE_BAD_REQUEST = 400; // Bad request
const EMPTY_VOUCHER_HASH = '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470'; // keccak256('0x') const EMPTY_VOUCHER_HASH = '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470'; // keccak256('0x')
// TODO: Configure // Config Defaults
const LRU_CACHE_MAX_ACCOUNT_COUNT = 1000; const DEFAULT_REQUEST_TIMEOUT = 10; // 10 seconds
const LRU_CACHE_ACCOUNT_TTL = 30 * 60 * 1000; // 30mins
const LRU_CACHE_MAX_VOUCHER_COUNT = 1000;
const LRU_CACHE_VOUCHER_TTL = 5 * 60 * 1000; // 5mins
const FREE_QUERY_LIMIT = 10; const DEFAULT_FREE_QUERIES_LIMIT = 10;
const FREE_QUERIES = ['latestBlock'];
const REQUEST_TIMEOUT = 10 * 1000; // 10 seconds const DEFAULT_FREE_QUERIES_LIST = ['latestBlock'];
const DEFAULT_LRU_CACHE_MAX_ACCOUNTS = 1000;
const DEFAULT_LRU_CACHE_ACCOUNT_TTL = 30 * 60; // 30mins
const DEFAULT_LRU_CACHE_MAX_VOUCHERS_PER_ACCOUNT = 1000;
const DEFAULT_LRU_CACHE_VOUCHER_TTL = 5 * 60; // 5mins
const DEFAULT_LRU_CACHE_MAX_PAYMENT_CHANNELS = 10000;
const DEFAULT_LRU_CACHE_PAYMENT_CHANNEL_TTL = DEFAULT_LRU_CACHE_ACCOUNT_TTL;
interface Payment {
voucher: Voucher;
amount: bigint;
}
export class PaymentsManager { export class PaymentsManager {
clientAddress?: string; clientAddress?: string;
private config: PaymentsConfig;
private ratesConfig: BaseRatesConfig;
// TODO: Persist data // TODO: Persist data
private remainingFreeQueriesMap: Map<string, number> = new Map(); private remainingFreeQueriesMap: Map<string, number> = new Map();
private receivedVouchers: LRUCache<string, LRUCache<string, Voucher>>; // TODO: Persist data
private receivedPayments: LRUCache<string, LRUCache<string, Payment>>;
private paidSoFarOnChannel: LRUCache<string, bigint>;
private stopSubscriptionLoop: ReadWriteChannel<void>; private stopSubscriptionLoop: ReadWriteChannel<void>;
private paymentListeners: ReadWriteChannel<string>[] = []; private paymentListeners: ReadWriteChannel<string>[] = [];
// TODO: Read query rate map from config constructor (config: PaymentsConfig, baseRatesConfig: BaseRatesConfig) {
// TODO: Add a method to get rate for a query this.config = config;
this.ratesConfig = baseRatesConfig;
constructor () { this.receivedPayments = new LRUCache<string, LRUCache<string, Payment>>({
this.receivedVouchers = new LRUCache<string, LRUCache<string, Voucher>>({ max: this.config.cache.maxAccounts ?? DEFAULT_LRU_CACHE_MAX_ACCOUNTS,
max: LRU_CACHE_MAX_ACCOUNT_COUNT, ttl: (this.config.cache.accountTTLInSecs ?? DEFAULT_LRU_CACHE_ACCOUNT_TTL) * 1000
ttl: LRU_CACHE_ACCOUNT_TTL
}); });
this.paidSoFarOnChannel = new LRUCache<string, bigint>({
max: this.config.cache.maxPaymentChannels ?? DEFAULT_LRU_CACHE_MAX_PAYMENT_CHANNELS,
ttl: (this.config.cache.paymentChannelTTLInSecs ?? DEFAULT_LRU_CACHE_PAYMENT_CHANNEL_TTL) * 1000
});
this.stopSubscriptionLoop = Channel(); this.stopSubscriptionLoop = Channel();
} }
get freeQueriesList (): string[] {
return this.ratesConfig.freeQueriesList ?? DEFAULT_FREE_QUERIES_LIST;
}
get mutationRates (): { [key: string]: string } {
return this.ratesConfig.mutations;
}
async subscribeToVouchers (client: Client): Promise<void> { async subscribeToVouchers (client: Client): Promise<void> {
this.clientAddress = client.address; this.clientAddress = client.address;
// Load existing open payment channels with amount paid so far from the stored state
await this.loadPaymentChannels(client);
const receivedVouchersChannel = client.receivedVouchers(); const receivedVouchersChannel = client.receivedVouchers();
log('Starting voucher subscription...'); log('Starting voucher subscription...');
@ -77,19 +111,27 @@ export class PaymentsManager {
const associatedPaymentChannel = await client.getPaymentChannel(voucher.channelId); const associatedPaymentChannel = await client.getPaymentChannel(voucher.channelId);
const payer = associatedPaymentChannel.balance.payer; const payer = associatedPaymentChannel.balance.payer;
log(`Received a payment voucher from ${payer}`);
let vouchersMap = this.receivedVouchers.get(payer); if (!voucher.amount) {
if (!vouchersMap) { log(`Amount not set in received voucher on payment channel ${voucher.channelId.string()}`);
vouchersMap = new LRUCache<string, Voucher>({ continue;
max: LRU_CACHE_MAX_VOUCHER_COUNT,
ttl: LRU_CACHE_VOUCHER_TTL
});
this.receivedVouchers.set(payer, vouchersMap);
} }
vouchersMap.set(voucher.hash(), voucher); const paymentAmount = voucher.amount - (this.paidSoFarOnChannel.get(voucher.channelId.string()) ?? BigInt(0));
this.paidSoFarOnChannel.set(voucher.channelId.string(), voucher.amount);
log(`Received a payment voucher of ${paymentAmount} from ${payer}`);
let paymentsMap = this.receivedPayments.get(payer);
if (!paymentsMap) {
paymentsMap = new LRUCache<string, Payment>({
max: this.config.cache.maxVouchersPerAccount ?? DEFAULT_LRU_CACHE_MAX_VOUCHERS_PER_ACCOUNT,
ttl: (this.config.cache.voucherTTLInSecs ?? DEFAULT_LRU_CACHE_VOUCHER_TTL) * 1000
});
this.receivedPayments.set(payer, paymentsMap);
}
paymentsMap.set(voucher.hash(), { voucher, amount: paymentAmount });
for await (const [, listener] of this.paymentListeners.entries()) { for await (const [, listener] of this.paymentListeners.entries()) {
await listener.push(payer); await listener.push(payer);
@ -109,42 +151,51 @@ export class PaymentsManager {
await this.stopSubscriptionLoop.close(); await this.stopSubscriptionLoop.close();
} }
async allowRequest (voucherHash: string, voucherSig: string): Promise<[boolean, string]> { async allowRequest (voucherHash: string, voucherSig: string, querySelection: string): Promise<[false, string] | [true, null]> {
const senderAddress = nitroUtils.getSignerAddress(voucherHash, voucherSig); const signerAddress = nitroUtils.getSignerAddress(voucherHash, voucherSig);
// Use free quota if EMPTY_VOUCHER_HASH passed
if (voucherHash === EMPTY_VOUCHER_HASH) { if (voucherHash === EMPTY_VOUCHER_HASH) {
let remainingFreeQueries = this.remainingFreeQueriesMap.get(senderAddress); let remainingFreeQueries = this.remainingFreeQueriesMap.get(signerAddress);
if (remainingFreeQueries === undefined) { if (remainingFreeQueries === undefined) {
remainingFreeQueries = FREE_QUERY_LIMIT; remainingFreeQueries = this.ratesConfig.freeQueriesLimit ?? DEFAULT_FREE_QUERIES_LIMIT;
} }
// Check if user has exhausted their free query limit // Check if user has exhausted their free query limit
if (remainingFreeQueries > 0) { if (remainingFreeQueries > 0) {
log(`Serving a free query for ${senderAddress}`); log(`Serving a free query to ${signerAddress}`);
this.remainingFreeQueriesMap.set(senderAddress, remainingFreeQueries - 1); this.remainingFreeQueriesMap.set(signerAddress, remainingFreeQueries - 1);
return [true, '']; return [true, null];
} }
log(`Rejecting query from ${senderAddress}, user has exhausted their free quota`); log(`Rejecting query from ${signerAddress}: ${ERR_FREE_QUOTA_EXHUASTED}`);
return [false, ERR_FREE_QUOTA_EXHUASTED]; return [false, ERR_FREE_QUOTA_EXHUASTED];
} }
// Check for payment voucher received from the Nitro account // Serve a query for free if rate is not configured
const paymentVoucherRecived = await this.authenticateVoucher(voucherHash, senderAddress); const configuredQueryCost = this.ratesConfig.queries[querySelection];
if (configuredQueryCost === undefined) {
log(`Query rate not configured for "${querySelection}", serving a free query to ${signerAddress}`);
return [true, null];
}
if (paymentVoucherRecived) { // Check if required payment received from the Nitro account
log(`Serving a paid query for ${senderAddress}`); const [paymentReceived, paymentError] = await this.authenticatePayment(voucherHash, signerAddress, BigInt(configuredQueryCost));
return [true, ''];
if (paymentReceived) {
log(`Serving a paid query for ${signerAddress}`);
return [true, null];
} else { } else {
log(`Rejecting query from ${senderAddress}, payment voucher not received`); log(`Rejecting query from ${signerAddress}: ${paymentError}`);
return [false, ERR_PAYMENT_NOT_RECEIVED]; return [false, paymentError];
} }
} }
async authenticateVoucher (voucherHash:string, senderAddress: string): Promise<boolean> { async authenticatePayment (voucherHash:string, signerAddress: string, value: bigint): Promise<[false, string] | [true, null]> {
if (this.acceptReceivedVouchers(voucherHash, senderAddress)) { const [isPaymentReceived, isOfSufficientValue] = this.acceptReceivedPayment(voucherHash, signerAddress, value);
return true; if (isPaymentReceived) {
return isOfSufficientValue ? [true, null] : [false, ERR_AMOUNT_INSUFFICIENT];
} }
// Wait for payment voucher from sender // Wait for payment voucher from sender
@ -153,7 +204,7 @@ export class PaymentsManager {
let requestTimeout; let requestTimeout;
const timeoutPromise = new Promise(resolve => { const timeoutPromise = new Promise(resolve => {
requestTimeout = setTimeout(resolve, REQUEST_TIMEOUT); requestTimeout = setTimeout(resolve, (this.config.requestTimeoutInSecs ?? DEFAULT_REQUEST_TIMEOUT) * 1000);
}); });
try { try {
@ -165,12 +216,13 @@ export class PaymentsManager {
// payer is undefined if timeout completes or channel is closed externally // payer is undefined if timeout completes or channel is closed externally
if (!payer) { if (!payer) {
return false; return [false, ERR_PAYMENT_NOT_RECEIVED];
} }
if (payer === senderAddress) { if (payer === signerAddress) {
if (this.acceptReceivedVouchers(voucherHash, senderAddress)) { const [isPaymentReceived, isOfSufficientValue] = this.acceptReceivedPayment(voucherHash, signerAddress, value);
return true; if (isPaymentReceived) {
return isOfSufficientValue ? [true, null] : [false, ERR_AMOUNT_INSUFFICIENT];
} }
} }
} }
@ -184,24 +236,43 @@ export class PaymentsManager {
} }
} }
// Check vouchers in LRU cache map and remove them // Check for a given payment voucher in LRU cache map
// Returns false if not found // Returns whether the voucher was found, whether it was of sufficient value
// Returns true after being found and removed private acceptReceivedPayment (voucherHash:string, signerAddress: string, minRequiredValue: bigint): [boolean, boolean] {
private acceptReceivedVouchers (voucherHash:string, senderAddress: string): boolean { const paymentsMap = this.receivedPayments.get(signerAddress);
const vouchersMap = this.receivedVouchers.get(senderAddress);
if (!vouchersMap) { if (!paymentsMap) {
return false; return [false, false];
} }
const receivedVoucher = vouchersMap.get(voucherHash); const receivedPayment = paymentsMap.get(voucherHash);
if (!receivedVoucher) { if (!receivedPayment) {
return false; return [false, false];
} }
vouchersMap.delete(voucherHash); if (receivedPayment.amount < minRequiredValue) {
return true; return [true, false];
}
paymentsMap.delete(voucherHash);
return [true, true];
}
private async loadPaymentChannels (client: Client): Promise<void> {
const ledgerChannels = await client.getAllLedgerChannels();
for await (const ledgerChannel of ledgerChannels) {
if (ledgerChannel.status === ChannelStatus.Open) {
const paymentChannels = await client.getPaymentChannelsByLedger(ledgerChannel.iD);
for (const paymentChannel of paymentChannels) {
if (paymentChannel.status === ChannelStatus.Open) {
this.paidSoFarOnChannel.set(paymentChannel.iD.string(), paymentChannel.balance.paidSoFar);
}
}
}
}
} }
} }
@ -243,22 +314,21 @@ export const paymentsPlugin = (paymentsManager?: PaymentsManager): ApolloServerP
} }
const querySelections = requestContext.operation?.selectionSet.selections const querySelections = requestContext.operation?.selectionSet.selections
.map((selection) => (selection as FieldNode).name.value); .map((selection: any) => (selection as FieldNode).name.value);
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
for await (const querySelection of querySelections ?? []) { for await (const querySelection of querySelections ?? []) {
// TODO: Charge according to the querySelection if (paymentsManager.freeQueriesList.includes(querySelection)) {
if (FREE_QUERIES.includes(querySelection)) {
continue; continue;
} }
const [allowRequest, rejectionMessage] = await paymentsManager.allowRequest(vhash, vsig); const [allowRequest, rejectionMessage] = await paymentsManager.allowRequest(vhash, vsig, querySelection);
if (!allowRequest) { if (!allowRequest) {
const failResponse: GraphQLResponse = { const failResponse: GraphQLResponse = {
errors: [{ message: rejectionMessage }], errors: [{ message: rejectionMessage }],
http: new HTTPResponse(undefined, { http: new HTTPResponse(undefined, {
headers: requestContext.response?.http?.headers, headers: requestContext.response?.http?.headers,
status: HTTP_CODE_PAYMENT_NOT_RECEIVED status: HTTP_CODE_PAYMENT_REQUIRED
}) })
}; };

View File

@ -350,14 +350,14 @@
wherearewe "^2.0.0" wherearewe "^2.0.0"
xsalsa20 "^1.1.0" xsalsa20 "^1.1.0"
"@cerc-io/nitro-client@^0.1.4": "@cerc-io/nitro-client@^0.1.5":
version "0.1.4" version "0.1.5"
resolved "https://git.vdb.to/api/packages/cerc-io/npm/%40cerc-io%2Fnitro-client/-/0.1.4/nitro-client-0.1.4.tgz#7f69fa5fa66beb7eedd6dddc81a29d2589e6d60d" resolved "https://git.vdb.to/api/packages/cerc-io/npm/%40cerc-io%2Fnitro-client/-/0.1.5/nitro-client-0.1.5.tgz#43152a8482b53431c35018064cc70031b759895b"
integrity sha512-2onCl2wygbXXhtjdMotRdeOXovFeiCegenE/y4uFKxOX83LpslTx/iK+d6HqcQHW4Kkc2pExu6PBntaPoKVpLA== integrity sha512-px/7IgOv1m+DWskJPQ4DUyX84MZHOYMEPN3iNK9uPf+TjyQQm0w2eTPcdQEQN20xzfFFx3k+Dzys3Ko06pXPDQ==
dependencies: dependencies:
"@cerc-io/libp2p" "0.42.2-laconic-0.1.3" "@cerc-io/libp2p" "0.42.2-laconic-0.1.3"
"@cerc-io/nitro-util" "^0.1.4" "@cerc-io/nitro-util" "^0.1.5"
"@cerc-io/peer" "^0.2.46" "@cerc-io/peer" "^0.2.49"
"@cerc-io/ts-channel" "1.0.3-ts-nitro-0.1.1" "@cerc-io/ts-channel" "1.0.3-ts-nitro-0.1.1"
"@libp2p/crypto" "^1.0.4" "@libp2p/crypto" "^1.0.4"
"@libp2p/tcp" "^6.0.0" "@libp2p/tcp" "^6.0.0"
@ -373,11 +373,12 @@
promjs "^0.4.2" promjs "^0.4.2"
uint8arrays "^4.0.3" uint8arrays "^4.0.3"
"@cerc-io/nitro-util@^0.1.4": "@cerc-io/nitro-util@^0.1.5":
version "0.1.4" version "0.1.5"
resolved "https://git.vdb.to/api/packages/cerc-io/npm/%40cerc-io%2Fnitro-util/-/0.1.4/nitro-util-0.1.4.tgz#101f61692050f0ae850b0a0a12e56252f3ddc5a8" resolved "https://git.vdb.to/api/packages/cerc-io/npm/%40cerc-io%2Fnitro-util/-/0.1.5/nitro-util-0.1.5.tgz#829d7cb56c436fcf29184d1d87ed47ac57a2a32f"
integrity sha512-dqzFA5czCxo/yyAhHMplzRzTyzf52WpU8U+ntZ9zFIfrWPHvHHyDY4YyjA2yVQEWfcyyIZ0GTzf6wDb/kP8dKg== integrity sha512-1nNXfoHVOV2QSnnQSiW/dgTuMGJYEcN2M12e5rMXAb4KrJIScT1lWTp+P5dja/F6jN8ZuNlu5REQTdOjdhMNwQ==
dependencies: dependencies:
"@statechannels/nitro-protocol" "^2.0.0-alpha.4"
assert "^2.0.0" assert "^2.0.0"
debug "^4.3.4" debug "^4.3.4"
ethers "^5.7.2" ethers "^5.7.2"