Skip to content
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

feat!: update to new network object #1623

Merged
merged 2 commits into from
Mar 7, 2024
Merged
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
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
Loading