Extract alamofire requests from fetch xml file coordinator #5955

pull/5955/head
Krypto Pank 2 years ago
parent 5dbf119cc1
commit a377023685
  1. 5
      AlphaWallet/AppCoordinator.swift
  2. 202
      AlphaWallet/Market/Coordinators/ImportMagicLinkCoordinator.swift
  3. 20
      AlphaWalletTests/Coordinators/ActiveWalletViewTests.swift
  4. 4
      AlphaWalletTests/Coordinators/SendCoordinatorTests.swift
  5. 2
      AlphaWalletTests/Coordinators/TokensCoordinatorTests.swift
  6. 2
      AlphaWalletTests/Factories/FakeMultiWalletBalanceService.swift
  7. 6
      AlphaWalletTests/Settings/ConfigTests.swift
  8. 8
      AlphaWalletTests/TokenScriptClient/AssetDefinitionStoreTests.swift
  9. 6
      AlphaWalletTests/TokenScriptClient/XMLHandlerTest.swift
  10. 4
      AlphaWalletTests/Tokens/Helpers/TokenAdaptorTest.swift
  11. 10
      AlphaWalletTests/ViewControllers/PaymentCoordinatorTests.swift
  12. 8
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Settings/Types/Constants.swift
  13. 196
      modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/Models/AssetDefinitionStore.swift
  14. 200
      modules/AlphaWalletFoundation/AlphaWalletFoundation/UniversalLink/ImportMagicLinkNetworkService.swift

@ -21,7 +21,7 @@ class AppCoordinator: NSObject, Coordinator {
private let legacyFileBasedKeystore: LegacyFileBasedKeystore
private lazy var lock: Lock = SecuredLock(securedStorage: securedStorage)
private var keystore: Keystore
private let assetDefinitionStore = AssetDefinitionStore(baseTokenScriptFiles: TokenScript.baseTokenScriptFiles)
private lazy var assetDefinitionStore = AssetDefinitionStore(baseTokenScriptFiles: TokenScript.baseTokenScriptFiles, networkService: networkService)
private let window: UIWindow
private var appTracker = AppTracker()
//TODO rename and replace type? Not Initializer but similar as of writing
@ -603,7 +603,8 @@ extension AppCoordinator: UniversalLinkServiceDelegate {
assetDefinitionStore: assetDefinitionStore,
url: url,
keystore: keystore,
tokensService: resolver.service)
tokensService: resolver.service,
networkService: networkService)
coordinator.delegate = self
let handled = coordinator.start(url: url)

@ -1,7 +1,6 @@
// Copyright © 2018 Stormbird PTE. LTD.
import Foundation
import Alamofire
import BigInt
import PromiseKit
import Combine
@ -15,10 +14,11 @@ protocol ImportMagicLinkCoordinatorDelegate: class, CanOpenURL {
func didImported(contract: AlphaWallet.Address, in coordinator: ImportMagicLinkCoordinator)
}
//TODO: Extract all logic to separate class
// swiftlint:disable type_body_length
class ImportMagicLinkCoordinator: Coordinator {
private enum TransactionType {
case freeTransfer(query: String, parameters: Parameters)
case freeTransfer(request: ImportMagicLinkNetworkService.FreeTransferRequest)
case paid(signedOrder: SignedOrder, token: Token)
}
@ -61,12 +61,14 @@ class ImportMagicLinkCoordinator: Coordinator {
private var cryptoToFiatRateWhenNotEnoughEthForPaidImportCancelable: AnyCancellable?
private var balanceWhenHandlePaidImportsCancelable: AnyCancellable?
private let session: WalletSession
private let networkService: ImportMagicLinkNetworkService
init(analytics: AnalyticsLogger, session: WalletSession, config: Config, assetDefinitionStore: AssetDefinitionStore, url: URL, keystore: Keystore, tokensService: TokenViewModelState & TokenProvidable) {
init(analytics: AnalyticsLogger, session: WalletSession, config: Config, assetDefinitionStore: AssetDefinitionStore, url: URL, keystore: Keystore, tokensService: TokenViewModelState & TokenProvidable, networkService: NetworkService) {
self.analytics = analytics
self.session = session
self.config = config
self.assetDefinitionStore = assetDefinitionStore
self.networkService = ImportMagicLinkNetworkService(networkService: networkService)
self.url = url
self.keystore = keystore
self.tokensService = tokensService
@ -76,64 +78,6 @@ class ImportMagicLinkCoordinator: Coordinator {
return handleMagicLink(url: url)
}
private func createHTTPParametersForCurrencyLinksToPaymentServer(
signedOrder: SignedOrder,
recipient: AlphaWallet.Address
) -> (Parameters, String) {
let signature = signedOrder.signature.drop0x
let parameters: Parameters = [
"prefix": Constants.xdaiDropPrefix,
"recipient": recipient.eip55String,
"amount": signedOrder.order.count.description,
"expiry": signedOrder.order.expiry.description,
"nonce": signedOrder.order.nonce,
"v": signature.substring(from: 128),
//Use string interpolation instead of concatenation to speed up build time. 160ms -> <100ms, as of Xcode 11.7
"r": "0x\(signature.substring(with: Range(uncheckedBounds: (0, 64))))",
"s": "0x\(signature.substring(with: Range(uncheckedBounds: (64, 128))))",
"networkId": server.chainID.description,
"contractAddress": signedOrder.order.contractAddress
]
return (parameters, Constants.currencyDropServer)
}
private func createHTTPParametersForNormalLinksToPaymentServer(
signedOrder: SignedOrder,
isForTransfer: Bool
) -> (Parameters, String) {
let query: String
let signature = signedOrder.signature.drop0x
let indices = signedOrder.order.indices
let indicesStringEncoded = stringEncodeIndices(indices)
let tokenIdsEncoded = stringEncodeTokenIds(signedOrder.order.tokenIds)
var parameters: Parameters = [
"address": wallet.address,
"contractAddress": signedOrder.order.contractAddress,
"indices": indicesStringEncoded,
"tokenIds": tokenIdsEncoded ?? "",
"price": signedOrder.order.price.description,
"expiry": signedOrder.order.expiry.description,
"v": signature.substring(from: 128),
"r": "0x" + signature.substring(with: Range(uncheckedBounds: (0, 64))),
"s": "0x" + signature.substring(with: Range(uncheckedBounds: (64, 128))),
"networkId": server.chainID.description,
]
if isForTransfer {
parameters.removeValue(forKey: "price")
}
if signedOrder.order.spawnable {
parameters.removeValue(forKey: "indices")
query = Constants.paymentServerSpawnable
} else {
parameters.removeValue(forKey: "tokenIds")
query = Constants.paymentServer
}
return (parameters, query)
}
@discardableResult private func handlePaidImportsImpl(signedOrder: SignedOrder) -> Bool {
guard isShowingImportUserInterface else { return false }
@ -148,8 +92,7 @@ class ImportMagicLinkCoordinator: Coordinator {
value: BigInt(signedOrder.order.price),
isCustom: true,
isDisabled: false,
type: .erc875
)
type: .erc875)
transactionType = .paid(signedOrder: signedOrder, token: token)
let ethCost = convert(ethCost: signedOrder.order.price)
@ -174,31 +117,18 @@ class ImportMagicLinkCoordinator: Coordinator {
@discardableResult private func usePaymentServerForFreeTransferLinks(signedOrder: SignedOrder) -> Bool {
guard isShowingImportUserInterface else { return false }
guard let (parameters, query) = getParametersAndQuery(signedOrder: signedOrder) else { return false }
transactionType = .freeTransfer(query: query, parameters: parameters)
let request = ImportMagicLinkNetworkService.FreeTransferRequest(signedOrder: signedOrder, wallet: wallet, server: server)
transactionType = .freeTransfer(request: request)
promptImportUniversalLink(cost: .free)
return true
}
private func getParametersAndQuery(signedOrder: SignedOrder) -> (Parameters, String)? {
switch signedOrder.order.nativeCurrencyDrop {
case true:
return createHTTPParametersForCurrencyLinksToPaymentServer(
signedOrder: signedOrder,
recipient: wallet.address
)
case false:
return createHTTPParametersForNormalLinksToPaymentServer(
signedOrder: signedOrder,
isForTransfer: true
)
}
return true
}
func completeOrderHandling(signedOrder: SignedOrder) {
let requiresPaymaster = requiresPaymasterForCurrencyLinks(signedOrder: signedOrder)
if signedOrder.order.price == 0 {
checkPaymentServerSupportsContract(contractAddress: signedOrder.order.contractAddress) { supported in
networkService.checkPaymentServerSupportsContract(contractAddress: signedOrder.order.contractAddress)
.sink { supported in
//Currency links on mainnet/classic/xdai without a paymaster should be rejected for security reasons (front running)
guard supported || !requiresPaymaster else {
self.showImportError(errorMessage: R.string.localizable.aClaimTokenFailedServerDown())
@ -209,7 +139,7 @@ class ImportMagicLinkCoordinator: Coordinator {
} else {
self.handlePaidImports(signedOrder: signedOrder)
}
}
}.store(in: &cancelable)
} else {
self.handlePaidImports(signedOrder: signedOrder)
}
@ -223,12 +153,10 @@ class ImportMagicLinkCoordinator: Coordinator {
private func handleSpawnableLink(signedOrder: SignedOrder, tokens: [BigUInt]) {
let tokenStrings: [String] = tokens.map { String($0, radix: 16) }
self.makeTokenHolder(
tokenStrings,
signedOrder.order.contractAddress
)
self.makeTokenHolder(tokenStrings, signedOrder.order.contractAddress)
completeOrderHandling(signedOrder: signedOrder)
}
private var cancelable = Set<AnyCancellable>()
private func handleNativeCurrencyDrop(signedOrder: SignedOrder) {
let amt: Decimal
@ -247,21 +175,18 @@ class ImportMagicLinkCoordinator: Coordinator {
name: server.symbol,
symbol: "",
status: .available,
values: [:]
)
self.tokenHolder = TokenHolder(
tokens: [token],
contractAddress: signedOrder.order.contractAddress,
hasAssetDefinition: false
)
values: [:])
self.tokenHolder = TokenHolder(tokens: [token], contractAddress: signedOrder.order.contractAddress, hasAssetDefinition: false)
let r = signedOrder.signature.substring(with: Range(uncheckedBounds: (2, 66)))
checkIfLinkClaimed(r: r) { claimed in
networkService.checkIfLinkClaimed(r: r)
.sink(receiveValue: { claimed in
if claimed {
self.showImportError(errorMessage: R.string.localizable.aClaimTokenLinkAlreadyRedeemed())
} else {
self.completeOrderHandling(signedOrder: signedOrder)
}
}
}).store(in: &cancelable)
}
private func handleNormalLinks(signedOrder: SignedOrder, recoverAddress: AlphaWallet.Address, contractAsAddress: AlphaWallet.Address) {
@ -276,10 +201,7 @@ class ImportMagicLinkCoordinator: Coordinator {
return
}
strongSelf.makeTokenHolder(
filteredTokens,
signedOrder.order.contractAddress
)
strongSelf.makeTokenHolder(filteredTokens, signedOrder.order.contractAddress)
strongSelf.completeOrderHandling(signedOrder: signedOrder)
}).catch({ [weak self] _ in
@ -333,43 +255,6 @@ class ImportMagicLinkCoordinator: Coordinator {
return true
}
private func checkPaymentServerSupportsContract(contractAddress: AlphaWallet.Address, completionHandler: @escaping (Bool) -> Void) {
let parameters: Parameters = [
"contractAddress": contractAddress.eip55String
]
Alamofire.request(
Constants.paymentServerSupportsContractEndPoint,
method: .get,
parameters: parameters
).responseJSON { result in
if let response = result.response {
let supported = response.statusCode >= 200 && response.statusCode <= 299
completionHandler(supported)
} else {
completionHandler(false)
}
}
}
private func checkIfLinkClaimed(r: String, completionHandler: @escaping (Bool) -> Void) {
let parameters: Parameters = [ "r": r ]
Alamofire.request(
Constants.paymentServerClaimedToken,
method: .get,
parameters: parameters
).responseJSON { result in
if let response = result.response {
if response.statusCode == 208 || response.statusCode > 299 {
completionHandler(true)
} else {
completionHandler(false)
}
} else {
completionHandler(false)
}
}
}
private func handlePaidImports(signedOrder: SignedOrder) {
let etherToken: Token = MultipleChainsTokensDataStore.functional.etherToken(forServer: server)
balanceWhenHandlePaidImportsCancelable = tokensService.tokenViewModelPublisher(for: etherToken)
@ -415,15 +300,6 @@ class ImportMagicLinkCoordinator: Coordinator {
}
}
private func stringEncodeIndices(_ indices: [UInt16]) -> String {
return indices.map(String.init).joined(separator: ",")
}
private func stringEncodeTokenIds(_ tokenIds: [BigUInt]?) -> String? {
guard let tokens = tokenIds else { return nil }
return tokens.map({ $0.serialize().hexString }).joined(separator: ",")
}
private func checkERC875TokensAreAvailable(indices: [UInt16], balance: [String]) -> [String] {
var filteredTokens = [String]()
if balance.count < indices.count {
@ -460,6 +336,7 @@ class ImportMagicLinkCoordinator: Coordinator {
let getContractName = session.tokenProvider.getContractName(for: contractAddress)
let getContractSymbol = session.tokenProvider.getContractSymbol(for: contractAddress)
let getTokenType = session.tokenProvider.getTokenType(for: contractAddress)
firstly {
when(fulfilled: getContractName, getContractSymbol, getTokenType)
}.done { name, symbol, type in
@ -481,11 +358,7 @@ class ImportMagicLinkCoordinator: Coordinator {
tokens.append(token)
}
}
tokenHolder = TokenHolder(
tokens: tokens,
contractAddress: contractAddress,
hasAssetDefinition: xmlHandler.hasAssetDefinition
)
tokenHolder = TokenHolder(tokens: tokens, contractAddress: contractAddress, hasAssetDefinition: xmlHandler.hasAssetDefinition)
}
private func preparingToImportUniversalLink() {
@ -550,35 +423,24 @@ class ImportMagicLinkCoordinator: Coordinator {
}
}
private func importFreeTransfer(query: String, parameters: Parameters) {
private func importFreeTransfer(request: ImportMagicLinkNetworkService.FreeTransferRequest) {
updateImportTokenController(with: .processing)
Alamofire.request(
query,
method: .post,
parameters: parameters
).responseJSON { [weak self] result in
networkService.freeTransfer(request: request)
.sink { [weak self] successful in
guard let strongSelf = self else { return }
var successful = false //need to set this to false by default else it will allow no connections to be considered successful etc
//401 code will be given if signature is invalid on the server
if let response = result.response {
if response.statusCode < 300 {
successful = true
if let contract = parameters["contractAddress"] as? AlphaWallet.Address {
strongSelf.delegate?.didImported(contract: contract, in: strongSelf)
}
}
}
strongSelf.delegate?.didImported(contract: request.contractAddress, in: strongSelf)
guard let vc = strongSelf.importTokenViewController, case .ready = vc.state else { return }
// TODO handle http response
if successful {
strongSelf.showImportSuccessful()
} else {
//TODO Pass in error message
//TODO: Pass in error message
strongSelf.showImportError(errorMessage: R.string.localizable.aClaimTokenFailedTitle())
}
}
}.store(in: &cancelable)
}
private func convert(ethCost: BigUInt, rate: Double) -> (ethCost: Decimal, dollarCost: Decimal) {
@ -603,8 +465,8 @@ extension ImportMagicLinkCoordinator: ImportMagicTokenViewControllerDelegate {
func didPressImport(in viewController: ImportMagicTokenViewController) {
guard let transactionType = transactionType else { return }
switch transactionType {
case .freeTransfer(let query, let parameters):
importFreeTransfer(query: query, parameters: parameters)
case .freeTransfer(let request):
importFreeTransfer(request: request)
case .paid(let signedOrder, let token):
importPaidSignedOrder(signedOrder: signedOrder, token: token)
}

@ -12,6 +12,12 @@ final class FakeNetworkService: NetworkService {
}
}
extension AssetDefinitionStore {
static func make() -> AssetDefinitionStore {
return .init(networkService: FakeNetworkService())
}
}
// swiftlint:disable type_body_length
class ActiveWalletViewTests: XCTestCase {
@ -33,7 +39,7 @@ class ActiveWalletViewTests: XCTestCase {
activitiesPipeLine: dep.activitiesPipeLine,
wallet: wallet,
keystore: keystore,
assetDefinitionStore: AssetDefinitionStore(),
assetDefinitionStore: .make(),
config: config,
analytics: FakeAnalyticsService(),
nftProvider: FakeNftProvider(),
@ -108,7 +114,7 @@ class ActiveWalletViewTests: XCTestCase {
activitiesPipeLine: dep1.activitiesPipeLine,
wallet: account1,
keystore: keystore,
assetDefinitionStore: AssetDefinitionStore(),
assetDefinitionStore: .make(),
config: .make(),
analytics: FakeAnalyticsService(),
nftProvider: FakeNftProvider(),
@ -145,7 +151,7 @@ class ActiveWalletViewTests: XCTestCase {
activitiesPipeLine: dep2.activitiesPipeLine,
wallet: account2,
keystore: keystore,
assetDefinitionStore: AssetDefinitionStore(),
assetDefinitionStore: .make(),
config: .make(),
analytics: FakeAnalyticsService(),
nftProvider: FakeNftProvider(),
@ -191,7 +197,7 @@ class ActiveWalletViewTests: XCTestCase {
activitiesPipeLine: dep.activitiesPipeLine,
wallet: wallet,
keystore: keystore,
assetDefinitionStore: AssetDefinitionStore(),
assetDefinitionStore: .make(),
config: .make(),
analytics: FakeAnalyticsService(),
nftProvider: FakeNftProvider(),
@ -239,7 +245,7 @@ class ActiveWalletViewTests: XCTestCase {
activitiesPipeLine: dep.activitiesPipeLine,
wallet: wallet,
keystore: keystore,
assetDefinitionStore: AssetDefinitionStore(),
assetDefinitionStore: .make(),
config: .make(),
analytics: FakeAnalyticsService(),
nftProvider: FakeNftProvider(),
@ -287,7 +293,7 @@ class ActiveWalletViewTests: XCTestCase {
activitiesPipeLine: dep.activitiesPipeLine,
wallet: wallet,
keystore: keystore,
assetDefinitionStore: AssetDefinitionStore(),
assetDefinitionStore: .make(),
config: .make(),
analytics: FakeAnalyticsService(),
nftProvider: FakeNftProvider(),
@ -353,7 +359,7 @@ class ActiveWalletViewTests: XCTestCase {
activitiesPipeLine: dep.activitiesPipeLine,
wallet: wallet,
keystore: keystore,
assetDefinitionStore: AssetDefinitionStore(),
assetDefinitionStore: .make(),
config: .make(),
analytics: FakeAnalyticsService(),
nftProvider: FakeNftProvider(),

@ -13,7 +13,7 @@ class SendCoordinatorTests: XCTestCase {
session: .make(),
keystore: FakeEtherKeystore(),
tokensService: WalletDataProcessingPipeline.make().pipeline,
assetDefinitionStore: AssetDefinitionStore(),
assetDefinitionStore: .make(),
analytics: FakeAnalyticsService(),
domainResolutionService: FakeDomainResolutionService(),
importToken: ImportToken.make(wallet: .make()),
@ -32,7 +32,7 @@ class SendCoordinatorTests: XCTestCase {
session: .make(),
keystore: FakeEtherKeystore(),
tokensService: WalletDataProcessingPipeline.make().pipeline,
assetDefinitionStore: AssetDefinitionStore(),
assetDefinitionStore: .make(),
analytics: FakeAnalyticsService(),
domainResolutionService: FakeDomainResolutionService(),
importToken: ImportToken.make(wallet: .make()),

@ -23,7 +23,7 @@ class TokensCoordinatorTests: XCTestCase {
sessions: sessions,
keystore: FakeEtherKeystore(),
config: config,
assetDefinitionStore: AssetDefinitionStore(),
assetDefinitionStore: .make(),
promptBackupCoordinator: PromptBackupCoordinator(keystore: FakeEtherKeystore(), wallet: .make(), config: config, analytics: FakeAnalyticsService()),
analytics: FakeAnalyticsService(),
nftProvider: FakeNftProvider(),

@ -32,7 +32,7 @@ final class FakeMultiWalletBalanceService: MultiWalletBalanceService {
let walletDependencyContainer = WalletComponentsFactory(
analytics: FakeAnalyticsService(),
nftProvider: FakeNftProvider(),
assetDefinitionStore: .init(),
assetDefinitionStore: .make(),
coinTickersFetcher: CoinTickersFetcherImpl.make(),
config: .make(),
currencyService: .make(),

@ -17,7 +17,7 @@ extension WalletConnectCoordinator {
domainResolutionService: FakeDomainResolutionService(),
config: .make(),
sessionProvider: sessionProvider,
assetDefinitionStore: AssetDefinitionStore(),
assetDefinitionStore: .make(),
networkService: FakeNetworkService())
}
}
@ -46,7 +46,7 @@ class ConfigTests: XCTestCase {
sessions: sessions,
keystore: FakeEtherKeystore(),
config: config,
assetDefinitionStore: AssetDefinitionStore(),
assetDefinitionStore: .make(),
promptBackupCoordinator: PromptBackupCoordinator(keystore: FakeEtherKeystore(), wallet: .make(), config: config, analytics: FakeAnalyticsService()),
analytics: FakeAnalyticsService(),
nftProvider: FakeNftProvider(),
@ -74,7 +74,7 @@ class ConfigTests: XCTestCase {
sessions: sessions,
keystore: FakeEtherKeystore(),
config: config,
assetDefinitionStore: AssetDefinitionStore(),
assetDefinitionStore: .make(),
promptBackupCoordinator: PromptBackupCoordinator(keystore: FakeEtherKeystore(), wallet: .make(), config: config, analytics: FakeAnalyticsService()),
analytics: FakeAnalyticsService(),
nftProvider: FakeNftProvider(),

@ -8,11 +8,11 @@ import AlphaWalletFoundation
class AssetDefinitionStoreTests: XCTestCase {
func testConvertsModifiedDateToStringForHTTPHeaderIfModifiedSince() {
let date = GeneralisedTime(string: "20230405111234+0000")!.date
XCTAssertEqual(AssetDefinitionStore().string(fromLastModifiedDate: date), "Wed, 05 Apr 2023 11:12:34 GMT")
XCTAssertEqual(AssetDefinitionNetwork.GetXmlFileRequest(url: URL(string: "http://google.com")!, lastModifiedDate: nil).string(fromLastModifiedDate: date), "Wed, 05 Apr 2023 11:12:34 GMT")
}
func testXMLAccess() {
let store = AssetDefinitionStore(backingStore: AssetDefinitionInMemoryBackingStore())
let store = AssetDefinitionStore(backingStore: AssetDefinitionInMemoryBackingStore(), networkService: FakeNetworkService())
let address = AlphaWallet.Address.make()
XCTAssertNil(store[address])
store[address] = "xml1"
@ -21,7 +21,7 @@ class AssetDefinitionStoreTests: XCTestCase {
func testShouldNotCallCompletionBlockWithCacheCaseIfNotAlreadyCached() {
let contractAddress = AlphaWallet.Address.make()
let store = AssetDefinitionStore(backingStore: AssetDefinitionInMemoryBackingStore())
let store = AssetDefinitionStore(backingStore: AssetDefinitionInMemoryBackingStore(), networkService: FakeNetworkService())
let expectation = XCTestExpectation(description: "cached case should not be called")
expectation.isInverted = true
store.fetchXML(forContract: contractAddress, server: nil, useCacheAndFetch: true) { [weak self] result in
@ -38,7 +38,7 @@ class AssetDefinitionStoreTests: XCTestCase {
func testShouldCallCompletionBlockWithCacheCaseIfAlreadyCached() {
let contractAddress = AlphaWallet.Address.ethereumAddress(eip55String: "0x0000000000000000000000000000000000000001")
let store = AssetDefinitionStore(backingStore: AssetDefinitionInMemoryBackingStore())
let store = AssetDefinitionStore(backingStore: AssetDefinitionInMemoryBackingStore(), networkService: FakeNetworkService())
store[contractAddress] = "something"
let expectation = XCTestExpectation(description: "cached case should be called")
store.fetchXML(forContract: contractAddress, server: nil, useCacheAndFetch: true) { [weak self] result in

@ -15,7 +15,7 @@ class XMLHandlerTest: XCTestCase {
let tokenHex = "0x00000000000000000000000000000000fefe5ae99a3000000000000000010001".substring(from: 2)
func testParser() {
let assetDefinitionStore = AssetDefinitionStore()
let assetDefinitionStore = AssetDefinitionStore.make()
let token = XMLHandler(contract: Constants.nullAddress, tokenType: .erc20, assetDefinitionStore: assetDefinitionStore).getToken(
name: "",
symbol: "",
@ -898,7 +898,7 @@ class XMLHandlerTest: XCTestCase {
</ts:token>
"""
let contractAddress = AlphaWallet.Address(string: "0xA66A3F08068174e8F005112A8b2c7A507a822335")!
let store = AssetDefinitionStore(backingStore: AssetDefinitionInMemoryBackingStore())
let store = AssetDefinitionStore(backingStore: AssetDefinitionInMemoryBackingStore(), networkService: FakeNetworkService())
XMLHandler.assetAttributeProvider = CallForAssetAttributeProvider()
@ -914,7 +914,7 @@ class XMLHandlerTest: XCTestCase {
// swiftlint:enable function_body_length
func testNoAssetDefinition() {
let store = AssetDefinitionStore(backingStore: AssetDefinitionInMemoryBackingStore())
let store = AssetDefinitionStore(backingStore: AssetDefinitionInMemoryBackingStore(), networkService: FakeNetworkService())
let xmlHandler = XMLHandler(contract: Constants.nullAddress, tokenType: .erc875, assetDefinitionStore: store)
let tokenId = BigUInt("0000000000000000000000000000000002000000000000000000000000000000", radix: 16)!
let server: RPCServer = .main

@ -41,7 +41,7 @@ class TokenAdaptorTest: XCTestCase {
"countryB": .init(directoryString: "Team B")
]),
]
let assetDefinitionStore = AssetDefinitionStore()
let assetDefinitionStore = AssetDefinitionStore.make()
let token = Token(contract: Constants.nullAddress)
let bundles = TokenAdaptor(token: token, assetDefinitionStore: assetDefinitionStore, eventsDataStore: FakeEventsDataStore()).bundleTestsOnly(tokens: tokens)
XCTAssertEqual(bundles.count, 2)
@ -91,7 +91,7 @@ class TokenAdaptorTest: XCTestCase {
"countryB": .init(directoryString: "Team B")
])
]
let assetDefinitionStore = AssetDefinitionStore()
let assetDefinitionStore = AssetDefinitionStore.make()
let token = Token(contract: Constants.nullAddress)
let bundles = TokenAdaptor(token: token, assetDefinitionStore: assetDefinitionStore, eventsDataStore: FakeEventsDataStore()).bundleTestsOnly(tokens: tokens)

@ -44,14 +44,14 @@ extension WalletDataProcessingPipeline {
importToken: importToken,
transactionsStorage: transactionsDataStore,
nftProvider: nftProvider,
assetDefinitionStore: .init(),
assetDefinitionStore: .make(),
networkService: FakeNetworkService())
let pipeline: TokensProcessingPipeline = WalletDataProcessingPipeline(
wallet: wallet,
tokensService: tokensService,
coinTickersFetcher: coinTickersFetcher,
assetDefinitionStore: .init(),
assetDefinitionStore: .make(),
eventsDataStore: eventsDataStore,
currencyService: .make())
@ -60,7 +60,7 @@ extension WalletDataProcessingPipeline {
let activitiesPipeLine = ActivitiesPipeLine(
config: .make(),
wallet: wallet,
assetDefinitionStore: .init(),
assetDefinitionStore: .make(),
transactionDataStore: transactionsDataStore,
tokensService: tokensService,
sessionsProvider: sessionsProvider,
@ -122,7 +122,7 @@ class PaymentCoordinatorTests: XCTestCase {
server: .main,
sessionProvider: dep.sessionsProvider,
keystore: FakeEtherKeystore(),
assetDefinitionStore: AssetDefinitionStore(),
assetDefinitionStore: .make(),
analytics: FakeAnalyticsService(),
tokenCollection: dep.pipeline,
domainResolutionService: FakeDomainResolutionService(),
@ -147,7 +147,7 @@ class PaymentCoordinatorTests: XCTestCase {
server: .main,
sessionProvider: dep.sessionsProvider,
keystore: FakeEtherKeystore(),
assetDefinitionStore: AssetDefinitionStore(),
assetDefinitionStore: .make(),
analytics: FakeAnalyticsService(),
tokenCollection: dep.pipeline,
domainResolutionService: FakeDomainResolutionService(),

@ -4,6 +4,8 @@ import Foundation
import BigInt
public struct Constants {
static let xdaiDropPrefix = Data(bytes: [0x58, 0x44, 0x41, 0x49, 0x44, 0x52, 0x4F, 0x50]).hex()
static let mainnetMagicLinkHost = "aw.app"
static let legacyMagicLinkHost = "app.awallet.io"
static let classicMagicLinkHost = "classic.aw.app"
@ -41,11 +43,7 @@ public struct Constants {
public static let legacyMagicLinkPrefix = "https://app.awallet.io/"
// fee master
public static let paymentServer = "https://paymaster.stormbird.sg/api/claimToken"
public static let paymentServerSpawnable = "https://paymaster.stormbird.sg/api/claimSpawnableToken"
public static let paymentServerSupportsContractEndPoint = "https://paymaster.stormbird.sg/api/checkContractIsSupportedForFreeTransfers"
public static let paymentServerClaimedToken = "https://paymaster.stormbird.sg/api/checkIfSignatureIsUsed"
public static let currencyDropServer = "https://paymaster.stormbird.sg/api/claimFreeCurrency"
static let paymentServerBaseUrl = URL(string: "https://paymaster.stormbird.sg")!
//Ethereum null variables
public static let nullTokenId = "0x0000000000000000000000000000000000000000000000000000000000000000"

@ -1,6 +1,5 @@
// Copyright © 2018 Stormbird PTE. LTD.
import Alamofire
import Combine
import PromiseKit
@ -28,22 +27,6 @@ public class AssetDefinitionStore: NSObject {
}
}
private var httpHeaders: HTTPHeaders = {
guard let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String else { return [:] }
return [
"Accept": "application/tokenscript+xml; charset=UTF-8",
"X-Client-Name": TokenScript.repoClientName,
"X-Client-Version": appVersion,
"X-Platform-Name": TokenScript.repoPlatformName,
"X-Platform-Version": UIDevice.current.systemVersion
]
}()
private var lastModifiedDateFormatter: DateFormatter = {
let df = DateFormatter()
df.dateFormat = "E, dd MMM yyyy HH:mm:ss z"
df.timeZone = TimeZone(secondsFromGMT: 0)
return df
}()
private var lastContractInPasteboard: String?
private var backingStore: AssetDefinitionBackingStore
private let _baseTokenScriptFiles: AtomicDictionary<TokenType, String> = .init()
@ -52,6 +35,8 @@ public class AssetDefinitionStore: NSObject {
private var signatureChangeSubject: PassthroughSubject<AlphaWallet.Address, Never> = .init()
private var bodyChangeSubject: PassthroughSubject<AlphaWallet.Address, Never> = .init()
private var listOfBadTokenScriptFilesSubject: CurrentValueSubject<[TokenScriptFileIndices.FileName], Never> = .init([])
private let network: AssetDefinitionNetwork
private var cancelable = AtomicDictionary<Int, AnyCancellable>()
public var listOfBadTokenScriptFiles: AnyPublisher<[TokenScriptFileIndices.FileName], Never> {
listOfBadTokenScriptFilesSubject.eraseToAnyPublisher()
@ -82,7 +67,7 @@ public class AssetDefinitionStore: NSObject {
public func assetBodyChanged(for contract: AlphaWallet.Address) -> AnyPublisher<Void, Never> {
return bodyChangeSubject
.filter { $0.sameContract(as: contract) }
.map { _ in return () }
.mapToVoid()
.share()
.eraseToAnyPublisher()
}
@ -90,7 +75,7 @@ public class AssetDefinitionStore: NSObject {
public func assetSignatureChanged(for contract: AlphaWallet.Address) -> AnyPublisher<Void, Never> {
return signatureChangeSubject
.filter { $0.sameContract(as: contract) }
.map { _ in return () }
.mapToVoid()
.share()
.eraseToAnyPublisher()
}
@ -98,7 +83,7 @@ public class AssetDefinitionStore: NSObject {
public func assetsSignatureOrBodyChange(for contract: AlphaWallet.Address) -> AnyPublisher<Void, Never> {
return Publishers
.Merge(assetSignatureChanged(for: contract), assetSignatureChanged(for: contract))
.map { _ in return () }
.mapToVoid()
.eraseToAnyPublisher()
}
@ -134,7 +119,8 @@ public class AssetDefinitionStore: NSObject {
"""
}
public init(backingStore: AssetDefinitionBackingStore = AssetDefinitionDiskBackingStoreWithOverrides(), baseTokenScriptFiles: [TokenType: String] = [:]) {
public init(backingStore: AssetDefinitionBackingStore = AssetDefinitionDiskBackingStoreWithOverrides(), baseTokenScriptFiles: [TokenType: String] = [:], networkService: NetworkService) {
self.network = AssetDefinitionNetwork(networkService: networkService)
self.backingStore = backingStore
self._baseTokenScriptFiles.set(value: baseTokenScriptFiles)
super.init()
@ -179,16 +165,8 @@ public class AssetDefinitionStore: NSObject {
}
public subscript(contract: AlphaWallet.Address) -> String? {
get {
backingStore[contract]
}
set(value) {
backingStore[contract] = value
}
}
private func cacheXml(_ xml: String, forContract contract: AlphaWallet.Address) {
backingStore[contract] = xml
get { backingStore[contract] }
set { backingStore[contract] = newValue }
}
public func isOfficial(contract: AlphaWallet.Address) -> Bool {
@ -217,14 +195,14 @@ public class AssetDefinitionStore: NSObject {
urlToFetch(contract: contract, server: server)
}.done { result in
guard let (url, isScriptUri) = result else { return }
self.fetchXML(forContract: contract, server: server, withUrl: url, useCacheAndFetch: useCacheAndFetch) { result in
self.fetchXML(contract: contract, server: server, url: url, useCacheAndFetch: useCacheAndFetch) { result in
//Try a bit harder if the TokenScript was specified via EIP-5169 (`scriptURI()`)
//TODO probably better to convert completionHandler to Promise so we can retry more elegantly
if isScriptUri && result.isError {
self.fetchXML(forContract: contract, server: server, withUrl: url, useCacheAndFetch: useCacheAndFetch) { result in
self.fetchXML(contract: contract, server: server, url: url, useCacheAndFetch: useCacheAndFetch) { result in
if isScriptUri && result.isError {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.fetchXML(forContract: contract, server: server, withUrl: url, useCacheAndFetch: useCacheAndFetch, completionHandler: completionHandler)
self.fetchXML(contract: contract, server: server, url: url, useCacheAndFetch: useCacheAndFetch, completionHandler: completionHandler)
}
}
}
@ -238,24 +216,23 @@ public class AssetDefinitionStore: NSObject {
}
}
private func fetchXML(forContract contract: AlphaWallet.Address, server: RPCServer?, withUrl url: URL, useCacheAndFetch: Bool = false, completionHandler: ((Result) -> Void)? = nil) {
//TODO improve check. We should store the IPFS hash, if the hash is different, download the new file, otherwise it has not changed
//IPFS, at least on Infura returns a `304` even though we pass in a timestamp that is older than the creation date for the "IF-Modified-Since" header. So we always download the entire file. This only works decently when we don't have many TokenScript using EIP-5169/`scriptURI()`
let includeLastModifiedTimestampHeader: Bool = !url.absoluteString.contains("ipfs")
Alamofire.request(
url,
method: .get,
headers: httpHeadersWithLastModifiedTimestamp(forContract: contract, includeLastModifiedTimestampHeader: includeLastModifiedTimestampHeader)
).response { [weak self] response in
private func fetchXML(contract: AlphaWallet.Address, server: RPCServer?, url: URL, useCacheAndFetch: Bool = false, completionHandler: ((Result) -> Void)? = nil) {
let lastModified = lastModifiedDateOfCachedAssetDefinitionFile(forContract: contract)
let request = AssetDefinitionNetwork.GetXmlFileRequest(url: url, lastModifiedDate: lastModified)
cancelable[request.hashValue] = network
.fetchXml(request: request)
.sink(receiveCompletion: { [cancelable] _ in
cancelable[request.hashValue] = .none
}, receiveValue: { [weak self] response in
guard let strongSelf = self else { return }
if response.response?.statusCode == 304 {
completionHandler?(.unmodified)
} else if response.response?.statusCode == 406 {
completionHandler?(.error)
} else if response.response?.statusCode == 404 {
switch response {
case .error:
completionHandler?(.error)
} else if response.response?.statusCode == 200 {
if let xml = response.data.flatMap({ String(data: $0, encoding: .utf8) }).nilIfEmpty {
case .unmodified:
completionHandler?(.unmodified)
case .xml(let xml):
//Note that Alamofire converts the 304 to a 200 if caching is enabled (which it is, by default). So we'll never get a 304 here. Checking against Charles proxy will show that a 304 is indeed returned by the server with an empty body. So we compare the contents instead. https://github.com/Alamofire/Alamofire/issues/615
if xml == strongSelf[contract] {
completionHandler?(.unmodified)
@ -264,17 +241,14 @@ public class AssetDefinitionStore: NSObject {
completionHandler?(result)
}
} else {
strongSelf.cacheXml(xml, forContract: contract)
strongSelf[contract] = xml
strongSelf.invalidate(forContract: contract)
completionHandler?(.updated)
strongSelf.triggerBodyChangedSubscribers(forContract: contract)
strongSelf.triggerSignatureChangedSubscribers(forContract: contract)
}
} else {
completionHandler?(.error)
}
}
}
})
}
private func isTruncatedXML(xml: String) -> Bool {
@ -318,20 +292,6 @@ public class AssetDefinitionStore: NSObject {
return backingStore.lastModifiedDateOfCachedAssetDefinitionFile(forContract: contract)
}
private func httpHeadersWithLastModifiedTimestamp(forContract contract: AlphaWallet.Address, includeLastModifiedTimestampHeader: Bool) -> HTTPHeaders {
var result = httpHeaders
if includeLastModifiedTimestampHeader, let lastModified = lastModifiedDateOfCachedAssetDefinitionFile(forContract: contract) {
result["IF-Modified-Since"] = string(fromLastModifiedDate: lastModified)
return result
} else {
return result
}
}
public func string(fromLastModifiedDate date: Date) -> String {
return lastModifiedDateFormatter.string(from: date)
}
public func forEachContractWithXML(_ body: (AlphaWallet.Address) -> Void) {
backingStore.forEachContractWithXML(body)
}
@ -387,6 +347,104 @@ extension AssetDefinitionStore {
}
}
import AlphaWalletCore
public class AssetDefinitionNetwork {
private let networkService: NetworkService
public init(networkService: NetworkService) {
self.networkService = networkService
}
public enum Response {
case unmodified
case error
case xml(String)
}
public func fetchXml(request: GetXmlFileRequest) -> AnyPublisher<AssetDefinitionNetwork.Response, Never> {
return networkService
.responseData(request)
.map { data -> AssetDefinitionNetwork.Response in
guard let response = data.response.response else { return .error }
if response.statusCode == 304 {
return .unmodified
} else if response.statusCode == 406 {
return .error
} else if response.statusCode == 404 {
return .error
} else if response.statusCode == 200 {
if let xml = String(data: data.data, encoding: .utf8).nilIfEmpty {
return .xml(xml)
} else {
return .error
}
}
return .error
}.replaceError(with: .error)
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
}
extension AssetDefinitionNetwork {
public struct GetXmlFileRequest: URLRequestConvertible, Hashable {
let url: URL
let lastModifiedDate: Date?
public init(url: URL, lastModifiedDate: Date?) {
self.url = url
self.lastModifiedDate = lastModifiedDate
}
public func asURLRequest() throws -> URLRequest {
guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { throw URLError(.badURL) }
var request = try URLRequest(url: components.asURL(), method: .get)
//TODO improve check. We should store the IPFS hash, if the hash is different, download the new file, otherwise it has not changed
//IPFS, at least on Infura returns a `304` even though we pass in a timestamp that is older than the creation date for the "IF-Modified-Since" header. So we always download the entire file. This only works decently when we don't have many TokenScript using EIP-5169/`scriptURI()`
request.allHTTPHeaderFields = httpHeadersWithLastModifiedTimestamp(
includeLastModifiedTimestampHeader: !url.absoluteString.contains("ipfs"),
lastModifiedDate: lastModifiedDate)
return request
}
private var httpHeaders: HTTPHeaders {
guard let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String else { return [:] }
return [
"Accept": "application/tokenscript+xml; charset=UTF-8",
"X-Client-Name": TokenScript.repoClientName,
"X-Client-Version": appVersion,
"X-Platform-Name": TokenScript.repoPlatformName,
"X-Platform-Version": UIDevice.current.systemVersion
]
}
private static let lastModifiedDateFormatter: DateFormatter = {
let df = DateFormatter()
df.dateFormat = "E, dd MMM yyyy HH:mm:ss z"
df.timeZone = TimeZone(secondsFromGMT: 0)
return df
}()
private func httpHeadersWithLastModifiedTimestamp(includeLastModifiedTimestampHeader: Bool, lastModifiedDate: Date?) -> HTTPHeaders {
var result = httpHeaders
if includeLastModifiedTimestampHeader, let lastModified = lastModifiedDate {
result["IF-Modified-Since"] = string(fromLastModifiedDate: lastModified)
return result
} else {
return result
}
}
public func string(fromLastModifiedDate date: Date) -> String {
return GetXmlFileRequest.lastModifiedDateFormatter.string(from: date)
}
}
}
extension AssetDefinitionStore {
enum functional {}
}

@ -0,0 +1,200 @@
//
// ImportMagicLinkNetworkService.swift
// AlphaWalletFoundation
//
// Created by Vladyslav Shepitko on 15.12.2022.
//
import Foundation
import BigInt
import Combine
import AlphaWalletCore
public class ImportMagicLinkNetworkService {
private let networkService: NetworkService
public init(networkService: NetworkService) {
self.networkService = networkService
}
public func checkPaymentServerSupportsContract(contractAddress: AlphaWallet.Address) -> AnyPublisher<Bool, Never> {
networkService
.responseData(CheckPaymentServerSupportsContractRequest(contractAddress: contractAddress))
.map {
if let response = $0.response.response {
return response.statusCode >= 200 && response.statusCode <= 299
} else {
return false
}
}.replaceError(with: false)
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
public func checkIfLinkClaimed(r: String) -> AnyPublisher<Bool, Never> {
networkService
.responseData(CheckIfLinkClaimedRequest(r: r))
.map {
if let response = $0.response.response {
return response.statusCode == 208 || response.statusCode > 299
} else {
return false
}
}.replaceError(with: false)
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
public func freeTransfer(request: FreeTransferRequest) -> AnyPublisher<Bool, Never> {
networkService
.responseData(request)
.map {
//need to set this to false by default else it will allow no connections to be considered successful etc
//401 code will be given if signature is invalid on the server
if let response = $0.response.response {
return response.statusCode < 300
} else {
return false
}
}.replaceError(with: false)
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
}
extension ImportMagicLinkNetworkService {
public struct FreeTransferRequest: URLRequestConvertible {
public var contractAddress: AlphaWallet.Address {
signedOrder.order.contractAddress
}
let signedOrder: SignedOrder
let wallet: Wallet
let server: RPCServer
public init(signedOrder: SignedOrder, wallet: Wallet, server: RPCServer) {
self.signedOrder = signedOrder
self.wallet = wallet
self.server = server
}
public func asURLRequest() throws -> URLRequest {
switch signedOrder.order.nativeCurrencyDrop {
case true:
return try FreeTransferRequestForCurrencyLinks(signedOrder: signedOrder, recipient: wallet.address, server: server).asURLRequest()
case false:
return try FreeTransferRequestForNormalLinks(signedOrder: signedOrder, isForTransfer: true, wallet: wallet, server: server).asURLRequest()
}
}
}
private struct FreeTransferRequestForNormalLinks: URLRequestConvertible {
let signedOrder: SignedOrder
let isForTransfer: Bool
let wallet: Wallet
let server: RPCServer
private func stringEncodeIndices(_ indices: [UInt16]) -> String {
return indices.map(String.init).joined(separator: ",")
}
private func stringEncodeTokenIds(_ tokenIds: [BigUInt]?) -> String? {
guard let tokens = tokenIds else { return nil }
return tokens.map({ $0.serialize().hexString }).joined(separator: ",")
}
func asURLRequest() throws -> URLRequest {
let signature = signedOrder.signature.drop0x
let indices = signedOrder.order.indices
let indicesStringEncoded = stringEncodeIndices(indices)
let tokenIdsEncoded = stringEncodeTokenIds(signedOrder.order.tokenIds)
var parameters: Parameters = [
"address": wallet.address,
"contractAddress": signedOrder.order.contractAddress,
"indices": indicesStringEncoded,
"tokenIds": tokenIdsEncoded ?? "",
"price": signedOrder.order.price.description,
"expiry": signedOrder.order.expiry.description,
"v": signature.substring(from: 128),
"r": "0x" + signature.substring(with: Range(uncheckedBounds: (0, 64))),
"s": "0x" + signature.substring(with: Range(uncheckedBounds: (64, 128))),
"networkId": server.chainID.description,
]
if isForTransfer {
parameters.removeValue(forKey: "price")
}
guard var components = URLComponents(url: Constants.paymentServerBaseUrl, resolvingAgainstBaseURL: false) else { throw URLError(.badURL) }
if signedOrder.order.spawnable {
parameters.removeValue(forKey: "indices")
components.path = "/api/claimSpawnableToken"
} else {
parameters.removeValue(forKey: "tokenIds")
components.path = "/api/claimToken"
}
var request = try URLRequest(url: components.asURL(), method: .get)
return try URLEncoding().encode(request, with: parameters)
}
}
private struct FreeTransferRequestForCurrencyLinks: URLRequestConvertible {
let signedOrder: SignedOrder
let recipient: AlphaWallet.Address
let server: RPCServer
func asURLRequest() throws -> URLRequest {
guard var components = URLComponents(url: Constants.paymentServerBaseUrl, resolvingAgainstBaseURL: false) else { throw URLError(.badURL) }
components.path = "/api/claimFreeCurrency"
var request = try URLRequest(url: components.asURL(), method: .get)
let signature = signedOrder.signature.drop0x
return try URLEncoding().encode(request, with: [
"prefix": Constants.xdaiDropPrefix,
"recipient": recipient.eip55String,
"amount": signedOrder.order.count.description,
"expiry": signedOrder.order.expiry.description,
"nonce": signedOrder.order.nonce,
"v": signature.substring(from: 128),
//Use string interpolation instead of concatenation to speed up build time. 160ms -> <100ms, as of Xcode 11.7
"r": "0x\(signature.substring(with: Range(uncheckedBounds: (0, 64))))",
"s": "0x\(signature.substring(with: Range(uncheckedBounds: (64, 128))))",
"networkId": server.chainID.description,
"contractAddress": signedOrder.order.contractAddress
])
}
}
private struct CheckIfLinkClaimedRequest: URLRequestConvertible {
let r: String
func asURLRequest() throws -> URLRequest {
guard var components = URLComponents(url: Constants.paymentServerBaseUrl, resolvingAgainstBaseURL: false) else { throw URLError(.badURL) }
components.path = "/api/checkIfSignatureIsUsed"
var request = try URLRequest(url: components.asURL(), method: .get)
return try URLEncoding().encode(request, with: [
"r": r
])
}
}
private struct CheckPaymentServerSupportsContractRequest: URLRequestConvertible {
let contractAddress: AlphaWallet.Address
func asURLRequest() throws -> URLRequest {
guard var components = URLComponents(url: Constants.paymentServerBaseUrl, resolvingAgainstBaseURL: false) else { throw URLError(.badURL) }
components.path = "/api/checkContractIsSupportedForFreeTransfers"
var request = try URLRequest(url: components.asURL(), method: .get)
return try URLEncoding().encode(request, with: [
"contractAddress": contractAddress.eip55String
])
}
}
}
Loading…
Cancel
Save