diff --git a/Packages/KeyAppKit/Package.swift b/Packages/KeyAppKit/Package.swift index da6fef76f2..c8d5234503 100644 --- a/Packages/KeyAppKit/Package.swift +++ b/Packages/KeyAppKit/Package.swift @@ -66,6 +66,12 @@ let package = Package( targets: ["History"] ), + // Repository + .library( + name: "Repository", + targets: ["Repository"] + ), + // Sell .library( name: "Sell", @@ -100,6 +106,11 @@ let package = Package( targets: ["TokenService"] ), + .library( + name: "SendService", + targets: ["SendService"] + ), + // KeyAppBusiness .library( name: "KeyAppBusiness", @@ -117,7 +128,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/p2p-org/solana-swift", branch: "feature/token-2022"), + .package(url: "https://github.com/p2p-org/solana-swift", branch: "main"), .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMajor(from: "1.6.0")), .package(url: "https://github.com/Boilertalk/Web3.swift.git", from: "0.6.0"), // .package(url: "https://github.com/trustwallet/wallet-core", branch: "master"), @@ -236,6 +247,7 @@ let package = Package( "Wormhole", "KeyAppNetworking", "TokenService", + "SendService", ] ), @@ -254,6 +266,12 @@ let package = Package( ] ), + // Repository + .target( + name: "Repository", + dependencies: [] + ), + // Sell .target( name: "Sell", @@ -300,6 +318,13 @@ let package = Package( ] ), + .target( + name: "SendService", + dependencies: [ + "KeyAppNetworking", + ] + ), + .target( name: "KeyAppBusiness", dependencies: [ diff --git a/Packages/KeyAppKit/Sources/FeeRelayerSwift/Helpers/SolanaAPIClient+FeeRelayer.swift b/Packages/KeyAppKit/Sources/FeeRelayerSwift/Helpers/SolanaAPIClient+FeeRelayer.swift index 76c9465a1c..eac4c1ab68 100644 --- a/Packages/KeyAppKit/Sources/FeeRelayerSwift/Helpers/SolanaAPIClient+FeeRelayer.swift +++ b/Packages/KeyAppKit/Sources/FeeRelayerSwift/Helpers/SolanaAPIClient+FeeRelayer.swift @@ -12,7 +12,7 @@ extension SolanaAPIClient { /// /// - Returns: The associated address. func getAssociatedSPLTokenAddress(for address: PublicKey, mint: PublicKey) async throws -> PublicKey { - let account: BufferInfo? = try? await getAccountInfo(account: address.base58EncodedString) + let account: BufferInfo? = try? await getAccountInfo(account: address.base58EncodedString) // The account doesn't exists if account == nil { diff --git a/Packages/KeyAppKit/Sources/FeeRelayerSwift/Relay/Helpers/DestinationAnalysator.swift b/Packages/KeyAppKit/Sources/FeeRelayerSwift/Relay/Helpers/DestinationAnalysator.swift index 7367efc6ab..e5d0df43ee 100644 --- a/Packages/KeyAppKit/Sources/FeeRelayerSwift/Relay/Helpers/DestinationAnalysator.swift +++ b/Packages/KeyAppKit/Sources/FeeRelayerSwift/Relay/Helpers/DestinationAnalysator.swift @@ -44,9 +44,9 @@ public class DestinationAnalysatorImpl: DestinationAnalysator { let address = try await solanaAPIClient.getAssociatedSPLTokenAddress(for: owner, mint: mint) // Check destination address is exist. - let info: BufferInfo? = try? await solanaAPIClient + let info: BufferInfo? = try? await solanaAPIClient .getAccountInfo(account: address.base58EncodedString) - let needsCreateDestinationTokenAccount = !PublicKey.isSPLTokenOrToken2022ProgramId(info?.owner) + let needsCreateDestinationTokenAccount = !PublicKey.isSPLTokenProgram(info?.owner) return .splAccount(needsCreation: needsCreateDestinationTokenAccount) } diff --git a/Packages/KeyAppKit/Sources/FeeRelayerSwift/Relay/Helpers/TransitTokenAccountManager.swift b/Packages/KeyAppKit/Sources/FeeRelayerSwift/Relay/Helpers/TransitTokenAccountManager.swift index c677d70d44..b90daed57d 100644 --- a/Packages/KeyAppKit/Sources/FeeRelayerSwift/Relay/Helpers/TransitTokenAccountManager.swift +++ b/Packages/KeyAppKit/Sources/FeeRelayerSwift/Relay/Helpers/TransitTokenAccountManager.swift @@ -48,7 +48,7 @@ public class TransitTokenAccountManagerImpl: TransitTokenAccountManager { public func checkIfNeedsCreateTransitTokenAccount(transitToken: TokenAccount?) async throws -> Bool? { guard let transitToken = transitToken else { return nil } - guard let account: BufferInfo = try await solanaAPIClient + guard let account: BufferInfo = try await solanaAPIClient .getAccountInfo(account: transitToken.address.base58EncodedString) else { return true diff --git a/Packages/KeyAppKit/Sources/FeeRelayerSwift/Relay/RelaySwap/TransactionBuilder/SwapTransactionBuilderImpl/SwapTransactionBuilderImpl+Checks/SwapTransactionBuilderImpl+CheckDestination.swift b/Packages/KeyAppKit/Sources/FeeRelayerSwift/Relay/RelaySwap/TransactionBuilder/SwapTransactionBuilderImpl/SwapTransactionBuilderImpl+Checks/SwapTransactionBuilderImpl+CheckDestination.swift index ac2b9ed9d0..bf5ab5ce68 100644 --- a/Packages/KeyAppKit/Sources/FeeRelayerSwift/Relay/RelaySwap/TransactionBuilder/SwapTransactionBuilderImpl/SwapTransactionBuilderImpl+Checks/SwapTransactionBuilderImpl+CheckDestination.swift +++ b/Packages/KeyAppKit/Sources/FeeRelayerSwift/Relay/RelaySwap/TransactionBuilder/SwapTransactionBuilderImpl/SwapTransactionBuilderImpl+Checks/SwapTransactionBuilderImpl+CheckDestination.swift @@ -32,7 +32,7 @@ extension SwapTransactionBuilderImpl { from: feePayerAddress, toNewPubkey: destinationNewAccount.publicKey, lamports: minimumTokenAccountBalance, - space: SPLTokenAccountState.BUFFER_LENGTH, + space: TokenAccountState.BUFFER_LENGTH, programId: TokenProgram.id ), TokenProgram.initializeAccountInstruction( diff --git a/Packages/KeyAppKit/Sources/FeeRelayerSwift/Relay/RelaySwap/TransactionBuilder/SwapTransactionBuilderImpl/SwapTransactionBuilderImpl+Checks/SwapTransactionBuilderImpl+CheckSource.swift b/Packages/KeyAppKit/Sources/FeeRelayerSwift/Relay/RelaySwap/TransactionBuilder/SwapTransactionBuilderImpl/SwapTransactionBuilderImpl+Checks/SwapTransactionBuilderImpl+CheckSource.swift index 3580969f52..bfbed5612e 100644 --- a/Packages/KeyAppKit/Sources/FeeRelayerSwift/Relay/RelaySwap/TransactionBuilder/SwapTransactionBuilderImpl/SwapTransactionBuilderImpl+Checks/SwapTransactionBuilderImpl+CheckSource.swift +++ b/Packages/KeyAppKit/Sources/FeeRelayerSwift/Relay/RelaySwap/TransactionBuilder/SwapTransactionBuilderImpl/SwapTransactionBuilderImpl+Checks/SwapTransactionBuilderImpl+CheckSource.swift @@ -28,7 +28,7 @@ extension SwapTransactionBuilderImpl { from: feePayerAddress, toNewPubkey: sourceWSOLNewAccount!.publicKey, lamports: minimumTokenAccountBalance + inputAmount, - space: SPLTokenAccountState.BUFFER_LENGTH, + space: TokenAccountState.BUFFER_LENGTH, programId: TokenProgram.id ), TokenProgram.initializeAccountInstruction( diff --git a/Packages/KeyAppKit/Sources/KeyAppBusiness/Solana/Socket/RealtimeSolanaAccountService.swift b/Packages/KeyAppKit/Sources/KeyAppBusiness/Solana/Socket/RealtimeSolanaAccountService.swift index 3e241db5bc..a7e4df2211 100644 --- a/Packages/KeyAppKit/Sources/KeyAppBusiness/Solana/Socket/RealtimeSolanaAccountService.swift +++ b/Packages/KeyAppKit/Sources/KeyAppBusiness/Solana/Socket/RealtimeSolanaAccountService.swift @@ -346,7 +346,7 @@ final class RealtimeSolanaAccountServiceImpl: RealtimeSolanaAccountService { // Parse var reader = BinaryReader(bytes: data.bytes) - let tokenAccountData = try SPLTokenAccountState(from: &reader) + let tokenAccountData = try TokenAccountState(from: &reader) // Get token let token = try await tokensService.get(address: tokenAccountData.mint) diff --git a/Packages/KeyAppKit/Sources/OrcaSwapSwift/OrcaSwap/OrcaSwap+Extensions.swift b/Packages/KeyAppKit/Sources/OrcaSwapSwift/OrcaSwap/OrcaSwap+Extensions.swift index c2f6897c3b..343cc7c768 100644 --- a/Packages/KeyAppKit/Sources/OrcaSwapSwift/OrcaSwap/OrcaSwap+Extensions.swift +++ b/Packages/KeyAppKit/Sources/OrcaSwapSwift/OrcaSwap/OrcaSwap+Extensions.swift @@ -108,9 +108,9 @@ extension OrcaSwap { } else { try Task.checkCancellation() let pool = pool - async let tokenAMinRentResult: BufferInfo? = solanaClient + async let tokenAMinRentResult: BufferInfo? = solanaClient .getAccountInfo(account: pool.tokenAccountA) - async let tokenBMinRentResult: BufferInfo? = solanaClient + async let tokenBMinRentResult: BufferInfo? = solanaClient .getAccountInfo(account: pool.tokenAccountB) (tokenAMinRent, tokenBMinRent) = try await( tokenAMinRentResult?.lamports ?? 2_039_280, diff --git a/Packages/KeyAppKit/Sources/Repository/Models/LoadingState.swift b/Packages/KeyAppKit/Sources/Repository/Models/LoadingState.swift new file mode 100644 index 0000000000..638e6368da --- /dev/null +++ b/Packages/KeyAppKit/Sources/Repository/Models/LoadingState.swift @@ -0,0 +1,39 @@ +import Foundation + +/// Loading state for a specific time-consuming operation +public enum LoadingState { + /// Nothing loaded + case initialized + /// Data is loading + case loading + /// Data is loaded + case loaded + /// Error + case error +} + +public enum ListLoadingState { + public enum Status { + case loading + case loaded + case error(Error) + } + + public enum LoadMoreStatus { + case loading + case reachedEndOfList + case error(Error) + } + + case empty(Status) + case nonEmpty(loadMoreStatus: LoadMoreStatus) + + public var isEmpty: Bool { + switch self { + case .empty: + return true + default: + return false + } + } +} diff --git a/Packages/KeyAppKit/Sources/Repository/Provider/ListProvider.swift b/Packages/KeyAppKit/Sources/Repository/Provider/ListProvider.swift new file mode 100644 index 0000000000..839f0670ce --- /dev/null +++ b/Packages/KeyAppKit/Sources/Repository/Provider/ListProvider.swift @@ -0,0 +1,17 @@ +import Foundation + +/// Repository that is only responsible for fetching list of items +public protocol ListProvider { + /// ListItemType to be fetched + associatedtype ItemType: Hashable & Identifiable + /// Indicate if should fetching item + func shouldFetch() -> Bool + /// Fetch list of item from outside + func fetch() async throws -> [ItemType] +} + +public extension ListProvider { + func shouldFetch() -> Bool { + true + } +} diff --git a/Packages/KeyAppKit/Sources/Repository/Provider/PaginatedListFetcher/PaginatedListProvider.swift b/Packages/KeyAppKit/Sources/Repository/Provider/PaginatedListFetcher/PaginatedListProvider.swift new file mode 100644 index 0000000000..2eb446dfe6 --- /dev/null +++ b/Packages/KeyAppKit/Sources/Repository/Provider/PaginatedListFetcher/PaginatedListProvider.swift @@ -0,0 +1,13 @@ +import Foundation + +public protocol PaginatedListProvider: ListProvider { + associatedtype PS: PaginationStrategy + /// Pagination strategy + var paginationStrategy: PS { get } +} + +public extension PaginatedListProvider { + @MainActor func shouldFetch() -> Bool { + !paginationStrategy.isLastPageLoaded + } +} diff --git a/Packages/KeyAppKit/Sources/Repository/Provider/PaginatedListFetcher/PaginationStrategy/LimitOffsetPaginationStrategy.swift b/Packages/KeyAppKit/Sources/Repository/Provider/PaginatedListFetcher/PaginationStrategy/LimitOffsetPaginationStrategy.swift new file mode 100644 index 0000000000..45718dc6af --- /dev/null +++ b/Packages/KeyAppKit/Sources/Repository/Provider/PaginatedListFetcher/PaginationStrategy/LimitOffsetPaginationStrategy.swift @@ -0,0 +1,34 @@ +import Foundation + +/// PaginationStrategy using limit and offset +@MainActor +public class LimitOffsetPaginationStrategy: PaginationStrategy { + // MARK: - Properties + + private let limit: Int + private(set) var offset: Int = 0 + public private(set) var isLastPageLoaded: Bool = false + + // MARK: - Initializer + + public nonisolated init(limit: Int) { + self.limit = limit + } + + public func checkIfLastPageLoaded(lastSnapshot: [ItemType]?) { + guard let lastSnapshot else { + isLastPageLoaded = true + return + } + isLastPageLoaded = lastSnapshot.count < limit + } + + public func resetPagination() { + offset = 0 + isLastPageLoaded = false + } + + public func moveToNextPage() { + offset += limit + } +} diff --git a/Packages/KeyAppKit/Sources/Repository/Provider/PaginatedListFetcher/PaginationStrategy/PaginationStrategy.swift b/Packages/KeyAppKit/Sources/Repository/Provider/PaginatedListFetcher/PaginationStrategy/PaginationStrategy.swift new file mode 100644 index 0000000000..c6d0940561 --- /dev/null +++ b/Packages/KeyAppKit/Sources/Repository/Provider/PaginatedListFetcher/PaginationStrategy/PaginationStrategy.swift @@ -0,0 +1,14 @@ +import Foundation + +/// Strategy of to define how pagination works in ListRepository +@MainActor +public protocol PaginationStrategy { + /// Boolean value to indicate that last page was loaded or not + var isLastPageLoaded: Bool { get } + /// Check if last page loaded + func checkIfLastPageLoaded(lastSnapshot: [ItemType]?) + /// Reset pagination + func resetPagination() + /// Move to next page + func moveToNextPage() +} diff --git a/Packages/KeyAppKit/Sources/Repository/Provider/Provider.swift b/Packages/KeyAppKit/Sources/Repository/Provider/Provider.swift new file mode 100644 index 0000000000..f730ec2212 --- /dev/null +++ b/Packages/KeyAppKit/Sources/Repository/Provider/Provider.swift @@ -0,0 +1,51 @@ +import Foundation + +/// Repository that is only responsible for fetching item +public protocol Provider { + /// ItemType to be fetched + associatedtype ItemType + /// Indicate if should fetching item + func shouldFetch() -> Bool + /// Fetch item from outside + func fetch() async throws -> ItemType? +} + +public extension Provider { + func shouldFetch() -> Bool { + true + } +} + +// class ListRepository: AnyListRepository { +// // MARK: - Properties +// +// /// Strategy that indicates how pagination works, nil if pagination is disabled +// let paginationStrategy: PaginationStrategy? +// +// // MARK: - Initializer +// init(paginationStrategy: PaginationStrategy? = nil) { +// self.paginationStrategy = paginationStrategy +// } +// +// func shouldFetch() -> Bool { +// var shouldRequest: Bool = true +// +// // check if isLastPageLoaded +// if let paginationStrategy { +// shouldRequest = shouldRequest && !paginationStrategy.isLastPageLoaded +// } +// +// return shouldRequest +// } +// +// func fetch() async throws -> [ItemType] { +// fatalError("Must override") +// } +// } + +// extension AsyncSequence: Repository { +// func fetch() async throws { +// let iterator = makeAsyncIterator() +// return iterator.next() +// } +// } diff --git a/Packages/KeyAppKit/Sources/Repository/Repository/ListRepository/ListRepository.swift b/Packages/KeyAppKit/Sources/Repository/Repository/ListRepository/ListRepository.swift new file mode 100644 index 0000000000..bb6f958ce2 --- /dev/null +++ b/Packages/KeyAppKit/Sources/Repository/Repository/ListRepository/ListRepository.swift @@ -0,0 +1,235 @@ +import Combine +import Foundation + +/// Reusable ViewModel to manage a List of some Kind of item +open class ListRepository: ObservableObject { + // MARK: - Associated types + + /// Type of the item + public typealias ItemType = P.ItemType + + // MARK: - Public properties + + /// Repository that is responsible for fetching data + public let provider: P + + /// Current running task + public let taskStorage = TaskStorage<[ItemType]>() + + /// The current data + @Published @MainActor public var data: [ItemType] = [] + + /// The current loading state of the data + @Published @MainActor public var isLoading = false + + /// Optional error if occurred + @Published @MainActor public var error: Error? + + // MARK: - Initializer + + /// ItemViewModel's initializer + /// - Parameters: + /// - initialData: initial data for begining state of the Repository + /// - repository: repository to handle data fetching + public init( + initialData: [ItemType] = [], + provider: P + ) { + self.provider = provider + + // feed data with initial data + if !initialData.isEmpty { + Task { + await handleNewData(initialData) + } + } + } + + // MARK: - Actions + + /// Erase data and reset repository to its initial state + @MainActor + open func flush() { + data = [] + isLoading = false + error = nil + } + + /// Erase and reload all data + open func reload() async { + await flush() + await request() + } + + /// Refresh data without erasing current data + open func refresh() async { + await request() + } + + /// Request data from outside to get new data + /// - Returns: New data + @discardableResult + open func request() async -> Result<[ItemType], Error> { + // prevent unwanted request + guard provider.shouldFetch() else { + return .failure(CancellationError()) + } + + // cancel previous request + await taskStorage.cancelCurrentTask() + + // mark as loading + await MainActor.run { + isLoading = true + error = nil + } + + // assign and execute loadingTask + await taskStorage.save( + Task { [unowned self] in + try await provider.fetch() + } + ) + + // await value + do { + let newData = try await taskStorage.loadingTask!.value + await handleNewData(newData) + return .success(newData) + } catch { + // ignore cancellation error + if !(error is CancellationError) { + await handleError(error) + } + return .failure(error) + } + } + + /// Handle new data that just received + /// - Parameter newData: the new data received + @MainActor open func handleNewData(_ newData: [ItemType]) { + data = newData + error = nil + isLoading = false + } + + /// Handle error when received + /// - Parameter err: the error received + @MainActor open func handleError(_ err: Error) { + error = err + isLoading = false + } + + // MARK: - Getter + + /// List loading state + @MainActor + open var state: ListLoadingState { + // Empty state + if data.isEmpty { + let status: ListLoadingState.Status + + // empty loading + if isLoading { + status = .loading + } + + // empty error + else if let error { + status = .error(error) + } + + // empty + else { + status = .loaded + } + return .empty(status) + } + + // Non-empty state + else { + return .nonEmpty(loadMoreStatus: .reachedEndOfList) + } + } + +// /// Override data +// func overrideData(by newData: [ItemType]) { +// guard state == .loaded else { return } +// handleNewData(newData) +// } + +// /// Update multiple records with a closure +// /// - Parameter closure: updating closure +// func batchUpdate(closure: ([ItemType]) -> [ItemType]) { +// let newData = closure(data) +// overrideData(by: newData) +// } +// +// /// Update item that matchs predicate +// /// - Parameters: +// /// - predicate: predicate to find item +// /// - transform: transform item before udpate +// /// - Returns: true if updated, false if not +// @discardableResult +// func updateItem(where predicate: (ItemType) -> Bool, transform: (ItemType) -> ItemType?) -> Bool { +// // modify items +// var itemsChanged = false +// if let index = data.firstIndex(where: predicate), +// let item = transform(data[index]), +// item != data[index] +// { +// itemsChanged = true +// var data = self.data +// data[index] = item +// overrideData(by: data) +// } +// +// return itemsChanged +// } +// +// /// Insert item into list or update if needed +// /// - Parameters: +// /// - item: item to be inserted +// /// - predicate: predicate to find item +// /// - shouldUpdate: should update instead +// /// - Returns: true if inserted, false if not +// @discardableResult +// func insert(_ item: ItemType, where predicate: ((ItemType) -> Bool)? = nil, shouldUpdate: Bool = false) -> Bool +// { +// var items = data +// +// // update mode +// if let predicate = predicate { +// if let index = items.firstIndex(where: predicate), shouldUpdate { +// items[index] = item +// overrideData(by: items) +// return true +// } +// } +// +// // insert mode +// else { +// items.append(item) +// overrideData(by: items) +// return true +// } +// +// return false +// } +// +// /// Remove item that matches a predicate from list +// /// - Parameter predicate: predicate to find item +// /// - Returns: removed item +// @discardableResult +// func removeItem(where predicate: (ItemType) -> Bool) -> ItemType? { +// var result: ItemType? +// var data = self.data +// if let index = data.firstIndex(where: predicate) { +// result = data.remove(at: index) +// } +// if result != nil { +// overrideData(by: data) +// } +// return nil +// } +} diff --git a/Packages/KeyAppKit/Sources/Repository/Repository/ListRepository/PaginatedListRepository.swift b/Packages/KeyAppKit/Sources/Repository/Repository/ListRepository/PaginatedListRepository.swift new file mode 100644 index 0000000000..1b6f680020 --- /dev/null +++ b/Packages/KeyAppKit/Sources/Repository/Repository/ListRepository/PaginatedListRepository.swift @@ -0,0 +1,136 @@ +import Foundation + +/// Reusable ViewModel to manage a paginated List of some Kind of item +open class PaginatedListRepository: ListRepository

{ + // MARK: - Properties + + /// Result count of first request, can be use to keeps first record on refreshing + @MainActor private var firstPageCount: Int? + + // MARK: - Actions + + /// Erase data and reset repository to its initial state + @MainActor + override open func flush() { + provider.paginationStrategy.resetPagination() + super.flush() + } + + /// Refresh data + override open func refresh() async { + // keep first page as placeholder + await MainActor.run { + data = Array(data.prefix(firstPageCount ?? 0)) + isLoading = false + error = nil + } + + // reset pagination + await provider.paginationStrategy.resetPagination() + + // request to update first page + let firstPageResult = await request() + switch firstPageResult { + case let .success(firstPage): + // replace first page + await MainActor.run { + data = firstPage + isLoading = false + error = nil + } + + case let .failure(failure): + guard !(failure is CancellationError) else { + return + } + + await MainActor.run { + data = [] + isLoading = false + error = failure + } + } + } + + /// Handle new data that just received + /// - Parameter newData: the new data received + @MainActor + override open func handleNewData(_ newData: [ItemType]) { + // cache first page count + if firstPageCount == nil { + firstPageCount = newData.count + } + + // check if last page loaded + provider.paginationStrategy.checkIfLastPageLoaded(lastSnapshot: newData) + + // move to next page + provider.paginationStrategy.moveToNextPage() + + // append data that is currently not existed in current data array + // keep objects in data unique by removing item with duplicated id + data = (data + newData) + .reduce([]) { result, current -> [ItemType] in + // keep last updated item by default + if result.contains(where: { $0.id == current.id }) { + return result + } + + // append new + else { + return result + [current] + } + } + super.handleNewData(data) + } + + /// Fetch next records if pagination is enabled + open func fetchNext() async { + // call request + await request() + } + + /// List loading state + @MainActor + override open var state: ListLoadingState { + let state = super.state + + switch state { + case .nonEmpty: + if provider.shouldFetch() { + // Error at the end of the list + if let error { + return .nonEmpty(loadMoreStatus: .error(error)) + } + + // Loading at the end of the list + else { + return .nonEmpty(loadMoreStatus: .loading) + } + } else { + return .nonEmpty(loadMoreStatus: .reachedEndOfList) + } + default: + return state + } + } + +// func updateFirstPage(onSuccessFilterNewData: (([ItemType]) -> [ItemType])? = nil) { +// let originalOffset = offset +// offset = 0 +// +// task?.cancel() +// +// task = Task { +// let onSuccess = onSuccessFilterNewData ?? {[weak self] newData in +// newData.filter {!(self?.data.contains($0) == true)} +// } +// var data = self.data +// let newData = try await self.createRequest() +// data = onSuccess(newData) + data +// self.overrideData(by: data) +// } +// +// offset = originalOffset +// } +} diff --git a/Packages/KeyAppKit/Sources/Repository/Repository/ListRepository/SectionedListRepository.swift b/Packages/KeyAppKit/Sources/Repository/Repository/ListRepository/SectionedListRepository.swift new file mode 100644 index 0000000000..8f13c4d345 --- /dev/null +++ b/Packages/KeyAppKit/Sources/Repository/Repository/ListRepository/SectionedListRepository.swift @@ -0,0 +1,131 @@ +import Combine +import Foundation + +/// Section of a list +protocol ListSection: Hashable { + /// Type of item in the section + associatedtype ItemType: Hashable & Identifiable + var id: String { get } + /// List of items in section + var items: [ItemType] { get } + /// state of the section + var loadingState: LoadingState { get } + /// error of the section + var error: String? { get } // TODO: - Error type is not Hashable +} + +/// Define if a viewModel can be convertible to sections +protocol SectionsConvertibleListRepository: ObservableObject { + /// Type of item in the section + associatedtype ItemType: Hashable & Identifiable + associatedtype Section: ListSection + /// Map data to sections + var sections: [Section] { get } + /// Erase all data + func flush() + /// Reload data + func reload() async throws + /// Refresh + func refresh() async throws +} + +/// Reusable list view model of a sectioned list +// @MainActor +// class SectionedListViewModel: ObservableObject { +// +// // MARK: - Properties +// +// /// Combine subscriptions +// private var subscriptions = Set() +// +// /// List view models to handle different type of data, each viewmodel represent one or more sections +// private let listViewModels: [any SectionsConvertibleListViewModel] +// +// /// Initial data for initializing state +// private let initialData: [any ListSection] +// +// /// Sections in list +// @Published var sections: [any ListSection] = [] +// +// // MARK: - Initializer +// +// /// SectionedListViewModel initializer +// /// - Parameters: +// /// - initialData: initial data for begining state +// /// - listViewModels: listViewModels to handle data +// init( +// initialData: [any ListSection] = [], +// listViewModels: [any SectionsConvertibleListViewModel] +// ) { +// self.initialData = initialData +// self.listViewModels = listViewModels +// +// sections = initialData +// bind() +// } +// +// // MARK: - Binding +// +// private func bind() { +// // assertion +// guard !listViewModels.isEmpty else { return } +// +// // combine data +// var publisher = listViewModels.first! +// .sectionsPublisher +// .eraseToAnyPublisher() +// +// for viewModel in listViewModels { +// publisher = publisher +// .combineLatest(viewModel.sectionsPublisher) +// .map { $0 + $1 } +// .eraseToAnyPublisher() +// } +// +// publisher +// .receive(on: DispatchQueue.main) +// .assign(to: \.sections, on: self) +// .store(in: &subscriptions) +// } +// +// // MARK: - Actions +// +// /// Erase data and reset repository to its initial state +// func flush() { +// // flush viewModels data +// for viewModel in listViewModels { +// viewModel.flush() +// } +// +// // flush this viewModel +// sections = initialData +// } +// +// /// Erase and reload all data +// func reload() async throws { +// // request in parallel +// try await withThrowingTaskGroup(of: Void.self) { group in +// for viewModel in listViewModels { +// group.addTask { +// try await viewModel.reload() +// } +// } +// +// for try await _ in group {} +// } +// } +// +// /// Refresh data without erasing current data +// func refresh() async throws { +// // request in parallel +// try await withThrowingTaskGroup(of: Void.self) { group in +// for viewModel in listViewModels { +// group.addTask { +// try await viewModel.refresh() +// } +// } +// +// for try await _ in group {} +// } +// } +// } diff --git a/Packages/KeyAppKit/Sources/Repository/Repository/Repository.swift b/Packages/KeyAppKit/Sources/Repository/Repository/Repository.swift new file mode 100644 index 0000000000..54d57bf8b4 --- /dev/null +++ b/Packages/KeyAppKit/Sources/Repository/Repository/Repository.swift @@ -0,0 +1,143 @@ +import Combine +import Foundation + +/// Reusable ViewModel to manage item +open class Repository: ObservableObject { + // MARK: - Associated types + + /// Type of the item + public typealias ItemType = P.ItemType + + // MARK: - Public properties + + /// Repository that is responsible for fetching data + public let provider: P + + /// Current running task + private let taskStorage = TaskStorage() + + /// The current data + @Published @MainActor public private(set) var data: ItemType? + + /// The current loading state of the data + @Published @MainActor public private(set) var isLoading = false + + /// Optional error if occurred + @Published @MainActor public private(set) var error: Error? + + // MARK: - Initializer + + /// ItemViewModel's initializer + /// - Parameters: + /// - initialData: initial data for begining state of the Repository + /// - repository: repository to handle data fetching + public init( + initialData: ItemType?, + provider: P + ) { + self.provider = provider + + // feed data with initial data + Task { + await handleNewData(initialData) + } + } + + // MARK: - Actions + + /// Erase data and reset repository to its initial state + @MainActor + open func flush() { + data = nil + isLoading = false + error = nil + } + + /// Erase and reload all data + open func reload() async { + await flush() + await request() + } + + /// Refresh data without erasing current data + open func refresh() async { + await request() + } + + /// Request data from outside to get new data + @discardableResult + open func request() async -> Result { + // prevent unwanted request + guard provider.shouldFetch() else { + return .failure(CancellationError()) + } + + // cancel previous request + await taskStorage.cancelCurrentTask() + + // mark as loading + await MainActor.run { + isLoading = true + error = nil + } + + // assign and execute loadingTask + await taskStorage.save( + Task { [unowned self] in + try await provider.fetch() + } + ) + + // await value + do { + let newData = try await taskStorage.loadingTask?.value + await handleNewData(newData) + return .success(newData) + } catch { + // ignore cancellation error + if !(error is CancellationError) { + await handleError(error) + } + return .failure(error) + } + } + + /// Handle new data that just received + /// - Parameter newData: the new data received + @MainActor open func handleNewData(_ newData: ItemType?) { + data = newData + error = nil + isLoading = false + } + + /// Handle error when received + /// - Parameter err: the error received + @MainActor open func handleError(_ err: Error) { + error = err + isLoading = false + } + + /// Override data + @MainActor open func overrideData(by newData: ItemType?) { + handleNewData(newData) + } + + // MARK: - Getters + + /// List loading state + @MainActor open var state: LoadingState { + if data == nil, isLoading == false, error == nil { + return .initialized + } + + if isLoading { + return .loading + } + + if error != nil { + return .error + } + + return .loaded + } +} diff --git a/Packages/KeyAppKit/Sources/Repository/Repository/RepositoryView/RepositoryListContentView/RepositoryListContentView.swift b/Packages/KeyAppKit/Sources/Repository/Repository/RepositoryView/RepositoryListContentView/RepositoryListContentView.swift new file mode 100644 index 0000000000..f7c2be2c97 --- /dev/null +++ b/Packages/KeyAppKit/Sources/Repository/Repository/RepositoryView/RepositoryListContentView/RepositoryListContentView.swift @@ -0,0 +1,130 @@ +import Foundation +import SwiftUI + +/// Reusable list view +public struct RepositoryListContentView< + Provider: ListProvider, + Repository: ListRepository, + EmptyLoadingView: View, + EmptyErrorView: View, + EmptyLoadedView: View, + ItemView: View, + LoadMoreView: View +>: View { + // MARK: - Properties + + /// ViewModel that handle data flow + @ObservedObject var repository: Repository + + /// Map result + let map: ([Provider.ItemType]) -> [Provider.ItemType] + + /// View to handle state when list is empty and is loading, for example ProgressView or Skeleton + let emptyLoadingView: () -> EmptyLoadingView + + /// View to handle state when list is empty and error occurred at the first time loading + let emptyErrorView: (Error) -> EmptyErrorView + + /// View to handle state when list is loaded and have no data + let emptyLoadedView: () -> EmptyLoadedView + + /// View of an section of the list + let itemView: (Provider.ItemType?) -> ItemView + + /// View showing at the bottom of the list + let loadMoreView: (ListLoadingState.LoadMoreStatus) -> LoadMoreView + + // MARK: - Initializer + + /// PaginatedListView's initializer + /// - Parameters: + /// - viewModel: ViewModel to handle data flow + /// - presentationStyle: Presenation type of the list + /// - emptyLoadingView: View when list is empty and is loading (ProgressView or Skeleton) + /// - emptyErrorView: View when list is empty and error occured + /// - emptyLoadedView: View when list is loaded and have no data + /// - contentView: Content view of the list + /// - loadMoreView: View showing at the bottom of the list (ex: load more) + public init( + repository: Repository, + map: @escaping (([Provider.ItemType]) -> [Provider.ItemType]) = { $0 }, + @ViewBuilder emptyLoadingView: @escaping () -> EmptyLoadingView, + @ViewBuilder emptyErrorView: @escaping (Error) -> EmptyErrorView = { error in + Text(String(reflecting: error)).foregroundStyle(Color.red) + }, + @ViewBuilder emptyLoadedView: @escaping () -> EmptyLoadedView, + @ViewBuilder itemView: @escaping (Provider.ItemType?) -> ItemView, + @ViewBuilder loadMoreView: @escaping (ListLoadingState.LoadMoreStatus) -> LoadMoreView = { _ in EmptyView() } + ) { + self.repository = repository + self.map = map + self.emptyLoadingView = emptyLoadingView + self.emptyErrorView = emptyErrorView + self.emptyLoadedView = emptyLoadedView + self.itemView = itemView + self.loadMoreView = loadMoreView + } + + // MARK: - View Buidler + + /// Body of the view + public var body: some View { + switch repository.state { + case let .empty(status): + VStack { + Spacer() + + switch status { + case .loading: + emptyLoadingView() + case .loaded: + emptyLoadedView() + case let .error(error): + emptyErrorView(error) + } + + Spacer() + } + case let .nonEmpty(loadMoreStatus): + // List of items + ForEach(map(repository.data)) { + itemView($0) + } + + // should fetch new items + loadMoreView(loadMoreStatus) + } + } +} + +/// Convenience ListView's initializers +public extension RepositoryListContentView where LoadMoreView == EmptyView { + /// PaginatedListView's initializer + /// - Parameters: + /// - viewModel: ViewModel to handle data flow + /// - emptyBooksLoadingView: View when list is empty and is loading (ProgressView or Skeleton) + /// - emptyErrorView: View when list is empty and error occured + /// - emptyLoadedView: View when list is loaded and have no data + /// - itemView: View of an Item on the list + /// - loadMoreView: View showing at the bottom of the list (ex: load more) + init( + repository: Repository, + map: @escaping (([Provider.ItemType]) -> [Provider.ItemType]) = { $0 }, + @ViewBuilder emptyLoadingView: @escaping () -> EmptyLoadingView, + @ViewBuilder emptyErrorView: @escaping (Error) -> EmptyErrorView, + @ViewBuilder emptyLoadedView: @escaping () -> EmptyLoadedView, + @ViewBuilder itemView: @escaping (Provider.ItemType?) -> ItemView + ) { + self.init( + repository: repository, + map: map, + emptyLoadingView: emptyLoadingView, + emptyErrorView: emptyErrorView, + emptyLoadedView: emptyLoadedView, + itemView: itemView, + loadMoreView: { _ in + EmptyView() + } + ) + } +} diff --git a/Packages/KeyAppKit/Sources/Repository/Repository/RepositoryView/RepositoryView.swift b/Packages/KeyAppKit/Sources/Repository/Repository/RepositoryView/RepositoryView.swift new file mode 100644 index 0000000000..33371ba08e --- /dev/null +++ b/Packages/KeyAppKit/Sources/Repository/Repository/RepositoryView/RepositoryView.swift @@ -0,0 +1,62 @@ +import Foundation +import SwiftUI + +/// Reusable view for a repository +public struct RepositoryView< + P: Provider, + R: Repository

, + LoadingView: View, + ErrorView: View, + LoadedView: View +>: View { + // MARK: - Properties + + /// ViewModel that handle data flow + @ObservedObject var repository: R + + /// View to handle state when repository is loading, for example ProgressView or Skeleton + let loadingView: () -> LoadingView + + /// View to handle state when an error occurred + let errorView: (Error) -> ErrorView + + /// View to handle state when repository is loaded + let content: (P.ItemType?) -> LoadedView + + // MARK: - Initializer + + /// RepositoryView's initializer + /// - Parameters: + /// - repository: Repository to handle data flow + /// - loadingView: View to handle state when repository is loading, for example ProgressView or Skeleton + /// - errorView: View to handle state when an error occurred + /// - loadedView: View to handle state when repository is loaded + public init( + repository: R, + @ViewBuilder loadingView: @escaping () -> LoadingView, + @ViewBuilder errorView: @escaping (Error) -> ErrorView = { error in + Text(String(reflecting: error)).foregroundStyle(Color.red) + }, + @ViewBuilder content: @escaping (P.ItemType?) -> LoadedView + ) { + self.repository = repository + self.loadingView = loadingView + self.errorView = errorView + self.content = content + } + + // MARK: - Body + + public var body: some View { + switch repository.state { + case .initialized, .loading: + loadingView() + case .loaded: + content(repository.data) + case .error: + if let error = repository.error { + errorView(error) + } + } + } +} diff --git a/Packages/KeyAppKit/Sources/Repository/Repository/TaskStorage.swift b/Packages/KeyAppKit/Sources/Repository/Repository/TaskStorage.swift new file mode 100644 index 0000000000..29cbeb5ba9 --- /dev/null +++ b/Packages/KeyAppKit/Sources/Repository/Repository/TaskStorage.swift @@ -0,0 +1,14 @@ +import Foundation + +public actor TaskStorage { + public var loadingTask: Task? + + public func save(_ task: Task) { + loadingTask = task + } + + public func cancelCurrentTask() { + loadingTask?.cancel() + loadingTask = nil + } +} diff --git a/Packages/KeyAppKit/Sources/Send/Action/SendAction+SendService.swift b/Packages/KeyAppKit/Sources/Send/Action/SendAction+SendService.swift index 4f43e3cef5..50382edfe4 100644 --- a/Packages/KeyAppKit/Sources/Send/Action/SendAction+SendService.swift +++ b/Packages/KeyAppKit/Sources/Send/Action/SendAction+SendService.swift @@ -1,6 +1,7 @@ import FeeRelayerSwift import Foundation import KeyAppKitCore +import SendService import SolanaSwift extension SendActionServiceImpl { diff --git a/Packages/KeyAppKit/Sources/Send/Action/SendAction.swift b/Packages/KeyAppKit/Sources/Send/Action/SendAction.swift index 8041543c27..47db2511c5 100644 --- a/Packages/KeyAppKit/Sources/Send/Action/SendAction.swift +++ b/Packages/KeyAppKit/Sources/Send/Action/SendAction.swift @@ -1,6 +1,7 @@ import FeeRelayerSwift import Foundation import KeyAppKitCore +import SendService import SolanaSwift public protocol SendActionService { diff --git a/Packages/KeyAppKit/Sources/Send/Input/SendInputBusinessLogic+ChangeToken.swift b/Packages/KeyAppKit/Sources/Send/Input/SendInputBusinessLogic+ChangeToken.swift index ce2c1b8019..ed1aecc78a 100644 --- a/Packages/KeyAppKit/Sources/Send/Input/SendInputBusinessLogic+ChangeToken.swift +++ b/Packages/KeyAppKit/Sources/Send/Input/SendInputBusinessLogic+ChangeToken.swift @@ -2,6 +2,7 @@ import BigDecimal import FeeRelayerSwift import Foundation import KeyAppKitCore +import SendService import SolanaSwift extension SendInputBusinessLogic { diff --git a/Packages/KeyAppKit/Sources/Send/Input/SendInputState.swift b/Packages/KeyAppKit/Sources/Send/Input/SendInputState.swift index af89ec7534..b805e61a4b 100644 --- a/Packages/KeyAppKit/Sources/Send/Input/SendInputState.swift +++ b/Packages/KeyAppKit/Sources/Send/Input/SendInputState.swift @@ -2,6 +2,7 @@ import BigDecimal import FeeRelayerSwift import Foundation import KeyAppKitCore +import SendService import SolanaSwift import TokenService @@ -65,11 +66,11 @@ public struct SendInputState: Equatable { public let walletAccount: BufferInfo? /// Usable when recipient category is ``Recipient.Category.solanaAddress`` - public let splAccounts: [SolanaSwift.TokenAccount] + public let splAccounts: [SolanaSwift.TokenAccount] public init( walletAccount: BufferInfo?, - splAccounts: [SolanaSwift.TokenAccount] + splAccounts: [SolanaSwift.TokenAccount] ) { self.walletAccount = walletAccount self.splAccounts = splAccounts diff --git a/Packages/KeyAppKit/Sources/Send/Input/Services/SendFeeCalculator.swift b/Packages/KeyAppKit/Sources/Send/Input/Services/SendFeeCalculator.swift index 7be8c06cfe..8a9ede1037 100644 --- a/Packages/KeyAppKit/Sources/Send/Input/Services/SendFeeCalculator.swift +++ b/Packages/KeyAppKit/Sources/Send/Input/Services/SendFeeCalculator.swift @@ -1,4 +1,5 @@ import KeyAppKitCore +import SendService import SolanaSwift import TokenService diff --git a/Packages/KeyAppKit/Sources/Send/RecipientSearch/SmartInfo.swift b/Packages/KeyAppKit/Sources/Send/RecipientSearch/SmartInfo.swift index 4a05f09fb4..ae8a6f880e 100644 --- a/Packages/KeyAppKit/Sources/Send/RecipientSearch/SmartInfo.swift +++ b/Packages/KeyAppKit/Sources/Send/RecipientSearch/SmartInfo.swift @@ -3,14 +3,14 @@ import SolanaSwift public enum SolanaAddressInfo { case empty - case splAccount(SPLTokenAccountState) + case splAccount(TokenAccountState) } extension SolanaAddressInfo: BufferLayout { public init(from reader: inout SolanaSwift.BinaryReader) throws { if reader.isEmpty { self = .empty - } else if let accountInfo = try? SPLTokenAccountState(from: &reader) { + } else if let accountInfo = try? TokenAccountState(from: &reader) { self = .splAccount(accountInfo) } else { self = .empty diff --git a/Packages/KeyAppKit/Sources/Send/SendServiceLimitRemaining+Extensions.swift b/Packages/KeyAppKit/Sources/Send/SendServiceLimitRemaining+Extensions.swift index a0fdf36af6..147c1cfefa 100644 --- a/Packages/KeyAppKit/Sources/Send/SendServiceLimitRemaining+Extensions.swift +++ b/Packages/KeyAppKit/Sources/Send/SendServiceLimitRemaining+Extensions.swift @@ -1,4 +1,5 @@ import Foundation +import SendService public extension SendServiceLimitRemaining { func isAvailable(forAmount amount: UInt64) -> Bool { diff --git a/Packages/KeyAppKit/Sources/Send/SendViaLink/SendViaLinkDataService.swift b/Packages/KeyAppKit/Sources/Send/SendViaLink/SendViaLinkDataService.swift index 4d6ca05c48..09999960d8 100644 --- a/Packages/KeyAppKit/Sources/Send/SendViaLink/SendViaLinkDataService.swift +++ b/Packages/KeyAppKit/Sources/Send/SendViaLink/SendViaLinkDataService.swift @@ -209,7 +209,7 @@ public final class SendViaLinkDataServiceImpl: SendViaLinkDataService { // spl token else { let mre = try await solanaAPIClient - .getMinimumBalanceForRentExemption(span: SPLTokenAccountState.BUFFER_LENGTH) + .getMinimumBalanceForRentExemption(span: TokenAccountState.BUFFER_LENGTH) return try await claimSPLToken( keypair: token.keypair, receiver: receiver, diff --git a/Packages/KeyAppKit/Sources/Send/SolanaAPIClient+Token2022.swift b/Packages/KeyAppKit/Sources/Send/SolanaAPIClient+Token2022.swift index f59ea67d34..b4234e2462 100644 --- a/Packages/KeyAppKit/Sources/Send/SolanaAPIClient+Token2022.swift +++ b/Packages/KeyAppKit/Sources/Send/SolanaAPIClient+Token2022.swift @@ -5,8 +5,8 @@ extension SolanaAPIClient { func getTokenAccountsByOwnerWithToken2022( pubkey: String, configs: RequestConfiguration? - ) async throws -> [TokenAccount] - { // Temporarily convert all state into basic SPLTokenAccountState layout + ) async throws -> [TokenAccount] + { // Temporarily convert all state into basic TokenAccountState layout async let classicTokenAccounts = getTokenAccountsByOwner( pubkey: pubkey, params: .init( @@ -14,7 +14,7 @@ extension SolanaAPIClient { programId: TokenProgram.id.base58EncodedString ), configs: configs, - decodingTo: SPLTokenAccountState.self + decodingTo: TokenAccountState.self ) async let token2022Accounts = getTokenAccountsByOwner( @@ -24,7 +24,7 @@ extension SolanaAPIClient { programId: Token2022Program.id.base58EncodedString ), configs: configs, - decodingTo: SPLTokenAccountState.self + decodingTo: TokenAccountState.self ) return try await classicTokenAccounts + token2022Accounts diff --git a/Packages/KeyAppKit/Sources/Send/JSONRPC/Models/SendServiceLimitResponse.swift b/Packages/KeyAppKit/Sources/SendService/Models/SendServiceLimitResponse.swift similarity index 60% rename from Packages/KeyAppKit/Sources/Send/JSONRPC/Models/SendServiceLimitResponse.swift rename to Packages/KeyAppKit/Sources/SendService/Models/SendServiceLimitResponse.swift index 5771832f33..693a59d98f 100644 --- a/Packages/KeyAppKit/Sources/Send/JSONRPC/Models/SendServiceLimitResponse.swift +++ b/Packages/KeyAppKit/Sources/SendService/Models/SendServiceLimitResponse.swift @@ -5,6 +5,14 @@ import Foundation public struct SendServiceLimitResponse: Codable, Equatable { public let networkFee, tokenAccountRent: SendServiceLimitRemaining + public init( + networkFee: SendServiceLimitRemaining, + tokenAccountRent: SendServiceLimitRemaining + ) { + self.networkFee = networkFee + self.tokenAccountRent = tokenAccountRent + } + enum CodingKeys: String, CodingKey { case networkFee = "network_fee" case tokenAccountRent = "token_account_rent" @@ -16,6 +24,14 @@ public struct SendServiceLimitResponse: Codable, Equatable { public struct SendServiceLimitRemaining: Codable, Equatable { public let remainingAmount, remainingTransactions: UInt64 + public init( + remainingAmount: UInt64, + remainingTransactions: UInt64 + ) { + self.remainingAmount = remainingAmount + self.remainingTransactions = remainingTransactions + } + enum CodingKeys: String, CodingKey { case remainingAmount = "remaining_amount" case remainingTransactions = "remaining_transactions" diff --git a/Packages/KeyAppKit/Sources/Send/JSONRPC/Models/SendServiceTransferRequest.swift b/Packages/KeyAppKit/Sources/SendService/Models/SendServiceTransferRequest.swift similarity index 100% rename from Packages/KeyAppKit/Sources/Send/JSONRPC/Models/SendServiceTransferRequest.swift rename to Packages/KeyAppKit/Sources/SendService/Models/SendServiceTransferRequest.swift diff --git a/Packages/KeyAppKit/Sources/Send/JSONRPC/Models/SendServiceTransferResponse.swift b/Packages/KeyAppKit/Sources/SendService/Models/SendServiceTransferResponse.swift similarity index 100% rename from Packages/KeyAppKit/Sources/Send/JSONRPC/Models/SendServiceTransferResponse.swift rename to Packages/KeyAppKit/Sources/SendService/Models/SendServiceTransferResponse.swift diff --git a/Packages/KeyAppKit/Sources/Send/JSONRPC/SendRPCService.swift b/Packages/KeyAppKit/Sources/SendService/SendRPCService.swift similarity index 92% rename from Packages/KeyAppKit/Sources/Send/JSONRPC/SendRPCService.swift rename to Packages/KeyAppKit/Sources/SendService/SendRPCService.swift index 30d2232749..16e12b96b2 100644 --- a/Packages/KeyAppKit/Sources/Send/JSONRPC/SendRPCService.swift +++ b/Packages/KeyAppKit/Sources/SendService/SendRPCService.swift @@ -1,6 +1,5 @@ import Foundation import KeyAppNetworking -import SolanaSwift public class SendRPCService { let host: String @@ -22,8 +21,9 @@ public class SendRPCService { ) ) - if !result.contains(PublicKey.wrappedSOLMint.base58EncodedString) { - result.append(PublicKey.wrappedSOLMint.base58EncodedString) + let solMint = "So11111111111111111111111111111111111111112" + if !result.contains(solMint) { + result.append(solMint) } return result diff --git a/Packages/KeyAppKit/Tests/FeeRelayerSwiftUnitTests/Helpers/MockSolanaAPIClientBase.swift b/Packages/KeyAppKit/Tests/FeeRelayerSwiftUnitTests/Helpers/MockSolanaAPIClientBase.swift index 64619cca21..48a79dfe2c 100644 --- a/Packages/KeyAppKit/Tests/FeeRelayerSwiftUnitTests/Helpers/MockSolanaAPIClientBase.swift +++ b/Packages/KeyAppKit/Tests/FeeRelayerSwiftUnitTests/Helpers/MockSolanaAPIClientBase.swift @@ -88,7 +88,7 @@ class MockSolanaAPIClientBase: SolanaAPIClient { mint _: String?, programId _: String?, configs _: SolanaSwift.RequestConfiguration? - ) async throws -> [SolanaSwift.TokenAccount] { + ) async throws -> [SolanaSwift.TokenAccount] { fatalError() } @@ -96,7 +96,7 @@ class MockSolanaAPIClientBase: SolanaAPIClient { pubkey _: String, params _: SolanaSwift.OwnerInfoParams?, configs _: SolanaSwift.RequestConfiguration? - ) async throws -> [SolanaSwift.TokenAccount] { + ) async throws -> [SolanaSwift.TokenAccount] { fatalError() } diff --git a/Packages/KeyAppKit/Tests/FeeRelayerSwiftUnitTests/Relay/Helpers/DestinationAnalysatorTests.swift b/Packages/KeyAppKit/Tests/FeeRelayerSwiftUnitTests/Relay/Helpers/DestinationAnalysatorTests.swift index 579eb1ee85..feb3d1c5f8 100644 --- a/Packages/KeyAppKit/Tests/FeeRelayerSwiftUnitTests/Relay/Helpers/DestinationAnalysatorTests.swift +++ b/Packages/KeyAppKit/Tests/FeeRelayerSwiftUnitTests/Relay/Helpers/DestinationAnalysatorTests.swift @@ -59,7 +59,7 @@ private class MockSolanaAPIClient: MockSolanaAPIClientBase { override func getAccountInfo(account: String) async throws -> BufferInfo? where T: BufferLayout { switch account { case usdcAssociatedAddress.base58EncodedString: - let info = BufferInfo( + let info = BufferInfo( lamports: 0, owner: testCase > 1 ? SystemProgram.id.base58EncodedString : TokenProgram.id.base58EncodedString, data: .init( diff --git a/Packages/KeyAppKit/Tests/FeeRelayerSwiftUnitTests/Relay/Helpers/TransitTokenAccountManagerTests.swift b/Packages/KeyAppKit/Tests/FeeRelayerSwiftUnitTests/Relay/Helpers/TransitTokenAccountManagerTests.swift index 9b7ddf363d..4a454a5210 100644 --- a/Packages/KeyAppKit/Tests/FeeRelayerSwiftUnitTests/Relay/Helpers/TransitTokenAccountManagerTests.swift +++ b/Packages/KeyAppKit/Tests/FeeRelayerSwiftUnitTests/Relay/Helpers/TransitTokenAccountManagerTests.swift @@ -130,7 +130,7 @@ private class MockSolanaAPIClient: MockSolanaAPIClientBase { override func getAccountInfo(account: String) async throws -> BufferInfo? where T: BufferLayout { switch account { case "JhhACrqV4LhpZY7ogW9Gy2MRLVanXXFxyiW548dsjBp" where testCase == 2: - let info = BufferInfo( + let info = BufferInfo( lamports: 0, owner: TokenProgram.id.base58EncodedString, data: .init( diff --git a/Packages/KeyAppKit/Tests/FeeRelayerSwiftUnitTests/Relay/RelaySwap/TransactionBuilder/Checking/CheckDestinationTests.swift b/Packages/KeyAppKit/Tests/FeeRelayerSwiftUnitTests/Relay/RelaySwap/TransactionBuilder/Checking/CheckDestinationTests.swift index c8a40957a6..5b34b5ea87 100644 --- a/Packages/KeyAppKit/Tests/FeeRelayerSwiftUnitTests/Relay/RelaySwap/TransactionBuilder/Checking/CheckDestinationTests.swift +++ b/Packages/KeyAppKit/Tests/FeeRelayerSwiftUnitTests/Relay/RelaySwap/TransactionBuilder/Checking/CheckDestinationTests.swift @@ -113,7 +113,7 @@ final class CheckDestinationTests: XCTestCase { from: .feePayerAddress, toNewPubkey: env.destinationNewAccount!.publicKey, lamports: minimumTokenAccountBalance, - space: SPLTokenAccountState.BUFFER_LENGTH, + space: TokenAccountState.BUFFER_LENGTH, programId: TokenProgram.id ), TokenProgram.initializeAccountInstruction( diff --git a/Packages/KeyAppKit/Tests/FeeRelayerSwiftUnitTests/Relay/RelaySwap/TransactionBuilder/Checking/CheckSourceTests.swift b/Packages/KeyAppKit/Tests/FeeRelayerSwiftUnitTests/Relay/RelaySwap/TransactionBuilder/Checking/CheckSourceTests.swift index e9716e03c7..bc5ae7dc33 100644 --- a/Packages/KeyAppKit/Tests/FeeRelayerSwiftUnitTests/Relay/RelaySwap/TransactionBuilder/Checking/CheckSourceTests.swift +++ b/Packages/KeyAppKit/Tests/FeeRelayerSwiftUnitTests/Relay/RelaySwap/TransactionBuilder/Checking/CheckSourceTests.swift @@ -73,7 +73,7 @@ final class CheckSourceTests: XCTestCase { from: .feePayerAddress, toNewPubkey: env.sourceWSOLNewAccount!.publicKey, lamports: minimumTokenAccountBalance + inputAmount, - space: SPLTokenAccountState.BUFFER_LENGTH, + space: TokenAccountState.BUFFER_LENGTH, programId: TokenProgram.id ), TokenProgram.initializeAccountInstruction( diff --git a/Packages/KeyAppKit/Tests/FeeRelayerSwiftUnitTests/Relay/Service/TopUpTransactionBuilder/TopUpTransactionBuilderWithTransitiveSwapWithFreeTransactionsTests.swift b/Packages/KeyAppKit/Tests/FeeRelayerSwiftUnitTests/Relay/Service/TopUpTransactionBuilder/TopUpTransactionBuilderWithTransitiveSwapWithFreeTransactionsTests.swift index 3c03765dbf..28658b0c57 100644 --- a/Packages/KeyAppKit/Tests/FeeRelayerSwiftUnitTests/Relay/Service/TopUpTransactionBuilder/TopUpTransactionBuilderWithTransitiveSwapWithFreeTransactionsTests.swift +++ b/Packages/KeyAppKit/Tests/FeeRelayerSwiftUnitTests/Relay/Service/TopUpTransactionBuilder/TopUpTransactionBuilderWithTransitiveSwapWithFreeTransactionsTests.swift @@ -565,7 +565,7 @@ private class MockSolanaAPIClient: MockSolanaAPIClientBase { case 0, 2: return nil case 1, 3: - let info = BufferInfo( + let info = BufferInfo( lamports: 0, owner: TokenProgram.id.base58EncodedString, data: .init( diff --git a/Packages/KeyAppKit/Tests/FeeRelayerSwiftUnitTests/Relay/Service/TopUpTransactionBuilder/TopUpTransactionBuilderWithTransitiveSwapWithOutFreeTransactionsTests.swift b/Packages/KeyAppKit/Tests/FeeRelayerSwiftUnitTests/Relay/Service/TopUpTransactionBuilder/TopUpTransactionBuilderWithTransitiveSwapWithOutFreeTransactionsTests.swift index 1543493ca9..d24bd0c59a 100644 --- a/Packages/KeyAppKit/Tests/FeeRelayerSwiftUnitTests/Relay/Service/TopUpTransactionBuilder/TopUpTransactionBuilderWithTransitiveSwapWithOutFreeTransactionsTests.swift +++ b/Packages/KeyAppKit/Tests/FeeRelayerSwiftUnitTests/Relay/Service/TopUpTransactionBuilder/TopUpTransactionBuilderWithTransitiveSwapWithOutFreeTransactionsTests.swift @@ -565,7 +565,7 @@ private class MockSolanaAPIClient: MockSolanaAPIClientBase { case 0, 2: return nil case 1, 3: - let info = BufferInfo( + let info = BufferInfo( lamports: 0, owner: TokenProgram.id.base58EncodedString, data: .init( diff --git a/Packages/KeyAppKit/Tests/UnitTests/KeyAppBusinessTests/Helper/MockSolanaAPIClientBase.swift b/Packages/KeyAppKit/Tests/UnitTests/KeyAppBusinessTests/Helper/MockSolanaAPIClientBase.swift index 9c466df713..b1121d3426 100644 --- a/Packages/KeyAppKit/Tests/UnitTests/KeyAppBusinessTests/Helper/MockSolanaAPIClientBase.swift +++ b/Packages/KeyAppKit/Tests/UnitTests/KeyAppBusinessTests/Helper/MockSolanaAPIClientBase.swift @@ -102,7 +102,7 @@ class MockSolanaAPIClientBase: SolanaAPIClient { mint _: String?, programId _: String?, configs _: SolanaSwift.RequestConfiguration? - ) async throws -> [SolanaSwift.TokenAccount] { + ) async throws -> [SolanaSwift.TokenAccount] { fatalError() } @@ -110,7 +110,7 @@ class MockSolanaAPIClientBase: SolanaAPIClient { pubkey _: String, params _: SolanaSwift.OwnerInfoParams?, configs _: SolanaSwift.RequestConfiguration? - ) async throws -> [SolanaSwift.TokenAccount] { + ) async throws -> [SolanaSwift.TokenAccount] { fatalError() } diff --git a/Packages/KeyAppKit/Tests/UnitTests/KeyAppBusinessTests/Solana/SolanaAccountsServiceTests.swift b/Packages/KeyAppKit/Tests/UnitTests/KeyAppBusinessTests/Solana/SolanaAccountsServiceTests.swift index 7d0ef0ca57..4ba497c390 100644 --- a/Packages/KeyAppKit/Tests/UnitTests/KeyAppBusinessTests/Solana/SolanaAccountsServiceTests.swift +++ b/Packages/KeyAppKit/Tests/UnitTests/KeyAppBusinessTests/Solana/SolanaAccountsServiceTests.swift @@ -101,7 +101,7 @@ private class MockSolanaAPIClient: MockSolanaAPIClientBase { pubkey _: String, params _: OwnerInfoParams?, configs _: RequestConfiguration? - ) async throws -> [TokenAccount] { + ) async throws -> [TokenAccount] { [] } diff --git a/Packages/KeyAppKit/Tests/UnitTests/SendTests/Recipient/RecipientSearchServiceImplTest.swift b/Packages/KeyAppKit/Tests/UnitTests/SendTests/Recipient/RecipientSearchServiceImplTest.swift index c3488b88d3..05567f28ba 100644 --- a/Packages/KeyAppKit/Tests/UnitTests/SendTests/Recipient/RecipientSearchServiceImplTest.swift +++ b/Packages/KeyAppKit/Tests/UnitTests/SendTests/Recipient/RecipientSearchServiceImplTest.swift @@ -200,7 +200,7 @@ class RecipientSearchServiceImplTest: XCTestCase { account: .init( lamports: 0, owner: TokenProgram.id.base58EncodedString, - data: SPLTokenAccountState( + data: TokenAccountState( mint: .usdcMint, owner: PublicKey(string: "9sdwzJWooFrjNGVX6GkkWUG9GyeBnhgJYqh27AsPqwbM"), lamports: 50000, diff --git a/p2p_wallet.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/p2p_wallet.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c84e1b1f95..0fe2c1e0af 100644 --- a/p2p_wallet.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/p2p_wallet.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -311,8 +311,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/p2p-org/solana-swift", "state" : { - "branch" : "feature/token-2022", - "revision" : "e07298936cd1561a81ee84dad2ae2341c9112d41" + "branch" : "main", + "revision" : "f9181079014c474c0e823a2f3f615ba953b41f1e" } }, { diff --git a/p2p_wallet/Injection/Resolver+registerAllServices.swift b/p2p_wallet/Injection/Resolver+registerAllServices.swift index c628ff748e..c3e78d2dd6 100644 --- a/p2p_wallet/Injection/Resolver+registerAllServices.swift +++ b/p2p_wallet/Injection/Resolver+registerAllServices.swift @@ -14,6 +14,7 @@ import Reachability import Resolver import Sell import Send +import SendService import SolanaSwift import SwiftyUserDefaults import TokenService diff --git a/p2p_wallet/Scenes/Main/Swap/State/BusinessLogic/Actions/JupiterSwapBusinessLogic+Initialize.swift b/p2p_wallet/Scenes/Main/Swap/State/BusinessLogic/Actions/JupiterSwapBusinessLogic+Initialize.swift index 0febcb4f74..19c86e892a 100644 --- a/p2p_wallet/Scenes/Main/Swap/State/BusinessLogic/Actions/JupiterSwapBusinessLogic+Initialize.swift +++ b/p2p_wallet/Scenes/Main/Swap/State/BusinessLogic/Actions/JupiterSwapBusinessLogic+Initialize.swift @@ -20,7 +20,7 @@ extension JupiterSwapBusinessLogic { async let pricesMap = getTokensPriceMap() async let lamportPerSignature = getLamportPerSignature(solanaAPIClient: services.solanaAPIClient) async let splAccountCreationFee = try? services.solanaAPIClient.getMinimumBalanceForRentExemption( - dataLength: SPLTokenAccountState.BUFFER_LENGTH, + dataLength: TokenAccountState.BUFFER_LENGTH, commitment: nil ) diff --git a/p2p_wallet/Scenes/Main/Swap/State/BusinessLogic/Helpers/JupiterSwapBusinessLogic+Calculate.swift b/p2p_wallet/Scenes/Main/Swap/State/BusinessLogic/Helpers/JupiterSwapBusinessLogic+Calculate.swift index cecee59a04..43f035f992 100644 --- a/p2p_wallet/Scenes/Main/Swap/State/BusinessLogic/Helpers/JupiterSwapBusinessLogic+Calculate.swift +++ b/p2p_wallet/Scenes/Main/Swap/State/BusinessLogic/Helpers/JupiterSwapBusinessLogic+Calculate.swift @@ -64,7 +64,7 @@ extension JupiterSwapBusinessLogic { var splAccountCreationFee: Lamports = state.splAccountCreationFee if splAccountCreationFee == 0 { splAccountCreationFee = try await services.solanaAPIClient.getMinimumBalanceForRentExemption( - dataLength: SPLTokenAccountState.BUFFER_LENGTH, + dataLength: TokenAccountState.BUFFER_LENGTH, commitment: nil ) }