Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
3520f48
init
KruGoL Feb 26, 2026
066d2a1
Fix popup styles: move CSS to @layer base, resize to 375×667
KruGoL Feb 26, 2026
e748b36
Fix TopUp faucet: use correct API endpoint and POST method
KruGoL Feb 26, 2026
c246a97
configure TokenRegistry
KruGoL Feb 26, 2026
fa49d4d
docs: add CLAUDE.md with architecture and dev commands
KruGoL Feb 27, 2026
e6003ca
feat: integrate Connect protocol via ConnectHost with dApp approval a…
KruGoL Feb 27, 2026
3d9e7a1
feat(connect): connected sites settings, silent auto-approve, onDisco…
KruGoL Feb 28, 2026
0b46ca0
docs: add CONNECT.md integration guide, update README and CLAUDE with…
KruGoL Feb 28, 2026
1fe52de
chore: remove helia overrides (no longer needed after sdk cleanup)
KruGoL Feb 28, 2026
e221e8b
feat: port full Connect Protocol wallet UI from sphere
KruGoL Feb 28, 2026
ea9df29
feat: replace Wallet icon with Union.svg logo in extension UI and man…
KruGoL Feb 28, 2026
7b34401
chore: switch to npm SDK ^0.5.4, fix tsconfig paths, add @types/node,…
KruGoL Feb 28, 2026
bdaf38d
feat: add vested/unvested coin selector to L1 send intent modal
KruGoL Feb 28, 2026
d901f0b
chore: sync manifest version from package.json at build time
KruGoL Feb 28, 2026
a3f4d75
style: remove icon background square and increase icon size in wallet…
KruGoL Feb 28, 2026
b94d704
chore: add ESLint, fix all type errors, add CI/release workflows
KruGoL Feb 28, 2026
4f6a31d
chore: add eslint, ci/release workflows, remove dead code and unused …
KruGoL Feb 28, 2026
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
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: TypeScript check & build
run: npm run build
64 changes: 64 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Release
run-name: Release v${{ inputs.version }}

on:
workflow_dispatch:
inputs:
version:
description: 'Version (e.g., 0.2.0, 0.2.1-beta.1)'
required: true
type: string
release_notes:
description: 'Release notes / changelog (leave empty for auto-generated)'
required: false
type: string

jobs:
build-and-release:
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Validate version format
run: |
if ! echo "${{ inputs.version }}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'; then
echo "Error: Invalid version format. Use semver (e.g., 0.2.0, 0.2.1-beta.1)"
exit 1
fi

- name: Update package version
run: npm version ${{ inputs.version }} --no-git-tag-version

- name: Install dependencies
run: npm ci

- name: Build & package
run: npm run package

- name: Commit version bump and push tag
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add package.json package-lock.json
git commit -m "chore: release v${{ inputs.version }}"
git tag "v${{ inputs.version }}"
git push origin HEAD --tags

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
name: Sphere Wallet v${{ inputs.version }}
tag_name: v${{ inputs.version }}
files: sphere-wallet-v${{ inputs.version }}.zip
body: ${{ inputs.release_notes }}
generate_release_notes: ${{ inputs.release_notes == '' }}
196 changes: 196 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
# CLAUDE.md — Sphere Extension

Chrome extension wallet for Unicity Protocol. Branch: `rn-wallet`. Version: 0.2.0.

## Dev Commands

```bash
npm run dev # Build + Vite watch (for development)
npm run build # tsc --noEmit + full build
npm run build:fast # Skip type check, just build
npm run lint # tsc --noEmit only
npm run package # Build + create .zip for distribution
```

After build: load `dist/` folder in Chrome → chrome://extensions → Developer mode → Load unpacked.

## Architecture

```
src/
components/ # React UI components
ui/ # Button, BaseModal, AlertMessage, EmptyState, MenuButton, ModalHeader
wallet/
modals/ # SendModal, PaymentRequestsModal, TransactionHistoryModal,
# SettingsModal, BackupWalletModal, LogoutConfirmModal,
# TopUpModal, SwapModal, SeedPhraseModal, LookupModal, SaveWalletModal
onboarding/ # CreateWalletFlow + 7 screens
shared/ # AddressSelector, AssetRow, TokenRow, RegisterNametagModal, etc.
platform/extension/
background/ # Service worker (wallet-manager.ts — ALL SDK calls here)
content/ # Content script (bridges web page ↔ background)
inject/ # window.sphere API exposed to web pages
popup/ # PopupApp.tsx — main entry
sdk/ # React adapter (context.ts, queryKeys.ts, hooks/)
shared/ # types.ts, constants.ts, messages.ts
```

## Critical Architecture Rules

### SDK calls ONLY in background/wallet-manager.ts
The popup UI never calls SDK directly. All SDK operations go through:
```
Popup component → chrome.runtime.sendMessage({ type: 'POPUP_*' }) → wallet-manager.ts → SDK
```

### Message Types
- `POPUP_*` — Popup UI → Background service worker (30+ types)
- `SPHERE_*` — Web page → Extension (via inject → content → background)

### Chrome Storage
All wallet data in `chrome.storage.local`:
- `encryptedMnemonic` — AES-GCM encrypted (PBKDF2, 100k iterations)
- `aggregatorConfig`, `nametag`, `pendingTransactions`, `preferences`

### NO direct IndexedDB access in popup
SDK's IndexedDB (sphere-storage, sphere-token-storage-*) is managed by background service worker via wallet-manager.ts

## SDK Usage (background/wallet-manager.ts)
```typescript
import { Sphere } from '@unicitylabs/sphere-sdk'
import { createBrowserProviders } from '@unicitylabs/sphere-sdk/impl/browser'

const providers = await createBrowserProviders({ network: 'testnet' })
const { sphere } = await Sphere.init({ ...providers, l1: {} })

// Key calls:
sphere.identity // WalletIdentity
sphere.payments.getTokens() // Token[]
sphere.payments.getAssets() // Asset[]
sphere.payments.send({ recipient, amount, coinId, memo })
sphere.payments.l1.getBalance() // L1Balance
sphere.payments.l1.send({ to, amount })
sphere.communications.sendDM(to, content)
sphere.registerNametag(name)
sphere.resolve(identifier) // @nametag, DIRECT://, alpha1...
sphere.destroy()
```

## Key Types (src/shared/types.ts)
```typescript
interface WalletState { hasWallet: boolean; isUnlocked: boolean; activeIdentityId: string | null }
interface TokenBalance { coinId, symbol, amount, pendingAmount? }
interface PendingTransaction { requestId, type: 'send'|'sign_message'|'sign_nostr', origin, tabId, data }
interface SendTransactionData { recipient, coinId, amount, message? }
```

## Supported Tokens (src/shared/constants.ts)
UCT (18), USDU (6), EURU (6), SOL (9), BTC (8), ETH (18), ALPHT (8), USDT (6), USDC (6)

Gateway URL: `https://goggregator-test.unicity.network` (testnet)

## Path Aliases (vite.config.ts)
```
@/shared → src/shared
@/sdk → src/sdk
@/components → src/components
@/platform → src/platform
```

## Build Output
- `dist/popup.html` + assets — popup UI
- `dist/background.js` — service worker
- `dist/content.js` — content script
- `dist/inject.js` — injected script (window.sphere)

## Connect Protocol (Sphere Connect — dApp integration)

The extension implements the Sphere Connect protocol on top of the SDK's `ConnectHost`. See [`CONNECT.md`](./CONNECT.md) for the full integration guide.

### Key Files

| File | Role |
|------|------|
| `src/platform/extension/background/connect-host.ts` | ConnectHost lifecycle, approved origins, approval/intent queues |
| `src/platform/extension/background/message-handler.ts` | Routes POPUP_* messages from popup to connect-host |
| `src/platform/extension/background/wallet-manager.ts` | Calls `initConnectHost()` after unlock, `destroyConnectHost()` on lock |
| `src/platform/extension/content/index.ts` | Relays `sphere-connect-ext` messages between page ↔ background |
| `src/components/wallet/modals/ConnectApprovalModal.tsx` | UI for first-time dApp connection approval |
| `src/components/wallet/modals/ConnectIntentModal.tsx` | UI for generic intent approval |
| `src/components/wallet/modals/ConnectedSitesModal.tsx` | Settings → Connected Sites (list + revoke) |

### Approved Origins Storage

```typescript
interface ApprovedOriginEntry {
permissions: PermissionScope[];
connectedAt: number; // first approval timestamp
lastSeenAt: number; // last successful silent-connect timestamp
dapp: DAppMetadata; // name, description, url, iconUrl?
}
// chrome.storage.local key: 'sphere_approved_origins'
```

### POPUP_* Messages for Connect Protocol

```typescript
// Poll for pending connection approval
{ type: 'POPUP_GET_CONNECT_APPROVAL' }
// → null | { id, dapp, requestedPermissions }

// Resolve approval (user clicked Connect or Reject)
{ type: 'POPUP_RESOLVE_CONNECT_APPROVAL', id, approved: true, grantedPermissions }

// Poll for pending intent
{ type: 'POPUP_GET_CONNECT_INTENT' }
// → null | { id, action, params, session }

// Resolve intent
{ type: 'POPUP_RESOLVE_CONNECT_INTENT', id, result: { result: {...} } | { error: {...} } }

// Get connected sites (for ConnectedSitesModal)
{ type: 'POPUP_GET_CONNECTED_SITES' }
// → { success: true, sites: Record<string, ApprovedOriginEntry> }

// Revoke a site
{ type: 'POPUP_REVOKE_CONNECTED_SITE', origin: 'https://...' }
// → { success: true }
```

### Silent Mode (auto-connect)

When `silent=true` arrives in handshake:
- Origin in approved storage → approve immediately (update `lastSeenAt`)
- Origin NOT in storage → reject immediately (no popup, no window)

### onDisconnect

When dApp calls `client.disconnect()` → SDK fires `onDisconnect(session)` → extension calls `revokeConnectedSite(origin)` → origin removed from storage → next silent-check fails → Connect button shown.

### Content Script Relay

```
Page: window.postMessage({ type: 'sphere-connect-ext:tohost', ... })
→ content script: chrome.runtime.sendMessage(envelope)
→ background: ConnectHost handles it

Background: chrome.tabs.sendMessage(tabId, { type: 'sphere-connect-ext:toclient', ... })
→ content script: window.postMessage(message, '*')
→ page: ExtensionTransport.forClient() receives it
```

---

## window.sphere API (exposed to web pages via inject/index.ts)
```typescript
window.sphere.isInstalled(): boolean
window.sphere.connect(): Promise<IdentityInfo>
window.sphere.getBalances(): Promise<TokenBalance[]>
window.sphere.sendTokens({ recipient, coinId, amount, message? }): Promise<{ transactionId }>
window.sphere.signMessage(message): Promise<string>
window.sphere.getNostrPublicKey(): Promise<{ hex, npub }>
window.sphere.nip44.encrypt(recipientPubkey, plaintext): Promise<string>
window.sphere.nip44.decrypt(senderPubkey, ciphertext): Promise<string>
window.sphere.getMyNametag(): Promise<{ name, proxyAddress } | null>
window.sphere.resolveNametag(nametag): Promise<NametagResolution | null>
```
Loading