Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add several methods #7

Merged
merged 17 commits into from
Dec 10, 2024
6 changes: 4 additions & 2 deletions .github/workflows/ci-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,18 @@ jobs:
run: |
echo "@secretkeylabs:registry=https://registry.npmjs.org/" > .npmrc
echo "//registry.npmjs.org/:_authToken=$AUTH_TOKEN" >> .npmrc
bunx npm@latest publish --access=public
bunx npm@latest publish --access=public --tag pr-$PR_NUMBER
env:
AUTH_TOKEN: ${{ secrets.NPM_PACKAGE_REGISTRY_TOKEN }}
PR_NUMBER: ${{ github.event.number }}

- name: Publish to GitHub package registry
# https://github.com/oven-sh/bun/issues/1976
run: |
echo "@secretkeylabs:registry=https://npm.pkg.github.com/" > .npmrc
echo "//npm.pkg.github.com/:_authToken=$AUTH_TOKEN" >> .npmrc
bunx npm@latest publish --access=public
bunx npm@latest publish --access=public --tag pr-$PR_NUMBER
env:
# https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry#authenticating-to-github-packages
AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.number }}
Binary file modified bun.lockb
Binary file not shown.
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@secretkeylabs/stacks-tools",
"version": "0.5.0",
"version": "0.6.0",
"type": "module",
"files": [
"dist"
Expand All @@ -25,11 +25,13 @@
"@arethetypeswrong/cli": "0.15.4",
"@types/bun": "latest",
"prettier": "^3.3.3",
"tsup": "^8.2.4"
"tsup": "^8.3.5",
"typescript": "^5.0.0"
},
"peerDependencies": {
"typescript": "^5.0.0",
"valibot": "^0.41.0"
"@stacks/blockchain-api-client": "^8.2.1",
"@stacks/transactions": "^7.0.0",
fedeerbes marked this conversation as resolved.
Show resolved Hide resolved
"valibot": "^0.42.1"
},
"dependencies": {
"exponential-backoff": "3.1.1"
Expand Down
44 changes: 44 additions & 0 deletions src/README.md

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙌🏼

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Clients

Several clients are provided for requests to Stacks APIs and PoX managment.

Types for `stacksApi` and `stacksRpcApi` are provided on a "best effort" basis. They are based on the types available or documented in:

- https://github.com/hirosystems/stacks-blockchain-api
- https://github.com/hirosystems/docs

The types change from time to time, either in value or location, and are ported here when needed. Help keep the types updated by opening a PR when type updates are needed.

## API Client

Helper methods to call Hiro's Stacks API. The helpers aim to be more convenient than raw `fetch` calls by

- typing all arguments, regardless of whether they end up as URL parameters or in the request body
- typing responses
- wrapping errors in a safe `Result` rather than throwing on error

Not all endpoints have helpers currently, this is a work in progress. PRs adding new helpers are welcome.

Each endpoint helper is in its own file, with the name of the path following that used by the Hiro docs. For example, a helper for an endpoint documented at `https://docs.hiro.so/stacks/api/{category}/{endpoint-name}` would be available as `endpointName()` from `./category/endpoint-name.ts`.

Types for responses are taken from `@stacks/blockchain-api-client`. Particularly, using its `OperationResponse` helper type and the operation name.

### Doesn't Hiro already have an API client?

The API client provided by Hiro is `class` based and keeps internal state. The helpers provided here are pure functions.

Hiro's client is not the most straight forward to use. It [requires users to remember the HTTP verb and path of the endpoint](https://github.com/hirosystems/stacks-blockchain-api/blob/develop/client/MIGRATION.md#performing-requests). Arguments for requests need to be provided in several places, such as the URL path, URL params, and payload, which is not very ergonomic.

The methods provided here conveniently use a single config object and take care of constructing the request with the appropriate URL path and HTTP verb.

## RPC API Client

Follows the same pattern as the API Client described above. The types for responses are copied from available documentation and source code since they are not available as exports.

## PoX4 API

Contains helpers to call functions and read values from the PoX 4 contract.

## Pool contract API

Coming soon. Contains helpers to call functions and read values from the pool manager contract.
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ export type * as StacksRpcApi from "./stacks-rpc-api/index.js";
export { queries } from "./queries/index.js";

export * from "./utils/index.js";

export { pox4Api } from "./pox4-api/index.js";
export type * as Pox4Api from "./pox4-api/index.js";
23 changes: 23 additions & 0 deletions src/pox4-api/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export type Network = "mainnet" | "testnet";
export type ContractPrincipal = { address: string; name: string };

const netValueMap: Record<
Network,
{
pox4ContractAddress: string;
pox4ContractName: string;
}
> = {
mainnet: {
pox4ContractAddress: "SP000000000000000000002Q6VF78",
pox4ContractName: "pox-4",
},
testnet: {
pox4ContractAddress: "ST000000000000000000002AMW42H",
pox4ContractName: "pox-4",
},
};

export function networkDependentValues(network: Network) {
return netValueMap[network];
}
7 changes: 7 additions & 0 deletions src/pox4-api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { maps } from "./maps/index.js";
export type * as Maps from "./maps/index.js";

import { readOnly } from "./read-only/index.js";
export type * as ReadOnly from "./read-only/index.js";

export const pox4Api = { maps, readOnly };
4 changes: 4 additions & 0 deletions src/pox4-api/maps/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { stackingState } from "./stacking-state.js";
export type * as StackingState from "./stacking-state.js";

export const maps = { stackingState };
61 changes: 61 additions & 0 deletions src/pox4-api/maps/stacking-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {
cvToHex,
hexToCV,
type BufferCV,
type ListCV,
type OptionalCV,
type PrincipalCV,
type TupleCV,
type UIntCV,
} from "@stacks/transactions";
import type { ApiRequestOptions, ProofAndTip } from "../../stacks-api/types.js";
import { mapEntry } from "../../stacks-rpc-api/smart-contracts/map-entry.js";
import { networkDependentValues, type Network } from "../constants.js";
import { error, success, type Result } from "../../utils/safe.js";

export type StackingStateKey = TupleCV<{ stacker: PrincipalCV }>;
export type StackingStateValue = TupleCV<{
"pox-addr": TupleCV<{ version: BufferCV; hashbytes: BufferCV }>;
"lock-period": UIntCV;
"first-reward-cycle": UIntCV;
"reward-set-indexes": ListCV<UIntCV>;
"delegated-to": OptionalCV<PrincipalCV>;
}>;

export type Args = {
key: StackingStateKey;
network: Network;
} & ApiRequestOptions &
ProofAndTip;

export async function stackingState({
key,
network,
baseUrl,
apiKeyConfig,
proof,
tip,
}: Args): Promise<Result<{ data: StackingStateValue; proof?: string }>> {
const [mapEntryError, mapEntryData] = await mapEntry({
contractAddress: networkDependentValues(network).pox4ContractAddress,
contractName: networkDependentValues(network).pox4ContractName,
mapKey: cvToHex(key),
mapName: "stacking-state",
apiKeyConfig,
proof,
tip,
baseUrl,
});

if (mapEntryError)
return error({
name: "FetchStackingStateError",
message: "Failed to fetch stacking state.",
data: mapEntryError,
});

return success({
data: hexToCV(mapEntryData.data) as StackingStateValue,
proof: mapEntryData.proof,
});
}
Empty file added src/pox4-api/public/index.ts
Empty file.
5 changes: 5 additions & 0 deletions src/pox4-api/read-only/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { OptionalCV, TupleCV, BufferCV } from "@stacks/transactions";

export type PoxAddr = OptionalCV<
TupleCV<{ version: BufferCV; hashbytes: BufferCV }>
>;
64 changes: 64 additions & 0 deletions src/pox4-api/read-only/get-check-delegation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {
cvToHex,
hexToCV,
principalCV,
type OptionalCV,
type PrincipalCV,
type TupleCV,
type UIntCV,
} from "@stacks/transactions";
import type { ApiRequestOptions } from "../../stacks-api/types.js";
import { stacksRpcApi } from "../../stacks-rpc-api/index.js";
import { error, success, type Result } from "../../utils/safe.js";
import { type Network, networkDependentValues } from "../constants.js";
import type { PoxAddr } from "./common.js";

type Args = {
principal: string;
network: Network;
} & ApiRequestOptions;

export type GetCheckDelegationReturn = OptionalCV<
TupleCV<{
"amount-ustx": UIntCV;
"delegated-to": PrincipalCV;
"until-burn-ht": OptionalCV<UIntCV>;
"pox-addr": OptionalCV<PoxAddr>;
}>
>;

export async function getCheckDelegation({
principal,
network,
baseUrl,
apiKeyConfig,
}: Args): Promise<Result<GetCheckDelegationReturn>> {
const [readOnlyError, readOnlyData] =
await stacksRpcApi.smartContracts.readOnly({
contractAddress: networkDependentValues(network).pox4ContractAddress,
contractName: networkDependentValues(network).pox4ContractName,
functionName: "get-check-delegation",
arguments: [cvToHex(principalCV(principal))],
baseUrl,
apiKeyConfig,
sender: principal,
});

if (readOnlyError) {
return error({
name: "GetCheckDelegationError",
message: "Failed to get check delegation.",
data: readOnlyError,
});
}

if (!readOnlyData.okay) {
return error({
name: "GetCheckDelegationFunctionCallError",
message: "Call to `get-check-delegation` failed.",
data: readOnlyData,
});
}

return success(hexToCV(readOnlyData.result) as GetCheckDelegationReturn);
}
66 changes: 66 additions & 0 deletions src/pox4-api/read-only/get-stacker-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {
cvToHex,
hexToCV,
principalCV,
type ListCV,
type OptionalCV,
type PrincipalCV,
type TupleCV,
type UIntCV,
} from "@stacks/transactions";
import { stacksRpcApi } from "../../stacks-rpc-api/index.js";
import { networkDependentValues, type Network } from "../constants.js";
import type { ApiRequestOptions } from "../../stacks-api/types.js";
import { error, success, type Result } from "../../utils/safe.js";
import type { PoxAddr } from "./common.js";

export type Args = {
principal: string;
network: Network;
} & ApiRequestOptions;

export type GetStackerInfoReturn = OptionalCV<
TupleCV<{
"pox-addr": PoxAddr;
"lock-period": UIntCV;
"first-reward-cycle": UIntCV;
"reward-set-indexes": ListCV<UIntCV>;
"delegated-to": OptionalCV<PrincipalCV>;
}>
>;

export async function getStackerInfo({
principal,
network,
baseUrl,
apiKeyConfig,
}: Args): Promise<Result<GetStackerInfoReturn>> {
const [readOnlyError, readOnlyData] =
await stacksRpcApi.smartContracts.readOnly({
contractAddress: networkDependentValues(network).pox4ContractAddress,
contractName: networkDependentValues(network).pox4ContractName,
functionName: "get-stacker-info",
arguments: [cvToHex(principalCV(principal))],
baseUrl,
apiKeyConfig,
sender: principal,
});

if (readOnlyError) {
return error({
name: "GetStackerInfoError",
message: "Failed to get stacker info.",
data: readOnlyError,
});
}

if (!readOnlyData.okay) {
return error({
name: "GetStackerInfoFunctionCallError",
message: "Call to `get-stacker-info` failed.",
data: readOnlyData,
});
}

return success(hexToCV(readOnlyData.result) as GetStackerInfoReturn);
}
10 changes: 10 additions & 0 deletions src/pox4-api/read-only/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { getStackerInfo } from "./get-stacker-info.js";
export type * as GetStackerInfo from "./get-stacker-info.js";

import { getCheckDelegation } from "./get-check-delegation.js";
export type * as GetCheckDelegation from "./get-check-delegation.js";

export const readOnly = {
getStackerInfo,
getCheckDelegation,
};
20 changes: 0 additions & 20 deletions src/stacks-api/README.md

This file was deleted.

Loading
Loading