Perform Transactions
Obligation Registry support (including the bill-of-exchange profile) is currently in beta. APIs, contract addresses, and behavior may change before the stable release. Use on testnet only and do not rely on this feature in production.
Background
Some documents carry an obligation that changes state over their life, not just an owner that changes hands. The party expected to perform can accept the obligation or reject it; once it has been satisfied, it is discharged. None of "accepted", "rejected" or "discharged" exist in a plain ownership registry; they are a real business status, not just a change of custody. The Obligation Registry adds exactly this status layer on top of the beneficiary/holder custody model.
A bill of exchange is the canonical example: the drawee accepts (becoming obligated to pay) or rejects the bill, and it is discharged once the payee is paid. This guide uses a bill of exchange to make the actions concrete, but the same mechanics apply to any instrument with an accept/reject/discharge lifecycle.
TrustVC Contribution
The Obligation Registry represents this with a beneficiary / holder custody model, plus a status field that only the current holder and beneficiary — not just anyone — are allowed to move. This is done through the ObligationEscrow smart contract.
ObligationEscrow
During minting, the Obligation Registry (TrustVCToken) creates and assigns an ObligationEscrow as the owner of that token, which then holds it in custody on behalf of the beneficiary and holder.
Beneficiary and Holder
The beneficiary holds the underlying rights to the document; the holder is the party currently in possession. If beneficiary and holder are the same party, that party can transfer the token directly in one transaction (immediate endorsement). If they're different parties, the beneficiary prepares a remote endorsement — nominating a new beneficiary — which the holder must then execute for it to take effect. The holder alone can transfer holdership without any nomination step.
Status
This is the part that's new. Every ObligationEscrow also tracks a status:
| Action | Caller | From → To | Effect |
|---|---|---|---|
accept | Holder (beneficiary ≠ holder required) | Issued → Accepted | Title stays active |
reject | Holder (beneficiary ≠ holder required) | Issued → Rejected | Auto-closes and burns the title |
discharge | Beneficiary | Accepted → Discharged | Auto-closes and burns the title |
reject and discharge close the title automatically, in the same transaction — the token is handed back to the registry and burned. There is no separate burn step to run afterwards for these two paths.
accept and reject both require beneficiary ≠ holder — a single wallet holding both roles cannot accept or reject on its own. discharge has no such requirement; the beneficiary can discharge regardless of who the holder is.
Return to Issuer
Return to issuer requires a single wallet holding both the current beneficiary and holder roles, and can be called at any point while the escrow is active — it does not require the title to already be Rejected or Discharged, and it does not change status.
Executing Transactions on the Obligation Registry
To execute these transactions, you can use either the Command Line Interface (CLI) or interact with the smart contract programmatically through code.
1) Using Code
Installation
npm install --save @trustvc/trustvc@beta
Usage
To use the package, you will need to provide your own Web3 provider or signer (if you are writing to the blockchain).
Full function reference: TrustVC SDK README — Obligation Registry (BoE).
Mint (Issue) a Document
Minting sets the document's status to Issued and creates its ObligationEscrow, which takes ownership of the newly minted token.
encryptionKeyId is whatever key you use to encrypt this remark -- it must be the exact same value later passed as keyId to fetchEndorsementChain, or the remark won't decrypt. The trustvc CLI always uses the signed document's own id for this (see Fetch Endorsement Chain for the read side), which is why we set it that way below; calling the SDK directly, you can use any string as long as every write and read for this document use the same one.
import { mintObligationRegistry } from "@trustvc/trustvc";
const encryptionKeyId = signedDocument.id; // matches the convention used by the CLI and by fetchEndorsementChain
await (
await mintObligationRegistry(
{ obligationRegistryAddress },
issuerSigner,
{ beneficiaryAddress, holderAddress, tokenId: "1", remarks: "issued" },
{ chainId, id: encryptionKeyId },
)
).wait();
Accept a Document
import { acceptObligationRegistry } from "@trustvc/trustvc";
// encryptionKeyId here must be the same value used at mint (see above)
// Holder accepts — Issued → Accepted
await (
await acceptObligationRegistry(
{ obligationRegistryAddress, tokenId: "1" },
holderSigner,
{ remarks: "accepted" },
{ chainId, id: encryptionKeyId },
)
).wait();
Reject a Document
This is an alternative to accepting — once a document is Issued, the holder calls either accept or reject, not both (reject auto-closes and burns the title, so there is nothing left to accept afterwards).
import { rejectObligationRegistry } from "@trustvc/trustvc";
// encryptionKeyId here must be the same value used at mint (see above)
// Holder rejects — Issued → Rejected (auto-closes and burns)
await (
await rejectObligationRegistry(
{ obligationRegistryAddress, tokenId: "1" },
holderSigner,
{ remarks: "rejected" },
{ chainId, id: encryptionKeyId },
)
).wait();
Discharge a Document
import { dischargeObligationRegistry } from "@trustvc/trustvc";
// Beneficiary discharges — Accepted → Discharged (auto-closes and burns)
// encryptionKeyId here must be the same value used at mint (see above)
await (
await dischargeObligationRegistry(
{ obligationRegistryAddress, tokenId: "1" },
beneficiarySigner,
{ remarks: "paid in full" },
{ chainId, id: encryptionKeyId },
)
).wait();
Transfer of Beneficiary/Holder
Transferring beneficiary and holder uses the following SDK functions:
import {
transferBeneficiaryObligationRegistry,
transferHolderObligationRegistry,
transferOwnersObligationRegistry,
nominateObligationRegistry,
} from "@trustvc/trustvc";
transferBeneficiaryObligationRegistry transfers only the beneficiary and transferHolderObligationRegistry transfers only the holder. To transfer both in a single transaction, use transferOwnersObligationRegistry.
When the holder is different from the beneficiary, transferring the beneficiary requires a nomination first, via nominateObligationRegistry.
Reject Transfers of Beneficiary/Holder
Don't confuse this with Reject a Document. Rejecting a document rejects the obligation itself (Issued → Rejected, burns the title). Rejecting a transfer declines an appointment as beneficiary or holder; the token and its status are untouched, custody simply doesn't move to you.
import {
rejectTransferBeneficiaryObligationRegistry,
rejectTransferHolderObligationRegistry,
rejectTransferOwnersObligationRegistry,
} from "@trustvc/trustvc";
Rejection must occur as the very next action after being appointed as beneficiary and/or holder. If any other transaction happens first, it counts as implicit acceptance of the appointment.
The reject window is per appointment, not per address. Any action a newly appointed holder takes other than rejectTransferHolder… (including calling accept on the obligation) implicitly accepts their own appointment and closes their reject window only. It has no effect on future holders: if that holder later transfers holdership again (transferHolderObligationRegistry), the next holder gets a fresh reject window, even if it's the same address that accepted before.
Return Document to Issuer
import {
returnToIssuerObligationRegistry,
acceptReturnedObligationRegistry,
rejectReturnedObligationRegistry,
} from "@trustvc/trustvc";
// Dual role (beneficiary == holder) returns the title to the registry
// encryptionKeyId here must be the same value used at mint (see above)
await (
await returnToIssuerObligationRegistry(
{ obligationRegistryAddress, tokenId: "1" },
dualRoleSigner,
{ remarks: "returning to issuer" },
{ chainId, id: encryptionKeyId },
)
).wait();
// Issuer accepts the return (burn) ...
await acceptReturnedObligationRegistry(/* ... */);
// ... or rejects it, restoring the title to escrow
await rejectReturnedObligationRegistry(/* ... */);
Reading Status
import { getObligationRegistryStatus, getObligationEscrowTerminationReason, ownerOfObligationRegistry } from "@trustvc/trustvc";
const status = await getObligationRegistryStatus({ obligationRegistryAddress, tokenId: "1" }, provider);
2) Using CLI
Installation
npm install -g @trustvc/trustvc-cli@beta
You can also opt to use npx:
npx @trustvc/trustvc-cli@beta <arguments>
Note: Before minting, set
credentialStatus.obligationRegistryon your document (nottokenRegistry) to your deployed registry address, then sign it withtrustvc w3c-sign. Mint only accepts a signed document.
Mint document to the Obligation Registry
trustvc w3c-sign
trustvc obligation-registry mint
The CLI extracts the registry address, token ID, and network from the signed document, then prompts you for the beneficiary and holder addresses and your wallet.
Accept / Reject / Discharge
trustvc obligation-escrow accept
trustvc obligation-escrow reject
trustvc obligation-escrow discharge
Each prompts you for the document path, your wallet, and an optional remark.
Status and History
# Read-only — no wallet required
trustvc obligation-escrow status
trustvc obligation-escrow endorsement-chain
Transfers
trustvc obligation-escrow transfer-holder
trustvc obligation-escrow nominate-transfer-owner
trustvc obligation-escrow endorse-transfer-owner
trustvc obligation-escrow transfer-owner-holder
trustvc obligation-escrow reject-transfer-holder
trustvc obligation-escrow reject-transfer-owner
trustvc obligation-escrow reject-transfer-owner-holder
Return to Issuer
trustvc obligation-escrow return-to-issuer
trustvc obligation-escrow accept-return-to-issuer
trustvc obligation-escrow reject-return-to-issuer
return-to-issuer requires one wallet holding both the current beneficiary and holder roles. accept-return-to-issuer requires the registry's accepter role (burns the title); reject-return-to-issuer requires the restorer role (restores it to escrow).
Verify
trustvc verify
The trustvc verify command verifies obligation documents (including bills of exchange). It auto-detects which check to run from the document's credentialStatus.