Skip to content

Commit 305d854

Browse files
authored
feat: Add createNetwork function for easy API key usage (#1800)
Co-authored-by: janniks <[email protected]>
1 parent 9fc4990 commit 305d854

File tree

2 files changed

+292
-2
lines changed

2 files changed

+292
-2
lines changed

packages/network/src/network.ts

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
import {
2-
ClientOpts,
32
DEVNET_URL,
43
FetchFn,
54
HIRO_MAINNET_URL,
65
HIRO_TESTNET_URL,
76
createFetchFn,
7+
createApiKeyMiddleware,
8+
ClientOpts,
9+
ApiKeyMiddlewareOpts,
810
} from '@stacks/common';
911
import { AddressVersion, ChainId, PeerNetworkId, TransactionVersion } from './constants';
1012
import { ClientParam } from '@stacks/common';
1113

1214
export type StacksNetwork = {
1315
chainId: number;
14-
transactionVersion: number; // todo: txVersion better?
16+
transactionVersion: number;
1517
peerNetworkId: number;
1618
magicBytes: string;
1719
bootAddress: string;
@@ -130,3 +132,117 @@ export function clientFromNetwork(network: StacksNetwork): Required<ClientOpts>
130132
fetch: createFetchFn(),
131133
};
132134
}
135+
136+
/**
137+
* Creates a customized Stacks network.
138+
*
139+
* This function allows you to create a network based on a predefined network
140+
* (mainnet, testnet, devnet, mocknet) or a custom network object. You can also customize
141+
* the network with an API key or other client options.
142+
*
143+
* @example
144+
* ```ts
145+
* // Create a basic network from a network name
146+
* const network = createNetwork('mainnet');
147+
* const network = createNetwork(STACKS_MAINNET);
148+
* ```
149+
*
150+
* @example
151+
* ```ts
152+
* // Create a network with an API key
153+
* const network = createNetwork('testnet', 'my-api-key');
154+
* const network = createNetwork(STACKS_TESTNET, 'my-api-key');
155+
* ```
156+
*
157+
* @example
158+
* ```ts
159+
* // Create a network with options object
160+
* const network = createNetwork({
161+
* network: 'mainnet',
162+
* apiKey: 'my-api-key',
163+
* });
164+
* ```
165+
*
166+
* @example
167+
* ```ts
168+
* // Create a network with options object with custom API key options
169+
* const network = createNetwork({
170+
* network: 'mainnet',
171+
* apiKey: 'my-api-key',
172+
* host: /\.example\.com$/, // default is /(.*)api(.*)(\.stacks\.co|\.hiro\.so)$/i
173+
* httpHeader: 'x-custom-api-key', // default is 'x-api-key'
174+
* });
175+
* ```
176+
*
177+
* @example
178+
* ```ts
179+
* // Create a network with custom client options
180+
* const network = createNetwork({
181+
* network: STACKS_TESTNET,
182+
* client: {
183+
* baseUrl: 'https://custom-api.example.com',
184+
* fetch: customFetchFunction
185+
* }
186+
* });
187+
* ```
188+
*/
189+
export function createNetwork(network: StacksNetworkName | StacksNetwork): StacksNetwork;
190+
export function createNetwork(
191+
network: StacksNetworkName | StacksNetwork,
192+
apiKey: string
193+
): StacksNetwork;
194+
export function createNetwork(
195+
options: {
196+
network: StacksNetworkName | StacksNetwork;
197+
client?: ClientOpts;
198+
} & Partial<ApiKeyMiddlewareOpts>
199+
): StacksNetwork;
200+
export function createNetwork(
201+
arg1:
202+
| StacksNetworkName
203+
| StacksNetwork
204+
| ({
205+
network: StacksNetworkName | StacksNetwork;
206+
client?: ClientOpts;
207+
} & Partial<ApiKeyMiddlewareOpts>),
208+
arg2?: string
209+
): StacksNetwork {
210+
const baseNetwork = networkFrom(
211+
typeof arg1 === 'object' && 'network' in arg1 ? arg1.network : arg1
212+
);
213+
214+
const newNetwork: StacksNetwork = {
215+
...baseNetwork,
216+
addressVersion: { ...baseNetwork.addressVersion }, // deep copy
217+
client: { ...baseNetwork.client }, // deep copy
218+
};
219+
220+
// Options object argument
221+
if (typeof arg1 === 'object' && 'network' in arg1) {
222+
if (arg1.client) {
223+
newNetwork.client.baseUrl = arg1.client.baseUrl ?? newNetwork.client.baseUrl;
224+
newNetwork.client.fetch = arg1.client.fetch ?? newNetwork.client.fetch;
225+
}
226+
227+
if (typeof arg1.apiKey === 'string') {
228+
const middleware = createApiKeyMiddleware(arg1 as ApiKeyMiddlewareOpts);
229+
newNetwork.client.fetch = newNetwork.client.fetch
230+
? createFetchFn(newNetwork.client.fetch, middleware)
231+
: createFetchFn(middleware);
232+
}
233+
234+
return newNetwork;
235+
}
236+
237+
// Additional API key argument
238+
if (typeof arg2 === 'string') {
239+
const middleware = createApiKeyMiddleware({ apiKey: arg2 });
240+
newNetwork.client.fetch = newNetwork.client.fetch
241+
? createFetchFn(newNetwork.client.fetch, middleware)
242+
: createFetchFn(middleware);
243+
return newNetwork;
244+
}
245+
246+
// Only network argument
247+
return newNetwork;
248+
}

packages/network/tests/network.test.ts

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1+
import { HIRO_MAINNET_URL, HIRO_TESTNET_URL, createFetchFn } from '@stacks/common';
12
import {
23
STACKS_DEVNET,
34
STACKS_MAINNET,
45
STACKS_MOCKNET,
56
STACKS_TESTNET,
7+
createNetwork,
68
networkFromName,
79
} from '../src';
810

11+
// eslint-disable-next-line
12+
import fetchMock from 'jest-fetch-mock';
13+
914
test(networkFromName.name, () => {
1015
expect(networkFromName('mainnet')).toEqual(STACKS_MAINNET);
1116
expect(networkFromName('testnet')).toEqual(STACKS_TESTNET);
@@ -14,3 +19,172 @@ test(networkFromName.name, () => {
1419

1520
expect(STACKS_DEVNET).toEqual(STACKS_MOCKNET);
1621
});
22+
23+
describe(createNetwork.name, () => {
24+
const TEST_API_KEY = 'test-api-key';
25+
26+
beforeEach(() => {
27+
fetchMock.resetMocks();
28+
fetchMock.mockResponse(JSON.stringify({ result: 'ok' }));
29+
});
30+
31+
test('creates network from network name string', () => {
32+
const network = createNetwork('mainnet');
33+
expect(network.chainId).toEqual(STACKS_MAINNET.chainId);
34+
expect(network.transactionVersion).toEqual(STACKS_MAINNET.transactionVersion);
35+
expect(network.client.baseUrl).toEqual(STACKS_MAINNET.client.baseUrl);
36+
});
37+
38+
test('creates network from network object', () => {
39+
const network = createNetwork(STACKS_TESTNET);
40+
expect(network.chainId).toEqual(STACKS_TESTNET.chainId);
41+
expect(network.transactionVersion).toEqual(STACKS_TESTNET.transactionVersion);
42+
expect(network.client.baseUrl).toEqual(STACKS_TESTNET.client.baseUrl);
43+
expect(network.client.fetch).toBeUndefined();
44+
});
45+
46+
test('creates network from network name string with API key', async () => {
47+
const network = createNetwork('testnet', TEST_API_KEY);
48+
expect(network.chainId).toEqual(STACKS_TESTNET.chainId);
49+
expect(network.transactionVersion).toEqual(STACKS_TESTNET.transactionVersion);
50+
expect(network.client.baseUrl).toEqual(STACKS_TESTNET.client.baseUrl);
51+
expect(network.client.fetch).toBeDefined();
52+
53+
// Test that API key is included in requests
54+
expect(network.client.fetch).not.toBeUndefined();
55+
if (!network.client.fetch) throw 'Type error';
56+
57+
await network.client.fetch(HIRO_TESTNET_URL);
58+
expect(fetchMock).toHaveBeenCalled();
59+
const callHeaders = new Headers(fetchMock.mock.calls[0][1]?.headers);
60+
expect(callHeaders.has('x-api-key')).toBeTruthy();
61+
expect(callHeaders.get('x-api-key')).toBe(TEST_API_KEY);
62+
});
63+
64+
test('creates network from network object with API key', async () => {
65+
const network = createNetwork(STACKS_MAINNET, TEST_API_KEY);
66+
expect(network.chainId).toEqual(STACKS_MAINNET.chainId);
67+
expect(network.transactionVersion).toEqual(STACKS_MAINNET.transactionVersion);
68+
expect(network.client.baseUrl).toEqual(STACKS_MAINNET.client.baseUrl);
69+
expect(network.client.fetch).toBeDefined();
70+
71+
// Test that API key is included in requests
72+
expect(network.client.fetch).not.toBeUndefined();
73+
if (!network.client.fetch) throw 'Type error';
74+
75+
await network.client.fetch(HIRO_TESTNET_URL);
76+
expect(fetchMock).toHaveBeenCalled();
77+
const callHeaders = new Headers(fetchMock.mock.calls[0][1]?.headers);
78+
expect(callHeaders.has('x-api-key')).toBeTruthy();
79+
expect(callHeaders.get('x-api-key')).toBe(TEST_API_KEY);
80+
});
81+
82+
test('creates network from options object with network name and API key', async () => {
83+
const network = createNetwork({
84+
network: 'mainnet',
85+
apiKey: TEST_API_KEY,
86+
});
87+
expect(network.chainId).toEqual(STACKS_MAINNET.chainId);
88+
expect(network.transactionVersion).toEqual(STACKS_MAINNET.transactionVersion);
89+
expect(network.client.baseUrl).toEqual(STACKS_MAINNET.client.baseUrl);
90+
expect(network.client.fetch).toBeDefined();
91+
92+
// Test that API key is included in requests
93+
expect(network.client.fetch).not.toBeUndefined();
94+
if (!network.client.fetch) throw 'Type error';
95+
96+
await network.client.fetch(HIRO_MAINNET_URL);
97+
expect(fetchMock).toHaveBeenCalled();
98+
const callHeaders = new Headers(fetchMock.mock.calls[0][1]?.headers);
99+
expect(callHeaders.has('x-api-key')).toBeTruthy();
100+
expect(callHeaders.get('x-api-key')).toBe(TEST_API_KEY);
101+
});
102+
103+
test('creates network from options object with network name, API key, and custom host', async () => {
104+
const network = createNetwork({
105+
network: 'devnet',
106+
apiKey: TEST_API_KEY,
107+
host: /^/, // any host
108+
});
109+
expect(network.chainId).toEqual(STACKS_DEVNET.chainId);
110+
expect(network.transactionVersion).toEqual(STACKS_DEVNET.transactionVersion);
111+
expect(network.client.baseUrl).toEqual(STACKS_DEVNET.client.baseUrl);
112+
expect(network.client.fetch).toBeDefined();
113+
114+
// Test that API key is included in requests
115+
expect(network.client.fetch).not.toBeUndefined();
116+
if (!network.client.fetch) throw 'Type error';
117+
118+
await network.client.fetch('https://example.com');
119+
expect(fetchMock).toHaveBeenCalled();
120+
const callHeaders = new Headers(fetchMock.mock.calls[0][1]?.headers);
121+
expect(callHeaders.has('x-api-key')).toBeTruthy();
122+
expect(callHeaders.get('x-api-key')).toBe(TEST_API_KEY);
123+
});
124+
125+
test('creates network from options object with network object and API key', async () => {
126+
const network = createNetwork({
127+
network: STACKS_MOCKNET,
128+
apiKey: TEST_API_KEY,
129+
});
130+
expect(network.chainId).toEqual(STACKS_MOCKNET.chainId);
131+
expect(network.transactionVersion).toEqual(STACKS_MOCKNET.transactionVersion);
132+
expect(network.client.baseUrl).toEqual(STACKS_MOCKNET.client.baseUrl);
133+
expect(network.client.fetch).toBeDefined();
134+
135+
// Test that API key is included in requests
136+
expect(network.client.fetch).not.toBeUndefined();
137+
if (!network.client.fetch) throw 'Type error';
138+
139+
await network.client.fetch(HIRO_TESTNET_URL);
140+
expect(fetchMock).toHaveBeenCalled();
141+
const callHeaders = new Headers(fetchMock.mock.calls[0][1]?.headers);
142+
expect(callHeaders.has('x-api-key')).toBeTruthy();
143+
expect(callHeaders.get('x-api-key')).toBe(TEST_API_KEY);
144+
});
145+
146+
test('creates network from options object with network name and custom client', () => {
147+
const customBaseUrl = 'https://custom-api.example.com';
148+
const customFetch = createFetchFn();
149+
150+
const network = createNetwork({
151+
network: 'mainnet',
152+
client: {
153+
baseUrl: customBaseUrl,
154+
fetch: customFetch,
155+
},
156+
});
157+
158+
expect(network.chainId).toEqual(STACKS_MAINNET.chainId);
159+
expect(network.transactionVersion).toEqual(STACKS_MAINNET.transactionVersion);
160+
expect(network.client.baseUrl).toEqual(customBaseUrl);
161+
expect(network.client.fetch).toBe(customFetch);
162+
});
163+
164+
test('creates network from options object with network object and custom client', () => {
165+
const customBaseUrl = 'https://custom-api.example.com';
166+
const customFetch = createFetchFn();
167+
168+
const network = createNetwork({
169+
network: STACKS_TESTNET,
170+
client: {
171+
baseUrl: customBaseUrl,
172+
fetch: customFetch,
173+
},
174+
});
175+
176+
expect(network.chainId).toEqual(STACKS_TESTNET.chainId);
177+
expect(network.transactionVersion).toEqual(STACKS_TESTNET.transactionVersion);
178+
expect(network.client.baseUrl).toEqual(customBaseUrl);
179+
expect(network.client.fetch).toBe(customFetch);
180+
});
181+
182+
test('throws error with invalid arguments', () => {
183+
// @ts-expect-error Testing invalid argument
184+
expect(() => createNetwork()).toThrow();
185+
// @ts-expect-error Testing invalid argument
186+
expect(() => createNetwork(null)).toThrow();
187+
// @ts-expect-error Testing invalid argument
188+
expect(() => createNetwork(undefined, undefined)).toThrow();
189+
});
190+
});

0 commit comments

Comments
 (0)