diff --git a/packages/util/package.json b/packages/util/package.json index 7bb021b4..bdf5bdbd 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -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", diff --git a/packages/util/src/payments-schema.gql b/packages/util/src/payments-schema.gql new file mode 100644 index 00000000..611ad34e --- /dev/null +++ b/packages/util/src/payments-schema.gql @@ -0,0 +1,16 @@ +scalar BigInt + +enum RateType { + QUERY + MUTATION +} + +type RateInfo { + type: RateType! + name: String! + amount: BigInt! +} + +type Query { + _rates_: [RateInfo!]! +} diff --git a/packages/util/src/payments.ts b/packages/util/src/payments.ts index 966cb673..e4d4bdd4 100644 --- a/packages/util/src/payments.ts +++ b/packages/util/src/payments.ts @@ -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:(.*)/; @@ -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; @@ -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 } { @@ -94,6 +109,35 @@ export class PaymentsManager { return this.ratesConfig.mutations ?? {}; } + getResolvers (): IResolvers { + return { + BigInt: new ApolloBigInt('bigInt'), + Query: { + _rates_: async (): Promise => { + 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 { this.clientAddress = client.address; @@ -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, { diff --git a/packages/util/src/server.ts b/packages/util/src/server.ts index 8c509941..56505560 100644 --- a/packages/util/src/server.ts +++ b/packages/util/src/server.ts @@ -1,3 +1,4 @@ +import path from 'path'; import { Application } from 'express'; import { ApolloServer } from 'apollo-server-express'; import { createServer } from 'http'; @@ -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'; @@ -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({ diff --git a/yarn.lock b/yarn.lock index a28efbd3..3cfae38c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -350,13 +350,13 @@ wherearewe "^2.0.0" xsalsa20 "^1.1.0" -"@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== +"@cerc-io/nitro-client@^0.1.6": + version "0.1.6" + resolved "https://git.vdb.to/api/packages/cerc-io/npm/%40cerc-io%2Fnitro-client/-/0.1.6/nitro-client-0.1.6.tgz#1da8a9f9055e79861a055686f86d1291e6d0bcc1" + integrity sha512-71yDfi+E4/xFyd77GZMM8t8OaxSJJl2DbaVgqGMObfZa9/cJkWD0nWi4mfW4O63qHhTraVkTyYBbDOGF7fSlsQ== dependencies: "@cerc-io/libp2p" "0.42.2-laconic-0.1.3" - "@cerc-io/nitro-util" "^0.1.5" + "@cerc-io/nitro-util" "^0.1.6" "@cerc-io/peer" "^0.2.49" "@cerc-io/ts-channel" "1.0.3-ts-nitro-0.1.1" "@libp2p/crypto" "^1.0.4" @@ -373,10 +373,10 @@ promjs "^0.4.2" uint8arrays "^4.0.3" -"@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== +"@cerc-io/nitro-util@^0.1.6": + version "0.1.6" + resolved "https://git.vdb.to/api/packages/cerc-io/npm/%40cerc-io%2Fnitro-util/-/0.1.6/nitro-util-0.1.6.tgz#7cdf6ee37c21c7863eea3332e84f9450b7534bf3" + integrity sha512-moalyL8uG4dvUT1NleHqPXnOSqeto8VxFnyW+nRoTkkjmyrn8cb5XF0nlEDxf2gR/pBzNeGv1NdYwhVYv5rkTA== dependencies: "@statechannels/nitro-protocol" "^2.0.0-alpha.4" assert "^2.0.0" @@ -1433,6 +1433,26 @@ dependencies: assemblyscript "0.19.10" +"@graphql-tools/graphql-file-loader@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@graphql-tools/graphql-file-loader/-/graphql-file-loader-8.0.0.tgz#a2026405bce86d974000455647511bf65df4f211" + integrity sha512-wRXj9Z1IFL3+zJG1HWEY0S4TXal7+s1vVhbZva96MSp0kbb/3JBF7j0cnJ44Eq0ClccMgGCDFqPFXty4JlpaPg== + dependencies: + "@graphql-tools/import" "7.0.0" + "@graphql-tools/utils" "^10.0.0" + globby "^11.0.3" + tslib "^2.4.0" + unixify "^1.0.0" + +"@graphql-tools/import@7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@graphql-tools/import/-/import-7.0.0.tgz#a6a91a90a707d5f46bad0fd3fde2f407b548b2be" + integrity sha512-NVZiTO8o1GZs6OXzNfjB+5CtQtqsZZpQOq+Uu0w57kdUkT4RlQKlwhT8T81arEsbV55KpzkpFsOZP7J1wdmhBw== + dependencies: + "@graphql-tools/utils" "^10.0.0" + resolve-from "5.0.0" + tslib "^2.4.0" + "@graphql-tools/load-files@^6.5.2": version "6.6.1" resolved "https://registry.yarnpkg.com/@graphql-tools/load-files/-/load-files-6.6.1.tgz#91ce18d910baf8678459486d8cccd474767bec0a" @@ -1442,6 +1462,16 @@ tslib "^2.4.0" unixify "1.0.0" +"@graphql-tools/load@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@graphql-tools/load/-/load-8.0.0.tgz#62e00f48c39b4085167a096f66ba6c21fb3fc796" + integrity sha512-Cy874bQJH0FP2Az7ELPM49iDzOljQmK1PPH6IuxsWzLSTxwTqd8dXA09dcVZrI7/LsN26heTY2R8q2aiiv0GxQ== + dependencies: + "@graphql-tools/schema" "^10.0.0" + "@graphql-tools/utils" "^10.0.0" + p-limit "3.1.0" + tslib "^2.4.0" + "@graphql-tools/merge@8.3.1": version "8.3.1" resolved "https://registry.yarnpkg.com/@graphql-tools/merge/-/merge-8.3.1.tgz#06121942ad28982a14635dbc87b5d488a041d722" @@ -1458,6 +1488,14 @@ "@graphql-tools/utils" "9.2.1" tslib "^2.4.0" +"@graphql-tools/merge@^9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@graphql-tools/merge/-/merge-9.0.0.tgz#b0a3636c82716454bff88e9bb40108b0471db281" + integrity sha512-J7/xqjkGTTwOJmaJQJ2C+VDBDOWJL3lKrHJN4yMaRLAJH3PosB7GiPRaSDZdErs0+F77sH2MKs2haMMkywzx7Q== + dependencies: + "@graphql-tools/utils" "^10.0.0" + tslib "^2.4.0" + "@graphql-tools/mock@^8.1.2": version "8.7.19" resolved "https://registry.yarnpkg.com/@graphql-tools/mock/-/mock-8.7.19.tgz#b6c01ecc44074a01d6f472213de5f56fe0a3380c" @@ -1478,6 +1516,16 @@ tslib "^2.4.0" value-or-promise "1.0.12" +"@graphql-tools/schema@^10.0.0": + version "10.0.0" + resolved "https://registry.yarnpkg.com/@graphql-tools/schema/-/schema-10.0.0.tgz#7b5f6b6a59f51c927de8c9069bde4ebbfefc64b3" + integrity sha512-kf3qOXMFcMs2f/S8Y3A8fm/2w+GaHAkfr3Gnhh2LOug/JgpY/ywgFVxO3jOeSpSEdoYcDKLcXVjMigNbY4AdQg== + dependencies: + "@graphql-tools/merge" "^9.0.0" + "@graphql-tools/utils" "^10.0.0" + tslib "^2.4.0" + value-or-promise "^1.0.12" + "@graphql-tools/schema@^8.0.0": version "8.5.1" resolved "https://registry.yarnpkg.com/@graphql-tools/schema/-/schema-8.5.1.tgz#c2f2ff1448380919a330312399c9471db2580b58" @@ -1503,6 +1551,15 @@ "@graphql-typed-document-node/core" "^3.1.1" tslib "^2.4.0" +"@graphql-tools/utils@^10.0.0": + version "10.0.4" + resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-10.0.4.tgz#3104bea54752ae54f1f4a67833d7e3b734400dbe" + integrity sha512-MF+nZgGROSnFgyOYWhrl2PuJMlIBvaCH48vtnlnDQKSeDc2fUfOzUVloBAQvnYmK9JBmHHks4Pxv25Ybg3r45Q== + dependencies: + "@graphql-typed-document-node/core" "^3.1.1" + dset "^3.1.2" + tslib "^2.4.0" + "@graphql-typed-document-node/core@^3.1.1": version "3.2.0" resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861" @@ -4612,6 +4669,11 @@ apollo-server-types@^3.8.0: apollo-reporting-protobuf "^3.4.0" apollo-server-env "^4.2.1" +apollo-type-bigint@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/apollo-type-bigint/-/apollo-type-bigint-0.1.3.tgz#9242115ca909b9467ba5c4bc6493a56a06984c0b" + integrity sha512-nyfwEWRZ+kon3Nnot20DufGm2EHZrkJoryYzw3soD+USdxhkcW434w1c/n+mjMLQDl86Z6EvlkvMX5Lordf2Wg== + app-root-path@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/app-root-path/-/app-root-path-3.0.0.tgz" @@ -7172,6 +7234,11 @@ dreamopt@~0.6.0: dependencies: wordwrap ">=0.0.2" +dset@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/dset/-/dset-3.1.2.tgz#89c436ca6450398396dc6538ea00abc0c54cd45a" + integrity sha512-g/M9sqy3oHe477Ar4voQxWtaPIFw1jTdKZuomOjhCcBx9nHUNn0pu6NopuFFrTh/TRZIKEj+76vLWFu9BNKk+Q== + dtrace-provider@~0.8: version "0.8.8" resolved "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz" @@ -9048,7 +9115,7 @@ globalthis@^1.0.3: dependencies: define-properties "^1.1.3" -globby@11.1.0, globby@^11.1.0: +globby@11.1.0, globby@^11.0.3, globby@^11.1.0: version "11.1.0" resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== @@ -12801,6 +12868,13 @@ p-finally@^1.0.0: resolved "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz" integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= +p-limit@3.1.0, p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + p-limit@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" @@ -12815,13 +12889,6 @@ p-limit@^2.2.0: dependencies: p-try "^2.0.0" -p-limit@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - p-limit@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-4.0.0.tgz#914af6544ed32bfa54670b061cafcbd04984b644" @@ -14189,16 +14256,16 @@ resolve-cwd@^3.0.0: dependencies: resolve-from "^5.0.0" +resolve-from@5.0.0, resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== -resolve-from@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" - integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== - resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz" @@ -15937,7 +16004,7 @@ universalify@^2.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== -unixify@1.0.0: +unixify@1.0.0, unixify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unixify/-/unixify-1.0.0.tgz#3a641c8c2ffbce4da683a5c70f03a462940c2090" integrity sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg== @@ -16137,7 +16204,7 @@ value-or-promise@1.0.11: resolved "https://registry.yarnpkg.com/value-or-promise/-/value-or-promise-1.0.11.tgz#3e90299af31dd014fe843fe309cefa7c1d94b140" integrity sha512-41BrgH+dIbCFXClcSapVs5M6GkENd3gQOJpEfPDNa71LsUGMXDL0jMWpI/Rh7WhX+Aalfz2TTS3Zt5pUsbnhLg== -value-or-promise@1.0.12: +value-or-promise@1.0.12, value-or-promise@^1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/value-or-promise/-/value-or-promise-1.0.12.tgz#0e5abfeec70148c78460a849f6b003ea7986f15c" integrity sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q==