@velocity-chain/sdk
v0.5.2
Published
TypeScript SDK for velocity-node. Isomorphic core with Node and browser entry points.
Maintainers
Readme
velocity-js-sdk
TypeScript / JavaScript SDK for velocity-node. Browser and Node analog of the C# velocity-unity-sdk.
Requires Node.js 22+ (or any runtime with native fetch, WebCrypto,
and ReadableStream).
Contents
- Install
- Quick start — read a balance
- Sub-packages — six entry points; pick one per use case
@velocity-chain/sdk— main / isomorphic@velocity-chain/sdk/schemas— typed client + Zod@velocity-chain/sdk/identity— velocity-identity (port 4100)@velocity-chain/sdk/wallet— velocity-wallet (port 4200)@velocity-chain/sdk/browser— session holder + SSE + pollers@velocity-chain/sdk/server— admin actions, headless flows
- Action signing
- Bridge out (server-side, headless)
- Bridge out (browser, via wallet)
- Typed block actions
- Schema source of truth
- Wire-type discipline
- Development
- Versioning
Install
npm install @velocity-chain/sdkPublished as @velocity-chain/sdk on npm public.
Quick start — read a balance
import { createRawClient, VelocityApiException } from "@velocity-chain/sdk";
const raw = createRawClient({ baseUrl: "http://localhost:3000" });
const desc = raw.balance({
account: "alice",
namespace: "astralwars",
token: "GOLD",
});
const res = await fetch(desc.url, { method: desc.method, headers: desc.headers });
if (!res.ok) throw new VelocityApiException(`HTTP ${res.status}`, res.status, await res.json());
const balance = await res.json(); // { balance, height, ... } — `balance` is a u64 string on the wireWant runtime validation + bigint coercion? Use the typed client from
@velocity-chain/sdk/schemas — it wraps the raw client with Zod parses
and decodes u64 fields (heights, amounts, *_at_unix timestamps) as
bigint:
import { createTypedClient } from "@velocity-chain/sdk/schemas";
const client = createTypedClient({ baseUrl: "http://localhost:3000" });
const balance = await client.balance({
account: "alice",
namespace: "astralwars",
token: "GOLD",
});
// typeof balance.balance === "bigint"
// typeof balance.height === "bigint"Sub-packages
| Import path | What it ships | Runtime |
|---|---|---|
| @velocity-chain/sdk | Types, raw HTTP descriptors, crypto, borsh, action signing | Isomorphic |
| @velocity-chain/sdk/schemas | Zod schemas + typed client + signerOf | Isomorphic |
| @velocity-chain/sdk/identity | velocity-identity client (port 4100) | Isomorphic |
| @velocity-chain/sdk/wallet | velocity-wallet client (port 4200) | Isomorphic |
| @velocity-chain/sdk/browser | Session holder, SSE block subscriber, bridge pollers | Browser-safe (no node:*) |
| @velocity-chain/sdk/server | Admin actions, session-key issuance, headless BridgeOut | Node only |
The main entry types-only graph is Zod-free and stays so under a
bundle-audit guard. Consumers who only import types pay zero runtime
cost. Sub-paths (/schemas, /identity, /wallet, /browser,
/server) are opt-in and may pull Zod for runtime validation — the
audit allows them by design.
Action signing
The SDK ships ed25519 signing helpers for every dApp-callable variant
of velocity-node's ChainAction union (23 kinds; consensus-only
variants like BridgeIn/AttestBatch/CommitteeChangeset are out of
scope). Each helper pairs the right domain tag with the borsh-serialized
preimage and returns a SignedAction with authorityKey + signature
as Uint8Array.
import { signTransfer, signSwap, signBridgeOut } from "@velocity-chain/sdk";
const seed: Uint8Array = /* 32-byte ed25519 seed */;
const signedTransfer = await signTransfer(
{
from: "alice",
to: "bob",
toNamespace: "astralwars",
token: { namespace: "astralwars", name: "GOLD" },
amount: 100n,
},
seed,
);
// signedTransfer.authorityKey — 32 bytes
// signedTransfer.signature — 64 bytes
// signedTransfer.preimage — borsh-serialized payload (for verification)Available signing helpers:
| Phase | Helpers |
|---|---|
| Token movements | signMint, signTransfer, signBurn |
| Account lifecycle | signRegisterAccount, signAddAccountKey, signRevokeAccountKey, signRegisterSessionKey |
| Namespace lifecycle | signNamespaceAdmin, signPlayerManage, signDepositFuel |
| Capability admin | signGrantCapability, signGrantCrossNamespaceAccess, signRevokeCrossNamespaceAccess, signPurchaseCapability |
| Phase 3 fixed-shop pools | signCreateSwapPool, signSetSwapRate, signSwap |
| Phase 4 AMM pools | signCreateAmmPool, signAddLiquidity, signRemoveLiquidity, signAmmSwap |
| Phase 5 flash loans | signFlashSwap |
| Bridge | signBridgeOut |
Byte-exact correctness of every signing preimage is verified against
chain's bridge_golden.rs
in colocated *.test.ts files — drift here trips CI before it ships.
Bridge out (server-side, headless)
For headless backends (game server, marketplace, CLI) that hold an
authorized account-key seed and want to bridge tokens without a wallet
round-trip, the /server entry has a single-call helper:
import { submitBridgeOut } from "@velocity-chain/sdk/server";
const result = await submitBridgeOut(
{
baseUrl: "http://localhost:3000",
accountKeySeed: loadFromVault(),
},
{
namespace: "astralwars",
from: "alice",
token: { namespace: "astralwars", name: "gold" },
amount: 100n,
destinationChain: "solana",
destination: solanaRecipientPubkeyBytes, // Uint8Array(32)
nonce: 7n, // pre-fetch via raw.bridgeNonce({ account })
},
);
// result.eventHash, result.height (bigint), result.proofUrlThe helper signs the BridgeOut payload, builds the wrapped
{ signed_action } envelope chain accepts at POST /bridge-out, and
parses the response so height decodes as bigint.
…and wait for delivery (submitBridgeOutAndPoll)
When a backend wants the combined "sign + submit + wait until the destination chain confirms" cycle as one await:
import { submitBridgeOutAndPoll } from "@velocity-chain/sdk/server";
const { submission, terminal } = await submitBridgeOutAndPoll(
{ baseUrl: "http://localhost:3000", accountKeySeed: loadFromVault() },
{
namespace: "astralwars",
from: "alice",
token: { namespace: "astralwars", name: "gold" },
amount: 100n,
destinationChain: "solana",
destination: solanaRecipientPubkeyBytes,
nonce: 7n,
},
{ intervalMs: 2000, timeoutMs: 30_000 },
);
if (terminal.status === "minted") {
console.log("delivered as", terminal.destination_tx);
} else {
console.error("relay failed:", terminal.reason);
}Throws PollTimeoutError (re-exported from @velocity-chain/sdk/browser)
when timeoutMs elapses without a terminal status. Default 30s matches
the bridge-relayer p50 + headroom; tune via pollOptions.
Bridge out (browser, via wallet)
For browser flows where the user's keys live in a wallet service:
import { createTypedClient } from "@velocity-chain/sdk/schemas";
import { createWalletTypedClient } from "@velocity-chain/sdk/wallet";
const chain = createTypedClient({ baseUrl: "http://localhost:3000" });
const wallet = createWalletTypedClient({ baseUrl: "http://localhost:4200" });
const { next_nonce } = await chain.bridgeNonce({ account: "alice" });
const signed = await wallet.sign(
{ token: signingClaimJWT },
{
kind: "BridgeOut",
payload: {
namespace: "astralwars",
from: "alice",
token: { namespace: "astralwars", name: "gold" },
amount: 100n,
destination_chain: "solana",
destination: solanaRecipientHex,
nonce: next_nonce,
},
},
);
// Submit signed.signed_action to chain's POST /bridge-out (wrapped path).Typed block actions
BlockResponse.actions is a discriminated union over 31 chain variants
(externally-tagged). Narrow with if ("Variant" in action):
import { BlockResponseSchema, signerOf, type ChainAction } from "@velocity-chain/sdk/schemas";
const block = BlockResponseSchema.parse(await res.json());
for (const action of block.actions) {
if ("Mint" in action) {
console.log(`mint ${action.Mint.amount} ${action.Mint.token.name} to ${action.Mint.to}`);
} else if ("Swap" in action) {
console.log(`swap ${action.Swap.amount_in} → ≥${action.Swap.min_amount_out}`);
}
// ... 29 more variants
// Or pull the signer(s) uniformly across variants — handles the
// per-variant field-name quirks (authority_*, admin_*, added_by,
// owner_*, namespace_admin_*, etc.) and the multi-signer cases
// (BridgeIn, CommitteeChangeset).
for (const { key, signature } of signerOf(action)) {
console.log(toHex(key), toHex(signature));
}
}Identity client
import { createIdentityTypedClient } from "@velocity-chain/sdk/identity";
const identity = createIdentityTypedClient({ baseUrl: "https://identity.example.com" });
await identity.signupEmail({
email: "[email protected]",
password: "…",
accept_terms: true,
});
const me = await identity.me({ accessToken: bearerToken });Cookie-based auth rides on every request via credentials: "include";
bearer attaches only on the documented allowlist endpoints
(me, links, claimsSigning).
Wallet client
import { createWalletTypedClient } from "@velocity-chain/sdk/wallet";
const wallet = createWalletTypedClient({ baseUrl: "https://wallet.example.com" });
const result = await wallet.sign(
{ token: signingClaimJWT },
{ kind: "Transfer", payload: { /* ... */ } },
);Wallet is stateless — every authenticated call takes a per-request
SigningClaim.
Browser
import {
BridgeSession,
createRawClient,
pollBridgeOut,
PollTimeoutError,
subscribeBlocks,
} from "@velocity-chain/sdk/browser";
const session = new BridgeSession({
seed: walletIssuedSeed,
publicKey: walletIssuedPubkey,
expiresAtUnix: 1_700_000_000n,
});
const handle = subscribeBlocks({
baseUrl: "http://localhost:3000",
from: 0,
onEvent: (msg) => console.log(msg.data),
});
// handle.close() to stop
const raw = createRawClient({ baseUrl: "http://localhost:3000" });
try {
const status = await pollBridgeOut(
raw,
{ eventHash: "..." },
{ intervalMs: 2000, timeoutMs: 30_000 },
);
// status.confirmed_at_unix is bigint
} catch (err) {
if (err instanceof PollTimeoutError) { /* timed out */ }
}The /browser entry has zero node:* imports — enforced by a
bundle-audit test that fails CI on any leak.
Server
import { issueSessionKey, submitBridgeOut } from "@velocity-chain/sdk/server";
const session = await issueSessionKey(
{
baseUrl: "http://localhost:3000",
namespaceAdminSeed: loadFromVault(),
},
{
namespace: "astralwars",
username: "alice",
ttlSeconds: 900,
},
);
// session.sessionSeed → hand off to the player; SDK does not persist itThe /server entry holds the namespace-admin seed in process memory
for the duration of a request. Backends typically load it from env /
Vault / SecretsManager at boot.
Schema source of truth
Wire schemas live in @velocity-chain/schemas
and are re-exported through @velocity-chain/sdk/schemas. The typed
ChainActionSchema / BlockActionSchema discriminated union, all
response shapes, and the action sub-enums (FlashStepSchema,
NamespaceAdminOpSchema, PlayerManageOpSchema) come from there.
Wire-type discipline
velocity-blockchain
is the source of truth for every byte on the wire. Golden vectors in
its bridge_golden.rs are mirrored byte-for-byte in this SDK's test
suite. If the SDK disagrees with velocity-node, the SDK is wrong.
Development
npm install
npm run typecheck
npm test # 600+ tests across 60+ files
npm run build # tsup → dist/ (ESM + CJS + d.ts)Versioning
Pre-1.0. The wire surface covers every endpoint and action variant
documented in CHANGELOG.md, but minor versions may
still bring breaking changes as the velocity-node wire stabilizes.
A note on the npm scope
The package is @velocity-chain/sdk, not @velocity/sdk. The
@velocity scope on npm public is owned by an unrelated party
and was unclaimable; the SDK ships under @velocity-chain (the
gitlab repo name velocity-js-sdk is unchanged).
License
Proprietary, source-available. Copyright (c) 2026 Surgent International FZ-LLC. All rights reserved.
Full license text: gitlab.syncad.com/hive/velocity-js-sdk/-/blob/main/LICENSE.
Copyright (c) 2026 Surgent International FZ-LLC. All rights reserved.
This software and associated documentation files (the "Software") are the
proprietary property of Surgent International FZ-LLC, registered in Ras Al Khaimah
Economic Zone (RAKEZ), United Arab Emirates.
No license, express or implied, is granted to any person or entity to use,
copy, modify, merge, publish, distribute, sublicense, or sell copies of the
Software, in whole or in part, without the prior written consent of Surgent
Gaming LLC.
The source code is made publicly visible for transparency and review purposes
only. Public visibility does not constitute a grant of any rights.
For licensing inquiries, contact: [email protected]