From 2bf4f02a3081347a6cb444ae6ae7d07e55a6cc63 Mon Sep 17 00:00:00 2001 From: Maayan Date: Mon, 25 Mar 2024 10:45:41 -0400 Subject: [PATCH] Add support to allow setting per-backend configuration (#336) --- CHANGELOG.md | 4 + src/api/aptosConfig.ts | 27 ++++++- src/client/core.ts | 14 ++-- src/client/get.ts | 13 +++- src/client/post.ts | 52 +++++++++++-- src/types/index.ts | 46 +++++++++-- tests/e2e/client/aptosRequest.test.ts | 47 ------------ tests/e2e/client/get.test.ts | 38 ++++++++++ tests/e2e/client/post.test.ts | 105 ++++++++++++++++++++++++++ tests/unit/aptosConfig.test.ts | 18 +++++ 10 files changed, 295 insertions(+), 69 deletions(-) create mode 100644 tests/e2e/client/get.test.ts create mode 100644 tests/e2e/client/post.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f4758e76c..ebc579064 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,11 @@ All notable changes to the Aptos TypeScript SDK will be captured in this file. This changelog is written by hand for now. It adheres to the format set out by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). # Unreleased + - Use indexer API via API Gateway +- Add support to allow setting per-backend (fullnode, indexer, faucet) configuration +- [`Breaking`] `AUTH_TOKEN` client config moved to be under `faucetConfig` property +- Handle `Unauthorized` server error # 1.10.0 (2024-03-11) diff --git a/src/api/aptosConfig.ts b/src/api/aptosConfig.ts index 7b8893066..4e7e18506 100644 --- a/src/api/aptosConfig.ts +++ b/src/api/aptosConfig.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import aptosClient from "@aptos-labs/aptos-client"; -import { AptosSettings, ClientConfig, Client } from "../types"; +import { AptosSettings, ClientConfig, Client, FullNodeConfig, IndexerConfig, FaucetConfig } from "../types"; import { NetworkToNodeAPI, NetworkToFaucetAPI, NetworkToIndexerAPI, Network } from "../utils/apiEndpoints"; import { AptosApiType } from "../utils/const"; @@ -10,7 +10,9 @@ import { AptosApiType } from "../utils/const"; * This class holds the config information for the SDK client instance. */ export class AptosConfig { - /** The Network that this SDK is associated with. Defaults to DEVNET */ + /** + * The Network that this SDK is associated with. Defaults to DEVNET + */ readonly network: Network; /** @@ -33,8 +35,26 @@ export class AptosConfig { */ readonly indexer?: string; + /** + * Optional client configurations + */ readonly clientConfig?: ClientConfig; + /** + * Optional specific Fullnode configurations + */ + readonly fullnodeConfig?: FullNodeConfig; + + /** + * Optional specific Indexer configurations + */ + readonly indexerConfig?: IndexerConfig; + + /** + * Optional specific Faucet configurations + */ + readonly faucetConfig?: FaucetConfig; + constructor(settings?: AptosSettings) { this.network = settings?.network ?? Network.DEVNET; this.fullnode = settings?.fullnode; @@ -42,6 +62,9 @@ export class AptosConfig { this.indexer = settings?.indexer; this.client = settings?.client ?? { provider: aptosClient }; this.clientConfig = settings?.clientConfig ?? {}; + this.fullnodeConfig = settings?.fullnodeConfig ?? {}; + this.indexerConfig = settings?.indexerConfig ?? {}; + this.faucetConfig = settings?.faucetConfig ?? {}; } /** diff --git a/src/client/core.ts b/src/client/core.ts index dbcc4fe8f..76eb944da 100644 --- a/src/client/core.ts +++ b/src/client/core.ts @@ -32,13 +32,10 @@ export async function request(options: ClientRequest, client: Cli "content-type": contentType ?? MimeType.JSON, }; - // TODO - auth token is being used only for faucet, it breaks full node requests. - // Find a more sophisticated way than that but without the need to add the - // auth_token on every `aptos.fundAccount()` call - if (overrides?.AUTH_TOKEN && url.includes("faucet")) { + if (overrides?.AUTH_TOKEN) { headers.Authorization = `Bearer ${overrides?.AUTH_TOKEN}`; } - if (overrides?.API_KEY && !url.includes("faucet")) { + if (overrides?.API_KEY) { headers.Authorization = `Bearer ${overrides?.API_KEY}`; } @@ -81,11 +78,16 @@ export async function aptosRequest( url: fullUrl, }; + // Handle case for `Unauthorized` error (i.e API_KEY error) + if (result.status === 401) { + throw new AptosApiError(options, result, `Error: ${result.data}`); + } + // to support both fullnode and indexer responses, // check if it is an indexer query, and adjust response.data if (aptosConfig.isIndexerRequest(url)) { const indexerResponse = result.data as any; - // errors from indexer + // Handle Indexer general errors if (indexerResponse.errors) { throw new AptosApiError( options, diff --git a/src/client/get.ts b/src/client/get.ts index 35f569403..db244b155 100644 --- a/src/client/get.ts +++ b/src/client/get.ts @@ -74,7 +74,18 @@ export async function get( export async function getAptosFullNode( options: GetAptosRequestOptions, ): Promise> { - return get({ ...options, type: AptosApiType.FULLNODE }); + const { aptosConfig } = options; + + return get({ + ...options, + type: AptosApiType.FULLNODE, + overrides: { + ...aptosConfig.clientConfig, + ...aptosConfig.fullnodeConfig, + ...options.overrides, + HEADERS: { ...aptosConfig.clientConfig?.HEADERS, ...aptosConfig.fullnodeConfig?.HEADERS }, + }, + }); } /// This function is a helper for paginating using a function wrapping an API diff --git a/src/client/post.ts b/src/client/post.ts index 74eab5e94..a04434e86 100644 --- a/src/client/post.ts +++ b/src/client/post.ts @@ -70,10 +70,7 @@ export async function post( contentType: contentType?.valueOf(), acceptType: acceptType?.valueOf(), params, - overrides: { - ...aptosConfig.clientConfig, - ...overrides, - }, + overrides, }, aptosConfig, ); @@ -82,17 +79,58 @@ export async function post( export async function postAptosFullNode( options: PostAptosRequestOptions, ): Promise> { - return post({ ...options, type: AptosApiType.FULLNODE }); + const { aptosConfig } = options; + + return post({ + ...options, + type: AptosApiType.FULLNODE, + overrides: { + ...aptosConfig.clientConfig, + ...aptosConfig.fullnodeConfig, + ...options.overrides, + HEADERS: { ...aptosConfig.clientConfig?.HEADERS, ...aptosConfig.fullnodeConfig?.HEADERS }, + }, + }); } export async function postAptosIndexer( options: PostAptosRequestOptions, ): Promise> { - return post({ ...options, type: AptosApiType.INDEXER }); + const { aptosConfig } = options; + + return post({ + ...options, + type: AptosApiType.INDEXER, + overrides: { + ...aptosConfig.clientConfig, + ...aptosConfig.indexerConfig, + ...options.overrides, + HEADERS: { ...aptosConfig.clientConfig?.HEADERS, ...aptosConfig.indexerConfig?.HEADERS }, + }, + }); } export async function postAptosFaucet( options: PostAptosRequestOptions, ): Promise> { - return post({ ...options, type: AptosApiType.FAUCET }); + const { aptosConfig } = options; + // Faucet does not support API_KEY + // Create a new object with the desired modification + const modifiedAptosConfig = { + ...aptosConfig, + clientConfig: { ...aptosConfig.clientConfig }, + }; + // Delete API_KEY config + delete modifiedAptosConfig?.clientConfig?.API_KEY; + + return post({ + ...options, + type: AptosApiType.FAUCET, + overrides: { + ...modifiedAptosConfig.clientConfig, + ...modifiedAptosConfig.faucetConfig, + ...options.overrides, + HEADERS: { ...modifiedAptosConfig.clientConfig?.HEADERS, ...modifiedAptosConfig.faucetConfig?.HEADERS }, + }, + }); } diff --git a/src/types/index.ts b/src/types/index.ts index af55a683a..f2ac134f7 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -142,6 +142,12 @@ export type AptosSettings = { readonly clientConfig?: ClientConfig; readonly client?: Client; + + readonly fullnodeConfig?: FullNodeConfig; + + readonly indexerConfig?: IndexerConfig; + + readonly faucetConfig?: FaucetConfig; }; /** @@ -174,16 +180,44 @@ export interface WhereArg { /** * A configuration object we can pass with the request to the server. * - * @param AUTH_TOKEN - an auth token to send with a faucet request * @param API_KEY - api key generated from developer portal {@link https://developers.aptoslabs.com/manage/api-keys}} * @param HEADERS - extra headers we want to send with the request * @param WITH_CREDENTIALS - whether to carry cookies. By default, it is set to true and cookies will be sent */ -export type ClientConfig = { - AUTH_TOKEN?: string; +export type ClientConfig = ClientHeadersType & { + WITH_CREDENTIALS?: boolean; API_KEY?: string; +}; + +/** + * A Fullnode only configuration object + * + * @param HEADERS - extra headers we want to send with the request + */ +export type FullNodeConfig = ClientHeadersType; + +/** + * An Indexer only configuration object + * + * @param HEADERS - extra headers we want to send with the request + */ +export type IndexerConfig = ClientHeadersType; + +/** + * A Faucet only configuration object + * + * @param HEADERS - extra headers we want to send with the request + * @param AUTH_TOKEN - an auth token to send with a faucet request + */ +export type FaucetConfig = ClientHeadersType & { + AUTH_TOKEN?: string; +}; + +/** + * General type definition for client HEADERS + */ +export type ClientHeadersType = { HEADERS?: Record; - WITH_CREDENTIALS?: boolean; }; export interface ClientRequest { @@ -192,7 +226,7 @@ export interface ClientRequest { body?: Req; contentType?: string; params?: any; - overrides?: ClientConfig; + overrides?: ClientConfig & FullNodeConfig & IndexerConfig & FaucetConfig; headers?: Record; } @@ -232,7 +266,7 @@ export type AptosRequest = { acceptType?: string; params?: Record; originMethod?: string; - overrides?: ClientConfig; + overrides?: ClientConfig & FullNodeConfig & IndexerConfig & FaucetConfig; }; /** diff --git a/tests/e2e/client/aptosRequest.test.ts b/tests/e2e/client/aptosRequest.test.ts index 2f22654aa..63f41e44d 100644 --- a/tests/e2e/client/aptosRequest.test.ts +++ b/tests/e2e/client/aptosRequest.test.ts @@ -63,53 +63,6 @@ describe("aptos request", () => { ); }); - describe("token", () => { - test( - "should not set auth_token for full node requests", - async () => { - try { - const response = await aptosRequest( - { - url: `${NetworkToNodeAPI[config.network]}`, - method: "GET", - path: "accounts/0x1", - overrides: { AUTH_TOKEN: "my-token" }, - originMethod: "test when token is set", - }, - config, - ); - expect(response.config.headers).not.toHaveProperty("authorization", "Bearer my-token"); - } catch (error: any) { - // should not get here - expect(true).toBe(false); - } - }, - longTestTimeout, - ); - - test( - "when token is not set", - async () => { - try { - const response = await aptosRequest( - { - url: `${NetworkToNodeAPI[config.network]}`, - method: "GET", - path: "accounts/0x1", - originMethod: "test when token is not set", - }, - config, - ); - expect(response.config.headers).not.toHaveProperty("authorization", "Bearer my-token"); - } catch (error: any) { - // should not get here - expect(true).toBe(false); - } - }, - longTestTimeout, - ); - }); - describe("api key", () => { test( "should set api_token for full node requests", diff --git a/tests/e2e/client/get.test.ts b/tests/e2e/client/get.test.ts new file mode 100644 index 000000000..771ecbbbd --- /dev/null +++ b/tests/e2e/client/get.test.ts @@ -0,0 +1,38 @@ +import { AptosConfig, LedgerInfo, getAptosFullNode } from "../../../src"; + +const aptosConfig = new AptosConfig({ + clientConfig: { + HEADERS: { clientConfig: "clientConfig-header" }, + API_KEY: "api-key", + }, + fullnodeConfig: { HEADERS: { fullnodeHeader: "fullnode-header" } }, + indexerConfig: { HEADERS: { indexerHeader: "indexer-header" } }, + faucetConfig: { HEADERS: { faucetHeader: "faucet-header" }, AUTH_TOKEN: "auth-token" }, +}); + +// All tests are expected to catch becuase server call will fail +// due to a fake API_KEY. But that is ok because we just want +// to test the config we set +describe("get request", () => { + describe("fullnode", () => { + test("it sets correct headers on get request", async () => { + try { + await getAptosFullNode<{}, LedgerInfo>({ + aptosConfig, + originMethod: "testGetFullnodeQuery", + path: "", + }); + } catch (e: any) { + expect(e.request.overrides.API_KEY).toEqual("api-key"); + expect(e.request.overrides.HEADERS).toHaveProperty("clientConfig"); + expect(e.request.overrides.HEADERS.clientConfig).toEqual("clientConfig-header"); + expect(e.request.overrides.HEADERS).toHaveProperty("fullnodeHeader"); + expect(e.request.overrides.HEADERS.fullnodeHeader).toEqual("fullnode-header"); + // Properties should not be included + expect(e.request.overrides.HEADERS).not.toHaveProperty("faucetConfig"); + expect(e.request.overrides.HEADERS).not.toHaveProperty("AUTH_TOKEN"); + expect(e.request.overrides.HEADERS).not.toHaveProperty("indexerHeader"); + } + }); + }); +}); diff --git a/tests/e2e/client/post.test.ts b/tests/e2e/client/post.test.ts new file mode 100644 index 000000000..31cb3ef7f --- /dev/null +++ b/tests/e2e/client/post.test.ts @@ -0,0 +1,105 @@ +import { + AptosConfig, + GraphqlQuery, + postAptosIndexer, + postAptosFullNode, + ViewRequest, + U8, + postAptosFaucet, + Account, +} from "../../../src"; +import { GetChainTopUserTransactionsQuery } from "../../../src/types/generated/operations"; +import { GetChainTopUserTransactions } from "../../../src/types/generated/queries"; + +const aptosConfig = new AptosConfig({ + clientConfig: { + HEADERS: { clientConfig: "clientConfig-header" }, + API_KEY: "api-key", + }, + fullnodeConfig: { HEADERS: { fullnodeHeader: "fullnode-header" } }, + indexerConfig: { HEADERS: { indexerHeader: "indexer-header" } }, + faucetConfig: { HEADERS: { faucetHeader: "faucet-header" }, AUTH_TOKEN: "auth-token" }, +}); + +// All tests are expected to catch becuase server call will fail +// due to a fake API_KEY. But that is ok because we just want +// to test the config we set +describe("post request", () => { + describe("indexer", () => { + test("it sets correct headers", async () => { + try { + await postAptosIndexer({ + aptosConfig, + originMethod: "testQueryIndexer", + path: "", + body: { + query: GetChainTopUserTransactions, + variables: { limit: 5 }, + }, + overrides: { WITH_CREDENTIALS: false }, + }); + } catch (e: any) { + expect(e.request.overrides.API_KEY).toEqual("api-key"); + expect(e.request.overrides.HEADERS).toHaveProperty("clientConfig"); + expect(e.request.overrides.HEADERS.clientConfig).toEqual("clientConfig-header"); + expect(e.request.overrides.HEADERS).toHaveProperty("indexerHeader"); + expect(e.request.overrides.HEADERS.indexerHeader).toEqual("indexer-header"); + // Properties should not be included + expect(e.request.overrides.HEADERS).not.toHaveProperty("fullnodeHeader"); + expect(e.request.overrides.HEADERS).not.toHaveProperty("faucetConfig"); + expect(e.request.overrides.HEADERS).not.toHaveProperty("AUTH_TOKEN"); + } + }); + }); + describe("fullnode", () => { + test("it sets correct headers on post request", async () => { + try { + await postAptosFullNode({ + aptosConfig, + originMethod: "testPostFullnodeQuery", + path: "view", + body: { + function: "0x1::aptos_chain::get", + }, + }); + } catch (e: any) { + expect(e.request.overrides.API_KEY).toEqual("api-key"); + expect(e.request.overrides.HEADERS).toHaveProperty("clientConfig"); + expect(e.request.overrides.HEADERS.clientConfig).toEqual("clientConfig-header"); + expect(e.request.overrides.HEADERS).toHaveProperty("fullnodeHeader"); + expect(e.request.overrides.HEADERS.fullnodeHeader).toEqual("fullnode-header"); + // Properties should not be included + expect(e.request.overrides.HEADERS).not.toHaveProperty("faucetConfig"); + expect(e.request.overrides.HEADERS).not.toHaveProperty("AUTH_TOKEN"); + expect(e.request.overrides.HEADERS).not.toHaveProperty("indexerHeader"); + } + }); + }); + describe("faucet", () => { + test("it sets correct headers", async () => { + const account = Account.generate(); + try { + await postAptosFaucet }>({ + aptosConfig, + path: "fund", + body: { + address: account.accountAddress.toString(), + amount: 1000, + }, + originMethod: "testQueryFaucet", + }); + } catch (e: any) { + expect(e.request.overrides).toHaveProperty("AUTH_TOKEN"); + expect(e.request.overrides.AUTH_TOKEN).toEqual("auth-token"); + expect(e.request.overrides.HEADERS).toHaveProperty("clientConfig"); + expect(e.request.overrides.HEADERS.clientConfig).toEqual("clientConfig-header"); + expect(e.request.overrides.HEADERS).toHaveProperty("faucetHeader"); + expect(e.request.overrides.HEADERS.fullnodeHeader).toEqual("faucet-header"); + // Properties should not be included + expect(e.request.overrides.HEADERS).not.toHaveProperty("fullnodeConfig"); + expect(e.request.overrides.HEADERS).not.toHaveProperty("indexerHeader"); + expect(e.request.overrides.API_KEY).not.toHaveProperty("API_KEY"); + } + }); + }); +}); diff --git a/tests/unit/aptosConfig.test.ts b/tests/unit/aptosConfig.test.ts index 64d4778ac..1f26445b4 100644 --- a/tests/unit/aptosConfig.test.ts +++ b/tests/unit/aptosConfig.test.ts @@ -91,4 +91,22 @@ describe("aptos config", () => { expect(aptosConfig.faucet).toBe("my-faucet-url"); expect(aptosConfig.indexer).toBe("my-indexer-url"); }); + + test("it sets the correct configs", () => { + const aptosConfig = new AptosConfig({ + clientConfig: { + HEADERS: { clientConfig: "header" }, + API_KEY: "api-key", + }, + faucetConfig: { HEADERS: { faucet: "header" }, AUTH_TOKEN: "auth-token" }, + indexerConfig: { HEADERS: { indexer: "header" } }, + fullnodeConfig: { HEADERS: { fullnode: "header" } }, + }); + + expect(aptosConfig.clientConfig?.HEADERS).toStrictEqual({ clientConfig: "header" }); + expect(aptosConfig.clientConfig?.API_KEY).toStrictEqual("api-key"); + expect(aptosConfig.faucetConfig).toStrictEqual({ HEADERS: { faucet: "header" }, AUTH_TOKEN: "auth-token" }); + expect(aptosConfig.indexerConfig).toStrictEqual({ HEADERS: { indexer: "header" } }); + expect(aptosConfig.fullnodeConfig).toStrictEqual({ HEADERS: { fullnode: "header" } }); + }); });