agentvault 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +9 -0
- package/README.md +1 -1
- package/dist/cli/commands/approve.js +5 -5
- package/dist/cli/commands/archive.js +5 -5
- package/dist/cli/commands/backup.js +5 -5
- package/dist/cli/commands/cloud-backup.js +12 -12
- package/dist/cli/commands/decrypt.js +2 -2
- package/dist/cli/commands/deploy.js +1 -1
- package/dist/cli/commands/exec.js +2 -2
- package/dist/cli/commands/fetch.js +4 -4
- package/dist/cli/commands/inference.js +5 -5
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.js +16 -16
- package/dist/cli/commands/list.js +4 -4
- package/dist/cli/commands/package.js +2 -2
- package/dist/cli/commands/profile.js +1 -1
- package/dist/cli/commands/rebuild.js +2 -2
- package/dist/cli/commands/show.js +1 -1
- package/dist/cli/commands/status.d.ts +1 -1
- package/dist/cli/commands/status.js +8 -8
- package/dist/cli/commands/trace.js +1 -1
- package/dist/cli/commands/wallet-export.js +1 -1
- package/dist/cli/commands/wallet-sign.js +1 -1
- package/dist/cli/commands/wallet.d.ts +1 -1
- package/dist/cli/commands/wallet.js +1 -1
- package/dist/cli/index.d.ts +2 -2
- package/dist/cli/index.js +3 -3
- package/dist/src/archival/archive-manager.d.ts +85 -0
- package/dist/src/archival/archive-manager.js +294 -0
- package/dist/src/archival/arweave-client.d.ts +88 -0
- package/dist/src/archival/arweave-client.js +223 -0
- package/dist/src/archival/index.d.ts +8 -0
- package/{src/archival/index.ts → dist/src/archival/index.js} +1 -1
- package/dist/src/backup/backup.d.ts +67 -0
- package/dist/src/backup/backup.js +231 -0
- package/dist/src/backup/index.d.ts +7 -0
- package/{src/backup/index.ts → dist/src/backup/index.js} +1 -1
- package/dist/src/cloud-storage/cloud-sync.d.ts +49 -0
- package/dist/src/cloud-storage/cloud-sync.js +372 -0
- package/dist/src/cloud-storage/index.d.ts +11 -0
- package/{src/cloud-storage/index.ts → dist/src/cloud-storage/index.js} +1 -1
- package/dist/src/cloud-storage/provider-detector.d.ts +34 -0
- package/dist/src/cloud-storage/provider-detector.js +158 -0
- package/{src/cloud-storage/types.ts → dist/src/cloud-storage/types.d.ts} +40 -53
- package/dist/src/cloud-storage/types.js +10 -0
- package/dist/src/debugging/index.d.ts +6 -0
- package/{src/debugging/index.ts → dist/src/debugging/index.js} +1 -1
- package/dist/src/debugging/logs.d.ts +32 -0
- package/dist/src/debugging/logs.js +158 -0
- package/dist/src/debugging/types.d.ts +91 -0
- package/dist/src/debugging/types.js +5 -0
- package/dist/src/deployment/deployer.d.ts +52 -0
- package/dist/src/deployment/deployer.js +211 -0
- package/dist/src/deployment/icpClient.d.ts +144 -0
- package/dist/src/deployment/icpClient.js +545 -0
- package/dist/src/deployment/index.d.ts +11 -0
- package/dist/src/deployment/index.js +14 -0
- package/dist/src/deployment/promotion.d.ts +32 -0
- package/dist/src/deployment/promotion.js +114 -0
- package/dist/src/deployment/types.d.ts +101 -0
- package/dist/src/deployment/types.js +5 -0
- package/dist/src/icp/batch.d.ts +112 -0
- package/dist/src/icp/batch.js +273 -0
- package/dist/src/icp/cycles.d.ts +29 -0
- package/{src/icp/cycles.ts → dist/src/icp/cycles.js} +8 -22
- package/dist/src/icp/environment.d.ts +60 -0
- package/dist/src/icp/environment.js +183 -0
- package/dist/src/icp/icpcli.d.ts +204 -0
- package/dist/src/icp/icpcli.js +374 -0
- package/dist/src/icp/icwasm.d.ts +94 -0
- package/dist/src/icp/icwasm.js +197 -0
- package/dist/src/icp/identity.d.ts +50 -0
- package/{src/icp/identity.ts → dist/src/icp/identity.js} +15 -28
- package/dist/src/icp/index.d.ts +16 -0
- package/dist/src/icp/index.js +20 -0
- package/dist/src/icp/optimization.d.ts +16 -0
- package/dist/src/icp/optimization.js +225 -0
- package/dist/src/icp/tokens.d.ts +24 -0
- package/{src/icp/tokens.ts → dist/src/icp/tokens.js} +5 -12
- package/dist/src/icp/tool-detector.d.ts +31 -0
- package/dist/src/icp/tool-detector.js +104 -0
- package/dist/src/icp/types.d.ts +493 -0
- package/dist/src/icp/types.js +7 -0
- package/dist/src/index.d.ts +12 -0
- package/dist/src/index.js +18 -0
- package/dist/src/inference/bittensor-client.d.ts +108 -0
- package/dist/src/inference/bittensor-client.js +224 -0
- package/dist/src/inference/index.d.ts +8 -0
- package/{src/inference/index.ts → dist/src/inference/index.js} +1 -1
- package/dist/src/inference/inference-manager.d.ts +76 -0
- package/dist/src/inference/inference-manager.js +228 -0
- package/dist/src/metrics/index.d.ts +7 -0
- package/{src/metrics/index.ts → dist/src/metrics/index.js} +1 -1
- package/dist/src/metrics/metrics.d.ts +39 -0
- package/dist/src/metrics/metrics.js +129 -0
- package/dist/src/monitoring/alerting.d.ts +51 -0
- package/dist/src/monitoring/alerting.js +169 -0
- package/dist/src/monitoring/health.d.ts +40 -0
- package/dist/src/monitoring/health.js +164 -0
- package/dist/src/monitoring/index.d.ts +10 -0
- package/dist/src/monitoring/index.js +12 -0
- package/dist/src/monitoring/info.d.ts +15 -0
- package/dist/src/monitoring/info.js +109 -0
- package/dist/src/monitoring/types.d.ts +93 -0
- package/dist/src/monitoring/types.js +7 -0
- package/dist/src/network/index.d.ts +5 -0
- package/{src/network/index.ts → dist/src/network/index.js} +1 -1
- package/dist/src/network/network-config.d.ts +31 -0
- package/dist/src/network/network-config.js +109 -0
- package/dist/src/packaging/compiler.d.ts +61 -0
- package/dist/src/packaging/compiler.js +562 -0
- package/dist/src/packaging/config-persistence.d.ts +46 -0
- package/dist/src/packaging/config-persistence.js +108 -0
- package/dist/src/packaging/config-schemas.d.ts +115 -0
- package/dist/src/packaging/config-schemas.js +43 -0
- package/dist/src/packaging/detector.d.ts +26 -0
- package/dist/src/packaging/detector.js +193 -0
- package/dist/src/packaging/index.d.ts +16 -0
- package/dist/src/packaging/index.js +22 -0
- package/dist/src/packaging/packager.d.ts +31 -0
- package/dist/src/packaging/packager.js +90 -0
- package/dist/src/packaging/parsers/clawdbot.d.ts +19 -0
- package/dist/src/packaging/parsers/clawdbot.js +231 -0
- package/dist/src/packaging/parsers/cline.d.ts +26 -0
- package/dist/src/packaging/parsers/cline.js +185 -0
- package/dist/src/packaging/parsers/generic.d.ts +27 -0
- package/dist/src/packaging/parsers/generic.js +228 -0
- package/dist/src/packaging/parsers/goose.d.ts +26 -0
- package/dist/src/packaging/parsers/goose.js +175 -0
- package/dist/src/packaging/parsers/index.d.ts +11 -0
- package/{src/packaging/parsers/index.ts → dist/src/packaging/parsers/index.js} +1 -1
- package/dist/src/packaging/serializer.d.ts +108 -0
- package/dist/src/packaging/serializer.js +153 -0
- package/dist/src/packaging/types.d.ts +131 -0
- package/dist/src/packaging/types.js +5 -0
- package/dist/src/packaging/wasmedge-compiler.d.ts +76 -0
- package/dist/src/packaging/wasmedge-compiler.js +349 -0
- package/dist/src/security/index.d.ts +11 -0
- package/{src/security/index.ts → dist/src/security/index.js} +1 -4
- package/dist/src/security/multisig.d.ts +102 -0
- package/dist/src/security/multisig.js +283 -0
- package/dist/src/security/types.d.ts +207 -0
- package/dist/src/security/types.js +217 -0
- package/dist/src/security/vetkeys.d.ts +179 -0
- package/dist/src/security/vetkeys.js +499 -0
- package/dist/src/testing/index.d.ts +6 -0
- package/{src/testing/index.ts → dist/src/testing/index.js} +1 -1
- package/dist/src/testing/local-runner.d.ts +23 -0
- package/dist/src/testing/local-runner.js +226 -0
- package/dist/src/testing/types.d.ts +98 -0
- package/dist/src/testing/types.js +5 -0
- package/dist/src/wallet/cbor-serializer.d.ts +82 -0
- package/dist/src/wallet/cbor-serializer.js +282 -0
- package/dist/src/wallet/chain-dispatcher.d.ts +112 -0
- package/dist/src/wallet/chain-dispatcher.js +241 -0
- package/dist/src/wallet/cross-chain-aggregator.d.ts +119 -0
- package/dist/src/wallet/cross-chain-aggregator.js +235 -0
- package/dist/src/wallet/index.d.ts +16 -0
- package/dist/src/wallet/index.js +22 -0
- package/dist/src/wallet/key-derivation.d.ts +117 -0
- package/dist/src/wallet/key-derivation.js +325 -0
- package/dist/src/wallet/providers/base-provider.d.ts +111 -0
- package/dist/src/wallet/providers/base-provider.js +58 -0
- package/dist/src/wallet/providers/cketh-provider.d.ts +104 -0
- package/dist/src/wallet/providers/cketh-provider.js +343 -0
- package/dist/src/wallet/providers/polkadot-provider.d.ts +115 -0
- package/dist/src/wallet/providers/polkadot-provider.js +407 -0
- package/dist/src/wallet/providers/solana-provider.d.ts +102 -0
- package/dist/src/wallet/providers/solana-provider.js +393 -0
- package/dist/src/wallet/transaction-queue.d.ts +133 -0
- package/dist/src/wallet/transaction-queue.js +195 -0
- package/dist/src/wallet/types.d.ts +167 -0
- package/dist/src/wallet/types.js +5 -0
- package/dist/src/wallet/vetkeys-adapter.d.ts +134 -0
- package/dist/src/wallet/vetkeys-adapter.js +313 -0
- package/dist/src/wallet/wallet-manager.d.ts +202 -0
- package/dist/src/wallet/wallet-manager.js +451 -0
- package/dist/src/wallet/wallet-storage.d.ts +131 -0
- package/dist/src/wallet/wallet-storage.js +274 -0
- package/macos-wallet-app/AgentVaultWallet/App/AgentVaultWalletApp.swift +54 -0
- package/macos-wallet-app/AgentVaultWallet/Models/AppState.swift +102 -0
- package/macos-wallet-app/AgentVaultWallet/Models/Chain.swift +121 -0
- package/macos-wallet-app/AgentVaultWallet/Models/Wallet.swift +98 -0
- package/macos-wallet-app/AgentVaultWallet/Resources/AgentVaultWallet.entitlements +27 -0
- package/macos-wallet-app/AgentVaultWallet/Resources/Info.plist +69 -0
- package/macos-wallet-app/AgentVaultWallet/Services/BackupService.swift +270 -0
- package/macos-wallet-app/AgentVaultWallet/Services/CLIBridge.swift +367 -0
- package/macos-wallet-app/AgentVaultWallet/Services/CryptoService.swift +157 -0
- package/macos-wallet-app/AgentVaultWallet/Services/FileService.swift +120 -0
- package/macos-wallet-app/AgentVaultWallet/Services/KeychainService.swift +219 -0
- package/macos-wallet-app/AgentVaultWallet/Utilities/Constants.swift +44 -0
- package/macos-wallet-app/AgentVaultWallet/Utilities/Extensions.swift +115 -0
- package/macos-wallet-app/AgentVaultWallet/ViewModels/BackupViewModel.swift +237 -0
- package/macos-wallet-app/AgentVaultWallet/ViewModels/CreateWalletViewModel.swift +137 -0
- package/macos-wallet-app/AgentVaultWallet/ViewModels/ImportWalletViewModel.swift +179 -0
- package/macos-wallet-app/AgentVaultWallet/ViewModels/WalletStore.swift +286 -0
- package/macos-wallet-app/AgentVaultWallet/Views/Backup/BackupView.swift +235 -0
- package/macos-wallet-app/AgentVaultWallet/Views/Backup/RestoreView.swift +316 -0
- package/macos-wallet-app/AgentVaultWallet/Views/Create/CreateWalletFlow.swift +438 -0
- package/macos-wallet-app/AgentVaultWallet/Views/Import/ImportWalletFlow.swift +399 -0
- package/macos-wallet-app/AgentVaultWallet/Views/MainView.swift +134 -0
- package/macos-wallet-app/AgentVaultWallet/Views/Settings/SettingsView.swift +276 -0
- package/macos-wallet-app/AgentVaultWallet/Views/Sidebar/SidebarView.swift +133 -0
- package/macos-wallet-app/AgentVaultWallet/Views/Wallet/DashboardView.swift +233 -0
- package/macos-wallet-app/AgentVaultWallet/Views/Wallet/WalletDetailView.swift +281 -0
- package/macos-wallet-app/AgentVaultWallet/Views/Wallet/WalletListView.swift +280 -0
- package/macos-wallet-app/AgentVaultWallet/Views/Welcome/WelcomeView.swift +176 -0
- package/macos-wallet-app/Makefile +47 -0
- package/macos-wallet-app/project.yml +40 -0
- package/macos-wallet-app/setup.sh +73 -0
- package/package.json +10 -2
- package/backups/agentvault-backup-test-agent-2026-02-12T17-54-28-967Z.json +0 -28
- package/backups/agentvault-backup-test-agent-2026-02-12T17-54-29-032Z.backup +0 -1
- package/backups/agentvault-backup-test-agent-2026-02-12T17-57-42-373Z.json +0 -28
- package/backups/agentvault-backup-test-agent-2026-02-12T17-57-42-428Z.backup +0 -1
- package/backups/agentvault-backup-test-agent-2026-02-12T18-52-25-132Z.json +0 -28
- package/backups/agentvault-backup-test-agent-2026-02-12T18-52-25-247Z.backup +0 -1
- package/backups/agentvault-backup-test-agent-2026-02-12T18-54-09-216Z.json +0 -28
- package/backups/agentvault-backup-test-agent-2026-02-12T18-54-09-283Z.backup +0 -1
- package/backups/agentvault-backup-test-agent-2026-02-12T22-18-22-772Z.backup +0 -1
- package/backups/agentvault-backup-test-agent-2026-02-12T22-18-22-793Z.json +0 -28
- package/backups/test-backup.json +0 -28
- package/scripts/dev-dashboard.mjs +0 -84
- package/site/README.md +0 -63
- package/site/docusaurus.config.ts +0 -148
- package/site/package-lock.json +0 -18383
- package/site/package.json +0 -47
- package/site/sidebars.ts +0 -86
- package/site/static/.gitkeep +0 -0
- package/site/static/img/logo.svg +0 -28
- package/site/static/img/og-image.svg +0 -35
- package/src/archival/archive-manager.ts +0 -372
- package/src/archival/arweave-client.ts +0 -289
- package/src/backup/backup.ts +0 -315
- package/src/cloud-storage/cloud-sync.ts +0 -461
- package/src/cloud-storage/provider-detector.ts +0 -198
- package/src/debugging/logs.ts +0 -193
- package/src/debugging/types.ts +0 -100
- package/src/deployment/deployer.ts +0 -274
- package/src/deployment/icpClient.ts +0 -620
- package/src/deployment/index.ts +0 -46
- package/src/deployment/promotion.ts +0 -161
- package/src/deployment/types.ts +0 -111
- package/src/icp/batch.ts +0 -374
- package/src/icp/environment.ts +0 -215
- package/src/icp/icpcli.ts +0 -438
- package/src/icp/icwasm.ts +0 -222
- package/src/icp/index.ts +0 -94
- package/src/icp/optimization.ts +0 -242
- package/src/icp/tool-detector.ts +0 -110
- package/src/icp/types.ts +0 -574
- package/src/index.ts +0 -25
- package/src/inference/bittensor-client.ts +0 -304
- package/src/inference/inference-manager.ts +0 -327
- package/src/metrics/metrics.ts +0 -186
- package/src/monitoring/alerting.ts +0 -190
- package/src/monitoring/health.ts +0 -197
- package/src/monitoring/index.ts +0 -38
- package/src/monitoring/info.ts +0 -114
- package/src/monitoring/types.ts +0 -99
- package/src/network/network-config.ts +0 -129
- package/src/packaging/compiler.ts +0 -647
- package/src/packaging/config-persistence.ts +0 -135
- package/src/packaging/config-schemas.ts +0 -156
- package/src/packaging/detector.ts +0 -220
- package/src/packaging/index.ts +0 -90
- package/src/packaging/packager.ts +0 -118
- package/src/packaging/parsers/clawdbot.ts +0 -278
- package/src/packaging/parsers/cline.ts +0 -223
- package/src/packaging/parsers/generic.ts +0 -266
- package/src/packaging/parsers/goose.ts +0 -214
- package/src/packaging/serializer.ts +0 -260
- package/src/packaging/types.ts +0 -144
- package/src/packaging/wasmedge-compiler.ts +0 -406
- package/src/security/multisig.ts +0 -415
- package/src/security/types.ts +0 -416
- package/src/security/vetkeys.ts +0 -655
- package/src/testing/local-runner.ts +0 -264
- package/src/testing/types.ts +0 -104
- package/src/wallet/cbor-serializer.ts +0 -323
- package/src/wallet/chain-dispatcher.ts +0 -313
- package/src/wallet/cross-chain-aggregator.ts +0 -346
- package/src/wallet/index.ts +0 -76
- package/src/wallet/key-derivation.ts +0 -425
- package/src/wallet/providers/base-provider.ts +0 -154
- package/src/wallet/providers/cketh-provider.ts +0 -434
- package/src/wallet/providers/polkadot-provider.ts +0 -503
- package/src/wallet/providers/solana-provider.ts +0 -490
- package/src/wallet/transaction-queue.ts +0 -284
- package/src/wallet/types.ts +0 -178
- package/src/wallet/vetkeys-adapter.ts +0 -431
- package/src/wallet/wallet-manager.ts +0 -597
- package/src/wallet/wallet-storage.ts +0 -380
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
/// Manages the multi-step wallet creation flow
|
|
4
|
+
@MainActor
|
|
5
|
+
final class CreateWalletViewModel: ObservableObject {
|
|
6
|
+
|
|
7
|
+
enum Step: Int, CaseIterable {
|
|
8
|
+
case selectChain = 0
|
|
9
|
+
case generating = 1
|
|
10
|
+
case showMnemonic = 2
|
|
11
|
+
case confirmMnemonic = 3
|
|
12
|
+
case nameWallet = 4
|
|
13
|
+
case complete = 5
|
|
14
|
+
|
|
15
|
+
var title: String {
|
|
16
|
+
switch self {
|
|
17
|
+
case .selectChain: return "Select Network"
|
|
18
|
+
case .generating: return "Generating Wallet"
|
|
19
|
+
case .showMnemonic: return "Recovery Phrase"
|
|
20
|
+
case .confirmMnemonic: return "Confirm Backup"
|
|
21
|
+
case .nameWallet: return "Name Your Wallet"
|
|
22
|
+
case .complete: return "Wallet Created"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@Published var currentStep: Step = .selectChain
|
|
28
|
+
@Published var selectedChain: Chain = .ethereum
|
|
29
|
+
@Published var walletName: String = ""
|
|
30
|
+
@Published var mnemonic: String = ""
|
|
31
|
+
@Published var mnemonicWords: [String] = []
|
|
32
|
+
@Published var confirmationInput: String = ""
|
|
33
|
+
@Published var confirmWordIndices: [Int] = [] // Which words to verify
|
|
34
|
+
@Published var createdWallet: Wallet?
|
|
35
|
+
@Published var isProcessing: Bool = false
|
|
36
|
+
@Published var errorMessage: String?
|
|
37
|
+
|
|
38
|
+
/// Words the user needs to confirm (randomly selected from the mnemonic)
|
|
39
|
+
var wordsToConfirm: [(index: Int, word: String)] {
|
|
40
|
+
confirmWordIndices.compactMap { idx in
|
|
41
|
+
guard idx < mnemonicWords.count else { return nil }
|
|
42
|
+
return (index: idx, word: mnemonicWords[idx])
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Whether the user can proceed from the current step
|
|
47
|
+
var canProceed: Bool {
|
|
48
|
+
switch currentStep {
|
|
49
|
+
case .selectChain: return true
|
|
50
|
+
case .generating: return false
|
|
51
|
+
case .showMnemonic: return true
|
|
52
|
+
case .confirmMnemonic: return isConfirmationValid
|
|
53
|
+
case .nameWallet: return !walletName.trimmingCharacters(in: .whitespaces).isEmpty
|
|
54
|
+
case .complete: return true
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Validate mnemonic confirmation
|
|
59
|
+
var isConfirmationValid: Bool {
|
|
60
|
+
let inputWords = confirmationInput
|
|
61
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
62
|
+
.lowercased()
|
|
63
|
+
.components(separatedBy: .whitespaces)
|
|
64
|
+
.filter { !$0.isEmpty }
|
|
65
|
+
|
|
66
|
+
guard inputWords.count == confirmWordIndices.count else { return false }
|
|
67
|
+
|
|
68
|
+
for (i, idx) in confirmWordIndices.enumerated() {
|
|
69
|
+
guard i < inputWords.count, idx < mnemonicWords.count else { return false }
|
|
70
|
+
if inputWords[i] != mnemonicWords[idx].lowercased() { return false }
|
|
71
|
+
}
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func goToNext() {
|
|
76
|
+
guard let nextStep = Step(rawValue: currentStep.rawValue + 1) else { return }
|
|
77
|
+
withAnimation(.easeInOut(duration: 0.3)) {
|
|
78
|
+
currentStep = nextStep
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func goBack() {
|
|
83
|
+
guard let prevStep = Step(rawValue: currentStep.rawValue - 1) else { return }
|
|
84
|
+
withAnimation(.easeInOut(duration: 0.3)) {
|
|
85
|
+
currentStep = prevStep
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Generate a new wallet using the CLI bridge
|
|
90
|
+
func generateWallet(store: WalletStore) async {
|
|
91
|
+
isProcessing = true
|
|
92
|
+
errorMessage = nil
|
|
93
|
+
currentStep = .generating
|
|
94
|
+
|
|
95
|
+
do {
|
|
96
|
+
let name = walletName.isEmpty ? "\(selectedChain.displayName) Wallet" : walletName
|
|
97
|
+
let result = try await store.createWallet(chain: selectedChain, name: name)
|
|
98
|
+
|
|
99
|
+
createdWallet = result.wallet
|
|
100
|
+
if let m = result.mnemonic {
|
|
101
|
+
mnemonic = m
|
|
102
|
+
mnemonicWords = m.components(separatedBy: " ")
|
|
103
|
+
selectRandomConfirmationWords()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
isProcessing = false
|
|
107
|
+
currentStep = .showMnemonic
|
|
108
|
+
} catch {
|
|
109
|
+
isProcessing = false
|
|
110
|
+
errorMessage = error.localizedDescription
|
|
111
|
+
currentStep = .selectChain
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// Select 3 random word indices for confirmation
|
|
116
|
+
private func selectRandomConfirmationWords() {
|
|
117
|
+
guard mnemonicWords.count >= 3 else { return }
|
|
118
|
+
var indices = Set<Int>()
|
|
119
|
+
while indices.count < 3 {
|
|
120
|
+
indices.insert(Int.random(in: 0..<mnemonicWords.count))
|
|
121
|
+
}
|
|
122
|
+
confirmWordIndices = indices.sorted()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
func reset() {
|
|
126
|
+
currentStep = .selectChain
|
|
127
|
+
selectedChain = .ethereum
|
|
128
|
+
walletName = ""
|
|
129
|
+
mnemonic = ""
|
|
130
|
+
mnemonicWords = []
|
|
131
|
+
confirmationInput = ""
|
|
132
|
+
confirmWordIndices = []
|
|
133
|
+
createdWallet = nil
|
|
134
|
+
isProcessing = false
|
|
135
|
+
errorMessage = nil
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import UniformTypeIdentifiers
|
|
3
|
+
|
|
4
|
+
/// Manages the multi-step wallet import flow
|
|
5
|
+
@MainActor
|
|
6
|
+
final class ImportWalletViewModel: ObservableObject {
|
|
7
|
+
|
|
8
|
+
enum Step: Int, CaseIterable {
|
|
9
|
+
case selectChain = 0
|
|
10
|
+
case selectMethod = 1
|
|
11
|
+
case enterInput = 2
|
|
12
|
+
case nameWallet = 3
|
|
13
|
+
case importing = 4
|
|
14
|
+
case complete = 5
|
|
15
|
+
|
|
16
|
+
var title: String {
|
|
17
|
+
switch self {
|
|
18
|
+
case .selectChain: return "Select Network"
|
|
19
|
+
case .selectMethod: return "Import Method"
|
|
20
|
+
case .enterInput: return "Enter Details"
|
|
21
|
+
case .nameWallet: return "Name Your Wallet"
|
|
22
|
+
case .importing: return "Importing"
|
|
23
|
+
case .complete: return "Import Complete"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@Published var currentStep: Step = .selectChain
|
|
29
|
+
@Published var selectedChain: Chain = .ethereum
|
|
30
|
+
@Published var selectedMethod: ImportMethod = .mnemonic
|
|
31
|
+
@Published var walletName: String = ""
|
|
32
|
+
@Published var inputText: String = "" // Mnemonic or private key
|
|
33
|
+
@Published var passwordInput: String = "" // For keystore files
|
|
34
|
+
@Published var selectedFileURL: URL? // For file-based imports
|
|
35
|
+
@Published var importedWallet: Wallet?
|
|
36
|
+
@Published var isProcessing: Bool = false
|
|
37
|
+
@Published var errorMessage: String?
|
|
38
|
+
@Published var showFileImporter: Bool = false
|
|
39
|
+
|
|
40
|
+
/// Available import methods for the selected chain
|
|
41
|
+
var availableMethods: [ImportMethod] {
|
|
42
|
+
selectedChain.supportedImportMethods
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Whether the current input is valid enough to proceed
|
|
46
|
+
var isInputValid: Bool {
|
|
47
|
+
switch selectedMethod {
|
|
48
|
+
case .mnemonic:
|
|
49
|
+
return CryptoService.shared.isValidMnemonic(inputText)
|
|
50
|
+
case .privateKey:
|
|
51
|
+
let cleaned = inputText.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
52
|
+
if selectedChain == .ethereum {
|
|
53
|
+
return CryptoService.shared.isValidEthPrivateKey(cleaned)
|
|
54
|
+
}
|
|
55
|
+
return cleaned.count >= 32
|
|
56
|
+
case .pemFile, .jwkFile, .keystoreJSON:
|
|
57
|
+
return selectedFileURL != nil
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
var canProceed: Bool {
|
|
62
|
+
switch currentStep {
|
|
63
|
+
case .selectChain: return true
|
|
64
|
+
case .selectMethod: return true
|
|
65
|
+
case .enterInput: return isInputValid
|
|
66
|
+
case .nameWallet: return !walletName.trimmingCharacters(in: .whitespaces).isEmpty
|
|
67
|
+
case .importing: return false
|
|
68
|
+
case .complete: return true
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// File types accepted for the current import method
|
|
73
|
+
var allowedFileTypes: [UTType] {
|
|
74
|
+
switch selectedMethod {
|
|
75
|
+
case .jwkFile: return [.json]
|
|
76
|
+
case .pemFile: return [UTType(filenameExtension: "pem") ?? .data]
|
|
77
|
+
case .keystoreJSON: return [.json]
|
|
78
|
+
default: return []
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Placeholder text for the input field
|
|
83
|
+
var inputPlaceholder: String {
|
|
84
|
+
switch selectedMethod {
|
|
85
|
+
case .mnemonic: return "Enter your 12 or 24-word recovery phrase, separated by spaces..."
|
|
86
|
+
case .privateKey:
|
|
87
|
+
if selectedChain == .ethereum { return "Enter your hex-encoded private key (0x...)" }
|
|
88
|
+
return "Enter your private key..."
|
|
89
|
+
default: return ""
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
func goToNext() {
|
|
94
|
+
guard let nextStep = Step(rawValue: currentStep.rawValue + 1) else { return }
|
|
95
|
+
withAnimation(.easeInOut(duration: 0.3)) {
|
|
96
|
+
currentStep = nextStep
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
func goBack() {
|
|
101
|
+
guard let prevStep = Step(rawValue: currentStep.rawValue - 1) else { return }
|
|
102
|
+
withAnimation(.easeInOut(duration: 0.3)) {
|
|
103
|
+
currentStep = prevStep
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// Execute the import
|
|
108
|
+
func importWallet(store: WalletStore) async {
|
|
109
|
+
isProcessing = true
|
|
110
|
+
errorMessage = nil
|
|
111
|
+
currentStep = .importing
|
|
112
|
+
|
|
113
|
+
do {
|
|
114
|
+
let name = walletName.isEmpty ? "\(selectedChain.displayName) Import" : walletName
|
|
115
|
+
|
|
116
|
+
switch selectedMethod {
|
|
117
|
+
case .mnemonic:
|
|
118
|
+
importedWallet = try await store.importFromMnemonic(
|
|
119
|
+
chain: selectedChain,
|
|
120
|
+
mnemonic: inputText.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
121
|
+
name: name
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
case .privateKey:
|
|
125
|
+
importedWallet = try await store.importFromPrivateKey(
|
|
126
|
+
chain: selectedChain,
|
|
127
|
+
privateKey: inputText.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
128
|
+
name: name
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
case .jwkFile:
|
|
132
|
+
guard let url = selectedFileURL else { throw ImportError.noFileSelected }
|
|
133
|
+
importedWallet = try await store.importFromJWK(fileURL: url, name: name)
|
|
134
|
+
|
|
135
|
+
case .pemFile:
|
|
136
|
+
guard let url = selectedFileURL else { throw ImportError.noFileSelected }
|
|
137
|
+
importedWallet = try await store.importFromPEM(fileURL: url, name: name)
|
|
138
|
+
|
|
139
|
+
case .keystoreJSON:
|
|
140
|
+
guard let url = selectedFileURL else { throw ImportError.noFileSelected }
|
|
141
|
+
importedWallet = try await store.importFromKeystore(
|
|
142
|
+
fileURL: url,
|
|
143
|
+
password: passwordInput,
|
|
144
|
+
name: name
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
isProcessing = false
|
|
149
|
+
currentStep = .complete
|
|
150
|
+
} catch {
|
|
151
|
+
isProcessing = false
|
|
152
|
+
errorMessage = error.localizedDescription
|
|
153
|
+
currentStep = .enterInput
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
func reset() {
|
|
158
|
+
currentStep = .selectChain
|
|
159
|
+
selectedChain = .ethereum
|
|
160
|
+
selectedMethod = .mnemonic
|
|
161
|
+
walletName = ""
|
|
162
|
+
inputText = ""
|
|
163
|
+
passwordInput = ""
|
|
164
|
+
selectedFileURL = nil
|
|
165
|
+
importedWallet = nil
|
|
166
|
+
isProcessing = false
|
|
167
|
+
errorMessage = nil
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
enum ImportError: LocalizedError {
|
|
171
|
+
case noFileSelected
|
|
172
|
+
|
|
173
|
+
var errorDescription: String? {
|
|
174
|
+
switch self {
|
|
175
|
+
case .noFileSelected: return "No file was selected for import"
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import Combine
|
|
3
|
+
|
|
4
|
+
/// Central state manager for all wallet operations.
|
|
5
|
+
/// Acts as the single source of truth for wallet data in the app.
|
|
6
|
+
@MainActor
|
|
7
|
+
final class WalletStore: ObservableObject {
|
|
8
|
+
|
|
9
|
+
@Published var wallets: [Wallet] = []
|
|
10
|
+
@Published var isLoading: Bool = false
|
|
11
|
+
@Published var errorMessage: String?
|
|
12
|
+
@Published var selectedWalletId: UUID?
|
|
13
|
+
|
|
14
|
+
private let fileService = FileService.shared
|
|
15
|
+
private let keychain = KeychainService.shared
|
|
16
|
+
private let cliBridge = CLIBridge()
|
|
17
|
+
private let backupService = BackupService.shared
|
|
18
|
+
|
|
19
|
+
// MARK: - Computed Properties
|
|
20
|
+
|
|
21
|
+
var selectedWallet: Wallet? {
|
|
22
|
+
guard let id = selectedWalletId else { return nil }
|
|
23
|
+
return wallets.first { $0.id == id }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
var walletsByChain: [Chain: [Wallet]] {
|
|
27
|
+
Dictionary(grouping: wallets, by: \.chain)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
var totalWalletCount: Int { wallets.count }
|
|
31
|
+
|
|
32
|
+
var icpWallets: [Wallet] { wallets.filter { $0.chain == .icp } }
|
|
33
|
+
var ethWallets: [Wallet] { wallets.filter { $0.chain == .ethereum } }
|
|
34
|
+
var arweaveWallets: [Wallet] { wallets.filter { $0.chain == .arweave } }
|
|
35
|
+
|
|
36
|
+
// MARK: - Persistence
|
|
37
|
+
|
|
38
|
+
func loadWallets() {
|
|
39
|
+
do {
|
|
40
|
+
wallets = try fileService.loadWallets()
|
|
41
|
+
} catch {
|
|
42
|
+
errorMessage = "Failed to load wallets: \(error.localizedDescription)"
|
|
43
|
+
wallets = []
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func saveWallets() {
|
|
48
|
+
do {
|
|
49
|
+
try fileService.saveWallets(wallets)
|
|
50
|
+
} catch {
|
|
51
|
+
errorMessage = "Failed to save wallets: \(error.localizedDescription)"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// MARK: - Create Wallet
|
|
56
|
+
|
|
57
|
+
/// Create a new wallet via the CLI bridge
|
|
58
|
+
func createWallet(chain: Chain, name: String) async throws -> WalletCreationResult {
|
|
59
|
+
isLoading = true
|
|
60
|
+
defer { isLoading = false }
|
|
61
|
+
|
|
62
|
+
let result = try await cliBridge.generateWallet(chain: chain, name: name)
|
|
63
|
+
|
|
64
|
+
let walletId = UUID()
|
|
65
|
+
let wallet = Wallet(
|
|
66
|
+
id: walletId,
|
|
67
|
+
name: name,
|
|
68
|
+
chain: chain,
|
|
69
|
+
address: result.address,
|
|
70
|
+
isImported: false,
|
|
71
|
+
derivationPath: chain.derivationPath,
|
|
72
|
+
hasMnemonicBackup: result.mnemonic != nil
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
// Save secrets to Keychain
|
|
76
|
+
if let mnemonic = result.mnemonic {
|
|
77
|
+
try keychain.saveMnemonic(mnemonic, forWalletId: walletId)
|
|
78
|
+
}
|
|
79
|
+
if let privateKey = result.privateKey {
|
|
80
|
+
try keychain.savePrivateKey(privateKey, forWalletId: walletId)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Add to wallet list and persist
|
|
84
|
+
wallets.append(wallet)
|
|
85
|
+
saveWallets()
|
|
86
|
+
|
|
87
|
+
return WalletCreationResult(
|
|
88
|
+
wallet: wallet,
|
|
89
|
+
mnemonic: result.mnemonic,
|
|
90
|
+
privateKeyHex: result.privateKey,
|
|
91
|
+
jwkData: nil
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// MARK: - Import Wallet
|
|
96
|
+
|
|
97
|
+
/// Import a wallet from a mnemonic phrase
|
|
98
|
+
func importFromMnemonic(chain: Chain, mnemonic: String, name: String) async throws -> Wallet {
|
|
99
|
+
isLoading = true
|
|
100
|
+
defer { isLoading = false }
|
|
101
|
+
|
|
102
|
+
let result = try await cliBridge.importFromMnemonic(chain: chain, mnemonic: mnemonic, name: name)
|
|
103
|
+
|
|
104
|
+
let walletId = UUID()
|
|
105
|
+
let wallet = Wallet(
|
|
106
|
+
id: walletId,
|
|
107
|
+
name: name,
|
|
108
|
+
chain: chain,
|
|
109
|
+
address: result.address,
|
|
110
|
+
isImported: true,
|
|
111
|
+
derivationPath: chain.derivationPath,
|
|
112
|
+
hasMnemonicBackup: true
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
try keychain.saveMnemonic(mnemonic, forWalletId: walletId)
|
|
116
|
+
if let privateKey = result.privateKey {
|
|
117
|
+
try keychain.savePrivateKey(privateKey, forWalletId: walletId)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
wallets.append(wallet)
|
|
121
|
+
saveWallets()
|
|
122
|
+
return wallet
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Import a wallet from a private key
|
|
126
|
+
func importFromPrivateKey(chain: Chain, privateKey: String, name: String) async throws -> Wallet {
|
|
127
|
+
isLoading = true
|
|
128
|
+
defer { isLoading = false }
|
|
129
|
+
|
|
130
|
+
let result = try await cliBridge.importFromPrivateKey(chain: chain, privateKey: privateKey, name: name)
|
|
131
|
+
|
|
132
|
+
let walletId = UUID()
|
|
133
|
+
let wallet = Wallet(
|
|
134
|
+
id: walletId,
|
|
135
|
+
name: name,
|
|
136
|
+
chain: chain,
|
|
137
|
+
address: result.address,
|
|
138
|
+
isImported: true,
|
|
139
|
+
derivationPath: chain.derivationPath,
|
|
140
|
+
hasMnemonicBackup: false
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
try keychain.savePrivateKey(privateKey, forWalletId: walletId)
|
|
144
|
+
|
|
145
|
+
wallets.append(wallet)
|
|
146
|
+
saveWallets()
|
|
147
|
+
return wallet
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// Import an Arweave wallet from a JWK file
|
|
151
|
+
func importFromJWK(fileURL: URL, name: String) async throws -> Wallet {
|
|
152
|
+
isLoading = true
|
|
153
|
+
defer { isLoading = false }
|
|
154
|
+
|
|
155
|
+
let jwkData = try FileService.shared.readJWKFile(at: fileURL)
|
|
156
|
+
let result = try await cliBridge.importFromJWK(filePath: fileURL.path, name: name)
|
|
157
|
+
|
|
158
|
+
let walletId = UUID()
|
|
159
|
+
let wallet = Wallet(
|
|
160
|
+
id: walletId,
|
|
161
|
+
name: name,
|
|
162
|
+
chain: .arweave,
|
|
163
|
+
address: result.address,
|
|
164
|
+
isImported: true,
|
|
165
|
+
derivationPath: "",
|
|
166
|
+
hasMnemonicBackup: false
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
try keychain.saveJWK(jwkData, forWalletId: walletId)
|
|
170
|
+
|
|
171
|
+
wallets.append(wallet)
|
|
172
|
+
saveWallets()
|
|
173
|
+
return wallet
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// Import an ICP identity from a PEM file
|
|
177
|
+
func importFromPEM(fileURL: URL, name: String) async throws -> Wallet {
|
|
178
|
+
isLoading = true
|
|
179
|
+
defer { isLoading = false }
|
|
180
|
+
|
|
181
|
+
let pemContent = try FileService.shared.readPEMFile(at: fileURL)
|
|
182
|
+
let result = try await cliBridge.importFromPEM(filePath: fileURL.path, name: name)
|
|
183
|
+
|
|
184
|
+
let walletId = UUID()
|
|
185
|
+
let wallet = Wallet(
|
|
186
|
+
id: walletId,
|
|
187
|
+
name: name,
|
|
188
|
+
chain: .icp,
|
|
189
|
+
address: result.address,
|
|
190
|
+
isImported: true,
|
|
191
|
+
derivationPath: Chain.icp.derivationPath,
|
|
192
|
+
hasMnemonicBackup: false
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
try keychain.savePrivateKey(pemContent, forWalletId: walletId)
|
|
196
|
+
|
|
197
|
+
wallets.append(wallet)
|
|
198
|
+
saveWallets()
|
|
199
|
+
return wallet
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// Import an Ethereum wallet from a keystore JSON
|
|
203
|
+
func importFromKeystore(fileURL: URL, password: String, name: String) async throws -> Wallet {
|
|
204
|
+
isLoading = true
|
|
205
|
+
defer { isLoading = false }
|
|
206
|
+
|
|
207
|
+
_ = try FileService.shared.readKeystoreFile(at: fileURL)
|
|
208
|
+
let result = try await cliBridge.importFromKeystore(filePath: fileURL.path, password: password, name: name)
|
|
209
|
+
|
|
210
|
+
let walletId = UUID()
|
|
211
|
+
let wallet = Wallet(
|
|
212
|
+
id: walletId,
|
|
213
|
+
name: name,
|
|
214
|
+
chain: .ethereum,
|
|
215
|
+
address: result.address,
|
|
216
|
+
isImported: true,
|
|
217
|
+
derivationPath: Chain.ethereum.derivationPath,
|
|
218
|
+
hasMnemonicBackup: false
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if let privateKey = result.privateKey {
|
|
222
|
+
try keychain.savePrivateKey(privateKey, forWalletId: walletId)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
wallets.append(wallet)
|
|
226
|
+
saveWallets()
|
|
227
|
+
return wallet
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// MARK: - Wallet Management
|
|
231
|
+
|
|
232
|
+
/// Delete a wallet and all its associated secrets
|
|
233
|
+
func deleteWallet(_ wallet: Wallet) {
|
|
234
|
+
keychain.deleteAllSecrets(forWalletId: wallet.id)
|
|
235
|
+
wallets.removeAll { $0.id == wallet.id }
|
|
236
|
+
if selectedWalletId == wallet.id {
|
|
237
|
+
selectedWalletId = nil
|
|
238
|
+
}
|
|
239
|
+
saveWallets()
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/// Rename a wallet
|
|
243
|
+
func renameWallet(_ wallet: Wallet, to newName: String) {
|
|
244
|
+
guard let index = wallets.firstIndex(where: { $0.id == wallet.id }) else { return }
|
|
245
|
+
wallets[index].name = newName
|
|
246
|
+
saveWallets()
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/// Refresh balance for a single wallet
|
|
250
|
+
func refreshBalance(for wallet: Wallet) async {
|
|
251
|
+
guard let index = wallets.firstIndex(where: { $0.id == wallet.id }) else { return }
|
|
252
|
+
|
|
253
|
+
do {
|
|
254
|
+
let balance = try await cliBridge.getBalance(chain: wallet.chain, address: wallet.address)
|
|
255
|
+
wallets[index].cachedBalance = balance
|
|
256
|
+
wallets[index].balanceLastUpdated = Date()
|
|
257
|
+
saveWallets()
|
|
258
|
+
} catch {
|
|
259
|
+
// Silently fail balance refresh — don't disrupt the UI
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/// Refresh balances for all wallets
|
|
264
|
+
func refreshAllBalances() async {
|
|
265
|
+
isLoading = true
|
|
266
|
+
defer { isLoading = false }
|
|
267
|
+
|
|
268
|
+
await withTaskGroup(of: Void.self) { group in
|
|
269
|
+
for wallet in wallets {
|
|
270
|
+
group.addTask { [weak self] in
|
|
271
|
+
await self?.refreshBalance(for: wallet)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/// Check if we have the mnemonic for a given wallet
|
|
278
|
+
func hasMnemonic(for wallet: Wallet) -> Bool {
|
|
279
|
+
keychain.hasMnemonic(forWalletId: wallet.id)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/// Retrieve mnemonic for a wallet (requires confirmation)
|
|
283
|
+
func getMnemonic(for wallet: Wallet) -> String? {
|
|
284
|
+
try? keychain.getMnemonic(forWalletId: wallet.id)
|
|
285
|
+
}
|
|
286
|
+
}
|