diff --git a/Features/Transactions/Sources/ViewModels/TransactionParticipantViewModel.swift b/Features/Transactions/Sources/ViewModels/TransactionParticipantViewModel.swift index 3a8cbbbd2..a74152060 100644 --- a/Features/Transactions/Sources/ViewModels/TransactionParticipantViewModel.swift +++ b/Features/Transactions/Sources/ViewModels/TransactionParticipantViewModel.swift @@ -38,11 +38,13 @@ extension TransactionParticipantViewModel { let address = transactionViewModel.participant let chain = transactionViewModel.transaction.transaction.assetId.chain + let addressName = transactionViewModel.getAddressName(address: address) let account = SimpleAccount( - name: transactionViewModel.getAddressName(address: address), + name: addressName?.name, chain: chain, address: address, - assetImage: nil + assetImage: nil, + addressType: addressName?.type ) return .participant( diff --git a/Features/Transfer/Sources/ViewModels/ConfirmRecipientViewModel.swift b/Features/Transfer/Sources/ViewModels/ConfirmRecipientViewModel.swift index f9d55aee3..68817c48b 100644 --- a/Features/Transfer/Sources/ViewModels/ConfirmRecipientViewModel.swift +++ b/Features/Transfer/Sources/ViewModels/ConfirmRecipientViewModel.swift @@ -4,6 +4,7 @@ import Components import Localization import Primitives import PrimitivesComponents +import Style struct ConfirmRecipientViewModel { private let model: TransferDataViewModel @@ -29,7 +30,8 @@ extension ConfirmRecipientViewModel: ItemModelProvidable { name: addressName?.name ?? model.recipient.name, chain: model.chain, address: model.recipient.address, - assetImage: .none + assetImage: addressNameImage, + addressType: addressName?.type ), mode: .nameOrAddress, addressLink: addressLink @@ -41,6 +43,13 @@ extension ConfirmRecipientViewModel: ItemModelProvidable { // MARK: - Private extension ConfirmRecipientViewModel { + private var addressNameImage: AssetImage? { + switch addressName?.type { + case .contact: .image(Images.System.person) + case .address, .contract, .validator, .none: nil + } + } + private var recipientTitle: String { switch model.type { case .swap: Localized.Common.provider diff --git a/Packages/ChainServices/StakeService/StakeService.swift b/Packages/ChainServices/StakeService/StakeService.swift index ca557abff..fc7493629 100644 --- a/Packages/ChainServices/StakeService/StakeService.swift +++ b/Packages/ChainServices/StakeService/StakeService.swift @@ -84,7 +84,7 @@ extension StakeService { } try store.updateValidators(updateValidators) - let addressNames = updateValidators.map { AddressName(chain: $0.chain, address: $0.id, name: $0.name)} + let addressNames = updateValidators.map { AddressName(chain: $0.chain, address: $0.id, name: $0.name, type: .validator) } try addressStore.addAddressNames(addressNames) } diff --git a/Packages/Components/Sources/AssetImage.swift b/Packages/Components/Sources/AssetImage.swift index 9f6855cd9..ba697e436 100644 --- a/Packages/Components/Sources/AssetImage.swift +++ b/Packages/Components/Sources/AssetImage.swift @@ -10,7 +10,7 @@ public struct AssetImage: Sendable, Equatable { public let chainPlaceholder: Image? public init( - type: String? = .none, + type: String? = .none, imageURL: URL? = .none, placeholder: Image? = .none, chainPlaceholder: Image? = .none diff --git a/Packages/Components/Sources/AssetImageView.swift b/Packages/Components/Sources/AssetImageView.swift index 20d024724..5d4e17a80 100644 --- a/Packages/Components/Sources/AssetImageView.swift +++ b/Packages/Components/Sources/AssetImageView.swift @@ -15,16 +15,17 @@ public struct AssetImageView: View { } private let assetImage: AssetImage private let overlayPadding: CGFloat = 2 - private let cornerRadius: CGFloat + private var cornerRadius: CGFloat { style?.cornerRadius ?? size / 2 } + private let style: Style? public init( assetImage: AssetImage, size: CGFloat = .image.asset, - cornerRadius: CGFloat? = nil + style: Style? = nil ) { self.assetImage = assetImage self.size = max(1, size) - self.cornerRadius = cornerRadius ?? size / 2 + self.style = style } private var overlayOffset: CGFloat { (size / 2) - (overlaySize / 2) } @@ -42,6 +43,9 @@ public struct AssetImageView: View { ) .frame(width: size, height: size) .cornerRadius(cornerRadius) + .ifLet(style?.foregroundColor) { view, color in + view.foregroundStyle(color) + } .overlay(overlayBadge) } @@ -100,6 +104,23 @@ public struct AssetImageView: View { } } +// MARK: - Style + +extension AssetImageView { + public struct Style: Sendable, Equatable { + public let foregroundColor: Color? + public let cornerRadius: CGFloat? + + public init( + foregroundColor: Color? = nil, + cornerRadius: CGFloat? = nil + ) { + self.foregroundColor = foregroundColor + self.cornerRadius = cornerRadius + } + } +} + #Preview { Group { AssetImageView( diff --git a/Packages/Components/Sources/Lists/ListItemImageView.swift b/Packages/Components/Sources/Lists/ListItemImageView.swift index d25f64ebb..e69485b9d 100644 --- a/Packages/Components/Sources/Lists/ListItemImageView.swift +++ b/Packages/Components/Sources/Lists/ListItemImageView.swift @@ -9,14 +9,16 @@ public struct ListItemImageView: View { public let subtitle: String? public let subtitleStyle: TextStyle public let assetImage: AssetImage? + public let assetImageStyle: AssetImageView.Style? public let imageSize: CGFloat public let infoAction: (() -> Void)? - + public init( title: String?, subtitle: String?, subtitleStyle: TextStyle = .calloutSecondary, assetImage: AssetImage? = nil, + assetImageStyle: AssetImageView.Style? = nil, imageSize: CGFloat = .list.image, infoAction: (() -> Void)? = nil ) { @@ -24,6 +26,7 @@ public struct ListItemImageView: View { self.subtitle = subtitle self.subtitleStyle = subtitleStyle self.assetImage = assetImage + self.assetImageStyle = assetImageStyle self.imageSize = imageSize self.infoAction = infoAction } @@ -48,7 +51,7 @@ public struct ListItemImageView: View { infoAction: infoAction ) if let assetImage { - AssetImageView(assetImage: assetImage, size: imageSize) + AssetImageView(assetImage: assetImage, size: imageSize, style: assetImageStyle) } } } diff --git a/Packages/Components/Sources/Lists/ListItemToggleView.swift b/Packages/Components/Sources/Lists/ListItemToggleView.swift index 144896fac..250e5e63e 100644 --- a/Packages/Components/Sources/Lists/ListItemToggleView.swift +++ b/Packages/Components/Sources/Lists/ListItemToggleView.swift @@ -28,7 +28,7 @@ public struct ListItemToggleView: View { AssetImageView( assetImage: imageStyle.assetImage, size: imageStyle.imageSize, - cornerRadius: imageStyle.cornerRadius + style: .init(cornerRadius: imageStyle.cornerRadius) ) } Text(title.text) diff --git a/Packages/Components/Sources/Lists/ListItemView.swift b/Packages/Components/Sources/Lists/ListItemView.swift index f4c786803..0fa2b39dd 100644 --- a/Packages/Components/Sources/Lists/ListItemView.swift +++ b/Packages/Components/Sources/Lists/ListItemView.swift @@ -84,7 +84,7 @@ public struct ListItemView: View { AssetImageView( assetImage: imageStyle.assetImage, size: imageStyle.imageSize, - cornerRadius: imageStyle.cornerRadius + style: .init(cornerRadius: imageStyle.cornerRadius) ) } HStack { diff --git a/Packages/FeatureServices/ContactService/ContactService.swift b/Packages/FeatureServices/ContactService/ContactService.swift index f92aca893..3d56e1e8a 100644 --- a/Packages/FeatureServices/ContactService/ContactService.swift +++ b/Packages/FeatureServices/ContactService/ContactService.swift @@ -37,7 +37,7 @@ public struct ContactService: Sendable { extension ContactService { private func syncAddressNames(contact: Contact, addresses: [ContactAddress]) throws { let addressNames = addresses.map { - AddressName(chain: $0.chain, address: $0.address, name: contact.name) + AddressName(chain: $0.chain, address: $0.address, name: contact.name, type: .contact) } try addressStore.addAddressNames(addressNames) } diff --git a/Packages/Primitives/Sources/AddressName.swift b/Packages/Primitives/Sources/AddressName.swift index c822feca6..17438b803 100644 --- a/Packages/Primitives/Sources/AddressName.swift +++ b/Packages/Primitives/Sources/AddressName.swift @@ -8,10 +8,12 @@ public struct AddressName: Codable, Equatable, Hashable, Sendable { public let chain: Chain public let address: String public let name: String + public let type: AddressType? - public init(chain: Chain, address: String, name: String) { + public init(chain: Chain, address: String, name: String, type: AddressType?) { self.chain = chain self.address = address self.name = name + self.type = type } } diff --git a/Packages/Primitives/Sources/Scan.swift b/Packages/Primitives/Sources/Scan.swift index 037301cf4..945835a03 100644 --- a/Packages/Primitives/Sources/Scan.swift +++ b/Packages/Primitives/Sources/Scan.swift @@ -8,6 +8,7 @@ public enum AddressType: String, Codable, Equatable, Sendable { case address case contract case validator + case contact } public struct ScanAddress: Codable, Equatable, Sendable { diff --git a/Packages/Primitives/TestKit/AddressName+PrimitivesTestKit.swift b/Packages/Primitives/TestKit/AddressName+PrimitivesTestKit.swift index 01d6e5b96..37acab523 100644 --- a/Packages/Primitives/TestKit/AddressName+PrimitivesTestKit.swift +++ b/Packages/Primitives/TestKit/AddressName+PrimitivesTestKit.swift @@ -7,12 +7,14 @@ public extension AddressName { static func mock( chain: Chain = .arbitrum, address: String = "0x2Df1c51E09aECF9cacB7bc98cB1742757f163dF7", - name: String = "Hyperliquid" + name: String = "Hyperliquid", + type: AddressType? = nil ) -> AddressName { AddressName( chain: chain, address: address, - name: name + name: name, + type: type ) } } diff --git a/Packages/PrimitivesComponents/Sources/Components/AddressListItemView.swift b/Packages/PrimitivesComponents/Sources/Components/AddressListItemView.swift index 93ecaff20..426dd303f 100644 --- a/Packages/PrimitivesComponents/Sources/Components/AddressListItemView.swift +++ b/Packages/PrimitivesComponents/Sources/Components/AddressListItemView.swift @@ -20,7 +20,9 @@ public struct AddressListItemView: View { ListItemImageView( title: model.title, subtitle: model.subtitle, - assetImage: model.assetImage + assetImage: model.assetImage, + assetImageStyle: model.assetImageStyle, + imageSize: model.assetImageSize ) .contextMenu( [ diff --git a/Packages/PrimitivesComponents/Sources/Components/TransactionView.swift b/Packages/PrimitivesComponents/Sources/Components/TransactionView.swift index e984f10c2..4b1577148 100644 --- a/Packages/PrimitivesComponents/Sources/Components/TransactionView.swift +++ b/Packages/PrimitivesComponents/Sources/Components/TransactionView.swift @@ -73,8 +73,8 @@ private struct ExplorerMock: ExplorerLinkFetchable { feePrice: nil, assets: [], prices: [], - fromAddress: AddressName(chain: .smartChain, address: "0x92abCE21234D71EC443E679f3a1feAFD3Fc830fB", name: "test1"), - toAddress: AddressName(chain: .smartChain, address: "0x8d7460E51bCf4eD26877cb77E56f3ce7E9f5EB8F", name: "test2") + fromAddress: AddressName(chain: .smartChain, address: "0x92abCE21234D71EC443E679f3a1feAFD3Fc830fB", name: "test1", type: nil), + toAddress: AddressName(chain: .smartChain, address: "0x8d7460E51bCf4eD26877cb77E56f3ce7E9f5EB8F", name: "test2", type: nil) ) let transactionVMMock = TransactionViewModel( diff --git a/Packages/PrimitivesComponents/Sources/Types/SimpleAccount.swift b/Packages/PrimitivesComponents/Sources/Types/SimpleAccount.swift index 955990666..af69bb454 100644 --- a/Packages/PrimitivesComponents/Sources/Types/SimpleAccount.swift +++ b/Packages/PrimitivesComponents/Sources/Types/SimpleAccount.swift @@ -9,11 +9,13 @@ public struct SimpleAccount { public let chain: Chain public let address: String public let assetImage: AssetImage? + public let addressType: AddressType? - public init(name: String?, chain: Chain, address: String, assetImage: AssetImage?) { + public init(name: String?, chain: Chain, address: String, assetImage: AssetImage?, addressType: AddressType? = nil) { self.name = name self.chain = chain self.address = address self.assetImage = assetImage + self.addressType = addressType } } diff --git a/Packages/PrimitivesComponents/Sources/ViewModels/AddressListItemViewModel.swift b/Packages/PrimitivesComponents/Sources/ViewModels/AddressListItemViewModel.swift index d007ea325..ace21c78b 100644 --- a/Packages/PrimitivesComponents/Sources/ViewModels/AddressListItemViewModel.swift +++ b/Packages/PrimitivesComponents/Sources/ViewModels/AddressListItemViewModel.swift @@ -8,7 +8,7 @@ import Formatters import Style public struct AddressListItemViewModel { - + public enum Mode { case auto(addressStyle: AddressFormatter.Style) case address(addressStyle: AddressFormatter.Style) @@ -44,6 +44,20 @@ public struct AddressListItemViewModel { account.assetImage } + public var assetImageStyle: AssetImageView.Style? { + switch account.addressType { + case .contact: AssetImageView.Style(foregroundColor: Colors.secondaryText, cornerRadius: 0) + case .address, .contract, .validator, .none: nil + } + } + + public var assetImageSize: CGFloat { + switch account.addressType { + case .contact: .list.accessory + case .address, .contract, .validator, .none: .list.image + } + } + public var addressExplorerText: String { Localized.Transaction.viewOn(addressLink.name) } diff --git a/Packages/PrimitivesComponents/Sources/ViewModels/TransactionViewModel.swift b/Packages/PrimitivesComponents/Sources/ViewModels/TransactionViewModel.swift index a1247a683..7d79630b6 100644 --- a/Packages/PrimitivesComponents/Sources/ViewModels/TransactionViewModel.swift +++ b/Packages/PrimitivesComponents/Sources/ViewModels/TransactionViewModel.swift @@ -332,13 +332,13 @@ public struct TransactionViewModel: Sendable { private var addressLink: BlockExplorerLink { explorerService.addressUrl(chain: assetId.chain, address: participant) } private var assetId: AssetId { transaction.transaction.assetId } - public func getAddressName(address: String) -> String? { + public func getAddressName(address: String) -> AddressName? { if address == transaction.transaction.from { - return transaction.fromAddress?.name + return transaction.fromAddress } if address == transaction.transaction.to { - return transaction.toAddress?.name + return transaction.toAddress } return .none @@ -351,7 +351,7 @@ public struct TransactionViewModel: Sendable { // MARK: - Private methods private func getDisplayName(address: String, chain: Chain) -> String { - if let name = getAddressName(address: address) { + if let name = getAddressName(address: address)?.name { return name } return AddressFormatter(address: address, chain: chain).value() diff --git a/Packages/PrimitivesComponents/Tests/PrimitivesComponentsTests/AddressListItemViewModelTests.swift b/Packages/PrimitivesComponents/Tests/PrimitivesComponentsTests/AddressListItemViewModelTests.swift index 19cb126e9..e5d65fcd7 100644 --- a/Packages/PrimitivesComponents/Tests/PrimitivesComponentsTests/AddressListItemViewModelTests.swift +++ b/Packages/PrimitivesComponents/Tests/PrimitivesComponentsTests/AddressListItemViewModelTests.swift @@ -52,7 +52,7 @@ extension AddressListItemViewModel { title: String = "Recipient", account: SimpleAccount = SimpleAccount(name: "Alice", chain: .ethereum, address: "0x123456789101112", assetImage: nil), mode: Mode = .auto(addressStyle: .short), - addressLink: BlockExplorerLink = BlockExplorerLink(name: "Mock", link: "https://mock.com") + addressLink: BlockExplorerLink = .init(name: "Mock", link: "https://mock.com") ) -> AddressListItemViewModel { AddressListItemViewModel( title: title, diff --git a/Packages/Store/Sources/Migrations.swift b/Packages/Store/Sources/Migrations.swift index e3191886a..cb28f906e 100644 --- a/Packages/Store/Sources/Migrations.swift +++ b/Packages/Store/Sources/Migrations.swift @@ -395,6 +395,12 @@ struct Migrations { try? ContactAddressRecord.create(db: db) } + migrator.registerMigration("Add type to \(AddressRecord.databaseTableName)") { db in + try? db.alter(table: AddressRecord.databaseTableName) { + $0.add(column: AddressRecord.Columns.type.name, .text) + } + } + try migrator.migrate(dbQueue) } } diff --git a/Packages/Store/Sources/Models/AddressRecord.swift b/Packages/Store/Sources/Models/AddressRecord.swift index a23236bc3..f9fe3c219 100644 --- a/Packages/Store/Sources/Models/AddressRecord.swift +++ b/Packages/Store/Sources/Models/AddressRecord.swift @@ -6,25 +6,29 @@ import Primitives struct AddressRecord: Codable, FetchableRecord, PersistableRecord, Sendable { static let databaseTableName = "addresses" - + enum Columns { static let chain = Column("chain") static let address = Column("address") static let name = Column("name") + static let type = Column("type") } - + let chain: Chain let address: String let name: String - + let type: AddressType? + init( chain: Chain, address: String, - name: String + name: String, + type: AddressType? ) { self.chain = chain self.address = address self.name = name + self.type = type } } @@ -38,6 +42,7 @@ extension AddressRecord: CreateTable { .notNull() $0.column(Columns.name.name, .text) .notNull() + $0.column(Columns.type.name, .text) $0.primaryKey([Columns.chain.name, Columns.address.name]) } } @@ -48,7 +53,8 @@ extension AddressRecord { AddressName( chain: chain, address: address, - name: name + name: name, + type: type ) } } diff --git a/Packages/Store/Sources/Stores/AddressStore.swift b/Packages/Store/Sources/Stores/AddressStore.swift index e4f17c2de..90664a164 100644 --- a/Packages/Store/Sources/Stores/AddressStore.swift +++ b/Packages/Store/Sources/Stores/AddressStore.swift @@ -18,7 +18,8 @@ public struct AddressStore: Sendable { try AddressRecord( chain: addressName.chain, address: addressName.address, - name: addressName.name + name: addressName.name, + type: addressName.type ).save(db, onConflict: .replace) } } diff --git a/Packages/Store/TestKit/AddressStore+TestKit.swift b/Packages/Store/TestKit/AddressStore+TestKit.swift index e180a91a7..fec3bb1a7 100644 --- a/Packages/Store/TestKit/AddressStore+TestKit.swift +++ b/Packages/Store/TestKit/AddressStore+TestKit.swift @@ -3,17 +3,18 @@ import Foundation import Store import Primitives +import PrimitivesTestKit public extension AddressStore { static func mock(db: DB = .mock()) -> Self { AddressStore(db: db) } - + static func mockAddresses(db: DB = .mock()) -> Self { let store = AddressStore(db: db) try? store.addAddressNames([ - AddressName(chain: .ethereum, address: "0x1234567890123456789012345678901234567890", name: "Ethereum"), - AddressName(chain: .bitcoin, address: "bc1qml9s2f9k8wc0882x63lyplzp97srzg2c39fyaw", name: "Bitcoin") + .mock(chain: .ethereum, address: "0x1234567890123456789012345678901234567890", name: "Ethereum"), + .mock(chain: .bitcoin, address: "bc1qml9s2f9k8wc0882x63lyplzp97srzg2c39fyaw", name: "Bitcoin"), ]) return store }