Fix testsuite, prevent fetching contract data for address `0x0000000000000000000000000000000000000000` #6097

pull/6108/head
Krypto Pank 2 years ago
parent dc9ac79799
commit 91688ab593
  1. 10
      AlphaWallet/ActiveWalletCoordinator.swift
  2. 24
      AlphaWallet/AppCoordinator.swift
  3. 25
      AlphaWallet/Browser/Coordinators/QRCodeResolutionCoordinator.swift
  4. 19
      AlphaWallet/Tokens/ViewControllers/AddHideTokensViewController.swift
  5. 20
      AlphaWallet/Tokens/ViewModels/AddHideTokensViewModel.swift
  6. 2
      AlphaWallet/Transfer/ViewModels/SendViewModel.swift
  7. 37
      AlphaWalletTests/Ens/EnsResolverTests.swift
  8. 6
      AlphaWalletTests/EtherClient/EtherKeystoreTests.swift
  9. 171
      AlphaWalletTests/Foundation/QRCodeValueParserTests.swift
  10. 26
      AlphaWalletTests/Transfer/Types/TransactionTypeFromQrCodeTests.swift
  11. 26
      AlphaWalletTests/Transfer/ViewControllers/SendViewControllerTests.swift
  12. 1
      AlphaWalletTests/ViewControllers/PaymentCoordinatorTests.swift
  13. 8
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Foundation/Eip681Parser.swift
  14. 40
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/Eip681UrlResolver.swift
  15. 110
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/TokensAutodetector/ImportToken.swift
  16. 22
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/TokensAutodetector/TokensAutodetector.swift

@ -564,11 +564,13 @@ class ActiveWalletCoordinator: NSObject, Coordinator, DappRequestHandlerDelegate
}
func addImported(contract: AlphaWallet.Address, forServer server: RPCServer) {
importToken.importToken(for: contract, server: server, onlyIfThereIsABalance: false)
.done { _ in }
.catch { error in
importToken
.importTokenPublisher(for: contract, server: server, onlyIfThereIsABalance: false)
.sink(receiveCompletion: { result in
guard case .failure(let error) = result else { return }
debugLog("Error while adding imported token contract: \(contract.eip55String) server: \(server) wallet: \(self.wallet.address.eip55String) error: \(error)")
}
}, receiveValue: { _ in })
.store(in: &cancelable)
}
func show(error: Error) {

@ -604,6 +604,7 @@ class AppCoordinator: NSObject, Coordinator {
let dependency = WalletDependencies(
activitiesPipeLine: activitiesPipeLine,
transactionsDataStore: transactionsDataStore,
tokensDataStore: tokensDataStore,
importToken: importToken,
tokensService: tokensService,
pipeline: pipeline,
@ -711,16 +712,18 @@ extension AppCoordinator: UniversalLinkServiceDelegate {
tokenScriptOverridesFileManager.importTokenScriptOverrides(url: url)
case .eip681(let url):
let paymentFlowResolver = Eip681UrlResolver(config: config, importToken: resolver.importToken, missingRPCServerStrategy: .fallbackToAnyMatching)
firstly {
paymentFlowResolver.resolve(url: url)
}.done { result in
switch result {
case .address:
break //Add handling address, maybe same action when scan qr code
case .transaction(let transactionType, let token):
resolver.showPaymentFlow(for: .send(type: .transaction(transactionType)), server: token.server, navigationController: resolver.presentationNavigationController)
}
}.cauterize()
paymentFlowResolver.resolve(url: url)
.sink(receiveCompletion: { result in
guard case .failure(let error) = result else { return }
verboseLog("[Eip681UrlResolver] failure to resolve value from: \(url) with error: \(error)")
}, receiveValue: { result in
switch result {
case .address:
break //Add handling address, maybe same action when scan qr code
case .transaction(let transactionType, let token):
resolver.showPaymentFlow(for: .send(type: .transaction(transactionType)), server: token.server, navigationController: resolver.presentationNavigationController)
}
}).store(in: &cancelable)
case .walletConnect(let url, let source):
switch source {
case .safariExtension:
@ -786,6 +789,7 @@ extension AppCoordinator {
struct WalletDependencies {
let activitiesPipeLine: ActivitiesPipeLine
let transactionsDataStore: TransactionDataStore
let tokensDataStore: TokensDataStore
let importToken: ImportToken
let tokensService: DetectedContractsProvideble & TokenProvidable & TokenAddable & TokensServiceTests
let pipeline: TokensProcessingPipeline

@ -9,6 +9,7 @@ import Foundation
import BigInt
import PromiseKit
import AlphaWalletFoundation
import Combine
enum QrCodeResolution {
case address(address: AlphaWallet.Address, action: ScanQRCodeAction)
@ -40,6 +41,8 @@ final class QRCodeResolutionCoordinator: Coordinator {
}
private let scanQRCodeCoordinator: ScanQRCodeCoordinator
private let account: Wallet
private var cancellable = Set<AnyCancellable>()
var coordinators: [Coordinator] = []
weak var delegate: QRCodeResolutionCoordinatorDelegate?
@ -108,16 +111,18 @@ extension QRCodeResolutionCoordinator: ScanQRCodeCoordinatorDelegate {
switch usage {
case .all(_, let importToken):
let resolver = Eip681UrlResolver(config: config, importToken: importToken, missingRPCServerStrategy: .fallbackToFirstMatching)
firstly {
resolver.resolve(protocolName: protocolName, address: address, functionName: functionName, params: params)
}.done { result in
switch result {
case .transaction(let transactionType, let token):
delegate.coordinator(self, didResolve: .transactionType(transactionType: transactionType, token: token))
case .address:
break // Not possible here
}
}.cauterize()
resolver.resolve(protocolName: protocolName, address: address, functionName: functionName, params: params)
.sink(receiveCompletion: { result in
guard case .failure(let error) = result else { return }
verboseLog("[Eip681UrlResolver] failure to resolve value from: \(qrCodeValue) with error: \(error)")
}, receiveValue: { result in
switch result {
case .transaction(let transactionType, let token):
delegate.coordinator(self, didResolve: .transactionType(transactionType: transactionType, token: token))
case .address:
break // Not possible here
}
}).store(in: &cancellable)
case .importWalletOnly:
break
}

@ -182,17 +182,20 @@ extension AddHideTokensViewController: UITableViewDataSource {
} else {
tableView.reloadData()
}
case .promise(let promise):
case .publisher(let publisher):
self.displayLoading()
promise.done(on: .none, flags: .barrier) { _ in
//no-op
}.catch { _ in
self.displayError(message: R.string.localizable.walletsHideTokenErrorAddTokenFailure())
}.finally {
publisher.sink(receiveCompletion: { result in
if case .failure = result {
self.displayError(message: R.string.localizable.walletsHideTokenErrorAddTokenFailure())
}
tableView.reloadData()
self.hideLoading()
}
}, receiveValue: { _ in
//no-op
}).store(in: &cancelable)
}
}
@ -213,7 +216,7 @@ extension AddHideTokensViewController: UITableViewDataSource {
completionHandler(false)
}
case .promise:
case .publisher:
break
}
}

@ -140,23 +140,25 @@ final class AddHideTokensViewModel {
}
case .popularTokens:
let token = popularTokens[indexPath.row]
let promise = importToken
.importToken(for: token.contractAddress, server: token.server, onlyIfThereIsABalance: false)
.then { [tokenCollection] _token -> Promise<TokenWithIndexToInsert?> in
guard let token = tokenCollection.tokenViewModel(for: _token) else { return .init(error: PMKError.cancelled) }
let publisher = importToken
.importTokenPublisher(for: token.contractAddress, server: token.server, onlyIfThereIsABalance: false)
.flatMap { [tokenCollection] _token -> AnyPublisher<TokenWithIndexToInsert?, ImportToken.ImportTokenError> in
guard let token = tokenCollection.tokenViewModel(for: _token) else {
return .fail(ImportToken.ImportTokenError.internal(error: PMKError.cancelled))
}
self.popularTokens.remove(at: indexPath.row)
self.displayedTokens.append(token)
if let sectionIndex = self.sections.firstIndex(of: .displayedTokens) {
tokenCollection.mark(token: token, isHidden: false)
return .value((token, IndexPath(row: max(0, self.displayedTokens.count - 1), section: Int(sectionIndex))))
return .just((token, IndexPath(row: max(0, self.displayedTokens.count - 1), section: Int(sectionIndex))))
}
return .value(nil)
}
return .just(nil)
}.eraseToAnyPublisher()
return .promise(promise)
return .publisher(publisher)
}
return .value(nil)
@ -285,7 +287,7 @@ extension AddHideTokensViewModel {
enum ShowHideTokenResult {
case value(TokenWithIndexToInsert?)
case promise(Promise<TokenWithIndexToInsert?>)
case publisher(AnyPublisher<TokenWithIndexToInsert?, ImportToken.ImportTokenError>)
}
}

@ -434,7 +434,7 @@ final class TransactionTypeFromQrCode {
}
return eip681UrlResolver
.resolvePublisher(url: url)
.resolve(url: url)
.flatMap { [session] result -> AnyPublisher<TransactionType, CheckEIP681Error> in
switch result {
case .transaction(let transactionType, let token):

@ -5,6 +5,8 @@ import Foundation
import XCTest
import Combine
import AlphaWalletFoundation
import AlphaWalletCore
import AlphaWalletWeb3
class EnsResolverTests: XCTestCase {
func testNameHash() {
@ -29,7 +31,16 @@ class EnsResolverTests: XCTestCase {
case .finished:
break
case .failure(let error):
XCTFail("Unknown error: \(error)")
guard case .embeded(let e) = error, let pe = e as? PromiseError, let e = pe.embedded as? AlphaWalletWeb3.Web3Error else {
XCTFail("Unknown error: \(error)")
return
}
switch e {
case .rateLimited:
break
default:
XCTFail("Unknown error: \(error)")
}
}
expectation.fulfill()
}, receiveValue: { address in
@ -50,7 +61,16 @@ class EnsResolverTests: XCTestCase {
case .finished:
break
case .failure(let error):
XCTFail("Unknown error: \(error)")
guard case .embeded(let e) = error, let pe = e as? PromiseError, let e = pe.embedded as? AlphaWalletWeb3.Web3Error else {
XCTFail("Unknown error: \(error)")
return
}
switch e {
case .rateLimited:
break
default:
XCTFail("Unknown error: \(error)")
}
}
expectation.fulfill()
}, receiveValue: { address in
@ -71,14 +91,23 @@ class EnsResolverTests: XCTestCase {
case .finished:
break
case .failure(let error):
XCTFail("Unknown error: \(error)")
guard case .embeded(let e) = error, let pe = e as? PromiseError, let e = pe.embedded as? AlphaWalletWeb3.Web3Error else {
XCTFail("Unknown error: \(error)")
return
}
switch e {
case .rateLimited:
break
default:
XCTFail("Unknown error: \(error)")
}
}
expectation.fulfill()
}, receiveValue: { address in
XCTAssertTrue(address.sameContract(as: "41563129cdbbd0c5d3e1c86cf9563926b243834d"), "ENS name did not resolve correctly")
}).store(in: &cancelable)
wait(for: expectations, timeout: 20)
wait(for: expectations, timeout: 100)
}
private func makeServerForMainnet() -> RPCServer {

@ -87,7 +87,7 @@ class EtherKeystoreTests: XCTestCase {
XCTAssertEqual(seedPhrase.split(separator: " ").count, 12)
}.store(in: &cancelable)
wait(for: [expectation], timeout: 0.01)
wait(for: [expectation], timeout: 600)
}
func testExportRawPrivateKeyToKeystoreFile() throws {
@ -107,8 +107,8 @@ class EtherKeystoreTests: XCTestCase {
expectation.fulfill()
}.store(in: &cancelable)
wait(for: [expectation], timeout: 20)
//NOTE: increase waiting time, latest version of iOS takes more time to decode encrypted data
wait(for: [expectation], timeout: 600)
}
func testRecentlyUsedAccount() throws {

@ -140,178 +140,139 @@ class QRCodeValueParserTests: XCTestCase {
func testParseNativeCryptoSend() {
guard let qrCodeValue = AddressOrEip681Parser.from(string: "ethereum:0xfb6916095ca1df60bb79Ce92ce3ea74c37c5d359?value=2.014e18") else { return XCTFail("Can't parse EIP 681") }
let expectation = self.expectation(description: "Promise resolves")
switch qrCodeValue {
case .address:
XCTFail("Can't parse EIP 681")
case .eip681(let protocolName, let address, let functionName, let params):
Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse().done { result in
expectation.fulfill()
switch result {
case .nativeCryptoSend(let chainId, let recipient, let amount):
XCTAssertNil(chainId)
XCTAssertTrue(recipient.sameContract(as: "0xfb6916095ca1df60bb79Ce92ce3ea74c37c5d359"))
XCTAssertEqual(amount.rawValue, "2014000000000000000")
case .erc20Send, .invalidOrNotSupported:
XCTFail("Parsed as wrong EIP 681 type")
}
}.cauterize()
switch Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse() {
case .nativeCryptoSend(let chainId, let recipient, let amount):
XCTAssertNil(chainId)
XCTAssertTrue(recipient.sameContract(as: "0xfb6916095ca1df60bb79Ce92ce3ea74c37c5d359"))
XCTAssertEqual(amount.rawValue, "2014000000000000000")
case .erc20Send, .invalidOrNotSupported:
XCTFail("Parsed as wrong EIP 681 type")
}
}
wait(for: [expectation], timeout: 20)
}
func testParseNativeCryptoSendWithScientificNotation() {
guard let qrCodeValue = AddressOrEip681Parser.from(string: "ethereum:0xfb6916095ca1df60bb79Ce92ce3ea74c37c5d359?value=2.014e18") else { return XCTFail("Can't parse EIP 681") }
let expectation = self.expectation(description: "Promise resolves")
switch qrCodeValue {
case .address:
XCTFail("Can't parse EIP 681")
case .eip681(let protocolName, let address, let functionName, let params):
Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse().done { result in
expectation.fulfill()
switch result {
case .nativeCryptoSend(let chainId, let recipient, let amount):
XCTAssertNil(chainId)
XCTAssertTrue(recipient.sameContract(as: "0xfb6916095ca1df60bb79Ce92ce3ea74c37c5d359"))
XCTAssertEqual(amount.rawValue, "2014000000000000000")
case .erc20Send, .invalidOrNotSupported:
XCTFail("Parsed as wrong EIP 681 type")
}
}.cauterize()
switch Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse() {
case .nativeCryptoSend(let chainId, let recipient, let amount):
XCTAssertNil(chainId)
XCTAssertTrue(recipient.sameContract(as: "0xfb6916095ca1df60bb79Ce92ce3ea74c37c5d359"))
XCTAssertEqual(amount.rawValue, "2014000000000000000")
case .erc20Send, .invalidOrNotSupported:
XCTFail("Parsed as wrong EIP 681 type")
}
}
wait(for: [expectation], timeout: 20)
}
func testParseErc20Send() {
guard let qrCodeValue = AddressOrEip681Parser.from(string: "ethereum:0x744d70fdbe2ba4cf95131626614a1763df805b9e/transfer?address=0x3d597789ea16054a084ac84ce87f50df9198f415&uint256=314e17") else { return XCTFail("Can't parse EIP 681") }
let expectation = self.expectation(description: "Promise resolves")
switch qrCodeValue {
case .address:
XCTFail("Can't parse EIP 681")
case .eip681(let protocolName, let address, let functionName, let params):
Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse().done { result in
expectation.fulfill()
switch result {
case .erc20Send(let contract, let chainId, let recipient, let amount):
XCTAssertEqual(contract, AlphaWallet.Address(string: "0x744d70fdbe2ba4cf95131626614a1763df805b9e"))
XCTAssertNil(chainId)
XCTAssertTrue(recipient?.sameContract(as: "0x3d597789ea16054a084ac84ce87f50df9198f415") ?? false)
XCTAssertEqual(amount.rawValue, "31400000000000000000")
case .nativeCryptoSend, .invalidOrNotSupported:
XCTFail("Parsed as wrong EIP 681 type")
}
}.cauterize()
switch Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse() {
case .erc20Send(let contract, let chainId, let recipient, let amount):
XCTAssertEqual(contract, AlphaWallet.Address(string: "0x744d70fdbe2ba4cf95131626614a1763df805b9e"))
XCTAssertNil(chainId)
XCTAssertTrue(recipient?.sameContract(as: "0x3d597789ea16054a084ac84ce87f50df9198f415") ?? false)
XCTAssertEqual(amount.rawValue, "31400000000000000000")
case .nativeCryptoSend, .invalidOrNotSupported:
XCTFail("Parsed as wrong EIP 681 type")
}
}
wait(for: [expectation], timeout: 20)
}
func testParseErc20SendWithoutRecipient() {
guard let qrCodeValue = AddressOrEip681Parser.from(string: "ethereum:0x60fa213f48cd0d83b54380108ccd03a6993247e0/transfer?uint256=1.5e18") else { return XCTFail("Can't parse EIP 681") }
let expectation = self.expectation(description: "Promise resolves")
switch qrCodeValue {
case .address:
XCTFail("Can't parse EIP 681")
case .eip681(let protocolName, let address, let functionName, let params):
Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse().done { result in
expectation.fulfill()
switch result {
case .erc20Send(let contract, let chainId, let recipient, let amount):
XCTAssertEqual(contract, AlphaWallet.Address(string: "0x60fa213f48cd0d83b54380108ccd03a6993247e0"))
XCTAssertNil(chainId)
XCTAssertNil(recipient)
XCTAssertEqual(amount.rawValue, "1500000000000000000")
case .nativeCryptoSend, .invalidOrNotSupported:
XCTFail("Parsed as wrong EIP 681 type")
}
}.cauterize()
switch Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse() {
case .erc20Send(let contract, let chainId, let recipient, let amount):
XCTAssertEqual(contract, AlphaWallet.Address(string: "0x60fa213f48cd0d83b54380108ccd03a6993247e0"))
XCTAssertNil(chainId)
XCTAssertNil(recipient)
XCTAssertEqual(amount.rawValue, "1500000000000000000")
case .nativeCryptoSend, .invalidOrNotSupported:
XCTFail("Parsed as wrong EIP 681 type")
}
}
wait(for: [expectation], timeout: 20)
}
func testParseNativeCryptoSendWithoutValue() {
guard let qrCodeValue = AddressOrEip681Parser.from(string: "ethereum:0xfb6916095ca1df60bb79Ce92ce3ea74c37c5d359") else { return XCTFail("Can't parse EIP 681") }
let expectation = self.expectation(description: "Promise resolves")
switch qrCodeValue {
case .address:
XCTFail("Can't parse EIP 681")
case .eip681(let protocolName, let address, let functionName, let params):
Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse().done { result in
expectation.fulfill()
switch result {
case .nativeCryptoSend(let chainId, let recipient, let amount):
XCTAssertNil(chainId)
XCTAssertTrue(recipient.sameContract(as: "0xfb6916095ca1df60bb79Ce92ce3ea74c37c5d359"))
XCTAssertEqual(amount.rawValue, "")
case .erc20Send, .invalidOrNotSupported:
XCTFail("Parsed as wrong EIP 681 type")
}
}.cauterize()
switch Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse() {
case .nativeCryptoSend(let chainId, let recipient, let amount):
XCTAssertNil(chainId)
XCTAssertTrue(recipient.sameContract(as: "0xfb6916095ca1df60bb79Ce92ce3ea74c37c5d359"))
XCTAssertEqual(amount.rawValue, "")
case .erc20Send, .invalidOrNotSupported:
XCTFail("Parsed as wrong EIP 681 type")
}
}
wait(for: [expectation], timeout: 20)
}
func testParseErc20SendWithoutAmount() {
guard let qrCodeValue = AddressOrEip681Parser.from(string: "ethereum:0x744d70fdbe2ba4cf95131626614a1763df805b9e/transfer?address=0x3d597789ea16054a084ac84ce87f50df9198f415") else { return XCTFail("Can't parse EIP 681") }
let expectation = self.expectation(description: "Promise resolves")
switch qrCodeValue {
case .address:
XCTFail("Can't parse EIP 681")
case .eip681(let protocolName, let address, let functionName, let params):
Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse().done { result in
expectation.fulfill()
switch result {
case .erc20Send(let contract, let chainId, let recipient, let amount):
XCTAssertEqual(contract, AlphaWallet.Address(string: "0x744d70fdbe2ba4cf95131626614a1763df805b9e"))
XCTAssertNil(chainId)
XCTAssertTrue(recipient?.sameContract(as: "0x3d597789ea16054a084ac84ce87f50df9198f415") ?? false)
XCTAssertEqual(amount.rawValue, "")
case .nativeCryptoSend, .invalidOrNotSupported:
XCTFail("Parsed as wrong EIP 681 type")
}
}.cauterize()
switch Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse() {
case .erc20Send(let contract, let chainId, let recipient, let amount):
XCTAssertEqual(contract, AlphaWallet.Address(string: "0x744d70fdbe2ba4cf95131626614a1763df805b9e"))
XCTAssertNil(chainId)
XCTAssertTrue(recipient?.sameContract(as: "0x3d597789ea16054a084ac84ce87f50df9198f415") ?? false)
XCTAssertEqual(amount.rawValue, "")
case .nativeCryptoSend, .invalidOrNotSupported:
XCTFail("Parsed as wrong EIP 681 type")
}
}
wait(for: [expectation], timeout: 20)
}
func testParseInvalidNativeCryptoSend() {
guard let qrCodeValue = AddressOrEip681Parser.from(string: "ethereum:0xfb6916095ca1df60bb79Ce92ce3ea74c37c5d359/foo?value=2.014e18") else { return XCTFail("Can't parse EIP 681") }
let expectation = self.expectation(description: "Promise resolves")
switch qrCodeValue {
case .address:
XCTFail("Can't parse EIP 681")
case .eip681(let protocolName, let address, let functionName, let params):
Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse().done { result in
expectation.fulfill()
switch result {
case .nativeCryptoSend, .erc20Send:
XCTFail("Parsed as wrong EIP 681 type")
case .invalidOrNotSupported:
XCTAssert(true)
}
}.cauterize()
switch Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse() {
case .nativeCryptoSend, .erc20Send:
XCTFail("Parsed as wrong EIP 681 type")
case .invalidOrNotSupported:
XCTAssert(true)
}
}
wait(for: [expectation], timeout: 20)
}
func testParseNativeCryptoSendWithoutValueWithEnsName() {
guard let qrCodeValue = AddressOrEip681Parser.from(string: "ethereum:foo.eth") else { return XCTFail("Can't parse EIP 681") }
let expectation = self.expectation(description: "Promise resolves")
switch qrCodeValue {
case .address:
XCTFail("Can't parse EIP 681")
case .eip681(let protocolName, let address, let functionName, let params):
Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse().done { result in
expectation.fulfill()
switch result {
case .nativeCryptoSend(let chainId, let recipient, let amount):
XCTAssertNil(chainId)
XCTAssertTrue(recipient.stringValue == "foo.eth")
XCTAssertEqual(amount.rawValue, "")
case .erc20Send, .invalidOrNotSupported:
XCTFail("Parsed as wrong EIP 681 type")
}
}.cauterize()
switch Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse() {
case .nativeCryptoSend(let chainId, let recipient, let amount):
XCTAssertNil(chainId)
XCTAssertTrue(recipient.stringValue == "foo.eth")
XCTAssertEqual(amount.rawValue, "")
case .erc20Send, .invalidOrNotSupported:
XCTFail("Parsed as wrong EIP 681 type")
}
}
wait(for: [expectation], timeout: 20)
}
}

@ -28,8 +28,7 @@ class ImportTokenTests: XCTestCase {
XCTAssertNil(tokensDataStore.token(forContract: address, server: server), "Initially token is nil")
let expectation = self.expectation(description: "did resolve erc20 token")
importToken.importToken(for: address, server: server)
.publisher
importToken.importTokenPublisher(for: address, server: server)
.sink(receiveCompletion: { _ in
expectation.fulfill()
}, receiveValue: { token in
@ -54,8 +53,7 @@ class ImportTokenTests: XCTestCase {
let expectation = self.expectation(description: "did resolve erc20 token")
expectation.isInverted = true
importToken.importToken(for: address, server: server)
.publisher
importToken.importTokenPublisher(for: address, server: server)
.sink(receiveCompletion: { _ in
expectation.fulfill()
}, receiveValue: { _ in
@ -79,8 +77,7 @@ class ImportTokenTests: XCTestCase {
let expectation = self.expectation(description: "did resolve erc20 token")
contractDataFetcher.contractData[.init(address: address, server: server)] = .fungibleTokenComplete(name: "erc20", symbol: "erc20", decimals: 6, value: .init("1"), tokenType: .erc20)
importToken.importToken(for: address, server: server)
.publisher
importToken.importTokenPublisher(for: address, server: server)
.sink(receiveCompletion: { _ in
expectation.fulfill()
}, receiveValue: { token in
@ -112,12 +109,22 @@ class TransactionTypeFromQrCodeTests: XCTestCase {
let etherToken = Token(contract: Constants.nativeCryptoAddressInDatabase, server: .main, name: "Ether", symbol: "eth", decimals: 18, type: .nativeCryptocurrency)
//NOTE: make sure we have a eth token, base impl resolves it automatically, for test does it manually
tokensDataStore.addOrUpdate(with: [.init(etherToken)])
tokensDataStore.addOrUpdate(with: [
.init(etherToken)
])
provider.buildTransactionType(qrCode: qrCode)
.sink { result in
guard case .success(let transactionType) = result else { fatalError() }
guard case .nativeCryptocurrency(let token, let destination, let amount) = transactionType else { fatalError() }
guard case .success(let transactionType) = result else {
XCTFail("failure as resolved: \(result)")
expectation.fulfill()
return
}
guard case .nativeCryptocurrency(let token, let destination, let amount) = transactionType else {
XCTFail("failure as resolved: \(transactionType)")
expectation.fulfill()
return
}
XCTAssertEqual(token, etherToken)
XCTAssertNotNil(destination)
XCTAssertNotNil(amount)
@ -138,6 +145,7 @@ class TransactionTypeFromQrCodeTests: XCTestCase {
//NOTE: make sure we have a eth token, base impl resolves it automatically, for test does it manually
tokensDataStore.addOrUpdate(with: [.init(erc20Token)])
transactionTypeSupportable.transactionType = .erc20Token(erc20Token, destination: nil, amount: .notSet)
provider.buildTransactionType(qrCode: qrCode)

@ -18,6 +18,7 @@ class SendViewControllerTests: XCTestCase {
return .nativeCryptocurrency(token, destination: nil, amount: .notSet)
}()
private let dep = WalletDataProcessingPipeline.make(wallet: .make(), server: .main)
let contractDataFetcher = FakeContractDataFetcher()
func testNativeCryptocurrencyAllFundsValueSpanish() {
let vc = createSendViewControllerAndSetLocale(locale: .spanish, transactionType: nativeCryptocurrencyTransactionType)
@ -160,20 +161,25 @@ class SendViewControllerTests: XCTestCase {
func testScanEip681QrCodeEnglish() {
let token = Token(contract: AlphaWallet.Address.make(), server: .main, decimals: 18, value: "0", type: .erc20)
dep.tokensService.addOrUpdateTokenTestsOnly(token: token)
let vc = createSendViewControllerAndSetLocale(locale: .english, transactionType: .erc20Token(token, destination: .none, amount: .amount(1.34)))
dep.tokensService.setBalanceTestsOnly(balance: .init(value: BigInt("2020224719101120")), for: token)
XCTAssertEqual(vc.amountTextField.value, "1.34")
let address = AlphaWallet.Address(string: "0xbc8dafeaca658ae0857c80d8aa6de4d487577c63")!
let server = RPCServer.main
contractDataFetcher.contractData[.init(address: address, server: server)] = .fungibleTokenComplete(name: "erc20", symbol: "erc20", decimals: 18, value: .zero, tokenType: .erc20)
let qrCode = "aw.app/ethereum:0xbc8dafeaca658ae0857c80d8aa6de4d487577c63@1?value=1e19"
vc.didScanQRCode(qrCode)
let expectation = self.expectation(description: "did update token balance expectation")
let destination = AlphaWallet.Address(string: "0xbc8dafeaca658ae0857c80d8aa6de4d487577c63").flatMap { AddressOrEnsName(address: $0) }
XCTAssertEqual(vc.viewModel.latestQrCode, qrCode)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
XCTAssertEqual(vc.viewModel.latestQrCode, qrCode)
switch vc.viewModel.scanQrCodeLatest {
case .success(let transactionType):
XCTAssertEqual(transactionType.amount, .amount(10))
@ -201,10 +207,23 @@ class SendViewControllerTests: XCTestCase {
dep.tokensService.setBalanceTestsOnly(balance: .init(value: BigInt("2020224719101120")), for: token)
XCTAssertEqual(vc.amountTextField.value, "1,34")
let address = AlphaWallet.Address(string: "0xbc8dafeaca658ae0857c80d8aa6de4d487577c63")!
let server = RPCServer.main
contractDataFetcher.contractData[.init(address: address, server: server)] = .fungibleTokenComplete(name: "erc20", symbol: "erc20", decimals: 18, value: .zero, tokenType: .erc20)
vc.didScanQRCode("aw.app/ethereum:0xbc8dafeaca658ae0857c80d8aa6de4d487577c63@1?value=1e17")
let expectation = self.expectation(description: "did update token balance expectation")
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
switch vc.viewModel.scanQrCodeLatest {
case .success(let transactionType):
XCTAssertEqual(transactionType.amount, .amount(0.1))
case .failure(let e):
XCTFail(e.description)
case .none:
XCTFail()
}
XCTAssertEqual(vc.amountTextField.value, "0,1")
expectation.fulfill()
}
@ -237,7 +256,8 @@ class SendViewControllerTests: XCTestCase {
private func createSendViewControllerAndSetLocale(locale: AppLocale, transactionType: TransactionType) -> SendViewController {
Config.setLocale(locale)
let viewModel = SendViewModel(transactionType: transactionType, session: dep.sessionsProvider.session(for: .main)!, tokensService: dep.pipeline, importToken: dep.importToken)
let importToken = ImportToken.make(tokensDataStore: dep.tokensDataStore, contractDataFetcher: contractDataFetcher)
let viewModel = SendViewModel(transactionType: transactionType, session: dep.sessionsProvider.session(for: .main)!, tokensService: dep.pipeline, importToken: importToken)
return SendViewController(viewModel: viewModel, domainResolutionService: FakeDomainResolutionService())
}
}

@ -72,6 +72,7 @@ extension WalletDataProcessingPipeline {
let dep = AppCoordinator.WalletDependencies(
activitiesPipeLine: activitiesPipeLine,
transactionsDataStore: transactionsDataStore,
tokensDataStore: tokensDataStore,
importToken: importToken,
tokensService: tokensService,
pipeline: pipeline,

@ -63,7 +63,7 @@ public struct Eip681Parser {
private let decimalParser = DecimalParser()
//https://github.com/ethereum/EIPs/blob/master/EIPS/eip-681.md
public func parse() -> Promise<Eip681Type> {
public func parse() -> Eip681Type {
let chainId = params["chainId"].flatMap { Int($0) }
if functionName == "transfer", let contract = address.contract {
let recipient = params["address"].flatMap({ AddressOrEnsName(string: $0) })
@ -76,7 +76,7 @@ public struct Eip681Parser {
} else {
amount = ""
}
return .value(.erc20Send(contract: contract, server: chainId.flatMap { .init(chainID: $0) }, recipient: recipient, amount: .uint256(amount, eNotation: eNotation)))
return .erc20Send(contract: contract, server: chainId.flatMap { .init(chainID: $0) }, recipient: recipient, amount: .uint256(amount, eNotation: eNotation))
} else if functionName == nil {
//TODO UNITS is either optional or "ETH" for native crypto sends. If it's not provided, we treat it as something like 3.14e18, but it also can be like 1
let amount: String
@ -85,9 +85,9 @@ public struct Eip681Parser {
} else {
amount = ""
}
return .value(.nativeCryptoSend(server: chainId.flatMap { .init(chainID: $0) }, recipient: address, amount: .ether(amount)))
return .nativeCryptoSend(server: chainId.flatMap { .init(chainID: $0) }, recipient: address, amount: .ether(amount))
} else {
return .value(.invalidOrNotSupported)
return .invalidOrNotSupported
}
}

@ -28,47 +28,43 @@ public final class Eip681UrlResolver {
self.missingRPCServerStrategy = missingRPCServerStrategy
}
@discardableResult public func resolvePublisher(url: URL) -> AnyPublisher<Eip681UrlResolver.Resolution, CheckEIP681Error> {
return resolve(url: url).publisher
.receive(on: RunLoop.main)
.mapError { return $0.embedded as? CheckEIP681Error ?? .embeded(error: $0.embedded) }
.eraseToAnyPublisher()
}
@discardableResult public func resolve(url: URL) -> Promise<Eip681UrlResolver.Resolution> {
@discardableResult public func resolve(url: URL) -> AnyPublisher<Eip681UrlResolver.Resolution, CheckEIP681Error> {
switch AddressOrEip681Parser.from(string: url.absoluteString) {
case .address(let address):
return .value(.address(address))
return .just(.address(address))
case .eip681(let protocolName, let address, let functionName, let params):
return resolve(protocolName: protocolName, address: address, functionName: functionName, params: params)
case .none:
return .init(error: CheckEIP681Error.notEIP681)
return .fail(CheckEIP681Error.notEIP681)
}
}
@discardableResult public func resolve(protocolName: String, address: AddressOrEnsName, functionName: String?, params: [String: String]) -> Promise<Eip681UrlResolver.Resolution> {
Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params)
.parse()
.then { [importToken] result -> Promise<Eip681UrlResolver.Resolution> in
@discardableResult public func resolve(protocolName: String, address: AddressOrEnsName, functionName: String?, params: [String: String]) -> AnyPublisher<Eip681UrlResolver.Resolution, CheckEIP681Error> {
return Just(protocolName)
.setFailureType(to: CheckEIP681Error.self)
.map { protocolName in
Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse()
}.flatMap { [importToken] result -> AnyPublisher<Eip681UrlResolver.Resolution, CheckEIP681Error> in
guard let (contract: contract, customServer, recipient, amount) = result.parameters else {
return .init(error: CheckEIP681Error.parameterInvalid)
return .fail(CheckEIP681Error.parameterInvalid)
}
guard let server = self.serverFromEip681LinkOrDefault(customServer) else {
return .init(error: CheckEIP681Error.missingRpcServer)
return .fail(CheckEIP681Error.missingRpcServer)
}
return importToken
.importToken(for: contract, server: server)
.map { token -> Eip681UrlResolver.Resolution in
.importTokenPublisher(for: contract, server: server)
.mapError { CheckEIP681Error.embeded(error: $0) }
.flatMap { token -> AnyPublisher<Eip681UrlResolver.Resolution, CheckEIP681Error> in
switch token.type {
case .erc20, .nativeCryptocurrency:
let transactionType = Eip681UrlResolver.buildFungibleTransactionType(token, recipient: recipient, amount: amount)
return .transaction(transactionType, token: token)
return .just(.transaction(transactionType, token: token))
case .erc1155, .erc721, .erc721ForTickets, .erc875:
throw CheckEIP681Error.tokenTypeNotSupported
return .fail(CheckEIP681Error.tokenTypeNotSupported)
}
}
}
}.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
private func serverFromEip681LinkOrDefault(_ serverInLink: RPCServer?) -> RPCServer? {

@ -13,10 +13,11 @@ import BigInt
public protocol TokenImportable {
func importToken(ercToken: ErcToken, shouldUpdateBalance: Bool) -> Token
func importToken(for contract: AlphaWallet.Address, server: RPCServer, onlyIfThereIsABalance: Bool) -> Promise<Token>
func importTokenPublisher(for contract: AlphaWallet.Address, server: RPCServer, onlyIfThereIsABalance: Bool) -> AnyPublisher<Token, ImportToken.ImportTokenError>
}
public protocol TokenOrContractFetchable: ContractDataFetchable {
func fetchTokenOrContract(for contract: AlphaWallet.Address, server: RPCServer, onlyIfThereIsABalance: Bool) -> Promise<TokenOrContract>
func fetchTokenOrContractPublisher(for contract: AlphaWallet.Address, server: RPCServer, onlyIfThereIsABalance: Bool) -> AnyPublisher<TokenOrContract, ImportToken.ImportTokenError>
}
//NOTE: actually its internal, public for tests
@ -34,7 +35,11 @@ public final class ContractDataFetcher: ContractDataFetchable {
private let reachability: ReachabilityManagerProtocol
private let sessionProvider: SessionsProvider
public init(sessionProvider: SessionsProvider, assetDefinitionStore: AssetDefinitionStore, analytics: AnalyticsLogger, reachability: ReachabilityManagerProtocol) {
public init(sessionProvider: SessionsProvider,
assetDefinitionStore: AssetDefinitionStore,
analytics: AnalyticsLogger,
reachability: ReachabilityManagerProtocol) {
self.assetDefinitionStore = assetDefinitionStore
self.sessionProvider = sessionProvider
self.analytics = analytics
@ -52,7 +57,7 @@ public final class ContractDataFetcher: ContractDataFetchable {
}
final public class ImportToken: TokenImportable, TokenOrContractFetchable {
enum ImportTokenError: Error {
public enum ImportTokenError: Error {
case nativeCryptoNotSupported
case serverIsDisabled
case zeroBalanceDetected
@ -65,6 +70,7 @@ final public class ImportToken: TokenImportable, TokenOrContractFetchable {
private var cancelable = Set<AnyCancellable>()
private let queue = DispatchQueue(label: "org.alphawallet.swift.importToken")
private var inFlightPromises: [String: Promise<TokenOrContract>] = [:]
private var inFlightPublishers: [String: AnyPublisher<TokenOrContract, ImportTokenError>] = [:]
private let reachability = ReachabilityManager()
public init(tokensDataStore: TokensDataStore, contractDataFetcher: ContractDataFetchable) {
@ -77,11 +83,6 @@ final public class ImportToken: TokenImportable, TokenOrContractFetchable {
firstly {
.value(server)
}.then(on: queue, { [queue, tokensDataStore] server -> Promise<Token> in
//Useful to check because we are/might action-only TokenScripts for native crypto currency
guard contract != Constants.nativeCryptoAddressInDatabase else {
return .init(error: ImportTokenError.nativeCryptoNotSupported)
}
if let token = tokensDataStore.token(forContract: contract, server: server) {
return .value(token)
} else {
@ -98,6 +99,30 @@ final public class ImportToken: TokenImportable, TokenOrContractFetchable {
})
}
public func importTokenPublisher(for contract: AlphaWallet.Address, server: RPCServer, onlyIfThereIsABalance: Bool = false) -> AnyPublisher<Token, ImportTokenError> {
return Just(server)
.receive(on: queue)
.setFailureType(to: ImportTokenError.self)
.flatMap { [tokensDataStore, queue] server -> AnyPublisher<Token, ImportTokenError> in
if let token = tokensDataStore.token(forContract: contract, server: server) {
return .just(token)
} else {
return self.fetchTokenOrContractPublisher(for: contract, server: server, onlyIfThereIsABalance: onlyIfThereIsABalance)
.flatMap { tokenOrContract -> AnyPublisher<Token, ImportTokenError> in
//FIXME: looks like blocking access to realm doesn't work well, after adding a new token and retrieving its value from bd, returns nil, adding delay in 1 sec to return a new token.
if let token = tokensDataStore.addOrUpdate(tokensOrContracts: [tokenOrContract]).first {
return .just(token)
.delay(for: .seconds(1), scheduler: queue)
.eraseToAnyPublisher()
} else {
return .fail(ImportTokenError.notContractOrFailed(tokenOrContract))
}
}.eraseToAnyPublisher()
}
}.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
public func importToken(ercToken: ErcToken, shouldUpdateBalance: Bool = true) -> Token {
let tokens = tokensDataStore.addOrUpdate(with: [.add(ercToken: ercToken, shouldUpdateBalance: shouldUpdateBalance)])
@ -108,12 +133,17 @@ final public class ImportToken: TokenImportable, TokenOrContractFetchable {
contractDataFetcher.fetchContractData(for: contract, server: server, completion: completion)
}
public func fetchTokenOrContract(for contract: AlphaWallet.Address, server: RPCServer, onlyIfThereIsABalance: Bool = false) -> Promise<TokenOrContract> {
private func fetchTokenOrContract(for contract: AlphaWallet.Address, server: RPCServer, onlyIfThereIsABalance: Bool = false) -> Promise<TokenOrContract> {
firstly {
.value(contract)
}.then(on: queue, { [queue] contract -> Promise<TokenOrContract> in
let key = "\(contract.hashValue)-\(onlyIfThereIsABalance)-\(server)"
//Useful to check because we are/might action-only TokenScripts for native crypto currency
guard contract != Constants.nativeCryptoAddressInDatabase else {
return .init(error: ImportTokenError.nativeCryptoNotSupported)
}
if let promise = self.inFlightPromises[key] {
return promise
} else {
@ -163,4 +193,66 @@ final public class ImportToken: TokenImportable, TokenOrContractFetchable {
}
})
}
public func fetchTokenOrContractPublisher(for contract: AlphaWallet.Address, server: RPCServer, onlyIfThereIsABalance: Bool = false) -> AnyPublisher<TokenOrContract, ImportTokenError> {
Just(contract)
.receive(on: queue)
.setFailureType(to: ImportTokenError.self)
.flatMap { [queue] contract -> AnyPublisher<TokenOrContract, ImportTokenError> in
//Useful to check because we are/might action-only TokenScripts for native crypto currency
guard contract != Constants.nativeCryptoAddressInDatabase else {
return .fail(ImportTokenError.nativeCryptoNotSupported)
}
let key = "\(contract.hashValue)-\(onlyIfThereIsABalance)-\(server)"
if let publisher = self.inFlightPublishers[key] {
return publisher
} else {
let publisher = Future<TokenOrContract, ImportTokenError> { seal in
self.fetchContractData(for: contract, server: server) { data in
switch data {
case .name, .symbol, .balance, .decimals:
break
case .nonFungibleTokenComplete(let name, let symbol, let balance, let tokenType):
guard !onlyIfThereIsABalance || (onlyIfThereIsABalance && !balance.isEmpty) else {
seal(.failure(ImportTokenError.zeroBalanceDetected))
return
}
let ercToken = ErcToken(contract: contract, server: server, name: name, symbol: symbol, decimals: 0, type: tokenType, value: "0", balance: balance)
seal(.success(.ercToken(ercToken)))
case .fungibleTokenComplete(let name, let symbol, let decimals, let value, let tokenType):
//NOTE: we want to make get balance for fungible token, fetching for token from data source might be unusefull as token hasn't created yes (when we fetch for a new contract) so we fetch tokens balance sync on `getFungibleBalanceQueue` and return result on `.main` queue
// one more additional network call, shouldn't be complex.
guard !onlyIfThereIsABalance || (onlyIfThereIsABalance && (value != .zero)) else {
seal(.failure(ImportTokenError.zeroBalanceDetected))
return
}
let ercToken = ErcToken(contract: contract, server: server, name: name, symbol: symbol, decimals: decimals, type: tokenType, value: value, balance: .balance(["0"]))
seal(.success(.ercToken(ercToken)))
case .delegateTokenComplete:
seal(.success(.delegateContracts([AddressAndRPCServer(address: contract, server: server)])))
case .failed(let networkReachable, let error):
//Receives first received error, e.g name, symbol, token type, decimals
//TODO: maybe its need to handle some cases of error here?
if networkReachable {
seal(.success(.deletedContracts([AddressAndRPCServer(address: contract, server: server)])))
} else {
seal(.failure(ImportTokenError.internal(error: error)))
}
}
}
}.receive(on: queue)
.handleEvents(receiveCompletion: { _ in self.inFlightPublishers[key] = nil })
.eraseToAnyPublisher()
self.inFlightPublishers[key] = publisher
return publisher
}
}.eraseToAnyPublisher()
}
}

@ -120,13 +120,12 @@ public class SingleChainTokensAutodetector: NSObject, TokensAutodetector {
return autoDetectTransactedContractsImpl(wallet: wallet, erc20: erc20, server: server)
.flatMap { [importToken, queue] detectedContracts -> AnyPublisher<[TokenOrContract], Never> in
let promises = self.contractsForTransactedTokens(detectedContracts: detectedContracts, forServer: server)
.map { importToken.fetchTokenOrContract(for: $0, server: server, onlyIfThereIsABalance: false) }
let publishers = self.contractsForTransactedTokens(detectedContracts: detectedContracts, forServer: server)
.map { importToken.fetchTokenOrContractPublisher(for: $0, server: server, onlyIfThereIsABalance: false).mapToResult() }
return when(resolved: promises)
.map(on: queue, { $0.compactMap { $0.optionalValue } })
.publisher
.replaceError(with: [])
return Publishers.MergeMany(publishers).collect()
.map { $0.compactMap { try? $0.get() } }
.receive(on: queue)
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
@ -165,13 +164,12 @@ extension SingleChainTokensAutodetector: AutoDetectTransactedTokensOperationDele
extension SingleChainTokensAutodetector: AutoDetectTokensOperationDelegate {
func autoDetectTokensImpl(withContracts contractsToDetect: [ContractToImport]) -> AnyPublisher<[TokenOrContract], Never> {
let promises = contractsToAutodetectTokens(contractsToDetect: contractsToDetect)
.map { importToken.fetchTokenOrContract(for: $0.contract, server: $0.server, onlyIfThereIsABalance: $0.onlyIfThereIsABalance) }
let publishers = contractsToAutodetectTokens(contractsToDetect: contractsToDetect)
.map { importToken.fetchTokenOrContractPublisher(for: $0.contract, server: $0.server, onlyIfThereIsABalance: $0.onlyIfThereIsABalance).mapToResult() }
return when(resolved: promises)
.map(on: queue, { $0.compactMap { $0.optionalValue } })
.publisher
.replaceError(with: [])
return Publishers.MergeMany(publishers).collect()
.map { $0.compactMap { try? $0.get() } }
.receive(on: queue)
.eraseToAnyPublisher()
}

Loading…
Cancel
Save