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>
142 changed files +20,001 −0
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 | +[](#requirements) | ||||
| 11 | +[](#build) | ||||
| 12 | +[](#architecture) | ||||
| 13 | +[](#tests) | ||||
| 14 | +[](../../releases) | ||||
| 15 | +[](#release-pipeline) | ||||
| 16 | +[](#infrastructure--no-api-keys) | ||||
| 17 | +[](#security-model) | ||||
| 18 | + | ||||
| 19 | +[](#chains) | ||||
| 20 | +[](#chains) | ||||
| 21 | +[](#chains) | ||||
| 22 | +[](#chains) | ||||
| 23 | +[](#chains) | ||||
| 24 | +[](#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.