Skip to content

Repository files navigation

qubic-typescript

TypeScript SDK for the Qubic network, split into focused packages for crypto, transaction building, RPC, ABI codec, and live node subscriptions. Built for Bun, works with Node.js.

Note

Public beta. Core APIs are stable and the packages are in production use, but the public surface has not frozen yet. Breaking changes may land in minor versions before 1.0. Pin to an exact version and check the changelog when upgrading.

Packages

Package Version Description
@qubic.org/types npm Branded primitives, protocol constants, shared error types
@qubic.org/crypto npm K12 hash, SchnorrQ signatures, FourQ key derivation
@qubic.org/tx npm Binary transaction builder, signer, and verifier
@qubic.org/rpc npm Type-safe REST client for the Qubic RPC gateway
@qubic.org/bob npm Live node client — JSON-RPC 2.0, REST, WebSocket subscriptions
@qubic.org/registry npm Versioned ABI registry for all Qubic smart contracts across all epochs
@qubic.org/contracts npm Generated typed wrappers for every deployed contract
@qubic.org/events npm Binary event decoder, filter builder, and typed Bob subscription helpers
@qubic.org/wallet npm Wallet, encrypted vault, and seed utilities
@qubic.org/vault npm Versioned vault library — Argon2id, binary envelope, v1 migration
@qubic.org/tcp npm Direct TCP transport — framing, connection pool, typed request helpers
@qubic.org/react npm React hooks and providers — TanStack Query, VaultProvider, WalletProvider (extension / WalletConnect / MetaMask Snap), contract queries, send mutations

Quick start

bun install
bun test          # run all tests
bun run typecheck # typecheck all packages
bun run build     # build all packages
bun run lint      # lint + format (biome)

Package dependency graph

graph TD
  types["@qubic.org/types<br/><sub>no deps — install anywhere</sub>"]
  crypto["@qubic.org/crypto<br/><sub>K12 · SchnorrQ · FourQ</sub>"]
  tx["@qubic.org/tx<br/><sub>transaction builder/signer</sub>"]
  rpc["@qubic.org/rpc<br/><sub>REST gateway client</sub>"]
  registry["@qubic.org/registry<br/><sub>ABI registry + binary codec</sub>"]
  contracts["@qubic.org/contracts<br/><sub>generated contract wrappers</sub>"]
  bob["@qubic.org/bob<br/><sub>live node client</sub>"]
  wallet["@qubic.org/wallet<br/><sub>key management</sub>"]
  vault["@qubic.org/vault<br/><sub>versioned vault library</sub>"]
  events["@qubic.org/events<br/><sub>typed event bus</sub>"]
  tcp["@qubic.org/tcp<br/><sub>raw TCP transport</sub>"]
  react["@qubic.org/react<br/><sub>React hooks</sub>"]

  types --> crypto
  types --> rpc
  types --> registry
  types --> bob
  types --> tcp
  types --> events
  crypto --> tx
  crypto --> wallet
  crypto --> tcp
  crypto --> events
  tx --> wallet
  registry --> contracts
  rpc --> contracts
  bob --> events
  wallet --> react
  vault --> wallet
  events --> react
Loading

End-to-end example: wallet transfer

import { generateSeed, createWallet, createVault, unlockVault } from '@qubic.org/wallet'
import { createLiveClient } from '@qubic.org/rpc'

const live = createLiveClient()
const tickInfoResult = await live.getTickInfo()
if (!tickInfoResult.ok) throw tickInfoResult.error
const tickInfo = tickInfoResult.value

// Generate and persist a seed in an encrypted vault
const seed = generateSeed()
const vault = await createVault('strong-password', [seed])
// exportVault(vault) → persist JSON to disk / localStorage

// Later: unlock and use
const [storedSeed] = await unlockVault(vault, 'strong-password')
const wallet = createWallet(storedSeed)

console.log(wallet.identity) // 60-char Qubic identity

const { encoded } = await wallet.buildTransaction({
  destination: 'BZBQFLLBNCXEMGLOBHUVFTLUPLVCPQUASSILFABOFFBCADQSSUPNWLZBQEXK',
  amount: 1_000_000n,
  targetTick: tickInfo.tick + 5,
  currentTick: tickInfo.tick,
})

const result = await live.broadcastTransaction(encoded)
if (!result.ok) throw result.error
console.log('peers:', result.value.peersBroadcastedTo)

End-to-end example: vault management

import { VaultManager, TaintStatusEnum } from '@qubic.org/vault'
import { deriveIdentityFromSeed } from '@qubic.org/crypto'

const vault = new VaultManager()

// Create vault with multiple seeds
const encrypted = await vault.create({
  seeds: [
    {
      publicId: deriveIdentityFromSeed('seed-one'),
      encryptedSeed: new TextEncoder().encode('seed-one'),
      alias: 'Main account',
      taintStatus: TaintStatusEnum.UNTAINTED,
    },
    {
      publicId: deriveIdentityFromSeed('seed-two'),
      encryptedSeed: new TextEncoder().encode('seed-two'),
      alias: 'Secondary account',
      taintStatus: TaintStatusEnum.UNTAINTED,
    },
  ],
  metadata: {
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString(),
    appVersion: '1.0.0',
    schemaVersion: 3,
  },
}, 'strong-password')

// Add another seed
const updated = await vault.addSeed(encrypted, 'strong-password', {
  publicId: deriveIdentityFromSeed('seed-three'),
  encryptedSeed: new TextEncoder().encode('seed-three'),
  alias: 'Trading account',
})

// Unlock and access seeds
const payload = await vault.unlock(updated, 'strong-password')
for (const entry of payload.seeds) {
  console.log(`${entry.alias}: ${entry.publicId}`)
}

// Upgrade legacy v1 vault
const upgraded = await vault.upgrade(v1VaultData, 'old-password', 'new-password')

End-to-end example: build and broadcast a contract call

import { createWallet } from '@qubic.org/wallet'
import { toSeed } from '@qubic.org/types'
import { createLiveClient } from '@qubic.org/rpc'
import { buildQearnUnlockInput } from '@qubic.org/contracts'
import { contractIndexToIdentity } from '@qubic.org/crypto'

const live = createLiveClient()
const tickInfoResult = await live.getTickInfo()
if (!tickInfoResult.ok) throw tickInfoResult.error
const tickInfo = tickInfoResult.value
const wallet = createWallet(toSeed('a'.repeat(55)))

const call = buildQearnUnlockInput({ amount: 10_000_000n, lockedEpoch: tickInfo.epoch - 1 })

const { encoded } = await wallet.buildContractCall({
  destination: contractIndexToIdentity(call.contractIndex),
  amount: 10_000_000n,
  targetTick: tickInfo.tick + 5,
  currentTick: tickInfo.tick,
  inputType: call.inputType,
  payload: call.payload,
})

const result = await live.broadcastTransaction(encoded)
if (!result.ok) throw result.error
console.log('peers:', result.value.peersBroadcastedTo)

End-to-end example: query a contract read function

import { createLiveClient } from '@qubic.org/rpc'
import { qearn } from '@qubic.org/contracts'
import { identityToPublicKey, publicKeyToIdentity } from '@qubic.org/crypto'

const live = createLiveClient()

const result = await qearn.getStateOfRound(live, { epoch: 212 }, {
  identityToPublicKey,
  publicKeyToIdentity,
})

if (result.ok) {
  console.log('state:', result.value.state)
} else {
  console.error('RPC error:', result.error.status, result.error.endpoint)
}

End-to-end example: subscribe to live events

import { subscribeQuTransfers, subscribeAssetEvents, eventFilter, LOG_TYPE } from '@qubic.org/events'
import { createBobSubscriptionClient } from '@qubic.org/bob'

const bob = createBobSubscriptionClient({ wsUrl: 'wss://bob.qubic.org/v1/ws' })

// Stream QU transfers to/from a specific identity — decoded and typed
for await (const event of subscribeQuTransfers(bob, { identity: 'BZBQFLLBNCXEMGLOBHUVFTLUPLVCPQUASSILFABOFFBCADQSSUPNWLZBQEXK' })) {
  const { source, destination, amount } = event.data.data
  console.log(`${source}${destination}: ${amount} QU at tick ${event.data.tick}`)
}

// Build a filter for asset events from a specific tick
const filter = eventFilter()
  .ofTypes(LOG_TYPE.ASSET_ISSUANCE, LOG_TYPE.ASSET_OWNERSHIP_CHANGE)
  .fromTick(25_000_000)
  .build()

for await (const event of subscribeAssetEvents(bob, filter)) {
  if (event.data.logType === LOG_TYPE.ASSET_ISSUANCE) {
    console.log('New asset:', event.data.data.assetName, 'by', event.data.data.issuer)
  }
}

End-to-end example: direct TCP node query

import { createNodePool, requestCurrentTickInfo, requestEntity, broadcastTransaction } from '@qubic.org/tcp'
import { createWallet } from '@qubic.org/wallet'
import { toSeed } from '@qubic.org/types'
import { identityToPublicKey } from '@qubic.org/crypto'

const pool = createNodePool([
  'node1.qubic.org',
  'node2.qubic.org',
  'node3.qubic.org',
])

// Current tick — retries automatically on peer failure
const { epoch, tick } = await requestCurrentTickInfo(pool)

// Entity balance directly from a peer
const pk = identityToPublicKey('BZBQFLLBNCXEMGLOBHUVFTLUPLVCPQUASSILFABOFFBCADQSSUPNWLZBQEXK')
const { entity, identity } = await requestEntity(pool, pk)
console.log(identity, entity.incomingAmount - entity.outgoingAmount)

// Broadcast a signed transaction
const wallet = createWallet(toSeed('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'))
const { bytes } = await wallet.buildTransaction({
  destination: 'BZBQFLLBNCXEMGLOBHUVFTLUPLVCPQUASSILFABOFFBCADQSSUPNWLZBQEXK',
  amount: 1n,
  targetTick: tick + 5,
  currentTick: tick,
})
await broadcastTransaction(pool, bytes)

pool.close()

End-to-end example: React dApp with wallet connector

import {
  QubicProvider, WalletProvider,
  extensionConnector,
  useWallet, useBalance, useContractQuery,
} from '@qubic.org/react'
import { createLiveClient, createQueryClient } from '@qubic.org/rpc'
import { qearnGetStateOfRound } from '@qubic.org/contracts'

const live = createLiveClient()
const archive = createQueryClient()

const connectors = [
  extensionConnector,
]

export function App() {
  return (
    <QubicProvider liveClient={live} archiveClient={archive}>
      <WalletProvider connectors={connectors}>
        <Dashboard />
      </WalletProvider>
    </QubicProvider>
  )
}

function Dashboard() {
  const { isConnected, account, connect, disconnect, connectors } = useWallet()
  const { data: balance } = useBalance(account?.identity)
  const { data: round } = useContractQuery(qearnGetStateOfRound, { epoch: 213 })

  if (!isConnected) {
    return connectors.filter(c => c.isAvailable()).map(c => (
      <button key={c.id} onClick={() => connect(c.id)}>{c.id}</button>
    ))
  }

  return (
    <div>
      <p>{account?.identity}{balance?.balance?.toString()} QU</p>
      <p>Round state: {round?.state}</p>
      <button onClick={disconnect}>Disconnect</button>
    </div>
  )
}

End-to-end example: decode a historical transaction payload

import { createRegistryClient } from '@qubic.org/registry/client'
import { decodePayload } from '@qubic.org/registry'
import { publicKeyToIdentity } from '@qubic.org/crypto'

// Get the ABI active at the epoch when the tx was submitted
const registry = createRegistryClient()
const { version } = await registry.getAbi(9 /* Qearn */, 180 /* epoch */)

const lockProc = version.procedures.find(p => p.name === 'lock_input')!
const decoded = decodePayload(payloadBytes, lockProc.inputFields, version.structs, publicKeyToIdentity)
// decoded.amount, decoded.numberOfEpochs, etc.

Contract ABI registry

@qubic.org/registry stores per-epoch ABI snapshots as JSON for every Qubic smart contract. Each snapshot records the exact binary field layout (inputFields, outputFields, nested structs) active in that epoch. The codec engine (buildPayload / decodePayload) handles all Qubic primitive types including uint128 and FourQ identity fields.

Covers epochs 122–217 — every epoch with deployed contracts since early mainnet.

packages/registry/data/
  epochs/
    122/  Quottery.json  Qx.json  ...
    ...
    212/  (27 contracts)
  epoch-refs.json   — epoch → git tag map
  registry.json     — compiled version-range index

To regenerate snapshots from source:

# Parse ABI from qubic/core git tags (requires GitHub access)
bun run packages/registry/scripts/backfill.ts --from-epoch 200

# Regenerate TypeScript wrappers from latest snapshot
bun run packages/contracts/scripts/generate.ts

Contract wrappers

@qubic.org/contracts has typed wrappers generated from the latest epoch snapshot. Each contract exports:

  • Constants: CONTRACT_INDEX and per-procedure INPUT_TYPE
  • Payload builders: build{Contract}{Procedure}Input(input) → Uint8Array
  • Output decoders: decode{Contract}{Procedure}Output(data) → Output
  • Async read callers: {contract}{Function}(live, input?, options?) → Promise<Result<Output, QubicRpcError>>
  • A namespace object: export const {contract} = { contractIndex, ...callers, ...builders, ...decoders }
import { qearn, qx, quottery } from '@qubic.org/contracts'

// Read-only query
const state = await qearn.getStateOfRound(live, { epoch: 212 })

// Payload builder (for transaction construction)
const payload = qx.buildAddToAskOrderInput({
  assetName: 'CFB',
  issuer: 'CFBMEMZOIDEXQAUXYYSZIURADQLAPWPMNJXQSNVQZAHYVOPYUKKJBJUCTVJL',
  price: 500n,
  numberOfShares: 10n,
})

Monorepo structure

graph LR
  subgraph packages
    types["types/\n@qubic.org/types"]
    crypto["crypto/\n@qubic.org/crypto"]
    tx["tx/\n@qubic.org/tx"]
    rpc["rpc/\n@qubic.org/rpc"]
    bob["bob/\n@qubic.org/bob"]
    registry["registry/\n@qubic.org/registry"]
    contracts["contracts/\n@qubic.org/contracts"]
    wallet["wallet/\n@qubic.org/wallet"]
    vault["vault/\n@qubic.org/vault"]
    events["events/\n@qubic.org/events"]
    tcp["tcp/\n@qubic.org/tcp (direct TCP)"]
    react["react/\n@qubic.org/react"]
  end
  subgraph apps
    api["registry-api/\nElysiaJS API"]
    web["registry-web/\nNext.js browser"]
  end
  subgraph resources
    core["core/ — C++ contracts & OpenAPI schemas"]
    static["static/ — contract list & metadata"]
  end
  registry --> api
  api --> web
Loading

Tooling

  • Bun v1.3+ (runtime)
  • Biome for formatting and linting — bun run lint
  • TypeScript 5.7+ for type checking — bun run typecheck
  • bun test for tests (no config needed)
  • TypeDoc for docs — bunx typedoc
  • Changesets for releases — bun run changeset
  • Conventional commits enforced by commitlint int

About

TypeScript SDK for the Qubic network — RPC, TCP, smart contracts, wallet, and React hooks

Topics

Resources

Stars

4 stars

Watchers

0 watching

Forks

Used by

Contributors

Languages