From d00e707015134fa10a17ed4724cad2d7c83bfaea Mon Sep 17 00:00:00 2001 From: gemcoder21 <104884878+gemcoder21@users.noreply.github.com> Date: Thu, 22 Jan 2026 00:31:17 +0000 Subject: [PATCH 01/10] Add externalId to Wallet and migrate wallet IDs Introduces an externalId field to the Wallet model and related records, updates all usages to support the new identifier, and adds a migration to convert existing wallet IDs to a new WalletIdentifier format. Also updates child table references and current wallet preference accordingly. AssetMarket and PriceRecord are updated to support new all-time high/low percentage fields. --- Packages/Keystore/Sources/LocalKeystore.swift | 8 +- .../Sources/Store/WalletKeyStore.swift | 8 +- Packages/Primitives/Sources/AssetPrice.swift | 6 +- .../Extensions/Wallet+Primitives.swift | 1 + Packages/Primitives/Sources/Wallet.swift | 4 +- .../Sources/Components/WalletBarView.swift | 1 + Packages/Store/Package.resolved | 20 +++- Packages/Store/Sources/Migrations.swift | 10 ++ .../Migrations/WalletIdMigration.swift | 113 ++++++++++++++++++ .../Store/Sources/Models/BannerRecord.swift | 2 +- .../Store/Sources/Models/PriceRecord.swift | 6 +- .../Store/Sources/Models/WalletRecord.swift | 3 + .../Store/Sources/Stores/WalletStore.swift | 4 +- .../Sources/Types/WalletRecordInfo.swift | 1 + core | 2 +- 15 files changed, 174 insertions(+), 15 deletions(-) create mode 100644 Packages/Store/Sources/Migrations/WalletIdMigration.swift diff --git a/Packages/Keystore/Sources/LocalKeystore.swift b/Packages/Keystore/Sources/LocalKeystore.swift index d4053cce6..620dedd22 100644 --- a/Packages/Keystore/Sources/LocalKeystore.swift +++ b/Packages/Keystore/Sources/LocalKeystore.swift @@ -86,7 +86,7 @@ public final class LocalKeystore: Keystore, @unchecked Sendable { let password = try await getPassword() try await queue.asyncTask { [walletKeyStore] in do { - try walletKeyStore.deleteWallet(id: wallet.id, password: password) + try walletKeyStore.deleteWallet(id: wallet.keystoreId, password: password) } catch let error as KeystoreError { // in some cases wallet already deleted, just ignore switch error { @@ -107,7 +107,7 @@ public final class LocalKeystore: Keystore, @unchecked Sendable { public func getPrivateKey(wallet: Primitives.Wallet, chain: Chain) async throws -> Data { let password = try await getPassword() return try await queue.asyncTask { [walletKeyStore] in - try walletKeyStore.getPrivateKey(id: wallet.id, type: wallet.type, chain: chain, password: password) + try walletKeyStore.getPrivateKey(id: wallet.keystoreId, type: wallet.type, chain: chain, password: password) } } @@ -127,7 +127,7 @@ public final class LocalKeystore: Keystore, @unchecked Sendable { public func getMnemonic(wallet: Primitives.Wallet) async throws -> [String] { let password = try await getPassword() return try await queue.asyncTask { [walletKeyStore] in - try walletKeyStore.getMnemonic(wallet: wallet, password: password) + try walletKeyStore.getMnemonic(walletId: wallet.keystoreId, password: password) } } @@ -140,7 +140,7 @@ public final class LocalKeystore: Keystore, @unchecked Sendable { return try await queue.asyncTask { [walletKeyStore] in try walletKeyStore.sign( hash: hash, - walletId: wallet.id, + walletId: wallet.keystoreId, type: wallet.type, password: password, chain: chain diff --git a/Packages/Keystore/Sources/Store/WalletKeyStore.swift b/Packages/Keystore/Sources/Store/WalletKeyStore.swift index ef3af1d0d..1802657d7 100644 --- a/Packages/Keystore/Sources/Store/WalletKeyStore.swift +++ b/Packages/Keystore/Sources/Store/WalletKeyStore.swift @@ -102,6 +102,7 @@ public struct WalletKeyStore: Sendable { ) return Primitives.Wallet( id: wallet.id, + externalId: nil, name: wallet.key.name, index: 0, type: .privateKey, @@ -146,6 +147,7 @@ public struct WalletKeyStore: Sendable { return Wallet( id: wallet.id, + externalId: nil, name: wallet.key.name, index: 0, type: type, @@ -165,7 +167,7 @@ public struct WalletKeyStore: Sendable { ) throws -> Primitives.Wallet { try addCoins( type: wallet.type, - wallet: try getWallet(id: wallet.id), + wallet: try getWallet(id: wallet.keystoreId), existingChains: existingChains, newChains: newChains, password: password, @@ -207,8 +209,8 @@ public struct WalletKeyStore: Sendable { } } - func getMnemonic(wallet: Primitives.Wallet, password: String) throws -> [String] { - let wallet = try getWallet(id: wallet.id) + func getMnemonic(walletId: String, password: String) throws -> [String] { + let wallet = try getWallet(id: walletId) guard let hdwallet = wallet.key.wallet(password: Data(password.utf8)) else { diff --git a/Packages/Primitives/Sources/AssetPrice.swift b/Packages/Primitives/Sources/AssetPrice.swift index d84843637..bc742708b 100644 --- a/Packages/Primitives/Sources/AssetPrice.swift +++ b/Packages/Primitives/Sources/AssetPrice.swift @@ -14,10 +14,12 @@ public struct AssetMarket: Codable, Equatable, Sendable { public let maxSupply: Double? public let allTimeHigh: Double? public let allTimeHighDate: Date? + public let allTimeHighChangePercentage: Double? public let allTimeLow: Double? public let allTimeLowDate: Date? + public let allTimeLowChangePercentage: Double? - public init(marketCap: Double?, marketCapFdv: Double?, marketCapRank: Int32?, totalVolume: Double?, circulatingSupply: Double?, totalSupply: Double?, maxSupply: Double?, allTimeHigh: Double?, allTimeHighDate: Date?, allTimeLow: Double?, allTimeLowDate: Date?) { + public init(marketCap: Double?, marketCapFdv: Double?, marketCapRank: Int32?, totalVolume: Double?, circulatingSupply: Double?, totalSupply: Double?, maxSupply: Double?, allTimeHigh: Double?, allTimeHighDate: Date?, allTimeHighChangePercentage: Double?, allTimeLow: Double?, allTimeLowDate: Date?, allTimeLowChangePercentage: Double?) { self.marketCap = marketCap self.marketCapFdv = marketCapFdv self.marketCapRank = marketCapRank @@ -27,8 +29,10 @@ public struct AssetMarket: Codable, Equatable, Sendable { self.maxSupply = maxSupply self.allTimeHigh = allTimeHigh self.allTimeHighDate = allTimeHighDate + self.allTimeHighChangePercentage = allTimeHighChangePercentage self.allTimeLow = allTimeLow self.allTimeLowDate = allTimeLowDate + self.allTimeLowChangePercentage = allTimeLowChangePercentage } } diff --git a/Packages/Primitives/Sources/Extensions/Wallet+Primitives.swift b/Packages/Primitives/Sources/Extensions/Wallet+Primitives.swift index aacf1d80f..51c791f1d 100644 --- a/Packages/Primitives/Sources/Extensions/Wallet+Primitives.swift +++ b/Packages/Primitives/Sources/Extensions/Wallet+Primitives.swift @@ -53,6 +53,7 @@ public extension Wallet { static func makeView(name: String, chain: Chain, address: String) -> Wallet { return Wallet( id: NSUUID().uuidString, + externalId: nil, name: name, index: 0, type: .view, diff --git a/Packages/Primitives/Sources/Wallet.swift b/Packages/Primitives/Sources/Wallet.swift index 7d7dbbd16..a6dce34af 100644 --- a/Packages/Primitives/Sources/Wallet.swift +++ b/Packages/Primitives/Sources/Wallet.swift @@ -11,6 +11,7 @@ public enum WalletSource: String, Codable, Equatable, Hashable, Sendable { public struct Wallet: Codable, Equatable, Hashable, Sendable { public let id: String + public let externalId: String? public let name: String public let index: Int32 public let type: WalletType @@ -20,8 +21,9 @@ public struct Wallet: Codable, Equatable, Hashable, Sendable { public let imageUrl: String? public let source: WalletSource - public init(id: String, name: String, index: Int32, type: WalletType, accounts: [Account], order: Int32, isPinned: Bool, imageUrl: String?, source: WalletSource) { + public init(id: String, externalId: String?, name: String, index: Int32, type: WalletType, accounts: [Account], order: Int32, isPinned: Bool, imageUrl: String?, source: WalletSource) { self.id = id + self.externalId = externalId self.name = name self.index = index self.type = type diff --git a/Packages/PrimitivesComponents/Sources/Components/WalletBarView.swift b/Packages/PrimitivesComponents/Sources/Components/WalletBarView.swift index 5cd521ada..5ef415440 100644 --- a/Packages/PrimitivesComponents/Sources/Components/WalletBarView.swift +++ b/Packages/PrimitivesComponents/Sources/Components/WalletBarView.swift @@ -49,6 +49,7 @@ public struct WalletBarView: View { name: WalletViewModel( wallet: .init( id: "", + externalId: nil, name: "Wallet #1", index: 1, type: .multicoin, diff --git a/Packages/Store/Package.resolved b/Packages/Store/Package.resolved index 36b3d58eb..97b849f2f 100644 --- a/Packages/Store/Package.resolved +++ b/Packages/Store/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "10fbc1c37eb400d548a9d41c22ed3508d3bad39e8c23dce0d075c9e5393a0a66", + "originHash" : "b6a3816c22734fc6e771530fbc907f0f3825a8c1fa1ad7a24bd3ce44182dcb49", "pins" : [ { "identity" : "bigint", @@ -9,6 +9,24 @@ "revision" : "e07e00fa1fd435143a2dcf8b7eec9a7710b2fdfe", "version" : "5.7.0" } + }, + { + "identity" : "grdb.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/groue/GRDB.swift.git", + "state" : { + "revision" : "aa0079aeb82a4bf00324561a40bffe68c6fe1c26", + "version" : "7.9.0" + } + }, + { + "identity" : "grdbquery", + "kind" : "remoteSourceControl", + "location" : "https://github.com/groue/GRDBQuery.git", + "state" : { + "revision" : "540dc48e86af2972b4f1815616aa1ed8ac97845a", + "version" : "0.11.0" + } } ], "version" : 3 diff --git a/Packages/Store/Sources/Migrations.swift b/Packages/Store/Sources/Migrations.swift index a344f38af..71c65d178 100644 --- a/Packages/Store/Sources/Migrations.swift +++ b/Packages/Store/Sources/Migrations.swift @@ -364,6 +364,16 @@ public struct Migrations { try? NotificationRecord.create(db: db) } + migrator.registerMigration("Add externalId to \(WalletRecord.databaseTableName)") { db in + try? db.alter(table: WalletRecord.databaseTableName) { + $0.add(column: WalletRecord.Columns.externalId.name, .text) + } + } + + migrator.registerMigration("Migrate wallet IDs to WalletIdentifier format") { db in + try WalletIdMigration.migrate(db: db) + } + try migrator.migrate(dbQueue) } } diff --git a/Packages/Store/Sources/Migrations/WalletIdMigration.swift b/Packages/Store/Sources/Migrations/WalletIdMigration.swift new file mode 100644 index 000000000..48147e5b6 --- /dev/null +++ b/Packages/Store/Sources/Migrations/WalletIdMigration.swift @@ -0,0 +1,113 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +import Foundation +import GRDB +import Primitives + +struct WalletIdMigration { + + private static let childTables = [ + AccountRecord.databaseTableName, + BalanceRecord.databaseTableName, + BannerRecord.databaseTableName, + NFTAssetAssociationRecord.databaseTableName, + NotificationRecord.databaseTableName, + PerpetualPositionRecord.databaseTableName, + RecentActivityRecord.databaseTableName, + StakeDelegationRecord.databaseTableName, + TransactionRecord.databaseTableName, + WalletConnectionRecord.databaseTableName, + ] + + private static let currentWalletKey = "currentWallet" + + static func migrate(db: Database) throws { + // Step 1: Build wallet mappings (oldId -> newId) + let mappings = try buildWalletMappings(db: db) + + // Step 2: Find and delete duplicates (keep wallet with lowest order) + let groups = Dictionary(grouping: mappings, by: { $0.newId }) + for (_, wallets) in groups where wallets.count > 1 { + let sorted = wallets.sorted { $0.order < $1.order } + let losers = sorted.dropFirst() + for loser in losers { + try db.execute( + sql: "DELETE FROM \(WalletRecord.databaseTableName) WHERE id = ?", + arguments: [loser.oldId] + ) + } + } + + // Step 3: Rename remaining wallets to new ID format + let remainingMappings = try buildWalletMappings(db: db) + for mapping in remainingMappings where mapping.oldId != mapping.newId { + // Update child table references FIRST (before wallet ID changes) + for table in childTables { + try db.execute( + sql: "UPDATE \(table) SET walletId = ? WHERE walletId = ?", + arguments: [mapping.newId, mapping.oldId] + ) + } + + // Update wallet ID + try db.execute(sql: """ + UPDATE \(WalletRecord.databaseTableName) SET externalId = id, id = ? WHERE id = ? + """, arguments: [mapping.newId, mapping.oldId]) + } + + // Step 4: Migrate currentWalletId preference + migrateCurrentWalletPreference(mappings: remainingMappings) + } + + private static func migrateCurrentWalletPreference(mappings: [WalletMapping]) { + let firstWallet = mappings.min(by: { $0.order < $1.order }) + + guard let currentId = UserDefaults.standard.string(forKey: currentWalletKey) else { + if let first = firstWallet { + UserDefaults.standard.set(first.newId, forKey: currentWalletKey) + } + return + } + + if let mapping = mappings.first(where: { $0.oldId == currentId && $0.oldId != $0.newId }) { + UserDefaults.standard.set(mapping.newId, forKey: currentWalletKey) + } else if let first = firstWallet { + UserDefaults.standard.set(first.newId, forKey: currentWalletKey) + } + } + + private static func buildWalletMappings(db: Database) throws -> [WalletMapping] { + try Row.fetchAll(db, sql: """ + SELECT w.id, w.type, w."order", + (SELECT a.address FROM \(AccountRecord.databaseTableName) a + WHERE a.walletId = w.id AND a.chain = 'ethereum' LIMIT 1) as ethereumAddress, + (SELECT a.address FROM \(AccountRecord.databaseTableName) a + WHERE a.walletId = w.id LIMIT 1) as address, + (SELECT a.chain FROM \(AccountRecord.databaseTableName) a + WHERE a.walletId = w.id LIMIT 1) as chain + FROM \(WalletRecord.databaseTableName) w + """).compactMap { row -> WalletMapping? in + guard let oldId: String = row["id"], + let typeRaw: String = row["type"], + let type = WalletType(rawValue: typeRaw) else { return nil } + + let newId: String + switch type { + case .multicoin: + guard let address: String = row["ethereumAddress"] else { return nil } + newId = "multicoin_\(address)" + case .single, .privateKey, .view: + guard let address: String = row["address"], + let chain: String = row["chain"] else { return nil } + newId = "\(type.rawValue)_\(chain)_\(address)" + } + return WalletMapping(oldId: oldId, newId: newId, order: row["order"] ?? 0) + } + } + + struct WalletMapping { + let oldId: String + let newId: String + let order: Int + } +} diff --git a/Packages/Store/Sources/Models/BannerRecord.swift b/Packages/Store/Sources/Models/BannerRecord.swift index 833f1cc7b..6ca659ccc 100644 --- a/Packages/Store/Sources/Models/BannerRecord.swift +++ b/Packages/Store/Sources/Models/BannerRecord.swift @@ -75,7 +75,7 @@ extension NewBanner { var record: BannerRecord { let wallet: Wallet? = { if let walletId { - return Wallet(id: walletId, name: "", index: 0, type: .multicoin, accounts: [], order: 0, isPinned: false, imageUrl: nil, source: .create) + return Wallet(id: walletId, externalId: nil, name: "", index: 0, type: .multicoin, accounts: [], order: 0, isPinned: false, imageUrl: nil, source: .create) } return .none }() diff --git a/Packages/Store/Sources/Models/PriceRecord.swift b/Packages/Store/Sources/Models/PriceRecord.swift index b3b11dbe7..37e79605b 100644 --- a/Packages/Store/Sources/Models/PriceRecord.swift +++ b/Packages/Store/Sources/Models/PriceRecord.swift @@ -109,9 +109,11 @@ extension PriceRecord { totalSupply: totalSupply, maxSupply: maxSupply, allTimeHigh: .none, - allTimeHighDate: .now, + allTimeHighDate: .none, + allTimeHighChangePercentage: .none, allTimeLow: .none, - allTimeLowDate: .none + allTimeLowDate: .none, + allTimeLowChangePercentage: .none ) } } diff --git a/Packages/Store/Sources/Models/WalletRecord.swift b/Packages/Store/Sources/Models/WalletRecord.swift index 863f46bd0..87cae0750 100644 --- a/Packages/Store/Sources/Models/WalletRecord.swift +++ b/Packages/Store/Sources/Models/WalletRecord.swift @@ -9,6 +9,7 @@ public struct WalletRecord: Codable, TableRecord, FetchableRecord, PersistableRe public struct Columns { static let id = Column("id") + static let externalId = Column("externalId") static let name = Column("name") static let index = Column("index") static let type = Column("type") @@ -20,6 +21,7 @@ public struct WalletRecord: Codable, TableRecord, FetchableRecord, PersistableRe } public var id: String + public var externalId: String? public var name: String public var type: WalletType public var index: Int @@ -39,6 +41,7 @@ extension WalletRecord: CreateTable { $0.column(Columns.id.name, .text) .primaryKey() .notNull() + $0.column(Columns.externalId.name, .text) $0.column(Columns.name.name, .text) .notNull() $0.column(Columns.type.name, .text) diff --git a/Packages/Store/Sources/Stores/WalletStore.swift b/Packages/Store/Sources/Stores/WalletStore.swift index 5ee19d6a9..b5d248d79 100644 --- a/Packages/Store/Sources/Stores/WalletStore.swift +++ b/Packages/Store/Sources/Stores/WalletStore.swift @@ -130,6 +130,7 @@ extension WalletRecord { func mapToWallet() -> Wallet { return Wallet( id: id, + externalId: externalId, name: name, index: index.asInt32, type: type, @@ -146,8 +147,9 @@ extension Wallet { var record: WalletRecord { return WalletRecord( id: id, + externalId: externalId, name: name, - type: type, + type: type, index: index.asInt, order: 0, isPinned: false, diff --git a/Packages/Store/Sources/Types/WalletRecordInfo.swift b/Packages/Store/Sources/Types/WalletRecordInfo.swift index 1d055fb10..8435a6ce5 100644 --- a/Packages/Store/Sources/Types/WalletRecordInfo.swift +++ b/Packages/Store/Sources/Types/WalletRecordInfo.swift @@ -13,6 +13,7 @@ extension WalletRecordInfo { func mapToWallet() -> Wallet? { return Wallet( id: wallet.id, + externalId: wallet.externalId, name: wallet.name, index: wallet.index.asInt32, type: wallet.type, diff --git a/core b/core index 8a7f7c046..7cc3801f1 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 8a7f7c046b25d8b801022e93fd57041553f3b0f4 +Subproject commit 7cc3801f1e45995d5dbbda3d92bdbdc4d404ab9a From 24044261d011aa8a679c852ad65654b6c1f1af30 Mon Sep 17 00:00:00 2001 From: gemcoder21 <104884878+gemcoder21@users.noreply.github.com> Date: Thu, 22 Jan 2026 00:34:00 +0000 Subject: [PATCH 02/10] Add keystoreId computed property to Wallet extension Introduces a keystoreId property to the Wallet extension, returning externalId if available, otherwise falling back to id. --- .../Keystore/Sources/Extensions/Wallet+Keystore.swift | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 Packages/Keystore/Sources/Extensions/Wallet+Keystore.swift diff --git a/Packages/Keystore/Sources/Extensions/Wallet+Keystore.swift b/Packages/Keystore/Sources/Extensions/Wallet+Keystore.swift new file mode 100644 index 000000000..6203d2064 --- /dev/null +++ b/Packages/Keystore/Sources/Extensions/Wallet+Keystore.swift @@ -0,0 +1,9 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +import Primitives + +extension Wallet { + var keystoreId: String { + externalId ?? id + } +} From 89ef533e8216b1ea7bf63ceecbd642f685b63893 Mon Sep 17 00:00:00 2001 From: gemcoder21 <104884878+gemcoder21@users.noreply.github.com> Date: Thu, 22 Jan 2026 01:38:20 +0000 Subject: [PATCH 03/10] Add WalletIdMigration tests and update migration logic Introduces comprehensive tests for WalletIdMigration covering various wallet types, duplicate handling, child table updates, and preference migration. Updates Wallet.mock to support externalId and refines migration logic to temporarily disable foreign key constraints during ID updates. --- .../TestKit/Wallet+PrimitivesTestKit.swift | 2 + .../Migrations/WalletIdMigration.swift | 14 +- .../StoreTests/WalletIdMigrationTests.swift | 497 ++++++++++++++++++ 3 files changed, 507 insertions(+), 6 deletions(-) create mode 100644 Packages/Store/Tests/StoreTests/WalletIdMigrationTests.swift diff --git a/Packages/Primitives/TestKit/Wallet+PrimitivesTestKit.swift b/Packages/Primitives/TestKit/Wallet+PrimitivesTestKit.swift index 743491a73..655548fa9 100644 --- a/Packages/Primitives/TestKit/Wallet+PrimitivesTestKit.swift +++ b/Packages/Primitives/TestKit/Wallet+PrimitivesTestKit.swift @@ -6,6 +6,7 @@ import Primitives public extension Wallet { static func mock( id: String = "", + externalId: String? = nil, name: String = "", type: WalletType = .multicoin, accounts: [Account] = [], @@ -15,6 +16,7 @@ public extension Wallet { ) -> Wallet { Wallet( id: id, + externalId: externalId, name: name, index: index.asInt32, type: type, diff --git a/Packages/Store/Sources/Migrations/WalletIdMigration.swift b/Packages/Store/Sources/Migrations/WalletIdMigration.swift index 48147e5b6..db4696cf8 100644 --- a/Packages/Store/Sources/Migrations/WalletIdMigration.swift +++ b/Packages/Store/Sources/Migrations/WalletIdMigration.swift @@ -39,22 +39,24 @@ struct WalletIdMigration { } // Step 3: Rename remaining wallets to new ID format + // Temporarily disable FK constraints for existing databases without onUpdate: .cascade + try db.execute(sql: "PRAGMA foreign_keys = OFF") + let remainingMappings = try buildWalletMappings(db: db) for mapping in remainingMappings where mapping.oldId != mapping.newId { - // Update child table references FIRST (before wallet ID changes) + try db.execute(sql: "UPDATE \(WalletRecord.databaseTableName) SET externalId = id, id = ? WHERE id = ?", arguments: [mapping.newId, mapping.oldId]) + + // Update child table references for table in childTables { try db.execute( sql: "UPDATE \(table) SET walletId = ? WHERE walletId = ?", arguments: [mapping.newId, mapping.oldId] ) } - - // Update wallet ID - try db.execute(sql: """ - UPDATE \(WalletRecord.databaseTableName) SET externalId = id, id = ? WHERE id = ? - """, arguments: [mapping.newId, mapping.oldId]) } + try db.execute(sql: "PRAGMA foreign_keys = ON") + // Step 4: Migrate currentWalletId preference migrateCurrentWalletPreference(mappings: remainingMappings) } diff --git a/Packages/Store/Tests/StoreTests/WalletIdMigrationTests.swift b/Packages/Store/Tests/StoreTests/WalletIdMigrationTests.swift new file mode 100644 index 000000000..efdb7efb2 --- /dev/null +++ b/Packages/Store/Tests/StoreTests/WalletIdMigrationTests.swift @@ -0,0 +1,497 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +import Testing +import Foundation +import GRDB +import Primitives +import PrimitivesTestKit +import StoreTestKit +@testable import Store + +private func setOrder(db: DB, walletId: String, order: Int) throws { + try db.dbQueue.write { db in + try db.execute(sql: "UPDATE wallets SET \"order\" = ? WHERE id = ?", arguments: [order, walletId]) + } +} + +@Suite(.serialized) +struct WalletIdMigrationTests { + + private let currentWalletKey = "currentWallet" + + @Test + func migrateMulticoinWallet() throws { + UserDefaults.standard.removeObject(forKey: currentWalletKey) + let db = DB.mock() + let walletStore = WalletStore(db: db) + + let oldId = "uuid-multicoin-1" + let ethAddress = "0x1234567890abcdef" + let wallet = Wallet.mock( + id: oldId, + type: .multicoin, + accounts: [.mock(chain: .ethereum, address: ethAddress), .mock(chain: .bitcoin)] + ) + try walletStore.addWallet(wallet) + + try db.dbQueue.write { db in + try WalletIdMigration.migrate(db: db) + } + + let wallets = try walletStore.getWallets() + #expect(wallets.count == 1) + #expect(wallets.first?.id == "multicoin_\(ethAddress)") + #expect(wallets.first?.externalId == oldId) + } + + @Test + func migrateViewWallet() throws { + UserDefaults.standard.removeObject(forKey: currentWalletKey) + let db = DB.mock() + let walletStore = WalletStore(db: db) + + let oldId = "uuid-view-1" + let address = "0xviewaddress" + let wallet = Wallet.mock( + id: oldId, + type: .view, + accounts: [.mock(chain: .ethereum, address: address)] + ) + try walletStore.addWallet(wallet) + + try db.dbQueue.write { db in + try WalletIdMigration.migrate(db: db) + } + + let wallets = try walletStore.getWallets() + #expect(wallets.count == 1) + #expect(wallets.first?.id == "view_ethereum_\(address)") + #expect(wallets.first?.externalId == oldId) + } + + @Test + func migrateSingleWallet() throws { + UserDefaults.standard.removeObject(forKey: currentWalletKey) + let db = DB.mock() + let walletStore = WalletStore(db: db) + + let oldId = "uuid-single-1" + let address = "bc1qsingleaddress" + let wallet = Wallet.mock( + id: oldId, + type: .single, + accounts: [.mock(chain: .bitcoin, address: address)] + ) + try walletStore.addWallet(wallet) + + try db.dbQueue.write { db in + try WalletIdMigration.migrate(db: db) + } + + let wallets = try walletStore.getWallets() + #expect(wallets.count == 1) + #expect(wallets.first?.id == "single_bitcoin_\(address)") + #expect(wallets.first?.externalId == oldId) + } + + @Test + func migratePrivateKeyWallet() throws { + UserDefaults.standard.removeObject(forKey: currentWalletKey) + let db = DB.mock() + let walletStore = WalletStore(db: db) + + let oldId = "uuid-pk-1" + let address = "0xprivatekey" + let wallet = Wallet.mock( + id: oldId, + type: .privateKey, + accounts: [.mock(chain: .ethereum, address: address)] + ) + try walletStore.addWallet(wallet) + + try db.dbQueue.write { db in + try WalletIdMigration.migrate(db: db) + } + + let wallets = try walletStore.getWallets() + #expect(wallets.count == 1) + #expect(wallets.first?.id == "privateKey_ethereum_\(address)") + #expect(wallets.first?.externalId == oldId) + } + + @Test + func removeDuplicateMulticoinWallets() throws { + UserDefaults.standard.removeObject(forKey: currentWalletKey) + let db = DB.mock() + let walletStore = WalletStore(db: db) + + let ethAddress = "0xsameaddress" + + let wallet1 = Wallet.mock( + id: "uuid-1", + type: .multicoin, + accounts: [.mock(chain: .ethereum, address: ethAddress)], + order: 0 + ) + let wallet2 = Wallet.mock( + id: "uuid-2", + type: .multicoin, + accounts: [.mock(chain: .ethereum, address: ethAddress)], + order: 1 + ) + let wallet3 = Wallet.mock( + id: "uuid-3", + type: .multicoin, + accounts: [.mock(chain: .ethereum, address: ethAddress)], + order: 2 + ) + + try walletStore.addWallet(wallet1) + try walletStore.addWallet(wallet2) + try walletStore.addWallet(wallet3) + + try db.dbQueue.write { db in + try WalletIdMigration.migrate(db: db) + } + + let wallets = try walletStore.getWallets() + #expect(wallets.count == 1) + #expect(wallets.first?.id == "multicoin_\(ethAddress)") + #expect(wallets.first?.externalId == "uuid-1") + } + + @Test + func mixedWalletTypes() throws { + UserDefaults.standard.removeObject(forKey: currentWalletKey) + let db = DB.mock() + let walletStore = WalletStore(db: db) + + let multicoin = Wallet.mock( + id: "uuid-multicoin", + type: .multicoin, + accounts: [.mock(chain: .ethereum, address: "0xmulti")], + order: 0 + ) + let single = Wallet.mock( + id: "uuid-single", + type: .single, + accounts: [.mock(chain: .bitcoin, address: "bc1single")], + order: 1 + ) + let view = Wallet.mock( + id: "uuid-view", + type: .view, + accounts: [.mock(chain: .solana, address: "solview")], + order: 2 + ) + + try walletStore.addWallet(multicoin) + try walletStore.addWallet(single) + try walletStore.addWallet(view) + + try db.dbQueue.write { db in + try WalletIdMigration.migrate(db: db) + } + + let wallets = try walletStore.getWallets() + #expect(wallets.count == 3) + + let ids = Set(wallets.map { $0.id }) + #expect(ids.contains("multicoin_0xmulti")) + #expect(ids.contains("single_bitcoin_bc1single")) + #expect(ids.contains("view_solana_solview")) + } + + @Test + func updateChildTableReferences() throws { + UserDefaults.standard.removeObject(forKey: currentWalletKey) + let db = DB.mock() + let walletStore = WalletStore(db: db) + let balanceStore = BalanceStore(db: db) + let assetStore = AssetStore(db: db) + + let oldId = "uuid-with-balances" + let ethAddress = "0xwithbalances" + let wallet = Wallet.mock( + id: oldId, + type: .multicoin, + accounts: [.mock(chain: .ethereum, address: ethAddress)] + ) + try walletStore.addWallet(wallet) + + let asset = AssetBasic.mock(asset: .mock(id: .mockEthereum())) + try assetStore.add(assets: [asset]) + try balanceStore.addBalance([AddBalance(assetId: asset.asset.id, isEnabled: true)], for: wallet.walletId) + + try db.dbQueue.write { db in + try WalletIdMigration.migrate(db: db) + } + + let newWalletId = WalletId(id: "multicoin_\(ethAddress)") + let balances = try balanceStore.getBalances(walletId: newWalletId, assetIds: [asset.asset.id]) + #expect(balances.count == 1) + } + + @Test + func multipleDuplicateGroups() throws { + UserDefaults.standard.removeObject(forKey: currentWalletKey) + let db = DB.mock() + let walletStore = WalletStore(db: db) + + // Group 1: 3 wallets with same eth address + let address1 = "0xaddress1" + try walletStore.addWallet(.mock(id: "uuid-1a", type: .multicoin, accounts: [.mock(chain: .ethereum, address: address1)], order: 0)) + try walletStore.addWallet(.mock(id: "uuid-1b", type: .multicoin, accounts: [.mock(chain: .ethereum, address: address1)], order: 1)) + try walletStore.addWallet(.mock(id: "uuid-1c", type: .multicoin, accounts: [.mock(chain: .ethereum, address: address1)], order: 2)) + + // Group 2: 2 wallets with different eth address + let address2 = "0xaddress2" + try walletStore.addWallet(.mock(id: "uuid-2a", type: .multicoin, accounts: [.mock(chain: .ethereum, address: address2)], order: 3)) + try walletStore.addWallet(.mock(id: "uuid-2b", type: .multicoin, accounts: [.mock(chain: .ethereum, address: address2)], order: 4)) + + // Unique wallet + let address3 = "0xaddress3" + try walletStore.addWallet(.mock(id: "uuid-3", type: .multicoin, accounts: [.mock(chain: .ethereum, address: address3)], order: 5)) + + try db.dbQueue.write { db in + try WalletIdMigration.migrate(db: db) + } + + let wallets = try walletStore.getWallets() + #expect(wallets.count == 3) + + let ids = Set(wallets.map { $0.id }) + #expect(ids.contains("multicoin_\(address1)")) + #expect(ids.contains("multicoin_\(address2)")) + #expect(ids.contains("multicoin_\(address3)")) + + // Verify correct wallet was kept (lowest order) + let wallet1 = wallets.first { $0.id == "multicoin_\(address1)" } + #expect(wallet1?.externalId == "uuid-1a") + + let wallet2 = wallets.first { $0.id == "multicoin_\(address2)" } + #expect(wallet2?.externalId == "uuid-2a") + } + + @Test + func duplicateViewWallets() throws { + UserDefaults.standard.removeObject(forKey: currentWalletKey) + let db = DB.mock() + let walletStore = WalletStore(db: db) + + let address = "0xviewaddr" + try walletStore.addWallet(.mock(id: "uuid-view-1", type: .view, accounts: [.mock(chain: .ethereum, address: address)])) + try walletStore.addWallet(.mock(id: "uuid-view-2", type: .view, accounts: [.mock(chain: .ethereum, address: address)])) + try setOrder(db: db, walletId: "uuid-view-1", order: 1) + try setOrder(db: db, walletId: "uuid-view-2", order: 0) + + try db.dbQueue.write { db in + try WalletIdMigration.migrate(db: db) + } + + let wallets = try walletStore.getWallets() + #expect(wallets.count == 1) + #expect(wallets.first?.id == "view_ethereum_\(address)") + #expect(wallets.first?.externalId == "uuid-view-2") // lower order wins + } + + @Test + func duplicateSingleWallets() throws { + UserDefaults.standard.removeObject(forKey: currentWalletKey) + let db = DB.mock() + let walletStore = WalletStore(db: db) + + let address = "bc1qsingle" + try walletStore.addWallet(.mock(id: "uuid-single-1", type: .single, accounts: [.mock(chain: .bitcoin, address: address)])) + try walletStore.addWallet(.mock(id: "uuid-single-2", type: .single, accounts: [.mock(chain: .bitcoin, address: address)])) + try walletStore.addWallet(.mock(id: "uuid-single-3", type: .single, accounts: [.mock(chain: .bitcoin, address: address)])) + try setOrder(db: db, walletId: "uuid-single-1", order: 2) + try setOrder(db: db, walletId: "uuid-single-2", order: 0) + try setOrder(db: db, walletId: "uuid-single-3", order: 1) + + try db.dbQueue.write { db in + try WalletIdMigration.migrate(db: db) + } + + let wallets = try walletStore.getWallets() + #expect(wallets.count == 1) + #expect(wallets.first?.id == "single_bitcoin_\(address)") + #expect(wallets.first?.externalId == "uuid-single-2") // order 0 wins + } + + @Test + func keepWalletWithLowestOrder() throws { + UserDefaults.standard.removeObject(forKey: currentWalletKey) + let db = DB.mock() + let walletStore = WalletStore(db: db) + + let address = "0xordertest" + try walletStore.addWallet(.mock(id: "uuid-order-5", type: .multicoin, accounts: [.mock(chain: .ethereum, address: address)])) + try walletStore.addWallet(.mock(id: "uuid-order-2", type: .multicoin, accounts: [.mock(chain: .ethereum, address: address)])) + try walletStore.addWallet(.mock(id: "uuid-order-8", type: .multicoin, accounts: [.mock(chain: .ethereum, address: address)])) + try walletStore.addWallet(.mock(id: "uuid-order-1", type: .multicoin, accounts: [.mock(chain: .ethereum, address: address)])) + try walletStore.addWallet(.mock(id: "uuid-order-3", type: .multicoin, accounts: [.mock(chain: .ethereum, address: address)])) + try setOrder(db: db, walletId: "uuid-order-5", order: 5) + try setOrder(db: db, walletId: "uuid-order-2", order: 2) + try setOrder(db: db, walletId: "uuid-order-8", order: 8) + try setOrder(db: db, walletId: "uuid-order-1", order: 1) + try setOrder(db: db, walletId: "uuid-order-3", order: 3) + + try db.dbQueue.write { db in + try WalletIdMigration.migrate(db: db) + } + + let wallets = try walletStore.getWallets() + #expect(wallets.count == 1) + #expect(wallets.first?.externalId == "uuid-order-1") // lowest order wins + } + + @Test + func accountsUpdatedAfterMigration() throws { + UserDefaults.standard.removeObject(forKey: currentWalletKey) + let db = DB.mock() + let walletStore = WalletStore(db: db) + + let oldId = "uuid-accounts" + let ethAddress = "0xaccounts" + let btcAddress = "bc1accounts" + let wallet = Wallet.mock( + id: oldId, + type: .multicoin, + accounts: [ + .mock(chain: .ethereum, address: ethAddress), + .mock(chain: .bitcoin, address: btcAddress) + ] + ) + try walletStore.addWallet(wallet) + + try db.dbQueue.write { db in + try WalletIdMigration.migrate(db: db) + } + + let wallets = try walletStore.getWallets() + #expect(wallets.count == 1) + #expect(wallets.first?.accounts.count == 2) + + let chains = Set(wallets.first?.accounts.map { $0.chain } ?? []) + #expect(chains.contains(.ethereum)) + #expect(chains.contains(.bitcoin)) + } + + @Test + func walletAlreadyInNewFormat() throws { + UserDefaults.standard.removeObject(forKey: currentWalletKey) + let db = DB.mock() + let walletStore = WalletStore(db: db) + + // Wallet already has new format ID (simulating re-running migration) + let ethAddress = "0xalreadymigrated" + let newFormatId = "multicoin_\(ethAddress)" + let wallet = Wallet.mock( + id: newFormatId, + externalId: "old-uuid", + type: .multicoin, + accounts: [.mock(chain: .ethereum, address: ethAddress)] + ) + try walletStore.addWallet(wallet) + + try db.dbQueue.write { db in + try WalletIdMigration.migrate(db: db) + } + + let wallets = try walletStore.getWallets() + #expect(wallets.count == 1) + #expect(wallets.first?.id == newFormatId) + #expect(wallets.first?.externalId == "old-uuid") // preserved + } + + @Test + func emptyDatabase() throws { + UserDefaults.standard.removeObject(forKey: currentWalletKey) + let db = DB.mock() + + try db.dbQueue.write { db in + try WalletIdMigration.migrate(db: db) + } + + let walletStore = WalletStore(db: db) + let wallets = try walletStore.getWallets() + #expect(wallets.isEmpty) + } +} + +@Suite(.serialized) +struct WalletIdMigrationPreferenceTests { + + private let currentWalletKey = "currentWallet" + + @Test + func migrateCurrentWalletPreference() throws { + UserDefaults.standard.removeObject(forKey: currentWalletKey) + + let db = DB.mock() + let walletStore = WalletStore(db: db) + + let oldId = "uuid-current" + let ethAddress = "0xcurrent" + let wallet = Wallet.mock( + id: oldId, + type: .multicoin, + accounts: [.mock(chain: .ethereum, address: ethAddress)] + ) + try walletStore.addWallet(wallet) + + UserDefaults.standard.set(oldId, forKey: currentWalletKey) + + try db.dbQueue.write { db in + try WalletIdMigration.migrate(db: db) + } + + let newCurrentWalletId = UserDefaults.standard.string(forKey: currentWalletKey) + #expect(newCurrentWalletId == "multicoin_\(ethAddress)") + + UserDefaults.standard.removeObject(forKey: currentWalletKey) + } + + @Test + func setCurrentWalletWhenNoneSet() throws { + UserDefaults.standard.removeObject(forKey: currentWalletKey) + + let db = DB.mock() + let walletStore = WalletStore(db: db) + + try walletStore.addWallet(.mock(id: "uuid-first", type: .multicoin, accounts: [.mock(chain: .ethereum, address: "0xfirst")])) + try setOrder(db: db, walletId: "uuid-first", order: 0) + + try db.dbQueue.write { db in + try WalletIdMigration.migrate(db: db) + } + + let currentWalletId = UserDefaults.standard.string(forKey: currentWalletKey) + #expect(currentWalletId == "multicoin_0xfirst") + + UserDefaults.standard.removeObject(forKey: currentWalletKey) + } + + @Test + func fallbackCurrentWalletWhenInvalid() throws { + UserDefaults.standard.removeObject(forKey: currentWalletKey) + + let db = DB.mock() + let walletStore = WalletStore(db: db) + + UserDefaults.standard.set("deleted-wallet-id", forKey: currentWalletKey) + + try walletStore.addWallet(.mock(id: "uuid-fallback", type: .multicoin, accounts: [.mock(chain: .ethereum, address: "0xfallback")])) + try setOrder(db: db, walletId: "uuid-fallback", order: 0) + + try db.dbQueue.write { db in + try WalletIdMigration.migrate(db: db) + } + + let currentWalletId = UserDefaults.standard.string(forKey: currentWalletKey) + #expect(currentWalletId == "multicoin_0xfallback") + + UserDefaults.standard.removeObject(forKey: currentWalletKey) + } +} From 1adc3c66245b04cb7767ed0f9560750f5a4a42b8 Mon Sep 17 00:00:00 2001 From: gemcoder21 <104884878+gemcoder21@users.noreply.github.com> Date: Thu, 22 Jan 2026 21:06:29 +0000 Subject: [PATCH 04/10] Add wallet externalId and migrate wallet IDs Registers migrations to add an externalId column to the wallet table and to migrate wallet IDs to the WalletIdentifier format. --- Packages/Store/Sources/Migrations.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Packages/Store/Sources/Migrations.swift b/Packages/Store/Sources/Migrations.swift index c59ce7d8a..5bde25e03 100644 --- a/Packages/Store/Sources/Migrations.swift +++ b/Packages/Store/Sources/Migrations.swift @@ -375,6 +375,16 @@ public struct Migrations { } } + migrator.registerMigration("Add externalId to \(WalletRecord.databaseTableName)") { db in + try? db.alter(table: WalletRecord.databaseTableName) { + $0.add(column: WalletRecord.Columns.externalId.name, .text) + } + } + + migrator.registerMigration("Migrate wallet IDs to WalletIdentifier format") { db in + try WalletIdMigration.migrate(db: db) + } + try migrator.migrate(dbQueue) } } From 5a612e4e84e2a60848484c03a74b419154453ff1 Mon Sep 17 00:00:00 2001 From: gemcoder21 <104884878+gemcoder21@users.noreply.github.com> Date: Fri, 23 Jan 2026 18:12:12 +0000 Subject: [PATCH 05/10] Refactor WalletIdMigration to accept UserDefaults WalletIdMigration now takes a UserDefaults instance for preference migration, improving testability and flexibility. Tests updated to use a mock UserDefaults, and WalletStore gains a setOrder method for setting wallet order in tests. --- .../Migrations/WalletIdMigration.swift | 20 +-- .../Store/Sources/Stores/WalletStore.swift | 8 ++ .../StoreTests/WalletIdMigrationTests.swift | 133 ++++++++---------- 3 files changed, 76 insertions(+), 85 deletions(-) diff --git a/Packages/Store/Sources/Migrations/WalletIdMigration.swift b/Packages/Store/Sources/Migrations/WalletIdMigration.swift index db4696cf8..09a1bf8c5 100644 --- a/Packages/Store/Sources/Migrations/WalletIdMigration.swift +++ b/Packages/Store/Sources/Migrations/WalletIdMigration.swift @@ -21,11 +21,9 @@ struct WalletIdMigration { private static let currentWalletKey = "currentWallet" - static func migrate(db: Database) throws { - // Step 1: Build wallet mappings (oldId -> newId) + static func migrate(db: Database, userDefaults: UserDefaults = .standard) throws { let mappings = try buildWalletMappings(db: db) - // Step 2: Find and delete duplicates (keep wallet with lowest order) let groups = Dictionary(grouping: mappings, by: { $0.newId }) for (_, wallets) in groups where wallets.count > 1 { let sorted = wallets.sorted { $0.order < $1.order } @@ -38,15 +36,12 @@ struct WalletIdMigration { } } - // Step 3: Rename remaining wallets to new ID format - // Temporarily disable FK constraints for existing databases without onUpdate: .cascade try db.execute(sql: "PRAGMA foreign_keys = OFF") let remainingMappings = try buildWalletMappings(db: db) for mapping in remainingMappings where mapping.oldId != mapping.newId { try db.execute(sql: "UPDATE \(WalletRecord.databaseTableName) SET externalId = id, id = ? WHERE id = ?", arguments: [mapping.newId, mapping.oldId]) - // Update child table references for table in childTables { try db.execute( sql: "UPDATE \(table) SET walletId = ? WHERE walletId = ?", @@ -57,24 +52,23 @@ struct WalletIdMigration { try db.execute(sql: "PRAGMA foreign_keys = ON") - // Step 4: Migrate currentWalletId preference - migrateCurrentWalletPreference(mappings: remainingMappings) + migrateCurrentWalletPreference(mappings: remainingMappings, userDefaults: userDefaults) } - private static func migrateCurrentWalletPreference(mappings: [WalletMapping]) { + private static func migrateCurrentWalletPreference(mappings: [WalletMapping], userDefaults: UserDefaults) { let firstWallet = mappings.min(by: { $0.order < $1.order }) - guard let currentId = UserDefaults.standard.string(forKey: currentWalletKey) else { + guard let currentId = userDefaults.string(forKey: currentWalletKey) else { if let first = firstWallet { - UserDefaults.standard.set(first.newId, forKey: currentWalletKey) + userDefaults.set(first.newId, forKey: currentWalletKey) } return } if let mapping = mappings.first(where: { $0.oldId == currentId && $0.oldId != $0.newId }) { - UserDefaults.standard.set(mapping.newId, forKey: currentWalletKey) + userDefaults.set(mapping.newId, forKey: currentWalletKey) } else if let first = firstWallet { - UserDefaults.standard.set(first.newId, forKey: currentWalletKey) + userDefaults.set(first.newId, forKey: currentWalletKey) } } diff --git a/Packages/Store/Sources/Stores/WalletStore.swift b/Packages/Store/Sources/Stores/WalletStore.swift index b5d248d79..5c1c4a4f4 100644 --- a/Packages/Store/Sources/Stores/WalletStore.swift +++ b/Packages/Store/Sources/Stores/WalletStore.swift @@ -124,6 +124,14 @@ public struct WalletStore: Sendable { .updateAll(db, assignments) } } + + public func setOrder(walletId: String, order: Int) throws { + let _ = try db.write { db in + try WalletRecord + .filter(WalletRecord.Columns.id == walletId) + .updateAll(db, WalletRecord.Columns.order.set(to: order)) + } + } } extension WalletRecord { diff --git a/Packages/Store/Tests/StoreTests/WalletIdMigrationTests.swift b/Packages/Store/Tests/StoreTests/WalletIdMigrationTests.swift index efdb7efb2..e75c689b8 100644 --- a/Packages/Store/Tests/StoreTests/WalletIdMigrationTests.swift +++ b/Packages/Store/Tests/StoreTests/WalletIdMigrationTests.swift @@ -8,9 +8,12 @@ import PrimitivesTestKit import StoreTestKit @testable import Store -private func setOrder(db: DB, walletId: String, order: Int) throws { - try db.dbQueue.write { db in - try db.execute(sql: "UPDATE wallets SET \"order\" = ? WHERE id = ?", arguments: [order, walletId]) +private extension UserDefaults { + static func mock() -> UserDefaults { + let suiteName = UUID().uuidString + let defaults = UserDefaults(suiteName: suiteName)! + defaults.removePersistentDomain(forName: suiteName) + return defaults } } @@ -21,7 +24,7 @@ struct WalletIdMigrationTests { @Test func migrateMulticoinWallet() throws { - UserDefaults.standard.removeObject(forKey: currentWalletKey) + let userDefaults = UserDefaults.mock() let db = DB.mock() let walletStore = WalletStore(db: db) @@ -35,7 +38,7 @@ struct WalletIdMigrationTests { try walletStore.addWallet(wallet) try db.dbQueue.write { db in - try WalletIdMigration.migrate(db: db) + try WalletIdMigration.migrate(db: db, userDefaults: userDefaults) } let wallets = try walletStore.getWallets() @@ -46,7 +49,7 @@ struct WalletIdMigrationTests { @Test func migrateViewWallet() throws { - UserDefaults.standard.removeObject(forKey: currentWalletKey) + let userDefaults = UserDefaults.mock() let db = DB.mock() let walletStore = WalletStore(db: db) @@ -60,7 +63,7 @@ struct WalletIdMigrationTests { try walletStore.addWallet(wallet) try db.dbQueue.write { db in - try WalletIdMigration.migrate(db: db) + try WalletIdMigration.migrate(db: db, userDefaults: userDefaults) } let wallets = try walletStore.getWallets() @@ -71,7 +74,7 @@ struct WalletIdMigrationTests { @Test func migrateSingleWallet() throws { - UserDefaults.standard.removeObject(forKey: currentWalletKey) + let userDefaults = UserDefaults.mock() let db = DB.mock() let walletStore = WalletStore(db: db) @@ -85,7 +88,7 @@ struct WalletIdMigrationTests { try walletStore.addWallet(wallet) try db.dbQueue.write { db in - try WalletIdMigration.migrate(db: db) + try WalletIdMigration.migrate(db: db, userDefaults: userDefaults) } let wallets = try walletStore.getWallets() @@ -96,7 +99,7 @@ struct WalletIdMigrationTests { @Test func migratePrivateKeyWallet() throws { - UserDefaults.standard.removeObject(forKey: currentWalletKey) + let userDefaults = UserDefaults.mock() let db = DB.mock() let walletStore = WalletStore(db: db) @@ -110,7 +113,7 @@ struct WalletIdMigrationTests { try walletStore.addWallet(wallet) try db.dbQueue.write { db in - try WalletIdMigration.migrate(db: db) + try WalletIdMigration.migrate(db: db, userDefaults: userDefaults) } let wallets = try walletStore.getWallets() @@ -121,7 +124,7 @@ struct WalletIdMigrationTests { @Test func removeDuplicateMulticoinWallets() throws { - UserDefaults.standard.removeObject(forKey: currentWalletKey) + let userDefaults = UserDefaults.mock() let db = DB.mock() let walletStore = WalletStore(db: db) @@ -151,7 +154,7 @@ struct WalletIdMigrationTests { try walletStore.addWallet(wallet3) try db.dbQueue.write { db in - try WalletIdMigration.migrate(db: db) + try WalletIdMigration.migrate(db: db, userDefaults: userDefaults) } let wallets = try walletStore.getWallets() @@ -162,7 +165,7 @@ struct WalletIdMigrationTests { @Test func mixedWalletTypes() throws { - UserDefaults.standard.removeObject(forKey: currentWalletKey) + let userDefaults = UserDefaults.mock() let db = DB.mock() let walletStore = WalletStore(db: db) @@ -190,7 +193,7 @@ struct WalletIdMigrationTests { try walletStore.addWallet(view) try db.dbQueue.write { db in - try WalletIdMigration.migrate(db: db) + try WalletIdMigration.migrate(db: db, userDefaults: userDefaults) } let wallets = try walletStore.getWallets() @@ -204,7 +207,7 @@ struct WalletIdMigrationTests { @Test func updateChildTableReferences() throws { - UserDefaults.standard.removeObject(forKey: currentWalletKey) + let userDefaults = UserDefaults.mock() let db = DB.mock() let walletStore = WalletStore(db: db) let balanceStore = BalanceStore(db: db) @@ -224,7 +227,7 @@ struct WalletIdMigrationTests { try balanceStore.addBalance([AddBalance(assetId: asset.asset.id, isEnabled: true)], for: wallet.walletId) try db.dbQueue.write { db in - try WalletIdMigration.migrate(db: db) + try WalletIdMigration.migrate(db: db, userDefaults: userDefaults) } let newWalletId = WalletId(id: "multicoin_\(ethAddress)") @@ -234,27 +237,24 @@ struct WalletIdMigrationTests { @Test func multipleDuplicateGroups() throws { - UserDefaults.standard.removeObject(forKey: currentWalletKey) + let userDefaults = UserDefaults.mock() let db = DB.mock() let walletStore = WalletStore(db: db) - // Group 1: 3 wallets with same eth address let address1 = "0xaddress1" try walletStore.addWallet(.mock(id: "uuid-1a", type: .multicoin, accounts: [.mock(chain: .ethereum, address: address1)], order: 0)) try walletStore.addWallet(.mock(id: "uuid-1b", type: .multicoin, accounts: [.mock(chain: .ethereum, address: address1)], order: 1)) try walletStore.addWallet(.mock(id: "uuid-1c", type: .multicoin, accounts: [.mock(chain: .ethereum, address: address1)], order: 2)) - // Group 2: 2 wallets with different eth address let address2 = "0xaddress2" try walletStore.addWallet(.mock(id: "uuid-2a", type: .multicoin, accounts: [.mock(chain: .ethereum, address: address2)], order: 3)) try walletStore.addWallet(.mock(id: "uuid-2b", type: .multicoin, accounts: [.mock(chain: .ethereum, address: address2)], order: 4)) - // Unique wallet let address3 = "0xaddress3" try walletStore.addWallet(.mock(id: "uuid-3", type: .multicoin, accounts: [.mock(chain: .ethereum, address: address3)], order: 5)) try db.dbQueue.write { db in - try WalletIdMigration.migrate(db: db) + try WalletIdMigration.migrate(db: db, userDefaults: userDefaults) } let wallets = try walletStore.getWallets() @@ -265,7 +265,6 @@ struct WalletIdMigrationTests { #expect(ids.contains("multicoin_\(address2)")) #expect(ids.contains("multicoin_\(address3)")) - // Verify correct wallet was kept (lowest order) let wallet1 = wallets.first { $0.id == "multicoin_\(address1)" } #expect(wallet1?.externalId == "uuid-1a") @@ -275,29 +274,29 @@ struct WalletIdMigrationTests { @Test func duplicateViewWallets() throws { - UserDefaults.standard.removeObject(forKey: currentWalletKey) + let userDefaults = UserDefaults.mock() let db = DB.mock() let walletStore = WalletStore(db: db) let address = "0xviewaddr" try walletStore.addWallet(.mock(id: "uuid-view-1", type: .view, accounts: [.mock(chain: .ethereum, address: address)])) try walletStore.addWallet(.mock(id: "uuid-view-2", type: .view, accounts: [.mock(chain: .ethereum, address: address)])) - try setOrder(db: db, walletId: "uuid-view-1", order: 1) - try setOrder(db: db, walletId: "uuid-view-2", order: 0) + try walletStore.setOrder(walletId: "uuid-view-1", order: 1) + try walletStore.setOrder(walletId: "uuid-view-2", order: 0) try db.dbQueue.write { db in - try WalletIdMigration.migrate(db: db) + try WalletIdMigration.migrate(db: db, userDefaults: userDefaults) } let wallets = try walletStore.getWallets() #expect(wallets.count == 1) #expect(wallets.first?.id == "view_ethereum_\(address)") - #expect(wallets.first?.externalId == "uuid-view-2") // lower order wins + #expect(wallets.first?.externalId == "uuid-view-2") } @Test func duplicateSingleWallets() throws { - UserDefaults.standard.removeObject(forKey: currentWalletKey) + let userDefaults = UserDefaults.mock() let db = DB.mock() let walletStore = WalletStore(db: db) @@ -305,23 +304,23 @@ struct WalletIdMigrationTests { try walletStore.addWallet(.mock(id: "uuid-single-1", type: .single, accounts: [.mock(chain: .bitcoin, address: address)])) try walletStore.addWallet(.mock(id: "uuid-single-2", type: .single, accounts: [.mock(chain: .bitcoin, address: address)])) try walletStore.addWallet(.mock(id: "uuid-single-3", type: .single, accounts: [.mock(chain: .bitcoin, address: address)])) - try setOrder(db: db, walletId: "uuid-single-1", order: 2) - try setOrder(db: db, walletId: "uuid-single-2", order: 0) - try setOrder(db: db, walletId: "uuid-single-3", order: 1) + try walletStore.setOrder(walletId: "uuid-single-1", order: 2) + try walletStore.setOrder(walletId: "uuid-single-2", order: 0) + try walletStore.setOrder(walletId: "uuid-single-3", order: 1) try db.dbQueue.write { db in - try WalletIdMigration.migrate(db: db) + try WalletIdMigration.migrate(db: db, userDefaults: userDefaults) } let wallets = try walletStore.getWallets() #expect(wallets.count == 1) #expect(wallets.first?.id == "single_bitcoin_\(address)") - #expect(wallets.first?.externalId == "uuid-single-2") // order 0 wins + #expect(wallets.first?.externalId == "uuid-single-2") } @Test func keepWalletWithLowestOrder() throws { - UserDefaults.standard.removeObject(forKey: currentWalletKey) + let userDefaults = UserDefaults.mock() let db = DB.mock() let walletStore = WalletStore(db: db) @@ -331,24 +330,24 @@ struct WalletIdMigrationTests { try walletStore.addWallet(.mock(id: "uuid-order-8", type: .multicoin, accounts: [.mock(chain: .ethereum, address: address)])) try walletStore.addWallet(.mock(id: "uuid-order-1", type: .multicoin, accounts: [.mock(chain: .ethereum, address: address)])) try walletStore.addWallet(.mock(id: "uuid-order-3", type: .multicoin, accounts: [.mock(chain: .ethereum, address: address)])) - try setOrder(db: db, walletId: "uuid-order-5", order: 5) - try setOrder(db: db, walletId: "uuid-order-2", order: 2) - try setOrder(db: db, walletId: "uuid-order-8", order: 8) - try setOrder(db: db, walletId: "uuid-order-1", order: 1) - try setOrder(db: db, walletId: "uuid-order-3", order: 3) + try walletStore.setOrder(walletId: "uuid-order-5", order: 5) + try walletStore.setOrder(walletId: "uuid-order-2", order: 2) + try walletStore.setOrder(walletId: "uuid-order-8", order: 8) + try walletStore.setOrder(walletId: "uuid-order-1", order: 1) + try walletStore.setOrder(walletId: "uuid-order-3", order: 3) try db.dbQueue.write { db in - try WalletIdMigration.migrate(db: db) + try WalletIdMigration.migrate(db: db, userDefaults: userDefaults) } let wallets = try walletStore.getWallets() #expect(wallets.count == 1) - #expect(wallets.first?.externalId == "uuid-order-1") // lowest order wins + #expect(wallets.first?.externalId == "uuid-order-1") } @Test func accountsUpdatedAfterMigration() throws { - UserDefaults.standard.removeObject(forKey: currentWalletKey) + let userDefaults = UserDefaults.mock() let db = DB.mock() let walletStore = WalletStore(db: db) @@ -366,7 +365,7 @@ struct WalletIdMigrationTests { try walletStore.addWallet(wallet) try db.dbQueue.write { db in - try WalletIdMigration.migrate(db: db) + try WalletIdMigration.migrate(db: db, userDefaults: userDefaults) } let wallets = try walletStore.getWallets() @@ -380,11 +379,10 @@ struct WalletIdMigrationTests { @Test func walletAlreadyInNewFormat() throws { - UserDefaults.standard.removeObject(forKey: currentWalletKey) + let userDefaults = UserDefaults.mock() let db = DB.mock() let walletStore = WalletStore(db: db) - // Wallet already has new format ID (simulating re-running migration) let ethAddress = "0xalreadymigrated" let newFormatId = "multicoin_\(ethAddress)" let wallet = Wallet.mock( @@ -396,22 +394,22 @@ struct WalletIdMigrationTests { try walletStore.addWallet(wallet) try db.dbQueue.write { db in - try WalletIdMigration.migrate(db: db) + try WalletIdMigration.migrate(db: db, userDefaults: userDefaults) } let wallets = try walletStore.getWallets() #expect(wallets.count == 1) #expect(wallets.first?.id == newFormatId) - #expect(wallets.first?.externalId == "old-uuid") // preserved + #expect(wallets.first?.externalId == "old-uuid") } @Test func emptyDatabase() throws { - UserDefaults.standard.removeObject(forKey: currentWalletKey) + let userDefaults = UserDefaults.mock() let db = DB.mock() try db.dbQueue.write { db in - try WalletIdMigration.migrate(db: db) + try WalletIdMigration.migrate(db: db, userDefaults: userDefaults) } let walletStore = WalletStore(db: db) @@ -427,8 +425,7 @@ struct WalletIdMigrationPreferenceTests { @Test func migrateCurrentWalletPreference() throws { - UserDefaults.standard.removeObject(forKey: currentWalletKey) - + let userDefaults = UserDefaults.mock() let db = DB.mock() let walletStore = WalletStore(db: db) @@ -441,57 +438,49 @@ struct WalletIdMigrationPreferenceTests { ) try walletStore.addWallet(wallet) - UserDefaults.standard.set(oldId, forKey: currentWalletKey) + userDefaults.set(oldId, forKey: currentWalletKey) try db.dbQueue.write { db in - try WalletIdMigration.migrate(db: db) + try WalletIdMigration.migrate(db: db, userDefaults: userDefaults) } - let newCurrentWalletId = UserDefaults.standard.string(forKey: currentWalletKey) + let newCurrentWalletId = userDefaults.string(forKey: currentWalletKey) #expect(newCurrentWalletId == "multicoin_\(ethAddress)") - - UserDefaults.standard.removeObject(forKey: currentWalletKey) } @Test func setCurrentWalletWhenNoneSet() throws { - UserDefaults.standard.removeObject(forKey: currentWalletKey) - + let userDefaults = UserDefaults.mock() let db = DB.mock() let walletStore = WalletStore(db: db) try walletStore.addWallet(.mock(id: "uuid-first", type: .multicoin, accounts: [.mock(chain: .ethereum, address: "0xfirst")])) - try setOrder(db: db, walletId: "uuid-first", order: 0) + try walletStore.setOrder(walletId: "uuid-first", order: 0) try db.dbQueue.write { db in - try WalletIdMigration.migrate(db: db) + try WalletIdMigration.migrate(db: db, userDefaults: userDefaults) } - let currentWalletId = UserDefaults.standard.string(forKey: currentWalletKey) + let currentWalletId = userDefaults.string(forKey: currentWalletKey) #expect(currentWalletId == "multicoin_0xfirst") - - UserDefaults.standard.removeObject(forKey: currentWalletKey) } @Test func fallbackCurrentWalletWhenInvalid() throws { - UserDefaults.standard.removeObject(forKey: currentWalletKey) - + let userDefaults = UserDefaults.mock() let db = DB.mock() let walletStore = WalletStore(db: db) - UserDefaults.standard.set("deleted-wallet-id", forKey: currentWalletKey) + userDefaults.set("deleted-wallet-id", forKey: currentWalletKey) try walletStore.addWallet(.mock(id: "uuid-fallback", type: .multicoin, accounts: [.mock(chain: .ethereum, address: "0xfallback")])) - try setOrder(db: db, walletId: "uuid-fallback", order: 0) + try walletStore.setOrder(walletId: "uuid-fallback", order: 0) try db.dbQueue.write { db in - try WalletIdMigration.migrate(db: db) + try WalletIdMigration.migrate(db: db, userDefaults: userDefaults) } - let currentWalletId = UserDefaults.standard.string(forKey: currentWalletKey) + let currentWalletId = userDefaults.string(forKey: currentWalletKey) #expect(currentWalletId == "multicoin_0xfallback") - - UserDefaults.standard.removeObject(forKey: currentWalletKey) } } From 0adabc54c206f47aaf4bac8c77d8120d7644cd8c Mon Sep 17 00:00:00 2001 From: gemcoder21 <104884878+gemcoder21@users.noreply.github.com> Date: Fri, 23 Jan 2026 18:14:21 +0000 Subject: [PATCH 06/10] Update core --- core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core b/core index 9693266d4..7cc3801f1 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 9693266d4d28ab1913bd2b77bc4996d77c4ad58e +Subproject commit 7cc3801f1e45995d5dbbda3d92bdbdc4d404ab9a From f541b008c36cecd8c172827c9a22f4abedf93f08 Mon Sep 17 00:00:00 2001 From: gemcoder21 <104884878+gemcoder21@users.noreply.github.com> Date: Fri, 23 Jan 2026 19:09:08 +0000 Subject: [PATCH 07/10] Refactor wallet ID handling to use WalletIdentifier Replaces wallet type-based ID logic with WalletIdentifier throughout keystore and primitives. Updates import and migration flows to use the new identifier format, ensuring consistent wallet identification. Adjusts tests and migration logic to support the new ID scheme and migrates wallet preferences accordingly. --- Packages/Keystore/Sources/LocalKeystore.swift | 7 ++- .../Sources/Store/WalletKeyStore.swift | 27 ++++----- .../Keystore/Tests/WalletKeyStoreTests.swift | 56 ++++++++++++------- .../Extensions/Wallet+Primitives.swift | 16 +----- .../Primitives/Sources/WalletIdentifier.swift | 15 +++++ Packages/Store/Sources/Migrations.swift | 5 +- .../Migrations/WalletIdMigration.swift | 33 +++++++---- 7 files changed, 95 insertions(+), 64 deletions(-) diff --git a/Packages/Keystore/Sources/LocalKeystore.swift b/Packages/Keystore/Sources/LocalKeystore.swift index 620dedd22..621495f12 100644 --- a/Packages/Keystore/Sources/LocalKeystore.swift +++ b/Packages/Keystore/Sources/LocalKeystore.swift @@ -40,15 +40,16 @@ public final class LocalKeystore: Keystore, @unchecked Sendable { source: WalletSource ) async throws -> Primitives.Wallet { let password = try await getOrCreatePassword(createPasswordIfNone: isWalletsEmpty) + let walletIdentifier = try ImportIdentifier.from(type).walletIdentifier() return try await queue.asyncTask { [walletKeyStore] in switch type { case .phrase(let words, let chains): - try walletKeyStore.importWallet(type: .multicoin, name: name, words: words, chains: chains, password: password, source: source) + try walletKeyStore.importWallet(id: walletIdentifier, name: name, words: words, chains: chains, password: password, source: source) case .single(let words, let chain): - try walletKeyStore.importWallet(type: .single, name: name, words: words, chains: [chain], password: password, source: source) + try walletKeyStore.importWallet(id: walletIdentifier, name: name, words: words, chains: [chain], password: password, source: source) case .privateKey(let text, let chain): - try walletKeyStore.importPrivateKey(name: name, key: text, chain: chain, password: password, source: source) + try walletKeyStore.importPrivateKey(id: walletIdentifier, name: name, key: text, chain: chain, password: password, source: source) case .address(let address, let chain): Wallet.makeView(name: name, chain: chain, address: address) } diff --git a/Packages/Keystore/Sources/Store/WalletKeyStore.swift b/Packages/Keystore/Sources/Store/WalletKeyStore.swift index 1802657d7..dc337bfa8 100644 --- a/Packages/Keystore/Sources/Store/WalletKeyStore.swift +++ b/Packages/Keystore/Sources/Store/WalletKeyStore.swift @@ -23,7 +23,7 @@ public struct WalletKeyStore: Sendable { } public func importWallet( - type: Primitives.WalletType, + id: WalletIdentifier, name: String, words: [String], chains: [Chain], @@ -36,7 +36,7 @@ public struct WalletKeyStore: Sendable { encryptPassword: password, coins: [] ) - return try addCoins(type: type, wallet: wallet, existingChains: [], newChains: chains, password: password, source: source) + return try addCoins(id: id, wallet: wallet, existingChains: [], newChains: chains, password: password, source: source) } public static func decodeKey(_ key: String, chain: Chain) throws -> PrivateKey { @@ -90,22 +90,23 @@ public struct WalletKeyStore: Sendable { } } - public func importPrivateKey(name: String, key: String, chain: Chain, password: String, source: WalletSource) throws -> Primitives.Wallet { + public func importPrivateKey(id: WalletIdentifier, name: String, key: String, chain: Chain, password: String, source: WalletSource) throws -> Primitives.Wallet { let privateKey = try Self.decodeKey(key, chain: chain) let wallet = try keyStore.import(privateKey: privateKey, name: name, password: password, coin: chain.coinType) + let address = chain.coinType.deriveAddress(privateKey: privateKey) let account = Primitives.Account( chain: chain, - address: chain.coinType.deriveAddress(privateKey: privateKey), + address: address, derivationPath: chain.coinType.derivationPath(), // not applicable extendedPublicKey: nil ) return Primitives.Wallet( - id: wallet.id, - externalId: nil, + id: id.id, + externalId: wallet.id, name: wallet.key.name, index: 0, - type: .privateKey, + type: id.walletType, accounts: [account], order: 0, isPinned: false, @@ -115,7 +116,7 @@ public struct WalletKeyStore: Sendable { } public func addCoins( - type: Primitives.WalletType, + id: WalletIdentifier, wallet: WalletCore.Wallet, existingChains: [Chain], newChains: [Chain], @@ -140,17 +141,17 @@ public struct WalletKeyStore: Sendable { if newChains.isNotEmpty && newCoinTypes.subtracting(existingCoinTypes).isNotEmpty { let _ = try keyStore.addAccounts(wallet: wallet, coins: coins, password: password) } - + let accounts = allChains.compactMap { chain in wallet.accounts.filter({ $0.coin == chain.coinType }).first?.mapToAccount(chain: chain) } return Wallet( - id: wallet.id, - externalId: nil, + id: id.id, + externalId: wallet.id, name: wallet.key.name, index: 0, - type: type, + type: id.walletType, accounts: accounts, order: 0, isPinned: false, @@ -166,7 +167,7 @@ public struct WalletKeyStore: Sendable { password: String ) throws -> Primitives.Wallet { try addCoins( - type: wallet.type, + id: try WalletIdentifier.from(id: wallet.id), wallet: try getWallet(id: wallet.keystoreId), existingChains: existingChains, newChains: newChains, diff --git a/Packages/Keystore/Tests/WalletKeyStoreTests.swift b/Packages/Keystore/Tests/WalletKeyStoreTests.swift index 17d8851c6..9d9f395d2 100644 --- a/Packages/Keystore/Tests/WalletKeyStoreTests.swift +++ b/Packages/Keystore/Tests/WalletKeyStoreTests.swift @@ -11,10 +11,24 @@ final class WalletKeyStoreTests { let words = "panda eternal chronic student column crumble endorse cushion whisper space carpet pitch praise tribe audit wing boil firm pink umbrella senior venture crouch confirm" let password = "test" + private var hdWallet: HDWallet { + HDWallet(mnemonic: words, passphrase: "")! + } + + private var ethereumAddress: String { + CoinType.ethereum.deriveAddress(privateKey: hdWallet.getKeyForCoin(coin: .ethereum)) + } + + private func address(for coinType: CoinType) -> String { + coinType.deriveAddress(privateKey: hdWallet.getKeyForCoin(coin: coinType)) + } + @Test func testImportPrivateKey() throws { let store = WalletKeyStore.mock() + let expectedAddress = "JSTURBrew3zGaJjtk7qcvd7gapeExX3GC7DiQBaCKzU" let wallet = try store.importPrivateKey( + id: .privateKey(chain: .solana, address: expectedAddress), name: "test", key: testBase58Key, chain: .solana, @@ -26,7 +40,7 @@ final class WalletKeyStoreTests { #expect(wallet.accounts == [ Primitives.Account( chain: .solana, - address: "JSTURBrew3zGaJjtk7qcvd7gapeExX3GC7DiQBaCKzU", + address: expectedAddress, derivationPath: "m/44\'/501\'/0\'", extendedPublicKey: .none ) @@ -70,84 +84,84 @@ final class WalletKeyStoreTests { @Test func addImportWallet() async throws { let store = WalletKeyStore.mock() let newWallet = try store.importWallet( - type: .multicoin, + id: .multicoin(address: ethereumAddress), name: "", - words: words.components(separatedBy: ", "), + words: words.components(separatedBy: " "), chains: [.bitcoin, .ethereum], password: password, source: .import ) - + #expect(newWallet.accounts.map { $0.chain } == [.bitcoin, .ethereum]) } @Test func addCoinsMany() async throws { let store = WalletKeyStore.mock() - let newWallet = try store.importWallet(type: .multicoin, name: "", words: words.components(separatedBy: ", "), chains: [], password: password, source: .create) - + let newWallet = try store.importWallet(id: .multicoin(address: ethereumAddress), name: "", words: words.components(separatedBy: " "), chains: [], password: password, source: .create) + let wallet = try store.addChains( wallet: newWallet, existingChains: [], newChains: [.ethereum, .bitcoin], password: password ) - + #expect(wallet.accounts.map { $0.chain } == [.ethereum, .bitcoin]) } @Test func addCoinsEmptyChain() async throws { let store = WalletKeyStore.mock() - let newWallet = try store.importWallet(type: .multicoin, name: "", words: words.components(separatedBy: ", "), chains: [], password: password, source: .create) - + let newWallet = try store.importWallet(id: .multicoin(address: ethereumAddress), name: "", words: words.components(separatedBy: " "), chains: [], password: password, source: .create) + let wallet = try store.addChains(wallet: newWallet, existingChains: [], newChains: [], password: password) - + #expect(wallet.accounts.isEmpty) } @Test func addCoinsSingleChain() async throws { let store = WalletKeyStore.mock() - let newWallet = try store.importWallet(type: .single, name: "", words: words.components(separatedBy: ", "), chains: [], password: password, source: .create) - + let newWallet = try store.importWallet(id: .single(chain: .algorand, address: address(for: .algorand)), name: "", words: words.components(separatedBy: " "), chains: [], password: password, source: .create) + let wallet = try store.addChains( wallet: newWallet, existingChains: [], newChains: [.algorand], password: password ) - + #expect(wallet.accounts.map { $0.chain } == [.algorand]) } @Test func addCoinsWhenSolana() async throws { let store = WalletKeyStore.mock() - let newWallet = try store.importWallet(type: .multicoin, name: "", words: words.components(separatedBy: ", "), chains: [], password: password, source: .create) - + let newWallet = try store.importWallet(id: .multicoin(address: ethereumAddress), name: "", words: words.components(separatedBy: " "), chains: [], password: password, source: .create) + let wallet = try store.addChains(wallet: newWallet, existingChains: [], newChains: [.solana], password: password) - + #expect(wallet.accounts.map { $0.chain } == [.solana]) #expect(wallet.accounts.map { $0.address } == ["9fb52fTJpTzYqV4be7u31TxxFfzs9ub9RehwLYuxhP6C"]) } @Test func addCoinsManyTries() async throws { let store = WalletKeyStore.mock() - let newWallet = try store.importWallet(type: .multicoin, name: "", words: words.components(separatedBy: ", "), chains: [], password: password, source: .create) - + let newWallet = try store.importWallet(id: .multicoin(address: ethereumAddress), name: "", words: words.components(separatedBy: " "), chains: [], password: password, source: .create) + let wallet = try store.addChains( wallet: newWallet, existingChains: [], newChains: [.ethereum], password: password ) - + #expect(wallet.accounts.map { $0.chain } == [.ethereum]) - + let wallet2 = try store.addChains( wallet: newWallet, existingChains: [.bitcoin], newChains: [.ethereum], password: password ) - + #expect(wallet2.accounts.map { $0.chain } == [.bitcoin, .ethereum]) } } diff --git a/Packages/Primitives/Sources/Extensions/Wallet+Primitives.swift b/Packages/Primitives/Sources/Extensions/Wallet+Primitives.swift index 26e163ed5..ead651284 100644 --- a/Packages/Primitives/Sources/Extensions/Wallet+Primitives.swift +++ b/Packages/Primitives/Sources/Extensions/Wallet+Primitives.swift @@ -22,18 +22,7 @@ public extension Wallet { } func walletIdentifier() throws -> WalletIdentifier { - switch type { - case .multicoin: - guard let address = accounts.first(where: { $0.chain == .ethereum })?.address else { - throw AnyError("multicoin wallet requires an ethereum account") - } - return .multicoin(address: address) - case .single, .privateKey, .view: - guard let account = accounts.first else { - throw AnyError("\(type) wallet requires at least one account") - } - return WalletIdentifier.make(walletType: type, chain: account.chain, address: account.address) - } + try WalletIdentifier.from(type: type, accounts: accounts) } var hasTokenSupport: Bool { @@ -55,8 +44,9 @@ public extension Wallet { // factory public extension Wallet { static func makeView(name: String, chain: Chain, address: String) -> Wallet { + let id = WalletIdentifier.make(walletType: .view, chain: chain, address: address).id return Wallet( - id: NSUUID().uuidString, + id: id, externalId: nil, name: name, index: 0, diff --git a/Packages/Primitives/Sources/WalletIdentifier.swift b/Packages/Primitives/Sources/WalletIdentifier.swift index cc5246f34..607fcac83 100644 --- a/Packages/Primitives/Sources/WalletIdentifier.swift +++ b/Packages/Primitives/Sources/WalletIdentifier.swift @@ -81,4 +81,19 @@ public enum WalletIdentifier: Equatable, Hashable, Sendable { case .view: .view(chain: chain, address: address) } } + + public static func from(type: WalletType, accounts: [Account]) throws -> WalletIdentifier { + switch type { + case .multicoin: + guard let address = accounts.first(where: { $0.chain == .ethereum })?.address else { + throw AnyError("multicoin wallet requires an ethereum account") + } + return .multicoin(address: address) + case .single, .privateKey, .view: + guard let account = accounts.first else { + throw AnyError("\(type) wallet requires at least one account") + } + return make(walletType: type, chain: account.chain, address: account.address) + } + } } diff --git a/Packages/Store/Sources/Migrations.swift b/Packages/Store/Sources/Migrations.swift index 5bde25e03..8c2c09c13 100644 --- a/Packages/Store/Sources/Migrations.swift +++ b/Packages/Store/Sources/Migrations.swift @@ -375,13 +375,10 @@ public struct Migrations { } } - migrator.registerMigration("Add externalId to \(WalletRecord.databaseTableName)") { db in + migrator.registerMigration("Migrate wallet IDs to WalletIdentifier format") { db in try? db.alter(table: WalletRecord.databaseTableName) { $0.add(column: WalletRecord.Columns.externalId.name, .text) } - } - - migrator.registerMigration("Migrate wallet IDs to WalletIdentifier format") { db in try WalletIdMigration.migrate(db: db) } diff --git a/Packages/Store/Sources/Migrations/WalletIdMigration.swift b/Packages/Store/Sources/Migrations/WalletIdMigration.swift index 09a1bf8c5..7d23db828 100644 --- a/Packages/Store/Sources/Migrations/WalletIdMigration.swift +++ b/Packages/Store/Sources/Migrations/WalletIdMigration.swift @@ -22,6 +22,8 @@ struct WalletIdMigration { private static let currentWalletKey = "currentWallet" static func migrate(db: Database, userDefaults: UserDefaults = .standard) throws { + try db.execute(sql: "PRAGMA foreign_keys = OFF") + let mappings = try buildWalletMappings(db: db) let groups = Dictionary(grouping: mappings, by: { $0.newId }) @@ -29,25 +31,22 @@ struct WalletIdMigration { let sorted = wallets.sorted { $0.order < $1.order } let losers = sorted.dropFirst() for loser in losers { - try db.execute( - sql: "DELETE FROM \(WalletRecord.databaseTableName) WHERE id = ?", - arguments: [loser.oldId] - ) + for table in childTables { + try? db.execute(sql: "DELETE FROM \(table) WHERE walletId = ?", arguments: [loser.oldId]) + } + try db.execute(sql: "DELETE FROM \(WalletRecord.databaseTableName) WHERE id = ?", arguments: [loser.oldId]) } } - try db.execute(sql: "PRAGMA foreign_keys = OFF") - let remainingMappings = try buildWalletMappings(db: db) for mapping in remainingMappings where mapping.oldId != mapping.newId { try db.execute(sql: "UPDATE \(WalletRecord.databaseTableName) SET externalId = id, id = ? WHERE id = ?", arguments: [mapping.newId, mapping.oldId]) for table in childTables { - try db.execute( - sql: "UPDATE \(table) SET walletId = ? WHERE walletId = ?", - arguments: [mapping.newId, mapping.oldId] - ) + try? db.execute(sql: "UPDATE \(table) SET walletId = ? WHERE walletId = ?", arguments: [mapping.newId, mapping.oldId]) } + + migrateWalletPreferences(oldId: mapping.oldId, newId: mapping.newId) } try db.execute(sql: "PRAGMA foreign_keys = ON") @@ -55,6 +54,20 @@ struct WalletIdMigration { migrateCurrentWalletPreference(mappings: remainingMappings, userDefaults: userDefaults) } + private static func migrateWalletPreferences(oldId: String, newId: String) { + let oldSuiteName = "wallet_preferences_\(oldId)_v2" + let newSuiteName = "wallet_preferences_\(newId)_v2" + + guard let oldDefaults = UserDefaults(suiteName: oldSuiteName), + let newDefaults = UserDefaults(suiteName: newSuiteName) else { return } + + for (key, value) in oldDefaults.dictionaryRepresentation() { + newDefaults.set(value, forKey: key) + } + + oldDefaults.removePersistentDomain(forName: oldSuiteName) + } + private static func migrateCurrentWalletPreference(mappings: [WalletMapping], userDefaults: UserDefaults) { let firstWallet = mappings.min(by: { $0.order < $1.order }) From eafaa80fe689ac6bccbb8e51aa2746d87b0a8b4c Mon Sep 17 00:00:00 2001 From: gemcoder21 <104884878+gemcoder21@users.noreply.github.com> Date: Fri, 23 Jan 2026 19:10:19 +0000 Subject: [PATCH 08/10] Update unit test targets in xctestplan Reordered and updated the list of test targets in unit_frameworks.xctestplan to reflect current project structure. Added, removed, and rearranged test targets for improved test coverage and organization. --- GemTests/unit_frameworks.xctestplan | 244 ++++++++++++++-------------- 1 file changed, 122 insertions(+), 122 deletions(-) diff --git a/GemTests/unit_frameworks.xctestplan b/GemTests/unit_frameworks.xctestplan index 26310015d..e5d88ff04 100644 --- a/GemTests/unit_frameworks.xctestplan +++ b/GemTests/unit_frameworks.xctestplan @@ -18,72 +18,79 @@ "testTargets" : [ { "target" : { - "containerPath" : "container:Features\/NFT", - "identifier" : "NFTTests", - "name" : "NFTTests" + "containerPath" : "container:Packages\/Style", + "identifier" : "StyleTests", + "name" : "StyleTests" } }, { "target" : { - "containerPath" : "container:Packages\/Style", - "identifier" : "StyleTests", - "name" : "StyleTests" + "containerPath" : "container:Packages\/Keystore", + "identifier" : "KeystoreTests", + "name" : "KeystoreTests" } }, { "target" : { - "containerPath" : "container:Features\/FiatConnect", - "identifier" : "FiatConnectTests", - "name" : "FiatConnectTests" + "containerPath" : "container:Packages\/WalletCore", + "identifier" : "WalletCorePrimitivesTests", + "name" : "WalletCorePrimitivesTests" } }, { "target" : { - "containerPath" : "container:Features\/LockManager", - "identifier" : "LockManagerTests", - "name" : "LockManagerTests" + "containerPath" : "container:Features\/Staking", + "identifier" : "StakingTests", + "name" : "StakingTests" } }, { "target" : { - "containerPath" : "container:Packages\/Validators", - "identifier" : "ValidatorsTests", - "name" : "ValidatorsTests" + "containerPath" : "container:Features\/ManageWallets", + "identifier" : "ManageWalletsTests", + "name" : "ManageWalletsTests" } }, { "target" : { - "containerPath" : "container:Features\/Perpetuals", - "identifier" : "PerpetualsTests", - "name" : "PerpetualsTests" + "containerPath" : "container:Packages\/Primitives", + "identifier" : "PrimitivesTests", + "name" : "PrimitivesTests" } }, { "target" : { - "containerPath" : "container:Packages\/Signer", - "identifier" : "SignerTests", - "name" : "SignerTests" + "containerPath" : "container:Packages\/Preferences", + "identifier" : "PreferencesTest", + "name" : "PreferencesTest" } }, { "target" : { - "containerPath" : "container:Packages\/Keystore", - "identifier" : "KeystoreTests", - "name" : "KeystoreTests" + "containerPath" : "container:Packages\/SwiftHTTPClient", + "identifier" : "WebSocketClientTests", + "name" : "WebSocketClientTests" } }, { "target" : { - "containerPath" : "container:Packages\/Store", - "identifier" : "StoreTests", - "name" : "StoreTests" + "containerPath" : "container:Packages\/Validators", + "identifier" : "ValidatorsTests", + "name" : "ValidatorsTests" } }, { "target" : { - "containerPath" : "container:Features\/Staking", - "identifier" : "StakingTests", - "name" : "StakingTests" + "containerPath" : "container:Features\/Onboarding", + "identifier" : "OnboardingTest", + "name" : "OnboardingTest" + } + }, + { + "target" : { + "containerPath" : "container:Features\/WalletConnector", + "identifier" : "WalletConnectorTests", + "name" : "WalletConnectorTests" } }, { @@ -94,106 +101,106 @@ } }, { + "skippedTests" : { + "testFunctions" : [ + "example()" + ] + }, "target" : { - "containerPath" : "container:Features\/PriceAlerts", - "identifier" : "PriceAlertsTests", - "name" : "PriceAlertsTests" + "containerPath" : "container:Features\/Transactions", + "identifier" : "TransactionsTests", + "name" : "TransactionsTests" } }, { "target" : { - "containerPath" : "container:Features\/Settings", - "identifier" : "SettingsTests", - "name" : "SettingsTests" + "containerPath" : "container:Features\/Perpetuals", + "identifier" : "PerpetualsTests", + "name" : "PerpetualsTests" } }, { "target" : { - "containerPath" : "container:Packages\/Formatters", - "identifier" : "FormattersTests", - "name" : "FormattersTests" + "containerPath" : "container:Features\/WalletTab", + "identifier" : "WalletTabTests", + "name" : "WalletTabTests" } }, { "target" : { - "containerPath" : "container:Packages\/Primitives", - "identifier" : "PrimitivesTests", - "name" : "PrimitivesTests" + "containerPath" : "container:Packages\/GemAPI", + "identifier" : "GemAPITests", + "name" : "GemAPITests" } }, { - "skippedTests" : { - "testFunctions" : [ - "example()" - ] - }, "target" : { - "containerPath" : "container:Features\/Transactions", - "identifier" : "TransactionsTests", - "name" : "TransactionsTests" + "containerPath" : "container:Packages\/FeatureServices", + "identifier" : "WalletServiceTests", + "name" : "WalletServiceTests" } }, { "target" : { - "containerPath" : "container:Features\/Swap", - "identifier" : "SwapTests", - "name" : "SwapTests" + "containerPath" : "container:Features\/PriceAlerts", + "identifier" : "PriceAlertsTests", + "name" : "PriceAlertsTests" } }, { "target" : { - "containerPath" : "container:Packages\/Components", - "identifier" : "ComponentsTests", - "name" : "ComponentsTests" + "containerPath" : "container:Features\/Support", + "identifier" : "SupportTests", + "name" : "SupportTests" } }, { "target" : { - "containerPath" : "container:Features\/Transfer", - "identifier" : "TransferTests", - "name" : "TransferTests" + "containerPath" : "container:Packages\/FeatureServices", + "identifier" : "WalletsServiceTests", + "name" : "WalletsServiceTests" } }, { "target" : { - "containerPath" : "container:Packages\/GemAPI", - "identifier" : "GemAPITests", - "name" : "GemAPITests" + "containerPath" : "container:Features\/Swap", + "identifier" : "SwapTests", + "name" : "SwapTests" } }, { "target" : { - "containerPath" : "container:Packages\/Blockchain", - "identifier" : "BlockchainTests", - "name" : "BlockchainTests" + "containerPath" : "container:Packages\/Formatters", + "identifier" : "FormattersTests", + "name" : "FormattersTests" } }, { "target" : { - "containerPath" : "container:Features\/WalletConnector", - "identifier" : "WalletConnectorTests", - "name" : "WalletConnectorTests" + "containerPath" : "container:Features\/Assets", + "identifier" : "AssetsTests", + "name" : "AssetsTests" } }, { "target" : { - "containerPath" : "container:Features\/WalletTab", - "identifier" : "WalletTabTests", - "name" : "WalletTabTests" + "containerPath" : "container:Packages\/FeatureServices", + "identifier" : "BannerServiceTests", + "name" : "BannerServiceTests" } }, { "target" : { - "containerPath" : "container:Features\/Onboarding", - "identifier" : "OnboardingTest", - "name" : "OnboardingTest" + "containerPath" : "container:Features\/NFT", + "identifier" : "NFTTests", + "name" : "NFTTests" } }, { "target" : { - "containerPath" : "container:Packages\/WalletCore", - "identifier" : "WalletCorePrimitivesTests", - "name" : "WalletCorePrimitivesTests" + "containerPath" : "container:Packages\/FeatureServices", + "identifier" : "WalletSessionServiceTests", + "name" : "WalletSessionServiceTests" } }, { @@ -205,100 +212,93 @@ }, { "target" : { - "containerPath" : "container:Packages\/Preferences", - "identifier" : "PreferencesTest", - "name" : "PreferencesTest" - } - }, - { - "target" : { - "containerPath" : "container:Packages\/GemstonePrimitives", - "identifier" : "GemstonePrimitivesTests", - "name" : "GemstonePrimitivesTests" + "containerPath" : "container:Features\/Settings", + "identifier" : "SettingsTests", + "name" : "SettingsTests" } }, { "target" : { - "containerPath" : "container:Features\/Assets", - "identifier" : "AssetsTests", - "name" : "AssetsTests" + "containerPath" : "container:Packages\/Signer", + "identifier" : "SignerTests", + "name" : "SignerTests" } }, { "target" : { - "containerPath" : "container:Features\/QRScanner", - "identifier" : "QRScannerTests", - "name" : "QRScannerTests" + "containerPath" : "container:Features\/FiatConnect", + "identifier" : "FiatConnectTests", + "name" : "FiatConnectTests" } }, { "target" : { - "containerPath" : "container:Packages\/PrimitivesComponents", - "identifier" : "PrimitivesComponentsTests", - "name" : "PrimitivesComponentsTests" + "containerPath" : "container:Packages\/FeatureServices", + "identifier" : "AppServiceTests", + "name" : "AppServiceTests" } }, { "target" : { - "containerPath" : "container:Packages\/FeatureServices", - "identifier" : "PriceAlertServiceTests", - "name" : "PriceAlertServiceTests" + "containerPath" : "container:Features\/LockManager", + "identifier" : "LockManagerTests", + "name" : "LockManagerTests" } }, { "target" : { - "containerPath" : "container:Packages\/FeatureServices", - "identifier" : "WalletsServiceTests", - "name" : "WalletsServiceTests" + "containerPath" : "container:Packages\/Store", + "identifier" : "StoreTests", + "name" : "StoreTests" } }, { "target" : { - "containerPath" : "container:Features\/ManageWallets", - "identifier" : "ManageWalletsTests", - "name" : "ManageWalletsTests" + "containerPath" : "container:Features\/Transfer", + "identifier" : "TransferTests", + "name" : "TransferTests" } }, { "target" : { "containerPath" : "container:Packages\/FeatureServices", - "identifier" : "BannerServiceTests", - "name" : "BannerServiceTests" + "identifier" : "PriceAlertServiceTests", + "name" : "PriceAlertServiceTests" } }, { "target" : { - "containerPath" : "container:Packages\/Keychain", - "identifier" : "KeychainTests", - "name" : "KeychainTests" + "containerPath" : "container:Packages\/Blockchain", + "identifier" : "BlockchainTests", + "name" : "BlockchainTests" } }, { "target" : { - "containerPath" : "container:Features\/Support", - "identifier" : "SupportTests", - "name" : "SupportTests" + "containerPath" : "container:Packages\/PrimitivesComponents", + "identifier" : "PrimitivesComponentsTests", + "name" : "PrimitivesComponentsTests" } }, { "target" : { - "containerPath" : "container:Packages\/FeatureServices", - "identifier" : "WalletServiceTests", - "name" : "WalletServiceTests" + "containerPath" : "container:Packages\/Components", + "identifier" : "ComponentsTests", + "name" : "ComponentsTests" } }, { "target" : { - "containerPath" : "container:Packages\/FeatureServices", - "identifier" : "WalletSessionServiceTests", - "name" : "WalletSessionServiceTests" + "containerPath" : "container:Features\/QRScanner", + "identifier" : "QRScannerTests", + "name" : "QRScannerTests" } }, { "target" : { - "containerPath" : "container:Packages\/FeatureServices", - "identifier" : "AppServiceTests", - "name" : "AppServiceTests" + "containerPath" : "container:Packages\/GemstonePrimitives", + "identifier" : "GemstonePrimitivesTests", + "name" : "GemstonePrimitivesTests" } } ], From a50f670fc30ec2f109d8853bd5f96283164d8858 Mon Sep 17 00:00:00 2001 From: gemcoder21 <104884878+gemcoder21@users.noreply.github.com> Date: Fri, 23 Jan 2026 19:20:17 +0000 Subject: [PATCH 09/10] Skip migration if wallet mappings are empty Adds a check to return early from the migrate function if no wallet mappings are found, preventing unnecessary execution of migration steps. --- Packages/Store/Sources/Migrations/WalletIdMigration.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Packages/Store/Sources/Migrations/WalletIdMigration.swift b/Packages/Store/Sources/Migrations/WalletIdMigration.swift index 7d23db828..67d7ee067 100644 --- a/Packages/Store/Sources/Migrations/WalletIdMigration.swift +++ b/Packages/Store/Sources/Migrations/WalletIdMigration.swift @@ -22,9 +22,12 @@ struct WalletIdMigration { private static let currentWalletKey = "currentWallet" static func migrate(db: Database, userDefaults: UserDefaults = .standard) throws { - try db.execute(sql: "PRAGMA foreign_keys = OFF") - let mappings = try buildWalletMappings(db: db) + if mappings.isEmpty { + return + } + + try db.execute(sql: "PRAGMA foreign_keys = OFF") let groups = Dictionary(grouping: mappings, by: { $0.newId }) for (_, wallets) in groups where wallets.count > 1 { From c992019e54343af6d004fe5e1c14c6e9372e862e Mon Sep 17 00:00:00 2001 From: gemcoder21 <104884878+gemcoder21@users.noreply.github.com> Date: Fri, 23 Jan 2026 19:26:56 +0000 Subject: [PATCH 10/10] Refactor notification handling to simplify wallet ID usage Updated notification services and store to pass notifications directly without mapping wallet IDs, simplifying the API and reducing unnecessary transformations. Adjusted related methods and models to accommodate this change. --- Gem/Navigation/NavigationHandler.swift | 2 +- .../NotificationService/InAppNotificationService.swift | 5 +---- Packages/FeatureServices/WalletService/WalletService.swift | 4 ++-- Packages/Store/Sources/Models/NotificationRecord.swift | 4 ++-- Packages/Store/Sources/Stores/InAppNotificationStore.swift | 6 +++--- 5 files changed, 9 insertions(+), 12 deletions(-) diff --git a/Gem/Navigation/NavigationHandler.swift b/Gem/Navigation/NavigationHandler.swift index 22e7be516..cf293d6be 100644 --- a/Gem/Navigation/NavigationHandler.swift +++ b/Gem/Navigation/NavigationHandler.swift @@ -146,7 +146,7 @@ extension NavigationHandler { } private func navigateToTransaction(walletIndex: Int?, walletId: String, assetId: AssetId, transaction: Primitives.Transaction) async throws { - guard let walletId = walletService.walletId(walletIndex: walletIndex, walletTypeId: walletId) else { + guard let walletId = walletService.walletId(walletIndex: walletIndex, walletId: walletId) else { throw AnyError("Wallet not found") } let asset = try await assetsService.getOrFetchAsset(for: assetId) diff --git a/Packages/FeatureServices/NotificationService/InAppNotificationService.swift b/Packages/FeatureServices/NotificationService/InAppNotificationService.swift index 27d14d15f..c735c8659 100644 --- a/Packages/FeatureServices/NotificationService/InAppNotificationService.swift +++ b/Packages/FeatureServices/NotificationService/InAppNotificationService.swift @@ -35,10 +35,7 @@ public struct InAppNotificationService: Sendable { deviceId: deviceId, fromTimestamp: preferences.notificationsTimestamp ) - let walletNotifications = notifications.compactMap { notification in - walletService.walletId(walletIndex: nil, walletTypeId: notification.walletId).map { ($0, notification) } - } - try store.addNotifications(walletNotifications) + try store.addNotifications(notifications) preferences.notificationsTimestamp = newTimestamp } diff --git a/Packages/FeatureServices/WalletService/WalletService.swift b/Packages/FeatureServices/WalletService/WalletService.swift index 7bb7e749e..2e5c0bea2 100644 --- a/Packages/FeatureServices/WalletService/WalletService.swift +++ b/Packages/FeatureServices/WalletService/WalletService.swift @@ -48,8 +48,8 @@ public struct WalletService: Sendable { try walletStore.nextWalletIndex() } - public func walletId(walletIndex: Int?, walletTypeId: String) -> WalletId? { - if !walletTypeId.isEmpty, let wallet = walletSessionService.wallets.first(where: { (try? $0.walletIdentifier().id) == walletTypeId }) { + public func walletId(walletIndex: Int?, walletId: String) -> WalletId? { + if !walletId.isEmpty, let wallet = walletSessionService.wallets.first(where: { $0.id == walletId }) { return wallet.walletId } if let index = walletIndex { diff --git a/Packages/Store/Sources/Models/NotificationRecord.swift b/Packages/Store/Sources/Models/NotificationRecord.swift index c25b61905..34a845ee7 100644 --- a/Packages/Store/Sources/Models/NotificationRecord.swift +++ b/Packages/Store/Sources/Models/NotificationRecord.swift @@ -53,10 +53,10 @@ extension NotificationRecord { } extension Primitives.InAppNotification { - func record(walletId: WalletId) -> NotificationRecord { + func record() -> NotificationRecord { NotificationRecord( id: item.id, - walletId: walletId.id, + walletId: walletId, readAt: readAt, createdAt: createdAt, item: item diff --git a/Packages/Store/Sources/Stores/InAppNotificationStore.swift b/Packages/Store/Sources/Stores/InAppNotificationStore.swift index c8a64c2bf..c6d36bee3 100644 --- a/Packages/Store/Sources/Stores/InAppNotificationStore.swift +++ b/Packages/Store/Sources/Stores/InAppNotificationStore.swift @@ -11,10 +11,10 @@ public struct InAppNotificationStore: Sendable { self.db = db.dbQueue } - public func addNotifications(_ notifications: [(WalletId, Primitives.InAppNotification)]) throws { + public func addNotifications(_ notifications: [InAppNotification]) throws { try db.write { db in - for (walletId, notification) in notifications { - try notification.record(walletId: walletId).upsert(db) + for notification in notifications { + try notification.record().upsert(db) } } }