Skip to content

RFC: Transactions

Felipe Gomes edited this page Nov 8, 2024 · 11 revisions

Context

The Transactions domain is responsible for the entire lifecycle of transactions. This includes the creation of transactions, movements, and accounting flows, which are explained throughout the document. The entities that are part of the Transactions domain are:

Transaction: the core of the ledger, representing the grouping of balance moved from account A to account B, and also allowing for n operations. For example, if the user specifies that the accounts will pay R$3 in fees during a P2P transaction, the transaction in question would have an origin (the account of the user who sent the balance) and two destinations (the account of the user who received the balance and the account that holds the transaction fee amounts).

Operation: the atomic operation of the ledger, being unilateral. In the example of "Transaction", there would be three operations: one debit in the origin and two credits, one in the destination account and another in the transaction fees deposit account.

Chart-of-accounts: defines the flows of accounting routes and the settlement of values in the ledger. The chart of accounts can be understood as a 1:1 tag with each leg of the transaction (the operations) and is grouped by a chart-of-accounts-group.

Components

The common components of a transaction are shown below.

Alias

Accounts can have aliases linked to them, making it easier to receive a transaction. One application could be linking user A's account to their Instagram handle (for example, @jhondoe), so in transactions, instead of sending the account ID 3172933b-50d2-4b17-96aa-9b378d6a6eac, you can simply send @jhondoe.

Asset

Defines what is being moved in that specific operation. The asset being moved in a transaction should use its code, such as BRL, BTC, etc. The asset must also be active on the platform, or an error will be returned.

If a transaction is going to involve more than one asset (for example, debiting BRL and sending USD), the values will be obtained from the assets_rates table.

Value

The amount being moved in the asset in question. It's important to note that Midaz uses a definition of scale transaction by transaction. For instance, for a grocery purchase in BRL, it makes sense to use BRL with a base of 2 (10^2 = 100), with a minimum value of BRL 0.01. However, at gas stations, a base of 4 (10^4 = 1000) is typically used, with a minimum value of BRL 0.0001.

The value must be sent in the format ASSET VALUE|SCALE, and will be calculated as: VALUE * 10^-SCALE. For example: BRL 25|4 = 25 * 10^-4 = 0.0025 BRL.

The values to be debited and credited for the origin(s) and destination(s) can be specified as:

  • Gross value: direct definition of a value according to the scale. Example: 10;

  • Percentage value : sending a percentage of balance.available. Example: 27.5% for tax payment. Always sent as :share 90, for example, meaning 90%. If it's a percentage of a percentage, use :share 90 of 25, meaning 90% of 25% (or 22.5%)

  • Remaining value(remaining) : sending part of the value to account A and the remaining value to account B (for debits and credits).

Transaction Flow

When a transaction is initiated, it first checks if the source account has sufficient balance in the asset being moved, as well as validating whether the statuses of the source and destination accounts allow them to send and receive transactions (allowSending, allowReceiving), respectively. If both conditions are met, the amount is moved from the balance.available of the source account to the destination account. This transaction occurs synchronously, and in the response, if successful, the status will be APPROVED. Therefore, in this scenario, the user should initiate the transaction only if they are certain they want to record the transaction in the ledger.

If the user prefers, a pre-transaction can be initiated, where the balance of the source account is moved from balance.available to balance.on_hold, and its status is changed to PRE_APPROVED. This is often interesting if the user wants to leave all transactions in the ledger, even if they might eventually not be approved due to some external validation the user performed. Additionally, the pre-transaction scenario also serves as a way to validate if any flow was broken during the day, for instance, by checking if there are any transactions with a status of PRE_APPROVED at the end of the day. When the user wants to send the balance to the destination account, they should call POST /transactions/{{transaction_id}}/commit, which will change the status to APPROVED if the statement can still be satisfied (basically, if both accounts can participate in the transaction).

If the user wants to cancel a pre-transaction, its status would change from PRE_APPROVED to CANCELED, and the amount in balance.on_hold of the source account will be returned to balance.available of the source account with a new transaction generated.

The last interesting feature for launching transactions is the ability to reverse a transaction if it no longer makes sense. It is important to note that, given that Midaz is an immutable ledger, a transaction cannot be canceled after it has occurred. What happens when using POST /transactions/{{transaction_id}}/revert is that the same statement from the original transaction is executed, but with the source and destination inverted. In this case, the reversal transaction would be created with the parameter parentTransactionId equal to the ID of the transaction that originated it. Both transactions would have the status APPROVED at the end.

Warning

The use of revert should be according to the specific business rules for the transaction. Using revert to reverse any and all transactions can create accounting problems for the user. For example, if a TED (bank transfer) is initiated and the sender pays R$2 in fees, if they provided incorrect banking details, using revert to return the TED amount after the processing error would also return the paid fee, which may not be the expected use case.

Value and Scale

To avoid rounding errors, we will always save transactions in the database considering the scale in which they were made, and when returning the balance, we will return the balance in the smallest possible scale. For example:

  • Transaction 1 - BRL 1000|4 (BRL 0.1): would be saved in the database with scale = 4 and amount = 1000;

  • Transaction 2 - BRL 2000|5 (BRL 0.02): would be saved in the database with scale = 5 and amount = 2000;

  • Transaction 3 - BRL 10|0 (BRL 10): would be saved in the database with scale = 0 and amount = 10;

  • Transaction 4 - BRL 100|1 (BRL 10): would be saved in the database with scale = 1 and amount = 100;

  • Transaction 5 - BRL 30|3 (BRL 0.03): would be saved in the database with scale = 3 and amount = 30;

The transactions in the transactions (and operations) table would be saved as shown above, and in the balance saved in the accounts table, we would always use the smallest possible scale. For example, in the first transaction, the smallest scale is 4, so we would return the total balance (considering only transaction 1) using scale 4. When the second transaction occurs, since the smallest scale has changed to 5, we will return the balance (saved in the accounts table) using scale 5 and considering transactions 1 and 2 (all that have occurred up to that point). Since the third transaction has a scale 0, we will continue saving the balance in the accounts table using scale 5, as it was the smallest scale used up to that moment.

Features

Inflows and outflows of the Ledger

As Midaz is designed with double-entry, the inflows and outflows of the Ledger must be made through the account @external/{{assetCode}}, which represents the connection to the outside world. For example, in the scenario of a PIX-out from account A of bank A to account B of bank B, we have:

Source Destination Amount
@accountId (Bank A) @external/BRL BRL 1000|0
@external/BRL @accountId (Bank B) BRL 1000|0

In other words, BRL 1000|0 is sent from account A of bank A to the account @external/BRL. The flow in the ledger of bank A stops there, and the transaction is settled via SPI (in the example of a PIX). Then, bank B receives such notification to credit account B, and the amount is moved from the account @external/BRL to the destination account of the PIX.

Warning

The account @external/{{assetCode}} can have a negative or zero balance, but never a positive balance (which would indicate that more assets were sent than exist in the ledger). Furthermore, its balance will always equal the negative of the sum of the balances of all accounts in the ledger.

Note

In the asset creation flow, if it does not exist, we always create the account @external/{{assetCode}}.

Source

The origin of the transaction can have two types: single-source and multi-source.

Important

The sum of the values in the source must always equal the value specified right after the send and equal the sum of the values in the distribute.

Single source

The simplest format for the origin, removing the value from the available.balance of the source and sending it to the destination (distribute).

Example:

(transaction v1
  (send BRL 30|4 
    (source
      (from @external/BRL :amount BRL 30|4)
  )
 )
  (distribute
    (to @destinationAccount :share 100)
  )
)

Multi-source

In this format, the value being sent is drawn from multiple source accounts. It is in transactions with multiple origins that it makes sense to use the other notations for values (:share, :remaining).

The behavior of the transaction below will seek the value of BRL 0.0015 from the account @John_Doe and the value of BRL 0.0015 from the account @Jane_Doe, totaling BRL 0.0030 to be sent to the account @Jane_Son.

Example:

(transaction v1
  (send BRL 30|4 
    (source
      (from @John_Doe :amount BRL 15|4),
      (from @Jane_Doe :amount BRL 15|4)
  )
 )
  (distribute
    (to @Jane_Son :share 100)
  )
)

Destination

Just like the origin, the destination of the transaction can have two types: single destination and multi-destination.

Important

The sum of the values in the source must always equal the value specified immediately after the send and equal the sum of the values in the distribute.

Single destination

The simplest format for the destination, removing from the available.balance of the source and sending to a single destination (distribute). It is the same as the model presented for "Single source."

Example:

(transaction v1
  (send BRL 30|4 
    (source
      (from @external/BRL :amount BRL 30|4)
  )
 )
  (distribute
    (to @destinationAccount :share 100)
  )
)

Multi-destination

In this model, the value sent is distributed among multiple destination accounts. It is in transactions with multiple destinations that it makes sense to use the other notations for values (:share, :remaining).

It is also possible to define where each portion of the total value should be directed. In the example below, the BRL 0.0030 sent from the account @sourceAccount will be distributed as follows: 38% to the account @John (BRL 0.0030 * 0.38 = 0.00114), 50% to the account @Joe (BRL 0.0030 * 0.50 = 0.0015), BRL 2|4 (BRL 0.0002) will be sent to the account @Mary, and the remainder (0.030 - 0.00114 - 0.0015 - 0.0002 = BRL 0.00016) will be sent to the account @Emma.

Example:

(transaction v1
  (send BRL 30|4 
    (source
      (from @sourceAccount :share 100)
  )
 )
  (distribute
    (to @John :share 38)
    (to @Joe :share 50)
    (to @Mary :amount BRL 2|4)
    (to @Emma :remaining)
  )
)

Multi-source e multi-destination

In this model, the value sent is drawn from multiple source accounts, and the distribution is also made to multiple destinations.

An example of a multi-source and multi-destination transaction would be a crowdfunding campaign set up to distribute money to various families affected by a flood. In the example below, a transaction of BRL 4000 would be sent with the amount coming from different sources and going to different destinations, according to specific weights (which have been explained in the previous examples).

Example:

(transaction v1
  (send BRL 400000|2 
    (source
      (from @account1 :share 25)		
      (from @account2 :share 25)
      (from @account3 :share 40)
      (from @account4 :share 10)
    )
  )
  (distribute
    (to @donation1 :share 25)
    (to @donation2 :share 25)
    (to @donation3 :share 25)
    (to @donation4 :share 25)
  )
)

Data

Data modeling

rfc_transactions_database