Skip to content

feat(sdk): add on-chain management and read boxes in one request - #24

Merged
pbennett merged 5 commits into
mainfrom
feat/add-more-onchain-calls
Aug 28, 2026
Merged

feat(sdk): add on-chain management and read boxes in one request#24
pbennett merged 5 commits into
mainfrom
feat/add-more-onchain-calls

Conversation

@pbennett

@pbennett pbennett commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Three substantive commits, plus a docs pass. Releases as 2.0.0 — confirmed with a semantic-release dry run.

feat(sdk): add NFD management, offers, verification, and suggestions

NfdManager gains renew, list/cancel sale, segment and vault locks, and vault transfers. Adds makeOffer, the two-step verification exchange, and name suggestions.

feat(core)!: read NFD boxes in a single algod request

Resolving an NFD listed every box, then fetched each value one at a time in a sequential loop — a cost paid on nearly every public entry point. getAllBoxes() now reads names and values together via algod's include=values, going through the raw algod client because algokit-utils exposes no bulk box API at any version. Measured on MainNet against armyids.voi.algo (14 boxes, view: 'full'): 16 requests and 1801 ms before, 2 requests and 373 ms after, identical output.

Breaking: algosdk must now be >= 3.6.0. .include() does not exist before it, so on an older algosdk this fails at runtime rather than at install.

fix(core): honor the instance contract's rules for vault and sale calls

The vault and sale methods were written against the ARC-56 JSON, which carries arg types and not a single assert. Reading the TealScript source turned up several rules the SDK ignored — one of which made sendToVault unusable:

  • sendToVault built its group as [vaultOptIn] first with no MBR payment, failing assert(groupIndex > 0, 'must have prior txn paying MBR') on every opt-in. Now [MBR payment] → [vaultOptIn] → [optional transfer], funding 0.1 ALGO per asset.
  • sendFromVault documented the opposite of what vaultSend accepts. It now validates the amount/asset combinations and refuses to send the NFD's own ASA to anyone but the owner.
  • The owner-driven writes now check assertOwnerCalledNotForSaleOrExpired() up front, and listForSale/cancelSale check their own preconditions, so a violation names the cure instead of returning an opaque assert failure.
  • lockSegment validates against the registry's segmentPlatformCostInUsd; renew reads maxYearsAllowed rather than a hardcoded 20.
  • parseAppId used parseInt, so the all-digit NFD name 123.algo resolved to app 123.

CLAUDE.md tabulates the asserts each method mirrors and points at the contract repo via $NFD_CONTRACTS. It also records what the suite cannot prove: the composer is mocked, so a green run says nothing about whether a node accepts the group — sendToVault was malformed while its tests passed.

Documentation

Two docs: commits, neither of which affects the version bump.

PUBLISHING.md still described an NPM_TOKEN secret, a manual-dispatch "Publish Package" workflow and alpha/beta/next branches, none of which exist. It now documents the real pipeline, the plugin chain, OIDC trusted publishing, and a local dry-run recipe.

CONTRIBUTING.md had drifted the same way: "pre-v1.0.0", pnpm v9, feature/* branches, a type list missing build and ci, a pre-PR checklist naming pnpm format rather than the format:check CI runs, and a dead link to the archived angular.js repo. It also now warns about the generated files under src/api and src/contracts, and about the ! breaking-change shorthand — the Angular preset has no breakingHeaderPattern, so feat(core)!: without the footer parses as no type at all and releases nothing.

CLAUDE.md listed pnpm test:watch and pnpm test:coverage as root commands; neither exists at the root.

Merging

Please merge, don't squash. The commits map to separate release-note entries; squashing collapses them into one, and the squash title would become the header semantic-release parses.

Expand the SDK with NFD lifecycle management, marketplace offers,
property verification, and name suggestion capabilities. These
features complete the core NFD owner experience beyond minting
and purchasing.

NfdManager additions:
- Add getRenewalPrice() and renew() for NFD renewal with
  configurable year count (1-20)
- Add listForSale() and cancelSale() for marketplace listing
  with optional address reservation
- Add lockSegment() for toggling segment minting with USD pricing
- Add lockVault() for controlling vault opt-in permissions
- Add sendToVault() for opting vault into assets with optional
  transfer (supports both ALGO and ASAs)
- Add sendFromVault() for sending assets from vault to a receiver

PurchasingModule additions:
- Add makeOffer() to post offers on NFDs via the instance
  contract's postOffer method

NfdApiClient additions:
- Add suggest() for name suggestion/autocomplete with buyer
  eligibility filtering
- Add verifyRequest() and verifyConfirm() for the two-step
  property verification flow (twitter, github, domain, email,
  bluesky, avatar, banner)

NfdClient additions:
- Add suggest(), makeOffer(), verifyRequest(), verifyConfirm()
  as top-level convenience methods with signer management
- verifyRequest resets signer after use; verifyConfirm does not
  require a signer

Type and export additions:
- Add SuggestOptions, ListForSaleOptions, SendToVaultOptions,
  SendFromVaultOptions, VerifyField, VerifyRequestResult,
  VerifyConfirmResult types
- Add ALGORAND_ZERO_ADDRESS constant for default sale reservation

Test updates:
- Add client-level tests for makeOffer, suggest, verifyRequest,
  verifyConfirm including signer reset behavior
- Add PurchasingModule tests for makeOffer with amount coercion,
  missing appID, and error wrapping
- Add new manager.test.ts test file (untracked)

Other:
- Add CLAUDE.md with project documentation for Claude Code
Resolving an NFD listed every box, then fetched each value one at a time
in a sequential loop. Every public entry point routes through
LookupModule.resolve(), so that cost was paid on nearly every call.

getAllBoxes() (src/utils/internal/boxes.ts) now reads names and values
together via the algod include=values query parameter. algokit-utils does
not expose it at any version (its AppManager.getBoxValues() is still one
HTTP request per box), so the read goes through the raw algod client.
Measured on MainNet against armyids.voi.algo (14 boxes, view: 'full'): 16
box requests and 1801ms before, 2 requests and 373ms after, with
identical output.

Both readers share it — LookupModule via a BaseModule wrapper and the
slim NfdResolver directly — so buildNfdRecord() no longer takes a
getBoxValue callback and is now synchronous. Global state and boxes are
also fetched concurrently rather than in sequence.

The read paginates, which it never did before. It follows nextToken, pins
pages after the first to the first page's round, and throws rather than
looping if a node repeats a cursor. It throws as well when a box comes
back without a value, since a node that ignores include=values would
otherwise yield an NFD silently missing all of its properties.

LookupModule.resolveWithBoxes() returns the raw boxes alongside the NFD.
NfdManager uses it to drop a duplicate read of v.caAlgo.0.as in
linkAddress(), which getNfd() had already fetched, and caches the NFD and
its boxes behind one invalidate() so the two cannot diverge.

Also declares typescript-eslint, which both eslint configs import but
nothing depended on. It resolved only under pnpm 9's hoisting, so lint
fails for every package under the pinned pnpm 10.

Tests: adds tests/utils/boxes.test.ts for the bulk read, pagination and
cursor guard, and extends nfd-record tests with split-field ordering and
zero-slot caAlgo cases. Adds linkAddress coverage to the manager tests,
which it lacked entirely.

BREAKING CHANGE: algosdk must now be v3.6.0 or later. The SDK reads NFD
properties with the include=values box query parameter, added in algosdk
v3.6.0; on an older algosdk, resolving an NFD fails at runtime rather
than at install time. It also needs an algod node new enough to honour
include=values, which the public MainNet and TestNet nodes already are.
The vault and sale methods were built against the ARC-56 JSON, which carries
arg types but not a single assert. Reading the TealScript source turned up
several rules the SDK was ignoring — one of which made sendToVault unusable.

- sendToVault built its group as [vaultOptIn] first with no MBR payment, so
  every opt-in failed `assert(groupIndex > 0, 'must have prior txn paying
  MBR')`. The group is now [MBR payment] -> [vaultOptIn] -> [optional
  transfer], funding 0.1 ALGO per asset (VAULT_OPT_IN_MBR).
- vaultSend rejects otherAssets whenever amount is non-zero, and requires
  amount > 0 with no other assets for ALGO. sendFromVault documented the
  opposite; it now validates both, and refuses to send the NFD's own ASA to
  anyone but the owner.
- Most owner-driven writes sit behind assertOwnerCalledNotForSaleOrExpired().
  sendToVault, sendFromVault, lockSegment and lockVault now check that up
  front, as do listForSale (expired, minting, and the zero-box requirement)
  and cancelSale (must be for sale).
- lockSegment validates an unlock price against the registry's
  segmentPlatformCostInUsd, so the default of 0 no longer guarantees failure.
- renew reads maxYearsAllowed from the registry rather than a hardcoded 20.

Two unrelated correctness fixes in the same surface:

- parseAppId used parseInt, so the all-digit NFD name '123.algo' resolved to
  app 123. It now requires the whole string to be numeric.
- resolveVaultReceiver validates a non-name receiver as an address instead of
  letting ABI encoding fail inside the send, where it read as a transaction
  failure.

CLAUDE.md tabulates the asserts each SDK method mirrors and points at the
contract repo via $NFD_CONTRACTS, so the next reader does not have to
re-derive them from a JSON that carries none of it. It also states what the
suite cannot prove: the composer is mocked, so a green run says nothing about
whether a node would accept the group — sendToVault was malformed while its
tests passed.

Also fixes the two ways `pnpm format` disagreed with `pnpm format:check` about
scope. It was `pnpm -r format`, which skipped root-level files and resolved a
different .prettierignore per package, rewriting the generated contract
clients that the root run correctly excludes. Both are now `prettier .` from
the root, and the package ignore lists the generated paths for direct runs.
PUBLISHING.md still documented the pre-semantic-release setup: an NPM_TOKEN
secret, a "Publish Package" workflow with manual dispatch and an override
tag input, and alpha/beta/next prerelease branches. None of those exist.
Releases come from .releaserc.js (branches: ['main']) via release.yml, which
authenticates to npm with OIDC trusted publishing and to GitHub with an App
token.

Documents what is actually there, plus two things that are easy to get wrong:

- The Angular preset has no breakingHeaderPattern, so `feat(core)!:` does not
  parse as a feat and contributes no bump on its own. A major needs the
  BREAKING CHANGE footer.
- Squash-merging makes the PR title the commit header semantic-release reads,
  so a PR titled without a conventional type releases nothing.

Also records the plugins-only invocation for a local dry run, since the npm
and github plugins abort in verifyConditions without credentials.
@pbennett pbennett changed the title feat(sdk)!: add on-chain management and read boxes in one request feat(sdk): add on-chain management and read boxes in one request Aug 28, 2026
CONTRIBUTING.md described a repository that has moved on:

- "early development (pre-v1.0.0)" — the package is at 1.1.0.
- "We use pnpm v9" — the root packageManager field pins pnpm 10, and
  engines requires >=10.
- Branches named feature/*, where every branch since has been <type>/*
  matching the commit type.
- The type list omitted build and ci, and gave no hint that the type
  decides the version bump.
- The pre-PR checklist listed pnpm format, which is the writer, not the
  check CI runs, and skipped build and build:examples entirely. `pnpm run
  ci` is the whole sequence in CI's order.
- The Angular convention link pointed at the archived angular.js repo.

Adds the two traps a new contributor has no way to see: the generated
files under src/api and src/contracts that a regeneration will discard,
and the `!` breaking-change shorthand, which the Angular preset's header
pattern rejects outright — so `feat(core)!:` without the footer parses as
no type at all and releases nothing. Also records what the mocked module
tests can and cannot prove, and points at PUBLISHING.md for the release
mechanics.

CLAUDE.md listed pnpm test:watch and pnpm test:coverage under commands
that "run from the repo root". Neither exists at the root — both are SDK
package scripts and need --filter.
@pbennett
pbennett merged commit b3f2ab0 into main Aug 28, 2026
1 check passed
@pbennett
pbennett deleted the feat/add-more-onchain-calls branch August 28, 2026 01:34
@txnlab-release-bot

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant