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/*"
],
"version": "0.2.49",
"version": "0.2.50",
"npmClient": "yarn",
"useWorkspaces": true,
"command": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -81,7 +81,7 @@ interface L2TxsConfig {
privateKey: string;
// Gas limit for tx
gasLimit?: number
gasLimit?: number;
}
// Peer config
@ -126,11 +126,37 @@ export interface PeerConfig {
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
export interface NitroConfig {
store: string
privateKey: string
chainPrivateKey: string
store: string;
privateKey: string;
chainPrivateKey: string;
payments: PaymentsConfig;
}
// P2P config
@ -179,7 +205,7 @@ export interface ServerConfig {
// Flag to specify whether RPC endpoint supports block hash as block tag parameter
// https://ethereum.org/en/developers/docs/apis/json-rpc/#default-block
rpcSupportsBlockHashParam: boolean
rpcSupportsBlockHashParam: boolean;
}
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 type { ReadWriteChannel } from '@cerc-io/ts-channel';
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');
@ -17,7 +19,8 @@ const PAYMENT_HEADER_REGEX = /vhash:(.*),vsig:(.*)/;
const ERR_FREE_QUOTA_EXHUASTED = 'Free quota exhausted';
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_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')
// TODO: Configure
const LRU_CACHE_MAX_ACCOUNT_COUNT = 1000;
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
// Config Defaults
const DEFAULT_REQUEST_TIMEOUT = 10; // 10 seconds
const FREE_QUERY_LIMIT = 10;
const FREE_QUERIES = ['latestBlock'];
const DEFAULT_FREE_QUERIES_LIMIT = 10;
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 {
clientAddress?: string;
private config: PaymentsConfig;
private ratesConfig: BaseRatesConfig;
// TODO: Persist data
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 paymentListeners: ReadWriteChannel<string>[] = [];
// TODO: Read query rate map from config
// TODO: Add a method to get rate for a query
constructor (config: PaymentsConfig, baseRatesConfig: BaseRatesConfig) {
this.config = config;
this.ratesConfig = baseRatesConfig;
constructor () {
this.receivedVouchers = new LRUCache<string, LRUCache<string, Voucher>>({
max: LRU_CACHE_MAX_ACCOUNT_COUNT,
ttl: LRU_CACHE_ACCOUNT_TTL
this.receivedPayments = new LRUCache<string, LRUCache<string, Payment>>({
max: this.config.cache.maxAccounts ?? DEFAULT_LRU_CACHE_MAX_ACCOUNTS,
ttl: (this.config.cache.accountTTLInSecs ?? DEFAULT_LRU_CACHE_ACCOUNT_TTL) * 1000
});
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();
}
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> {
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();
log('Starting voucher subscription...');
@ -77,19 +111,27 @@ export class PaymentsManager {
const associatedPaymentChannel = await client.getPaymentChannel(voucher.channelId);
const payer = associatedPaymentChannel.balance.payer;
log(`Received a payment voucher from ${payer}`);
let vouchersMap = this.receivedVouchers.get(payer);
if (!vouchersMap) {
vouchersMap = new LRUCache<string, Voucher>({
max: LRU_CACHE_MAX_VOUCHER_COUNT,
ttl: LRU_CACHE_VOUCHER_TTL
});
this.receivedVouchers.set(payer, vouchersMap);
if (!voucher.amount) {
log(`Amount not set in received voucher on payment channel ${voucher.channelId.string()}`);
continue;
}
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()) {
await listener.push(payer);
@ -109,42 +151,51 @@ export class PaymentsManager {
await this.stopSubscriptionLoop.close();
}
async allowRequest (voucherHash: string, voucherSig: string): Promise<[boolean, string]> {
const senderAddress = nitroUtils.getSignerAddress(voucherHash, voucherSig);
async allowRequest (voucherHash: string, voucherSig: string, querySelection: string): Promise<[false, string] | [true, null]> {
const signerAddress = nitroUtils.getSignerAddress(voucherHash, voucherSig);
// Use free quota if EMPTY_VOUCHER_HASH passed
if (voucherHash === EMPTY_VOUCHER_HASH) {
let remainingFreeQueries = this.remainingFreeQueriesMap.get(senderAddress);
let remainingFreeQueries = this.remainingFreeQueriesMap.get(signerAddress);
if (remainingFreeQueries === undefined) {
remainingFreeQueries = FREE_QUERY_LIMIT;
remainingFreeQueries = this.ratesConfig.freeQueriesLimit ?? DEFAULT_FREE_QUERIES_LIMIT;
}
// Check if user has exhausted their free query limit
if (remainingFreeQueries > 0) {
log(`Serving a free query for ${senderAddress}`);
this.remainingFreeQueriesMap.set(senderAddress, remainingFreeQueries - 1);
log(`Serving a free query to ${signerAddress}`);
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];
}
// Check for payment voucher received from the Nitro account
const paymentVoucherRecived = await this.authenticateVoucher(voucherHash, senderAddress);
// Serve a query for free if rate is not configured
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) {
log(`Serving a paid query for ${senderAddress}`);
return [true, ''];
// Check if required payment received from the Nitro account
const [paymentReceived, paymentError] = await this.authenticatePayment(voucherHash, signerAddress, BigInt(configuredQueryCost));
if (paymentReceived) {
log(`Serving a paid query for ${signerAddress}`);
return [true, null];
} else {
log(`Rejecting query from ${senderAddress}, payment voucher not received`);
return [false, ERR_PAYMENT_NOT_RECEIVED];
log(`Rejecting query from ${signerAddress}: ${paymentError}`);
return [false, paymentError];
}
}
async authenticateVoucher (voucherHash:string, senderAddress: string): Promise<boolean> {
if (this.acceptReceivedVouchers(voucherHash, senderAddress)) {
return true;
async authenticatePayment (voucherHash:string, signerAddress: string, value: bigint): Promise<[false, string] | [true, null]> {
const [isPaymentReceived, isOfSufficientValue] = this.acceptReceivedPayment(voucherHash, signerAddress, value);
if (isPaymentReceived) {
return isOfSufficientValue ? [true, null] : [false, ERR_AMOUNT_INSUFFICIENT];
}
// Wait for payment voucher from sender
@ -153,7 +204,7 @@ export class PaymentsManager {
let requestTimeout;
const timeoutPromise = new Promise(resolve => {
requestTimeout = setTimeout(resolve, REQUEST_TIMEOUT);
requestTimeout = setTimeout(resolve, (this.config.requestTimeoutInSecs ?? DEFAULT_REQUEST_TIMEOUT) * 1000);
});
try {
@ -165,12 +216,13 @@ export class PaymentsManager {
// payer is undefined if timeout completes or channel is closed externally
if (!payer) {
return false;
return [false, ERR_PAYMENT_NOT_RECEIVED];
}
if (payer === senderAddress) {
if (this.acceptReceivedVouchers(voucherHash, senderAddress)) {
return true;
if (payer === signerAddress) {
const [isPaymentReceived, isOfSufficientValue] = this.acceptReceivedPayment(voucherHash, signerAddress, value);
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
// Returns false if not found
// Returns true after being found and removed
private acceptReceivedVouchers (voucherHash:string, senderAddress: string): boolean {
const vouchersMap = this.receivedVouchers.get(senderAddress);
// Check for a given payment voucher in LRU cache map
// Returns whether the voucher was found, whether it was of sufficient value
private acceptReceivedPayment (voucherHash:string, signerAddress: string, minRequiredValue: bigint): [boolean, boolean] {
const paymentsMap = this.receivedPayments.get(signerAddress);
if (!vouchersMap) {
return false;
if (!paymentsMap) {
return [false, false];
}
const receivedVoucher = vouchersMap.get(voucherHash);
const receivedPayment = paymentsMap.get(voucherHash);
if (!receivedVoucher) {
return false;
if (!receivedPayment) {
return [false, false];
}
vouchersMap.delete(voucherHash);
return true;
if (receivedPayment.amount < minRequiredValue) {
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
.map((selection) => (selection as FieldNode).name.value);
.map((selection: any) => (selection as FieldNode).name.value);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for await (const querySelection of querySelections ?? []) {
// TODO: Charge according to the querySelection
if (FREE_QUERIES.includes(querySelection)) {
if (paymentsManager.freeQueriesList.includes(querySelection)) {
continue;
}
const [allowRequest, rejectionMessage] = await paymentsManager.allowRequest(vhash, vsig);
const [allowRequest, rejectionMessage] = await paymentsManager.allowRequest(vhash, vsig, querySelection);
if (!allowRequest) {
const failResponse: GraphQLResponse = {
errors: [{ message: rejectionMessage }],
http: new HTTPResponse(undefined, {
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"
xsalsa20 "^1.1.0"
"@cerc-io/nitro-client@^0.1.4":
version "0.1.4"
resolved "https://git.vdb.to/api/packages/cerc-io/npm/%40cerc-io%2Fnitro-client/-/0.1.4/nitro-client-0.1.4.tgz#7f69fa5fa66beb7eedd6dddc81a29d2589e6d60d"
integrity sha512-2onCl2wygbXXhtjdMotRdeOXovFeiCegenE/y4uFKxOX83LpslTx/iK+d6HqcQHW4Kkc2pExu6PBntaPoKVpLA==
"@cerc-io/nitro-client@^0.1.5":
version "0.1.5"
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-px/7IgOv1m+DWskJPQ4DUyX84MZHOYMEPN3iNK9uPf+TjyQQm0w2eTPcdQEQN20xzfFFx3k+Dzys3Ko06pXPDQ==
dependencies:
"@cerc-io/libp2p" "0.42.2-laconic-0.1.3"
"@cerc-io/nitro-util" "^0.1.4"
"@cerc-io/peer" "^0.2.46"
"@cerc-io/nitro-util" "^0.1.5"
"@cerc-io/peer" "^0.2.49"
"@cerc-io/ts-channel" "1.0.3-ts-nitro-0.1.1"
"@libp2p/crypto" "^1.0.4"
"@libp2p/tcp" "^6.0.0"
@ -373,11 +373,12 @@
promjs "^0.4.2"
uint8arrays "^4.0.3"
"@cerc-io/nitro-util@^0.1.4":
version "0.1.4"
resolved "https://git.vdb.to/api/packages/cerc-io/npm/%40cerc-io%2Fnitro-util/-/0.1.4/nitro-util-0.1.4.tgz#101f61692050f0ae850b0a0a12e56252f3ddc5a8"
integrity sha512-dqzFA5czCxo/yyAhHMplzRzTyzf52WpU8U+ntZ9zFIfrWPHvHHyDY4YyjA2yVQEWfcyyIZ0GTzf6wDb/kP8dKg==
"@cerc-io/nitro-util@^0.1.5":
version "0.1.5"
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-1nNXfoHVOV2QSnnQSiW/dgTuMGJYEcN2M12e5rMXAb4KrJIScT1lWTp+P5dja/F6jN8ZuNlu5REQTdOjdhMNwQ==
dependencies:
"@statechannels/nitro-protocol" "^2.0.0-alpha.4"
assert "^2.0.0"
debug "^4.3.4"
ethers "^5.7.2"