Skip to content

Commit

Permalink
- update error handling to display nicer messages for some explicit c…
Browse files Browse the repository at this point in the history
…ases

- add extra test to catch another case
- update existing strings
  • Loading branch information
simonmcl committed Jul 16, 2024
1 parent a975365 commit 2935cd7
Show file tree
Hide file tree
Showing 5 changed files with 414 additions and 16 deletions.
63 changes: 59 additions & 4 deletions Sources/KukaiCoreSwift/Services/ErrorHandlingService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ public struct KukaiError: CustomStringConvertible, Error {
switch errorType {
case .rpc:
if let rpcErrorString = rpcErrorString {
return "RPC: \(rpcErrorString.removeLeadingProtocolFromRPCError() ?? rpcErrorString)"
return rpcErrorString
}
return "RPC: Unknown"

Expand Down Expand Up @@ -238,9 +238,13 @@ public class ErrorHandlingService {
/// Convert an `OperationResponseInternalResultError` into a `KukaiError` and optionally log it to the central logger
public static func fromOperationError(_ opError: OperationResponseInternalResultError, requestURL: URL?, andLog: Bool = true) -> KukaiError {
let errorWithoutProtocol = opError.id.removeLeadingProtocolFromRPCError()
var errorToReturn = KukaiError(errorType: .rpc, knownErrorMessage: nil, subType: nil, rpcErrorString: errorWithoutProtocol, failWith: nil, requestURL: nil, requestJSON: nil, responseJSON: nil, httpStatusCode: nil)
var errorToReturn = KukaiError(errorType: .rpc, knownErrorMessage: nil, subType: nil, rpcErrorString: "RPC error code: \(errorWithoutProtocol ?? "Unknown")", failWith: nil, requestURL: nil, requestJSON: nil, responseJSON: nil, httpStatusCode: nil)

if (errorWithoutProtocol == "michelson_v1.runtime_error" || errorWithoutProtocol == "michelson_v1.script_rejected"), let withError = opError.with {

if let result = knownRPCErrorString(rpcStringWithoutLeadingProtocol: errorWithoutProtocol, with: opError.with) {
errorToReturn = KukaiError.rpcError(rpcErrorString: result, andFailWith: opError.with, requestURL: requestURL)

} else if (errorWithoutProtocol == "michelson_v1.runtime_error" || errorWithoutProtocol == "michelson_v1.script_rejected"), let withError = opError.with {

if let failwith = withError.int, let failwithInt = Int(failwith) {
// Smart contract failwith reached with an Int denoting an error code
Expand All @@ -265,6 +269,56 @@ public class ErrorHandlingService {
return errorToReturn
}

public static func knownRPCErrorString(rpcStringWithoutLeadingProtocol: String?, with: FailWith?) -> String? {
guard let rpcStringWithoutLeadingProtocol = rpcStringWithoutLeadingProtocol else { return nil }

switch rpcStringWithoutLeadingProtocol {

// top level protocol errors
case "implicit.empty_implicit_contract":
return "Your account has a 0 XTZ balance and is unable to pay for the fees to complete the transaction."

case "tez.subtraction_underflow":
return "Your account does not have enough XTZ to complete the transaction."


// known michelson contract related errors (e.g. insufficent balance for a known token standard)
// can appear in 2 places
case "michelson_v1.script_rejected":
if let string = with?.string {
return compareInnerString(string)

} else if let string = with?.args?.first?["string"] {
return compareInnerString(string)

} else {
return nil
}

default:
return nil
}
}

private static func compareInnerString(_ remoteString: String?) -> String? {
switch remoteString?.lowercased() {
case "fa2_insufficient_balance":
return "Insufficient NFT/DeFi token balance to complete the transaction"

case "fa1.2_insufficientbalance":
return "Insufficient DeFi token balance to complete the transaction"

case "notenoughbalance":
return "Insufficient token balance to complete the transaction"

case .none:
return nil

case .some(_):
return nil
}
}

/// Search an `OperationResponse` to see does it contain any errors, if so return the last one as a `KukaiError` and optionally log it to the central logger
public static func searchOperationResponseForErrors(_ opResponse: OperationResponse, requestURL: URL?, andLog: Bool = true) -> KukaiError? {
if let lastError = opResponse.errors().last {
Expand Down Expand Up @@ -311,7 +365,8 @@ public class ErrorHandlingService {
let lastError = errorArray.last,
let errorString = lastError.id.removeLeadingProtocolFromRPCError()
{
errorToReturn = KukaiError.rpcError(rpcErrorString: errorString, andFailWith: lastError.with, requestURL: requestURL)
let updatedString = knownRPCErrorString(rpcStringWithoutLeadingProtocol: errorString, with: lastError.with)
errorToReturn = KukaiError.rpcError(rpcErrorString: updatedString ?? "RPC error code: \(errorString)", andFailWith: lastError.with, requestURL: requestURL)
} else {
errorToReturn.addNetworkData(requestURL: requestURL, requestJSON: requestData, responseJSON: data, httpStatusCode: httpResponse.statusCode)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ class TezosNodeClientTests: XCTestCase {
XCTFail("Should have failed, got opHash instead")

case .failure(let error):
XCTAssert(error.description == "RPC: contract.counter_in_the_future", error.description)
XCTAssert(error.description == "RPC error code: contract.counter_in_the_future", error.description)
}

expectation.fulfill()
Expand Down
33 changes: 23 additions & 10 deletions Tests/KukaiCoreSwiftTests/Services/ErrorHandlingServiceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ class ErrorHandlingServiceTests: XCTestCase {
func testStaticConstrutors() {
let error1 = KukaiError.rpcError(rpcErrorString: "testing RPC string", andFailWith: nil, requestURL: nil)
XCTAssert(error1.rpcErrorString == "testing RPC string", error1.rpcErrorString ?? "-")
XCTAssert(error1.description == "RPC: testing RPC string", error1.description)
XCTAssert(error1.description == "testing RPC string", error1.description)

let error2 = KukaiError.rpcError(rpcErrorString: "testing RPC string", andFailWith: FailWith(string: nil, int: "1", args: nil), requestURL: nil)
XCTAssert(error2.rpcErrorString == "testing RPC string", error2.rpcErrorString ?? "-")
XCTAssert(error2.description == "RPC: testing RPC string", error2.description)
XCTAssert(error2.description == "testing RPC string", error2.description)

let error3 = KukaiError.unknown(withString: "test unknown string")
XCTAssert(error3.rpcErrorString == "test unknown string", error3.rpcErrorString ?? "-")
Expand Down Expand Up @@ -105,8 +105,8 @@ class ErrorHandlingServiceTests: XCTestCase {

let containsErrors1 = ErrorHandlingService.searchOperationResponseForErrors(ops, requestURL: nil)
XCTAssert(containsErrors1?.errorType == .rpc)
XCTAssert(containsErrors1?.rpcErrorString == "gas_exhausted.operation", containsErrors1?.rpcErrorString ?? "-")
XCTAssert(containsErrors1?.description == "RPC: gas_exhausted.operation", containsErrors1?.description ?? "-")
XCTAssert(containsErrors1?.rpcErrorString == "RPC error code: gas_exhausted.operation", containsErrors1?.rpcErrorString ?? "-")
XCTAssert(containsErrors1?.description == "RPC error code: gas_exhausted.operation", containsErrors1?.description ?? "-")
}

func testOperationResponseParserFailWith() {
Expand All @@ -127,7 +127,7 @@ class ErrorHandlingServiceTests: XCTestCase {
let containsErrors1 = ErrorHandlingService.searchOperationResponseForErrors(ops, requestURL: nil)
XCTAssert(containsErrors1?.errorType == .rpc)
XCTAssert(containsErrors1?.rpcErrorString == "A FAILWITH instruction was reached: {\"int\": 14}", containsErrors1?.rpcErrorString ?? "-")
XCTAssert(containsErrors1?.description == "RPC: A FAILWITH instruction was reached: {\"int\": 14}", containsErrors1?.description ?? "-")
XCTAssert(containsErrors1?.description == "A FAILWITH instruction was reached: {\"int\": 14}", containsErrors1?.description ?? "-")
}

func testJsonResponse() {
Expand All @@ -139,8 +139,8 @@ class ErrorHandlingServiceTests: XCTestCase {
}

let result2 = ErrorHandlingService.searchOperationResponseForErrors(opResponse, requestURL: nil)
XCTAssert(result2?.rpcErrorString == "gas_exhausted.operation", result2?.rpcErrorString ?? "-")
XCTAssert(result2?.description == "RPC: gas_exhausted.operation", result2?.description ?? "-")
XCTAssert(result2?.rpcErrorString == "RPC error code: gas_exhausted.operation", result2?.rpcErrorString ?? "-")
XCTAssert(result2?.description == "RPC error code: gas_exhausted.operation", result2?.description ?? "-")

}

Expand All @@ -154,7 +154,7 @@ class ErrorHandlingServiceTests: XCTestCase {

let result = ErrorHandlingService.searchOperationResponseForErrors(opResponse, requestURL: nil)
XCTAssert(result?.rpcErrorString == "A FAILWITH instruction was reached: {\"string\": Dex/wrong-min-out}", result?.rpcErrorString ?? "-")
XCTAssert(result?.description == "RPC: A FAILWITH instruction was reached: {\"string\": Dex/wrong-min-out}", result?.description ?? "-")
XCTAssert(result?.description == "A FAILWITH instruction was reached: {\"string\": Dex/wrong-min-out}", result?.description ?? "-")
}

func testNotEnoughBalanceResponse() {
Expand All @@ -166,8 +166,21 @@ class ErrorHandlingServiceTests: XCTestCase {
}

let result = ErrorHandlingService.searchOperationResponseForErrors(opResponse, requestURL: nil)
XCTAssert(result?.rpcErrorString == "A FAILWITH instruction was reached: {\"args\": [[\"string\": \"NotEnoughBalance\"]]}", result?.rpcErrorString ?? "-")
XCTAssert(result?.description == "RPC: A FAILWITH instruction was reached: {\"args\": [[\"string\": \"NotEnoughBalance\"]]}", result?.description ?? "-")
XCTAssert(result?.rpcErrorString == "Insufficient token balance to complete the transaction", result?.rpcErrorString ?? "-")
XCTAssert(result?.description == "Insufficient token balance to complete the transaction", result?.description ?? "-")
}

func testInsufficientBalance() {
let errorData = MockConstants.jsonStub(fromFilename: "rpc_error_fa2_insufficient_balance")

guard let opResponse = try? JSONDecoder().decode(OperationResponse.self, from: errorData) else {
XCTFail("Couldn't parse data as [OperationResponse]")
return
}

let result = ErrorHandlingService.searchOperationResponseForErrors(opResponse, requestURL: nil)
XCTAssert(result?.rpcErrorString == "Insufficient NFT/DeFi token balance to complete the transaction", result?.rpcErrorString ?? "-")
XCTAssert(result?.description == "Insufficient NFT/DeFi token balance to complete the transaction", result?.description ?? "-")
}

func testFailWithParsers() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ class NetworkServiceTests: XCTestCase {

case .failure(let error):
XCTAssert(error.errorType == .rpc)
XCTAssert(error.description == "RPC: contract.counter_in_the_future", error.description)
XCTAssert(error.description == "RPC error code: contract.counter_in_the_future", error.description)
XCTAssert(error.requestURL?.absoluteString == MockConstants.shared.config.nodeURLs[1].appendingPathComponent("chains/main/blocks/head/helpers/preapply/operations").absoluteString, error.requestURL?.absoluteString ?? "-")
}

Expand Down
Loading

0 comments on commit 2935cd7

Please sign in to comment.