Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,35 @@ jobs:
- run: npm install
- run: npm test
- run: npx tsc -b

bot:
runs-on: ubuntu-latest
defaults:
run:
working-directory: bot
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: bot/package-lock.json
- run: npm install
- run: npm test
- run: npm run typecheck

backend-auth-backend:
runs-on: ubuntu-latest
defaults:
run:
working-directory: backend-auth/backend
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: backend-auth/backend/package-lock.json
- run: npm install
- run: npm test
- run: npm run typecheck
52 changes: 37 additions & 15 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# CLAUDE.md - Sphere SDK Connect Example

> **SDK floor:** every package pins `@unicitylabs/sphere-sdk` **0.14.1** exactly. Wallet hosts
> from 0.14.1 enforce an SDK version floor at the Connect handshake (`ConnectHost`'s built-in
> default is `0.14.1-0`, overridable via `ConnectHostConfig.minSdkVersion`): a client on an older
> SDK — or one too old to report a version, i.e. anything before 0.14.1 — is refused with
> `UNSUPPORTED_PROTOCOL_VERSION` (4007) carrying `data.requiredSdk` / `data.actualSdk`. The
> Connect protocol is unchanged at **2.1**.

Demonstration project with four runnable examples of working with a Sphere wallet: a **browser dApp** and a **Node.js dApp** (both use the Connect protocol to drive a user's wallet), a **bot** that runs its own wallet (direct SDK, no Connect), and a **backend-auth** flow (a frontend brokers a wallet signature, a backend verifies it and issues a JWT). The Connect module enables dApps to interact with Sphere wallets through a transport-agnostic, permission-based RPC interface.

## Project Structure
Expand Down Expand Up @@ -55,9 +62,10 @@ sphere-sdk-connect-example/
├── bot/ # Bot that runs its OWN wallet (direct SDK, NOT Connect)
│ ├── src/
│ │ ├── sphere.ts # own-wallet Sphere.init (+ optional wallet-api receive rail)
│ │ ├── sphere.ts # own-wallet Sphere.init (createNodeProviders + createWalletApiProviders)
│ │ ├── index.ts # DM auto-reply, self-mint, money-safe send loop
│ │ ├── coins.ts # symbol→coinId + human→base-unit helpers (unit-tested)
│ │ ├── coins.ts # symbol→coinId + human↔base-unit helpers (unit-tested)
│ │ ├── sendSafety.ts # mayBeCommitted(): never re-send a possibly-committed spend (unit-tested)
│ │ ├── aggregatorKey.ts # env key → saved → SGW auto-provision + persist (unit-tested)
│ │ ├── provisionAggregatorKey.ts, sgwChallenge.ts # SGW challenge/sign/verify
│ │ └── *.test.ts
Expand Down Expand Up @@ -111,7 +119,7 @@ cp .env.example .env
npm start # boots its own wallet on testnet2, self-mints, DM-echoes
```

No Connect — the bot *is* the wallet. Leave `AGGREGATOR_API_KEY` empty and it auto-provisions its own free-plan SGW key on first boot and persists it.
No Connect — the bot *is* the wallet. Leave `AGGREGATOR_API_KEY` empty and it auto-provisions its own free-plan SGW key on first boot and persists it. `WALLET_API_URL` is **required**: sphere-sdk 0.14 deleted own-storage custody (`tokenStorage` / `tokensDir` and both `TokenStorageProvider` implementations are gone), so `Sphere.init` throws `INVALID_CONFIG` without a `walletApi` composition. Keys/identity stay local under `BOT_DATA_DIR`; tokens, history and payment requests live in wallet-api.

### Backend Auth (sign in with a wallet)

Expand All @@ -126,9 +134,9 @@ Frontend brokers a `sign_message`; backend recovers the pubkey via `recoverPubke

## Dependencies

All five packages pin the same published SDK version:
All five packages pin the same published SDK version, exactly (no caret):
```json
"@unicitylabs/sphere-sdk": "0.13.0"
"@unicitylabs/sphere-sdk": "0.14.1"
```

- **Browser / backend-auth frontend:** React 19, Vite 7
Expand Down Expand Up @@ -195,6 +203,12 @@ The browser `tsconfig.json` requires explicit path mappings for connect submodul
| `sphere_subscribe` | Subscribe to events | `events:subscribe` |
| `sphere_unsubscribe` | Unsubscribe | `events:subscribe` |

`RPC_METHODS` has **14** members (the 9 above plus `sphere_disconnect` and the four DM reads:
`sphere_getConversations`, `sphere_getMessages`, `sphere_getDMUnreadCount`, `sphere_markAsRead`).
`PERMISSION_SCOPES` has **13**. The invoice surface (`sphere_getInvoices`,
`sphere_getInvoiceStatus`, the nine invoice intents, `invoice:read` / `invoice:write`) was
**deleted in sphere-sdk 0.14** — it never shipped enabled in any wallet host.

**Intents** (require user approval each time):
| Intent Action | Description | Required Permission |
|--------------|-------------|---------------------|
Expand Down Expand Up @@ -245,12 +259,14 @@ HOST_READY_TIMEOUT = 30_000 // ms
| 4004 | `SESSION_EXPIRED` | Session TTL exceeded |
| 4005 | `ORIGIN_BLOCKED` | Origin not allowed |
| 4006 | `RATE_LIMITED` | Too many requests |
| 4007 | `UNSUPPORTED_PROTOCOL_VERSION` | Connect protocol MAJOR mismatch |
| 4007 | `UNSUPPORTED_PROTOCOL_VERSION` | Connect protocol MAJOR mismatch **or** the client's npm SDK version is below the host's floor (`data.requiredSdk` / `data.actualSdk`) |
| 4008 | `INCOMPATIBLE_NETWORK` | dApp targets a different network than the wallet |
| 4009 | `WALLET_LOCKED` | Wallet locked; the session is still alive |
| 4100 | `INSUFFICIENT_BALANCE` | Not enough tokens |
| 4101 | `INVALID_RECIPIENT` | Bad recipient address |
| 4102 | `TRANSFER_FAILED` | Transfer error |
| 4200 | `INTENT_CANCELLED` | Intent cancelled |
| 4201 | `INTENT_OUTCOME_UNKNOWN` | The wallet took the intent and the answer was lost — **never retry**, reconcile |

## Key Implementation Details

Expand Down Expand Up @@ -299,7 +315,7 @@ Token metadata (symbol, name, decimals, iconUrl) comes from the wallet's TokenRe

### Mock Wallet Server (`nodejs/src/mock-wallet-server.ts`)

- Creates `ConnectHost` with a mock `SphereInstance` (`src/mockSphere.ts`, shared with the tests)
- Creates `ConnectHost` with a mock `SphereInstance` (`src/mockSphere.ts`, shared with the tests). The mock is shaped like a real 0.14 wallet: `payments` is the payments-v2 facade (`assets()` / `tokens()` / paged `history()`) and `paymentsV2` is the deprecated alias `ConnectHost` reads to detect a v2 wallet
- Auto-approves all connection requests with full permissions
- Auto-approves all intents with action-specific success responses
- Returns rich mock data: identity, assets (UCT + USDU with fiat/24h change), tokens (with statuses), history
Expand All @@ -313,16 +329,22 @@ Token metadata (symbol, name, decimals, iconUrl) comes from the wallet's TokenRe
- `wallet:disconnected` — session destroyed; re-handshake to continue
- `identity:changed` — address switch

Subscribable events (via `client.on()`):
- `transfer:incoming` — Received tokens
- `transfer:confirmed` — Outgoing confirmed
- `transfer:failed` — Outgoing failed
Subscribable events (via `client.on()`), using the **sphere-sdk 0.14 names**:
- `transfer:incoming` — Received tokens (unchanged across the flip)
- `transfer:updated` — A transfer advanced; read `status` / `deliveryPending` (replaces `transfer:confirmed`, `transfer:delivery_pending`, `transfer:failed`)
- `transfer:attention` — `{ transferId, code, detail? }` (replaces `split:checkpoint-stuck`, `delivery:undeliverable`, `delivery:deferred`)
- `inventory:updated` — Token inventory changed (replaces the `sync:*` family)
- `history:updated` — A history entry was recorded
- `payment_request:incoming` — Payment request received
- `payment_request:updated` — `{ id, status }` (replaces `payment_request:paid` / `:rejected` / `:expired`)
- `connection:status` — `{ status: 'connected' | 'degraded' | 'offline' }` (replaces `realtime:status` + `storage:degraded`)
- `identity:changed` — Address switch
- `nametag:registered` — Nametag registered
- `nametag:recovered` — Nametag recovered
- `nametag:registered` / `nametag:recovered` — Nametag lifecycle
- `address:activated` — New address tracked
- `sync:provider` — Sync result
- `payment_request:incoming` — Payment request received

Every pre-0.14 name still fires: the host re-emits each one from the new event through a
compatibility adapter, so no dApp subscription silently went dead. New code uses the names above.
`browser/src/components/events/EventLogPanel.tsx` holds the canonical list this repo subscribes to.

## Connect Module Source (in sphere-sdk)

Expand Down
23 changes: 17 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ seeing the private keys**. The dApp runs a `ConnectClient`; the wallet runs a
dApp (ConnectClient) ←→ Transport ←→ Wallet (ConnectHost)
```

> **All five packages require `@unicitylabs/sphere-sdk` ≥ 0.14.1.** Wallet hosts
> from 0.14.1 onward enforce an **SDK version floor at the handshake**: a client
> built on an older SDK is refused with `UNSUPPORTED_PROTOCOL_VERSION` (4007)
> before any approval UI appears, so a dApp that is not bumped simply stops
> connecting. The Connect protocol itself is unchanged (still **2.1**) — this is
> a dependency bump and a rebuild, nothing more. See
> [browser/CONNECT.md](browser/CONNECT.md#-your-dapp-needs-unicitylabssphere-sdk--0141).

Not every example uses Connect: the **bot** runs its own wallet directly, and
the **backend** in backend-auth only verifies a signature. See the guide below.

Expand All @@ -27,7 +35,7 @@ it needs to do** with it.
|---|---|---|---|---|
| **`browser/`** | a web page / dApp | the **user's**, elsewhere | ✅ with per-action approval | your dApp runs in a browser and the user is present to approve each send — e.g. a **browser game** where the player approves in-game purchases |
| **`nodejs/`** | a Node CLI / service | the **user's**, over WebSocket | ✅ with approval | you need a **CLI / desktop / server** dApp that drives a user's wallet over a WebSocket (see the caveat in its README) |
| **`bot/`** | an autonomous agent | **its own** (own keys) | ✅ no approval — it owns the funds | you're building a **tipping bot, faucet, game NPC that pays rewards, or an agent** that acts from its own float with no human in the loop |
| **`bot/`** | an autonomous agent | **its own** (own keys, wallet-api custody) | ✅ no approval — it owns the funds | you're building a **tipping bot, faucet, game NPC that pays rewards, or an agent** that acts from its own float with no human in the loop |
| **`backend-auth/`** | a frontend + backend | the **user's** (frontend only signs) | ❌ **auth only** | you need to know **who** the player is — login, leaderboards, ownership, sessions — but do **not** need to move their tokens. **Recommended for games/apps that need authenticated identity, not custody or intents** |

Rules of thumb:
Expand Down Expand Up @@ -68,10 +76,12 @@ Drives a wallet over `WebSocketTransport`. See [nodejs/README.md](nodejs/README.
```bash
cd bot
npm install
cp .env.example .env
npm start # boots its own wallet on testnet2, self-mints, DM-echoes
cp .env.example .env # WALLET_API_URL is REQUIRED — token custody lives there
npm start # boots its own wallet on testnet2, self-mints, DM-echoes
```
No Connect involved — the bot *is* the wallet. See [bot/README.md](bot/README.md).
No Connect involved — the bot *is* the wallet. Since sphere-sdk 0.14 there is no
own-storage custody: `Sphere.init` throws `INVALID_CONFIG` without a `walletApi`
composition, so `WALLET_API_URL` must be set. See [bot/README.md](bot/README.md).

### `backend-auth/` — sign in with a Sphere wallet

Expand All @@ -85,10 +95,11 @@ Frontend brokers a `sign_message`; backend recovers the pubkey and issues a JWT.

## Dependencies

All five packages pin the same published SDK version:
All five packages pin the same published SDK version, exactly (no caret — these
are examples, and pin clarity matters more than float):

```json
"@unicitylabs/sphere-sdk": "0.13.0"
"@unicitylabs/sphere-sdk": "0.14.1"
```

## Documentation
Expand Down
8 changes: 8 additions & 0 deletions backend-auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ Sphere wallet's signature — not by trusting anything the client claims. A
server independently recovers the signer's public key from that signature
and issues a session JWT.

> **The frontend needs `@unicitylabs/sphere-sdk` ≥ 0.14.1.** Wallet hosts from
> 0.14.1 enforce an SDK version floor at the handshake and refuse older clients
> with `UNSUPPORTED_PROTOCOL_VERSION` (4007) before any approval UI appears —
> so an un-bumped dApp simply stops signing anybody in. `src/errors.ts`
> (`describeVersionFloor`) turns that refusal into copy that names the required
> version. The **backend** is unaffected: it only calls
> `recoverPubkeyFromSignature` / `verifySignedMessage`, which are unchanged.

## Topology

```
Expand Down
Loading
Loading