Skip to content

Commit

Permalink
wal2-68: add search token by id functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
ramakser committed Mar 19, 2024
1 parent 172ab00 commit 501f2f4
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 65 deletions.
10 changes: 10 additions & 0 deletions ConcordiumWallet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,10 @@
07E8573526F1D24F001FB2D2 /* RealmHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07E8573326F1D24F001FB2D2 /* RealmHelper.swift */; };
07E8573626F1D24F001FB2D2 /* RealmHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07E8573326F1D24F001FB2D2 /* RealmHelper.swift */; };
07E8573726F1D24F001FB2D2 /* RealmHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07E8573326F1D24F001FB2D2 /* RealmHelper.swift */; };
080F53A32BA9E38800BADD38 /* SearchTokenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080F53A22BA9E38800BADD38 /* SearchTokenViewModel.swift */; };
080F53A42BA9E38800BADD38 /* SearchTokenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080F53A22BA9E38800BADD38 /* SearchTokenViewModel.swift */; };
080F53A52BA9E38800BADD38 /* SearchTokenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080F53A22BA9E38800BADD38 /* SearchTokenViewModel.swift */; };
080F53A62BA9E38800BADD38 /* SearchTokenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080F53A22BA9E38800BADD38 /* SearchTokenViewModel.swift */; };
08F7ABD32B962CCA008588D2 /* CIS2TokenSelectViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08F7ABD22B962CCA008588D2 /* CIS2TokenSelectViewModel.swift */; };
08F7ABD42B962CCA008588D2 /* CIS2TokenSelectViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08F7ABD22B962CCA008588D2 /* CIS2TokenSelectViewModel.swift */; };
08F7ABD52B962CCA008588D2 /* CIS2TokenSelectViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08F7ABD22B962CCA008588D2 /* CIS2TokenSelectViewModel.swift */; };
Expand Down Expand Up @@ -2948,6 +2952,7 @@
07E6E27327B558A30083A852 /* intro_flow_onboarding_en_1.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = intro_flow_onboarding_en_1.html; sourceTree = "<group>"; };
07E6E27527B558A30083A852 /* intro_flow_style.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = intro_flow_style.css; sourceTree = "<group>"; };
07E8573326F1D24F001FB2D2 /* RealmHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealmHelper.swift; sourceTree = "<group>"; };
080F53A22BA9E38800BADD38 /* SearchTokenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTokenViewModel.swift; sourceTree = "<group>"; };
08F7ABD22B962CCA008588D2 /* CIS2TokenSelectViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIS2TokenSelectViewModel.swift; sourceTree = "<group>"; };
17A41E4A266789F10079BD3A /* StagingNet.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = StagingNet.entitlements; sourceTree = "<group>"; };
17A41E4B26678FF80079BD3A /* Concordium ID.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Concordium ID.entitlements"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3716,6 +3721,7 @@
children = (
0154A6772A8CEA0500C33954 /* CIS2TokenSelectView.swift */,
08F7ABD22B962CCA008588D2 /* CIS2TokenSelectViewModel.swift */,
080F53A22BA9E38800BADD38 /* SearchTokenViewModel.swift */,
);
path = TokenSelectionView;
sourceTree = "<group>";
Expand Down Expand Up @@ -6736,6 +6742,7 @@
01145A3D2B03A4F8008331F5 /* Double+Rounding.swift in Sources */,
8DD48C7A2953136E00CA9085 /* EarnView.swift in Sources */,
7F48C1DA244DCD0B00997684 /* LetterSpacingLabel.swift in Sources */,
080F53A32BA9E38800BADD38 /* SearchTokenViewModel.swift in Sources */,
FABA9D95291888E40094012B /* SubmittedSeedAccountView.swift in Sources */,
8992BB212840D056005279C9 /* ShowAlertProtocol+ForceUpdate.swift in Sources */,
7F48C1DB244DCD0B00997684 /* IdentityConfirmedPresenter.swift in Sources */,
Expand Down Expand Up @@ -7270,6 +7277,7 @@
017A5F412A49C32B00E80B56 /* WalletConnectError.swift in Sources */,
5243C034278E2384005CAC8D /* SanityChecker.swift in Sources */,
7F85B787246A9A7C00ED09B8 /* LetterSpacingLabel.swift in Sources */,
080F53A52BA9E38800BADD38 /* SearchTokenViewModel.swift in Sources */,
7F85B788246A9A7C00ED09B8 /* IdentityConfirmedPresenter.swift in Sources */,
89D467DB289B8CAA007D3FC8 /* PrivateIDObjectDataWrapper.swift in Sources */,
6461F5DF29FB2A6C00975BC5 /* TermsAndConditionsViewModel.swift in Sources */,
Expand Down Expand Up @@ -7863,6 +7871,7 @@
017A5F422A49C32C00E80B56 /* WalletConnectError.swift in Sources */,
7F85B881246A9AB600ED09B8 /* AccountTransactionsDataPresenter.swift in Sources */,
5243C035278E2384005CAC8D /* SanityChecker.swift in Sources */,
080F53A62BA9E38800BADD38 /* SearchTokenViewModel.swift in Sources */,
7F85B882246A9AB600ED09B8 /* Metadata.swift in Sources */,
89D467DC289B8CAA007D3FC8 /* PrivateIDObjectDataWrapper.swift in Sources */,
6461F5E029FB2A6C00975BC5 /* TermsAndConditionsViewModel.swift in Sources */,
Expand Down Expand Up @@ -8360,6 +8369,7 @@
7FF07AC423EADBB100F1FC04 /* UITableView+Extensions.swift in Sources */,
7FF07AC123EADBB100F1FC04 /* UIColor+Additions.swift in Sources */,
5237188025753C4700B2BC31 /* MakeGenerateAccountsResponseElement.swift in Sources */,
080F53A42BA9E38800BADD38 /* SearchTokenViewModel.swift in Sources */,
C938F533241FB9B100ECAD47 /* UITextField+Extensions.swift in Sources */,
7FD878592406AF7C00509BDB /* LoginCoordinator.swift in Sources */,
5211BAB7250126B000EE1658 /* ShieldedAccountEncryptionStatus.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ protocol CIS2ServiceProtocol {
func fetchTokensBalance(contractIndex: String, contractSubindex: String, accountAddress: String, tokenId: String) async throws -> [CIS2TokenBalance]
func deleteTokenFromCache(_ token: CIS2TokenSelectionRepresentable) throws
func fetchTokensMetadataDetails(url: URL) async throws -> CIS2TokenMetadataDetails

func getTokenMetadataPair(metadata: CIS2TokensMetadata) async throws -> [(CIS2TokensMetadataItem, CIS2TokenMetadataDetails)]
}

class CIS2Service: CIS2ServiceProtocol {
Expand Down Expand Up @@ -196,3 +198,40 @@ extension CIS2Service {
return metadata
}
}

extension CIS2Service {
/// Retrieves a pair of metadata items and their corresponding token metadata details.
///
/// This function asynchronously retrieves pairs of metadata items and their corresponding token metadata details from the provided `CIS2TokensMetadata`.
/// It utilizes the `fetchTokensMetadataDetails` method of the `service` object to fetch token metadata details for each metadata item's URL.
/// If a metadata item's URL is invalid or fetching the details fails, it will be skipped in the result.
///
/// - Parameter metadata: The `CIS2TokensMetadata` containing the metadata items.
///
/// - Returns: An array of tuples, each containing a `CIS2TokensMetadataItem` and its corresponding `CIS2TokenMetadataDetails`.
///
func getTokenMetadataPair(metadata: CIS2TokensMetadata) async throws -> [(CIS2TokensMetadataItem, CIS2TokenMetadataDetails)] {
var allData = [(CIS2TokensMetadataItem, CIS2TokenMetadataDetails?)]()

try await withThrowingTaskGroup(of: (CIS2TokensMetadataItem, CIS2TokenMetadataDetails?).self) { [weak self] group in
guard let self = self else { return }
for metadata in metadata.metadata {
if let url = URL(string: metadata.metadataURL) {
group.addTask {
let result = try? await self.fetchTokensMetadataDetails(url: url)
return (metadata, result)
}
}
}

for try await data in group {
allData.append(data)
}
}

return allData.compactMap { (metadataItem, tokenMetadataDetails) -> (CIS2TokensMetadataItem, CIS2TokenMetadataDetails)? in
guard let tokenMetadataDetails = tokenMetadataDetails else { return nil }
return (metadataItem, tokenMetadataDetails)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ struct CIS2TokenSelectView: View {
@State var selectedItems: Set<CIS2TokenSelectionRepresentable>

@StateObject var viewModel: CIS2TokenSelectViewModel
@StateObject var searchTokenViewModel: SearchTokenViewModel

var popView: () -> Void
var showDetails: (_ token: CIS2TokenSelectionRepresentable) -> Void
Expand All @@ -19,11 +20,7 @@ struct CIS2TokenSelectView: View {

private let accountAddress: String
private let contractIndex: String

private var filteredTokens: [CIS2TokenSelectionRepresentable] {
viewModel.tokens.filter { tokenIndex.isEmpty ? true : $0.tokenId.contains(tokenIndex) }
}


init(
tokens: [CIS2Token],
accountAdress: String,
Expand All @@ -42,6 +39,7 @@ struct CIS2TokenSelectView: View {
_selectedItems = State(initialValue: Set(service.observedTokens(for: accountAdress, filteredBy: contractIndex)))

_viewModel = .init(wrappedValue: CIS2TokenSelectViewModel(allContractTokens: tokens, accountAdress: accountAdress, contractIndex: contractIndex, service: service))
_searchTokenViewModel = .init(wrappedValue: SearchTokenViewModel(accountAdress: accountAdress, contractIndex: contractIndex, service: service))
}

var body: some View {
Expand All @@ -53,9 +51,15 @@ struct CIS2TokenSelectView: View {
.foregroundColor(.gray)
.padding(.leading, 8)
TextField("Search for token ID", text: $tokenIndex)
.onChange(of: tokenIndex) { _ in searchTokenViewModel.tokens = [] }
.onSubmit {
searchTokenViewModel.runSearch(tokenIndex)
}
.textInputAutocapitalization(.never)
.keyboardType(.numberPad)
/// we should allow user to type in letters since `token_id` is represented in hex
.keyboardType(.default)
.padding(8)
.submitLabel(.search)
}
.background(
RoundedRectangle(cornerRadius: 8)
Expand All @@ -66,34 +70,19 @@ struct CIS2TokenSelectView: View {
GeometryReader { proxy in
ScrollView {
LazyVStack {
if !viewModel.isLoading && viewModel.tokens.isEmpty && viewModel.currentPage != 1 {
ZStack {
Text("No tokens found.")
}
.frame(width: proxy.size.width, height: proxy.size.height)
}

if filteredTokens.isEmpty && !tokenIndex.isEmpty {
ZStack {
Text("No tokens matching given predicate.")
}
.frame(width: proxy.size.width, height: proxy.size.height)
if tokenIndex.isEmpty {
AllTokensListView(proxy)
} else {
ForEach(filteredTokens, id: \.self) { model in
CIS2TokenView(model: model)
.onTapGesture { showDetails(model) }
.padding(.vertical, 8)
.padding(.horizontal, 16)
}
SearchTokensListView(proxy)
}

LoadingStateView(proxy.size)
}

}
}
.refreshable {
viewModel.loadInitial()
if tokenIndex.isEmpty {
viewModel.loadInitial()
}
}
.overlay(
RoundedRectangle(cornerRadius: 8)
Expand Down Expand Up @@ -136,7 +125,44 @@ struct CIS2TokenSelectView: View {
}
}

@ViewBuilder
func AllTokensListView(_ proxy: GeometryProxy) -> some View {
Group {
if !viewModel.isLoading && viewModel.tokens.isEmpty && viewModel.currentPage != 1 {
ZStack {
Text("No tokens found.")
}
.frame(width: proxy.size.width, height: proxy.size.height)
} else {
ForEach(viewModel.tokens, id: \.self) { model in
CIS2TokenView(model: model)
.onTapGesture { showDetails(model) }
.padding(.vertical, 8)
.padding(.horizontal, 16)
}
LoadingStateView(proxy.size)
}
}
}

func SearchTokensListView(_ proxy: GeometryProxy) -> some View {
Group {
if searchTokenViewModel.tokens.isEmpty {
ZStack {
Text("No tokens matching given predicate.")
}
.frame(width: proxy.size.width, height: proxy.size.height)
} else {
ProgressView().opacity(searchTokenViewModel.isSearching ? 1.0 : 0.0)
ForEach(searchTokenViewModel.tokens, id: \.self) { model in
CIS2TokenView(model: model)
.onTapGesture { showDetails(model) }
.padding(.vertical, 8)
.padding(.horizontal, 16)
}
}
}
}

func LoadingStateView(_ size: CGSize) -> some View {
ZStack {
switch(viewModel.isLoading, viewModel.hasMore) {
Expand All @@ -153,7 +179,6 @@ struct CIS2TokenSelectView: View {
.padding(16)
}

@ViewBuilder
func CIS2TokenView(model: CIS2TokenSelectionRepresentable) -> some View {
HStack {
AsyncImage(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ final class CIS2TokenSelectViewModel: ObservableObject {
}

let metadata = try await service.fetchTokensMetadata(contractIndex: contractIndex, contractSubindex: "0", tokenId: ids.map { $0.token }.joined(separator: ","))
let metadataPairs = try await getTokenMetadataPair(metadata: metadata)
let metadataPairs = try await service.getTokenMetadataPair(metadata: metadata)
let balances = try await service.fetchTokensBalance(contractIndex: contractIndex, contractSubindex: "0", accountAddress: accountAddress, tokenId: ids.map { $0.token }.joined(separator: ","))

let representables = metadataPairs.map { (metadataItem, details) in
Expand Down Expand Up @@ -97,39 +97,4 @@ final class CIS2TokenSelectViewModel: ObservableObject {
}
}
}

/// Retrieves a pair of metadata items and their corresponding token metadata details.
///
/// This function asynchronously retrieves pairs of metadata items and their corresponding token metadata details from the provided `CIS2TokensMetadata`.
/// It utilizes the `fetchTokensMetadataDetails` method of the `service` object to fetch token metadata details for each metadata item's URL.
/// If a metadata item's URL is invalid or fetching the details fails, it will be skipped in the result.
///
/// - Parameter metadata: The `CIS2TokensMetadata` containing the metadata items.
///
/// - Returns: An array of tuples, each containing a `CIS2TokensMetadataItem` and its corresponding `CIS2TokenMetadataDetails`.
///
func getTokenMetadataPair(metadata: CIS2TokensMetadata) async throws -> [(CIS2TokensMetadataItem, CIS2TokenMetadataDetails)] {
var allData = [(CIS2TokensMetadataItem, CIS2TokenMetadataDetails?)]()

try await withThrowingTaskGroup(of: (CIS2TokensMetadataItem, CIS2TokenMetadataDetails?).self) { [weak self] group in
guard let self = self else { return }
for metadata in metadata.metadata {
if let url = URL(string: metadata.metadataURL) {
group.addTask {
let result = try? await self.service.fetchTokensMetadataDetails(url: url)
return (metadata, result)
}
}
}

for try await data in group {
allData.append(data)
}
}

return allData.compactMap { (metadataItem, tokenMetadataDetails) -> (CIS2TokensMetadataItem, CIS2TokenMetadataDetails)? in
guard let tokenMetadataDetails = tokenMetadataDetails else { return nil }
return (metadataItem, tokenMetadataDetails)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//
// SearchTokenViewModel.swift
// ConcordiumWallet
//
// Created by Maksym Rachytskyy on 19.03.2024.
// Copyright © 2024 concordium. All rights reserved.
//

import Foundation
import BigInt

final class SearchTokenViewModel: ObservableObject {
@Published var tokens: [CIS2TokenSelectionRepresentable] = []
@Published var isSearching: Bool = false

private let service: CIS2ServiceProtocol
private let accountAddress: String
private let contractIndex: String

init(
accountAdress: String,
contractIndex: String,
service: CIS2ServiceProtocol
){
self.service = service
self.accountAddress = accountAdress
self.contractIndex = contractIndex
}

func runSearch(_ tokenIndex: String) {
isSearching = true
Task {
do {
let data = try await searchTokenData(by: tokenIndex)
await MainActor.run {
tokens = data
isSearching = false
}
} catch {
await MainActor.run {
isSearching = false
}
}
}
}

func searchTokenData(by tokenId: String) async throws -> [CIS2TokenSelectionRepresentable] {
let metadata = try await service.fetchTokensMetadata(contractIndex: contractIndex, contractSubindex: "0", tokenId: tokenId)
let metadataPairs = try await service.getTokenMetadataPair(metadata: metadata)
let balances = try await service.fetchTokensBalance(contractIndex: contractIndex, contractSubindex: "0", accountAddress: accountAddress, tokenId: tokenId)

return metadataPairs.map { (metadataItem, details) in
CIS2TokenSelectionRepresentable(
contractName: metadata.contractName,
tokenId: metadataItem.tokenId,
balance: BigInt(balances.first(where: { $0.tokenId == metadataItem.tokenId })?.balance ?? "") ?? .zero,
contractIndex: contractIndex,
name: details.name,
symbol: details.symbol,
decimals: details.decimals ?? 6,
description: details.description,
thumbnail: details.thumbnail?.url,
display: details.display?.url,
unique: details.unique ?? false,
accountAddress: accountAddress)
}
}
}

0 comments on commit 501f2f4

Please sign in to comment.