# Minka Docs — Full Content
## Table of Contents
- Ledger
- What is Minka Ledger
- Start Here
- Moving Money
- Structuring Data
- Securing the Ledger
- Connecting Systems
- Reporting
- Solutions
- Reference
- Release Notes
- RTP Switch
- What is the RTP Switch
- Integration Hub
- What is the Integration Hub
- Payment Experiences
- About Payment Experiences
# What is Minka Ledger (/ledger)
import { Rocket, ArrowLeftRight, Database, LockKeyhole, Plug, ChartBar, Lightbulb, BookOpen } from 'lucide-react'
Minka is an open network for publishing money. It connects balances across systems and ledgers, making it possible to move money instantly, securely, and at low cost.
Minka Ledger is the core infrastructure — a cloud-hosted ledger-as-a-service that provides a common language for representing balances and transactions. Organizations use a small set of primitives to express otherwise complex payment flows, and digital signatures ensure that every balance movement is authentic, auditable, and immutable.
Key benefits [#key-benefits]
* Express otherwise complex payment flows by combining a few basic ledger primitives
* Accelerate the creation of new payment services by modelling everything as ledger entries
* Enable business models like micropayments by streaming transactions to your ledger
* A simple API with a small number of endpoints that are easy to understand and integrate
* The right abstractions for money. Use the same low-level API calls for multiple high-level payment flows
* Integrate once, implement anything. Build multiple financial products on the same ledger integration
* Move balances between formerly separate systems
* Transact with remote ledgers by forwarding requests that look like calls to your local ledger
* Expose business functionality for other companies to consume using a clean and uniform interface
* Leverage the security properties of various cryptographic primitives from the Bitcoin protocol
* Ensure the integrity of transaction requests and records using cryptographic hashes
* Verify the authenticity of payment requests using public-key cryptography and digital signatures
* Cloud solution, hosted and managed by Minka
* Regular security updates delivered automatically
* New features released on regular basis
Explore the docs [#explore-the-docs]
} title="Start Here" description="Understand what a ledger is and how to begin building with it." href="/ledger/start-here" />
} title="Moving Money" description="Create transactions, manage wallets, and track balances." href="/ledger/moving-money" />
} title="Structuring Data" description="Organize records with custom fields, schemas, and layouts." href="/ledger/structuring-data" />
} title="Securing the Ledger" description="Control who can access and operate on your ledger." href="/ledger/securing-the-ledger" />
} title="Connecting Systems" description="Integrate external systems and move money across ledgers." href="/ledger/connecting-systems" />
} title="Reporting" description="Generate reports and export transaction data." href="/ledger/reporting" />
} title="Solutions" description="Build real-time payments, bank integrations, and clearinghouses." href="/ledger/solutions" />
} title="Reference" description="Look up CLI commands, SDK methods, and API endpoints." href="/ledger/reference" />
# Start Here (/ledger/start-here)
import { Info, Layers, Terminal } from 'lucide-react'
New to Minka Ledger? This section introduces the core ideas and walks you through your first interaction with the system.
} title="About Ledgers" description="Learn what a ledger is and how Minka uses it to represent money and balances." href="/ledger/start-here/about-ledgers" />
} title="Key Concepts" description="Understand records, signers, wallets, intents, and the other core primitives." href="/ledger/start-here/key-concepts" />
} title="Quickstart" description="Build your first ledger and make your first transaction using the CLI." href="/ledger/start-here/quickstart" />
What's next [#whats-next]
Once you're comfortable with the basics, explore these sections based on what you need to do:
* **[Moving Money](/ledger/moving-money)** — Create transactions, manage wallets, and track balances
* **[Structuring Data](/ledger/structuring-data)** — Customize records with schemas, anchors, and layouts
* **[Securing the Ledger](/ledger/securing-the-ledger)** — Set up authentication, authorization, and access policies
# About Ledgers (/ledger/start-here/about-ledgers)
What is a ledger? [#what-is-a-ledger]
A ledger is a book that holds bookkeeping records. Ledger shows the account’s opening balance, records transactions related to accounts in the form of debit and credit operations for the period, and the final account’s balance.
Organizations use ledgers to track all kinds of accounts, for example accounts receivable, accounts payable, sales, payroll, etc. Ledgers can be connected and organized to represent data in various granularities, depending on the use case. Usually organizations have a general ledger that contains summarized data separated into accounts organized by types important for accounting, for example a general ledger may have accounts representing assets, liabilities, revenue, etc. Data from other ledgers which have more granular account structure is usually periodically summarized and transferred to a general ledger.
Double entry bookkeeping [#double-entry-bookkeeping]
A common way of representing transactions in a ledger is called double entry bookkeeping. This is a way of recording each transaction as two entries, a debit on one account and a corresponding credit on another account. The total debits and credits must balance, i.e., equal each other in such a system.
This way of recording transactions helps reduce bookkeeping errors and makes mistakes easier to find. This also helps to prevent fraud and embezzlement. Ledgers evolved over time, from numbers on stone tablets to bits on a hard drive, but the core concept still mostly remains the same.
Blockchain benefits [#blockchain-benefits]
With blockchain technology some of the basic concepts of a ledger can be simplified. Using some of the blockchain ideas we can avoid having to store records multiple times in order to prevent tampering with the data and avoid mistakes. All transactions in such a ledger are hashed, cryptographically signed and chained. This makes it very difficult to change ledger records, and even when it is possible it can be quickly detected by other participants in the network because such ledgers are usually publicly auditable.
| Date | Changes |
| ----------------------- | --------------- |
| 2022-09-07 | Initial version |
# Ledger Key Concepts (/ledger/start-here/key-concepts)
The ledger organizes financial operations around a small set of building blocks called **records**. Each represents a different concern (identity, currency, accounts, movements) and together they form the foundation for building payment systems.
Four core records are used in every ledger interaction: **signers**, **symbols**, **wallets**, and **intents**.
| Record | Think of it as | Purpose |
| ------ | --------------------------- | ----------------------------- |
| Signer | User identity | Proves who is acting |
| Symbol | Currency (e.g. USD, points) | Defines what is being counted |
| Wallet | Bank account | Holds balances |
| Intent | Payment order | Moves balances |
Together, these form the foundation for creating accounts, defining currencies, and processing payments. For example, when Alice pays Bob $50: Alice's **signer** authorizes the operation by creating an **intent** that moves 50 of the USD **symbol** from Alice's **wallet** to Bob's **wallet**.
Signer [#signer]
A signer is a digital identity that holds keys used to authorize ledger operations by signing them. No operation can proceed without a valid signature. In practice, the [Ledger SDK](/ledger/reference/sdk-cheat-sheet) handles signing and hashing automatically. Signers can also carry metadata such as security constraints or links to external identity systems. Learn more in [About Signers](/ledger/securing-the-ledger/about-signers).
Symbol [#symbol]
A symbol identifies a unit of exchange and has a unique handle (e.g. `usd`, `points`). Ledger balances are always denominated in a symbol. Symbols can identify currencies, loyalty points, or any other concept that can be counted. Whoever controls a symbol also controls the issuance of that symbol in the ledger.
Wallet [#wallet]
Wallets hold balances. Each wallet has a unique handle that identifies it. This handle is used in intents, balance queries, and any other operations involving a wallet.
The handle is defined by the user, so it is useful to choose a value that is user-friendly and easy to remember. Handles can be phone numbers, emails, bank account numbers, usernames, etc. The format and types of handles mostly depend on the use case that is being built on top of a ledger.
Wallets are used to represent anything that can hold a balance. For example, bank accounts, loyalty point accounts, subscriptions, bills, loans, etc. All of those use cases can be modeled by mapping them to wallets. Learn more in [About Wallets](/ledger/moving-money/about-wallets).
Intent [#intent]
An intent is a request to move or modify balances. Most commonly, intents transfer balances between wallets, but they can also issue new balances or perform other operations. Each intent contains one or more **claims**, the actual balance movements it will execute. Most intents have a single claim, but when multiple movements need to happen together they are grouped in one intent. For example, transferring 100 USD to a recipient while collecting a 2 USD fee into a bank fees wallet would be a single intent with two claims.
All claims in an intent are processed atomically — either every claim succeeds, or none of them do. When an intent fails, the ledger reverses all claims and records the reason. See [About Intents](/ledger/moving-money/about-intents) for the full lifecycle and the [Error Reference](/ledger/reference/error-reference) for common failure codes.
Each intent must have a unique handle, which prevents duplicate processing if a request is retried (idempotency) and provides a convenient way to look up past operations.
Intents support custom data like all other records. For example, an invoice ID, payment description, or location. Learn more in [Custom Fields](/ledger/structuring-data/custom-fields).
REST API [#rest-api]
All records are available through a standard REST API and the [CLI](/ledger/reference/cli-reference). Every API resource follows the same JSON format:
```json
{
"data": { ... },
"meta": { ... },
"hash": "...",
"luid": "$wlt.abc123..."
}
```
| Field | Description |
| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data` | The resource's primary content, including any custom fields an application needs |
| `meta` | System-managed metadata: status, timestamps, and [proofs](/ledger/securing-the-ledger/about-security) (digital signatures that verify authenticity) |
| `hash` | [Cryptographic hash](/ledger/securing-the-ledger/hash-and-sign-requests) of `data`, ensuring content has not been altered |
| `luid` | Ledger Unique Identifier with a type prefix (e.g. `$wlt.` for wallets, `$int.` for intents) |
See the [API Reference](/ledger/reference/api) for the full list of resource types.
Every resource is independently verifiable by checking its hash and proofs, so any participant can confirm who created it and that nothing has changed.
Beyond the Core [#beyond-the-core]
The ledger includes additional records beyond these four:
* **[Effects](/ledger/connecting-systems/extending-ledger)** trigger notifications when ledger data changes, enabling event-driven integrations like webhooks.
* **[Bridges](/ledger/connecting-systems/about-bridges)** connect third-party systems to the ledger, coordinating balance movements so both systems stay in sync.
* **[Access policies](/ledger/securing-the-ledger/about-policies)** and **[circles](/ledger/securing-the-ledger/about-circles)** control who can perform operations and under what conditions.
What's next [#whats-next]
Ready to try it? Continue to the [Quickstart](/ledger/start-here/quickstart).
| Date | Changes |
| ----------------------- | ------------------------------------------------------------------------------ |
| 2025-03-06 | Rewrite with business context, record structure, diagram, and cross-references |
| 2023-01-27 | Document bridge records |
| 2022-09-07 | Initial version |
# Quickstart (/ledger/start-here/quickstart)
Introduction [#introduction]
In this tutorial you are going to build a ledger that manages business account balances and allows businesses to make payments to other businesses using a simple REST API.
This example represents a simplified banking core.
In order to follow this tutorial you have to have [nodeJS v20](https://nodejs.org/en/download/package-manager/) or newer installed on your machine. This tutorial uses a Minka Ledger, so our first task is to connect to it, for this we will use the Minka CLI tool.
Install the Minka CLI through your terminal:
```bash
$ npm install -g @minka/cli
```
After installing the CLI tool, you will be able to interact with local or remote ledger instances by using the `minka` command. Checkout all t he commands available by typing `minka --help`.
After we have installed a CLI, we can connect to a ledger by typing:
```console
$ minka server connect
? Server URL:
✅ Connected to server ()
Active ledger:
```
Ledger URL required as input has the following format:
* `https://./api/v2`
* ledger referenced by `` will automatically be set as active ledger which can be later changed with `minka ledger select` command
* if you haven’t received a Ledger URL, you can ask responsible person to send it to you
Now we have everything ready to start tracking balances in the ledger.
Before storing data in the ledger, we need to create a **signer**.
Initial configuration [#initial-configuration]
Minka Ledger security model is based on asymmetric cryptography, because of that we need to sign each request using a public and private key pair. A signer is a ledger concept that encapsulates those keys with optional metadata.
First, we will create a `treasury` key that we will use to control the money supply of our system:
```console
$ minka signer create
? Handle: treasury
? Key pair source: Generate new key pair
? Add custom data (y/N)? No
? Signer password: [hidden]
? Repeat password: [hidden]
? Store to ledger? No
✅ Signer created successfully:
Handle: treasury
Public: Zxy3I/cezrqMf4mlpVlA86gapO5syX+Dt/Y5hnHvvNk=
Secret: *[value is hidden]*
⚠️ WARNING: Secret or private key is critical data that should be handled
with care. Private keys are used to modify balances and it is important to
understand that anyone who has access to that key can perform sensitive
ledger operations.
```
Created signer is stored locally and can be referenced in other commands by using its handle, in this case `treasury`.
Next, we will create a **symbol**. Symbols are used to issue balances to the system, they can represent currencies or anything that can be counted, for example, loyalty points, tokens, cryptocurrency etc.
You can create a symbol secured by our `treasury` key with the following command:
```console
$ minka symbol create
? Handle: usd
? Factor: 100
? Add custom data (y/N): No
? Signer: treasury
✅ Symbol created successfully:
Handle: usd
Factor: 100
Signer: Zxy3I/cezrqMf4mlpVlA86gapO5syX+Dt/Y5hnHvvNk= (treasury)
```
Provided signer (`treasury`) has full access to this symbol by default, that means all operations related to this symbol need to be signed with a private key of that signer.
Now we have a basic monetary model of our ledger in place. We have a symbol that represents a fiat currency and we can allow businesses to join our ledger.
Onboarding businesses [#onboarding-businesses]
Businesses can join by creating their **wallet.** Wallets are similar to bank accounts only more flexible because they can represent anything that can hold a balance, for example, checking or savings accounts, gift cards, phone numbers, etc.
A business can join our ledger in a similar way we have created our symbol earlier. First, they need to create their signer:
```console
$ minka signer create
? Handle: zet
? Key pair source: Generate new key pair
? Add custom data (y/N)? No
? Signer password: [hidden]
? Repeat password: [hidden]
? Store to ledger? No
✅ Signer created successfully:
Handle: zet
Public: /hICkEzKuSMeJ5b/T7A2qKL3Mml7avznoV2/chwqDh4=
Secret: [value is hidden]
⚠️ WARNING: Secret or private key is critical data that should be handled
with care. Private keys are used to modify balances and it is important to
understand that anyone who has access to that key can perform sensitive
ledger operations.
```
After that they would create a wallet:
```console
$ minka wallet create
? Handle: zet
? Bridge: [none]
? Add custom data (y/N): No
? Add routes (y/N): No
? Signer: zet
✅ Wallet created successfully:
Handle: zet
Signer: /hICkEzKuSMeJ5b/T7A2qKL3Mml7avznoV2/chwqDh4= (zet)
```
We can also check the balance of our new wallet by typing:
```console
$ minka wallet balances zet
Balances:
No balances found
```
Although anyone can create new wallets using this process, these wallets don’t hold any balance. The only currency we are going to accept is `usd` and that currency is controlled by us (a bank).
We can now validate all required business documentation and issue a balance to that business once we receive a deposit from them.
Balance changes are represented in the Minka Ledger as signed **intents**. Let’s issue a balance of 10.000,00 USD to `zet`:
```console
$ minka intent create
? Handle: 01LAZz_MfjtkP6BPu
? Action: issue
? Target: zet
? Symbol: usd
? Amount: 10000
? Add another action (y/N): No
? Add custom data (y/N): No
? Signers: treasury
Intent summary:
------------------------------------------------------------------------
Handle: 01LAZz_MfjtkP6BPu
Action: issue
- Target: zet
- Symbol: usd
- Amount: $10,000.00
? Sign this intent using signer treasury (y/N)? Yes
✅ Intent signed and sent to ledger .
Intent status: pending
```
If we now check the balance of `zet` wallet we will see the issued amount is available:
```console
$ minka wallet balances zet
Balances:
$10.000,00 (usd)
```
Making payments [#making-payments]
We can now add one more business to show how B2B payments would work. Let’s create a business called `hep`. First, we create a signer:
```console
$ minka signer create
? Handle: hep
? Key pair source: Generate new key pair
? Add custom data (y/N)? No
? Signer password: [hidden]
? Repeat password: [hidden]
? Store to ledger? No
✅ Signer created successfully:
Handle: hep
Public: CqopzcoGI8PoygX5uGNEn5nTx+RkDI3rwkeD56226ho=
Secret: [value is hidden]
⚠️ WARNING: Secret or private key is critical data that should be handled
with care. Private keys are used to modify balances and it is important to
understand that anyone who has access to that key can perform sensitive
ledger operations.
```
And a wallet using that signer:
```console
$ minka wallet create
? Handle: hep
? Bridge: [none]
? Add custom data (y/N): No
? Add routes (y/N): No
? Signer: hep
✅ Wallet created successfully:
Handle: hep
Signer: CqopzcoGI8PoygX5uGNEn5nTx+RkDI3rwkeD56226ho= (hep)
```
Now we can send a payment from `zet` to `hep` using a simple intent again:
```console
$ minka intent create
? Handle: CWuH_N4Ah_jyW7Ffi
? Action: transfer
? Source: zet
? Target: hep
? Symbol: usd
? Amount: 3000
? Add another action (y/N): No
? Add custom data (y/N): No
? Signers: zet
Intent summary:
------------------------------------------------------------------------
Handle: CWuH_N4Ah_jyW7Ffi
Action: transfer
- Source: zet
- Target: hep
- Symbol: usd
- Amount: $3,000.00
? Sign this intent using signer zet (y/N)? Yes
✅ Intent signed and sent to ledger .
Intent status: pending
```
If we now check balances of both business wallets we will see that they reflect the payment we just made:
```console
$ minka wallet balances zet
Balances:
$7.000,00 (usd)
```
```console
$ minka wallet balances hep
Balances:
$3.000,00 (usd)
```
With everything we did until now, we have a ledger that allows us to easily onboard new business, track their balances and enable them to make payments between each other.
Withdrawing deposits [#withdrawing-deposits]
The last part we will add in this tutorial is a way for businesses to withdraw money from the system. We will model the withdrawal process in multiple steps to demonstrate how settlements can be modeled using wallets.
We will create a special settlement wallet (`sw`) that is going to hold balances until they are settled manually by our treasury team. After external payment is confirmed the reserved amount is going to be destroyed to record that it left the system.
First, we create our `sw` using our existing `treasury` key because it is going to be managed by the same team:
```console
$ minka wallet create
? Handle: sw
? Bridge: [none]
? Add custom data (y/N): No
? Add routes (y/N): No
? Signer: treasury
✅ Wallet created successfully:
Handle: sw
Signer: Zxy3I/cezrqMf4mlpVlA86gapO5syX+Dt/Y5hnHvvNk= (treasury)
```
When a business wants to withdraw money from the system, they will transfer this amount to this new wallet where it will wait for the treasury team to manually make a payment in external systems. This is how we would withdraw $1.000,00 from `hep` wallet:
```console
$ minka intent create
? Handle: 8AKKgcdRDTvwS1w5k
? Action: transfer
? Source: hep
? Target: sw
? Symbol: usd
? Amount: 1000
? Add another action (y/N): No
? Add custom data (y/N): No
? Signers: hep
Intent summary:
------------------------------------------------------------------------
Handle: 8AKKgcdRDTvwS1w5k
Action: transfer
- Source: hep
- Target: sw
- Symbol: usd
- Amount: $1,000.00
? Sign this intent using signer hep (y/N)? Yes
✅ Intent signed and sent to ledger .
Intent status: pending
```
After this intent is successfully cleared in the ledger, a treasury team employee would manually make a required payment and send an intent to destroy this amount in the ledger:
```console
$ minka intent create
? Handle: 8KInmo5vlo270zYnd
? Action: destroy
? Source: sw
? Symbol: usd
? Amount: 1000
? Add another action (y/N): No
? Add custom data (y/N): No
? Signer: treasury
Intent summary:
------------------------------------------------------------------------
Handle: 8KInmo5vlo270zYnd
Action: destroy
- Source: sw
- Symbol: usd
- Amount: $1,000.00
? Sign this intent using signer treasury (y/N)? Yes
✅ Intent signed and sent to ledger .
Intent status: pending
```
You have built a simple and secure ledger that supports tracking business account balances, allows those businesses to make deposits and withdrawals to external systems and gives them API access to make payments in a very simple way.
What’s next? [#whats-next]
This solution can be further improved by adding integrations to connect to external systems automatically instead of relying on people to perform those operations manually.
You can find more details on how to do that in our other **tutorials** and **how to guides**.
| Date | Changes |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2024-06-05 | Updated `signer create` examples with latest UX |
| 2023-03-10 | • Using placeholders for Ledger URL when connecting and explained the format of this URL.
• Updated cli inputs and outputs according to latest CLI. |
| 2023-03-07 | Changed the URL to `ldg-stg.one` |
| 2023-02-27 | Added Version to Ledger URL |
| 2023-02-02 | Destroy intent corrected |
| 2022-09-07 | Initial version |
# Moving Money (/ledger/moving-money)
This section covers how value moves on the ledger — from creating wallets and managing balances to understanding how transactions are processed and settled.
Wallets and Balances [#wallets-and-balances]
Payment Intents [#payment-intents]
Target Discovery [#target-discovery]
Anchors map human-readable identifiers (phone numbers, emails, account numbers) to payment targets with predefined metadata.
Tutorials [#tutorials]
Prerequisites [#prerequisites]
* **[Start Here](/ledger/start-here)** — Understand ledger basics, records, and key concepts
# About Wallets (/ledger/moving-money/about-wallets)
All ledger records have a very similar structure, but each one has a specific purpose. It is important to understand when to use each of those core concepts to avoid abusing them in certain situations or modelling a system that can be difficult to extend in the future.
Wallets are the only ledger records that can **hold balances**. This is a very important distinction from other core concepts. Because of that, it should be relatively easy to decide what should be modelled as a wallet. In general, anything that holds a balance should be a wallet. The most common examples include: bank or any other type of financial accounts, credit cards, loyalty cards, etc.
Some other, less common uses could also include concepts like invoices, loans, bills and similar. It may be unintuitive to think that we could model those in the same way as, let’s say, bank accounts, but it is important to understand that wallets are a lightweight concept in ledger. This means that they can be created very quickly and this makes them a good option for single-use situations as well. As a quick example, modelling an invoice as a wallet can be beneficial in some situations because that would allow us to leave it open until balance is settled. This opens a lot of interesting possibilities in the future, things like partial payments, payments through instalments, increasing the outstanding amount by adding interests, etc.
Although wallets can hold balances, it isn’t required that they always must hold a balance. Some cases for wallets that can’t hold balances include representing **virtual** ledger accounts or **remote** accounts from external systems. It makes sense to represent those concepts as wallets as well, since they are accounts, but we can configure ledger in a way to prevent assigning balances to such wallets. Balances are usually assigned to a settlement account in such cases, this can be configured as well. Representing such concepts as wallets also leaves room for future improvements, for example, we could decide to convert those wallets to native ledger wallets that can hold balances at some point. This is very simple to do if those accounts are already represented as wallets.
Check out [about balance reservations](/ledger/moving-money/balance-reservations) for a better understanding
on how wallet balances are managed in ledger.
Naming Wallets [#naming-wallets]
The identifier of each wallet is its handle. The most basic wallet handle is a simple name, containing only letters and numbers. This can be enough in some cases, but in more complex systems we need more structure around wallet names. Wallet handles have a syntax that can be used in order to provide a consistent naming structure. The main parts of wallet handle are `schema`, `handle` and `parent`:
```jsx
:@
```
* **schema** (optional) - defines a type of wallet, schema can be used to categorise wallets depending on their purpose, and to attach specific policies to those categories. Examples of schemas: `tel`, `account`, `loan`, etc.
* **handle** (required) - represents the wallet identifier within an (optional) schema. This is usually the primary wallet identifier, it can be an organization name, phone number, account number or similar. For example `hpb`, `+1356545666` or `HR1210010051863000160`.
* **parent** (optional) - represents the parent wallet which is authoritative wallet for the `handle` on the left side. If parent wallet is specified in addressing then it usually means that the handle part is not used by ledger when assigning balances, although the handle can be used to configure ledger routing rules more easily. Data in handle is usually interpreted by external ledgers in order to identify resources within their system. For example, in case a `parent` identifier is a bank, `handle` could be an account within that bank.
Address resolution [#address-resolution]
Although the **wallet handle** and **address** both have the same structure they don’t represent the same concept.
* **wallet handle** is identifier of existing wallet in the ledger. Existing wallet can receive balance or be the source of balance movement.
* **address** is identifier which is used as source and target in balance movements. Sometimes an address can be used which is not the handle of existing wallet in the ledger. In this case address resolution is used to resolve the **authoritative** wallet for this address. Resolved wallet is then used as source or target of balance movement and also as the point of contact for external ledger through Bridges. See more here: [About Bridges](/ledger/connecting-systems/about-bridges).
Address resolution is performed by going up the **addressing hierarchy** until a wallet with the matching handle is found. Addressing hierarchy for our generic address format is listed below. Note that each step in hierarchy will be considered only if all components for this step are present in address:
```markdown
1. :@
2. @
3.
4.
```
Examples:
```markdown
account:1050000029@hpb -> account@hpb -> hpb -> account
loan:4534500003@hpb -> loan@hpb -> hpb -> loan
tel:15261234578 -> tel
41111339@zaba -> zaba
```
Addressing examples [#addressing-examples]
Let’s look at two examples of addressing in real world scenarios. First, let’s assume we are modelling external bank accounts and using ledger only to track settlement amounts between banks. An address naming structure for this case could look like `account:1050000029@hpb`. There are no wallets with such handles, but instead there is an authoritative wallet with handle `hpb` representing the bank.
* `account` is our schema, it tells us that this is a bank account
* `1070000029` is a handle representing an account number in the banking core. This is something that bank uses when processing payments to know which account to debit or credit.
* `hpb` is the name of a bank and this means that all balances targeting `account:*@hpb` are going to be credited to authoritative `hpb` wallet in our ledger. Ledger is not taking the `handle` into account here.
The second example we can look at is a case when we want to model wallets that represent phone numbers in the system. This is an example of how to model an phone alias directory service. We can use addresses with structure `tel:15261234578` to send balance to phone numbers. In this example there exists an authoritative wallet `tel` which is in charge of handling all balance movements to phone numbers.
* `tel` is our schema, it identifies all wallets that represent phone alias directory records. Since there is authoritative wallet `tel`, it will be used to pass all intents with `tel:*` target to a service responsible for our alias directory by using [Bridge](/ledger/connecting-systems/about-bridges).
* `15261234578` is a phone number handle that we want to target, this handle can be used by our phone alias directory service to onboard users by sending them SMS messages, etc.
💡 Phone alias directory service can be used to onboard the user by creating the wallet with handle `tel:15261234578` and making sure that this wallet has a **route** which forwards balance to a bank account address. After that, this phone number wallet is more specific and will supersede previous authoritative `tel` wallet and will become in charge for it’s own address.
Wallet Types [#wallet-types]
There are several types of wallets that can be created in ledger, those include:
* **native wallets** - default type of ledger wallets, balances of those wallets are fully managed by ledger, there are no external systems involved in processing of balance movements related to these wallets
* **bridge wallets** - these are wallets that are linked to external systems. Every balance movement related to these wallets needs to be processed in a remote system before it is confirmed in the ledger.
Default wallet type for all new wallets is a native ledger wallet. To create a native wallet is enough to provide only a handle, for example:
```jsx
{
"handle": "hpb",
"access": [ ... ]
}
```
If we want to create a bridge wallet with the same handle, we can do it by providing a name of a bridge along with the handle like this:
```jsx
{
"handle": "hpb",
"bridge": "hpb".
"access": [ ... ]
}
```
Note: A bridge with handle `hpb` must be registered in ledger in order for this to work.
To learn more about bridges, see [About Bridges](/ledger/connecting-systems/about-bridges).
Ledger is going to handle operations on bridge wallets by performing two-phase commits for all balance movements related to those wallets. If a bridge wallet is a source of a claim, ledger is going to require a confirmation that a **debit** was performed in a remote system before confirming the operation. Similarly, if a bridge wallet is used as a target, ledger is going to require a confirmation that a **credit** was performed before the operation is confirmed. In case any of those operations isn’t performed successfully, ledger is going to abort the whole intent or chain of related intents. This ensures that all balance movements are performed cleanly end-to-end, regardless of the number of remote systems involved in the process.
Wallet Routes [#wallet-routes]
Routes are a ledger concept that allows setting up automatic rules for funds management on wallets. Routes need to be registered on specific wallets by users with sufficient permissions to perform the resulting operation. By registering a route, user is **delegating authority** to ledger to perform the described operation under defined permissions in the name of that user.
Ledger is going to perform those operations **automatically** during intent processing if the conditions described in routes are matched. This means that a resulting operation is going to extend the intent processing chain and any potential failures are also going to rollback the whole intent chain. This is an important concept to understand, it makes routes a very powerful tool for modelling more advanced transactional flows in ledgers.
Effects are a good alternative for cases when an operation is optional and it isn’t necessary to rollback all previous operations. For more details about effects, see [About Extending Ledger](/ledger/connecting-systems/extending-ledger).
Currently supported wallet routes are:
* debit
* Instead of the money being debited from the resolved source wallet of the claim, it will be debited from the `target` of this route. Bridge calls for debit will also be sent to the bridge assigned to the target wallet of this route, not of the resolved source wallet of the claim.
* credit
* Instead of the money being credited to the target wallet of the claim, it will be credited to the `target` of this route. Bridge calls for credit will also be sent to the bridge assigned to the target wallet of this route, and not the bridge of the resolved target wallet of the claim.
* accept
* While other routes affect the flow of symbol balance, this one does not. If applied to a claim, defines whether or not an intent will be accepted. If the filter is not satisfied, the intent will be automatically rejected.
* forward
* If applied to target of a claim, will result in the amount of the claim being forwarded as a new intent in the same [thread](/ledger/moving-money/about-intents).
These routes are divided into two categories - `input` routes and `output` routes. `input` routes may affect `transfer` claims, while `output` routes may affect `transfer` and `issue` claims.
Note: Ledger resolves routes up to a configurable depth (default 3 levels).
Routing cycles are detected and prevented - if a wallet routes to itself directly or indirectly through a chain of routes,
the system will throw an error and intents that affect the wallet are rejected.
Route filters [#route-filters]
Each route has an optional `filter` property. Routes are triggered during intent processing flow, and they are evaluated for each claim of the intent individually. That is why route can target the data from claims.
When registering a new route you can specify a `filter` property that allows you to match any available fields either from an incoming claim or intent. If those conditions pass, a route action is going to be performed by ledger.
Here is an example of such a route which will be satisfied only if the symbol of the claim is `eur`
```jsx
{
"handle": "hpb",
"bridge": "hpb",
"routes": [{
"filter": {
"symbol.handle": "eur"
},
"action": "accept"
}],
"access": [ ... ]
}
```
Here is an example of such a route which will be satisfied only if the intent schema is `p2p`
```jsx
{
"handle": "hpb",
"bridge": "hpb",
"routes": [{
"filter": {
"ctx.intent.data.schema": "p2p"
},
"action": "accept"
}],
"access": [ ... ]
}
```
With a route like the above, only claims with symbol `eur` are going to be accepted, all other claims are going to be rejected automatically. This route allows us to create a single-currency wallet.
input routes [#input-routes]
These routes affect `in-routable` claims. Currently only `transfer` claims are `in-routable`.
Input routes are as follows:
* `debit`
output Routes [#output-routes]
These routes affect `out-routable` claims. Currently `transfer` and `issue` claims are `out-routable`.
Output routes are as follows:
* `credit`
* `forward`
* `accept`
An example of such a route is given here:
```jsx
{
"handle": "tel:15261234578",
"routes": [{
"filter": {
"symbol.handle": "eur",
"ctx.intent.data.schema": "p2p"
},
"action": "forward",
"target": "account:1070000029@hpb"
}, {
"filter": {
"symbol.handle": "usd",
"ctx.intent.data.schema": "p2p"
},
"action": "forward",
"target": "account:41111339@zaba"
}],
"access": [ ... ]
}
```
The route in the example above will forward intents to different wallets depending on the received currency. Forwarding will be done in a new intent within the same [thread](/ledger/moving-money/about-intents).
Catch-all Route [#catch-all-route]
Routes are processed in order how they are declared within their route group (see `in` and `out` routes above). If some `in` routes are defined, but none of their filters match, the claim is going to be rejected and the containing intent is going to fail as well. The same behaviour applies, separately, to `out` routes.
To avoid this behaviour in situations when routes are considered optional it is necessary to define a catch-all route. This route should be placed last and can accept all claims by omitting a filter. For `out` routes this could be a neutral route like `accept` :
```json
{
"action": "accept"
}
```
For `in` routes, this can be a neutral route such as a `debit` that debits from that same wallet (essentially it has no observable consequence). So if our wallet handle is `my-wallet`, it would be:
```json
{
"action": "debit",
"target": "my-wallet"
}
```
If no routes are specified in the wallet it is considered that there are no conditions on incoming claims. As soon as you specify at least one route it is expected that routes cover all acceptable cases and any unmatched claim is going to be rejected.
Anchors resolution [#anchors-resolution]
* The wallet API allows to fetch anchors via `GET /v2/wallets/:address/anchors` endpoint.
Consider the address `tel:123`.
Firstly, it queries in ledger's database the anchors which are registered for the wallet `tel:123` via `wallet` property.
Then, the ledger uses the address passed by parameter `tel:123` to resolve a wallet in the database by using [the address resolution algorithm](/ledger/moving-money/about-wallets#address-resolution). If a wallet is found and has bridge defined, the ledger calls the bridge for fetching the anchors for the address `tel:123` passed via query: `v2/anchors?wallet=tel:123`. If valid, the result is appended to the ledger response.
* it is also possible to resolve anchors via lookup `POST /v2/wallets/:address/anchors/!lookup`.
Consider the address `tel:123`.
The ledger uses the address passed by parameter `tel:123` to resolve a wallet in the database by using [the address resolution algorithm](/ledger/moving-money/about-wallets#address-resolution). If a wallet is found and has bridge defined, the ledger calls the bridge's anchor lookup handler for resolving the anchors with request payload sent by the user: `POST /v2/anchors/!lookup`.
The body of lookup query should follows the same schema as an anchor record. Except by the fact all the properties in `data` are optional.
See [About Anchors](/ledger/moving-money/about-anchors) for more details.
Bridge is called if it implements `anchors` trait. Please read [About Bridges](/ledger/connecting-systems/about-bridges) for more details about bridge traits.
Domains resolution [#domains-resolution]
The wallet exposes an endpoint for querying domains corresponding to an address via `GET /v2/wallets/:address/domains`.
Consider the address `tel:123`.
Firstly, it queries in ledger's database the wallets which their handle starts with `tel:123@` in order to get the list of domains connected to `tel:123` schema:identifier address.
Then, the ledger uses the address passed by parameter `tel:123` to resolve a wallet in the database by using [the address resolution algorithm](/ledger/moving-money/about-wallets#address-resolution). If a wallet is found and has bridge defined, the ledger calls the bridge for resolving domains for the address `tel:123` passed via query: `v2/domains?wallet=tel:123`. If valid, the result is appended to the ledger response.
Bridge is called if it implements `domains` trait. Please read [About Bridges](/ledger/connecting-systems/about-bridges) for more details about bridge traits.
| Date | Changes |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2024-10-23 | Added list of all supported routes and descriptions to the [first section of routes in wallet docs](/ledger/moving-money/about-wallets). Updated wallet routes and added input/output routes concept. |
| 2023-10-24 | • Added Domains resolution section
• Added Anchors resolution section |
| 2023-01-20 | Removed Nested Wallet concept since it is already explained in Addressing part. |
| 2023-01-19 | Update wallet naming and addressing |
| 2022-12-15 | Initial version |
# About Balance Reservations (/ledger/moving-money/balance-reservations)
What are balance reservations [#what-are-balance-reservations]
Balance reservations are temporary holds placed on wallet balances while an intent is being processed. They guarantee that the funds required to execute the intent will still be available at commit time. This protects the intent from race conditions and double-spend, while making sure that
balances will be moved correctly after all participants agreeing on the intent action.
Reservations are created during the two-phase commit (2PC) prepare step `status: pending` for every debit side that the ledger itself must honor. If an external system (via a Bridge) is responsible for a debit, that external system will perform its own reservation when it receives the prepare request. The ledger always participates as a 2PC participant for native balances and will reserve the needed amount before replying that it is prepared.
Reservations are performed base on information read from **resolution proofs** with `schema: "debit"`. See [About Resolution Proofs](/ledger/moving-money/resolution-proofs) to understand how the ledger determines which amount and from which wallets wallets it will reserve.
Why they matter [#why-they-matter]
* **Atomicity safety**: Ensure that once all participants report prepared, the intent can be safely committed without balance shortfalls.
* **Concurrency safety**: Prevent other concurrent intents from consuming the same funds by removing reserved amounts from the available balance calculation.
Flow [#flow]
Reservations are part of the 2PC step in the pending phase of [intent processing](/ledger/moving-money/about-intents#intent-processing-phases):
1. Aspects validate the intent and its claims. No balance change or reservation occurs here.
2. 2PC starts. The coordinator identifies participants, writes down the resolved information on participants into resolution proofs, reserves funds for debits and sends prepare requests.
* Prepare is sent first to all debit sources and then to credit targets. See the note in [About Intents](/ledger/moving-money/about-intents#intent-processing-phases).
* Upon a debit prepare, participants validate the intent on their side.
* After validating, the participant replies prepared.
3. If all participants are prepared and signature requirements are satisfied, the coordinator proceeds to commit.
4. On commit, reservations are consumed and balances are persisted. On abort, reservations are released after notifying bridges and no balance changes are persisted.
Reservations exist only for the lifetime of an in-flight intent. They are created in prepare, consumed on commit, and released on abort.
Reservation semantics [#reservation-semantics]
* **Scope**: Reservations are tracked per wallet and symbol for the specific address referenced by a claim.
* **Available balance**: When checking an incoming intent, the ledger evaluates the available balance as the wallet balance minus any active reservations for pending/prepared intents. By default balances cannot go below 0, so reservations will fail if they would breach that limit.
* **Debit-only**: Reservations apply to the debit side. Credits do not require a reservation by the ledger core (external targets may perform their own checks on prepare).
* **Lifetime**: A reservation lives from the moment preparation starts until the intent ends (committed or aborted).
* **Timeouts and failures**: If any participant fails on prepare or a timeout occurs while waiting for confirmations, the coordinator aborts the intent and all reservations are released.
Commit vs abort behavior [#commit-vs-abort-behavior]
* **Commit**: Reserved amounts are applied to balances atomically with credits to targets. All reservations in the thread are cleared once the commit succeeds.
* **Abort/Reject**: No balance changes are persisted. All reservations are released, restoring the funds to available status.
Abort notifications are sent in reverse order of prepares (first to credit targets, then to debit sources). Reservations on debit sources are released as part of handling the abort.
Simple example [#simple-example]
Consider an intent to transfer 30 USD from `alice` to `bob`.
Initial available balances:
* `alice` 100 USD
* `bob` 0 USD
1. Aspects pass; 2PC begins.
2. Intent is pending, so core signer reserves 30 USD from `alice` wallet.
* `alice` has now 70 USD available and 30 USD reserved.
* `bob` USD balance remains 0.
2. Prepare for debit `alice` 30 USD:
* The participant responsible for `alice` does their check and prepare.
3. Prepare for credit `bob` 30 USD: The participant acknowledges it can accept the credit and replies prepared.
4. All participants are prepared and required signatures are collected.
5. Commit:
* `alice` is debited 30 USD from reserved amount. Her available balance is not changed.
* `bob` is credited 30 USD, so his available balance in ledger is now 30 USD.
* All reservations for this intent/thread are cleared.
Final available balances:
* `alice` 70 USD.
* `bob` 30 USD.
If instead any participant failed on prepare (or a timeout was reached), the coordinator would abort the intent and release the reserved 30 USD for `alice`.
Interactions with Bridges [#interactions-with-bridges]
* If the debit source or credit target references a Bridge, that Bridge is a 2PC participant and must respond to prepare.
* For debits handled by a Bridge, the Bridge performs the equivalent of a reservation in its own system before replying prepared. Ledger still evaluates available balances for any native debits it must honor.
* On commit/abort, Bridges receive the corresponding commands and finalize or release their own holds.
Operational notes [#operational-notes]
* Intent expiration leads to reserved balances release, since the intent is aborted.
* Model limits explicitly. If a wallet supports overdrafts or custom limits, reservations still respect those limits during prepare.
# Wallet Limits (/ledger/moving-money/wallet-limits)
Applying limits [#applying-limits]
A wallet can hold balance in multiple symbols. By default this balance is only limited to be at least 0.
Limits allow wallets to have negative balance up to an specified amount or to have an upper limit, for example.
In order to apply limits to a wallet an `intent` with valid `limit` claims needs to be executed.
For example, in order to allow wallet `bank` to have negative balance up to `-200`, for the symbol `usd`,
the metric `minBalance` should be limited to `-200` with an intent that looks like this:
For -200 usd the amount is -20000 because of a factor of 100
```json
{
"handle": "limiting-usd-in-bank-wallet",
"claims": [
{
"action": "limit",
"metric": "minBalance",
"wallet": {
"handle": "bank"
},
"symbol": {
"handle": "usd"
},
"amount": -20000
}
],
"access": []
}
```
Or using the ledger sdk
```tsx
import { AccessAction, ClaimAction, LedgerIntent } from "@minka/ledger-sdk/types";
const sdk = new LedgerSdk({
server: "",
ledger: "",
});
const limitIntent: LedgerIntent = {
handle: 'limiting-usd-in-bank-wallet',
claims: [
{
action: ClaimAction.Limit,
metric: LimitClaimMetric.MinBalance,
wallet: {
handle: 'bank',
},
symbol: {
handle: 'usd',
},
amount: -20000,
},
],
access: [
{
action: AccessAction.Any,
},
],
}
await sdk.intent
.init()
.data(limitIntent)
.hash()
.sign([{keyPair}])
.send()
```
Limit evaluation and atomicity [#limit-evaluation-and-atomicity]
Keeping in mind that an intent (set of claims) is atomic - checks like balance limits (by default balance cannot go under 0)
are applied having taken the cumulative claims of an intent in mind.
Because of this, even if one claim brings a wallet below or above their limit, if there's another claim in the same intent
that brings it back within normal range, the intent will not break the limit and will succeed.
Available metrics for limiting [#available-metrics-for-limiting]
This is a list of available metrics for use in limit claims.
Some of them might not be enabled by default and require a change of specific ledger config entries to enable for a ledger.
Some configs might not be enabled server-wide, depending on which environment (shared, dedicated, test) you use.
| Name | Description | Default | Config to enable | Enabled by default |
| ----------- | --------------------------------------------------------------------------------------------------------------------- | ------- | ------------------------- | ------------------ |
| minBalance | Lowest balance this wallet can hold | 0 | N/A | Yes |
| maxBalance | Highest balance this wallet can hold | N/A | N/A | Yes |
| dailyCount | Maximal number of transfers this wallet can do in a 24 hours timeframe | N/A | limits.aggregated.enabled | No |
| dailyAmount | Maximal cumulative amount this wallet can transfer in a 24 hours timeframe. This includes both receiving and sending. | N/A | limits.aggregated.enabled | No |
| Date | Changes |
| ----------------------- | --------------- |
| 2024-03-11 | Initial version |
# Drop a Wallet (/ledger/moving-money/drop-wallet)
Ledger SDK allows users to drop wallets.
Dropping a wallet [#dropping-a-wallet]
Bellow is an example of how to drop wallet `tel:123` by using the Ledger SDK.
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
}
})
await sdk.wallet
.drop('tel:123')
.hash()
.sign([{ keyPair: yourKeyPair }])
.send()
```
You can also drop a wallet by passing the ledger record you want to delete, in cases where you have fetched it before.
```tsx
const record: LedgerRecord = {
hash: '',
data: {
handle: '',
access: [
{
action: "",
signer: {
public: ""
}
}
],
},
luid: '',
meta: {
proofs: [
{
custom: {
moment: '',
status: '',
},
digest: '',
method: '',
public: '',
result: '',
},
],
status: '',
moment: '',
owners: [''],
},
}
// Initialize a drop record builder for the wallet
const dropWalletRecord = await sdk.wallet.drop(record).hash().sign([{ keyPair: yourKeyPair }])
/**
* Send the DELETE request to the backend.
* Optionally, you can pass authParams and headers to the send method.
*/
const response = await dropWalletRecord.send({authParams, headers})
```
Constraints [#constraints]
* Users cannot drop a wallet that has any balance different from 0. Those balances should be spent or destroyed beforehand.
* All the anchors associated with the wallet should be dropped if ledger is configured
to require wallet presence when creating anchors.
Useful links [#useful-links]
* See [About Wallets](/ledger/moving-money/about-wallets) for more details about wallet concept.
| Date | Changes |
| ----------------------- | ---------------------------------------------------------- |
| 2023-12-12 | Initial version |
| 2025-12-12 | Add an example for dropping a wallet using a ledger record |
# About Intents (/ledger/moving-money/about-intents)
What are intents [#what-are-intents]
Intents represent a way to execute a change in ledger. Most common changes involving intent is the movement of balances between wallets.
Intent author constructs and signs the intent with his private key that way ensuring integrity and authenticity of his intention.
Intents guarantee atomicity of changes in ledger, meaning that if there are multiple changes they will either be all persisted completely or no change will be persisted at all.
There are 6 intent statuses related to atomicity:
| **Status** | **Description** |
| ----------- | ------------------------------------------------------------------------------------------------------------------- |
| `Pending` | Intent is submitted but still being processed. Ledger data changes are not yet persisted. |
| `Prepared` | Intent is prepared by all participants and can be processed. |
| `Committed` | Intent is processed successfully. Ledger data changes are persisted and irreversible. Will eventually be completed. |
| `Completed` | Intent was successfully processed. |
| `Aborted` | Intent was aborted while in pending status. Will eventually be rejected. |
| `Rejected` | Intent processing failed. Ledger data changes are rejected and cannot be persisted. |
**completed** and **rejected** are so called **final statuses** and no changes to an intent are possible after an intent ends up in one of those.
**committed** and **aborted** depend on whether quorum is reached. By default, and intent will be committed unless a status policy restricts it. See [status policy](/ledger/structuring-data/status-policies#what-is-a-status-policy) for more details.
Intent claims [#intent-claims]
Claim is the smallest unit of change stated in intent.
Intent can have one or multiple claims which can be used to represent anything from simple to complex data changes in ledger.
Atomicity property mentioned above ensures that either all claims will be executed or none.
The simplest example of an intent with 1 claim is a transfer of balance between two wallets.
Example of an intent with 2 claims can be an exchange transaction where 2 wallets exchange one currency for another at an agreed rate where the atomicity property plays an important role of ensuring that one party in the transaction can’t be tricked by another.
An example with 3 claims can be similar to the exchange with the addition that the provider of exchange can take a fee for the exchange.
Checks like balance limits (by default balance cannot go under 0) are applied having taken the cumulative claims of an intent in mind.
Because of this, even if one claim brings a wallet below or above their limit, if there's another claim in the same intent that brings it back
within normal range, the intent will not break the limit and will succeed.
Check out [about balance reservations](/ledger/moving-money/balance-reservations) for a better understanding
on how intents affect wallet balances.
Intent signatures [#intent-signatures]
Intent signature is a proof which guarantees authenticity and integrity of the intent. In other words it guarantees:
* that the signer is really the author or witness of the intent and that he agreed with it
* that the intent is not tampered with in transport or at rest
Intent signatures are also the mechanism to achieve the agreement between multiple parties involved in the intent without the need to trust to third party mediator. In our example of exchange intent where 2 wallets are involved, signatures from owners of both wallets will be required in order to clear the intent.
Both parties know that the intent needs 2 signatures in order to be cleared and each will independently decide whether the terms of the exchange are acceptable and confirm that with their signature.
Required signatures for intent are determined by the content of the intent claims. For example, a transfer claim which transfers balance from source to target requires a signature which has permission to perform `spend` action on the source wallet. Security rules which define which signers have the permission to spend a wallet is configured through [authorization rules](/ledger/securing-the-ledger/about-security#authorization-rules).
Intent processing phases [#intent-processing-phases]
Intent is processed in 3 sequential steps:
1. Aspects
2. Two-phase commit (2PC)
3. Effects
Aspects for the intent are executed first as a precondition for accepting the intent. They should check preconditions for an intent such as schema validity, business rules and limits. Aspects are executed synchronously and ledger waits for results of aspects. Error returned from aspect will abort the intent processing and put it into **aborted** status.
Two-phase commit step is executed after all aspects are successful. It is used to coordinate distributed commit of the intent across multiple ledgers or systems. In case of new intents added during this step, for each new intent the aspects for this intent will be executed first and after that 2PC will be continued to cover these new intents also. If any participant reports a failure during that step, intent will be **aborted** and all participants will get abort request.
Effects for the intent will be executed after intent is successfully committed and changed to **committed/completed** status. Effects are used just to notify listeners about ledger change and in this step they can’t affect the result of ledger operations. If intent fails during first 2 steps and becomes **aborted/rejected**, effects can also be executed to notify listeners about that.
**prepare** entries are sent first to all the *debit* sources and then to the *credit* targets.On the other hand, **abort** entries are sent in reverse order. First to *credit* targets and then to *debit* sources.
**commit** calls are sent in parallel to all the participants.
Cross-ledger intent commits [#cross-ledger-intent-commits]
Often, changes in one ledger are accompanied or backed by changes in another, external ledger. For example, let’s imagine a clearing system for interbank transfers implemented on ledger (RTP - Real Time Payment) where banks A and B are participating. This ledger will keep the balances for both banks and update them when a transfer occurs between them. If a user from bank A wants to send money to user in bank B, balance of bank A will decrease while balance of bank B will increase by the same amount in the RTP ledger. At the same time, bank A will debit the user account in their own ledger to cover the RTP balance decrease while the bank B will credit their user account in their own ledger as a response to RTP balance increase. Similar process is also performed in double-entry bookkeeping system. We want atomicity of this distributed transaction so that either all 3 participants will update their ledger records or none of them will.
Ledger has the ability to coordinate such distributed commits by employing 2PC (2-phase commit protocol) as a precondition before clearing an intent. 2PC process has 2 phases: **prepare** + **commit**.
P**repare** phase is used to notify participants about the intent and let them prepare and agree with it. **commit** phase is used to notify participants that intent should be committed on their side. If a participant replied positively during the **prepare** phase it is assumed that he can’t fail after that, meaning that he has to commit the transaction on his side after receiving the **commit** request.
From ledger perspective, this is performed by the following steps:
1. After intent is submitted and successfully verified, ledger will identify all participants involved in distributed commit of the intent and construct **debit** and **credit** entries for participants
2. Ledger sends **prepare** request to all participants. Participants which is referenced as a source of balance movement will get **prepare** request for **debit** entry, and participants which are referenced as target will get **prepare** request for **credit** entry.
3. Participant decides whether it will process the entry, makes the preparations on their side (reservation of funds etc.) and replies **prepared**
4. After ledger receives **prepared** from all participants, the ledger clears and commits the intent and sends **commit** request to all participants so they can commit the prepared entry. Status policy may apply if configured. See [status policy](/ledger/structuring-data/status-policies#what-is-a-status-policy) for more details.
5. Every participant commits the entry on their side and replies with **committed**
Intent can also fail if one of participants responds with **failed** after they receive **prepare** or if timeout happens while waiting for confirmations from participants. In that case step 4 and next steps are different:
1. Ledger discards all pending changes for intent, aborts it and sends **abort** request to all participants
2. Every participant discards prepared changes, aborts the entry on their side and replies with **aborted**
Before sending prepare requests (notifying bridges to prepare), the ledger creates **resolution proofs** that determine which bridges participate and what operations they perform. See [About Resolution Proofs](/ledger/moving-money/resolution-proofs) for details on how the ledger routes intents to bridges.
Requests and confirmations in detail:
* **prepare** - Ledger uses one of the transport mechanisms which are also used for [effects](/ledger/connecting-systems/extending-ledger). In case of REST, the **POST /v2/debits** or **POST /v2/credits** endpoints are called depending on whether the participant represents source or target of balance movement and the entry is sent in the payload. Debit entry contains the source address and the credit entry contains target address so the participant knows which account to debit or credit in their system. After that ledger awaits for **prepared** confirmation from participant. Entry has a **handle** which is unique identifier of the entry and is used for idempotency.
* **prepared** - Participant signals that it validated and prepared the entry (made reservations, etc…) by adding a signature to the intent referenced in the entry. Signature has `custom.status = 'prepared'` and other fields where they reference the received entry. It also adds a reference to the operation in their internal system related to this preparation by setting `custom.coreId` in the signature.
* **failed** - Participant rejects processing of the entry after it receives **prepare** by adding a signature with `custom.status = 'failed'` to the intent with additional rejection reason in `custom.reason` and `custom.detail`, and their internal error code in `custom.failId` if applicable.
* **commit** - Ledger notifies the participant that entry should be committed. In case of REST, the **POST /v2/debits/:handle/commit** or **POST /v2/credits/:handle/commit** endpoints are called. Payload contains the **handle** and the **action** which has the value `'commit'`.
* **abort** - Ledger notifies the participant that the entry should be aborted. In case of REST, the **POST /v2/debits/:handle/abort** or **POST /v2/credits/:handle/abort** endpoints are called. Payload also contains the **handle** and the **action** which has the value `'abort'`.
* **committed** - Participant replies to **commit** by adding a signature to an intent with `custom.status = 'committed'`. It’s expected from a participant to always respond to commit with **committed** as a proof of successful commit on their side. It can’t do anything to change the outcome of intent at this moment. Ledger will retry **commit** if participant doesn’t respond with **committed**. Participant can add additional field `custom.coreId` to signature in order to reference operation in their internal system related to this commit.
* **aborted** - Participant replies to **abort** by adding a signature to an intent with `custom.status = 'aborted'`. It’s expected from a participant to always respond to abort with **aborted** as a proof of successful abort on their side. It can’t do anything to change the outcome of intent at this moment. Ledger will retry **abort** if participant doesn’t respond with **aborted**. Participant can also add additional field `custom.coreId` to signature in order to reference operation in their internal system related to this abort.
Payload sent in prepare requests [#payload-sent-in-prepare-requests]
Although different transport mechanism can be used to deliver this request to participants, the payload of the request always has the same JSON structure representing the **entry** record signed by the ledger. The **data** of the entry record contains the following fields:
* **handle** - unique handle of the entry, used for idempotency, **prepare** and corresponding **commit** request will contain the same handle
* **schema** - identifier of the entry schema, represents the standard entry types in double-entry bookkeeping system, it can be: **debit** or **credit**
* **source** - source of balance movement in ledger, used only in **debit** entries
* **target** - target of balance movement in ledger, used only in **credit** entries
* **symbol** - symbol, unit of balance
* **amount** - amount of balance, it is a sum of amount from all intent claims which has the same **source** in case of **debit** entry, or the same **target** in case of **credit** entry
* **intent** - complete intent record which is the cause of balance movement
Examples of payloads for different **prepare** requests are given below:
* **POST /v2/debits**
```json
{
"hash": "...",
"data": {
"handle": "deb_baC0LUTVW9lzYA284",
"schema": "debit",
"source": {
"handle": "account:1234@bank1.com"
},
"symbol": {
"handle": "usd"
},
"amount": 100,
"intent": {
"hash": " ... ",
"data": { ... },
"meta": {
"status": "pending",
"thread": "YJxAMz52rMPiNYOZV",
"proofs": [ ... ]
}
}
},
"meta": { ... }
}
```
* **POST /v2/credits**
```json
{
"hash": "...",
"data": {
"handle": "cre_baC0LUTVW9lzYA284",
"schema": "credit",
"target": {
"handle": "account:1234@bank1.com"
},
"symbol": {
"handle": "usd"
},
"amount": 100,
"intent": {
"hash": " ... ",
"data": { ... },
"meta": {
"status": "pending",
"thread": "YJxAMz52rMPiNYOZV",
"proofs": [ ... ]
}
}
},
"meta": { ... }
}
```
Payload sent in commit and abort requests [#payload-sent-in-commit-and-abort-requests]
These requests contain the **command** related to previous entry. The payload also has standard record structure which is signed by the ledger. The **data** of the command record contains the following fields:
* **handle** - unique handle of the **entry** previously sent in **prepare** request
* **action** - action which needs to be performed with the entry, it can be: **commit** or **abort**
* **intent** - complete intent record which is the cause of balance movement with current status and signatures
Examples of payloads for different **commit** and **abort** requests are given below:
* **POST /v2/debits/:handle/commit**
```json
{
"hash": "...",
"data": {
"handle": "deb_baC0LUTVW9lzYA284",
"action": "commit",
"intent": {
"hash": " ... ",
"data": { ... },
"meta": {
"status": "committed",
"thread": "YJxAMz52rMPiNYOZV",
"proofs": [ ... ]
}
}
},
"meta": { ... },
}
```
* **POST /v2/credits/:handle/commit**
```json
{
"hash": "...",
"data": {
"handle": "cre_baC0LUTVW9lzYA284",
"action": "commit",
"intent": {
"hash": " ... ",
"data": { ... },
"meta": {
"status": "committed",
"thread": "YJxAMz52rMPiNYOZV",
"proofs": [ ... ]
}
}
},
"meta": { ... },
}
```
* **POST /v2/debits/:handle/abort**
```json
{
"hash": "...",
"data": {
"handle": "deb_baC0LUTVW9lzYA284",
"action": "abort",
"intent": {
"hash": " ... ",
"data": { ... },
"meta": {
"status": "aborted",
"thread": "YJxAMz52rMPiNYOZV",
"proofs": [ ... ]
}
}
},
"meta": { ... },
}
```
* **POST /v2/credits/:handle/abort**
```json
{
"hash": "...",
"data": {
"handle": "cre_baC0LUTVW9lzYA284",
"action": "abort",
"intent": {
"hash": " ... ",
"data": { ... },
"meta": {
"status": "aborted",
"thread": "YJxAMz52rMPiNYOZV",
"proofs": [ ... ]
}
}
},
"meta": { ... }
}
```
Signature used for prepared, failed, committed, aborted responses [#signature-used-for-prepared-failed-committed-aborted-responses]
Request confirmation response from 2PC participant is implemented by adding a signature to the intent proofs by the participant. 2PC coordinator keeps track of participant confirmations by verifying that they contain the correct **handle** related to the entries sent to participant.
Signature is added to intent proofs by calling ledger endpoint:
`POST /v2/intents/:handle/proofs`
Signature has a standard structure with addition of custom fields related to 2PC confirmation:
* **method** - reference to cryptographic algorithm
* **public** - public key of the signer
* **digest** - digest of the signature, created from `intent.data` and `signature.custom`
* **result** - signature value
* **custom.handle** - handle of the corresponding entry received from ledger
* **custom.status** - status reported to ledger as a response to a request, can be: **prepared**, **failed**, **committed** or **aborted**
* **custom.coreId** - reference in the participant system related to received request, can be internal core transaction ID or similar, used for referencing
* **custom.reason** - error reason code reported back to ledger, sent together with **failed** status, can be one of predefined ledger error reasons with prefix `bridge.`, see [Errors](/ledger/reference/error-reference)
* **custom.detail** - error detail message which describes the error
* **custom.failId** - reference to error in the participant system, can be internal error code, used for referencing
* **custom.moment** - date-time representing the moment when the signature is calculated and added to a record. It has a value in ISO datetime format expressed as UTC with millisecond precision, for example: `2023-02-20T21:42:10.279Z`
For each intent signature with **custom.handle** the signer who adds a signature must have granted action **sign** for record **intent** on the bridge record which is related to the entry. Otherwise the request to add signature will be rejected. See [About Authorization](/ledger/securing-the-ledger/about-authorization) for more details about access rules.
Examples of signatures for different types of confirmations are given below:
* **debit prepared**
```json
{
"method": "ed25519-v2",
"public": "...",
"digest": "...",
"result": "...",
"custom": {
"handle": "deb_baC0LUTVW9lzYA284",
"status": "prepared",
"coreId": "83147512640",
"moment": "2023-02-20T21:42:10.279Z"
}
}
```
* **credit prepared**
```json
{
"method": "ed25519-v2",
"public": "...",
"digest": "...",
"result": "...",
"custom": {
"handle": "cre_baC0LUTVW9lzYA284",
"status": "prepared",
"coreId": "83147512640",
"moment": "2023-02-20T21:42:10.279Z"
}
}
```
* **debit committed**
```json
{
"method": "ed25519-v2",
"public": "...",
"digest": "...",
"result": "...",
"custom": {
"handle": "deb_baC0LUTVW9lzYA284",
"status": "committed",
"coreId": "83147512640",
"moment": "2023-02-20T21:42:10.279Z"
}
}
```
* **credit committed**
```json
{
"method": "ed25519-v2",
"public": "...",
"digest": "...",
"result": "...",
"custom": {
"handle": "cre_baC0LUTVW9lzYA284",
"status": "committed",
"coreId": "83147512640",
"moment": "2023-02-20T21:42:10.279Z"
}
}
```
* **debit failed**
```json
{
"method": "ed25519-v2",
"public": "...",
"digest": "...",
"result": "...",
"custom": {
"handle": "deb_baC0LUTVW9lzYA284",
"status": "failed",
"coreId": "83147512640",
"reason": "bridge.account-insufficient-balance",
"detail": "Insufficient balance on the source account",
"failId": "6702",
"moment": "2023-02-20T21:42:10.279Z"
}
}
```
* **credit failed**
```json
{
"method": "ed25519-v2",
"public": "...",
"digest": "...",
"result": "...",
"custom": {
"handle": "cre_baC0LUTVW9lzYA284",
"status": "failed",
"coreId": "83147512640",
"reason": "bridge.account-inactive",
"detail": "Target account is inactive",
"failId": "4011",
"moment": "2023-02-20T21:42:10.279Z"
}
}
```
* **debit aborted**
```json
{
"method": "ed25519-v2",
"public": "...",
"digest": "...",
"result": "...",
"custom": {
"handle": "deb_baC0LUTVW9lzYA284",
"status": "aborted",
"coreId": "83147512640",
"moment": "2023-02-20T21:42:10.279Z"
}
}
```
* **credit aborted**
```json
{
"method": "ed25519-v2",
"public": "...",
"digest": "...",
"result": "...",
"custom": {
"handle": "cre_baC0LUTVW9lzYA284",
"status": "aborted",
"coreId": "83147512640",
"moment": "2023-02-20T21:42:10.279Z"
}
}
```
Identifying participants in 2PC [#identifying-participants-in-2pc]
2PC will be performed for intent with balance moving claims where source or target wallet has a bridge reference. Bridge is an entity registered in ledger which represents an external system or ledger which needs to execute entries as a response to balance movements in ledger. It is defined by **config.server:** the base url where bridge listens for ledger requests.
* **Example bridge record**
```json
// BRIDGE
{
"hash": "...",
"data": {
"handle": "hpb",
"schema": "rest",
"config": {
"server": "https://example.com/v2"
},
"secure": [ ... ],
"access": [{
"action": "sign-intent",
"signer": {
"public": ""
}
}]
},
"meta": { ... }
}
```
There is also **access** rule which grants the bridge public key to add confirmation signatures to an intent in the name of the bridge `{ "action": "sign", "record": "intent" }`
* **Example wallet with bridge reference**
```json
{
"hash": "...",
"data": {
"handle": "hpb",
"bridge": "hpb",
"access": [ ... ]
},
"meta": { ... }
}
```
Each bridge will be included in 2PC once per every occurrence either as the source or the target and also once per each different address it is associated with. For example:
* If same bridge is associated with both source and target of the intent claim, them it will be included in 2PC twice, once for **debit** on source and once for **credit** on target.
* If same bridge is associated with sources of two intent claims but the source addresses are different, then it will be also included twice in 2PC, once for **debit** on source address of the first claim and once for **debit** on source address of the second claim. (same pattern for **credit**)
* If same bridge is associated with sources of two intent claims but the source addresses are the same, then it will be included only once for **debit** on this source address in 2PC. Received entry in **prepare** request will then contain aggregated amount from both claims. (same pattern for **credit**)
* Any combination of 3 cases above is possible also with many claims and bridges.
Collecting confirmations from participants [#collecting-confirmations-from-participants]
Transaction coordinator will expect **prepared** response from every participant to which it sent prepare request. After all participants respond with **prepared** it will go to next phase where it will try to determine whether all required signatures are collected in order to perform `spend` on all involved source wallets. Signature requirements for spending from the wallet are configured through authorization rules described here: [About Authorization](/ledger/securing-the-ledger/about-authorization).
If some signatures are still missing after all participants responded, the intent will still be **pending** and **commit** phase will be delayed until all required signatures are collected, or timeout happens in the meantime. When finally all signatures are collected, **commit** phase will start and all participants will get **commit** request in order to complete the entry processing on their side.
Ledger 2PC roles [#ledger-2pc-roles]
**Ledger 2PC Coordinator**
Ledger has a role of 2PC transaction coordinator which communicates with participants and manages the **prepare** and **commit** phase.
**Ledger TXN Core - participant**
Ledger also has a participant role in 2PC in which it behaves as any external ledger. When it receives **prepare** request, it will validate that there are enough balances in the wallets in order to clear all claims in an intent, and then make a reservation of these balances so that these balances can’t be spent by other intents. After that it will respond with **prepared**, by adding a signature to the intent confirming that all balance movements stated in claims are ready to be committed. After that, when it receives **commit** from coordinator, it will persist changes of balances between the wallets.
Ledger TXN core is default participant in 2PC for every intent so it will automatically be notified with **prepare** request when 2PC starts.
Intent threads [#intent-threads]
Used to span 2PC protocol across multiple intents and that way guaranteeing atomicity over the set of intents. Sometimes a participant, as a precondition to accepting the distributed commit of intent in the **prepare** phase could add additional intent to be executed as the part of the same commit. For example, we use this mechanism in ledger routes where a new intent is added in case an outgoing route is defined on the target.
Every new intent is added to a thread by referencing original intent through `origin` property of the intent with the value of the previous intent handle. Original intent will not have this property.
Every new intent added to thread can then require additional participants which need to be prepared for a distributed commit. This process can repeat until all participants, including additional, become **prepared** and no new intents/participants are added. The set of the intents built that way is called INTENT THREAD and it’s committed atomically.
**prepare** requests are sent for each new intent in the thread, and each intent is prepared separately by the participants. So, it’s possible that one participant gets multiple prepare operations.
| Date | Changes |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 2023-12-13 | • Added description of intent status `prepared`
• Added description of intent `config` object
• Added details about intent action proofs to `commit` and `abort` with examples |
| 2023-09-19 | Replaced `/v2/intents/:handle/sign` endpoint to `/v2/intents/:handle/proofs` |
| 2023-08-22 | Replaced references to action `sign-intent` to `sign` on record `intent` |
| 2023-04-24 | Removed `signer` from `bridge.config` example and updated descriptions to not mention this property. Added sections which explain the access requirements for adding the 2PC related signature to intents (`sign-intent` action). |
| 2023-03-09 | Added `intent` to payload of commit/abort commands. Updated all API calls with /v2 prefix. |
| 2023-03-07 | Updated schema of bridge payloads (target, source and symbols are now objects) |
| 2023-03-07 | • Refactored `signature.schema` → `signature.method`, `meta.signatures` → `meta.proofs`
• Refactored `signer.schema` → `signer.format`
• Added `custom.moment` to examples |
| 2023-02-17 | Updated naming: changed ‘operation’ into \`request'. Updated rest interface examples with '/v2' prefix to follow bridge api versioning. |
| 2023-02-15 | Updated naming: introduced operation an entry. Updated payload of prepare operation: removed ‘action’ and added ‘schema’, removed ‘select’. Updated payloads of commit/abort operations: not they contain just ‘handle’ and ‘action’. Removed ‘action’ from bridge signatures. |
| 2023-01-27 | Added **action** property to event data which needs to be matched by the related signature added by the bridge. Updated JSON examples with new **action** property. |
| 2023-01-26 | Updated JSON examples with new signature schema. Added more detailed explanations and examples for messages exchanged in 2PC. Collapsed code examples into toggle lists. |
| 2023-01-19 | Updated intent states, event names, and updated json payloads of events and signatures. |
| 2022-12-20 | Initial version |
# About Resolution Proofs (/ledger/moving-money/resolution-proofs)
General [#general]
What are resolution proofs [#what-are-resolution-proofs]
Resolution proofs (also called "resolved proofs") are proofs created by the ledger during intent processing that serve as routing instructions for the two-phase commit (2PC) protocol.
They are added to an intent after ledger adds `pending` proof, and act as the foundation for all subsequent 2PC coordination and intent processing.
They are derived from `claims` present in the intent, and express how those claims are going to be executed and with which consequences, namely:
* They determine which external systems (bridges), if any, will participate in executing each part of an intent and establish the ledger's commitment to coordinate the distributed transaction.
* They determine which `wallet(s)` will receive and lose balance, of which `symbol` and of which `amount`
Claim types `destroy` and `issue` currently do not contact bridges. Resolved proofs will still contain references to bridge(s) assigned to the wallet from the claim(s).
Unlike participant proofs (prepared, committed, failed, aborted) which are created by bridges responding to ledger requests, resolution proofs are always created by the ledger itself using its own signing key.
Related documentation [#related-documentation]
* [About Intents](/ledger/moving-money/about-intents) - Overall intent processing lifecycle
* [About Balance Reservations](/ledger/moving-money/balance-reservations) - How balances are reserved based on info from debit resolution proofs
* [About Bridges](/ledger/connecting-systems/about-bridges) - How bridges communicate in 2PC
* [About Wallets](/ledger/moving-money/about-wallets) - Wallet routing and bridge assignment
How to find them [#how-to-find-them]
Resolution proofs are proofs in the intent, identifiable by having the value of `custom.status` as `resolved`, and being signed by ledger's public key.
Why they matter [#why-they-matter]
* **Immutable source of truth**: They create an auditable record of the ledger's routing decisions for the intent. These are the source of truth for:
* how much balance(s) should be moved from which wallet to which wallet's exact handle (no need to guess what the wallet address resolves into)
* which bridges the ledger will communicate with as participants
* **Entry tracking**: Each resolution proof contains a unique entry handle (starting with `deb_` or `cre_`) that is used throughout the 2PC lifecycle.
* **Balance reservation**: Resolution proofs with schema `debit` contain the information (wallet, amount, symbol) that the ledger uses to create balance reservations when it prepares internally (as a 2PC participant).
When they are created [#when-they-are-created]
Resolution proofs are created during the **pending** phase of intent processing, specifically:
1. Intent is created and validated (aspects execute)
2. Intent enters **pending** status
3. Ledger analyzes claims and determines routing
4. **Resolution proofs are created** for each debit/credit operation
5. DTC initiates communication with participant bridges, and processes intent
Resolution proofs are created **before** any bridge is contacted. They represent the ledger's plan for executing the intent, not the result of bridge responses.
Resolution proof structure [#resolution-proof-structure]
Resolution proofs follow the standard ledger proof structure with specific custom fields:
Example resolution proof:
```json
{
"method": "ed25519-v2",
"digest": "...",
"result": "...",
"public": "",
"custom": {
"status": "resolved",
"handle": "deb_xyz123", // Unique entry identifier
"schema": "debit", // Operation type: "debit" or "credit"
"wallet": "wallet_alice", // Target wallet for the operation
"bridge": "bridge_bank_a", // Bridge that must handle this operation
"symbol": "usd", // Symbol being transferred
"amount": 1000, // Amount for this operation
"inputs": [0], // Claim indices that contribute to this entry
"moment": "2025-01-15T10:30:00.000Z"
}
}
```
Key fields [#key-fields]
| Field | Type | Description |
| --------------- | --------- | ------------------------------------------------------------------------------------------ |
| `public` | string | Always the ledger's public key (identifies this as a ledger-created proof) |
| `custom.status` | string | Always `"resolved"` for resolution proofs |
| `custom.handle` | string | Unique resolution proof identifier, prefixed with `deb_` (debit) or `cre_` (credit) |
| `custom.schema` | string | Operation type: `"debit"` or `"credit"` |
| `custom.wallet` | string | Handle of the wallet to be debited or credited (exact wallet, no more resolution involved) |
| `custom.bridge` | string | Handle of the bridge responsible for this operation (optional) |
| `custom.symbol` | string | Symbol (currency/token) being transferred |
| `custom.amount` | number | Amount for this specific resolution proof |
| `custom.inputs` | number\[] | Array of claim indices that contribute to this resolution proof |
Examples [#examples]
Example 1: Simple transfer [#example-1-simple-transfer]
**Intent**: Transfer 100 USD from Alice to Bob
**Claims**:
```json
[
{
"action": "transfer",
"source": { "handle": "wallet_alice" },
"target": { "handle": "wallet_bob" },
"symbol": { "handle": "usd" },
"amount": 10000 // extra 00 because of `symbol` factor
}
]
```
**Resolution proofs created**:
```json
[
// Debit resolution proof for Alice
{
"method": "ed25519-v2",
"digest": "...",
"result": "...",
"public": "",
"custom": {
"status": "resolved",
"handle": "deb_abc123",
"schema": "debit",
"wallet": "wallet_alice",
"bridge": "bridge_bank_a",
"symbol": "usd",
"amount": 10000,
"inputs": [0]
}
},
// Credit resolution proof for Bob
{
"method": "ed25519-v2",
"digest": "...",
"result": "...",
"public": "",
"custom": {
"status": "resolved",
"handle": "cre_xyz789",
"schema": "credit",
"wallet": "wallet_bob",
"bridge": "bridge_bank_b",
"symbol": "usd",
"amount": 10000,
"inputs": [0]
}
}
]
```
Example 2: Multi-claim intent [#example-2-multi-claim-intent]
**Intent**: Exchange 100 USD for 85 EUR with 2 USD fee
**Claims**:
```json
[
{
"action": "transfer",
"source": { "handle": "wallet_alice" },
"target": { "handle": "wallet_exchange" },
"symbol": { "handle": "usd" },
"amount": 10000
},
{
"action": "transfer",
"source": { "handle": "wallet_exchange" },
"target": { "handle": "wallet_alice" },
"symbol": { "handle": "eur" },
"amount": 8500
},
{
"action": "transfer",
"source": { "handle": "wallet_exchange" },
"target": { "handle": "wallet_fee_collector" },
"symbol": { "handle": "usd" },
"amount": 200
}
]
```
**Resolution proofs created**: 6 total (one debit + one credit per claim)
```json
[
// Claim 0: Debit Alice 100 USD
{ "custom": { "handle": "deb_001", "wallet": "wallet_alice", "amount": 10000, "symbol": "usd", "inputs": [0] } },
// Claim 0: Credit Exchange 100 USD
{ "custom": { "handle": "cre_001", "wallet": "wallet_exchange", "amount": 10000, "symbol": "usd", "inputs": [0] } },
// Claim 1: Debit Exchange 85 EUR
{ "custom": { "handle": "deb_002", "wallet": "wallet_exchange", "amount": 8500, "symbol": "eur", "inputs": [1] } },
// Claim 1: Credit Alice 85 EUR
{ "custom": { "handle": "cre_002", "wallet": "wallet_alice", "amount": 8500, "symbol": "eur", "inputs": [1] } },
// Claim 2: Debit Exchange 2 USD fee
{ "custom": { "handle": "deb_003", "wallet": "wallet_exchange", "amount": 200, "symbol": "usd", "inputs": [2] } },
// Claim 2: Credit Fee Collector 2 USD
{ "custom": { "handle": "cre_003", "wallet": "wallet_fee_collector", "amount": 200, "symbol": "usd", "inputs": [2] } }
]
```
Example 3: Issue claim [#example-3-issue-claim]
**Intent**: Issue 1000 USD to merchant wallet
**Claims**:
```json
[
{
"action": "issue",
"target": { "handle": "wallet_merchant" },
"symbol": { "handle": "usd" },
"amount": 100000
}
]
```
**Resolution proofs created**: 1 (credit only, no debit for issue actions)
```json
[
{
"method": "ed25519-v2",
"digest": "...",
"result": "...",
"public": "",
"custom": {
"status": "resolved",
"handle": "cre_abc123",
"schema": "credit",
"wallet": "wallet_merchant",
"bridge": "bridge_merchant_system",
"symbol": "usd",
"amount": 100000,
"inputs": [0]
}
}
]
```
How resolution proofs work [#how-resolution-proofs-work]
The process of creating resolution proofs involves several steps:
1. Claim analysis [#1-claim-analysis]
The ledger analyzes the intent's claims to determine which operations need resolution:
* **Transfer claims** → Create both debit and credit entries (2 resolution proofs)
* **Issue claims** → Create credit resolution proof only (1 resolution proof)
* **Destroy claims** → Create debit resolution proof only (1 resolution proof)
2. Wallet routing [#2-wallet-routing]
The ledger follows wallet routing rules (if configured) to determine the final wallets that will be debited/credited and which bridges are assigned to those wallets. See [About Wallets](/ledger/moving-money/about-wallets) for details on routing mechanics.
Wallet routing can create chains where the actual debited/credited wallet differs from the wallet specified in the claim. Resolution proofs always reference the **final routed wallet**, not the original claim wallet.
3. Bridge entry creation [#3-bridge-entry-creation]
For each routed wallet operation, the ledger creates a bridge entry with:
* A unique handle (e.g., `deb_abc123` or `cre_xyz789`), of same value as corresponding resolution proof's handle
* The operation schema (debit or credit)
* The final routed wallet
* The bridge assigned to that wallet (if any)
* The symbol and amount
* The claim indices that contribute to this bridge entry
4. Proof signing and attachment [#4-proof-signing-and-attachment]
Each bridge entry is used to generate and sign a proof, which is then appended to the intent's `meta.proofs` array.
Relationship to 2PC requests [#relationship-to-2pc-requests]
Resolution proofs directly determine which prepare/commit/abort requests are sent to which bridge(s):
1. **Debit resolution proofs** → Determine which bridge receives debit prepare/commit/abort requests
2. **Credit resolution proofs** → Determine which bridge receives credit prepare/commit/abort requests
The bridge entry handle in prepare requests (e.g., `deb_abc123`) comes directly from the corresponding resolution proof's handle. Bridges must respond with participant proofs using this same handle to link their response to the correct resolution proof.
For complete details on 2PC request/response structures, see [About Bridges](/ledger/connecting-systems/about-bridges) and [About Intents](/ledger/moving-money/about-intents#cross-ledger-intent-commits).
Resolution proof lifecycle [#resolution-proof-lifecycle]
```
1. Intent created
↓
2. Resolution proofs created by ledger
- Signed with ledger's key, their `custom.status` is `resolved`
- Added to intent.meta.proofs
↓
3. Processing starts, using resolution proofs as source of truth
```
Resolution proofs remain in `intent.meta.proofs` throughout the entire intent lifecycle. They are never removed or modified, providing a permanent record of the original routing decisions.
Interaction with other concepts in system [#interaction-with-other-concepts-in-system]
Resolution proofs and balance reservations [#resolution-proofs-and-balance-reservations]
Resolution proofs with `schema: "debit"` contain the wallet, amount, and symbol information that the ledger uses to create balance reservations when it prepares internally as a 2PC participant.
See [About Balance Reservations](/ledger/moving-money/balance-reservations) for complete details on how reservations work throughout the 2PC lifecycle.
Wallet routing and resolution [#wallet-routing-and-resolution]
Resolution proofs reflect the **final routed wallets**, not necessarily the wallets specified in claims. This is important to understand if you've configured wallet routing rules.
**Scenario**: Alice's wallet has an outgoing route to Bob's wallet
**Claim**:
```json
{
"action": "transfer",
"source": { "handle": "wallet_alice" },
"target": { "handle": "wallet_charlie" },
"symbol": { "handle": "usd" },
"amount": 10000
}
```
**If wallet\_alice has route**: `{ "action": "debit", "target": "wallet_bob" }`
**Resolution proof reflects the routed wallet**:
```json
{
"custom": {
"status": "resolved",
"handle": "deb_xyz",
"schema": "debit",
"wallet": "wallet_bob", // Routed wallet, not wallet_alice
"bridge": "bridge_bank_b", // Bridge assigned to wallet_bob
"amount": 10000,
"inputs": [0]
}
}
```
See [About Wallets](/ledger/moving-money/about-wallets) for complete details on routing mechanics, depth limits, and cycle detection.
Troubleshooting [#troubleshooting]
Debugging with resolution proofs [#debugging-with-resolution-proofs]
Resolution proofs are valuable for debugging intent processing issues:
Check if resolution occurred [#check-if-resolution-occurred]
**Using the API**:
```bash
GET /v2/intents/:handle
# Check for proofs with custom.status === "resolved"
```
**Using the CLI**:
```bash
minka intent show -v
# Look for proofs with status "resolved" signed by ledger
```
Identify which bridges are involved [#identify-which-bridges-are-involved]
From the intent response, filter resolution proofs and extract the `custom.bridge` field:
```json
// Example: Extract bridges from resolution proofs
{
"meta": {
"proofs": [
{ "custom": { "status": "resolved", "bridge": "bridge_bank_a" } },
{ "custom": { "status": "resolved", "bridge": "bridge_bank_b" } },
{ "custom": { "status": "resolved", "bridge": "bridge_bank_a" } }
]
}
}
// Unique bridges involved: bridge_bank_a, bridge_bank_b
```
Match resolution proofs with participant responses [#match-resolution-proofs-with-participant-responses]
For each resolution proof, check if a participant proof with the same `custom.handle` exists:
```json
// Resolution proof
{ "custom": { "status": "resolved", "handle": "deb_abc123", "bridge": "bridge_x" } }
// Expected participant prepared proof (same handle)
{ "custom": { "status": "prepared", "handle": "deb_abc123" } }
// If missing → bridge_x has not responded to prepare request
```
Detect missing responses [#detect-missing-responses]
Compare resolution proof handles with participant proof handles to find missing responses:
```
Resolution proofs: [deb_001, cre_001, deb_002]
Participant proofs: [deb_001, cre_001]
Missing: deb_002 → Check which bridge was assigned deb_002
```
Common issues [#common-issues]
Issue: Intent stuck in pending, no resolution proofs [#issue-intent-stuck-in-pending-no-resolution-proofs]
**Symptom**: Intent remains in `pending` status, no proofs with `status: "resolved"` exist
**Possible causes**:
* Intent has no resolvable claims (only limit claims or unsupported actions)
* Routing failed (routed wallet doesn't exist or routing cycle detected)
* Ledger signer not configured (contact support to report a bug)
**What to check**:
* Verify all wallet handles in claims exist
* Check if wallets have routing rules that might cause issues
* Ensure claims use supported actions: `transfer`, `issue`, `destroy`
Issue: Wrong bridge assigned in resolution proof [#issue-wrong-bridge-assigned-in-resolution-proof]
**Symptom**: Resolution proof shows unexpected bridge
**Possible causes**:
* Wallet routing redirected to a different wallet with different bridge
* Wallet's `bridge` field was updated after you created the intent
**What to check**:
```bash
# Check wallet's current bridge assignment
GET /v2/wallets/:handle
# Response shows bridge field
{
"data": {
"handle": "wallet_alice",
"bridge": "bridge_bank_a" // ← Current bridge assignment
}
}
# Check wallet routes
{
"data": {
"routes": [
{ "action": "debit", "target": "wallet_bob" } // ← May redirect to different bridge
]
}
}
```
Issue: Bridge not receiving prepare requests [#issue-bridge-not-receiving-prepare-requests]
**Symptom**: Resolution proof exists with your bridge handle, but you're not receiving prepare requests
**Possible causes**:
* Bridge endpoint (`config.server`) misconfigured in bridge record
* Network connectivity issues between ledger and your Bridge
* Bridge not assigned to resolved wallet
**What to check**:
```bash
# Verify bridge configuration
GET /v2/bridges/:bridgeHandle (or `minka bridge show :bridgeHandle`)
# Check config.server URL is correct
{
"data": {
"handle": "your_bridge",
"schema": "rest",
"config": {
"server": "https://your-bridge.example.com/v2" // ← Verify this URL
}
}
}
```
```bash
# Verify bridge assigned to wallet from resolution proof
GET /v2/wallets/:walletHandle (or `minka wallet show :walletHandle`)
# Check bridge handle is correct
{
"data": {
"handle": "your_wallet",
"bridge": "your_bridge",
}
}
```
* Test your bridge `/status` endpoint is accessible
* Check your bridge logs for incoming requests
* Verify firewall rules allow ledger to reach your bridge
**Bridge implementers**: The prepare request entry includes the full intent with resolution proofs in `intent.meta.proofs`. When responding with participant proofs, you must use the same handle from the entry. For complete bridge implementation details, see [About Bridges](/ledger/connecting-systems/about-bridges) and [About Intents](/ledger/moving-money/about-intents#cross-ledger-intent-commits).
# About Intent Expiry (/ledger/moving-money/intent-expiry)
Introduction [#introduction]
A lot can happen during processing of an intent that can cause it to pause temporarily or even stop completely, especially when communicating with outside services like bridges. If an intent is not processed in time for whatever reason, it will be expired. This means that processing will never be finished and the intent will be aborted. As a consequence, the entire thread that the intent is a part of will also be aborted.
Processing [#processing]
Ledger continuously monitors the system for expired intents looking at the created moment which is the `custom.moment` of the signature with `custom.status = 'created'`. If an intent has no created moment, it will not be aborted as part of the expiration process. If the elapsed time is greater than the threshold and the intent is still in `pending` status, the entire thread is aborted which means all the intents will get an `aborted` signature with `core.intent-expired` reason and end up in the `aborted` status. An intent that is not `pending`, but still not in a final status like `completed` or `rejected` (eg. if it is `committed`), will not be expired.
An intent is not guaranteed to be aborted if it is expired, it only becomes eligible to be aborted and will be picked up by the next iteration of the expiry process. This process is run constantly, but it is still possible that intent processing continues between becoming expired and getting picked up by the process.
The expiry signature is of the form:
```json
{
"custom": {
"detail": "Intent j50DjJE9kWYtbNEYC expired",
"moment": "2023-07-30T22:35:02.262Z",
"reason": "core.intent-expired",
"status": "aborted"
},
"digest": "a36394c187c81f829ef3bb3df7e1a1c2e4afb65b14469a40401ef08a7314c63c",
"method": "ed25519-v2",
"public": "hrbD8DkjISL0t66M/rLtTZRqXC7N2P+gXYW5OohAIk4=",
"result": "pfRiROjKuOinZ9gYzIqLlpDDdxavExJ1SGuPF8CLn/6/gZAA+eOAI1v+W11f2c7kPXkaKHSdJBiZcijyO41gBQ=="
}
```
Configuration [#configuration]
To configure expiry, the `data.config.intentExpiryThresholdMinutes` property needs to be set to the number of minutes after which an intent should be expired.
```json
{
"hash": "...",
"data": {
"handle": "ledger-handle",
"signer": "ledger-signer",
"access": [ ... ],
"config": {
"dtc.intentExpiryMinutes": 60
}
}
"meta": {
...
}
}
```
Check out [about balance reservations](/ledger/moving-money/balance-reservations) for a better understanding
on how balances are managed for expired intents.
| Date | Changes |
| ----------------------- | --------------- |
| 2023-07-31 | Initial version |
# About Anchors (/ledger/moving-money/about-anchors)
What is an anchor? [#what-is-an-anchor]
An anchor is a ledger record that represents a link between user wallets in the ledger. It can be used in the onboarding process of alias directories.
An anchor is a ledger record that represents a more detailed information about some of the wallets property. A wallet can have one or more anchors and they are connected to wallet by using source field. Anchors can function with or without wallets, depending on the use case.
Anchor can be used in an alias directory as a representation of an account that is connected to a specific wallet. Anchors cannot hold balances.
```tsx
{
"data": {
"wallet": "tel:1234555555",
"target": "account:1234@bank"
"schema": "...",
"custom": {
...
},
"access": [...],
},
"hash": "...",
"meta": {...}
}
```
It is possible to define a `symbol` for an anchor. Users can find for anchors by filtering a `symbol`.
```tsx
{
"data": {
"wallet": "tel:1234555555",
"target": "account:1235@bank"
"schema": "...",
"symbol": "usd",
"custom": {
...
},
"access": [...],
},
"hash": "...",
"meta": {...}
}
```
The `custom` property can be used to include additional information such as its name and document info which is supported by the target account.
```tsx
{
"data": {
"source": "tel:1234555555",
"target": "account:1234@bank"
"schema": "account",
"symbol": "USD",
"custom": {
"name": "Melissa Ruiz",
"documentType": "ccpt",
"documentNumber": "814282133995806",
},
"access": [...],
},
"hash": "...",
"meta": {...}
}
```
| Date | Changes |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| 2023-10-24 | • Renamed `data.source` → `data.wallet`
• Added `data.symbol` property
• Removed reference to `advices` API |
| 2023-09-19 | Initial version |
# About Anchor Forwarding (/ledger/moving-money/anchor-forwarding)
Introduction [#introduction]
Anchor forwarding lets a ledger delegate anchor operations to a Bridge. This is useful when anchors are managed by an external system (for example, an external alias directory) while the ledger remains the public API surface and source of authorization decisions.
Before diving in, a brief note on alias directories. An alias directory is a service that maps human‑readable identifiers (like phone numbers or emails) to payment credentials. In deployments where an external alias directory (Bridge) owns anchors, anchor forwarding allows the ledger to forward create/update/sign/drop/read operations to that Bridge while still enforcing access rules and signing responses.
What gets forwarded [#what-gets-forwarded]
Ledger can forward the following anchor actions:
* Create
* Update
* Sign (add a proof)
* Drop (delete)
* Get (read one)
* FindAll (get many with filters)
Forwarding is controlled by configuration per action and, optionally, by a forwarding strategy.
Configuration [#configuration]
Anchor forwarding can be configured in two ways: **ledger configuration** or **processing policies**.
Option 1: Processing policies (Preferable) [#option-1-processing-policies-preferable]
Create a processing policy with `schema: 'processing'` to define forwarding rules. This approach offers more flexibility including conditional forwarding with filters. This can be used
to configure multiple alias directories depending on context e.g. request headers.
```tsx
await sdk.policy.init().data({
handle: 'anchor-forwarding',
schema: 'processing',
record: 'anchor',
filter: { 'ctx.req.headers': 'rail-1' }, // Optional: forward only matching anchors
values: [{
action: 'read',
schema: 'aspect',
invoke: { bridge: 'MyMailBridgeHandle' },
config: { strategy: 'fallback' | 'proxy' },
filter: { 'ctx.req.headers': 'rail-2' } // Value filter overrides root filter
}, {
action: 'read',
schema: 'aspect',
invoke: { bridge: 'MyBridgeHandle' },
config: { strategy: 'fallback' | 'proxy' }
}, {
action: 'query',
schema: 'aspect',
invoke: { bridge: 'MyBridgeHandle' },
config: { strategy: 'fallback' | 'proxy' }
}, {
action: 'create',
schema: 'aspect',
invoke: { bridge: 'MyBridgeHandle' },
config: { strategy: 'validate' | 'proxy' | 'synchronize' },
filter: { 'new.data.schema': 'qr-code' } // resolves to this config if created anchor is has schema qr-code
}, {
action: 'update',
schema: 'aspect',
invoke: { bridge: 'MyBridgeHandle' },
config: { strategy: 'validate' | 'proxy' | 'synchronize' }
}, {
action: 'sign',
schema: 'aspect',
invoke: { bridge: 'MyBridgeHandle' },
config: { strategy: 'validate' | 'proxy' | 'synchronize' }
}, {
action: 'drop',
schema: 'aspect',
invoke: { bridge: 'MyBridgeHandle' },
config: { strategy: 'validate' | 'proxy' }
}]
}).hash().sign([{ keyPair }]).send()
```
For complete details on processing policies, see [Processing Policies](/ledger/connecting-systems/processing-policies).
Option 2: Ledger configuration [#option-2-ledger-configuration]
Set the Bridge handle directly in ledger config. Update with CLI using `minka ledger update -e`:
```json
{
"forward.anchor.create": "MyBridgeHandle",
"forward.anchor.update": "MyBridgeHandle",
"forward.anchor.drop": "MyBridgeHandle",
"forward.anchor.sign": "MyBridgeHandle",
"forward.anchor.get": "MyBridgeHandle",
"forward.anchor.findAll": "MyBridgeHandle"
}
```
Optionally configure a global or per‑action strategy:
```json
{
"forward.anchor.strategy": "proxy | fallback | validate | none",
"forward.anchor.create.strategy": "proxy | validate | none",
"forward.anchor.update.strategy": "proxy | validate | none",
"forward.anchor.drop.strategy": "proxy | validate | none",
"forward.anchor.get.strategy": "proxy | fallback | none",
"forward.anchor.sign.strategy": "proxy | validate | none",
"forward.anchor.findAll.strategy": "proxy | fallback | none"
}
```
**Deprecation notice:** Configuring anchor forwarding via ledger configuration will be deprecated in a future release. We recommend using processing policies instead, as they provide more flexibility and better integration with the policy system. Please, migrate your forwarding config to processing policies.
Forwarding strategies [#forwarding-strategies]
* **proxy**: Forward the request without persisting locally (proxy‑only).
* **fallback**: Return local data when present; otherwise call the Bridge. Only for read/query actions.
* **validate**: Validate locally first, forward to Bridge, then persist if Bridge succeeds. Only for write actions.
* **synchronize**: Validate locally first, forward to Bridge, then persist user input to ledger and update it with bridge response. Only for `create`, `update` and `sign` actions.
* **none**: Do not forward for this action (ledger config only).
If no strategy is specified, sensible defaults are used: reads default to fallback; writes default to validate.
Bridge endpoints [#bridge-endpoints]
The Bridge must expose the same API surface as the ledger for anchors. Requests are forwarded as‑is (with some headers filtered and a ledger authorization added), and responses are expected to match the ledger Anchor API.
Required endpoints:
* POST `/v2/anchors`
* PUT `/v2/anchors/:id`
* DELETE `/v2/anchors/:id`
* GET `/v2/anchors/:id`
* POST `/v2/anchors/:id/proofs` (sign)
* GET `/v2/anchors` (findAll with filters)
Responses must conform to the ledger Anchor API. See the Anchor section in the [API Reference](/ledger/reference/api).
What the ledger forwards [#what-the-ledger-forwards]
* Request body/query params: Forwarded unchanged.
* Headers: Custom client headers are forwarded except a forbidden set. The original client Authorization (if present) is moved to `x-forwarded-authorization` and is not sent as `authorization` to the Bridge.
* Ledger authorization: The ledger adds its own Bearer JWT in `Authorization`, signed by the ledger. The JWT contains claims identifying the ledger and the intended Bridge audience.
Forbidden headers that are not forwarded include: `authorization`, `x-ledger`, `content-type`, `content-length`, `accept`, `accept-encoding`, `caller`, `connection`, `host`.
Responses and errors [#responses-and-errors]
Success responses [#success-responses]
The ledger returns the response from the Bridge after verifying hash and proofs are consistent:
* **Proxy mode:** Returns the Bridge response directly to the client.
* **Validate mode:** Returns the result of the local operation to the client (after confirming the Bridge call succeeded).
* **FindAll queries:** The entire page body from the Bridge is forwarded.
Error handling [#error-handling]
When a forwarded request to a Bridge fails, the ledger interprets and returns Bridge errors with meaningful structure whenever possible. The handling depends on the error type, status code, and response structure.
Error handling summary table [#error-handling-summary-table]
| Case | Bridge Response | Bridge Status Code | Ledger Response Status Code | Ledger Error Reason | Notes |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------- | --------------------------- | -------------------------------- | --------------------------------------------------------------- |
| **Valid bridge error** | Well-formed error with `data` (containing `reason`, `detail`, optional `custom`), `meta`, `hash`, and valid proofs | Any (e.g., 400, 404, 422) | **Same as bridge** | **Same as bridge `data.reason`** | Bridge error is forwarded to client with ledger signature added |
| **Missing fields** | Missing `data`, `meta`, or `hash` | Any | 502 | `forward.invalid-response` | Response structure is incomplete |
| **Invalid data structure** | `data` missing required `reason` or `detail` fields | Any | 502 | `forward.invalid-response` | Data doesn't conform to ledger error schema |
| **Invalid data types** | `reason` or `detail` not strings, or `custom` not a plain object | Any | 502 | `forward.invalid-response` | Data fields have wrong types |
| **Extra fields in data** | `data` contains fields other than `reason`, `detail`, `custom` | Any | 502 | `forward.invalid-response` | Data has unevaluated properties |
| **Invalid hash or proofs** | Hash doesn't match data or proofs fail verification | Any | 502 | `forward.invalid-response` | Cryptographic validation failed |
| **Ledger at fault** | Any response structure | 401 or 403 | 500 | `forward.unexpected-error` | Ledger credentials or config issue |
| **Connection error** | Network, timeout, or code error | N/A | 500 | `forward.unexpected-error` | Unexpected system error |
Detailed error cases with examples [#detailed-error-cases-with-examples]
1. Valid bridge error (forwarded to client) [#1-valid-bridge-error-forwarded-to-client]
When the Bridge returns a well-formed error response, the ledger validates the structure and cryptographic integrity, then forwards the error to the client with the Bridge's original status code.
**Bridge response (status 404):**
```json
{
"data": {
"reason": "record.not-found",
"detail": "Anchor with handle 'missing-anchor' not found",
"custom": {
"searchedHandle": "missing-anchor",
"bridgeContext": "alias-directory"
}
},
"hash": "7f3e8a2b9c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f",
"meta": {
"moment": "2025-11-04T10:30:00.000Z",
"proofs": [{
"custom": {
"moment": "2025-11-04T10:30:00.000Z"
},
"method": "ed25519-v2",
"result": "abc123...",
"public": "bridge-public-key",
"digest": "def456..."
}]
}
}
```
**Ledger response to client (status 404):**
```json
{
"data": {
"reason": "record.not-found",
"detail": "Anchor with handle 'missing-anchor' not found",
"custom": {
"searchedHandle": "missing-anchor",
"bridgeContext": "alias-directory"
}
},
"hash": "7f3e8a2b9c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f",
"meta": {
"moment": "2025-11-04T10:30:01.000Z",
"proofs": [
{
"custom": {
"moment": "2025-11-04T10:30:00.000Z"
},
"method": "ed25519-v2",
"result": "abc123...",
"public": "bridge-public-key",
"digest": "def456..."
},
{
"custom": {
"moment": "2025-11-04T10:30:01.000Z",
"causedBy": {
"detail": "Error derived from anchor forwarding response"
}
},
"method": "ed25519-v2",
"result": "xyz789...",
"public": "ledger-public-key",
"digest": "ghi012..."
}
]
}
}
```
**Key points:**
* Status code is preserved (404)
* Bridge error `reason` and `detail` are forwarded as-is
* Bridge `custom` data is preserved in `data.custom`
* Ledger appends its own proof to the `meta.proofs` array
* Ledger adds `causedBy` context in its proof's `custom` field
* Hash remains the same (hash of the original `data`)
2. Invalid bridge response - Missing required fields [#2-invalid-bridge-response---missing-required-fields]
**Bridge response (status 400):**
```json
{
"error": {
"code": "INVALID_ANCHOR",
"message": "Anchor data is invalid"
}
}
```
**Ledger response to client (status 502):**
```json
{
"data": {
"reason": "forward.invalid-response",
"detail": "Invalid response from bridge test-bridge"
},
"hash": "...",
"meta": {
"moment": "2025-11-04T10:30:01.000Z",
"proofs": [{
"custom": {
"moment": "2025-11-04T10:30:01.000Z"
},
"method": "ed25519-v2",
"result": "...",
"public": "ledger-public-key",
"digest": "..."
}]
}
}
```
**Key points:**
* Status code changes to 502 (Bad Gateway)
* Ledger generates its own error with reason `forward.invalid-response`
* Bridge response doesn't contain required `data`, `meta`, `hash` structure
3. Invalid bridge response - Wrong data structure [#3-invalid-bridge-response---wrong-data-structure]
**Bridge response (status 400):**
```json
{
"data": {
"errorCode": "ANCHOR_EXISTS",
"message": "Anchor already exists"
},
"hash": "...",
"meta": {
"proofs": [...]
}
}
```
**Ledger response to client (status 502):**
```json
{
"data": {
"reason": "forward.invalid-response",
"detail": "Invalid response from bridge test-bridge"
},
"hash": "...",
"meta": {
"moment": "2025-11-04T10:30:01.000Z",
"proofs": [{
"custom": {
"moment": "2025-11-04T10:30:01.000Z"
},
"method": "ed25519-v2",
"result": "...",
"public": "ledger-public-key",
"digest": "..."
}]
}
}
```
**Key points:**
* Status code changes to 502
* Bridge `data` object must contain `reason` and `detail` fields (not `errorCode` and `message`)
* Only `reason`, `detail`, and `custom` fields are allowed in `data`
4. Invalid bridge response - Invalid hash or proofs [#4-invalid-bridge-response---invalid-hash-or-proofs]
**Bridge response (status 422):**
```json
{
"data": {
"reason": "record.invalid",
"detail": "Anchor validation failed"
},
"hash": "incorrect-hash-value",
"meta": {
"proofs": [{
"method": "ed25519-v2",
"result": "invalid-signature",
"public": "bridge-public-key",
"digest": "..."
}]
}
}
```
**Ledger response to client (status 502):**
```json
{
"data": {
"reason": "forward.invalid-response",
"detail": "Invalid response from bridge test-bridge"
},
"hash": "...",
"meta": {
"moment": "2025-11-04T10:30:01.000Z",
"proofs": [{
"custom": {
"moment": "2025-11-04T10:30:01.000Z"
},
"method": "ed25519-v2",
"result": "...",
"public": "ledger-public-key",
"digest": "..."
}]
}
}
```
**Key points:**
* Status code changes to 502
* Ledger validates that `hash` matches the hash of `data`
* Ledger validates that proofs are valid for the given `hash`
* If validation fails, returns `forward.invalid-response`
5. Ledger credentials error (401/403) [#5-ledger-credentials-error-401403]
**Bridge response (status 401):**
```json
{
"data": {
"reason": "auth.unauthorized",
"detail": "Invalid JWT signature"
},
"hash": "...",
"meta": {
"proofs": [...]
}
}
```
**Ledger response to client (status 500):**
```json
{
"data": {
"reason": "forward.unexpected-error",
"detail": "Unexpected error while forwarding request to bridge"
},
"hash": "...",
"meta": {
"moment": "2025-11-04T10:30:01.000Z",
"proofs": [{
"custom": {
"moment": "2025-11-04T10:30:01.000Z"
},
"method": "ed25519-v2",
"result": "...",
"public": "ledger-public-key",
"digest": "..."
}]
}
}
```
**Key points:**
* Bridge returns 401 or 403, indicating ledger's credentials are invalid
* Status code changes to 500 (Internal Server Error)
* Ledger treats this as its own fault, not a bridge business error
6. Connection error (network, timeout, etc.) [#6-connection-error-network-timeout-etc]
**Example:** Network connection timeout, DNS resolution failure, or code exception.
**Ledger response to client (status 500):**
```json
{
"data": {
"reason": "forward.unexpected-error",
"detail": "Unexpected error while forwarding request to bridge"
},
"hash": "...",
"meta": {
"moment": "2025-11-04T10:30:01.000Z",
"proofs": [{
"custom": {
"moment": "2025-11-04T10:30:01.000Z"
},
"method": "ed25519-v2",
"result": "...",
"public": "ledger-public-key",
"digest": "..."
}]
}
}
```
**Key points:**
* Any error that's not from the bridge (HTTP response errors)
* Status code is 500
* Indicates system-level error, not a business error
Additional notes [#additional-notes]
* **No local mutation on Bridge failure:** In all write operations, if the Bridge returns any kind of error, the operation is not persisted or mutated locally, regardless of strategy.
* **Bridge error schema requirements:** For errors to be properly forwarded, Bridges must return error responses using the ledger error schema with `data.reason`, `data.detail`, and optional `data.custom` fields.
* **Proof validation:** All Bridge responses (both success and error) must include valid cryptographic proofs that the ledger can verify.
*See the [Anchor API Spec Reference](/ledger/reference/api/anchor/create-anchor) for additional details on error response schemas.*
Access checks and validation [#access-checks-and-validation]
* Writes (create/update/sign/drop): The ledger validates authorization before any forwarding occurs. It also performs local pre‑checks to ensure the DB operation would succeed (insert/update/sign/drop) when using the validate strategy.
* Reads (get/findAll): The ledger validates authorization on returned data. With `fallback`, the ledger will return local data when available; otherwise it fetches from the Bridge and then validates proofs and access before returning.
* Schema and proofs: Returned anchors are validated for proofs integrity. In validate strategy, local state is also verified before commit.
Proofs added by the ledger when forwarding [#proofs-added-by-the-ledger-when-forwarding]
When forwarding write operations, the ledger appends a proof with `custom.status = "forwarded"` to the outgoing meta before calling the Bridge. The Bridge response must still contain valid proofs; the ledger will verify them before returning to the client (or before persisting, in validate mode).
Flows [#flows]
Create [#create]
Update [#update]
Sign [#sign]
Drop [#drop]
Get by id/handle [#get-by-idhandle]
Get many (findAll) [#get-many-findall]
Security notes [#security-notes]
* The client Authorization header is not forwarded as `authorization`; it is copied to `x-forwarded-authorization` for the Bridge to inspect if needed.
* The ledger signs forwarded requests by adding its own Bearer JWT in `authorization`. Bridges can validate the signature with the ledger public key and may also require OAuth on top if desired.
* Standard Bridge authentication can be configured separately (e.g., OAuth 2.0). If a Bridge uses OAuth, the ledger will still send its own JWT while preserving the client token in `x-forwarded-authorization`.
* All communication between ledger and Bridge must use TLS.
Operational notes [#operational-notes]
* Errors from the Bridge are surfaced to clients with ledger signatures and without persisting local mutations.
* In proxy strategy, records are not stored locally. Reads will come from the Bridge and writes will only affect the Bridge.
* In validate strategy, local operations are performed only after the Bridge confirms success.
# Query Wallet Anchors (/ledger/moving-money/query-wallet-anchors)
Ledger SDK allows users to create anchors connected wallets and query them.
Creating anchors [#creating-anchors]
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
}
})
await sdk.anchor.init()
.data({
handle: 'tel_123_account_usd',
wallet: 'tel:123',
target: 'account',
symbol: 'usd',
access: [...]
})
.hash()
.sign([{ keyPair: yourKeyPair }])
.send()
await sdk.anchor.init()
.data({
handle: 'tel_123_account2_usd',
wallet: 'tel:123',
target: 'account2',
symbol: 'dop',
access: [...]
})
.hash()
.sign([{ keyPair: yourKeyPair }])
.send()
```
Query wallet anchors [#query-wallet-anchors]
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
}
})
const { anchors } = await sdk.wallet
.getAnchors('tel:123')
```
We are also able to apply some filters for the `target` and/or for the `symbol`
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
}
})
const { anchors } = await sdk.wallet
.getAnchors('tel:123', {
'data.target': 'account'
})
```
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
}
})
const { anchors } = await sdk.wallet
.getAnchors('tel:123', {
'data.symbol': 'usd'
})
```
Anchors resolved by wallet bridge are also included in the response if the ledger is able to resolve a bridge for the address `tel:123` . The bridge should implement `anchors` trait in order to receive this request. See [About Bridges](/ledger/connecting-systems/about-bridges) for more details about bridge anchors.
Useful links [#useful-links]
* See [About Anchors](/ledger/moving-money/about-anchors) for more details about this concept.
* See [About Bridges](/ledger/connecting-systems/about-bridges)
| Date | Changes |
| ----------------------- | --------------- |
| 2023-11-16 | Initial version |
# Manage Anchor Labels (/ledger/moving-money/manage-anchor-labels)
Ledger SDK allows users to tag anchors with labels.
Tagging an anchor with Ledger SDK [#tagging-an-anchor-with-ledger-sdk]
Bellow is an example of how to set a label `preferred` to the anchor `tel-123-usd` by using the Ledger SDK.
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
}
})
const { anchor, meta, hash } = await sdk.anchor.read('tel-123-usd')
await sdk.anchor
.from({
meta,
hash,
data: anchor
})
.hash()
.sign([{
keyPair: yourKeyPair,
custom: [{
labels: ['preferred']
}]
}])
.send()
```
Removing a tag from anchor with Ledger SDK [#removing-a-tag-from-anchor-with-ledger-sdk]
Bellow is an example of how to pull the label `preferred` from anchor `tel-123-usd` by using the Ledger SDK.
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
}
})
const { anchor, meta, hash } = await sdk.anchor.read('tel-123-usd')
await sdk.anchor
.from({
meta,
hash,
data: anchor
})
.hash()
.sign([{
keyPair: yourKeyPair,
custom: [{
labels: {
$pull: 'preferred'
}
}]
}])
.send()
```
Useful links [#useful-links]
* See [About Anchors](/ledger/moving-money/about-anchors) for more details about anchor concept.
* See [About Labels](/ledger/structuring-data/labels) for more details about labels concept and extensive list of operations allowed for managing record labels.
* See [About Labels Policies](/ledger/structuring-data/labels-policies) for more details about constraining labels and uniqueness criteria
| Date | Changes |
| ---------- | --------------- |
| 2024-07-15 | Initial version |
# Drop an Anchor (/ledger/moving-money/drop-anchor)
Ledger SDK allows users to drop anchors.
Dropping an anchor [#dropping-an-anchor]
Bellow is an example of how to drop anchor `tel-123-usd` by using the Ledger SDK.
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
}
})
await sdk.anchor
.drop('tel-123-usd')
.hash()
.sign([{ keyPair: yourKeyPair }])
.send()
```
You can also drop an anchor by passing the ledger record you want to delete, in cases where you have fetched it before.
```tsx
const record: LedgerRecord = {
hash: '',
data: {
handle: '',
wallet: '',
target: '',
symbol: '',
},
luid: '',
meta: {
proofs: [
{
custom: {
luid: '',
moment: '',
status: '',
},
digest: '',
method: '',
public: '',
result: '',
},
],
status: '',
moment: '',
owners: [''],
},
}
// Initialize a drop record builder for the anchor
const dropAnchorRecord = await sdk.anchor.drop(record).hash().sign([{ keyPair: yourKeyPair }])
/**
* Send the DELETE request to the backend.
* Optionally, you can pass authParams and headers to the send method.
*/
const response = await dropAnchorRecord.send({authParams, headers})
```
Useful links [#useful-links]
* See [About Anchors](/ledger/moving-money/about-anchors) for more details about anchor concept.
| Date | Changes |
| ----------------------- | ----------------------------------------------------------- |
| 2023-12-12 | Initial version |
| 2025-12-12 | Add an example for dropping an anchor using a ledger record |
# Building a clearinghouse (/ledger/moving-money/building-a-clearinghouse)
What we'll build [#what-well-build]
So far, we covered the basic concepts and created our user (signer).
Here is an overview of the steps we need to make in order to have a functional clearinghouse set up on Minka Ledger system:
Glossary [#glossary]
Server [#server]
Server is a place where Minka Ledger system is running on. The server stores one or multiple ledgers. And each clearinghouse is represented with one ledger.
Ledger [#ledger]
`ledger` is a record to which everything we do will be connected to. Clearinghouse is the owner of the ledger and has complete control over what's happening inside it.
Symbol [#symbol]
`symbol` is a ledger record through which we will define a currency.
Wallet [#wallet]
`wallet` record is a place where all balances will be stored. Every RTP participant (bank) will have one. And also a clearinghouse will have one settlement wallet.
Intent [#intent]
`intent` is a record that represents a transaction between wallets. And after an intent is created, signed, and validated (e.g. there are enough funds on the sending wallet), the wallet balance is also updated.
Signer [#signer]
`signer` is a public & private key-pair that is used to authorize every action on the Minka Ledger. Every record created, every update, and every permission change needs to be signed with the private key (which you can store locally on your computer).
Next steps [#next-steps]
This was a general overview of the system. Now it is time to start building it.
In our next lesson we will start by [creating a ledger](/ledger/moving-money/building-a-clearinghouse/create-ledger).
# Create a Ledger (/ledger/moving-money/building-a-clearinghouse/create-ledger)
Make a new ledger [#make-a-new-ledger]
By running `minka ledger create` and just choosing *default values* you'll be able to easily create a new ledger.
The only thing you need to choose is a unique name (`handle`) for your ledger.
Also, you will choose the signer you created in previous step and enter the password when prompted (if you defined a password during the signer creation process).
```console
$ minka ledger create
? Handle: tutorial-ledger
? Enter access content: Received
? Intent expiry threshold (minutes): 60
? Is wallet required before creating an anchor? Yes
? Is schedule policy processor enabled? No
? Add custom data? No
? Signer: clearinghouse
? Signer password for clearinghouse [hidden]
Ledger summary:
---------------------------------------------------------------------------
Handle: tutorial-ledger
Intent expiry threshold (minutes): 60
Wallet required before creating an anchor: true
Schedule policy processor enabled: false
⚠️ To allow a new ledger instance to sign specific operations
when needed, a new signer will be created and attached to it.
This new signer will be owned and managed by the server.
Its public key and key format are going to be available
for verification of the signature.
? Sign this ledger instance using signer clearinghouse? Yes
? Set 'tutorial-ledger' as an active ledger? Yes
? Apply layout to 'tutorial-ledger'? No
✅ Ledger instance created successfully:
Handle: tutorial-ledger
Intent expiry threshold (minutes): 60
Wallet required before creating an anchor: true
Schedule policy processor enabled: false
Access rules:
#0
- Action: any
- Record: any
Status: created
Luid: $ldg.-0-hWhZyOQRmvmytu
Handle: clearinghouse
Public: eIHlrG0Wr5vl3pDi5Vgqzu5WGE7q/60jsysBmehYKtg=
```
Selecting a ledger [#selecting-a-ledger]
One of the steps asked you whether you want to set `tutorial-ledger` as an active ledger. By choosing "yes", every action you perform, from that moment on, will be performed inside this ledger.
Alternative way to select [#alternative-way-to-select]
In case you forgot to set it as an active ledger during the creating process, or you just connected to the server at a later date, you can still use the `select` command:
```console
$ minka ledger select
? Handle: tutorial-ledger
Active ledger:
---------------------------------------------------------------------------
Handle: tutorial-ledger
Intent expiry threshold (minutes): 60
Wallet required before creating an anchor: true
Schedule policy processor enabled: false
Access rules:
#0
- Action: any
- Record: any
Status: created
```
# Define a Currency (/ledger/moving-money/building-a-clearinghouse/define-currency)
Creating a symbol [#creating-a-symbol]
Next, we will create a `symbol`.
Symbols are used to represent currencies (or anything that can be counted, for example, loyalty points, tokens, cryptocurrency etc.)
You can create a symbol by running `minka symbol create` command and entering the same values as described below:
```console
$ minka symbol create
? Handle: usd
? Factor: 100
? Add custom data? No
? Signer: clearinghouse
? Signer password for clearinghouse [hidden]
✅ Symbol created successfully:
Symbol summary:
---------------------------------------------------------------------------
Handle: usd
Factor: 100
Access rules:
#0
- Action: any
- Signer:
- public: eIHlrG0Wr5vl3pDi5Vgqzu5WGE7q/60jsysBmehYKtg=
Status: created
Luid: $sym.-0-jhViAkJbbQLgp5
Handle: clearinghouse
Public: eIHlrG0Wr5vl3pDi5Vgqzu5WGE7q/60jsysBmehYKtg=
```
Here we created an USD symbol (i.e. currency) with a minimal currency fraction of `1/100th` of a dollar (i.e. one cent).
# Grant Permissions (/ledger/moving-money/building-a-clearinghouse/granting-permissions)
Granting permissions [#granting-permissions]
Before proceeding with the tutorial, we need to grant permissions to the signer. By default, only the owner (creator) of a ledger has full access to it. Since we are going to create new signers, they need permissions to operate the system. Please note that this step is only required for the purposes of this tutorial. Doing so opens the ledger to anyone, which is extremely insecure and not recommended for production. You can learn more about these concepts in [About Authorization](/ledger/securing-the-ledger/about-authorization) and [About Authentication](/ledger/securing-the-ledger/about-authentication).
Next, we will grant permissions to signers in the system by creating a new `access` policy. This policy should have schema `access` and target `any` record. Type `minka policy create` command and proceed with suggested values.
```console
$ minka policy create
? Handle: full-access
? Record: any
? Extend an existing policy?: No
? Schema: access
? Add filter? No
? Define rules for this policy? Yes
? Enter policy values content: [{ "action": "any", "record": "any" }]
? Add custom data? No
? Set this policy inactive? No
? Signer: clearinghouse
? Signer password for clearinghouse [hidden]
✅ Policy created successfully:
Policy summary:
---------------------------------------------------------------------------
Handle: full-access
Schema: access
Record: any
Values:
#0
- Action: any
- Record: any
Access rules:
#0
- Action: any
- Signer:
- public:
Status: created
Luid: $plc.-0vdAocAmYNlIA_1q
Handle: clearinghouse
Public:
```
# Deposit Funds (/ledger/moving-money/building-a-clearinghouse/deposit-funds)
Use case [#use-case]
One of the most fundamental use cases is deposit/withdrawal functionality.
A bank might want to deposit funds that can be used inside the Minka Ledger and transact with other participants (accounts/wallets) in the system (banks, private or business accounts, etc.)
A bank might also want to withdraw excess funds from the Ledger system.
And in this tutorial we will go through the process of issuing new balances.
Creating a bank wallet [#creating-a-bank-wallet]
Creating a bank wallet is something a bank usually does with its own signer. To showcase deposit/withdraw functionality we need a "demo" bank wallet, so we will create bank signer and bank wallet using that signer.
To create a wallet we will pretend to be a bank and do the same steps we did previously:
* create a new "bank" signer (`demo-bank`)
* create a wallet using that signer
```console
$ minka signer create
? Handle: demo-bank
? Key pair source: Generate new key pair
? Add custom data? No
? Signer password: [hidden]
? Repeat password: [hidden]
Signer demo-bank saved locally.
? Store to ledger? No
✅ Signer created successfully:
Signer summary:
---------------------------------------------------------------------------
Handle: demo-bank
Public: RiQu4adTcR1elbgSSSOW84rviHLofGhXJQpOYdvHcQc=
Access rules:
#0
- Action: any
- Signer:
- public: RiQu4adTcR1elbgSSSOW84rviHLofGhXJQpOYdvHcQc=
⚠️ WARNING:: Secret or private key is critical data that should be handled
with care. Private keys are used to modify balances and it is important to
understand that anyone who has access to that key can perform sensitive
ledger operations.
```
In real life, this bank `signer` would never be on our (clearinghouse) computer and we would never have access to a private key (`secret`) from the bank. But here we're creating it so we can have a bank `wallet` on this `ledger`. And then we can showcase other actions that are possible for clearinghouse (publishing and removing currency from the system).
Now our "demo-bank" will create its `wallet`:
```console
$ minka wallet create
? Handle: demo-bank-wallet
? Bridge: [none]
? Add custom data? No
? Add routes? No
? Signer: demo-bank
? Signer password for demo-bank [hidden]
✅ Wallet created successfully:
Wallet summary:
---------------------------------------------------------------------------
Handle: demo-bank-wallet
Access rules:
#0
- Action: any
- Signer:
- public: RiQu4adTcR1elbgSSSOW84rviHLofGhXJQpOYdvHcQc=
Status: created
Luid: $wlt.-00e5aGYCTTf7TONk
Handle: demo-bank
Public: RiQu4adTcR1elbgSSSOW84rviHLofGhXJQpOYdvHcQc=
```
Notice how `luid` from this record starts with `$wlt...`, indicating it's a wallet record.
And also note, we used `demo-bank` signer to create this wallet, and not the `clearinghouse` signer.
Here is how we can check the balance of this newly created wallet:
```console
$ minka wallet balances demo-bank-wallet
Balances:
No balances found
```
The wallet is empty, but now we can go to the next step, and that's issuing money to bank's wallet.
Publish (issue) new balance [#publish-issue-new-balance]
We will issue `$1,000` to the `demo-bank-wallet` using `usd` currency we created in [previous tutorial](/ledger/moving-money/building-a-clearinghouse/define-currency).
Balance change in the `wallet` is represented in Minka Ledger system by `intent`. And intent is just another type of record.
To learn more about how wallet balance updates work, and where they are stored, check out [this document](/ledger/moving-money/building-a-clearinghouse/wallet-balances).
```console
$ minka intent create
? Handle: 1tBaX8vZcH1V0bFmU7XKP
? Action: issue
? Target: demo-bank-wallet
? Symbol: usd
? Amount: 1000
? Add another action? No
? Add custom data for this intent? No
? Signers: clearinghouse
? Signer password for clearinghouse [hidden]
Intent summary:
---------------------------------------------------------------------------
Handle: 1tBaX8vZcH1V0bFmU7XKP
Action: issue
- Target: demo-bank-wallet
- Symbol: usd
- Amount: $1,000.00
? Sign this intent using signer clearinghouse? Yes
✅ Intent signed and sent to ledger tutorial-ledger
Intent summary:
---------------------------------------------------------------------------
Handle: 1tBaX8vZcH1V0bFmU7XKP
Action: issue
- Target: demo-bank-wallet
- Symbol: usd
- Amount: $1,000.00
Access rules:
#0
- Action: any
- Signer:
- public: eIHlrG0Wr5vl3pDi5Vgqzu5WGE7q/60jsysBmehYKtg=
Luid: $int.-00eg3W9K0qai6mml
```
`handle` is a record ID which you can set to whatever value you want. But in most cases you can just leave the default one.
The defult handle is is created automatically (just for intents) as a convenience.
We can see new `intent` is created and `luid` starts with `$int...` which indicates intent record.
And it is signed with `clearinghouse` signer.
We can also list all the intents using the following command:
```console
$ minka intent list
╔══════════════════════════════════════════════════════════════════════════════════════════════════╗
║ Handle Action Source Target Amount Status ║
╟──────────────────────────────────────────────────────────────────────────────────────────────────╢
║ 1tBaX8vZcH1V0bFmU7XKP Issue - demo-bank-wallet $1,000.00 usd completed ║
╚══════════════════════════════════════════════════════════════════════════════════════════════════╝
```
And we can check `demo-bank-wallet` balance and see that the new amount is $1,000:
```console
$ minka wallet balances demo-bank-wallet
Balances:
$1,000.00 (usd)
```
# Withdraw Funds (/ledger/moving-money/building-a-clearinghouse/withdraw-funds)
Use case [#use-case]
In this tutorial we will focus on banks removing/withdrawing the funds from the Minka Ledger system.
Here is a more detailed overview of the steps when it comes to withdrawal process.
Creating a settlement wallet [#creating-a-settlement-wallet]
When a bank chooses to make a withdrawal, it will make a transaction to a `settlement wallet` that belongs to a clearinghouse.
This means a clearinghouse needs to have such a wallet.
And here we will create one.
```console
$ minka wallet create
? Handle: settlement-wallet
? Bridge: [none]
? Add custom data? No
? Add routes? No
? Signer: clearinghouse
? Signer password for clearinghouse [hidden]
✅ Wallet created successfully:
Wallet summary:
---------------------------------------------------------------------------
Handle: settlement-wallet
Access rules:
#0
- Action: any
- Signer:
- public: eIHlrG0Wr5vl3pDi5Vgqzu5WGE7q/60jsysBmehYKtg=
Status: created
Luid: $wlt.-00cL3j-UW8I7cV4l
Handle: clearinghouse
Public: eIHlrG0Wr5vl3pDi5Vgqzu5WGE7q/60jsysBmehYKtg=
```
Bank decides to withdraw funds [#bank-decides-to-withdraw-funds]
The first step in this process is for the bank to send the amount they wish to withdraw to the `settlement-wallet`.
Just as a reminder, currently we have 2 wallets in our ledger (`demo-bank-wallet` and `settlement-wallet`) which you can check using this CLI command in your terminal:
```console
$ minka wallet list
╔═════════════════════╤══════════╤══════════╤══════════╗
║ Handle │ Bridge │ Custom │ Routes ║
╟─────────────────────┼──────────┼──────────┼──────────╢
║ demo-bank-wallet │ None │ │ None ║
╟─────────────────────┼──────────┼──────────┼──────────╢
║ settlement-wallet │ None │ │ None ║
╚═════════════════════╧══════════╧══════════╧══════════╝
```
To initiate payout, `demo-bank` signer/user, will create an `intent` to make a `transfer` from `demo-bank-wallet` to `settlement-wallet`.
This is also how regular balance transfers are made between **any other two wallets** in the system (and not just `settlement-wallet`).
```console
$ minka intent create
? Handle: eHFmr4O2iSrrxYGAmuRnt
? Action: transfer
? Source: demo-bank-wallet
? Target: settlement-wallet
? Symbol: usd
? Amount: 300
? Add another action? No
? Add custom data for this intent? No
? Signers: demo-bank
? Signer password for demo-bank [hidden]
Intent summary:
---------------------------------------------------------------------------
Handle: eHFmr4O2iSrrxYGAmuRnt
Action: transfer
- Source: demo-bank-wallet
- Target: settlement-wallet
- Symbol: usd
- Amount: $300.00
? Sign this intent using signer demo-bank? Yes
✅ Intent signed and sent to ledger tutorial-ledger
Intent summary:
---------------------------------------------------------------------------
Handle: eHFmr4O2iSrrxYGAmuRnt
Action: transfer
- Source: demo-bank-wallet
- Target: settlement-wallet
- Symbol: usd
- Amount: $300.00
Access rules:
#0
- Action: any
- Signer:
- public: RiQu4adTcR1elbgSSSOW84rviHLofGhXJQpOYdvHcQc=
Luid: $int.-03C0rlvfdcD2ywor
```
We can also see this new intent in the list of intents:
```console
$ minka intent list
╔═══════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
║ Handle Action Source Target Amount Status ║
╟───────────────────────────────────────────────────────────────────────────────────────────────────────────────╢
║ eHFmr4O2iSrrxYGAmuRnt Transfer demo-bank-wallet settlement-wallet $300.00 usd completed ║
╟───────────────────────────────────────────────────────────────────────────────────────────────────────────────╢
║ 1tBaX8vZcH1V0bFmU7XKP Issue - demo-bank-wallet $1,000.00 usd completed ║
╚═══════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
```
The resulting balance of `demo-bank-wallet` is now `$700`:
```console
$ minka wallet balances demo-bank-wallet
Balances:
$700.00 (usd)
```
And the balance of the `settlement-wallet` is `$300`:
```console
$ minka wallet balances settlement-wallet
Balances:
$300.00 (usd)
```
Payout and removing the funds [#payout-and-removing-the-funds]
After the previous steps are done, clearinghouse will:
* make a payout to the bank
* remove balance from the Ledger system
To achieve this, a `clearinghouse` signer will make a new (`destroy`) intent that removes the balance from the `settlement-wallet`:
```console
$ minka intent create
? Handle: iBCFhzFpuCxuDI4hoErCp
? Action: destroy
? Source: settlement-wallet
? Symbol: usd
? Amount: 300
? Add another action? No
? Add custom data for this intent? No
? Signers: clearinghouse
? Signer password for clearinghouse [hidden]
Intent summary:
---------------------------------------------------------------------------
Handle: iBCFhzFpuCxuDI4hoErCp
Action: destroy
- Source: settlement-wallet
- Symbol: usd
- Amount: $300.00
? Sign this intent using signer clearinghouse? Yes
✅ Intent signed and sent to ledger tutorial-ledger
Intent summary:
---------------------------------------------------------------------------
Handle: iBCFhzFpuCxuDI4hoErCp
Action: destroy
- Source: settlement-wallet
- Symbol: usd
- Amount: $300.00
Access rules:
#0
- Action: any
- Signer:
- public: eIHlrG0Wr5vl3pDi5Vgqzu5WGE7q/60jsysBmehYKtg=
Luid: $int.-04-tX6uUuLUzCMPk
```
We can check that new `intent` was created:
```console
$ minka intent list
╔════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
║ Handle Action Source Target Amount Status ║
╟────────────────────────────────────────────────────────────────────────────────────────────────────────────────╢
║ iBCFhzFpuCxuDI4hoErCp Destroy settlement-wallet - $300.00 usd completed ║
╟────────────────────────────────────────────────────────────────────────────────────────────────────────────────╢
║ eHFmr4O2iSrrxYGAmuRnt Transfer demo-bank-wallet settlement-wallet $300.00 usd completed ║
╟────────────────────────────────────────────────────────────────────────────────────────────────────────────────╢
║ 1tBaX8vZcH1V0bFmU7XKP Issue - demo-bank-wallet $1,000.00 usd completed ║
╚════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
```
And we can also check the new `settlement-wallet` balance:
```console
$ minka wallet balances settlement-wallet
Balances:
$0.00 (usd)
```
This shows the balance is truly removed from the Ledger system and currency withdrawal is completed.
# Full Record Data (/ledger/moving-money/building-a-clearinghouse/full-record)
Overview [#overview]
To see full record response just use `-v` flag with any CLI command.
This will return an entire JSON record.
Wallet standard CLI response [#wallet-standard-cli-response]
If you want to see individual wallet, then run `minka wallet show demo-bank-wallet`.
This will give you limited information on the wallet:
```console
$ minka wallet show demo-bank-wallet
Wallet summary:
---------------------------------------------------------------------------
Handle: demo-bank-wallet
Access rules:
#0
- Action: any
- Signer:
- public: RiQu4adTcR1elbgSSSOW84rviHLofGhXJQpOYdvHcQc=
Status: created
```
Wallet record example [#wallet-record-example]
However if you run that same command with `-v` flag you will see the full `wallet` record (plus some head)
```json
$ minka wallet show demo-bank-wallet -v
{
"hash": "7fb437cc6302ccbdc8d62c5a96214ae01aa7ee63a6183f73f001af5dfad49603",
"data": {
"handle": "demo-bank-wallet",
"access": [
{
"action": "any",
"signer": {
"public": "RiQu4adTcR1elbgSSSOW84rviHLofGhXJQpOYdvHcQc="
}
}
]
},
"luid": "$wlt.-00e5aGYCTTf7TONk",
"meta": {
"proofs": [
{
"custom": {
"moment": "2025-03-10T16:42:12.255Z",
"status": "created"
},
"digest": "9041a65e20d0cf39f590442b1184fc77156bded7847504c21070b2ff3fcbd826",
"method": "ed25519-v2",
"public": "RiQu4adTcR1elbgSSSOW84rviHLofGhXJQpOYdvHcQc=",
"result": "A0UwWpWBVjyS0dd8pq92B+8XFLn1h+fOCLLSKt1X/0Gs/uChvrYjulaoY7/WoMqofFpAobfVCrh7dQ1oAGxtAA=="
},
{
"custom": {
"luid": "$wlt.-00e5aGYCTTf7TONk",
"moment": "2025-03-10T16:42:12.487Z",
"status": "created"
},
"digest": "c7b264e2b53909a8520c01c323b27a100d30ba0254fd4fd258bdf5a3d8987ec1",
"method": "ed25519-v2",
"public": "hnKFzEDkpMka/BnYj+s7+leFCpc9yNk8T7tNVPm8Xgc=",
"result": "QX2q9KPbmk7U9UFwesZxZIcndQrbHaun/+pSN+P5rtWjXmvTrkAam5BC1fqLZmmpsh90xpnp/fiVjbxc9V5HAg=="
}
],
"status": "created",
"moment": "2025-03-10T16:42:12.481Z",
"owners": [
"RiQu4adTcR1elbgSSSOW84rviHLofGhXJQpOYdvHcQc="
]
}
}
```
Where is the balance? [#where-is-the-balance]
One of the most important things in the Ledger is knowing the exact balance of each wallet or account.
However, as you can see the balance is ***not*** part of the wallet record.
So where is it?
We will discuss this in our [next lesson](/ledger/moving-money/building-a-clearinghouse/wallet-balances).
# Wallet Balances (/ledger/moving-money/building-a-clearinghouse/wallet-balances)
Keeping track of balances is the most important feature of the Ledger.
In our [previous lesson](/ledger/moving-money/building-a-clearinghouse/full-record) you could see that the `wallet` record doesn't actually hold any balance information.
Instead, it's located in the `wallet-balance` record.
Getting balance [#getting-balance]
When we run `minka wallet balances demo-bank-wallet` CLI command we see that we're sending `wallet` handle. Also, the CLI returns the actual balance:
```console
$ minka wallet balances demo-bank-wallet
Balances:
$700.00 (usd)
```
However, we already saw that this balance is not stored in the wallet record.
So where is it?
Getting wallet balance record [#getting-wallet-balance-record]
Let's request this same command with `-v` verbose flag:
```json
$ minka wallet balances demo-bank-wallet -v
{
"hash": "ac71d8add9fde91f5b4cbe9eed9c37011b8e5fcd7006e174976b307cbcfebc3b",
"meta": {
"proofs": [
{
"method": "ed25519-v2",
"public": "hnKFzEDkpMka/BnYj+s7+leFCpc9yNk8T7tNVPm8Xgc=",
"digest": "0f79a8cb1b45835b5c915af3d05ed0d8f5c6773ce65e6682d2d7188291079653",
"result": "yNZZ98Xzp3J3iu6QfUIKQSjR74i3+aShHjicaxR1ZnvbtMtOWZBBhRFGBB8TYgeyHR24g1ar9tF0RjKICFxkCQ==",
"custom": {
"moment": "2025-03-24T13:05:20.989Z"
}
}
],
"moment": "2025-03-24T13:05:20.989Z"
},
"data": [
{
"hash": "447c425fce7c25eb7c8c54e92715cb00d5360f7586c92163d61c1ea504f4e7e7",
"data": {
"parent": "d38a77bcef65c5033bc007cdbcbb15bad52ed0bdd3787cfaf237c61917c79a1a",
"wallet": "demo-bank-wallet",
"symbol": "usd",
"amount": 70000
},
"luid": "$wbl.-00eg3GMeRWR9HVOb",
"meta": {
"proofs": [
{
"custom": {
"moment": "2025-03-18T10:04:37.338Z"
},
"digest": "3441f22b0cb91947fad218e0c6ced5d778bb0c993db51df2c4bcd76b4eee6e77",
"method": "ed25519-v2",
"public": "hnKFzEDkpMka/BnYj+s7+leFCpc9yNk8T7tNVPm8Xgc=",
"result": "M+edyGoZrcxqLn6JGiOrOZ1GrHZH6DaXvjl6sNTS7oH3rz2+qbBSLH5FuFdw1lfX27M/CGHCODaP92yJ5ZpmBg=="
}
],
"moment": "2025-03-18T10:04:37.326Z"
}
}
],
"page": {
"index": 0,
"limit": 20,
"total": 1
}
}
```
From the record `luid` we can see that the ballance is stored in `wallet-balance` record:
```
"luid": "$wbl.-00eg3GMeRWR9HVOb"
```
And the amount is expressed in fractions of the currency. This was defined during the creation of new currency through `symbol` record in one of the [earlier lessons](/ledger/moving-money/building-a-clearinghouse/define-currency).
Updating balances [#updating-balances]
When we create a new `intent` (for example - type `transfer`), Ledger will check whether there are sufficient balances.
If a transaction is valid, the balances will be updated. If not, the transfer will fail.
Also, whenever the amount is changed, this change must also be signed. This ensures the integrity of the balances.
You can see the history of all signatures in the `proofs` array.
# Structuring Data (/ledger/structuring-data)
The ledger supports flexible data structures beyond the built-in record types. This section covers how to attach custom data, enforce schemas, organize records with labels, and manage configurations with layouts.
Basics [#basics]
Data Policies [#data-policies]
Layouts [#layouts]
Layouts define reusable configurations of ledger records that can be applied, exported, and synced across environments.
Tutorials [#tutorials]
Prerequisites [#prerequisites]
* **[Start Here](/ledger/start-here)** — Understand ledger basics and record types
* **[Moving Money](/ledger/moving-money)** — Familiarity with wallets and intents
# Custom Fields (/ledger/structuring-data/custom-fields)
Each ledger record supports attaching custom data through the `custom` property. This property can contain any key/value pairs that represent valid JSON. When making api requests using the SDK those fields can be added when constructing an object, for example when creating a wallet through nodeJS:
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
}
})
const { wallet } = await sdk.wallet.init()
.data({
handle: 'test-wallet',
custom: {
stringProperty: 'some string value',
numberProperty: 134.2,
booleanProperty: false,
}
})
.hash()
.sign([{ keyPair: yourKeyPair }])
.send()
```
Custom data can be also added interactively through CLI:
```console
$ minka wallet create
? Handle: test-wallet
? Bridge: [none]
? Add custom data? Yes
? Field class: string
? Field title: stringProperty
? Field value: some string value
? Add another field? Yes
? Field class: number
? Field title: numberProperty
? Field value: 134.2
? Add another field? Yes
? Field class: boolean
? Field title: booleanProperty
? Field value: false
? Add another field? No
? Add routes? No
? Signer: treasury
✅ Wallet created successfully:
Handle: test-wallet
Signer: MCowBQYDK2VwAyEAqySfsabYtxyEwAYtJQRv0NyRkfjC65hmZ9ZwnCmsso4= (treasury)
Custom:
- stringProperty: some string value
- numberProperty: 134.2
- booleanProperty: true
```
| Date | Changes |
| ----------------------- | ----------------------------------------------------------------- |
| 2023-03-09 | Refactored `ledger` → `sdk` |
| 2023-02-27 | Refactored `signer.schema` → `signer.format` |
| 2023-02-10 | SDK Options refactoring (`url` to `server` and `key` to `signer`) |
| 2022-09-07 | Initial version |
# About Schemas (/ledger/structuring-data/schemas)
Ledger database provides multiple record types with strict (built-in) schemas for all root fields except the `custom` field which can contain anything. On different scenarios, users of the ledger may need to extend those schemas to match their use cases.
What is a Schema [#what-is-a-schema]
A `schema`, in the ledger context, is just another record that can be created by using the API. The schema record defines a set of rules and constraints that will be used to validate the record that schema is intended for. All `schemas` have a record property which refers to the type of record that schema will be used on, that record property can be one of `symbol`, `signer`, `wallet`, `intent`, `effect`, `bridge` or `circle`.
Custom schemas extend the built-in schemas of the ledger, but they don't replace them. This mean that incoming records are validated first against built-in schemas and then again custom schemas.
How are schemas structured [#how-are-schemas-structured]
The only supported schema format, for now, is json-schema. **Json Schema** is a declarative language that allows to specify different rules, types and constraints that will be used to validate the input JSON. You can read more about it on it's [oficial webpage](https://json-schema.org/). Please notice that ledger uses the **Draft-07** from the JSON Schema specifications in order to have a better performance.
* Example of Schema with this Draft
```jsx
const schema = {
definitions: {
IssueClaim: {
type: "object",
required: ["action", "target", "symbol", "amount"],
properties: {
action: {
type: "string",
const: "issue",
},
target: {
type: "string",
},
symbol: {
type: "string",
},
amount: {
type: "integer",
},
},
},
TransferClaim: {
type: "object",
required: ["action", "source", "target", "symbol", "amount"],
properties: {
action: {
type: "string",
const: "transfer",
},
source: {
type: "string",
},
target: {
type: "string",
},
symbol: {
type: "string",
},
amount: {
type: "integer",
},
},
},
DestroyClaim: {
type: "object",
required: ["action", "source", "symbol", "amount"],
properties: {
action: {
type: "string",
const: "destroy",
},
source: {
type: "string",
},
symbol: {
type: "string",
},
amount: {
type: "integer",
},
},
},
},
type: "object",
required: ["data"],
properties: {
data: {
type: "object",
required: ["handle", "claims"],
properties: {
handle: {
type: "string",
},
claims: {
type: "array",
items: {
type: "object",
discriminator: { propertyName: "action" },
oneOf: [
{
$ref: "#/definitions/IssueClaim",
},
{
$ref: "#/definitions/TransferClaim",
},
{
$ref: "#/definitions/DestroyClaim",
},
],
},
},
},
},
},
};
```
* Example of Data that can be validated against previous schema
```jsx
const intent = {
data: {
// If no handle is provided, it will return error
// handle: 'x12345',
claims: [
// Valid transfer claim
{
action: "transfer",
source: "acc1",
target: "acc2",
symbol: "usd",
amount: 10,
},
// Valid issue claim
{
action: "issue",
target: "acc2",
symbol: "usd",
amount: 10,
},
// Valid destroy claim
{
action: "destroy",
source: "acc1",
symbol: "usd",
amount: 10,
},
// missing symbol
{
action: "transfer",
source: "acc1",
target: "acc2",
amount: 10,
},
// invalid action
{
action: "fake",
source: "acc1",
target: "acc2",
amount: 10,
},
// missing amount
{
action: "issue",
target: "acc1",
symbol: "usd",
},
// missing symbol
{
action: "destroy",
source: "acc1",
amount: 10,
},
],
},
};
```
All errors are thrown, if you have 3 errors in your schema they will be thrown and separated by comma. Example for provided schema and data:
`data.data.claims.3 must have required property 'symbol', data.data.claims.4 value of tag "action" must be in oneOf, data.data.claims.5 must have required property 'amount', data.data.claims.6 must have required property 'symbol’`
When are schemas validated [#when-are-schemas-validated]
Schemas are validated at the moment of attempting to create or update a new record. The `.data` property is validated against the specified schema. If that validation fails, error will be thrown and the intended operation will not be completed.
💡 Schemas for records are specified by passing `schema's` handle in `.data.schema` property.
Schemas are optional, but… [#schemas-are-optional-but]
Custom `schema` validation is an optional feature of the ledger, however, if there is at least one schema created for any record type, then the `schema` property is required for all subsequent records being updated or created under said type.
| Date | Changes |
| ----------------------- | --------------- |
| 2023-05-25 | Initial version |
# About Statuses (/ledger/structuring-data/statuses)
This document aims to help users managing record statuses.
What is a record status? [#what-is-a-record-status]
Status - `meta.status` - is a property of records that describe its current state on the system.
Ledger by default creates stores records with status `created` - except intents, that are initially stored as `pending`.
Record statuses can be customized by users to adapt their use cases. For example:
* Managing signers:
* `invited` : The signer was invited to join the network
* `active` : The signer has activated its account
* `blocked` : The signer was blocked due to suspicious activity on the system
* and so on
* Managing wallets:
* `suspended`: The wallet has been temporarily disabled due to suspicious activity or policy violations.
* `locked`: The wallet is temporarily locked, to prevent unauthorized access.
* and so on
```tsx
GET /v2/signer/bank-admin
{
hash: '...',
data: {
handle: 'bank-admin',
...
},
meta: {
...,
status: 'active'
},
...
}
```
```tsx
GET /v2/wallets/acc:1234
{
hash: '...',
data: {
...,
handle: 'acc:1234',
},
meta: {
...,
status: 'suspended'
},
...
}
```
Managing record statuses [#managing-record-statuses]
Users can manage status of a record within a proof. Proofs should be sent to separate call to
`POST /{record}s/{id}/proofs` e.g. `POST /signers/bank-admin/proofs`.
```tsx
POST /v2/signers/bank-admin/proofs
{
method: 'ed25519-v2',
public: '',
digest: '',
result: '',
custom: {
moment: '2023-11-27T17:18:13.034Z',
status: 'blocked'
}
}
```
```tsx
GET /v2/signer/bank-admin
{
hash: '...',
data: {
handle: 'bank-admin',
...
},
meta: {
...,
status: 'blocked'
},
...
}
```
A proof can also completely remove the current status of a record, by passing the special value `null`.
```tsx
POST /v2/signers/bank-admin/proofs
{
method: 'ed25519-v2',
public: '',
digest: '',
result: '',
custom: {
moment: '2023-11-27T17:20:13.034Z',
status: null
}
}
```
```tsx
GET /v2/signer/bank-admin
{
hash: '...',
data: {
handle: 'bank-admin',
...
},
meta: {
proofs: [...],
owners: [...],
moment: ...
},
...
}
```
Managing record statuses with policies [#managing-record-statuses-with-policies]
This section uses status policies to define which statuses are valid for a record as well as their quorum. Please, read [About Status Policies](/ledger/structuring-data/status-policies) for keeping up to date with this concept.
```json
POST /v2/policies
{
...,
data: {
handle: 'bank-signer-status',
schema: 'status',
record: 'signer',
filter: {
schema: 'bank-signer'
},
values: [
{
quorum: [{
$circle: 'admin'
}]
}
],
custom: {
description: 'Policy for any status of a bank signer in ledger'
}
}
}
```
Given the policy above, let's imagine that the following signers, represented by their public keys, are members of circle `admin` :
* signer A: WAweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd/aILprE=
* signer B: AILprEWWAweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd=
First, a signer with public key `FmzUTaV74NRFAd/aILprE=WAweF9PHlboQoW0z8NqhZX`, which is not part of circle `admin`, tries to update a bank signer status from `active` to `blocked`
```tsx
POST /v2/signers/bank-admin/proofs
{
method: 'ed25519-v2',
public: 'FmzUTaV74NRFAd/aILprE=WAweF9PHlboQoW0z8NqhZX',
digest: '4969e3c012b66d88cec597bf337fc01eab8d651e6ed2d5c40236cc1f7d93435a',
result: '0G2gvSfBx6MwPT8ShBaiYx7zwa5Kqc4Cq3S3NXV1m5/ZPozoH/SUouuhi9sQU+f0yo0eX4ygH7PzE3PAdlxsCQ==',
custom: {
status: 'blocked',
moment: '2023-11-27T17:21:13.034Z'
}
}
```
The proof will be stored but it won't modify the signer status, because this signer is not part of part of circle admin.
```tsx
GET /v2/signer/bank-admin
{
hash: '...',
data: {
handle: 'bank-admin',
...
},
meta: {
proofs: [
...,
{
method: 'ed25519-v2',
public: 'FmzUTaV74NRFAd/aILprE=WAweF9PHlboQoW0z8NqhZX',
digest: '4969e3c012b66d88cec597bf337fc01eab8d651e6ed2d5c40236cc1f7d93435a',
result: '0G2gvSfBx6MwPT8ShBaiYx7zwa5Kqc4Cq3S3NXV1m5/ZPozoH/SUouuhi9sQU+f0yo0eX4ygH7PzE3PAdlxsCQ==',
custom: {
status: 'blocked',
moment: '2023-11-27T17:21:13.034Z'
}
}
],
...,
status: 'active'
},
...
}
```
Then, the signer A, with public WAweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd/aILprE= , that is part of circle `admin` , sends a proof to update a bank signer status from `active` to `blocked`
```tsx
POST /v2/signers/bank-admin/proofs
{
method: 'ed25519-v2',
public: 'WAweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd/aILprE=',
digest: '4969e3c012b66d88cec597bf337fc01eab8d651e6ed2d5c40236cc1f7d93435a',
result: 'BaiYx7zwa5Kqc4Cq3S3N0G2gvSfBx6MwPT8ShXV1m5/ZPozoH/SUouuhi9sQU+f0yo0eX4ygH7PzE3PAdlxsCQ==',
custom: {
status: 'blocked',
moment: '2023-11-27T17:25:14.034Z'
}
}
```
The proof will be stored and it will, because quorum required by policy to signer status has fulfilled.
```tsx
GET /v2/signer/bank-admin
{
hash: '...',
data: {
handle: 'bank-admin',
...
},
meta: {
proofs: [
...,
{
method: 'ed25519-v2',
public: 'WAweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd/aILprE=',
digest: '4969e3c012b66d88cec597bf337fc01eab8d651e6ed2d5c40236cc1f7d93435a',
result: 'BaiYx7zwa5Kqc4Cq3S3N0G2gvSfBx6MwPT8ShXV1m5/ZPozoH/SUouuhi9sQU+f0yo0eX4ygH7PzE3PAdlxsCQ==',
custom: {
status: 'blocked',
moment: '2023-11-27T17:25:14.034Z'
}
}
],
...,
status: 'blocked'
},
...
}
```
Querying by statuses [#querying-by-statuses]
Statuses can also be useful for querying, ledger allows users to query records by statuses
```json
GET /v2/signers?meta.status=active
// Returns an array of all signers that are "active" in ledger
```
MongoDB Query Language works by doing contains operation on arrays if you pass a single value, see more info [here](https://www.mongodb.com/docs/manual/tutorial/query-arrays/#query-an-array-for-an-element).
Notifying about changed record status [#notifying-about-changed-record-status]
Changing the status of the record fires also `-updated` event where the new state of record will be delivered to the listener in the record meta of the payload. Also the last proof which caused the change in status will be included in this payload.
For the signer `bank-admin` above when the `blocked` status is set, applied the following event payload will be delivered to the listener who registered the effect for `signer-update`
```json
{
...,
data: {
signal: 'signer-updated',
parent: {
...,
data: {
...
},
meta: {
status: 'active',
proofs: [ ... ],
}
},
signer: {
...,
data: {
...
},
meta: {
status: 'blocked',
proofs: [ ..., {
...,
custom: {
status: 'blocked',
moment: '2023-11-27T17:25:14.034Z'
}
}]
}
}
}
}
```
| Date | Changes |
| ----------------------- | --------------- |
| 2024-07-15 | Initial version |
# About Proofs (/ledger/structuring-data/about-proofs)
A proof is a cryptographic signature appended to a ledger record. Proofs are the mechanism for changing a record's status, managing its labels, recording authorization decisions, and storing application-specific data. Every record maintains a list of proofs in `meta.proofs`.
Adding a proof [#adding-a-proof]
To add a proof, send a POST request to the record's `/proofs` endpoint:
```
POST /v2/{resource}/{id}/proofs
```
The proof must be signed against the record's **current hash**. This ensures the signer has seen the latest version of the record before approving a change. The signature is verified by the server — if it doesn't match the current hash, the proof is rejected.
What proofs can change [#what-proofs-can-change]
Each proof can include custom fields that request changes to the record:
* **`custom.status`** — request a status transition (e.g., `pending` → `active`)
* **`custom.labels`** — add or remove labels (supports `$push`, `$pull`, `$pullAll`)
* **`custom.moment`** — timestamp of when the proof was signed
The record's data fields are not modified by proofs — only metadata like status and labels.
Custom fields and audit trail [#custom-fields-and-audit-trail]
Beyond status and label changes, proofs can carry arbitrary custom fields in the `custom` object. This makes proofs a signed, append-only audit trail for recording application-specific events, decisions, or external references.
For example, a proof might record a compliance review decision, an external transaction ID, or a processing note — all cryptographically signed by the acting signer. Since proofs are never modified or removed, they provide a tamper-evident history of all actions taken on a record.
Quorum and incremental signing [#quorum-and-incremental-signing]
Policies can require multiple signers to approve a status transition. This is called a **quorum**. When a quorum is defined:
1. The first signer adds a proof requesting a status change
2. The proof is **accepted and stored** in `meta.proofs`, but the status **does not change** yet
3. Additional signers add their proofs for the same status
4. When the required quorum is met, the status transitions
This allows each signer to review and approve independently. There is no error when quorum is not yet met — the proof is simply stored until enough signatures are collected.
When proofs are rejected [#when-proofs-are-rejected]
A proof is rejected entirely (not stored) when:
* **Invalid signature** — the proof was not signed against the record's current hash
* **No permission** — the signer does not have access to add proofs to this record
* **Invalid status transition** — a status policy does not allow the requested status value
* **Invalid labels** — a label policy does not allow the requested labels
When a proof is rejected, the record is not modified and an error is returned.
Response [#response]
On success, the endpoint returns the updated record with the new proof appended to `meta.proofs`. If the proof triggered a status or label change, the updated values are reflected in `meta.status` and `meta.labels`.
Related [#related]
* [About Statuses](/ledger/structuring-data/statuses) — how statuses work and how to manage them
* [Status Policies](/ledger/structuring-data/status-policies) — define valid statuses and quorum requirements
* [Labels](/ledger/structuring-data/labels) — how labels work on records
* [Keys and Signatures](/ledger/securing-the-ledger/keys-and-signatures) — cryptographic key pairs and signing
# About Labels (/ledger/structuring-data/labels)
This document describes the concept of labels and helps users leveraging its use in their use cases.
What is a label? [#what-is-a-label]
Label - `meta.labels` - is a property of records that allows users to attach a label or tag anything in ledger with a descriptive name. Some examples of the usage of labels in ledger:
* preferred accounts: marking certain wallets or anchors as preferred.
* organizing records: marking wallets as saving accounts, cards, loans, etc.
```tsx
GET /v2/anchors/anchor
{
hash: '...',
data: {
handle: 'anchor',
wallet: 'anchor-wallet',
target: 'target-wallet',
symbol: 'usd',
...
},
meta: {
...,
labels: ['preferred']
},
...
}
```
```tsx
GET /v2/wallets/saving-wallet
{
hash: '...',
data: {
...,
handle: 'saving-wallet',
},
meta: {
...,
labels: ['saving-account']
},
luid: '$wlt.ijidjiddpsdpl'
}
```
The regular `custom` fields can be used to accomplish many of those use cases as well and they are preferable in most situations. Labels can be an alternative when it makes sense in same ways you can use tags in many apps to organize and group.
Labels vs Custom [#labels-vs-custom]
Even though `custom` is applicable for most of the cases, labels can have uniqueness constraints defined via policies in ledger.
Let's dive into the example of marking a preferred account in the system.
An account could be marked as preferred by having a preferred property in `custom`,
i.e.
```tsx
{
...
handle: 'anchor',
wallet: 'anchor-wallet',
target: 'target-wallet',
symbol: 'usd',
custom: {
preferred: true
},
}
```
but this basic approach is very limited in several main ways.
1. The first issue is that you can have only one preferred account, sometimes you want to have a preferred per symbol, this becomes difficult to manage with `custom` .
2. The second issue is related to security, marking something as preferred requires you to have full access to manage that record in ledger, since you need to update it to set a value.
3. The last main issue is related to data consistency but it overlaps a bit with the security aspect. The issue is related to removing the preferred field from the record that was a preferred previously. This can either cause multiple records being marked as preferred or requires someone to have access to both the new and the currently preferred records. Often this is very difficult to implement while keeping everything secure and protected.
Managing record labels [#managing-record-labels]
Users can manage labels of a record within a proof. Proofs with labels can be attached to records when they are created - `POST /{record}s` e.g. `POST /anchors` - or with a separate call to
`POST /{record}s/{id}/proofs` e.g. `POST /anchors/anchor-handle/proofs`.
```tsx
POST /v2/wallets
{
hash: '...',
data: {
...,
handle: 'saving-wallet',
},
meta: {
...,
proofs: [{
method: 'ed25519-v2',
public: '',
digest: '',
result: '',
custom: {
status: 'created',
moment: '2024-07-10T00:00:00',
labels: ['saving-account']
}
}]
}
}
```
```tsx
POST /v2/wallets/saving-wallet/proofs
{
method: 'ed25519-v2',
public: '',
digest: '',
result: '',
custom: {
moment: '2023-11-27T17:18:13.034Z',
labels: ['saving-account']
}
}
```
A proof can push, pull and make complex changes in a record labels list.
Below is extensive list of expressions the ledger supports for changing record labels:
```json
// 1. Append label "preferred"
"labels": { "$push": "preferred" }
// 2. Append labels "preferred" and "verified"
"labels": { "$push": { "$each": ["preferred", "verified"] } }
// 3. Insert labels "preferred" and "verified" before all other labels
"labels": { "$push": { "$each": ["preferred", "verified"], "$position": 0 } }
// 4. Insert labels "preferred" and "verified" before all other labels
// and limit the resulting labels count to 3 by removing labels
// from the tail
"labels": { "$push": { "$each": ["preferred", "verified"], "$position": 0, "$slice": 3 } }
// 5. Append labels "preferred" and "verified" and sort the resulting labels
// descending
"labels": { "$push": { "$each": ["preferred", "verified"], "$sort": -1 } }
// 6. Append label "preferred" unless it's already present
"labels": { "$addToSet": "preferred" }
// 7. Append labels "preferred" and "verified", each label will be appended
// independently unless it's already present
"labels": { "$addToSet": { "$each": ["preferred", "verified"] } }
// 8. Remove label "preferred" if present
"labels": { "$pull": "preferred" }
// 9. Remove labels "preferred" and "verified" if present
"labels": { "$pull": { "$in": ["preferred", "verified"] } }
// 10. Remove labels "preferred" and "verified" if present, the same result
// as previous one but different operator
"labels": { "$pullAll": ["preferred", "verified"] }
// 11. Remove the first label
"labels": { "$pop": -1 }
// 12. Remove the last label
"labels": { "$pop": 1 }
```
The simple array expression from above which replaces the `labels` array in record completely can be also expressed as complex expression. So the following 2 have the same meaning:
```tsx
// Replace all labels with array of 2 labels: "preferred" and "verified". It can
// be expressed in 2 ways.
// 1. Simple expression which sets labels to exact value
"labels": ["preferred", "verified"]
// 2. Complex expression: push "preferred" and "verified" but slice the resulting
// labels so that it contains 2 items, by removing surplus prefix items.
"labels": { "$push": { "$each": ["preferred", "verified"], "$slice": -2 } }
```
Some examples:
```tsx
Wallet before proof
{
...,
meta: {
...,
labels: ['saving-account', 'preferred']
}
}
POST /v2/wallets/saving-wallet/proofs
{
method: 'ed25519-v2',
public: '',
digest: '',
result: ''
custom: {
labels: {
$pull: 'saving-account'
},
moment: '2023-11-27T17:18:13.034Z'
}
}
Wallet after proof
{
...,
meta: {
...,
labels: ['preferred']
}
}
```
```tsx
Wallet before proof
{
...,
meta: {
...,
labels: ['saving-account', 'preferred', 'active']
}
}
POST /v2/wallets/saving-wallet/proofs
{
method: 'ed25519-v2',
public: '',
digest: '',
result: ''
custom: {
labels: {
$pullAll: [
'saving-account',
'preferred'
]
},
moment: '2023-11-27T17:18:13.034Z'
}
}
Wallet after proof
{
...,
meta: {
...,
labels: ['active']
}
}
```
```tsx
Wallet before proof
{
...,
meta: {
...,
labels: ['active']
}
}
POST /v2/wallets/saving-wallet/proofs
{
method: 'ed25519-v2',
public: '',
digest: '',
result: ''
custom: {
labels: {
$push: {
$each: ['saving-account', 'preferred'],
$position: 0
}
},
moment: '2023-11-27T17:18:13.034Z'
}
}
Wallet after proof
{
...,
meta: {
...,
labels: ['saving-account', 'preferred', 'active']
}
}
```
```tsx
Wallet before proof
{
...,
meta: {
...,
labels: ['active']
}
}
POST /v2/wallets/saving-wallet/proofs
{
method: 'ed25519-v2',
public: '',
digest: '',
result: ''
custom: {
labels: {
$addToSet: {
$each: ['active', 'preferred']
}
},
moment: '2023-11-27T17:18:13.034Z'
}
}
Wallet after proof
{
...,
meta: {
...,
labels: ['active', 'preferred']
}
}
```
```tsx
Wallet before proof
{
...,
meta: {
...,
labels: ['active', 'preferred']
}
}
POST /v2/wallets/saving-wallet/proofs
{
method: 'ed25519-v2',
public: '',
digest: '',
result: ''
custom: {
labels: {
$pop: 1
},
moment: '2023-11-27T17:18:13.034Z'
}
}
Wallet after proof
{
...,
meta: {
...,
labels: ['active']
}
}
```
```tsx
Wallet before proof
{
...,
meta: {
...,
labels: ['active', 'preferred']
}
}
POST /v2/wallets/saving-wallet/proofs
{
method: 'ed25519-v2',
public: '',
digest: '',
result: ''
custom: {
labels: {
$pop: -1
},
moment: '2023-11-27T17:18:13.034Z'
}
}
Wallet after proof
{
...,
meta: {
...,
labels: ['preferred']
}
}
```
Managing labels with policies [#managing-labels-with-policies]
This section uses labels policies to define which labels are valid for a record as well as their uniqueness criteria. Please, read [About Labels Policies](/ledger/structuring-data/labels-policies) for keeping up to date with this concept.
```json
POST /v2/policies
{
...,
data: {
handle: 'preferred-account-anchor-per-wallet',
schema: 'labels',
record: 'anchor',
filter: {
schema: 'account'
},
values: [
{
labels: ['preferred'],
unique: ['wallet']
}
],
custom: {
description: 'Marks a preferred account anchor per wallet'
}
}
}
```
Given the policy above, let’s look at an example when we have two account anchors on the same wallet:
```json
[
{
handle: 'svgs.100100.mint',
wallet: 'tel:1234',
schema: 'account',
target: 'svgs:100100@mint',
symbol: 'usd'
},
{
handle: 'svgs.200200.tesla',
wallet: 'tel:1234',
schema: 'account',
target: 'svgs:200200@tesla',
symbol: 'usd'
}
]
```
First, we label the `svgs.100100.min` anchor as `preferred`:
```tsx
POST /v2/anchors/svgs.100100.mint/proofs
{
method: 'ed25519-v2',
public: 'WAweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd/aILprE=',
digest: '4969e3c012b66d88cec597bf337fc01eab8d651e6ed2d5c40236cc1f7d93435a',
result: '0G2gvSfBx6MwPT8ShBaiYx7zwa5Kqc4Cq3S3NXV1m5/ZPozoH/SUouuhi9sQU+f0yo0eX4ygH7PzE3PAdlxsCQ==',
custom: {
labels: ['preferred'],
moment: '2023-11-27T17:18:13.034Z'
}
}
```
After that, someone else marks the `svgs.200200.tesla` anchor as `preferred`:
```tsx
POST /v2/anchors/svgs.200200.tesla/proofs
{
method: 'ed25519-v2',
public: 'WAweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd/aILprE=',
digest: '4969e3c012b66d88cec597bf337fc01eab8d651e6ed2d5c40236cc1f7d93435a',
result: '0G2gvSfBx6MwPT8ShBaiYx7zwa5Kqc4Cq3S3NXV1m5/ZPozoH/SUouuhi9sQU+f0yo0eX4ygH7PzE3PAdlxsCQ==',
custom: {
labels: ['preferred'],
moment: '2023-11-27T17:18:14.034Z'
}
}
```
Because the labels policy doesn’t allow `preferred` to be assigned to multiple anchors with the same wallet and symbol, the ledger will actually move the `preferred` label from `svgs.100100.mint` to `svgs.200200.tesla`.
Reassigning labels automatically prevents bugs because of incorrect implementations and makes it simpler to connect to the system because there are fewer operations that external systems need to perform.
This behavior makes the feature easy to use to end users, and makes labels the way to go in complex situations like that. In this example, users don’t have to worry about what is marked as `preferred` currently, they only need to state what they want as final state. Because of policies, we also get a strict data model, without duplicates and invalid states.
Automatic removal of unique label [#automatic-removal-of-unique-label]
Since the initiator appends a proof with `labels` operation to a record, this proof will correspond to the labels mutation on the same record. On the other hand, the record that looses a label due to `unique` constraint will have a proof signed by ledger system signer which will instruct the removal of this label. An example of a proof added by the ledger for example above.
```tsx
// Ledger adds this proof to `svgs.100100.mint` anchor internally
{
method: 'ed25519-v2',
public: '',
digest: '4969e3c012b66d88cec597bf337fc01eab8d651e6ed2d5c40236cc1f7d93435a',
result: '0G2gvSfBx6MwPT8ShBaiYx7zwa5Kqc4Cq3S3NXV1m5/ZPozoH/SUouuhi9sQU+f0yo0eX4ygH7PzE3PAdlxsCQ==',
custom: {
labels: { $pull: 'preferred' },
detail: 'Reassigning {preferred} label to the record {svgs.200200.tesla}'
moment: '2023-11-27T17:18:13.034Z'
}
}
```
Querying by labels [#querying-by-labels]
Labels can also be useful for querying, ledger allows users to query labeled records by using label names:
```json
GET /v2/wallets/tel:1234/anchors?meta.labels=preferred
// Returns an array of all anchors labeled as "preferred"
// Always returns an array, even it there is a single match
// because it is a query on a collection
```
MongoDB Query Language works by doing contains operation on arrays if you pass a single value, see more info [here](https://www.mongodb.com/docs/manual/tutorial/query-arrays/#query-an-array-for-an-element).
Notifying about changed record labels [#notifying-about-changed-record-labels]
Changing the labels of the record fires also `-updated` event where the new state of record labels will be delivered to the listener in the record meta of the payload. Also the last proof which caused the change in labels will be included in this payload.
For the anchor `svgs.200200.tesla` above when the `preferred` label is applied the following event payload will be delivered to the listener who registered the effect for `anchor-updated`
```json
{
...,
data: {
signal: 'anchor-updated',
parent: {
...,
data: {
...
},
meta: {
labels: [],
proofs: [ ... ]
}
},
anchor: {
...,
data: {
...
},
meta: {
labels: ['preferred'],
proofs: [ ..., {
...,
custom: {
labels: ['preferred'],
moment: '2023-11-27T17:18:14.034Z'
}
}]
}
}
}
}
```
Also for the anchor `svgs.100100.mint` the `anchor-updated` event will be delivered where the `preferred` label is removed:
```json
{
...,
data: {
signal: "anchor-updated",
parent: {
...,
data: {
...
},
meta: {
labels: ['preferred'],
proofs: [ ... ]
}
},
anchor: {
...,
data: {
...
},
meta: {
labels: [],
proofs: [ ..., {
...,
custom: {
labels: { '$pull': 'preferred'},
moment: '2023-11-27T17:18:14.034Z'
}
}]
}
}
}
}
```
| Date | Changes |
| ----------------------- | --------------- |
| 2024-07-10 | Initial version |
# About Referenced Records (/ledger/structuring-data/referenced-records)
In some cases, records need to reference other records in the ledger. In those cases, the **handle** property is used as value and the **record name in singular** is used as key.
For example, referenced **signer** of a **ledger** is shown as follows:
```jsx
{
"hash": "",
"data": {
"handle": "",
"signer": "",
"access": [...]
},
"meta": {
"proofs": [...]
}
}
```
This means that signer with handle `` exists in the active ledger.
Other examples [#other-examples]
Wallet → Bridge
```jsx
{
"hash": "",
"data": {
"handle": "",
"bridge": "",
"access": [...]
},
"meta": {
"proofs": [...]
}
}
```
Balance → Wallet, Balance → Symbol
```jsx
{
"hash": "",
"data": {
"wallet": "",
"symbol": "",
"amount": 0
},
"meta": {
"proofs": [...],
}
}
```
| Date | Changes |
| -------------- | ------- |
| - | - |
# About Status Policies (/ledger/structuring-data/status-policies)
What is a status policy? [#what-is-a-status-policy]
Status policies are used to define which custom statuses a record can have as well as the required quorum to change to these statuses. They have schema `status` which defines a specific structure of policy rules contained in `values` of the policy.
```tsx
POST /v2/policies
{
...,
data: {
handle: '...',
schema: 'status',
record: '',
filter: { ... },
values: [ ... ]
}
}
```
At root level, the status policy can have fields which limit the policy to specific records:
* `record` - defines that policy is only for specific record type, optional, if not defined policy applies to all record types
* `filter` - record must match the filter in order for policy to be applied to it, optional, if not defined policy applies to all records
```jsx
POST /v2/policies
{
...,
"data": {
"handle": "fintech-wallet-status",
"schema": "status",
"record": "wallet",
"filter": { "data.schema": "fintech" }
"values": [ ... ]
}]
}
}
```
The principle `open unless whitelisted` which we follow in all kinds of ledger rules is valid here also. So if for a record whose status is to be changed there is not any matching status policy (by record and filter) then the operation will be accepted. Otherwise the operation needs to be explicitly granted by one of applicable policy rules.
Property `filter` represents standard ledger filtering concept where language used in filter expression is mongo-compatible. This concept is already used in: access rules and effects.
Statuses policies are exposed on the same endpoint as access policies with distinction that the schema property is equal to `status` while for access policies schema is `access`. Schema of the policy record defines the structure of `values` array in policy.
Constraining quorum for statuses [#constraining-quorum-for-statuses]
```jsx
POST /v2/policies
{
...,
"data": {
"handle": "wallet-status",
"schema": "status",
"record": "wallet",
"values": [{
"quorum": [{
"public": "WAweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd/aILprE="
}]
}]
}
}
```
In the above case, in order for status to be changed there must be a proof with `custom.status` made by a signer with public key `WAweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd/aILprE=` as specified in the `quorum` field of a policy. Quorum is array of signer references which follows the same structure as the signer references in [access rules and policies](/ledger/securing-the-ledger/about-authorization). For example record owner can also be referenced in policy by using `"quorum": [{ "$record": "owner" }]` .
If based on policies, the signer is not in quorum required to set a status, or if additional proofs are required by quorum list, there will be no error. Only side effect will be that proof with status will be stored in record proofs but record status will not be changed.
Except `quorum`, the rule in status policy has optional field `status` which makes a rule applicable only to specific status. The policy below will requires a proof from signer(public) `AweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd/aILprE=` to change `wallet` status to `"active"`. Setting other statuses to wallet will not be allowed unless there is another policy/rule which grants that explicitly.
```jsx
POST /v2/policies
{
...,
"data": {
"handle": "wallet-active",
"schema": "status",
"record": "wallet",
"values": [{
"status": "active",
"quorum": [{
"public": "WAweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd/aILprE="
}]
}]
}
}
```
By default the `status` in policy rule is string and in this form it represents equality. It can also be a valid mongo compatible sub-expression for matching single object field as shown below by using `$in`.
```jsx
POST /v2/policies
{
...,
"data": {
"handle": "wallet-active-inactive",
"schema": "status",
"record": "wallet",
"values": [{
"status": { $in: ["active", "inactive"] },
"quorum": [{
"public": "WAweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd/aILprE="
}]
}]
}
}
```
To define a quorum for **removing** a label from a record, there is special value `null`. The same value is used in proofs for requesting the status removal.
```jsx
POST /v2/policies
{
...,
"data": {
"handle": "wallet-remove-status",
"schema": "status",
"record": "wallet",
"values": [{
"status": null,
"quorum": [{
"public": "WAweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd/aILprE="
}]
}]
}
}
```
A rule can also allow combination of removing and setting specific statuses, for example: `"status": { $in: ["active", "inactive", null] }`
Controlling proof selection [#controlling-proof-selection]
The property `quorum.proofSelection` in the `config` object can be used to define how proofs are selected when evaluating quorum for status changes. This setting is specified at the policy level, not within individual rules.
Latest Chain of Proofs (default) [#latest-chain-of-proofs-default]
When `quorum.proofSelection` is set to `latest-chain` or omitted, only the proofs from the latest set of proofs with the target status are considered for quorum calculation.
A "chain" means a sequence of proofs with same status.
```jsx
POST /v2/policies
{
...,
"data": {
"handle": "wallet-activate-status",
"schema": "status",
"record": "wallet",
"config": {
"quorum.proofSelection": "latest-chain"
},
"values": [{
"status": {"$in": ["activated", "deactivated"]},
"quorum": [{
"public": "WAweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd/aILprE="
}]
}]
}
}
```
This is often required so that proofs used in the past to set some status cannot be reused to set this status again in the future. The latest chain of proofs approach ensures that only the most recent consecutive proofs with the same status are considered. This rule is easy to demonstrate and remember with this example:
```
[
activated, ❌ (not in latest chain)
activated, ❌ (not in latest chain)
deactivated, ❌ (different status)
deactivated, ❌ (different status)
activated ✅ (latest chain with target status)
activated ✅ (latest chain with target status)
]
```
Entire Set [#entire-set]
If `quorum.proofSelection` is set to `entire-set`, all proofs for the target status are considered for quorum calculation, regardless of whether they form a continuous chain.
```jsx
POST /v2/policies
{
...,
"data": {
"handle": "wallet-activate-status",
"schema": "status",
"record": "wallet",
"config": {
"quorum.proofSelection": "entire-set"
},
"values": [{
"status": {"$in": ["activated", "deactivated"]},
"quorum": [{
"public": "WAweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd/aILprE="
}]
}]
}
}
```
Using the same example, if the user wants to activate a wallet with `entire-set` selection, all proofs with the `activated` status would be considered:
```
[
activated, ✅ (included in entire set)
activated, ✅ (included in entire set)
deactivated, ❌ (different status)
deactivated, ❌ (different status)
activated ✅ (included in entire set)
]
```
Filtering by status transition [#filtering-by-status-transition]
Status policies support a `filter` field at two levels:
* **Policy root** — when the filter does not match, the entire policy is excluded from evaluation.
* **Individual value** — when the filter does not match, just that value is excluded; the rest of the policy's values are still evaluated.
Both levels are evaluated against the same context: the **static record data** (e.g. `data.schema`, `handle`) plus the **transition context**, which exposes the entity's state at the moment of evaluation.
The transition context provides the following top-level keys inside the filter expression:
| Key | Description |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `old` | Record DTO before the transition. Exposes `old.meta.status`, `old.meta.labels`, `old.meta.created`, `old.meta.updated`, `old.data.handle`, `old.data.schema`, and any other `old.data.*` field. |
| `new` | Record DTO after the incoming proof is appended. Same shape as `old`. |
| `ctx.req` | HTTP request context (method, headers, etc.). |
Using `ctx.req` in a filter couples the policy evaluation to an HTTP request. Some status transitions are calculated internally — for example, during settlement, reconciliation, or bridge processing — and carry no request context. When `ctx.req` is absent, any filter that references it will not match, causing the policy or value to be excluded. Avoid `ctx.req` filters unless you intend to restrict a transition exclusively to direct API calls.
For backward compatibility, all fields from `old.data` are also spread at the **root level** of the filter context without any prefix. This means `schema` and `old.data.schema` refer to the same value, so existing filters that reference data fields directly (e.g. `{ "schema": "fintech" }`) continue to work without changes.
This lets you create **transition-aware** policies — policies that only activate for specific "from" or "to" statuses and grant or restrict the transition accordingly.
Example: root-level filter — allow a custom status only from a specific state [#example-root-level-filter--allow-a-custom-status-only-from-a-specific-state]
The policy below activates only when the wallet's current status is `active` (root-level filter). All of its values are evaluated only when that condition holds. If the wallet is in any other status, the entire policy is excluded; and if no other matching policy grants `post-active`, the transition is rejected.
```jsx
POST /v2/policies
{
...,
"data": {
"handle": "wallet-post-active",
"schema": "status",
"record": "wallet",
"filter": { "old.meta.status": "active" },
"values": [{
"status": "post-active",
"quorum": [{
"public": "WAweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd/aILprE="
}]
}]
}
}
```
Example: value-level filter — restrict one value within a shared policy [#example-value-level-filter--restrict-one-value-within-a-shared-policy]
The policy below covers multiple statuses in a single policy. The third value uses a value-level filter to restrict `rejected` — it applies only when the current status is `prepared`. The other values (without a filter) are always evaluated.
```jsx
POST /v2/policies
{
...,
"data": {
"handle": "intent-status",
"schema": "status",
"record": "intent",
"values": [
{
"status": { "$in": ["pending", "prepared", "committed", "completed"] },
"quorum": [{ "handle": "system" }]
},
{
"filter": { "old.meta.status": "prepared" },
"status": "rejected",
"quorum": [{ "public": "WAweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd/aILprE=" }]
}
]
}
}
```
In this policy, `rejected` can only be set when the intent is currently in `prepared` status, using the specified key. Attempts to set `rejected` from any other status fail because the filtered value is excluded and no other value covers `rejected`.
When using transition filters, keep in mind the **open unless whitelisted** principle. If the filter does not match, the policy or value is excluded. If no remaining policy has a value for the target status, the transition is **rejected** with an error — not silently allowed. This is different from the case where there are no policies at all (which allows the transition freely).
Example: block a status transition from a terminal state [#example-block-a-status-transition-from-a-terminal-state]
The combination of a base policy (covering all normal statuses, without the target status in its values) and a transition-filtered policy creates a strict gate: the transition is only open when the filter matches.
```jsx
// Base policy — covers all standard statuses, no 'post-completed' value
POST /v2/policies
{
...,
"data": {
"handle": "intent-standard-statuses",
"schema": "status",
"record": "intent",
"values": [{
"status": { "$in": ["pending", "prepared", "committed", "completed", "rejected"] },
"quorum": [{ "handle": "system" }]
}]
}
}
// Transition-filtered policy — only active when intent is already 'completed'
POST /v2/policies
{
...,
"data": {
"handle": "intent-post-completed",
"schema": "status",
"record": "intent",
"filter": { "old.meta.status": "completed" },
"values": [{
"status": "post-completed",
"quorum": [{ "public": "WAweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd/aILprE=" }]
}]
}
}
```
When an intent in `completed` status receives a proof for `post-completed`, both policies are evaluated. The base policy has no value for `post-completed`, but the filtered policy matches (`old.meta.status === 'completed'`) and grants the transition. When the intent is in `rejected` status, the filtered policy is excluded and the base policy has no matching value — the transition is rejected.
Status policies evaluation [#status-policies-evaluation]
When evaluating status policies for setting a status to record there can be multiple matching policies with multiple values and multiple signers in quorum. To easier understand how they are combined the following rules are applied top-down:
1. If there is at least one status policy which matches the record by `record` and `filter` then, in order to allow setting the status of record, at least one policy must be found which allows setting this specific status based on policy rules. If there are no matching policies, then setting of status is allowed to anyone.
2. Policy can have multiple rules in `values` field and it is considered that the policy grants setting status to record if at least one rule is found which grants this for this specific status. Empty `values` array grants nothing.
3. Single rule can have `status` field which when exists will make rule applicable only if the desired status matches. If `status` field is omitted then the rule allows all statuses - according to quorum.
If there is any policy which matches `record/filter` for a record but none of them explicitly allows setting specific status then the proof which is to be added is not valid and will be rejected. Proof will not be stored and error response will be returned to caller.
Restrictive nature of field `status` in policy rule is opposite than fields `wallet, filter` in policy. While setting specific `wallet` and/or `filter` in the policy will narrow status restrictions only to records which matches them, setting the specific `status` in policy rule will narrow the set of acceptable statuses this rule can apply to.
So interpretation that rule with `status: 'active'` implies that `quorum` is required only for status `active` is wrong. Correct interpretation is that this rule with its `quorum` can be used only to grant setting status `active`.
4. Rule also has `quorum` array of signer references and it is used to determine when the status from the proof can be persisted to a record. Quorum step will be evaluated only after all previous steps are executed successfully and the specific status from proof is allowed. Status will be persisted to a record only when all signers referenced in quorum had signed the record with a proof which has the desired status in `custom.status`. Empty `quorum` array means that quorum is not required and will cause that status is persisted to record immediately.
If a proof with `status` passes steps 1.-4. but the key used in this proof is not in the quorum or if another key is required to fulfill the quorum then the proof will be stored successfully, error will not be returned to caller, but status will not be persisted to record until the quorum of the rule which allows this status is achieved.
When a record is modified, it may fall under a policy that it does not satisfy. For example, there could be a wallet status policy that requires a signature from a specific key, but only if the wallet record has a schema like `fintech`. In this scenario, someone could first create a wallet without a schema or with a schema such as `bank`, then set the status to `active` and afterward change the schema to `fintech`. As a result, the wallet status is now subject to the policy, but the status was set without enforcing this policy.
| Date | Changes |
| ----------------------- | --------------- |
| 2024-07-15 | Initial version |
# About Label Policies (/ledger/structuring-data/labels-policies)
What is a labels policy? [#what-is-a-labels-policy]
Label policies are used to define which labels can be used, and how, by records in ledger. They have schema `labels` which defines a specific structure of policy rules contained in `values` of the policy.
```json
POST /v2/policies
{
...,
data: {
handle: '...',
schema: 'labels',
record: '',
filter: { ... },
values: [ ... ]
}
}
```
There are root properties in policy `record` and `filter` which refer to the record type and the record value and are used to evaluate whether the policy will be applied to record when a proof to change record labels is submitted.
The principle `open unless whitelisted` which ledger follows in all kinds of rules is valid here also. So if for a record whose labels are to be changed there isn’t any matching labels policy (by record and filter) then the operation of changing labels will be by default accepted. Otherwise the operation needs to be explicitly granted by one of applicable policy rules.
Property `filter` represents standard ledger filtering concept where language used in filter expression is mongo-compatible.
Labels policies are exposed on the same endpoint as access policies with distinction that the schema property is equal to `labels` while for access policies schema is `access`. Schema of the policy record defines the structure of `values` array in policy.
Defining allowed labels [#defining-allowed-labels]
To define which labels can be used, we’ll use `labels` field in the policy values. It is an array of strings and has a meaning that the rule grants to users the ability of attaching any of these labels to a record.
```json
POST /v2/policies
{
...,
data: {
handle: 'preferred-account-anchor',
schema: 'labels',
record: 'anchor', // It applies only to anchors
'filter: {
schema: 'account' // It applies only to anchors with a schema "account"
},
values: [
{
labels: ['preferred']
}
],
custom: {
description: 'Marks a account anchor as preferred in the system'
}
}
}
```
Policies are evaluated when a user try to tag records with a label. So, when existing record labels conflict with a policy created, the ledger doesn't fix the inconsistencies when the policy is created, it should be done manually by the user.
Constraining Uniqueness [#constraining-uniqueness]
Sometimes we want to limit a label so that it can’t repeat, e.g. only one account anchor of a wallet can be labeled as preferred. It can be done by adding a set of `unique` record properties to a label policy rule:
```json
POST /v2/policies
{
...,
data: {
handle: 'preferred-account-anchor-per-wallet',
schema: 'labels',
record: 'anchor',
filter: {
schema: 'account'
},
values: [
{
labels: ['preferred'],
unique: ['wallet']
}
],
custom: {
description: 'Marks a preferred account anchor per wallet'
}
}
}
```
Unique property names have the root in the `data` of the record, so root properties such as `meta`, `hash`, and `luid` are not addressable.
This example can be expanded to other use cases by listing more properties as unique. For example, if you want to control the preferred anchor per wallet and symbol, you want to declare it like this:
```json
POST /v2/policies
{
...,
data: {
handle: 'preferred-account-anchor-per-wallet-symbol',
schema: 'labels',
record: 'anchor',
filter: {
schema: 'account'
},
values: [
{
labels: ['preferred'],
unique: ['wallet', 'symbol']
}
],
custom: {
description: 'Marks a preferred account anchor per wallet and symbol'
}
}
}
```
| Date | Changes |
| ----------------------- | --------------- |
| 2024-07-15 | Initial version |
# About Custom Policies (/ledger/structuring-data/custom-policies)
What is a custom policy? [#what-is-a-custom-policy]
Policies are used to define values that can be consumed by different services. Ledger has built-in policies
like [`access`](/ledger/securing-the-ledger/about-policies), [`labels`](/ledger/structuring-data/labels-policies) or
[`status`](/ledger/structuring-data/status-policies), for these policies the `values` will be validated and consumed by the ledger.
If you create a new `schema` that targets record `policy` you can create custom policies to store values which format won't be
validated by the ledger's API Schema.
Data validation will still be performed based on the `schema` you define, see [About Schemas](/ledger/structuring-data/schemas) for more details
on how to create your schema.
```tsx
POST /v2/policies
{
...,
data: {
handle: '...',
schema: 'my-schema',
values: [ ... ]
}
}
```
These custom policies will exposed on the same endpoint as other policies, such as status or access. If you want to read only your policies,
you can use a filter.
```jsx
GET /v2/policies?data.schema=my-schema
```
Example use case [#example-use-case]
As policies with custom schemas can store data with defined structure in its values, they are useful for storing
ledger-level configuration. For example, studio uses a custom `navigation` schema defined for policies to configure
how the navigation menu will be rendered, this is how a navigation policy looks:
```jsx
{
handle: 'my-custom-navigation',
schema: 'navigation',
values: [
{
header: 'Movements',
screen: 'intents',
},
{
header: 'Connections',
screen: 'bridges',
},
{
header: 'Security',
items: [
{
header: 'Members',
screen: 'members',
},
{
header: 'Security Policies',
screen: 'policies',
params: {
query: '?schema=access',
}
},
{
header: 'roles',
screen: 'circles',
}
],
}
],
}
```
| Date | Changes |
| ----------------------- | --------------- |
| 2025-07-04 | Initial version |
# About Layouts (/ledger/structuring-data/about-layouts)
Ledger Layouts [#ledger-layouts]
Overview [#overview]
A **ledger layout** is a structured template that defines records that should be created or updated within a ledger. It serves as a blueprint for managing standardized models and keep consistency of data for operations in different environments.
Key Concepts [#key-concepts]
What is a Ledger Layout? [#what-is-a-ledger-layout]
A **ledger layout** is essentially a predefined structure that maps how different records or fields should be stored in a ledger in order to compose a model or operation that is repeated all the time. It is used to maintain consistency especially when working with complex datasets that require specific formatting, like [schemas](/ledger/structuring-data/schemas) and [policies](/ledger/securing-the-ledger/about-policies).
Why to use Ledger Layouts? [#why-to-use-ledger-layouts]
* **Standardization**: Layouts provide a consistent format for data across different operations, reducing errors and ensuring uniformity.
* **Automation**: Using layouts allows for automated data transformations and validations, streamlining ledger updates.
* **Flexibility**: Variables in layouts enable dynamic customization, making it possible to adapt to changing data requirements with minimal effort.
Layout Inputs [#layout-inputs]
Layouts can include variables, represented by placeholders in the format `{{ inputs. }}`, which allow for dynamic adjustments to accommodate different requirements while using a standard template.
The schemas of inputs are defined with JSON Schema vocabulary, which supports not only the enumeration of variables, but also their types and constraints e.g. `minLength` for consistency.
See an example of a layout with inputs.
```jsx
{
"custom": {
"inputs": {
"properties": {
"bankBic": {
"minLength": 1,
"title": "Bank BIC",
"type": "string"
},
"bankName": {
"minLength": 1,
"title": "Bank name",
"type": "string"
},
"domainName": {
"minLength": 1,
"title": "Domain name",
"type": "string"
}
},
"required": [
"domainName",
"bankName",
"bankBic"
],
"type": "object"
},
"title": "RTP Bank Domain"
},
"values": [
{
"record": "domain",
"values": [
{
"data": {
"custom": {
"bankBic": "{{inputs.bankBic}}",
"bankName": "{{inputs.bankName}}"
},
"handle": "{{inputs.domainName}}",
"schema": "bank"
}
}
]
},
{
"record": "wallet",
"values": [
{
"data": {
"access": [
{
"action": "any",
"signer": {
"$record": "owner"
}
},
{
"action": "read",
"bearer": {
"$signer": {
"$record": "owner"
}
}
}
],
"handle": "{{inputs.domainName}}",
"schema": "bank-wallet"
},
"exec": {
"domain": "{{inputs.domainName}}"
}
}
]
},
{
"record": "circle",
"values": [
{
"data": {
"access": [
{
"action": "any",
"signer": {
"$record": "owner"
}
},
{
"action": "read",
"bearer": {
"$signer": {
"$record": "owner"
}
}
}
],
"handle": "owner@{{inputs.domainName}}"
}
},
{
"data": {
"access": [
{
"action": "any",
"signer": {
"$record": "owner"
}
},
{
"action": "read",
"bearer": {
"$signer": {
"$record": "owner"
}
}
}
],
"handle": "admin@{{inputs.domainName}}"
}
},
{
"data": {
"access": [
{
"action": "any",
"signer": {
"$record": "owner"
}
},
{
"action": "read",
"bearer": {
"$signer": {
"$record": "owner"
}
}
}
],
"handle": "security@{{inputs.domainName}}"
}
},
{
"data": {
"access": [
{
"action": "any",
"signer": {
"$record": "owner"
}
},
{
"action": "read",
"bearer": {
"$signer": {
"$record": "owner"
}
}
}
],
"handle": "operation@{{inputs.domainName}}"
}
},
{
"data": {
"access": [
{
"action": "any",
"signer": {
"$record": "owner"
}
},
{
"action": "read",
"bearer": {
"$signer": {
"$record": "owner"
}
}
}
],
"handle": "support@{{inputs.domainName}}"
}
},
{
"data": {
"access": [
{
"action": "any",
"signer": {
"$record": "owner"
}
},
{
"action": "read",
"bearer": {
"$signer": {
"$record": "owner"
}
}
}
],
"handle": "bridge@{{inputs.domainName}}"
}
}
]
}
]
}
```
The layout above defines the foundation of a bank domain, which includes the following records:
* domain
* wallet
* circles
The variable `domainName` is widely used in the layout and composes the handle of the new domain and wallet, as well as the circle handles.
For more information on the template language used in layouts, consult the [SelectTransform](https://selecttransform.github.io/site/transform.html) documentation.
Useful links [#useful-links]
* [How to apply a layout](/ledger/structuring-data/apply-layout)
* [How to read a layout](/ledger/structuring-data/read-layout)
* [How to display differences between a layout and a ledger](/ledger/structuring-data/display-differences)
* [How to sync a layout to a ledger](/ledger/structuring-data/sync-layout)
* [How to export a layout](/ledger/structuring-data/export-layout)
| Date | Changes |
| ----------------------- | --------------- |
| 2024-10-24 | Initial version |
# Read a Layout (/ledger/structuring-data/read-layout)
CLI allows users to visualize layouts.
Reading a layout with CLI [#reading-a-layout-with-cli]
Bellow is an example of how to read a layout with minka CLI.
Make sure you are connected to a server and have an active ledger set in context.
You will be asked about the layout to be read.
```console
$ minka layout show
? Select Layout: (Use arrow keys)
----Policy----
my_policy_layout
----Custom----
my_custom_layout
----Built-in----
RTP Bank Domain Update
> Cross Ledger Payments
RTP Setup
RTP Studio
RTP Reporting
RTP Member
RTP Bank Domain
RTP Domain Owner
RTP Domain Member
RTP Bank Bridge
```
The list presented on the command line might be different, according to the built-in layouts added to the system, as well as the custom layouts available on your local machine and policy layouts created in the ledger
```console
$ minka layout show
? Select Layout: Cross Ledger Payments
Cross Ledger Payments
---------------------------------------------------------------------------
Schemas
- p2p
- intent
- tel-alias
- bank-wallet
Wallets
- ach
- tesla
- minka
Circles
- bank
- ach
Policies
- default
- p2p-intent
- alias-directory
Symbols
- usd
- dop
Signers
- tesla
- minka
- ach
Intents
- issue_money_usd_ach
- issue_money_dop_ach
- issue_money_usd_tesla
- issue_money_dop_tesla
- issue_money_usd_minka
- issue_money_dop_minka
```
If you want to visualize the layout in its JSON structure, you can pass the flag `--verbose`
```console
$ minka layout show --verbose
? Select Layout: Cross Ledger Payments
{
"custom": {
"title": "Cross Ledger Payments"
},
"values": [
{
"record": "signer",
"values": [
...
]
},
{
"record": "circle",
"values": [
...
]
},
{
"record": "policy",
"values": [
{
"data": {
"access": [
{
"action": "any",
"signer": {
"$record": "owner"
}
},
{
"action": "read",
"bearer": {
"$signer": {
"$record": "owner"
}
}
}
],
"handle": "default",
"record": "any",
"schema": "access",
"values": [
{
"action": "any",
"signer": {
"$record": "owner"
}
},
{
"action": "read",
"bearer": {
"$signer": {
"$record": "owner"
}
}
},
{
"action": "read",
"bearer": {
"$signer": {
"$ledger": "owner"
}
}
},
{
"action": "spend",
"bearer": {
"$signer": {
"$ledger": "owner"
}
},
"record": "wallet"
},
{
"action": "read",
"bearer": {
"$signer": {
"$circle": "ach"
}
}
}
]
}
},
...
]
},
{
"record": "schema",
"values": [
{
"data": {
"access": [
{
"action": "any",
"signer": {
"$record": "owner"
}
},
{
"action": "read",
"bearer": {
"$signer": {
"$record": "owner"
}
}
}
],
"format": "json-schema",
"handle": "p2p",
"record": "intent",
"schema": {
"type": "object"
}
}
},
...
]
},
{
"record": "symbol",
"values": [
{
"data": {
"access": [
{
"policy": "default"
}
],
"factor": 100,
"handle": "usd"
},
"exec": {
"keyPair": "key@ach"
}
},
...
]
},
{
"record": "wallet",
"values": [
{
"data": {
"access": [
{
"policy": "default"
}
],
"custom": {
"name": "Primary ACH Wallet"
},
"handle": "ach",
"schema": "bank-wallet"
},
"exec": {
"keyPair": "key@ach"
}
},
...
]
},
{
"record": "intent",
"values": [
{
"data": {
"access": [
{
"policy": "default"
}
],
"claims": [
{
"action": "issue",
"amount": 1000000000000000,
"symbol": {
"handle": "usd"
},
"target": {
"handle": "ach"
}
}
],
"handle": "issue_money_usd_ach",
"schema": "intent"
},
"exec": {
"keyPair": "key@ach"
}
},
...
]
}
]
}
Cross Ledger Payments
---------------------------------------------------------------------------
Schemas
- p2p
- intent
- tel-alias
- bank-wallet
Wallets
- ach
- tesla
- minka
Circles
- bank
- ach
Policies
- default
- p2p-intent
- alias-directory
Symbols
- usd
- dop
Signers
- tesla
- minka
- ach
Intents
- issue_money_usd_ach
- issue_money_dop_ach
- issue_money_usd_tesla
- issue_money_dop_tesla
- issue_money_usd_minka
- issue_money_dop_minka
```
Reading a layout with inputs [#reading-a-layout-with-inputs]
Some layouts can have variables, and they are represented with `{{ inputs. }}`. It allows the creation of generic models that may vary in minimal details.
We can see the layouts with their variables by using the flag `--raw`
```console
$ minka layout show --raw
? Select Layout: RTP Bank Domain
RTP Bank Domain
---------------------------------------------------------------------------
Wallets
- {{inputs.domainName}}
Circles
- owner@{{inputs.domainName}}
- admin@{{inputs.domainName}}
- security@{{inputs.domainName}}
- operation@{{inputs.domainName}}
- support@{{inputs.domainName}}
- bridge@{{inputs.domainName}}
Policies
- studio-navigation@{{inputs.domainName}}
- owner@{{inputs.domainName}}
- admin@{{inputs.domainName}}
- security@{{inputs.domainName}}
- operation@{{inputs.domainName}}
- support@{{inputs.domainName}}
- bridge@{{inputs.domainName}}
- reporting-bridge@{{inputs.domainName}}
Domains
- {{inputs.domainName}}
```
It's also possible to simulate how the layout would look like with its variables fulfilled, you just need to read the layout without the option `—raw`
```console
$ minka layout show
? Select Layout: RTP Bank Domain
? Domain name: example.io
? Bank name: Example Bank
? Bank BIC: EXAMPLE BANK
RTP Bank Domain
---------------------------------------------------------------------------
Wallets
- example.io
Circles
- owner@example.io
- admin@example.io
- security@example.io
- operation@example.io
- support@example.io
- bridge@example.io
Policies
- studio-navigation@example.io
- owner@example.io
- admin@example.io
- security@example.io
- operation@example.io
- support@example.io
- bridge@example.io
- reporting-bridge@example.io
Domains
- example.io
```
| Date | Changes |
| ----------------------- | --------------- |
| 2024-10-24 | Initial version |
# Apply a Layout (/ledger/structuring-data/apply-layout)
CLI allows users to apply layouts to a ledger.
Applying a layout with CLI [#applying-a-layout-with-cli]
Bellow is an example of how to apply a layout using the Minka CLI.
Ensure that you are connected to a server and have an active ledger set in the context before proceeding.
```console
$ minka layout apply
? Select Layout: Cross Ledger Payments
? Select main signer:
✅ Layout applied successfully:
Ledger: testing-layout
Layout: Cross Ledger Payments
Layout changelog:
Signer
+ tesla (added)
+ minka (added)
+ ach (added)
Circle
+ bank (added)
+ ach (added)
Circle Members (signer => circle)
+ tesla => bank (added)
+ minka => bank (added)
+ ach => ach (added)
Policy
+ default (added)
+ p2p-intent (added)
+ alias-directory (added)
Schema
+ p2p (added)
+ intent (added)
+ tel-alias (added)
+ bank-wallet (added)
Symbol
+ usd (added)
+ dop (added)
Wallet
+ ach (added)
+ tesla (added)
+ minka (added)
Intent
+ issue_money_usd_ach (added)
+ issue_money_dop_ach (added)
+ issue_money_usd_tesla (added)
+ issue_money_dop_tesla (added)
+ issue_money_usd_minka (added)
+ issue_money_dop_minka (added)
```
Applying a layout from a file [#applying-a-layout-from-a-file]
To apply a layout from a file, you can use the `--local` flag. You will be prompted to input the path to the layout file.
```bash
minka layout apply --local
```
Applying a layout with variables [#applying-a-layout-with-variables]
Certain layouts may include variables represented using the syntax `{{ inputs. }}`. This feature enables the creation of generic models that can be customized with minor variations.
In such cases, the CLI will prompt you to provide the necessary values for these variables, transforming the layout as needed before applying it to the ledger.
```console
$ minka layout apply
? Select Layout: RTP Bank Domain
? Domain name: example.io
? Bank name: Example Bank
? Bank BIC: EXAMPLE BANK
✅ Layout applied successfully:
Ledger: testing-layout
Layout: RTP Bank Domain
Layout changelog:
Wallets
+ example.io
Circles
+ owner@example.io (added)
+ admin@example.io (added)
+ security@example.io (added)
+ operation@example.io (added)
+ support@example.io (added)
+ bridge@example.io (added)
Policies
+ studio-navigation@example.io (added)
+ owner@example.io (added)
+ admin@example.io (added)
+ security@example.io (added)
+ operation@example.io (added)
+ support@example.io (added)
+ bridge@example.io (added)
+ reporting-bridge@example.io (added)
Domains
+ example.io (added)
```
| Date | Changes |
| ----------------------- | --------------- |
| 2024-10-24 | Initial version |
# Export a Layout (/ledger/structuring-data/export-layout)
CLI allows users to export layouts.
Exporting a layout with CLI [#exporting-a-layout-with-cli]
Ensure that you are connected to a server and have an active ledger set in the context before proceeding.
There are couple of ways to export a layout:
Exporting a full layout from the currently active ledger [#exporting-a-full-layout-from-the-currently-active-ledger]
```bash
minka layout export --ledger
```
This command will export the layout from the currently active ledger and save it to the `layouts` folder in the minka CLI directory. The layout will be saved as a JSON file named after the current ledger, with proper formatting.
Exporting other layouts [#exporting-other-layouts]
```bash
minka layout export [--raw]
```
This command will open an interactive prompt that will allow you to select the layout you want to export. There are three types of layouts you can choose from:
* Built-in layouts - layouts that are provided by the Minka team and reside in the Minka CLI.
* Custom layouts - layouts that are present in the `layouts` folder in the minka CLI directory.
* Policy layouts - policy layouts that are stored in the ledger.
The `--raw` flag is optional and will export the layout in raw format - format where inputs values are not resolved.
# Sync a Layout (/ledger/structuring-data/sync-layout)
Layout synchronization is a tool that can be used when one or more records defined in a layout are not up-to-date in the ledger.
See [How to display differences between a layout and a ledger](/ledger/structuring-data/display-differences)
Syncing a layout with CLI [#syncing-a-layout-with-cli]
Here is an example of how to synchronize a layout using the Minka CLI.
Ensure you are connected to a server and have an active ledger set in the context before proceeding.
Start by selecting the layout and the specific changes you want to synchronize.
**Note:** The command `minka layout apply` functions the same as executing `minka layout sync` with all records selected.
```console
$ minka layout sync
? Select layout: Cross Ledger Payments
? Select main signer:
? Select the changes you want to sync: (Press to select, to toggle all, to invert selection, and to proceed)
----Signer----
◯ tesla
◯ minka
◯ ach
----Circle----
◯ bank
◯ ach
----Policy----
◯ default
◯ p2p-intent
◯ alias-directory
----Schema----
❯◉ p2p
◯ intent
◯ tel-alias
◯ bank-wallet
----Symbol----
◯ usd
◯ dop
----Wallet----
◯ ach
◯ tesla
◯ minka
----Intent----
◯ issue_money_usd_ach
◯ issue_money_dop_ach
◯ issue_money_usd_tesla
◯ issue_money_dop_tesla
◯ issue_money_usd_minka
◯ issue_money_dop_minka
```
The change log displays the records that were added, removed, or modified during synchronization.
```console
$ minka layout sync
? Select layout: Cross Ledger Payments
? Select main signer:
? Select the changes you want to sync: p2p
✅ Layout synced successfully:
Ledger: test-sync
Layout: Cross Ledger Payments
Layout changelog:
Schema
o p2p (changed)
```
| Date | Changes |
| ----------------------- | --------------- |
| 2024-10-24 | Initial version |
# Show Layout Diff (/ledger/structuring-data/display-differences)
CLI allows users to check differences between the active ledger and a selected layout.
Displaying differences using CLI [#displaying-differences-using-cli]
Bellow is an example of how to find changes between the active ledger and a selected layout using the Minka CLI.
Ensure that you are connected to a server and have an active ledger set in the context before proceeding.
```bash
$ minka layout diff
? Select layout: Cross Ledger Payments
Schemas
---------------------------------------------------------------------------
[
...
...
{
data: {
handle: "p2p"
schema: {
properties: { # [!code --]
custom: { # [!code --]
properties: { # [!code --]
description: { # [!code --]
type: "string" # [!code --]
} # [!code --]
} # [!code --]
required: [ # [!code --]
"description" # [!code --]
] # [!code --]
type: "object" # [!code --]
} # [!code --]
} # [!code --]
required: [ # [!code --]
"custom" # [!code --]
] # [!code --]
}
}
}
...
]
```
Changes marked with **red** are present in ledger but not in the layout.
On the other hand, changes marked with **green** are present in layout but not in ledger
```bash
$ minka layout diff
? Select layout: Cross Ledger Payments
Schemas
---------------------------------------------------------------------------
[
...
...
{
data: {
handle: "p2p"
schema: {
properties: { # [!code --]
custom: { # [!code --]
properties: { # [!code --]
description: { # [!code --]
type: "string" # [!code --]
} # [!code --]
} # [!code --]
required: [ # [!code --]
"description" # [!code --]
] # [!code --]
type: "object" # [!code --]
} # [!code --]
} # [!code --]
required: [ # [!code --]
"custom" # [!code --]
] # [!code --]
}
}
}
...
]
Policies
---------------------------------------------------------------------------
[
...
{
data: {
handle: "default"
values: [
... (3 entries)
{
action: "spend" # [!code --]
action: "any" # [!code ++]
}
...
]
}
}
...
]
```
Use the option `--select` to filter the record types you want to see
```bash
$ minka layout diff --select=policy
? Select layout: Cross Ledger Payments
Policies
---------------------------------------------------------------------------
[
...
{
data: {
handle: "default"
values: [
... (3 entries)
{
action: "spend" # [!code --]
action: "any" # [!code ++]
}
...
]
}
}
...
]
```
| Date | Changes |
| ----------------------- | --------------- |
| 2024-10-24 | Initial version |
# Working with Layouts (/ledger/structuring-data/working-with-layouts)
Layouts are predefined configurations that help you quickly set up ledgers with proper security, authorization, and business logic. They provide a declarative way to set up complex ledger configurations without manual API calls.
This tutorial series guides you through building a complete ledger application using Minka's Layout system.
What You'll Learn [#what-youll-learn]
In this tutorial, you'll learn how to:
* **Connect to the Minka Sandbox** - Set up your development environment
* **Create and configure ledgers** - Set up basic ledger infrastructure with proper security
* **Work with signers and circles** - Understand identity and authorization management
* **Create wallets and symbols** - Set up accounts and currencies for transactions
* **Define schemas and policies** - Create reusable templates for data structure and access control
* **Make transactions** - Execute transfers between wallets
* **Connect bank cores** - Integrate with external banking systems
Key Concepts [#key-concepts]
Layouts [#layouts]
Layouts are JSON configuration files that define ledger structure, security policies, and business logic.
Signers and Circles [#signers-and-circles]
* **Signers** are cryptographic identities that authenticate and authorize ledger operations
* **Circles** are groups of signers that simplify access control management
Wallets and Symbols [#wallets-and-symbols]
* **Wallets** are accounts that hold balances and participate in transactions
* **Symbols** represent different currencies or units of value
Schemas and Policies [#schemas-and-policies]
* **Schemas** define the structure and validation rules for ledger records
* **Policies** are reusable sets of access rules that control who can perform what actions
# Connect to Sandbox (/ledger/structuring-data/working-with-layouts/connecting-to-minka-sandbox)
Introduction [#introduction]
The Minka Sandbox server provides a development and testing environment for building and testing ledger applications.
This environment is specifically designed for development purposes and should not be used for production deployments.
Prerequisites [#prerequisites]
Before connecting to the sandbox, ensure you have:
* Node.js 20.17 or later installed
* npm or yarn package manager
* A stable internet connection
Installation [#installation]
Install the Minka Command Line Interface (CLI) using one of the following methods:
```bash
# Using npm
npm install -g @minka/cli
# Using yarn
yarn global add @minka/cli
```
The CLI requires Node.js 20.17 or later. For detailed system requirements and troubleshooting, see the [CLI Reference](/ledger/reference/cli-reference) documentation.
Connecting to the Minka Sandbox server [#connecting-to-the-minka-sandbox-server]
To connect to the Minka development environment, use the following command:
```bash
minka server connect https://ldg-stg.one/api/v2
```
After establishing the connection, you can use the Minka CLI to create and interact with ledgers.
Creating a Signer [#creating-a-signer]
In the Minka ecosystem, every server and ledger operation requires a signer. Signers are cryptographic identities that provide:
* Authentication: They identify the entity responsible for each ledger action
* Authorization: They enable fine-grained control over which operations each entity can perform
A signer consists of a cryptographic key pair:
* A private key for signing operations
* A corresponding public key for verifying signatures
Your private key should never be shared with anyone. Anyone with access to your private key can perform any action that your signer is authorized to do, including transferring balances.
To create a signer named `ledger-creator`, execute the following command:
```bash
minka signer create
```
You can see the signer details, including the private key, using the following command:
```bash
minka signer show ledger-creator --secret
```
For comprehensive information about signers, including security best practices and technical implementation details, refer to the following documentation:
* [Signers](/ledger/securing-the-ledger/about-signers)
* [Keys and Signatures](/ledger/securing-the-ledger/keys-and-signatures)
* [User Creation](/ledger/securing-the-ledger/user-creation)
Security and Authorization [#security-and-authorization]
The Minka ledger system implements a multi-level security model with three distinct levels:
* Server level
* Ledger level
* Record level
Authorization Process [#authorization-process]
When checking whether a signer can perform an action, the system follows a two-stage authorization process:
1. **Access Gate**: The system performs a top-to-bottom check to verify if the `access` rule is granted to the signer. For example, if a signer attempts to perform an action on a specific record but doesn't have the `access` rule on the ledger, the action is blocked.
2. **Action Authorization**: If the signer passes the Access Gate for a particular level, the system collects all access rules from every level and applies an additive model: if ANY rule grants the requested action, the action is granted.
Sandbox Server Security Configuration [#sandbox-server-security-configuration]
The Sandbox Server is configured with two default rules:
```json
[
{
"action": "access"
},
{
"action": "create",
"record": "ledger"
}
]
```
These rules provide:
* *Access Gate*: Any signer has `access` to the server, fulfilling the base level requirement for performing actions
* *Action Authorization*: Any signer can create a ledger
For a detailed explanation of authorization in Minka, see [About Authorization](/ledger/securing-the-ledger/about-authorization) documentation.
# Basic Setup (/ledger/structuring-data/working-with-layouts/ledger-basic-setup)
Ledger basic setup [#ledger-basic-setup]
In this section, we will create a ledger, setup its basic security and authorization, and create additional signers and wallets.
Creating a ledger [#creating-a-ledger]
In this sandbox, we will now setup our own ledger using `ledger-creator` signer.
```bash
minka ledger create
```
You will have to input the name (handle) you would like to use for your ledger - `layout-ledger`.
Set access rules to this:
```json
[
{
"action": "access"
},
{
"action": "any",
"record": "any",
"signer": {
"$ledger": "owner"
}
},
{
"action": "read",
"record": "any",
"bearer": {
"$signer": {
"$ledger": "owner"
}
}
}
]
```
This translates to:
* grant pass-through access for ledger to everyone
* the owner of the ledger can perform any action on any record (by signing the `data` part of the request)
* the owner of the ledger can read any record on the ledger (by using JWT bearer token)
Leave the rest of the fields as default. Use `ledger-creator` signer to create the ledger.
Applying the layout [#applying-the-layout]
This tutorial heavily relies on applying layouts from the file. To apply the layout from the file, use the following command:
```bash
minka layout apply --local
```
You will use this command throughout the tutorial, with different layout files. More details about the `layout apply` command can be found in the [How to apply a layout](/ledger/structuring-data/apply-layout) documentation.
Creating additional signers on the ledger [#creating-additional-signers-on-the-ledger]
For the purpose of this tutorial, we will create three additional signers on the ledger:
* `tesla-signer` - signer that will be used to create Tesla wallet, which will represent a Tesla bank account
* `minka-signer` - signer that will be used to create Minka wallet, which will represent a Minka bank account
* `ach-signer` - signer that will be used to create ACH wallet, which will represent ACH
For that, we will use a layout file that is already created for us. It will create three signers on the ledger. Download the `signers.json` layout file and **apply it**.
Looking at the layout, we can see that the following applies:
* Only owner can see its data and update it.
* Password for each signer is the same as the signer's handle root name. For example, `tesla-signer` signer has password `tesla`.
The password is only used to encrypt the signer's private key when it is stored. API requests are authenticated with digital signatures or JWTs created with the private key, and verified using the public key. The password is never used for request authentication.
Define default-policy policy [#define-default-policy-policy]
Policies in Minka are **reusable sets of access rules** that can be applied to records, to control who can perform what actions. They serve as templates for access control. So, instead of duplicating the same access rules everywhere, we can create a policy and reference it in multiple places.
First policy we will create in this tutorial is the `default-policy` policy. It contains the default access rules we want to have for the majority of the records, and as the base policy for other policies. This policy establishes the following access rules:
1. Record owners can perform any action on their records
2. Record owners can read their records using a bearer token
3. Ledger owners can read any record using a bearer token
4. Ledger owners can spend from any wallet using a bearer token
5. Members of the `ach-circle` circle can read records using a bearer token (circle is basically a group of signers, we will create it later)
To create the policy, download the `default-policy.json` layout file and **apply it**.
For more detailed information about policies and their implementation, refer to the [About Security Policies](/ledger/securing-the-ledger/about-policies) documentation.
Circles [#circles]
Circles in Minka are **groups or roles that contain multiple signers**. They serve as a way to organize signers and simplify access control management across the ledger. Instead of granting permissions to individual signers (which can become complex with many users), you can assign permissions to entire circles.
For a detailed explanation of circles and how they work with authorization, see [About Circles](/ledger/securing-the-ledger/about-circles) and [About Authorization](/ledger/securing-the-ledger/about-authorization).
For the sake of assigning policies to signers, we will create the following circles:
* `bank-circle` - circle that will contain bank signers - `tesla-signer` and `minka-signer`
* `ach-circle` - circle that will contain ACH signer - `ach-signer`
Regarding access rules - each circle will have access rules defined by `default-policy` policy.
Also, layout functionality allows us to define the members of the circle. `bank-circle` will contain `tesla-signer` and `minka-signer` signers, and `ach-circle` will contain `ach-signer` signer.
To create these circles on the ledger, download the `circles.json` layout file and **apply it**.
Wallets [#wallets]
Wallets in Minka are **accounts that are used for transactions and balances**. For our purposes, we will create three wallets that will represent banks:
* `ach-wallet` - wallet that will represent ACH
* `tesla-wallet` - wallet that will represent Tesla bank
* `minka-wallet` - wallet that will represent Minka bank
When creating a wallet, we will be using a new ledger entity, called `schema` - this will be explained in more detail in the next section.
Schemas for wallets [#schemas-for-wallets]
Schemas in Minka are **templates for data that is stored on the ledger**. They define the structure of the record data that is stored on the ledger - what fields are required, what fields are optional, possible values in terms of type, set of possible values, regular expressions, etc. They are executed when a record is created or updated. Often, they are used simply as a way to group records together, for organization, readability, and filtering purposes.
We will create two schemas for wallets:
* `tel-alias-schema` - Schema for **wallet records** which defines that data should simply be an object and have a `handle` field that should be a string and start with `tel:`. This schema will be used for defining wallets that are associated with tel aliases.
* `bank-wallet-schema` - Schema for **wallet records** which defines that data should simply be an object. This schema will be used for defining wallets that are associated with bank accounts.
To create the schemas for wallets, download the `wallets-schemas.json` layout file and **apply it**.
Creating wallets [#creating-wallets]
Now the schemas are created, we can use them when creating wallets. To create the wallets, download the `wallets.json` layout file and **apply it**.
`bank-wallet-schema` schema and `default-policy` access policy is used for all three wallets. Additionally, each wallet is signed by the respective bank signer, what makes these bank signers the owners of these bank wallets.
Symbols [#symbols]
Symbols in Minka are **units of value that are used to represent different currencies**.
For the sake of this tutorial, we will create two symbols:
* `usd` - symbol that will represent US dollars
* `dop` - symbol that will represent Dominican pesos
Both symbols will use `default-policy` access policy. They will be signed by the `ach-signer` signer, making `ach-signer` signer the owner of these symbols.
Also, we will use `factor` field to define the factor for the symbol. Transaction amount is always integer, so `factor` is used to define the decimal places for the symbol.
In this case, we will use 100, which means that 1 unit of the symbol will be equal to 100 units of the base symbol. In other words, 1 unit of the symbol is 100 cents.
To create the symbols, download the `symbols.json` layout file and **apply it**.
Intent schemas, policies, and initial intents [#intent-schemas-policies-and-initial-intents]
Intents in Minka are **requests for actions that are sent to the ledger**. They are usually used to create new money in the ledger, transfer money between wallets, destroy money, etc.
As we have done for other records, we will define schemas and policies for intents as well. After that, we will create initial intents that will be used to create the initial money in the ledger.
Intent schemas [#intent-schemas]
To group intents by their purpose, we will create two schemas:
* `p2p-intent-schema` - Schema for **intent records** which defines that data should simply be an object. It will be used for P2P payments.
* `intent-schema` - Schema for **intent records** which defines that data should simply be an object. This one will be used for general intent records.
To create the schemas, download the `intents-schemas.json` layout file and **apply it**.
Intent policies [#intent-policies]
We will create the `p2p-intent-policy` policy, which will be used for P2P payment intents. Its properties are:
* It is an access policy
* It extends the `default-policy` policy
* It will be applied only to `intent-schema` type records
* Its access policy is defined by the `default-policy` policy
* It adds the following access rules for the record it is applied to:
* **Bank circle members can read P2P intents**: Any signer in the `bank-circle` circle can read intents with schema `p2p-intent-schema` if they are allowed to read any of the wallets involved in any claim of this intent as `source` or `target` (requires JWT bearer token).
* **Bank members can read related P2P intents**: Any signer in the `bank-circle` circle can read intents with schema `p2p-intent-schema` if they are allowed to read any of the wallets involved in any claim of the thread that this intent is part of - as `source` or `target` (requires JWT bearer token).
* It is signed by the `ach-signer` signer, making `ach-signer` signer the owner of this policy.
To create the policy, download the `intents-policy.json` layout file and **apply it**.
For more detailed information about `invoke` field in policies and their possible values, refer to the [Attaching built-in functions to policy values - invoke](/ledger/securing-the-ledger/about-policies#attaching-built-in-functions-to-policy-values---invoke) chapter of the [About Security Policies](/ledger/securing-the-ledger/about-policies) documentation.
Initial intents [#initial-intents]
Now that we have the policies and schemas defined, we can create the initial intents which will be used to create the initial money in the ledger. All intents have the following properties:
* Signed by the `ach-signer` signer, making `ach-signer` signer the owner of these intents.
* Schema `intent-schema` is used for all intents.
* Policy `default-policy` is used for all intents.
Following intents will be created:
* `issue_money_usd_ach` - intent to issue 10,000,000.00 US dollars to the `ach-wallet` wallet.
* `issue_money_dop_ach` - intent to issue 10,000,000.00 Dominican pesos to the `ach-wallet` wallet.
* `issue_money_usd_tesla` - intent to issue 1,000,000.00 US dollars to the `tesla-wallet` wallet.
* `issue_money_dop_tesla` - intent to issue 5,000,000.00 Dominican pesos to the `tesla-wallet` wallet.
* `issue_money_usd_minka` - intent to issue 1,000,000.00 US dollars to the `minka-wallet` wallet.
* `issue_money_dop_minka` - intent to issue 5,000,000.00 Dominican pesos to the `minka-wallet` wallet.
To create the initial intents, download the `intents.json` layout file and **apply it**.
# Make Transactions (/ledger/structuring-data/working-with-layouts/making-transactions-between-wallets)
* Transaction from `tesla-wallet` to `minka-wallet`
* Transaction from `tesla-wallet` to `minka-wallet`, using accounts in them, e.g. `account:1001001234@tesla-wallet` to `account:1001009876@minka-wallet`
* Introducing outside bank core and a wallet for it, and making transaction. Here, introduce the concept of 2 phase commit and everything else that is needed.
Making first transaction between wallets [#making-first-transaction-between-wallets]
Before we make the transaction, we will check the balances of the wallets to see what is their initial state.
Due to our `default-policy` policy that allows records (in this case wallets) to be read only by their owner, we have to login as `tesla-signer` signer first. Remember to use `tesla` for password as well, as it is defined in `signers.json` layout file. To login, use the following command:
```bash
minka ledger login
```
To check the balance of the `tesla-wallet` wallet, we will use the following CLI command:
```bash
minka wallet balances tesla-wallet
```
We can do the same for the `minka-wallet` wallet, using the `minka-signer` signer.
We see that the inital balances for these wallets are:
| Wallet | USD Balance | DOP Balance |
| ------------ | ------------ | ------------ |
| tesla-wallet | 1,000,000.00 | 5,000,000.00 |
| minka-wallet | 1,000,000.00 | 5,000,000.00 |
Now, we will make a transaction between `tesla-wallet` and `minka-wallet` wallets. To do that, make sure you log in as `tesla-signer` signer again. The transfer intent will be initiated using layout file `p2p-transaction.json`. Apply this layout file using the following command:
```bash
minka ledger apply --local
```
When running the balance commands again (with separate signers for each wallet), we will see that the balance of the `tesla-wallet` wallet has decreased by 5000 and the balance of the `minka-wallet` wallet has increased by 5000, and the new balances are:
| Wallet | USD Balance | DOP Balance |
| ------------ | ------------ | ------------ |
| tesla-wallet | 995,000.00 | 5,000,000.00 |
| minka-wallet | 1,005,000.00 | 5,000,000.00 |
Making transaction between wallets using complex addressing [#making-transaction-between-wallets-using-complex-addressing]
In the previous example, we used the `tesla-wallet` and `minka-wallet` wallets directly. In this example, we will show how complex addressing can be used to make a transaction between wallets.
Introduction to wallet addressing [#introduction-to-wallet-addressing]
Wallet handle is read as a pattern `:@`. While `schema` and `parent` are optional, `handle` is required.
Here are some examples of wallet handles and how their components are interpreted:
| Wallet Handle | Schema | Handle | Parent |
| --------------------------------- | ------- | ----------- | ------------ |
| `account:1001001234@tesla-wallet` | account | 1001001234 | tesla-wallet |
| `1001009876@minka-wallet` | - | 1001009876 | minka-wallet |
| `tel:15261234578` | tel | 15261234578 | - |
| `tel@zaba` | - | tel | zaba |
| `zaba` | - | zaba | - |
Once wallet components are identified, ledger engine is using the following hierarchy to find the wallet handle in the ledger:
1. `:@`
2. `@`
3. ``
4. ``
When wallet is found in the hierarchy, it is used as the source or target of the transaction. Otherwise, the wallet is not found, the transaction is not possible and the error is returned.
For more detailed information about wallet addressing, refer to the [Wallets](/ledger/moving-money/about-wallets) chapter of the [About Wallets](/ledger/moving-money/about-wallets) documentation.
Transaction between tesla-wallet and minka-wallet using complex addressing [#transaction-between-tesla-wallet-and-minka-wallet-using-complex-addressing]
In this example, we will make a transaction between `tesla-wallet` and `minka-wallet` wallets, using complex addressing.
Source wallet will be `account:1001001234@tesla-wallet` and target wallet will be `account:1001009876@minka-wallet`. Once these wallets handles are passed through the hierarchy, they will be resolved in the following way:
* `account:1001001234@tesla-wallet`
* `account:1001001234@tesla-wallet` -> wallet found
* `account@tesla-wallet` -> wallet found
* `tesla-wallet` -> wallet found
* `account:1001009876@minka-wallet`
* `account:1001009876@minka-wallet` -> wallet found
* `account@minka-wallet` -> wallet found
* `minka-wallet` -> wallet found
This means that the transaction will be made between `tesla-wallet` and `minka-wallet` wallets.
Download the `p2p-transaction-complex.json` layout file and **apply it**.
When running the balance commands again (with separate signers for each wallet), we will see that the balance of the `tesla-wallet` wallet has decreased by 20000 and the balance of the `minka-wallet` wallet has increased by 20000, and the new balances are:
| Wallet | USD Balance | DOP Balance |
| ------------ | ------------ | ------------ |
| tesla-wallet | 975,000.00 | 5,000,000.00 |
| minka-wallet | 1,025,000.00 | 5,000,000.00 |
# Connect Bank Core (/ledger/structuring-data/working-with-layouts/connecting-bank-core-to-a-wallet)
Introduction [#introduction]
External transactional systems can be connected to a wallet. This way, the system can be notified about the transaction events that happen in the wallet and react to it. In order to achieve that, external transactional system needs to conform to Minka's `bridge` protocol.
In the image below, the bank core itself implements `bridge` protocol to establish connection with its dedicated wallet in the ledger.
In practice, more common approach is to use a dedicated service - called **bridge service** - that implements `bridge` protocol to communicate with the ledger on behalf of the external transactional system. That bridge service acts as a "bridge" between the external transactional system and the ledger. The benefit of this approach is that the external transactional system does not need to implement `bridge` protocol - instead it is contained in the separate bridge service. This simplifies the integration process. The diagram below shows this approach.
Connecting mint bank to a wallet [#connecting-mint-bank-to-a-wallet]
For the purpose of this tutorial, we will connect the fictional `mint` bank to a wallet. To do that, we will need to setup a mocked bridge service and create a dedicated `mint-signer` signer, and a dedicated `mint-wallet` wallet in the ledger.
Setting up a mocked bridge service [#setting-up-a-mocked-bridge-service]
For the purpose of this tutorial, we will use a mocked bridge service. This service will be used to simulate the communication between the external transactional system and the ledger.
This bridge service needs a dedicated signer to be able to sign the transactions that are made towards the ledger.
Creating mint-signer signer [#creating-mint-signer-signer]
`mint-signer` signer will be used to sign the transactions that will be made towards the ledger, on behalf of the mint's bank. It is created using the following command:
```bash
minka ledger signer create
```
Starting the mocked bridge service [#starting-the-mocked-bridge-service]
The bridge service will be started using the following command:
```bash
minka bridge start
```
Choose `mint-signer` signer as the signer for the bridge service.
Also, choose an option to register this bridge service with the ledger and name it `mint-bridge`. This will create a `bridge` record in the ledger, which will be used by the wallet to communicate with the bridge service.
Once started, leave the bridge running. It will be used to track the incoming requests from the ledger and respond to them.
Creating a mint-wallet wallet [#creating-a-mint-wallet-wallet]
Download the `mint-wallet.json` layout file and **apply it**.
This will create a `mint-wallet` wallet with the properties the same as previous wallets, but with the `bridge` field set to `mint-bridge`. This will allow the wallet to communicate with the bridge service.
Setup overview [#setup-overview]
Everything is set up and ready to go. The setup looks like this:
Now, whenever any kind of transaction involving `mint-wallet` wallet is initiated, the `mint-bridge` bridge will initiate a request to the mocked bridge service. This will allow external transactional system to react to the transaction events.
**Note:** For more detailed information about how to build a real bridge service, refer to the [Building a bridge service](/ledger/connecting-systems/build-a-bridge) tutorial.
# Securing the Ledger (/ledger/securing-the-ledger)
Every operation on the ledger must be authenticated and authorized. This section covers how to manage identities, configure access control, and enforce security policies.
Identity [#identity]
Create and manage the cryptographic identities that interact with the ledger.
Access Control [#access-control]
Configure who can read and write ledger records.
Circles [#circles]
Group signers into roles for simplified access management.
Domains [#domains]
Access Policies [#access-policies]
Define and manage security policies that govern ledger access.
Tutorials [#tutorials]
Prerequisites [#prerequisites]
* **[Start Here](/ledger/start-here)** — Understand ledger basics and key concepts
# About Signers (/ledger/securing-the-ledger/about-signers)
A **signer** is a digital identity in the Minka Ledger system that consists of a cryptographic key pair (private and public keys). The private key is used to digitally sign actions and transactions, while the public key allows others to verify those signatures. Anyone can create a signer, and signers serve multiple critical purposes:
* **Identity verification**: Every action on the ledger is tied to a specific signer, creating an audit trail
* **Access control**: Signers can be granted specific permissions to perform certain actions
* **Authentication**: Signers can authenticate with the ledger using JWT tokens or by signing request bodies
* **Non-repudiation**: Since only the holder of the private key can create valid signatures, actions cannot be denied later
Signers use the Ed25519 cryptographic algorithm and can be stored either locally on your device or remotely on the ledger (with the private key encrypted). When you create records like wallets, symbols, or intents, you must sign them with your signer's private key to prove you have authorization to perform that action.
Your private key should never be shared with anyone. Anyone with access to your private key can perform any action that your signer is authorized to do, including transferring balances.
# About Keys and Signatures (/ledger/securing-the-ledger/keys-and-signatures)
Overview [#overview]
As we mentioned before, every action in Minka Ledger system is a database JSON record that needs to be signed. And in order to sign it, you need to generate your public and private key pair.
You will NOT be creating any kind of "user" on the server, nor will such user exist anywhere (in any way, shape, or form).
Users themselves do not exist as an entity. We only work here with keys.
And those keys are used to sign every record you create.
Public vs private [#public-vs-private]
Public and private keys might look something like this:
```console
Public: eIHlrG0Wr5vl3pDi5Vgqzu5WGE7q/60jsysBmehYKtg=
Secret: VRrwzUf7gagiIFXhKyUfb4nyVab3fYeMqTKF5q5XQoc=
```
`private` / `secret` key must not be shared with anyone. It must be stored in a place that is secure and not accessible by unauthorized people.
Private key is used to sign everything you do inside the Minka Ledger system.
`public` key can be shared with anyone.
And it can be used by everyone to verify your signature.
Minka Ledger system is easily **auditable** because public keys allow you to verify anyone's signature.
Wallet record example [#wallet-record-example]
If you create a `wallet` record, it might look something like this:
```console
"data": {
"handle": "demo-bank-wallet",
"access": [
{
"action": "any",
"signer": {
"public": "eIHlrG0Wr5vl3pDi5Vgqzu5WGE7q/60jsysBmehYKtg="
}
}
]
},
"meta": { ... },
"luid": "$wlt...",
"hash": "..."
```
Notice that the value inside `data -> access -> signer -> public` is your public key.
This means you have full access and you can do any action (`"action": "any"`) when you sign it with your private key.
If, for example, anyone else tries to make a transaction from your wallet, that transaction will fail. Because they will sign it with a different public/private key pair.
Next steps [#next-steps]
Now that you understand how the keys and signatures work, in our [next tutorial](/ledger/securing-the-ledger/user-creation) we'll create our own public and private keys (i.e., our "user"/signer).
# About User Creation (/ledger/securing-the-ledger/user-creation)
Create your signer (user) [#create-your-signer-user]
Now that you understand how keys and signatures work, let's create a signer...
After entering `minka signer create` command you will be prompted with additional questions. Answer them the same way as shown below.
```console
$ minka signer create
? Handle: clearinghouse
? Key pair source: Generate new key pair
? Add custom data? No
? Signer password: [hidden]
? Repeat password: [hidden]
Signer clearinghouse saved locally.
✅ Signer created successfully:
Signer summary:
---------------------------------------------------------------------------
Handle: clearinghouse
Public: eIHlrG0Wr5vl3pDi5Vgqzu5WGE7q/60jsysBmehYKtg=
Access rules:
#0
- Action: any
- Signer:
- public: eIHlrG0Wr5vl3pDi5Vgqzu5WGE7q/60jsysBmehYKtg=
⚠️ WARNING:: Secret or private key is critical data that should be
handled with care. Private keys are used to modify balances and it is
important to understand that anyone who has access to that key can
perform sensitive ledger operations.
```
In the console (by default) you can only see your public key. This is for security purposes, plus, most of the time, you will only need your `public key`.
However, you can also see your `private key` if you pass the `-s` flag:
```console
$ minka signer show clearinghouse -s
? Signer password for clearinghouse [hidden]
Signer summary:
---------------------------------------------------------------------------
Handle: clearinghouse
Public: eIHlrG0Wr5vl3pDi5Vgqzu5WGE7q/60jsysBmehYKtg=
Secret: VRrwzUf7gagiIFXhKyUfb4nyVab3fYeMqTKF5q5XQoc=
⚠️ WARNING:: Secret or private key is critical data that should be handled
with care. Private keys are used to modify balances and it is important to
understand that anyone who has access to that key can perform sensitive
ledger operations.
```
IMPORTANT: **You *NEVER* want to share your private keys publicly!** But since this is a tutorial running a demo ledger on a test server it doesn't really matter who can see it.
# Generate Signing Keys (/ledger/securing-the-ledger/generate-signing-keys)
Most of our tutorials show you how to configure the Minka Ledger using our CLI tool. This is the simplest and most straightforward way, and it is our recommendation to use it when starting out. In more advanced integration scenarios it becomes necessary to dynamically create new ledger resources.
Algorithms [#algorithms]
We are using standard elliptic curve cryptography in order to create keys and verify signatures. This allows you to create signing keys using any of the already widely available libraries and tools which support the required algorithms. We are using `Ed25519 (EdDSA, Curve25519)` to generate our signing keys, so this is something to keep in mind when looking for libraries to use.
Ed25519 keys are in fact just very large integers represented with 256 bits. When these keys are used in text-based transport they are encoded as text. There are different standard formats and encodings used to represent the key as text. Minka Ledger uses the `raw` format to represent Ed25519 public and secret keys which is produced by encoding the raw 256-bit integer key value with `base64` encoding. Keys represented in this format have length 44. Example public key:
```
tB/gTevBYDYYYUgOOlsKV2Iq8DzmEtleeTopaY63wqs=
```
Raw format is smaller in size than alternatives because there are no additional headers that need to be included. Additionally, a lot of existing libraries expect Ed25519 raw keys, so this format also provides good compatibility with existing ecosystem.
Generating keys in NodeJs [#generating-keys-in-nodejs]
In this section we will show you how to generate a key pair step by step using NodeJs. From NodeJS (≥12.0.0) we can generate these keys by using the built-in `crypto` module. Before we had to use a third-party library like `elliptic`.
This module can’t directly export Ed25519 keys in `raw` format which is used in ledger, but instead we have to depend on the supported `der` format and transform it to our `raw` format.
Key encoded in `der` binary format uses `ASN.1` syntax and contains the binary data prefix with metadata about key (algorithm identifier, key length, etc…) followed by the raw binary key. This prefix is different for secret and public key as shown below.
**Secret key**
**Public key**
More details about `der` format of these keys can be found at:
* [https://datatracker.ietf.org/doc/html/rfc8410#section-10.1](https://datatracker.ietf.org/doc/html/rfc8410#section-10.1)
* [https://datatracker.ietf.org/doc/html/rfc5208#section-5](https://datatracker.ietf.org/doc/html/rfc5208#section-5)
There is also an useful tool for debugging the content of `der` formatted records which are commonly used to represent keys, certficates, signatures and other cryptographic entities:
* [https://lapo.it/asn1js/](https://lapo.it/asn1js/)
Although this tool doesn’t send any data on server when it decodes records, be careful, it’s not recommended to paste private keys used in production or other sensitive data.
In order to get the **raw** key used by ledger we simply need to strip the `der` prefix from key exported from `crypto` NodeJS module. Exact prefixes represented in hex encoding are:
* secret key hex prefix: `302e020100300506032b657004220420`
* public key hex prefix: `302a300506032b6570032100`
After these prefixes are stripped from `der` exports we need to encode the remaining 256-bit raw keys with `base64` in order to get 44 character long key required for ledger.
str
This is how to generate a new key pair using the built-in `crypto` package:
```jsx
const crypto = require('crypto')
const DER_SECRET_PREFIX = '302e020100300506032b657004220420'
const DER_PUBLIC_PREFIX = '302a300506032b6570032100'
function generateLedgerKeyPair() {
// Generate random ed25519 keys
const keyPairDer = crypto.generateKeyPairSync('ed25519')
// Export secret and public keys in der format
const secretDer = keyPairDer.privateKey
.export({ format: 'der', type: 'pkcs8' })
.toString('hex')
const publicDer = keyPairDer.publicKey
.export({ format: 'der', type: 'spki' })
.toString('hex')
const secretRaw = Buffer.from(
secretDer.slice(DER_SECRET_PREFIX.length),
'hex',
).toString('base64')
const publicRaw = Buffer.from(
publicDer.slice(DER_PUBLIC_PREFIX.length),
'hex',
).toString('base64')
return {
format: 'ed25519-raw',
secret: secretRaw,
public: publicRaw,
}
}
// Generate keys
const keyPair = generateLedgerKeyPair()
// Print keys
console.log(JSON.stringify(keyPair, null, 2))
```
| Date | Changes |
| ----------------------- | ------------------------------------------------ |
| 2023-02-07 | Updated to match `raw` key format used by ledger |
| 2022-09-07 | Initial version |
# Hash and Sign Requests (/ledger/securing-the-ledger/hash-and-sign-requests)
Each ledger operation needs to be signed by a private key that has permissions to perform that operation. We need to have a deterministic way of generating hashes and signatures in order to be able to verify them on other devices.
Hashing [#hashing]
The first part of this process is making sure to generate stable string representations of our content in order to always get the same hash for the same input. We are using JSON as our wire transport, so it makes sense to use JSON for hashing as well.
JSON is not a deterministic format, so JSON serializers in various programming languages don’t necessarily provide the same output for the same input data. To resolve this, ledger uses a JSON serialization standard described in [RFC 8785](https://www.rfc-editor.org/rfc/rfc8785). The RFC document describes the rules for serialization, there are reference implementations for several popular programming languages linked in the specification as well.
Many existing serializers should be compatible with the rules described in the RFC, the most important rule that usually needs to be implemented manually is the ordering of JSON object properties. Each JSON object in the output needs to have all properties alphabetically ordered.
Some serializers implemented in static programming languages actually output object properties in the same order as they are defined in classes, so the most basic way to accomplish this requirement can be to simply sort all properties in your strongly typed objects.
Example payload serialization in nodeJS:
```tsx
import stringify from 'safe-stable-stringify'
export function serializeData(data: any): string {
return stringify(data)
}
```
Here we use a third party package called `safe-stable-stringify` which is compatible with the rules from [RFC 8785](https://datatracker.ietf.org/doc/html/rfc8785). There are many such packages available on npm.
After we have a canonically serialized data, now we can hash it. For hashing we will use `SHA-256` hashing algorithm, and we will calculate our hash by performing the following steps:
1. Serialize the input data using RFC 8785 compatible serializer
2. Hash the serialized data using `SHA-256` as a `hex` encoded string
Example implementation in nodeJs:
```tsx
import crypto from 'crypto'
const HASHING_ALGORITHM = 'sha256'
export function createHash(data: any): string {
const serializedData = serializeData(data)
return crypto
.createHash(HASHING_ALGORITHM)
.update(serializedData)
.digest('hex')
}
```
`serializeData` function used here is the serialization function from previous example.
Signature digests [#signature-digests]
Ledger also supports attaching additional data when signing objects. This can be done by including data in the `custom` property of the signature object.
Signing only the primary payload hash like the one we generated in the previous chapter wouldn’t allow us to ensure the integrity of this additional data included in signatures. This is why an additional signature digest is calculated in these situations.
To include additional data in the hash we are using double hashing algorithm. Double hashing has some other benefits besides the ability to extend the hash with additional data. Most important among those is that it prevents certain cryptographic attacks. That is why signature digest is used for all signatures, regardless if they include custom data.
Steps to calculate a signature digest are as follows:
1. Create a primary payload hash following the steps from previous chapter
2. Serialize the additional data (`custom`) that is added in the signature using the serialization algorithm from the previous chapter, use an empty string if `custom` doesn’t exists
3. Perform another `SHA-256` hash by concatenating the hash from step 1 with the serialized `custom` data of the signature: `sha256(dataHash + serializedCustomData)` and return this hash as a `hex` encoded string
4. Use the hash from step 3 as the signature digest
Here is an example implementation in nodeJs:
```tsx
export function createSignatureDigest(
dataHash: string,
signatureCustom?: Record,
): string {
// Serialize the custom data, if it exists
const serializedCustomData = signatureCustom ?
serializeData(signatureCustom) :
''
// Create a hash by concatenating the data hash
// with serialized custom data
return crypto
.createHash(HASHING_ALGORITHM)
.update(dataHash + serializedCustomData)
.digest('hex')
}
```
`serializeData` function used here is the function from previous chapter.
Signing [#signing]
We can now sign ledger requests by signing the digest we calculated previously with our private key using `ed25519` compatible implementation. In nodeJS we can perform this by using the standard node `crypto` package:
```tsx
import crypto from 'crypto'
const digestBuffer = Buffer.from(digest, 'hex')
// This assumes you have a private key in DER format,
// it may be necessary to convert keys if that is not
// the case
const key = crypto.createPrivateKey({
format: 'der',
type: 'pkcs8',
key: keyDer,
})
// The first argument must be undefined for ed25519, it defines a digest
// algorithm, but ed25519 makes a sha512 digest as part of the algorithm.
// Signing and verification with ed25119 doesn't work correctly if that
// argument is provided. Also, most crypto examples use createSign(algorithm)
// to first create a Sign class and use that for signing, but that way also
// doesn't work with ed25519.
// see: https://github.com/mscdex/io.js/commit/7d0e50dcfef98ca56715adf74678bcaf4aa08796
const result = crypto.sign(undefined, digestBuffer, key).toString('base64')
```
| Date | Changes |
| ----------------------- | ------------------------------------------------------------------------------- |
| 2023-03-08 | Simplified hashing algorithm by reducing number of hashing operations performed |
| 2023-01-27 | Instructions about hashing signature custom data |
| 2022-09-07 | Initial version |
# Verify Payloads from Ledger (/ledger/securing-the-ledger/verify-payloads-from-ledger)
The ledger signs all records it produces with its own key pair. This applies to two categories of communication:
* **HTTP responses**: records returned when reading or creating resources (wallets, intents, signers, etc.)
* **HTTP requests made by the ledger**: outbound calls such as effect deliveries and two-phase commit (2PC) requests sent to your bridge
Verifying these signatures lets you confirm that the content was genuinely produced by the ledger and has not been tampered with.
Obtaining the ledger's public key [#obtaining-the-ledgers-public-key]
All signing is done by the ledger's built-in **System** signer. Read it and store the public key, you will need it whether you are using the SDK or verifying proofs manually.
```tsx
const systemSigner = await sdk.signer.read('system')
const ledgerPublicKey = systemSigner.data.public
```
Using the SDK client [#using-the-sdk-client]
The recommended approach is to use the `ProofVerificationClient` exposed by the SDK. Pass the public key obtained above during initialization so the SDK can verify proofs automatically on every response.
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: 'https://ledger.minka.io/api/v2',
signer: { format: 'ed25519-raw', public: ledgerPublicKey },
verifyResponseProofs: true, // automatically verify all incoming responses
})
```
You can also verify individual records on demand using the `sdk.proofs` client:
```tsx
// Basic verification
await sdk.proofs.verify(record)
// Chained assertions
await sdk.proofs
.ledger() // assert the ledger's own key signed it
.length(1) // assert at least one proof is present
.verify(record)
```
When a proof is invalid the client throws a `LedgerSdkError` describing the failure.
Manual verification [#manual-verification]
If you are not using the SDK, you can verify proofs by hand by following the steps below.
Proof ordering is not guaranteed. Because the ledger is a distributed system, the array of proofs on a record may arrive in any order. Always locate the proof you want to verify by its `public` key rather than by its position in the array.
Step 1: Recalculate the record hash and validate it. [#step-1-recalculate-the-record-hash-and-validate-it]
The hash covers the record's data payload, not its metadata (proofs, hash field, etc.). The serialization and hashing algorithm is the same one used when signing requests: RFC 8785 canonicalization followed by a SHA-256 hex digest. See [How to hash and sign Ledger requests](/ledger/securing-the-ledger/hash-and-sign-requests) for the full explanation and the `serializeData` and `createHash` helper implementations used in the steps below.
```tsx
const dataHash = createHash(ledgerResponse.data)
const hashIsValid = dataHash === ledgerResponse.hash
```
Step 2: Recalculate the signature digest [#step-2-recalculate-the-signature-digest]
Each proof may include a `custom` object with additional data (timestamp, status, etc.). The digest that was actually signed combines the record hash with that custom data using the double-hashing algorithm. See the [Signature digests](/ledger/securing-the-ledger/hash-and-sign-requests#signature-digests) section of the signing guide for the full explanation and the `createSignatureDigest` helper implementation used below.
For each proof you want to verify, pass the `dataHash` from the previous step and the proof's `custom` field:
```tsx
const digest = createSignatureDigest(dataHash, proof.custom)
```
You can cross-check `digest` against the `digest` field stored on the proof object, they must match before you proceed to signature verification.
Step 3: Verify the signature [#step-3-verify-the-signature]
With the digest in hand, verify that the proof's `result` (the actual signature bytes) was produced by the private key that corresponds to `ledgerPublicKey`.
```tsx
function verifySignature(
digest: string,
proof: LedgerProof,
): boolean {
const digestBuffer = Buffer.from(digest, 'hex')
const signatureBuffer = Buffer.from(proof.result, 'base64')
const key = crypto.createPublicKey({
format: 'der',
type: 'spki',
key: Buffer.from(proof.public, 'base64'),
})
// The first argument must be undefined for ed25519, the algorithm
// uses SHA-512 internally and does not accept an external digest algorithm.
return crypto.verify(undefined, digestBuffer, key, signatureBuffer)
}
const isValid = verifySignature(digest, proof)
```
A `true` return value means the proof was created by the holder of the corresponding private key and that the record data (including any custom proof data) has not been modified.
Putting it all together [#putting-it-all-together]
```tsx
function verifyLedgerProof(
ledgerResponse: any,
expectedPublicKey: string,
): boolean {
const { hash, meta, data } = ledgerResponse
const dataHash = createHash(data)
const hashIsValid = dataHash === hash
if ( !hashIsValid ) {
throw new Error('Invalid ledger response hash')
}
// Find the proof signed by the expected public key
const proof = meta?.proofs?.find(
(p: LedgerProof) => p.public === expectedPublicKey,
)
if (!proof) {
throw new Error('No proof found for the given public key')
}
const digest = createSignatureDigest(dataHash, proof.custom)
if (digest !== proof.digest) {
throw new Error('Digest mismatch')
}
return verifySignature(digest, proof)
}
```
| Date | Changes |
| ----------------------- | --------------- |
| 2026-02-27 | Initial version |
# Authenticate with JWT (/ledger/securing-the-ledger/authenticate-with-jwt)
Ledger SDK allows users to authenticate to ledger by sending tokens.
The JWT options object (`JwtConfig`) has the following definition
```tsx
type JwtConfig = {
/**
* Represents a client identifier.
*
*/
iss: string
/**
* Represents a user identifier of the token sender.
*
*/
sub: string
/**
* Represents a recipient for which a token is intended, ledger public key or handle
*
* */
aud: string
/**
* Time after which a token expires, seconds since epoch.
*
*/
exp: number
/**
* Unique id of the token, can be used to prevent replay attacks
*
*/
jti?: string
/**
* Defines the request hash claim (hsh) must
* be created and sent.
*
* Set as "true" if this value is not
* given.
*/
createHsh?: boolean
/**
* ED25519 Key pair for signing token
*
*/
keyPair: LedgerKeyPair
/**
* Token verification key identifier.
* Accepts a public key or a ledger signer handle.
*/
kid?: LedgerHandle | LedgerKeyPair['public']
}
```
JWT configuration can be set and mixed at three different levels - SDK, client and request.
SDK - Securing SDK instance [#sdk---securing-sdk-instance]
SDK can be secured when initializing a new object by using the property `secure` of SDK constructor options.
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
},
secure: {
aud: '',
iss: '',
keyPair: {
public: '',
format: '',
secret: ''
},
sub: '',
exp: 3600 // (1 hour)
createHsh: true,
kid: '',
}
})
```
This can also be set dynamically after creating a new instance with the method `setAuthParams`
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
}
})
sdk.setAuthParams({
aud: '',
iss: '',
keyPair: {
public: '',
format: '',
secret: ''
},
sub: '',
exp: 3600 // (1 hour)
createHsh: true,
kid: '',
})
```
Client - Securing SDK Client [#client---securing-sdk-client]
A client can be dynamically secured with the method `setAuthParams`
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
}
})
// Securing wallet client
sdk.wallet.setAuthParams({
aud: '',
iss: '',
keyPair: {
public: '',
format: '',
secret: ''
},
sub: '',
exp: 3600 // (1 hour)
createHsh: true,
kid: '',
})
```
Request - Securing a API call [#request---securing-a-api-call]
A request can be dynamically secured with the method `setAuthParams`
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
}
})
// Securing a search for a wallet
const { wallet } = await sdk.wallet.read('wallet-handle', {
aud: '',
iss: '',
keyPair: {
public: '',
format: '',
secret: ''
},
sub: '',
exp: 3600 // (1 hour)
createHsh: true,
kid: '',
})
```
See [About Authentication](/ledger/securing-the-ledger/about-authentication) for more details.
| Date | Changes |
| ----------------------- | --------------- |
| 2023-05-08 | Initial version |
# Authenticate with OAuth 2.0 (/ledger/securing-the-ledger/authenticate-with-oauth)
***
The ledger supports the [OAuth 2.0 Client Credentials Grant](https://www.rfc-editor.org/rfc/rfc6749#section-4.4) (RFC 6749 §4.4) as an alternative to key-pair JWT authentication. This is designed for machine-to-machine integrations where a client application exchanges a `clientId` and `clientSecret` for a short-lived access token, without managing asymmetric keys directly. The returned token is an RS256-signed JWT issued by a designated provider signer on your ledger.
The identity provider (idP) is the entity that issues OAuth2 access tokens: this could either be the ledger itself or an external idP like Auth0.
Prerequisites [#prerequisites]
* A ledger with an active authentication policy (configured below)
* The `@minka/ledger-sdk` package
Setting up OAuth2 in Ledger [#setting-up-oauth2-in-ledger]
There are one-time setup steps before a client application can exchange credentials for a token.
Option A: Ledger as the id provider [#option-a-ledger-as-the-id-provider]
Step 1: Create the identity provider's signer and factor [#step-1-create-the-identity-providers-signer-and-factor]
Create the provider's signer:
```ts
import { createRsaKeyPair } from '@minka/crypto'
import { LedgerSdk } from '@minka/ledger-sdk'
import { AccessAction } from '@minka/types'
// Generate an RSA-2048 key pair for the provider signer.
const { secret: rsaPrivateKeyPem, public: rsaPublicKeyDerBase64 } =
await createRsaKeyPair('der')
// Create the provider signer record on the ledger.
const { keyPair: providerKeyPair } = await sdk.signer
.init()
.data({ handle: 'my-oauth-provider' })
.hash()
.sign([{ keyPair: adminKeyPair }])
.send()
```
Create the provider's signer factor:
```ts
// Attach an RSA key-pair factor so the provider can sign JWTs.
// The factor stores the RSA public key (DER base64) and the encrypted private key (PEM).
// The factor handle becomes the `kid` header in every JWT this provider issues,
// so receivers can identify which public key to use for verification.
await sdk.signer
.with('my-oauth-provider')
.factor.init()
.data({
handle: 'my-oauth-provider-key',
signer: 'my-oauth-provider',
schema: 'key-pair',
format: 'rsa-der',
public: rsaPublicKeyDerBase64,
secret: '{{ secret.private }}',
access: [{ action: AccessAction.Any }],
})
.meta({
proofs: [],
secret: { private: rsaPrivateKeyPem },
})
.hash()
.sign([{ keyPair: providerKeyPair }])
.send()
```
Step 2: Create an authentication policy [#step-2-create-an-authentication-policy]
An `authentication` policy designates which signers are allowed to issue OAuth tokens on your ledger.
The `values` array contains at least one entry with `schema: 'oauth2'`.
```ts
import { AccessRecord, AccessAction, PolicyValue } from '@minka/types'
const oauthPolicyValues: PolicyValue[] = [
{
schema: 'oauth2', // required
signer: { handle: 'my-oauth-provider' }, // required: references the signer of the provider created in the previous step.
config: { "jwt.ttl": 3600 }, // optional: token time-to-live in seconds. Defaults to 3600
target: { schema: 'oauth-application' }, // optional: scope this provider to only authenticate signers with this specific schema.
} as PolicyValue,
]
```
You may also have multiple providers (either ledger-based or external idPs) within the authentication policy.
Note: Each additional provider must also be created in ledger as in the previous step.
For example:
```ts
const oauthPolicyValues: PolicyValue[] = [
{
schema: 'oauth2',
signer: { handle: 'my-oauth-provider' },
target: { schema: 'oauth-application' },
} as PolicyValue,
{
schema: 'oauth2',
signer: { handle: 'another-provider' },
target: { schema: 'another-application' },
} as PolicyValue,
]
```
Proceed to create the authentication policy:
```ts
await sdk.policy
.init()
.data({
handle: 'my-oauth-policy',
schema: 'authentication',
record: AccessRecord.Any,
access: [{ action: AccessAction.Any }],
values: oauthPolicyValues,
})
.hash()
.sign([{ keyPair: adminKeyPair }])
.send()
```
Option B: An external idP is the provider (e.g. Auth0) [#option-b-an-external-idp-is-the-provider-eg-auth0]
When an external IdP issues tokens, the ledger does not mint JWTs itself — it only validates them.
Step 1: Create the identity provider's signer and factor [#step-1-create-the-identity-providers-signer-and-factor-1]
Create a signer referencing the external provider:
```ts
// Create the provider signer. Its handle can be any valid identifier.
const { keyPair: providerKeyPair } = await sdk.signer
.init()
.data({ handle: 'auth0-idp' })
.hash()
.sign([{ keyPair: adminKeyPair }])
.send()
```
The ledger identifies the verification key from the `kid` header in the incoming JWT. It looks up a **key-pair factor whose handle equals the `kid`**, then uses that factor's public key for RS256 signature verification.
Create the provider's signer factor:
```ts
// Fetch the kid and RSA public key (SPKI DER, base64-encoded) from the IdP's JWKS.
// For Auth0, the JWKS endpoint is: https:///.well-known/jwks.json
const idpKid = ''
const idpPublicKeyDerBase64 = ''
// Attach a key-pair factor whose handle equals the kid from the IdP's JWKS.
// The ledger uses this factor's public key to verify incoming Bearer JWTs.
// No private key is stored — the ledger only verifies, never signs.
await sdk.signer
.with('auth0-idp')
.factor.init()
.data({
handle: idpKid, // the handle must equal the kid from the IdP's JWKS.
signer: 'auth0-idp',
schema: 'key-pair',
format: 'rsa-der',
public: idpPublicKeyDerBase64,
access: [{ action: AccessAction.Any }],
})
.meta({ proofs: [] })
.hash()
.sign([{ keyPair: providerKeyPair }])
.send()
```
**Client flow:** clients use their `clientId` and `clientSecret` with the external IdP's token endpoint (e.g., Auth0's `/oauth/token`) to obtain a signed JWT. They then use that JWT as the Bearer token for ledger API calls — no call to the ledger's `/oauth/token` is needed.
Step 2: Create the authentication policy [#step-2-create-the-authentication-policy]
For example:
```ts
const oauthPolicyValues: PolicyValue[] = [
{
schema: 'oauth2', // required
signer: { handle: 'auth0-idp' }, // required: references the signer of the provider created in the previous step
} as PolicyValue,
]
```
Proceed to create the authentication policy:
```ts
await sdk.policy
.init()
.data({
handle: 'my-oauth-policy',
schema: 'authentication',
record: AccessRecord.Any,
access: [{ action: AccessAction.Any }],
values: oauthPolicyValues,
})
.hash()
.sign([{ keyPair: adminKeyPair }])
.send()
```
Specifying a `target` with an external idP will require **all** signers authenticating via this provider to exist in ledger AND match the target fields.
For example: For an Auth0 JWT with `sub: 'Auth0|12345'`, a signer with `handle: Auth0|12345` must exist with the schema matching to the policy's target.
Onboarding a user [#onboarding-a-user]
The following onboarding is required for all signers when the identity provider is the ledger itself, or when a `target` is assigned to an external identity provider.
Create the application signer and OAuth credentials factor [#create-the-application-signer-and-oauth-credentials-factor]
Create the applcation signer: the entity authenticating via OAuth. It appears as the `sub` (subject) claim in the JWT.
```ts
import { LedgerOAuthFactor } from '@minka/types'
// Create the application signer.
await sdk.signer
.init()
.data({ handle: 'my-app-signer' })
.hash()
.sign([{ keyPair: adminKeyPair }])
.send()
```
Create an `oauth-client-credentials` factor: the credentials used by the application signer to fetch the access token.
The ledger auto-generates the `clientId` and `clientSecret` — retrieve the `clientSecret` immediately after creation using `include: ['meta.secret']`.
```ts
// Attach an OAuth credentials factor.
const { factor, meta } = await sdk.signer
.with('my-app-signer')
.factor.init()
.data({
handle: 'my-app-oauth-credentials',
signer: 'my-app-signer',
schema: 'oauth-client-credentials',
access: [{ action: AccessAction.Any }],
})
.meta({ proofs: [] })
.hash()
.sign([{ keyPair: appKeyPair }])
.send({
query: { include: ['meta.secret'] },
})
const clientId = factor.clientId
const clientSecret = meta.secret.clientSecret
```
The factor credentials can also be fetched after creation using:
```ts
const { factor, meta } = await sdk.signer
.with('my-app-signer')
.factor.read('my-app-oauth-credentials', {
query: { include: ['meta.secret'] },
})
```
Exchanging credentials for a token [#exchanging-credentials-for-a-token]
For ledger-based identity providers, you may exchange the `clientId` and `clientSecret` of your application for an access token using the SDK:
```ts
const { accessToken, tokenType, expiresIn } =
await sdk.oauth.exchangeToken(clientId, clientSecret)
// accessToken: RS256-signed JWT
// tokenType: 'Bearer'
// expiresIn: seconds until the token expires (default: 3600)
```
The SDK handles the `POST /v2/oauth/token` request, the `Authorization: Basic` header encoding, and the `application/x-www-form-urlencoded` body format automatically.
Using the token for API calls [#using-the-token-for-api-calls]
`exchangeToken` returns the token but does not configure the SDK automatically. Pass the token to `sdk.setAuthParams` to authenticate all subsequent calls on that SDK instance.
```ts
sdk.setAuthParams({ overrideToken: accessToken })
// All subsequent calls on this SDK instance use the OAuth token.
const { wallet } = await sdk.wallet.read('my-wallet')
```
Alternatively, construct the SDK with a pre-obtained token from the start:
```ts
const sdk = new LedgerSdk({
server: '',
secure: { overrideToken: accessToken },
})
```
The token is valid for `expiresIn` seconds. Call `exchangeToken` again before expiry and update the SDK:
```ts
const { accessToken: refreshed } = await sdk.oauth.exchangeToken(clientId, clientSecret)
sdk.setAuthParams({ overrideToken: refreshed })
```
| Date | Changes |
| ----------------------- | --------------- |
| 2026-03-23 | Initial version |
# About Security (/ledger/securing-the-ledger/about-security)
The core of ledger security is based on asymmetric or public-key cryptography. Besides this, a third-part authentication strategies are also supported by validating JWT bearer tokens. Other aspects of security are covered by a flexible authorization system based on configurable rules, exchanging messages through secure (encrypted) channels and encrypting data at rest.
Asymmetric Cryptography [#asymmetric-cryptography]
This is an approach that relies on key pairs, a public and a private key in order to sign messages that are exchanged by the system. Messages are signed by first creating a hash of the message content. It is very important that private keys are kept secret, they should never be shared to anyone. Only a public key is shared to ledger and/or other parties. This allows anyone to validate received messages, but only the owner of the private key can sign new messages.
The model described here makes sure that we can always verify both message integrity and authenticity. If anyone tampers with a message that has been signed, a hash of that message will change and that will invalidate the provided signature. Authenticity of the parties can be verified through key pairs used when signing messages. A new key pair needs to be created for every participant of the system and that allows us to link message signatures to participants of the system.
Non-repudiation is an important aspect of the system that the described security model helps us to enforce. Non-repudiation is a legal concept that refers to an assurance that the sender of the information is provided with proof of delivery and the recipient is provided with proof of sender’s identity. Because of this, neither can later deny having processed the information or performed a certain action.
The primary digital signatures algorithms used by the ledger is **ed25519.** To learn more about it, see [A Deep Dive into Ed25519 Signatures](https://cendyne.dev/posts/2022-03-06-ed25519-signatures.html).
Bearer Authentication [#bearer-authentication]
Asymmetric cryptography provides the best security guarantees, but in some cases it is impractical to use. This is most commonly noticeable in client facing applications, like websites used for configuration or analytics. In some situations it is acceptable to have a security model which is a bit more flexible. For this purpose we have also supported standard authentication mechanism using JWT bearer tokens.
JWT tokens can be used to carry additional information about the user or request, so we can achieve similar level of security as the explicit signatures used in the asymmetric cryptography model.
Most common use case where pure signatures are impractical is related to data reading operations. In mutation requests the payload or body of the request is hashed and signed. Read API requests don’t contain any body that should be stored in the system, so we cannot use this method to secure the requests. For these use cases we support providing a JWT token with the request that is issued by the client, using its private key. This JWT token may also contain a hash of the entire request which adds an additional security check which prevents anyone from tampering with the data in transit. Single use JWT tokens which include a request hash and are issued using private keys of participants have the same security properties as the signatures described previously.
JWT tokens can be used in a way that is a bit less secure as well. This is useful for less sensitive system operations, but it provides a much better user experience. JWT tokens used this way are multi-use tokens with expiration time. This allows users to login (create their token) and use the same token for all requests in a certain time period. This flow is something users are used to with other services and tools, so it is preferred for direct user interactions. Downside of this approach is that those tokens cannot contain a signed hash of the request and they could be used by attackers until they expire, in case they are leaked. This is usually acceptable for non-critical system operations in most cases.
Ledger also supports third party bearer tokens. This security model allows the system to validate bearer tokens issued by third party authentication systems as well. Those tokens have similar security properties as the multi-use tokens issued using ledger private keys, if the third party issuer is trusted. To configure ledger to accept those tokens it is necessary to register a JWT verification key provided by third party issuer along with any other JWT claims that describe tokens that should be accepted. This model allows for simple integration with third party user management systems which support JWT, for example Auth0, Firebase Auth, GCP Cloud Identity, AWS Cognito, etc.
To learn more about JWT, see [JSON Web Token Introduction](https://jwt.io/introduction).
Authorization Rules [#authorization-rules]
Ledger supports granular security controls on all resources using authorization rules. Authorization rules can be configured on every record stored in the ledger, and allow users to declaratively specify security constraints of a record. Authorization rules are part of record payload, this means that they are auditable and signed along with all other record data.
Authorization rules can be defined globally, on the level of an entire ledger. Since all ledger data belongs to a specific ledger instance, ledger level rules allow us to configure security on all records belonging to that ledger instance. This is very useful to setup global security framework of the entire system.
Local authorization rules can also be defined on the level of each record. Those rules allow us to configure more granular access permissions on individual records. The syntax and features of both types of rules is the same, the only difference between them is the scope of records they can target and affect.
Each ledger rules can target a specific ledger operation. It can additionally use any data present in the ledger operation or record for more specific conditions and targeting. Permissions can be granted to specific public keys, signers or bearer tokens.
For more details, see [About Authorization](/ledger/securing-the-ledger/about-authorization).
Secure Communication Channel [#secure-communication-channel]
Most of ledger communication is performed using HTTPS protocol. HTTPS protocol is a secure version of the HTTP protocol. HTTPS is encrypted in order to increase security of data transfer. The encryption protocol used to encrypt data is called Transport Layer Security (TLS), although formerly it was knows as Secure Sockets Layer (SSL). TLS uses Public Key Infrastructure (PKI) in order to encrypt data. PKI is an asymmetric encryption security system, so it is also used by generating a private and public key pair. A private key in this model is used by the server to decrypt data, and is always kept private, as the name suggests. A public key is shared to users, it is used to encrypt data sent to the server. This means anyone can encrypt data, but only the intended recipient, in our case ledger, can decrypt it.
TLS evolved over time, the latest and most secure version is TLS 1.3. This is the version that is required in order to communicate securely with the ledger over HTTPS.
TLS 1.3 is considered more secure because it eliminates weaker cipher suites, provides perfect forward secrecy, streamlines the handshake process, enhances session resumption, and includes several new security features. These improvements significantly reduce the risk of successful attacks on the communication, making TLS 1.3 the preferred protocol for secure communication over the internet.
To learn more about TLS and related cryptographic principles, see [Transport Layer Security](https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/tls.html).
Data Encryption at Rest [#data-encryption-at-rest]
For data encryption at rest we use services provided by Google GCP tools. All databases are provisioned as CloudSQL databases with configured data encryption features. Database backups are also encrypted with their own data encryption keys. Google uses a [FIPS 140-2 validated](https://cloud.google.com/security/compliance/fips-140-2-validated) encryption module [(certificate 3318)](https://csrc.nist.gov/projects/cryptographic-module-validation-program/Certificate/3318) in production environments.
All data is encrypted using the 256-bit [Advanced Encryption Standard](https://wikipedia.org/wiki/Advanced_Encryption_Standard) (AES-256), or better, with symmetric keys: that is, the same key is used to encrypt the data when it is stored, and to decrypt it when it is used. These data keys are themselves encrypted using a key stored in a secure keystore, and changed regularly.
For more details, see [Encryption at Rest in Google Cloud](https://cloud.google.com/security/encryption/default-encryption).
| Date | Changes |
| ----------------------- | --------------- |
| 2022-12-15 | Initial version |
# About Authentication (/ledger/securing-the-ledger/about-authentication)
Ledger security is based on asymmetric cryptography. Each ledger record is secured by a public and private key pair. Public keys are registered in the ledger and each ledger operation is verified by checking the provided signatures.
There are two main types of ledger requests, mutations and reads. A mutation stands for any kind of request to store or modify a record in the ledger.
Authenticating by signing a mutation body [#authenticating-by-signing-a-mutation-body]
Mutation requests always contain a payload which is designed to convey all information required to perform the mutation.
Because of that mutations are more straightforward and secure. The primary security mechanism for mutations is contained in the `proofs` array that is provided in the `meta` object as part of the payload. For example, a body of a create wallet request looks like this:
```json
{
"hash": ""
"data": {
"handle": "wallet-handle"
},
"meta": {
"proofs": [{
"method": "ed25519-v2",
"public": "",
"result": "",
"digest": "",
"custom": {
"moment": "2023-02-20T21:42:10.279Z"
}
}]
}
}
```
Steps to sign the object like above:
1. Serialize the data
2. Hash the serialized data
3. Sign the hash with one or more private keys
The keys used for signing are going to be used by the ledger to verify if it is allowed to execute the request in question.
Steps to verify an incoming mutation:
1. Serialize the data
2. Hash the serialized data
3. Compare the received hash with the hash from the payload
4. Verify each received signature using public keys from the signatures array and the calculated hash
If the steps above are successful, this means that the received payload is valid and that it was sent by the owners of the provided public keys. We still need to check the permissions of those public keys in order to make sure they are authorized to perform the required operation. See [About Authorization](/ledger/securing-the-ledger/about-authorization) for more details about authorization.
Even though mutations are authenticated via body signature, clients can also use [JWT Tokens](/ledger/securing-the-ledger/about-authentication#authenticating-with-jwt-token) to authenticate and provide a second layer of security. Additionally, clients can skip body signing entirely and authenticate mutations with just a token via [token impersonation](/ledger/securing-the-ledger/about-authentication#token-impersonation).
Authenticating with JWT token [#authenticating-with-jwt-token]
The presence of a token - Authorization header - is not mandatory. It becomes required through the configuration of authorization access rules that requires a token to grant access. Once sent, the token is validated for its format, signature and expiration, regardless of the presence of access rules.
The [above](/ledger/securing-the-ledger/about-authentication#authenticating-with-jwt-token) mechanism doesn't work for read requests, since those are usually `GET` HTTP requests without any body. For those requests the URL, query parameters and headers define what is going to be returned by the API. To make the ledger API easy to use, but in order to still keep the same security model, the ledger supports JWT tokens for security context exchange between clients and the server. JWT tokens are very flexible and also allow users to transport additional information as part of their payload that can be verified by the ledger. To keep the primary security model compatible with the model for body signatures, clients which hold private keys can issue JWT tokens that can be validated by public keys which ledger has access.
In this model, JWT token replaces the body that is sent in mutation requests, but the whole security remains the same. A client issues a JWT and signs it with its own private key. The ledger can verify that JWT with a public key and enforce security constraints configured for that public key in case the verification is successful. Requests with invalid tokens are rejected regardless of authorization rules set to the ledger.
JWT should be sent to the ledger in a standard way, using the `Authorization` header:
```
Authorization: Bearer
```
JWT required headers:
```
kid - public key which should be used to verify the token signature
alg - algorithm used to sign the JWT Token
```
Supported algorithms at the present moment:
* EdDSA (ed25519 schema)
JWT payload claims definition:
| | Definition |
| ------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| **iss** | issuer of the token, represents a client identifier, for example `cli`, `studio`, etc. |
| **sub** | subject of the token, represents a user identifier, either public key or handle of the signer or an arbitrary string for external tokens. |
| **aud** | audience of the token, represents a recipient for which a token is intended |
| **iat** | issued at time, time at which a token was issued, seconds since epoch |
| **exp** | expiration time, time after which a token expires, seconds since epoch |
| **jti** | (optional) unique id of the token, can be used to prevent replay attacks |
| **hsh** | (optional) request hash (sha256), custom ledger field that enables request content validation |
All claims listed above except for `jti` and `hsh` are required. Public keys or handles can be used for all entities that have them as identifiers. Handles are preferable, if a key is registered with the ledger, since they are shorter.
Tokens without `hsh` are not linked to a specific request and can be used for multiple API requests. Token expiration is controlled by `exp` claim. If a `jti` claim is present the ledger needs to store the token id until the token expiration time expires. Clients should create short lived tokens if they provide a `jti`, max `exp` allowed is 5 minutes for single use tokens.
A more secure token can also be created by including a request hash. This ties a token to a specific request, so it limits the possible attacks in case a token is leaked. The hashing algorithm used is the same as for the request body described above. The steps to create a request hash:
Create an JSON object representing a request
```json
{
url: "",
method: "", // for example POST
headers: {
// Any protected headers or null if no headers should be protected
// key/value pairs, keys should be lowercased,
// for example "content-type": "application/json"
},
body: {
// Request body or null. This should be an object in case of JSON.
}
}
```
1. Serialize the request JSON object to string by using a deterministic algorithm - two objects with same property names and values should result in the same string. For example: the resulting string should be equal for the following request objects.
```tsx
const aRequest = {
url: "https://minka.io",
method: "GET"
}
const anotherRequest = {
method: "GET"
method: "https://minka.io"
}
```
2. Hash the serialized request object with `sha256` algorithm.
3. Format the `hsh` field by using the following format
```json
"hsh": ":"
// example, if Content-Type and X-Api-Key headers are protected
"hsh": "3da5df75f03a365b0bc4f53946c77f017aa4fd03ba49977fb7ceb8d75f65cb8f:content-type,x-api-key"
// example, if there are no protected headers, only hash should be included
"hsh": "3da5df75f03a365b0bc4f53946c77f017aa4fd03ba49977fb7ceb8d75f65cb8f"
```
We still need to check the permissions of those public keys in order to make sure they are authorized to perform the required operation.
See [About Authorization](/ledger/securing-the-ledger/about-authorization) for more details about authorization.
Authenticating mutations with token impersonation [#token-impersonation]
The standard way to authenticate a mutation is to sign the request body with a private key and include the proof in `meta.proofs`. However, the ledger also supports **token impersonation**, which allows clients to authenticate mutation requests using only a JWT token in the `Authorization` header — without having to sign the request body themselves.
This is useful when the client has access to a JWT token but does not have direct access to the private key needed to sign the payload, or when simplifying client-side integration is preferred.
How token impersonation works [#how-token-impersonation-works]
When the ledger receives a mutation request (create, update, add proof, or drop) with a valid JWT token and no body signature, it performs the following steps:
1. **Token validation**: The JWT is extracted from the `Authorization` header and validated as described in [Authenticating with JWT token](#authenticating-with-jwt-token). The ledger resolves the signer associated with the token's `kid` header.
2. **Impersonation signing**: The ledger's built-in `system.auth` signer re-signs the request data using its own key pair, on behalf of the authenticated token holder. The generated proof is annotated with:
* `signer` — set to the handle of the signer identified by the token
* `origin` — set to `self-signed-token` to distinguish it from a direct key-pair signature
* `issuer` — set to the handle of the token issuer
3. **Proof injection**: The system-generated proofs are appended to the request's `meta.proofs` array. The rest of the middleware pipeline (signature validation, authorization) processes them as normal proofs.
4. **Authorization**: The authorization check uses the resolved signer from the token, so the same access rules that apply to key-pair-signed mutations apply to token-impersonated mutations.
Sending a token-only mutation [#sending-a-token-only-mutation]
To create a wallet using only a JWT token, send the request body without `hash` or `meta.proofs`:
```json
{
"data": {
"handle": "wallet-handle"
}
}
```
With the `Authorization` header:
```
Authorization: Bearer
```
The ledger will hash the data, generate the proof using the `system.auth` signer on behalf of the token's signer, and validate signatures and authorization as usual.
Partial proofs [#partial-proofs]
Clients can also send **partial proofs** — proof objects in the `meta.proofs` array that omit the `public` key field. Partial proofs act as templates: the impersonation middleware signs on behalf of each partial proof, carrying over any `custom` data the client included.
This is useful when the client needs to attach custom metadata (such as `status` or `labels`) to the proof while letting the ledger handle the cryptographic signing.
```json
{
"data": {
"handle": "wallet-handle"
},
"meta": {
"proofs": [{
"custom": {
"status": "active"
}
}]
}
}
```
The ledger will merge the custom data from each partial proof into the generated signature, producing a fully-formed proof with the appropriate origin.
OAuth2 bearer token impersonation [#oauth2-bearer-token-impersonation]
When the bearer token is an OAuth2 token — either issued by the ledger's `/oauth/token` endpoint or by an external identity provider (IdP) — impersonation follows the same pattern but with two differences.
**Proof origin**: The injected proof carries `origin: oauth2-token` instead of `self-signed-token`, allowing consumers to distinguish the authentication method.
**Signer resolution from `sub`**:
* **Ledger-issued tokens** — `iss` matches the provider signer handle. The `sub` claim must follow the `signer:` format, and the referenced signer must exist on the ledger.
* **External IdP tokens** — `iss` contains `://` (e.g. an Auth0 domain). The `sub` claim is trusted as-is and used directly as the impersonated signer handle. No corresponding signer needs to exist on the ledger.
In both cases `issuer` on the proof is set to the token's `iss` claim.
Supported operations [#supported-operations]
Token impersonation is available for all mutation types:
| Operation | Endpoint pattern | Impersonation middleware |
| ------------- | ----------------------------------------------------------- | ------------------------------------------- |
| **Create** | `POST /v2/` | Signs and injects proofs into `meta.proofs` |
| **Update** | `PUT /v2//:id` | Signs and injects proofs into `meta.proofs` |
| **Drop** | `POST /v2//:id/drop` or `DELETE /v2//:id` | Signs and injects proofs into `meta.proofs` |
| **Add proof** | `POST /v2//:id/proofs` | Signs and replaces the proof payload body |
Token impersonation only activates when a valid JWT token is present. If the request already contains fully-signed proofs (with a `public` key), those are used as-is and impersonation is skipped for those proofs.
Security considerations [#security-considerations]
* The `system.auth` signer is a ledger-managed internal signer. Its key pairs are never exposed to clients.
* Impersonated proofs are marked with `origin: self-signed-token` (self-signed JWT) or `origin: oauth2-token` (OAuth2 bearer token). This allows the ledger to distinguish between direct key-pair signatures and token-based impersonated signatures during proof resolution.
* The same authorization rules apply regardless of whether the proof originated from a direct signature or token impersonation. The resolved signer from the token must have the required permissions.
* Clients cannot forge impersonated proofs. During proof resolution, the ledger verifies that proofs claiming either impersonated origin were actually signed by the `system.auth` signer's public key. Proofs with spoofed origin values are stripped and treated as regular key-pair proofs.
| Date | Changes |
| ----------------------- | ----------------------------------------------------------------------------------------- |
| 2026-03-12 | Added token impersonation section for mutation authentication via JWT. |
| 2023-05-08 | Added callout about Authorization header optionality. |
| 2023-04-17 | Fixed list of supported algorithms for token signature-`EdDSA` |
| 2023-03-07 | • Added `digest` as part of `signature` object
• Added `custom.moment` to examples |
| 2023-01-16 | Initial version |
# About Authorization (/ledger/securing-the-ledger/about-authorization)
Authorization stages in Minka [#authorization-stages-in-minka]
Authorization in Minka happens in two distinct stages:
Stage 1: Children Access Gates - Sequential Top-Down [#stage-1-children-access-gates---sequential-top-down]
Before any specific action can be performed, the system checks `access` rule in a strict top-down sequence:
1. **Server level**: "Can this signer access the server children (ledgers)?"
2. **Ledger level**: "Can this signer access the ledger children (records)?"
If either check fails, the request is blocked. This acts as a gatekeeper mechanism.
Stage 2: Action Authorization - Parallel Rule Collection [#stage-2-action-authorization---parallel-rule-collection]
Once the access gates pass, the system:
1. **Collects ALL access rules** from all rule sources which can affect particular record:
* Server-level rules
* Ledger-level rules
* Domain-level rules
* Record-level rules
2. **Applies additive model**: If ANY rule grants the requested action, access is granted.
**Key Insight**: Stage 1 uses restrictive gates (ALL must pass), while Stage 2 uses an additive model (ANY can grant access). This means overly permissive rules at higher levels (server/ledger) can override restrictive rules at lower levels (record).
Autheticating actions [#autheticating-actions]
There are two ways in which clients can authenticate to the ledger, the first by signing the mutation bodies, and the second by sending a JWT token. See [About Authentication](/ledger/securing-the-ledger/about-authentication) for more details about it.
These two forms of authentication allow users to say who they are, but do not guarantee that they have privileges to access the ledger.
Access Strategies [#access-strategies]
Minka ledgers support two access strategies that determine how access control is managed:
Record-Based Access Strategy (Default) [#record-based-access-strategy-default]
In **record-based** access strategy, access control rules are defined directly on individual records (ledger, domains, wallets, etc.). The authorization layer checks permissions by following the hierarchy:
```jsx
record → ledger → server
```
This is the traditional approach where each record can define its own access rules, including ledger and domains - which applies to children records.
Policy-Based Access Strategy (Recommended) [#policy-based-access-strategy-recommended]
In **policy-based** access strategy, access control is managed globally through policies without requiring attachment to specific records. The authorization layer evaluates active policies globally and ignores access rules defined directly on individual records.
```jsx
active policies → server
```
**Key Differences:**
* **Record-based**: Access rules are attached to individual records
* **Policy-based**: Access rules are defined in global policies
* **Migration**: Ledgers can be migrated from record-based to policy-based, but not vice versa
* **Policy evaluation**: Policy-based ledgers only consider active policies with `schema: 'access'`
Access Rules and Policies [#access-rules-and-policies]
An access rule can be either a simple rule or a reference to a policy. See [About Security Policies](/ledger/securing-the-ledger/about-policies) for more details about policies.
Access permission rules follow the following format.
```tsx
type Rule: {
/**
* Defines which action the access rule is assigned to
*/
action: AccessAction
/**
* Defines which ledger record class the access rule is assigned to
*/
record?: AccessRecord
/**
* Define conditions that the subscriber of the request body
* must follow to grant access.
*/
signer?: AccessSigner
/**
* Define claims and metadata about the JWT token to grant access
*/
bearer?: AccessBearer
}
type AccessPolicy: {
/**
* Attaches a policy to the access rule
*/
policy: string
}
type AccessRule = Rule | AccessPolicy
```
Where
```tsx
type AccessAction:
'any' | // Applies to all actions below
'access' | // Defines permissions for accessing child records of a record
'create' | // Defines permissions for creating a record
'read' | // Defines permissions for reading a record
'drop' | // Defines permissions for dropping a record
'update' | // Defines permissions for updating a record
'lookup' | // Defines permissions for looking up from a record
'assign-signer' | // Defines permissions for assigning a signer to a circle
'remove-signer' | // Defines permissions for unassigning a signer from a circle
'issue' | // Defines permissions for issuing a symbol
'destroy' | // Defines permissions for destroying a symbol
'spend' | // Defines permissions for spending from a wallet
'limit' // Defines permissions for limiting a wallet
```
The `create` action is not valid when defining access to a specific record - record level rules - since the record already exists. Access rules with `create` action are defined on parent records, usually this is ledger or server.
`access` is also not valid for records without children, it should be defined either at server or ledger levels. See the section about [child records access constraints](/ledger/securing-the-ledger/about-authorization#child-record-access-constraints)
```tsx
type AccessRecord:
'any' | // Applies all records below
'server' | // Defines constraints to access the server
'ledger' | // Defines constraints to perform actions on ledgers
'signer' | // Defines constraints to perform actions on signers
'symbol' | // Defines constraints to perform actions on symbols
'wallet' | // Defines constraints to perform actions on wallets
'intent' | // Defines constraints to perform actions on intents
'intent-proof' | // Defines constraints to perform actions on intent proofs
'effect' | // Defines constraints to perform actions on effects
'bridge' | // Defines constraints to perform actions on bridges
'circle' | // Defines constraints to perform actions on circles
'circle-signer' | // Defines constraints to perform actions on circle signers
'policy' | // Defines constraints to perform actions on policies
'schema' | // Defines constraints to perform actions on schemas
'anchor' | // Defines constraints to perform actions on anchors
'domain' // Defines constraints to perform actions on domains
```
Access constraints must be defined respecting the hierarchy of server > ledger > record which means:
* The `server` record rule cannot be set on ledger or record level rules
* The `ledger` record rule cannot be set on ledger or record level rules
* The property `record` should me omitted or defined with the same value of the record's type when defining access control on record level i.e it's not allowed to define access rules to manage `symbol` within a `wallet` record.
The property `record` can be omitted when defining access rules, and in this case it indicates the access rule is intended to be applied to record which defines the access rule.
```tsx
type CircleConstraint = string
type CircleAggregation = {
$in: Array
}
type AccessCircle = CircleConstraint | CircleAggregation
enum RecordOwnership {
Creator = 'creator'
}
type AccessSigner = SignerConstraint | SignerAggregation
type SignerConstraint = {
handle?: string // defines constraints for signer handle
format?: string // defines constraints for signer format
public?: string // defines constraints for signer public key
$circle?: AccessCircle // defines constraints for signer circles
schema?: string // defines constraints for signer schema
$record?: RecordOwnership // defines constraints for the relationship between
// the signer and the target record
$ledger?: RecordOwnership // defines constraints for the relationship between
// the signer and the active ledger
}
type SignerAggregation = {
$in: Array // signature must respect at least
// one of the constraints defined in the list
}
/**
* @example
*
* Defining a signer aggregation rules which
* requires the signature of 'owner' signer or
* a key pair with` '1bZhhSgwDZ5C9pXsD2Q79A7rhxAZPBM3912G+lW/xIU='
* public key.
*/
{
...,
signer: {
$in: [
{
handle: 'owner',
},
{
public: '1bZhhSgwDZ5C9pXsD2Q79A7rhxAZPBM3912G+lW/xIU='
}
]
}
}
/**
* @example
*
* Defining a signer rule which requires the signature
* of a signer from circle 'admin'
*/
{
...,
signer: {
$circle: 'admin'
}
}
/**
* @example
*
* Defining a signer rule which requires the signature
* of the record creator
*/
{
...,
signer: {
$record: 'creator'
}
}
/**
* @example
*
* Defining a signer rule which requires the signature
* of the ledger creator
*/
{
...,
signer: {
$ledger: 'creator'
}
}
```
`signer` property is only valid for mutations, since a GET Http request doesn't have a body.
Since a record can hold multiple signatures, these access rules defines that **at least one of the signers must fulfill**.
```tsx
type AccessBearer = {
/**
* Defines access rule regarding the issuer of bearer token
*
* @example company.org
*/
iss?: string
/**
* Defines access rule regarding the subject of bearer token
*
* @example admin
*/
sub?: string
/**
* Defines access rule regarding the audience of bearer token
*
* @example ledger
*/
aud?: string
/**
* Defines if the request hash is mandatory
*
* @example true
*/
hsh?: boolean
/**
* Defines the key required to verify the signature of bearer token
*
* @example WAweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd/aILprE=
*/
$signer?: SignerConstraint
} | BearerAggregation
/**
* @example
*
* Defining a bearer aggregation rule which
* requires the token to be signed by 'owner' signer or
* a key pair with` '1bZhhSgwDZ5C9pXsD2Q79A7rhxAZPBM3912G+lW/xIU='
* public key.
*/
{
...,
bearer: {
$in: [{
$signer: {
handle: 'owner'
}
}, {
$signer: {
public: '1bZhhSgwDZ5C9pXsD2Q79A7rhxAZPBM3912G+lW/xIU='
}
}]
}
}
/**
* @example
*
* Defining a bearer rule which requires the token to be signed
* by a signer from circle 'admin'
*/
{
...,
bearer: {
$signer: {
$circle: 'admin'
}
}
}
```
Both **AccessSigner** and **AccessBearer** are matcher objects. Please don’t confuse them with [Referenced Records](/ledger/structuring-data/referenced-records). Matcher objects are more flexible because the whole object can be matched instead of just handle.
Access Control Flow [#access-control-flow]
The access control flow depends on the ledger's access strategy:
Record-Based Access Flow [#record-based-access-flow]
Child record access constraints work as filters and describe minimum conditions required for accessing a child record. They can be set on any record that has child records in the ledger. Most common records like that are `ledger` and `server` records.
Those access constraints are checked before validating the actual `action` the user wants to perform, and it follows the hierarchy:
```json
server → ledger → record (top down approach)
```
Examples:
1. `user A` wants to `read` a `symbol` , so firstly it must fulfill requirements for accessing the `server` and then fulfill requirements to access the `ledger`. Only after those requirements are satisfied, the ledger will check if the `user A` can perform the `read` operation on the `symbol`.
2. `user B` wants to `create` a `ledger` , so it should have permissions to access the `server` first. After that the ledger will check if the user has permissions to `create` a `ledger`.
Policy-Based Access Flow [#policy-based-access-flow]
In policy-based ledgers, access control is simplified:
```json
server → active policies (global evaluation)
```
The system:
1. Checks server-level access constraints
2. Evaluates all active policies with `schema: 'access'` globally
3. Ignores access rules defined on individual records
4. Applies policies based on the target record type and domains
**Important for Policy-Based Ledgers:**
* Individual record access rules are **ignored**
* Only active policies with `schema: 'access'` are evaluated
* Policies can be domain-specific or global
* Policy status (`active`, `inactive`, `created`) determines enforcement
Examples: Record-Based Access Rules [#examples-record-based-access-rules]
Examples of child record access rules at server level (applies to both strategies):
```tsx
/**
* Defines constraints to access the server. To access the server the
* user must send a token signed by the specified key.
*/
{
action: 'access',
bearer: {
$signer: {
public: '1bZhhSgwDZ5C9pXsD2Q79A7rhxAZPBM3912G+lW/xIU='
}
}
}
/**
* Defines constraints to access a ledger. To access a ledger the
* user must send a token signed by the specified key.
*/
{
action: 'access',
record: 'ledger',
bearer: {
$signer: {
public: '1bZhhSgwDZ5C9pXsD2Q79A7rhxAZPBM3912G+lW/xIU='
}
}
}
/**
* Defines constraints to access any record. To access any record the
* user must send a token signed by the specified key.
*/
{
action: 'access',
record: 'any',
bearer: {
$signer: {
public: '1bZhhSgwDZ5C9pXsD2Q79A7rhxAZPBM3912G+lW/xIU='
}
}
}
```
Examples of child record access rules at ledger level (record-based ledgers):
```tsx
/**
* Defines constraints to access a ledger. To access the ledger the user
* must send a token signed by any signer.
*/
{
action: 'access',
bearer: {
$signer: {}
}
}
/**
* Defines constraint to access any wallet. The access any wallet the user
* must send a token signed by any signer.
*/
{
action: 'access',
record: 'wallet',
bearer: {
$signer: {}
}
}
```
Server level rules [#server-level-rules]
Server level rules are defined via an environment variable named `SERVER_ACCESS_RULES` set on ledger API.
Access to any record and action can be defined at server level since it's the root level of access control.
```tsx
/**
* server access rules that restrict server access to token-signed users
* and allow any signer to create ledger instances.
*/
[
{
"action": "access",
"bearer": {
"$signer": {}
}
},
{
"action": "create",
"record": "ledger",
"signer": {}
}
]
```
Ledger level rules [#ledger-level-rules]
Record-Based Ledgers [#record-based-ledgers]
In record-based ledgers, ledger level rules are defined in the `access` property when creating a ledger instance.
Access to any record can be defined at ledger level, except from `server`.
```tsx
/**
* Payload of a record-based ledger which allows every
* signer to perform any operation on any ledger record.
*/
{
"hash": "99dbff500c451a7480ce2dc5928875478cb47b2a358baeaf4064a60eadd897a5",
"data": {
"handle": "some_ledger",
"signer": "some_signer",
"config": {
"access.strategy": "record-based"
},
"access": [
{
"action": "any",
"record": "any",
"bearer": {
"$signer": {}
}
}
]
},
"meta":{
"proofs":[
{
"method": "ed25519-v2",
"public": "1bZhhSgwDZ5C9pXsD2Q79A7rhxAZPBM3912G+lW/xIU=",
"result": "lPZsbs+BlWnu5Y5SMWH8AflAFzfKIvfvCgQ2dxZHC6D0j91N5o6F90hiWe6B8JV4MqSYsfGTzb9Rpfz8ecSbAg==",
"digest": "3f294cf7533bf5c24675ad238fbfd235860fcc2bc02f8d666894726b7f4ce523",
"custom": {
"moment": "2023-02-20T21:42:10.279Z"
}
}
]
}
}
```
Policy-Based Ledgers [#policy-based-ledgers]
In policy-based ledgers, access control is managed through policies rather than ledger-level access rules. The ledger's `access` property may contain policy references, but individual record access rules are ignored.
```tsx
/**
* Payload of a policy-based ledger that uses policies for access control
*/
{
"hash": "88dbff500c451a7480ce2dc5928875478cb47b2a358baeaf4064a60eadd897a5",
"data": {
"handle": "policy_ledger",
"signer": "some_signer",
"config": {
"access.strategy": "policy-based"
},
"access": [
{
"action": "access"
},
{
"policy": "ledger-owner"
},
{
"policy": "team-access"
}
]
},
"meta":{
"proofs":[
{
"method": "ed25519-v2",
"public": "1bZhhSgwDZ5C9pXsD2Q79A7rhxAZPBM3912G+lW/xIU=",
"result": "lPZsbs+BlWnu5Y5SMWH8AflAFzfKIvfvCgQ2dxZHC6D0j91N5o6F90hiWe6B8JV4MqSYsfGTzb9Rpfz8ecSbAg==",
"digest": "3f294cf7533bf5c24675ad238fbfd235860fcc2bc02f8d666894726b7f4ce523",
"custom": {
"moment": "2023-02-20T21:42:10.279Z"
}
}
]
}
}
```
Record level rules [#record-level-rules]
**Record-based ledgers only**: Record level rules are only enforced in ledgers with `access.strategy` set to `record-based`. In policy-based ledgers, these rules are ignored in favor of global policy evaluation.
Record level rules are defined in the `access` property when creating a record.
It can be used to define access to `read` and `update` the target record.
```tsx
/**
* Payload of a signer which can be updated and read by the signer who
* created the record. (Only enforced in record-based ledgers)
*/
{
"hash":"914816628f3481e57a246d4906b90e8b0125fb0f508dd24b5f1a849545f2a5d1",
"data":{
"handle": "some_signer",
"public": "1bZhhSgwDZ5C9pXsD2Q79A7rhxAZPBM3912G+lW/xIU=",
"format": "ed25519-raw",
"access": [{
"action": "read",
"bearer": {
"$signer": {
public: "1bZhhSgwDZ5C9pXsD2Q79A7rhxAZPBM3912G+lW/xIU="
}
}
}, {
"action": "update",
"signer": "1bZhhSgwDZ5C9pXsD2Q79A7rhxAZPBM3912G+lW/xIU=",
"bearer": {
"$signer": {
public: "1bZhhSgwDZ5C9pXsD2Q79A7rhxAZPBM3912G+lW/xIU="
}
}
}]
},
"meta": {
"proofs": [
{
"method": "ed25519-v2",
"public": "1bZhhSgwDZ5C9pXsD2Q79A7rhxAZPBM3912G+lW/xIU=",
"result": "lPZsbs+BlWnu5Y5SMWH8AflAFzfKIvfvCgQ2dxZHC6D0j91N5o6F90hiWe6B8JV4MqSYsfGTzb9Rpfz8ecSbAg==",
"digest": "3f294cf7533bf5c24675ad238fbfd235860fcc2bc02f8d666894726b7f4ce523",
"custom": {
"moment": "2023-02-20T21:42:10.279Z"
}
}
]
}
}
```
Policy-Based Access Control [#policy-based-access-control]
In policy-based ledgers, access control is managed through global policies instead of individual record access rules. This provides a centralized approach to access management.
Policy Structure [#policy-structure]
Policies with `schema: 'access'` contain access rules that are evaluated globally:
```tsx
/**
* Example access policy that grants wallet access to team members
*/
{
"handle": "team-wallet-access",
"schema": "access",
"record": "wallet",
"values": [
{
"action": "read",
"record": "wallet",
"bearer": {
"$signer": {
"$circle": "team-members"
}
}
},
{
"action": "update",
"record": "wallet",
"signer": {
"$circle": "team-leads"
}
}
]
}
```
Policy Status and Enforcement [#policy-status-and-enforcement]
Only **active** policies are enforced:
* **`created`**: Policy exists but is not enforced
* **`active`**: Policy is enforced and controls access
* **`inactive`**: Policy is temporarily disabled
```bash
# Activate a policy to enforce it
minka policy activate team-wallet-access
# Deactivate a policy to temporarily disable it
minka policy deactivate team-wallet-access
```
Domain-Specific Policies [#domain-specific-policies]
Policies can be applied globally or to specific domains:
```tsx
/**
* Policy that applies only to records in the "payments" domain
*/
{
"handle": "payments-access@payments",
"schema": "access",
"record": "wallet",
"values": [
{
"action": "any",
"signer": {
"$circle": "payment-operators"
}
}
]
}
```
Configuring Access Strategy [#configuring-access-strategy]
Setting Access Strategy [#setting-access-strategy]
The access strategy is configured in the ledger's configuration:
```tsx
{
"config": {
"access.strategy": "record-based" // Default
// OR
"access.strategy": "policy-based" // Centralized policy management
}
}
See [How to migrate ledger access strategy](/ledger/securing-the-ledger/migrate-access-strategy) for more details.
```
Migration Considerations [#migration-considerations]
**One-way migration**: Ledgers can be migrated from `record-based` to `policy-based`, but **cannot** be changed back. This prevents users from accidentally locking themselves out of the ledger.
When migrating to policy-based access:
1. **Plan your policies**: Design your policy structure before migration to ensure you won't lock yourself out of ledger.
2. **Test thoroughly**: Test the migration in a development environment
3. **Record rules ignored**: All existing record-level access rules are ignored in favor of global policies.
Choosing the Right Strategy [#choosing-the-right-strategy]
Use Record-Based When: [#use-record-based-when]
* You need granular, per-record access control
* Different records require different access patterns
* You prefer decentralized access management
Use Policy-Based When: [#use-policy-based-when]
* You want centralized access management
* You have complex, organization-wide access policies
* You require dynamic policy activation/deactivation
Related Documentation [#related-documentation]
* [How to Migrate Ledger Access Strategy](/ledger/securing-the-ledger/migrate-access-strategy) - Step-by-step guide to migrate from record-based to policy-based
* [How to Activate Access Policy](/ledger/securing-the-ledger/activate-policy) - Activate policies to enforce access control
* [How to Deactivate Access Policy](/ledger/securing-the-ledger/deactivate-policy) - Temporarily disable access policies
* [About Security Policies](/ledger/securing-the-ledger/about-policies) - Detailed information about policy structure and management
| Date | Changes |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 2025-01-13 | • Added comprehensive documentation for `policy-based` access strategy
• Explained differences between `record-based` and `policy-based` access
• Added policy structure, status management, and domain-specific policies
• Included migration considerations and strategy selection guidance |
| 2024-02-06 | Updated enums for AccessAction and AccessRecord |
| 2023-08-22 | Removed action `sign-intent` from enum |
| 2023-07-19 | Add `schema`, `$record` and `$ledger` signer properties. |
| 2023-06-14 | Describe `policy` referencing to access rules. |
| 2023-05-25 | • Describe `circle` access constraint for signers.
• Add `circle` to the list of records.
• Add `assign-signer` and `unassign-signer` to the list of actions. |
| 2023-04-17 | Added `sign-intent` action to enum |
| 2023-03-07 | Added `digest` as part of `signature` object.
• Refactored `signer.schema` → `signer.format`
• Added `custom.moment` to examples |
| 2023-02-27 | • Update `access.signer` schema and rename `access.bearer.$key` to `access.bearer.$signer` in order to reuse the same schema.
• Add `sign` access action.
• Rename `meta.signatures[*].schema` to `meta.signatures[*].method`. |
| 2023-01-16 | Initial version |
# Set Up Ledger Access Rules (/ledger/securing-the-ledger/ledger-access-rules)
Each ledger instance record supports attaching access rules through the `access` property. This property holds a list of permissions to signers and/or JWT tokens to access ledger records i.e wallets, symbols, etc, or the ledger instance itself. When making api requests using the SDK those access rules can be added when building the record.
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
}
})
const { ledger } = await sdk.ledger.init()
.data({
handle: 'test-ledger',
signer: 'ledger-signer',
access: [{
action: 'read',
record: 'wallet',
bearer: {
$signer: {
handle: 'bearer-key-value'
}
}
}]
})
.hash()
.sign([{ keyPair: yourKeyPair }])
.send()
```
Access data can be also added interactively through CLI:
```console
$ minka ledger create
? Handle: test-ledger
? Enter access content: Press to launch your preferred editor.
```
Then the default editor will open with an empty list `[]` as default value. This list must be fulfilled with the desired access rules
After confirming those permissions, a summary of the ledger instance as well as a confirmation question will be prompt in order to finish the operation without any mistake
```console
$ minka ledger create
? Handle: test-ledger
? Enter access content: Received
? Add custom data? No
? Signer: signerlocal
Ledger summary:
------------------------------------------------------------------------
Handle: test-ledger
Access rules:
#0
- Action: read
- Record: wallet
- Bearer:
- $signer:
- handle: owner
⚠️ To allow a new ledger instance to sign specific operations
when needed, a new signer will be created and attached to it.
This new signer will be owned and managed by the server.
Its public key and key format are going to be available
for verification of the signature.
? Sign this ledger instance using signer signerlocal? Yes
✅ Ledger instance created successfully:
Handle: test-ledger
Public: orr9rbdN4gtFLOqxk2rZsEZl4qj6IdOZ+R6PPaHTN0U=
Signer: 5y3Dv13lLQ4Ewd1WG0Pmi9gV/wc8Lh1UnidXMdMG0+w= (signerlocal)
```
See [About Authorization](/ledger/securing-the-ledger/about-authorization) for more details about this concept.
| Date | Changes |
| ----------------------- | --------------------------------------------------------------------------------------- |
| 2023-03-09 | Changed `ledger` → `sdk` and `instance` → `ledger` in order to match new SDK interface. |
| 2023-03-07 | Changed `bearer` action to `read` |
| 2023-02-27 | Refactored `signer.schema` → `signer.format` |
| 2023-02-27 | Rename `bearer.$key` access constraints to `bearer.$signer` |
| 2023-02-10 | SDK Options refactoring (`url` to `server` and `key` to `signer`) |
| 2023-01-24 | Initial version |
# Set Up Record Access Rules (/ledger/securing-the-ledger/record-access-rules)
Each record supports attaching access rules through the `access` property. This property holds a list of permissions to signers and/or JWT tokens to access it. When making api requests using the SDK those access rules can be added when building the record or changed when updating it.
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
}
})
const { wallet } = await sdk.wallet.init()
.data({
handle: 'test-wallet',
access: [{
action: 'read',
bearer: {
// Give permission to perform 'read' action on this wallet to requests
// with a bearer token which subject is owner@mail.com
sub: 'owner@mail.com'
}
}]
})
.hash()
.sign([{ keyPair: yourKeyPair }])
.send()
```
See [About Authorization](/ledger/securing-the-ledger/about-authorization) for more details.
| Date | Changes |
| ----------------------- | ----------------------------------------------------------------- |
| 2023-03-09 | Changed `ledger` → `sdk` |
| 2023-03-07 | Changed `bearer` action to `read` |
| 2023-02-27 | Refactored `signer.schema` → `signer.format` |
| 2023-02-10 | SDK Options refactoring (`url` to `server` and `key` to `signer`) |
| 2023-01-24 | Initial version |
# About Circles (/ledger/securing-the-ledger/about-circles)
What is a circle? [#what-is-a-circle]
A circle is a ledger record that represents a role or group of signers.
**Key benefits of circles:**
* **Simplified access control**: Grant permissions to entire groups rather than individual signers
* **Role-based organization**: Group signers by their function (e.g., banks, ACH processors, administrators)
* **Easier maintenance**: Add or remove signers from roles without changing access rules
```tsx
{
"data": {
"handle": "admin",
"custom": {
...
},
"access": [...],
},
"hash": "...",
"meta": {...}
}
```
Signer assignment [#signer-assignment]
The relation between circle and signer is `N:N` which means one circle can have multiple signers assigned to it as well as a signer can be part of many circles.
A circle can be useful for grouping signers and easing access rules management since access can be granted to circles instead of specific signer handles or public keys. See [About Authorization](/ledger/securing-the-ledger/about-authorization) for more details.
```tsx
{
"action": "create",
"record": "wallet",
"signer": {
"$circle": "admin"
}
}
```
| Date | Changes |
| ----------------------- | --------------- |
| 2023-05-25 | Initial version |
# Assign Signer to Circle (/ledger/securing-the-ledger/assign-signer-to-circle)
Ledger SDK allows users to create a circle and assign signers stored in the server to it.
Creating a circle [#creating-a-circle]
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
}
})
const { circle } = await sdk.circle.init()
.data({
handle: 'admin',
access: [{
action: 'any',
record: 'any'
}]
})
.hash()
.sign([{ keyPair: yourKeyPair }])
.send()
```
Creating a signer [#creating-a-signer]
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
}
})
const { signer } = await sdk.signer.init({
data: {
handle: 'test-signer',
public: '',
format: 'ed25519-raw',
access: [{
action: 'any',
record: 'any'
}]
})
.hash()
.sign([{ keyPair: yourKeyPair }])
.send()
```
Assigning the signer to the circle [#assigning-the-signer-to-the-circle]
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
}
})
const { signer } = await sdk.circle
.with('admin')
.signers
.init({
data: {
circle: 'admin',
signer: 'test-signer'
}
})
.hash()
.sign([{ keyPair: yourKeyPair }])
.send()
```
Useful links [#useful-links]
* See [About Circles](/ledger/securing-the-ledger/about-circles) for more details about circle concept.
* See [About Authorization](/ledger/securing-the-ledger/about-authorization) for more details about how to secure the ledger based on circles.
| Date | Changes |
| ----------------------- | --------------- |
| 2023-05-25 | Initial version |
# Drop Signer from Circle (/ledger/securing-the-ledger/drop-signer-from-circle)
Ledger SDK allows users to remove signers from a circle.
Removing a signer from a circle [#removing-a-signer-from-a-circle]
See [How to assign a signer to a circle](/ledger/securing-the-ledger/assign-signer-to-circle)
Bellow is an example of how to remove signer `test-signer` from circle `admin` by using the Ledger SDK.
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
}
})
const { circle } = await sdk.circle
.with('admin')
.signers
.with('test-signer')
.drop()
.hash()
.sign([{ keyPair: yourKeyPair }])
.send()
```
Useful links [#useful-links]
* See [About Circles](/ledger/securing-the-ledger/about-circles) for more details about circle concept.
| Date | Changes |
| ----------------------- | --------------- |
| 2023-05-25 | Initial version |
# Circle-Based Access Rules (/ledger/securing-the-ledger/circle-based-access)
Circles can be used to ease the management of access rules. It is not necessary to provide individual signer handles or public keys, which can be hard to maintain in case of many signers in the server.
Ledger allows users to secure the ledger based on the circle of request participants - `jwt` token signer and record signatures.
See [How to assign a signer to a circle](/ledger/securing-the-ledger/assign-signer-to-circle).
Follow some examples of access rules that grants permissions to circles.
The examples below depict only some applicabilities of access rules. See [About Authorization](/ledger/securing-the-ledger/about-authorization) for a full overview about access constraints.
Granting access to circles for mutating records by Ledger SDK [#granting--access-to-circles-for-mutating-records-by-ledger-sdk]
Granting access to all the signers from circle `admin` to create `any` record in the ledger:
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
}
})
const { ledger } = await sdk.ledger.init()
.data({
handle: 'test-ledger',
signer: 'ledger-signer',
access: [{
action: 'create',
record: 'any',
signer: {
$circle: 'admin'
}
}]
})
.hash()
.sign([{ keyPair: yourKeyPair }])
.send()
```
Granting access to all the signers from circle `admin` and/or `owner` to create any record in the ledger:
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
}
})
const { ledger } = await sdk.ledger.init()
.data({
handle: 'test-ledger',
signer: 'ledger-signer',
access: [{
action: 'create',
record: 'any',
signer: {
$circle: {
$in: ['admin', 'owner']
}
}
}]
})
.hash()
.sign([{ keyPair: yourKeyPair }])
.send()
```
Granting access to all the signers from circle `owner` to update wallets in the ledger:
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
}
})
const { ledger } = await sdk.ledger.init()
.data({
handle: 'test-ledger',
signer: 'ledger-signer',
access: [{
action: 'update',
record: 'wallet',
signer: {
$circle: 'owner'
}
}]
})
.hash()
.sign([{ keyPair: yourKeyPair }])
.send()
```
Granting access to create any record for record signatures made by a signer from circle `admin` and token signed by a signer from circle `oauth0-signers`:
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
}
})
const { ledger } = await sdk.ledger.init()
.data({
handle: 'test-ledger',
signer: 'ledger-signer',
access: [{
action: 'create',
record: 'any',
bearer: {
$signer: {
$circle: 'oauth0-signers'
}
},
signer: {
$circle: 'admin'
}
}]
})
.hash()
.sign([{ keyPair: yourKeyPair }])
.send()
```
Granting access to all the signers from circle `owner` to update a symbol:
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
}
})
const { ledger } = await sdk.symbol.init()
.data({
handle: 'test-ledger',
factor: 100,
access: [{
action: 'update',
signer: {
$circle: 'owner'
}
}]
})
.hash()
.sign([{ keyPair: yourKeyPair }])
.send()
```
Granting access to all the signers from circle `admin` and/or `owner` to update a symbol:
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
}
})
const { ledger } = await sdk.symbol.init()
.data({
handle: 'test-ledger',
factor: 100,
access: [{
action: 'update',
signer: {
$circle: {
$in: ['admin', 'owner']
}
}
}]
})
.hash()
.sign([{ keyPair: yourKeyPair }])
.send()
```
Granting access to circles to read records by Ledger SDK [#granting-access-to-circles-to-read-records-by-ledger-sdk]
Granting access to all the signers from circle `admin` to read any record in the ledger.
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
}
})
const { ledger } = await sdk.ledger.init()
.data({
handle: 'test-ledger',
signer: 'ledger-signer',
access: [{
action: 'read',
record: 'any',
bearer: {
$signer: {
$circle: 'admin'
}
}
}]
})
.hash()
.sign([{ keyPair: yourKeyPair }])
.send()
```
Granting access to all the signers from circle `owner` to read wallets in the ledger:
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
}
})
const { ledger } = await sdk.ledger.init()
.data({
handle: 'test-ledger',
signer: 'ledger-signer',
access: [{
action: 'read',
record: 'wallet',
bearer: {
$signer: {
$circle: 'owner'
}
}
}]
})
.hash()
.sign([{ keyPair: yourKeyPair }])
.send()
```
Granting access to all the signers from circle `admin` and/or `owner` to read any record in the ledger:
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
}
})
const { ledger } = await sdk.ledger.init()
.data({
handle: 'test-ledger',
signer: 'ledger-signer',
access: [{
action: 'read',
record: 'any',
bearer: {
$signer: {
$circle: {
$in: ['admin', 'owner']
}
}
}
}]
})
.hash()
.sign([{ keyPair: yourKeyPair }])
.send()
```
Granting access to all the signers from circle `owner` to read a single symbol:
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
}
})
const { ledger } = await sdk.symbol.init()
.data({
handle: 'test-ledger',
factor: 100,
access: [{
action: 'read',
bearer: {
$signer: {
$circle: 'owner'
}
}
}]
})
.hash()
.sign([{ keyPair: yourKeyPair }])
.send()
```
Granting access to all the signers from circles `admin` and/or `owner` to read a single symbol:
```tsx
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
server: '',
signer: {
format: 'ed25519-raw',
public: ''
}
})
const { ledger } = await sdk.symbol.init()
.data({
handle: 'test-ledger',
factor: 100,
access: [{
action: 'read',
bearer: {
$signer: {
$circle: {
$in: ['admin', 'owner']
}
}
}
}]
})
.hash()
.sign([{ keyPair: yourKeyPair }])
.send()
```
Useful links [#useful-links]
* See [About Circles](/ledger/securing-the-ledger/about-circles) for more details about circle concept.
* See [About Authorization](/ledger/securing-the-ledger/about-authorization) and [About Authentication](/ledger/securing-the-ledger/about-authentication) for more details about security.
| Date | Changes |
| ----------------------- | --------------- |
| 2023-05-25 | Initial version |
# About Domains (/ledger/securing-the-ledger/about-domains)
What is a domain [#what-is-a-domain]
Domains are namespaces inside ledger, allowing to group all other ledger records and inherit access rules inside this scoped namespace.
Domains exist as a record and are optional. If no domain exists the users can assign ledger records to any domain, but once the first domain is created, ledger starts enforcing domains rules. This means that the domain being assigned should exist as a record and the user should have access to create records inside that domain.
Defining domains creates restrictions that apply only to records created after it’s definition. Any record created while domains were not defined will still be valid and won’t be automatically migrated.
The root domain [#the-root-domain]
When a record does not belong to any specific domain, we say that it belongs to the “root” domain. When querying the domain of a record that is in the root domain, it will be undefined, this is the default behavior of the ledger.
Domain-Record assignation [#domain-record-assignation]
Currently domain of a record can only be assigned during creation and can’t be updated later. There are two methods for defining the domain of a record:
Implicit assignation (With handle) [#implicit-assignation-with-handle]
When creating a record, a domain can be assigned by appending `@` at the end of the handle. For example, creating a wallet with handle `treasury` will assign it to root domain, but creating a wallet with handle `treasury@minka.io` will assign the wallet to the domain `minka.io`
Explicit assignation (With proof) [#explicit-assignation-with-proof]
Domain can also be explicitly assigned by sending the `domain` property as part of the proof’s custom data when signing the record. And example using ledger SDK would be:
```tsx
sdk.wallet
.init()
.data({
handle: 'treasury', // No domain is required in the handle
access: [...],
})
.hash()
.sign([
{
keyPair: signerKeyPair,
custom: {
domain: 'minka.io',
}
}
])
```
Handle uniqueness still applies for different domains inside the same ledger. This means that wallet “treasury” can’t exist with the same handle in different domains. This includes root domain.
Explicit assignation have preference over implicit assignation, that means if the record’s handle is `domain-a` but creation is signed with `domain: domain-b` then the record will be created inside `domain-b`
Records that can be assigned to a domain [#records-that-can-be-assigned-to-a-domain]
* Anchors
* Bridges
* Circles
* Domains (To create sub domains)
* Effects
* Intents
* Policies
* Reports
* Schemas
* Signers
* Symbols
* Wallets
Querying a record’s domain [#querying-a-records-domain]
The domain of a record is available as part of it’s `metadata`, this means you can query it from the `meta` property of the response when reading it.
```tsx
const { meta } = await sdk.wallet.read('')
const domain = meta.domain
```
Metadata can’t be modified in update operations
Domain security [#domain-security]
When trying to perform write or read operations against a record, the security rules of it’s domain are checked. Access rules are inherited from the root to the domains and subdomains below it. This means that any access rules that grant access to perform an operation in the top level, will also allow you to perform this same operation in a lower level.
* Inheritance diagram
* Sample scenario
Given the scenario where we have access rules at top level that allows signer `admin` to perform any operation, then in the domain `domainA` we have access rules that allow the signer `admin@domainA` to perform any operation and in the domain `domainC` we have access rules that allow the signer `admin@domainC` to perform any operation, this is a comparison table of resulting permissions:
| Permission/User | admin | admin\@domainA | admin\@domainC |
| --------------------------------- | ----- | -------------- | -------------- |
| Operations at root level (Ledger) | 🟢 | 🔴 | 🔴 |
| Operations at domainA level | 🟢 | 🟢 | 🔴 |
| Operations at domainC level | 🟢 | 🟢 | 🟢 |
Define domain access rules [#define-domain-access-rules]
Defining access rules for the domain is similar to defining the access rules for the ledger. In this case, the access rules or policies should be set in the `access` property of the `domain` record.
For example, to allow the signer with the public key `my-public-key` to perform any operation inside the domain, you can define the access rules like this (Example using ledger SDK):
```tsx
sdk.domain
.init()
.data({
handle: 'minka.io',
access: [
{
action: AccessAction.Any,
record: AccessRecord.Any,
signer: {
public: 'my-public-key',
}
}
],
})
```
Read also [About Authorization](/ledger/securing-the-ledger/about-authorization)
| Date | Changes |
| ----------------------- | --------------- |
| 2024-11-14 | Initial version |
# About Security Policies (/ledger/securing-the-ledger/about-policies)
What is a security policy? [#what-is-a-security-policy]
A security policy is a ledger record that represents a set of access rules that can be attached to records.
```tsx
{
"data": {
"handle": "bank",
"schema": "access",
"record": "wallet",
"values": [{
"action": "update",
"signer": {
"$circle": "bank-circle"
}
}, {
"action": "read",
"bearer": {
"$signer": {
"$circle": "bank-circle"
}
}
}],
"custom": {
...
},
"access": [...],
},
"hash": "...",
"meta": {...}
}
```
Policy Features [#policy-features]
Policies support several powerful features:
1. **Record Targeting**: Policies can be scoped to specific record types using the `record` field
2. **Extension**: Policies can extend other policies using the `extend` field, inheriting their rules
3. **Schema Validation**: Policies use schemas to validate their structure and rules
Best Practices [#best-practices]
When working with policies:
1. Start with restrictive policies and gradually add permissions as needed
2. Create your own default policy as a base for common access patterns
3. Leverage policy extension to build on existing rules
4. Create specialized policies for specific use cases
Policy (as well as access rules in general) can be applied at different levels:
* **Server level** - Applied via server configuration, affects all ledgers
* **Ledger level** - Applied to all records within a specific ledger
* **Record level** - Applied to individual records only
**Security Consideration**: When designing access rules, be careful with ledger-level rules. Since Minka uses an additive authorization model, permissive rules at the ledger level can grant access even when individual records have restrictive rules. Always design access rules with the principle of least privilege in mind.
Targeting records - record [#targeting-records---record]
To create a policy it is necessary to define the target class of record which can reuse it and it's done with `record` and `filter` properties.
Below, there are some examples of policies that defines security rules to be used by ledger records.
```tsx
{
"data": {
"handle": "signer-management",
"record": "signer",
"schema": "access",
"values": [{
"action": "update",
"signer": {
"$circle": "owner"
}
}, {
"action": "read",
"bearer": {
"$signer": {
"$circle": "owner"
}
}
}],
"custom": {
...
},
"access": [...],
},
"hash": "...",
"meta": {...}
}
```
```tsx
{
"data": {
"handle": "record-factory",
"record": "any",
"schema": "access",
"values": [{
"action": "create",
"signer": {
"$circle": "owner"
}
}],
"custom": {
...
},
"access": [...],
},
"hash": "...",
"meta": {...}
}
```
```tsx
{
"data": {
"handle": "wallet-reader",
"record": "wallet",
"schema": "access",
"filter": {
"schema": "bank-wallet"
},
"values": [{
"action": "read",
"signer": {
"$circle": "bank"
}
}],
"custom": {
...
},
"access": [...],
},
"hash": "...",
"meta": {...}
}
```
Extending policies - extend [#extending-policies---extend]
Policies can be extended, which means that one policy can reuse values of an existing policy by using the property `extend`
```tsx
// Policy "reader"
{
"data": {
"handle": "reader",
"record": "any",
"schema": "access",
"values": [{
"action": "read",
"signer": {
"$circle": "admin"
}
}],
"custom": {
...
},
"access": [...],
},
"hash": "...",
"meta": {...}
}
// Policy "wallet-reader"
{
"data": {
"handle": "wallet-reader",
"extend": "reader",
"record": "wallet",
"values": [{
"action": "read",
"signer": {
"$circle": "bank"
}
}],
"custom": {
...
},
"access": [...],
},
"hash": "...",
"meta": {...}
}
```
Adding filter to policy values - filter [#adding-filter-to-policy-values---filter]
Policy values accepts `filter`, which is used to filter to which records the value applies. Filter can be defined with any content, and its value is used to match properties of a record `data`.
```tsx
// Symbol "usd"
{
"data": {
"handle": "usd",
"factor": 100,
"schema": "fiat",
...
},
"hash": "...",
"meta": {...}
}
// Symbol "bitcoin"
{
"data": {
"handle": "bitcoin",
"factor": 100000000,
"schema": "crypto",
...
},
"hash": "...",
"meta": {...}
}
// Policy "symbol-reader"
{
"data": {
"handle": "symbol-reader",
"record": "symbol",
"schema": "access",
"values": [{
"action": "read",
"signer": {
"$circle": "bank"
},
"filter": {
"schema": "fiat"
},
}, {
"action": "read",
"signer": {
"$circle": "exchange"
},
"filter": {
"schema": "crypto"
}
}],
"custom": {
...
},
"access": [...],
},
"hash": "...",
"meta": {...}
}
```
Attaching built-in functions to policy values - invoke [#attaching-built-in-functions-to-policy-values---invoke]
Policy values accepts `invoke`, which can be used to attach built-in functions to policies.
Follow the functions implemented and their respective records.
| **RECORD** | **FUNCTION NAME** | **Description** |
| ---------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| wallet | wallet.canSpendAllChangedRouteTargets | A route of action `forward` or `debit` can be added to a wallet only if the signer has access to `spend` the target wallet of the route. |
| intent | intent.canReadAnyClaimWallet | An intent can be read only if the user is allowed to read any of the wallets involved in any claim of this intent as `source` or `target`. |
| intent | intent.canReadAnyClaimWalletInThread | An intent can be read only if the user is allowed to read any of the wallets involved in any claim of the thread that this intent is part of - as `source` or `target`. For intent threads with a single intent, this function has the same effect of `intent.canReadAnyClaimWallet` |
| intent | intent.canSpendEveryClaimWallet | Intents can be created only if the signer has access to `spend` all the participant wallets of this intent - as `source` and `target` . |
```tsx
// Policy "wallet-mutation"
{
"data": {
"handle": "wallet-mutation",
"record": "wallet",
"schema": "access",
"values": [{
"action": "create",
"invoke": "wallet.canSpendAllChangedRouteTargets"
}, {
"action": "update",
"invoke": "wallet.canSpendAllChangedRouteTargets"
}],
"custom": {
...
},
"access": [...],
},
"hash": "...",
"meta": {...}
}
```
Policy usage [#policy-usage]
A policy can be attached to access rules of ledger records by referencing its `handle`
```tsx
{
"data": {
"handle": "bank-wallet",
"custom": {
...
},
"access": [{
"policy": "bank"
}],
},
"hash": "...",
"meta": {...}
}
```
Policies can be also reused alongside regular access rules.
```tsx
{
"data": {
"handle": "bank-wallet",
"custom": {
...
},
"access": [{
"policy": "bank"
}, {
"action": "spend",
"signer": {
"handle": "treasury"
}
}],
},
"hash": "...",
"meta": {...}
}
```
Security policies are powerful since they centralize security rules and ease their management. See [About Authorization](/ledger/securing-the-ledger/about-authorization) for more details.
| Date | Changes |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2024-10-23 | Updated security policies built-in functions table to note that `debit` acts like `forward` when it comes to access rights during creation (has to have access to spend from the route target) |
| 2024-07-15 | • Moved `About Policies` to `About Security Policies`.
• Add `schema: access` to security policies. |
| 2023-07-19 | Add properties `invoke` and `filter` of policy values |
| 2023-06-14 | Initial version |
# Activate a Policy (/ledger/securing-the-ledger/activate-policy)
How to activate an access policy [#how-to-activate-an-access-policy]
This guide explains how to activate access policies in your ledger using the Minka CLI. Activating a policy makes it enforceable and allows it to control access permissions within your ledger.
Overview [#overview]
Access policies in ledger can have different statuses:
* **`created`** - Policy exists but is not active
* **`active`** - Policy is enforced and controls access
* **`inactive`** - Policy is temporarily disabled
Only policies with the `access` schema can be activated or deactivated using the CLI commands.
Prerequisites [#prerequisites]
Before activating an access policy, ensure you have:
1. **Minka CLI installed** and configured
2. **Active ledger connection** with appropriate permissions
3. **Signer access** to add proofs to a policy (action: `create`, record: `policy-proof`)
4. **Existing access policy** to activate
Step-by-Step Process [#step-by-step-process]
1. Connect to Your Ledger [#1-connect-to-your-ledger]
First, ensure you're connected to the correct ledger:
```bash
# Connect to your server
minka server connect
# Select the target ledger
minka ledger select
```
2. Login with appropriate permissions [#2-login-with-appropriate-permissions]
Make sure you're authenticated with a signer that has policy management permissions:
```bash
# Login to the ledger
minka ledger login
```
3. List available policies [#3-list-available-policies]
Before activating, check which policies are available and their current status:
```bash
# List all policies
minka policy list
# Show specific policy details
minka policy show --verbose
```
4. Activate the policy [#4-activate-the-policy]
Use the activate command to make the policy active:
```bash
# Activate a specific policy
minka policy activate
? Signer:
? Signer password for:
✅ Policy activated successfully
```
For example:
```bash
# Activate a policy named "team-access"
minka policy activate team-access
```
5. Confirm activation [#5-confirm-activation]
The CLI will:
1. **Check the policy exists** and has the correct schema
2. **Verify current status** to ensure it's not already active
3. **Prompt for signer selection** if multiple signers are available
4. **Submit the activation request** to the ledger
5. **Display confirmation** once successful
6. Verify the policy status [#6-verify-the-policy-status]
After activation, verify the policy is now active:
```bash
# Check the policy status
minka policy show
```
Look for the `status` field in the output to confirm it shows `active`.
For more information on policy management, see:
* [How to Deactivate Access Policy](/ledger/securing-the-ledger/deactivate-policy)
* [How to Migrate Ledger Access Strategy](/ledger/securing-the-ledger/migrate-access-strategy)
# Deactivate a Policy (/ledger/securing-the-ledger/deactivate-policy)
How to deactivate an access policy [#how-to-deactivate-an-access-policy]
This guide explains how to deactivate access policies in your ledger using the Minka CLI. Activating a policy makes it enforceable and allows it to control access permissions within your ledger.
Overview [#overview]
Access policies in ledger can have different statuses:
* **`created`** - Policy exists but is not inactive
* **`active`** - Policy is enforced and controls access
* **`inactive`** - Policy is temporarily disabled
Only policies with the `access` schema can be activated or deactivated using the CLI commands.
Prerequisites [#prerequisites]
Before deactivating an access policy, ensure you have:
1. **Minka CLI installed** and configured
2. **Active ledger connection** with appropriate permissions
3. **Signer access** to add proofs to a policy (action: `create`, record: `policy-proof`)
4. **Existing access policy** to deactivate
Step-by-Step Process [#step-by-step-process]
1. Connect to your server and ledger [#1-connect-to-your-server-and-ledger]
First, ensure you're connected to the correct ledger:
```bash
# Connect to your server
minka server connect
# Select the target ledger
minka ledger select
```
2. Login with appropriate permissions [#2-login-with-appropriate-permissions]
Make sure you're authenticated with a signer that has policy management permissions:
```bash
# Login to the ledger
minka ledger login
```
3. List available policies [#3-list-available-policies]
Before deactivating, check which policies are available and their current status:
```bash
# List all policies
minka policy list
# Show specific policy details
minka policy show --verbose
```
4. Deactivate the policy [#4-deactivate-the-policy]
Use the deactivate command to make the policy inactive:
```bash
# Deactivate a specific policy
minka policy deactivate