Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/util/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@
"license": "AGPL-3.0",
"dependencies": {
"@apollo/utils.keyvaluecache": "^1.0.1",
"@cerc-io/nitro-client": "^0.1.5",
"@cerc-io/nitro-client": "^0.1.6",
"@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/graphql-file-loader": "^8.0.0",
"@graphql-tools/load": "^8.0.0",
"@graphql-tools/schema": "^9.0.10",
"@graphql-tools/utils": "^9.1.1",
"@ipld/dag-cbor": "^6.0.12",
"apollo-server-core": "^3.11.1",
"apollo-server-express": "^3.11.1",
"apollo-server-plugin-response-cache": "^3.8.1",
"apollo-type-bigint": "^0.1.3",
"debug": "^4.3.1",
"decimal.js": "^10.3.1",
"ethers": "^5.4.4",
Expand Down
16 changes: 16 additions & 0 deletions packages/util/src/payments-schema.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
scalar BigInt

enum RateType {
QUERY
MUTATION
}

type RateInfo {
type: RateType!
name: String!
amount: BigInt!
}

type Query {
_rates_: [RateInfo!]!
}
55 changes: 50 additions & 5 deletions packages/util/src/payments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,22 @@ import { LRUCache } from 'lru-cache';
import { FieldNode } from 'graphql';
import { ApolloServerPlugin, GraphQLResponse, GraphQLRequestContext } from 'apollo-server-plugin-base';
import { Response as HTTPResponse } from 'apollo-server-env';
import ApolloBigInt from 'apollo-type-bigint';

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, ChannelStatus } from '@cerc-io/nitro-client';
import { IResolvers } from '@graphql-tools/utils';

import { BaseRatesConfig, PaymentsConfig } from './config';
import { gqlQueryCount, gqlTotalQueryCount } from './gql-metrics';

const log = debug('laconic:payments');

const IntrospectionQuery = 'IntrospectionQuery';
const IntrospectionQuerySelection = '__schema';
const INTROSPECTION_QUERY = 'IntrospectionQuery';
const INTROSPECTION_QUERY_SELECTION = '__schema';
const RATES_QUERY_SELECTION = '_rates_';

const PAYMENT_HEADER_KEY = 'x-payment';
const PAYMENT_HEADER_REGEX = /vhash:(.*),vsig:(.*)/;
Expand Down Expand Up @@ -49,6 +53,17 @@ interface Payment {
amount: bigint;
}

enum RateType {
Query = 'QUERY',
Mutation = 'MUTATION'
}

interface RateInfo {
type: RateType;
name: string;
amount: bigint;
}

export class PaymentsManager {
clientAddress?: string;

Expand Down Expand Up @@ -83,7 +98,7 @@ export class PaymentsManager {
}

get freeQueriesList (): string[] {
return this.ratesConfig.freeQueriesList ?? DEFAULT_FREE_QUERIES_LIST;
return [RATES_QUERY_SELECTION, ...(this.ratesConfig.freeQueriesList ?? DEFAULT_FREE_QUERIES_LIST)];
}

get queryRates (): { [key: string]: string } {
Expand All @@ -94,6 +109,35 @@ export class PaymentsManager {
return this.ratesConfig.mutations ?? {};
}

getResolvers (): IResolvers {
return {
BigInt: new ApolloBigInt('bigInt'),
Query: {
_rates_: async (): Promise<RateInfo[]> => {
log('_rates_');
gqlTotalQueryCount.inc(1);
gqlQueryCount.labels('_rates_').inc(1);

const queryRates = this.queryRates;
const rateInfos = Object.entries(queryRates).map(([name, amount]) => ({
type: RateType.Query,
name,
amount: BigInt(amount)
}));

const mutationRates = this.mutationRates;
Object.entries(mutationRates).forEach(([name, amount]) => rateInfos.push({
type: RateType.Mutation,
name,
amount: BigInt(amount)
}));

return rateInfos;
}
}
};
}

async subscribeToVouchers (client: Client): Promise<void> {
this.clientAddress = client.address;

Expand Down Expand Up @@ -291,15 +335,16 @@ export const paymentsPlugin = (paymentsManager?: PaymentsManager): ApolloServerP
// Continue if it's an introspection query for schema
// (made by ApolloServer playground / default landing page)
if (
requestContext.operationName === IntrospectionQuery &&
requestContext.operationName === INTROSPECTION_QUERY &&
querySelections && querySelections.length === 1 &&
querySelections[0] === IntrospectionQuerySelection
querySelections[0] === INTROSPECTION_QUERY_SELECTION
) {
return null;
}

const paymentHeader = requestContext.request.http?.headers.get(PAYMENT_HEADER_KEY);
if (paymentHeader == null) {
// TODO: Make payment header optional and check only for rate configured queries in loop below
return {
errors: [{ message: ERR_HEADER_MISSING }],
http: new HTTPResponse(undefined, {
Expand Down
21 changes: 18 additions & 3 deletions packages/util/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import path from 'path';
import { Application } from 'express';
import { ApolloServer } from 'apollo-server-express';
import { createServer } from 'http';
Expand All @@ -6,11 +7,13 @@ import { useServer } from 'graphql-ws/lib/use/ws';
import { ApolloServerPluginDrainHttpServer, ApolloServerPluginLandingPageLocalDefault } from 'apollo-server-core';
import debug from 'debug';
import responseCachePlugin from 'apollo-server-plugin-response-cache';
import { InMemoryLRUCache } from '@apollo/utils.keyvaluecache';
import queue from 'express-queue';

import { InMemoryLRUCache } from '@apollo/utils.keyvaluecache';
import { TypeSource } from '@graphql-tools/utils';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { makeExecutableSchema, addResolversToSchema, mergeSchemas } from '@graphql-tools/schema';
import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader';
import { loadSchema } from '@graphql-tools/load';

import { DEFAULT_MAX_GQL_CACHE_SIZE } from './constants';
import { ServerConfig } from './config';
Expand All @@ -33,7 +36,19 @@ export const createAndStartServer = async (
const httpServer = createServer(app);

// Create the schema
const schema = makeExecutableSchema({ typeDefs, resolvers });
let schema = makeExecutableSchema({ typeDefs, resolvers });

if (paymentsManager) {
let paymentsSchema = await loadSchema(path.join(__dirname, 'payments-schema.gql'), {
loaders: [new GraphQLFileLoader()]
});

const resolvers = paymentsManager.getResolvers();
paymentsSchema = addResolversToSchema({ schema: paymentsSchema, resolvers });
schema = mergeSchemas({
schemas: [schema, paymentsSchema]
});
}

// Create our WebSocket server using the HTTP server we just set up.
const wsServer = new WebSocketServer({
Expand Down
Loading