From b335ae8b916e290340468921a403174acb9ceda7 Mon Sep 17 00:00:00 2001 From: janniks Date: Thu, 1 Feb 2024 14:16:54 +0100 Subject: [PATCH] feat: add new api package --- package-lock.json | 32 ++++++++ packages/api/README.md | 15 ++++ packages/api/jest.config.js | 3 + packages/api/package.json | 53 +++++++++++++ packages/api/src/api.ts | 125 +++++++++++++++++++++++++++++++ packages/api/src/index.ts | 1 + packages/api/tests/api.test.ts | 14 ++++ packages/api/tests/setup.js | 2 + packages/api/tsconfig.build.json | 22 ++++++ packages/api/tsconfig.json | 10 +++ packages/api/webpack.config.js | 7 ++ tsconfig.json | 2 + 12 files changed, 286 insertions(+) create mode 100644 packages/api/README.md create mode 100644 packages/api/jest.config.js create mode 100644 packages/api/package.json create mode 100644 packages/api/src/api.ts create mode 100644 packages/api/src/index.ts create mode 100644 packages/api/tests/api.test.ts create mode 100644 packages/api/tests/setup.js create mode 100644 packages/api/tsconfig.build.json create mode 100644 packages/api/tsconfig.json create mode 100644 packages/api/webpack.config.js diff --git a/package-lock.json b/package-lock.json index 759fefbbe..677fd1926 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4308,6 +4308,10 @@ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" }, + "node_modules/@stacks/api": { + "resolved": "packages/api", + "link": true + }, "node_modules/@stacks/auth": { "resolved": "packages/auth", "link": true @@ -23606,6 +23610,34 @@ "node": ">=10" } }, + "packages/api": { + "name": "@stacks/api", + "version": "6.9.0", + "license": "MIT", + "dependencies": { + "@stacks/common": "^6.10.0", + "@stacks/network": "^6.11.3", + "@stacks/transactions": "^6.9.0" + }, + "devDependencies": { + "rimraf": "^3.0.2" + } + }, + "packages/api/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "packages/auth": { "name": "@stacks/auth", "version": "6.11.3", diff --git a/packages/api/README.md b/packages/api/README.md new file mode 100644 index 000000000..d372277e9 --- /dev/null +++ b/packages/api/README.md @@ -0,0 +1,15 @@ +# @stacks/api [![npm](https://img.shields.io/npm/v/@stacks/api?color=red)](https://www.npmjs.com/package/@stacks/api) + +todo: one-liner + +## Installation + +``` +npm install @stacks/api +``` + +## Overview + +todo + +## Todo diff --git a/packages/api/jest.config.js b/packages/api/jest.config.js new file mode 100644 index 000000000..fe8e05183 --- /dev/null +++ b/packages/api/jest.config.js @@ -0,0 +1,3 @@ +const makeJestConfig = require('../../configs/jestConfig'); + +module.exports = makeJestConfig(__dirname); diff --git a/packages/api/package.json b/packages/api/package.json new file mode 100644 index 000000000..fc09a7106 --- /dev/null +++ b/packages/api/package.json @@ -0,0 +1,53 @@ +{ + "name": "@stacks/api", + "version": "6.9.0", + "description": "Javascript library for interacting with the Stacks Blockchain Node and API.", + "license": "MIT", + "author": "Hiro Systems PBC (https://hiro.so)", + "contributors": [ + "janniks" + ], + "homepage": "https://hiro.so/stacks-js", + "scripts": { + "build": "npm run clean && npm run build:cjs && npm run build:esm && npm run build:umd", + "build:cjs": "tsc -b tsconfig.build.json", + "build:esm": "tsc -p tsconfig.build.json --module ES6 --outDir ./dist/esm", + "build:umd": "NODE_OPTIONS=--max-old-space-size=8192 webpack --config webpack.config.js", + "clean": "rimraf dist && tsc -b tsconfig.build.json --clean", + "pack": "npm pack", + "prepublishOnly": "npm run test && NODE_ENV=production npm run build", + "start": "tsc -b tsconfig.build.json --watch --verbose", + "test": "jest", + "test:watch": "jest --watch --coverage=false", + "typecheck": "tsc --noEmit", + "typecheck:watch": "npm run typecheck -- --watch" + }, + "dependencies": { + "@stacks/common": "^6.10.0", + "@stacks/network": "^6.11.3", + "@stacks/transactions": "^6.9.0" + }, + "devDependencies": { + "rimraf": "^3.0.2" + }, + "sideEffects": false, + "publishConfig": { + "access": "public" + }, + "typings": "dist/index.d.ts", + "main": "dist/index.js", + "module": "dist/esm/index.js", + "umd:main": "dist/umd/index.js", + "unpkg": "dist/umd/index.js", + "files": [ + "dist", + "src" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/hirosystems/stacks.js.git" + }, + "bugs": { + "url": "https://github.com/hirosystems/stacks.js/issues" + } +} diff --git a/packages/api/src/api.ts b/packages/api/src/api.ts new file mode 100644 index 000000000..c2efdeea2 --- /dev/null +++ b/packages/api/src/api.ts @@ -0,0 +1,125 @@ +import { FetchFn, Hex, createFetchFn } from '@stacks/common'; +import { + STACKS_MAINNET, + StacksNetwork, + StacksNetworkName, + TransactionVersion, + deriveDefaultUrl, + networkFrom, +} from '@stacks/network'; +import { + ClarityAbi, + FeeEstimation, + StacksTransaction, + TxBroadcastResult, + broadcastTransaction, + estimateTransaction, + getAbi, + getNonce, +} from '@stacks/transactions'; + +export class StacksNodeApi { + // TODO + bnsLookupUrl = 'https://stacks-node-api.mainnet.stacks.co'; + + public url: string; + public fetch: FetchFn; + + public network: StacksNetwork; + + constructor({ + url, + fetch, + network = STACKS_MAINNET, + }: { + /** The base API/node URL for the network fetch calls */ + url?: string; + /** Stacks network object (defaults to {@link STACKS_MAINNET}) */ + network?: StacksNetworkName | StacksNetwork; + /** An optional custom fetch function to override default behaviors */ + fetch?: FetchFn; + } = {}) { + this.url = url ?? deriveDefaultUrl(network); + this.fetch = fetch ?? createFetchFn(); + this.network = networkFrom(network); + } + + /** Returns `true` if the network is configured to 'mainnet', based on the TransactionVersion */ + isMainnet = () => this.network.transactionVersion === TransactionVersion.Mainnet; + + /** + * Broadcast a serialized transaction to a Stacks node (which will validate and forward to the network). + * @param transaction - The transaction to broadcast + * @param attachment - Optional attachment to include with the transaction + * @returns a Promise that resolves to a {@link TxBroadcastResult} object + */ + broadcastTransaction = async ( + transaction: StacksTransaction, + attachment?: Uint8Array | string + ): Promise => { + // todo: should we use a opts object instead of positional args here? + return broadcastTransaction({ transaction, attachment, api: this }); + }; + + /** + * Lookup the nonce for an address from a core node + * @param address - The Stacks address to look up the next nonce for + * @return A promise that resolves to a bigint of the next nonce + */ + getNonce = async (address: string): Promise => { + return getNonce({ address, api: this }); + }; + + /** + * Estimate the total transaction fee in microstacks for a Stacks transaction + * @param payload - The transaction to estimate fees for + * @param estimatedLength - Optional argument that provides the endpoint with an + * estimation of the final length (in bytes) of the transaction, including any post-conditions + * and signatures + * @return A promise that resolves to an array of {@link FeeEstimate} + */ + estimateTransaction = async ( + payload: Hex, + estimatedLength?: number + ): Promise<[FeeEstimation, FeeEstimation, FeeEstimation]> => { + return estimateTransaction({ payload, estimatedLength, api: this }); + }; + + /** + * Fetch a contract's ABI + * @param contractAddress - The contracts address + * @param contractName - The contracts name + * @returns A promise that resolves to a ClarityAbi if the operation succeeds + */ + getAbi = async (contractAddress: string, contractName: string): Promise => { + return getAbi({ contractAddress, contractName, api: this }); + }; + + // todo: migrate to new api pattern + getNameInfo(fullyQualifiedName: string) { + /* + TODO: Update to v2 API URL for name lookups + */ + const nameLookupURL = `${this.bnsLookupUrl}/v1/names/${fullyQualifiedName}`; + return this.fetch(nameLookupURL) + .then((resp: any) => { + 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: any) => { + // 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; + } + }); + } +} diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts new file mode 100644 index 000000000..b1c13e734 --- /dev/null +++ b/packages/api/src/index.ts @@ -0,0 +1 @@ +export * from './api'; diff --git a/packages/api/tests/api.test.ts b/packages/api/tests/api.test.ts new file mode 100644 index 000000000..5ed0d1b33 --- /dev/null +++ b/packages/api/tests/api.test.ts @@ -0,0 +1,14 @@ +import { DEVNET_URL, HIRO_MAINNET_URL, HIRO_TESTNET_URL } from '@stacks/common'; +import { STACKS_DEVNET, STACKS_MAINNET, STACKS_TESTNET } from '@stacks/network'; +import { StacksNodeApi } from '../src'; + +describe('setting StacksApi URL', () => { + test.each([ + { network: STACKS_MAINNET, url: HIRO_MAINNET_URL }, + { network: STACKS_TESTNET, url: HIRO_TESTNET_URL }, + { network: STACKS_DEVNET, url: DEVNET_URL }, + ])('the api class determines the correct url for each network object', ({ network, url }) => { + const api = new StacksNodeApi({ network }); + expect(api.url).toEqual(url); + }); +}); diff --git a/packages/api/tests/setup.js b/packages/api/tests/setup.js new file mode 100644 index 000000000..88c4e7b1b --- /dev/null +++ b/packages/api/tests/setup.js @@ -0,0 +1,2 @@ +const fetchMock = require('jest-fetch-mock'); +fetchMock.enableFetchMocks(); \ No newline at end of file diff --git a/packages/api/tsconfig.build.json b/packages/api/tsconfig.build.json new file mode 100644 index 000000000..e6c63a0c7 --- /dev/null +++ b/packages/api/tsconfig.build.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "noEmit": false, + "outDir": "./dist", + "rootDir": "./src", + "tsBuildInfoFile": "./tsconfig.build.tsbuildinfo", + "composite": true + }, + "references": [ + { + "path": "../common/tsconfig.build.json" + }, + { + "path": "../network/tsconfig.build.json" + }, + { + "path": "../transactions/tsconfig.build.json" + } + ], + "include": ["src/**/*"] +} diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json new file mode 100644 index 000000000..ca7a34f43 --- /dev/null +++ b/packages/api/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "target": "ES2020" + }, + "include": ["src/**/*", "tests/**/*"], + "typedocOptions": { + "entryPoints": ["src/index.ts"] + } +} diff --git a/packages/api/webpack.config.js b/packages/api/webpack.config.js new file mode 100644 index 000000000..93e69b8ab --- /dev/null +++ b/packages/api/webpack.config.js @@ -0,0 +1,7 @@ +const config = require('../../configs/webpack.config.js'); + +config.output.library.name = 'StacksApi'; + +config.resolve.fallback = {}; + +module.exports = config; diff --git a/tsconfig.json b/tsconfig.json index 14cc61235..5cd8f8fa2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "baseUrl": ".", "paths": { + "@stacks/api": ["packages/api/src"], "@stacks/auth": ["packages/auth/src"], "@stacks/bns": ["packages/bns/src"], "@stacks/cli": ["packages/cli/src"], @@ -23,6 +24,7 @@ "entryPointStrategy": "packages", "entryPoints": [ // "packages/*", without packages/cli + "packages/api", "packages/auth", "packages/encryption", "packages/network",