# browser-web3-signer

Sign EVM and TRON transactions and messages from the command line **using your own
browser wallet** (MetaMask, Rabby, TronLink, …). A command opens a local page in your
browser, you approve in your wallet, and the result is printed. **The private key never
leaves the browser** — this tool only routes the request and reads the result back.

It's a Rust reimplementation of the browser-signing capability of `mcp-wallet-signer`,
with a CLI as the interface for agents (no MCP). The core is a library so it can be
embedded from other languages; TypeScript adaptors (viem/ethers) over a managed Rust bridge
subprocess are planned (see [Status](#status)).

## How it works

```
  CLI command ──► local HTTP bridge (127.0.0.1) ──► opens browser tab
                        ▲                                   │
                        │  POST /api/complete/:id           ▼
                  result (addr / tx hash / sig) ◄──── you approve in your wallet
```

Each command starts a tiny localhost-only HTTP server, opens the browser to an approval
page, blocks until you act in your wallet (or a 5-minute timeout), prints the result, and
exits. Nothing binds a public interface; the bridge is `127.0.0.1` only.

## Install

**Prebuilt binaries**: each [GitHub release](https://github.com/nikicat/browser-web3-signer/releases)
ships static binaries for linux x64/arm64 (musl, runs on any distro), macOS x64/arm64, and
windows x64, plus a `SHA256SUMS` file. Download `browser-web3-signer-<target>`, verify, `chmod +x`.

**npm** (for the [TypeScript binding](ts), which spawns the binary for you):

```sh
npm install browser-web3-signer   # pulls the right platform binary as an optionalDependency
```

**From source** — requires a Rust toolchain (pinned to 1.95 via `rust-toolchain.toml`):

```sh
cargo build --release
# binary at target/release/browser-web3-signer
```

## Usage

```sh
browser-web3-signer <evm|tron> <command> [flags]
```

Global flags (any command): `--browser <name>` (open a specific browser instead of the
default), `--print` (print the approval URL but don't open a browser), `--json` (machine-
readable JSON on stdout; human text otherwise). Progress/prompts go to stderr, results to
stdout.

### EVM

```sh
browser-web3-signer evm connect --chain 1
browser-web3-signer evm send-transaction --to 0x… --value 1000000000000000 --chain 1
browser-web3-signer evm sign-message --message "hello"
browser-web3-signer evm sign-typed-data --file ./typed-data.json    # {domain,types,primaryType,message}
```

Built-in chains: Ethereum (1), Sepolia (11155111), Polygon (137), Arbitrum (42161),
Optimism (10), Base (8453), Avalanche (43114), BNB Smart Chain (56). `--value` and the
fee flags are in wei.

### TRON

```sh
browser-web3-signer tron connect
browser-web3-signer tron send-transaction --to T… --amount 1000000          # SUN (1 TRX = 1e6 SUN)
browser-web3-signer tron trigger-contract --contract T… --selector 'transfer(address,uint256)' \
    --params '[{"type":"address","value":"T…"},{"type":"uint256","value":"1"}]'
browser-web3-signer tron sign-message --message "hello"
browser-web3-signer tron deploy-contract --abi-file ./abi.json --bytecode 0x…
```

Networks: `mainnet`, `shasta`, `nile`. Signing and transaction building happen browser-side
in TronLink's `tronWeb`; the Rust side only routes requests.

### Serve (control API for language bindings)

```sh
browser-web3-signer serve --chain evm     # prints the bound port, then blocks
```

Runs the bridge on a stable port for the process lifetime and exposes `POST /api/v1/request`
(body is a request `{type, …}`; opens the wallet, blocks, returns `{success, result}` or
`{success:false, error, code?}`) and `GET /api/v1/health`. A language binding spawns this and
drives the wallet over HTTP — see the [TypeScript binding](ts). Honors the global `--browser` /
`--print` flags for how the approval page opens.

## Configuration (env)

| Variable | Default | Meaning |
| --- | --- | --- |
| `BROWSER_WEB3_EVM_PORT` | `3847` | Preferred bridge port for EVM |
| `BROWSER_WEB3_EVM_CHAIN` | `1` | Default EVM chain id |
| `BROWSER_WEB3_TRON_PORT` | `3848` | Preferred bridge port for TRON |
| `BROWSER_WEB3_TRON_NETWORK` | `mainnet` | Default TRON network |
| `BROWSER` | — | Browser binary to open (else OS default) |
| `BROWSER_WEB3_SIGNER_BIN` | — | Explicit signer-binary path for the language bindings |

The port is *preferred*, not mandatory: if it's already in use (a concurrent command, or a
daemon), the command falls back to an OS-assigned ephemeral port instead of failing.

## Development

```sh
just            # list recipes
just ci         # fmt + taplo + clippy + build + test (what CI runs)
just test
just lint       # clippy -D warnings
just coverage   # cargo-llvm-cov summary
```

`cargo`/`taplo`/`clippy` are pinned and gated by CI and a `prek` pre-commit hook
(`.pre-commit-config.yaml`). See [ARCHITECTURE.md](ARCHITECTURE.md) for the design and the
rationale behind the key decisions.

### Manual real-wallet test (local anvil)

[`scripts/manual-test-evm.sh`](scripts/manual-test-evm.sh) drives your **real** browser wallet
against a throwaway local [anvil](https://book.getfoundry.sh/anvil/) chain: it starts anvil, has
you connect (adding the local network via `--rpc-url`), funds your address with a cheat code, then
walks through sign-message, sign-typed-data, send-transaction, and an ERC-20 transfer — verifying
each result on-chain with `cast`. You only approve each step in the wallet; no testnet, no faucet,
no real funds. Requires foundry (`anvil`/`cast`/`forge`) + `jq` and a `cargo build`.

[`scripts/manual-test-tron.sh`](scripts/manual-test-tron.sh) is the TronLink counterpart, driven by
a throwaway local [`tronbox/tre`](https://hub.docker.com/r/tronbox/tre) node in Docker. TRON has no
`anvil`/`cast`, so a small TronWeb helper ([`scripts/tron/tron-tool.ts`](scripts/tron/tron-tool.ts),
run directly by node ≥ 22.6) is the funding + verification layer: it funds your address from the
node's genesis key, deploys/mints a demo TRC-20, and verifies signatures and receipts. TronLink
can't be pointed at a node from the CLI, so the one-time setup is to add `http://127.0.0.1:9090` as
a custom node in TronLink and select it; the script prints the exact steps. Same five stages,
same "you only approve in the wallet" flow. Requires Docker, node ≥ 22.6, `forge`, `jq`, `cargo build`.

One TRON-specific wrinkle: TronLink only knows a chainId for its built-in networks and never queries
a custom node's, so **TIP-712 typed-data signing fails on a local node** (`"Current chainId cannot be
null"`) until you inject it once via [`scripts/tron/inject-chainid.js`](scripts/tron/inject-chainid.js)
(pasted into TronLink's service-worker console — the script's setup step explains it). The other four
stages don't need it. `DEBUG_RPC=1` logs all wallet→node traffic through a proxy for diagnosis.

## Status

Working today: the one-shot CLI for **EVM and TRON** (connect, send/trigger/deploy,
message + typed-data signing), with an embedded approval UI per chain.

**E2E browser tests**: a Playwright suite drives a mock wallet against the real Rust bridge for
**both EVM and TRON** (connect, sign, send/trigger/deploy, reject, cancel, address mismatch),
testing the full browser interaction flow. Run with `just e2e-setup && just e2e` (one-time
`npm install` + Chromium download, then `just e2e`). CI runs it as a dedicated job on every
push and PR.

**Control API** (`serve`): `browser-web3-signer serve --chain evm|tron` runs the bridge on a
stable port for its lifetime and exposes `POST /api/v1/request` + `GET /api/v1/health`, printing
the bound port to stdout. This is the long-lived mode language bindings spawn and drive.

**TypeScript binding** ([`ts/`](ts)): a `WalletSignerClient` that spawns and supervises the
`serve` subprocess and drives it over `/api/v1`, plus a **viem** transport + hybrid account — so a
TS program signs with the user's browser wallet, and the persistent tab skips the reconnect prompt
across calls. Published to npm as [`browser-web3-signer`](https://www.npmjs.com/package/browser-web3-signer);
the Rust binary ships as per-platform `optionalDependencies`, so `npm install` is all it takes —
no Rust toolchain needed.

**Go binding** ([`go/`](go)): `EVMClient` / `TronClient` — a thin client that spawns and
supervises the `serve` subprocess and drives it over `/api/v1`, covering **both EVM and TRON**.
Every operation takes a `context.Context`; coded errors surface as typed values
(`WrongWalletAddressError`); results are go-ethereum types (`common.Address`, `common.Hash`,
`hexutil.Bytes`) that plug directly into the go-ethereum code most consumers already have.

Persistent sessions in Rust: hold a single `EvmSigner` / `TronSigner` and reuse it — same stable
port, same effect (the pattern the reference's long-lived `WalletSigner` uses; no daemon required).

Deferred: a full multi-client daemon (discovery file, auth, request queue, SSE), warranted only if
several independent processes must share one connected tab. See
[ARCHITECTURE.md](ARCHITECTURE.md#roadmap).

## License

MIT — see [LICENSE](LICENSE).
