2019-10-31 22:34:42 +04:00

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
}