mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
1344 lines
56 KiB
Swift
1344 lines
56 KiB
Swift
import Foundation
|
|
#if os(macOS)
|
|
import SwiftSignalKitMac
|
|
import MtProtoKitMac
|
|
#else
|
|
import SwiftSignalKit
|
|
import TonBinding
|
|
#endif
|
|
|
|
public struct TonKeychainEncryptedData: Codable, Equatable {
|
|
public let publicKey: Data
|
|
public let data: Data
|
|
|
|
public init(publicKey: Data, data: Data) {
|
|
self.publicKey = publicKey
|
|
self.data = data
|
|
}
|
|
}
|
|
|
|
public enum TonKeychainEncryptDataError {
|
|
case generic
|
|
}
|
|
|
|
public enum TonKeychainDecryptDataError {
|
|
case generic
|
|
case publicKeyMismatch
|
|
case cancelled
|
|
}
|
|
|
|
public struct TonKeychain {
|
|
public let encryptionPublicKey: () -> Signal<Data?, NoError>
|
|
public let encrypt: (Data) -> Signal<TonKeychainEncryptedData, TonKeychainEncryptDataError>
|
|
public let decrypt: (TonKeychainEncryptedData) -> Signal<Data, TonKeychainDecryptDataError>
|
|
|
|
public init(encryptionPublicKey: @escaping () -> Signal<Data?, NoError>, encrypt: @escaping (Data) -> Signal<TonKeychainEncryptedData, TonKeychainEncryptDataError>, decrypt: @escaping (TonKeychainEncryptedData) -> Signal<Data, TonKeychainDecryptDataError>) {
|
|
self.encryptionPublicKey = encryptionPublicKey
|
|
self.encrypt = encrypt
|
|
self.decrypt = decrypt
|
|
}
|
|
}
|
|
|
|
public enum TonNetworkProxyResult {
|
|
case reponse(Data)
|
|
case error(String)
|
|
}
|
|
|
|
public protocol TonNetworkProxy: class {
|
|
func request(data: Data, timeout: Double, completion: @escaping (TonNetworkProxyResult) -> Void) -> Disposable
|
|
}
|
|
|
|
private final class TonInstanceImpl {
|
|
private let queue: Queue
|
|
private let basePath: String
|
|
fileprivate var config: String
|
|
fileprivate var blockchainName: String
|
|
private let proxy: TonNetworkProxy?
|
|
private var instance: TON?
|
|
fileprivate let syncStateProgress = ValuePromise<Float>(0.0)
|
|
|
|
init(queue: Queue, basePath: String, config: String, blockchainName: String, proxy: TonNetworkProxy?) {
|
|
self.queue = queue
|
|
self.basePath = basePath
|
|
self.config = config
|
|
self.blockchainName = blockchainName
|
|
self.proxy = proxy
|
|
}
|
|
|
|
func withInstance(_ f: (TON) -> Void) {
|
|
let instance: TON
|
|
if let current = self.instance {
|
|
instance = current
|
|
} else {
|
|
let proxy = self.proxy
|
|
let syncStateProgress = self.syncStateProgress
|
|
instance = TON(keystoreDirectory: self.basePath + "/ton-keystore", config: self.config, blockchainName: self.blockchainName, performExternalRequest: { request in
|
|
if let proxy = proxy {
|
|
let _ = proxy.request(data: request.data, timeout: 20.0, completion: { result in
|
|
switch result {
|
|
case let .reponse(data):
|
|
request.onResult(data, nil)
|
|
case let .error(description):
|
|
request.onResult(nil, description)
|
|
}
|
|
})
|
|
} else {
|
|
request.onResult(nil, "NETWORK_DISABLED")
|
|
}
|
|
}, enableExternalRequests: proxy != nil, syncStateUpdated: { progress in
|
|
syncStateProgress.set(progress)
|
|
})
|
|
self.instance = instance
|
|
}
|
|
f(instance)
|
|
}
|
|
}
|
|
|
|
public final class TonInstance {
|
|
private let queue: Queue
|
|
private let impl: QueueLocalObject<TonInstanceImpl>
|
|
|
|
public var syncProgress: Signal<Float, NoError> {
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
self.impl.with { impl in
|
|
disposable.set(impl.syncStateProgress.get().start(next: { value in
|
|
subscriber.putNext(value)
|
|
}))
|
|
}
|
|
return disposable
|
|
}
|
|
}
|
|
|
|
public init(basePath: String, config: String, blockchainName: String, proxy: TonNetworkProxy?) {
|
|
self.queue = .mainQueue()
|
|
let queue = self.queue
|
|
self.impl = QueueLocalObject(queue: queue, generate: {
|
|
return TonInstanceImpl(queue: queue, basePath: basePath, config: config, blockchainName: blockchainName, proxy: proxy)
|
|
})
|
|
}
|
|
|
|
public func updateConfig(config: String, blockchainName: String) -> Signal<Never, NoError> {
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
self.impl.with { impl in
|
|
impl.config = config
|
|
impl.blockchainName = blockchainName
|
|
impl.withInstance { ton in
|
|
let cancel = ton.updateConfig(config, blockchainName: blockchainName).start(next: nil, error: { _ in
|
|
}, completed: {
|
|
subscriber.putCompletion()
|
|
})
|
|
disposable.set(ActionDisposable {
|
|
cancel?.dispose()
|
|
})
|
|
}
|
|
}
|
|
return disposable
|
|
}
|
|
}
|
|
|
|
public func validateConfig(config: String, blockchainName: String) -> Signal<WalletValidateConfigResult, WalletValidateConfigError> {
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
self.impl.with { impl in
|
|
impl.withInstance { ton in
|
|
let cancel = ton.validateConfig(config, blockchainName: blockchainName).start(next: { result in
|
|
guard let result = result as? TONValidatedConfig else {
|
|
subscriber.putError(.generic)
|
|
return
|
|
}
|
|
subscriber.putNext(WalletValidateConfigResult(defaultWalletId: result.defaultWalletId))
|
|
subscriber.putCompletion()
|
|
}, error: { error in
|
|
guard let _ = error as? TONError else {
|
|
subscriber.putError(.generic)
|
|
return
|
|
}
|
|
subscriber.putError(.generic)
|
|
}, completed: nil)
|
|
disposable.set(ActionDisposable {
|
|
cancel?.dispose()
|
|
})
|
|
}
|
|
}
|
|
return disposable
|
|
}
|
|
}
|
|
|
|
fileprivate func exportKey(key: TONKey, localPassword: Data) -> Signal<[String], NoError> {
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
|
|
self.impl.with { impl in
|
|
impl.withInstance { ton in
|
|
let cancel = ton.export(key, localPassword: localPassword).start(next: { wordList in
|
|
guard let wordList = wordList as? [String] else {
|
|
assertionFailure()
|
|
return
|
|
}
|
|
subscriber.putNext(wordList)
|
|
subscriber.putCompletion()
|
|
})
|
|
disposable.set(ActionDisposable {
|
|
cancel?.dispose()
|
|
})
|
|
}
|
|
}
|
|
|
|
return disposable
|
|
}
|
|
}
|
|
|
|
fileprivate func createWallet(keychain: TonKeychain, localPassword: Data) -> Signal<(WalletInfo, [String]), CreateWalletError> {
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
self.impl.with { impl in
|
|
impl.withInstance { ton in
|
|
let cancel = ton.createKey(withLocalPassword: localPassword, mnemonicPassword: Data()).start(next: { key in
|
|
guard let key = key as? TONKey else {
|
|
assertionFailure()
|
|
return
|
|
}
|
|
let cancel = keychain.encrypt(key.secret).start(next: { encryptedSecretData in
|
|
let _ = self.exportKey(key: key, localPassword: localPassword).start(next: { wordList in
|
|
subscriber.putNext((WalletInfo(publicKey: WalletPublicKey(rawValue: key.publicKey), encryptedSecret: encryptedSecretData), wordList))
|
|
subscriber.putCompletion()
|
|
}, error: { error in
|
|
subscriber.putError(.generic)
|
|
})
|
|
}, error: { _ in
|
|
subscriber.putError(.generic)
|
|
}, completed: {
|
|
})
|
|
}, error: { _ in
|
|
}, completed: {
|
|
})
|
|
disposable.set(ActionDisposable {
|
|
cancel?.dispose()
|
|
})
|
|
}
|
|
}
|
|
|
|
return disposable
|
|
}
|
|
}
|
|
|
|
fileprivate func importWallet(keychain: TonKeychain, wordList: [String], localPassword: Data) -> Signal<WalletInfo, ImportWalletInternalError> {
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
|
|
self.impl.with { impl in
|
|
impl.withInstance { ton in
|
|
let cancel = ton.importKey(withLocalPassword: localPassword, mnemonicPassword: Data(), wordList: wordList).start(next: { key in
|
|
guard let key = key as? TONKey else {
|
|
subscriber.putError(.generic)
|
|
return
|
|
}
|
|
let cancel = keychain.encrypt(key.secret).start(next: { encryptedSecretData in
|
|
subscriber.putNext(WalletInfo(publicKey: WalletPublicKey(rawValue: key.publicKey), encryptedSecret: encryptedSecretData))
|
|
subscriber.putCompletion()
|
|
}, error: { _ in
|
|
subscriber.putError(.generic)
|
|
}, completed: {
|
|
})
|
|
}, error: { _ in
|
|
subscriber.putError(.generic)
|
|
}, completed: {
|
|
})
|
|
disposable.set(ActionDisposable {
|
|
cancel?.dispose()
|
|
})
|
|
}
|
|
}
|
|
|
|
return disposable
|
|
}
|
|
}
|
|
|
|
fileprivate func getInitialWalletId() -> Signal<Int64, WalletValidateConfigError> {
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
self.impl.with { impl in
|
|
let config = impl.config
|
|
let blockchainName = impl.blockchainName
|
|
|
|
impl.withInstance { ton in
|
|
let cancel = ton.validateConfig(config, blockchainName: blockchainName).start(next: { result in
|
|
guard let result = result as? TONValidatedConfig else {
|
|
subscriber.putError(.generic)
|
|
return
|
|
}
|
|
subscriber.putNext(result.defaultWalletId)
|
|
subscriber.putCompletion()
|
|
}, error: { error in
|
|
guard let _ = error as? TONError else {
|
|
subscriber.putError(.generic)
|
|
return
|
|
}
|
|
subscriber.putError(.generic)
|
|
}, completed: nil)
|
|
disposable.set(ActionDisposable {
|
|
cancel?.dispose()
|
|
})
|
|
}
|
|
}
|
|
return disposable
|
|
}
|
|
}
|
|
|
|
fileprivate func walletAddress(publicKey: WalletPublicKey) -> Signal<String, NoError> {
|
|
return self.getInitialWalletId()
|
|
|> `catch` { _ -> Signal<Int64, NoError> in
|
|
return .single(0)
|
|
}
|
|
|> mapToSignal { initialWalletId -> Signal<String, NoError> in
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
|
|
self.impl.with { impl in
|
|
impl.withInstance { ton in
|
|
let cancel = ton.getWalletAccountAddress(withPublicKey: publicKey.rawValue, initialWalletId: initialWalletId).start(next: { address in
|
|
guard let address = address as? String else {
|
|
return
|
|
}
|
|
subscriber.putNext(address)
|
|
subscriber.putCompletion()
|
|
}, error: { _ in
|
|
subscriber.putNext("ERROR")
|
|
subscriber.putCompletion()
|
|
}, completed: {
|
|
})
|
|
disposable.set(ActionDisposable {
|
|
cancel?.dispose()
|
|
})
|
|
}
|
|
}
|
|
|
|
return disposable
|
|
}
|
|
}
|
|
}
|
|
|
|
private func getWalletStateRaw(address: String) -> Signal<TONAccountState, GetWalletStateError> {
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
|
|
self.impl.with { impl in
|
|
impl.withInstance { ton in
|
|
let cancel = ton.getAccountState(withAddress: address).start(next: { state in
|
|
guard let state = state as? TONAccountState else {
|
|
return
|
|
}
|
|
subscriber.putNext(state)
|
|
}, error: { error in
|
|
if let error = error as? TONError {
|
|
if error.text.hasPrefix("LITE_SERVER_") {
|
|
subscriber.putError(.network)
|
|
} else {
|
|
subscriber.putError(.generic)
|
|
}
|
|
} else {
|
|
subscriber.putError(.generic)
|
|
}
|
|
}, completed: {
|
|
subscriber.putCompletion()
|
|
})
|
|
disposable.set(ActionDisposable {
|
|
cancel?.dispose()
|
|
})
|
|
}
|
|
}
|
|
|
|
return disposable
|
|
}
|
|
}
|
|
|
|
fileprivate func getWalletState(address: String) -> Signal<(WalletState, Int64), GetWalletStateError> {
|
|
return self.getWalletStateRaw(address: address)
|
|
|> map { state in
|
|
return (WalletState(balance: state.balance, lastTransactionId: state.lastTransactionId.flatMap(WalletTransactionId.init(tonTransactionId:))), state.syncUtime)
|
|
}
|
|
}
|
|
|
|
fileprivate func walletLastTransactionId(address: String) -> Signal<WalletTransactionId?, WalletLastTransactionIdError> {
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
|
|
self.impl.with { impl in
|
|
impl.withInstance { ton in
|
|
let cancel = ton.getAccountState(withAddress: address).start(next: { state in
|
|
guard let state = state as? TONAccountState else {
|
|
subscriber.putNext(nil)
|
|
return
|
|
}
|
|
subscriber.putNext(state.lastTransactionId.flatMap(WalletTransactionId.init(tonTransactionId:)))
|
|
}, error: { error in
|
|
if let error = error as? TONError {
|
|
if error.text.hasPrefix("ДITE_SERVER_") {
|
|
subscriber.putError(.network)
|
|
} else {
|
|
subscriber.putError(.generic)
|
|
}
|
|
} else {
|
|
subscriber.putError(.generic)
|
|
}
|
|
}, completed: {
|
|
subscriber.putCompletion()
|
|
})
|
|
disposable.set(ActionDisposable {
|
|
cancel?.dispose()
|
|
})
|
|
}
|
|
}
|
|
|
|
return disposable
|
|
}
|
|
}
|
|
|
|
fileprivate func getWalletTransactions(address: String, previousId: WalletTransactionId) -> Signal<[WalletTransaction], GetWalletTransactionsError> {
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
|
|
self.impl.with { impl in
|
|
impl.withInstance { ton in
|
|
let cancel = ton.getTransactionList(withAddress: address, lt: previousId.lt, hash: previousId.transactionHash).start(next: { transactions in
|
|
guard let transactions = transactions as? [TONTransaction] else {
|
|
subscriber.putError(.generic)
|
|
return
|
|
}
|
|
subscriber.putNext(transactions.map(WalletTransaction.init(tonTransaction:)))
|
|
}, error: { error in
|
|
if let error = error as? TONError {
|
|
if error.text.hasPrefix("LITE_SERVER_") {
|
|
subscriber.putError(.network)
|
|
} else {
|
|
subscriber.putError(.generic)
|
|
}
|
|
} else {
|
|
subscriber.putError(.generic)
|
|
}
|
|
}, completed: {
|
|
subscriber.putCompletion()
|
|
})
|
|
disposable.set(ActionDisposable {
|
|
cancel?.dispose()
|
|
})
|
|
}
|
|
}
|
|
|
|
return disposable
|
|
}
|
|
}
|
|
|
|
fileprivate func prepareSendGramsFromWalletQuery(decryptedSecret: Data, localPassword: Data, walletInfo: WalletInfo, fromAddress: String, toAddress: String, amount: Int64, textMessage: Data, forceIfDestinationNotInitialized: Bool, timeout: Int32, randomId: Int64) -> Signal<TONPreparedSendGramsQuery, SendGramsFromWalletError> {
|
|
let key = TONKey(publicKey: walletInfo.publicKey.rawValue, secret: decryptedSecret)
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
|
|
self.impl.with { impl in
|
|
impl.withInstance { ton in
|
|
let cancel = ton.generateSendGramsQuery(from: key, localPassword: localPassword, fromAddress: fromAddress, toAddress: toAddress, amount: amount, textMessage: textMessage, forceIfDestinationNotInitialized: forceIfDestinationNotInitialized, timeout: timeout, randomId: randomId).start(next: { result in
|
|
guard let result = result as? TONPreparedSendGramsQuery else {
|
|
subscriber.putError(.generic)
|
|
return
|
|
}
|
|
subscriber.putNext(result)
|
|
subscriber.putCompletion()
|
|
}, error: { error in
|
|
if let error = error as? TONError {
|
|
if error.text.hasPrefix("INVALID_ACCOUNT_ADDRESS") {
|
|
subscriber.putError(.invalidAddress)
|
|
} else if error.text.hasPrefix("DANGEROUS_TRANSACTION") {
|
|
subscriber.putError(.destinationIsNotInitialized)
|
|
} else if error.text.hasPrefix("MESSAGE_TOO_LONG") {
|
|
subscriber.putError(.messageTooLong)
|
|
} else if error.text.hasPrefix("NOT_ENOUGH_FUNDS") {
|
|
subscriber.putError(.notEnoughFunds)
|
|
} else if error.text.hasPrefix("LITE_SERVER_") {
|
|
subscriber.putError(.network)
|
|
} else {
|
|
subscriber.putError(.generic)
|
|
}
|
|
} else {
|
|
subscriber.putError(.generic)
|
|
}
|
|
}, completed: {
|
|
subscriber.putCompletion()
|
|
})
|
|
disposable.set(ActionDisposable {
|
|
cancel?.dispose()
|
|
})
|
|
}
|
|
}
|
|
|
|
return disposable
|
|
}
|
|
}
|
|
|
|
fileprivate func prepareFakeSendGramsFromWalletQuery(walletInfo: WalletInfo, fromAddress: String, toAddress: String, amount: Int64, textMessage: Data, timeout: Int32) -> Signal<TONPreparedSendGramsQuery, SendGramsFromWalletError> {
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
|
|
self.impl.with { impl in
|
|
impl.withInstance { ton in
|
|
let cancel = ton.generateFakeSendGramsQuery(fromAddress: fromAddress, toAddress: toAddress, amount: amount, textMessage: textMessage, forceIfDestinationNotInitialized: true, timeout: timeout).start(next: { result in
|
|
guard let result = result as? TONPreparedSendGramsQuery else {
|
|
subscriber.putError(.generic)
|
|
return
|
|
}
|
|
subscriber.putNext(result)
|
|
subscriber.putCompletion()
|
|
}, error: { error in
|
|
if let error = error as? TONError {
|
|
if error.text.hasPrefix("INVALID_ACCOUNT_ADDRESS") {
|
|
subscriber.putError(.invalidAddress)
|
|
} else if error.text.hasPrefix("DANGEROUS_TRANSACTION") {
|
|
subscriber.putError(.destinationIsNotInitialized)
|
|
} else if error.text.hasPrefix("MESSAGE_TOO_LONG") {
|
|
subscriber.putError(.messageTooLong)
|
|
} else if error.text.hasPrefix("NOT_ENOUGH_FUNDS") {
|
|
subscriber.putError(.notEnoughFunds)
|
|
} else if error.text.hasPrefix("LITE_SERVER_") {
|
|
subscriber.putError(.network)
|
|
} else {
|
|
subscriber.putError(.generic)
|
|
}
|
|
} else {
|
|
subscriber.putError(.generic)
|
|
}
|
|
}, completed: {
|
|
subscriber.putCompletion()
|
|
})
|
|
disposable.set(ActionDisposable {
|
|
cancel?.dispose()
|
|
})
|
|
}
|
|
}
|
|
|
|
return disposable
|
|
}
|
|
}
|
|
|
|
fileprivate func estimateSendGramsQueryFees(preparedQuery: TONPreparedSendGramsQuery) -> Signal<TONSendGramsQueryFees, SendGramsFromWalletError> {
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
|
|
self.impl.with { impl in
|
|
impl.withInstance { ton in
|
|
let cancel = ton.estimateSendGramsQueryFees(preparedQuery).start(next: { result in
|
|
guard let result = result as? TONSendGramsQueryFees else {
|
|
subscriber.putError(.generic)
|
|
return
|
|
}
|
|
subscriber.putNext(result)
|
|
subscriber.putCompletion()
|
|
}, error: { error in
|
|
if let error = error as? TONError {
|
|
if error.text.hasPrefix("INVALID_ACCOUNT_ADDRESS") {
|
|
subscriber.putError(.invalidAddress)
|
|
} else if error.text.hasPrefix("DANGEROUS_TRANSACTION") {
|
|
subscriber.putError(.destinationIsNotInitialized)
|
|
} else if error.text.hasPrefix("MESSAGE_TOO_LONG") {
|
|
subscriber.putError(.messageTooLong)
|
|
} else if error.text.hasPrefix("NOT_ENOUGH_FUNDS") {
|
|
subscriber.putError(.notEnoughFunds)
|
|
} else if error.text.hasPrefix("LITE_SERVER_") {
|
|
subscriber.putError(.network)
|
|
} else {
|
|
subscriber.putError(.generic)
|
|
}
|
|
} else {
|
|
subscriber.putError(.generic)
|
|
}
|
|
}, completed: nil)
|
|
disposable.set(ActionDisposable {
|
|
cancel?.dispose()
|
|
})
|
|
}
|
|
}
|
|
|
|
return disposable
|
|
}
|
|
}
|
|
|
|
fileprivate func commitPreparedSendGramsQuery(_ preparedQuery: TONPreparedSendGramsQuery) -> Signal<Never, SendGramsFromWalletError> {
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
|
|
self.impl.with { impl in
|
|
impl.withInstance { ton in
|
|
let cancel = ton.commit(preparedQuery).start(next: { result in
|
|
preconditionFailure()
|
|
}, error: { error in
|
|
if let error = error as? TONError {
|
|
if error.text.hasPrefix("INVALID_ACCOUNT_ADDRESS") {
|
|
subscriber.putError(.invalidAddress)
|
|
} else if error.text.hasPrefix("DANGEROUS_TRANSACTION") {
|
|
subscriber.putError(.destinationIsNotInitialized)
|
|
} else if error.text.hasPrefix("MESSAGE_TOO_LONG") {
|
|
subscriber.putError(.messageTooLong)
|
|
} else if error.text.hasPrefix("NOT_ENOUGH_FUNDS") {
|
|
subscriber.putError(.notEnoughFunds)
|
|
} else if error.text.hasPrefix("LITE_SERVER_") {
|
|
subscriber.putError(.network)
|
|
} else {
|
|
subscriber.putError(.generic)
|
|
}
|
|
} else {
|
|
subscriber.putError(.generic)
|
|
}
|
|
}, completed: {
|
|
subscriber.putCompletion()
|
|
})
|
|
disposable.set(ActionDisposable {
|
|
cancel?.dispose()
|
|
})
|
|
}
|
|
}
|
|
|
|
return disposable
|
|
}
|
|
}
|
|
|
|
fileprivate func walletRestoreWords(publicKey: WalletPublicKey, decryptedSecret: Data, localPassword: Data) -> Signal<[String], WalletRestoreWordsError> {
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
|
|
self.impl.with { impl in
|
|
impl.withInstance { ton in
|
|
let cancel = ton.export(TONKey(publicKey: publicKey.rawValue, secret: decryptedSecret), localPassword: localPassword).start(next: { wordList in
|
|
guard let wordList = wordList as? [String] else {
|
|
subscriber.putError(.generic)
|
|
return
|
|
}
|
|
subscriber.putNext(wordList)
|
|
}, error: { _ in
|
|
subscriber.putError(.generic)
|
|
}, completed: {
|
|
subscriber.putCompletion()
|
|
})
|
|
disposable.set(ActionDisposable {
|
|
cancel?.dispose()
|
|
})
|
|
}
|
|
}
|
|
|
|
return disposable
|
|
}
|
|
}
|
|
|
|
fileprivate func deleteAllLocalWalletsData() -> Signal<Never, DeleteAllLocalWalletsDataError> {
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
|
|
self.impl.with { impl in
|
|
impl.withInstance { ton in
|
|
let cancel = ton.deleteAllKeys().start(next: { _ in
|
|
assertionFailure()
|
|
}, error: { _ in
|
|
subscriber.putError(.generic)
|
|
}, completed: {
|
|
subscriber.putCompletion()
|
|
})
|
|
disposable.set(ActionDisposable {
|
|
cancel?.dispose()
|
|
})
|
|
}
|
|
}
|
|
|
|
return disposable
|
|
}
|
|
}
|
|
fileprivate func encrypt(_ decryptedData: Data, secret: Data) -> Signal<Data, NoError> {
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
|
|
self.impl.with { impl in
|
|
impl.withInstance { ton in
|
|
subscriber.putNext(ton.encrypt(decryptedData, secret: secret))
|
|
subscriber.putCompletion()
|
|
}
|
|
}
|
|
|
|
return disposable
|
|
}
|
|
}
|
|
fileprivate func decrypt(_ encryptedData: Data, secret: Data) -> Signal<Data?, NoError> {
|
|
return Signal { subscriber in
|
|
let disposable = MetaDisposable()
|
|
|
|
self.impl.with { impl in
|
|
impl.withInstance { ton in
|
|
subscriber.putNext(ton.decrypt(encryptedData, secret: secret))
|
|
subscriber.putCompletion()
|
|
}
|
|
}
|
|
|
|
return disposable
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct WalletPublicKey: Codable, Hashable {
|
|
public var rawValue: String
|
|
|
|
public init(rawValue: String) {
|
|
self.rawValue = rawValue
|
|
}
|
|
}
|
|
|
|
public struct WalletInfo: Codable, Equatable {
|
|
public let publicKey: WalletPublicKey
|
|
public let encryptedSecret: TonKeychainEncryptedData
|
|
|
|
public init(publicKey: WalletPublicKey, encryptedSecret: TonKeychainEncryptedData) {
|
|
self.publicKey = publicKey
|
|
self.encryptedSecret = encryptedSecret
|
|
}
|
|
}
|
|
|
|
public struct CombinedWalletState: Codable, Equatable {
|
|
public var walletState: WalletState
|
|
public var timestamp: Int64
|
|
public var topTransactions: [WalletTransaction]
|
|
public var pendingTransactions: [PendingWalletTransaction]
|
|
}
|
|
|
|
public struct WalletStateRecord: Codable, Equatable {
|
|
public let info: WalletInfo
|
|
public var exportCompleted: Bool
|
|
public var state: CombinedWalletState?
|
|
|
|
public init(info: WalletInfo, exportCompleted: Bool, state: CombinedWalletState?) {
|
|
self.info = info
|
|
self.exportCompleted = exportCompleted
|
|
self.state = state
|
|
}
|
|
}
|
|
|
|
public enum CreateWalletError {
|
|
case generic
|
|
}
|
|
|
|
public func tonlibEncrypt(tonInstance: TonInstance, decryptedData: Data, secret: Data) -> Signal<Data, NoError> {
|
|
return tonInstance.encrypt(decryptedData, secret: secret)
|
|
}
|
|
public func tonlibDecrypt(tonInstance: TonInstance, encryptedData: Data, secret: Data) -> Signal<Data?, NoError> {
|
|
return tonInstance.decrypt(encryptedData, secret: secret)
|
|
}
|
|
|
|
public func createWallet(storage: WalletStorageInterface, tonInstance: TonInstance, keychain: TonKeychain, localPassword: Data) -> Signal<(WalletInfo, [String]), CreateWalletError> {
|
|
return tonInstance.createWallet(keychain: keychain, localPassword: localPassword)
|
|
|> mapToSignal { walletInfo, wordList -> Signal<(WalletInfo, [String]), CreateWalletError> in
|
|
return storage.updateWalletRecords({ records in
|
|
var records = records
|
|
records.append(WalletStateRecord(info: walletInfo, exportCompleted: false, state: nil))
|
|
return records
|
|
})
|
|
|> map { _ -> (WalletInfo, [String]) in
|
|
return (walletInfo, wordList)
|
|
}
|
|
|> castError(CreateWalletError.self)
|
|
}
|
|
}
|
|
|
|
public func confirmWalletExported(storage: WalletStorageInterface, publicKey: WalletPublicKey) -> Signal<Never, NoError> {
|
|
return storage.updateWalletRecords { records in
|
|
var records = records
|
|
for i in 0 ..< records.count {
|
|
if records[i].info.publicKey == publicKey {
|
|
records[i].exportCompleted = true
|
|
}
|
|
}
|
|
return records
|
|
}
|
|
|> ignoreValues
|
|
}
|
|
|
|
private enum ImportWalletInternalError {
|
|
case generic
|
|
}
|
|
|
|
public enum ImportWalletError {
|
|
case generic
|
|
}
|
|
|
|
public func importWallet(storage: WalletStorageInterface, tonInstance: TonInstance, keychain: TonKeychain, wordList: [String], localPassword: Data) -> Signal<WalletInfo, ImportWalletError> {
|
|
return tonInstance.importWallet(keychain: keychain, wordList: wordList, localPassword: localPassword)
|
|
|> `catch` { error -> Signal<WalletInfo, ImportWalletError> in
|
|
switch error {
|
|
case .generic:
|
|
return .fail(.generic)
|
|
}
|
|
}
|
|
|> mapToSignal { walletInfo -> Signal<WalletInfo, ImportWalletError> in
|
|
return storage.updateWalletRecords { records in
|
|
var records = records
|
|
records.append(WalletStateRecord(info: walletInfo, exportCompleted: true, state: nil))
|
|
return records
|
|
}
|
|
|> map { _ -> WalletInfo in
|
|
return walletInfo
|
|
}
|
|
|> castError(ImportWalletError.self)
|
|
}
|
|
}
|
|
|
|
public enum DeleteAllLocalWalletsDataError {
|
|
case generic
|
|
}
|
|
|
|
public func deleteAllLocalWalletsData(storage: WalletStorageInterface, tonInstance: TonInstance) -> Signal<Never, DeleteAllLocalWalletsDataError> {
|
|
return tonInstance.deleteAllLocalWalletsData()
|
|
|> `catch` { _ -> Signal<Never, DeleteAllLocalWalletsDataError> in
|
|
return .complete()
|
|
}
|
|
|> then(
|
|
storage.updateWalletRecords { _ in [] }
|
|
|> castError(DeleteAllLocalWalletsDataError.self)
|
|
|> ignoreValues
|
|
)
|
|
}
|
|
|
|
public enum WalletRestoreWordsError {
|
|
case generic
|
|
}
|
|
|
|
public func walletRestoreWords(tonInstance: TonInstance, publicKey: WalletPublicKey, decryptedSecret: Data, localPassword: Data) -> Signal<[String], WalletRestoreWordsError> {
|
|
return tonInstance.walletRestoreWords(publicKey: publicKey, decryptedSecret: decryptedSecret, localPassword: localPassword)
|
|
}
|
|
|
|
public struct WalletState: Codable, Equatable {
|
|
public let balance: Int64
|
|
public let lastTransactionId: WalletTransactionId?
|
|
|
|
public init(balance: Int64, lastTransactionId: WalletTransactionId?) {
|
|
self.balance = balance
|
|
self.lastTransactionId = lastTransactionId
|
|
}
|
|
}
|
|
|
|
public func walletAddress(publicKey: WalletPublicKey, tonInstance: TonInstance) -> Signal<String, NoError> {
|
|
return tonInstance.walletAddress(publicKey: publicKey)
|
|
}
|
|
|
|
private enum GetWalletStateError {
|
|
case generic
|
|
case network
|
|
}
|
|
|
|
private func getWalletState(address: String, tonInstance: TonInstance) -> Signal<(WalletState, Int64), GetWalletStateError> {
|
|
return tonInstance.getWalletState(address: address)
|
|
}
|
|
|
|
public enum GetCombinedWalletStateError {
|
|
case generic
|
|
case network
|
|
}
|
|
|
|
public enum CombinedWalletStateResult {
|
|
case cached(CombinedWalletState?)
|
|
case updated(CombinedWalletState)
|
|
}
|
|
|
|
public enum CombinedWalletStateSubject {
|
|
case wallet(WalletInfo)
|
|
case address(String)
|
|
}
|
|
|
|
public func getCombinedWalletState(storage: WalletStorageInterface, subject: CombinedWalletStateSubject, tonInstance: TonInstance, onlyCached: Bool = false) -> Signal<CombinedWalletStateResult, GetCombinedWalletStateError> {
|
|
switch subject {
|
|
case let .wallet(walletInfo):
|
|
return storage.getWalletRecords()
|
|
|> map { records -> CombinedWalletState? in
|
|
for item in records {
|
|
if item.info.publicKey == walletInfo.publicKey {
|
|
return item.state
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|> castError(GetCombinedWalletStateError.self)
|
|
|> mapToSignal { cachedState -> Signal<CombinedWalletStateResult, GetCombinedWalletStateError> in
|
|
if onlyCached {
|
|
return .single(.cached(cachedState))
|
|
}
|
|
return .single(.cached(cachedState))
|
|
|> then(
|
|
tonInstance.walletAddress(publicKey: walletInfo.publicKey)
|
|
|> castError(GetCombinedWalletStateError.self)
|
|
|> mapToSignal { address -> Signal<CombinedWalletStateResult, GetCombinedWalletStateError> in
|
|
|
|
let walletState: Signal<(WalletState, Int64), GetCombinedWalletStateError>
|
|
if cachedState == nil {
|
|
walletState = getWalletState(address: address, tonInstance: tonInstance)
|
|
|> retry(1.0, maxDelay: 5.0, onQueue: .concurrentDefaultQueue())
|
|
|> castError(GetCombinedWalletStateError.self)
|
|
} else {
|
|
walletState = getWalletState(address: address, tonInstance: tonInstance)
|
|
|> retryTonRequest(isNetworkError: { error in
|
|
if case .network = error {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
})
|
|
|> mapError { error -> GetCombinedWalletStateError in
|
|
if case .network = error {
|
|
return .network
|
|
} else {
|
|
return .generic
|
|
}
|
|
}
|
|
}
|
|
|
|
return walletState
|
|
|> mapToSignal { walletState, syncUtime -> Signal<CombinedWalletStateResult, GetCombinedWalletStateError> in
|
|
let topTransactions: Signal<[WalletTransaction], GetCombinedWalletStateError>
|
|
if walletState.lastTransactionId == cachedState?.walletState.lastTransactionId {
|
|
topTransactions = .single(cachedState?.topTransactions ?? [])
|
|
} else {
|
|
if cachedState == nil {
|
|
topTransactions = getWalletTransactions(address: address, previousId: nil, tonInstance: tonInstance)
|
|
|> retry(1.0, maxDelay: 5.0, onQueue: .concurrentDefaultQueue())
|
|
|> castError(GetCombinedWalletStateError.self)
|
|
} else {
|
|
topTransactions = getWalletTransactions(address: address, previousId: nil, tonInstance: tonInstance)
|
|
|> mapError { error -> GetCombinedWalletStateError in
|
|
if case .network = error {
|
|
return .network
|
|
} else {
|
|
return .generic
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return topTransactions
|
|
|> mapToSignal { topTransactions -> Signal<CombinedWalletStateResult, GetCombinedWalletStateError> in
|
|
let lastTransactionTimestamp = topTransactions.last?.timestamp
|
|
var listTransactionBodyHashes = Set<Data>()
|
|
for transaction in topTransactions {
|
|
if let message = transaction.inMessage {
|
|
listTransactionBodyHashes.insert(message.bodyHash)
|
|
}
|
|
for message in transaction.outMessages {
|
|
listTransactionBodyHashes.insert(message.bodyHash)
|
|
}
|
|
}
|
|
let pendingTransactions = (cachedState?.pendingTransactions ?? []).filter { transaction in
|
|
if transaction.validUntilTimestamp <= syncUtime {
|
|
return false
|
|
} else if let lastTransactionTimestamp = lastTransactionTimestamp, transaction.validUntilTimestamp <= lastTransactionTimestamp {
|
|
return false
|
|
} else {
|
|
if listTransactionBodyHashes.contains(transaction.bodyHash) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
let combinedState = CombinedWalletState(walletState: walletState, timestamp: syncUtime, topTransactions: topTransactions, pendingTransactions: pendingTransactions)
|
|
|
|
return storage.updateWalletRecords { records in
|
|
var records = records
|
|
for i in 0 ..< records.count {
|
|
if records[i].info.publicKey == walletInfo.publicKey {
|
|
records[i].state = combinedState
|
|
}
|
|
}
|
|
return records
|
|
}
|
|
|> map { _ -> CombinedWalletStateResult in
|
|
return .updated(combinedState)
|
|
}
|
|
|> castError(GetCombinedWalletStateError.self)
|
|
}
|
|
}
|
|
}
|
|
)
|
|
}
|
|
case let .address(address):
|
|
let updated = getWalletState(address: address, tonInstance: tonInstance)
|
|
|> mapError { _ -> GetCombinedWalletStateError in
|
|
return .generic
|
|
}
|
|
|> mapToSignal { walletState, syncUtime -> Signal<CombinedWalletStateResult, GetCombinedWalletStateError> in
|
|
let topTransactions: Signal<[WalletTransaction], GetCombinedWalletStateError>
|
|
|
|
topTransactions = getWalletTransactions(address: address, previousId: nil, tonInstance: tonInstance)
|
|
|> mapError { _ -> GetCombinedWalletStateError in
|
|
return .generic
|
|
}
|
|
return topTransactions
|
|
|> mapToSignal { topTransactions -> Signal<CombinedWalletStateResult, GetCombinedWalletStateError> in
|
|
let combinedState = CombinedWalletState(walletState: walletState, timestamp: syncUtime, topTransactions: topTransactions, pendingTransactions: [])
|
|
return .single(.updated(combinedState))
|
|
}
|
|
}
|
|
return .single(.cached(nil))
|
|
|> then(updated)
|
|
}
|
|
}
|
|
|
|
public enum SendGramsFromWalletError {
|
|
case generic
|
|
case secretDecryptionFailed
|
|
case invalidAddress
|
|
case destinationIsNotInitialized
|
|
case messageTooLong
|
|
case notEnoughFunds
|
|
case network
|
|
}
|
|
|
|
public struct EstimatedSendGramsFees {
|
|
public let inFwdFee: Int64
|
|
public let storageFee: Int64
|
|
public let gasFee: Int64
|
|
public let fwdFee: Int64
|
|
}
|
|
|
|
public func verifySendGramsRequestAndEstimateFees(tonInstance: TonInstance, walletInfo: WalletInfo, toAddress: String, amount: Int64, textMessage: Data, timeout: Int32) -> Signal<EstimatedSendGramsFees, SendGramsFromWalletError> {
|
|
return walletAddress(publicKey: walletInfo.publicKey, tonInstance: tonInstance)
|
|
|> castError(SendGramsFromWalletError.self)
|
|
|> mapToSignal { fromAddress -> Signal<EstimatedSendGramsFees, SendGramsFromWalletError> in
|
|
return tonInstance.prepareFakeSendGramsFromWalletQuery(walletInfo: walletInfo, fromAddress: fromAddress, toAddress: toAddress, amount: amount, textMessage: textMessage, timeout: timeout)
|
|
|> mapToSignal { preparedQuery -> Signal<EstimatedSendGramsFees, SendGramsFromWalletError> in
|
|
return tonInstance.estimateSendGramsQueryFees(preparedQuery: preparedQuery)
|
|
|> map { result -> EstimatedSendGramsFees in
|
|
return EstimatedSendGramsFees(inFwdFee: result.sourceFees.inFwdFee, storageFee: result.sourceFees.storageFee, gasFee: result.sourceFees.gasFee, fwdFee: result.sourceFees.fwdFee)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public func sendGramsFromWallet(storage: WalletStorageInterface, tonInstance: TonInstance, walletInfo: WalletInfo, decryptedSecret: Data, localPassword: Data, toAddress: String, amount: Int64, textMessage: Data, forceIfDestinationNotInitialized: Bool, timeout: Int32, randomId: Int64) -> Signal<PendingWalletTransaction, SendGramsFromWalletError> {
|
|
return walletAddress(publicKey: walletInfo.publicKey, tonInstance: tonInstance)
|
|
|> castError(SendGramsFromWalletError.self)
|
|
|> mapToSignal { fromAddress -> Signal<PendingWalletTransaction, SendGramsFromWalletError> in
|
|
return tonInstance.prepareSendGramsFromWalletQuery(decryptedSecret: decryptedSecret, localPassword: localPassword, walletInfo: walletInfo, fromAddress: fromAddress, toAddress: toAddress, amount: amount, textMessage: textMessage, forceIfDestinationNotInitialized: forceIfDestinationNotInitialized, timeout: timeout, randomId: randomId)
|
|
|> mapToSignal { preparedQuery -> Signal<PendingWalletTransaction, SendGramsFromWalletError> in
|
|
return tonInstance.commitPreparedSendGramsQuery(preparedQuery)
|
|
|> retryTonRequest(isNetworkError: { error in
|
|
if case .network = error {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
})
|
|
|> mapToSignal { _ -> Signal<PendingWalletTransaction, SendGramsFromWalletError> in
|
|
return .complete()
|
|
}
|
|
|> then(.single(PendingWalletTransaction(timestamp: Int64(Date().timeIntervalSince1970), validUntilTimestamp: preparedQuery.validUntil, bodyHash: preparedQuery.bodyHash, address: toAddress, value: amount, comment: textMessage)))
|
|
|> mapToSignal { result in
|
|
return storage.updateWalletRecords { records in
|
|
var records = records
|
|
for i in 0 ..< records.count {
|
|
if records[i].info.publicKey == walletInfo.publicKey {
|
|
if var state = records[i].state {
|
|
state.pendingTransactions.insert(result, at: 0)
|
|
records[i].state = state
|
|
}
|
|
}
|
|
}
|
|
return records
|
|
}
|
|
|> map { _ -> PendingWalletTransaction in
|
|
return result
|
|
}
|
|
|> castError(SendGramsFromWalletError.self)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct WalletTransactionId: Codable, Hashable {
|
|
public var lt: Int64
|
|
public var transactionHash: Data
|
|
}
|
|
|
|
private extension WalletTransactionId {
|
|
init(tonTransactionId: TONTransactionId) {
|
|
self.lt = tonTransactionId.lt
|
|
self.transactionHash = tonTransactionId.transactionHash
|
|
}
|
|
}
|
|
|
|
public final class WalletTransactionMessage: Codable, Equatable {
|
|
public let value: Int64
|
|
public let source: String
|
|
public let destination: String
|
|
public let textMessage: String
|
|
public let bodyHash: Data
|
|
|
|
init(value: Int64, source: String, destination: String, textMessage: String, bodyHash: Data) {
|
|
self.value = value
|
|
self.source = source
|
|
self.destination = destination
|
|
self.textMessage = textMessage
|
|
self.bodyHash = bodyHash
|
|
}
|
|
|
|
public static func ==(lhs: WalletTransactionMessage, rhs: WalletTransactionMessage) -> Bool {
|
|
if lhs.value != rhs.value {
|
|
return false
|
|
}
|
|
if lhs.source != rhs.source {
|
|
return false
|
|
}
|
|
if lhs.destination != rhs.destination {
|
|
return false
|
|
}
|
|
if lhs.textMessage != rhs.textMessage {
|
|
return false
|
|
}
|
|
if lhs.bodyHash != rhs.bodyHash {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
private extension WalletTransactionMessage {
|
|
convenience init(tonTransactionMessage: TONTransactionMessage) {
|
|
self.init(value: tonTransactionMessage.value, source: tonTransactionMessage.source, destination: tonTransactionMessage.destination, textMessage: tonTransactionMessage.textMessage, bodyHash: tonTransactionMessage.bodyHash)
|
|
}
|
|
}
|
|
|
|
public final class PendingWalletTransaction: Codable, Equatable {
|
|
public let timestamp: Int64
|
|
public let validUntilTimestamp: Int64
|
|
public let bodyHash: Data
|
|
public let address: String
|
|
public let value: Int64
|
|
public let comment: Data
|
|
|
|
public init(timestamp: Int64, validUntilTimestamp: Int64, bodyHash: Data, address: String, value: Int64, comment: Data) {
|
|
self.timestamp = timestamp
|
|
self.validUntilTimestamp = validUntilTimestamp
|
|
self.bodyHash = bodyHash
|
|
self.address = address
|
|
self.value = value
|
|
self.comment = comment
|
|
}
|
|
|
|
public static func ==(lhs: PendingWalletTransaction, rhs: PendingWalletTransaction) -> Bool {
|
|
if lhs.timestamp != rhs.timestamp {
|
|
return false
|
|
}
|
|
if lhs.validUntilTimestamp != rhs.validUntilTimestamp {
|
|
return false
|
|
}
|
|
if lhs.bodyHash != rhs.bodyHash {
|
|
return false
|
|
}
|
|
if lhs.value != rhs.value {
|
|
return false
|
|
}
|
|
if lhs.comment != rhs.comment {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
public final class WalletTransaction: Codable, Equatable {
|
|
public let data: Data
|
|
public let transactionId: WalletTransactionId
|
|
public let timestamp: Int64
|
|
public let storageFee: Int64
|
|
public let otherFee: Int64
|
|
public let inMessage: WalletTransactionMessage?
|
|
public let outMessages: [WalletTransactionMessage]
|
|
|
|
public var transferredValueWithoutFees: Int64 {
|
|
var value: Int64 = 0
|
|
if let inMessage = self.inMessage {
|
|
value += inMessage.value
|
|
}
|
|
for message in self.outMessages {
|
|
value -= message.value
|
|
}
|
|
return value
|
|
}
|
|
|
|
init(data: Data, transactionId: WalletTransactionId, timestamp: Int64, storageFee: Int64, otherFee: Int64, inMessage: WalletTransactionMessage?, outMessages: [WalletTransactionMessage]) {
|
|
self.data = data
|
|
self.transactionId = transactionId
|
|
self.timestamp = timestamp
|
|
self.storageFee = storageFee
|
|
self.otherFee = otherFee
|
|
self.inMessage = inMessage
|
|
self.outMessages = outMessages
|
|
}
|
|
|
|
public static func ==(lhs: WalletTransaction, rhs: WalletTransaction) -> Bool {
|
|
if lhs.data != rhs.data {
|
|
return false
|
|
}
|
|
if lhs.transactionId != rhs.transactionId {
|
|
return false
|
|
}
|
|
if lhs.timestamp != rhs.timestamp {
|
|
return false
|
|
}
|
|
if lhs.storageFee != rhs.storageFee {
|
|
return false
|
|
}
|
|
if lhs.otherFee != rhs.otherFee {
|
|
return false
|
|
}
|
|
if lhs.inMessage != rhs.inMessage {
|
|
return false
|
|
}
|
|
if lhs.outMessages != rhs.outMessages {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
private extension WalletTransaction {
|
|
convenience init(tonTransaction: TONTransaction) {
|
|
self.init(data: tonTransaction.data, transactionId: WalletTransactionId(tonTransactionId: tonTransaction.transactionId), timestamp: tonTransaction.timestamp, storageFee: tonTransaction.storageFee, otherFee: tonTransaction.otherFee, inMessage: tonTransaction.inMessage.flatMap(WalletTransactionMessage.init(tonTransactionMessage:)), outMessages: tonTransaction.outMessages.map(WalletTransactionMessage.init(tonTransactionMessage:)))
|
|
}
|
|
}
|
|
|
|
public enum GetWalletTransactionsError {
|
|
case generic
|
|
case network
|
|
}
|
|
|
|
public func getWalletTransactions(address: String, previousId: WalletTransactionId?, tonInstance: TonInstance) -> Signal<[WalletTransaction], GetWalletTransactionsError> {
|
|
return getWalletTransactionsOnce(address: address, previousId: previousId, tonInstance: tonInstance)
|
|
|> mapToSignal { transactions in
|
|
guard let lastTransaction = transactions.last, transactions.count >= 2 else {
|
|
return .single(transactions)
|
|
}
|
|
return getWalletTransactionsOnce(address: address, previousId: lastTransaction.transactionId, tonInstance: tonInstance)
|
|
|> map { additionalTransactions in
|
|
var result = transactions
|
|
var existingIds = Set(result.map { $0.transactionId })
|
|
for transaction in additionalTransactions {
|
|
if !existingIds.contains(transaction.transactionId) {
|
|
existingIds.insert(transaction.transactionId)
|
|
result.append(transaction)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
|
|
private func retryTonRequest<T, E>(isNetworkError: @escaping (E) -> Bool) -> (Signal<T, E>) -> Signal<T, E> {
|
|
return { signal in
|
|
return signal
|
|
|> retry(retryOnError: isNetworkError, delayIncrement: 0.2, maxDelay: 5.0, maxRetries: 3, onQueue: Queue.concurrentDefaultQueue())
|
|
}
|
|
}
|
|
|
|
private enum WalletLastTransactionIdError {
|
|
case generic
|
|
case network
|
|
}
|
|
|
|
private func getWalletTransactionsOnce(address: String, previousId: WalletTransactionId?, tonInstance: TonInstance) -> Signal<[WalletTransaction], GetWalletTransactionsError> {
|
|
let previousIdValue: Signal<WalletTransactionId?, GetWalletTransactionsError>
|
|
if let previousId = previousId {
|
|
previousIdValue = .single(previousId)
|
|
} else {
|
|
previousIdValue = tonInstance.walletLastTransactionId(address: address)
|
|
|> retryTonRequest(isNetworkError: { error in
|
|
if case .network = error {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
})
|
|
|> mapError { error -> GetWalletTransactionsError in
|
|
if case .network = error {
|
|
return .network
|
|
} else {
|
|
return .generic
|
|
}
|
|
}
|
|
}
|
|
return previousIdValue
|
|
|> mapToSignal { previousId in
|
|
if let previousId = previousId {
|
|
return tonInstance.getWalletTransactions(address: address, previousId: previousId)
|
|
|> retryTonRequest(isNetworkError: { error in
|
|
if case .network = error {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
})
|
|
} else {
|
|
return .single([])
|
|
}
|
|
}
|
|
}
|
|
|
|
public enum LocalWalletConfigurationDecodingError: Error {
|
|
case generic
|
|
}
|
|
|
|
public enum LocalWalletConfigurationSource: Codable, Equatable {
|
|
enum Key: CodingKey {
|
|
case url
|
|
case string
|
|
}
|
|
|
|
case url(String)
|
|
case string(String)
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: Key.self)
|
|
if let url = try? container.decode(String.self, forKey: .url) {
|
|
self = .url(url)
|
|
} else if let string = try? container.decode(String.self, forKey: .string) {
|
|
self = .string(string)
|
|
} else {
|
|
throw LocalWalletConfigurationDecodingError.generic
|
|
}
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
var container = try encoder.container(keyedBy: Key.self)
|
|
switch self {
|
|
case let .url(url):
|
|
try container.encode(url, forKey: .url)
|
|
case let .string(string):
|
|
try container.encode(string, forKey: .string)
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct LocalWalletConfiguration: Codable, Equatable {
|
|
public var source: LocalWalletConfigurationSource
|
|
public var blockchainName: String
|
|
|
|
public init(source: LocalWalletConfigurationSource, blockchainName: String) {
|
|
self.source = source
|
|
self.blockchainName = blockchainName
|
|
}
|
|
}
|
|
|
|
public protocol WalletStorageInterface {
|
|
func watchWalletRecords() -> Signal<[WalletStateRecord], NoError>
|
|
func getWalletRecords() -> Signal<[WalletStateRecord], NoError>
|
|
func updateWalletRecords(_ f: @escaping ([WalletStateRecord]) -> [WalletStateRecord]) -> Signal<[WalletStateRecord], NoError>
|
|
func localWalletConfiguration() -> Signal<LocalWalletConfiguration, NoError>
|
|
func updateLocalWalletConfiguration(_ f: @escaping (LocalWalletConfiguration) -> LocalWalletConfiguration) -> Signal<Never, NoError>
|
|
}
|
|
|
|
public struct WalletValidateConfigResult {
|
|
public var defaultWalletId: Int64
|
|
}
|
|
|
|
public enum WalletValidateConfigError {
|
|
case generic
|
|
|
|
}
|