Payment plugin fixes (#396)

* Avoid deducting from free quota for queries with unconfigured rates

* Check query selections on an introspection query

* Update comment
This commit is contained in:
prathamesh0 2023-07-31 17:09:58 +05:30 committed by GitHub
parent 198d3e65db
commit 47d4b667f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 35 additions and 16 deletions

View File

@ -21,6 +21,7 @@
"debug": "^4.3.1", "debug": "^4.3.1",
"express": "^4.18.2", "express": "^4.18.2",
"graphql-subscriptions": "^2.0.0", "graphql-subscriptions": "^2.0.0",
"pluralize": "^8.0.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"typeorm": "0.2.37", "typeorm": "0.2.37",
"yargs": "^17.0.1" "yargs": "^17.0.1"
@ -28,6 +29,7 @@
"devDependencies": { "devDependencies": {
"@types/express": "^4.17.14", "@types/express": "^4.17.14",
"@types/node": "16.11.7", "@types/node": "16.11.7",
"@types/pluralize": "^0.0.29",
"@types/yargs": "^17.0.0", "@types/yargs": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^5.47.1", "@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1", "@typescript-eslint/parser": "^5.47.1",

View File

@ -14,6 +14,8 @@ import { BaseRatesConfig, PaymentsConfig } from './config';
const log = debug('laconic:payments'); const log = debug('laconic:payments');
const IntrospectionQuery = 'IntrospectionQuery'; const IntrospectionQuery = 'IntrospectionQuery';
const IntrospectionQuerySelection = '__schema';
const PAYMENT_HEADER_KEY = 'x-payment'; const PAYMENT_HEADER_KEY = 'x-payment';
const PAYMENT_HEADER_REGEX = /vhash:(.*),vsig:(.*)/; const PAYMENT_HEADER_REGEX = /vhash:(.*),vsig:(.*)/;
@ -84,8 +86,12 @@ export class PaymentsManager {
return this.ratesConfig.freeQueriesList ?? DEFAULT_FREE_QUERIES_LIST; return this.ratesConfig.freeQueriesList ?? DEFAULT_FREE_QUERIES_LIST;
} }
get queryRates (): { [key: string]: string } {
return this.ratesConfig.queries ?? {};
}
get mutationRates (): { [key: string]: string } { get mutationRates (): { [key: string]: string } {
return this.ratesConfig.mutations; return this.ratesConfig.mutations ?? {};
} }
async subscribeToVouchers (client: Client): Promise<void> { async subscribeToVouchers (client: Client): Promise<void> {
@ -151,9 +157,7 @@ export class PaymentsManager {
await this.stopSubscriptionLoop.close(); await this.stopSubscriptionLoop.close();
} }
async allowRequest (voucherHash: string, voucherSig: string, querySelection: string): Promise<[false, string] | [true, null]> { async allowRequest (voucherHash: string, signerAddress: string, querySelection: string): Promise<[false, string] | [true, null]> {
const signerAddress = nitroUtils.getSignerAddress(voucherHash, voucherSig);
// Use free quota if EMPTY_VOUCHER_HASH passed // Use free quota if EMPTY_VOUCHER_HASH passed
if (voucherHash === EMPTY_VOUCHER_HASH) { if (voucherHash === EMPTY_VOUCHER_HASH) {
let remainingFreeQueries = this.remainingFreeQueriesMap.get(signerAddress); let remainingFreeQueries = this.remainingFreeQueriesMap.get(signerAddress);
@ -173,14 +177,8 @@ export class PaymentsManager {
return [false, ERR_FREE_QUOTA_EXHUASTED]; return [false, ERR_FREE_QUOTA_EXHUASTED];
} }
// 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];
}
// Check if required payment received from the Nitro account // Check if required payment received from the Nitro account
const configuredQueryCost = this.ratesConfig.queries[querySelection];
const [paymentReceived, paymentError] = await this.authenticatePayment(voucherHash, signerAddress, BigInt(configuredQueryCost)); const [paymentReceived, paymentError] = await this.authenticatePayment(voucherHash, signerAddress, BigInt(configuredQueryCost));
if (paymentReceived) { if (paymentReceived) {
@ -282,8 +280,21 @@ export const paymentsPlugin = (paymentsManager?: PaymentsManager): ApolloServerP
async requestDidStart (requestContext: GraphQLRequestContext) { async requestDidStart (requestContext: GraphQLRequestContext) {
return { return {
async responseForOperation (requestContext: GraphQLRequestContext): Promise<GraphQLResponse | null> { async responseForOperation (requestContext: GraphQLRequestContext): Promise<GraphQLResponse | null> {
// Continue if payments is not setup or it's an introspection query // Continue if payments is not setup
if (!paymentsManager || requestContext.operationName === IntrospectionQuery) { if (!paymentsManager) {
return null;
}
const querySelections = requestContext.operation?.selectionSet.selections
.map((selection: any) => (selection as FieldNode).name.value);
// Continue if it's an introspection query for schema
// (made by ApolloServer playground / default landing page)
if (
requestContext.operationName === IntrospectionQuery &&
querySelections && querySelections.length === 1 &&
querySelections[0] === IntrospectionQuerySelection
) {
return null; return null;
} }
@ -313,8 +324,7 @@ export const paymentsPlugin = (paymentsManager?: PaymentsManager): ApolloServerP
}; };
} }
const querySelections = requestContext.operation?.selectionSet.selections const signerAddress = nitroUtils.getSignerAddress(vhash, vsig);
.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 ?? []) {
@ -322,7 +332,14 @@ export const paymentsPlugin = (paymentsManager?: PaymentsManager): ApolloServerP
continue; continue;
} }
const [allowRequest, rejectionMessage] = await paymentsManager.allowRequest(vhash, vsig, querySelection); // Serve a query for free if rate is not configured
const configuredQueryCost = paymentsManager.queryRates[querySelection];
if (configuredQueryCost === undefined) {
log(`Query rate not configured for "${querySelection}", serving a free query to ${signerAddress}`);
continue;
}
const [allowRequest, rejectionMessage] = await paymentsManager.allowRequest(vhash, signerAddress, querySelection);
if (!allowRequest) { if (!allowRequest) {
const failResponse: GraphQLResponse = { const failResponse: GraphQLResponse = {
errors: [{ message: rejectionMessage }], errors: [{ message: rejectionMessage }],