Skip to content

feat: Add createNetwork function for easy API key usage #1800

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
120 changes: 118 additions & 2 deletions packages/network/src/network.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import {
ClientOpts,
DEVNET_URL,
FetchFn,
HIRO_MAINNET_URL,
HIRO_TESTNET_URL,
createFetchFn,
createApiKeyMiddleware,
ClientOpts,
ApiKeyMiddlewareOpts,
} from '@stacks/common';
import { AddressVersion, ChainId, PeerNetworkId, TransactionVersion } from './constants';
import { ClientParam } from '@stacks/common';

export type StacksNetwork = {
chainId: number;
transactionVersion: number; // todo: txVersion better?
transactionVersion: number;
peerNetworkId: number;
magicBytes: string;
bootAddress: string;
Expand Down Expand Up @@ -130,3 +132,117 @@
fetch: createFetchFn(),
};
}

/**
* Creates a customized Stacks network.
*
* This function allows you to create a network based on a predefined network
* (mainnet, testnet, devnet, mocknet) or a custom network object. You can also customize
* the network with an API key or other client options.
*
* @example
* ```ts
* // Create a basic network from a network name
* const network = createNetwork('mainnet');
* const network = createNetwork(STACKS_MAINNET);
* ```
*
* @example
* ```ts
* // Create a network with an API key
* const network = createNetwork('testnet', 'my-api-key');
* const network = createNetwork(STACKS_TESTNET, 'my-api-key');
* ```
*
* @example
* ```ts
* // Create a network with options object
* const network = createNetwork({
* network: 'mainnet',
* apiKey: 'my-api-key',
* });
* ```
*
* @example
* ```ts
* // Create a network with options object with custom API key options
* const network = createNetwork({
* network: 'mainnet',
* apiKey: 'my-api-key',
* host: /\.example\.com$/, // default is /(.*)api(.*)(\.stacks\.co|\.hiro\.so)$/i
* httpHeader: 'x-custom-api-key', // default is 'x-api-key'
* });
* ```
*
* @example
* ```ts
* // Create a network with custom client options
* const network = createNetwork({
* network: STACKS_TESTNET,
* client: {
* baseUrl: 'https://custom-api.example.com',
* fetch: customFetchFunction
* }
* });
* ```
*/
export function createNetwork(network: StacksNetworkName | StacksNetwork): StacksNetwork;
export function createNetwork(
network: StacksNetworkName | StacksNetwork,
apiKey: string
): StacksNetwork;
export function createNetwork(
options: {
network: StacksNetworkName | StacksNetwork;
client?: ClientOpts;
} & Partial<ApiKeyMiddlewareOpts>
): StacksNetwork;
export function createNetwork(
arg1:
| StacksNetworkName
| StacksNetwork
| ({
network: StacksNetworkName | StacksNetwork;
client?: ClientOpts;
} & Partial<ApiKeyMiddlewareOpts>),
arg2?: string
): StacksNetwork {
const baseNetwork = networkFrom(
typeof arg1 === 'object' && 'network' in arg1 ? arg1.network : arg1
);

const newNetwork: StacksNetwork = {
...baseNetwork,
addressVersion: { ...baseNetwork.addressVersion }, // deep copy
client: { ...baseNetwork.client }, // deep copy
};

// Options object argument
if (typeof arg1 === 'object' && 'network' in arg1) {
if (arg1.client) {
newNetwork.client.baseUrl = arg1.client.baseUrl ?? newNetwork.client.baseUrl;
newNetwork.client.fetch = arg1.client.fetch ?? newNetwork.client.fetch;
}

if (typeof arg1.apiKey === 'string') {
const middleware = createApiKeyMiddleware(arg1 as ApiKeyMiddlewareOpts);
newNetwork.client.fetch = newNetwork.client.fetch
? createFetchFn(newNetwork.client.fetch, middleware)

Check warning on line 230 in packages/network/src/network.ts

View check run for this annotation

Codecov / codecov/patch

packages/network/src/network.ts#L230

Added line #L230 was not covered by tests
: createFetchFn(middleware);
}

return newNetwork;
}

// Additional API key argument
if (typeof arg2 === 'string') {
const middleware = createApiKeyMiddleware({ apiKey: arg2 });
newNetwork.client.fetch = newNetwork.client.fetch
? createFetchFn(newNetwork.client.fetch, middleware)

Check warning on line 241 in packages/network/src/network.ts

View check run for this annotation

Codecov / codecov/patch

packages/network/src/network.ts#L241

Added line #L241 was not covered by tests
: createFetchFn(middleware);
return newNetwork;
}

// Only network argument
return newNetwork;
}
174 changes: 174 additions & 0 deletions packages/network/tests/network.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { HIRO_MAINNET_URL, HIRO_TESTNET_URL, createFetchFn } from '@stacks/common';
import {
STACKS_DEVNET,
STACKS_MAINNET,
STACKS_MOCKNET,
STACKS_TESTNET,
createNetwork,
networkFromName,
} from '../src';

// eslint-disable-next-line
import fetchMock from 'jest-fetch-mock';

test(networkFromName.name, () => {
expect(networkFromName('mainnet')).toEqual(STACKS_MAINNET);
expect(networkFromName('testnet')).toEqual(STACKS_TESTNET);
Expand All @@ -14,3 +19,172 @@ test(networkFromName.name, () => {

expect(STACKS_DEVNET).toEqual(STACKS_MOCKNET);
});

describe(createNetwork.name, () => {
const TEST_API_KEY = 'test-api-key';

beforeEach(() => {
fetchMock.resetMocks();
fetchMock.mockResponse(JSON.stringify({ result: 'ok' }));
});

test('creates network from network name string', () => {
const network = createNetwork('mainnet');
expect(network.chainId).toEqual(STACKS_MAINNET.chainId);
expect(network.transactionVersion).toEqual(STACKS_MAINNET.transactionVersion);
expect(network.client.baseUrl).toEqual(STACKS_MAINNET.client.baseUrl);
});

test('creates network from network object', () => {
const network = createNetwork(STACKS_TESTNET);
expect(network.chainId).toEqual(STACKS_TESTNET.chainId);
expect(network.transactionVersion).toEqual(STACKS_TESTNET.transactionVersion);
expect(network.client.baseUrl).toEqual(STACKS_TESTNET.client.baseUrl);
expect(network.client.fetch).toBeUndefined();
});

test('creates network from network name string with API key', async () => {
const network = createNetwork('testnet', TEST_API_KEY);
expect(network.chainId).toEqual(STACKS_TESTNET.chainId);
expect(network.transactionVersion).toEqual(STACKS_TESTNET.transactionVersion);
expect(network.client.baseUrl).toEqual(STACKS_TESTNET.client.baseUrl);
expect(network.client.fetch).toBeDefined();

// Test that API key is included in requests
expect(network.client.fetch).not.toBeUndefined();
if (!network.client.fetch) throw 'Type error';

await network.client.fetch(HIRO_TESTNET_URL);
expect(fetchMock).toHaveBeenCalled();
const callHeaders = new Headers(fetchMock.mock.calls[0][1]?.headers);
expect(callHeaders.has('x-api-key')).toBeTruthy();
expect(callHeaders.get('x-api-key')).toBe(TEST_API_KEY);
});

test('creates network from network object with API key', async () => {
const network = createNetwork(STACKS_MAINNET, TEST_API_KEY);
expect(network.chainId).toEqual(STACKS_MAINNET.chainId);
expect(network.transactionVersion).toEqual(STACKS_MAINNET.transactionVersion);
expect(network.client.baseUrl).toEqual(STACKS_MAINNET.client.baseUrl);
expect(network.client.fetch).toBeDefined();

// Test that API key is included in requests
expect(network.client.fetch).not.toBeUndefined();
if (!network.client.fetch) throw 'Type error';

await network.client.fetch(HIRO_TESTNET_URL);
expect(fetchMock).toHaveBeenCalled();
const callHeaders = new Headers(fetchMock.mock.calls[0][1]?.headers);
expect(callHeaders.has('x-api-key')).toBeTruthy();
expect(callHeaders.get('x-api-key')).toBe(TEST_API_KEY);
});

test('creates network from options object with network name and API key', async () => {
const network = createNetwork({
network: 'mainnet',
apiKey: TEST_API_KEY,
});
expect(network.chainId).toEqual(STACKS_MAINNET.chainId);
expect(network.transactionVersion).toEqual(STACKS_MAINNET.transactionVersion);
expect(network.client.baseUrl).toEqual(STACKS_MAINNET.client.baseUrl);
expect(network.client.fetch).toBeDefined();

// Test that API key is included in requests
expect(network.client.fetch).not.toBeUndefined();
if (!network.client.fetch) throw 'Type error';

await network.client.fetch(HIRO_MAINNET_URL);
expect(fetchMock).toHaveBeenCalled();
const callHeaders = new Headers(fetchMock.mock.calls[0][1]?.headers);
expect(callHeaders.has('x-api-key')).toBeTruthy();
expect(callHeaders.get('x-api-key')).toBe(TEST_API_KEY);
});

test('creates network from options object with network name, API key, and custom host', async () => {
const network = createNetwork({
network: 'devnet',
apiKey: TEST_API_KEY,
host: /^/, // any host
});
expect(network.chainId).toEqual(STACKS_DEVNET.chainId);
expect(network.transactionVersion).toEqual(STACKS_DEVNET.transactionVersion);
expect(network.client.baseUrl).toEqual(STACKS_DEVNET.client.baseUrl);
expect(network.client.fetch).toBeDefined();

// Test that API key is included in requests
expect(network.client.fetch).not.toBeUndefined();
if (!network.client.fetch) throw 'Type error';

await network.client.fetch('https://example.com');
expect(fetchMock).toHaveBeenCalled();
const callHeaders = new Headers(fetchMock.mock.calls[0][1]?.headers);
expect(callHeaders.has('x-api-key')).toBeTruthy();
expect(callHeaders.get('x-api-key')).toBe(TEST_API_KEY);
});

test('creates network from options object with network object and API key', async () => {
const network = createNetwork({
network: STACKS_MOCKNET,
apiKey: TEST_API_KEY,
});
expect(network.chainId).toEqual(STACKS_MOCKNET.chainId);
expect(network.transactionVersion).toEqual(STACKS_MOCKNET.transactionVersion);
expect(network.client.baseUrl).toEqual(STACKS_MOCKNET.client.baseUrl);
expect(network.client.fetch).toBeDefined();

// Test that API key is included in requests
expect(network.client.fetch).not.toBeUndefined();
if (!network.client.fetch) throw 'Type error';

await network.client.fetch(HIRO_TESTNET_URL);
expect(fetchMock).toHaveBeenCalled();
const callHeaders = new Headers(fetchMock.mock.calls[0][1]?.headers);
expect(callHeaders.has('x-api-key')).toBeTruthy();
expect(callHeaders.get('x-api-key')).toBe(TEST_API_KEY);
});

test('creates network from options object with network name and custom client', () => {
const customBaseUrl = 'https://custom-api.example.com';
const customFetch = createFetchFn();

const network = createNetwork({
network: 'mainnet',
client: {
baseUrl: customBaseUrl,
fetch: customFetch,
},
});

expect(network.chainId).toEqual(STACKS_MAINNET.chainId);
expect(network.transactionVersion).toEqual(STACKS_MAINNET.transactionVersion);
expect(network.client.baseUrl).toEqual(customBaseUrl);
expect(network.client.fetch).toBe(customFetch);
});

test('creates network from options object with network object and custom client', () => {
const customBaseUrl = 'https://custom-api.example.com';
const customFetch = createFetchFn();

const network = createNetwork({
network: STACKS_TESTNET,
client: {
baseUrl: customBaseUrl,
fetch: customFetch,
},
});

expect(network.chainId).toEqual(STACKS_TESTNET.chainId);
expect(network.transactionVersion).toEqual(STACKS_TESTNET.transactionVersion);
expect(network.client.baseUrl).toEqual(customBaseUrl);
expect(network.client.fetch).toBe(customFetch);
});

test('throws error with invalid arguments', () => {
// @ts-expect-error Testing invalid argument
expect(() => createNetwork()).toThrow();
// @ts-expect-error Testing invalid argument
expect(() => createNetwork(null)).toThrow();
// @ts-expect-error Testing invalid argument
expect(() => createNetwork(undefined, undefined)).toThrow();
});
});
Loading