Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support batching multiple RPC requests #187

Open
wants to merge 44 commits into
base: main
Choose a base branch
from

Conversation

franciszekjob
Copy link
Collaborator

@franciszekjob franciszekjob commented Apr 25, 2024

Describe your changes

Suport batching multiple RPC requests

  • add StarknetRequest and StarknetBatchRequest classes
  • add in StarknetProvider.batchRequests()

Linked issues

Closes #186

Breaking changes

  • This issue contains breaking changes
  • StarknetAccount
    • executeV1(...) async throws -> StarknetInvokeTransactionResponse -> executeV1(...) async throws -> StarknetRequest<StarknetInvokeTransactionResponse, AddInvokeTransactionParams>
    • executeV3(...) async throws -> StarknetInvokeTransactionResponse -> executeV3(...) async throws -> StarknetRequest<StarknetInvokeTransactionResponse, AddInvokeTransactionParams>
    • executeV1(...) async throws -> StarknetInvokeTransactionResponse -> executeV1(...) async throws -> StarknetRequest<StarknetInvokeTransactionResponse, AddInvokeTransactionParams>
    • executeV3(...) async throws -> StarknetInvokeTransactionResponse -> executeV3(...) async throws -> StarknetRequest<StarknetInvokeTransactionResponse, AddInvokeTransactionParams>
    • estimateFeeV1(...) async throws -> StarknetFeeEstimate -> estimateFeeV1(...) async throws -> StarknetRequest<[StarknetFeeEstimate], EstimateFeeParams>
    • estimateFeeV3(...) async throws -> StarknetFeeEstimate -> estimateFeeV3(...) async throws -> StarknetRequest<[StarknetFeeEstimate], EstimateFeeParams>
    • estimateDeployAccountFeeV1(...) async throws -> StarknetFeeEstimate -> estimateDeployAccountFeeV1(...) async throws -> StarknetRequest<[StarknetFeeEstimate], EstimateFeeParams>
    • estimateDeployAccountFeeV3(...) async throws -> StarknetFeeEstimate -> estimateDeployAccountFeeV3(...) async throws -> StarknetRequest<[StarknetFeeEstimate], EstimateFeeParams>
    • getNonce() async throws -> Felt -> getNonce() async throws -> StarknetRequest<Felt, GetNonceParams>


XCTAssertEqual(simulations2.count, 2)
XCTAssertTrue(simulations2[0].transactionTrace is StarknetInvokeTransactionTrace)
XCTAssertTrue(simulations2[1].transactionTrace is StarknetDeployAccountTransactionTrace)
}

func testBatchGetTransactionByHash() async throws {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Do we need to include more calls in this test?
  2. There should be a test for incorrect responses order. Imho it should be placed in a separate class eg. JsonRpcResponseTests as it focuses on the responses parsing mechanism.

Wdyt? @DelevoXDG

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I think that can be enough
  2. Yes, should add such test to JsonRpcResponseTests

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added test for incorrect responses order
Also getOrderedRpcResults() is now a separate internal function. It doesn't affect StarknetBatchRequest, but simplifies creating test for incorrect order.

}

public func estimateFeeV1(calls: [StarknetCall], nonce: Felt, skipValidate: Bool) async throws -> StarknetFeeEstimate {
let params = StarknetInvokeParamsV1(nonce: nonce, maxFee: .zero)
let signedTransaction = try signV1(calls: calls, params: params, forFeeEstimation: true)

return try await provider.estimateFee(for: signedTransaction, simulationFlags: skipValidate ? [.skipValidate] : [])
return try await provider.estimateFee(for: signedTransaction, simulationFlags: skipValidate ? [.skipValidate] : []).send()[0]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be return [0] here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, please look at the old implementation of calling estimateFee. We could directly access value at index 0, because the returned type here was [StarknetFeeEstimate]. Now it's Request<[StarknetFeeEstimate], EstimateFeeParams>, therefore first we need to send and then access [0] element.

Comment on lines 17 to 26
var orderedRpcResults: [Result<U, StarknetProviderError>?] = Array(repeating: nil, count: rpcPayloads.count)
for rpcResponse in rpcResponses {
if let error = rpcResponse.error {
orderedRpcResults[rpcResponse.id] = .failure(StarknetProviderError.jsonRpcError(error.code, error.message, error.data))
} else if let result = rpcResponse.result {
orderedRpcResults[rpcResponse.id] = .success(result)
} else {
orderedRpcResults[rpcResponse.id] = .failure(StarknetProviderError.unknownError)
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems like it could be really slow for longer response lists. I suppose we need ordering to properly deserialize? is there any other way?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to reorder the responses to match the order in which they were sent. This can be achieved with sorting O(nlogn) or direct placement O(n). Second solution was accepted and implemented in JVM SDK, where there is same situation, here.

Comment on lines 51 to 55
let result = try await sendBatch(payload: [payload], config: config, receive: [U.self])
guard let firstResult = result.first else {
throw HttpNetworkProviderError.unknownError
}
return firstResult
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why cannot we just send single request, why does it have to be sent as a batch?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I wanted to reuse sendBatch to reduce duplicated code. However, sending single request is more valid in the conceptual aspect. I've refactored it - now there are two send methods with different signatures. They send request with single json payload and array of jsons as payload respectively.

I've also added private methods for error handling to reduce duplicated code.

@franciszekjob franciszekjob marked this pull request as ready for review May 15, 2024 11:13
.gitignore Outdated Show resolved Hide resolved
@@ -106,7 +106,7 @@ public class StarknetAccount: StarknetAccountProtocol {
let params = StarknetInvokeParamsV1(nonce: nonce, maxFee: maxFee)
let signedTransaction = try signV1(calls: calls, params: params, forFeeEstimation: false)

return try await provider.addInvokeTransaction(signedTransaction)
return try await provider.addInvokeTransaction(signedTransaction).send()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not return Request in StarknetAccount methods as well?

Seems to me we might wanna batch multiple requests created by an account.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, they also use provider methods. I've updated StarknetAccount methods to return Request.

@@ -1,6 +1,6 @@
import Foundation

enum TransactionReceiptWrapper: Decodable {
public enum TransactionReceiptWrapper: Decodable {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this change necessary?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, TransactionReceiptWrapper is public because it's used in getTransactionReceiptBy()

Sources/Starknet/Network/BatchRequest.swift Outdated Show resolved Hide resolved
Sources/Starknet/Network/BatchRequest.swift Outdated Show resolved Hide resolved
Comment on lines 66 to 67
let rpcPayloads = getBatchRequestRpcPayloads(requests: requests)
let config = getHttpConfiguration()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we refactor batchRequests<U: Decodable, P: Encodable>(requests: Request<U, P>...) to just call batchRequests<U: Decodable, P: Encodable>(requests: [Request<U, P>]), we can get rid of both getBatchRequestRpcPayloads and getHttpConfiguration and inline their contents there.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I inlined content of getBatchRequestRpcPayloads there, but had to keep getHttpConfiguration, which is also used in buildRequest.

Sources/Starknet/Providers/StarknetProviderProtocol.swift Outdated Show resolved Hide resolved
@@ -5,7 +5,7 @@ import XCTest
final class ProviderTests: XCTestCase {
static var devnetClient: DevnetClientProtocol!

var provider: StarknetProviderProtocol!
var provider: StarknetProvider!
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

Maybe we should just add separate provider instance / cast for batch tests?

I don't have a strong opinion on this though. But the idea is to verify provider as a protocol can be used with no issues

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I restored var provider: StarknetProviderProtocol!, added separate batchProvider and used it in test for batchRequests.

Comment on lines 419 to 422
do {
let _ = try transactionsResponse[1].get().transaction.hash
XCTFail("Fetching transaction with nonexistent hash should fail")
} catch {}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can catch an error and verify if it's correct one

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I've added check for error type and error message 👍


XCTAssertEqual(simulations2.count, 2)
XCTAssertTrue(simulations2[0].transactionTrace is StarknetInvokeTransactionTrace)
XCTAssertTrue(simulations2[1].transactionTrace is StarknetDeployAccountTransactionTrace)
}

func testBatchGetTransactionByHash() async throws {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I think that can be enough
  2. Yes, should add such test to JsonRpcResponseTests

let nonce = try await account.getNonce()
let feeEstimate = try await account.estimateFeeV1(call: call, nonce: nonce)
let nonce = try await account.getNonce().send()
let feeEstimate = try await account.estimateFeeV1(call: call, nonce: nonce).send()[0]
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change of estimate fee return type resulted in having to access element [0] every time we use this metod. Unfortunately there isn't any other way, because first we need to send the request and the access its result.

@@ -30,16 +31,17 @@ final class ProviderTests: XCTestCase {
}

provider = makeStarknetProvider(url: Self.devnetClient.rpcUrl)
batchProvider = makeStarknetProvider(url: Self.devnetClient.rpcUrl)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: could the name variable name be improved?

Comment on lines +421 to +433
do {
let _ = try transactionsResponse[1].get().transaction.hash
XCTFail("Fetching transaction with nonexistent hash should fail")
} catch let error as StarknetProviderError {
switch error {
case let .jsonRpcError(_, message, _):
XCTAssertEqual(message, "Transaction hash not found", "Unexpected error message received")
default:
XCTFail("Expected JsonRpcError but received \(error)")
}
} catch {
XCTFail("Error was not a StarknetProviderError. Received error type: \(type(of: error))")
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really like the fact that there is double nesting here, but I don't any other way to access jsonRpcError message.

@franciszekjob
Copy link
Collaborator Author

franciszekjob commented May 16, 2024

All requested changes have been addressed, I've also added few remarks about newly applied changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support batching multiple RPC requests
3 participants