npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@canopyhub/canopy-sdk

v2.4.0

Published

TypeScript SDK for Canopy Protocol

Readme

Canopy SDK

TypeScript SDK for Canopy Protocol on Movement and Aptos.

It includes:

  • Canopy vault reads and transaction builders
  • curator vault deposits, redemptions, and previews
  • rewards staking / claim helpers
  • Meridian ALM vault support
  • deployment + ABI registries
  • contract lookup helpers
  • Movement helper-module-backed batch reads

Packages

The repo publishes four packages:

  • @canopyhub/canopy-sdk
  • @canopyhub/canopy-sdk-core
  • @canopyhub/canopy-sdk-deployments
  • @canopyhub/canopy-sdk-bindings

Most applications should install only the root SDK, alongside @aptos-labs/ts-sdk:

pnpm add @canopyhub/canopy-sdk @aptos-labs/ts-sdk

@aptos-labs/ts-sdk is a peer dependency (^7.0.0), not a bundled one. The SDK never imports it at runtime — every reference is import type — and its public API takes an Aptos client that you construct. Declaring it as a peer keeps a single copy in your tree, so the Aptos type in your code is the same nominal type the SDK's signatures refer to. Bundling it produced two copies whose types did not match at the API boundary, forcing consumers to cast.

Quick Start

import { Aptos, AptosConfig, Network } from "@aptos-labs/ts-sdk";
import { createCanopySdk } from "@canopyhub/canopy-sdk";

const client = new Aptos(
  new AptosConfig({
    network: Network.MAINNET
  })
);

const sdk = createCanopySdk(client, {
  chain: "movement-mainnet",
  offchain: {
    sentioApiKey: process.env.SENTIO_API_KEY, // optional, enables dynamic rewards pool discovery
  },
});

CanopySdk only exposes protocol clients that exist on the selected chain:

  • sdk.canopy
  • sdk.curator
  • sdk.rewards
  • sdk.alm.meridian

Chain Support

| Chain | Canopy | Curator | Rewards | Meridian ALM | | --- | --- | --- | --- | --- | | movement-mainnet | yes | no | yes | yes | | movement-testnet | no | yes | no | no | | aptos-testnet | yes | no | yes | no | | aptos-mainnet | no | no | no | yes |

What The SDK Exposes

Canopy vaults

const { vaults } = await sdk.canopy!.listVaults({ limit: 20, offset: 0 });

const vault = await sdk.canopy!.getVault(vaultAddress);

const position = await sdk.canopy!.getUserVaultPosition(userAddress, vaultAddress);

const depositPayload = await sdk.canopy!.buildDepositPayload({
  vaultAddress,
  amount: 1_000_000n,
  minSharesOut: 0n,
});

const withdrawPayload = await sdk.canopy!.buildWithdrawPayload({
  vaultAddress,
  shares: 1_000_000n,
  maxLossBps: 50n,
  minAmountOut: 0n,
});

Other Canopy methods:

  • unstakeAndWithdraw(...)
  • getStrategyDetails(...)
  • getVaultAllocation(...)

Canopy batch helpers

These are currently backed by the Movement helper module and are available on movement-mainnet.

const balances = await sdk.canopy!.getBatchFungibleAssetBalances(
  [metadataA, metadataB],
  userAddress
);

const shareBalances = await sdk.canopy!.getBatchVaultSharesBalances(
  [vaultA, vaultB],
  userAddress
);

const baseMetadata = await sdk.canopy!.getBatchVaultBaseMetadataAndBalances(
  [vaultA, vaultB],
  userAddress
);

const sharesMetadata = await sdk.canopy!.getBatchVaultSharesMetadataAndBalances(
  [vaultA, vaultB],
  userAddress
);

const fullMetadata = await sdk.canopy!.getBatchVaultAllMetadataAndBalances(
  [vaultA, vaultB],
  userAddress
);

Curator vaults

This is the curated-vault system with a redemption queue, partner attribution, and preview-based validation. The SDK covers the depositor surface only — curator/owner/guardian governance is not exposed.

const vaults = await sdk.curator!.listVaults({ limit: 20, offset: 0 });

const vault = await sdk.curator!.getVault(vaultAddress);

const position = await sdk.curator!.getUserVaultPosition({ userAddress, vaultAddress });

const depositPayload = sdk.curator!.buildDepositPayload({
  vaultAddress,
  amount: 5_000_000n,
  minSharesOut: 4_900_000n,
});

const partnerPayload = sdk.curator!.buildDepositWithPartnerPayload({
  vaultAddress,
  amount: 5_000_000n,
  partnerId: 7n,
});

Payload builders are synchronous and return InputEntryFunctionData:

  • buildDepositPayload(...) / buildDepositWithPartnerPayload(...)
  • buildInstantRedeemPayload(...)
  • buildRequestRedemptionPayload(...)
  • buildClaimRedemptionPayload(...) / buildCancelRedemptionPayload(...)
  • buildClaimbackEscrowedSharesPayload(...)

Reads:

  • listVaults({ offset, limit }), getVaultCount()
  • getVault(vaultAddress), getVaultConfig(...), getVaultAccounting(...), getLiquidityBreakdown(...)
  • getUserVaultPosition({ userAddress, vaultAddress }), getShareBalance({ userAddress, vaultAddress })
  • getRedemptionRequest(requestAddress), getUserRedemptionRequests({ vaultAddress, ownerAddress }), getOpenRequestCount({ vaultAddress, ownerAddress })
  • getRequestForceProcessAt({ vaultAddress, requestAddress }), getActiveLockDuration(vaultAddress), getEffectiveNav24hSharePriceDeviationBps(vaultAddress)
  • isPartnerRegistered(partnerId), getPartnerPayoutAddress(partnerId)

Sentio projections

Event-driven Sentio consumers can import the canonical bigint projection helpers from @canopyhub/canopy-sdk/core or @canopyhub/canopy-sdk-core:

  • projectVaultAt(...) for accounting, share price, cap headroom, locked profit, and NAV freshness.
  • activeVelocityEpochBoundsAt(...) for bounded bucket queries.
  • projectAggregateVelocityAt(...) and projectWalletVelocityAt(...) for current velocity use.
  • remainingFundedShares(...) for partial-claim queue share projection.

Map GraphQL numeric scalars to bigint and supply the projection timestamp. GraphQL introspection describes the stored rows after processor upload; these helpers own the financial arithmetic.

Previews are the validation API

Rather than simulating and reading an abort, ask the vault directly. Each preview returns its own gate field plus stable machine-readable reasons:

const preview = await sdk.curator!.previewDeposit({
  vaultAddress,
  depositor: userAddress,
  amount: 5_000_000n,
});

if (!preview.canDeposit) {
  // e.g. ["DepositBelowMinimum", "IdleBreachActive"]
  console.log(preview.blockingReasons.map((reason) => reason.reasonId));
}

previewInstantRedeem(...) gates on canRedeem and previewQueuedRedemption(...) on canSubmit — the three names differ because they answer different questions. reasonId is a string, not a union, because the on-chain reason enum is append-only.

Three reads return values that no composite DTO in this SDK carries:

  • getRequestForceProcessAt({ vaultAddress, requestAddress }) — the effective force-processing deadline, which can be earlier than the request's stored snapshot.
  • getActiveLockDuration(vaultAddress) — the duration the running locked-profit schedule is using. Equals config.lockDuration when no profit is locked, but keeps its own snapshot while a schedule is active, so the two diverge after a config change.
  • getEffectiveNav24hSharePriceDeviationBps(vaultAddress) — the enforced NAV deviation bound, i.e. config.nav24hSharePriceDeviationBps clamped to the current system bounds. The config field is what was requested; this is what the guard applies. (The same value also appears on-chain as nav_24h_share_price_band.threshold_bps, which this SDK does not bind.)

Queued redemptions

buildRequestRedemptionPayload does not return the request address, so read it from the transaction:

import { findRedemptionRequest } from "@canopyhub/canopy-sdk";

const submitted = await sdk.signSubmitAndWaitForTransaction({
  signer: account,
  payload: sdk.curator!.buildRequestRedemptionPayload({
    vaultAddress,
    shares: 1_000_000n,
    // Persisted on the request, not just checked at submission: if the final payout
    // comes out below this, funding is skipped, the request stays `Pending` and no
    // liquidity is consumed. Omit it and you accept any payout.
    minAssetsOut: 995_000n,
  }),
});

const requested = findRedemptionRequest(submitted, { userAddress, vaultAddress });
const request = await sdk.curator!.getRedemptionRequest(requested!.requestAddress);

requested.minAssetsOut is the same optional floor carried by the request event; request.minAssetsOut reads its persisted value from queue state. Both are bigint when present and null when absent; malformed wire options throw rather than defaulting.

Pass packageAddress too when parsing a transaction that may include events from another ::vault::RedemptionRequestedEvent.

If a funding or force-processing transaction leaves a request pending because its minimum was missed, findRedemptionFundingMinimumNotMetEvent(...) returns the calculated payout, required minimum, request, vault, actor, and attempt time.

A queued redemption is not self-service: the request must be funded and request.claimableAt must pass before buildClaimRedemptionPayload will succeed. Funding happens either when an allocator funds the request or through permissionless force-processing once its deadline is reached — so do not read fundedAmount > 0 as evidence that an allocator acted.

For that deadline, read getRequestForceProcessAt({ vaultAddress, requestAddress }). request.storedForceProcessAt is only the snapshot taken at submission; the contract enforces min(claimableAt + live SLA, snapshot), so a later SLA tightening moves the real deadline earlier. The getter is authoritative for the deadline alone — status, expiry, available liquidity and minAssetsOut all still apply.

getUserRedemptionRequests returns live requests and ones awaiting claim-back (Cancelled / Denied / Expired with escrow outstanding). The latter are inspect-and-claimback only — filter on status before passing an address to claim or cancel. Ordering is not stable, so never treat position as identity.

Rewards

Transaction builders:

  • buildStakeCoinPayload(...)
  • buildStakeAndSubscribeCoinPayload(...)
  • buildStakeAssetPayload(...)
  • buildStakeAndSubscribeAssetPayload(...)
  • buildWithdrawCoinPayload(...)
  • buildWithdrawAssetPayload(...)
  • buildClaimRewardsPayload(...)
  • buildSubscribePayload(...)
  • buildUnsubscribePayload(...)
  • buildUnsubscribeAndWithdrawCoinPayload(...)
  • buildUnsubscribeAndWithdrawAssetPayload(...)
  • buildCreateStakingPoolPayload(...)
  • buildStakeTokenPayload(...)
  • buildStakeVaultSharesPayload(...)

Core rewards reads:

const earned = await sdk.rewards!.getEarned({
  userAddress,
  poolAddress,
  rewardTokenAddress,
});

const poolInfo = await sdk.rewards!.getPoolInfo(poolAddress);

const rewardData = await sdk.rewards!.getRewardData(poolAddress, rewardTokenAddress);

const stakingPosition = await sdk.rewards!.getUserStakingPosition({
  userAddress,
  stakingAsset,
});

rewardRate, rewardPerTokenStored, and rewardPerToken are returned as raw fixed-point values scaled by 1e12. Divide by 10^12 in application code when you want a human decimal representation.

Rewards helper-module reads

These helper-backed reads are currently available on movement-mainnet.

const snapshot = await sdk.rewards!.getRewardsSnapshot({
  offset: 0,
  limit: 20,
  userAddress,
});

const overview = await sdk.rewards!.getRegistryOverview({
  offset: 0,
  limit: 20,
  includePools: true,
});

const userOverview = await sdk.rewards!.getUserRewardsOverview({
  userAddress,
  offset: 0,
  limit: 20,
  includePools: true,
});

Additional helper reads:

  • getRegisteredPoolCount()
  • getPoolDetails(poolAddress)
  • getRewardTokenDetails(poolAddress)
  • getUserPoolPositions({ userAddress, offset, limit })
  • getUserPoolPositionsByToken({ userAddress, stakingAsset, offset, limit })
  • getUserPoolPositionsByTokens({ userAddress, stakingAssets, offset, limit })
  • isPoolRegistered(poolAddress)
  • getUnsubscribedPools(...)
  • getUserStakedBalance(...)
  • getUserSubscribedPools(...)
  • isUserSubscribed(...)

Meridian ALM

Available on movement-mainnet and aptos-mainnet.

const vaultAddresses = await sdk.alm.meridian!.listVaults({ limit: 20, offset: 0 });

const count = await sdk.alm.meridian!.getVaultCount();

const summary = await sdk.alm.meridian!.getVaultSummary(vaultAddress);

const position = await sdk.alm.meridian!.getUserVaultPosition(vaultAddress, userAddress);

const preview = await sdk.alm.meridian!.previewWithdraw(vaultAddress, 1_000_000n);

const depositPayload = sdk.alm.meridian!.buildDepositPayload({
  vaultAddress,
  amount: 1_000_000n,
  minSharesOut: 0n,
});

Movement batch-view-backed Meridian reads:

  • getBatchVaultInfo(vaultAddresses)
  • getBatchUserVaultBalances(vaultAddresses, userAddress)
  • getBatchVaultPositions(vaultAddresses)

Transactions

All build*Payload methods return InputEntryFunctionData compatible with @aptos-labs/ts-sdk.

const payload = await sdk.canopy!.buildDepositPayload({
  vaultAddress,
  amount: 1_000_000n,
  minSharesOut: 0n,
});

await client.transaction.build.simple({
  sender: account.accountAddress,
  data: payload,
});

await sdk.simulateTransaction({
  sender: account.accountAddress,
  payload,
});

If you are using a wallet adapter, pass the same payload object into your wallet’s sign-and-submit flow. If a Move abort is hit, whether simulating a transaction or reading a view, it throws a CanopyError with code: "MOVE_ABORT" and structured details.moveAbort metadata for UI handling. All three abort string shapes fullnodes emit are recognized, because simulation and /v1/view do not report aborts the same way even on the same chain:

  • ViewVMError { major_status: ABORTED, sub_status: Some(N), ... }. Carries the code and a full function id, but no name or description.
  • Simulation on MovementENAME(0xHEX): description, where abortName and abortMessage come from the chain itself, and the location stops at the module.
  • Aptos — a bare abort code N, where the name is looked up from those the SDK knows.

details.moveAbort.rawMessage always carries the original text, and abortName is absent rather than guessed when the chain does not send one.

A Movement simulation abort names only the module it happened in, which is often an inner module the caller never invoked — a router entry function aborting inside a vault. In that case details.moveAbort reports module without a functionName; the function you actually called is on the enclosing details.function.

Offchain Helpers

The SDK exposes one optional data client under sdk.data:

  • sdk.data.rewardsDiscovery

This is useful for rewards pool discovery. It is only constructed on chains with rewards support, or when you explicitly pass offchain.sentioEndpoint.

Rewards pool resolution for buildStakeVaultSharesPayload(...) uses:

  1. explicit poolAddresses
  2. Sentio lookup, if configured for the chain

You can inspect the active discovery source with:

const status = sdk.data.rewardsDiscovery?.getStatus();

Contract And ABI Lookup

import {
  getContract,
  requireContract,
  getCanopyStrategyContract,
  inferCanopyStrategyProtocol,
} from "@canopyhub/canopy-sdk";
import { getDeployment, getContractAddress } from "@canopyhub/canopy-sdk/deployments";
import { getAbi, requireAbi } from "@canopyhub/canopy-sdk/bindings";

const deployment = getDeployment("movement-mainnet");
const vaultAddress = getContractAddress("movement-mainnet", "canopy.vault");
const rewardsAbi = requireAbi("movement-mainnet", "rewards.module");
const meridianRegistry = requireContract("movement-mainnet", "meridian.registry");
const maybeCanopy = getContract("movement-testnet", "canopy.router");

const protocol = inferCanopyStrategyProtocol("movement-mainnet", strategyAddress);
const strategy = protocol
  ? getCanopyStrategyContract("movement-mainnet", protocol)
  : null;

Lookup semantics:

  • get* returns undefined or null when a supported chain lacks that deployment
  • require* throws for missing deployments or ABIs
  • unsupported chain names throw explicit errors

Subpath Imports

The root package also exports three subpaths:

import { normalizeMoveAddress } from "@canopyhub/canopy-sdk/core";
import { getDeployment } from "@canopyhub/canopy-sdk/deployments";
import { requireAbi } from "@canopyhub/canopy-sdk/bindings";

If you need the leaf packages directly:

import { normalizeMoveAddress } from "@canopyhub/canopy-sdk-core";
import { getDeployment } from "@canopyhub/canopy-sdk-deployments";
import { requireAbi } from "@canopyhub/canopy-sdk-bindings";

Repo Layout

canopy-sdk/
├── packages/
│   ├── core/
│   ├── deployments/
│   ├── bindings/
│   └── sdk/
├── scripts/
├── tests/
└── examples/

Package roles:

  • packages/core shared Move/address/view/payload/error utilities
  • packages/deployments chain registry, feature flags, contract addresses
  • packages/bindings checked-in ABI registry by chain
  • packages/sdk user-facing protocol clients

Development

pnpm install
pnpm run hooks:install
pnpm run typecheck
pnpm test
pnpm run check:exports
pnpm run check:imports
pnpm run abi:check-local
pnpm build
pnpm run check:payloads

pnpm run hooks:install configures the repo-local .githooks/pre-commit hook, which runs abi:check-local when staged changes touch deployment addresses, generated ABI files, chain bindings, or the ABI manifest.

Live payload and view checks

pnpm run check:payloads builds every build*Payload against live fullnodes and asserts transaction.build.simple succeeds, then reads every view the clients use. It must run after pnpm build, because it exercises dist/.

This exists because the unit tests assert payload shape and never build a transaction, which is how a release shipped where every entry payload was rejected with Type mismatch for argument 0, type '&signer'.

pnpm run check:bundle inspects an already-built examples/react/dist and fails if a Node-only dependency path is bundled — a previous dependency pulled in Node's Buffer and made the SDK unusable in browsers. It does not build anything itself:

pnpm build
pnpm --filter @canopy-sdk-example/sdk-react run build
pnpm run check:bundle

Both run in CI. They need network access, as abi:check already does.

For the example app:

cd examples/react
pnpm install
pnpm dev

License

MIT