Skip to content

feat(js): add TypeScript SDK package (packages/js)#4

Merged
danbaruka merged 2 commits into
Safrochain-Org:mainfrom
mbagalwa:feat/js-sdk
Jul 7, 2026
Merged

feat(js): add TypeScript SDK package (packages/js)#4
danbaruka merged 2 commits into
Safrochain-Org:mainfrom
mbagalwa:feat/js-sdk

Conversation

@mbagalwa

@mbagalwa mbagalwa commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the JavaScript/TypeScript SDK as packages/js, the first concrete
package in the repository. It provides the SafHandle client for resolving
and registering handles on Safrochain.

What's included

  • src/ — client, signing client, validation, errors, types, constants
  • test/ — unit tests (validation, errors, signing client) + gated
    integration suite
  • examples/resolve.ts, register.ts
  • Build config — ESM + CJS output via tsconfig.json / tsconfig.cjs.json
  • package.json published as @safrochain/safhandle

Build artifacts (dist/, node_modules/) are excluded and covered by the
existing .gitignore.

Verification

  • npm install && npm run build — ESM + CJS build OK
  • npm test — 41 unit tests pass, 6 integration tests skipped
    (require SAFHANDLE_INTEGRATION=1)
  • Root npm run verify still passes (docs/format/audit gate unaffected)

Notes / follow-ups

  • The package is intentionally self-contained (own lockfile) rather than
    wired as an npm workspace, so the root documentation-verification CI stays
    dependency-free — wiring it as a workspace hoists dev deps and breaks the
    audit --audit-level=high gate.
  • The current client API (SafHandleClient, SafHandleError) does not yet
    match the "Phase 2" API described in docs/ (SafHandle, getAddress,
    registerName, …). Aligning code and docs is a separate follow-up.

Test plan

cd packages/js
npm install
npm run build
npm test

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **New Features**
  * Added a TypeScript/JavaScript SDK for SafHandle with read-only lookup, reverse lookup, and name record retrieval.
  * Added signing support for registering, transferring, and releasing names.
  * Added input validation and address parsing helpers for names and SafroChain addresses.
  * Added runnable examples and package setup for both modern and CommonJS builds.

* **Bug Fixes**
  * Improved handling of missing records and invalid inputs with clearer, consistent errors.

* **Documentation**
  * Added full package documentation, usage examples, build instructions, and licensing details.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@mbagalwa, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 47 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d1078493-9a1e-480c-8faf-67ff88a00761

📥 Commits

Reviewing files that changed from the base of the PR and between bf32034 and 97d94ec.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (2)
  • .github/markdown-link-check-config.json
  • packages/js/README.md
📝 Walkthrough

Walkthrough

Introduces a new @safrochain/safhandle TypeScript SDK package under packages/js, including read-only and signing clients for a SafHandle smart contract, name/address validation utilities, typed message/response shapes, error handling, build configuration, unit/integration tests, example scripts, README, and MIT license.

Changes

SafHandle JS SDK

Layer / File(s) Summary
Core types and error handling
packages/js/src/types.ts, packages/js/src/errors.ts, packages/js/test/errors.test.ts
Defines QueryMsg/ExecuteMsg/response/config/record types, SafHandleError class, SafHandleErrorCode, and isNotFound helper with tests.
Input validation utilities
packages/js/src/validation.ts, packages/js/test/validation.test.ts
Implements normalizeName, validateLabel, isSafrochainAddress, toDisplayName, parseInput and related types, with a comprehensive test suite.
Network constants
packages/js/src/constants.ts
Adds NetworkConfig type, SAFROCHAIN_TESTNET config, and DEFAULT_GAS_PRICE.
Read-only client implementation
packages/js/src/client.ts, packages/js/test/integration.test.ts
Implements SafHandleClient with connect, smart-query methods (getAddress, lookup, resolveName, getHandles, getNameRecord, getConfig), and integration tests gated by an env var.
Signing client for write operations
packages/js/src/signing-client.ts, packages/js/test/signing-client.test.ts
Implements SafHandleSigningClient with connectWithSigner, registerName, transferName, releaseName, and a private exec/nameFee helper, verified by mocked tests.
Public API barrel exports
packages/js/src/index.ts
Re-exports client, signing client, error, validation, constants, and type modules from a single entrypoint.
Build configuration and packaging
packages/js/package.json, packages/js/tsconfig.json, packages/js/tsconfig.cjs.json, packages/js/scripts/write-cjs-package.mjs
Defines package manifest with ESM/CJS exports, TypeScript configs for dual builds, and a script writing the CJS package marker.
Examples, license, and documentation
packages/js/examples/register.ts, packages/js/examples/resolve.ts, packages/js/LICENSE, packages/js/README.md
Adds runnable register/resolve example scripts, MIT license text, and README covering install, usage, validation, build, and testing.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Example as register.ts
  participant Wallet as DirectSecp256k1HdWallet
  participant SigningClient as SafHandleSigningClient
  participant Validation as normalizeName
  participant Chain as SigningCosmWasmClient

  Example->>Wallet: fromMnemonic(SAFHANDLE_MNEMONIC)
  Example->>SigningClient: connectWithSigner(rpcEndpoint, signer, contractAddress)
  SigningClient->>Chain: SigningCosmWasmClient.connectWithSigner
  Example->>SigningClient: registerName(name)
  SigningClient->>Validation: normalizeName(name)
  SigningClient->>SigningClient: nameFee() via getConfig
  SigningClient->>Chain: execute(register_name, fee)
  Chain-->>SigningClient: ExecuteResult (transactionHash)
  SigningClient-->>Example: ExecuteResult
Loading
sequenceDiagram
  participant Example as resolve.ts
  participant Client as SafHandleClient
  participant Chain as CosmWasmClient

  Example->>Client: SafHandleClient.connect(rpcEndpoint, contractAddress)
  Client->>Chain: CosmWasmClient.connect
  Example->>Client: lookup(input)
  Client->>Chain: queryContractSmart(get_address)
  alt not found
    Chain-->>Client: error
    Client-->>Example: null
  else found
    Chain-->>Client: address
    Client->>Chain: getAddress(input)
    Chain-->>Client: record details
    Client-->>Example: address + record
  end
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description covers summary and testing, but misses required template sections like Why, change type, security checklist, and docs checklist. Add the missing template sections: Why, Type of change, Security checklist, and Documentation, plus any required checkboxes and issue links.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main change: adding a TypeScript SDK package under packages/js.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
packages/js/src/constants.ts (1)

13-25: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Avoid duplicating the gas price literal.

SAFROCHAIN_TESTNET.gasPrice and DEFAULT_GAS_PRICE both hardcode "0.025usaf" independently; if one changes, the other can silently drift out of sync.

♻️ Proposed fix
-export const SAFROCHAIN_TESTNET: NetworkConfig = {
-  chainId: "safro-testnet-1",
-  rpcEndpoint: "https://rpc.testnet.safrochain.com",
-  addressPrefix: "addr_safro",
-  denom: "usaf",
-  gasPrice: "0.025usaf",
-  // SafHandle v1 (name-only) contract on safro-testnet-1, code_id 157.
-  // Source of truth: safhandle-contract/config/testnet.json.
-  contractAddress: "addr_safro1j6n2q333gy80pmpd6avss32y4nhd8deayv8m9x4uazt6zkdczk9sxjfun6",
-};
-
 /** Default gas price used when constructing a signing client. */
 export const DEFAULT_GAS_PRICE = "0.025usaf";
+
+export const SAFROCHAIN_TESTNET: NetworkConfig = {
+  chainId: "safro-testnet-1",
+  rpcEndpoint: "https://rpc.testnet.safrochain.com",
+  addressPrefix: "addr_safro",
+  denom: "usaf",
+  gasPrice: DEFAULT_GAS_PRICE,
+  // SafHandle v1 (name-only) contract on safro-testnet-1, code_id 157.
+  // Source of truth: safhandle-contract/config/testnet.json.
+  contractAddress: "addr_safro1j6n2q333gy80pmpd6avss32y4nhd8deayv8m9x4uazt6zkdczk9sxjfun6",
+};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/js/src/constants.ts` around lines 13 - 25, The gas price literal is
duplicated between SAFROCHAIN_TESTNET.gasPrice and DEFAULT_GAS_PRICE, which can
drift out of sync. Update the constants in constants.ts so there is a single
source of truth for the gas price, and have both SAFROCHAIN_TESTNET and
DEFAULT_GAS_PRICE reference that shared value instead of hardcoding the string
twice. Use the SAFROCHAIN_TESTNET and DEFAULT_GAS_PRICE symbols to keep the
change localized and easy to follow.
packages/js/test/signing-client.test.ts (1)

5-5: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Unit tests import from dist/ instead of src/, coupling them to a manual build step.

Importing SafHandleSigningClient from ../dist/index.js means these tests only work after npm run build (pretest covers npm test, but not test:watch), so watch-mode runs can silently execute against a stale build if src changes without a rebuild. Since vitest can run TypeScript directly, consider importing from ../src/index.ts for unit/mocked tests like this one, reserving the dist-based import for the integration suite that intentionally exercises the built package.

The empty beforeEach at Line 40-42 is also a no-op (mocks are created fresh in makeClient() per test) and can be removed.

Also applies to: 40-42

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/js/test/signing-client.test.ts` at line 5, The unit test in
signing-client.test.ts is coupled to the built output by importing
SafHandleSigningClient from the dist entry instead of the source entry, which
can leave watch-mode tests running against stale compiled code. Update the test
to import from the src index so vitest executes the latest TypeScript directly,
and keep the dist-based import only for integration coverage if needed. Also
remove the empty beforeEach in this test file since makeClient() already creates
fresh mocks per test and the hook is a no-op.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/js/examples/register.ts`:
- Around line 10-12: The register example currently falls back to a placeholder
name when the CLI argument is missing, which can accidentally broadcast a real
transaction with the wrong value. Update the argument handling in register.ts so
the name is required and the script fails fast with a clear error if
process.argv[2] is absent, similar to the existing SAFHANDLE_MNEMONIC check; use
the existing name and mnemonic setup in the example to locate the change.

In `@packages/js/examples/resolve.ts`:
- Around line 5-7: The CONTRACT_ADDRESS fallback in resolve.ts still accepts an
empty SAFHANDLE_CONTRACT value, which can let connect() proceed with an invalid
address. Update the CONTRACT_ADDRESS assignment to trim and validate the
environment variable before using it, and fall back to
SAFROCHAIN_TESTNET.contractAddress only when the env var is missing or blank;
use the existing resolve.ts setup around CONTRACT_ADDRESS and connect() to keep
the example from passing a bad address downstream.

---

Nitpick comments:
In `@packages/js/src/constants.ts`:
- Around line 13-25: The gas price literal is duplicated between
SAFROCHAIN_TESTNET.gasPrice and DEFAULT_GAS_PRICE, which can drift out of sync.
Update the constants in constants.ts so there is a single source of truth for
the gas price, and have both SAFROCHAIN_TESTNET and DEFAULT_GAS_PRICE reference
that shared value instead of hardcoding the string twice. Use the
SAFROCHAIN_TESTNET and DEFAULT_GAS_PRICE symbols to keep the change localized
and easy to follow.

In `@packages/js/test/signing-client.test.ts`:
- Line 5: The unit test in signing-client.test.ts is coupled to the built output
by importing SafHandleSigningClient from the dist entry instead of the source
entry, which can leave watch-mode tests running against stale compiled code.
Update the test to import from the src index so vitest executes the latest
TypeScript directly, and keep the dist-based import only for integration
coverage if needed. Also remove the empty beforeEach in this test file since
makeClient() already creates fresh mocks per test and the hook is a no-op.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b6c714e6-1e06-450a-a32e-6192d67998b3

📥 Commits

Reviewing files that changed from the base of the PR and between f364e3a and bf32034.

⛔ Files ignored due to path filters (1)
  • packages/js/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (19)
  • packages/js/LICENSE
  • packages/js/README.md
  • packages/js/examples/register.ts
  • packages/js/examples/resolve.ts
  • packages/js/package.json
  • packages/js/scripts/write-cjs-package.mjs
  • packages/js/src/client.ts
  • packages/js/src/constants.ts
  • packages/js/src/errors.ts
  • packages/js/src/index.ts
  • packages/js/src/signing-client.ts
  • packages/js/src/types.ts
  • packages/js/src/validation.ts
  • packages/js/test/errors.test.ts
  • packages/js/test/integration.test.ts
  • packages/js/test/signing-client.test.ts
  • packages/js/test/validation.test.ts
  • packages/js/tsconfig.cjs.json
  • packages/js/tsconfig.json

Comment on lines +10 to +12
const name = process.argv[2] ?? "my-name";
const mnemonic = process.env.SAFHANDLE_MNEMONIC;
if (!mnemonic) throw new Error("Set SAFHANDLE_MNEMONIC to a funded testnet account.");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Require an explicit name argument.

Defaulting to "my-name" makes a copied example capable of broadcasting a real register transaction for a placeholder name when the arg is omitted. Fail fast instead.

Suggested fix
-  const name = process.argv[2] ?? "my-name";
+  const name = process.argv[2];
+  if (!name) {
+    throw new Error("Pass the name to register as the first CLI argument.");
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const name = process.argv[2] ?? "my-name";
const mnemonic = process.env.SAFHANDLE_MNEMONIC;
if (!mnemonic) throw new Error("Set SAFHANDLE_MNEMONIC to a funded testnet account.");
const name = process.argv[2];
if (!name) {
throw new Error("Pass the name to register as the first CLI argument.");
}
const mnemonic = process.env.SAFHANDLE_MNEMONIC;
if (!mnemonic) throw new Error("Set SAFHANDLE_MNEMONIC to a funded testnet account.");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/js/examples/register.ts` around lines 10 - 12, The register example
currently falls back to a placeholder name when the CLI argument is missing,
which can accidentally broadcast a real transaction with the wrong value. Update
the argument handling in register.ts so the name is required and the script
fails fast with a clear error if process.argv[2] is absent, similar to the
existing SAFHANDLE_MNEMONIC check; use the existing name and mnemonic setup in
the example to locate the change.

Comment on lines +5 to +7
const CONTRACT_ADDRESS =
process.env.SAFHANDLE_CONTRACT ?? SAFROCHAIN_TESTNET.contractAddress!;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Normalize the contract-address fallback.

?? will not fall back when SAFHANDLE_CONTRACT is set to an empty string, so the example can still reach connect() with a bad address and fail later with a less clear error. Trim/validate the env var before using it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/js/examples/resolve.ts` around lines 5 - 7, The CONTRACT_ADDRESS
fallback in resolve.ts still accepts an empty SAFHANDLE_CONTRACT value, which
can let connect() proceed with an invalid address. Update the CONTRACT_ADDRESS
assignment to trim and validate the environment variable before using it, and
fall back to SAFROCHAIN_TESTNET.contractAddress only when the env var is missing
or blank; use the existing resolve.ts setup around CONTRACT_ADDRESS and
connect() to keep the example from passing a bad address downstream.

@mbagalwa
mbagalwa requested a review from danbaruka July 7, 2026 19:03
@mbagalwa mbagalwa added the enhancement New feature or request label Jul 7, 2026
@mbagalwa mbagalwa self-assigned this Jul 7, 2026
@danbaruka
danbaruka merged commit ea6c4cc into Safrochain-Org:main Jul 7, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants