Skip to content

Commit

Permalink
feat!: update to new network object (#1623)
Browse files Browse the repository at this point in the history
* feat: split network into object and api opts

* fix: update imports

---------

Co-authored-by: janniks <[email protected]>
  • Loading branch information
janniks and janniks committed Mar 18, 2024
1 parent b7e00da commit f28eb69
Show file tree
Hide file tree
Showing 26 changed files with 997 additions and 1,418 deletions.
23 changes: 19 additions & 4 deletions packages/common/src/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
// Define a default request options and allow modification using getters, setters

import { HIRO_MAINNET_URL } from './constants';

// Reference: https://developer.mozilla.org/en-US/docs/Web/API/Request/Request
const defaultFetchOpts: RequestInit = {
// By default referrer value will be client:origin: above reference link
Expand Down Expand Up @@ -48,13 +51,16 @@ export async function fetchWrapper(input: RequestInfo, init?: RequestInit): Prom

export type FetchFn = (url: string, init?: RequestInit) => Promise<Response>;

/** @ignore Internally used for letting networking functions specify "API" options */
export type ApiOpts = {
url?: string;
fetch?: FetchFn;
};

/** @ignore Internally used for letting networking functions specify "API" options */
export type ApiParam = {
/** Optional API object (for `.url` and `.fetch`) used for API/Node, defaults to use mainnet */
api?: {
url: string;
fetch: FetchFn;
};
api?: ApiOpts;
};

export interface RequestContext {
Expand Down Expand Up @@ -183,3 +189,12 @@ export function createFetchFn(...args: any[]): FetchFn {
};
return fetchFn;
}

/** @ignore Creates a API-like object, which can be used without circular dependencies */
export function defaultApiLike(opts?: { url?: string; fetch?: FetchFn }) {
return {
// todo: do we want network here as well?
url: opts?.url ?? HIRO_MAINNET_URL,
fetch: opts?.fetch ?? createFetchFn(),
};
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import fetchMock from 'jest-fetch-mock';
import { fetchWrapper, getFetchOptions, setFetchOptions } from '@stacks/common';
import { fetchWrapper, getFetchOptions, setFetchOptions } from '../src/fetch';

test('Verify fetch private options', async () => {
const defaultOptioins = getFetchOptions();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
FetchMiddleware,
RequestContext,
ResponseContext,
} from '@stacks/common';
} from '../src/fetch';

beforeEach(() => {
fetchMock.resetMocks();
Expand Down
42 changes: 42 additions & 0 deletions packages/network/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* The chain ID (unsigned 32-bit integer), used so transactions can't be replayed on other chains.
* Similar to the {@link TransactionVersion}.
*/
export enum ChainId {
Mainnet = 0x00000001,
Testnet = 0x80000000,
}

/**
* The **peer** network ID.
* Typically not used in signing, but used for broadcasting to the P2P network.
* It can also be used to determine the parent of a subnet.
*
* **Attention:**
* For mainnet/testnet the v2/info response `.network_id` refers to the chain ID.
* For subnets the v2/info response `.network_id` refers to the peer network ID and the chain ID (they are the same for subnets).
* The `.parent_network_id` refers to the actual peer network ID (of the parent) in both cases.
*/
export enum PeerNetworkId {
Mainnet = 0x17000000,
Testnet = 0xff000000,
}

export const DEFAULT_CHAIN_ID = ChainId.Mainnet;

/**
* The transaction version, used so transactions can't be replayed on other networks.
* Similar to the {@link ChainId}.
* Used internally for serializing and deserializing transactions.
*/
export enum TransactionVersion {
Mainnet = 0x00,
Testnet = 0x80,
}

export const DEFAULT_TRANSACTION_VERSION = TransactionVersion.Mainnet;

/** @ignore */
export function whenTransactionVersion(transactionVersion: TransactionVersion) {
return <T>(map: Record<TransactionVersion, T>): T => map[transactionVersion];
}
1 change: 1 addition & 0 deletions packages/network/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './constants';
export * from './network';
252 changes: 51 additions & 201 deletions packages/network/src/network.ts
Original file line number Diff line number Diff line change
@@ -1,215 +1,65 @@
import { TransactionVersion, ChainID } from '@stacks/common';
import { createFetchFn, FetchFn } from '@stacks/common';

export const HIRO_MAINNET_DEFAULT = 'https://api.mainnet.hiro.so';
export const HIRO_TESTNET_DEFAULT = 'https://api.testnet.hiro.so';
export const HIRO_MOCKNET_DEFAULT = 'http://localhost:3999';

/**
* Used for constructing Network instances
* @related {@link StacksNetwork}, {@link StacksMainnet}, {@link StacksTestnet}, {@link StacksDevnet}, {@link StacksMocknet}
*/
export interface NetworkConfig {
/** The base API/node URL for the network fetch calls */
url: string;
/** An optional custom fetch function to override default behaviors */
fetchFn?: FetchFn;
import { DEVNET_URL, HIRO_MAINNET_URL, HIRO_TESTNET_URL } from '@stacks/common';
import { ChainId, PeerNetworkId, TransactionVersion } from './constants';

export interface StacksNetwork {
chainId: number;
transactionVersion: number; // todo: txVersion better?
peerNetworkId: number;
magicBytes: string;
// todo: add check32 character bytes string
}

export const STACKS_MAINNET: StacksNetwork = {
chainId: ChainId.Mainnet,
transactionVersion: TransactionVersion.Mainnet,
peerNetworkId: PeerNetworkId.Mainnet,
magicBytes: 'X2', // todo: comment bytes version of magic bytes
};

export const STACKS_TESTNET: StacksNetwork = {
chainId: ChainId.Testnet,
transactionVersion: TransactionVersion.Testnet,
peerNetworkId: PeerNetworkId.Testnet,
magicBytes: 'T2', // todo: comment bytes version of magic bytes
};

export const STACKS_DEVNET: StacksNetwork = {
...STACKS_TESTNET,
magicBytes: 'id', // todo: comment bytes version of magic bytes
};
export const STACKS_MOCKNET: StacksNetwork = { ...STACKS_DEVNET };

/** @ignore internal */
export const StacksNetworks = ['mainnet', 'testnet', 'devnet', 'mocknet'] as const;
/** The enum-style names of different common Stacks networks */
export type StacksNetworkName = (typeof StacksNetworks)[number];

/**
* The base class for Stacks networks. Typically used via its subclasses.
* @related {@link StacksMainnet}, {@link StacksTestnet}, {@link StacksDevnet}, {@link StacksMocknet}
*/
export class StacksNetwork {
version: TransactionVersion = TransactionVersion.Mainnet;
chainId: ChainID = ChainID.Mainnet;
bnsLookupUrl = 'https://api.mainnet.hiro.so';
broadcastEndpoint = '/v2/transactions';
transferFeeEstimateEndpoint = '/v2/fees/transfer';
transactionFeeEstimateEndpoint = '/v2/fees/transaction';
accountEndpoint = '/v2/accounts';
contractAbiEndpoint = '/v2/contracts/interface';
readOnlyFunctionCallEndpoint = '/v2/contracts/call-read';

readonly coreApiUrl: string;

fetchFn: FetchFn;

constructor(networkConfig: NetworkConfig) {
this.coreApiUrl = networkConfig.url;
this.fetchFn = networkConfig.fetchFn ?? createFetchFn();
}

/** A static network constructor from a network name */
static fromName = (networkName: StacksNetworkName): StacksNetwork => {
switch (networkName) {
case 'mainnet':
return new StacksMainnet();
case 'testnet':
return new StacksTestnet();
case 'devnet':
return new StacksDevnet();
case 'mocknet':
return new StacksMocknet();
default:
throw new Error(
`Invalid network name provided. Must be one of the following: ${StacksNetworks.join(
', '
)}`
);
}
};

/** @ignore internal */
static fromNameOrNetwork = (network: StacksNetworkName | StacksNetwork) => {
if (typeof network !== 'string' && 'version' in network) {
return network;
}

return StacksNetwork.fromName(network);
};

/** Returns `true` if the network is configured to 'mainnet', based on the TransactionVersion */
isMainnet = () => this.version === TransactionVersion.Mainnet;
getBroadcastApiUrl = () => `${this.coreApiUrl}${this.broadcastEndpoint}`;
getTransferFeeEstimateApiUrl = () => `${this.coreApiUrl}${this.transferFeeEstimateEndpoint}`;
getTransactionFeeEstimateApiUrl = () =>
`${this.coreApiUrl}${this.transactionFeeEstimateEndpoint}`;
getAccountApiUrl = (address: string) =>
`${this.coreApiUrl}${this.accountEndpoint}/${address}?proof=0`;
getAccountExtendedBalancesApiUrl = (address: string) =>
`${this.coreApiUrl}/extended/v1/address/${address}/balances`;
getAbiApiUrl = (address: string, contract: string) =>
`${this.coreApiUrl}${this.contractAbiEndpoint}/${address}/${contract}`;
getReadOnlyFunctionCallApiUrl = (
contractAddress: string,
contractName: string,
functionName: string
) =>
`${this.coreApiUrl}${
this.readOnlyFunctionCallEndpoint
}/${contractAddress}/${contractName}/${encodeURIComponent(functionName)}`;
getInfoUrl = () => `${this.coreApiUrl}/v2/info`;
getBlockTimeInfoUrl = () => `${this.coreApiUrl}/extended/v1/info/network_block_times`;
getPoxInfoUrl = () => `${this.coreApiUrl}/v2/pox`;
getRewardsUrl = (address: string, options?: any) => {
let url = `${this.coreApiUrl}/extended/v1/burnchain/rewards/${address}`;
if (options) {
url = `${url}?limit=${options.limit}&offset=${options.offset}`;
}
return url;
};
getRewardsTotalUrl = (address: string) =>
`${this.coreApiUrl}/extended/v1/burnchain/rewards/${address}/total`;
getRewardHoldersUrl = (address: string, options?: any) => {
let url = `${this.coreApiUrl}/extended/v1/burnchain/reward_slot_holders/${address}`;
if (options) {
url = `${url}?limit=${options.limit}&offset=${options.offset}`;
}
return url;
};
getStackerInfoUrl = (contractAddress: string, contractName: string) =>
`${this.coreApiUrl}${this.readOnlyFunctionCallEndpoint}
${contractAddress}/${contractName}/get-stacker-info`;
getDataVarUrl = (contractAddress: string, contractName: string, dataVarName: string) =>
`${this.coreApiUrl}/v2/data_var/${contractAddress}/${contractName}/${dataVarName}?proof=0`;
getMapEntryUrl = (contractAddress: string, contractName: string, mapName: string) =>
`${this.coreApiUrl}/v2/map_entry/${contractAddress}/${contractName}/${mapName}?proof=0`;
getNameInfo(fullyQualifiedName: string) {
/*
TODO: Update to v2 API URL for name lookups
*/
const nameLookupURL = `${this.bnsLookupUrl}/v1/names/${fullyQualifiedName}`;
return this.fetchFn(nameLookupURL)
.then(resp => {
if (resp.status === 404) {
throw new Error('Name not found');
} else if (resp.status !== 200) {
throw new Error(`Bad response status: ${resp.status}`);
} else {
return resp.json();
}
})
.then(nameInfo => {
// the returned address _should_ be in the correct network ---
// stacks node gets into trouble because it tries to coerce back to mainnet
// and the regtest transaction generation libraries want to use testnet addresses
if (nameInfo.address) {
return Object.assign({}, nameInfo, { address: nameInfo.address });
} else {
return nameInfo;
}
});
export function networkFromName(name: StacksNetworkName) {
switch (name) {
case 'mainnet':
return STACKS_MAINNET;
case 'testnet':
return STACKS_TESTNET;
case 'devnet':
return STACKS_DEVNET;
case 'mocknet':
return STACKS_MOCKNET;
default:
throw new Error(`Unknown network name: ${name}`);
}
}

/**
* A {@link StacksNetwork} with the parameters for the Stacks mainnet.
* Pass a `url` option to override the default Hiro hosted Stacks node API.
* Pass a `fetchFn` option to customize the default networking functions.
* @example
* ```
* const network = new StacksMainnet();
* const network = new StacksMainnet({ url: "https://api.mainnet.hiro.so" });
* const network = new StacksMainnet({ fetch: createFetchFn() });
* ```
* @related {@link createFetchFn}, {@link createApiKeyMiddleware}
*/
export class StacksMainnet extends StacksNetwork {
version = TransactionVersion.Mainnet;
chainId = ChainID.Mainnet;

constructor(opts?: Partial<NetworkConfig>) {
super({
url: opts?.url ?? HIRO_MAINNET_DEFAULT,
fetchFn: opts?.fetchFn,
});
}
export function networkFrom(network: StacksNetworkName | StacksNetwork) {
if (typeof network === 'string') return networkFromName(network);
return network;
}

/**
* A {@link StacksNetwork} with the parameters for the Stacks testnet.
* Pass a `url` option to override the default Hiro hosted Stacks node API.
* Pass a `fetchFn` option to customize the default networking functions.
* @example
* ```
* const network = new StacksTestnet();
* const network = new StacksTestnet({ url: "https://api.testnet.hiro.so" });
* const network = new StacksTestnet({ fetch: createFetchFn() });
* ```
* @related {@link createFetchFn}, {@link createApiKeyMiddleware}
*/
export class StacksTestnet extends StacksNetwork {
version = TransactionVersion.Testnet;
chainId = ChainID.Testnet;
export function deriveDefaultUrl(network: StacksNetwork | StacksNetworkName) {
network = networkFrom(network);

constructor(opts?: Partial<NetworkConfig>) {
super({
url: opts?.url ?? HIRO_TESTNET_DEFAULT,
fetchFn: opts?.fetchFn,
});
}
return !network || network.transactionVersion === TransactionVersion.Mainnet
? HIRO_MAINNET_URL // default to mainnet if no network is given or txVersion is mainnet
: network.magicBytes === 'id'
? DEVNET_URL // default to devnet if magicBytes are devnet
: HIRO_TESTNET_URL;
}

/**
* A {@link StacksNetwork} using the testnet parameters, but `localhost:3999` as the API URL.
*/
export class StacksMocknet extends StacksNetwork {
version = TransactionVersion.Testnet;
chainId = ChainID.Testnet;

constructor(opts?: Partial<NetworkConfig>) {
super({
url: opts?.url ?? HIRO_MOCKNET_DEFAULT,
fetchFn: opts?.fetchFn,
});
}
}

/** Alias for {@link StacksMocknet} */
export const StacksDevnet = StacksMocknet;
Loading

0 comments on commit f28eb69

Please sign in to comment.