> For the complete documentation index, see [llms.txt](https://docs.beldex.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.beldex.io/advanced/developer/bdx-web3js.md).

# 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).

### Install

```bash
npm install @bdxi/web3js
```

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

### Quickstart

```ts
import { BeldexWeb3, detectProvider, toAtomic, fromAtomic } from '@bdxi/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):

```ts
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.

```ts
// 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:

```ts
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":

```ts
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

```ts
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 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

```tsx
import { BeldexProvider, ConnectButton, useConnect, useBalance, fromAtomic } from '@bdxi/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:

```bash
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

```bash
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).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.beldex.io/advanced/developer/bdx-web3js.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
