Skip to content

Commit

Permalink
Add support to allow setting per-backend configuration (#336)
Browse files Browse the repository at this point in the history
  • Loading branch information
0xmaayan authored Mar 25, 2024
1 parent 07728a2 commit 2bf4f02
Show file tree
Hide file tree
Showing 10 changed files with 295 additions and 69 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
27 changes: 25 additions & 2 deletions src/api/aptosConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@
// 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";

/**
* 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;

/**
Expand All @@ -33,15 +35,36 @@ 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;
this.faucet = settings?.faucet;
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 ?? {};
}

/**
Expand Down
14 changes: 8 additions & 6 deletions src/client/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,10 @@ export async function request<Req, Res>(options: ClientRequest<Req>, 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}`;
}

Expand Down Expand Up @@ -81,11 +78,16 @@ export async function aptosRequest<Req extends {}, Res extends {}>(
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,
Expand Down
13 changes: 12 additions & 1 deletion src/client/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,18 @@ export async function get<Req extends {}, Res extends {}>(
export async function getAptosFullNode<Req extends {}, Res extends {}>(
options: GetAptosRequestOptions,
): Promise<AptosResponse<Req, Res>> {
return get<Req, Res>({ ...options, type: AptosApiType.FULLNODE });
const { aptosConfig } = options;

return get<Req, Res>({
...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
Expand Down
52 changes: 45 additions & 7 deletions src/client/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,7 @@ export async function post<Req extends {}, Res extends {}>(
contentType: contentType?.valueOf(),
acceptType: acceptType?.valueOf(),
params,
overrides: {
...aptosConfig.clientConfig,
...overrides,
},
overrides,
},
aptosConfig,
);
Expand All @@ -82,17 +79,58 @@ export async function post<Req extends {}, Res extends {}>(
export async function postAptosFullNode<Req extends {}, Res extends {}>(
options: PostAptosRequestOptions,
): Promise<AptosResponse<Req, Res>> {
return post<Req, Res>({ ...options, type: AptosApiType.FULLNODE });
const { aptosConfig } = options;

return post<Req, Res>({
...options,
type: AptosApiType.FULLNODE,
overrides: {
...aptosConfig.clientConfig,
...aptosConfig.fullnodeConfig,
...options.overrides,
HEADERS: { ...aptosConfig.clientConfig?.HEADERS, ...aptosConfig.fullnodeConfig?.HEADERS },
},
});
}

export async function postAptosIndexer<Req extends {}, Res extends {}>(
options: PostAptosRequestOptions,
): Promise<AptosResponse<Req, Res>> {
return post<Req, Res>({ ...options, type: AptosApiType.INDEXER });
const { aptosConfig } = options;

return post<Req, Res>({
...options,
type: AptosApiType.INDEXER,
overrides: {
...aptosConfig.clientConfig,
...aptosConfig.indexerConfig,
...options.overrides,
HEADERS: { ...aptosConfig.clientConfig?.HEADERS, ...aptosConfig.indexerConfig?.HEADERS },
},
});
}

export async function postAptosFaucet<Req extends {}, Res extends {}>(
options: PostAptosRequestOptions,
): Promise<AptosResponse<Req, Res>> {
return post<Req, Res>({ ...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<Req, Res>({
...options,
type: AptosApiType.FAUCET,
overrides: {
...modifiedAptosConfig.clientConfig,
...modifiedAptosConfig.faucetConfig,
...options.overrides,
HEADERS: { ...modifiedAptosConfig.clientConfig?.HEADERS, ...modifiedAptosConfig.faucetConfig?.HEADERS },
},
});
}
46 changes: 40 additions & 6 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ export type AptosSettings = {
readonly clientConfig?: ClientConfig;

readonly client?: Client;

readonly fullnodeConfig?: FullNodeConfig;

readonly indexerConfig?: IndexerConfig;

readonly faucetConfig?: FaucetConfig;
};

/**
Expand Down Expand Up @@ -174,16 +180,44 @@ export interface WhereArg<T extends {}> {
/**
* 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<string, string | number | boolean>;
WITH_CREDENTIALS?: boolean;
};

export interface ClientRequest<Req> {
Expand All @@ -192,7 +226,7 @@ export interface ClientRequest<Req> {
body?: Req;
contentType?: string;
params?: any;
overrides?: ClientConfig;
overrides?: ClientConfig & FullNodeConfig & IndexerConfig & FaucetConfig;
headers?: Record<string, any>;
}

Expand Down Expand Up @@ -232,7 +266,7 @@ export type AptosRequest = {
acceptType?: string;
params?: Record<string, string | AnyNumber | boolean | undefined>;
originMethod?: string;
overrides?: ClientConfig;
overrides?: ClientConfig & FullNodeConfig & IndexerConfig & FaucetConfig;
};

/**
Expand Down
47 changes: 0 additions & 47 deletions tests/e2e/client/aptosRequest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
38 changes: 38 additions & 0 deletions tests/e2e/client/get.test.ts
Original file line number Diff line number Diff line change
@@ -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");
}
});
});
});
Loading

0 comments on commit 2bf4f02

Please sign in to comment.