Skip to content

Conversation

@Chengxuan
Copy link
Contributor

@Chengxuan Chengxuan commented Dec 8, 2025

This PR introduces a new /batch endpoint that allows clients to submit multiple transactions and contract deployment requests in a single HTTP call. This significantly improves throughput and reduces overhead for bulk operations.

Users can now submit multiple SendTransaction and DeployContract requests in a single API call

All valid transactions in a batch are persisted in a single database transaction (for transactions with the same signer).

API Usage

POST /batch

{
  "requests": [
    {
      "headers": { "id": "tx1", "type": "SendTransaction" },
      "from": "0x123...",
      "to": "0x456...",
      "method": {...},
      "params": [...]
    },
    {
      "headers": { "id": "tx2", "type": "SendTransaction" },
      ...
    },
    {
      "headers": { "id": "deploy1", "type": "DeployContract" },
      ...
    }
  ]
}

Response:

{
  "responses": [
    {
      "id": "tx1",
      "success": true,
      "output": { /* ManagedTX */ }
    },
    {
      "id": "tx2",
      "success": false,
      "error": {
        "error": "transaction operation invalid",
        "submissionRejected": true
      }
    }
  ]
}

Signed-off-by: Chengxuan Xing <[email protected]>
@Chengxuan Chengxuan requested a review from a team as a code owner December 8, 2025 14:49
Comment on lines +20 to +22
type BatchRequest struct {
Requests []*BaseRequest `json:"requests"`
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The proposal uses a top-level requests attribute to group the requests instead of accepting an array of requests directly.

This makes the input payload extensible if we want to add batch options in the feature.

Same thinking on the response object.

@Chengxuan
Copy link
Contributor Author

Chengxuan commented Dec 10, 2025

Example input:

{
  "requests": [
    {
      "to": "0x9948f2b29cade89eb69ebfbd8614781e9fa87fff",
      "headers": {
        "id": "perf-test:41d6c1ef-2b20-4e60-8888-d8be79ee2313",
        "type": "SendTransaction"
      },
      "method": {
        "type": "function",
        "name": "transfer",
        "stateMutability": "nonpayable",
        "inputs": [
          { "name": "to", "type": "address", "internalType": "address" },
          { "name": "value", "type": "uint256", "internalType": "uint256" }
        ],
        "outputs": [{ "name": "", "type": "bool", "internalType": "bool" }]
      },
      "params": ["0x2a131459e0af1d97e942f52e9573ed5a35d6db96", 1],
      "from": "0x2a131459e0af1d97e942f52e9573ed5a35d6db96"
    },
    {
      "to": "0x9948f2b29cade89eb69ebfbd8614781e9fa87fff",
      "headers": {
        "id": "perf-test:41d6c1ef-2b20-4e60-8888-d8be79ee1328",
        "type": "SendTransaction"
      },
      "method": {
        "type": "function",
        "name": "transfer",
        "stateMutability": "nonpayable",
        "inputs": [
          { "name": "to", "type": "address", "internalType": "address" },
          { "name": "value", "type": "uint256", "internalType": "uint256" }
        ],
        "outputs": [{ "name": "", "type": "bool", "internalType": "bool" }]
      },
      "params": ["0x2a131459e0af1d97e942f52e9573ed5a35d6db96", 1],
      "from": "0x2a131459e0af1d97e942f52e9573ed5a35d6db96"
    }
  ]
}

Example output:

{
  "responses": [
    {
      "id": "perf-test:41d6c1ef-2b20-4e60-8888-d8be79ee2313",
      "success": true,
      "output": {
        "id": "perf-test:41d6c1ef-2b20-4e60-8888-d8be79ee2313",
        "created": "2025-12-10T14:45:31.3789791Z",
        "updated": "2025-12-10T14:45:31.3789791Z",
        "status": "Pending",
        "sequenceId": "000000043926",
        "from": "0x2a131459e0af1d97e942f52e9573ed5a35d6db96",
        "to": "0x9948f2b29cade89eb69ebfbd8614781e9fa87fff",
        "nonce": "43924",
        "gas": "44776",
        "gasPrice": 0,
        "transactionData": "0xa9059cbb0000000000000000000000002a131459e0af1d97e942f52e9573ed5a35d6db960000000000000000000000000000000000000000000000000000000000000001",
        "policyInfo": null
      }
    },
    {
      "id": "perf-test:41d6c1ef-2b20-4e60-8888-d8be79ee1328",
      "success": true,
      "output": {
        "id": "perf-test:41d6c1ef-2b20-4e60-8888-d8be79ee1328",
        "created": "2025-12-10T14:45:31.3789848Z",
        "updated": "2025-12-10T14:45:31.3789848Z",
        "status": "Pending",
        "sequenceId": "000000043927",
        "from": "0x2a131459e0af1d97e942f52e9573ed5a35d6db96",
        "to": "0x9948f2b29cade89eb69ebfbd8614781e9fa87fff",
        "nonce": "43925",
        "gas": "44776",
        "gasPrice": 0,
        "transactionData": "0xa9059cbb0000000000000000000000002a131459e0af1d97e942f52e9573ed5a35d6db960000000000000000000000000000000000000000000000000000000000000001",
        "policyInfo": null
      }
    }
  ]
}

Mixed failure and success:

{
  "responses": [
    {
      "id": "perf-test:41d6c1ef-2b20-4e60-8888-111111111111",
      "success": true,
      "output": {
        "id": "perf-test:41d6c1ef-2b20-4e60-8888-111111111111",
        "created": "2025-12-10T14:48:56.596206946Z",
        "updated": "2025-12-10T14:48:56.596206946Z",
        "status": "Pending",
        "sequenceId": "000000043929",
        "from": "0x2a131459e0af1d97e942f52e9573ed5a35d6db96",
        "to": "0x9948f2b29cade89eb69ebfbd8614781e9fa87fff",
        "nonce": "43927",
        "gas": "44776",
        "gasPrice": 0,
        "transactionData": "0xa9059cbb0000000000000000000000002a131459e0af1d97e942f52e9573ed5a35d6db960000000000000000000000000000000000000000000000000000000000000001",
        "policyInfo": null
      }
    },
    {
      "id": "perf-test:41d6c1ef-2b20-4e60-8888-d8be79ee1328",
      "success": false,
      "error": {
        "error": "FF21065: ID 'perf-test:41d6c1ef-2b20-4e60-8888-d8be79ee1328' is not unique"
      }
    }
  ]
}

Signed-off-by: Chengxuan Xing <[email protected]>
Signed-off-by: Chengxuan Xing <[email protected]>
Signed-off-by: Chengxuan Xing <[email protected]>
deployReqs = append(deployReqs, &tReq)
deployIndices = append(deployIndices, i)

default:
Copy link
Contributor

Choose a reason for hiding this comment

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

I do think it's a little confusing that:

  • We have a separate /batch API ...
  • ... but we don't support batch submission of all the types (only SendTransaction and DeployContract)
  • ... and we don't actually execute a parallel batch across those

Can you elaborate on the reason for not doing either of:

  1. Having two new payload types on the existing / RPC-style payload like DeployContractBatch and SendTransactionBatch
  2. Just having a Batch type on / that supports any set of the existing types

Copy link
Contributor Author

@Chengxuan Chengxuan Dec 12, 2025

Choose a reason for hiding this comment

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

@peterbroadhurst that's a great question.

There are 3 design considerations:

Introduce new types in / vs introduce a /batch API.

The latter is my preferred pick because it does not further complicate the schema of the API, here is how the schema look like for / today, introduces a batch spelling inside it will be messy:
image

Why is the execution order of batch not cross-type?

For example, if the input is:

  • SendTransactionBatch 1, DeployContractBatch 2, SendTransactionBatch 3, SendTransactionBatch 4

Why the execution order is not 1, 2, 3, 4 but 1, 3, 4, 2 instead?

I think my implementation was influeced by how the existing code is structured. 1, 2, 3, 4 is actually the preferred order. Especially given that contract deployment can generate a deterministic address, so that putting contract deployment and its invocations in the same batch is valid.

I'll fix the code to use the correct order This will change the txHandler interface to have a generic batch API interface. Unlike the existing singular methods, which spell HandleNewTransaction and HandleNewContractDeployment as separate functions, other than a single generic function (e.g. HandleTransaction that's type aware.)

Why doesn't batch support query or receipt checking?

Because conveying the meaning of their orders is tricky.

Unlike the batching behaviour in Ethereum API (following JSON-RPC spec, in which the items are not guaranteed to be executed in the input order), the introduced /batch API uses the order of the input to calculate nonce (which enables order preserving despite the parallel execution if batch is used in JSON-RPC API).

Despite keeping the order for queries might not make a huge difference in the result, because transactions are processed asynchronously, it is still hard to explain the execution order differences.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is a great write-up. Thank you

I wonder given all of the complexity above, I personally feel:

  • You've fully convinced me we should scope batches to things that can be ordered correctly in a batch-insert
  • The user should decide the order, not us. They know their transactions, not us.

So actually this leads me to question - why do we want separate SendTransaction and DeployContract types at all?

Can we not:

  1. Make the batch payload to include the combined fields of SendTransaction and DeployContract
  2. Make all the entries in the batch the same - no type on the headers
  3. Call the API /batchsubmit

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.

2 participants