mirror of
https://github.com/cerc-io/watcher-ts
synced 2025-01-08 12:28:05 +00:00
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:
parent
aa10a2dc05
commit
198d3e65db
@ -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": {
|
||||||
|
2
packages/cache/package.json
vendored
2
packages/cache/package.json
vendored
@ -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": {
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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,
|
||||||
|
@ -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": {
|
||||||
|
@ -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",
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
21
yarn.lock
21
yarn.lock
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user