Merge pull request #4943 from AlphaWallet/attach-domainname-chainId-to-rpc-node-error-logging

Attach domain name and chain ID to RPC node rate limited error analytics logging
pull/4944/head
Hwee-Boon Yar 2 years ago committed by GitHub
commit cf8cc97293
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      AlphaWallet/Analytics/Models/AnalyticsTypes.swift
  2. 2
      AlphaWallet/Browser/Coordinators/DappBrowserCoordinator.swift
  3. 2
      AlphaWallet/Core/Ethereum/EthereumTransaction.swift
  4. 2
      AlphaWallet/Covalent/TransactionProviders/PendingTransaction/PendingTransactionFetcher.swift
  5. 2
      AlphaWallet/EtherClient/ChainState.swift
  6. 8
      AlphaWallet/EtherClient/Models/GetNextNonce.swift
  7. 8
      AlphaWallet/Extensions/Publishers/Session+Publishers.swift
  8. 18
      AlphaWallet/Extensions/Session+PromiseKit.swift
  9. 5
      AlphaWallet/Settings/Coordinators/PingInfuraCoordinator.swift
  10. 2
      AlphaWallet/Settings/Models/AddCustomChain.swift
  11. 6
      AlphaWallet/Tokens/Logic/GetDASNameLookup.swift
  12. 2
      AlphaWallet/Tokens/Logic/GetNativeCryptoCurrencyBalance.swift
  13. 2
      AlphaWallet/Transactions/EtherscanSingleChainTransactionProvider.swift
  14. 2
      AlphaWallet/Transfer/Controllers/GasPriceEstimator.swift
  15. 2
      AlphaWallet/Transfer/Controllers/TransactionConfigurator.swift
  16. 6
      AlphaWallet/Transfer/Coordinators/SendTransactionCoordinator.swift
  17. 2
      AlphaWallet/Transfer/Types/SendTransactionError.swift
  18. 3
      AlphaWalletTests/Core/Types/SessionTests.swift

@ -117,6 +117,7 @@ enum Analytics {
case addCustomChainType case addCustomChainType
case isAccepted case isAccepted
case reason case reason
case domainName
} }
enum UserProperties: String, AnalyticsUserProperty { enum UserProperties: String, AnalyticsUserProperty {

@ -180,7 +180,7 @@ final class DappBrowserCoordinator: NSObject, Coordinator {
private func ethCall(callbackID: Int, from: AlphaWallet.Address?, to: AlphaWallet.Address?, value: String?, data: String, server: RPCServer) { private func ethCall(callbackID: Int, from: AlphaWallet.Address?, to: AlphaWallet.Address?, value: String?, data: String, server: RPCServer) {
let request = EthCallRequest(from: from, to: to, value: value, data: data) let request = EthCallRequest(from: from, to: to, value: value, data: data)
firstly { firstly {
Session.send(EtherServiceRequest(server: server, batch: BatchFactory().create(request)), analyticsCoordinator: analyticsCoordinator) Session.send(EtherServiceRequest(server: server, batch: BatchFactory().create(request)), server: server, analyticsCoordinator: analyticsCoordinator)
}.done { result in }.done { result in
let callback = DappCallback(id: callbackID, value: .ethCall(result)) let callback = DappCallback(id: callbackID, value: .ethCall(result))
self.browserViewController.notifyFinish(callbackID: callbackID, value: .success(callback)) self.browserViewController.notifyFinish(callbackID: callbackID, value: .success(callback))

@ -14,7 +14,7 @@ enum EthereumTransaction {
static func isCompleted(transactionId: Id, server: RPCServer, analyticsCoordinator: AnalyticsCoordinator) -> Promise<Bool> { static func isCompleted(transactionId: Id, server: RPCServer, analyticsCoordinator: AnalyticsCoordinator) -> Promise<Bool> {
let request = GetTransactionRequest(hash: transactionId) let request = GetTransactionRequest(hash: transactionId)
return firstly { return firstly {
Session.send(EtherServiceRequest(server: server, batch: BatchFactory().create(request)), analyticsCoordinator: analyticsCoordinator) Session.send(EtherServiceRequest(server: server, batch: BatchFactory().create(request)), server: server, analyticsCoordinator: analyticsCoordinator)
}.map { pendingTransaction in }.map { pendingTransaction in
if let blockNumber = Int(pendingTransaction.blockNumber), blockNumber > 0 { if let blockNumber = Int(pendingTransaction.blockNumber), blockNumber > 0 {
return true return true

@ -19,7 +19,7 @@ final class PendingTransactionFetcher {
let request = GetTransactionRequest(hash: id) let request = GetTransactionRequest(hash: id)
return Session return Session
.sendPublisher(EtherServiceRequest(server: server, batch: BatchFactory().create(request))) .sendPublisher(EtherServiceRequest(server: server, batch: BatchFactory().create(request)), server: server)
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
} }

@ -56,7 +56,7 @@ class ChainState {
@objc func fetch() { @objc func fetch() {
let request = EtherServiceRequest(server: server, batch: BatchFactory().create(BlockNumberRequest())) let request = EtherServiceRequest(server: server, batch: BatchFactory().create(BlockNumberRequest()))
firstly { firstly {
Session.send(request, analyticsCoordinator: analyticsCoordinator) Session.send(request, server: server, analyticsCoordinator: analyticsCoordinator)
}.done { [weak self] in }.done { [weak self] in
self?.latestBlock = $0 self?.latestBlock = $0
}.catch { error in }.catch { error in

@ -6,24 +6,28 @@ import JSONRPCKit
import PromiseKit import PromiseKit
class GetNextNonce { class GetNextNonce {
//Important to store the RPC URL and not read `RPCServer.server` because it might be overridden by a RPC node that supports private transactions
private let rpcURL: URL private let rpcURL: URL
private let server: RPCServer
private let wallet: AlphaWallet.Address private let wallet: AlphaWallet.Address
private let analyticsCoordinator: AnalyticsCoordinator private let analyticsCoordinator: AnalyticsCoordinator
init(server: RPCServer, wallet: AlphaWallet.Address, analyticsCoordinator: AnalyticsCoordinator) { init(server: RPCServer, wallet: AlphaWallet.Address, analyticsCoordinator: AnalyticsCoordinator) {
self.rpcURL = server.rpcURL self.rpcURL = server.rpcURL
self.server = server
self.wallet = wallet self.wallet = wallet
self.analyticsCoordinator = analyticsCoordinator self.analyticsCoordinator = analyticsCoordinator
} }
init(rpcURL: URL, wallet: AlphaWallet.Address, analyticsCoordinator: AnalyticsCoordinator) { init(rpcURL: URL, server: RPCServer, wallet: AlphaWallet.Address, analyticsCoordinator: AnalyticsCoordinator) {
self.rpcURL = rpcURL self.rpcURL = rpcURL
self.server = server
self.wallet = wallet self.wallet = wallet
self.analyticsCoordinator = analyticsCoordinator self.analyticsCoordinator = analyticsCoordinator
} }
func promise() -> Promise<Int> { func promise() -> Promise<Int> {
let request = EtherServiceRequest(rpcURL: rpcURL, batch: BatchFactory().create(GetTransactionCountRequest(address: wallet, state: "pending"))) let request = EtherServiceRequest(rpcURL: rpcURL, batch: BatchFactory().create(GetTransactionCountRequest(address: wallet, state: "pending")))
return Session.send(request, analyticsCoordinator: analyticsCoordinator) return Session.send(request, server: server, analyticsCoordinator: analyticsCoordinator)
} }
} }

@ -12,8 +12,8 @@ import Combine
extension Session { extension Session {
class func sendPublisher<Request: APIKit.Request>(_ request: Request, callbackQueue: CallbackQueue? = nil) -> AnyPublisher<Request.Response, SessionTaskError> { class func sendPublisher<Request: APIKit.Request>(_ request: Request, server: RPCServer, callbackQueue: CallbackQueue? = nil) -> AnyPublisher<Request.Response, SessionTaskError> {
sendImplPublisher(request, callbackQueue: callbackQueue) sendImplPublisher(request, server: server, callbackQueue: callbackQueue)
.retry(times: 2, when: { .retry(times: 2, when: {
guard case SessionTaskError.requestError(let e) = $0 else { return false } guard case SessionTaskError.requestError(let e) = $0 else { return false }
return e is SendTransactionRetryableError return e is SendTransactionRetryableError
@ -21,7 +21,7 @@ extension Session {
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
private class func sendImplPublisher<Request: APIKit.Request>(_ request: Request, callbackQueue: CallbackQueue? = nil) -> AnyPublisher<Request.Response, SessionTaskError> { private class func sendImplPublisher<Request: APIKit.Request>(_ request: Request, server: RPCServer, callbackQueue: CallbackQueue? = nil) -> AnyPublisher<Request.Response, SessionTaskError> {
var sessionTask: SessionTask? var sessionTask: SessionTask?
let publisher = Deferred { let publisher = Deferred {
Future<Request.Response, SessionTaskError> { seal in Future<Request.Response, SessionTaskError> { seal in
@ -30,7 +30,7 @@ extension Session {
case .success(let result): case .success(let result):
seal(.success(result)) seal(.success(result))
case .failure(let error): case .failure(let error):
if let e = convertToUserFriendlyError(error: error, baseUrl: request.baseURL) { if let e = convertToUserFriendlyError(error: error, server: server, baseUrl: request.baseURL) {
seal(.failure(.requestError(e))) seal(.failure(.requestError(e)))
} else { } else {
seal(.failure(error)) seal(.failure(error))

@ -7,14 +7,14 @@ import PromiseKit
extension Session { extension Session {
private class func sendImpl<Request: APIKit.Request>(_ request: Request, analyticsCoordinator: AnalyticsCoordinator, callbackQueue: CallbackQueue? = nil) -> Promise<Request.Response> { private class func sendImpl<Request: APIKit.Request>(_ request: Request, server: RPCServer, analyticsCoordinator: AnalyticsCoordinator, callbackQueue: CallbackQueue? = nil) -> Promise<Request.Response> {
let (promise, seal) = Promise<Request.Response>.pending() let (promise, seal) = Promise<Request.Response>.pending()
Session.send(request, callbackQueue: callbackQueue) { result in Session.send(request, callbackQueue: callbackQueue) { result in
switch result { switch result {
case .success(let result): case .success(let result):
seal.fulfill(result) seal.fulfill(result)
case .failure(let error): case .failure(let error):
if let e = convertToUserFriendlyError(error: error, baseUrl: request.baseURL) { if let e = convertToUserFriendlyError(error: error, server: server, baseUrl: request.baseURL) {
if let e = e as? SendTransactionRetryableError { if let e = e as? SendTransactionRetryableError {
logRpcNodeError(e, analyticsCoordinator: analyticsCoordinator) logRpcNodeError(e, analyticsCoordinator: analyticsCoordinator)
} }
@ -31,27 +31,27 @@ extension Session {
private static func logRpcNodeError(_ rpcNodeError: SendTransactionRetryableError, analyticsCoordinator: AnalyticsCoordinator) { private static func logRpcNodeError(_ rpcNodeError: SendTransactionRetryableError, analyticsCoordinator: AnalyticsCoordinator) {
switch rpcNodeError { switch rpcNodeError {
case .rateLimited: case .rateLimited(let server, let domainName):
analyticsCoordinator.log(error: Analytics.WebApiErrors.rpcNodeRateLimited) analyticsCoordinator.log(error: Analytics.WebApiErrors.rpcNodeRateLimited, properties: [Analytics.Properties.chain.rawValue: server.chainID, Analytics.Properties.domainName.rawValue: domainName])
case .possibleBinanceTestnetTimeout, .networkConnectionWasLost, .invalidCertificate, .requestTimedOut: case .possibleBinanceTestnetTimeout, .networkConnectionWasLost, .invalidCertificate, .requestTimedOut:
return return
} }
} }
class func send<Request: APIKit.Request>(_ request: Request, analyticsCoordinator: AnalyticsCoordinator, callbackQueue: CallbackQueue? = nil) -> Promise<Request.Response> { class func send<Request: APIKit.Request>(_ request: Request, server: RPCServer, analyticsCoordinator: AnalyticsCoordinator, callbackQueue: CallbackQueue? = nil) -> Promise<Request.Response> {
let promise = sendImpl(request, analyticsCoordinator: analyticsCoordinator, callbackQueue: callbackQueue) let promise = sendImpl(request, server: server, analyticsCoordinator: analyticsCoordinator, callbackQueue: callbackQueue)
return firstly { return firstly {
promise promise
}.recover { error -> Promise<Request.Response> in }.recover { error -> Promise<Request.Response> in
if error is SendTransactionRetryableError { if error is SendTransactionRetryableError {
return sendImpl(request, analyticsCoordinator: analyticsCoordinator, callbackQueue: callbackQueue) return sendImpl(request, server: server, analyticsCoordinator: analyticsCoordinator, callbackQueue: callbackQueue)
} else { } else {
return promise return promise
} }
} }
} }
static func convertToUserFriendlyError(error: SessionTaskError, baseUrl: URL) -> Error? { static func convertToUserFriendlyError(error: SessionTaskError, server: RPCServer, baseUrl: URL) -> Error? {
infoLog("convertToUserFriendlyError URL: \(baseUrl.absoluteString) error: \(error)") infoLog("convertToUserFriendlyError URL: \(baseUrl.absoluteString) error: \(error)")
switch error { switch error {
case .connectionError(let e): case .connectionError(let e):
@ -121,7 +121,7 @@ extension Session {
case .unacceptableStatusCode(let statusCode): case .unacceptableStatusCode(let statusCode):
if statusCode == 429 { if statusCode == 429 {
warnLog("[API] Rate limited by baseURL: \(baseUrl.absoluteString)") warnLog("[API] Rate limited by baseURL: \(baseUrl.absoluteString)")
return SendTransactionRetryableError.rateLimited return SendTransactionRetryableError.rateLimited(server: server, domainName: baseUrl.host ?? "")
} else { } else {
RemoteLogger.instance.logRpcOrOtherWebError("APIKit.ResponseError.unacceptableStatusCode | status: \(statusCode)", url: baseUrl.absoluteString) RemoteLogger.instance.logRpcOrOtherWebError("APIKit.ResponseError.unacceptableStatusCode | status: \(statusCode)", url: baseUrl.absoluteString)
} }

@ -40,9 +40,10 @@ class PingInfuraCoordinator: Coordinator {
} }
private func pingInfura() { private func pingInfura() {
let request = EtherServiceRequest(server: .main, batch: BatchFactory().create(BlockNumberRequest())) let server = RPCServer.main
let request = EtherServiceRequest(server: server, batch: BatchFactory().create(BlockNumberRequest()))
firstly { firstly {
Session.send(request, analyticsCoordinator: analyticsCoordinator) Session.send(request, server: server, analyticsCoordinator: analyticsCoordinator)
}.done { _ in }.done { _ in
UIAlertController.alert( UIAlertController.alert(
title: R.string.localizable.settingsPingInfuraSuccessful(), title: R.string.localizable.settingsPingInfuraSuccessful(),

@ -172,7 +172,7 @@ extension AddCustomChain.functional {
let server = RPCServer.custom(customRpc) let server = RPCServer.custom(customRpc)
let request = EthChainIdRequest() let request = EthChainIdRequest()
return firstly { return firstly {
Session.send(EtherServiceRequest(server: server, batch: BatchFactory().create(request)), analyticsCoordinator: analyticsCoordinator) Session.send(EtherServiceRequest(server: server, batch: BatchFactory().create(request)), server: server, analyticsCoordinator: analyticsCoordinator)
}.map { result in }.map { result in
if let retrievedChainId = Int(chainId0xString: result), retrievedChainId == chainId { if let retrievedChainId = Int(chainId0xString: result), retrievedChainId == chainId {
return (chainId: chainId, rpcUrl: rpcUrl) return (chainId: chainId, rpcUrl: rpcUrl)

@ -17,9 +17,11 @@ public final class GetDASNameLookup {
private static let ethAddressKey = "address.eth" private static let ethAddressKey = "address.eth"
private let server: RPCServer
private let analyticsCoordinator: AnalyticsCoordinator private let analyticsCoordinator: AnalyticsCoordinator
init(analyticsCoordinator: AnalyticsCoordinator) { init(server: RPCServer, analyticsCoordinator: AnalyticsCoordinator) {
self.server = server
self.analyticsCoordinator = analyticsCoordinator self.analyticsCoordinator = analyticsCoordinator
} }
@ -35,7 +37,7 @@ public final class GetDASNameLookup {
let request = EtherServiceRequest(rpcURL: rpcURL, batch: BatchFactory().create(DASLookupRequest(value: value))) let request = EtherServiceRequest(rpcURL: rpcURL, batch: BatchFactory().create(DASLookupRequest(value: value)))
debugLog("[DAS] Looking up value \(value)") debugLog("[DAS] Looking up value \(value)")
return Session.send(request, analyticsCoordinator: analyticsCoordinator).map { response -> AlphaWallet.Address in return Session.send(request, server: server, analyticsCoordinator: analyticsCoordinator).map { response -> AlphaWallet.Address in
debugLog("[DAS] response for value: \(value) response : \(response)") debugLog("[DAS] response for value: \(value) response : \(response)")
if let record = response.records.first(where: { $0.key == GetDASNameLookup.ethAddressKey }), let address = AlphaWallet.Address(string: record.value) { if let record = response.records.first(where: { $0.key == GetDASNameLookup.ethAddressKey }), let address = AlphaWallet.Address(string: record.value) {
infoLog("[DAS] resolve value: \(value) to address: \(address)") infoLog("[DAS] resolve value: \(value) to address: \(address)")

@ -18,6 +18,6 @@ class GetNativeCryptoCurrencyBalance {
func getBalance(for address: AlphaWallet.Address) -> Promise<Balance> { func getBalance(for address: AlphaWallet.Address) -> Promise<Balance> {
let request = EtherServiceRequest(server: server, batch: BatchFactory().create(BalanceRequest(address: address))) let request = EtherServiceRequest(server: server, batch: BatchFactory().create(BalanceRequest(address: address)))
return Session.send(request, analyticsCoordinator: analyticsCoordinator, callbackQueue: queue.flatMap { .dispatchQueue($0) }) return Session.send(request, server: server, analyticsCoordinator: analyticsCoordinator, callbackQueue: queue.flatMap { .dispatchQueue($0) })
} }
} }

@ -162,7 +162,7 @@ class EtherscanSingleChainTransactionProvider: SingleChainTransactionProvider {
let request = GetTransactionRequest(hash: transaction.id) let request = GetTransactionRequest(hash: transaction.id)
firstly { firstly {
Session.send(EtherServiceRequest(server: session.server, batch: BatchFactory().create(request)), analyticsCoordinator: analyticsCoordinator) Session.send(EtherServiceRequest(server: session.server, batch: BatchFactory().create(request)), server: session.server, analyticsCoordinator: analyticsCoordinator)
}.done(on: queue, { [weak self] pendingTransaction in }.done(on: queue, { [weak self] pendingTransaction in
guard let strongSelf = self else { return } guard let strongSelf = self else { return }

@ -83,7 +83,7 @@ final class GasPriceEstimator {
let defaultPrice: BigInt = GasPriceConfiguration.defaultPrice(forServer: server) let defaultPrice: BigInt = GasPriceConfiguration.defaultPrice(forServer: server)
return firstly { return firstly {
Session.send(request, analyticsCoordinator: analyticsCoordinator) Session.send(request, server: server, analyticsCoordinator: analyticsCoordinator)
}.map { }.map {
if let gasPrice = BigInt($0.drop0x, radix: 16) { if let gasPrice = BigInt($0.drop0x, radix: 16) {
if (gasPrice + GasPriceConfiguration.oneGwei) > maxPrice { if (gasPrice + GasPriceConfiguration.oneGwei) > maxPrice {

@ -112,7 +112,7 @@ class TransactionConfigurator {
) )
firstly { firstly {
Session.send(EtherServiceRequest(server: session.server, batch: BatchFactory().create(request)), analyticsCoordinator: analyticsCoordinator) Session.send(EtherServiceRequest(server: session.server, batch: BatchFactory().create(request)), server: session.server, analyticsCoordinator: analyticsCoordinator)
}.done { gasLimit in }.done { gasLimit in
infoLog("Estimated gas limit with eth_estimateGas: \(gasLimit)") infoLog("Estimated gas limit with eth_estimateGas: \(gasLimit)")
let gasLimit: BigInt = { let gasLimit: BigInt = {

@ -33,7 +33,7 @@ class SendTransactionCoordinator {
let request = EtherServiceRequest(rpcURL: rpcURL, batch: BatchFactory().create(rawRequest)) let request = EtherServiceRequest(rpcURL: rpcURL, batch: BatchFactory().create(rawRequest))
return firstly { return firstly {
Session.send(request, analyticsCoordinator: analyticsCoordinator) Session.send(request, server: session.server, analyticsCoordinator: analyticsCoordinator)
}.recover { error -> Promise<SendRawTransactionRequest.Response> in }.recover { error -> Promise<SendRawTransactionRequest.Response> in
self.logSelectSendError(error) self.logSelectSendError(error)
throw error throw error
@ -72,7 +72,7 @@ class SendTransactionCoordinator {
private func resolveNextNonce(for transaction: UnsignedTransaction) -> Promise<UnsignedTransaction> { private func resolveNextNonce(for transaction: UnsignedTransaction) -> Promise<UnsignedTransaction> {
firstly { firstly {
GetNextNonce(rpcURL: rpcURL, wallet: session.account.address, analyticsCoordinator: analyticsCoordinator).promise() GetNextNonce(rpcURL: rpcURL, server: session.server, wallet: session.account.address, analyticsCoordinator: analyticsCoordinator).promise()
}.map { nonce -> UnsignedTransaction in }.map { nonce -> UnsignedTransaction in
let transaction = self.appendNonce(to: transaction, currentNonce: nonce) let transaction = self.appendNonce(to: transaction, currentNonce: nonce)
return transaction return transaction
@ -97,7 +97,7 @@ class SendTransactionCoordinator {
let request = EtherServiceRequest(rpcURL: rpcURL, batch: BatchFactory().create(rawTransaction)) let request = EtherServiceRequest(rpcURL: rpcURL, batch: BatchFactory().create(rawTransaction))
return firstly { return firstly {
Session.send(request, analyticsCoordinator: analyticsCoordinator) Session.send(request, server: session.server, analyticsCoordinator: analyticsCoordinator)
}.recover { error -> Promise<SendRawTransactionRequest.Response> in }.recover { error -> Promise<SendRawTransactionRequest.Response> in
self.logSelectSendError(error) self.logSelectSendError(error)
throw error throw error

@ -15,7 +15,7 @@ enum SendTransactionNotRetryableError: Error {
//TODO name is not right. It's not "SendTransaction" since `eth_getBalance` etc uses it too. Maybe RpcNodeRetryableRequestError? //TODO name is not right. It's not "SendTransaction" since `eth_getBalance` etc uses it too. Maybe RpcNodeRetryableRequestError?
enum SendTransactionRetryableError: LocalizedError { enum SendTransactionRetryableError: LocalizedError {
case possibleBinanceTestnetTimeout case possibleBinanceTestnetTimeout
case rateLimited case rateLimited(server: RPCServer, domainName: String)
case networkConnectionWasLost case networkConnectionWasLost
case invalidCertificate case invalidCertificate
case requestTimedOut case requestTimedOut

@ -22,7 +22,8 @@ extension Session {
closure { error in closure { error in
guard !isCanceled else { return } guard !isCanceled else { return }
if let error = error { if let error = error {
if let e = convertToUserFriendlyError(error: error, baseUrl: URL(string: "http:/google.com")!) { let server = RPCServer.main
if let e = convertToUserFriendlyError(error: error, server: server, baseUrl: URL(string: "http:/google.com")!) {
seal(.failure(.requestError(e))) seal(.failure(.requestError(e)))
} else { } else {
seal(.failure(error)) seal(.failure(error))

Loading…
Cancel
Save