Skip to content
 
 

Repository files navigation

bdx-web3js

Typed SDK for connecting web apps to the Beldex Wallet browser extension — connect, read the address and balance, and initiate transactions the user approves in the wallet, MetaMask-style.

Zero runtime dependencies. ESM + CJS + browser global builds. Amounts are BigInt end-to-end (1 BDX = 10⁹ atomic units — floats are never trusted).

Protocol spec: PROTOCOL.md · Roadmap: IMPLEMENTATION_PLAN.md

Install

npm install bdx-web3js

Or as a browser global: <script src="…/bdx-web3.global.js"></script> → window.BdxWeb3.

Quickstart

import { BeldexWeb3, detectProvider, toAtomic, fromAtomic } from 'bdx-web3js'

const provider = await detectProvider({ timeoutMs: 3000 })
if (!provider) {
  // Extension not installed — show an "Install Beldex Wallet" CTA.
  return
}
const bdx = new BeldexWeb3(provider)

// Connect (opens the wallet's approval UI; idempotent once granted)
const { address, network } = await bdx.connect()

// Balance — BigInt atomic units; format with fromAtomic()
const { unlocked, approximate } = await bdx.getBalance()
console.log(`spendable: ${fromAtomic(unlocked)} BDX${approximate ? ' (approx.)' : ''}`)

// Send — the user reviews and approves inside the wallet
try {
  const { txHash, fee } = await bdx.sendTransaction({
    to: 'bx…',                  // resolve BNS names first (see below)
    amount: toAtomic('1.25'),   // display BDX → atomic units
    priority: 5                 // 1 default … 5 flash (instant)
  })
  console.log('sent', txHash, 'fee', fromAtomic(fee))
} catch (e) {
  if (BeldexWeb3.isUserRejection(e)) {
    // user said no — not an error state
  } else throw e
}

// Events
bdx.on('accountsChanged', () => {/* wallet switched — call connect() again */})
bdx.on('balanceChanged', d => {/* refresh UI */})
bdx.on('disconnect', () => {/* show connect button */})

Message signing

Ask the wallet to sign a plain-text message with the account spend key (the user approves in the wallet; "SigV1…" encoding, verifiable by beldex-wallet-cli and the explorer — see PROTOCOL.md §4.6):

const { signature, address } = await bdx.signMessage('I own this address — challenge #42')
// → { signature: 'SigV1…', address: 'bx…' }

// Verification is public — no connection or approval needed:
const valid = await bdx.verifyMessage({ message, address, signature })

Constraints (enforced client-side before the request leaves the page, and again by the wallet): non-empty plain text only — no control characters (\n included) — and the reference wallet caps messages at 512 characters.

Ownership proof on connect. connectWithProof() connects and immediately has the wallet sign a domain-bound, single-line beldex-auth-v1 statement (two approvals back-to-back):

beldex-auth-v1 domain=<origin> uri=<origin+path> address=<addr> network=<net> nonce=<n> iat=<ms> exp=<ms>[ rid=<id>]

The page's origin is baked into the signed bytes, so a proof obtained by one site fails verification at any other relying party that checks domain. All-or-nothing by default: if the user declines the signature, the fresh connection is revoked again and the 4001 is rethrown.

// AUTHENTICATION — use a server-issued challenge (nonce made, tracked, and
// consumed single-use by YOUR backend):
const challenge = await fetch('/api/auth/challenge').then(r => r.json())
// → { nonce, requestId?, expiresInMs? }
const { proof } = await bdx.connectWithProof({ challenge })   // proof.serverIssued === true
await fetch('/api/auth/verify', { method: 'POST', body: JSON.stringify(proof) })

// Ownership/liveness signal only (SDK self-generates the nonce — do NOT
// accept these for login; the server can't tell the statement was made for
// its session):
const { proof: p2 } = await bdx.connectWithProof()            // p2.serverIssued === false

// lenient variant — declined signature keeps the connection, proof is null:
await bdx.connectWithProof({ required: false })

Server-side verification checklist: parse with parseAuthChallenge(message) (or rebuild via buildAuthChallenge(fields) from stored parts), then check all of: signature via bdx_verifyMessage/CLI verify_value · domain equals your origin · address is the claimed account · network is the expected chain · nonce was issued by you, unused, and consume it atomically · now within iat/exp. Reject anything that fails a single check. (The statement is single-line by design — the wallet rejects control characters, so multi-line SIWE-style layouts cannot be signed.) connectWithProof() obtains the statement via bdx_signAuthChallenge (wallet v1.2+ required): the wallet composes it from the origin it observes via the browser, so page-side code cannot forge the bound domain — and the reserved beldex-auth-v1 prefix is rejected in generic signMessage(), so an auth statement cannot be forged through raw message signing either. signAuthChallenge(params) is also exposed directly for custom flows.

BNS names

sendTransaction accepts only concrete addresses — resolve names first so users approve exactly what gets paid:

const { address } = await bdx.resolveBns('shop.bdx')
// ALWAYS show `address` to the user before sending — resolution is not
// verified on-chain (verified: false).

Error handling

All failures throw BdxRpcError with a protocol code:

Code Meaning Suggested UX
4001 User rejected Neutral UI, no error toast
4100 Not connected Call connect()
4900 Wallet locked "Open your Beldex Wallet to continue"
4901 No wallet created Point to wallet onboarding
4999 Request/approval expired or timed out Safe to retry (reads/undecided approvals)
4998 Send outcome unknown (local timeout) Do NOT resend blindly — use sendTransactionSafe() / getOperationStatus()
-32602 Invalid params Developer bug — check the message
-32603 Internal wallet error Retry later

Helpers: BeldexWeb3.isUserRejection(e), .isLocked(e), .isUnauthorized(e), .isUnknownOutcome(e).

Safe sends (no duplicate payments)

Every send automatically carries an idempotencyKey (or pass your own to make it resumable): retrying with the same key can never create a second approved payment — the wallet (v1.2+) replays the recorded outcome. A local timeout on a send throws 4998 UNKNOWN_OUTCOME (the transaction may still be broadcasting), never "expired":

const r = await bdx.sendTransactionSafe({ to, amount, idempotencyKey: orderId })
if (r.status === 'confirmed') {
  console.log('paid', r.txHash, r.idempotent ? '(recovered, not re-sent)' : '')
} else {
  // 'unresolved' — may still complete; re-run later with the SAME orderId,
  // or poll bdx.getOperationStatus(operationId). Never treat as "not sent".
}

Utilities

toAtomic('1.25')            // 1250000000n  (display BDX → atomic)
fromAtomic(1250000000n)     // '1.25'       (atomic → display BDX)
checkAddress('bx…')         // { valid, kind: 'standard' | 'integrated', reason? }

checkAddress validates shape only (base58, bx prefix, length) — the wallet fully re-validates every address before signing.

What a dapp can and cannot do

Beldex is a private-by-default (Monero-family) chain, so this SDK is payments-oriented, and everything sensitive is permissioned:

  • Balances aren't public — getBalance() works only after the user approves connect(), and is answered by the wallet itself. Your app never sees the view key.
  • Every sendTransaction/signMessage requires fresh in-wallet approval. There are no allowances or auto-approvals.
  • verifyMessage is the one keyless call: pure signature arithmetic, public, no grant.
  • Before connect(), a page can only learn that a wallet exists (getState()).

React

import { BeldexProvider, ConnectButton, useConnect, useBalance, fromAtomic } from 'bdx-web3js/react'

function App() {
  return (
    <BeldexProvider>
      <ConnectButton />
      <Balance />
    </BeldexProvider>
  )
}

function Balance() {
  const { isConnected } = useConnect()
  const { balance } = useBalance({ pollMs: 15000 })
  return isConnected && balance ? <p>{fromAtomic(balance.unlocked)} BDX</p> : null
}

Hooks: useBeldex() (full context incl. the raw BeldexWeb3 client), useConnect(), useBalance({ pollMs }), useSignMessage() (sign(msg) resolves null on user rejection; signing/data/error state). Pass signOnConnect to <BeldexProvider> to run connectWithProof() on connect and expose the result as proof (via context and useConnect()). react >= 18 is an optional peer dependency — plain-JS users install nothing extra. See examples/react-demo.

API surface

detectProvider(opts?) · new BeldexWeb3(provider, opts?) · connect() · connectWithProof(opts?) · disconnect() · getAddress() · getBalance() · sendTransaction(params) · sendTransactionSafe(params, opts?) · getOperationStatus(operationId) · signMessage(msg) · signAuthChallenge(params) · verifyMessage(params) · resolveBns(name) · getNetwork() · getState() · buildAuthChallenge(address, nonce, ts) · on/off/once(event, fn) · address / isConnected getters.

Events: connect, disconnect, accountsChanged, networkChanged, balanceChanged, lock, unlock.

E2E tests

e2e/run.mjs drives the built extension in real Chromium over the real wire protocol: discovery, connect approve/reject, reads, client-side validation, and wallet-side revoke → disconnect event. Run on a desktop machine:

cd ../beldex-wallet-extension && npm run build:chrome
cd ../bdx-web3js && npm run build
npm i -D puppeteer     # not committed: heavyweight Chromium download
npm run e2e            # E2E_HEADED=1 to watch it

Send-path testing requires a funded testnet wallet and a testnet LWS (CONFIG in the extension) — keep real sends manual.

Development

npm install
npm run build       # dist/: ESM + CJS + IIFE + .d.ts
npm test            # vitest (jsdom) against a mock wallet
npm run typecheck

examples/vanilla.html runs against an inline mock wallet — open it after npm run build to try the full connect → balance → send flow with no extension installed. The mock steps aside automatically when the real extension is present.

Status

Protocol v1 implemented end-to-end against the Beldex Wallet extension: connect (optionally with ownership proof), reads, user-approved sends, and message sign/verify (extension v1.1+; SigV1 wallet2::sign scheme). See CHANGELOG.md.

About

A JavaScript/TypeScript SDK for connecting web apps to the Beldex Wallet browser extension — MetaMask-style connect, balances, and user-approved BDX transactions. Zero dependencies, BigInt-safe, React hooks included.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages