Skip to content

Real-World Assets ​

Real-world assets (RWAs) model off-chain assets whose ownership or control is tracked on-chain. In Iroha, an RWA is a registered ledger lot with a generated identifier, an owner account, a quantity, business metadata, provenance, and optional lifecycle controls.

RWAs are different from numeric asset balances:

  • a numeric asset is a fungible balance held by an account
  • an NFT is a unique on-chain record with one owner
  • an RWA is a lot that can carry business metadata, quantity, holds, freezes, redemption state, provenance, and controller policy

Use RWAs when the ledger needs to represent a specific off-chain lot instead of only a fungible balance.

RWA Lot ​

An RWA lot contains:

  • id: the generated canonical RWA identifier, displayed as <hash>$<domain>
  • owned_by: the account that currently owns the lot
  • quantity: the outstanding quantity represented by the lot
  • spec: quantity specification, such as decimal scale
  • primary_reference: the main off-chain receipt, certificate, invoice, or registry reference
  • status: optional business status text
  • metadata: compact JSON fields used for business context and indexing
  • parents: source lots used to derive this lot
  • controls: controller accounts, controller roles, and enabled controller operations
  • is_frozen and held_quantity: lifecycle state enforced by the runtime

Keep the on-chain payload compact. Store large legal documents, inspection reports, and audit bundles outside the WSV, then put a digest, URI, SoraFS path, or manifest reference in RWA metadata.

Identifiers ​

RegisterRwa does not accept a caller-chosen id, and it does not accept an owner field. The transaction authority becomes the initial owned_by account, and the runtime generates the RwaId in the target domain.

The textual form of an RWA ID is:

text
<generated-hash>$<domain>

For example:

text
0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef$commodities.universal

Applications should store their business identifier in primary_reference or metadata, then discover the generated RwaId from RwaEvent::Created, FindRwas, /v1/rwas, or the explorer route set after the transaction commits.

Lifecycle ​

Common RWA workflows include:

OperationImplemented behavior
RegisterRwaCreate a generated-ID lot in a domain; the transaction authority becomes owned_by.
TransferRwaMove quantity to another account. A full transfer can change owned_by. A partial transfer creates a separate child lot with a generated ID.
HoldRwaReserve quantity. Requires a configured controller and hold_enabled.
ReleaseRwaRemove held quantity. Requires a configured controller and hold_enabled.
FreezeRwaBlock ordinary owner operations. Requires a configured controller and freeze_enabled.
UnfreezeRwaRe-enable ordinary owner operations. Requires a configured controller and freeze_enabled.
RedeemRwaPermanently subtract quantity from circulation. The owner or a controller may submit it when redeem_enabled is true.
MergeRwasCombine quantities from parent lots with the same domain and spec into a generated child lot.
ForceTransferRwaMove quantity through a controller flow. Requires a configured controller and force_transfer_enabled.
SetRwaControlsReplace the lot control policy. Requires the owner or a controller.
SetKeyValue<Rwa> / RemoveKeyValue<Rwa>Update lot metadata. Requires the owner or a controller; frozen lots require a controller.

There is no UnregisterRwa instruction in the current code. Retire an off-chain lot with RedeemRwa when the represented quantity is delivered, consumed, settled, or otherwise removed from circulation.

Metadata and Controls ​

Use metadata for compact facts that help applications identify and verify the lot:

  • asset class, issuer, custodian, or registry reference
  • warehouse, vault, ISIN, invoice, or certificate identifiers
  • content hashes for attestations and legal documents
  • SoraFS paths or manifest references for larger evidence bundles
  • maturity, jurisdiction, or compliance tags used by off-chain services

The implemented RwaControlPolicy has these fields:

json
{
  "controller_accounts": [],
  "controller_roles": [],
  "freeze_enabled": true,
  "hold_enabled": true,
  "force_transfer_enabled": false,
  "redeem_enabled": true
}

Controller accounts and roles can perform only the operations enabled by the corresponding boolean flags. The current control payload contains controller identities and operation flags. Transfer allow-lists and nested transfers rules are outside this payload.

Queries, Events, and APIs ​

Use FindRwas to list registered RWA lots. Applications that need live updates can subscribe to Rwa data events for created, owner-changed, split, merged, redeemed, frozen, unfrozen, held, released, force-transferred, controls-changed, and metadata events.

Torii exposes chain-state routes such as /v1/rwas and /v1/rwas/query, plus explorer routes such as /v1/explorer/rwas and /v1/explorer/rwas/{rwa_id} when that route family is enabled. Generated clients should prefer the live /openapi document for the exact response shape exposed by a node.

Try It on Taira ​

Check whether public Taira currently has registered RWA lots:

bash
curl -fsS 'https://taira.sora.org/v1/rwas?limit=5' \
  | jq '{total, rwa_ids: [.items[].id]}'

List the RWA routes exposed by the live Taira OpenAPI document:

bash
curl -fsS https://taira.sora.org/openapi.json \
  | jq -r '.paths | keys[] | select(startswith("/v1/rwas") or startswith("/v1/explorer/rwas"))'

Empty items output is expected when no public lots have been registered yet. Registration, transfer, hold, freeze, and redemption are signed transactions.

Try It ​

The examples below use the Python SDK surfaces from Shared Setup. Replace the account IDs, private keys, and generated lot IDs with values from your own network before submitting a transaction.

Discover RWA API Routes ​

This read-only example asks a running Torii node which app-facing RWA routes are enabled:

python
from iroha_python import create_torii_client

client = create_torii_client("https://taira.sora.org")
openapi = client.request_json("GET", "/openapi", expected_status=(200,))

rwa_paths = sorted(
    path for path in openapi.get("paths", {}) if path.startswith("/v1/rwas")
)

for path in rwa_paths:
    print(path)

If the list is empty, the node may still support RWA instructions and queries through other Torii APIs, but it is not exposing the optional JSON route family.

Register a Warehouse Receipt ​

Use a draft when one business action should become one signed transaction. The business receipt number goes in primary_reference; the ledger ID is generated after the transaction commits.

python
from iroha_python import TransactionConfig, TransactionDraft

config = TransactionConfig(
    chain_id=CHAIN_ID,
    authority=alice,
    metadata={**TX_METADATA, "source": "rwa-docs"},
)

draft = TransactionDraft(config)
draft.register_rwa(
    {
        "domain": "commodities.universal",
        "quantity": "100",
        "spec": {"scale": 0},
        "primary_reference": "warehouse-receipt-001",
        "status": "active",
        "metadata": {
            "asset_class": "commodity",
            "commodity": "copper",
            "warehouse": "DXB-01",
            "inspection_report": "sorafs://reports/copper-001.json",
        },
        "parents": [],
        "controls": {
            "controller_accounts": [alice],
            "controller_roles": [],
            "freeze_enabled": True,
            "hold_enabled": True,
            "force_transfer_enabled": False,
            "redeem_enabled": True,
        },
    }
)

envelope = draft.sign_with_keypair(alice_pair)
client.submit_transaction_envelope_and_wait(envelope)

After the transaction commits, list generated RWA IDs. Chain-state routes expose the canonical IDs; use events or explorer detail routes when you need to match an ID back to primary_reference or metadata:

python
page = client.list_rwas_typed(limit=20, offset=0)

for lot in page.items:
    print(lot.id)

Explorer-enabled nodes can also return richer projections:

python
page = client.list_explorer_rwas_typed(domain="commodities.universal")

for lot in page.items:
    print(lot.id, lot.primary_reference, lot.owned_by, lot.quantity)

Transfer With a Temporary Hold ​

Use the generated RWA ID returned by the chain. This example assumes alice is the owner and is also configured as a controller with hold_enabled.

python
warehouse_lot_id = (
    "0123456789abcdef0123456789abcdef"
    "0123456789abcdef0123456789abcdef$commodities.universal"
)

draft = TransactionDraft(
    TransactionConfig(chain_id=CHAIN_ID, authority=alice, metadata=TX_METADATA)
)

draft.transfer_rwa(warehouse_lot_id, quantity="10", destination=bob)
draft.hold_rwa(warehouse_lot_id, quantity="5")

envelope = draft.sign_with_keypair(alice_pair)
client.submit_transaction_envelope_and_wait(envelope)

Submit ReleaseRwa after the off-chain process succeeds:

python
draft = TransactionDraft(
    TransactionConfig(chain_id=CHAIN_ID, authority=alice, metadata=TX_METADATA)
)
draft.release_rwa(warehouse_lot_id, quantity="5")

envelope = draft.sign_with_keypair(alice_pair)
client.submit_transaction_envelope_and_wait(envelope)

Add Controls and Audit Metadata ​

Controls and metadata are separate. Use controls for controller policy, and metadata for facts that applications or auditors need to display:

python
draft = TransactionDraft(
    TransactionConfig(chain_id=CHAIN_ID, authority=alice, metadata=TX_METADATA)
)

draft.set_rwa_controls(
    warehouse_lot_id,
    {
        "controller_accounts": [alice],
        "controller_roles": [],
        "freeze_enabled": True,
        "hold_enabled": True,
        "force_transfer_enabled": True,
        "redeem_enabled": True,
    },
)
draft.set_rwa_key_value(warehouse_lot_id, "auditor", "alice")
draft.set_rwa_key_value(
    warehouse_lot_id,
    "proof_hash",
    "sha256:2b1c7a4e...",
)

envelope = draft.sign_with_keypair(alice_pair)
client.submit_transaction_envelope_and_wait(envelope)

Redeem or Retire Quantity ​

Submit RedeemRwa after the represented off-chain asset is delivered, consumed, retired, or otherwise removed from circulation. This permanently subtracts the submitted quantity from the lot. The lot must have redeem_enabled. The signer must be the owner or a controller.

python
draft = TransactionDraft(
    TransactionConfig(chain_id=CHAIN_ID, authority=alice, metadata=TX_METADATA)
)
draft.redeem_rwa(warehouse_lot_id, quantity="1")

envelope = draft.sign_with_keypair(alice_pair)
client.submit_transaction_envelope_and_wait(envelope)

Freeze During Compliance Review ​

Submit FreezeRwa when an off-chain review must block ordinary owner operations. The signer must be a controller. The lot must have freeze_enabled.

python
draft = TransactionDraft(
    TransactionConfig(chain_id=CHAIN_ID, authority=alice, metadata=TX_METADATA)
)
draft.freeze_rwa(warehouse_lot_id)
draft.set_rwa_key_value(
    warehouse_lot_id,
    "review",
    {
        "status": "frozen",
        "reason": "custodian inventory check",
        "case_id": "OPS-2026-0042",
    },
)

envelope = draft.sign_with_keypair(alice_pair)
client.submit_transaction_envelope_and_wait(envelope)

Submit UnfreezeRwa after the review passes:

python
draft = TransactionDraft(
    TransactionConfig(chain_id=CHAIN_ID, authority=alice, metadata=TX_METADATA)
)
draft.unfreeze_rwa(warehouse_lot_id)
draft.set_rwa_key_value(
    warehouse_lot_id,
    "review",
    {"status": "cleared", "case_id": "OPS-2026-0042"},
)

envelope = draft.sign_with_keypair(alice_pair)
client.submit_transaction_envelope_and_wait(envelope)

Invoice Receivable ​

Represent an invoice as an RWA lot by storing the invoice number in primary_reference and metadata. After registration, use the generated ID for transfer and redemption.

python
draft = TransactionDraft(
    TransactionConfig(chain_id=CHAIN_ID, authority=alice, metadata=TX_METADATA)
)
draft.register_rwa(
    {
        "domain": "receivables.universal",
        "quantity": "50000",
        "spec": {"scale": 2},
        "primary_reference": "INV-2026-0007",
        "status": "issued",
        "metadata": {
            "asset_class": "invoice",
            "currency": "USD",
            "debtor": "example-buyer",
            "due_date": "2026-06-30",
            "document_hash": "sha256:4df4c8...",
        },
        "parents": [],
        "controls": {
            "controller_accounts": [alice],
            "controller_roles": [],
            "freeze_enabled": True,
            "hold_enabled": False,
            "force_transfer_enabled": False,
            "redeem_enabled": True,
        },
    }
)

envelope = draft.sign_with_keypair(alice_pair)
client.submit_transaction_envelope_and_wait(envelope)

When the receivable is financed or paid, use the generated invoice lot ID:

python
invoice_lot_id = (
    "fedcba9876543210fedcba9876543210"
    "fedcba9876543210fedcba9876543210$receivables.universal"
)

draft = TransactionDraft(
    TransactionConfig(chain_id=CHAIN_ID, authority=alice, metadata=TX_METADATA)
)
draft.transfer_rwa(invoice_lot_id, quantity="50000", destination=bob)

envelope = draft.sign_with_keypair(alice_pair)
client.submit_transaction_envelope_and_wait(envelope)

Redeem the represented amount after off-chain settlement:

python
draft = TransactionDraft(
    TransactionConfig(chain_id=CHAIN_ID, authority=bob, metadata=TX_METADATA)
)
draft.redeem_rwa(invoice_lot_id, quantity="50000")

envelope = draft.sign_with_keypair(bob_pair)
client.submit_transaction_envelope_and_wait(envelope)

Carbon Credit Retirement ​

Submit RedeemRwa to remove claimed carbon credits from circulation. Store the off-chain certificate or registry proof in metadata:

python
carbon_lot_id = (
    "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
    "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa$carbon.universal"
)

draft = TransactionDraft(
    TransactionConfig(chain_id=CHAIN_ID, authority=alice, metadata=TX_METADATA)
)
draft.redeem_rwa(carbon_lot_id, quantity="250")
draft.set_rwa_key_value(
    carbon_lot_id,
    "retirement_certificate",
    "sorafs://certificates/carbon-credit-2026-001-retired.json",
)

envelope = draft.sign_with_keypair(alice_pair)
client.submit_transaction_envelope_and_wait(envelope)

Merge Two Lots ​

Merge lots when two off-chain positions are consolidated. The parents must be in the same domain and use the same quantity spec. The runtime generates the child lot ID.

python
warehouse_lot_id_2 = (
    "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
    "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb$commodities.universal"
)

draft = TransactionDraft(
    TransactionConfig(chain_id=CHAIN_ID, authority=alice, metadata=TX_METADATA)
)
draft.merge_rwas(
    {
        "parents": [
            {"rwa": warehouse_lot_id, "quantity": "40"},
            {"rwa": warehouse_lot_id_2, "quantity": "60"},
        ],
        "primary_reference": "warehouse-receipt-003",
        "status": "merged",
        "metadata": {
            "asset_class": "commodity",
            "commodity": "copper",
            "warehouse": "DXB-01",
            "merge_reason": "same custodian and quality grade",
        },
    }
)

envelope = draft.sign_with_keypair(alice_pair)
client.submit_transaction_envelope_and_wait(envelope)

For the full Python transaction example, see Real-World Assets.