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