SPB Git

spb/os-vault

Public

Self-custody, multi-chain crypto wallet for macOS. One recovery phrase, six chain families, zero API keys — nothing leaves your Mac.

OS Vault 1.0.0 — self-custody multi-chain wallet for macOS

One BIP-39 phrase drives six chain families: 11 EVM chains, Bitcoin
(BIP-84 via bdk-swift), Solana (SPL), Tron (TRC-20 via vendored Trust
wallet-core), XRP Ledger (RLUSD trustlines) and TON (jettons). Keys are
sealed in OS Vault's own PBKDF2→AES-GCM vault (no Keychain); every send
re-derives and discards the key. All endpoints keyless with failover;
32 unit tests incl. BIP-39/BIP-84 vectors and cross-stack derivation
checks; signed, notarized, stapled release pipeline with DMG.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Aug 6, 2026) Browse files

142 changed files +20,001 −0

142 changed files
added .gitignore +14 −0
@@ -0,0 +1,14 @@
1+# Build products
2+.build/
3+dist/
4+*.xcodeproj
5+
6+# Vendored binary too large for git (270 MB) — regenerate with
7+# scripts/vendor-walletcore.sh
8+vendor/WalletCoreSPM/WalletCoreCommon.xcframework/
9+
10+# macOS
11+.DS_Store
12+
13+# Local state
14+*.sqlite
added CLAUDE.md +157 −0
@@ -0,0 +1,157 @@
1+# CLAUDE.md — Zyquo Wallet
2+
3+## Project Overview
4+
5+**Zyquo Wallet** is a native **macOS** self-custody stablecoin wallet built in **Swift + SwiftUI**. It manages **real on-chain USDC** on **Base** (Ethereum L2 by Coinbase — chosen for sub-cent gas fees and native Circle-issued USDC).
6+
7+This is NOT a mock/demo app. Every feature must work against a real blockchain:
8+1. Real HD wallet creation (BIP-39 mnemonic → secp256k1 keys)
9+2. Receive USDC (real address + QR code)
10+3. Send USDC (real signed ERC-20 transfers broadcast via JSON-RPC)
11+
12+**Development rule: build and test everything on Base Sepolia testnet first. Mainnet support is enabled last, behind an explicit network switch.**
13+
14+---
15+
16+## Tech Stack
17+
18+| Layer | Choice |
19+|---|---|
20+| Language | Swift 5.10+ (strict concurrency where possible) |
21+| UI | SwiftUI, macOS 14+ target |
22+| Project | Xcode project, dependencies via Swift Package Manager only |
23+| Web3 | `web3swift` (https://github.com/web3swift-team/web3swift) — actively maintained, macOS support, BIP-39/BIP-32, ERC-20, EIP-1559 |
24+| QR codes | CoreImage (`CIQRCodeGenerator`) — no extra dependency |
25+| Key storage | macOS Keychain (Security framework) |
26+| Persistence | UserDefaults for settings; no database needed for v1 (history read from chain/explorer API) |
27+
28+If `web3swift` causes build issues on macOS, fallback is `argentlabs/web3.swift`. Never mix both.
29+
30+---
31+
32+## Network Configuration (source of truth)
33+
34+```swift
35+enum Network {
36+ case baseSepolia // DEFAULT during development
37+ case baseMainnet
38+}
39+```
40+
41+| | Base Sepolia (testnet) | Base Mainnet |
42+|---|---|---|
43+| Chain ID | 84532 | 8453 |
44+| RPC (dev) | https://sepolia.base.org | https://mainnet.base.org |
45+| USDC contract | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` |
46+| Explorer | https://sepolia.basescan.org | https://basescan.org |
47+| Faucets | Circle faucet (https://faucet.circle.com — 20 USDC / 2h) + Base Sepolia ETH faucet for gas | — |
48+
49+- **USDC has 6 decimals.** Always convert with `BigUInt` / decimal math — never `Double`. 1 USDC = 1_000_000 base units.
50+- Public RPCs are fine for dev. For production, make the RPC URL configurable (Alchemy/QuickNode) via Settings — but never require an API key to run the app.
51+- Gas is paid in **ETH**, not USDC. The UI must clearly warn when the ETH balance is too low to send.
52+
53+---
54+
55+## Architecture
56+
57+MVVM + service layer. Keep views dumb; all blockchain logic lives in services.
58+
59+```
60+ZyquoWallet/
61+├── App/ ZyquoWalletApp.swift, AppState (ObservableObject / @Observable)
62+├── Models/ Wallet, Transaction, Network, TokenAmount
63+├── Services/
64+│ ├── KeyManager.swift // mnemonic gen/import, keystore, Keychain I/O
65+│ ├── WalletService.swift // address derivation, wallet lifecycle
66+│ ├── RPCService.swift // web3 provider, network switching
67+│ ├── BalanceService.swift // ETH + USDC balances, polling every 15s
68+│ └── TransactionService.swift// build, estimate gas, sign, send, track receipt
69+├── ViewModels/ OnboardingVM, HomeVM, SendVM, ReceiveVM, SettingsVM
70+├── Views/
71+│ ├── Onboarding/ Welcome, CreateWallet (show + verify mnemonic), ImportWallet
72+│ ├── Home/ Balance card, tx list, Send/Receive buttons
73+│ ├── Send/ recipient + amount + gas preview + confirm sheet
74+│ ├── Receive/ address display, QR, copy button
75+│ └── Settings/ network switch, RPC override, export seed (auth-gated), reset
76+└── Utils/ formatting, address validation (EIP-55), errors
77+```
78+
79+- Use `async/await` everywhere; wrap web3swift callbacks if needed.
80+- One `AppState` injected via `.environment`; services are protocol-backed for testability.
81+- Every service method that touches the network returns typed errors (`WalletError` enum), never `fatalError`.
82+
83+---
84+
85+## Security Requirements (non-negotiable)
86+
87+1. **Mnemonic & private key never leave the device.** No analytics, no telemetry, no network calls except JSON-RPC and explorer API.
88+2. Store the mnemonic (or derived keystore) in **Keychain** with `kSecAttrAccessibleWhenUnlockedThisDeviceOnly`. Never in UserDefaults, files, or iCloud.
89+3. Gate seed export and every send behind **LocalAuthentication** (Touch ID / password).
90+4. **Never log** mnemonics, private keys, or signatures. Never put them in error messages.
91+5. Never hardcode keys, mnemonics, or API keys anywhere in the repo — including tests. Test wallets are generated at runtime.
92+6. Onboarding must force a **mnemonic backup verification** step (user re-enters 3 random words) before showing the wallet.
93+7. Validate recipient addresses: hex format + EIP-55 checksum. Warn (don't block) on all-lowercase addresses.
94+8. Enable **App Sandbox** (with network client entitlement) and **Hardened Runtime**.
95+9. Confirm screen before send must show: recipient, amount, network, estimated gas, and total — no one-tap sends.
96+10. If the clipboard is used for addresses, never auto-paste into the recipient field without user action (clipboard-hijack protection).
97+
98+---
99+
100+## Feature Phases (implement in order, each must compile + pass tests)
101+
102+### Phase 1 — Wallet core
103+- Generate 12-word BIP-39 mnemonic, derive account at `m/44'/60'/0'/0/0`
104+- Import from mnemonic
105+- Persist via KeyManager/Keychain; app relaunch restores wallet
106+- Onboarding flow with backup verification
107+
108+### Phase 2 — Receive
109+- Show checksummed address, copy button, QR code (address only, no `ethereum:` URI needed for v1)
110+
111+### Phase 3 — Balances
112+- ETH balance (`eth_getBalance`) + USDC balance (`balanceOf(address)`)
113+- Poll every 15s while app is active; manual refresh; loading/error states
114+
115+### Phase 4 — Send USDC
116+- ERC-20 `transfer(to, amount)` with EIP-1559 fees
117+- Gas estimation shown in ETH + rough USD
118+- Sign locally, broadcast, then poll `eth_getTransactionReceipt` until confirmed
119+- Pending/confirmed/failed states surfaced in UI, link to BaseScan
120+
121+### Phase 5 — History & polish
122+- Transaction list from BaseScan API (USDC transfers in/out) with graceful fallback if API unavailable
123+- Network switcher (Sepolia ⇄ Mainnet) with a prominent "TESTNET" badge on Sepolia
124+- Settings: custom RPC, export seed (auth-gated), delete wallet (typed confirmation)
125+
126+---
127+
128+## Build & Test Commands
129+
130+```bash
131+# Build
132+xcodebuild -scheme ZyquoWallet -destination 'platform=macOS' build
133+
134+# Tests
135+xcodebuild -scheme ZyquoWallet -destination 'platform=macOS' test
136+```
137+
138+- Unit-test: mnemonic → address derivation (against known BIP-39 test vectors), USDC decimal conversion, address validation, amount formatting.
139+- Never write tests that send real mainnet funds. Network-dependent tests run against Base Sepolia and must be skippable offline.
140+
141+## Manual test checklist (Base Sepolia)
142+1. Create wallet → relaunch app → same address restored
143+2. Fund via Circle faucet (USDC) + Base faucet (ETH for gas)
144+3. Balance appears within one polling cycle
145+4. Send 1 USDC to a second test wallet → confirm on sepolia.basescan.org
146+5. Import that second wallet from mnemonic → balance matches
147+
148+---
149+
150+## Conventions & Pitfalls
151+
152+- All amounts are `BigUInt` in base units internally; convert to display strings only at the UI edge.
153+- USDC = 6 decimals, ETH = 18 decimals. Mixing these up is the #1 bug source.
154+- Chain ID must match the selected network in every signed transaction (replay protection).
155+- Handle RPC failures with retry + user-visible error; the app must never crash on network errors.
156+- Comments and UI strings in English; French localization can come later via String Catalog.
157+- Small, focused commits per phase. Do not start a later phase before the earlier one builds and passes tests.
added Makefile +60 −0
@@ -0,0 +1,60 @@
1+#
2+# Makefile
3+# OS Vault
4+#
5+# Author: Simon-Pierre Boucher
6+# Mail: contact@spboucher.ai
7+#
8+
9+APP_NAME := OS Vault
10+EXEC_NAME := OSVault
11+BUNDLE_ID := com.zyquo.osvault
12+VERSION := 1.0.0
13+BUILD_DIR := .build
14+DIST_DIR := dist
15+APP_BUNDLE := $(DIST_DIR)/$(APP_NAME).app
16+SIGN_IDENTITY := Developer ID Application: Simon-Pierre Boucher (3YM54G49SN)
17+NOTARY_PROFILE := MacLustr-Notarize
18+
19+.PHONY: build release-build test clean dev bundle-debug bundle-release run release
20+
21+build:
22+ swift build
23+
24+release-build:
25+ swift build -c release
26+
27+test:
28+ swift test
29+
30+clean:
31+ swift package clean
32+ rm -rf $(DIST_DIR)
33+
34+# ── App bundle assembly ──────────────────────────────────────────────────────
35+# $(1) = built products dir (.build/debug or .build/release)
36+define assemble_bundle
37+ rm -rf "$(APP_BUNDLE)"
38+ mkdir -p "$(APP_BUNDLE)/Contents/MacOS" "$(APP_BUNDLE)/Contents/Resources"
39+ cp "$(1)/$(EXEC_NAME)" "$(APP_BUNDLE)/Contents/MacOS/$(EXEC_NAME)"
40+ cp Support/Info.plist "$(APP_BUNDLE)/Contents/Info.plist"
41+ printf 'APPL????' > "$(APP_BUNDLE)/Contents/PkgInfo"
42+ [ -f assets/icon/AppIcon.icns ] && cp assets/icon/AppIcon.icns "$(APP_BUNDLE)/Contents/Resources/AppIcon.icns" || true
43+endef
44+
45+bundle-debug: build
46+ $(call assemble_bundle,$(BUILD_DIR)/debug)
47+ codesign --force --deep --entitlements Support/entitlements.plist --sign - "$(APP_BUNDLE)"
48+
49+bundle-release: release-build
50+ $(call assemble_bundle,$(BUILD_DIR)/release)
51+
52+# Debug bundle, ad-hoc signed, launched.
53+dev: bundle-debug
54+ open "$(APP_BUNDLE)"
55+
56+run: dev
57+
58+# Signed, notarized, stapled release: scripts/release.sh
59+release:
60+ scripts/release.sh
added Package.resolved +78 −0
@@ -0,0 +1,78 @@
1+{
2+ "originHash" : "a8d3a77901f33178ea03447308323e177a207103fb42a71c5911e627b913f154",
3+ "pins" : [
4+ {
5+ "identity" : "bdk-swift",
6+ "kind" : "remoteSourceControl",
7+ "location" : "https://github.com/bitcoindevkit/bdk-swift",
8+ "state" : {
9+ "revision" : "5bc9c3cddf203f6aa147d77364782bf095e4f84e",
10+ "version" : "3.0.0"
11+ }
12+ },
13+ {
14+ "identity" : "bigint",
15+ "kind" : "remoteSourceControl",
16+ "location" : "https://github.com/attaswift/BigInt.git",
17+ "state" : {
18+ "revision" : "793a7fac0bfc318e85994bf6900652e827aef33e",
19+ "version" : "5.4.1"
20+ }
21+ },
22+ {
23+ "identity" : "cryptoswift",
24+ "kind" : "remoteSourceControl",
25+ "location" : "https://github.com/krzyzanowskim/CryptoSwift.git",
26+ "state" : {
27+ "revision" : "039f56c5d7960f277087a0be51f5eb04ed0ec073",
28+ "version" : "1.5.1"
29+ }
30+ },
31+ {
32+ "identity" : "secp256k1.swift",
33+ "kind" : "remoteSourceControl",
34+ "location" : "https://github.com/GigaBitcoin/secp256k1.swift",
35+ "state" : {
36+ "revision" : "48fb20fce4ca3aad89180448a127d5bc16f0e44c",
37+ "version" : "0.10.0"
38+ }
39+ },
40+ {
41+ "identity" : "swift-protobuf",
42+ "kind" : "remoteSourceControl",
43+ "location" : "https://github.com/apple/swift-protobuf.git",
44+ "state" : {
45+ "revision" : "55d7a1cc5666b85c13464aea1c4b4a90feccb4c8",
46+ "version" : "1.38.1"
47+ }
48+ },
49+ {
50+ "identity" : "task-retrying-swift",
51+ "kind" : "remoteSourceControl",
52+ "location" : "https://github.com/bigearsenal/task-retrying-swift.git",
53+ "state" : {
54+ "revision" : "208f1e8dfa93022a7d39ab5b334d5f43a934d4b1",
55+ "version" : "2.0.0"
56+ }
57+ },
58+ {
59+ "identity" : "tweetnacl-swiftwrap",
60+ "kind" : "remoteSourceControl",
61+ "location" : "https://github.com/bitmark-inc/tweetnacl-swiftwrap.git",
62+ "state" : {
63+ "revision" : "f8fd111642bf2336b11ef9ea828510693106e954",
64+ "version" : "1.1.0"
65+ }
66+ },
67+ {
68+ "identity" : "web3swift",
69+ "kind" : "remoteSourceControl",
70+ "location" : "https://github.com/web3swift-team/web3swift.git",
71+ "state" : {
72+ "revision" : "b9c771bf3b94983d2f9b22096b4a267a72c16006",
73+ "version" : "3.3.2"
74+ }
75+ }
76+ ],
77+ "version" : 3
78+}
added Package.swift +47 −0
@@ -0,0 +1,47 @@
1+// swift-tools-version:5.10
2+//
3+// Package.swift
4+// OS Vault
5+//
6+// Author: Simon-Pierre Boucher
7+// Mail: contact@spboucher.ai
8+//
9+
10+import PackageDescription
11+
12+let package = Package(
13+ name: "OSVault",
14+ platforms: [
15+ .macOS(.v14)
16+ ],
17+ dependencies: [
18+ .package(url: "https://github.com/web3swift-team/web3swift.git", from: "3.3.2"),
19+ .package(url: "https://github.com/attaswift/BigInt.git", from: "5.3.0"),
20+ .package(url: "https://github.com/bitcoindevkit/bdk-swift", exact: "3.0.0"),
21+ .package(path: "vendor/solana-swift"),
22+ .package(path: "vendor/WalletCoreSPM")
23+ ],
24+ targets: [
25+ .target(
26+ name: "OSVaultKit",
27+ dependencies: [
28+ .product(name: "web3swift", package: "web3swift"),
29+ .product(name: "BigInt", package: "BigInt"),
30+ .product(name: "BitcoinDevKit", package: "bdk-swift"),
31+ .product(name: "SolanaSwift", package: "solana-swift"),
32+ .product(name: "WalletCore", package: "WalletCoreSPM")
33+ ],
34+ path: "Sources/OSVaultKit"
35+ ),
36+ .executableTarget(
37+ name: "OSVault",
38+ dependencies: ["OSVaultKit"],
39+ path: "Sources/OSVault"
40+ ),
41+ .testTarget(
42+ name: "OSVaultTests",
43+ dependencies: ["OSVaultKit"],
44+ path: "Tests/OSVaultTests"
45+ )
46+ ]
47+)
added README.md +154 −0
@@ -0,0 +1,154 @@
1+<div align="center">
2+
3+<img src="assets/icon/os-vault.svg" width="128" alt="OS Vault icon"/>
4+
5+# OS Vault
6+
7+**Self-custody, multi-chain crypto wallet for macOS.**
8+One recovery phrase. Six chain families. Zero API keys. Nothing ever leaves your Mac.
9+
10+[![Platform](https://img.shields.io/badge/platform-macOS%2015.5%2B-111?logo=apple&logoColor=white)](#requirements)
11+[![Swift](https://img.shields.io/badge/Swift-6-F05138?logo=swift&logoColor=white)](#build)
12+[![SwiftUI](https://img.shields.io/badge/UI-SwiftUI-0A84FF)](#architecture)
13+[![Tests](https://img.shields.io/badge/tests-32%20passing-2EA043?logo=github)](#tests)
14+[![Release](https://img.shields.io/badge/release-v1.0.0-blueviolet)](../../releases)
15+[![Notarized](https://img.shields.io/badge/Apple-notarized%20%2B%20stapled-000?logo=apple&logoColor=white)](#release-pipeline)
16+[![Keyless](https://img.shields.io/badge/API%20keys-zero-2EA043)](#infrastructure--no-api-keys)
17+[![Self--custody](https://img.shields.io/badge/custody-yours%20only-orange)](#security-model)
18+
19+[![Bitcoin](https://img.shields.io/badge/Bitcoin-BIP--84-F7931A?logo=bitcoin&logoColor=white)](#chains)
20+[![Ethereum](https://img.shields.io/badge/EVM-11%20chains-3C3C3D?logo=ethereum&logoColor=white)](#chains)
21+[![Solana](https://img.shields.io/badge/Solana-SPL-9945FF?logo=solana&logoColor=white)](#chains)
22+[![Tron](https://img.shields.io/badge/Tron-TRC--20-EB0029)](#chains)
23+[![XRPL](https://img.shields.io/badge/XRPL-RLUSD-25A768?logo=xrp&logoColor=white)](#chains)
24+[![TON](https://img.shields.io/badge/TON-jettons-0098EA?logo=ton&logoColor=white)](#chains)
25+
26+</div>
27+
28+---
29+
30+## Why OS Vault
31+
32+Most wallets make you choose: convenience (custodial, tracked, keyed APIs) or
33+sovereignty (CLI tools, manual everything). OS Vault refuses the trade:
34+
35+- 🔐 **Your keys, your Mac, your encryption.** The BIP-39 mnemonic is sealed
36+ with OS Vault's **own vault format** — PBKDF2-HMAC-SHA512 (600k rounds) →
37+ AES-256-GCM — in a local file. No macOS Keychain, no iCloud, no telemetry.
38+- 🌐 **One phrase, every chain.** The same 12 words derive Bitcoin (BIP-84),
39+ 11 EVM chains, Solana, Tron, XRPL and TON — cross-validated against
40+ independent crypto stacks in the test suite.
41+- 🗝️ **Zero mandatory API keys.** Every endpoint is public and keyless, with
42+ automatic failover. The only egress is blockchain RPC (+ optional CoinGecko
43+ prices, one toggle to kill).
44+- ✍️ **Sign-and-forget.** The private key exists only for the milliseconds a
45+ transaction is being signed — every send re-derives it from your password
46+ and discards it.
47+
48+## Chains
49+
50+| Family | Assets | Fees handled | Testnet default |
51+|---|---|---|---|
52+| **EVM × 11** — Ethereum, Base, Arbitrum, OP, Polygon, BNB, Avalanche, Gnosis, Linea, Scroll (+ Base Sepolia) | USDC, USDC.e, USDT, DAI, USDS, EURC + native coin | EIP-1559, BSC zero-base-fee, **OP-stack/Scroll L1 data fee via oracle**, Arbitrum inclusive estimates, Linea pinned base | Base Sepolia |
53+| **Bitcoin** | BTC (native SegWit `bc1q…`) | sat/vB presets live from mempool.space, RBF on | Signet |
54+| **Solana** | SOL + USDC (SPL) | ATA rent surfaced when the recipient has no token account | Devnet |
55+| **Tron** | TRX + USDT (TRC-20) | **Energy burn estimated pre-send** (~13–27 TRX), fee_limit capped | Nile |
56+| **XRP Ledger** | XRP + RLUSD | Reserves shown as locked; **one-tap RLUSD trustline**; recipient trustline checked | Testnet |
57+| **TON** | TON + USDT (jetton) | Jetton-wallet indirection handled; ~0.07 TON attached, excess refunded | Testnet |
58+
59+Every stablecoin contract address and decimal count was **verified live
60+on-chain** before registration — including the traps: BNB-peg USDT/USDC are
61+18 decimals, bridged USDC.e is indistinguishable from native USDC by
62+`symbol()` alone, DAI is 18 while USDC is 6. See
63+[`docs/STABLECOINS.md`](docs/STABLECOINS.md).
64+
65+## Security model
66+
67+```
68+password ──▶ PBKDF2-HMAC-SHA512 (600k) ──▶ AES-256-GCM ──▶ vault.json (0600)
69+ ▲
70+ wrong password / tampering ─┘ indistinguishable (GCM auth)
71+
72+unlock ──▶ public addresses only stay in memory
73+send ──▶ password → derive key → sign → discard (every single time)
74+```
75+
76+- Forced written-backup verification (3 random words) before the wallet exists
77+- Confirm screen with recipient, amount, network, worst-case fees — always
78+- EIP-55 checksums; per-chain address validation; TESTNET badges everywhere
79+- App Sandbox + Hardened Runtime; signed, **notarized and stapled** by Apple
80+- The watch-only Bitcoin wallet holds public descriptors only; a throwaway
81+ in-memory signer signs PSBTs
82+
83+## Infrastructure — no API keys
84+
85+| Concern | Source (keyless) | Fallback |
86+|---|---|---|
87+| EVM RPC | PublicNode (11/11 verified) | official chain RPCs, health-scored failover |
88+| Bitcoin | mempool.space Esplora | blockstream.info |
89+| Solana | PublicNode | api.mainnet-beta.solana.com |
90+| Tron | TronGrid (anonymous, backoff) | — |
91+| XRPL | xrplcluster.com | s1.ripple.com |
92+| TON | toncenter v2/v3 (1 req/s, throttled client-side) | — |
93+| Prices | CoinGecko batched (USD/CAD/EUR), stale-while-revalidate, **off switch** | DefiLlama |
94+
95+## Build
96+
97+Pure SwiftPM — no `.xcodeproj`, no CocoaPods, no manual steps:
98+
99+```sh
100+scripts/vendor-walletcore.sh # one-time: vendors Trust wallet-core for macOS (~270 MB, not in git)
101+swift build # compile
102+swift test # 32 tests: BIP-39/BIP-84 vectors, vault crypto, decimals, validators
103+make dev # debug bundle, ad-hoc signed, launched
104+make release # Developer ID + notarize + staple + DMG with volume icon
105+```
106+
107+The wallet-core vendoring is this repo's party trick: upstream ships no macOS
108+SwiftPM support, so the script repackages the official CocoaPods tarball as a
109+local `binaryTarget` — including a surgical `ld -r` pass that demotes the
110+duplicate Rust runtime symbol it shares with the Bitcoin Dev Kit. Details in
111+[`docs/RESEARCH-MULTICHAIN.md`](docs/RESEARCH-MULTICHAIN.md).
112+
113+## Architecture
114+
115+```
116+Sources/OSVaultKit
117+├── Services
118+│ ├── VaultCrypto ← the encryption mechanism (no Keychain)
119+│ ├── KeyManager ← BIP-39, HD derivation, vault lifecycle
120+│ ├── RPCService ← EVM JSON-RPC with endpoint failover
121+│ ├── TransactionService ← EIP-1559 + 5 other real fee models
122+│ ├── BitcoinService ← bdk-swift, watch-only + transient signer
123+│ ├── SolanaService ← solana-swift (vendored), SPL + ATA
124+│ ├── TronService ← wallet-core signing + TronGrid REST
125+│ ├── XRPLService ← wallet-core signing + xrplcluster JSON-RPC
126+│ ├── TONService ← wallet-core signing + toncenter v2/v3
127+│ └── PriceService ← CoinGecko keyless, cached, optional
128+├── Models ← Network (chain registry), Token (verified matrix)…
129+└── Views ← SwiftUI: onboarding, home, per-chain panels
130+```
131+
132+## Requirements
133+
134+- macOS 15.5+ (Apple silicon or Intel)
135+- That's it. No accounts, no keys, no configuration.
136+
137+## Testnet quickstart
138+
139+1. Create a wallet (write the 12 words down — the app makes you prove it).
140+2. Fund: [Circle faucet](https://faucet.circle.com) (USDC on Base Sepolia +
141+ Solana devnet), a Base Sepolia ETH faucet, [mempool.space signet
142+ faucet](https://signetfaucet.com), [nileex.io](https://nileex.io) (TRX +
143+ test USDT), [XRPL faucet](https://xrpl.org/resources/dev-tools/xrp-faucets),
144+ [@testgiver_ton_bot](https://t.me/testgiver_ton_bot).
145+3. Send. Watch it confirm on the linked explorer.
146+
147+---
148+
149+<div align="center">
150+
151+**Built with Swift, paranoia, and a refusal to type API keys.**
152+© 2026 Simon-Pierre Boucher · [contact@spboucher.ai](mailto:contact@spboucher.ai)
153+
154+</div>
added Sources/OSVault/main.swift +11 −0
@@ -0,0 +1,11 @@
1+//
2+// main.swift
3+// OS Vault
4+//
5+// Author: Simon-Pierre Boucher
6+// Mail: contact@spboucher.ai
7+//
8+
9+import OSVaultKit
10+
11+OSVaultApp.main()
added Sources/OSVaultKit/App/AppState.swift +344 −0
@@ -0,0 +1,344 @@
1+//
2+// AppState.swift
3+// OS Vault
4+//
5+// Author: Simon-Pierre Boucher
6+// Mail: contact@spboucher.ai
7+//
8+
9+import Foundation
10+import SwiftUI
11+import BigInt
12+
13+/// Single source of truth for the UI. Holds the public wallet address and
14+/// balances; never the private key — signing re-derives it from the vault
15+/// password at confirmation time and discards it immediately.
16+@MainActor
17+public final class AppState: ObservableObject {
18+
19+ public enum Phase: Equatable {
20+ case onboarding
21+ case locked
22+ case unlocked
23+ }
24+
25+ @Published public var phase: Phase
26+ @Published public var address: String?
27+ @Published public var network: Network {
28+ didSet {
29+ UserDefaults.standard.set(network.rawValue, forKey: Self.networkKey)
30+ restartPollingIfUnlocked()
31+ }
32+ }
33+ @Published public var balances = Balances()
34+ @Published public var balancesLoaded = false
35+ @Published public var balanceError: String?
36+ @Published public var history: [TransactionRecord] = []
37+ @Published public var fiatPrices: [String: Decimal] = [:]
38+ @Published public var pricesEnabled: Bool {
39+ didSet {
40+ UserDefaults.standard.set(pricesEnabled, forKey: PriceService.enabledKey)
41+ if pricesEnabled { Task { await refreshPrices() } } else { fiatPrices = [:] }
42+ }
43+ }
44+ @Published public var fiatCurrency: String {
45+ didSet {
46+ UserDefaults.standard.set(fiatCurrency, forKey: PriceService.fiatKey)
47+ Task { await refreshPrices() }
48+ }
49+ }
50+
51+ public let priceService = PriceService()
52+ public let bitcoinService = BitcoinService()
53+ public let solanaService = SolanaService()
54+ public let tronService = TronService()
55+ public let xrplService = XRPLService()
56+ @Published public var xrplBalances: XRPLService.XRPLBalances?
57+ @Published public var xrplAddress: String?
58+ @Published public var xrplError: String?
59+ @Published public var xrplNetwork: XRPLService.XRPLNetwork = .testnet
60+ public let tonService = TONService()
61+ @Published public var tonBalancesState: TONService.TONBalances?
62+ @Published public var tonAddress: String?
63+ @Published public var tonError: String?
64+ @Published public var tonNetwork: TONService.TONNetwork = .testnet
65+ @Published public var tronBalances: TronService.TronBalances?
66+ @Published public var tronAddress: String?
67+ @Published public var tronError: String?
68+ @Published public var tronNetwork: TronService.TronNetwork = .nile
69+ @Published public var solBalances: SolanaService.SOLBalances?
70+ @Published public var solAddress: String?
71+ @Published public var solError: String?
72+ @Published public var solNetwork: SolanaService.SOLNetwork = .devnet
73+ @Published public var btcBalance: BitcoinService.BTCBalance?
74+ @Published public var btcSyncing = false
75+ @Published public var btcError: String?
76+ @Published public var btcNetwork: BitcoinService.BTCNetwork = .signet
77+ public let keyManager: KeyManager
78+ let historyStore: HistoryStore
79+ private var pollTask: Task<Void, Never>?
80+
81+ static let networkKey = "osvault.network"
82+ public static let pollInterval: Duration = .seconds(15)
83+
84+ public init(keyManager: KeyManager = KeyManager(), historyStore: HistoryStore = HistoryStore()) {
85+ self.keyManager = keyManager
86+ self.historyStore = historyStore
87+ let saved = UserDefaults.standard.string(forKey: Self.networkKey)
88+ self.network = saved.flatMap(Network.init(rawValue:)) ?? .baseSepolia
89+ self.phase = keyManager.hasVault ? .locked : .onboarding
90+ self.history = historyStore.load()
91+ self.pricesEnabled = UserDefaults.standard.object(forKey: PriceService.enabledKey) as? Bool ?? true
92+ self.fiatCurrency = UserDefaults.standard.string(forKey: PriceService.fiatKey) ?? "USD"
93+ }
94+
95+ public func rpc() -> RPCService {
96+ RPCService(urls: network.rpcURLs)
97+ }
98+
99+ // MARK: - Session
100+
101+ public func didUnlock(wallet: KeyManager.UnlockedWallet) {
102+ self.address = wallet.address
103+ self.phase = .unlocked
104+ let mnemonic = wallet.mnemonic
105+ Task {
106+ do {
107+ try await bitcoinService.configure(mnemonic: mnemonic)
108+ btcNetwork = await bitcoinService.currentNetwork
109+ await refreshBitcoin()
110+ } catch {
111+ btcError = error.localizedDescription
112+ }
113+ }
114+ Task {
115+ do {
116+ try await solanaService.configure(mnemonic: mnemonic)
117+ solNetwork = await solanaService.currentNetwork
118+ solAddress = await solanaService.publicAddress
119+ await refreshSolana()
120+ } catch {
121+ solError = error.localizedDescription
122+ }
123+ }
124+ Task {
125+ do {
126+ try await tronService.configure(mnemonic: mnemonic)
127+ tronNetwork = await tronService.currentNetwork
128+ tronAddress = await tronService.publicAddress
129+ await refreshTron()
130+ } catch {
131+ tronError = error.localizedDescription
132+ }
133+ }
134+ Task {
135+ do {
136+ try await xrplService.configure(mnemonic: mnemonic)
137+ xrplNetwork = await xrplService.currentNetwork
138+ xrplAddress = await xrplService.publicAddress
139+ await refreshXRPL()
140+ } catch {
141+ xrplError = error.localizedDescription
142+ }
143+ }
144+ Task {
145+ do {
146+ try await tonService.configure(mnemonic: mnemonic)
147+ tonNetwork = await tonService.currentNetwork
148+ tonAddress = await tonService.publicAddress
149+ await refreshTON()
150+ } catch {
151+ tonError = error.localizedDescription
152+ }
153+ }
154+ startPolling()
155+ }
156+
157+ public func refreshBitcoin() async {
158+ guard await bitcoinService.isConfigured else { return }
159+ btcSyncing = true
160+ defer { btcSyncing = false }
161+ do {
162+ try await bitcoinService.sync()
163+ btcBalance = await bitcoinService.balance()
164+ btcError = nil
165+ } catch {
166+ btcError = error.localizedDescription
167+ }
168+ }
169+
170+ public func refreshSolana() async {
171+ guard await solanaService.isConfigured else { return }
172+ do {
173+ solBalances = try await solanaService.fetchBalances()
174+ solError = nil
175+ } catch {
176+ solError = error.localizedDescription
177+ }
178+ }
179+
180+ public func refreshTron() async {
181+ guard await tronService.isConfigured else { return }
182+ do {
183+ tronBalances = try await tronService.fetchBalances()
184+ tronError = nil
185+ } catch {
186+ tronError = error.localizedDescription
187+ }
188+ }
189+
190+ public func switchTronNetwork(to newNetwork: TronService.TronNetwork) async {
191+ await tronService.switchNetwork(to: newNetwork)
192+ tronNetwork = newNetwork
193+ tronBalances = nil
194+ await refreshTron()
195+ }
196+
197+ public func refreshXRPL() async {
198+ guard await xrplService.isConfigured else { return }
199+ do {
200+ xrplBalances = try await xrplService.fetchBalances()
201+ xrplError = nil
202+ } catch {
203+ xrplError = error.localizedDescription
204+ }
205+ }
206+
207+ public func switchXRPLNetwork(to newNetwork: XRPLService.XRPLNetwork) async {
208+ await xrplService.switchNetwork(to: newNetwork)
209+ xrplNetwork = newNetwork
210+ xrplBalances = nil
211+ await refreshXRPL()
212+ }
213+
214+ public func refreshTON() async {
215+ guard await tonService.isConfigured else { return }
216+ do {
217+ tonBalancesState = try await tonService.fetchBalances()
218+ tonError = nil
219+ } catch {
220+ tonError = error.localizedDescription
221+ }
222+ }
223+
224+ public func switchTONNetwork(to newNetwork: TONService.TONNetwork) async {
225+ await tonService.switchNetwork(to: newNetwork)
226+ tonNetwork = newNetwork
227+ tonBalancesState = nil
228+ await refreshTON()
229+ }
230+
231+ public func switchSolanaNetwork(to newNetwork: SolanaService.SOLNetwork) async {
232+ await solanaService.switchNetwork(to: newNetwork)
233+ solNetwork = newNetwork
234+ solBalances = nil
235+ await refreshSolana()
236+ }
237+
238+ /// Re-derives descriptors for the new BTC network; needs the password.
239+ public func switchBitcoinNetwork(to newNetwork: BitcoinService.BTCNetwork, password: String) async throws {
240+ let mnemonic = try await Task.detached { [keyManager] in
241+ try keyManager.unlock(password: password).mnemonic
242+ }.value
243+ try await bitcoinService.switchNetwork(to: newNetwork, mnemonic: mnemonic)
244+ btcNetwork = newNetwork
245+ btcBalance = nil
246+ await refreshBitcoin()
247+ }
248+
249+ public func lock() {
250+ pollTask?.cancel()
251+ pollTask = nil
252+ address = nil
253+ balances = Balances()
254+ balancesLoaded = false
255+ phase = .locked
256+ }
257+
258+ public func walletDeleted() {
259+ pollTask?.cancel()
260+ pollTask = nil
261+ address = nil
262+ balances = Balances()
263+ balancesLoaded = false
264+ history = []
265+ historyStore.clear()
266+ btcBalance = nil
267+ solBalances = nil
268+ solAddress = nil
269+ tronBalances = nil
270+ tronAddress = nil
271+ xrplBalances = nil
272+ xrplAddress = nil
273+ tonBalancesState = nil
274+ tonAddress = nil
275+ // Remove the BDK watch-only databases and scan flags with the vault.
276+ let support = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
277+ for network in BitcoinService.BTCNetwork.allCases {
278+ try? FileManager.default.removeItem(
279+ at: support.appendingPathComponent("OSVault/bdk-\(network.rawValue).sqlite"))
280+ UserDefaults.standard.removeObject(forKey: "osvault.bitcoin.scanned.\(network.rawValue)")
281+ }
282+ phase = .onboarding
283+ }
284+
285+ // MARK: - Balances
286+
287+ public func refreshBalances() async {
288+ guard let address else { return }
289+ do {
290+ let fetched = try await BalanceService.fetch(address: address, network: network, rpc: rpc())
291+ balances = fetched
292+ balancesLoaded = true
293+ balanceError = nil
294+ } catch {
295+ balanceError = error.localizedDescription
296+ }
297+ }
298+
299+ public func refreshPrices() async {
300+ guard pricesEnabled else { return }
301+ let symbols = Token.available(on: network).map(\.symbol)
302+ + [network.config.nativeSymbol, "BTC", "SOL", "TRX", "USDT", "XRP", "TON"]
303+ fiatPrices = await priceService.prices(for: symbols, fiat: fiatCurrency)
304+ }
305+
306+ private func startPolling() {
307+ pollTask?.cancel()
308+ pollTask = Task { [weak self] in
309+ var tick = 0
310+ while !Task.isCancelled {
311+ await self?.refreshBalances()
312+ await self?.refreshPrices()
313+ // Bitcoin blocks are ~10 min; sync every 4th tick (60 s).
314+ if tick % 4 == 0 { await self?.refreshBitcoin() }
315+ if tick % 2 == 0 { await self?.refreshSolana() }
316+ if tick % 2 == 1 { await self?.refreshTron() }
317+ if tick % 4 == 1 { await self?.refreshXRPL() }
318+ if tick % 4 == 3 { await self?.refreshTON() }
319+ tick += 1
320+ try? await Task.sleep(for: Self.pollInterval)
321+ }
322+ }
323+ }
324+
325+ private func restartPollingIfUnlocked() {
326+ guard phase == .unlocked else { return }
327+ balances = Balances()
328+ balancesLoaded = false
329+ startPolling()
330+ }
331+
332+ // MARK: - History
333+
334+ public func recordSend(_ record: TransactionRecord) {
335+ history.insert(record, at: 0)
336+ historyStore.save(history)
337+ }
338+
339+ public func updateRecord(hash: String, status: TransactionRecord.Status) {
340+ guard let index = history.firstIndex(where: { $0.hash == hash }) else { return }
341+ history[index].status = status
342+ historyStore.save(history)
343+ }
344+}
added Sources/OSVaultKit/App/OSVaultApp.swift +39 −0
@@ -0,0 +1,39 @@
1+//
2+// OSVaultApp.swift
3+// OS Vault
4+//
5+// Author: Simon-Pierre Boucher
6+// Mail: contact@spboucher.ai
7+//
8+
9+import SwiftUI
10+
11+public struct OSVaultApp: App {
12+ @StateObject private var app = AppState()
13+
14+ public init() {}
15+
16+ public var body: some Scene {
17+ WindowGroup {
18+ ContentView()
19+ .environmentObject(app)
20+ .frame(minWidth: 520, minHeight: 640)
21+ }
22+ .windowResizability(.contentSize)
23+ }
24+}
25+
26+struct ContentView: View {
27+ @EnvironmentObject var app: AppState
28+
29+ var body: some View {
30+ switch app.phase {
31+ case .onboarding:
32+ OnboardingView()
33+ case .locked:
34+ UnlockView()
35+ case .unlocked:
36+ HomeView()
37+ }
38+ }
39+}
added Sources/OSVaultKit/Models/Asset.swift +51 −0
@@ -0,0 +1,51 @@
1+//
2+// Asset.swift
3+// OS Vault
4+//
5+// Author: Simon-Pierre Boucher
6+// Mail: contact@spboucher.ai
7+//
8+
9+import Foundation
10+
11+/// Anything the wallet can send on an EVM chain: the chain's native coin
12+/// (ETH, POL, BNB, AVAX, xDAI… — also the gas currency) or a registered
13+/// ERC-20 stablecoin. Native symbol/name depend on the chain, so they are
14+/// resolved against a Network.
15+public enum Asset: Identifiable, Hashable, Sendable {
16+ case native
17+ case token(Token)
18+
19+ public var id: String {
20+ switch self {
21+ case .native: return "native"
22+ case .token(let token): return "token-\(token.symbol)"
23+ }
24+ }
25+
26+ public func symbol(on network: Network) -> String {
27+ switch self {
28+ case .native: return network.config.nativeSymbol
29+ case .token(let token): return token.symbol
30+ }
31+ }
32+
33+ public func name(on network: Network) -> String {
34+ switch self {
35+ case .native: return network.config.nativeName
36+ case .token(let token): return token.name
37+ }
38+ }
39+
40+ /// All supported natives are 18 decimals (ETH, POL, BNB, AVAX, xDAI).
41+ public var decimals: Int {
42+ switch self {
43+ case .native: return 18
44+ case .token(let token): return token.decimals
45+ }
46+ }
47+
48+ public static func available(on network: Network) -> [Asset] {
49+ Token.available(on: network).map(Asset.token) + [.native]
50+ }
51+}
added Sources/OSVaultKit/Models/Network.swift +195 −0
@@ -0,0 +1,195 @@
1+//
2+// Network.swift
3+// OS Vault
4+//
5+// Author: Simon-Pierre Boucher
6+// Mail: contact@spboucher.ai
7+//
8+
9+import Foundation
10+import BigInt
11+
12+/// The EVM chains OS Vault can talk to. Fully data-driven: adding a chain is
13+/// a new case + ChainConfig entry. Every chain id and RPC below was verified
14+/// live before being listed (see docs/RESEARCH-MULTICHAIN.md). Non-EVM chains
15+/// (Bitcoin, Solana, Tron, TON, XRPL…) use their own adapters.
16+public enum Network: String, CaseIterable, Codable, Identifiable, Sendable {
17+ case baseSepolia // testnet — safe default for development
18+ case baseMainnet
19+ case ethereum
20+ case arbitrum
21+ case optimism
22+ case polygon
23+ case bnb
24+ case avalanche
25+ case gnosis
26+ case linea
27+ case scroll
28+
29+ public var id: String { rawValue }
30+
31+ /// How transaction fees actually work on this chain — the research
32+ /// finding is that "EIP-1559" hides four distinct realities.
33+ public enum FeeModel: Sendable {
34+ /// Standard: maxFee = 2×baseFee + tip.
35+ case eip1559
36+ /// BNB Chain (BEP-226): baseFee is 0; price via eth_gasPrice.
37+ case zeroBaseFee
38+ /// OP-stack (Base, Optimism): L2 fee + L1 data fee from the
39+ /// GasPriceOracle predeploy at 0x42…0F, deducted silently.
40+ case opStackL1Fee
41+ /// Scroll: same idea, oracle at 0x5300…0002.
42+ case scrollL1Fee
43+ /// Arbitrum: estimateGas already folds the L1 buffer into gasLimit;
44+ /// suggested tip is 0.
45+ case arbitrumInclusive
46+ /// Linea: base fee pinned at 7 wei; real cost rides in the tip.
47+ case lineaPinnedBase
48+ }
49+
50+ public struct ChainConfig: Sendable {
51+ public let chainID: BigUInt
52+ public let displayName: String
53+ public let nativeSymbol: String
54+ public let nativeName: String
55+ /// Ordered keyless endpoints: primary first, chain id verified live.
56+ public let rpcs: [URL]
57+ public let explorerBase: URL
58+ public let feeModel: FeeModel
59+ public let isTestnet: Bool
60+ }
61+
62+ public var config: ChainConfig {
63+ switch self {
64+ case .baseSepolia:
65+ return ChainConfig(
66+ chainID: 84532, displayName: "Base Sepolia",
67+ nativeSymbol: "ETH", nativeName: "Ether",
68+ rpcs: [URL(string: "https://sepolia.base.org")!,
69+ URL(string: "https://base-sepolia-rpc.publicnode.com")!],
70+ explorerBase: URL(string: "https://sepolia.basescan.org")!,
71+ feeModel: .opStackL1Fee, isTestnet: true
72+ )
73+ case .baseMainnet:
74+ return ChainConfig(
75+ chainID: 8453, displayName: "Base",
76+ nativeSymbol: "ETH", nativeName: "Ether",
77+ rpcs: [URL(string: "https://base-rpc.publicnode.com")!,
78+ URL(string: "https://mainnet.base.org")!],
79+ explorerBase: URL(string: "https://basescan.org")!,
80+ feeModel: .opStackL1Fee, isTestnet: false
81+ )
82+ case .ethereum:
83+ return ChainConfig(
84+ chainID: 1, displayName: "Ethereum",
85+ nativeSymbol: "ETH", nativeName: "Ether",
86+ rpcs: [URL(string: "https://ethereum-rpc.publicnode.com")!,
87+ URL(string: "https://cloudflare-eth.com")!,
88+ URL(string: "https://eth.merkle.io")!],
89+ explorerBase: URL(string: "https://etherscan.io")!,
90+ feeModel: .eip1559, isTestnet: false
91+ )
92+ case .arbitrum:
93+ return ChainConfig(
94+ chainID: 42161, displayName: "Arbitrum One",
95+ nativeSymbol: "ETH", nativeName: "Ether",
96+ rpcs: [URL(string: "https://arbitrum-one-rpc.publicnode.com")!,
97+ URL(string: "https://arb1.arbitrum.io/rpc")!],
98+ explorerBase: URL(string: "https://arbiscan.io")!,
99+ feeModel: .arbitrumInclusive, isTestnet: false
100+ )
101+ case .optimism:
102+ return ChainConfig(
103+ chainID: 10, displayName: "OP Mainnet",
104+ nativeSymbol: "ETH", nativeName: "Ether",
105+ rpcs: [URL(string: "https://optimism-rpc.publicnode.com")!,
106+ URL(string: "https://mainnet.optimism.io")!],
107+ explorerBase: URL(string: "https://optimistic.etherscan.io")!,
108+ feeModel: .opStackL1Fee, isTestnet: false
109+ )
110+ case .polygon:
111+ return ChainConfig(
112+ chainID: 137, displayName: "Polygon",
113+ nativeSymbol: "POL", nativeName: "Polygon Ecosystem Token",
114+ rpcs: [URL(string: "https://polygon-bor-rpc.publicnode.com")!,
115+ URL(string: "https://polygon.drpc.org")!],
116+ explorerBase: URL(string: "https://polygonscan.com")!,
117+ feeModel: .eip1559, isTestnet: false
118+ )
119+ case .bnb:
120+ return ChainConfig(
121+ chainID: 56, displayName: "BNB Chain",
122+ nativeSymbol: "BNB", nativeName: "BNB",
123+ rpcs: [URL(string: "https://bsc-rpc.publicnode.com")!,
124+ URL(string: "https://bsc-dataseed.bnbchain.org")!],
125+ explorerBase: URL(string: "https://bscscan.com")!,
126+ feeModel: .zeroBaseFee, isTestnet: false
127+ )
128+ case .avalanche:
129+ return ChainConfig(
130+ chainID: 43114, displayName: "Avalanche C-Chain",
131+ nativeSymbol: "AVAX", nativeName: "Avalanche",
132+ rpcs: [URL(string: "https://avalanche-c-chain-rpc.publicnode.com")!,
133+ URL(string: "https://api.avax.network/ext/bc/C/rpc")!],
134+ explorerBase: URL(string: "https://snowtrace.io")!,
135+ feeModel: .eip1559, isTestnet: false
136+ )
137+ case .gnosis:
138+ return ChainConfig(
139+ chainID: 100, displayName: "Gnosis",
140+ nativeSymbol: "xDAI", nativeName: "xDAI (dollar-pegged)",
141+ rpcs: [URL(string: "https://gnosis-rpc.publicnode.com")!,
142+ URL(string: "https://rpc.gnosischain.com")!],
143+ explorerBase: URL(string: "https://gnosisscan.io")!,
144+ feeModel: .eip1559, isTestnet: false
145+ )
146+ case .linea:
147+ return ChainConfig(
148+ chainID: 59144, displayName: "Linea",
149+ nativeSymbol: "ETH", nativeName: "Ether",
150+ rpcs: [URL(string: "https://linea-rpc.publicnode.com")!,
151+ URL(string: "https://rpc.linea.build")!],
152+ explorerBase: URL(string: "https://lineascan.build")!,
153+ feeModel: .lineaPinnedBase, isTestnet: false
154+ )
155+ case .scroll:
156+ return ChainConfig(
157+ chainID: 534352, displayName: "Scroll",
158+ nativeSymbol: "ETH", nativeName: "Ether",
159+ rpcs: [URL(string: "https://scroll-rpc.publicnode.com")!,
160+ URL(string: "https://rpc.scroll.io")!],
161+ explorerBase: URL(string: "https://scrollscan.com")!,
162+ feeModel: .scrollL1Fee, isTestnet: false
163+ )
164+ }
165+ }
166+
167+ /// L1 data-fee oracle for rollups that charge one (`getL1Fee(bytes)`).
168+ public var l1FeeOracle: String? {
169+ switch config.feeModel {
170+ case .opStackL1Fee: return "0x420000000000000000000000000000000000000F"
171+ case .scrollL1Fee: return "0x5300000000000000000000000000000000000002"
172+ default: return nil
173+ }
174+ }
175+
176+ /// Active RPC endpoints, user override (Settings) first.
177+ public var rpcURLs: [URL] {
178+ var urls = config.rpcs
179+ if let raw = UserDefaults.standard.string(forKey: rpcOverrideKey),
180+ !raw.isEmpty, let url = URL(string: raw), url.scheme?.hasPrefix("http") == true {
181+ urls.insert(url, at: 0)
182+ }
183+ return urls
184+ }
185+
186+ public var rpcOverrideKey: String { "osvault.rpcOverride.\(rawValue)" }
187+
188+ public func explorerTxURL(_ hash: String) -> URL {
189+ config.explorerBase.appendingPathComponent("tx/\(hash)")
190+ }
191+
192+ public func explorerAddressURL(_ address: String) -> URL {
193+ config.explorerBase.appendingPathComponent("address/\(address)")
194+ }
195+}
added Sources/OSVaultKit/Models/Token.swift +128 −0
@@ -0,0 +1,128 @@
1+//
2+// Token.swift
3+// OS Vault
4+//
5+// Author: Simon-Pierre Boucher
6+// Mail: contact@spboucher.ai
7+//
8+
9+import Foundation
10+
11+/// An ERC-20 stablecoin deployment set. Every address and decimal count below
12+/// was verified live on-chain via `symbol()`/`decimals()` eth_calls
13+/// (docs/STABLECOINS.md + docs/RESEARCH-MULTICHAIN.md, 2026-08-05).
14+///
15+/// Registry rules learned from that research:
16+/// - decimals are per-DEPLOYMENT data: USDT/USDC are 18 on BNB Chain, 6
17+/// everywhere else — hence the separate BNB-peg entries.
18+/// - bridged USDC.e reports the same `symbol()` as native USDC on
19+/// Arbitrum/Optimism/Polygon; the registry distinguishes them by address
20+/// and shows "USDC.e" to the user.
21+/// - Arbitrum/Polygon USDT was upgraded in place to USDT0 (same address,
22+/// same 6 decimals) — never assert on-chain symbol strings.
23+public struct Token: Identifiable, Hashable, Sendable {
24+ public let symbol: String
25+ public let name: String
26+ public let decimals: Int
27+ /// Checksummed contract address per network the token is deployed on.
28+ public let addresses: [Network: String]
29+
30+ public var id: String { symbol }
31+
32+ public func address(on network: Network) -> String? {
33+ addresses[network]
34+ }
35+
36+ // MARK: - Registry
37+
38+ public static let usdc = Token(
39+ symbol: "USDC", name: "USD Coin", decimals: 6,
40+ addresses: [
41+ .baseSepolia: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
42+ .baseMainnet: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
43+ .ethereum: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
44+ .arbitrum: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
45+ .optimism: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
46+ .polygon: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
47+ .avalanche: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
48+ .linea: "0x176211869cA2b568f2A7D4EE941E073a821EE1ff"
49+ ]
50+ )
51+
52+ /// Bridged USDC — same on-chain symbol as native USDC, different address.
53+ public static let usdce = Token(
54+ symbol: "USDC.e", name: "Bridged USDC", decimals: 6,
55+ addresses: [
56+ .arbitrum: "0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8",
57+ .optimism: "0x7F5c764cBc14f9669B88837ca1490cCa17c31607",
58+ .polygon: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
59+ .avalanche: "0xA7D7079b0FEaD91F3e65f86E8915Cb59c1a4C664",
60+ .gnosis: "0x2a22f9c3b484c3629090FeED35F17Ff8F88f76F0",
61+ .scroll: "0x06eFdBFf2a14a7c8E15944D1F4A48F9F95F663A4"
62+ ]
63+ )
64+
65+ public static let usdt = Token(
66+ symbol: "USDT", name: "Tether USD", decimals: 6,
67+ addresses: [
68+ .baseMainnet: "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2",
69+ .ethereum: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
70+ .arbitrum: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", // USDT0 in-place upgrade
71+ .optimism: "0x94b008aA00579c1307B0EF2c499aD98a8ce58e58",
72+ .polygon: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", // USDT0 in-place upgrade
73+ .avalanche: "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7", // native USDt
74+ .gnosis: "0x4ECaBa5870353805a9F068101A40E0f32ed605C6",
75+ .linea: "0xA219439258ca9da29E9Cc4cE5596924745e12B93",
76+ .scroll: "0xf55BEC9cafDbE8730f096Aa55dad6D22d44099Df"
77+ ]
78+ )
79+
80+ /// Binance-peg variants: 18 decimals, not issuer-native. Kept as separate
81+ /// registry entries because decimals differ from every other deployment.
82+ public static let usdtBNB = Token(
83+ symbol: "USDT", name: "Tether USD (BNB peg)", decimals: 18,
84+ addresses: [.bnb: "0x55d398326f99059fF775485246999027B3197955"]
85+ )
86+
87+ public static let usdcBNB = Token(
88+ symbol: "USDC", name: "USD Coin (BNB peg)", decimals: 18,
89+ addresses: [.bnb: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d"]
90+ )
91+
92+ public static let dai = Token(
93+ symbol: "DAI", name: "Dai Stablecoin", decimals: 18,
94+ addresses: [
95+ .baseMainnet: "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb",
96+ .ethereum: "0x6B175474E89094C44Da98b954EedeAC495271d0F",
97+ .arbitrum: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1",
98+ .optimism: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1",
99+ .polygon: "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063",
100+ .bnb: "0x1AF3F329e8BE154074D8769D1FFa4eE058B1DBc3",
101+ .avalanche: "0xd586E7F844cEa2F87f50152665BCbc2C279D8d70", // DAI.e
102+ .linea: "0x4AF15ec2A0BD43Db75dd04E62FAA3B8EF36b00d5",
103+ .scroll: "0xcA77eB3fEFe3725Dc33bccB54eDEFc3D9f764f97"
104+ ]
105+ )
106+
107+ public static let usds = Token(
108+ symbol: "USDS", name: "Sky Dollar", decimals: 18,
109+ addresses: [
110+ .ethereum: "0xdC035D45d973E3EC169d2276DDab16f1e407384F",
111+ .baseMainnet: "0x820C137fa70C8691f0e44Dc420a5e53c168921Dc"
112+ ]
113+ )
114+
115+ public static let eurc = Token(
116+ symbol: "EURC", name: "Euro Coin", decimals: 6,
117+ addresses: [
118+ .baseMainnet: "0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42"
119+ ]
120+ )
121+
122+ public static let all: [Token] = [usdc, usdcBNB, usdce, usdt, usdtBNB, dai, usds, eurc]
123+
124+ /// Tokens usable on a given network, USDC-family first.
125+ public static func available(on network: Network) -> [Token] {
126+ all.filter { $0.addresses[network] != nil }
127+ }
128+}
added Sources/OSVaultKit/Models/TokenAmount.swift +59 −0
@@ -0,0 +1,59 @@
1+//
2+// TokenAmount.swift
3+// OS Vault
4+//
5+// Author: Simon-Pierre Boucher
6+// Mail: contact@spboucher.ai
7+//
8+
9+import Foundation
10+import BigInt
11+
12+/// Integer-only conversion between base units and display strings.
13+/// `Double` is banned for money: 6- and 18-decimal tokens both exceed the
14+/// 53-bit mantissa long before real-world balances do.
15+public enum TokenAmount {
16+
17+ /// "12.5" with decimals 6 → 12_500_000. Returns nil on malformed input or
18+ /// more fraction digits than the token supports. Accepts "," as separator.
19+ public static func parse(_ input: String, decimals: Int) -> BigUInt? {
20+ let normalized = input
21+ .trimmingCharacters(in: .whitespacesAndNewlines)
22+ .replacingOccurrences(of: ",", with: ".")
23+ guard !normalized.isEmpty, normalized != "." else { return nil }
24+
25+ let parts = normalized.split(separator: ".", omittingEmptySubsequences: false)
26+ guard parts.count <= 2 else { return nil }
27+
28+ let wholePart = String(parts[0])
29+ let fracPart = parts.count == 2 ? String(parts[1]) : ""
30+ guard wholePart.allSatisfy(\.isNumber), fracPart.allSatisfy(\.isNumber) else { return nil }
31+ guard !(wholePart.isEmpty && fracPart.isEmpty) else { return nil }
32+ guard fracPart.count <= decimals else { return nil }
33+
34+ let whole = BigUInt(wholePart.isEmpty ? "0" : wholePart, radix: 10) ?? 0
35+ let paddedFrac = fracPart.padding(toLength: decimals, withPad: "0", startingAt: 0)
36+ let frac = paddedFrac.isEmpty ? BigUInt(0) : (BigUInt(paddedFrac, radix: 10) ?? 0)
37+ return whole * BigUInt(10).power(decimals) + frac
38+ }
39+
40+ /// 12_500_000 with decimals 6 → "12.5". Trailing zeros trimmed,
41+ /// optionally capped to `maxFractionDigits` (truncated, never rounded up —
42+ /// a wallet must not display more than the user owns).
43+ public static func format(_ units: BigUInt, decimals: Int, maxFractionDigits: Int? = nil) -> String {
44+ let divisor = BigUInt(10).power(decimals)
45+ let whole = units / divisor
46+ var frac = String(units % divisor)
47+ frac = String(repeating: "0", count: max(0, decimals - frac.count)) + frac
48+ if let cap = maxFractionDigits, frac.count > cap {
49+ frac = String(frac.prefix(cap))
50+ }
51+ while frac.hasSuffix("0") { frac.removeLast() }
52+ return frac.isEmpty ? String(whole) : "\(whole).\(frac)"
53+ }
54+
55+ /// Wei → ETH display string (18 decimals), for gas costs.
56+ public static func formatWei(_ wei: BigUInt, maxFractionDigits: Int = 8) -> String {
57+ format(wei, decimals: 18, maxFractionDigits: maxFractionDigits)
58+ }
59+}
added Sources/OSVaultKit/Models/TransactionRecord.swift +85 −0
@@ -0,0 +1,85 @@
1+//
2+// TransactionRecord.swift
3+// OS Vault
4+//
5+// Author: Simon-Pierre Boucher
6+// Mail: contact@spboucher.ai
7+//
8+
9+import Foundation
10+import BigInt
11+
12+/// A send initiated from this app, persisted locally (non-sensitive data
13+/// only: it is all public on-chain anyway). Full history lives on the
14+/// explorer; each record links there.
15+public struct TransactionRecord: Identifiable, Codable, Equatable {
16+ public enum Status: String, Codable {
17+ case pending, confirmed, failed
18+ }
19+
20+ public let id: UUID
21+ public let hash: String
22+ public let tokenSymbol: String
23+ public let tokenDecimals: Int
24+ public let amountUnits: String // BigUInt as decimal string (Codable-safe)
25+ public let recipient: String
26+ public let network: Network
27+ public var status: Status
28+ public let date: Date
29+
30+ public init(hash: String, asset: Asset, amountUnits: BigUInt,
31+ recipient: String, network: Network, status: Status, date: Date = Date()) {
32+ self.id = UUID()
33+ self.hash = hash
34+ self.tokenSymbol = asset.symbol(on: network)
35+ self.tokenDecimals = asset.decimals
36+ self.amountUnits = String(amountUnits)
37+ self.recipient = recipient
38+ self.network = network
39+ self.status = status
40+ self.date = date
41+ }
42+
43+ public var displayAmount: String {
44+ guard let units = BigUInt(amountUnits, radix: 10) else { return amountUnits }
45+ return TokenAmount.format(units, decimals: tokenDecimals)
46+ }
47+
48+ public var explorerURL: URL { network.explorerTxURL(hash) }
49+}
50+
51+/// JSON-file persistence for local send history.
52+public final class HistoryStore {
53+ private let fileURL: URL
54+ private let maxEntries = 200
55+
56+ public init(fileURL: URL? = nil) {
57+ if let fileURL {
58+ self.fileURL = fileURL
59+ } else {
60+ let support = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
61+ self.fileURL = support.appendingPathComponent("OSVault/history.json")
62+ }
63+ }
64+
65+ public func load() -> [TransactionRecord] {
66+ guard let data = try? Data(contentsOf: fileURL),
67+ let records = try? JSONDecoder().decode([TransactionRecord].self, from: data) else {
68+ return []
69+ }
70+ return records
71+ }
72+
73+ public func save(_ records: [TransactionRecord]) {
74+ let trimmed = Array(records.prefix(maxEntries))
75+ let dir = fileURL.deletingLastPathComponent()
76+ try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
77+ if let data = try? JSONEncoder().encode(trimmed) {
78+ try? data.write(to: fileURL, options: [.atomic])
79+ }
80+ }
81+
82+ public func clear() {
83+ try? FileManager.default.removeItem(at: fileURL)
84+ }
85+}
added Sources/OSVaultKit/Models/WalletError.swift +48 −0
@@ -0,0 +1,48 @@
1+//
2+// WalletError.swift
3+// OS Vault
4+//
5+// Author: Simon-Pierre Boucher
6+// Mail: contact@spboucher.ai
7+//
8+
9+import Foundation
10+
11+/// Typed errors for every service call. Messages never contain key material.
12+public enum WalletError: LocalizedError, Equatable {
13+ case noVault
14+ case vaultCorrupted
15+ case wrongPassword
16+ case invalidMnemonic
17+ case invalidAddress
18+ case insufficientETHForGas(needWei: String, haveWei: String)
19+ case insufficientTokenBalance
20+ case rpc(String)
21+ case signingFailed
22+ case internalError(String)
23+
24+ public var errorDescription: String? {
25+ switch self {
26+ case .noVault:
27+ return "No wallet exists on this Mac yet."
28+ case .vaultCorrupted:
29+ return "The vault file is damaged and cannot be read."
30+ case .wrongPassword:
31+ return "Incorrect password."
32+ case .invalidMnemonic:
33+ return "This recovery phrase is not valid. Check the words and their order."
34+ case .invalidAddress:
35+ return "This is not a valid Ethereum address."
36+ case .insufficientETHForGas(let need, let have):
37+ return "Not enough of the chain's native coin to pay gas (need ~\(need), have \(have)). Gas is always paid in the native coin, never in stablecoins."
38+ case .insufficientTokenBalance:
39+ return "Amount exceeds your token balance."
40+ case .rpc(let message):
41+ return "Network error: \(message)"
42+ case .signingFailed:
43+ return "The transaction could not be signed."
44+ case .internalError(let message):
45+ return message
46+ }
47+ }
48+}
added Sources/OSVaultKit/Services/BalanceService.swift +38 −0
@@ -0,0 +1,38 @@
1+//
2+// BalanceService.swift
3+// OS Vault
4+//
5+// Author: Simon-Pierre Boucher
6+// Mail: contact@spboucher.ai
7+//
8+
9+import Foundation
10+import BigInt
11+
12+/// Fetches the native ETH balance (gas money) plus every registered
13+/// stablecoin's `balanceOf` for the current network.
14+public struct Balances: Sendable {
15+ public var ethWei: BigUInt
16+ public var tokenUnits: [String: BigUInt] // keyed by token symbol
17+
18+ public init(ethWei: BigUInt = 0, tokenUnits: [String: BigUInt] = [:]) {
19+ self.ethWei = ethWei
20+ self.tokenUnits = tokenUnits
21+ }
22+}
23+
24+public enum BalanceService {
25+
26+ public static func fetch(address: String, network: Network, rpc: RPCService) async throws -> Balances {
27+ var balances = Balances()
28+ balances.ethWei = try await rpc.balance(of: address)
29+
30+ for token in Token.available(on: network) {
31+ guard let contract = token.address(on: network),
32+ let calldata = Hex.erc20BalanceOfData(owner: address) else { continue }
33+ let result = try await rpc.call(to: contract, data: calldata)
34+ balances.tokenUnits[token.symbol] = Hex.toBigUInt(result) ?? 0
35+ }
36+ return balances
37+ }
38+}
added Sources/OSVaultKit/Services/BitcoinService.swift +310 −0
Large diff (310 lines) is not loaded by default to keep the page fast.Load diff
added Sources/OSVaultKit/Services/KeyManager.swift +136 −0
Large diff (136 lines) is not loaded by default to keep the page fast.Load diff
added Sources/OSVaultKit/Services/PriceService.swift +129 −0
Large diff (129 lines) is not loaded by default to keep the page fast.Load diff
added Sources/OSVaultKit/Services/RPCService.swift +186 −0
Large diff (186 lines) is not loaded by default to keep the page fast.Load diff
added Sources/OSVaultKit/Services/SolanaService.swift +263 −0
Large diff (263 lines) is not loaded by default to keep the page fast.Load diff
added Sources/OSVaultKit/Services/TONService.swift +297 −0
Large diff (297 lines) is not loaded by default to keep the page fast.Load diff
added Sources/OSVaultKit/Services/TransactionService.swift +221 −0
Large diff (221 lines) is not loaded by default to keep the page fast.Load diff
added Sources/OSVaultKit/Services/TronService.swift +323 −0
Large diff (323 lines) is not loaded by default to keep the page fast.Load diff
added Sources/OSVaultKit/Services/VaultCrypto.swift +96 −0
Large diff (96 lines) is not loaded by default to keep the page fast.Load diff
added Sources/OSVaultKit/Services/XRPLService.swift +326 −0
Large diff (326 lines) is not loaded by default to keep the page fast.Load diff
added Sources/OSVaultKit/Utils/AddressValidator.swift +43 −0
Large diff (43 lines) is not loaded by default to keep the page fast.Load diff
added Sources/OSVaultKit/Utils/Hex.swift +69 −0
Large diff (69 lines) is not loaded by default to keep the page fast.Load diff
added Sources/OSVaultKit/Utils/QRCode.swift +25 −0
Large diff (25 lines) is not loaded by default to keep the page fast.Load diff
added Sources/OSVaultKit/Views/Bitcoin/BitcoinView.swift +373 −0
Large diff (373 lines) is not loaded by default to keep the page fast.Load diff
added Sources/OSVaultKit/Views/Home/HomeView.swift +254 −0
Large diff (254 lines) is not loaded by default to keep the page fast.Load diff
added Sources/OSVaultKit/Views/Onboarding/OnboardingView.swift +307 −0
Large diff (307 lines) is not loaded by default to keep the page fast.Load diff
added Sources/OSVaultKit/Views/Receive/ReceiveView.swift +49 −0
Large diff (49 lines) is not loaded by default to keep the page fast.Load diff
added Sources/OSVaultKit/Views/Send/SendView.swift +324 −0
Large diff (324 lines) is not loaded by default to keep the page fast.Load diff
added Sources/OSVaultKit/Views/Settings/SettingsView.swift +201 −0
Large diff (201 lines) is not loaded by default to keep the page fast.Load diff
added Sources/OSVaultKit/Views/Shared.swift +51 −0
Large diff (51 lines) is not loaded by default to keep the page fast.Load diff
added Sources/OSVaultKit/Views/Solana/SolanaView.swift +337 −0
Large diff (337 lines) is not loaded by default to keep the page fast.Load diff
added Sources/OSVaultKit/Views/TON/TONView.swift +345 −0
Large diff (345 lines) is not loaded by default to keep the page fast.Load diff
added Sources/OSVaultKit/Views/Tron/TronView.swift +340 −0
Large diff (340 lines) is not loaded by default to keep the page fast.Load diff
added Sources/OSVaultKit/Views/XRPL/XRPLView.swift +389 −0
Large diff (389 lines) is not loaded by default to keep the page fast.Load diff
added Support/Info.plist +49 −0
Large diff (49 lines) is not loaded by default to keep the page fast.Load diff
added Support/entitlements.plist +21 −0
Large diff (21 lines) is not loaded by default to keep the page fast.Load diff
added Tests/OSVaultTests/BitcoinTests.swift +57 −0
Large diff (57 lines) is not loaded by default to keep the page fast.Load diff
added Tests/OSVaultTests/CoreTests.swift +211 −0
Large diff (211 lines) is not loaded by default to keep the page fast.Load diff
added Tests/OSVaultTests/SolanaTests.swift +42 −0
Large diff (42 lines) is not loaded by default to keep the page fast.Load diff
added Tests/OSVaultTests/WalletCoreTests.swift +78 −0
Large diff (78 lines) is not loaded by default to keep the page fast.Load diff
added assets/icon/AppIcon.icns +0 −0

Binary file not shown.

added assets/icon/os-vault-simple.svg +25 −0
Large diff (25 lines) is not loaded by default to keep the page fast.Load diff
added assets/icon/os-vault.svg +158 −0
Large diff (158 lines) is not loaded by default to keep the page fast.Load diff
added docs/RESEARCH-MULTICHAIN.md +93 −0
Large diff (93 lines) is not loaded by default to keep the page fast.Load diff
added docs/ROADMAP.md +65 −0
Large diff (65 lines) is not loaded by default to keep the page fast.Load diff
added docs/STABLECOINS.md +94 −0
Large diff (94 lines) is not loaded by default to keep the page fast.Load diff
added scripts/make-icon.sh +36 −0
Large diff (36 lines) is not loaded by default to keep the page fast.Load diff
added scripts/release.sh +84 −0
Large diff (84 lines) is not loaded by default to keep the page fast.Load diff
added scripts/vendor-walletcore.sh +129 −0
Large diff (130 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Package.swift +40 −0
Large diff (40 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/AnySigner.swift +96 −0
Large diff (96 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Extensions/Account+Codable.swift +92 −0
Large diff (92 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Extensions/AddressProtocol.swift +12 −0
Large diff (12 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Extensions/BitcoinAddress+Extension.swift +25 −0
Large diff (25 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Extensions/CoinType+Address.swift +14 −0
Large diff (14 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Extensions/Data+Hex.swift +84 −0
Large diff (84 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Extensions/DerivationPath+Extension.swift +119 −0
Large diff (119 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Extensions/Mnemonic+Extension.swift +26 −0
Large diff (26 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Extensions/PublicKey+Bitcoin.swift +12 −0
Large diff (12 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/AES.swift +122 −0
Large diff (122 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Account.swift +90 −0
Large diff (90 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/AnyAddress.swift +165 −0
Large diff (165 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/AsnParser.swift +35 −0
Large diff (35 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Barz.swift +125 −0
Large diff (125 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Base32.swift +102 −0
Large diff (102 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Base58.swift +74 −0
Large diff (74 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Base64.swift +74 −0
Large diff (74 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Bech32.swift +84 −0
Large diff (84 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/BitcoinAddress.swift +108 −0
Large diff (108 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/BitcoinMessageSigner.swift +65 −0
Large diff (65 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/BitcoinScript.swift +269 −0
Large diff (269 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/BitcoinSigHashType+Extension.swift +29 −0
Large diff (29 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Biz.swift +107 −0
Large diff (107 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/BizPasskeySession.swift +94 −0
Large diff (94 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Cardano.swift +79 −0
Large diff (79 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/CoinType+Extension.swift +162 −0
Large diff (162 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/CoinTypeConfiguration.swift +87 −0
Large diff (87 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/CryptoBox.swift +54 −0
Large diff (54 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/CryptoBoxPublicKey.swift +56 −0
Large diff (56 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/CryptoBoxSecretKey.swift +68 −0
Large diff (68 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/DataVector.swift +71 −0
Large diff (71 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/DerivationPath.swift +108 −0
Large diff (108 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/DerivationPathIndex.swift +52 −0
Large diff (52 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Eip7702.swift +78 −0
Large diff (78 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Enums/AESPaddingMode.swift +14 −0
Large diff (14 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Enums/BitcoinSigHashType.swift +17 −0
Large diff (17 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Enums/Blockchain.swift +70 −0
Large diff (70 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Enums/CoinType.swift +181 −0
Large diff (181 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Enums/Curve.swift +31 −0
Large diff (31 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Enums/Derivation.swift +26 −0
Large diff (26 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Enums/EthereumChainID.swift +74 −0
Large diff (74 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Enums/FilecoinAddressType.swift +14 −0
Large diff (14 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Enums/FiroAddressType.swift +14 −0
Large diff (14 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Enums/HDVersion.swift +35 −0
Large diff (35 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Enums/HRP.swift +141 −0
Large diff (141 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Enums/PrivateKeyType.swift +14 −0
Large diff (14 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Enums/PublicKeyType.swift +21 −0
Large diff (21 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Enums/Purpose.swift +21 −0
Large diff (21 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Enums/StellarMemoType.swift +17 −0
Large diff (17 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Enums/StellarPassphrase.swift +21 −0
Large diff (21 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Enums/StellarVersionByte.swift +16 −0
Large diff (16 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Enums/StoredKeyEncryption.swift +15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Enums/StoredKeyEncryptionLevel.swift +16 −0
Large diff (16 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Ethereum.swift +112 −0
Large diff (112 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/EthereumAbi.swift +174 −0
Large diff (174 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/EthereumAbiFunction.swift +560 −0
Large diff (560 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/EthereumAbiValue.swift +162 −0
Large diff (162 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/EthereumMessageSigner.swift +108 −0
Large diff (108 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/EthereumRlp.swift +33 −0
Large diff (33 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/FIOAccount.swift +44 −0
Large diff (44 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/FilecoinAddressConverter.swift +44 −0
Large diff (44 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/GroestlcoinAddress.swift +69 −0
Large diff (69 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/HDVersion+Extension.swift +25 −0
Large diff (25 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/HDWallet.swift +314 −0
Large diff (314 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Hash.swift +249 −0
Large diff (249 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/LiquidStaking.swift +32 −0
Large diff (32 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/MessageSigner.swift +67 −0
Large diff (67 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Mnemonic.swift +56 −0
Large diff (56 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/NEARAccount.swift +44 −0
Large diff (44 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/NervosAddress.swift +85 −0
Large diff (85 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/PBKDF2.swift +64 −0
Large diff (64 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/PrivateKey.swift +184 −0
Large diff (184 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Protobuf/Aeternity+Proto.swift +8 −0
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Protobuf/Aeternity.pb.swift +209 −0
Large diff (209 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Protobuf/Aion+Proto.swift +8 −0
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Protobuf/Aion.pb.swift +209 −0
Large diff (209 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Protobuf/Algorand+Proto.swift +11 −0
Large diff (11 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Protobuf/Algorand.pb.swift +493 −0
Large diff (493 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Protobuf/Aptos+Proto.swift +23 −0
Large diff (23 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Protobuf/Aptos.pb.swift +1636 −0
Large diff (1,636 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Protobuf/BabylonStaking+Proto.swift +10 −0
Large diff (10 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Protobuf/BabylonStaking.pb.swift +755 −0
Large diff (755 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Protobuf/Barz+Proto.swift +10 −0
Large diff (10 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Protobuf/Barz.pb.swift +327 −0
Large diff (327 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Protobuf/Binance+Proto.swift +25 −0
Large diff (25 lines) is not loaded by default to keep the page fast.Load diff
added vendor/WalletCoreSPM/Sources/WalletCore/Generated/Protobuf/Binance.pb.swift +2171 −0
Large diff (2,171 lines) is not loaded by default to keep the page fast.Load diff

Diff truncated — file too large.

Menu

Theme