Skip to content

Conversation

@martinserts
Copy link
Contributor

@martinserts martinserts commented Dec 16, 2025

Description

Adds /v1/payment-batches API endpoint to create instant batch.
It may contain up to 100 recipients, which will be included in the same transaction (batch).
This batch is created instantly, and will not be part of batching worker.

Closes: #8

Motivation and Context

Up until now, users could create payments specifying amount and recipient.
After that all newly created payments where processed by batching worker, which gathered them into batches on certain invervals.

This change allows to create instant batches.

How Has This Been Tested?

Invalid account name

curl -X 'POST' \
  'http://localhost:9145/v1/payment-batches' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "account_name": "unknown",
  "items": [
    {
      "amount": 1000000,
      "client_id": "first",
      "payment_id": "none",
      "recipient_address": "f23tYJkf4QZfCS4kGSXJug1wUokDN3gEixfN6XXYpjUfhsKwugBcNU8mrRoSv82gMv72wbH79DAp3LvGdsCPSgCkE5B"
    }
  ]
}'
{
  "error": "Account 'unknown' not found in configuration"
}

Empty batch

curl -X 'POST' \
  'http://localhost:9145/v1/payment-batches' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "account_name": "default",
  "items": [
  ]
}
{
  "error": "Batch cannot be empty"
}

Successful instant batch

curl -X 'POST' \
  'http://localhost:9145/v1/payment-batches' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "account_name": "default",
  "items": [
    {
      "amount": 1000001,
      "client_id": "first",
      "payment_id": "1",
      "recipient_address": "f23tYJkf4QZfCS4kGSXJug1wUokDN3gEixfN6XXYpjUfhsKwugBcNU8mrRoSv82gMv72wbH79DAp3LvGdsCPSgCkE5B"
    },
    {
      "amount": 1000002,
      "client_id": "second",
      "payment_id": "2",
      "recipient_address": "f2MWPuubAoeJyhgeNqaPTG4xXeo4rjtSFWLcXysboCwC2ZAJuD4pb7Ldkd9BCFACf77K4RiEufCi7e6D52fQEnTR3CA"
    },
    {
      "amount": 1000003,
      "client_id": "third",
      "payment_id": "3",
      "recipient_address": "f2LUUip8F3LCQ6PyJvQiySCEpQyzTQSfvQF4YX3N9pY4rz4gAUprFgaL9JANYinYN2HGtKexmfX2odeNHfatoNhozaL"
    }
  ]
}'
{
  "batch_id": "1e942c3e-f1fb-4e2e-a52a-57b2297d37dd",
  "account_name": "default",
  "status": "PENDING_BATCHING",
  "payments": [
    {
      "payment_id": "60e14dc6-ae5e-4b8c-94db-db2607e9821f",
      "status": "BATCHED",
      "client_id": "first",
      "account_name": "default",
      "recipient_address": "f23tYJkf4QZfCS4kGSXJug1wUokDN3gEixfN6XXYpjUfhsKwugBcNU8mrRoSv82gMv72wbH79DAp3LvGdsCPSgCkE5B",
      "amount": 1000001,
      "created_at": "2025-12-16T12:57:23Z",
      "updated_at": "2025-12-16T12:57:23Z"
    },
    {
      "payment_id": "fa51e062-3af7-47d5-bb2e-3b3cb900cf50",
      "status": "BATCHED",
      "client_id": "second",
      "account_name": "default",
      "recipient_address": "f2MWPuubAoeJyhgeNqaPTG4xXeo4rjtSFWLcXysboCwC2ZAJuD4pb7Ldkd9BCFACf77K4RiEufCi7e6D52fQEnTR3CA",
      "amount": 1000002,
      "created_at": "2025-12-16T12:57:23Z",
      "updated_at": "2025-12-16T12:57:23Z"
    },
    {
      "payment_id": "8a96fdb1-e4d8-4079-acf8-ded819c74912",
      "status": "BATCHED",
      "client_id": "third",
      "account_name": "default",
      "recipient_address": "f2LUUip8F3LCQ6PyJvQiySCEpQyzTQSfvQF4YX3N9pY4rz4gAUprFgaL9JANYinYN2HGtKexmfX2odeNHfatoNhozaL",
      "amount": 1000003,
      "created_at": "2025-12-16T12:57:23Z",
      "updated_at": "2025-12-16T12:57:23Z"
    }
  ]
}

Idempotency

Posted the request (that is above AGAIN), it returned the same and did not create new batch.

What process can a PR reviewer use to test or verify this change?

Breaking Changes

  • None
  • Requires data directory on base node to be deleted
  • Requires hard fork
  • Other - Please specify

@martinserts martinserts self-assigned this Dec 16, 2025
@gemini-code-assist
Copy link

Summary of Changes

Hello @martinserts, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a new API endpoint /v1/payment-batches that allows users to create "instant batches" of payments. Unlike the existing system where individual payments are batched by a worker at intervals, this new endpoint enables immediate processing of up to 100 payments as a single transaction. The change provides more control over payment timing and includes robust validation and idempotency checks to prevent duplicate batch creations.

Highlights

  • New Instant Batch API: A new /v1/payment-batches API endpoint has been introduced, allowing for the creation of instant payment batches.
  • Batch Size Limit: Instant batches can contain up to 100 recipients, which will be included in the same transaction.
  • Bypasses Batching Worker: These instant batches are created immediately and do not participate in the regular batching worker's processing cycle.
  • Idempotency Support: The new API endpoint handles idempotency, ensuring that re-submitting the same batch request will return the existing batch without creating duplicates.
  • New Data Models: New data structures, BulkPaymentItem, BulkPaymentRequest, and BulkPaymentResponse, have been added to support the bulk payment API.
  • Centralized Batch Size Constant: The MAX_BATCH_SIZE constant has been moved to src/lib.rs for global access and consistency across the application.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new API endpoint /v1/payment-batches for creating instant payment batches. The implementation is well-structured, including robust validation and idempotency checks. The changes also include a good refactoring of the MAX_BATCH_SIZE constant for better code organization.

My review includes two main points:

  1. A high-severity fix in the database layer to prevent a potential server panic caused by an .unwrap().
  2. A medium-severity suggestion to improve the maintainability and readability of the new API handler by making state synchronization more explicit.

Overall, this is a solid contribution that adds valuable functionality.

payment_ids_for_batch.push(new_payment.id.clone());
created_payments.push(new_payment);
}

Copy link

Choose a reason for hiding this comment

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

I might be misinterpreting the purpose here so please correct me.

I think the idempotentency key exists to ensure that multiple similar requests that come in aren't processed multiple times? But if we recreate a new key every time a request comes in we're not really performing any kind of check to prevent re-processing the same batch? It does look like we check for existing payments based entirely on the list of payees which makes the idempotency key superfluous and could also make for false positives. You should be able to create multiple transactions to the same group of addresses, i would think.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I might be misinterpreting the purpose here so please correct me.

I think the idempotentency key exists to ensure that multiple similar requests that come in aren't processed multiple times? But if we recreate a new key every time a request comes in we're not really performing any kind of check to prevent re-processing the same batch? It does look like we check for existing payments based entirely on the list of payees which makes the idempotency key superfluous and could also make for false positives. You should be able to create multiple transactions to the same group of addresses, i would think.

@brianp

You are right regarding idempotency, but there is a distinction here between the incoming request and the outgoing operation.

There are actually two different idempotency keys in this flow:

  1. client_id (Incoming): This is the key provided by the API consumer. We use this to prevent processing the same request from our clients twice. This is stored in payments table for each payment.
  2. pr_idempotency_key (Outgoing): This is the key we generate on line 267. We use this when acting as a client to the internal minatari_cli (Payment Receiver) (via the accounts_api::api_lock_funds call). This is stored in payment_batches table.

I think the idempotentency key exists to ensure that multiple similar requests that come in aren't processed multiple times?

Correct. The check for incoming duplicate requests happens before line 267.

In api_create_payment_batch, we extract the client_id from every item in the incoming request into item_client_ids. We then query the DB:

let existing_payments = Payment::find_by_client_ids(&mut tx, &item_client_ids, &request.account_name).await?;
  • If these IDs exist: that means this as a duplicate request. We return the existing batch immediately and never reach line 267 (so a new UUID is not generated).
  • If these IDs do not exist: We proceed to create new records.

It does look like we check for existing payments based entirely on the list of payees... You should be able to create multiple transactions to the same group of addresses.

We actually check based on the client_id (the unique string provided by the user for each payment), not the recipient address.

If you send a request to pay Bob 10 tari with client_id: "A", and then send a second request to pay Bob 10 tari with client_id: "B", the system sees "B" does not exist and allows the second payment.

But if we recreate a new key every time a request comes in...

Line 267 (Uuid::new_v4()) is only reached once we have confirmed this is a new, non-duplicate request.

We generate this UUID to ensure that if our worker crashes while talking to the minatari_cli (Payment Receiver), we can retry the lock_funds call safely using this stored UUID (pr_idempotency_key).

Copy link

Choose a reason for hiding this comment

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

We actually check based on the client_id (the unique string provided by the user for each payment), not the recipient address.

I feel like this clears it up the most. I think I was confusing the two and thought that client_id was the recipient address.

Thanks!

@brianp brianp merged commit 0bbbab7 into tari-project:main Dec 18, 2025
2 checks passed
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.

Batched mode for payment

2 participants