diff --git a/packages/common/src/fetch.ts b/packages/common/src/fetch.ts index 4335abef4..1dbc87036 100644 --- a/packages/common/src/fetch.ts +++ b/packages/common/src/fetch.ts @@ -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 @@ -48,13 +51,16 @@ export async function fetchWrapper(input: RequestInfo, init?: RequestInit): Prom export type FetchFn = (url: string, init?: RequestInit) => Promise; +/** @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 { @@ -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(), + }; +} diff --git a/packages/network/tests/fetch.test.ts b/packages/common/tests/fetch.test.ts similarity index 98% rename from packages/network/tests/fetch.test.ts rename to packages/common/tests/fetch.test.ts index f801fe5b2..f17d3ad02 100644 --- a/packages/network/tests/fetch.test.ts +++ b/packages/common/tests/fetch.test.ts @@ -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(); diff --git a/packages/network/tests/fetchMiddleware.test.ts b/packages/common/tests/fetchMiddleware.test.ts similarity index 99% rename from packages/network/tests/fetchMiddleware.test.ts rename to packages/common/tests/fetchMiddleware.test.ts index 8ca4d0bcb..1999ef6b7 100644 --- a/packages/network/tests/fetchMiddleware.test.ts +++ b/packages/common/tests/fetchMiddleware.test.ts @@ -5,7 +5,7 @@ import { FetchMiddleware, RequestContext, ResponseContext, -} from '@stacks/common'; +} from '../src/fetch'; beforeEach(() => { fetchMock.resetMocks(); diff --git a/packages/network/src/constants.ts b/packages/network/src/constants.ts new file mode 100644 index 000000000..29f73b5b6 --- /dev/null +++ b/packages/network/src/constants.ts @@ -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 (map: Record): T => map[transactionVersion]; +} diff --git a/packages/network/src/index.ts b/packages/network/src/index.ts index a31b9ed5f..6af1fb13e 100644 --- a/packages/network/src/index.ts +++ b/packages/network/src/index.ts @@ -1 +1,2 @@ +export * from './constants'; export * from './network'; diff --git a/packages/network/src/network.ts b/packages/network/src/network.ts index 74a6dc909..702239cea 100644 --- a/packages/network/src/network.ts +++ b/packages/network/src/network.ts @@ -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) { - 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) { - 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) { - super({ - url: opts?.url ?? HIRO_MOCKNET_DEFAULT, - fetchFn: opts?.fetchFn, - }); - } -} - -/** Alias for {@link StacksMocknet} */ -export const StacksDevnet = StacksMocknet; diff --git a/packages/network/tests/network.test.ts b/packages/network/tests/network.test.ts index efe35a444..630b5b00d 100644 --- a/packages/network/tests/network.test.ts +++ b/packages/network/tests/network.test.ts @@ -1,36 +1,16 @@ import { - HIRO_MAINNET_DEFAULT, - HIRO_MOCKNET_DEFAULT, - HIRO_TESTNET_DEFAULT, - StacksMainnet, - StacksMocknet, - StacksNetwork, - StacksTestnet, -} from '../src/network'; + STACKS_DEVNET, + STACKS_MAINNET, + STACKS_MOCKNET, + STACKS_TESTNET, + networkFromName, +} from '../src'; -describe('Setting coreApiUrl', () => { - it('sets mainnet default url', () => { - const mainnet = new StacksMainnet(); - expect(mainnet.coreApiUrl).toEqual(HIRO_MAINNET_DEFAULT); - }); - it('sets testnet url', () => { - const testnet = new StacksTestnet(); - expect(testnet.coreApiUrl).toEqual(HIRO_TESTNET_DEFAULT); - }); - it('sets mocknet url', () => { - const mocknet = new StacksMocknet(); - expect(mocknet.coreApiUrl).toEqual(HIRO_MOCKNET_DEFAULT); - }); - it('sets custom url', () => { - const customURL = 'https://customurl.com'; - const customNET = new StacksMainnet({ url: customURL }); - expect(customNET.coreApiUrl).toEqual(customURL); - }); -}); +test(networkFromName.name, () => { + expect(networkFromName('mainnet')).toEqual(STACKS_MAINNET); + expect(networkFromName('testnet')).toEqual(STACKS_TESTNET); + expect(networkFromName('devnet')).toEqual(STACKS_DEVNET); + expect(networkFromName('mocknet')).toEqual(STACKS_MOCKNET); -it('uses the correct constructor for stacks network from name strings', () => { - expect(StacksNetwork.fromName('mainnet').constructor.toString()).toContain('StacksMainnet'); - expect(StacksNetwork.fromName('testnet').constructor.toString()).toContain('StacksTestnet'); - expect(StacksNetwork.fromName('devnet').constructor.toString()).toContain('StacksMocknet'); // devnet is an alias for mocknet - expect(StacksNetwork.fromName('mocknet').constructor.toString()).toContain('StacksMocknet'); + expect(STACKS_DEVNET).toEqual(STACKS_MOCKNET); }); diff --git a/packages/transactions/src/authorization.ts b/packages/transactions/src/authorization.ts index 69f6976ba..3fb85db82 100644 --- a/packages/transactions/src/authorization.ts +++ b/packages/transactions/src/authorization.ts @@ -86,6 +86,40 @@ export type SpendingCondition = SingleSigSpendingCondition | MultiSigSpendingCon export type SpendingConditionOpts = SingleSigSpendingConditionOpts | MultiSigSpendingConditionOpts; +export function createSpendingCondition( + options: + | { + // Single-sig + publicKey: string; + nonce: IntegerType; + fee: IntegerType; + } + | { + // Multi-sig + publicKeys: string[]; + numSignatures: number; + nonce: IntegerType; + fee: IntegerType; + } +) { + if ('publicKey' in options) { + return createSingleSigSpendingCondition( + AddressHashMode.SerializeP2PKH, + options.publicKey, + options.nonce, + options.fee + ); + } + // multi-sig + return createMultiSigSpendingCondition( + AddressHashMode.SerializeP2SH, + options.numSignatures, + options.publicKeys, + options.nonce, + options.fee + ); +} + export function createSingleSigSpendingCondition( hashMode: SingleSigHashMode, pubKey: string, diff --git a/packages/transactions/src/builders.ts b/packages/transactions/src/builders.ts index 24219d19d..fa4437b60 100644 --- a/packages/transactions/src/builders.ts +++ b/packages/transactions/src/builders.ts @@ -1,41 +1,29 @@ -import { bytesToHex, hexToBytes, IntegerType, intToBigInt } from '@stacks/common'; -import { - StacksNetwork, - StacksMainnet, - StacksNetworkName, - StacksTestnet, - FetchFn, - createFetchFn, -} from '@stacks/network'; +import { ApiOpts, ApiParam, bytesToHex, hexToBytes, IntegerType } from '@stacks/common'; import { c32address } from 'c32check'; import { - Authorization, - createMultiSigSpendingCondition, createSingleSigSpendingCondition, + createSpendingCondition, createSponsoredAuth, createStandardAuth, - SpendingCondition, MultiSigSpendingCondition, } from './authorization'; -import { ClarityValue, deserializeCV, NoneCV, PrincipalCV, serializeCV } from './clarity'; +import { ClarityValue, PrincipalCV } from './clarity'; import { AddressHashMode, AddressVersion, AnchorMode, + AnchorModeName, + ClarityVersion, FungibleConditionCode, NonFungibleConditionCode, PayloadType, PostConditionMode, - SingleSigHashMode, - TransactionVersion, - TxRejectedReason, RECOVERABLE_ECDSA_SIG_LENGTH_BYTES, + SingleSigHashMode, StacksMessageType, - ClarityVersion, - AnchorModeName, } from './constants'; import { ClarityAbi, validateContractCall } from './contract-abi'; -import { NoEstimateAvailableError } from './errors'; +import { estimateFee, getAbi, getNonce } from './fetch'; import { createStacksPrivateKey, getPublicKey, @@ -48,8 +36,6 @@ import { createContractCallPayload, createSmartContractPayload, createTokenTransferPayload, - Payload, - serializePayload, } from './payload'; import { createFungiblePostCondition, @@ -68,518 +54,16 @@ import { import { TransactionSigner } from './signer'; import { StacksTransaction } from './transaction'; import { createLPList } from './types'; -import { cvToHex, omit, parseReadOnlyResponse, validateTxId } from './utils'; - -/** - * Lookup the nonce for an address from a core node - * - * @param {string} address - the c32check address to look up - * @param {StacksNetworkName | StacksNetwork} network - the Stacks network to look up address on - * - * @return a promise that resolves to an integer - */ -export async function getNonce( - address: string, - network?: StacksNetworkName | StacksNetwork -): Promise { - const derivedNetwork = StacksNetwork.fromNameOrNetwork(network ?? new StacksMainnet()); - const url = derivedNetwork.getAccountApiUrl(address); - - const response = await derivedNetwork.fetchFn(url); - if (!response.ok) { - let msg = ''; - try { - msg = await response.text(); - } catch (error) {} - throw new Error( - `Error fetching nonce. Response ${response.status}: ${response.statusText}. Attempted to fetch ${url} and failed with the message: "${msg}"` - ); - } - const responseText = await response.text(); - const result = JSON.parse(responseText) as { nonce: string }; - return BigInt(result.nonce); -} - -/** - * @deprecated Use the new {@link estimateTransaction} function instead. - * - * Estimate the total transaction fee in microstacks for a token transfer - * - * @param {StacksTransaction} transaction - the token transfer transaction to estimate fees for - * @param {StacksNetworkName | StacksNetwork} network - the Stacks network to estimate transaction for - * - * @return a promise that resolves to number of microstacks per byte - */ -export async function estimateTransfer( - transaction: StacksTransaction, - network?: StacksNetworkName | StacksNetwork -): Promise { - if (transaction.payload.payloadType !== PayloadType.TokenTransfer) { - throw new Error( - `Transaction fee estimation only possible with ${ - PayloadType[PayloadType.TokenTransfer] - } transactions. Invoked with: ${PayloadType[transaction.payload.payloadType]}` - ); - } - - return estimateTransferUnsafe(transaction, network); -} - -/** - * @deprecated Use the new {@link estimateTransaction} function instead. - * @internal - */ -export async function estimateTransferUnsafe( - transaction: StacksTransaction, - network?: StacksNetworkName | StacksNetwork -): Promise { - const requestHeaders = { - Accept: 'application/text', - }; - - const fetchOptions = { - method: 'GET', - headers: requestHeaders, - }; - - const derivedNetwork = StacksNetwork.fromNameOrNetwork(network ?? deriveNetwork(transaction)); - const url = derivedNetwork.getTransferFeeEstimateApiUrl(); - - const response = await derivedNetwork.fetchFn(url, fetchOptions); - if (!response.ok) { - let msg = ''; - try { - msg = await response.text(); - } catch (error) {} - throw new Error( - `Error estimating transaction fee. Response ${response.status}: ${response.statusText}. Attempted to fetch ${url} and failed with the message: "${msg}"` - ); - } - const feeRateResult = await response.text(); - const txBytes = BigInt(transaction.serialize().byteLength); - const feeRate = BigInt(feeRateResult); - return feeRate * txBytes; -} - -interface FeeEstimation { - fee: number; - fee_rate: number; -} -interface FeeEstimateResponse { - cost_scalar_change_by_byte: bigint; - estimated_cost: { - read_count: bigint; - read_length: bigint; - runtime: bigint; - write_count: bigint; - write_length: bigint; - }; - estimated_cost_scalar: bigint; - estimations: [FeeEstimation, FeeEstimation, FeeEstimation]; -} - -/** - * Estimate the total transaction fee in microstacks for a Stacks transaction - * - * @param {StacksTransaction} transaction - the transaction to estimate fees for - * @param {number} estimatedLen - is an optional argument that provides the endpoint with an - * estimation of the final length (in bytes) of the transaction, including any post-conditions - * and signatures - * @param {StacksNetworkName | StacksNetwork} network - the Stacks network to estimate transaction fees for - * - * @return a promise that resolves to FeeEstimate - */ -export async function estimateTransaction( - transactionPayload: Payload, - estimatedLen?: number, - network?: StacksNetworkName | StacksNetwork -): Promise<[FeeEstimation, FeeEstimation, FeeEstimation]> { - const options = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - transaction_payload: bytesToHex(serializePayload(transactionPayload)), - ...(estimatedLen ? { estimated_len: estimatedLen } : {}), - }), - }; - - const derivedNetwork = StacksNetwork.fromNameOrNetwork(network ?? new StacksMainnet()); - const url = derivedNetwork.getTransactionFeeEstimateApiUrl(); - - const response = await derivedNetwork.fetchFn(url, options); - - if (!response.ok) { - const body = await response.text().then(str => { - try { - return JSON.parse(str); - } catch (error) { - return str; - } - }); - - if ( - body?.reason === 'NoEstimateAvailable' || - (typeof body === 'string' && body.includes('NoEstimateAvailable')) - ) { - throw new NoEstimateAvailableError(body?.reason_data?.message ?? ''); - } - - throw new Error( - `Error estimating transaction fee. Response ${response.status}: ${response.statusText}. Attempted to fetch ${url} and failed with the message: "${body}"` - ); - } - - const data: FeeEstimateResponse = await response.json(); - return data.estimations; -} - -export type SerializationRejection = { - error: string; - reason: TxRejectedReason.Serialization; - reason_data: { - message: string; - }; - txid: string; -}; - -export type DeserializationRejection = { - error: string; - reason: TxRejectedReason.Deserialization; - reason_data: { - message: string; - }; - txid: string; -}; - -export type SignatureValidationRejection = { - error: string; - reason: TxRejectedReason.SignatureValidation; - reason_data: { - message: string; - }; - txid: string; -}; - -export type BadNonceRejection = { - error: string; - reason: TxRejectedReason.BadNonce; - reason_data: { - expected: number; - actual: number; - is_origin: boolean; - principal: boolean; - }; - txid: string; -}; - -export type FeeTooLowRejection = { - error: string; - reason: TxRejectedReason.FeeTooLow; - reason_data: { - expected: number; - actual: number; - }; - txid: string; -}; - -export type NotEnoughFundsRejection = { - error: string; - reason: TxRejectedReason.NotEnoughFunds; - reason_data: { - expected: string; - actual: string; - }; - txid: string; -}; - -export type NoSuchContractRejection = { - error: string; - reason: TxRejectedReason.NoSuchContract; - reason_data?: undefined; - txid: string; -}; - -export type NoSuchPublicFunctionRejection = { - error: string; - reason: TxRejectedReason.NoSuchPublicFunction; - reason_data?: undefined; - txid: string; -}; - -export type BadFunctionArgumentRejection = { - error: string; - reason: TxRejectedReason.BadFunctionArgument; - reason_data: { - message: string; - }; - txid: string; -}; - -export type ContractAlreadyExistsRejection = { - error: string; - reason: TxRejectedReason.ContractAlreadyExists; - reason_data: { - contract_identifier: string; - }; - txid: string; -}; - -export type PoisonMicroblocksDoNotConflictRejection = { - error: string; - reason: TxRejectedReason.PoisonMicroblocksDoNotConflict; - reason_data?: undefined; - txid: string; -}; - -export type PoisonMicroblockHasUnknownPubKeyHashRejection = { - error: string; - reason: TxRejectedReason.PoisonMicroblockHasUnknownPubKeyHash; - reason_data?: undefined; - txid: string; -}; - -export type PoisonMicroblockIsInvalidRejection = { - error: string; - reason: TxRejectedReason.PoisonMicroblockIsInvalid; - reason_data?: undefined; - txid: string; -}; - -export type BadAddressVersionByteRejection = { - error: string; - reason: TxRejectedReason.BadAddressVersionByte; - reason_data?: undefined; - txid: string; -}; - -export type NoCoinbaseViaMempoolRejection = { - error: string; - reason: TxRejectedReason.NoCoinbaseViaMempool; - reason_data?: undefined; - txid: string; -}; - -export type ServerFailureNoSuchChainTipRejection = { - error: string; - reason: TxRejectedReason.ServerFailureNoSuchChainTip; - reason_data?: undefined; - txid: string; -}; - -export type TooMuchChainingRejection = { - error: string; - reason: TxRejectedReason.TooMuchChaining; - reason_data: { - actual: number; - expected: number; - is_origin: boolean; - message: string; - principal: string; - }; - txid: string; -}; - -export type ConflictingNonceInMempoolRejection = { - error: string; - reason: TxRejectedReason.ConflictingNonceInMempool; - reason_data?: undefined; - txid: string; -}; - -export type BadTransactionVersionRejection = { - error: string; - reason: TxRejectedReason.BadTransactionVersion; - reason_data?: undefined; - txid: string; -}; - -export type TransferRecipientCannotEqualSenderRejection = { - error: string; - reason: TxRejectedReason.TransferRecipientCannotEqualSender; - reason_data: { - recipient: string; - }; - txid: string; -}; - -export type TransferAmountMustBePositiveRejection = { - error: string; - reason: TxRejectedReason.TransferAmountMustBePositive; - reason_data?: undefined; - txid: string; -}; - -export type ServerFailureDatabaseRejection = { - error: string; - reason: TxRejectedReason.ServerFailureDatabase; - reason_data: { - message: string; - }; - txid: string; -}; - -export type EstimatorErrorRejection = { - error: string; - reason: TxRejectedReason.EstimatorError; - reason_data: { - message: string; - }; - txid: string; -}; - -export type TemporarilyBlacklistedRejection = { - error: string; - reason: TxRejectedReason.TemporarilyBlacklisted; - reason_data?: undefined; - txid: string; -}; - -export type ServerFailureOtherRejection = { - error: string; - reason: TxRejectedReason.ServerFailureOther; - reason_data: { - message: string; - }; - txid: string; -}; - -export type TxBroadcastResultOk = { - txid: string; - error?: undefined; - reason?: undefined; - reason_data?: undefined; -}; - -export type TxBroadcastResultRejected = - | SerializationRejection - | DeserializationRejection - | SignatureValidationRejection - | BadNonceRejection - | FeeTooLowRejection - | NotEnoughFundsRejection - | NoSuchContractRejection - | NoSuchPublicFunctionRejection - | BadFunctionArgumentRejection - | ContractAlreadyExistsRejection - | PoisonMicroblocksDoNotConflictRejection - | PoisonMicroblockHasUnknownPubKeyHashRejection - | PoisonMicroblockIsInvalidRejection - | BadAddressVersionByteRejection - | NoCoinbaseViaMempoolRejection - | ServerFailureNoSuchChainTipRejection - | TooMuchChainingRejection - | ConflictingNonceInMempoolRejection - | BadTransactionVersionRejection - | TransferRecipientCannotEqualSenderRejection - | TransferAmountMustBePositiveRejection - | ServerFailureDatabaseRejection - | EstimatorErrorRejection - | TemporarilyBlacklistedRejection - | ServerFailureOtherRejection; - -export type TxBroadcastResult = TxBroadcastResultOk | TxBroadcastResultRejected; - -/** - * Broadcast the signed transaction to a core node - * - * @param {StacksTransaction} transaction - the token transfer transaction to broadcast - * @param {StacksNetworkName | StacksNetwork} network - the Stacks network to broadcast transaction to - * - * @returns {Promise} that resolves to a response if the operation succeeds - */ -export async function broadcastTransaction( - transaction: StacksTransaction, - network?: StacksNetworkName | StacksNetwork, - attachment?: Uint8Array -): Promise { - const rawTx = transaction.serialize(); - const derivedNetwork = StacksNetwork.fromNameOrNetwork(network ?? deriveNetwork(transaction)); - const url = derivedNetwork.getBroadcastApiUrl(); - - return broadcastRawTransaction(rawTx, url, attachment, derivedNetwork.fetchFn); -} - -/** - * Broadcast the signed transaction to a core node - * - * @param {Uint8Array} rawTx - the raw serialized transaction bytes to broadcast - * @param {string} url - the broadcast endpoint URL - * - * @returns {Promise} that resolves to a response if the operation succeeds - */ -export async function broadcastRawTransaction( - rawTx: Uint8Array, - url: string, - attachment?: Uint8Array, - fetchFn: FetchFn = createFetchFn() -): Promise { - const options = { - method: 'POST', - headers: { 'Content-Type': attachment ? 'application/json' : 'application/octet-stream' }, - body: attachment - ? JSON.stringify({ - tx: bytesToHex(rawTx), - attachment: bytesToHex(attachment), - }) - : rawTx, - }; - - const response = await fetchFn(url, options); - if (!response.ok) { - try { - return (await response.json()) as TxBroadcastResult; - } catch (e) { - throw Error(`Failed to broadcast transaction: ${(e as Error).message}`); - } - } - - const text = await response.text(); - // Replace extra quotes around txid string - const txid = text.replace(/["]+/g, ''); - if (!validateTxId(txid)) throw new Error(text); - return { txid } as TxBroadcastResult; -} - -/** - * Fetch a contract's ABI - * - * @param {string} address - the contracts address - * @param {string} contractName - the contracts name - * @param {StacksNetworkName | StacksNetwork} network - the Stacks network to broadcast transaction to - * - * @returns {Promise} that resolves to a ClarityAbi if the operation succeeds - */ -export async function getAbi( - address: string, - contractName: string, - network: StacksNetworkName | StacksNetwork -): Promise { - const options = { - method: 'GET', - }; - - const derivedNetwork = StacksNetwork.fromNameOrNetwork(network); - const url = derivedNetwork.getAbiApiUrl(address, contractName); - - const response = await derivedNetwork.fetchFn(url, options); - if (!response.ok) { - const msg = await response.text().catch(() => ''); - throw new Error( - `Error fetching contract ABI for contract "${contractName}" at address ${address}. Response ${response.status}: ${response.statusText}. Attempted to fetch ${url} and failed with the message: "${msg}"` - ); - } - - return JSON.parse(await response.text()) as ClarityAbi; -} - -function deriveNetwork(transaction: StacksTransaction) { - switch (transaction.version) { - case TransactionVersion.Mainnet: - return new StacksMainnet(); - case TransactionVersion.Testnet: - return new StacksTestnet(); - } -} +import { defaultApiFromNetwork, omit } from './utils'; +import { + networkFrom, + STACKS_MAINNET, + STACKS_TESTNET, + StacksNetworkName, + TransactionVersion, + whenTransactionVersion, +} from '@stacks/network'; +import { StacksNetwork } from '@stacks/network'; export interface MultiSigOptions { numSignatures: number; @@ -589,8 +73,10 @@ export interface MultiSigOptions { /** * STX token transfer transaction options + * + * Note: Standard STX transfer does not allow post-conditions. */ -export interface TokenTransferOptions { +export type TokenTransferOptions = { /** the address of the recipient of the token transfer */ recipient: string | PrincipalCV; /** the amount to be transfered in microstacks */ @@ -608,7 +94,7 @@ export interface TokenTransferOptions { memo?: string; /** set to true if another account is sponsoring the transaction (covering the transaction fee) */ sponsored?: boolean; -} +} & ApiParam; export interface UnsignedTokenTransferOptions extends TokenTransferOptions { publicKey: string; @@ -644,47 +130,24 @@ export async function makeUnsignedSTXTokenTransfer( const defaultOptions = { fee: BigInt(0), nonce: BigInt(0), - network: new StacksMainnet(), + network: STACKS_MAINNET, memo: '', sponsored: false, }; const options = Object.assign(defaultOptions, txOptions); + options.api = defaultApiFromNetwork(options.network, txOptions.api); const payload = createTokenTransferPayload(options.recipient, options.amount, options.memo); - let authorization: Authorization | null = null; - let spendingCondition: SpendingCondition | null = null; - - if ('publicKey' in options) { - // single-sig - spendingCondition = createSingleSigSpendingCondition( - AddressHashMode.SerializeP2PKH, - options.publicKey, - options.nonce, - options.fee - ); - } else { - // multi-sig - spendingCondition = createMultiSigSpendingCondition( - AddressHashMode.SerializeP2SH, - options.numSignatures, - options.publicKeys, - options.nonce, - options.fee - ); - } - - if (options.sponsored) { - authorization = createSponsoredAuth(spendingCondition); - } else { - authorization = createStandardAuth(spendingCondition); - } - - const network = StacksNetwork.fromNameOrNetwork(options.network); + const network = networkFrom(options.network); + const spendingCondition = createSpendingCondition(options); + const authorization = options.sponsored + ? createSponsoredAuth(spendingCondition) + : createStandardAuth(spendingCondition); const transaction = new StacksTransaction( - network.version, + network.transactionVersion, authorization, payload, undefined, // no post conditions on STX transfers (see SIP-005) @@ -693,18 +156,18 @@ export async function makeUnsignedSTXTokenTransfer( network.chainId ); - if (txOptions.fee === undefined || txOptions.fee === null) { - const fee = await estimateTransactionFeeWithFallback(transaction, network); + if (txOptions.fee == null) { + const fee = await estimateFee({ transaction, api: options.api }); transaction.setFee(fee); } - if (txOptions.nonce === undefined || txOptions.nonce === null) { + if (txOptions.nonce == null) { const addressVersion = - options.network.version === TransactionVersion.Mainnet + options.network.transactionVersion === TransactionVersion.Mainnet ? AddressVersion.MainnetSingleSig : AddressVersion.TestnetSingleSig; - const senderAddress = c32address(addressVersion, transaction.auth.spendingCondition!.signer); - const txNonce = await getNonce(senderAddress, options.network); + const address = c32address(addressVersion, transaction.auth.spendingCondition!.signer); + const txNonce = await getNonce({ address, api: options.api }); transaction.setNonce(txNonce); } @@ -769,6 +232,8 @@ export interface BaseContractDeployOptions { nonce?: IntegerType; /** the network that the transaction will ultimately be broadcast to */ network?: StacksNetworkName | StacksNetwork; + /** the node/API used for estimating fee & nonce (using the `api.fetchFn` */ + api?: ApiOpts; /** the transaction anchorMode, which specifies whether it should be * included in an anchor block or a microblock */ anchorMode: AnchorModeName | AnchorMode; @@ -804,58 +269,6 @@ export interface SignedMultiSigContractDeployOptions extends BaseContractDeployO signerKeys: string[]; } -/** - * @deprecated Use the new {@link estimateTransaction} function insterad. - * - * Estimate the total transaction fee in microstacks for a contract deploy - * - * @param {StacksTransaction} transaction - the token transfer transaction to estimate fees for - * @param {StacksNetworkName | StacksNetwork} network - the Stacks network to estimate transaction for - * - * @return a promise that resolves to number of microstacks per byte - */ -export async function estimateContractDeploy( - transaction: StacksTransaction, - network?: StacksNetworkName | StacksNetwork -): Promise { - if ( - transaction.payload.payloadType !== PayloadType.SmartContract && - transaction.payload.payloadType !== PayloadType.VersionedSmartContract - ) { - throw new Error( - `Contract deploy fee estimation only possible with ${ - PayloadType[PayloadType.SmartContract] - } transactions. Invoked with: ${PayloadType[transaction.payload.payloadType]}` - ); - } - - const requestHeaders = { - Accept: 'application/text', - }; - - const fetchOptions = { - method: 'GET', - headers: requestHeaders, - }; - - // Place holder estimate until contract deploy fee estimation is fully implemented on Stacks - // blockchain core - const derivedNetwork = StacksNetwork.fromNameOrNetwork(network ?? deriveNetwork(transaction)); - const url = derivedNetwork.getTransferFeeEstimateApiUrl(); - - const response = await derivedNetwork.fetchFn(url, fetchOptions); - if (!response.ok) { - const msg = await response.text().catch(() => ''); - throw new Error( - `Error estimating contract deploy fee. Response ${response.status}: ${response.statusText}. Attempted to fetch ${url} and failed with the message: "${msg}"` - ); - } - const feeRateResult = await response.text(); - const txBytes = intToBigInt(transaction.serialize().byteLength, false); - const feeRate = intToBigInt(feeRateResult, false); - return feeRate * txBytes; -} - /** * Generates a Clarity smart contract deploy transaction * @@ -906,13 +319,14 @@ export async function makeUnsignedContractDeploy( const defaultOptions = { fee: BigInt(0), nonce: BigInt(0), - network: new StacksMainnet(), + network: STACKS_MAINNET, postConditionMode: PostConditionMode.Deny, sponsored: false, clarityVersion: ClarityVersion.Clarity2, }; const options = Object.assign(defaultOptions, txOptions); + options.api = defaultApiFromNetwork(options.network, txOptions.api); const payload = createSmartContractPayload( options.contractName, @@ -920,36 +334,11 @@ export async function makeUnsignedContractDeploy( options.clarityVersion ); - let authorization: Authorization | null = null; - - let spendingCondition: SpendingCondition | null = null; - - if ('publicKey' in options) { - // single-sig - spendingCondition = createSingleSigSpendingCondition( - AddressHashMode.SerializeP2PKH, - options.publicKey, - options.nonce, - options.fee - ); - } else { - // multi-sig - spendingCondition = createMultiSigSpendingCondition( - AddressHashMode.SerializeP2SH, - options.numSignatures, - options.publicKeys, - options.nonce, - options.fee - ); - } - - if (options.sponsored) { - authorization = createSponsoredAuth(spendingCondition); - } else { - authorization = createStandardAuth(spendingCondition); - } - - const network = StacksNetwork.fromNameOrNetwork(options.network); + const network = networkFrom(options.network); + const spendingCondition = createSpendingCondition(options); + const authorization = options.sponsored + ? createSponsoredAuth(spendingCondition) + : createStandardAuth(spendingCondition); const postConditions: PostCondition[] = []; if (options.postConditions && options.postConditions.length > 0) { @@ -960,7 +349,7 @@ export async function makeUnsignedContractDeploy( const lpPostConditions = createLPList(postConditions); const transaction = new StacksTransaction( - network.version, + network.transactionVersion, authorization, payload, lpPostConditions, @@ -970,17 +359,17 @@ export async function makeUnsignedContractDeploy( ); if (txOptions.fee === undefined || txOptions.fee === null) { - const fee = await estimateTransactionFeeWithFallback(transaction, network); + const fee = await estimateFee({ transaction, api: options.api }); transaction.setFee(fee); } if (txOptions.nonce === undefined || txOptions.nonce === null) { const addressVersion = - options.network.version === TransactionVersion.Mainnet + options.network.transactionVersion === TransactionVersion.Mainnet ? AddressVersion.MainnetSingleSig : AddressVersion.TestnetSingleSig; - const senderAddress = c32address(addressVersion, transaction.auth.spendingCondition!.signer); - const txNonce = await getNonce(senderAddress, options.network); + const address = c32address(addressVersion, transaction.auth.spendingCondition!.signer); + const txNonce = await getNonce({ address, api: options.api }); transaction.setNonce(txNonce); } @@ -998,11 +387,12 @@ export interface ContractCallOptions { functionArgs: ClarityValue[]; /** transaction fee in microstacks */ fee?: IntegerType; - feeEstimateApiUrl?: string; /** the transaction nonce, which must be increased monotonically with each new transaction */ nonce?: IntegerType; /** the Stacks blockchain network that will ultimately be used to broadcast this transaction */ network?: StacksNetworkName | StacksNetwork; + /** the node/API used for estimating fee & nonce (using the `api.fetchFn` */ + api?: ApiOpts; /** the transaction anchorMode, which specifies whether it should be * included in an anchor block or a microblock */ anchorMode: AnchorModeName | AnchorMode; @@ -1037,55 +427,6 @@ export interface SignedMultiSigContractCallOptions extends ContractCallOptions { signerKeys: string[]; } -/** - * @deprecated Use the new {@link estimateTransaction} function insterad. - * - * Estimate the total transaction fee in microstacks for a contract function call - * - * @param {StacksTransaction} transaction - the token transfer transaction to estimate fees for - * @param {StacksNetworkName | StacksNetwork} network - the Stacks network to estimate transaction for - * - * @return a promise that resolves to number of microstacks per byte - */ -export async function estimateContractFunctionCall( - transaction: StacksTransaction, - network?: StacksNetworkName | StacksNetwork -): Promise { - if (transaction.payload.payloadType !== PayloadType.ContractCall) { - throw new Error( - `Contract call fee estimation only possible with ${ - PayloadType[PayloadType.ContractCall] - } transactions. Invoked with: ${PayloadType[transaction.payload.payloadType]}` - ); - } - - const requestHeaders = { - Accept: 'application/text', - }; - - const fetchOptions = { - method: 'GET', - headers: requestHeaders, - }; - - // Place holder estimate until contract call fee estimation is fully implemented on Stacks - // blockchain core - const derivedNetwork = StacksNetwork.fromNameOrNetwork(network ?? deriveNetwork(transaction)); - const url = derivedNetwork.getTransferFeeEstimateApiUrl(); - - const response = await derivedNetwork.fetchFn(url, fetchOptions); - if (!response.ok) { - const msg = await response.text().catch(() => ''); - throw new Error( - `Error estimating contract call fee. Response ${response.status}: ${response.statusText}. Attempted to fetch ${url} and failed with the message: "${msg}"` - ); - } - const feeRateResult = await response.text(); - const txBytes = intToBigInt(transaction.serialize().byteLength, false); - const feeRate = intToBigInt(feeRateResult, false); - return feeRate * txBytes; -} - /** * Generates an unsigned Clarity smart contract function call transaction * @@ -1099,12 +440,13 @@ export async function makeUnsignedContractCall( const defaultOptions = { fee: BigInt(0), nonce: BigInt(0), - network: new StacksMainnet(), + network: STACKS_MAINNET, postConditionMode: PostConditionMode.Deny, sponsored: false, }; const options = Object.assign(defaultOptions, txOptions); + options.api = defaultApiFromNetwork(options.network, txOptions.api); const payload = createContractCallPayload( options.contractAddress, @@ -1117,7 +459,7 @@ export async function makeUnsignedContractCall( let abi: ClarityAbi; if (typeof options.validateWithAbi === 'boolean') { if (options?.network) { - abi = await getAbi(options.contractAddress, options.contractName, options.network); + abi = await getAbi({ ...options }); } else { throw new Error('Network option must be provided in order to validate with ABI'); } @@ -1128,35 +470,11 @@ export async function makeUnsignedContractCall( validateContractCall(payload, abi); } - let spendingCondition: SpendingCondition | null = null; - let authorization: Authorization | null = null; - - if ('publicKey' in options) { - // single-sig - spendingCondition = createSingleSigSpendingCondition( - AddressHashMode.SerializeP2PKH, - options.publicKey, - options.nonce, - options.fee - ); - } else { - // multi-sig - spendingCondition = createMultiSigSpendingCondition( - AddressHashMode.SerializeP2SH, - options.numSignatures, - options.publicKeys, - options.nonce, - options.fee - ); - } - - if (options.sponsored) { - authorization = createSponsoredAuth(spendingCondition); - } else { - authorization = createStandardAuth(spendingCondition); - } - - const network = StacksNetwork.fromNameOrNetwork(options.network); + const network = networkFrom(options.network); + const spendingCondition = createSpendingCondition(options); + const authorization = options.sponsored + ? createSponsoredAuth(spendingCondition) + : createStandardAuth(spendingCondition); const postConditions: PostCondition[] = []; if (options.postConditions && options.postConditions.length > 0) { @@ -1167,7 +485,7 @@ export async function makeUnsignedContractCall( const lpPostConditions = createLPList(postConditions); const transaction = new StacksTransaction( - network.version, + network.transactionVersion, authorization, payload, lpPostConditions, @@ -1177,17 +495,17 @@ export async function makeUnsignedContractCall( ); if (txOptions.fee === undefined || txOptions.fee === null) { - const fee = await estimateTransactionFeeWithFallback(transaction, network); + const fee = await estimateFee({ transaction, api: options.api }); transaction.setFee(fee); } if (txOptions.nonce === undefined || txOptions.nonce === null) { const addressVersion = - network.version === TransactionVersion.Mainnet + network.transactionVersion === TransactionVersion.Mainnet ? AddressVersion.MainnetSingleSig : AddressVersion.TestnetSingleSig; - const senderAddress = c32address(addressVersion, transaction.auth.spendingCondition!.signer); - const txNonce = await getNonce(senderAddress, network); + const address = c32address(addressVersion, transaction.auth.spendingCondition!.signer); + const txNonce = await getNonce({ address, api: options.api }); transaction.setNonce(txNonce); } @@ -1382,146 +700,6 @@ export function makeContractNonFungiblePostCondition( ); } -/** - * Read only function options - * - * @param {String} contractAddress - the c32check address of the contract - * @param {String} contractName - the contract name - * @param {String} functionName - name of the function to be called - * @param {[ClarityValue]} functionArgs - an array of Clarity values as arguments to the function call - * @param {StacksNetwork} network - the Stacks blockchain network this transaction is destined for - * @param {String} senderAddress - the c32check address of the sender - */ - -export interface ReadOnlyFunctionOptions { - contractName: string; - contractAddress: string; - functionName: string; - functionArgs: ClarityValue[]; - /** the network that the contract which contains the function is deployed to */ - network?: StacksNetworkName | StacksNetwork; - /** address of the sender */ - senderAddress: string; -} - -/** - * Calls a function as read-only from a contract interface - * It is not necessary that the function is defined as read-only in the contract - * - * @param {ReadOnlyFunctionOptions} readOnlyFunctionOptions - the options object - * - * Returns an object with a status bool (okay) and a result string that is a serialized clarity value in hex format. - * - * @return {ClarityValue} - */ -export async function callReadOnlyFunction( - readOnlyFunctionOptions: ReadOnlyFunctionOptions -): Promise { - const defaultOptions = { - network: new StacksMainnet(), - }; - - const options = Object.assign(defaultOptions, readOnlyFunctionOptions); - - const { contractName, contractAddress, functionName, functionArgs, senderAddress } = options; - - const network = StacksNetwork.fromNameOrNetwork(options.network); - const url = network.getReadOnlyFunctionCallApiUrl(contractAddress, contractName, functionName); - - const args = functionArgs.map(arg => cvToHex(arg)); - - const body = JSON.stringify({ - sender: senderAddress, - arguments: args, - }); - - const response = await network.fetchFn(url, { - method: 'POST', - body, - headers: { - 'Content-Type': 'application/json', - }, - }); - - if (!response.ok) { - const msg = await response.text().catch(() => ''); - throw new Error( - `Error calling read-only function. Response ${response.status}: ${response.statusText}. Attempted to fetch ${url} and failed with the message: "${msg}"` - ); - } - - return response.json().then(responseJson => parseReadOnlyResponse(responseJson)); -} - -export interface GetContractMapEntryOptions { - /** the contracts address */ - contractAddress: string; - /** the contracts name */ - contractName: string; - /** the map name */ - mapName: string; - /** key to lookup in the map */ - mapKey: ClarityValue; - /** the network that has the contract */ - network?: StacksNetworkName | StacksNetwork; -} - -/** - * Fetch data from a contract data map. - * @param getContractMapEntryOptions - the options object - * @returns - * Promise that resolves to a ClarityValue if the operation succeeds. - * Resolves to NoneCV if the map does not contain the given key, if the map does not exist, or if the contract prinicipal does not exist - */ -export async function getContractMapEntry( - getContractMapEntryOptions: GetContractMapEntryOptions -): Promise { - const defaultOptions = { - network: new StacksMainnet(), - }; - const { contractAddress, contractName, mapName, mapKey, network } = Object.assign( - defaultOptions, - getContractMapEntryOptions - ); - - const derivedNetwork = StacksNetwork.fromNameOrNetwork(network); - const url = derivedNetwork.getMapEntryUrl(contractAddress, contractName, mapName); - - const serializedKeyBytes = serializeCV(mapKey); - const serializedKeyHex = '0x' + bytesToHex(serializedKeyBytes); - - const fetchOptions: RequestInit = { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - body: JSON.stringify(serializedKeyHex), // endpoint expects a JSON string atom (quote wrapped string) - }; - - const response = await derivedNetwork.fetchFn(url, fetchOptions); - if (!response.ok) { - const msg = await response.text().catch(() => ''); - throw new Error( - `Error fetching map entry for map "${mapName}" in contract "${contractName}" at address ${contractAddress}, using map key "${serializedKeyHex}". Response ${response.status}: ${response.statusText}. Attempted to fetch ${url} and failed with the message: "${msg}"` - ); - } - const responseBody = await response.text(); - const responseJson: { data?: string } = JSON.parse(responseBody); - if (!responseJson.data) { - throw new Error( - `Error fetching map entry for map "${mapName}" in contract "${contractName}" at address ${contractAddress}, using map key "${serializedKeyHex}". Response ${response.status}: ${response.statusText}. Attempted to fetch ${url} and failed with the response: "${responseBody}"` - ); - } - let deserializedCv: T; - try { - deserializedCv = deserializeCV(responseJson.data); - } catch (error) { - throw new Error(`Error deserializing Clarity value "${responseJson.data}": ${error}`); - } - return deserializedCv; -} - /** * Sponsored transaction options */ @@ -1538,6 +716,8 @@ export interface SponsorOptionsOpts { sponsorAddressHashmode?: AddressHashMode; /** the Stacks blockchain network that this transaction will ultimately be broadcast to */ network?: StacksNetworkName | StacksNetwork; + /** the node/API used for estimating fee & nonce (using the `api.fetchFn` */ + api?: ApiOpts; } /** @@ -1552,35 +732,31 @@ export interface SponsorOptionsOpts { export async function sponsorTransaction( sponsorOptions: SponsorOptionsOpts ): Promise { + const defaultNetwork = whenTransactionVersion(sponsorOptions.transaction.version)({ + [TransactionVersion.Mainnet]: STACKS_MAINNET, + [TransactionVersion.Testnet]: STACKS_TESTNET, + }); // detect network from transaction version + const defaultOptions = { fee: 0 as IntegerType, sponsorNonce: 0 as IntegerType, sponsorAddressHashmode: AddressHashMode.SerializeP2PKH as SingleSigHashMode, - network: - sponsorOptions.transaction.version === TransactionVersion.Mainnet - ? new StacksMainnet() - : new StacksTestnet(), + network: defaultNetwork, }; const options = Object.assign(defaultOptions, sponsorOptions); + options.api = defaultApiFromNetwork(options.network, sponsorOptions.api); - const network = StacksNetwork.fromNameOrNetwork(options.network); const sponsorPubKey = pubKeyfromPrivKey(options.sponsorPrivateKey); - if (sponsorOptions.fee === undefined || sponsorOptions.fee === null) { - let txFee = 0; + if (sponsorOptions.fee == null) { + let txFee: bigint | number = 0; switch (options.transaction.payload.payloadType) { case PayloadType.TokenTransfer: case PayloadType.SmartContract: case PayloadType.VersionedSmartContract: case PayloadType.ContractCall: - const estimatedLen = estimateTransactionByteLength(options.transaction); - try { - txFee = (await estimateTransaction(options.transaction.payload, estimatedLen, network))[1] - .fee; - } catch (e) { - throw e; - } + txFee = BigInt(await estimateFee({ ...options })); break; default: throw new Error( @@ -1593,14 +769,13 @@ export async function sponsorTransaction( options.fee = txFee; } - if (sponsorOptions.sponsorNonce === undefined || sponsorOptions.sponsorNonce === null) { - const addressVersion = - network.version === TransactionVersion.Mainnet - ? AddressVersion.MainnetSingleSig - : AddressVersion.TestnetSingleSig; - - const senderAddress = publicKeyToAddress(addressVersion, sponsorPubKey); - const sponsorNonce = await getNonce(senderAddress, network); + if (sponsorOptions.sponsorNonce == null) { + const addressVersion = whenTransactionVersion(options.transaction.version)({ + [TransactionVersion.Mainnet]: AddressVersion.MainnetSingleSig, + [TransactionVersion.Testnet]: AddressVersion.TestnetSingleSig, + }); // detect address version from transaction version + const address = publicKeyToAddress(addressVersion, sponsorPubKey); + const sponsorNonce = await getNonce({ address, api: options.api }); options.sponsorNonce = sponsorNonce; } @@ -1660,23 +835,3 @@ export function estimateTransactionByteLength(transaction: StacksTransaction): n return transaction.serialize().byteLength; } } - -/** - * Estimates the fee using {@link estimateTransfer} as a fallback if - * {@link estimateTransaction} does not get an estimation due to the - * {@link NoEstimateAvailableError} error. - */ -export async function estimateTransactionFeeWithFallback( - transaction: StacksTransaction, - network: StacksNetwork -): Promise { - try { - const estimatedLen = estimateTransactionByteLength(transaction); - return (await estimateTransaction(transaction.payload, estimatedLen, network))[1].fee; - } catch (error) { - if (error instanceof NoEstimateAvailableError) { - return await estimateTransferUnsafe(transaction, network); - } - throw error; - } -} diff --git a/packages/transactions/src/common.ts b/packages/transactions/src/common.ts index bde9bf6c0..f8081c331 100644 --- a/packages/transactions/src/common.ts +++ b/packages/transactions/src/common.ts @@ -3,11 +3,11 @@ import { AddressVersion, RECOVERABLE_ECDSA_SIG_LENGTH_BYTES, StacksMessageType, - TransactionVersion, } from './constants'; import { c32address } from 'c32check'; import { hexToBytes } from '@stacks/common'; +import { TransactionVersion } from '@stacks/network'; export interface Address { readonly type: StacksMessageType.Address; diff --git a/packages/transactions/src/constants.ts b/packages/transactions/src/constants.ts index 6c79aa245..1ebc01acd 100644 --- a/packages/transactions/src/constants.ts +++ b/packages/transactions/src/constants.ts @@ -1,13 +1,6 @@ -/** - * 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 { - Testnet = 0x80000000, - Mainnet = 0x00000001, -} +/** @ignore internal */ +export const BLOCKSTACK_DEFAULT_GAIA_HUB_URL = 'https://hub.blockstack.org'; -export const DEFAULT_CHAIN_ID = ChainID.Mainnet; export const MAX_STRING_LENGTH_BYTES = 128; export const CLARITY_INT_SIZE = 128; export const CLARITY_INT_BYTE_SIZE = 16; @@ -16,7 +9,6 @@ export const RECOVERABLE_ECDSA_SIG_LENGTH_BYTES = 65; export const COMPRESSED_PUBKEY_LENGTH_BYTES = 32; export const UNCOMPRESSED_PUBKEY_LENGTH_BYTES = 64; export const MEMO_MAX_LENGTH_BYTES = 34; -export const DEFAULT_CORE_NODE_API_URL = 'https://api.mainnet.hiro.so'; // todo: add explicit enum values /** @@ -100,23 +92,11 @@ const AnchorModeMap = { }; /** @ignore */ -export function anchorModeFromNameOrValue(mode: AnchorModeName | AnchorMode): AnchorMode { +export function anchorModeFrom(mode: AnchorModeName | AnchorMode): AnchorMode { if (mode in AnchorModeMap) return AnchorModeMap[mode]; throw new Error(`Invalid anchor mode "${mode}", must be one of: ${AnchorModeNames.join(', ')}`); } -/** - * 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; - /** * How to treat unspecified transfers of a transaction. * Used for creating transactions. @@ -218,7 +198,7 @@ export enum NonFungibleConditionCode { /** * The type of sender for a post-condition. */ -export enum PostConditionPrincipalID { +export enum PostConditionPrincipalId { Origin = 0x01, Standard = 0x02, Contract = 0x03, @@ -252,13 +232,6 @@ export enum TxRejectedReason { BadAddressVersionByte = 'BadAddressVersionByte', NoCoinbaseViaMempool = 'NoCoinbaseViaMempool', ServerFailureNoSuchChainTip = 'ServerFailureNoSuchChainTip', - TooMuchChaining = 'TooMuchChaining', - ConflictingNonceInMempool = 'ConflictingNonceInMempool', - BadTransactionVersion = 'BadTransactionVersion', - TransferRecipientCannotEqualSender = 'TransferRecipientCannotEqualSender', - TransferAmountMustBePositive = 'TransferAmountMustBePositive', ServerFailureDatabase = 'ServerFailureDatabase', - EstimatorError = 'EstimatorError', - TemporarilyBlacklisted = 'TemporarilyBlacklisted', ServerFailureOther = 'ServerFailureOther', } diff --git a/packages/transactions/src/fetch.ts b/packages/transactions/src/fetch.ts new file mode 100644 index 000000000..82b66e983 --- /dev/null +++ b/packages/transactions/src/fetch.ts @@ -0,0 +1,382 @@ +import { + ApiParam, + bytesToHex, + createFetchFn, + defaultApiLike, + validateHash256, + with0x, +} from '@stacks/common'; +import { estimateTransactionByteLength } from './builders'; +import { ClarityValue, NoneCV, deserializeCV, serializeCV } from './clarity'; +import { NoEstimateAvailableError } from './errors'; +import { serializePayload } from './payload'; +import { StacksTransaction, deriveNetworkFromTx } from './transaction'; +import { cvToHex, defaultApiFromNetwork, parseReadOnlyResponse } from './utils'; +import { + FeeEstimateResponse, + FeeEstimation, + TxBroadcastResult, + TxBroadcastResultOk, + TxBroadcastResultRejected, +} from './types'; +import { deriveDefaultUrl } from '@stacks/network'; +import { ClarityAbi } from './contract-abi'; + +export const BROADCAST_PATH = '/v2/transactions'; +export const TRANSFER_FEE_ESTIMATE_PATH = '/v2/fees/transfer'; +export const TRANSACTION_FEE_ESTIMATE_PATH = '/v2/fees/transaction'; +export const ACCOUNT_PATH = '/v2/accounts'; +export const CONTRACT_ABI_PATH = '/v2/contracts/interface'; +export const READONLY_FUNCTION_CALL_PATH = '/v2/contracts/call-read'; +export const MAP_ENTRY_PATH = '/v2/map_entry'; + +/** + * Broadcast a serialized transaction to a Stacks node (which will validate and forward to the network). + * @param opts.transaction - The transaction to broadcast + * @param opts.attachment - Optional attachment encoded as a hex string + * @param opts.api - Optional API info (`.url` & `.fetch`) used for fetch call + * @returns A Promise that resolves to a {@link TxBroadcastResult} object + */ +export async function broadcastTransaction({ + transaction: txOpt, + attachment: attachOpt, + api: apiOpt, +}: { + /** The transaction to broadcast */ + transaction: StacksTransaction; + /** Optional attachment in bytes or encoded as a hex string */ + attachment?: Uint8Array | string; +} & ApiParam): Promise { + const tx = bytesToHex(txOpt.serialize()); + const attachment = attachOpt + ? typeof attachOpt === 'string' + ? attachOpt + : bytesToHex(attachOpt) + : undefined; + const json = attachOpt ? { tx, attachment } : { tx }; + const options = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(json), + }; + + const api = defaultApiFromNetwork(deriveNetworkFromTx(txOpt), apiOpt); + const url = `${api.url}${BROADCAST_PATH}`; + const response = await api.fetch(url, options); + + if (!response.ok) { + try { + return (await response.json()) as TxBroadcastResultRejected; + } catch (e) { + throw Error('Failed to broadcast transaction (unable to parse node response).', { cause: e }); + } + } + + const text = await response.text(); + const txid = text.replace(/["]+/g, ''); // Replace extra quotes around txid string + if (!validateHash256(txid)) throw new Error(text); + + return { txid } as TxBroadcastResultOk; +} + +/** + * Lookup the nonce for an address from a core node + * @param opts.address - The Stacks address to look up the next nonce for + * @param opts.api - Optional API info (`.url` & `.fetch`) used for fetch call + * @return A promise that resolves to an integer + */ +export async function getNonce({ + address, + api: apiOpt, +}: { + /** The Stacks address to look up the next nonce for */ + address: string; +} & ApiParam): Promise { + // todo: could derive the network from the address and use as default if no apiOd + + const api = defaultApiLike(apiOpt); + const url = `${api.url}${ACCOUNT_PATH}/${address}?proof=0`; + const response = await api.fetch(url); + + if (!response.ok) { + const msg = await response.text().catch(() => ''); + throw new Error( + `Error fetching nonce. Response ${response.status}: ${response.statusText}. Attempted to fetch ${url} and failed with the message: "${msg}"` + ); + } + + const json = (await response.json()) as { nonce: string }; + return BigInt(json.nonce); +} + +/** + * @deprecated Use the new {@link estimateTransaction} function instead. + * + * Estimate the total transaction fee in microstacks for a token transfer + * + * ⚠ Only sensible for token transfer transactions! + * @param opts.transaction - The token transfer transaction to estimate fees for + * @param opts.api - Optional API info (`.url` & `.fetch`) used for fetch call + * @return A promise that resolves to number of microstacks per byte + */ +export async function estimateTransfer({ + transaction: txOpt, + api: apiOpt, +}: { + /** The token transfer transaction to estimate fees for */ + transaction: StacksTransaction; +} & ApiParam): Promise { + const api = Object.assign( + {}, + { + url: deriveDefaultUrl(deriveNetworkFromTx(txOpt)), + fetch: createFetchFn(), + }, + apiOpt + ); + const url = `${api.url}${TRANSFER_FEE_ESTIMATE_PATH}`; + const response = await api.fetch(url, { + headers: { Accept: 'application/text' }, + }); + + if (!response.ok) { + const msg = await response.text().catch(() => ''); + throw new Error( + `Error estimating transfer fee. Response ${response.status}: ${response.statusText}. Attempted to fetch ${url} and failed with the message: "${msg}"` + ); + } + + const feeRateResult = await response.text(); + const txBytes = BigInt(Math.ceil(txOpt.serialize().byteLength)); + const feeRate = BigInt(feeRateResult); + return feeRate * txBytes; +} + +/** + * Estimate the total transaction fee in microstacks for a Stacks transaction + * @param opts.payload - The transaction to estimate fees for + * @param opts.estimatedLength - Optional estimation of the final length (in + * bytes) of the transaction, including any post-conditions and signatures + * @param opts.api - Optional API info (`.url` & `.fetch`) used for fetch call + * @return A promise that resolves to FeeEstimate + */ +export async function estimateTransaction({ + payload, + estimatedLength, + api: apiOpt, +}: { + payload: string; + estimatedLength?: number; +} & ApiParam): Promise<[FeeEstimation, FeeEstimation, FeeEstimation]> { + const json = { + transaction_payload: payload, + estimated_len: estimatedLength, + }; + const options = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(json), + }; + + const api = defaultApiLike(apiOpt); + const url = `${api.url}${TRANSACTION_FEE_ESTIMATE_PATH}`; + const response = await api.fetch(url, options); + + if (!response.ok) { + const body = await response.json().catch(() => ({})); + + if (body?.reason === 'NoEstimateAvailable') { + throw new NoEstimateAvailableError(body?.reason_data?.message ?? ''); + } + + throw new Error( + `Error estimating transaction fee. Response ${response.status}: ${response.statusText}. Attempted to fetch ${url} and failed with the message: "${body}"` + ); + } + + const data: FeeEstimateResponse = await response.json(); + return data.estimations; +} + +/** + * Estimates the fee using {@link estimateTransaction}, but retries to estimate + * with {@link estimateTransfer} as a fallback if does not get an estimation due + * to the {@link NoEstimateAvailableError} error. + * @param opts.transaction - The transaction to estimate fees for + * @param opts.api - Optional API info (`.url` & `.fetch`) used for fetch call + */ +export async function estimateFee({ + transaction: txOpt, + api: apiOpt, +}: { + transaction: StacksTransaction; +} & ApiParam): Promise { + const api = Object.assign( + {}, + { + url: deriveDefaultUrl(deriveNetworkFromTx(txOpt)), + fetch: createFetchFn(), + }, + apiOpt + ); + + try { + const estimatedLength = estimateTransactionByteLength(txOpt); + return ( + await estimateTransaction({ + payload: bytesToHex(serializePayload(txOpt.payload)), + estimatedLength, + api, + }) + )[1].fee; + } catch (error) { + if (!(error instanceof NoEstimateAvailableError)) throw error; + return await estimateTransfer({ transaction: txOpt, api }); + } +} + +/** + * Fetch a contract's ABI + * @param opts.address - The contracts address + * @param opts.contractName - The contracts name + * @param opts.api - Optional API info (`.url` & `.fetch`) used for fetch call + * @returns A promise that resolves to a ClarityAbi if the operation succeeds + */ +export async function getAbi({ + contractAddress: address, + contractName: name, + api: apiOpt, +}: { + contractAddress: string; + contractName: string; +} & ApiParam): Promise { + const api = defaultApiLike(apiOpt); + const url = `${api.url}${CONTRACT_ABI_PATH}/${address}/${name}`; + const response = await api.fetch(url); + + if (!response.ok) { + const msg = await response.text().catch(() => ''); + throw new Error( + `Error fetching contract ABI for contract "${name}" at address ${address}. Response ${response.status}: ${response.statusText}. Attempted to fetch ${url} and failed with the message: "${msg}"` + ); + } + + return (await response.json()) as ClarityAbi; +} + +/** + * Calls a function as read-only from a contract interface. + * It is not necessary that the function is defined as read-only in the contract + * @param opts.contractName - The contract name + * @param opts.contractAddress - The contract address + * @param opts.functionName - The contract function name + * @param opts.functionArgs - The contract function arguments + * @param opts.senderAddress - The address of the (simulated) sender + * @param opts.api - Optional API info (`.url` & `.fetch`) used for fetch call + * @return Returns an object with a status bool (okay) and a result string that + * is a serialized clarity value in hex format. + */ +export async function callReadOnlyFunction({ + contractName, + contractAddress, + functionName, + functionArgs, + senderAddress, + api: apiOpt, +}: { + contractName: string; + contractAddress: string; + functionName: string; + functionArgs: ClarityValue[]; + /** address of the sender */ + senderAddress: string; +} & ApiParam): Promise { + const json = { + sender: senderAddress, + arguments: functionArgs.map(arg => cvToHex(arg)), + }; + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(json), + }; + + const name = encodeURIComponent(functionName); + + const api = defaultApiLike(apiOpt); + const url = `${api.url}${READONLY_FUNCTION_CALL_PATH}/${contractAddress}/${contractName}/${name}`; + const response = await api.fetch(url, options); + + if (!response.ok) { + const msg = await response.text().catch(() => ''); + throw new Error( + `Error calling read-only function. Response ${response.status}: ${response.statusText}. Attempted to fetch ${url} and failed with the message: "${msg}"` + ); + } + + return await response.json().then(parseReadOnlyResponse); +} + +/** + * Fetch data from a contract data map. + * @param opts.contractAddress - The contract address + * @param opts.contractName - The contract name + * @param opts.mapName - The map variable name + * @param opts.mapKey - The key of the map entry to look up + * @param opts.api - Optional API info (`.url` & `.fetch`) used for fetch call + * @returns Promise that resolves to a ClarityValue if the operation succeeds. + * Resolves to NoneCV if the map does not contain the given key, if the map does not exist, or if the contract prinicipal does not exist + */ +export async function getContractMapEntry({ + contractAddress, + contractName, + mapName, + mapKey, + api: apiOpt, +}: { + contractAddress: string; + contractName: string; + mapName: string; + mapKey: ClarityValue; +} & ApiParam): Promise { + const keyHex = with0x(bytesToHex(serializeCV(mapKey))); + + const options = { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(keyHex), // endpoint expects a JSON string atom (quote wrapped string) + }; + + const api = defaultApiLike(apiOpt); + const url = `${api.url}${MAP_ENTRY_PATH}/${contractAddress}/${contractName}/${mapName}?proof=0`; + const response = await api.fetch(url, options); + + if (!response.ok) { + const msg = await response.text().catch(() => ''); + throw new Error( + `Error fetching map entry for map "${mapName}" in contract "${contractName}" at address ${contractAddress}, using map key "${keyHex}". Response ${response.status}: ${response.statusText}. Attempted to fetch ${url} and failed with the message: "${msg}"` + ); + } + + const json: { data?: string } = await response.json(); + if (!json.data) { + throw new Error( + `Error fetching map entry for map "${mapName}" in contract "${contractName}" at address ${contractAddress}, using map key "${keyHex}". Response ${ + response.status + }: ${response.statusText}. Attempted to fetch ${ + api.url + } and failed with the response: "${JSON.stringify(json)}"` + ); + } + + try { + return deserializeCV(json.data); + } catch (error) { + throw new Error(`Error deserializing Clarity value "${json.data}": ${error}`); + } +} diff --git a/packages/transactions/src/index.ts b/packages/transactions/src/index.ts index 7bea2f327..36bf8ec60 100644 --- a/packages/transactions/src/index.ts +++ b/packages/transactions/src/index.ts @@ -8,7 +8,7 @@ export { isSingleSig, } from './authorization'; export * from './builders'; -export { BytesReader as BytesReader } from './bytesReader'; +export { BytesReader } from './bytesReader'; /** * ### `Cl.` Clarity Value Namespace * The `Cl` namespace is provided as a convenience to build/parse Clarity Value objects. @@ -101,3 +101,4 @@ export * from './structuredDataSignature'; export { StacksTransaction, deserializeTransaction } from './transaction'; export * from './types'; export * from './utils'; +export * from './fetch'; diff --git a/packages/transactions/src/keys.ts b/packages/transactions/src/keys.ts index 4fc0953cc..8d28fc0de 100644 --- a/packages/transactions/src/keys.ts +++ b/packages/transactions/src/keys.ts @@ -34,10 +34,10 @@ import { COMPRESSED_PUBKEY_LENGTH_BYTES, PubKeyEncoding, StacksMessageType, - TransactionVersion, UNCOMPRESSED_PUBKEY_LENGTH_BYTES, } from './constants'; import { hash160, hashP2PKH } from './utils'; +import { TransactionVersion } from '@stacks/network'; /** * To use secp256k1.signSync set utils.hmacSha256Sync to a function using noble-hashes diff --git a/packages/transactions/src/pc.ts b/packages/transactions/src/pc.ts index 2ca20eccf..0e60f6dca 100644 --- a/packages/transactions/src/pc.ts +++ b/packages/transactions/src/pc.ts @@ -30,7 +30,7 @@ type AddressString = string; type ContractIdString = `${string}.${string}`; /** - * An asset identifier string given as `::` aka `.::` + * An asset name string given as `::` aka `.::` */ type NftString = `${ContractIdString}::${string}`; diff --git a/packages/transactions/src/postcondition-types.ts b/packages/transactions/src/postcondition-types.ts index 74c95c787..ab24d6701 100644 --- a/packages/transactions/src/postcondition-types.ts +++ b/packages/transactions/src/postcondition-types.ts @@ -2,7 +2,7 @@ import { FungibleConditionCode, MAX_STRING_LENGTH_BYTES, NonFungibleConditionCode, - PostConditionPrincipalID, + PostConditionPrincipalId, PostConditionType, StacksMessageType, } from './constants'; @@ -13,13 +13,13 @@ import { exceedsMaxLengthBytes } from './utils'; export interface StandardPrincipal { readonly type: StacksMessageType.Principal; - readonly prefix: PostConditionPrincipalID.Standard; + readonly prefix: PostConditionPrincipalId.Standard; readonly address: Address; } export interface ContractPrincipal { readonly type: StacksMessageType.Principal; - readonly prefix: PostConditionPrincipalID.Contract; + readonly prefix: PostConditionPrincipalId.Contract; readonly address: Address; readonly contractName: LengthPrefixedString; } @@ -144,7 +144,7 @@ export function createContractPrincipal( const name = createLPString(contractName); return { type: StacksMessageType.Principal, - prefix: PostConditionPrincipalID.Contract, + prefix: PostConditionPrincipalId.Contract, address: addr, contractName: name, }; @@ -154,7 +154,7 @@ export function createStandardPrincipal(addressString: string): StandardPrincipa const addr = createAddress(addressString); return { type: StacksMessageType.Principal, - prefix: PostConditionPrincipalID.Standard, + prefix: PostConditionPrincipalId.Standard, address: addr, }; } diff --git a/packages/transactions/src/transaction.ts b/packages/transactions/src/transaction.ts index d5dd3d752..cbad96b81 100644 --- a/packages/transactions/src/transaction.ts +++ b/packages/transactions/src/transaction.ts @@ -8,16 +8,13 @@ import { } from '@stacks/common'; import { AnchorMode, - anchorModeFromNameOrValue, + anchorModeFrom, AnchorModeName, AuthType, - ChainID, - DEFAULT_CHAIN_ID, PayloadType, PostConditionMode, PubKeyEncoding, StacksMessageType, - TransactionVersion, } from './constants'; import { @@ -47,10 +44,18 @@ import { isCompressed, StacksPrivateKey, StacksPublicKey } from './keys'; import { BytesReader } from './bytesReader'; import { SerializationError, SigningError } from './errors'; +import { + ChainId, + DEFAULT_CHAIN_ID, + STACKS_MAINNET, + STACKS_TESTNET, + TransactionVersion, + whenTransactionVersion, +} from '@stacks/network'; export class StacksTransaction { version: TransactionVersion; - chainId: ChainID; + chainId: ChainId; auth: Authorization; anchorMode: AnchorMode; payload: Payload; @@ -64,7 +69,7 @@ export class StacksTransaction { postConditions?: LengthPrefixedList, postConditionMode?: PostConditionMode, anchorMode?: AnchorModeName | AnchorMode, - chainId?: ChainID + chainId?: ChainId ) { this.version = version; this.auth = auth; @@ -81,7 +86,7 @@ export class StacksTransaction { this.postConditions = postConditions ?? createLPList([]); if (anchorMode) { - this.anchorMode = anchorModeFromNameOrValue(anchorMode); + this.anchorMode = anchorModeFrom(anchorMode); } else { switch (payload.payloadType) { case PayloadType.Coinbase: @@ -300,3 +305,11 @@ export function deserializeTransaction(tx: string | Uint8Array | BytesReader) { chainId ); } + +/** @ignore */ +export function deriveNetworkFromTx(transaction: StacksTransaction) { + return whenTransactionVersion(transaction.version)({ + [TransactionVersion.Mainnet]: STACKS_MAINNET, + [TransactionVersion.Testnet]: STACKS_TESTNET, + }); +} diff --git a/packages/transactions/src/types.ts b/packages/transactions/src/types.ts index 62773edc2..9f89a41c3 100644 --- a/packages/transactions/src/types.ts +++ b/packages/transactions/src/types.ts @@ -12,9 +12,8 @@ import { MEMO_MAX_LENGTH_BYTES, AddressHashMode, AddressVersion, - TransactionVersion, StacksMessageType, - PostConditionPrincipalID, + PostConditionPrincipalId, PostConditionType, FungibleConditionCode, NonFungibleConditionCode, @@ -57,6 +56,8 @@ import { addressFromVersionHash, } from './common'; import { deserializeCV, serializeCV } from './clarity'; +import { TransactionVersion } from '@stacks/network'; + export type StacksMessage = | Address | PostConditionPrincipal @@ -205,18 +206,18 @@ export function serializePrincipal(principal: PostConditionPrincipal): Uint8Arra const bytesArray = []; bytesArray.push(principal.prefix); bytesArray.push(serializeAddress(principal.address)); - if (principal.prefix === PostConditionPrincipalID.Contract) { + if (principal.prefix === PostConditionPrincipalId.Contract) { bytesArray.push(serializeLPString(principal.contractName)); } return concatArray(bytesArray); } export function deserializePrincipal(bytesReader: BytesReader): PostConditionPrincipal { - const prefix = bytesReader.readUInt8Enum(PostConditionPrincipalID, n => { + const prefix = bytesReader.readUInt8Enum(PostConditionPrincipalId, n => { throw new DeserializationError(`Unexpected Principal payload type: ${n}`); }); const address = deserializeAddress(bytesReader); - if (prefix === PostConditionPrincipalID.Standard) { + if (prefix === PostConditionPrincipalId.Standard) { return { type: StacksMessageType.Principal, prefix, address } as StandardPrincipal; } const contractName = deserializeLPString(bytesReader); @@ -441,3 +442,160 @@ export function deserializePostCondition(bytesReader: BytesReader): PostConditio }; } } + +export type BaseRejection = { + error: string; + reason: string; + txid: string; +}; + +export type SerializationRejection = { + reason: 'Serialization'; + reason_data: { + message: string; + }; +} & BaseRejection; + +export type DeserializationRejection = { + reason: 'Deserialization'; + reason_data: { + message: string; + }; +} & BaseRejection; + +export type SignatureValidationRejection = { + reason: 'SignatureValidation'; + reason_data: { + message: string; + }; +} & BaseRejection; + +export type BadNonceRejection = { + reason: 'BadNonce'; + reason_data: { + expected: number; + actual: number; + is_origin: boolean; + principal: boolean; + }; +} & BaseRejection; + +export type FeeTooLowRejection = { + reason: 'FeeTooLow'; + reason_data: { + expected: number; + actual: number; + }; +} & BaseRejection; + +export type NotEnoughFundsRejection = { + reason: 'NotEnoughFunds'; + reason_data: { + expected: string; + actual: string; + }; +} & BaseRejection; + +export type NoSuchContractRejection = { + reason: 'NoSuchContract'; +} & BaseRejection; + +export type NoSuchPublicFunctionRejection = { + reason: 'NoSuchPublicFunction'; +}; + +export type BadFunctionArgumentRejection = { + reason: 'BadFunctionArgument'; + reason_data: { + message: string; + }; +} & BaseRejection; + +export type ContractAlreadyExistsRejection = { + reason: 'ContractAlreadyExists'; + reason_data: { + contract_identifier: string; + }; +} & BaseRejection; + +export type PoisonMicroblocksDoNotConflictRejection = { + reason: 'PoisonMicroblocksDoNotConflict'; +} & BaseRejection; + +export type PoisonMicroblockHasUnknownPubKeyHashRejection = { + reason: 'PoisonMicroblockHasUnknownPubKeyHash'; +} & BaseRejection; + +export type PoisonMicroblockIsInvalidRejection = { + reason: 'PoisonMicroblockIsInvalid'; +} & BaseRejection; + +export type BadAddressVersionByteRejection = { + reason: 'BadAddressVersionByte'; +} & BaseRejection; + +export type NoCoinbaseViaMempoolRejection = { + reason: 'NoCoinbaseViaMempool'; +} & BaseRejection; + +export type ServerFailureNoSuchChainTipRejection = { + reason: 'ServerFailureNoSuchChainTip'; +} & BaseRejection; + +export type ServerFailureDatabaseRejection = { + reason: 'ServerFailureDatabase'; + reason_data: { + message: string; + }; +} & BaseRejection; + +export type ServerFailureOtherRejection = { + reason: 'ServerFailureOther'; + reason_data: { + message: string; + }; +} & BaseRejection; + +export type TxBroadcastResultOk = { + txid: string; +}; + +export type TxBroadcastResultRejected = + | SerializationRejection + | DeserializationRejection + | SignatureValidationRejection + | BadNonceRejection + | FeeTooLowRejection + | NotEnoughFundsRejection + | NoSuchContractRejection + | NoSuchPublicFunctionRejection + | BadFunctionArgumentRejection + | ContractAlreadyExistsRejection + | PoisonMicroblocksDoNotConflictRejection + | PoisonMicroblockHasUnknownPubKeyHashRejection + | PoisonMicroblockIsInvalidRejection + | BadAddressVersionByteRejection + | NoCoinbaseViaMempoolRejection + | ServerFailureNoSuchChainTipRejection + | ServerFailureDatabaseRejection + | ServerFailureOtherRejection; + +export type TxBroadcastResult = TxBroadcastResultOk | TxBroadcastResultRejected; + +export interface FeeEstimation { + fee: number; + fee_rate: number; +} + +export interface FeeEstimateResponse { + cost_scalar_change_by_byte: bigint; + estimated_cost: { + read_count: bigint; + read_length: bigint; + runtime: bigint; + write_count: bigint; + write_length: bigint; + }; + estimated_cost_scalar: bigint; + estimations: [FeeEstimation, FeeEstimation, FeeEstimation]; +} diff --git a/packages/transactions/src/utils.ts b/packages/transactions/src/utils.ts index 85a7a3804..781d676e3 100644 --- a/packages/transactions/src/utils.ts +++ b/packages/transactions/src/utils.ts @@ -2,10 +2,20 @@ import { ripemd160 } from '@noble/hashes/ripemd160'; import { sha256 } from '@noble/hashes/sha256'; import { sha512_256 } from '@noble/hashes/sha512'; import { utils } from '@noble/secp256k1'; -import { bytesToHex, concatArray, concatBytes, utf8ToBytes, with0x } from '@stacks/common'; +import { + ApiOpts, + bytesToHex, + concatArray, + concatBytes, + createFetchFn, + utf8ToBytes, +} from '@stacks/common'; import { c32addressDecode } from 'c32check'; import lodashCloneDeep from 'lodash.clonedeep'; import { ClarityValue, deserializeCV, serializeCV } from './clarity'; +import { deriveDefaultUrl } from '@stacks/network/src'; +import { StacksNetworkName } from '@stacks/network'; +import { StacksNetwork } from '@stacks/network'; // Export verify as utility method for signature verification export { verify as verifySignature } from '@noble/secp256k1'; @@ -155,13 +165,13 @@ export function cvToHex(cv: ClarityValue) { export function hexToCV(hex: string) { return deserializeCV(hex); } + /** * Read only function response object * * @param {Boolean} okay - the status of the response * @param {string} result - serialized hex clarity value */ - export interface ReadOnlyFunctionSuccessResponse { okay: true; result: string; @@ -185,18 +195,26 @@ export const parseReadOnlyResponse = (response: ReadOnlyFunctionResponse): Clari throw new Error(response.cause); }; -export const validateStacksAddress = (stacksAddress: string): boolean => { +export const validateStacksAddress = (address: string): boolean => { try { - c32addressDecode(stacksAddress); + c32addressDecode(address); return true; } catch (e) { return false; } }; -export const validateTxId = (txid: string): boolean => { - if (txid === 'success') return true; // Bypass fetchMock tests // todo: move this line into mocks in test files - const value = with0x(txid).toLowerCase(); - if (value.length !== 66) return false; - return with0x(BigInt(value).toString(16).padStart(64, '0')) === value; +/** @ignore */ +export const defaultApiFromNetwork = ( + network: StacksNetworkName | StacksNetwork, + override?: ApiOpts +): Required => { + return Object.assign( + {}, + { + url: deriveDefaultUrl(network), + fetch: createFetchFn(), + }, + override + ); }; diff --git a/packages/transactions/tests/builder.test.ts b/packages/transactions/tests/builder.test.ts index 9f705c3c9..87e665f0b 100644 --- a/packages/transactions/tests/builder.test.ts +++ b/packages/transactions/tests/builder.test.ts @@ -1,58 +1,68 @@ -import { bytesToHex, utf8ToBytes } from '@stacks/common'; import { + HIRO_MAINNET_URL, + HIRO_TESTNET_URL, + bytesToHex, createApiKeyMiddleware, createFetchFn, - StacksMainnet, - StacksTestnet, -} from '@stacks/network'; + utf8ToBytes, +} from '@stacks/common'; import * as fs from 'fs'; import fetchMock from 'jest-fetch-mock'; import { + ACCOUNT_PATH, + BROADCAST_PATH, + BadNonceRejection, + CONTRACT_ABI_PATH, + ClarityAbi, + READONLY_FUNCTION_CALL_PATH, + TRANSACTION_FEE_ESTIMATE_PATH, + TxBroadcastResult, + TxBroadcastResultOk, + TxBroadcastResultRejected, + broadcastTransaction, + callReadOnlyFunction, + estimateFee, + estimateTransaction, + getContractMapEntry, + getNonce, +} from '../src'; +import { + MultiSigSpendingCondition, + SingleSigSpendingCondition, + SponsoredAuthorization, + StandardAuthorization, createSingleSigSpendingCondition, createSponsoredAuth, emptyMessageSignature, isSingleSig, - MultiSigSpendingCondition, nextSignature, - SingleSigSpendingCondition, - SponsoredAuthorization, - StandardAuthorization, } from '../src/authorization'; import { - broadcastTransaction, - callReadOnlyFunction, - estimateTransaction, + SignedTokenTransferOptions, estimateTransactionByteLength, - estimateTransactionFeeWithFallback, - getContractMapEntry, - getNonce, makeContractCall, makeContractDeploy, makeContractFungiblePostCondition, makeContractNonFungiblePostCondition, makeContractSTXPostCondition, + makeSTXTokenTransfer, makeStandardFungiblePostCondition, makeStandardNonFungiblePostCondition, makeStandardSTXPostCondition, - makeSTXTokenTransfer, makeUnsignedContractCall, makeUnsignedContractDeploy, makeUnsignedSTXTokenTransfer, - SignedTokenTransferOptions, sponsorTransaction, - TxBroadcastResult, - TxBroadcastResultOk, - TxBroadcastResultRejected, } from '../src/builders'; import { BytesReader } from '../src/bytesReader'; import { + ClarityType, + UIntCV, bufferCV, bufferCVFromString, - ClarityType, noneCV, serializeCV, standardPrincipalCV, - UIntCV, uintCV, } from '../src/clarity'; import { principalCV } from '../src/clarity/types/principalCV'; @@ -63,32 +73,25 @@ import { AnchorModeName, AuthType, ClarityVersion, - DEFAULT_CORE_NODE_API_URL, FungibleConditionCode, NonFungibleConditionCode, PostConditionMode, PubKeyEncoding, - TransactionVersion, TxRejectedReason, } from '../src/constants'; -import { ClarityAbi } from '../src/contract-abi'; import { createStacksPrivateKey, isCompressed, pubKeyfromPrivKey, publicKeyToString, } from '../src/keys'; -import { createTokenTransferPayload, serializePayload, TokenTransferPayload } from '../src/payload'; +import { TokenTransferPayload, createTokenTransferPayload, serializePayload } from '../src/payload'; import { createAssetInfo } from '../src/postcondition-types'; import { createTransactionAuthField } from '../src/signature'; import { TransactionSigner } from '../src/signer'; -import { deserializeTransaction, StacksTransaction } from '../src/transaction'; +import { StacksTransaction, deserializeTransaction } from '../src/transaction'; import { cloneDeep } from '../src/utils'; -import { - createFungiblePostCondition, - createSTXPostCondition, - serializePostCondition, -} from '../src'; +import { STACKS_TESTNET, TransactionVersion } from '@stacks/network'; function setSignature( unsignedTransaction: StacksTransaction, @@ -113,15 +116,15 @@ beforeEach(() => { }); test('API key middleware - get nonce', async () => { - const senderAddress = 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6'; + const address = 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6'; const apiKey = '1234-my-api-key-example'; - const fetchFn = createFetchFn(createApiKeyMiddleware({ apiKey })); - const network = new StacksMainnet({ fetchFn }); + const fetch = createFetchFn(createApiKeyMiddleware({ apiKey })); + const api = { fetch }; fetchMock.mockOnce(`{"balance": "0", "nonce": "123"}`); - const fetchNonce = await getNonce(senderAddress, network); + const fetchNonce = await getNonce({ address, api }); expect(fetchNonce).toBe(123n); expect(fetchMock.mock.calls.length).toEqual(1); expect(fetchMock.mock.calls[0][0]).toEqual( @@ -203,7 +206,7 @@ test('Make STX token transfer with set tx fee', async () => { }); test('Make STX token transfer with fee estimate', async () => { - const apiUrl = `${DEFAULT_CORE_NODE_API_URL}/v2/fees/transaction`; + const apiUrl = `${HIRO_MAINNET_URL}/v2/fees/transaction`; const recipient = standardPrincipalCV('SP3FGQ8Z7JY9BWYZ5WM53E0M9NK7WHJF0691NZ159'); const amount = 12345; const nonce = 0; @@ -277,7 +280,7 @@ test('Make STX token transfer with testnet', async () => { senderKey, fee, nonce, - network: new StacksTestnet(), + network: STACKS_TESTNET, memo: memo, anchorMode: AnchorMode.Any, }); @@ -727,7 +730,7 @@ test('Make versioned smart contract deploy', async () => { senderKey, fee, nonce, - network: new StacksTestnet(), + network: STACKS_TESTNET, anchorMode: AnchorMode.Any, clarityVersion: ClarityVersion.Clarity2, }); @@ -754,7 +757,7 @@ test('Make smart contract deploy (defaults to versioned smart contract, as of 2. senderKey, fee, nonce, - network: new StacksTestnet(), + network: STACKS_TESTNET, anchorMode: AnchorMode.Any, }); expect(() => transaction.verifyOrigin()).not.toThrow(); @@ -802,7 +805,7 @@ test('Make smart contract deploy unsigned', async () => { publicKey, fee, nonce, - network: new StacksTestnet(), + network: STACKS_TESTNET, anchorMode: AnchorMode.Any, }); @@ -840,7 +843,7 @@ test('make a multi-sig contract deploy', async () => { signerKeys: privKeyStrings, fee, nonce, - network: new StacksTestnet(), + network: STACKS_TESTNET, anchorMode: AnchorMode.Any, }); expect(() => transaction.verifyOrigin()).not.toThrow(); @@ -864,7 +867,7 @@ test('Make smart contract deploy signed', async () => { senderKey, fee, nonce, - network: new StacksTestnet(), + network: STACKS_TESTNET, anchorMode: AnchorMode.Any, }); expect(() => transaction.verifyOrigin()).not.toThrow(); @@ -897,7 +900,7 @@ test('Make contract-call', async () => { senderKey, fee, nonce: 1, - network: new StacksTestnet(), + network: STACKS_TESTNET, anchorMode: AnchorMode.Any, }); expect(() => transaction.verifyOrigin()).not.toThrow(); @@ -992,7 +995,7 @@ test('Make contract-call with post conditions', async () => { senderKey, fee, nonce: 1, - network: new StacksTestnet(), + network: STACKS_TESTNET, postConditions, postConditionMode: PostConditionMode.Deny, anchorMode: AnchorMode.Any, @@ -1038,7 +1041,7 @@ test('Make contract-call with post condition allow mode', async () => { senderKey, fee, nonce: 1, - network: new StacksTestnet(), + network: STACKS_TESTNET, postConditionMode: PostConditionMode.Allow, anchorMode: AnchorMode.Any, }); @@ -1071,7 +1074,7 @@ test('addSignature to an unsigned contract call transaction', async () => { publicKey, fee, nonce: 1, - network: new StacksTestnet(), + network: STACKS_TESTNET, postConditionMode: PostConditionMode.Allow, anchorMode: AnchorMode.Any, }); @@ -1116,7 +1119,7 @@ test('make a multi-sig contract call', async () => { signerKeys: privKeyStrings, fee, nonce: 1, - network: new StacksTestnet(), + network: STACKS_TESTNET, postConditionMode: PostConditionMode.Allow, anchorMode: AnchorMode.Any, }); @@ -1177,31 +1180,31 @@ test('Estimate transaction transfer fee', async () => { fetchMock.mockOnce(mockedResponse); - const mainnet = new StacksMainnet(); - const resultEstimateFee = await estimateTransaction( - transaction.payload, - transactionByteLength, - mainnet - ); + const mainnet = {}; // default + const resultEstimateFee = await estimateTransaction({ + payload: bytesToHex(serializePayload(transaction.payload)), + estimatedLength: transactionByteLength, + api: mainnet, + }); fetchMock.mockOnce(mockedResponse); - const testnet = new StacksTestnet(); - const resultEstimateFee2 = await estimateTransaction( - transaction.payload, - transactionByteLength, - testnet - ); + const testnet = { url: HIRO_TESTNET_URL }; + const resultEstimateFee2 = await estimateTransaction({ + payload: bytesToHex(serializePayload(transaction.payload)), + estimatedLength: transactionByteLength, + api: testnet, + }); expect(fetchMock.mock.calls.length).toEqual(2); - expect(fetchMock.mock.calls[0][0]).toEqual(mainnet.getTransactionFeeEstimateApiUrl()); + expect(fetchMock.mock.calls[0][0]).toEqual(`${HIRO_MAINNET_URL}${TRANSACTION_FEE_ESTIMATE_PATH}`); expect(fetchMock.mock.calls[0][1]?.body).toEqual( JSON.stringify({ transaction_payload: bytesToHex(serializePayload(transaction.payload)), estimated_len: transactionByteLength, }) ); - expect(fetchMock.mock.calls[1][0]).toEqual(testnet.getTransactionFeeEstimateApiUrl()); + expect(fetchMock.mock.calls[1][0]).toEqual(`${HIRO_TESTNET_URL}${TRANSACTION_FEE_ESTIMATE_PATH}`); expect(fetchMock.mock.calls[1][1]?.body).toEqual( JSON.stringify({ transaction_payload: bytesToHex(serializePayload(transaction.payload)), @@ -1215,7 +1218,7 @@ test('Estimate transaction transfer fee', async () => { test('Estimate transaction fee fallback', async () => { const privateKey = 'cb3df38053d132895220b9ce471f6b676db5b9bf0b4adefb55f2118ece2478df01'; const poolAddress = 'ST11NJTTKGVT6D1HY4NJRVQWMQM7TVAR091EJ8P2Y'; - const network = new StacksTestnet({ url: 'http://localhost:3999' }); + const api = { url: 'http://localhost:3999' }; // http://localhost:3999/v2/fees/transaction fetchMock.once( @@ -1226,7 +1229,7 @@ test('Estimate transaction fee fallback', async () => { // http://localhost:3999/v2/fees/transfer fetchMock.once('1'); - const tx = await makeContractCall({ + const transaction = await makeContractCall({ senderKey: privateKey, contractAddress: 'ST000000000000000000002AMW42H', contractName: 'pox-2', @@ -1234,7 +1237,8 @@ test('Estimate transaction fee fallback', async () => { functionArgs: [uintCV(100_000), principalCV(poolAddress), noneCV(), noneCV()], anchorMode: AnchorMode.OnChainOnly, nonce: 1, - network, + network: STACKS_TESTNET, + api, }); // http://localhost:3999/v2/fees/transaction @@ -1246,23 +1250,10 @@ test('Estimate transaction fee fallback', async () => { // http://localhost:3999/v2/fees/transfer fetchMock.once('1'); - const testnet = new StacksTestnet(); - const resultEstimateFee = await estimateTransactionFeeWithFallback(tx, testnet); + const testnet = { url: HIRO_TESTNET_URL }; + const resultEstimateFee = await estimateFee({ transaction, api: testnet }); expect(resultEstimateFee).toBe(201n); - // Test with plain-text response - // http://localhost:3999/v2/fees/transaction - fetchMock.once( - `Estimator RPC endpoint failed to estimate tx TokenTransfer: NoEstimateAvailable`, - { status: 400 } - ); - - // http://localhost:3999/v2/fees/transfer - fetchMock.once('1'); - - const resultEstimateFee2 = await estimateTransactionFeeWithFallback(tx, testnet); - expect(resultEstimateFee2).toBe(201n); - // http://localhost:3999/v2/fees/transaction fetchMock.once( `{"error":"Estimation could not be performed","reason":"NoEstimateAvailable","reason_data":{"message":"No estimate available for the provided payload."}}`, @@ -1272,10 +1263,10 @@ test('Estimate transaction fee fallback', async () => { // http://localhost:3999/v2/fees/transfer fetchMock.once('2'); // double - const doubleRate = await estimateTransactionFeeWithFallback(tx, testnet); + const doubleRate = await estimateFee({ transaction, api: testnet }); expect(doubleRate).toBe(402n); - expect(fetchMock.mock.calls.length).toEqual(8); + expect(fetchMock.mock.calls.length).toEqual(6); }); test('Single-sig transaction byte length must include signature', async () => { @@ -1398,30 +1389,30 @@ test('Make STX token transfer with fetch account nonce', async () => { const amount = 12345; const fee = 0; const senderKey = 'cb3df38053d132895220b9ce471f6b676db5b9bf0b4adefb55f2118ece2478df01'; - const senderAddress = 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6'; + const address = 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6'; const memo = 'test memo'; - const network = new StacksTestnet(); - const apiUrl = network.getAccountApiUrl(senderAddress); fetchMock.mockOnce(`{"balance":"0", "nonce":${nonce}}`); - - const fetchNonce = await getNonce(senderAddress, network); + const fetchNonce = await getNonce({ + address, + api: { url: HIRO_TESTNET_URL }, + }); fetchMock.mockOnce(`{"balance":"0", "nonce":${nonce}}`); - const transaction = await makeSTXTokenTransfer({ recipient, amount, senderKey, fee, memo, - network, anchorMode: AnchorMode.Any, + network: 'testnet', }); + const EXPECTED_ACCOUNT_URL = `${HIRO_TESTNET_URL}${ACCOUNT_PATH}/${address}?proof=0`; expect(fetchMock.mock.calls.length).toEqual(2); - expect(fetchMock.mock.calls[0][0]).toEqual(apiUrl); - expect(fetchMock.mock.calls[1][0]).toEqual(apiUrl); + expect(fetchMock.mock.calls[0][0]).toEqual(EXPECTED_ACCOUNT_URL); + expect(fetchMock.mock.calls[1][0]).toEqual(EXPECTED_ACCOUNT_URL); expect(fetchNonce.toString()).toEqual(nonce.toString()); expect(transaction.auth.spendingCondition?.nonce?.toString()).toEqual(nonce.toString()); }); @@ -1558,7 +1549,7 @@ test('Make sponsored STX token transfer with sponsor fee estimate', async () => const nonce = 2; const senderKey = 'edf9aee84d9b7abc145504dde6726c64f369d37ee34ded868fabd876c26570bc01'; const memo = 'test memo'; - const network = new StacksMainnet(); + const api = { url: HIRO_MAINNET_URL }; const sponsorKey = '9888d734e6e80a943a6544159e31d6c7e342f695ec867d549c569fa0028892d401'; const sponsorNonce = 55; @@ -1572,7 +1563,7 @@ test('Make sponsored STX token transfer with sponsor fee estimate', async () => senderKey, fee, nonce, - memo: memo, + memo, sponsored: true, anchorMode: AnchorMode.Any, }); @@ -1614,7 +1605,7 @@ test('Make sponsored STX token transfer with sponsor fee estimate', async () => const sponsorSignedTx = await sponsorTransaction(sponsorOptions); expect(fetchMock.mock.calls.length).toEqual(1); - expect(fetchMock.mock.calls[0][0]).toEqual(network.getTransactionFeeEstimateApiUrl()); + expect(fetchMock.mock.calls[0][0]).toEqual(`${api.url}${TRANSACTION_FEE_ESTIMATE_PATH}`); const sponsorSignedTxSerialized = sponsorSignedTx.serialize(); @@ -1645,7 +1636,7 @@ test('Make sponsored STX token transfer with set tx fee', async () => { const nonce = 0; const senderKey = '8ca861519c4fa4a08de4beaa41688f60a24b575a976cf84099f38dc099a6d74401'; // const senderAddress = 'ST2HTEQF50SW4X8077F8RSR8WCT57QG166TVG0GCE'; - const network = new StacksTestnet(); + const api = { url: HIRO_TESTNET_URL }; const sponsorKey = '9888d734e6e80a943a6544159e31d6c7e342f695ec867d549c569fa0028892d401'; // const sponsorAddress = 'ST2TPJ3NEZ63MMJ8AY9S45HZ10QSH51YF93GE89GQ'; @@ -1658,9 +1649,9 @@ test('Make sponsored STX token transfer with set tx fee', async () => { senderKey, fee, nonce, - network, sponsored: true, anchorMode: AnchorMode.Any, + api, }); const sponsorOptions = { @@ -1697,7 +1688,7 @@ test('Make sponsored contract deploy with sponsor fee estimate', async () => { const senderKey = '8ca861519c4fa4a08de4beaa41688f60a24b575a976cf84099f38dc099a6d74401'; const fee = 0; const nonce = 0; - const network = new StacksTestnet(); + const api = { url: HIRO_TESTNET_URL }; const sponsorKey = '9888d734e6e80a943a6544159e31d6c7e342f695ec867d549c569fa0028892d401'; // const sponsorAddress = 'ST2TPJ3NEZ63MMJ8AY9S45HZ10QSH51YF93GE89GQ'; @@ -1713,9 +1704,9 @@ test('Make sponsored contract deploy with sponsor fee estimate', async () => { senderKey, fee, nonce, - network, sponsored: true, anchorMode: AnchorMode.Any, + api, }); const sponsorOptions = { @@ -1755,7 +1746,6 @@ test('Make sponsored contract call with sponsor nonce fetch', async () => { const buffer = bufferCV(utf8ToBytes('foo')); const senderKey = 'e494f188c2d35887531ba474c433b1e41fadd8eb824aca983447fd4bb8b277a801'; const nonce = 0; - const network = new StacksTestnet(); const fee = 0; const sponsorFee = 1000; @@ -1774,9 +1764,9 @@ test('Make sponsored contract call with sponsor nonce fetch', async () => { senderKey, fee, nonce, - network, sponsored: true, anchorMode: AnchorMode.Any, + network: 'testnet', }); const sponsorOptions = { @@ -1790,7 +1780,9 @@ test('Make sponsored contract call with sponsor nonce fetch', async () => { const sponsorSignedTx = await sponsorTransaction(sponsorOptions); expect(fetchMock.mock.calls.length).toEqual(1); - expect(fetchMock.mock.calls[0][0]).toEqual(network.getAccountApiUrl(sponsorAddress)); + expect(fetchMock.mock.calls[0][0]).toEqual( + `${HIRO_TESTNET_URL}${ACCOUNT_PATH}/${sponsorAddress}?proof=0` + ); const sponsorSignedTxSerialized = sponsorSignedTx.serialize(); @@ -1819,7 +1811,7 @@ test('Transaction broadcast success', async () => { const senderKey = 'edf9aee84d9b7abc145504dde6726c64f369d37ee34ded868fabd876c26570bc01'; const memo = 'test memo'; - const network = new StacksMainnet(); + const api = { url: HIRO_MAINNET_URL }; const transaction = await makeSTXTokenTransfer({ recipient, @@ -1831,14 +1823,17 @@ test('Transaction broadcast success', async () => { anchorMode: AnchorMode.Any, }); - fetchMock.mockOnce('success'); + const txid = transaction.txid(); + fetchMock.mockOnce(`"${txid}"`); - const response: TxBroadcastResult = await broadcastTransaction(transaction, network); + const response: TxBroadcastResult = await broadcastTransaction({ transaction, api }); expect(fetchMock.mock.calls.length).toEqual(1); - expect(fetchMock.mock.calls[0][0]).toEqual(network.getBroadcastApiUrl()); - expect(fetchMock.mock.calls[0][1]?.body).toEqual(transaction.serialize()); - expect(response as TxBroadcastResultOk).toEqual({ txid: 'success' }); + expect(fetchMock.mock.calls[0][0]).toEqual(`${api.url}${BROADCAST_PATH}`); + expect(fetchMock.mock.calls[0][1]?.body).toEqual( + JSON.stringify({ tx: bytesToHex(transaction.serialize()) }) + ); + expect(response as TxBroadcastResultOk).toEqual({ txid }); }); test('Transaction broadcast success with string network name', async () => { @@ -1853,14 +1848,17 @@ test('Transaction broadcast success with string network name', async () => { anchorMode: AnchorMode.Any, }); - fetchMock.mockOnce('success'); + const txid = transaction.txid(); + fetchMock.mockOnce(`"${txid}"`); - const response: TxBroadcastResult = await broadcastTransaction(transaction, 'mainnet'); + const response: TxBroadcastResult = await broadcastTransaction({ transaction }); expect(fetchMock.mock.calls.length).toEqual(1); - expect(fetchMock.mock.calls[0][0]).toEqual(new StacksMainnet().getBroadcastApiUrl()); - expect(fetchMock.mock.calls[0][1]?.body).toEqual(transaction.serialize()); - expect(response as TxBroadcastResultOk).toEqual({ txid: 'success' }); + expect(fetchMock.mock.calls[0][0]).toEqual(`${HIRO_MAINNET_URL}${BROADCAST_PATH}`); + expect(fetchMock.mock.calls[0][1]?.body).toEqual( + JSON.stringify({ tx: bytesToHex(transaction.serialize()) }) + ); + expect(response as TxBroadcastResultOk).toEqual({ txid }); }); test('Transaction broadcast success with network detection', async () => { @@ -1875,14 +1873,17 @@ test('Transaction broadcast success with network detection', async () => { anchorMode: AnchorMode.Any, }); - fetchMock.mockOnce('success'); + const txid = transaction.txid(); + fetchMock.mockOnce(`"${txid}"`); - const response: TxBroadcastResult = await broadcastTransaction(transaction); + const response: TxBroadcastResult = await broadcastTransaction({ transaction }); expect(fetchMock.mock.calls.length).toEqual(1); - expect(fetchMock.mock.calls[0][0]).toEqual(new StacksTestnet().getBroadcastApiUrl()); - expect(fetchMock.mock.calls[0][1]?.body).toEqual(transaction.serialize()); - expect(response as TxBroadcastResultOk).toEqual({ txid: 'success' }); + expect(fetchMock.mock.calls[0][0]).toEqual(`${HIRO_TESTNET_URL}${BROADCAST_PATH}`); + expect(fetchMock.mock.calls[0][1]?.body).toEqual( + JSON.stringify({ tx: bytesToHex(transaction.serialize()) }) + ); + expect(response as TxBroadcastResultOk).toEqual({ txid }); }); test('Transaction broadcast with attachment', async () => { @@ -1892,9 +1893,7 @@ test('Transaction broadcast with attachment', async () => { const nonce = 0; const senderKey = 'edf9aee84d9b7abc145504dde6726c64f369d37ee34ded868fabd876c26570bc01'; const memo = 'test memo'; - const attachment = utf8ToBytes('this is an attachment...'); - - const network = new StacksMainnet(); + const attachment = bytesToHex(utf8ToBytes('this is an attachment...')); const transaction = await makeSTXTokenTransfer({ recipient, @@ -1906,19 +1905,20 @@ test('Transaction broadcast with attachment', async () => { anchorMode: AnchorMode.Any, }); - fetchMock.mockOnce('success'); + const txid = transaction.txid(); + fetchMock.mockOnce(`"${txid}"`); - const response: TxBroadcastResult = await broadcastTransaction(transaction, network, attachment); + const response: TxBroadcastResult = await broadcastTransaction({ transaction, attachment }); expect(fetchMock.mock.calls.length).toEqual(1); - expect(fetchMock.mock.calls[0][0]).toEqual(network.getBroadcastApiUrl()); + expect(fetchMock.mock.calls[0][0]).toEqual(`${HIRO_MAINNET_URL}${BROADCAST_PATH}`); expect(fetchMock.mock.calls[0][1]?.body).toEqual( JSON.stringify({ tx: bytesToHex(transaction.serialize()), - attachment: bytesToHex(attachment), + attachment, }) ); - expect(response as TxBroadcastResultOk).toEqual({ txid: 'success' }); + expect(response as TxBroadcastResultOk).toEqual({ txid }); }); test('Transaction broadcast returns error', async () => { @@ -1929,8 +1929,6 @@ test('Transaction broadcast returns error', async () => { const senderKey = 'edf9aee84d9b7abc145504dde6726c64f369d37ee34ded868fabd876c26570bc01'; const memo = 'test memo'; - const network = new StacksMainnet(); - const transaction = await makeSTXTokenTransfer({ recipient, amount, @@ -1955,9 +1953,9 @@ test('Transaction broadcast returns error', async () => { fetchMock.mockOnce(JSON.stringify(rejection), { status: 400 }); - const result = await broadcastTransaction(transaction, network); + const result = await broadcastTransaction({ transaction }); expect((result as TxBroadcastResultRejected).reason).toEqual(TxRejectedReason.BadNonce); - expect((result as TxBroadcastResultRejected).reason_data).toEqual(rejection.reason_data); + expect((result as BadNonceRejection).reason_data).toEqual(rejection.reason_data); }); test('Transaction broadcast fails', async () => { @@ -1968,8 +1966,6 @@ test('Transaction broadcast fails', async () => { const senderKey = 'edf9aee84d9b7abc145504dde6726c64f369d37ee34ded868fabd876c26570bc01'; const memo = 'test memo'; - const network = new StacksMainnet(); - const transaction = await makeSTXTokenTransfer({ recipient, amount, @@ -1982,7 +1978,7 @@ test('Transaction broadcast fails', async () => { fetchMock.mockOnce('test', { status: 400 }); - await expect(broadcastTransaction(transaction, network)).rejects.toThrow(); + await expect(broadcastTransaction({ transaction })).rejects.toThrow(); }); test('Make contract-call with network ABI validation', async () => { @@ -1994,8 +1990,6 @@ test('Make contract-call with network ABI validation', async () => { const fee = 0; - const network = new StacksTestnet(); - const abi = fs.readFileSync('./tests/abi/kv-store-abi.json').toString(); fetchMock.mockOnce(abi); @@ -2007,14 +2001,16 @@ test('Make contract-call with network ABI validation', async () => { functionArgs: [buffer], fee, nonce: 1, - network: new StacksTestnet(), + network: STACKS_TESTNET, validateWithAbi: true, postConditionMode: PostConditionMode.Allow, anchorMode: AnchorMode.Any, }); expect(fetchMock.mock.calls.length).toEqual(1); - expect(fetchMock.mock.calls[0][0]).toEqual(network.getAbiApiUrl(contractAddress, contractName)); + expect(fetchMock.mock.calls[0][0]).toEqual( + `${HIRO_TESTNET_URL}${CONTRACT_ABI_PATH}/${contractAddress}/${contractName}` + ); }); test('Make contract-call with provided ABI validation', async () => { @@ -2051,8 +2047,6 @@ test('Make contract-call with network ABI validation failure', async () => { const fee = 0; - const network = new StacksTestnet(); - fetchMock.mockOnce('failed', { status: 404 }); let error; @@ -2074,7 +2068,7 @@ test('Make contract-call with network ABI validation failure', async () => { error = e; } - const abiUrl = network.getAbiApiUrl(contractAddress, contractName); + const abiUrl = `${HIRO_TESTNET_URL}${CONTRACT_ABI_PATH}/${contractAddress}/${contractName}`; expect(error).toEqual( new Error( `Error fetching contract ABI for contract "kv-store" at address ST3KC0MTNW34S1ZXD36JYKFD3JJMWA01M55DSJ4JE. Response 404: Not Found. Attempted to fetch ${abiUrl} and failed with the message: "failed"` @@ -2087,7 +2081,7 @@ test('Call read-only function', async () => { const contractName = 'kv-store'; const functionName = 'get-value?'; const buffer = bufferCVFromString('foo'); - const network = new StacksTestnet(); + const api = { url: HIRO_TESTNET_URL }; const senderAddress = 'ST2F4BK4GZH6YFBNHYDDGN4T1RKBA7DA1BJZPJEJJ'; const mockResult = bufferCVFromString('test'); @@ -2096,11 +2090,11 @@ test('Call read-only function', async () => { contractName, functionName, functionArgs: [buffer], - network, senderAddress, + api, }; - const apiUrl = network.getReadOnlyFunctionCallApiUrl(contractAddress, contractName, functionName); + const apiUrl = `${api.url}${READONLY_FUNCTION_CALL_PATH}/${contractAddress}/kv-store/get-value%3F`; // uri encoded fetchMock.mockOnce(`{"okay": true, "result": "0x${bytesToHex(serializeCV(mockResult))}"}`); const result = await callReadOnlyFunction(options); @@ -2110,23 +2104,6 @@ test('Call read-only function', async () => { expect(result).toEqual(mockResult); }); -test('Call read-only function with network string', async () => { - const mockResult = bufferCVFromString('test'); - fetchMock.mockOnce(`{"okay": true, "result": "0x${bytesToHex(serializeCV(mockResult))}"}`); - - const result = await callReadOnlyFunction({ - contractAddress: 'ST3KC0MTNW34S1ZXD36JYKFD3JJMWA01M55DSJ4JE', - contractName: 'kv-store', - functionName: 'get-value?', - functionArgs: [bufferCVFromString('foo')], - network: 'testnet', - senderAddress: 'ST2F4BK4GZH6YFBNHYDDGN4T1RKBA7DA1BJZPJEJJ', - }); - - expect(fetchMock.mock.calls.length).toEqual(1); - expect(result).toEqual(mockResult); -}); - test('Get contract map entry - success', async () => { const mockValue = 60n; const mockResult = uintCV(mockValue); @@ -2162,28 +2139,3 @@ test('Get contract map entry - no match', async () => { expect(result).toEqual(mockResult); expect(result.type).toBe(ClarityType.OptionalNone); }); - -test('Post-conditions with amount larger than 8 bytes throw an error', () => { - const amount = BigInt('0xffffffffffffffff') + 1n; - - const stxPc = createSTXPostCondition( - 'SP34EBMKMRR6SXX65GRKJ1FHEXV7AGHJ2D8ASQ5M3', - FungibleConditionCode.Equal, - amount - ); - - const fungiblePc = createFungiblePostCondition( - 'SP34EBMKMRR6SXX65GRKJ1FHEXV7AGHJ2D8ASQ5M3', - FungibleConditionCode.Equal, - amount, - 'SP34EBMKMRR6SXX65GRKJ1FHEXV7AGHJ2D8ASQ5M3.token::frank' - ); - - expect(() => { - serializePostCondition(stxPc); - }).toThrowError('The post-condition amount may not be larger than 8 bytes'); - - expect(() => { - serializePostCondition(fungiblePc); - }).toThrowError('The post-condition amount may not be larger than 8 bytes'); -}); diff --git a/packages/transactions/tests/fetchUtil.test.ts b/packages/transactions/tests/fetchUtil.test.ts index d2702be1c..6916102c8 100644 --- a/packages/transactions/tests/fetchUtil.test.ts +++ b/packages/transactions/tests/fetchUtil.test.ts @@ -1,13 +1,14 @@ -import { createApiKeyMiddleware, createFetchFn, StacksTestnet } from '@stacks/network'; import fetchMock from 'jest-fetch-mock'; -import { broadcastTransaction, makeSTXTokenTransfer } from '../src/builders'; +import { broadcastTransaction } from '../src'; +import { makeSTXTokenTransfer } from '../src/builders'; import { AnchorMode } from '../src/constants'; +import { createApiKeyMiddleware, createFetchFn } from '@stacks/common'; test('fetchFn is used in network requests', async () => { const apiKey = 'MY_KEY'; const middleware = createApiKeyMiddleware({ apiKey }); const fetchFn = createFetchFn(middleware); - const network = new StacksTestnet({ fetchFn }); + const api = { fetch: fetchFn }; const transaction = await makeSTXTokenTransfer({ recipient: 'SP3FGQ8Z7JY9BWYZ5WM53E0M9NK7WHJF0691NZ159', @@ -19,8 +20,10 @@ test('fetchFn is used in network requests', async () => { anchorMode: AnchorMode.Any, }); - fetchMock.mockOnce('success'); - await broadcastTransaction(transaction, network); + const txid = transaction.txid(); + fetchMock.mockOnce(`"${txid}"`); + + await broadcastTransaction({ transaction, api }); expect((fetchMock.mock.calls[0][1]?.headers as Headers)?.get('x-api-key')).toContain(apiKey); }); diff --git a/packages/transactions/tests/keys.test.ts b/packages/transactions/tests/keys.test.ts index a18baa59f..7fb8165ed 100644 --- a/packages/transactions/tests/keys.test.ts +++ b/packages/transactions/tests/keys.test.ts @@ -31,10 +31,10 @@ import { signWithKey, StacksMessageType, StacksPublicKey, - TransactionVersion, } from '../src'; import { randomBytes } from '../src/utils'; import { serializeDeserialize } from './macros'; +import { TransactionVersion } from '@stacks/network'; // Create and initialize EC context // Better do it once and reuse it diff --git a/packages/transactions/tests/postcondition.test.ts b/packages/transactions/tests/postcondition.test.ts index 73e1b06aa..45354c077 100644 --- a/packages/transactions/tests/postcondition.test.ts +++ b/packages/transactions/tests/postcondition.test.ts @@ -20,7 +20,7 @@ import { FungibleConditionCode, NonFungibleConditionCode, StacksMessageType, - PostConditionPrincipalID, + PostConditionPrincipalId, } from '../src/constants'; import { serializeDeserialize } from './macros'; @@ -44,7 +44,7 @@ test('STX post condition serialization and deserialization', () => { StacksMessageType.PostCondition ) as STXPostCondition; expect(deserialized.conditionType).toBe(postConditionType); - expect(deserialized.principal.prefix).toBe(PostConditionPrincipalID.Standard); + expect(deserialized.principal.prefix).toBe(PostConditionPrincipalId.Standard); expect(addressToString(deserialized.principal.address)).toBe(address); expect(deserialized.conditionCode).toBe(conditionCode); expect(deserialized.amount.toString()).toBe(amount.toString()); @@ -71,7 +71,7 @@ test('Fungible post condition serialization and deserialization', () => { StacksMessageType.PostCondition ) as FungiblePostCondition; expect(deserialized.conditionType).toBe(postConditionType); - expect(deserialized.principal.prefix).toBe(PostConditionPrincipalID.Standard); + expect(deserialized.principal.prefix).toBe(PostConditionPrincipalId.Standard); expect(addressToString(deserialized.principal.address)).toBe(address); expect(deserialized.conditionCode).toBe(conditionCode); expect(deserialized.amount.toString()).toBe(amount.toString()); @@ -108,7 +108,7 @@ test('Non-fungible post condition serialization and deserialization', () => { StacksMessageType.PostCondition ) as NonFungiblePostCondition; expect(deserialized.conditionType).toBe(postConditionType); - expect(deserialized.principal.prefix).toBe(PostConditionPrincipalID.Contract); + expect(deserialized.principal.prefix).toBe(PostConditionPrincipalId.Contract); expect(addressToString(deserialized.principal.address)).toBe(address); expect((deserialized.principal as ContractPrincipal).contractName.content).toBe(contractName); expect(deserialized.conditionCode).toBe(conditionCode); @@ -144,7 +144,7 @@ test('Non-fungible post condition with string IDs serialization and deserializat StacksMessageType.PostCondition ) as NonFungiblePostCondition; expect(deserialized.conditionType).toBe(postConditionType); - expect(deserialized.principal.prefix).toBe(PostConditionPrincipalID.Contract); + expect(deserialized.principal.prefix).toBe(PostConditionPrincipalId.Contract); expect(addressToString(deserialized.principal.address)).toBe(address); expect((deserialized.principal as ContractPrincipal).contractName.content).toBe(contractName); expect(deserialized.conditionCode).toBe(conditionCode); diff --git a/packages/transactions/tests/transaction.test.ts b/packages/transactions/tests/transaction.test.ts index 04949fa1b..3b7d2c0de 100644 --- a/packages/transactions/tests/transaction.test.ts +++ b/packages/transactions/tests/transaction.test.ts @@ -25,10 +25,8 @@ import { AddressHashMode, AnchorMode, AuthType, - DEFAULT_CHAIN_ID, FungibleConditionCode, PostConditionMode, - TransactionVersion, } from '../src/constants'; import { createStacksPrivateKey, pubKeyfromPrivKey, publicKeyToString } from '../src/keys'; @@ -39,6 +37,7 @@ import { bytesToHex, hexToBytes } from '@stacks/common'; import fetchMock from 'jest-fetch-mock'; import { BytesReader } from '../src/bytesReader'; import { contractPrincipalCV, standardPrincipalCV } from '../src/clarity'; +import { DEFAULT_CHAIN_ID, TransactionVersion } from '@stacks/network'; beforeEach(() => { fetchMock.resetMocks(); diff --git a/packages/transactions/tests/types.test.ts b/packages/transactions/tests/types.test.ts index 0ff906afc..0d2246f10 100644 --- a/packages/transactions/tests/types.test.ts +++ b/packages/transactions/tests/types.test.ts @@ -14,11 +14,12 @@ import { createAssetInfo, } from '../src/postcondition-types'; import { Address, addressToString } from '../src/common'; -import { TransactionVersion, AddressHashMode, StacksMessageType } from '../src/constants'; +import { AddressHashMode, StacksMessageType } from '../src/constants'; import { serializeDeserialize } from './macros'; import { BytesReader } from '../src/bytesReader'; import { createStacksPublicKey } from '../src/keys'; +import { TransactionVersion } from '@stacks/network'; test('Length prefixed strings serialization and deserialization', () => { const testString = 'test message string'; @@ -55,7 +56,7 @@ test('Length prefixed list serialization and deserialization', () => { expect(deserialized.values.length).toBe(addressList.length); for (let index = 0; index < addressList.length; index++) { - expect(deserialized.values[index].toString()).toBe(addressList[index].toString()); + expect(deserialized.values[index]).toEqual(addressList[index]); } }); diff --git a/packages/transactions/tests/utils.test.ts b/packages/transactions/tests/utils.test.ts index 830766a0c..8fbe7c408 100644 --- a/packages/transactions/tests/utils.test.ts +++ b/packages/transactions/tests/utils.test.ts @@ -1,11 +1,4 @@ -import { validateStacksAddress, validateTxId } from '../src/utils'; - -const TX_ID_WITH_NO_0x = '117a6522b4e9ec27ff10bbe3940a4a07fd58e5352010b4143992edb05a7130c7'; -const TX_ID = '0x117a6522b4e9ec27ff10bbe3940a4a07fd58e5352010b4143992edb05a7130c7'; -const INVALID_EXAMPLE = - 'Failed to deserialize posted transaction: Invalid Stacks string: non-printable or non-ASCII string'; - -const INVALID_EXAMPLE_WITH_TXID = `Failed to deserialize posted transaction: Invalid Stacks string: non-printable or non-ASCII string. ${TX_ID}`; +import { validateStacksAddress } from '../src/utils'; describe(validateStacksAddress.name, () => { test('it returns true for a legit address', () => { @@ -32,24 +25,3 @@ describe(validateStacksAddress.name, () => { ); }); }); - -describe(validateTxId.name, () => { - test('correctly validates a txid without 0x', () => { - expect(validateTxId(TX_ID_WITH_NO_0x)).toEqual(true); - }); - test('correctly validates a txid with 0x', () => { - expect(validateTxId(TX_ID)).toEqual(true); - }); - test('errors when it is too short', () => { - expect(validateTxId(TX_ID.split('30c7')[0])).toEqual(false); - }); - test('errors when it is too long', () => { - expect(validateTxId(TX_ID + TX_ID)).toEqual(false); - }); - test('errors when a message is passed', () => { - expect(validateTxId(INVALID_EXAMPLE)).toEqual(false); - }); - test('errors when a message is passed even though there is a valid txid included', () => { - expect(validateTxId(INVALID_EXAMPLE_WITH_TXID)).toEqual(false); - }); -});