From f7c0c785cb88235cb6c8efafbdd20be4c3512f17 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Mon, 27 Jul 2026 14:46:39 +0530 Subject: [PATCH 01/20] feat: add obligation escrow commands and types - Introduced new commands for managing BoE obligations, including accept, reject, transfer, and return functionalities. - Added types for obligation registry and escrow commands to enhance type safety and clarity. - Implemented shared utilities for prompting user inputs across obligation escrow commands. - Enhanced transaction handling with detailed logging and error management for better user experience. --- README.md | 364 +++++++++++++++++- src/commands/helpers.ts | 74 ++++ .../accept-return-to-issuer.ts | 51 +++ src/commands/obligation-escrow/accept.ts | 49 +++ src/commands/obligation-escrow/discharge.ts | 50 +++ .../endorse-transfer-owner.ts | 55 +++ src/commands/obligation-escrow/index.ts | 11 + .../nominate-transfer-owner.ts | 55 +++ .../reject-return-to-issuer.ts | 51 +++ .../reject-transfer-holder.ts | 50 +++ .../reject-transfer-owner-holder.ts | 50 +++ .../reject-transfer-owner.ts | 50 +++ src/commands/obligation-escrow/reject.ts | 49 +++ .../obligation-escrow/return-to-issuer.ts | 50 +++ src/commands/obligation-escrow/runTx.ts | 93 +++++ src/commands/obligation-escrow/shared.ts | 40 ++ src/commands/obligation-escrow/status.ts | 59 +++ .../obligation-escrow/transfer-holder.ts | 58 +++ .../transfer-owner-holder.ts | 65 ++++ src/commands/obligation-registry/deploy.ts | 145 +++++++ src/commands/obligation-registry/index.ts | 11 + src/commands/obligation-registry/mint.ts | 161 ++++++++ src/commands/verify-obligation.ts | 139 +++++++ src/types.ts | 33 ++ src/utils/index.ts | 3 + src/utils/obligation-document.ts | 68 ++++ .../accept-return-to-issuer.test.ts | 121 ++++++ .../commands/obligation-escrow/accept.test.ts | 85 ++++ .../obligation-escrow/discharge.test.ts | 92 +++++ .../endorse-transfer-owner.test.ts | 95 +++++ .../nominate-transfer-owner.test.ts | 95 +++++ .../reject-return-to-issuer.test.ts | 121 ++++++ .../reject-transfer-holder.test.ts | 92 +++++ .../reject-transfer-owner-holder.test.ts | 92 +++++ .../reject-transfer-owner.test.ts | 92 +++++ .../commands/obligation-escrow/reject.test.ts | 89 +++++ .../return-to-issuer.test.ts | 68 ++++ .../commands/obligation-escrow/status.test.ts | 71 ++++ .../obligation-escrow/transfer-holder.test.ts | 95 +++++ .../transfer-owner-holder.test.ts | 101 +++++ .../obligation-registry/deploy.test.ts | 135 +++++++ .../commands/obligation-registry/mint.test.ts | 158 ++++++++ tests/commands/verify-obligation.test.ts | 102 +++++ .../obligation/w3c-obligation-record.json | 116 ++++++ 44 files changed, 3703 insertions(+), 1 deletion(-) create mode 100644 src/commands/obligation-escrow/accept-return-to-issuer.ts create mode 100644 src/commands/obligation-escrow/accept.ts create mode 100644 src/commands/obligation-escrow/discharge.ts create mode 100644 src/commands/obligation-escrow/endorse-transfer-owner.ts create mode 100644 src/commands/obligation-escrow/index.ts create mode 100644 src/commands/obligation-escrow/nominate-transfer-owner.ts create mode 100644 src/commands/obligation-escrow/reject-return-to-issuer.ts create mode 100644 src/commands/obligation-escrow/reject-transfer-holder.ts create mode 100644 src/commands/obligation-escrow/reject-transfer-owner-holder.ts create mode 100644 src/commands/obligation-escrow/reject-transfer-owner.ts create mode 100644 src/commands/obligation-escrow/reject.ts create mode 100644 src/commands/obligation-escrow/return-to-issuer.ts create mode 100644 src/commands/obligation-escrow/runTx.ts create mode 100644 src/commands/obligation-escrow/shared.ts create mode 100644 src/commands/obligation-escrow/status.ts create mode 100644 src/commands/obligation-escrow/transfer-holder.ts create mode 100644 src/commands/obligation-escrow/transfer-owner-holder.ts create mode 100644 src/commands/obligation-registry/deploy.ts create mode 100644 src/commands/obligation-registry/index.ts create mode 100644 src/commands/obligation-registry/mint.ts create mode 100644 src/commands/verify-obligation.ts create mode 100644 src/utils/obligation-document.ts create mode 100644 tests/commands/obligation-escrow/accept-return-to-issuer.test.ts create mode 100644 tests/commands/obligation-escrow/accept.test.ts create mode 100644 tests/commands/obligation-escrow/discharge.test.ts create mode 100644 tests/commands/obligation-escrow/endorse-transfer-owner.test.ts create mode 100644 tests/commands/obligation-escrow/nominate-transfer-owner.test.ts create mode 100644 tests/commands/obligation-escrow/reject-return-to-issuer.test.ts create mode 100644 tests/commands/obligation-escrow/reject-transfer-holder.test.ts create mode 100644 tests/commands/obligation-escrow/reject-transfer-owner-holder.test.ts create mode 100644 tests/commands/obligation-escrow/reject-transfer-owner.test.ts create mode 100644 tests/commands/obligation-escrow/reject.test.ts create mode 100644 tests/commands/obligation-escrow/return-to-issuer.test.ts create mode 100644 tests/commands/obligation-escrow/status.test.ts create mode 100644 tests/commands/obligation-escrow/transfer-holder.test.ts create mode 100644 tests/commands/obligation-escrow/transfer-owner-holder.test.ts create mode 100644 tests/commands/obligation-registry/deploy.test.ts create mode 100644 tests/commands/obligation-registry/mint.test.ts create mode 100644 tests/commands/verify-obligation.test.ts create mode 100644 tests/fixtures/obligation/w3c-obligation-record.json diff --git a/README.md b/README.md index c846ef5..fa3c3dd 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ A comprehensive command-line interface for managing W3C Verifiable Credentials, - ✅ **Token Registry**: Mint tokens to blockchain-based token registries - ✅ **Document Store**: Deploy and manage document store contracts - ✅ **Title Escrow**: Complete transferable records management (holder/beneficiary transfers) +- ✅ **Obligation Registry (BoE)**: Deploy/mint obligation registries, manage ObligationEscrow lifecycle, and verify BoE documents via a dedicated pipeline - ✅ **Credential Status**: Create and update W3C credential status lists - ✅ **W3C Standards**: Compliant with latest W3C DID and Verifiable Credentials specifications - ✅ **Multi-Network Support**: Ethereum, Polygon, XDC, Stability, and Astron networks @@ -38,6 +39,7 @@ This CLI leverages the TrustVC package: - [Setup](#setup) - [Project Structure](#project-structure) - [License](#license) +- [Obligation Registry user guide](#obligation-registry-user-guide) ## Prerequisites @@ -188,6 +190,37 @@ trustvc title-escrow reject-transfer-owner trustvc title-escrow reject-transfer-owner-holder ``` +### Obligation Registry & Escrow (BoE) + +```sh +# Deploy Obligation Registry (TrustVCToken + factory) +trustvc obligation-registry deploy + +# Mint a BoE tokenId to obligationRegistry +trustvc obligation-registry mint + +# Lifecycle +trustvc obligation-escrow accept +trustvc obligation-escrow reject +trustvc obligation-escrow discharge +trustvc obligation-escrow status + +# Transfers (mirror title-escrow) +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 +trustvc obligation-escrow return-to-issuer +trustvc obligation-escrow accept-return-to-issuer +trustvc obligation-escrow reject-return-to-issuer + +# Verify via obligation pipeline (not classic verify) +trustvc verify-obligation +``` + ## How It Works ### W3C Credentials @@ -224,6 +257,8 @@ trustvc title-escrow reject-transfer-owner-holder - **Title Escrow**: Provides comprehensive transferable records management including holder transfers, beneficiary nominations, endorsements, returns, and rejections using smart contracts. +- **Obligation Registry (BoE)**: Separate command trees for electronic Bill of Exchange flows — `obligation-registry` (deploy/mint), `obligation-escrow` (accept/reject/discharge, transfers, return), and `verify-obligation` (dedicated BoE verify pipeline). Do not use classic `token-registry` / `title-escrow` / `verify` for obligation documents. See [Obligation Registry user guide](#obligation-registry-user-guide). + ## Commands ### Available Commands @@ -275,12 +310,27 @@ trustvc title-escrow reject-transfer-owner-holder | | [`reject-transfer-owner`](#title-escrow-reject-transfer-owner) | Reject owner transfer | | | [`reject-transfer-owner-holder`](#title-escrow-reject-transfer-owner-holder) | Reject full transfer | | | `title-escrow reject-transfer-owner-holder` | Alternative: `reject-transfer-owner-holder` | +| **Obligation / BoE**| [`obligation-registry deploy`](#obligation-registry-deploy) | Deploy Obligation Registry | +| | [`obligation-registry mint`](#obligation-registry-mint) | Mint BoE token to obligationRegistry | +| | [`obligation-escrow accept`](#obligation-escrow-accept) | Accept obligation | +| | [`obligation-escrow reject`](#obligation-escrow-reject) | Reject obligation | +| | [`obligation-escrow discharge`](#obligation-escrow-discharge) | Discharge obligation | +| | [`obligation-escrow status`](#obligation-escrow-status) | Read obligation / escrow status | +| | [`obligation-escrow transfer-holder`](#obligation-escrow-transfer-holder) | Transfer BoE holder | +| | `obligation-escrow nominate-transfer-owner` | Nominate BoE beneficiary | +| | `obligation-escrow endorse-transfer-owner` | Endorse BoE beneficiary change | +| | `obligation-escrow transfer-owner-holder` | Endorse full BoE ownership transfer | +| | `obligation-escrow return-to-issuer` | Return BoE to issuer | +| | `obligation-escrow accept-return-to-issuer` | Accept returned BoE | +| | `obligation-escrow reject-return-to-issuer` | Reject returned BoE | +| | `obligation-escrow reject-transfer-*` | Reject BoE transfer requests | +| | [`verify-obligation`](#verify-obligation) | Verify BoE via obligation pipeline | --- ### Wallet/Private Key Options -All title-escrow, token registry, document-store, and transaction commands require a wallet or private key to sign transactions. You can provide your private key in one of the following ways: +All title-escrow, obligation-registry, obligation-escrow, token registry, document-store, and transaction commands require a wallet or private key to sign transactions. You can provide your private key in one of the following ways: **Select wallet/private key option:** @@ -1342,6 +1392,196 @@ Transaction receipt confirming rejection. +
+

obligation-registry deploy

+ +Deploys an Obligation Registry (`TrustVCToken` + `ObligationEscrowFactory`) for electronic Bill of Exchange (BoE) flows. + +**Do not use** `token-registry deploy` for BoE documents. + +**Usage:** + +```sh +trustvc obligation-registry deploy +``` + +**Interactive Prompts:** + +- Network selection +- Registry name and symbol +- Optionally reuse an existing ObligationEscrowFactory address +- Wallet/private key option +- Dry-run confirmation before broadcasting + +**Output:** +Deployed obligation registry (and factory) addresses plus transaction receipt. + +
+ +
+

obligation-registry mint

+ +Mints a BoE tokenId to an Obligation Registry and creates the linked ObligationEscrow. + +**Do not use** classic `mint` / `token-registry mint` for BoE documents. + +**Usage:** + +```sh +trustvc obligation-registry mint +``` + +**Interactive Prompts:** + +- Path to signed BoE / obligation document + - _Network, obligationRegistry address, token ID, and document ID are extracted from the document_ +- Holder and beneficiary (drawer/drawee) addresses as required +- Wallet/private key option +- Dry-run confirmation before broadcasting + +**Output:** +Transaction receipt confirming mint. + +
+ +
+

obligation-escrow accept

+ +Accepts an obligation on the ObligationEscrow (drawee acceptance). + +**Usage:** + +```sh +trustvc obligation-escrow accept +``` + +**Interactive Prompts:** + +- Path to BoE / obligation document + - _Network, obligationRegistry, token ID, and document ID are extracted from the document_ +- Wallet/private key option +- Remark (optional) + +**Output:** +Transaction receipt confirming acceptance. + +
+ +
+

obligation-escrow reject

+ +Rejects an obligation on the ObligationEscrow. + +**Usage:** + +```sh +trustvc obligation-escrow reject +``` + +**Interactive Prompts:** + +- Path to BoE / obligation document +- Wallet/private key option +- Remark (optional) + +**Output:** +Transaction receipt confirming rejection. + +
+ +
+

obligation-escrow discharge

+ +Discharges an accepted obligation on the ObligationEscrow. + +**Usage:** + +```sh +trustvc obligation-escrow discharge +``` + +**Interactive Prompts:** + +- Path to BoE / obligation document +- Wallet/private key option +- Remark (optional) + +**Output:** +Transaction receipt confirming discharge. + +
+ +
+

obligation-escrow status

+ +Reads on-chain obligation / escrow status for a BoE document (read-only; no wallet required for the status call itself beyond network connectivity). + +**Usage:** + +```sh +trustvc obligation-escrow status +``` + +**Interactive Prompts:** + +- Path to BoE / obligation document + - _Network, obligationRegistry, and token ID are extracted from the document_ + +**Output:** +Obligation and escrow status fields from the chain. + +
+ +
+

obligation-escrow transfer-holder

+ +Transfers the holder of a BoE obligation record. Transfer, nominate, endorse, return, and reject variants mirror `title-escrow` but operate on ObligationEscrow via `obligation-escrow `. + +**Usage:** + +```sh +trustvc obligation-escrow transfer-holder +# Also: +# nominate-transfer-owner, endorse-transfer-owner, transfer-owner-holder +# return-to-issuer, accept-return-to-issuer, reject-return-to-issuer +# reject-transfer-holder, reject-transfer-owner, reject-transfer-owner-holder +``` + +**Interactive Prompts:** + +- Path to BoE / obligation document +- New address(es) where applicable +- Wallet/private key option +- Remark (optional) + +**Output:** +Transaction receipt confirming the escrow action. + +
+ +
+

verify-obligation

+ +Verifies a BoE / obligation document using the dedicated obligation verify pipeline (`verifyObligationDocument`). + +**Do not use** classic `verify` for BoE documents — it targets ETR / classic transferable records and will skip or mis-handle ObligationRecords. + +**Usage:** + +```sh +trustvc verify-obligation +``` + +**Interactive Prompts:** + +- Path to signed BoE / obligation document +- Network selection when needed for on-chain checks + +**Output:** +Verification fragments and overall validity (VALID / INVALID / SKIPPED outcomes as applicable). + +
+ ## Configuration ### Custom RPC Endpoints @@ -1428,12 +1668,31 @@ src/commands/ │ ├── reject-transfer-holder.ts # Reject holder transfer │ ├── reject-transfer-owner.ts # Reject owner transfer │ └── reject-transfer-owner-holder.ts # Reject full transfer +├── obligation-registry/ +│ ├── deploy.ts # Deploy Obligation Registry +│ └── mint.ts # Mint BoE token to obligationRegistry +├── obligation-escrow/ +│ ├── accept.ts # Accept obligation +│ ├── reject.ts # Reject obligation +│ ├── discharge.ts # Discharge obligation +│ ├── status.ts # Read obligation / escrow status +│ ├── transfer-holder.ts # Transfer BoE holder +│ ├── nominate-transfer-owner.ts # Nominate BoE beneficiary +│ ├── endorse-transfer-owner.ts # Endorse BoE beneficiary change +│ ├── transfer-owner-holder.ts # Endorse full BoE transfer +│ ├── return-to-issuer.ts # Return BoE to issuer +│ ├── accept-return-to-issuer.ts # Accept returned BoE +│ ├── reject-return-to-issuer.ts # Reject returned BoE +│ ├── reject-transfer-holder.ts # Reject BoE holder transfer +│ ├── reject-transfer-owner.ts # Reject BoE owner transfer +│ └── reject-transfer-owner-holder.ts # Reject full BoE transfer ├── transaction/ │ └── cancel.ts # Cancel a pending transaction ├── wallet/ │ ├── create.ts # Create encrypted wallet │ ├── encrypt.ts # Encrypt private key to wallet │ └── decrypt.ts # Decrypt wallet file +├── verify-obligation.ts # Verify BoE via obligation pipeline └── w3c/ ├── did.ts # Generate DID ├── key-pair.ts # Generate key pairs @@ -1444,6 +1703,109 @@ src/commands/ └── verify.ts # Verify W3C or OA document ``` +## Obligation Registry user guide + +User guide for electronic Bill of Exchange (BoE) / obligation flows with **`trustvc-cli`**. + +Classic transferable records (eBL / Token Registry + Title Escrow) use different commands. This section covers **only** Obligation Registry. + +For library / SDK details, see the TrustVC README [§7c Obligation Registry (BoE)](https://github.com/TrustVC/trustvc#c-obligation-registry-boe). + +### Who this is for + +Operators and integrators who: + +- Deploy an Obligation Registry on a supported network +- Mint a signed BoE credential on-chain +- Accept, reject, discharge, transfer, or return the obligation +- Verify a BoE document + +You do **not** need to call the TypeScript SDK directly — the CLI wraps it with interactive prompts. + +### Before you start + +1. **Install the CLI** — `npm install -g @trustvc/trustvc-cli` or `npx @trustvc/trustvc-cli ` +2. **Node.js 22+** +3. **A wallet** — encrypted wallet file (recommended), private key, or key file +4. **A signed BoE document** — W3C VC with `credentialStatus.obligationRegistry` (not `tokenRegistry`) +5. **Network access** — select network in prompts, or set a custom RPC (e.g. `export SEPOLIA_RPC=…`) + +### Classic vs Obligation — pick the right commands + +| Task | Classic ETR | Obligation / BoE | +|------|-------------|------------------| +| Deploy registry | `token-registry deploy` | `obligation-registry deploy` | +| Mint | `mint` / `token-registry mint` | `obligation-registry mint` | +| Escrow actions | `title-escrow …` | `obligation-escrow …` | +| Verify | `verify` | `verify-obligation` | + +Using classic commands on a BoE document will fail or skip obligation checks. Using obligation commands on a classic eBL document will fail extraction (missing `obligationRegistry`). + +### Typical workflow + +``` +1. Deploy Obligation Registry +2. Put the registry address into your BoE credential status, then sign the credential +3. Mint (issuer wallet) +4. Accept or reject (holder) ──► optional transfers / discharge / return +5. Verify with verify-obligation +``` + +Signing/building the VC is usually done with TrustVC library tools or your app (`ObligationDocumentBuilder`). The CLI focuses on **on-chain** steps and **verify**. + +### Step-by-step + +**1. Deploy** — `trustvc obligation-registry deploy` (network, name/symbol, optional factory reuse, wallet, dry-run). + +**2. Mint** — `trustvc obligation-registry mint` with a signed BoE JSON that already references your registry (registry, network, token ID, document ID are extracted). + +**3. Accept or reject (holder)** — `trustvc obligation-escrow accept` or `reject` while **beneficiary ≠ holder**. + +**4. Status** — `trustvc obligation-escrow status` reads `Issued` / `Accepted` / `Rejected` / `Discharged`, registration, and termination reason. + +**5. Transfers (optional)** — `obligation-escrow transfer-holder`, nominate/endorse/return variants mirror `title-escrow`. + +**6. Discharge (optional)** — `trustvc obligation-escrow discharge` by **beneficiary** after Accepted. + +**7. Return to issuer (optional)** — requires **beneficiary == holder**; then issuer runs `reject-return-to-issuer` (restore) or `accept-return-to-issuer` (burn). + +**8. Verify** — `trustvc verify-obligation` (not classic `verify`). + +### Who can run what + +| Action | Typical role | +|--------|----------------| +| `obligation-registry deploy` | Deployer / issuer org | +| `obligation-registry mint` | Issuer (registry minter) | +| `obligation-escrow accept` / `reject` | Holder (roles split) | +| `obligation-escrow discharge` | Beneficiary | +| Transfer / nominate / endorse | Current holder or beneficiary per Title Escrow–style rules | +| `return-to-issuer` | Dual role (beneficiary == holder) | +| `accept-return-to-issuer` / `reject-return-to-issuer` | Issuer | +| `obligation-escrow status` | Anyone with network access + document | +| `verify-obligation` | Anyone with the document (+ RPC when on-chain checks run) | + +### What the CLI reads from your document + +For mint, escrow, and related flows, the CLI extracts: + +- Network +- `obligationRegistry` address +- Token ID +- Document ID (remark encryption key when remarks are set) + +If extraction fails with a message about classic transferable records / `tokenRegistry`, you are using the wrong document type or command family. + +### Things to know (not bugs) + +1. **Wrong command family** — BoE → `obligation-*` / `verify-obligation`. Classic eBL → `token-registry` / `title-escrow` / `verify`. +2. **Accept / reject need split roles** — `beneficiary != holder`. +3. **Return needs dual role** — `beneficiary == holder`. +4. **`terminationReason` is not set by return alone** — set on reject / discharge / burn (accept-return). +5. **Deploy + mint do not create the VC** — you still need a signed BoE file; mint only puts the token on-chain. + +Per-command prompts and outputs are in [Detailed Command Reference](#detailed-command-reference) above (`obligation-registry deploy`, `obligation-registry mint`, `obligation-escrow *`, `verify-obligation`). + ## License Apache-2.0 diff --git a/src/commands/helpers.ts b/src/commands/helpers.ts index b11785e..1bab3a3 100644 --- a/src/commands/helpers.ts +++ b/src/commands/helpers.ts @@ -7,12 +7,15 @@ import { v4SupportInterfaceIds, v5SupportInterfaceIds, DocumentStore__factory, + obligationRegistryContracts, } from '@trustvc/trustvc'; import { encrypt } from '@trustvc/trustvc'; // Internal utilities import { ConnectedSigner } from '../utils'; +const { TrustVCToken__factory, ObligationEscrow__factory } = obligationRegistryContracts; + // Contract factories from TrustVC v5 const { TitleEscrow__factory, TradeTrustToken__factory, TDocDeployer__factory } = v5Contracts; @@ -27,6 +30,11 @@ interface ConnectToTDocDeployerContractArgs { wallet: Wallet | HDNodeWallet | ConnectedSigner | Signer; } +interface ConnectToObligationRegistryArgs { + address: string; + wallet: Wallet | HDNodeWallet | ConnectedSigner | Signer; +} + /** * Connects to a token registry contract instance. * @@ -419,3 +427,69 @@ export async function waitForTransaction(tx: any | string, provider: Provider, c throw new Error('Provider required for transaction hash'); } } + +/** + * Connects to a TrustVCToken (obligation registry) contract instance. + */ +export const connectToObligationRegistry = async ({ + address, + wallet, +}: ConnectToObligationRegistryArgs) => { + try { + signale.info(`Connecting to obligation registry at: ${address}`); + const registry = new ethers.Contract(address, TrustVCToken__factory.abi, wallet as any); + if (!registry) { + throw new Error(`Failed to connect to obligation registry at address: ${address}`); + } + signale.success(`Successfully connected to obligation registry`); + return registry; + } catch (error) { + signale.error( + `Error in connectToObligationRegistry: ${error instanceof Error ? error.message : String(error)}`, + ); + throw error; + } +}; + +interface ConnectToObligationEscrowArgs { + tokenId: string; + address: string; + wallet: Wallet | HDNodeWallet | ConnectedSigner | Signer; +} + +/** + * Resolves ObligationEscrow via ownerOf(tokenId) on the obligation registry and connects. + */ +export const connectToObligationEscrow = async ({ + tokenId, + address, + wallet, +}: ConnectToObligationEscrowArgs) => { + try { + signale.info(`Connecting to obligation registry at: ${address}`); + const registry = new ethers.Contract(address, TrustVCToken__factory.abi, wallet as any); + + signale.info(`Fetching obligation escrow address for tokenId: ${tokenId}`); + const escrowAddress = await registry.ownerOf(tokenId); + signale.info(`Obligation escrow address: ${escrowAddress}`); + + if (!escrowAddress || escrowAddress === ZeroAddress) { + throw new Error( + `Invalid obligation escrow address for tokenId: ${tokenId}. Address: ${escrowAddress}`, + ); + } + + signale.info(`Connecting to obligation escrow at: ${escrowAddress}`); + const escrow = new ethers.Contract(escrowAddress, ObligationEscrow__factory.abi, wallet as any); + if (!escrow) { + throw new Error(`Failed to connect to obligation escrow at address: ${escrowAddress}`); + } + signale.success(`Successfully connected to obligation escrow`); + return escrow; + } catch (error) { + signale.error( + `Error in connectToObligationEscrow: ${error instanceof Error ? error.message : String(error)}`, + ); + throw error; + } +}; diff --git a/src/commands/obligation-escrow/accept-return-to-issuer.ts b/src/commands/obligation-escrow/accept-return-to-issuer.ts new file mode 100644 index 0000000..a4ec7f0 --- /dev/null +++ b/src/commands/obligation-escrow/accept-return-to-issuer.ts @@ -0,0 +1,51 @@ +import { error, info, success } from 'signale'; +import { acceptReturnedObligationRegistry } from '@trustvc/trustvc'; +import { BaseObligationEscrowCommand } from '../../types'; +import { + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + TransactionReceiptFees, +} from '../../utils'; +import { promptBaseObligationEscrowInputs } from './shared'; +import { runObligationEscrowTx } from './runTx'; + +export const command = 'accept-return-to-issuer'; +export const describe = 'Issuer accepts a returned BoE obligation (burn / shred)'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + await acceptReturnedHandler(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = promptBaseObligationEscrowInputs; + +export const acceptReturnedHandler = async (args: BaseObligationEscrowCommand) => { + try { + info(`Accepting returned obligation ${args.tokenId}`); + const transaction = await runObligationEscrowTx({ + args, + // acceptReturned burns the title, which lives on the TrustVCToken registry, not the escrow. + populate: ({ registry }, encryptedRemark) => + registry.burn.populateTransaction(args.tokenId, encryptedRemark), + sdk: acceptReturnedObligationRegistry as any, + sdkParams: { tokenId: args.tokenId, remarks: args.remark }, + }); + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Return accepted (burned)`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-escrow/accept.ts b/src/commands/obligation-escrow/accept.ts new file mode 100644 index 0000000..b908318 --- /dev/null +++ b/src/commands/obligation-escrow/accept.ts @@ -0,0 +1,49 @@ +import { error, info, success } from 'signale'; +import { acceptObligationRegistry } from '@trustvc/trustvc'; +import { BaseObligationEscrowCommand } from '../../types'; +import { + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + TransactionReceiptFees, +} from '../../utils'; +import { promptBaseObligationEscrowInputs } from './shared'; +import { runObligationEscrowTx } from './runTx'; + +export const command = 'accept'; +export const describe = 'Holder accepts an issued BoE obligation (Issued → Accepted)'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + await acceptHandler(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = promptBaseObligationEscrowInputs; + +export const acceptHandler = async (args: BaseObligationEscrowCommand) => { + try { + info(`Accepting obligation token ${args.tokenId} on ${args.obligationRegistryAddress}`); + const transaction = await runObligationEscrowTx({ + args, + populate: ({ escrow }, encryptedRemark) => escrow.accept.populateTransaction(encryptedRemark), + sdk: acceptObligationRegistry as any, + sdkParams: { tokenId: args.tokenId, remarks: args.remark }, + }); + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Obligation ${args.tokenId} accepted`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-escrow/discharge.ts b/src/commands/obligation-escrow/discharge.ts new file mode 100644 index 0000000..a8b7f3a --- /dev/null +++ b/src/commands/obligation-escrow/discharge.ts @@ -0,0 +1,50 @@ +import { error, info, success } from 'signale'; +import { dischargeObligationRegistry } from '@trustvc/trustvc'; +import { BaseObligationEscrowCommand } from '../../types'; +import { + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + TransactionReceiptFees, +} from '../../utils'; +import { promptBaseObligationEscrowInputs } from './shared'; +import { runObligationEscrowTx } from './runTx'; + +export const command = 'discharge'; +export const describe = 'Beneficiary discharges an accepted BoE obligation (terminal Discharged)'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + await dischargeHandler(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = promptBaseObligationEscrowInputs; + +export const dischargeHandler = async (args: BaseObligationEscrowCommand) => { + try { + info(`Discharging obligation token ${args.tokenId} on ${args.obligationRegistryAddress}`); + const transaction = await runObligationEscrowTx({ + args, + populate: ({ escrow }, encryptedRemark) => + escrow.discharge.populateTransaction(encryptedRemark), + sdk: dischargeObligationRegistry as any, + sdkParams: { tokenId: args.tokenId, remarks: args.remark }, + }); + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Obligation ${args.tokenId} discharged`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-escrow/endorse-transfer-owner.ts b/src/commands/obligation-escrow/endorse-transfer-owner.ts new file mode 100644 index 0000000..d275c11 --- /dev/null +++ b/src/commands/obligation-escrow/endorse-transfer-owner.ts @@ -0,0 +1,55 @@ +import { error, info, success } from 'signale'; +import { transferBeneficiaryObligationRegistry } from '@trustvc/trustvc'; +import { ObligationEscrowNominateBeneficiaryCommand } from '../../types'; +import { + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + promptAddress, + TransactionReceiptFees, +} from '../../utils'; +import { promptBaseObligationEscrowInputs } from './shared'; +import { runObligationEscrowTx } from './runTx'; + +export const command = 'endorse-transfer-owner'; +export const describe = 'Endorse / transfer beneficiary (owner) of a BoE obligation'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + await endorseHandler(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = async (): Promise => { + const base = await promptBaseObligationEscrowInputs(); + const newBeneficiary = await promptAddress('new beneficiary', 'endorsed owner'); + return { ...base, newBeneficiary } as ObligationEscrowNominateBeneficiaryCommand; +}; + +export const endorseHandler = async (args: ObligationEscrowNominateBeneficiaryCommand) => { + try { + info(`Transferring beneficiary to ${args.newBeneficiary} for obligation ${args.tokenId}`); + const transaction = await runObligationEscrowTx({ + args, + populate: ({ escrow }, encryptedRemark) => + escrow.transferBeneficiary.populateTransaction(args.newBeneficiary, encryptedRemark), + sdk: transferBeneficiaryObligationRegistry as any, + sdkParams: { newBeneficiaryAddress: args.newBeneficiary, remarks: args.remark }, + }); + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Beneficiary transferred to ${args.newBeneficiary}`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-escrow/index.ts b/src/commands/obligation-escrow/index.ts new file mode 100644 index 0000000..28969ed --- /dev/null +++ b/src/commands/obligation-escrow/index.ts @@ -0,0 +1,11 @@ +import { Argv } from 'yargs'; + +export const command = 'obligation-escrow '; + +export const describe = + 'Invoke ObligationEscrow lifecycle / transfer functions for BoE obligation records'; + +export const builder = (yargs: Argv): Argv => + yargs.commandDir(__dirname, { extensions: ['ts', 'js'] }); + +export const handler = (): void => {}; diff --git a/src/commands/obligation-escrow/nominate-transfer-owner.ts b/src/commands/obligation-escrow/nominate-transfer-owner.ts new file mode 100644 index 0000000..0d94a55 --- /dev/null +++ b/src/commands/obligation-escrow/nominate-transfer-owner.ts @@ -0,0 +1,55 @@ +import { error, info, success } from 'signale'; +import { nominateObligationRegistry } from '@trustvc/trustvc'; +import { ObligationEscrowNominateBeneficiaryCommand } from '../../types'; +import { + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + promptAddress, + TransactionReceiptFees, +} from '../../utils'; +import { promptBaseObligationEscrowInputs } from './shared'; +import { runObligationEscrowTx } from './runTx'; + +export const command = 'nominate-transfer-owner'; +export const describe = 'Nominate a new beneficiary (owner) for a BoE obligation'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + await nominateHandler(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = async (): Promise => { + const base = await promptBaseObligationEscrowInputs(); + const newBeneficiary = await promptAddress('new beneficiary', 'nominee'); + return { ...base, newBeneficiary } as ObligationEscrowNominateBeneficiaryCommand; +}; + +export const nominateHandler = async (args: ObligationEscrowNominateBeneficiaryCommand) => { + try { + info(`Nominating beneficiary ${args.newBeneficiary} for obligation ${args.tokenId}`); + const transaction = await runObligationEscrowTx({ + args, + populate: ({ escrow }, encryptedRemark) => + escrow.nominate.populateTransaction(args.newBeneficiary, encryptedRemark), + sdk: nominateObligationRegistry as any, + sdkParams: { newBeneficiaryAddress: args.newBeneficiary, remarks: args.remark }, + }); + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Nominated ${args.newBeneficiary}`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-escrow/reject-return-to-issuer.ts b/src/commands/obligation-escrow/reject-return-to-issuer.ts new file mode 100644 index 0000000..113d537 --- /dev/null +++ b/src/commands/obligation-escrow/reject-return-to-issuer.ts @@ -0,0 +1,51 @@ +import { error, info, success } from 'signale'; +import { rejectReturnedObligationRegistry } from '@trustvc/trustvc'; +import { BaseObligationEscrowCommand } from '../../types'; +import { + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + TransactionReceiptFees, +} from '../../utils'; +import { promptBaseObligationEscrowInputs } from './shared'; +import { runObligationEscrowTx } from './runTx'; + +export const command = 'reject-return-to-issuer'; +export const describe = 'Issuer rejects a returned BoE obligation (restore to escrow)'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + await rejectReturnedHandler(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = promptBaseObligationEscrowInputs; + +export const rejectReturnedHandler = async (args: BaseObligationEscrowCommand) => { + try { + info(`Rejecting returned obligation ${args.tokenId} (restore)`); + const transaction = await runObligationEscrowTx({ + args, + // rejectReturned restores the title, which lives on the TrustVCToken registry, not the escrow. + populate: ({ registry }, encryptedRemark) => + registry.restore.populateTransaction(args.tokenId, encryptedRemark), + sdk: rejectReturnedObligationRegistry as any, + sdkParams: { tokenId: args.tokenId, remarks: args.remark }, + }); + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Return rejected (restored to escrow)`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-escrow/reject-transfer-holder.ts b/src/commands/obligation-escrow/reject-transfer-holder.ts new file mode 100644 index 0000000..d4fc2ff --- /dev/null +++ b/src/commands/obligation-escrow/reject-transfer-holder.ts @@ -0,0 +1,50 @@ +import { error, info, success } from 'signale'; +import { rejectTransferHolderObligationRegistry } from '@trustvc/trustvc'; +import { BaseObligationEscrowCommand } from '../../types'; +import { + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + TransactionReceiptFees, +} from '../../utils'; +import { promptBaseObligationEscrowInputs } from './shared'; +import { runObligationEscrowTx } from './runTx'; + +export const command = 'reject-transfer-holder'; +export const describe = 'Reject a pending holder transfer on a BoE obligation'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + await rejectTransferHolderHandler(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = promptBaseObligationEscrowInputs; + +export const rejectTransferHolderHandler = async (args: BaseObligationEscrowCommand) => { + try { + info(`Rejecting holder transfer for obligation ${args.tokenId}`); + const transaction = await runObligationEscrowTx({ + args, + populate: ({ escrow }, encryptedRemark) => + escrow.rejectTransferHolder.populateTransaction(encryptedRemark), + sdk: rejectTransferHolderObligationRegistry as any, + sdkParams: { remarks: args.remark }, + }); + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Holder transfer rejected`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-escrow/reject-transfer-owner-holder.ts b/src/commands/obligation-escrow/reject-transfer-owner-holder.ts new file mode 100644 index 0000000..85a0883 --- /dev/null +++ b/src/commands/obligation-escrow/reject-transfer-owner-holder.ts @@ -0,0 +1,50 @@ +import { error, info, success } from 'signale'; +import { rejectTransferOwnersObligationRegistry } from '@trustvc/trustvc'; +import { BaseObligationEscrowCommand } from '../../types'; +import { + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + TransactionReceiptFees, +} from '../../utils'; +import { promptBaseObligationEscrowInputs } from './shared'; +import { runObligationEscrowTx } from './runTx'; + +export const command = 'reject-transfer-owner-holder'; +export const describe = 'Reject a pending joint owner+holder transfer on a BoE obligation'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + await rejectTransferOwnersHandler(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = promptBaseObligationEscrowInputs; + +export const rejectTransferOwnersHandler = async (args: BaseObligationEscrowCommand) => { + try { + info(`Rejecting owners transfer for obligation ${args.tokenId}`); + const transaction = await runObligationEscrowTx({ + args, + populate: ({ escrow }, encryptedRemark) => + escrow.rejectTransferOwners.populateTransaction(encryptedRemark), + sdk: rejectTransferOwnersObligationRegistry as any, + sdkParams: { remarks: args.remark }, + }); + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Owners transfer rejected`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-escrow/reject-transfer-owner.ts b/src/commands/obligation-escrow/reject-transfer-owner.ts new file mode 100644 index 0000000..c6e4b99 --- /dev/null +++ b/src/commands/obligation-escrow/reject-transfer-owner.ts @@ -0,0 +1,50 @@ +import { error, info, success } from 'signale'; +import { rejectTransferBeneficiaryObligationRegistry } from '@trustvc/trustvc'; +import { BaseObligationEscrowCommand } from '../../types'; +import { + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + TransactionReceiptFees, +} from '../../utils'; +import { promptBaseObligationEscrowInputs } from './shared'; +import { runObligationEscrowTx } from './runTx'; + +export const command = 'reject-transfer-owner'; +export const describe = 'Reject a pending beneficiary transfer on a BoE obligation'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + await rejectTransferOwnerHandler(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = promptBaseObligationEscrowInputs; + +export const rejectTransferOwnerHandler = async (args: BaseObligationEscrowCommand) => { + try { + info(`Rejecting beneficiary transfer for obligation ${args.tokenId}`); + const transaction = await runObligationEscrowTx({ + args, + populate: ({ escrow }, encryptedRemark) => + escrow.rejectTransferBeneficiary.populateTransaction(encryptedRemark), + sdk: rejectTransferBeneficiaryObligationRegistry as any, + sdkParams: { remarks: args.remark }, + }); + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Beneficiary transfer rejected`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-escrow/reject.ts b/src/commands/obligation-escrow/reject.ts new file mode 100644 index 0000000..ef23c1d --- /dev/null +++ b/src/commands/obligation-escrow/reject.ts @@ -0,0 +1,49 @@ +import { error, info, success } from 'signale'; +import { rejectObligationRegistry } from '@trustvc/trustvc'; +import { BaseObligationEscrowCommand } from '../../types'; +import { + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + TransactionReceiptFees, +} from '../../utils'; +import { promptBaseObligationEscrowInputs } from './shared'; +import { runObligationEscrowTx } from './runTx'; + +export const command = 'reject'; +export const describe = 'Holder rejects an issued BoE obligation (terminal Rejected)'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + await rejectHandler(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = promptBaseObligationEscrowInputs; + +export const rejectHandler = async (args: BaseObligationEscrowCommand) => { + try { + info(`Rejecting obligation token ${args.tokenId} on ${args.obligationRegistryAddress}`); + const transaction = await runObligationEscrowTx({ + args, + populate: ({ escrow }, encryptedRemark) => escrow.reject.populateTransaction(encryptedRemark), + sdk: rejectObligationRegistry as any, + sdkParams: { tokenId: args.tokenId, remarks: args.remark }, + }); + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Obligation ${args.tokenId} rejected`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-escrow/return-to-issuer.ts b/src/commands/obligation-escrow/return-to-issuer.ts new file mode 100644 index 0000000..236dbc1 --- /dev/null +++ b/src/commands/obligation-escrow/return-to-issuer.ts @@ -0,0 +1,50 @@ +import { error, info, success } from 'signale'; +import { returnToIssuerObligationRegistry } from '@trustvc/trustvc'; +import { BaseObligationEscrowCommand } from '../../types'; +import { + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + TransactionReceiptFees, +} from '../../utils'; +import { promptBaseObligationEscrowInputs } from './shared'; +import { runObligationEscrowTx } from './runTx'; + +export const command = 'return-to-issuer'; +export const describe = 'Return a BoE obligation to the issuer (requires dual role)'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + await returnToIssuerHandler(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = promptBaseObligationEscrowInputs; + +export const returnToIssuerHandler = async (args: BaseObligationEscrowCommand) => { + try { + info(`Returning obligation ${args.tokenId} to issuer`); + const transaction = await runObligationEscrowTx({ + args, + populate: ({ escrow }, encryptedRemark) => + escrow.returnToIssuer.populateTransaction(encryptedRemark), + sdk: returnToIssuerObligationRegistry as any, + sdkParams: { remarks: args.remark }, + }); + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Returned to issuer`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-escrow/runTx.ts b/src/commands/obligation-escrow/runTx.ts new file mode 100644 index 0000000..4270e9d --- /dev/null +++ b/src/commands/obligation-escrow/runTx.ts @@ -0,0 +1,93 @@ +import signale from 'signale'; +import { BytesLike, TransactionReceipt } from 'ethers'; +import { CHAIN_ID } from '@trustvc/trustvc'; +import { + canEstimateGasPrice, + getGasFees, + getSupportedNetwork, + getWalletOrSigner, + performDryRunWithConfirmation, +} from '../../utils'; +import { + connectToObligationEscrow, + connectToObligationRegistry, + validateAndEncryptRemark, +} from '../helpers'; +import { BaseObligationEscrowCommand } from '../../types'; + +type PopulateFn = ( + contracts: { escrow: any; registry: any }, + encryptedRemark: BytesLike, +) => Promise; + +type SdkCall = ( + contractOptions: { obligationRegistry: string; tokenId: string }, + wallet: any, + params: Record, + txOptions: Record, +) => Promise<{ hash: string; wait: () => Promise }>; + +/** + * Shared dry-run + gas + SDK send for obligation-escrow write methods that take remarks. + */ +export const runObligationEscrowTx = async (opts: { + args: BaseObligationEscrowCommand; + populate: PopulateFn; + sdk: SdkCall; + sdkParams: Record; +}): Promise => { + const { args, populate, sdk, sdkParams } = opts; + const { obligationRegistryAddress, tokenId, remark, encryptionKey, network, ...rest } = args; + const wallet = await getWalletOrSigner({ network, ...rest }); + const networkId = getSupportedNetwork(network).networkId; + + const shouldProceed = await performDryRunWithConfirmation({ + network, + getTransactionCallback: async () => { + const escrow = await connectToObligationEscrow({ + tokenId, + address: obligationRegistryAddress, + wallet, + }); + const registry = await connectToObligationRegistry({ + address: obligationRegistryAddress, + wallet, + }); + const encryptedRemark = validateAndEncryptRemark(remark, encryptionKey); + const tx = await populate({ escrow, registry }, encryptedRemark); + return { ...tx, from: await wallet.getAddress() }; + }, + }); + + if (!shouldProceed) { + process.exit(0); + } + + const contractOptions = { obligationRegistry: obligationRegistryAddress, tokenId }; + let transaction; + + if (canEstimateGasPrice(network)) { + if (!wallet.provider) { + throw new Error('Provider is required for gas estimation'); + } + const gasFees = await getGasFees({ provider: wallet.provider, ...rest }); + transaction = await sdk(contractOptions, wallet, sdkParams, { + chainId: networkId as unknown as CHAIN_ID, + maxFeePerGas: gasFees.maxFeePerGas?.toString(), + maxPriorityFeePerGas: gasFees.maxPriorityFeePerGas?.toString(), + id: encryptionKey, + }); + } else { + transaction = await sdk(contractOptions, wallet, sdkParams, { + chainId: networkId as unknown as CHAIN_ID, + id: encryptionKey, + }); + } + + signale.await(`Waiting for transaction ${transaction.hash} to be mined`); + const receipt = (await transaction.wait()) as unknown as TransactionReceipt; + if (!receipt) { + throw new Error('Transaction receipt not found'); + } + return receipt; +}; diff --git a/src/commands/obligation-escrow/shared.ts b/src/commands/obligation-escrow/shared.ts new file mode 100644 index 0000000..15fd7f4 --- /dev/null +++ b/src/commands/obligation-escrow/shared.ts @@ -0,0 +1,40 @@ +import { + promptWalletSelection, + promptAndReadDocument, + promptRemark, + extractObligationDocumentInfo, + verifyDocumentSignature, +} from '../../utils'; +import { BaseObligationEscrowCommand } from '../../types'; + +/** + * Shared prompt flow for obligation-escrow commands that only need document + wallet + remark. + */ +export const promptBaseObligationEscrowInputs = async (): Promise => { + const document = await promptAndReadDocument(); + await verifyDocumentSignature(document); + const { obligationRegistry, tokenId, network, documentId } = + await extractObligationDocumentInfo(document); + const { encryptedWalletPath, key, keyFile } = await promptWalletSelection(); + const remark = await promptRemark('v5'); + + const baseResult = { + network, + obligationRegistryAddress: obligationRegistry, + tokenId, + remark, + encryptionKey: documentId, + maxPriorityFeePerGasScale: 1, + }; + + if (encryptedWalletPath) { + return { ...baseResult, encryptedWalletPath } as BaseObligationEscrowCommand; + } + if (keyFile) { + return { ...baseResult, keyFile } as BaseObligationEscrowCommand; + } + if (key) { + return { ...baseResult, key } as BaseObligationEscrowCommand; + } + return baseResult as BaseObligationEscrowCommand; +}; diff --git a/src/commands/obligation-escrow/status.ts b/src/commands/obligation-escrow/status.ts new file mode 100644 index 0000000..dfe904b --- /dev/null +++ b/src/commands/obligation-escrow/status.ts @@ -0,0 +1,59 @@ +import { error, info, success } from 'signale'; +import { + DocumentStatus, + ObligationEscrowTerminationReason, + getObligationEscrowTerminationReason, + getObligationRegistryStatus, + isObligationRegistryRegistered, +} from '@trustvc/trustvc'; +import { BaseObligationEscrowCommand } from '../../types'; +import { getErrorMessage, getWalletOrSigner } from '../../utils'; +import { promptBaseObligationEscrowInputs } from './shared'; + +export const command = 'status'; +export const describe = 'Read BoE obligation escrow status / registration / termination reason'; + +const STATUS_LABEL: Record = { + [DocumentStatus.Issued]: 'Issued', + [DocumentStatus.Accepted]: 'Accepted', + [DocumentStatus.Rejected]: 'Rejected', + [DocumentStatus.Discharged]: 'Discharged', +}; + +const REASON_LABEL: Record = { + [ObligationEscrowTerminationReason.None]: 'None', + [ObligationEscrowTerminationReason.ReturnToIssuer]: 'ReturnToIssuer', + [ObligationEscrowTerminationReason.Rejected]: 'Rejected', + [ObligationEscrowTerminationReason.Discharged]: 'Discharged', +}; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + await statusHandler(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = promptBaseObligationEscrowInputs; + +export const statusHandler = async (args: BaseObligationEscrowCommand) => { + try { + const { obligationRegistryAddress, tokenId, network, ...rest } = args; + const wallet = await getWalletOrSigner({ network, ...rest }); + const opts = { obligationRegistry: obligationRegistryAddress }; + + const status = await getObligationRegistryStatus(opts, wallet, { tokenId }); + const registered = await isObligationRegistryRegistered(opts, wallet, { tokenId }); + const reason = await getObligationEscrowTerminationReason(opts, wallet, { tokenId }); + + success(`Obligation ${tokenId} on ${obligationRegistryAddress}`); + info(` Status: ${STATUS_LABEL[status] ?? status} (${status})`); + info(` Registered: ${registered}`); + info(` Termination reason: ${REASON_LABEL[reason] ?? reason} (${reason})`); + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-escrow/transfer-holder.ts b/src/commands/obligation-escrow/transfer-holder.ts new file mode 100644 index 0000000..74d7146 --- /dev/null +++ b/src/commands/obligation-escrow/transfer-holder.ts @@ -0,0 +1,58 @@ +import { error, info, success, warn } from 'signale'; +import { transferHolderObligationRegistry } from '@trustvc/trustvc'; +import { ObligationEscrowTransferHolderCommand } from '../../types'; +import { + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + promptAddress, + TransactionReceiptFees, +} from '../../utils'; +import { promptBaseObligationEscrowInputs } from './shared'; +import { runObligationEscrowTx } from './runTx'; + +export const command = 'transfer-holder'; +export const describe = 'Transfer the holder of a BoE obligation record'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + await changeHolderHandler(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = async (): Promise => { + const base = await promptBaseObligationEscrowInputs(); + const newHolder = await promptAddress('new holder', 'new holder'); + return { ...base, newHolder } as ObligationEscrowTransferHolderCommand; +}; + +export const changeHolderHandler = async (args: ObligationEscrowTransferHolderCommand) => { + try { + info( + `Changing holder of obligation ${args.tokenId} on ${args.obligationRegistryAddress} to ${args.newHolder}`, + ); + warn('Only the current holder can transfer the holder role.'); + const transaction = await runObligationEscrowTx({ + args, + populate: ({ escrow }, encryptedRemark) => + escrow.transferHolder.populateTransaction(args.newHolder, encryptedRemark), + sdk: transferHolderObligationRegistry as any, + sdkParams: { holderAddress: args.newHolder, remarks: args.remark }, + }); + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Holder changed to ${args.newHolder}`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-escrow/transfer-owner-holder.ts b/src/commands/obligation-escrow/transfer-owner-holder.ts new file mode 100644 index 0000000..db39e7f --- /dev/null +++ b/src/commands/obligation-escrow/transfer-owner-holder.ts @@ -0,0 +1,65 @@ +import { error, info, success } from 'signale'; +import { transferOwnersObligationRegistry } from '@trustvc/trustvc'; +import { ObligationEscrowEndorseTransferOfOwnersCommand } from '../../types'; +import { + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + promptAddress, + TransactionReceiptFees, +} from '../../utils'; +import { promptBaseObligationEscrowInputs } from './shared'; +import { runObligationEscrowTx } from './runTx'; + +export const command = 'transfer-owner-holder'; +export const describe = 'Transfer both beneficiary and holder of a BoE obligation'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + await transferOwnersHandler(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = + async (): Promise => { + const base = await promptBaseObligationEscrowInputs(); + const newOwner = await promptAddress('new beneficiary', 'new owner'); + const newHolder = await promptAddress('new holder', 'new holder'); + return { ...base, newOwner, newHolder } as ObligationEscrowEndorseTransferOfOwnersCommand; + }; + +export const transferOwnersHandler = async ( + args: ObligationEscrowEndorseTransferOfOwnersCommand, +) => { + try { + info( + `Transferring owners of obligation ${args.tokenId} to owner ${args.newOwner} / holder ${args.newHolder}`, + ); + const transaction = await runObligationEscrowTx({ + args, + populate: ({ escrow }, encryptedRemark) => + escrow.transferOwners.populateTransaction(args.newOwner, args.newHolder, encryptedRemark), + sdk: transferOwnersObligationRegistry as any, + sdkParams: { + newBeneficiaryAddress: args.newOwner, + newHolderAddress: args.newHolder, + remarks: args.remark, + }, + }); + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Owners transferred`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-registry/deploy.ts b/src/commands/obligation-registry/deploy.ts new file mode 100644 index 0000000..ee14fbe --- /dev/null +++ b/src/commands/obligation-registry/deploy.ts @@ -0,0 +1,145 @@ +import { error, info, success } from 'signale'; +import { CHAIN_ID, deployObligationRegistry } from '@trustvc/trustvc'; +import { input, confirm } from '@inquirer/prompts'; +import { + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + promptWalletSelection, + promptNetworkSelection, + getWalletOrSigner, + getSupportedNetwork, + performDryRunWithConfirmation, + promptAddress, + supportedNetwork, +} from '../../utils'; +import { TransactionReceipt } from 'ethers'; + +export const command = 'deploy'; + +export const describe = 'Deploys an Obligation Registry (TrustVCToken + ObligationEscrowFactory)'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + + await deployObligationRegistryContract(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +type DeployObligationRegistryCommand = { + registryName: string; + registrySymbol: string; + escrowFactoryAddress?: string; + network: string; + encryptedWalletPath?: string; + key?: string; + keyFile?: string; + maxPriorityFeePerGasScale: number; +}; + +export const promptForInputs = async (): Promise => { + const network = await promptNetworkSelection(); + + const registryName = await input({ + message: 'Enter the name of the obligation registry:', + required: true, + validate: (value: string) => { + if (!value || value.trim() === '') { + return 'Registry name is required'; + } + return true; + }, + }); + + const registrySymbol = await input({ + message: 'Enter the symbol of the obligation registry:', + required: true, + validate: (value: string) => { + if (!value || value.trim() === '') { + return 'Registry symbol is required'; + } + return true; + }, + }); + + const reuseFactory = await confirm({ + message: 'Reuse an existing ObligationEscrowFactory address?', + default: false, + }); + + const escrowFactoryAddress = reuseFactory + ? await promptAddress('ObligationEscrowFactory', 'optional', true) + : undefined; + + const { encryptedWalletPath, key, keyFile } = await promptWalletSelection(); + + const baseResult = { + network, + registryName, + registrySymbol, + escrowFactoryAddress, + maxPriorityFeePerGasScale: 1, + }; + + if (encryptedWalletPath) { + return { ...baseResult, encryptedWalletPath } as DeployObligationRegistryCommand; + } + if (keyFile) { + return { ...baseResult, keyFile } as DeployObligationRegistryCommand; + } + if (key) { + return { ...baseResult, key } as DeployObligationRegistryCommand; + } + return baseResult as DeployObligationRegistryCommand; +}; + +export const deployObligationRegistryContract = async ({ + network, + registryName, + registrySymbol, + escrowFactoryAddress, + ...rest +}: DeployObligationRegistryCommand) => { + try { + const wallet = await getWalletOrSigner({ network, ...rest }); + const chainId = supportedNetwork[network as NetworkCmdName].networkId; + + const shouldProceed = await performDryRunWithConfirmation({ + network, + getTransactionCallback: async () => ({ + to: await wallet.getAddress(), + data: '0x', + from: await wallet.getAddress(), + value: 0n, + }), + }); + + if (!shouldProceed) { + process.exit(0); + } + + info(`Deploying obligation registry ${registryName}`); + + const result = await deployObligationRegistry(registryName, registrySymbol, wallet, { + chainId: getSupportedNetwork(network).networkId as unknown as CHAIN_ID, + escrowFactoryAddress, + }); + + displayTransactionPrice(result.receipt as any, network as NetworkCmdName); + + const { hash } = result.receipt as unknown as TransactionReceipt; + success(`Obligation registry ${registryName} deployed at ${result.obligationRegistry}`); + info(`ObligationEscrowFactory: ${result.obligationEscrowFactoryAddress}`); + info(`Find more details at ${getEtherscanAddress({ network })}/tx/${hash}`); + info(`Chain ID used: ${chainId}`); + + return result.obligationRegistry; + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-registry/index.ts b/src/commands/obligation-registry/index.ts new file mode 100644 index 0000000..0ac0d6c --- /dev/null +++ b/src/commands/obligation-registry/index.ts @@ -0,0 +1,11 @@ +import { Argv } from 'yargs'; + +export const command = 'obligation-registry '; + +export const describe = + 'Invoke a function over an Obligation Registry (BoE / TrustVCToken) smart contract'; + +export const builder = (yargs: Argv): Argv => + yargs.commandDir(__dirname, { extensions: ['ts', 'js'] }); + +export const handler = (): void => {}; diff --git a/src/commands/obligation-registry/mint.ts b/src/commands/obligation-registry/mint.ts new file mode 100644 index 0000000..f3cca8d --- /dev/null +++ b/src/commands/obligation-registry/mint.ts @@ -0,0 +1,161 @@ +import signale, { error, info, success } from 'signale'; +import { ObligationRegistryMintCommand } from '../../types'; +import { + addAddressPrefix, + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + promptWalletSelection, + getWalletOrSigner, + canEstimateGasPrice, + getGasFees, + extractObligationDocumentInfo, + promptAndReadDocument, + promptRemark, + promptAddress, + performDryRunWithConfirmation, + verifyDocumentSignature, +} from '../../utils'; +import { connectToObligationRegistry, validateAndEncryptRemark } from '../helpers'; +import { TransactionReceipt } from 'ethers'; +import { mintObligationRegistry } from '@trustvc/trustvc'; + +export const command = 'mint'; + +export const describe = 'Mint a tokenId to an Obligation Registry deployed on the blockchain'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + + await mintObligationToken(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = async (): Promise => { + const document = await promptAndReadDocument(); + await verifyDocumentSignature(document); + + const { obligationRegistry, tokenId, network, documentId } = + await extractObligationDocumentInfo(document); + + const beneficiary = await promptAddress('beneficiary', 'initial recipient'); + const holder = await promptAddress('holder', 'initial holder'); + const { encryptedWalletPath, key, keyFile } = await promptWalletSelection(); + const remark = await promptRemark('v5'); + const encryptionKey = documentId; + + const baseResult = { + network, + address: obligationRegistry, + tokenId, + beneficiary, + holder, + remark, + encryptionKey, + maxPriorityFeePerGasScale: 1, + }; + + if (encryptedWalletPath) { + return { ...baseResult, encryptedWalletPath } as ObligationRegistryMintCommand; + } + if (keyFile) { + return { ...baseResult, keyFile } as ObligationRegistryMintCommand; + } + if (key) { + return { ...baseResult, key } as ObligationRegistryMintCommand; + } + return baseResult as ObligationRegistryMintCommand; +}; + +export const mintObligationToken = async (args: ObligationRegistryMintCommand) => { + try { + info( + `Issuing ${args.tokenId} to recipient ${args.beneficiary} and holder ${args.holder} in obligation registry ${args.address}`, + ); + + const transaction = await mintToObligationRegistry({ + ...args, + tokenId: addAddressPrefix(args.tokenId), + }); + + displayTransactionPrice(transaction as any, args.network as NetworkCmdName); + success( + `Token ${args.tokenId} minted on obligation registry ${args.address} (beneficiary ${args.beneficiary}, holder ${args.holder})`, + ); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); + return args.address; + } catch (e) { + error(getErrorMessage(e)); + } +}; + +const mintToObligationRegistry = async ({ + address, + beneficiary, + holder, + tokenId, + remark, + encryptionKey, + network, + ...rest +}: ObligationRegistryMintCommand): Promise => { + const wallet = await getWalletOrSigner({ network, ...rest }); + + const shouldProceed = await performDryRunWithConfirmation({ + network, + getTransactionCallback: async () => { + const registry = await connectToObligationRegistry({ address, wallet }); + const encryptedRemark = validateAndEncryptRemark(remark, encryptionKey); + const tx = await registry.mint.populateTransaction( + beneficiary, + holder, + tokenId, + encryptedRemark, + ); + return { ...tx, from: await wallet.getAddress() }; + }, + }); + + if (!shouldProceed) { + process.exit(0); + } + + let transaction; + if (canEstimateGasPrice(network)) { + if (!wallet.provider) { + throw new Error('Provider is required for gas estimation'); + } + const gasFees = await getGasFees({ provider: wallet.provider, ...rest }); + transaction = await mintObligationRegistry( + { obligationRegistry: address }, + wallet, + { beneficiaryAddress: beneficiary, holderAddress: holder, tokenId, remarks: remark }, + { + id: encryptionKey, + maxFeePerGas: gasFees.maxFeePerGas?.toString(), + maxPriorityFeePerGas: gasFees.maxPriorityFeePerGas?.toString(), + }, + ); + } else { + transaction = await mintObligationRegistry( + { obligationRegistry: address }, + wallet, + { beneficiaryAddress: beneficiary, holderAddress: holder, tokenId, remarks: remark }, + { id: encryptionKey }, + ); + } + + signale.await(`Waiting for transaction ${transaction.hash} to be mined`); + const receipt = (await transaction.wait()) as unknown as TransactionReceipt; + if (!receipt) { + throw new Error('Transaction receipt not found'); + } + return receipt; +}; diff --git a/src/commands/verify-obligation.ts b/src/commands/verify-obligation.ts new file mode 100644 index 0000000..4781b58 --- /dev/null +++ b/src/commands/verify-obligation.ts @@ -0,0 +1,139 @@ +import { input } from '@inquirer/prompts'; +import { + getChainId, + getObligationDocumentStatus, + isObligationRecord, + isWrappedV2Document, + isWrappedV3Document, + SignedVerifiableCredential, + SUPPORTED_CHAINS, + CHAIN_ID, + VerificationFragment, + verifyObligationDocument, +} from '@trustvc/trustvc'; +import signale from 'signale'; +import { + CaptureConsoleWarnAsync, + getSupportedNetworkNameFromId, + promptNetworkSelection, + readJsonFile, + supportedNetwork, + NetworkCmdName, +} from '../utils'; +import { FragmentType } from '../types'; +import { getResultFromFragment, handleExpiredCredentialWarning, logResultStatus } from './verify'; + +export const command = 'verify-obligation'; +export const describe = + 'Verify a BoE / obligation document via the obligation verify pipeline (not classic verify)'; + +export const handler = async () => { + try { + const signedVC = await promptQuestions(); + if (!signedVC) return; + await verifyObligation(signedVC); + } catch (err: unknown) { + signale.error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptQuestions = async (): Promise => { + const pathToSignedVC = await input({ + message: 'Please enter the path to your obligation / BoE document:', + required: true, + validate: (value: string) => { + if (!value || value.trim() === '') { + return 'Document file path is required'; + } + return true; + }, + }); + return readJsonFile(pathToSignedVC, 'document'); +}; + +export const verifyObligation = async (signedVC: SignedVerifiableCredential) => { + const isOpenAttestation = isWrappedV2Document(signedVC) || isWrappedV3Document(signedVC); + if (isOpenAttestation) { + signale.error( + 'OpenAttestation documents are not supported by verify-obligation. Use classic `trustvc verify`.', + ); + return; + } + + if (!isObligationRecord(signedVC)) { + signale.warn( + 'Document does not look like an obligation record (missing obligationRegistry). Continuing with obligation pipeline anyway.', + ); + } + + signale.info('Verifying obligation / BoE document...'); + const { result, warnings } = await verifyObligationW3C(signedVC); + if (warnings) { + handleExpiredCredentialWarning(warnings); + } + + logResultStatus(getResultFromFragment(FragmentType.DOCUMENT_INTEGRITY, result)); + logResultStatus(getResultFromFragment(FragmentType.DOCUMENT_STATUS, result)); + logResultStatus(getResultFromFragment(FragmentType.ISSUER_IDENTITY, result)); + + const obligationStatus = getObligationDocumentStatus(result); + if (obligationStatus) { + signale.info( + `Obligation document status: registry=${obligationStatus.obligationRegistry} status=${obligationStatus.status} terminationReason=${obligationStatus.terminationReason}`, + ); + } +}; + +const resolveRpcUrl = (networkName: string): string | undefined => { + const chain = Object.values(SUPPORTED_CHAINS).find((c) => c.name === networkName); + return (chain as { rpcUrl?: string } | undefined)?.rpcUrl; +}; + +const verifyObligationW3C = async ( + signedVC: SignedVerifiableCredential, +): Promise<{ result: VerificationFragment[]; warnings: unknown[][] }> => { + const run = async (rpcProviderUrl?: string) => { + const { fragments } = await verifyObligationDocument( + signedVC, + rpcProviderUrl ? { rpcProviderUrl } : {}, + ); + return fragments; + }; + + try { + const chainId = getChainId(signedVC) as CHAIN_ID | undefined; + if (chainId !== undefined && chainId !== null) { + const chainName = getSupportedNetworkNameFromId(Number(chainId)); + const rpcProviderUrl = + resolveRpcUrl(chainName) ?? resolveRpcUrl(SUPPORTED_CHAINS[chainId]?.name); + if (rpcProviderUrl) { + return await CaptureConsoleWarnAsync(() => run(rpcProviderUrl)); + } + // Fall back to public URLs for common testnets when SUPPORTED_CHAINS has no rpcUrl + const fallback: Partial> = { + [NetworkCmdName.Amoy]: 'https://rpc-amoy.polygon.technology', + [NetworkCmdName.Sepolia]: 'https://rpc.sepolia.org', + [NetworkCmdName.Local]: 'http://127.0.0.1:8545', + }; + const cmdName = Object.entries(supportedNetwork).find( + ([, v]) => v.networkName === chainName || v.networkId === Number(chainId), + )?.[0] as NetworkCmdName | undefined; + if (cmdName && fallback[cmdName]) { + return await CaptureConsoleWarnAsync(() => run(fallback[cmdName])); + } + } + } catch (err: unknown) { + signale.warn(`${err instanceof Error ? err.message : String(err)}`); + } + + const networkName = await promptNetworkSelection(); + const rpcProviderUrl = + resolveRpcUrl(networkName) ?? + (networkName === NetworkCmdName.Amoy + ? 'https://rpc-amoy.polygon.technology' + : networkName === NetworkCmdName.Sepolia + ? 'https://rpc.sepolia.org' + : undefined); + + return await CaptureConsoleWarnAsync(() => run(rpcProviderUrl)); +}; diff --git a/src/types.ts b/src/types.ts index 9b58676..d15108d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -102,6 +102,39 @@ export type TitleEscrowNominateBeneficiaryCommand = BaseTitleEscrowCommand & { newBeneficiary: string; }; +export type ObligationRegistryMintCommand = NetworkOption & + Partial & + WalletOrSignerOption & + GasPriceScale & { + address: string; + beneficiary: string; + holder: string; + tokenId: string; + remark?: string; + encryptionKey?: string; + }; + +export type BaseObligationEscrowCommand = NetworkAndWalletSignerOption & + GasPriceScale & { + obligationRegistryAddress: string; + tokenId: string; + remark?: string; + encryptionKey?: string; + }; + +export type ObligationEscrowTransferHolderCommand = BaseObligationEscrowCommand & { + newHolder: string; +}; + +export type ObligationEscrowEndorseTransferOfOwnersCommand = BaseObligationEscrowCommand & { + newHolder: string; + newOwner: string; +}; + +export type ObligationEscrowNominateBeneficiaryCommand = BaseObligationEscrowCommand & { + newBeneficiary: string; +}; + export type EncryptWalletCommand = PrivateKeyOption & { outputFile: string; }; diff --git a/src/utils/index.ts b/src/utils/index.ts index 081f499..3d83e77 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -30,3 +30,6 @@ export * from './dryRun'; // Document Verification export * from './document-verification'; + +// Obligation / BoE document helpers +export * from './obligation-document'; diff --git a/src/utils/obligation-document.ts b/src/utils/obligation-document.ts new file mode 100644 index 0000000..c4c9ceb --- /dev/null +++ b/src/utils/obligation-document.ts @@ -0,0 +1,68 @@ +import { info } from 'signale'; +import { + getObligationRegistryAddress, + getTokenId, + getChainId, + isObligationRecord, +} from '@trustvc/trustvc'; +import { SUPPORTED_CHAINS, CHAIN_ID } from './networks'; + +/** + * Extracts obligation / BoE document fields. Fails for classic ETR (tokenRegistry-only) docs. + */ +export const extractObligationDocumentInfo = async ( + document: any, +): Promise<{ + document: any; + obligationRegistry: string; + tokenId: string; + network: string; + documentId: string; +}> => { + if (!isObligationRecord(document)) { + throw new Error( + 'Document is not an obligation / BoE record. Use classic token-registry / title-escrow commands for TransferableRecords with tokenRegistry.', + ); + } + + let obligationRegistry: string | undefined; + let tokenId: string | undefined; + let chainId: CHAIN_ID | undefined; + + try { + obligationRegistry = getObligationRegistryAddress(document); + tokenId = getTokenId(document); + chainId = getChainId(document); + } catch (err) { + throw new Error( + `Failed to extract obligation document information: ${err instanceof Error ? err.message : String(err)}`, + ); + } + + if (!obligationRegistry) { + throw new Error('Document does not contain a valid obligationRegistry address'); + } + if (!tokenId) { + throw new Error('Document does not contain a valid token ID'); + } + if (!chainId) { + throw new Error('Document does not contain a valid chain ID'); + } + + const network = SUPPORTED_CHAINS[chainId].name; + const documentId = document.id || 'N/A'; + + info(`Extracted from obligation document:`); + info(` Network: ${network} (Chain ID: ${chainId})`); + info(` Obligation Registry: ${obligationRegistry}`); + info(` Token ID: ${tokenId}`); + info(` Document ID: ${documentId}`); + + return { + document, + obligationRegistry, + tokenId, + network, + documentId, + }; +}; diff --git a/tests/commands/obligation-escrow/accept-return-to-issuer.test.ts b/tests/commands/obligation-escrow/accept-return-to-issuer.test.ts new file mode 100644 index 0000000..b175b05 --- /dev/null +++ b/tests/commands/obligation-escrow/accept-return-to-issuer.test.ts @@ -0,0 +1,121 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { + acceptReturnedHandler, + promptForInputs, +} from '../../../src/commands/obligation-escrow/accept-return-to-issuer'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + acceptReturnedObligationRegistry: vi.fn(), + }; +}); + +vi.mock('../../../src/commands/obligation-escrow/shared', () => ({ + promptBaseObligationEscrowInputs: vi.fn().mockResolvedValue({ + network: 'amoy', + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + remark: 'ok', + encryptionKey: 'doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }), +})); + +const { burnPopulateTransaction, escrowAcceptReturned } = vi.hoisted(() => ({ + burnPopulateTransaction: vi.fn().mockResolvedValue({ to: '0xRegistry', data: '0xburn' }), + escrowAcceptReturned: vi.fn(), +})); + +vi.mock('../../../src/commands/helpers', () => ({ + connectToObligationEscrow: vi.fn().mockResolvedValue({ + // The escrow contract intentionally has no acceptReturned method: burning + // a returned title happens on the registry, not the escrow. + acceptReturned: escrowAcceptReturned, + }), + connectToObligationRegistry: vi.fn().mockResolvedValue({ + burn: { populateTransaction: burnPopulateTransaction }, + }), + validateAndEncryptRemark: vi.fn().mockReturnValue('0xencrypted'), +})); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getWalletOrSigner: vi.fn().mockResolvedValue({ + getAddress: vi.fn().mockResolvedValue('0xFrom'), + provider: {}, + }), + getSupportedNetwork: vi.fn().mockReturnValue({ networkId: 80002 }), + canEstimateGasPrice: vi.fn().mockReturnValue(false), + performDryRunWithConfirmation: vi.fn(async ({ getTransactionCallback }) => { + await getTransactionCallback(); + return true; + }), + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-escrow/accept-return-to-issuer', () => { + beforeEach(() => vi.clearAllMocks()); + + it('promptForInputs uses shared obligation prompts', async () => { + const result = await promptForInputs(); + expect(result.obligationRegistryAddress).toBe('0xRegistry'); + }); + + it('dry-run populates burn() on the registry contract, not the escrow', async () => { + const trustvc = await import('@trustvc/trustvc'); + (trustvc.acceptReturnedObligationRegistry as MockedFunction).mockResolvedValue({ + hash: '0xaccept-return', + wait: vi.fn().mockResolvedValue({ hash: '0xaccept-return' }), + }); + + await acceptReturnedHandler({ + network: NetworkCmdName.Amoy, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + remark: 'ok', + encryptionKey: 'doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + + expect(burnPopulateTransaction).toHaveBeenCalledWith('0x1', '0xencrypted'); + expect(escrowAcceptReturned).not.toHaveBeenCalled(); + expect(trustvc.acceptReturnedObligationRegistry).toHaveBeenCalled(); + }); +}); diff --git a/tests/commands/obligation-escrow/accept.test.ts b/tests/commands/obligation-escrow/accept.test.ts new file mode 100644 index 0000000..231bf3a --- /dev/null +++ b/tests/commands/obligation-escrow/accept.test.ts @@ -0,0 +1,85 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { acceptHandler, promptForInputs } from '../../../src/commands/obligation-escrow/accept'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + acceptObligationRegistry: vi.fn(), + }; +}); + +vi.mock('../../../src/commands/obligation-escrow/shared', () => ({ + promptBaseObligationEscrowInputs: vi.fn().mockResolvedValue({ + network: 'amoy', + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + remark: 'ok', + encryptionKey: 'doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }), +})); + +vi.mock('../../../src/commands/obligation-escrow/runTx', () => ({ + runObligationEscrowTx: vi.fn().mockResolvedValue({ hash: '0xaccept' }), +})); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-escrow/accept', () => { + beforeEach(() => vi.clearAllMocks()); + + it('promptForInputs uses shared obligation prompts', async () => { + const result = await promptForInputs(); + expect(result.obligationRegistryAddress).toBe('0xRegistry'); + }); + + it('acceptHandler runs SDK via runObligationEscrowTx', async () => { + const { runObligationEscrowTx } = await import('../../../src/commands/obligation-escrow/runTx'); + await acceptHandler({ + network: NetworkCmdName.Amoy, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + expect(runObligationEscrowTx as MockedFunction).toHaveBeenCalled(); + }); +}); diff --git a/tests/commands/obligation-escrow/discharge.test.ts b/tests/commands/obligation-escrow/discharge.test.ts new file mode 100644 index 0000000..e60089e --- /dev/null +++ b/tests/commands/obligation-escrow/discharge.test.ts @@ -0,0 +1,92 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { + dischargeHandler, + promptForInputs, +} from '../../../src/commands/obligation-escrow/discharge'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + dischargeObligationRegistry: vi.fn(), + }; +}); + +vi.mock('../../../src/commands/obligation-escrow/shared', () => ({ + promptBaseObligationEscrowInputs: vi.fn().mockResolvedValue({ + network: 'amoy', + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + remark: 'ok', + encryptionKey: 'doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }), +})); + +vi.mock('../../../src/commands/obligation-escrow/runTx', () => ({ + runObligationEscrowTx: vi.fn().mockResolvedValue({ hash: '0xdischarge' }), +})); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-escrow/discharge', () => { + beforeEach(() => vi.clearAllMocks()); + + it('promptForInputs uses shared obligation prompts', async () => { + const result = await promptForInputs(); + expect(result.obligationRegistryAddress).toBe('0xRegistry'); + }); + + it('dischargeHandler runs SDK via runObligationEscrowTx', async () => { + const { runObligationEscrowTx } = await import('../../../src/commands/obligation-escrow/runTx'); + await dischargeHandler({ + network: NetworkCmdName.Amoy, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + expect(runObligationEscrowTx as MockedFunction).toHaveBeenCalledWith( + expect.objectContaining({ + sdkParams: expect.objectContaining({ tokenId: '0x1' }), + }), + ); + }); +}); diff --git a/tests/commands/obligation-escrow/endorse-transfer-owner.test.ts b/tests/commands/obligation-escrow/endorse-transfer-owner.test.ts new file mode 100644 index 0000000..deb6ba8 --- /dev/null +++ b/tests/commands/obligation-escrow/endorse-transfer-owner.test.ts @@ -0,0 +1,95 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { + endorseHandler, + promptForInputs, +} from '../../../src/commands/obligation-escrow/endorse-transfer-owner'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + transferBeneficiaryObligationRegistry: vi.fn(), + }; +}); + +vi.mock('../../../src/commands/obligation-escrow/shared', () => ({ + promptBaseObligationEscrowInputs: vi.fn().mockResolvedValue({ + network: 'amoy', + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + encryptionKey: 'doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }), +})); + +vi.mock('../../../src/commands/obligation-escrow/runTx', () => ({ + runObligationEscrowTx: vi.fn().mockResolvedValue({ hash: '0xendorse' }), +})); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + promptAddress: vi.fn().mockResolvedValue('0xEndorsee0000000000000000000000000000001'), + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-escrow/endorse-transfer-owner', () => { + beforeEach(() => vi.clearAllMocks()); + + it('promptForInputs includes newBeneficiary', async () => { + const result = await promptForInputs(); + expect(result.newBeneficiary).toBe('0xEndorsee0000000000000000000000000000001'); + }); + + it('endorseHandler calls runObligationEscrowTx with beneficiary params', async () => { + const { runObligationEscrowTx } = await import('../../../src/commands/obligation-escrow/runTx'); + await endorseHandler({ + network: NetworkCmdName.Amoy, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + newBeneficiary: '0xEndorsee0000000000000000000000000000001', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + expect(runObligationEscrowTx as MockedFunction).toHaveBeenCalledWith( + expect.objectContaining({ + sdkParams: expect.objectContaining({ + newBeneficiaryAddress: '0xEndorsee0000000000000000000000000000001', + }), + }), + ); + }); +}); diff --git a/tests/commands/obligation-escrow/nominate-transfer-owner.test.ts b/tests/commands/obligation-escrow/nominate-transfer-owner.test.ts new file mode 100644 index 0000000..a3da87a --- /dev/null +++ b/tests/commands/obligation-escrow/nominate-transfer-owner.test.ts @@ -0,0 +1,95 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { + nominateHandler, + promptForInputs, +} from '../../../src/commands/obligation-escrow/nominate-transfer-owner'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + nominateObligationRegistry: vi.fn(), + }; +}); + +vi.mock('../../../src/commands/obligation-escrow/shared', () => ({ + promptBaseObligationEscrowInputs: vi.fn().mockResolvedValue({ + network: 'amoy', + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + encryptionKey: 'doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }), +})); + +vi.mock('../../../src/commands/obligation-escrow/runTx', () => ({ + runObligationEscrowTx: vi.fn().mockResolvedValue({ hash: '0xnominate' }), +})); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + promptAddress: vi.fn().mockResolvedValue('0xNominee00000000000000000000000000000001'), + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-escrow/nominate-transfer-owner', () => { + beforeEach(() => vi.clearAllMocks()); + + it('promptForInputs includes newBeneficiary', async () => { + const result = await promptForInputs(); + expect(result.newBeneficiary).toBe('0xNominee00000000000000000000000000000001'); + }); + + it('nominateHandler calls runObligationEscrowTx with nominee params', async () => { + const { runObligationEscrowTx } = await import('../../../src/commands/obligation-escrow/runTx'); + await nominateHandler({ + network: NetworkCmdName.Amoy, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + newBeneficiary: '0xNominee00000000000000000000000000000001', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + expect(runObligationEscrowTx as MockedFunction).toHaveBeenCalledWith( + expect.objectContaining({ + sdkParams: expect.objectContaining({ + newBeneficiaryAddress: '0xNominee00000000000000000000000000000001', + }), + }), + ); + }); +}); diff --git a/tests/commands/obligation-escrow/reject-return-to-issuer.test.ts b/tests/commands/obligation-escrow/reject-return-to-issuer.test.ts new file mode 100644 index 0000000..83c6c2c --- /dev/null +++ b/tests/commands/obligation-escrow/reject-return-to-issuer.test.ts @@ -0,0 +1,121 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { + rejectReturnedHandler, + promptForInputs, +} from '../../../src/commands/obligation-escrow/reject-return-to-issuer'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + rejectReturnedObligationRegistry: vi.fn(), + }; +}); + +vi.mock('../../../src/commands/obligation-escrow/shared', () => ({ + promptBaseObligationEscrowInputs: vi.fn().mockResolvedValue({ + network: 'amoy', + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + remark: 'ok', + encryptionKey: 'doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }), +})); + +const { restorePopulateTransaction, escrowRejectReturned } = vi.hoisted(() => ({ + restorePopulateTransaction: vi.fn().mockResolvedValue({ to: '0xRegistry', data: '0xrestore' }), + escrowRejectReturned: vi.fn(), +})); + +vi.mock('../../../src/commands/helpers', () => ({ + connectToObligationEscrow: vi.fn().mockResolvedValue({ + // The escrow contract intentionally has no rejectReturned method: restoring + // a returned title happens on the registry, not the escrow. + rejectReturned: escrowRejectReturned, + }), + connectToObligationRegistry: vi.fn().mockResolvedValue({ + restore: { populateTransaction: restorePopulateTransaction }, + }), + validateAndEncryptRemark: vi.fn().mockReturnValue('0xencrypted'), +})); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getWalletOrSigner: vi.fn().mockResolvedValue({ + getAddress: vi.fn().mockResolvedValue('0xFrom'), + provider: {}, + }), + getSupportedNetwork: vi.fn().mockReturnValue({ networkId: 80002 }), + canEstimateGasPrice: vi.fn().mockReturnValue(false), + performDryRunWithConfirmation: vi.fn(async ({ getTransactionCallback }) => { + await getTransactionCallback(); + return true; + }), + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-escrow/reject-return-to-issuer', () => { + beforeEach(() => vi.clearAllMocks()); + + it('promptForInputs uses shared obligation prompts', async () => { + const result = await promptForInputs(); + expect(result.obligationRegistryAddress).toBe('0xRegistry'); + }); + + it('dry-run populates restore() on the registry contract, not the escrow', async () => { + const trustvc = await import('@trustvc/trustvc'); + (trustvc.rejectReturnedObligationRegistry as MockedFunction).mockResolvedValue({ + hash: '0xreject-return', + wait: vi.fn().mockResolvedValue({ hash: '0xreject-return' }), + }); + + await rejectReturnedHandler({ + network: NetworkCmdName.Amoy, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + remark: 'ok', + encryptionKey: 'doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + + expect(restorePopulateTransaction).toHaveBeenCalledWith('0x1', '0xencrypted'); + expect(escrowRejectReturned).not.toHaveBeenCalled(); + expect(trustvc.rejectReturnedObligationRegistry).toHaveBeenCalled(); + }); +}); diff --git a/tests/commands/obligation-escrow/reject-transfer-holder.test.ts b/tests/commands/obligation-escrow/reject-transfer-holder.test.ts new file mode 100644 index 0000000..e596fe2 --- /dev/null +++ b/tests/commands/obligation-escrow/reject-transfer-holder.test.ts @@ -0,0 +1,92 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { + rejectTransferHolderHandler, + promptForInputs, +} from '../../../src/commands/obligation-escrow/reject-transfer-holder'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + rejectTransferHolderObligationRegistry: vi.fn(), + }; +}); + +vi.mock('../../../src/commands/obligation-escrow/shared', () => ({ + promptBaseObligationEscrowInputs: vi.fn().mockResolvedValue({ + network: 'amoy', + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + remark: 'ok', + encryptionKey: 'doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }), +})); + +vi.mock('../../../src/commands/obligation-escrow/runTx', () => ({ + runObligationEscrowTx: vi.fn().mockResolvedValue({ hash: '0xreject-transfer-holder' }), +})); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-escrow/reject-transfer-holder', () => { + beforeEach(() => vi.clearAllMocks()); + + it('promptForInputs uses shared obligation prompts', async () => { + const result = await promptForInputs(); + expect(result.obligationRegistryAddress).toBe('0xRegistry'); + }); + + it('rejectTransferHolderHandler runs SDK via runObligationEscrowTx', async () => { + const { runObligationEscrowTx } = await import('../../../src/commands/obligation-escrow/runTx'); + await rejectTransferHolderHandler({ + network: NetworkCmdName.Amoy, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + expect(runObligationEscrowTx as MockedFunction).toHaveBeenCalledWith( + expect.objectContaining({ + sdkParams: expect.objectContaining({ remarks: undefined }), + }), + ); + }); +}); diff --git a/tests/commands/obligation-escrow/reject-transfer-owner-holder.test.ts b/tests/commands/obligation-escrow/reject-transfer-owner-holder.test.ts new file mode 100644 index 0000000..d01c1e2 --- /dev/null +++ b/tests/commands/obligation-escrow/reject-transfer-owner-holder.test.ts @@ -0,0 +1,92 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { + rejectTransferOwnersHandler, + promptForInputs, +} from '../../../src/commands/obligation-escrow/reject-transfer-owner-holder'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + rejectTransferOwnersObligationRegistry: vi.fn(), + }; +}); + +vi.mock('../../../src/commands/obligation-escrow/shared', () => ({ + promptBaseObligationEscrowInputs: vi.fn().mockResolvedValue({ + network: 'amoy', + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + remark: 'ok', + encryptionKey: 'doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }), +})); + +vi.mock('../../../src/commands/obligation-escrow/runTx', () => ({ + runObligationEscrowTx: vi.fn().mockResolvedValue({ hash: '0xreject-transfer-owner-holder' }), +})); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-escrow/reject-transfer-owner-holder', () => { + beforeEach(() => vi.clearAllMocks()); + + it('promptForInputs uses shared obligation prompts', async () => { + const result = await promptForInputs(); + expect(result.obligationRegistryAddress).toBe('0xRegistry'); + }); + + it('rejectTransferOwnersHandler runs SDK via runObligationEscrowTx', async () => { + const { runObligationEscrowTx } = await import('../../../src/commands/obligation-escrow/runTx'); + await rejectTransferOwnersHandler({ + network: NetworkCmdName.Amoy, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + expect(runObligationEscrowTx as MockedFunction).toHaveBeenCalledWith( + expect.objectContaining({ + sdkParams: expect.objectContaining({ remarks: undefined }), + }), + ); + }); +}); diff --git a/tests/commands/obligation-escrow/reject-transfer-owner.test.ts b/tests/commands/obligation-escrow/reject-transfer-owner.test.ts new file mode 100644 index 0000000..9ef97d3 --- /dev/null +++ b/tests/commands/obligation-escrow/reject-transfer-owner.test.ts @@ -0,0 +1,92 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { + rejectTransferOwnerHandler, + promptForInputs, +} from '../../../src/commands/obligation-escrow/reject-transfer-owner'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + rejectTransferBeneficiaryObligationRegistry: vi.fn(), + }; +}); + +vi.mock('../../../src/commands/obligation-escrow/shared', () => ({ + promptBaseObligationEscrowInputs: vi.fn().mockResolvedValue({ + network: 'amoy', + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + remark: 'ok', + encryptionKey: 'doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }), +})); + +vi.mock('../../../src/commands/obligation-escrow/runTx', () => ({ + runObligationEscrowTx: vi.fn().mockResolvedValue({ hash: '0xreject-transfer-owner' }), +})); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-escrow/reject-transfer-owner', () => { + beforeEach(() => vi.clearAllMocks()); + + it('promptForInputs uses shared obligation prompts', async () => { + const result = await promptForInputs(); + expect(result.obligationRegistryAddress).toBe('0xRegistry'); + }); + + it('rejectTransferOwnerHandler runs SDK via runObligationEscrowTx', async () => { + const { runObligationEscrowTx } = await import('../../../src/commands/obligation-escrow/runTx'); + await rejectTransferOwnerHandler({ + network: NetworkCmdName.Amoy, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + expect(runObligationEscrowTx as MockedFunction).toHaveBeenCalledWith( + expect.objectContaining({ + sdkParams: expect.objectContaining({ remarks: undefined }), + }), + ); + }); +}); diff --git a/tests/commands/obligation-escrow/reject.test.ts b/tests/commands/obligation-escrow/reject.test.ts new file mode 100644 index 0000000..e03034b --- /dev/null +++ b/tests/commands/obligation-escrow/reject.test.ts @@ -0,0 +1,89 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { rejectHandler, promptForInputs } from '../../../src/commands/obligation-escrow/reject'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + rejectObligationRegistry: vi.fn(), + }; +}); + +vi.mock('../../../src/commands/obligation-escrow/shared', () => ({ + promptBaseObligationEscrowInputs: vi.fn().mockResolvedValue({ + network: 'amoy', + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + remark: 'ok', + encryptionKey: 'doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }), +})); + +vi.mock('../../../src/commands/obligation-escrow/runTx', () => ({ + runObligationEscrowTx: vi.fn().mockResolvedValue({ hash: '0xreject' }), +})); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-escrow/reject', () => { + beforeEach(() => vi.clearAllMocks()); + + it('promptForInputs uses shared obligation prompts', async () => { + const result = await promptForInputs(); + expect(result.obligationRegistryAddress).toBe('0xRegistry'); + }); + + it('rejectHandler runs SDK via runObligationEscrowTx', async () => { + const { runObligationEscrowTx } = await import('../../../src/commands/obligation-escrow/runTx'); + await rejectHandler({ + network: NetworkCmdName.Amoy, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + expect(runObligationEscrowTx as MockedFunction).toHaveBeenCalledWith( + expect.objectContaining({ + sdkParams: expect.objectContaining({ tokenId: '0x1' }), + }), + ); + }); +}); diff --git a/tests/commands/obligation-escrow/return-to-issuer.test.ts b/tests/commands/obligation-escrow/return-to-issuer.test.ts new file mode 100644 index 0000000..bd13c7b --- /dev/null +++ b/tests/commands/obligation-escrow/return-to-issuer.test.ts @@ -0,0 +1,68 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { returnToIssuerHandler } from '../../../src/commands/obligation-escrow/return-to-issuer'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + returnToIssuerObligationRegistry: vi.fn(), + }; +}); + +vi.mock('../../../src/commands/obligation-escrow/runTx', () => ({ + runObligationEscrowTx: vi.fn().mockResolvedValue({ hash: '0xreturn' }), +})); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-escrow/return-to-issuer', () => { + beforeEach(() => vi.clearAllMocks()); + + it('returnToIssuerHandler invokes runObligationEscrowTx', async () => { + const { runObligationEscrowTx } = await import('../../../src/commands/obligation-escrow/runTx'); + await returnToIssuerHandler({ + network: NetworkCmdName.Amoy, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + expect(runObligationEscrowTx as MockedFunction).toHaveBeenCalled(); + }); +}); diff --git a/tests/commands/obligation-escrow/status.test.ts b/tests/commands/obligation-escrow/status.test.ts new file mode 100644 index 0000000..103f7b1 --- /dev/null +++ b/tests/commands/obligation-escrow/status.test.ts @@ -0,0 +1,71 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { statusHandler } from '../../../src/commands/obligation-escrow/status'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + getObligationRegistryStatus: vi.fn().mockResolvedValue(0), + isObligationRegistryRegistered: vi.fn().mockResolvedValue(true), + getObligationEscrowTerminationReason: vi.fn().mockResolvedValue(0), + }; +}); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getWalletOrSigner: vi.fn().mockResolvedValue({ provider: {} }), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-escrow/status', () => { + beforeEach(() => vi.clearAllMocks()); + + it('reads status, registration, and termination reason', async () => { + const trustvc = await import('@trustvc/trustvc'); + await statusHandler({ + network: NetworkCmdName.Amoy, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + expect(trustvc.getObligationRegistryStatus as MockedFunction).toHaveBeenCalledWith( + { obligationRegistry: '0xRegistry' }, + expect.anything(), + { tokenId: '0x1' }, + ); + expect(trustvc.isObligationRegistryRegistered).toHaveBeenCalled(); + expect(trustvc.getObligationEscrowTerminationReason).toHaveBeenCalled(); + }); +}); diff --git a/tests/commands/obligation-escrow/transfer-holder.test.ts b/tests/commands/obligation-escrow/transfer-holder.test.ts new file mode 100644 index 0000000..4a5c7f6 --- /dev/null +++ b/tests/commands/obligation-escrow/transfer-holder.test.ts @@ -0,0 +1,95 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { + changeHolderHandler, + promptForInputs, +} from '../../../src/commands/obligation-escrow/transfer-holder'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + transferHolderObligationRegistry: vi.fn(), + }; +}); + +vi.mock('../../../src/commands/obligation-escrow/shared', () => ({ + promptBaseObligationEscrowInputs: vi.fn().mockResolvedValue({ + network: 'amoy', + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + encryptionKey: 'doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }), +})); + +vi.mock('../../../src/commands/obligation-escrow/runTx', () => ({ + runObligationEscrowTx: vi.fn().mockResolvedValue({ hash: '0xtransfer' }), +})); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + promptAddress: vi.fn().mockResolvedValue('0xNewHolder000000000000000000000000000001'), + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-escrow/transfer-holder', () => { + beforeEach(() => vi.clearAllMocks()); + + it('promptForInputs includes newHolder', async () => { + const result = await promptForInputs(); + expect(result.newHolder).toBe('0xNewHolder000000000000000000000000000001'); + }); + + it('changeHolderHandler calls runObligationEscrowTx with holder params', async () => { + const { runObligationEscrowTx } = await import('../../../src/commands/obligation-escrow/runTx'); + await changeHolderHandler({ + network: NetworkCmdName.Amoy, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + newHolder: '0xNewHolder000000000000000000000000000001', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + expect(runObligationEscrowTx as MockedFunction).toHaveBeenCalledWith( + expect.objectContaining({ + sdkParams: expect.objectContaining({ + holderAddress: '0xNewHolder000000000000000000000000000001', + }), + }), + ); + }); +}); diff --git a/tests/commands/obligation-escrow/transfer-owner-holder.test.ts b/tests/commands/obligation-escrow/transfer-owner-holder.test.ts new file mode 100644 index 0000000..05f89fb --- /dev/null +++ b/tests/commands/obligation-escrow/transfer-owner-holder.test.ts @@ -0,0 +1,101 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { + transferOwnersHandler, + promptForInputs, +} from '../../../src/commands/obligation-escrow/transfer-owner-holder'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + transferOwnersObligationRegistry: vi.fn(), + }; +}); + +vi.mock('../../../src/commands/obligation-escrow/shared', () => ({ + promptBaseObligationEscrowInputs: vi.fn().mockResolvedValue({ + network: 'amoy', + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + encryptionKey: 'doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }), +})); + +vi.mock('../../../src/commands/obligation-escrow/runTx', () => ({ + runObligationEscrowTx: vi.fn().mockResolvedValue({ hash: '0xtransfer-owner-holder' }), +})); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + promptAddress: vi + .fn() + .mockResolvedValueOnce('0xNewOwner000000000000000000000000000001') + .mockResolvedValueOnce('0xNewHolder00000000000000000000000000001'), + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-escrow/transfer-owner-holder', () => { + beforeEach(() => vi.clearAllMocks()); + + it('promptForInputs includes newOwner and newHolder', async () => { + const result = await promptForInputs(); + expect(result.newOwner).toBe('0xNewOwner000000000000000000000000000001'); + expect(result.newHolder).toBe('0xNewHolder00000000000000000000000000001'); + }); + + it('transferOwnersHandler calls runObligationEscrowTx with owner+holder params', async () => { + const { runObligationEscrowTx } = await import('../../../src/commands/obligation-escrow/runTx'); + await transferOwnersHandler({ + network: NetworkCmdName.Amoy, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + newOwner: '0xNewOwner000000000000000000000000000001', + newHolder: '0xNewHolder00000000000000000000000000001', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + expect(runObligationEscrowTx as MockedFunction).toHaveBeenCalledWith( + expect.objectContaining({ + sdkParams: expect.objectContaining({ + newBeneficiaryAddress: '0xNewOwner000000000000000000000000000001', + newHolderAddress: '0xNewHolder00000000000000000000000000001', + }), + }), + ); + }); +}); diff --git a/tests/commands/obligation-registry/deploy.test.ts b/tests/commands/obligation-registry/deploy.test.ts new file mode 100644 index 0000000..557c5bc --- /dev/null +++ b/tests/commands/obligation-registry/deploy.test.ts @@ -0,0 +1,135 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { + handler, + deployObligationRegistryContract, + promptForInputs, +} from '../../../src/commands/obligation-registry/deploy'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@inquirer/prompts', () => ({ + select: vi.fn(), + input: vi.fn(), + confirm: vi.fn(), + password: vi.fn(), +})); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + deployObligationRegistry: vi.fn(), + }; +}); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + promptNetworkSelection: vi.fn().mockResolvedValue('amoy'), + promptWalletSelection: vi.fn().mockResolvedValue({ key: '0xabc' }), + promptAddress: vi.fn(), + getWalletOrSigner: vi.fn().mockResolvedValue({ + getAddress: vi.fn().mockResolvedValue('0xWallet'), + provider: {}, + }), + performDryRunWithConfirmation: vi.fn().mockResolvedValue(true), + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + getSupportedNetwork: vi.fn().mockReturnValue({ networkId: 80002 }), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + supportedNetwork: { + amoy: { networkId: 80002 }, + }, + }; +}); + +describe('obligation-registry/deploy', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('promptForInputs', () => { + it('collects registry name, symbol, and wallet', async () => { + const { input, confirm } = await import('@inquirer/prompts'); + (input as MockedFunction) + .mockResolvedValueOnce('My BoE Registry') + .mockResolvedValueOnce('BOE'); + (confirm as MockedFunction).mockResolvedValueOnce(false); + + const result = await promptForInputs(); + expect(result.registryName).toBe('My BoE Registry'); + expect(result.registrySymbol).toBe('BOE'); + expect(result.network).toBe(NetworkCmdName.Amoy); + expect(result.key).toBe('0xabc'); + }); + }); + + describe('deployObligationRegistryContract', () => { + it('deploys via SDK and returns registry address', async () => { + const trustvc = await import('@trustvc/trustvc'); + const deployMock = trustvc.deployObligationRegistry as MockedFunction; + deployMock.mockResolvedValue({ + receipt: { hash: '0xhash', logs: [] }, + obligationRegistry: '0xRegistry', + obligationEscrowFactoryAddress: '0xFactory', + }); + + const address = await deployObligationRegistryContract({ + network: NetworkCmdName.Amoy, + registryName: 'My BoE Registry', + registrySymbol: 'BOE', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + + expect(deployMock).toHaveBeenCalled(); + expect(address).toBe('0xRegistry'); + }); + }); + + describe('handler', () => { + it('runs without throwing when prompts succeed', async () => { + const { input, confirm } = await import('@inquirer/prompts'); + (input as MockedFunction) + .mockResolvedValueOnce('My BoE Registry') + .mockResolvedValueOnce('BOE'); + (confirm as MockedFunction).mockResolvedValueOnce(false); + + const trustvc = await import('@trustvc/trustvc'); + (trustvc.deployObligationRegistry as MockedFunction).mockResolvedValue({ + receipt: { hash: '0xhash', logs: [] }, + obligationRegistry: '0xRegistry', + obligationEscrowFactoryAddress: '0xFactory', + }); + + await expect(handler()).resolves.toBeUndefined(); + }); + }); +}); diff --git a/tests/commands/obligation-registry/mint.test.ts b/tests/commands/obligation-registry/mint.test.ts new file mode 100644 index 0000000..a66070f --- /dev/null +++ b/tests/commands/obligation-registry/mint.test.ts @@ -0,0 +1,158 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { + handler, + mintObligationToken, + promptForInputs, +} from '../../../src/commands/obligation-registry/mint'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@inquirer/prompts', () => ({ + select: vi.fn(), + input: vi.fn(), + confirm: vi.fn(), + password: vi.fn(), +})); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + mintObligationRegistry: vi.fn(), + isObligationRecord: vi.fn(() => true), + getObligationRegistryAddress: vi.fn(() => '0x71D28767662cB233F887aD2Bb65d048d760bA694'), + getTokenId: vi.fn(() => '0x23f719b016c88ba1ef2e10c0718d7d0f0026b1dc6e219629f81e2f0f811c4e3e'), + getChainId: vi.fn(() => 80002), + }; +}); + +vi.mock('../../../src/commands/helpers', () => ({ + connectToObligationRegistry: vi.fn().mockResolvedValue({ + mint: { + populateTransaction: vi.fn().mockResolvedValue({ to: '0xRegistry', data: '0x' }), + }, + }), + validateAndEncryptRemark: vi.fn((r?: string) => r || '0x'), +})); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + const { default: obligationFixture } = + await import('../../fixtures/obligation/w3c-obligation-record.json'); + return { + ...actual, + promptAndReadDocument: vi.fn().mockResolvedValue(obligationFixture), + verifyDocumentSignature: vi.fn().mockResolvedValue(undefined), + extractObligationDocumentInfo: vi.fn().mockResolvedValue({ + document: obligationFixture, + obligationRegistry: '0x71D28767662cB233F887aD2Bb65d048d760bA694', + tokenId: '0x23f719b016c88ba1ef2e10c0718d7d0f0026b1dc6e219629f81e2f0f811c4e3e', + network: 'amoy', + documentId: 'urn:uuid:obligation-test-doc-id', + }), + promptAddress: vi.fn(), + promptWalletSelection: vi.fn().mockResolvedValue({ key: '0xabc' }), + promptRemark: vi.fn().mockResolvedValue('issued'), + getWalletOrSigner: vi.fn().mockResolvedValue({ + getAddress: vi.fn().mockResolvedValue('0xWallet'), + provider: {}, + }), + performDryRunWithConfirmation: vi.fn().mockResolvedValue(true), + canEstimateGasPrice: vi.fn().mockReturnValue(false), + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + addAddressPrefix: (v: string) => (v.startsWith('0x') ? v : `0x${v}`), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-registry/mint', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('promptForInputs builds mint command from obligation document', async () => { + const utils = await import('../../../src/utils'); + (utils.promptAddress as MockedFunction) + .mockResolvedValueOnce('0xBeneficiary000000000000000000000000000001') + .mockResolvedValueOnce('0xHolder0000000000000000000000000000000002'); + + const result = await promptForInputs(); + expect(result.address).toBe('0x71D28767662cB233F887aD2Bb65d048d760bA694'); + expect(result.beneficiary).toBe('0xBeneficiary000000000000000000000000000001'); + expect(result.holder).toBe('0xHolder0000000000000000000000000000000002'); + expect(result.remark).toBe('issued'); + }); + + it('mintObligationToken calls mintObligationRegistry', async () => { + const trustvc = await import('@trustvc/trustvc'); + const mintMock = trustvc.mintObligationRegistry as MockedFunction; + mintMock.mockResolvedValue({ + hash: '0xmint', + wait: vi.fn().mockResolvedValue({ hash: '0xmint' }), + }); + + await mintObligationToken({ + network: NetworkCmdName.Amoy, + address: '0x71D28767662cB233F887aD2Bb65d048d760bA694', + tokenId: '0x23f719b016c88ba1ef2e10c0718d7d0f0026b1dc6e219629f81e2f0f811c4e3e', + beneficiary: '0xBeneficiary000000000000000000000000000001', + holder: '0xHolder0000000000000000000000000000000002', + remark: 'issued', + encryptionKey: 'urn:uuid:obligation-test-doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + + expect(mintMock).toHaveBeenCalledWith( + { obligationRegistry: '0x71D28767662cB233F887aD2Bb65d048d760bA694' }, + expect.anything(), + expect.objectContaining({ + beneficiaryAddress: '0xBeneficiary000000000000000000000000000001', + holderAddress: '0xHolder0000000000000000000000000000000002', + }), + expect.objectContaining({ id: 'urn:uuid:obligation-test-doc-id' }), + ); + }); + + it('handler completes mint flow', async () => { + const utils = await import('../../../src/utils'); + (utils.promptAddress as MockedFunction) + .mockResolvedValueOnce('0xBeneficiary000000000000000000000000000001') + .mockResolvedValueOnce('0xHolder0000000000000000000000000000000002'); + + const trustvc = await import('@trustvc/trustvc'); + (trustvc.mintObligationRegistry as MockedFunction).mockResolvedValue({ + hash: '0xmint', + wait: vi.fn().mockResolvedValue({ hash: '0xmint' }), + }); + + await expect(handler()).resolves.toBeUndefined(); + }); +}); diff --git a/tests/commands/verify-obligation.test.ts b/tests/commands/verify-obligation.test.ts new file mode 100644 index 0000000..6958216 --- /dev/null +++ b/tests/commands/verify-obligation.test.ts @@ -0,0 +1,102 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { verifyObligation, promptQuestions } from '../../src/commands/verify-obligation'; +import fixture from '../fixtures/obligation/w3c-obligation-record.json'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@inquirer/prompts', () => ({ + input: vi.fn().mockResolvedValue('/tmp/doc.json'), + select: vi.fn(), + confirm: vi.fn(), + password: vi.fn(), +})); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + verifyObligationDocument: vi.fn().mockResolvedValue({ + valid: true, + fragments: [ + { type: 'DOCUMENT_INTEGRITY', status: 'VALID', name: 'W3CSignatureIntegrity', data: true }, + { + type: 'DOCUMENT_STATUS', + status: 'VALID', + name: 'ObligationRecords', + data: { + obligationRegistry: '0x71D28767662cB233F887aD2Bb65d048d760bA694', + status: 0, + terminationReason: 0, + }, + }, + { type: 'ISSUER_IDENTITY', status: 'VALID', name: 'W3CIssuerIdentity', data: true }, + ], + }), + getObligationDocumentStatus: vi.fn().mockReturnValue({ + obligationRegistry: '0x71D28767662cB233F887aD2Bb65d048d760bA694', + status: 0, + terminationReason: 0, + }), + isObligationRecord: vi.fn(() => true), + isWrappedV2Document: vi.fn(() => false), + isWrappedV3Document: vi.fn(() => false), + getChainId: vi.fn(() => 80002), + }; +}); + +vi.mock('../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + const { default: obligationFixture } = + await import('../fixtures/obligation/w3c-obligation-record.json'); + return { + ...actual, + readJsonFile: vi.fn().mockReturnValue(obligationFixture), + getSupportedNetworkNameFromId: vi.fn().mockReturnValue('amoy'), + promptNetworkSelection: vi.fn(), + CaptureConsoleWarnAsync: async (fn: () => Promise) => ({ + result: await fn(), + warnings: [], + }), + }; +}); + +describe('verify-obligation', () => { + beforeEach(() => vi.clearAllMocks()); + + it('promptQuestions reads document path', async () => { + const doc = await promptQuestions(); + expect(doc).toEqual(fixture); + }); + + it('verifyObligation runs obligation pipeline and logs status', async () => { + const trustvc = await import('@trustvc/trustvc'); + await verifyObligation(fixture as any); + expect(trustvc.verifyObligationDocument as MockedFunction).toHaveBeenCalled(); + expect(trustvc.getObligationDocumentStatus).toHaveBeenCalled(); + }); +}); diff --git a/tests/fixtures/obligation/w3c-obligation-record.json b/tests/fixtures/obligation/w3c-obligation-record.json new file mode 100644 index 0000000..f4d19c7 --- /dev/null +++ b/tests/fixtures/obligation/w3c-obligation-record.json @@ -0,0 +1,116 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://w3id.org/security/data-integrity/v2", + "https://trustvc.io/context/obligation-records-context.json", + "https://trustvc.io/context/bill-of-exchange.json" + ], + "type": ["VerifiableCredential"], + "issuer": "did:web:example.tradetrust.io", + "id": "urn:uuid:obligation-test-doc-id", + "credentialStatus": { + "type": "TransferableRecords", + "tokenNetwork": { + "chain": "MATIC", + "chainId": "80002" + }, + "obligationRegistry": "0x71D28767662cB233F887aD2Bb65d048d760bA694", + "tokenId": "23f719b016c88ba1ef2e10c0718d7d0f0026b1dc6e219629f81e2f0f811c4e3e" + }, + "credentialSubject": { + "documentIdentifier": "BOE-TEST-001", + "bookingReferenceNumber": "BK-TEST-77821", + "purchaseOrderNumber": "PO-TEST-2026-0912", + "houseWaybillDocumentIdentifier": "HWB-TEST-44102", + "documentaryCreditIdentifier": "LC-TEST-2026-88341", + "transportContractDocumentNumber": "BL-TEST-2026-0781", + "uniqueConsignmentReference": "UCR-TEST-2026-004812", + "contractNumber": "CTR-TEST-2026-017", + "invoiceNumber": "INV-TEST-2026-0456", + "freightForwarderReferenceNumber": "FF-TEST-99021", + "issueDate": "2026-07-01", + "documentaryCreditIssueDate": "2026-06-15", + "documentaryCreditEffectiveEndDate": "2026-10-15", + "paymentDueDate": "2026-09-29", + "buyer": { + "name": "Fairview Industries Inc.", + "address": "1201 Market Street, Suite 900, Wilmington, DE 19801, USA" + }, + "invoicee": { + "name": "Fairview Industries Inc.", + "address": "1201 Market Street, Suite 900, Wilmington, DE 19801, USA" + }, + "sellersBank": { + "name": "DBS Bank Ltd", + "address": "12 Marina Boulevard, Marina Bay Financial Centre Tower 3, Singapore 018982" + }, + "importer": { + "name": "Fairview Industries Inc.", + "address": "1201 Market Street, Suite 900, Wilmington, DE 19801, USA" + }, + "exporter": { + "name": "Meridian Commodities Pte Ltd", + "address": "8 Marina Boulevard, #24-01, Singapore 018981" + }, + "documentaryCreditApplicantAgentBank": { + "name": "JPMorgan Chase Bank, N.A.", + "address": "383 Madison Avenue, New York, NY 10179, USA" + }, + "documentaryCreditAvailableBank": { + "name": "DBS Bank Ltd", + "address": "12 Marina Boulevard, Marina Bay Financial Centre Tower 3, Singapore 018982" + }, + "documentaryCreditBeneficiary": { + "name": "Meridian Commodities Pte Ltd", + "address": "8 Marina Boulevard, #24-01, Singapore 018981" + }, + "documentaryCreditDrawee": { + "name": "Fairview Industries Inc.", + "address": "1201 Market Street, Suite 900, Wilmington, DE 19801, USA" + }, + "seller": { + "name": "Meridian Commodities Pte Ltd", + "address": "8 Marina Boulevard, #24-01, Singapore 018981" + }, + "documentaryCreditReimbursingBank": { + "name": "Citibank N.A.", + "address": "388 Greenwich Street, New York, NY 10013, USA" + }, + "payee": { + "name": "Meridian Commodities Pte Ltd", + "address": "8 Marina Boulevard, #24-01, Singapore 018981" + }, + "buyersBank": { + "name": "Bank of America, N.A.", + "address": "100 North Tryon Street, Charlotte, NC 28255, USA" + }, + "drawer": { + "name": "Meridian Commodities Pte Ltd", + "address": "8 Marina Boulevard, #24-01, Singapore 018981" + }, + "drawee": { + "name": "Fairview Industries Inc.", + "address": "1201 Market Street, Suite 900, Wilmington, DE 19801, USA" + }, + "guarantor": { + "name": "Trade Guarantee Insurance Co.", + "address": "1 Raffles Place, #40-00, Singapore 048616" + }, + "paymentLocation": "Singapore", + "placeOfIssue": "Singapore", + "authentication": "Authenticated by Wei Ling Tan, Director of Trade Finance, Meridian Commodities Pte Ltd", + "paymentTerm": "90 days after sight", + "documentEndorsement": "Endorsed to the order of Meridian Commodities Pte Ltd", + "paymentMethod": "TT", + "monetaryAmount": "128500.00", + "currencyQualifier": "invoice currency", + "currencyCode": "USD" + }, + "proof": { + "type": "DataIntegrityProof", + "cryptosuite": "ecdsa-sd-2023", + "proofPurpose": "assertionMethod", + "verificationMethod": "did:web:example.tradetrust.io#key-1", + "proofValue": "uTEST" + } +} From 99ffec9bd60044ab5e467538708d552d4b8944b1 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Tue, 28 Jul 2026 06:30:54 +0530 Subject: [PATCH 02/20] docs: update README for improved clarity and formatting --- README.md | 490 +++++++++++++++++++++++++++--------------------------- 1 file changed, 242 insertions(+), 248 deletions(-) diff --git a/README.md b/README.md index fa3c3dd..8f6e00d 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,15 @@ A comprehensive command-line interface for managing W3C Verifiable Credentials, - ✅ **Multi-Network Support**: Ethereum, Polygon, XDC, Stability, and Astron networks - ✅ **Interactive CLI**: User-friendly prompts for all operations + + ## Powered By This CLI leverages the TrustVC package: -- [`@trustvc/trustvc`](https://github.com/TrustVC/trustvc) - Core library for W3C credentials, OpenAttestation, token registries, and blockchain operations +- `[@trustvc/trustvc](https://github.com/TrustVC/trustvc)` - Core library for W3C credentials, OpenAttestation, token registries, and blockchain operations + + ## Table of Contents @@ -41,6 +45,8 @@ This CLI leverages the TrustVC package: - [License](#license) - [Obligation Registry user guide](#obligation-registry-user-guide) + + ## Prerequisites - **Node.js 22+** — Download from [nodejs.org](https://nodejs.org) or use [nvm](https://github.com/nvm-sh/nvm): @@ -50,6 +56,8 @@ nvm install 22 nvm use 22 ``` + + ## Installation Install the CLI globally: @@ -64,8 +72,12 @@ Or run a single command without installing: npx @trustvc/trustvc-cli ``` + + ## Quick Start + + ### W3C Verifiable Credentials ```sh @@ -88,6 +100,8 @@ trustvc credential-status-create trustvc credential-status-update ``` + + ### OpenAttestation Documents ```sh @@ -110,6 +124,8 @@ trustvc oa-encrypt trustvc oa-decrypt ``` + + ### Wallet Management ```sh @@ -123,6 +139,8 @@ trustvc wallet encrypt trustvc wallet decrypt ``` + + ### Document Store ```sh @@ -145,6 +163,8 @@ trustvc document-store revoke-role trustvc document-store transfer-ownership ``` + + ### Transaction ```sh @@ -156,6 +176,8 @@ trustvc transaction cancel [options] # e.g. trustvc transaction cancel --transaction-hash 0x... --network sepolia --encrypted-wallet-path ./wallet.json ``` + + ### Token Registry & Title Escrow ```sh @@ -190,6 +212,8 @@ trustvc title-escrow reject-transfer-owner trustvc title-escrow reject-transfer-owner-holder ``` + + ### Obligation Registry & Escrow (BoE) ```sh @@ -221,113 +245,119 @@ trustvc obligation-escrow reject-return-to-issuer trustvc verify-obligation ``` + + ## How It Works + + ### W3C Credentials - **Key Pair Generation**: Uses `generateKeyPair` from `@trustvc/trustvc` to create cryptographic key pairs supporting ECDSA-SD-2023 and BBS-2023 cryptosuites in Multikey format. - - **DID Creation**: Uses `issueDID` to generate did:web identifiers, allowing self-hosted DIDs as unique identifiers in decentralized systems. - - **Credential Signing**: Uses `signW3C` to sign verifiable credentials with did:web identifiers and modern cryptosuites. - - **Credential Verification**: Uses `verifyDocument` to verify W3C verifiable credentials. - - **Credential Status**: Provides commands to create and update W3C credential status lists for managing credential revocation and suspension. + + ### OpenAttestation - **Document Signing**: Uses `signOA` to cryptographically sign OpenAttestation v2 and v3 documents with private keys. - - **Document Verification**: Uses `verifyDocument` to verify OpenAttestation documents. - - **Document Wrapping**: Uses `wrapOA` to wrap OpenAttestation documents. - - **Document Unwrapping**: Uses `unwrapOA` to unwrap OpenAttestation documents. - - **Document Encryption**: Uses `oa-encrypt` to encrypt OA documents for safe sharing; use `oa-decrypt` with the same key to recover the document. + + ### Blockchain Operations - **Token Registry**: Deploy token registry contracts and mint document hashes (tokenIds) to blockchain-based token registries across multiple networks (Ethereum, Polygon, XDC, Stability, Astron). - - **Document Store**: Deploy document store contracts and use `documentStoreIssue` and `documentStoreRevoke` to issue and revoke document hashes in deployed contracts. - - **Transaction Cancel**: Cancel a pending transaction by replacing it with a 0-value transaction to yourself (same nonce, higher gas price). Supports specifying by transaction hash or by nonce and gas price. - - **Title Escrow**: Provides comprehensive transferable records management including holder transfers, beneficiary nominations, endorsements, returns, and rejections using smart contracts. - - **Obligation Registry (BoE)**: Separate command trees for electronic Bill of Exchange flows — `obligation-registry` (deploy/mint), `obligation-escrow` (accept/reject/discharge, transfers, return), and `verify-obligation` (dedicated BoE verify pipeline). Do not use classic `token-registry` / `title-escrow` / `verify` for obligation documents. See [Obligation Registry user guide](#obligation-registry-user-guide). + + ## Commands + + ### Available Commands -| Category | Command | Description | -| ------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------- | -| **W3C Credentials** | [`key-pair-generation`](#key-pair-generation) | Generate cryptographic key pairs (ECDSA-SD-2023, BBS-2023) | -| | [`did-web`](#did-web) | Create did:web identifiers from key pairs | -| | [`w3c-sign`](#w3c-sign) | Sign W3C verifiable credentials | -| | [`verify`](#verify) | Verify W3C verifiable credentials | -| | [`credential-status-create`](#credential-status-create) | Create credential status lists | -| | [`credential-status-update`](#credential-status-update) | Update credential status (revoke/suspend) | -| **OpenAttestation** | [`oa-sign`](#oa-sign) | Sign OpenAttestation v2/v3 documents | -| | [`verify`](#verify) | Verify OpenAttestation documents | -| | [`oa-wrap`](#oa-wrap) | Wrap OpenAttestation documents | -| | [`oa-unwrap`](#oa-unwrap) | Unwrap OpenAttestation documents | -| | [`oa-encrypt`](#oa-encrypt) | Encrypt an OA document for safe sharing and storage | -| | [`oa-decrypt`](#oa-decrypt) | Decrypt an OA document encrypted with oa-encrypt | -| **Token Registry** | [`mint`](#mint) | Mint tokens to blockchain registries | -| | [`token-registry deploy`](#token-registry-deploy) | Deploy token registry contracts | -| | [`mint`](#mint) | Mint tokens to blockchain registries | -| | `token-registry mint` | Alternative: `mint` | -| **Document Store** | [`document-store deploy`](#document-store-deploy) | Deploy document store contracts | -| | [`document-store issue`](#document-store-issue) | Issue document hashes | -| | [`document-store revoke`](#document-store-revoke) | Revoke document hashes | -| | [`document-store grant-role`](#document-store-grant-role) | Grant roles to accounts | -| | [`document-store revoke-role`](#document-store-revoke-role) | Revoke roles from accounts | -| | [`document-store transfer-ownership`](#document-store-transfer-ownership) | Transfer document store ownership | -| **Transaction** | [`transaction cancel`](#transaction-cancel) | Cancel a pending transaction | -| **Wallet** | [`wallet create`](#wallet-create) | Create a new encrypted wallet file | -| | [`wallet encrypt`](#wallet-encrypt) | Encrypt a wallet using a private key | -| | [`wallet decrypt`](#wallet-decrypt) | Decrypt an encrypted wallet file | -| **Title Escrow** | [`transfer-holder`](#title-escrow-transfer-holder) | Transfer document holder | -| | `title-escrow transfer-holder` | Alternative: `transfer-holder` | -| | [`nominate-transfer-owner`](#title-escrow-nominate-transfer-owner) | Nominate new beneficiary | -| | `title-escrow nominate-transfer-owner` | Alternative: `nominate-transfer-owner` | -| | [`endorse-transfer-owner`](#title-escrow-endorse-transfer-owner) | Endorse beneficiary change | -| | `title-escrow endorse-transfer-owner` | Alternative: `endorse-transfer-owner` | -| | [`transfer-owner-holder`](#title-escrow-transfer-owner-holder) | Endorse full ownership transfer | -| | `title-escrow transfer-owner-holder` | Alternative: `transfer-owner-holder` | -| | [`return-to-issuer`](#title-escrow-return-to-issuer) | Return document to issuer | -| | `title-escrow return-to-issuer` | Alternative: `return-to-issuer` | -| | [`accept-return-to-issuer`](#title-escrow-accept-return-to-issuer) | Accept returned document | -| | `title-escrow accept-return-to-issuer` | Alternative: `accept-return-to-issuer` | -| | [`reject-return-to-issuer`](#title-escrow-reject-return-to-issuer) | Reject returned document | -| | `title-escrow reject-return-to-issuer` | Alternative: `reject-return-to-issuer` | -| | [`reject-transfer-holder`](#title-escrow-reject-transfer-holder) | Reject holder transfer | -| | `title-escrow reject-transfer-holder` | Alternative: `reject-transfer-holder` | -| | [`reject-transfer-owner`](#title-escrow-reject-transfer-owner) | Reject owner transfer | -| | [`reject-transfer-owner-holder`](#title-escrow-reject-transfer-owner-holder) | Reject full transfer | -| | `title-escrow reject-transfer-owner-holder` | Alternative: `reject-transfer-owner-holder` | -| **Obligation / BoE**| [`obligation-registry deploy`](#obligation-registry-deploy) | Deploy Obligation Registry | -| | [`obligation-registry mint`](#obligation-registry-mint) | Mint BoE token to obligationRegistry | -| | [`obligation-escrow accept`](#obligation-escrow-accept) | Accept obligation | -| | [`obligation-escrow reject`](#obligation-escrow-reject) | Reject obligation | -| | [`obligation-escrow discharge`](#obligation-escrow-discharge) | Discharge obligation | -| | [`obligation-escrow status`](#obligation-escrow-status) | Read obligation / escrow status | -| | [`obligation-escrow transfer-holder`](#obligation-escrow-transfer-holder) | Transfer BoE holder | -| | `obligation-escrow nominate-transfer-owner` | Nominate BoE beneficiary | -| | `obligation-escrow endorse-transfer-owner` | Endorse BoE beneficiary change | -| | `obligation-escrow transfer-owner-holder` | Endorse full BoE ownership transfer | -| | `obligation-escrow return-to-issuer` | Return BoE to issuer | -| | `obligation-escrow accept-return-to-issuer` | Accept returned BoE | -| | `obligation-escrow reject-return-to-issuer` | Reject returned BoE | -| | `obligation-escrow reject-transfer-*` | Reject BoE transfer requests | -| | [`verify-obligation`](#verify-obligation) | Verify BoE via obligation pipeline | + +| Category | Command | Description | +| -------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------- | +| **W3C Credentials** | `[key-pair-generation](#key-pair-generation)` | Generate cryptographic key pairs (ECDSA-SD-2023, BBS-2023) | +| | `[did-web](#did-web)` | Create did:web identifiers from key pairs | +| | `[w3c-sign](#w3c-sign)` | Sign W3C verifiable credentials | +| | `[verify](#verify)` | Verify W3C verifiable credentials | +| | `[credential-status-create](#credential-status-create)` | Create credential status lists | +| | `[credential-status-update](#credential-status-update)` | Update credential status (revoke/suspend) | +| **OpenAttestation** | `[oa-sign](#oa-sign)` | Sign OpenAttestation v2/v3 documents | +| | `[verify](#verify)` | Verify OpenAttestation documents | +| | `[oa-wrap](#oa-wrap)` | Wrap OpenAttestation documents | +| | `[oa-unwrap](#oa-unwrap)` | Unwrap OpenAttestation documents | +| | `[oa-encrypt](#oa-encrypt)` | Encrypt an OA document for safe sharing and storage | +| | `[oa-decrypt](#oa-decrypt)` | Decrypt an OA document encrypted with oa-encrypt | +| **Token Registry** | `[mint](#mint)` | Mint tokens to blockchain registries | +| | `[token-registry deploy](#token-registry-deploy)` | Deploy token registry contracts | +| | `[mint](#mint)` | Mint tokens to blockchain registries | +| | `token-registry mint` | Alternative: `mint` | +| **Document Store** | `[document-store deploy](#document-store-deploy)` | Deploy document store contracts | +| | `[document-store issue](#document-store-issue)` | Issue document hashes | +| | `[document-store revoke](#document-store-revoke)` | Revoke document hashes | +| | `[document-store grant-role](#document-store-grant-role)` | Grant roles to accounts | +| | `[document-store revoke-role](#document-store-revoke-role)` | Revoke roles from accounts | +| | `[document-store transfer-ownership](#document-store-transfer-ownership)` | Transfer document store ownership | +| **Transaction** | `[transaction cancel](#transaction-cancel)` | Cancel a pending transaction | +| **Wallet** | `[wallet create](#wallet-create)` | Create a new encrypted wallet file | +| | `[wallet encrypt](#wallet-encrypt)` | Encrypt a wallet using a private key | +| | `[wallet decrypt](#wallet-decrypt)` | Decrypt an encrypted wallet file | +| **Title Escrow** | `[transfer-holder](#title-escrow-transfer-holder)` | Transfer document holder | +| | `title-escrow transfer-holder` | Alternative: `transfer-holder` | +| | `[nominate-transfer-owner](#title-escrow-nominate-transfer-owner)` | Nominate new beneficiary | +| | `title-escrow nominate-transfer-owner` | Alternative: `nominate-transfer-owner` | +| | `[endorse-transfer-owner](#title-escrow-endorse-transfer-owner)` | Endorse beneficiary change | +| | `title-escrow endorse-transfer-owner` | Alternative: `endorse-transfer-owner` | +| | `[transfer-owner-holder](#title-escrow-transfer-owner-holder)` | Endorse full ownership transfer | +| | `title-escrow transfer-owner-holder` | Alternative: `transfer-owner-holder` | +| | `[return-to-issuer](#title-escrow-return-to-issuer)` | Return document to issuer | +| | `title-escrow return-to-issuer` | Alternative: `return-to-issuer` | +| | `[accept-return-to-issuer](#title-escrow-accept-return-to-issuer)` | Accept returned document | +| | `title-escrow accept-return-to-issuer` | Alternative: `accept-return-to-issuer` | +| | `[reject-return-to-issuer](#title-escrow-reject-return-to-issuer)` | Reject returned document | +| | `title-escrow reject-return-to-issuer` | Alternative: `reject-return-to-issuer` | +| | `[reject-transfer-holder](#title-escrow-reject-transfer-holder)` | Reject holder transfer | +| | `title-escrow reject-transfer-holder` | Alternative: `reject-transfer-holder` | +| | `[reject-transfer-owner](#title-escrow-reject-transfer-owner)` | Reject owner transfer | +| | `[reject-transfer-owner-holder](#title-escrow-reject-transfer-owner-holder)` | Reject full transfer | +| | `title-escrow reject-transfer-owner-holder` | Alternative: `reject-transfer-owner-holder` | +| **Obligation / BoE** | `[obligation-registry deploy](#obligation-registry-deploy)` | Deploy Obligation Registry | +| | `[obligation-registry mint](#obligation-registry-mint)` | Mint BoE token to obligationRegistry | +| | `[obligation-escrow accept](#obligation-escrow-accept)` | Accept obligation | +| | `[obligation-escrow reject](#obligation-escrow-reject)` | Reject obligation | +| | `[obligation-escrow discharge](#obligation-escrow-discharge)` | Discharge obligation | +| | `[obligation-escrow status](#obligation-escrow-status)` | Read obligation / escrow status | +| | `[obligation-escrow transfer-holder](#obligation-escrow-transfer-holder)` | Transfer BoE holder | +| | `obligation-escrow nominate-transfer-owner` | Nominate BoE beneficiary | +| | `obligation-escrow endorse-transfer-owner` | Endorse BoE beneficiary change | +| | `obligation-escrow transfer-owner-holder` | Endorse full BoE ownership transfer | +| | `obligation-escrow return-to-issuer` | Return BoE to issuer | +| | `obligation-escrow accept-return-to-issuer` | Accept returned BoE | +| | `obligation-escrow reject-return-to-issuer` | Reject returned BoE | +| | `obligation-escrow reject-transfer-*` | Reject BoE transfer requests | +| | `[verify-obligation](#verify-obligation)` | Verify BoE via obligation pipeline | + + + --- + + ### Wallet/Private Key Options All title-escrow, obligation-registry, obligation-escrow, token registry, document-store, and transaction commands require a wallet or private key to sign transactions. You can provide your private key in one of the following ways: @@ -341,10 +371,11 @@ All title-escrow, obligation-registry, obligation-escrow, token registry, docume --- + + ### Detailed Command Reference -
-

transaction cancel

+#### transaction cancel Cancels a pending transaction by replacing it with a 0-value transaction to yourself using the same nonce and a higher gas price (replace-by-fee). This action is irreversible. @@ -357,11 +388,9 @@ trustvc transaction cancel You will be prompted for: 1. **How to specify the pending transaction** - - **By transaction hash (recommended)** – Enter the pending transaction hash (0x...). Nonce and gas price are fetched from the network and the gas price is increased by 100% for the replacement. - - **By nonce and gas price** – Enter the pending transaction nonce and a higher gas price (wei) for the replacement. Use this when the pending transaction uses EIP-1559 (no legacy `gasPrice`) or when you prefer to set the replacement gas manually. - + - **By transaction hash (recommended)** – Enter the pending transaction hash (0x...). Nonce and gas price are fetched from the network and the gas price is increased by 100% for the replacement. + - **By nonce and gas price** – Enter the pending transaction nonce and a higher gas price (wei) for the replacement. Use this when the pending transaction uses EIP-1559 (no legacy `gasPrice`) or when you prefer to set the replacement gas manually. 2. **Network** – Select the network (e.g. Sepolia, Mainnet). - 3. **Wallet / private key** – Choose encrypted wallet file, environment variable (OA_PRIVATE_KEY), key file, or enter the private key. **With options (non-interactive):** @@ -393,10 +422,9 @@ trustvc transaction cancel \ **Note:** If the pending transaction uses EIP-1559 (maxFeePerGas / maxPriorityFeePerGas), it has no legacy `gasPrice`. In that case, specify the transaction by **nonce and gas price** and set a gas price (in wei) for the replacement. -
-
-

key-pair-generation

+ +#### key-pair-generation Generates cryptographic key pairs for modern cryptosuites (ECDSA-SD-2023, BBS-2023). @@ -420,10 +448,9 @@ Creates `keypair.json` containing: - `secretKeyMultibase`: Secret key in multibase format - `seedBase58`: Seed (if provided for BBS-2023) -
-
-

did-web

+ +#### did-web Generates a did:web identifier from an existing key pair. @@ -445,10 +472,9 @@ trustvc did-web - `wellknown.json`: DID document for hosting at `/.well-known/did.json` - `didKeyPairs.json`: Key pair information with DID references -
-
-

w3c-sign

+ +#### w3c-sign Signs a W3C verifiable credential using a did:web identifier. @@ -468,10 +494,9 @@ trustvc w3c-sign **Output:** Creates `signed_vc.json` with cryptographic proof. -
-
-

verify

+ +#### verify Verifies a W3C or OA document using respective verification methods. @@ -495,10 +520,9 @@ Verifies the document integrity, status, and issuer identity. - OpenAttestation v2 - OpenAttestation v3 -
-
-

credential-status-create

+ +#### credential-status-create Creates a new W3C credential status list for managing revocation. @@ -519,10 +543,9 @@ trustvc credential-status-create **Output:** Signed credential status list file. -
-
-

credential-status-update

+ +#### credential-status-update Updates an existing W3C credential status list to revoke or suspend credentials. @@ -542,10 +565,9 @@ trustvc credential-status-update **Output:** Updated credential status list file. -
-
-

oa-sign

+ +#### oa-sign Signs OpenAttestation v2 or v3 documents with a private key. @@ -573,10 +595,9 @@ Signed OpenAttestation documents in the specified directory. - OpenAttestation v2 - OpenAttestation v3 -
-
-

oa-wrap

+ +#### oa-wrap Wraps OpenAttestation v2 or v3 documents @@ -600,10 +621,9 @@ Wrapped OpenAttestation document(s) in the specified directory. - OpenAttestation v2 - OpenAttestation v3 -
-
-

oa-unwrap

+ +#### oa-unwrap Unwraps OpenAttestation v2 or v3 documents @@ -626,10 +646,9 @@ Unwrapped OpenAttestation document(s) in the specified directory. - OpenAttestation v2 - OpenAttestation v3 -
-
-

oa-encrypt

+ +#### oa-encrypt Encrypts an Open Attestation document for safe sharing and storage. You will be prompted for an encryption key — remember it to decrypt later. @@ -654,10 +673,9 @@ Writes an encrypted document file containing `type: "encrypted-document"` and a - OpenAttestation v2 (raw or wrapped) - OpenAttestation v3 (raw or wrapped) -
-
-

oa-decrypt

+ +#### oa-decrypt Decrypts an Open Attestation document that was encrypted using `oa-encrypt`. You will be prompted for the decryption key. @@ -677,10 +695,9 @@ trustvc oa-decrypt Writes the decrypted Open Attestation document (raw OA v2/v3 or wrapped OA v2/v3) to the specified path. Fails if the key is wrong or the file is not a valid encrypted OA document. -
-
-

mint

+ +#### mint Mints a document hash (tokenId) to a token registry smart contract. @@ -693,7 +710,7 @@ trustvc mint **Interactive Prompts:** - Path to TT/JSON document file (or manual input) - - _Network, token registry address, token ID, and document ID are extracted from the document_ + - *Network, token registry address, token ID, and document ID are extracted from the document* - Beneficiary address (initial recipient) - Holder address (initial holder) - Wallet/private key option @@ -710,10 +727,9 @@ Transaction receipt with hash, block number, gas used, and explorer link. - Stability (Mainnet, Testnet) - Astron (Mainnet, Testnet) -
-
-

token-registry deploy

+ +#### token-registry deploy Deploys a token registry contract on the blockchain. @@ -742,10 +758,9 @@ Transaction receipt with deployed contract address, hash, block number, gas used - Stability (Mainnet, Testnet) - Astron (Mainnet, Testnet) -
-
-

token-registry mint

+ +#### token-registry mint Alternative command for minting tokens. Functionally identical to `mint`. @@ -759,10 +774,9 @@ trustvc mint trustvc token-registry mint ``` -
-
-

document-store deploy

+ +#### document-store deploy Deploys a document store contract on the blockchain. @@ -790,10 +804,9 @@ Transaction receipt with contract address, hash, block number, gas used, and exp - Stability (Mainnet, Testnet) - Astron (Mainnet, Testnet) -
-
-

document-store issue

+ +#### document-store issue Issues a document hash to a deployed document store. @@ -811,10 +824,9 @@ trustvc document-store issue **Output:** Transaction receipt confirming the hash issuance. -
-
-

document-store revoke

+ +#### document-store revoke Revokes a document hash from a deployed document store. @@ -832,10 +844,9 @@ trustvc document-store revoke **Output:** Transaction receipt confirming the hash revocation. -
-
-

document-store grant-role

+ +#### document-store grant-role Grants a role (ISSUER_ROLE, REVOKER_ROLE, or DEFAULT_ADMIN_ROLE) to an account in a deployed document store. @@ -864,10 +875,9 @@ Transaction receipt with hash, block number, gas used, and explorer link. - Stability (Mainnet, Testnet) - Astron (Mainnet, Testnet) -
-
-

document-store revoke-role

+ +#### document-store revoke-role Revokes a role (ISSUER_ROLE, REVOKER_ROLE, or DEFAULT_ADMIN_ROLE) from an account in a deployed document store. @@ -896,10 +906,9 @@ Transaction receipt with hash, block number, gas used, and explorer link. - Stability (Mainnet, Testnet) - Astron (Mainnet, Testnet) -
-
-

document-store transfer-ownership

+ +#### document-store transfer-ownership Transfers ownership of a document store contract to a new owner. This grants DEFAULT_ADMIN_ROLE to the new owner and revokes it from the current owner. @@ -926,10 +935,9 @@ Transaction receipts for both grant and revoke operations with hashes, block num - Stability (Mainnet, Testnet) - Astron (Mainnet, Testnet) -
-
-

wallet create

+ +#### wallet create Creates a new encrypted wallet file with a randomly generated private key. @@ -974,10 +982,9 @@ Creates `wallet.json` containing the encrypted wallet in the specified directory ⚠ IMPORTANT: If you lose your password, you will not be able to recover your wallet! ``` -
-
-

wallet encrypt

+ +#### wallet encrypt Encrypts an existing private key into a secure wallet file. @@ -1028,10 +1035,9 @@ Creates `wallet.json` containing the encrypted wallet in the specified directory ⚠ IMPORTANT: If you lose your password, you will not be able to recover your wallet! ``` -
-
-

wallet decrypt

+ +#### wallet decrypt Decrypts an encrypted wallet file and displays the private key and mnemonic phrase. @@ -1085,10 +1091,9 @@ Displays the decrypted wallet information: ⚠ IMPORTANT: Store this information securely and delete it from your terminal history! ``` -
-
-

title-escrow transfer-holder

+ +#### title-escrow transfer-holder Transfers the holder of a transferable record to a new address. @@ -1108,7 +1113,7 @@ trustvc title-escrow transfer-holder **Interactive Prompts:** - Path to TT/JSON document file (or manual input) - - _Network, token registry address, token ID, and document ID are extracted from the document_ + - *Network, token registry address, token ID, and document ID are extracted from the document* - New holder address - Wallet/private key option - Remark (optional, V5 registries only - will be encrypted with document ID as encryption key) @@ -1116,10 +1121,9 @@ trustvc title-escrow transfer-holder **Output:** Transaction receipt confirming holder transfer. -
-
-

title-escrow nominate-transfer-owner

+ +#### title-escrow nominate-transfer-owner Nominates a new beneficiary (owner) for the transferable record. @@ -1139,7 +1143,7 @@ trustvc title-escrow nominate-transfer-owner **Interactive Prompts:** - Path to TT/JSON document file (or manual input) - - _Network, token registry address, token ID, and document ID are extracted from the document_ + - *Network, token registry address, token ID, and document ID are extracted from the document* - New beneficiary address - Wallet/private key option - Remark (optional, V5 registries only - will be encrypted with document ID as encryption key) @@ -1147,10 +1151,9 @@ trustvc title-escrow nominate-transfer-owner **Output:** Transaction receipt confirming beneficiary nomination. -
-
-

title-escrow endorse-transfer-owner

+ +#### title-escrow endorse-transfer-owner Endorses the change of beneficiary (owner) for the transferable record. @@ -1170,7 +1173,7 @@ trustvc title-escrow endorse-transfer-owner **Interactive Prompts:** - Path to TT/JSON document file (or manual input) - - _Network, token registry address, token ID, and document ID are extracted from the document_ + - *Network, token registry address, token ID, and document ID are extracted from the document* - New beneficiary address - Wallet/private key option - Remark (optional, V5 registries only - will be encrypted with document ID as encryption key) @@ -1178,10 +1181,9 @@ trustvc title-escrow endorse-transfer-owner **Output:** Transaction receipt confirming beneficiary endorsement. -
-
-

title-escrow transfer-owner-holder

+ +#### title-escrow transfer-owner-holder Endorses the transfer of both beneficiary and holder to new addresses. @@ -1201,7 +1203,7 @@ trustvc title-escrow transfer-owner-holder **Interactive Prompts:** - Path to TT/JSON document file (or manual input) - - _Network, token registry address, token ID, and document ID are extracted from the document_ + - *Network, token registry address, token ID, and document ID are extracted from the document* - New beneficiary address - New holder address - Wallet/private key option @@ -1210,10 +1212,9 @@ trustvc title-escrow transfer-owner-holder **Output:** Transaction receipt confirming full ownership transfer. -
-
-

title-escrow return-to-issuer

+ +#### title-escrow return-to-issuer Returns the transferable record to the issuer. @@ -1233,17 +1234,16 @@ trustvc title-escrow return-to-issuer **Interactive Prompts:** - Path to TT/JSON document file (or manual input) - - _Network, token registry address, token ID, and document ID are extracted from the document_ + - *Network, token registry address, token ID, and document ID are extracted from the document* - Wallet/private key option - Remark (optional, V5 registries only - will be encrypted with document ID as encryption key) **Output:** Transaction receipt confirming document return. -
-
-

title-escrow accept-return-to-issuer

+ +#### title-escrow accept-return-to-issuer Accepts a returned transferable record (issuer action). @@ -1263,17 +1263,16 @@ trustvc title-escrow accept-return-to-issuer **Interactive Prompts:** - Path to TT/JSON document file (or manual input) - - _Network, token registry address, token ID, and document ID are extracted from the document_ + - *Network, token registry address, token ID, and document ID are extracted from the document* - Wallet/private key option - Remark (optional, V5 registries only - will be encrypted with document ID as encryption key) **Output:** Transaction receipt confirming acceptance. -
-
-

title-escrow reject-return-to-issuer

+ +#### title-escrow reject-return-to-issuer Rejects a returned transferable record (issuer action). @@ -1293,17 +1292,16 @@ trustvc title-escrow reject-return-to-issuer **Interactive Prompts:** - Path to TT/JSON document file (or manual input) - - _Network, token registry address, token ID, and document ID are extracted from the document_ + - *Network, token registry address, token ID, and document ID are extracted from the document* - Wallet/private key option - Remark (optional, V5 registries only - will be encrypted with document ID as encryption key) **Output:** Transaction receipt confirming rejection. -
-
-

title-escrow reject-transfer-holder

+ +#### title-escrow reject-transfer-holder Rejects a holder transfer request. @@ -1323,17 +1321,16 @@ trustvc title-escrow reject-transfer-holder **Interactive Prompts:** - Path to TT/JSON document file (or manual input) - - _Network, token registry address, token ID, and document ID are extracted from the document_ + - *Network, token registry address, token ID, and document ID are extracted from the document* - Wallet/private key option - Remark (optional, V5 registries only - will be encrypted with document ID as encryption key) **Output:** Transaction receipt confirming rejection. -
-
-

title-escrow reject-transfer-owner

+ +#### title-escrow reject-transfer-owner Rejects a beneficiary transfer request. @@ -1353,17 +1350,16 @@ trustvc title-escrow reject-transfer-owner **Interactive Prompts:** - Path to TT/JSON document file (or manual input) - - _Network, token registry address, token ID, and document ID are extracted from the document_ + - *Network, token registry address, token ID, and document ID are extracted from the document* - Wallet/private key option - Remark (optional, V5 registries only - will be encrypted with document ID as encryption key) **Output:** Transaction receipt confirming rejection. -
-
-

title-escrow reject-transfer-owner-holder

+ +#### title-escrow reject-transfer-owner-holder Rejects a full ownership transfer (both beneficiary and holder). @@ -1383,17 +1379,16 @@ trustvc title-escrow reject-transfer-owner-holder **Interactive Prompts:** - Path to TT/JSON document file (or manual input) - - _Network, token registry address, token ID, and document ID are extracted from the document_ + - *Network, token registry address, token ID, and document ID are extracted from the document* - Wallet/private key option - Remark (optional, V5 registries only - will be encrypted with document ID as encryption key) **Output:** Transaction receipt confirming rejection. -
-
-

obligation-registry deploy

+ +#### obligation-registry deploy Deploys an Obligation Registry (`TrustVCToken` + `ObligationEscrowFactory`) for electronic Bill of Exchange (BoE) flows. @@ -1416,10 +1411,9 @@ trustvc obligation-registry deploy **Output:** Deployed obligation registry (and factory) addresses plus transaction receipt. -
-
-

obligation-registry mint

+ +#### obligation-registry mint Mints a BoE tokenId to an Obligation Registry and creates the linked ObligationEscrow. @@ -1434,7 +1428,7 @@ trustvc obligation-registry mint **Interactive Prompts:** - Path to signed BoE / obligation document - - _Network, obligationRegistry address, token ID, and document ID are extracted from the document_ + - *Network, obligationRegistry address, token ID, and document ID are extracted from the document* - Holder and beneficiary (drawer/drawee) addresses as required - Wallet/private key option - Dry-run confirmation before broadcasting @@ -1442,10 +1436,9 @@ trustvc obligation-registry mint **Output:** Transaction receipt confirming mint. -
-
-

obligation-escrow accept

+ +#### obligation-escrow accept Accepts an obligation on the ObligationEscrow (drawee acceptance). @@ -1458,17 +1451,16 @@ trustvc obligation-escrow accept **Interactive Prompts:** - Path to BoE / obligation document - - _Network, obligationRegistry, token ID, and document ID are extracted from the document_ + - *Network, obligationRegistry, token ID, and document ID are extracted from the document* - Wallet/private key option - Remark (optional) **Output:** Transaction receipt confirming acceptance. -
-
-

obligation-escrow reject

+ +#### obligation-escrow reject Rejects an obligation on the ObligationEscrow. @@ -1487,10 +1479,9 @@ trustvc obligation-escrow reject **Output:** Transaction receipt confirming rejection. -
-
-

obligation-escrow discharge

+ +#### obligation-escrow discharge Discharges an accepted obligation on the ObligationEscrow. @@ -1509,10 +1500,9 @@ trustvc obligation-escrow discharge **Output:** Transaction receipt confirming discharge. -
-
-

obligation-escrow status

+ +#### obligation-escrow status Reads on-chain obligation / escrow status for a BoE document (read-only; no wallet required for the status call itself beyond network connectivity). @@ -1525,15 +1515,14 @@ trustvc obligation-escrow status **Interactive Prompts:** - Path to BoE / obligation document - - _Network, obligationRegistry, and token ID are extracted from the document_ + - *Network, obligationRegistry, and token ID are extracted from the document* **Output:** Obligation and escrow status fields from the chain. -
-
-

obligation-escrow transfer-holder

+ +#### obligation-escrow transfer-holder Transfers the holder of a BoE obligation record. Transfer, nominate, endorse, return, and reject variants mirror `title-escrow` but operate on ObligationEscrow via `obligation-escrow `. @@ -1557,10 +1546,9 @@ trustvc obligation-escrow transfer-holder **Output:** Transaction receipt confirming the escrow action. -
-
-

verify-obligation

+ +#### verify-obligation Verifies a BoE / obligation document using the dedicated obligation verify pipeline (`verifyObligationDocument`). @@ -1580,10 +1568,12 @@ trustvc verify-obligation **Output:** Verification fragments and overall validity (VALID / INVALID / SKIPPED outcomes as applicable). -
+ ## Configuration + + ### Custom RPC Endpoints You can override the default RPC endpoints for any network by setting environment variables. The format is `{NETWORK_NAME}_RPC`. @@ -1621,6 +1611,8 @@ If no environment variable is set, the CLI will use the default RPC endpoint for ## Development + + ### Setup ```sh @@ -1637,6 +1629,8 @@ npm link npm test ``` + + ### Project Structure ``` @@ -1703,9 +1697,11 @@ src/commands/ └── verify.ts # Verify W3C or OA document ``` + + ## Obligation Registry user guide -User guide for electronic Bill of Exchange (BoE) / obligation flows with **`trustvc-cli`**. +User guide for electronic Bill of Exchange (BoE) / obligation flows with `trustvc-cli`. Classic transferable records (eBL / Token Registry + Title Escrow) use different commands. This section covers **only** Obligation Registry. @@ -1730,14 +1726,18 @@ You do **not** need to call the TypeScript SDK directly — the CLI wraps it wit 4. **A signed BoE document** — W3C VC with `credentialStatus.obligationRegistry` (not `tokenRegistry`) 5. **Network access** — select network in prompts, or set a custom RPC (e.g. `export SEPOLIA_RPC=…`) + + ### Classic vs Obligation — pick the right commands -| Task | Classic ETR | Obligation / BoE | -|------|-------------|------------------| -| Deploy registry | `token-registry deploy` | `obligation-registry deploy` | -| Mint | `mint` / `token-registry mint` | `obligation-registry mint` | -| Escrow actions | `title-escrow …` | `obligation-escrow …` | -| Verify | `verify` | `verify-obligation` | + +| Task | Classic ETR | Obligation / BoE | +| --------------- | ------------------------------ | ---------------------------- | +| Deploy registry | `token-registry deploy` | `obligation-registry deploy` | +| Mint | `mint` / `token-registry mint` | `obligation-registry mint` | +| Escrow actions | `title-escrow …` | `obligation-escrow …` | +| Verify | `verify` | `verify-obligation` | + Using classic commands on a BoE document will fail or skip obligation checks. Using obligation commands on a classic eBL document will fail extraction (missing `obligationRegistry`). @@ -1773,17 +1773,21 @@ Signing/building the VC is usually done with TrustVC library tools or your app ( ### Who can run what -| Action | Typical role | -|--------|----------------| -| `obligation-registry deploy` | Deployer / issuer org | -| `obligation-registry mint` | Issuer (registry minter) | -| `obligation-escrow accept` / `reject` | Holder (roles split) | -| `obligation-escrow discharge` | Beneficiary | -| Transfer / nominate / endorse | Current holder or beneficiary per Title Escrow–style rules | -| `return-to-issuer` | Dual role (beneficiary == holder) | -| `accept-return-to-issuer` / `reject-return-to-issuer` | Issuer | -| `obligation-escrow status` | Anyone with network access + document | -| `verify-obligation` | Anyone with the document (+ RPC when on-chain checks run) | + +| Action | Typical role | +| ----------------------------------------------------- | ---------------------------------------------------------- | +| `obligation-registry deploy` | Deployer / issuer org | +| `obligation-registry mint` | Issuer (registry minter) | +| `obligation-escrow accept` / `reject` | Holder (roles split) | +| `obligation-escrow discharge` | Beneficiary | +| Transfer / nominate / endorse | Current holder or beneficiary per Title Escrow–style rules | +| `return-to-issuer` | Dual role (beneficiary == holder) | +| `accept-return-to-issuer` / `reject-return-to-issuer` | Issuer | +| `obligation-escrow status` | Anyone with network access + document | +| `verify-obligation` | Anyone with the document (+ RPC when on-chain checks run) | + + + ### What the CLI reads from your document @@ -1796,16 +1800,6 @@ For mint, escrow, and related flows, the CLI extracts: If extraction fails with a message about classic transferable records / `tokenRegistry`, you are using the wrong document type or command family. -### Things to know (not bugs) - -1. **Wrong command family** — BoE → `obligation-*` / `verify-obligation`. Classic eBL → `token-registry` / `title-escrow` / `verify`. -2. **Accept / reject need split roles** — `beneficiary != holder`. -3. **Return needs dual role** — `beneficiary == holder`. -4. **`terminationReason` is not set by return alone** — set on reject / discharge / burn (accept-return). -5. **Deploy + mint do not create the VC** — you still need a signed BoE file; mint only puts the token on-chain. - -Per-command prompts and outputs are in [Detailed Command Reference](#detailed-command-reference) above (`obligation-registry deploy`, `obligation-registry mint`, `obligation-escrow *`, `verify-obligation`). - ## License -Apache-2.0 +Apache-2.0 \ No newline at end of file From 0c0bfed5edda3ddf200da9252a25f41c4032b0c8 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Tue, 28 Jul 2026 11:08:03 +0530 Subject: [PATCH 03/20] fix: update W3C obligation record with new issuer and restructure credentialSubject fields for improved clarity and accuracy --- .../obligation/w3c-obligation-record.json | 108 ++++-------------- 1 file changed, 23 insertions(+), 85 deletions(-) diff --git a/tests/fixtures/obligation/w3c-obligation-record.json b/tests/fixtures/obligation/w3c-obligation-record.json index f4d19c7..d9f4edb 100644 --- a/tests/fixtures/obligation/w3c-obligation-record.json +++ b/tests/fixtures/obligation/w3c-obligation-record.json @@ -6,7 +6,7 @@ "https://trustvc.io/context/bill-of-exchange.json" ], "type": ["VerifiableCredential"], - "issuer": "did:web:example.tradetrust.io", + "issuer": "did:web:didhost.vercel.app", "id": "urn:uuid:obligation-test-doc-id", "credentialStatus": { "type": "TransferableRecords", @@ -18,99 +18,37 @@ "tokenId": "23f719b016c88ba1ef2e10c0718d7d0f0026b1dc6e219629f81e2f0f811c4e3e" }, "credentialSubject": { - "documentIdentifier": "BOE-TEST-001", - "bookingReferenceNumber": "BK-TEST-77821", - "purchaseOrderNumber": "PO-TEST-2026-0912", - "houseWaybillDocumentIdentifier": "HWB-TEST-44102", - "documentaryCreditIdentifier": "LC-TEST-2026-88341", - "transportContractDocumentNumber": "BL-TEST-2026-0781", - "uniqueConsignmentReference": "UCR-TEST-2026-004812", - "contractNumber": "CTR-TEST-2026-017", - "invoiceNumber": "INV-TEST-2026-0456", - "freightForwarderReferenceNumber": "FF-TEST-99021", - "issueDate": "2026-07-01", - "documentaryCreditIssueDate": "2026-06-15", - "documentaryCreditEffectiveEndDate": "2026-10-15", - "paymentDueDate": "2026-09-29", - "buyer": { - "name": "Fairview Industries Inc.", - "address": "1201 Market Street, Suite 900, Wilmington, DE 19801, USA" - }, - "invoicee": { - "name": "Fairview Industries Inc.", - "address": "1201 Market Street, Suite 900, Wilmington, DE 19801, USA" - }, - "sellersBank": { - "name": "DBS Bank Ltd", - "address": "12 Marina Boulevard, Marina Bay Financial Centre Tower 3, Singapore 018982" - }, - "importer": { - "name": "Fairview Industries Inc.", - "address": "1201 Market Street, Suite 900, Wilmington, DE 19801, USA" - }, - "exporter": { - "name": "Meridian Commodities Pte Ltd", - "address": "8 Marina Boulevard, #24-01, Singapore 018981" - }, - "documentaryCreditApplicantAgentBank": { - "name": "JPMorgan Chase Bank, N.A.", - "address": "383 Madison Avenue, New York, NY 10179, USA" - }, - "documentaryCreditAvailableBank": { - "name": "DBS Bank Ltd", - "address": "12 Marina Boulevard, Marina Bay Financial Centre Tower 3, Singapore 018982" - }, - "documentaryCreditBeneficiary": { - "name": "Meridian Commodities Pte Ltd", - "address": "8 Marina Boulevard, #24-01, Singapore 018981" - }, - "documentaryCreditDrawee": { + "electronicDocumentIdentifier": "urn:uuid:obligation-test-doc-id", + "referenceNumber": "BOE-TEST-001", + "amountInFigures": "128500.00", + "amountInWords": "One hundred twenty-eight thousand five hundred United States Dollars only", + "currencyCode": "USD", + "blDate": "2026-06-28", + "placeOfIssue": "Singapore", + "dateOfIssue": "2026-07-06", + "tenor": "At 90 days sight", + "payee": "Meridian Commodities Pte Ltd", + "drawnUnder": "Documentary Credit No. LC-TEST-2026-88341 / Invoice No. INV-TEST-2026-0456 / B/L No. BL-TEST-2026-0781", + "drawnUnderDate": "2026-06-15", + "issuedBy": "DBS Bank Ltd", + "drawee": { "name": "Fairview Industries Inc.", - "address": "1201 Market Street, Suite 900, Wilmington, DE 19801, USA" - }, - "seller": { - "name": "Meridian Commodities Pte Ltd", - "address": "8 Marina Boulevard, #24-01, Singapore 018981" - }, - "documentaryCreditReimbursingBank": { - "name": "Citibank N.A.", - "address": "388 Greenwich Street, New York, NY 10013, USA" - }, - "payee": { - "name": "Meridian Commodities Pte Ltd", - "address": "8 Marina Boulevard, #24-01, Singapore 018981" - }, - "buyersBank": { - "name": "Bank of America, N.A.", - "address": "100 North Tryon Street, Charlotte, NC 28255, USA" + "address": "1201 Market Street, Suite 900, Wilmington, DE 19801, USA", + "authorisedSignatoryName": "James R. Carter", + "signature": "" }, "drawer": { "name": "Meridian Commodities Pte Ltd", - "address": "8 Marina Boulevard, #24-01, Singapore 018981" - }, - "drawee": { - "name": "Fairview Industries Inc.", - "address": "1201 Market Street, Suite 900, Wilmington, DE 19801, USA" - }, - "guarantor": { - "name": "Trade Guarantee Insurance Co.", - "address": "1 Raffles Place, #40-00, Singapore 048616" - }, - "paymentLocation": "Singapore", - "placeOfIssue": "Singapore", - "authentication": "Authenticated by Wei Ling Tan, Director of Trade Finance, Meridian Commodities Pte Ltd", - "paymentTerm": "90 days after sight", - "documentEndorsement": "Endorsed to the order of Meridian Commodities Pte Ltd", - "paymentMethod": "TT", - "monetaryAmount": "128500.00", - "currencyQualifier": "invoice currency", - "currencyCode": "USD" + "address": "8 Marina Boulevard, #24-01, Singapore 018981", + "authorisedSignatoryName": "Wei Ling Tan", + "signature": "" + } }, "proof": { "type": "DataIntegrityProof", "cryptosuite": "ecdsa-sd-2023", "proofPurpose": "assertionMethod", - "verificationMethod": "did:web:example.tradetrust.io#key-1", + "verificationMethod": "did:web:didhost.vercel.app#keys-1", "proofValue": "uTEST" } } From 9802e14af5607a18675fc24d725d0b0cdcdc59a2 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Tue, 28 Jul 2026 20:05:19 +0530 Subject: [PATCH 04/20] refactor: consolidate verification commands into a unified pipeline --- README.md | 48 +++----- src/commands/verify-obligation.ts | 139 ----------------------- src/commands/verify.ts | 24 +++- tests/commands/verify-obligation.test.ts | 102 ----------------- 4 files changed, 35 insertions(+), 278 deletions(-) delete mode 100644 src/commands/verify-obligation.ts delete mode 100644 tests/commands/verify-obligation.test.ts diff --git a/README.md b/README.md index 8f6e00d..6bf830e 100644 --- a/README.md +++ b/README.md @@ -241,8 +241,8 @@ trustvc obligation-escrow return-to-issuer trustvc obligation-escrow accept-return-to-issuer trustvc obligation-escrow reject-return-to-issuer -# Verify via obligation pipeline (not classic verify) -trustvc verify-obligation +# Verify BoE or ETR documents (unified pipeline — auto-detects ObligationRecords) +trustvc verify ``` @@ -277,7 +277,7 @@ trustvc verify-obligation - **Document Store**: Deploy document store contracts and use `documentStoreIssue` and `documentStoreRevoke` to issue and revoke document hashes in deployed contracts. - **Transaction Cancel**: Cancel a pending transaction by replacing it with a 0-value transaction to yourself (same nonce, higher gas price). Supports specifying by transaction hash or by nonce and gas price. - **Title Escrow**: Provides comprehensive transferable records management including holder transfers, beneficiary nominations, endorsements, returns, and rejections using smart contracts. -- **Obligation Registry (BoE)**: Separate command trees for electronic Bill of Exchange flows — `obligation-registry` (deploy/mint), `obligation-escrow` (accept/reject/discharge, transfers, return), and `verify-obligation` (dedicated BoE verify pipeline). Do not use classic `token-registry` / `title-escrow` / `verify` for obligation documents. See [Obligation Registry user guide](#obligation-registry-user-guide). +- **Obligation Registry (BoE)**: Separate command trees for electronic Bill of Exchange on-chain flows — `obligation-registry` (deploy/mint) and `obligation-escrow` (accept/reject/discharge, transfers, return). Use **`trustvc verify`** for both ETR and BoE documents (ObligationRecords vs TransferableRecords is auto-detected). Do not use classic `token-registry` / `title-escrow` for obligation documents. See [Obligation Registry user guide](#obligation-registry-user-guide). @@ -349,7 +349,6 @@ trustvc verify-obligation | | `obligation-escrow accept-return-to-issuer` | Accept returned BoE | | | `obligation-escrow reject-return-to-issuer` | Reject returned BoE | | | `obligation-escrow reject-transfer-*` | Reject BoE transfer requests | -| | `[verify-obligation](#verify-obligation)` | Verify BoE via obligation pipeline | @@ -498,7 +497,9 @@ Creates `signed_vc.json` with cryptographic proof. #### verify -Verifies a W3C or OA document using respective verification methods. +Verifies a W3C or OA document using the unified TrustVC verification pipeline. + +Works for **classic ETR** (`tokenRegistry` → TransferableRecords fragment) and **BoE** (`obligationRegistry` → ObligationRecords fragment). When the document is an obligation record, the CLI also prints enriched on-chain status when available. **Usage:** @@ -512,11 +513,11 @@ trustvc verify - [If network required but no network detected]: Select network **Output:** -Verifies the document integrity, status, and issuer identity. +Verifies document integrity, status, and issuer identity. For BoE documents, logs obligation registry status when the ObligationRecords fragment is VALID. **Supported Formats:** -- W3C Verifiable Credential +- W3C Verifiable Credential (ETR, BoE, revocable VDs) - OpenAttestation v2 - OpenAttestation v3 @@ -1548,28 +1549,6 @@ Transaction receipt confirming the escrow action. -#### verify-obligation - -Verifies a BoE / obligation document using the dedicated obligation verify pipeline (`verifyObligationDocument`). - -**Do not use** classic `verify` for BoE documents — it targets ETR / classic transferable records and will skip or mis-handle ObligationRecords. - -**Usage:** - -```sh -trustvc verify-obligation -``` - -**Interactive Prompts:** - -- Path to signed BoE / obligation document -- Network selection when needed for on-chain checks - -**Output:** -Verification fragments and overall validity (VALID / INVALID / SKIPPED outcomes as applicable). - - - ## Configuration @@ -1686,7 +1665,6 @@ src/commands/ │ ├── create.ts # Create encrypted wallet │ ├── encrypt.ts # Encrypt private key to wallet │ └── decrypt.ts # Decrypt wallet file -├── verify-obligation.ts # Verify BoE via obligation pipeline └── w3c/ ├── did.ts # Generate DID ├── key-pair.ts # Generate key pairs @@ -1736,7 +1714,7 @@ You do **not** need to call the TypeScript SDK directly — the CLI wraps it wit | Deploy registry | `token-registry deploy` | `obligation-registry deploy` | | Mint | `mint` / `token-registry mint` | `obligation-registry mint` | | Escrow actions | `title-escrow …` | `obligation-escrow …` | -| Verify | `verify` | `verify-obligation` | +| Verify | `verify` (ETR and BoE) | `verify` (same command) | Using classic commands on a BoE document will fail or skip obligation checks. Using obligation commands on a classic eBL document will fail extraction (missing `obligationRegistry`). @@ -1748,10 +1726,10 @@ Using classic commands on a BoE document will fail or skip obligation checks. Us 2. Put the registry address into your BoE credential status, then sign the credential 3. Mint (issuer wallet) 4. Accept or reject (holder) ──► optional transfers / discharge / return -5. Verify with verify-obligation +5. Verify with `trustvc verify` ``` -Signing/building the VC is usually done with TrustVC library tools or your app (`ObligationDocumentBuilder`). The CLI focuses on **on-chain** steps and **verify**. +Signing/building the VC is usually done with TrustVC library tools or your app (`DocumentBuilder` + `obligationRegistry`). The CLI focuses on **on-chain** steps and **verify**. ### Step-by-step @@ -1769,7 +1747,7 @@ Signing/building the VC is usually done with TrustVC library tools or your app ( **7. Return to issuer (optional)** — requires **beneficiary == holder**; then issuer runs `reject-return-to-issuer` (restore) or `accept-return-to-issuer` (burn). -**8. Verify** — `trustvc verify-obligation` (not classic `verify`). +**8. Verify** — `trustvc verify` (auto-detects BoE vs ETR). ### Who can run what @@ -1784,7 +1762,7 @@ Signing/building the VC is usually done with TrustVC library tools or your app ( | `return-to-issuer` | Dual role (beneficiary == holder) | | `accept-return-to-issuer` / `reject-return-to-issuer` | Issuer | | `obligation-escrow status` | Anyone with network access + document | -| `verify-obligation` | Anyone with the document (+ RPC when on-chain checks run) | +| `verify` | Anyone with the document (+ RPC when on-chain checks run) | diff --git a/src/commands/verify-obligation.ts b/src/commands/verify-obligation.ts deleted file mode 100644 index 4781b58..0000000 --- a/src/commands/verify-obligation.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { input } from '@inquirer/prompts'; -import { - getChainId, - getObligationDocumentStatus, - isObligationRecord, - isWrappedV2Document, - isWrappedV3Document, - SignedVerifiableCredential, - SUPPORTED_CHAINS, - CHAIN_ID, - VerificationFragment, - verifyObligationDocument, -} from '@trustvc/trustvc'; -import signale from 'signale'; -import { - CaptureConsoleWarnAsync, - getSupportedNetworkNameFromId, - promptNetworkSelection, - readJsonFile, - supportedNetwork, - NetworkCmdName, -} from '../utils'; -import { FragmentType } from '../types'; -import { getResultFromFragment, handleExpiredCredentialWarning, logResultStatus } from './verify'; - -export const command = 'verify-obligation'; -export const describe = - 'Verify a BoE / obligation document via the obligation verify pipeline (not classic verify)'; - -export const handler = async () => { - try { - const signedVC = await promptQuestions(); - if (!signedVC) return; - await verifyObligation(signedVC); - } catch (err: unknown) { - signale.error(err instanceof Error ? err.message : String(err)); - } -}; - -export const promptQuestions = async (): Promise => { - const pathToSignedVC = await input({ - message: 'Please enter the path to your obligation / BoE document:', - required: true, - validate: (value: string) => { - if (!value || value.trim() === '') { - return 'Document file path is required'; - } - return true; - }, - }); - return readJsonFile(pathToSignedVC, 'document'); -}; - -export const verifyObligation = async (signedVC: SignedVerifiableCredential) => { - const isOpenAttestation = isWrappedV2Document(signedVC) || isWrappedV3Document(signedVC); - if (isOpenAttestation) { - signale.error( - 'OpenAttestation documents are not supported by verify-obligation. Use classic `trustvc verify`.', - ); - return; - } - - if (!isObligationRecord(signedVC)) { - signale.warn( - 'Document does not look like an obligation record (missing obligationRegistry). Continuing with obligation pipeline anyway.', - ); - } - - signale.info('Verifying obligation / BoE document...'); - const { result, warnings } = await verifyObligationW3C(signedVC); - if (warnings) { - handleExpiredCredentialWarning(warnings); - } - - logResultStatus(getResultFromFragment(FragmentType.DOCUMENT_INTEGRITY, result)); - logResultStatus(getResultFromFragment(FragmentType.DOCUMENT_STATUS, result)); - logResultStatus(getResultFromFragment(FragmentType.ISSUER_IDENTITY, result)); - - const obligationStatus = getObligationDocumentStatus(result); - if (obligationStatus) { - signale.info( - `Obligation document status: registry=${obligationStatus.obligationRegistry} status=${obligationStatus.status} terminationReason=${obligationStatus.terminationReason}`, - ); - } -}; - -const resolveRpcUrl = (networkName: string): string | undefined => { - const chain = Object.values(SUPPORTED_CHAINS).find((c) => c.name === networkName); - return (chain as { rpcUrl?: string } | undefined)?.rpcUrl; -}; - -const verifyObligationW3C = async ( - signedVC: SignedVerifiableCredential, -): Promise<{ result: VerificationFragment[]; warnings: unknown[][] }> => { - const run = async (rpcProviderUrl?: string) => { - const { fragments } = await verifyObligationDocument( - signedVC, - rpcProviderUrl ? { rpcProviderUrl } : {}, - ); - return fragments; - }; - - try { - const chainId = getChainId(signedVC) as CHAIN_ID | undefined; - if (chainId !== undefined && chainId !== null) { - const chainName = getSupportedNetworkNameFromId(Number(chainId)); - const rpcProviderUrl = - resolveRpcUrl(chainName) ?? resolveRpcUrl(SUPPORTED_CHAINS[chainId]?.name); - if (rpcProviderUrl) { - return await CaptureConsoleWarnAsync(() => run(rpcProviderUrl)); - } - // Fall back to public URLs for common testnets when SUPPORTED_CHAINS has no rpcUrl - const fallback: Partial> = { - [NetworkCmdName.Amoy]: 'https://rpc-amoy.polygon.technology', - [NetworkCmdName.Sepolia]: 'https://rpc.sepolia.org', - [NetworkCmdName.Local]: 'http://127.0.0.1:8545', - }; - const cmdName = Object.entries(supportedNetwork).find( - ([, v]) => v.networkName === chainName || v.networkId === Number(chainId), - )?.[0] as NetworkCmdName | undefined; - if (cmdName && fallback[cmdName]) { - return await CaptureConsoleWarnAsync(() => run(fallback[cmdName])); - } - } - } catch (err: unknown) { - signale.warn(`${err instanceof Error ? err.message : String(err)}`); - } - - const networkName = await promptNetworkSelection(); - const rpcProviderUrl = - resolveRpcUrl(networkName) ?? - (networkName === NetworkCmdName.Amoy - ? 'https://rpc-amoy.polygon.technology' - : networkName === NetworkCmdName.Sepolia - ? 'https://rpc.sepolia.org' - : undefined); - - return await CaptureConsoleWarnAsync(() => run(rpcProviderUrl)); -}; diff --git a/src/commands/verify.ts b/src/commands/verify.ts index 0220349..0d8fba6 100644 --- a/src/commands/verify.ts +++ b/src/commands/verify.ts @@ -10,7 +10,9 @@ import { import { getChainId, getDocumentData, + getObligationDocumentStatus, isDocumentRevokable, + isObligationRecord, isTransferableRecord, isWrappedV2Document, isWrappedV3Document, @@ -25,7 +27,7 @@ import type { Provider as V5Provider } from '@ethersproject/providers'; import { FragmentType } from '../types'; export const command = 'verify'; -export const describe = 'Verify a document signed using w3c or OpenAttestation'; +export const describe = 'Verify a W3C or OpenAttestation document (ETR, BoE, or revocable VC)'; export const handler = async () => { try { @@ -69,6 +71,13 @@ export const verify = async (signedVC: SignedVerifiableCredential) => { logResultStatus(getResultFromFragment(FragmentType.DOCUMENT_INTEGRITY, result)); logResultStatus(getResultFromFragment(FragmentType.DOCUMENT_STATUS, result)); logResultStatus(getResultFromFragment(FragmentType.ISSUER_IDENTITY, result)); + + const obligationStatus = getObligationDocumentStatus(result); + if (obligationStatus) { + signale.info( + `Obligation document status: registry=${obligationStatus.obligationRegistry} status=${obligationStatus.status} terminationReason=${obligationStatus.terminationReason}`, + ); + } }; // ==== Helper Functions ==== @@ -80,8 +89,13 @@ const verifyW3CDocument = async ( // To capture the console.warn from trustvc function const { result: isTransferable } = CaptureConsoleWarn(() => isTransferableRecord(signedVC)); + const isObligation = isObligationRecord(signedVC); const isRevokable = isDocumentRevokable(signedVC); - const requiresNetwork = isTransferable || isRevokable; + const requiresNetwork = isTransferable || isObligation || isRevokable; + + if (isObligation) { + signale.info('Verifying obligation / BoE document...'); + } // If the document is not transferable or revokable, verify directly // To capture the console.warn from trustvc function @@ -99,6 +113,12 @@ const verifyW3CDocument = async ( signale.warn(`${err instanceof Error ? err.message : String(err)}`); } + const networkName = await promptNetworkSelection(); + const provider = getSupportedNetwork(networkName).provider() as unknown as V5Provider; + if (provider) { + return await CaptureConsoleWarnAsync(() => verifyDocument(signedVC, { provider })); + } + // Fallback: Verify without provider return await CaptureConsoleWarnAsync(() => verifyDocument(signedVC)); }; diff --git a/tests/commands/verify-obligation.test.ts b/tests/commands/verify-obligation.test.ts deleted file mode 100644 index 6958216..0000000 --- a/tests/commands/verify-obligation.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; -import { verifyObligation, promptQuestions } from '../../src/commands/verify-obligation'; -import fixture from '../fixtures/obligation/w3c-obligation-record.json'; - -vi.mock('signale', async (importOriginal) => { - const originalSignale = await importOriginal(); - return { - ...originalSignale, - Signale: class MockSignale { - await = vi.fn(); - success = vi.fn(); - error = vi.fn(); - info = vi.fn(); - warn = vi.fn(); - constructor() {} - }, - error: vi.fn(), - info: vi.fn(), - success: vi.fn(), - warn: vi.fn(), - await: vi.fn(), - default: { - await: vi.fn(), - success: vi.fn(), - error: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - }, - }; -}); - -vi.mock('@inquirer/prompts', () => ({ - input: vi.fn().mockResolvedValue('/tmp/doc.json'), - select: vi.fn(), - confirm: vi.fn(), - password: vi.fn(), -})); - -vi.mock('@trustvc/trustvc', async () => { - const actual = await vi.importActual('@trustvc/trustvc'); - return { - ...actual, - verifyObligationDocument: vi.fn().mockResolvedValue({ - valid: true, - fragments: [ - { type: 'DOCUMENT_INTEGRITY', status: 'VALID', name: 'W3CSignatureIntegrity', data: true }, - { - type: 'DOCUMENT_STATUS', - status: 'VALID', - name: 'ObligationRecords', - data: { - obligationRegistry: '0x71D28767662cB233F887aD2Bb65d048d760bA694', - status: 0, - terminationReason: 0, - }, - }, - { type: 'ISSUER_IDENTITY', status: 'VALID', name: 'W3CIssuerIdentity', data: true }, - ], - }), - getObligationDocumentStatus: vi.fn().mockReturnValue({ - obligationRegistry: '0x71D28767662cB233F887aD2Bb65d048d760bA694', - status: 0, - terminationReason: 0, - }), - isObligationRecord: vi.fn(() => true), - isWrappedV2Document: vi.fn(() => false), - isWrappedV3Document: vi.fn(() => false), - getChainId: vi.fn(() => 80002), - }; -}); - -vi.mock('../../src/utils', async (importOriginal) => { - const actual = await importOriginal(); - const { default: obligationFixture } = - await import('../fixtures/obligation/w3c-obligation-record.json'); - return { - ...actual, - readJsonFile: vi.fn().mockReturnValue(obligationFixture), - getSupportedNetworkNameFromId: vi.fn().mockReturnValue('amoy'), - promptNetworkSelection: vi.fn(), - CaptureConsoleWarnAsync: async (fn: () => Promise) => ({ - result: await fn(), - warnings: [], - }), - }; -}); - -describe('verify-obligation', () => { - beforeEach(() => vi.clearAllMocks()); - - it('promptQuestions reads document path', async () => { - const doc = await promptQuestions(); - expect(doc).toEqual(fixture); - }); - - it('verifyObligation runs obligation pipeline and logs status', async () => { - const trustvc = await import('@trustvc/trustvc'); - await verifyObligation(fixture as any); - expect(trustvc.verifyObligationDocument as MockedFunction).toHaveBeenCalled(); - expect(trustvc.getObligationDocumentStatus).toHaveBeenCalled(); - }); -}); From 1e5f18a02a514f22a56d16411a12005234edcef3 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Thu, 30 Jul 2026 19:36:23 +0530 Subject: [PATCH 05/20] docs: enhance README with updated TrustVC library details and local development instructions --- README.md | 27 ++++++++++-- src/commands/helpers.ts | 14 ++++--- src/commands/obligation-escrow/accept.ts | 2 +- src/commands/obligation-escrow/discharge.ts | 2 +- src/commands/obligation-escrow/reject.ts | 2 +- src/commands/obligation-escrow/runTx.ts | 4 +- src/commands/obligation-escrow/status.ts | 14 +++---- src/commands/obligation-registry/mint.ts | 4 +- src/commands/verify.ts | 41 +++++++++++++++++-- .../obligation-escrow/discharge.test.ts | 2 +- .../commands/obligation-escrow/reject.test.ts | 2 +- .../commands/obligation-escrow/status.test.ts | 2 +- .../commands/obligation-registry/mint.test.ts | 2 +- .../obligation/w3c-obligation-record.json | 6 +-- 14 files changed, 90 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 6bf830e..d31da94 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,9 @@ A comprehensive command-line interface for managing W3C Verifiable Credentials, This CLI leverages the TrustVC package: -- `[@trustvc/trustvc](https://github.com/TrustVC/trustvc)` - Core library for W3C credentials, OpenAttestation, token registries, and blockchain operations +- [`@trustvc/trustvc`](https://github.com/TrustVC/trustvc) — Core library for W3C credentials, OpenAttestation, token registries, Obligation Registry (BoE), and blockchain operations + +BoE on-chain helpers are imported from the **root** package (`mintObligationRegistry`, `acceptObligationRegistry`, …). Prefer those over any older `@trustvc/trustvc/obligation-registry` path. @@ -1598,6 +1600,12 @@ If no environment variable is set, the CLI will use the default RPC endpoint for # Install dependencies npm install +# (Optional) develop against a local @trustvc/trustvc build +# From the trustvc repo: +# NODE_OPTIONS='--max-old-space-size=8192' npm run build && yalc publish +# Then in this repo: +# yalc add @trustvc/trustvc && npm install + # Build the project npm run build @@ -1685,6 +1693,19 @@ Classic transferable records (eBL / Token Registry + Title Escrow) use different For library / SDK details, see the TrustVC README [§7c Obligation Registry (BoE)](https://github.com/TrustVC/trustvc#c-obligation-registry-boe). +SDK imports use the **root** package with the `*ObligationRegistry` suffix (no clash with classic ETR helpers): + +```ts +import { + mintObligationRegistry, + acceptObligationRegistry, + DocumentBuilder, + verifyDocument, +} from '@trustvc/trustvc'; +``` + +Build BoE credentials with `DocumentBuilder.obligationCredentialStatus({ obligationRegistry, chain, chainId, rpcProviderUrl })` (credential status uses `type: 'TransferableRecords'` plus an `obligationRegistry` field — not `tokenRegistry`). On-chain minting is separate via `obligation-registry mint` / `mintObligationRegistry`. + ### Who this is for Operators and integrators who: @@ -1737,13 +1758,13 @@ Signing/building the VC is usually done with TrustVC library tools or your app ( **2. Mint** — `trustvc obligation-registry mint` with a signed BoE JSON that already references your registry (registry, network, token ID, document ID are extracted). -**3. Accept or reject (holder)** — `trustvc obligation-escrow accept` or `reject` while **beneficiary ≠ holder**. +**3. Accept or reject (holder)** — `trustvc obligation-escrow accept` or `reject` while **beneficiary ≠ holder**. Reject (and discharge) auto-burn the SBT on-chain in the same transaction. **4. Status** — `trustvc obligation-escrow status` reads `Issued` / `Accepted` / `Rejected` / `Discharged`, registration, and termination reason. **5. Transfers (optional)** — `obligation-escrow transfer-holder`, nominate/endorse/return variants mirror `title-escrow`. -**6. Discharge (optional)** — `trustvc obligation-escrow discharge` by **beneficiary** after Accepted. +**6. Discharge (optional)** — `trustvc obligation-escrow discharge` by **beneficiary** after Accepted (also auto-burns). **7. Return to issuer (optional)** — requires **beneficiary == holder**; then issuer runs `reject-return-to-issuer` (restore) or `accept-return-to-issuer` (burn). diff --git a/src/commands/helpers.ts b/src/commands/helpers.ts index 1bab3a3..f92927f 100644 --- a/src/commands/helpers.ts +++ b/src/commands/helpers.ts @@ -7,17 +7,19 @@ import { v4SupportInterfaceIds, v5SupportInterfaceIds, DocumentStore__factory, - obligationRegistryContracts, + encrypt, } from '@trustvc/trustvc'; -import { encrypt } from '@trustvc/trustvc'; // Internal utilities import { ConnectedSigner } from '../utils'; -const { TrustVCToken__factory, ObligationEscrow__factory } = obligationRegistryContracts; - -// Contract factories from TrustVC v5 -const { TitleEscrow__factory, TradeTrustToken__factory, TDocDeployer__factory } = v5Contracts; +const { + TrustVCToken__factory, + ObligationEscrow__factory, + TitleEscrow__factory, + TradeTrustToken__factory, + TDocDeployer__factory, +} = v5Contracts; // Interface for connectToTokenRegistry function arguments interface ConnectToTokenRegistryArgs { diff --git a/src/commands/obligation-escrow/accept.ts b/src/commands/obligation-escrow/accept.ts index b908318..d3b2c61 100644 --- a/src/commands/obligation-escrow/accept.ts +++ b/src/commands/obligation-escrow/accept.ts @@ -33,7 +33,7 @@ export const acceptHandler = async (args: BaseObligationEscrowCommand) => { args, populate: ({ escrow }, encryptedRemark) => escrow.accept.populateTransaction(encryptedRemark), sdk: acceptObligationRegistry as any, - sdkParams: { tokenId: args.tokenId, remarks: args.remark }, + sdkParams: { remarks: args.remark }, }); displayTransactionPrice( transaction as unknown as TransactionReceiptFees, diff --git a/src/commands/obligation-escrow/discharge.ts b/src/commands/obligation-escrow/discharge.ts index a8b7f3a..8ae5907 100644 --- a/src/commands/obligation-escrow/discharge.ts +++ b/src/commands/obligation-escrow/discharge.ts @@ -34,7 +34,7 @@ export const dischargeHandler = async (args: BaseObligationEscrowCommand) => { populate: ({ escrow }, encryptedRemark) => escrow.discharge.populateTransaction(encryptedRemark), sdk: dischargeObligationRegistry as any, - sdkParams: { tokenId: args.tokenId, remarks: args.remark }, + sdkParams: { remarks: args.remark }, }); displayTransactionPrice( transaction as unknown as TransactionReceiptFees, diff --git a/src/commands/obligation-escrow/reject.ts b/src/commands/obligation-escrow/reject.ts index ef23c1d..6e5e5e2 100644 --- a/src/commands/obligation-escrow/reject.ts +++ b/src/commands/obligation-escrow/reject.ts @@ -33,7 +33,7 @@ export const rejectHandler = async (args: BaseObligationEscrowCommand) => { args, populate: ({ escrow }, encryptedRemark) => escrow.reject.populateTransaction(encryptedRemark), sdk: rejectObligationRegistry as any, - sdkParams: { tokenId: args.tokenId, remarks: args.remark }, + sdkParams: { remarks: args.remark }, }); displayTransactionPrice( transaction as unknown as TransactionReceiptFees, diff --git a/src/commands/obligation-escrow/runTx.ts b/src/commands/obligation-escrow/runTx.ts index 4270e9d..fd5053a 100644 --- a/src/commands/obligation-escrow/runTx.ts +++ b/src/commands/obligation-escrow/runTx.ts @@ -21,7 +21,7 @@ type PopulateFn = ( ) => Promise; type SdkCall = ( - contractOptions: { obligationRegistry: string; tokenId: string }, + contractOptions: { obligationRegistryAddress: string; tokenId: string }, wallet: any, params: Record, txOptions: Record, @@ -63,7 +63,7 @@ export const runObligationEscrowTx = async (opts: { process.exit(0); } - const contractOptions = { obligationRegistry: obligationRegistryAddress, tokenId }; + const contractOptions = { obligationRegistryAddress, tokenId }; let transaction; if (canEstimateGasPrice(network)) { diff --git a/src/commands/obligation-escrow/status.ts b/src/commands/obligation-escrow/status.ts index dfe904b..2f3f7fd 100644 --- a/src/commands/obligation-escrow/status.ts +++ b/src/commands/obligation-escrow/status.ts @@ -1,6 +1,6 @@ import { error, info, success } from 'signale'; import { - DocumentStatus, + ObligationDocumentStatus, ObligationEscrowTerminationReason, getObligationEscrowTerminationReason, getObligationRegistryStatus, @@ -14,12 +14,11 @@ export const command = 'status'; export const describe = 'Read BoE obligation escrow status / registration / termination reason'; const STATUS_LABEL: Record = { - [DocumentStatus.Issued]: 'Issued', - [DocumentStatus.Accepted]: 'Accepted', - [DocumentStatus.Rejected]: 'Rejected', - [DocumentStatus.Discharged]: 'Discharged', + [ObligationDocumentStatus.Issued]: 'Issued', + [ObligationDocumentStatus.Accepted]: 'Accepted', + [ObligationDocumentStatus.Rejected]: 'Rejected', + [ObligationDocumentStatus.Discharged]: 'Discharged', }; - const REASON_LABEL: Record = { [ObligationEscrowTerminationReason.None]: 'None', [ObligationEscrowTerminationReason.ReturnToIssuer]: 'ReturnToIssuer', @@ -43,7 +42,8 @@ export const statusHandler = async (args: BaseObligationEscrowCommand) => { try { const { obligationRegistryAddress, tokenId, network, ...rest } = args; const wallet = await getWalletOrSigner({ network, ...rest }); - const opts = { obligationRegistry: obligationRegistryAddress }; + // Escrow resolution uses contractOptions.tokenId (params.tokenId is API parity only). + const opts = { obligationRegistryAddress, tokenId }; const status = await getObligationRegistryStatus(opts, wallet, { tokenId }); const registered = await isObligationRegistryRegistered(opts, wallet, { tokenId }); diff --git a/src/commands/obligation-registry/mint.ts b/src/commands/obligation-registry/mint.ts index f3cca8d..9997df1 100644 --- a/src/commands/obligation-registry/mint.ts +++ b/src/commands/obligation-registry/mint.ts @@ -134,7 +134,7 @@ const mintToObligationRegistry = async ({ } const gasFees = await getGasFees({ provider: wallet.provider, ...rest }); transaction = await mintObligationRegistry( - { obligationRegistry: address }, + { obligationRegistryAddress: address }, wallet, { beneficiaryAddress: beneficiary, holderAddress: holder, tokenId, remarks: remark }, { @@ -145,7 +145,7 @@ const mintToObligationRegistry = async ({ ); } else { transaction = await mintObligationRegistry( - { obligationRegistry: address }, + { obligationRegistryAddress: address }, wallet, { beneficiaryAddress: beneficiary, holderAddress: holder, tokenId, remarks: remark }, { id: encryptionKey }, diff --git a/src/commands/verify.ts b/src/commands/verify.ts index 0d8fba6..25d9084 100644 --- a/src/commands/verify.ts +++ b/src/commands/verify.ts @@ -10,7 +10,6 @@ import { import { getChainId, getDocumentData, - getObligationDocumentStatus, isDocumentRevokable, isObligationRecord, isTransferableRecord, @@ -26,6 +25,35 @@ import signale from 'signale'; import type { Provider as V5Provider } from '@ethersproject/providers'; import { FragmentType } from '../types'; +const OBLIGATION_RECORDS_FRAGMENT = 'ObligationRecords'; + +type ObligationDocumentStatusInfo = { + obligationRegistry: string; + status?: number; + terminationReason?: number; +}; + +/** Extract obligation registry info from a VALID ObligationRecords verify fragment. */ +export const getObligationDocumentStatus = ( + fragments: VerificationFragment[], +): ObligationDocumentStatusInfo | null => { + const fragment = fragments.find((f) => f.name === OBLIGATION_RECORDS_FRAGMENT); + if (!fragment || fragment.status !== 'VALID') return null; + + const data = ( + fragment as { + data?: { obligationRegistry?: string; status?: number; terminationReason?: number }; + } + ).data; + if (!data?.obligationRegistry) return null; + + return { + obligationRegistry: data.obligationRegistry, + status: data.status, + terminationReason: data.terminationReason, + }; +}; + export const command = 'verify'; export const describe = 'Verify a W3C or OpenAttestation document (ETR, BoE, or revocable VC)'; @@ -74,9 +102,14 @@ export const verify = async (signedVC: SignedVerifiableCredential) => { const obligationStatus = getObligationDocumentStatus(result); if (obligationStatus) { - signale.info( - `Obligation document status: registry=${obligationStatus.obligationRegistry} status=${obligationStatus.status} terminationReason=${obligationStatus.terminationReason}`, - ); + const parts = [`registry=${obligationStatus.obligationRegistry}`]; + if (obligationStatus.status !== undefined) { + parts.push(`status=${obligationStatus.status}`); + } + if (obligationStatus.terminationReason !== undefined) { + parts.push(`terminationReason=${obligationStatus.terminationReason}`); + } + signale.info(`Obligation document status: ${parts.join(' ')}`); } }; diff --git a/tests/commands/obligation-escrow/discharge.test.ts b/tests/commands/obligation-escrow/discharge.test.ts index e60089e..5a0c010 100644 --- a/tests/commands/obligation-escrow/discharge.test.ts +++ b/tests/commands/obligation-escrow/discharge.test.ts @@ -85,7 +85,7 @@ describe('obligation-escrow/discharge', () => { }); expect(runObligationEscrowTx as MockedFunction).toHaveBeenCalledWith( expect.objectContaining({ - sdkParams: expect.objectContaining({ tokenId: '0x1' }), + sdkParams: expect.objectContaining({ remarks: undefined }), }), ); }); diff --git a/tests/commands/obligation-escrow/reject.test.ts b/tests/commands/obligation-escrow/reject.test.ts index e03034b..90d0c07 100644 --- a/tests/commands/obligation-escrow/reject.test.ts +++ b/tests/commands/obligation-escrow/reject.test.ts @@ -82,7 +82,7 @@ describe('obligation-escrow/reject', () => { }); expect(runObligationEscrowTx as MockedFunction).toHaveBeenCalledWith( expect.objectContaining({ - sdkParams: expect.objectContaining({ tokenId: '0x1' }), + sdkParams: expect.objectContaining({ remarks: undefined }), }), ); }); diff --git a/tests/commands/obligation-escrow/status.test.ts b/tests/commands/obligation-escrow/status.test.ts index 103f7b1..e80f2cc 100644 --- a/tests/commands/obligation-escrow/status.test.ts +++ b/tests/commands/obligation-escrow/status.test.ts @@ -61,7 +61,7 @@ describe('obligation-escrow/status', () => { maxPriorityFeePerGasScale: 1, }); expect(trustvc.getObligationRegistryStatus as MockedFunction).toHaveBeenCalledWith( - { obligationRegistry: '0xRegistry' }, + { obligationRegistryAddress: '0xRegistry', tokenId: '0x1' }, expect.anything(), { tokenId: '0x1' }, ); diff --git a/tests/commands/obligation-registry/mint.test.ts b/tests/commands/obligation-registry/mint.test.ts index a66070f..02c41d9 100644 --- a/tests/commands/obligation-registry/mint.test.ts +++ b/tests/commands/obligation-registry/mint.test.ts @@ -131,7 +131,7 @@ describe('obligation-registry/mint', () => { }); expect(mintMock).toHaveBeenCalledWith( - { obligationRegistry: '0x71D28767662cB233F887aD2Bb65d048d760bA694' }, + { obligationRegistryAddress: '0x71D28767662cB233F887aD2Bb65d048d760bA694' }, expect.anything(), expect.objectContaining({ beneficiaryAddress: '0xBeneficiary000000000000000000000000000001', diff --git a/tests/fixtures/obligation/w3c-obligation-record.json b/tests/fixtures/obligation/w3c-obligation-record.json index d9f4edb..3371dfb 100644 --- a/tests/fixtures/obligation/w3c-obligation-record.json +++ b/tests/fixtures/obligation/w3c-obligation-record.json @@ -2,11 +2,11 @@ "@context": [ "https://www.w3.org/ns/credentials/v2", "https://w3id.org/security/data-integrity/v2", - "https://trustvc.io/context/obligation-records-context.json", + "https://trustvc.io/context/transferable-records-context.json", "https://trustvc.io/context/bill-of-exchange.json" ], "type": ["VerifiableCredential"], - "issuer": "did:web:didhost.vercel.app", + "issuer": "did:web:trustvc.github.io:did:1", "id": "urn:uuid:obligation-test-doc-id", "credentialStatus": { "type": "TransferableRecords", @@ -48,7 +48,7 @@ "type": "DataIntegrityProof", "cryptosuite": "ecdsa-sd-2023", "proofPurpose": "assertionMethod", - "verificationMethod": "did:web:didhost.vercel.app#keys-1", + "verificationMethod": "did:web:trustvc.github.io:did:1#multikey-1", "proofValue": "uTEST" } } From a28a14e9ecbdfa666a67d121fd6e50f5bcca8b46 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Thu, 30 Jul 2026 19:40:58 +0530 Subject: [PATCH 06/20] chore(release): add beta-boe branch for prerelease channel - Updated .releaserc.json to include a new branch 'beta-boe' for beta prereleases. - Modified release.yml to trigger workflows on pushes to the 'beta-boe' branch. --- .github/workflows/release.yml | 1 + .releaserc.json | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 51da03e..84209c1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - beta-boe jobs: test: diff --git a/.releaserc.json b/.releaserc.json index d11fc16..1d46820 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -1,5 +1,12 @@ { - "branches": ["main"], + "branches": [ + "main", + { + "name": "beta-boe", + "prerelease": "beta", + "channel": "beta" + } + ], "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", From 81a4d24daae078e92fb282330d227cc4941e3810 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Fri, 31 Jul 2026 14:31:22 +0530 Subject: [PATCH 07/20] chore(release): update to version 1.1.0 and enhance dependencies - Bumped version to 1.1.0 in package.json and package-lock.json. - Updated @trustvc/trustvc dependency to version 2.15.0-beta.3. - Added new dependencies including @account-abstraction/contracts and @digitalbazaar/ecdsa-rdfc-2019-cryptosuite. - Improved README for clarity on obligation escrow commands. --- README.md | 12 +- package-lock.json | 535 ++++++++++++++++++++++++++++++++++++++++++---- package.json | 2 +- 3 files changed, 494 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index d31da94..5a5222f 100644 --- a/README.md +++ b/README.md @@ -1600,12 +1600,6 @@ If no environment variable is set, the CLI will use the default RPC endpoint for # Install dependencies npm install -# (Optional) develop against a local @trustvc/trustvc build -# From the trustvc repo: -# NODE_OPTIONS='--max-old-space-size=8192' npm run build && yalc publish -# Then in this repo: -# yalc add @trustvc/trustvc && npm install - # Build the project npm run build @@ -1758,15 +1752,15 @@ Signing/building the VC is usually done with TrustVC library tools or your app ( **2. Mint** — `trustvc obligation-registry mint` with a signed BoE JSON that already references your registry (registry, network, token ID, document ID are extracted). -**3. Accept or reject (holder)** — `trustvc obligation-escrow accept` or `reject` while **beneficiary ≠ holder**. Reject (and discharge) auto-burn the SBT on-chain in the same transaction. +**3. Accept or reject (holder)** — `trustvc obligation-escrow accept` or `reject` while **beneficiary ≠ holder**. Reject only sets status to Rejected; closing requires return-to-issuer afterward. **4. Status** — `trustvc obligation-escrow status` reads `Issued` / `Accepted` / `Rejected` / `Discharged`, registration, and termination reason. **5. Transfers (optional)** — `obligation-escrow transfer-holder`, nominate/endorse/return variants mirror `title-escrow`. -**6. Discharge (optional)** — `trustvc obligation-escrow discharge` by **beneficiary** after Accepted (also auto-burns). +**6. Discharge (optional)** — `trustvc obligation-escrow discharge` by **beneficiary** after Accepted (sets Discharged; does not burn). -**7. Return to issuer (optional)** — requires **beneficiary == holder**; then issuer runs `reject-return-to-issuer` (restore) or `accept-return-to-issuer` (burn). +**7. Return to issuer** — requires status **Rejected** or **Discharged** and **beneficiary == holder**; then issuer runs `reject-return-to-issuer` (restore) or `accept-return-to-issuer` (burn). **8. Verify** — `trustvc verify` (auto-detects BoE vs ETR). diff --git a/package-lock.json b/package-lock.json index 737cc65..29c40c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "@trustvc/trustvc-cli", - "version": "1.0.1", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@trustvc/trustvc-cli", - "version": "1.0.1", + "version": "1.1.0", "license": "Apache-2.0", "dependencies": { "@inquirer/prompts": "^5.3.8", - "@trustvc/trustvc": "^2.14.1", + "@trustvc/trustvc": "2.15.0-beta.3", "@types/yargs": "^17.0.32", "chalk": "^4.1.2", "dotenv": "^16.0.0", @@ -134,6 +134,16 @@ "ethers": "^5.8.0" } }, + "node_modules/@account-abstraction/contracts": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@account-abstraction/contracts/-/contracts-0.8.0.tgz", + "integrity": "sha512-8krPx/gpnoT+5xAroagVCbeA7FbUigMZWXFKKPm+oghyr29Dksssdx5sI7xGv9212i4JPaDDUGFk58dpuwVgHA==", + "license": "MIT", + "dependencies": { + "@openzeppelin/contracts": "^5.1.0", + "@uniswap/v3-periphery": "^1.4.3" + } + }, "node_modules/@actions/core": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.0.tgz", @@ -972,6 +982,81 @@ "node": ">=18" } }, + "node_modules/@digitalbazaar/ecdsa-rdfc-2019-cryptosuite": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@digitalbazaar/ecdsa-rdfc-2019-cryptosuite/-/ecdsa-rdfc-2019-cryptosuite-1.3.0.tgz", + "integrity": "sha512-Rhg++GnGWHJ29QyWTFW0tRqd/uGLADIsLVEq10zEIAY7D9ScoSLtyYzwZ/zBueBTXHXrtDUEL5SfrSvcKTLFyg==", + "license": "BSD-3-Clause", + "dependencies": { + "@digitalbazaar/ecdsa-multikey": "^1.6.0", + "jsonld": "^9.0.0", + "rdf-canonize": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@digitalbazaar/ecdsa-rdfc-2019-cryptosuite/node_modules/@digitalbazaar/http-client": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@digitalbazaar/http-client/-/http-client-4.3.0.tgz", + "integrity": "sha512-6lMpxpt9BOmqHKGs9Xm6DP4LlZTBFer/ZjHvP3FcW3IaUWYIWC7dw5RFZnvw4fP57kAVcm1dp3IF+Y50qhBvAw==", + "license": "BSD-3-Clause", + "dependencies": { + "ky": "^1.14.2", + "undici": "^6.23.0" + }, + "engines": { + "node": ">=18.0" + } + }, + "node_modules/@digitalbazaar/ecdsa-rdfc-2019-cryptosuite/node_modules/canonicalize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/canonicalize/-/canonicalize-2.1.0.tgz", + "integrity": "sha512-F705O3xrsUtgt98j7leetNhTWPe+5S72rlL5O4jA1pKqBVQ/dT1O1D6PFxmSXvc0SUOinWS57DKx0I3CHrXJHQ==", + "license": "Apache-2.0", + "bin": { + "canonicalize": "bin/canonicalize.js" + } + }, + "node_modules/@digitalbazaar/ecdsa-rdfc-2019-cryptosuite/node_modules/jsonld": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonld/-/jsonld-9.0.0.tgz", + "integrity": "sha512-pjMIdkXfC1T2wrX9B9i2uXhGdyCmgec3qgMht+TDj+S0qX3bjWMQUfL7NeqEhuRTi8G5ESzmL9uGlST7nzSEWg==", + "license": "BSD-3-Clause", + "dependencies": { + "@digitalbazaar/http-client": "^4.2.0", + "canonicalize": "^2.1.0", + "lru-cache": "^6.0.0", + "rdf-canonize": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@digitalbazaar/ecdsa-rdfc-2019-cryptosuite/node_modules/ky": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/ky/-/ky-1.14.3.tgz", + "integrity": "sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/ky?sponsor=1" + } + }, + "node_modules/@digitalbazaar/ecdsa-rdfc-2019-cryptosuite/node_modules/rdf-canonize": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/rdf-canonize/-/rdf-canonize-5.0.0.tgz", + "integrity": "sha512-g8OUrgMXAR9ys/ZuJVfBr05sPPoMA7nHIVs8VEvg9QwM5W4GR2qSFEEHjsyHF1eWlBaf8Ev40WNjQFQ+nJTO3w==", + "license": "BSD-3-Clause", + "dependencies": { + "setimmediate": "^1.0.5" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@digitalbazaar/ecdsa-sd-2023-cryptosuite": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/@digitalbazaar/ecdsa-sd-2023-cryptosuite/-/ecdsa-sd-2023-cryptosuite-3.4.1.tgz", @@ -2936,6 +3021,18 @@ "yarn": "1.x" } }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/curves": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.2.0.tgz", @@ -3144,6 +3241,12 @@ "@octokit/openapi-types": "^27.0.0" } }, + "node_modules/@openzeppelin/contracts": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.6.1.tgz", + "integrity": "sha512-Ly6SlsVJ3mj+b18W3R8gNufB7dTICT105fJhodGAGgyC2oqnBAhqSiNDJ8V8DLY05cCz81GLI0CU5vNYA1EC/w==", + "license": "MIT" + }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", @@ -3510,6 +3613,81 @@ "win32" ] }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", @@ -5004,9 +5182,9 @@ }, "node_modules/@tradetrust-tt/token-registry-v5": { "name": "@tradetrust-tt/token-registry", - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@tradetrust-tt/token-registry/-/token-registry-5.5.1.tgz", - "integrity": "sha512-yxOdjRLSI0fPptFbicYpnb/fn4Ngbi/LuGDCQKL0vb7PLCvhT8DLpiAFbFs0cc+QAb439lnZjhPg+UbWQHufRA==", + "version": "5.6.0-beta.1", + "resolved": "https://registry.npmjs.org/@tradetrust-tt/token-registry/-/token-registry-5.6.0-beta.1.tgz", + "integrity": "sha512-EtxZw/WnvGQMb/MB243eHy4ejSTN8bkV7I7Y9f19xk9v/mOojFL74C/tD1zjsZUUjiQi9rzLRYlzSUNbZBN2gg==", "license": "Apache-2.0", "dependencies": { "ethers": "^6.13.4" @@ -5359,30 +5537,43 @@ "integrity": "sha512-YIECQwcoreIfyTbol1/5u9CGK6mbg0Q0bSN2/Ks388zLus1IXELWK5EHYuyrFPbb9d8ajsvz7m+ySsMIY9574w==", "license": "Apache-2.0" }, + "node_modules/@trustvc/eip7702": { + "version": "1.0.0-beta.1", + "resolved": "https://registry.npmjs.org/@trustvc/eip7702/-/eip7702-1.0.0-beta.1.tgz", + "integrity": "sha512-cOGuZVLHd0+Uu7rZ/Dp2iF1VuNq+L+pWWGe+tPD/u74rSvCAzIc17dIk5HOwRBiC5awh1rXG46vLG8IRc5O01Q==", + "license": "MIT", + "dependencies": { + "@account-abstraction/contracts": "^0.8.0", + "@openzeppelin/contracts": "^5.6.1" + } + }, "node_modules/@trustvc/trustvc": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/@trustvc/trustvc/-/trustvc-2.14.1.tgz", - "integrity": "sha512-vGR6/s8nn69sl3Ntr/rE8+kg64RuqsRN2AeB0yeNOAUwTWEmQRk16cvB4g1xW7+CsLc5yI5sRuAqeAmbhHdV0g==", + "version": "2.15.0-beta.3", + "resolved": "https://registry.npmjs.org/@trustvc/trustvc/-/trustvc-2.15.0-beta.3.tgz", + "integrity": "sha512-EFiYmXZulyWnq+g3tb/yrpANkggE76QjDILuNE/68ZWdh9cw8yUxR+hcrqkk8tbhapmgulzQjtTuuASnuD1xog==", "license": "Apache-2.0", "dependencies": { "@tradetrust-tt/dnsprove": "^2.18.0", "@tradetrust-tt/ethers-aws-kms-signer": "^2.1.4", "@tradetrust-tt/token-registry-v4": "npm:@tradetrust-tt/token-registry@^4.16.0", - "@tradetrust-tt/token-registry-v5": "npm:@tradetrust-tt/token-registry@^5.5.0", + "@tradetrust-tt/token-registry-v5": "npm:@tradetrust-tt/token-registry@^5.6.0-beta.1", "@tradetrust-tt/tradetrust": "^6.10.3", "@tradetrust-tt/tt-verify": "^9.7.5", "@trustvc/document-store": "^1.0.3", - "@trustvc/w3c": "^2.2.0", - "@trustvc/w3c-context": "^2.2.0", - "@trustvc/w3c-credential-status": "^2.2.0", - "@trustvc/w3c-issuer": "^2.2.0", - "@trustvc/w3c-vc": "^2.2.0", + "@trustvc/eip7702": "^1.0.0-beta.1", + "@trustvc/w3c": "^2.4.1", + "@trustvc/w3c-context": "^2.4.0", + "@trustvc/w3c-credential-status": "^2.4.0", + "@trustvc/w3c-issuer": "^2.3.0", + "@trustvc/w3c-vc": "^2.4.1", "ethers": "^5.8.0", "ethersV6": "npm:ethers@^6.14.4", "js-sha3": "^0.9.3", "node-fetch": "^2.7.0", "node-forge": "^1.3.3", - "ts-chacha20": "^1.2.0" + "permissionless": "^0.3.6", + "ts-chacha20": "^1.2.0", + "viem": "^2.53.1" }, "engines": { "node": ">=20.0.0" @@ -5469,33 +5660,33 @@ } }, "node_modules/@trustvc/trustvc/node_modules/node-forge": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", - "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" } }, "node_modules/@trustvc/w3c": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@trustvc/w3c/-/w3c-2.2.0.tgz", - "integrity": "sha512-2WhAoYZW7JLt9H1OrT1qcwTL0g6LymMVg9uiZ3xxlaRgYzOmEqClN3Pq6apo1kRZTzJMHcKWECKADkHGa0Jgqw==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@trustvc/w3c/-/w3c-2.4.1.tgz", + "integrity": "sha512-yCyztSAbialRn9VYvXrHUE4v13q4g/BHUN4XUctzF5lrN3WsHKSgUpFUflocLdp1h5fxvJTbqyVoMSh5SDLwQw==", "license": "Apache-2.0", "dependencies": { - "@trustvc/w3c-context": "^2.2.0", - "@trustvc/w3c-credential-status": "^2.2.0", - "@trustvc/w3c-issuer": "^2.2.0", - "@trustvc/w3c-vc": "^2.2.0" + "@trustvc/w3c-context": "^2.4.0", + "@trustvc/w3c-credential-status": "^2.4.0", + "@trustvc/w3c-issuer": "^2.3.0", + "@trustvc/w3c-vc": "^2.4.1" }, "engines": { "node": ">=18.x" } }, "node_modules/@trustvc/w3c-context": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@trustvc/w3c-context/-/w3c-context-2.2.0.tgz", - "integrity": "sha512-p9mtIWZ1v1hhqiGLJ5Fu+2PK9ClIRsdo04vgCVC8BxhIjwUU7ZHb95sYF1E8Ay9pP2BRyFujBdoaYXHH8n5v4A==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@trustvc/w3c-context/-/w3c-context-2.4.0.tgz", + "integrity": "sha512-OCRfqZfTyEZ2Lpd5RPBl36DwvaKv3qV1PgZOEA9hrnEtHMWxwS5iSdv+xFYK8pZIoeheFZKymfgcB5LL7UBQRw==", "license": "Apache-2.0", "dependencies": { "did-resolver": "^4.1.0", @@ -5512,13 +5703,13 @@ "license": "Apache-2.0" }, "node_modules/@trustvc/w3c-credential-status": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@trustvc/w3c-credential-status/-/w3c-credential-status-2.2.0.tgz", - "integrity": "sha512-lfgnvAUSwdi5hWnuf+wqTkpPTYxmZyZ8kdzVPRQeKWqb0ysdWN+n32ROoNpQAFSPqYlSL0pLWuI/vg35WuhnEA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@trustvc/w3c-credential-status/-/w3c-credential-status-2.4.0.tgz", + "integrity": "sha512-fiMGtfOmq1x9e+vd89ZKqguYtmW2w2k2hxINkno+mDNHdt5CV8tfTH5EbL1fY0oPiuXM5SK+dV6iSBTIWGPFog==", "license": "Apache-2.0", "dependencies": { - "@trustvc/w3c-context": "^2.2.0", - "@trustvc/w3c-issuer": "^2.2.0", + "@trustvc/w3c-context": "^2.4.0", + "@trustvc/w3c-issuer": "^2.3.0", "base64url-universal": "^2.0.0", "pako": "^2.1.0" }, @@ -5527,9 +5718,9 @@ } }, "node_modules/@trustvc/w3c-issuer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@trustvc/w3c-issuer/-/w3c-issuer-2.2.0.tgz", - "integrity": "sha512-o5XWh52c3KeNqrrIpSvjPt+3zwZ/wwh2hlGOst6PZXVzS9nMab+jUwhs52d+HBhe2r8BL4Z81sdMGA8YAEnk6Q==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@trustvc/w3c-issuer/-/w3c-issuer-2.3.0.tgz", + "integrity": "sha512-J/Rlae2s/ihkF0q6OmBofmzdRjDI98ED9lRFIB6Uh2WaEl8eZP7+FoStip+t1Piy6ZbO/6MwaS0QCw/6N6viMA==", "license": "Apache-2.0", "dependencies": { "@digitalbazaar/bls12-381-multikey": "^2.1.0", @@ -5561,19 +5752,20 @@ } }, "node_modules/@trustvc/w3c-vc": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@trustvc/w3c-vc/-/w3c-vc-2.2.0.tgz", - "integrity": "sha512-QAfoEgNndi2X+V0Nz9nBGiUk4Ko0XUFVLn0BY6qJK8GHJndMMCEcZ14PlaHyKG8nIossQeKZbsBwnavm2jRFdg==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@trustvc/w3c-vc/-/w3c-vc-2.4.1.tgz", + "integrity": "sha512-f/pqBu70epEYVmtWvKvlhCd2F5QkV885rCcbfa9KZq9xYbOzeBueIF7P3gSdv3ILAiMCQTxvpL7hd8I8hj+M1A==", "license": "Apache-2.0", "dependencies": { "@digitalbazaar/bbs-2023-cryptosuite": "^2.0.1", "@digitalbazaar/bls12-381-multikey": "^2.1.0", "@digitalbazaar/data-integrity": "^2.5.0", "@digitalbazaar/ecdsa-multikey": "^1.8.0", + "@digitalbazaar/ecdsa-rdfc-2019-cryptosuite": "^1.3.0", "@digitalbazaar/ecdsa-sd-2023-cryptosuite": "^3.4.1", "@mattrglobal/jsonld-signatures-bbs": "^1.2.0", - "@trustvc/w3c-credential-status": "^2.2.0", - "@trustvc/w3c-issuer": "^2.2.0", + "@trustvc/w3c-credential-status": "^2.4.0", + "@trustvc/w3c-issuer": "^2.3.0", "base64url-universal": "^2.0.0", "cbor": "^9.0.2", "did-resolver": "^4.1.0", @@ -5906,6 +6098,55 @@ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "dev": true }, + "node_modules/@uniswap/lib": { + "version": "4.0.1-alpha", + "resolved": "https://registry.npmjs.org/@uniswap/lib/-/lib-4.0.1-alpha.tgz", + "integrity": "sha512-f6UIliwBbRsgVLxIaBANF6w09tYqc6Y/qXdsrbEmXHyFA7ILiKrIwRFXe1yOg8M3cksgVsO9N7yuL2DdCGQKBA==", + "license": "GPL-3.0-or-later", + "engines": { + "node": ">=10" + } + }, + "node_modules/@uniswap/v2-core": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@uniswap/v2-core/-/v2-core-1.0.1.tgz", + "integrity": "sha512-MtybtkUPSyysqLY2U210NBDeCHX+ltHt3oADGdjqoThZaFRDKwM6k1Nb3F0A3hk5hwuQvytFWhrWHOEq6nVJ8Q==", + "license": "GPL-3.0-or-later", + "engines": { + "node": ">=10" + } + }, + "node_modules/@uniswap/v3-core": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@uniswap/v3-core/-/v3-core-1.0.1.tgz", + "integrity": "sha512-7pVk4hEm00j9tc71Y9+ssYpO6ytkeI0y7WE9P6UcmNzhxPePwyAxImuhVsTqWK9YFvzgtvzJHi64pBl4jUzKMQ==", + "license": "BUSL-1.1", + "engines": { + "node": ">=10" + } + }, + "node_modules/@uniswap/v3-periphery": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@uniswap/v3-periphery/-/v3-periphery-1.4.4.tgz", + "integrity": "sha512-S4+m+wh8HbWSO3DKk4LwUCPZJTpCugIsHrWR86m/OrUyvSqGDTXKFfc2sMuGXCZrD1ZqO3rhQsKgdWg3Hbb2Kw==", + "license": "GPL-2.0-or-later", + "dependencies": { + "@openzeppelin/contracts": "3.4.2-solc-0.7", + "@uniswap/lib": "^4.0.1-alpha", + "@uniswap/v2-core": "^1.0.1", + "@uniswap/v3-core": "^1.0.0", + "base64-sol": "1.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@uniswap/v3-periphery/node_modules/@openzeppelin/contracts": { + "version": "3.4.2-solc-0.7", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-3.4.2-solc-0.7.tgz", + "integrity": "sha512-W6QmqgkADuFcTLzHL8vVoNBtkwjvQRpYIAom7KiUNoLKghyx3FgH0GBjt8NRvigV1ZmMOBllvE1By1C+bi8WpA==", + "license": "MIT" + }, "node_modules/@vitest/expect": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", @@ -6021,6 +6262,27 @@ "license": "ISC", "optional": true }, + "node_modules/abitype": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", + "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -6391,6 +6653,12 @@ "node": ">=14" } }, + "node_modules/base64-sol": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/base64-sol/-/base64-sol-1.0.1.tgz", + "integrity": "sha512-ld3cCNMeXt4uJXmLZBHFGMvVpK9KsLVEhPpFRXnvSVAqABKbuNZg/+dsq3NuM+wxFLb/UrVkz7m1ciWmkMfTbg==", + "license": "MIT" + }, "node_modules/base64url": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", @@ -6620,9 +6888,9 @@ } }, "node_modules/cborg": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/cborg/-/cborg-5.1.1.tgz", - "integrity": "sha512-BDbSRIp6XrQXkTc7g+DN0RB9RrDPTUfals2ecWUlt3juPLjbAvy/V72mJcXY0Ehu0Dq/3WpNCOCT68HUTbW+lw==", + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/cborg/-/cborg-5.1.11.tgz", + "integrity": "sha512-oc6Pzg/gkTobxHZNgMmny+G99dOeBMbAmnGHcZWMKtolxZBIVwfi0Pj0khxEtNU8HMFdbT5sK0HmtgecDUPP0A==", "license": "Apache-2.0", "bin": { "cborg": "lib/bin.js" @@ -8319,6 +8587,12 @@ "node": ">=6" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -10441,6 +10715,21 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -13835,6 +14124,69 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ox": { + "version": "0.14.33", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.33.tgz", + "integrity": "sha512-rooA/4o7bBof4Ge2VH/eovfNPb/AEEYyrNj03wggc55g5HZD8Pjs/OeWhttgjic3dDcqn0r29bDuvQEdTiUemQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.2.3", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/ox/node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT" + }, + "node_modules/ox/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ox/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/p-each-series": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-3.0.0.tgz", @@ -14076,6 +14428,21 @@ "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "license": "MIT" }, + "node_modules/permissionless": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/permissionless/-/permissionless-0.3.7.tgz", + "integrity": "sha512-r1UGWkVMqFzzJ0UcKEVHh6XvOm7+SfTvy7AoGA7ZlxpE96XR+zbtb+rRAA3DOQuq6/ifov5NvUyt1xf4zxbR0g==", + "license": "MIT", + "peerDependencies": { + "ox": "^0.11.3", + "viem": "^2.44.4" + }, + "peerDependenciesMeta": { + "ox": { + "optional": true + } + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -17012,6 +17379,84 @@ "extsprintf": "^1.2.0" } }, + "node_modules/viem": { + "version": "2.55.10", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.55.10.tgz", + "integrity": "sha512-Q9Ba+/ma81U2M5o5P2AQ7Ux8rTIwmCZvUcr8rKdQ22bV0IBFHllM2m5gWDP8hFaUN2nH2oW3QG44amRazflYNQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.9.1", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0", + "abitype": "1.2.3", + "isows": "1.0.7", + "ox": "0.14.33", + "ws": "8.21.0" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", diff --git a/package.json b/package.json index 34257d5..93c3156 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ }, "dependencies": { "@inquirer/prompts": "^5.3.8", - "@trustvc/trustvc": "^2.14.1", + "@trustvc/trustvc": "2.15.0-beta.3", "@types/yargs": "^17.0.32", "chalk": "^4.1.2", "dotenv": "^16.0.0", From c431c3ad48970792b9aed81a0a7444280f70d286 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Fri, 31 Jul 2026 15:45:38 +0530 Subject: [PATCH 08/20] docs: update README for obligation registry usage and enhance error handling - Clarified the process for minting BoE token IDs, emphasizing the need to sign with `w3c-sign` before minting. - Improved instructions for using the Obligation Registry address in documents. - Enhanced error messages in CLI to provide clearer guidance on contract call exceptions. - Updated deployment logs to include instructions for using the Obligation Registry address. --- README.md | 37 +-- .../obligation-credential-subject.sample.json | 55 +++++ src/commands/obligation-registry/deploy.ts | 4 + src/utils/cli-errors.ts | 223 +++++++++++++++++- src/utils/cli-options.ts | 14 +- src/utils/formatting.ts | 23 +- .../obligation-registry/deploy.test.ts | 10 + tests/utils/contract-errors.test.ts | 56 +++++ 8 files changed, 401 insertions(+), 21 deletions(-) create mode 100644 samples/obligation-credential-subject.sample.json create mode 100644 tests/utils/contract-errors.test.ts diff --git a/README.md b/README.md index 5a5222f..65370f5 100644 --- a/README.md +++ b/README.md @@ -222,7 +222,8 @@ trustvc title-escrow reject-transfer-owner-holder # Deploy Obligation Registry (TrustVCToken + factory) trustvc obligation-registry deploy -# Mint a BoE tokenId to obligationRegistry +# Mint a BoE tokenId to obligationRegistry (sign first with w3c-sign) +trustvc w3c-sign trustvc obligation-registry mint # Lifecycle @@ -1412,7 +1413,7 @@ trustvc obligation-registry deploy - Dry-run confirmation before broadcasting **Output:** -Deployed obligation registry (and factory) addresses plus transaction receipt. +Deployed `ObligationEscrowFactory` and Obligation Registry addresses plus transaction receipt. Copy the Obligation Registry address into `credentialStatus.obligationRegistry` in your BoE document before signing and minting. @@ -1422,15 +1423,18 @@ Mints a BoE tokenId to an Obligation Registry and creates the linked ObligationE **Do not use** classic `mint` / `token-registry mint` for BoE documents. +**Before minting:** set `credentialStatus.obligationRegistry` to your deployed registry address, then sign the document with [`w3c-sign`](#w3c-sign). Mint only accepts a signed Verifiable Credential. + **Usage:** ```sh +trustvc w3c-sign trustvc obligation-registry mint ``` **Interactive Prompts:** -- Path to signed BoE / obligation document +- Path to signed BoE / obligation document (output of `w3c-sign`) - *Network, obligationRegistry address, token ID, and document ID are extracted from the document* - Holder and beneficiary (drawer/drawee) addresses as required - Wallet/private key option @@ -1716,7 +1720,7 @@ You do **not** need to call the TypeScript SDK directly — the CLI wraps it wit 1. **Install the CLI** — `npm install -g @trustvc/trustvc-cli` or `npx @trustvc/trustvc-cli ` 2. **Node.js 22+** 3. **A wallet** — encrypted wallet file (recommended), private key, or key file -4. **A signed BoE document** — W3C VC with `credentialStatus.obligationRegistry` (not `tokenRegistry`) +4. **A signed BoE document** — set `credentialStatus.obligationRegistry`, then sign with `trustvc w3c-sign` (not `tokenRegistry`) 5. **Network access** — select network in prompts, or set a custom RPC (e.g. `export SEPOLIA_RPC=…`) @@ -1738,29 +1742,32 @@ Using classic commands on a BoE document will fail or skip obligation checks. Us ``` 1. Deploy Obligation Registry -2. Put the registry address into your BoE credential status, then sign the credential -3. Mint (issuer wallet) -4. Accept or reject (holder) ──► optional transfers / discharge / return -5. Verify with `trustvc verify` +2. Put the registry address into your BoE credential status +3. Sign the credential with `trustvc w3c-sign` +4. Mint (issuer wallet) +5. Accept or reject (holder) ──► optional transfers / discharge / return +6. Verify with `trustvc verify` ``` -Signing/building the VC is usually done with TrustVC library tools or your app (`DocumentBuilder` + `obligationRegistry`). The CLI focuses on **on-chain** steps and **verify**. +Signing/building the VC can also be done with TrustVC library tools or your app (`DocumentBuilder` + `obligationRegistry`). With the CLI, use `w3c-sign` before minting. ### Step-by-step **1. Deploy** — `trustvc obligation-registry deploy` (network, name/symbol, optional factory reuse, wallet, dry-run). -**2. Mint** — `trustvc obligation-registry mint` with a signed BoE JSON that already references your registry (registry, network, token ID, document ID are extracted). +**2. Sign** — put the deployed registry into `credentialStatus.obligationRegistry`, then run `trustvc w3c-sign` on the unsigned BoE JSON. + +**3. Mint** — `trustvc obligation-registry mint` with the signed BoE JSON from `w3c-sign` (registry, network, token ID, document ID are extracted). -**3. Accept or reject (holder)** — `trustvc obligation-escrow accept` or `reject` while **beneficiary ≠ holder**. Reject only sets status to Rejected; closing requires return-to-issuer afterward. +**4. Accept or reject (holder)** — `trustvc obligation-escrow accept` or `reject` while **beneficiary ≠ holder**. Reject only sets status to Rejected; closing requires return-to-issuer afterward. -**4. Status** — `trustvc obligation-escrow status` reads `Issued` / `Accepted` / `Rejected` / `Discharged`, registration, and termination reason. +**5. Status** — `trustvc obligation-escrow status` reads `Issued` / `Accepted` / `Rejected` / `Discharged`, registration, and termination reason. -**5. Transfers (optional)** — `obligation-escrow transfer-holder`, nominate/endorse/return variants mirror `title-escrow`. +**6. Transfers (optional)** — `obligation-escrow transfer-holder`, nominate/endorse/return variants mirror `title-escrow`. -**6. Discharge (optional)** — `trustvc obligation-escrow discharge` by **beneficiary** after Accepted (sets Discharged; does not burn). +**7. Discharge (optional)** — `trustvc obligation-escrow discharge` by **beneficiary** after Accepted (sets Discharged; does not burn). -**7. Return to issuer** — requires status **Rejected** or **Discharged** and **beneficiary == holder**; then issuer runs `reject-return-to-issuer` (restore) or `accept-return-to-issuer` (burn). +**8. Return to issuer** — requires status **Rejected** or **Discharged** and **beneficiary == holder**; then issuer runs `reject-return-to-issuer` (restore) or `accept-return-to-issuer` (burn). **8. Verify** — `trustvc verify` (auto-detects BoE vs ETR). diff --git a/samples/obligation-credential-subject.sample.json b/samples/obligation-credential-subject.sample.json new file mode 100644 index 0000000..933ee8e --- /dev/null +++ b/samples/obligation-credential-subject.sample.json @@ -0,0 +1,55 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://w3id.org/security/data-integrity/v2", + "https://trustvc.io/context/render-method-context-v2.json", + "https://trustvc.io/context/bill-of-exchange.json", + "https://trustvc.io/context/obligation-records-context.json", + "https://trustvc.io/context/qrcode-context.json" + ], + "renderMethod": [ + { + "type": "EMBEDDED_RENDERER", + "templateName": "BILL_OF_EXCHANGE", + "id": "http://localhost:9000" + } + ], + "credentialSubject": { + "electronicDocumentIdentifier": "urn:uuid:e6f4b2a1-9c3d-4e8f-a7b0-1d2e3f4a5b6c", + "referenceNumber": "BOE-2026-00147", + "amountInFigures": "128500.00", + "amountInWords": "One hundred twenty-eight thousand five hundred United States Dollars only", + "currencyCode": "USD", + "blDate": "2026-06-28", + "placeOfIssue": "Singapore", + "dateOfIssue": "2026-07-06", + "tenor": "At 90 days sight", + "payee": "Meridian Commodities Pte Ltd", + "drawnUnder": "Documentary Credit No. LC-DBS-2026-88341 / Invoice No. INV-2026-0456 / B/L No. BL-SIN-2026-0781", + "drawnUnderDate": "2026-06-15", + "issuedBy": "DBS Bank Ltd", + "drawee": { + "name": "Fairview Industries Inc.", + "address": "1201 Market Street, Suite 900, Wilmington, DE 19801, USA", + "authorisedSignatoryName": "James R. Carter", + "signature": "data:image/png;base64,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" + }, + "drawer": { + "name": "Meridian Commodities Pte Ltd", + "address": "8 Marina Boulevard, #24-01, Singapore 018981", + "authorisedSignatoryName": "Wei Ling Tan", + "signature": "data:image/png;base64,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" + } + }, + "type": ["VerifiableCredential"], + "credentialStatus": { + "type": "TransferableRecords", + "tokenNetwork": { + "chain": "Sepolia", + "chainId": 11155111 + }, + "obligationRegistry": "0xE8c7516BD4FC64DF039BAA586035dfaA57520BE4" + }, + "issuer": "did:web:didhost.vercel.app", + "validFrom": "2026-07-06T09:00:00Z" +} diff --git a/src/commands/obligation-registry/deploy.ts b/src/commands/obligation-registry/deploy.ts index ee14fbe..3c6cc55 100644 --- a/src/commands/obligation-registry/deploy.ts +++ b/src/commands/obligation-registry/deploy.ts @@ -135,6 +135,10 @@ export const deployObligationRegistryContract = async ({ const { hash } = result.receipt as unknown as TransactionReceipt; success(`Obligation registry ${registryName} deployed at ${result.obligationRegistry}`); info(`ObligationEscrowFactory: ${result.obligationEscrowFactoryAddress}`); + info(`Obligation Registry: ${result.obligationRegistry}`); + info( + `Use this Obligation Registry address as credentialStatus.obligationRegistry in your document before signing and minting.`, + ); info(`Find more details at ${getEtherscanAddress({ network })}/tx/${hash}`); info(`Chain ID used: ${chainId}`); diff --git a/src/utils/cli-errors.ts b/src/utils/cli-errors.ts index 9a8a400..358190e 100644 --- a/src/utils/cli-errors.ts +++ b/src/utils/cli-errors.ts @@ -1,4 +1,7 @@ -// CLI error helpers: turn caught errors into clear messages for the user. +/** + * Shared helpers for decoding ethers CALL_EXCEPTION / custom errors into + * clear CLI messages (gas estimation often leaves revert.name unset). + */ export type ErrnoException = NodeJS.ErrnoException; @@ -11,6 +14,224 @@ export type CliErrorOptions = { const DEFAULT_PATH_PLACEHOLDER = 'the specified path'; +/** Known on-chain custom errors → actionable CLI guidance. */ +const KNOWN_REVERT_MESSAGES: Record = { + OwnerHolderMustDiffer: + 'Beneficiary (owner) and holder must be different wallets before accept/reject can run. Remint with different beneficiary and holder addresses.', + CallerNotBeneficiary: 'Connected wallet is not the beneficiary for this obligation.', + CallerNotHolder: 'Connected wallet is not the holder for this obligation.', + NotRegistered: "This token ID hasn't been minted on this registry yet.", + RemarkLengthExceeded: 'The remark is too long. Shorten it and try again.', + RegistryContractPaused: 'This registry is paused — unpause it before continuing.', + InactiveTitleEscrow: 'This obligation escrow is inactive.', + TitleEscrowNotHoldingToken: 'This escrow is not holding the token.', + DualRoleRejectionRequired: + 'Both beneficiary and holder roles are required to reject this transfer.', + TokenNotReturnedToIssuer: 'Token must be returned to issuer before this action.', + RecipientAlreadyHolder: 'Recipient is already the holder.', + InvalidNominee: 'The nominee address is invalid.', + InvalidTransferToZeroAddress: 'Cannot transfer to the zero address.', + NomineeAlreadyNominated: 'This nominee is already nominated.', + TargetNomineeAlreadyBeneficiary: 'The nominee is already the beneficiary.', + AlreadyRegistered: 'This token ID is already minted on this registry.', + TokenExists: 'This token ID already exists on this registry.', + AccessControlUnauthorizedAccount: 'Connected wallet is not authorized for this action.', + CallerNotMinter: 'Connected wallet does not hold MINTER_ROLE on this registry.', + InvalidStatusTransition: 'This action is not allowed from the current obligation status.', + EmptyReceivingData: 'Receiving data was empty.', + InvalidTokenId: 'The token ID is invalid.', + InvalidRegistry: 'The registry address is invalid.', +}; + +/** + * keccak256 selectors for no-arg / known signatures. Gas estimation often returns + * only `data: 0x` with shortMessage "execution reverted (unknown custom error)". + */ +const REVERT_SELECTOR_TO_NAME: Record = { + '0x7e288225': 'OwnerHolderMustDiffer', + '0xf52018ac': 'CallerNotBeneficiary', + '0x667b3fbf': 'CallerNotHolder', + '0xaba47339': 'NotRegistered', + '0x14086584': 'RemarkLengthExceeded', + '0xad49811c': 'RegistryContractPaused', + '0x7c339b19': 'InactiveTitleEscrow', + '0x2fd39523': 'TitleEscrowNotHoldingToken', + '0x8c90bf45': 'DualRoleRejectionRequired', + '0xd5475a0b': 'TokenNotReturnedToIssuer', + '0x702e6e62': 'RecipientAlreadyHolder', + '0x1c98cac3': 'InvalidNominee', + '0x2c75c45a': 'InvalidTransferToZeroAddress', + '0x98f83ce8': 'NomineeAlreadyNominated', + '0xdd9f921d': 'TargetNomineeAlreadyBeneficiary', + '0x3a81d6fc': 'AlreadyRegistered', + '0x55c7e8ba': 'TokenExists', + '0x10daa94f': 'EmptyReceivingData', + '0xe3962ba3': 'InvalidStatusTransition', + '0xed15e6cf': 'InvalidTokenId', + '0x540b9601': 'InvalidRegistry', + '0xbd805e91': 'InvalidTokenTransferToZeroAddressOwners', + '0xe2517d3f': 'AccessControlUnauthorizedAccount', + '0x5eee367a': 'CallerNotMinter', +}; + +const UNKNOWN_CUSTOM_ERROR = /unknown custom error/i; + +type EthersCallException = { + reason?: unknown; + shortMessage?: unknown; + message?: unknown; + revert?: { name?: unknown }; + errorName?: unknown; + code?: unknown; + data?: unknown; + info?: { error?: { data?: unknown; message?: unknown } }; + error?: { data?: unknown; message?: unknown }; +}; + +const asEthersError = (value: unknown): EthersCallException | null => + typeof value === 'object' && value !== null ? (value as EthersCallException) : null; + +const normalizeLabel = (raw: string): string | undefined => { + const cleaned = raw.trim().replace(/\(\)$/, ''); + if (!cleaned || UNKNOWN_CUSTOM_ERROR.test(cleaned) || cleaned === '(unknown custom error)') { + return undefined; + } + // Drop leading junk like ": OwnerHolderMustDiffer" or parentheses wrappers + const ident = cleaned.match(/([A-Za-z][A-Za-z0-9_]*)/); + return ident?.[1]; +}; + +const collectPossibleData = (err: EthersCallException): string[] => { + const values: unknown[] = [err.data, err.info?.error?.data, err.error?.data]; + const out: string[] = []; + for (const v of values) { + if (typeof v === 'string' && /^0x[0-9a-fA-F]+$/i.test(v)) { + out.push(v.toLowerCase()); + } else if (typeof v === 'object' && v !== null && 'data' in v) { + const nested = (v as { data?: unknown }).data; + if (typeof nested === 'string' && /^0x[0-9a-fA-F]+$/i.test(nested)) { + out.push(nested.toLowerCase()); + } + } + } + return out; +}; + +const labelFromRevertData = (err: EthersCallException): string | undefined => { + for (const data of collectPossibleData(err)) { + if (data === '0x') continue; + const selector = data.slice(0, 10); + if (REVERT_SELECTOR_TO_NAME[selector]) { + return REVERT_SELECTOR_TO_NAME[selector]; + } + } + return undefined; +}; + +export function extractContractRevertLabel(error: unknown): string | undefined { + const err = asEthersError(error); + if (!err) { + if (error instanceof Error) { + const fromMessage = error.message.match( + /(?:execution reverted:\s*|Contract reverted with\s*|failed:\s*)([A-Za-z0-9_]+)/, + ); + return fromMessage?.[1] ? normalizeLabel(fromMessage[1]) : undefined; + } + return undefined; + } + + const revertName = err.revert?.name ?? err.errorName; + if (typeof revertName === 'string') { + const label = normalizeLabel(revertName); + if (label) return label; + } + + if (typeof err.reason === 'string') { + const label = normalizeLabel(err.reason); + if (label) return label; + } + + // Prefer selector decoding before shortMessage — gas estimate often only has + // "execution reverted (unknown custom error)" + data: 0x7e288225 + const fromData = labelFromRevertData(err); + if (fromData) return fromData; + + if (typeof err.shortMessage === 'string' && err.shortMessage.trim()) { + if (!UNKNOWN_CUSTOM_ERROR.test(err.shortMessage)) { + const match = err.shortMessage.match(/execution reverted(?::\s*)?(.+)?/i); + if (match?.[1]) { + const label = normalizeLabel(match[1]); + if (label) return label; + } + } + } + + if (typeof err.message === 'string') { + const custom = err.message.match(/execution reverted:\s*([A-Za-z0-9_]+)/); + if (custom?.[1]) { + const label = normalizeLabel(custom[1]); + if (label) return label; + } + const wrapped = err.message.match(/(?:Contract reverted with\s*|failed:\s*)([A-Za-z0-9_]+)/); + if (wrapped?.[1]) { + const label = normalizeLabel(wrapped[1]); + if (label) return label; + } + } + + return undefined; +} + +/** True when the failure is a definitive on-chain revert (not a transient gas/RPC issue). */ +export function isContractCallException(error: unknown): boolean { + const err = asEthersError(error); + if (!err) { + if (error instanceof Error && /Pre-check .* failed:/i.test(error.message)) { + return Boolean(extractContractRevertLabel(error)); + } + return false; + } + if (err.code === 'CALL_EXCEPTION') return true; + if (err.revert?.name || err.errorName) return true; + if (labelFromRevertData(err)) return true; + return Boolean(extractContractRevertLabel(error)); +} + +/** Maps ethers / contract errors to a short user-facing message. */ +export function describeContractError(error: unknown): string { + const label = extractContractRevertLabel(error); + if (label && KNOWN_REVERT_MESSAGES[label]) { + return `${label}: ${KNOWN_REVERT_MESSAGES[label]}`; + } + if (label) { + return `Contract reverted with ${label}`; + } + + const err = asEthersError(error); + if (err) { + // Never surface the useless ethers placeholder as the final message + if ( + typeof err.shortMessage === 'string' && + err.shortMessage.trim() && + !UNKNOWN_CUSTOM_ERROR.test(err.shortMessage) + ) { + return err.shortMessage; + } + if (typeof err.reason === 'string' && err.reason.trim()) { + return err.reason; + } + if (typeof err.message === 'string' && err.message.trim()) { + return err.message.split('\n')[0]!; + } + } + + if (error instanceof Error && error.message) { + return error.message.split('\n')[0]!; + } + + return String(error); +} + export function isErrnoException(err: unknown): err is ErrnoException { return ( typeof err === 'object' && diff --git a/src/utils/cli-options.ts b/src/utils/cli-options.ts index 456bfba..652cd1e 100644 --- a/src/utils/cli-options.ts +++ b/src/utils/cli-options.ts @@ -12,7 +12,8 @@ import { readDocumentFile } from './file-io'; import { getTokenRegistryAddress, getTokenId, getChainId } from '@trustvc/trustvc'; import fs from 'fs'; import { getTokenRegistryVersion } from '../commands/helpers'; -import { getErrorMessage } from './index'; +import { getErrorMessage } from './formatting'; +import { isContractCallException } from './cli-errors'; import { dryRunMode } from './dryRun'; export interface NetworkOption { @@ -572,7 +573,16 @@ export const performDryRunWithConfirmation = async ({ info('\n✅ Proceeding with transaction...'); return true; } catch (estimateError) { - error(`Gas estimation failed: ${getErrorMessage(estimateError)}`); + const message = getErrorMessage(estimateError); + // Definitive on-chain reverts will never succeed if broadcast — abort instead of + // offering "proceed anyway" (which only dumps a larger callStatic stack next). + if (isContractCallException(estimateError)) { + error(`Transaction would revert: ${message}`); + info('Transaction cancelled.'); + return false; + } + + error(`Gas estimation failed: ${message}`); const proceedAnyway = await confirm({ message: 'Gas estimation failed. Do you want to proceed anyway?', default: false, diff --git a/src/utils/formatting.ts b/src/utils/formatting.ts index dd7faf9..cbdd83b 100644 --- a/src/utils/formatting.ts +++ b/src/utils/formatting.ts @@ -1,4 +1,5 @@ import chalk from 'chalk'; +import { describeContractError, isContractCallException } from './cli-errors'; export const addAddressPrefix = (address: string): string => address.startsWith('0x') ? address : `0x${address}`; @@ -33,11 +34,27 @@ const toErrorWithMessage = function (maybeError: unknown): ErrorWithMessage { export const extractErrorMessage = (error: unknown): string => toErrorWithMessage(error).message; export const getErrorMessage = function (error: unknown): string { + // Prefer structured ethers / custom-error decoding over raw `reason` (often null). + if (isContractCallException(error)) { + return describeContractError(error); + } + if (error instanceof Error) { - return 'reason' in error ? (error['reason'] as string) : error.message; - } else { - return extractErrorMessage(error); + const reason = + 'reason' in error && typeof (error as { reason?: unknown }).reason === 'string' + ? (error as { reason: string }).reason.trim() + : ''; + if (reason) return reason; + + // SDK may already wrap: "Pre-check for accept failed: OwnerHolderMustDiffer: ..." + if (/Pre-check .* failed:/i.test(error.message)) { + return describeContractError(error); + } + + return error.message.split('\n')[0]!; } + + return describeContractError(error) || extractErrorMessage(error); }; // Captures console.warn for a function to handle expected console.warn. diff --git a/tests/commands/obligation-registry/deploy.test.ts b/tests/commands/obligation-registry/deploy.test.ts index 557c5bc..11d1913 100644 --- a/tests/commands/obligation-registry/deploy.test.ts +++ b/tests/commands/obligation-registry/deploy.test.ts @@ -111,6 +111,16 @@ describe('obligation-registry/deploy', () => { expect(deployMock).toHaveBeenCalled(); expect(address).toBe('0xRegistry'); + + const { success, info } = await import('signale'); + expect(success).toHaveBeenCalledWith( + 'Obligation registry My BoE Registry deployed at 0xRegistry', + ); + expect(info).toHaveBeenCalledWith('ObligationEscrowFactory: 0xFactory'); + expect(info).toHaveBeenCalledWith('Obligation Registry: 0xRegistry'); + expect(info).toHaveBeenCalledWith( + 'Use this Obligation Registry address as credentialStatus.obligationRegistry in your document before signing and minting.', + ); }); }); diff --git a/tests/utils/contract-errors.test.ts b/tests/utils/contract-errors.test.ts new file mode 100644 index 0000000..45c6482 --- /dev/null +++ b/tests/utils/contract-errors.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest'; +import { + describeContractError, + extractContractRevertLabel, + getErrorMessage, + isContractCallException, +} from '../../src/utils'; + +describe('contract revert error formatting', () => { + const ownerHolderError = Object.assign(new Error('execution reverted: OwnerHolderMustDiffer()'), { + code: 'CALL_EXCEPTION', + reason: null, + revert: { name: 'OwnerHolderMustDiffer', signature: 'OwnerHolderMustDiffer()', args: [] }, + shortMessage: 'execution reverted (unknown custom error)', + }); + + it('extracts OwnerHolderMustDiffer from ethers call exceptions', () => { + expect(extractContractRevertLabel(ownerHolderError)).toBe('OwnerHolderMustDiffer'); + expect(isContractCallException(ownerHolderError)).toBe(true); + }); + + it('maps OwnerHolderMustDiffer to an actionable message', () => { + expect(describeContractError(ownerHolderError)).toMatch( + /OwnerHolderMustDiffer: Beneficiary \(owner\) and holder must be different/, + ); + expect(getErrorMessage(ownerHolderError)).toMatch(/OwnerHolderMustDiffer:/); + }); + + it('does not return null when reason is null', () => { + expect(getErrorMessage(ownerHolderError)).not.toBe('null'); + expect(getErrorMessage(ownerHolderError)).toBeTruthy(); + }); + + it('decodes OwnerHolderMustDiffer from revert data when name is missing', () => { + // Typical gas-estimation shape: undecoded shortMessage + selector only + const gasEstimateError = Object.assign(new Error('execution reverted'), { + code: 'CALL_EXCEPTION', + reason: null, + data: '0x7e288225', + shortMessage: 'execution reverted (unknown custom error)', + }); + + expect(extractContractRevertLabel(gasEstimateError)).toBe('OwnerHolderMustDiffer'); + expect(getErrorMessage(gasEstimateError)).toMatch( + /OwnerHolderMustDiffer: Beneficiary \(owner\) and holder must be different/, + ); + expect(getErrorMessage(gasEstimateError)).not.toMatch(/unknown custom error/i); + }); + + it('rewrites SDK pre-check wrappers', () => { + const wrapped = new Error( + 'Pre-check for accept failed: OwnerHolderMustDiffer: Beneficiary (owner) and holder must be different wallets before accept/reject can run. Remint with different beneficiary and holder addresses.', + ); + expect(getErrorMessage(wrapped)).toMatch(/OwnerHolderMustDiffer:/); + }); +}); From fef37017b031762c4a6860404d70229706de9b7b Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Fri, 31 Jul 2026 15:51:36 +0530 Subject: [PATCH 09/20] chore(release): update release configuration and CI workflows - Renamed the 'beta-boe' branch to 'beta' in .releaserc.json for clarity. - Updated CI workflow to include checks for pushes and pull requests to 'main' and 'beta' branches, as well as feature branches. - Enhanced release workflow permissions and added npm registry URL for publishing. - Adjusted npm publish settings in the release configuration to ensure proper deployment. --- .github/workflows/ci.yml | 6 +++++- .github/workflows/release.yml | 12 +++++++++++- .releaserc.json | 9 +++++++-- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad76a6c..e9c6c0e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,14 +1,18 @@ name: CI +# Same checks for PRs targeting main or beta (and for pushes to those branches). on: push: pull_request: + branches: + - main + - beta jobs: test: name: Test runs-on: ubuntu-latest - + strategy: matrix: node-version: [22.x, 24.x] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 84209c1..918d69e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,13 @@ name: Release +# Publishes to npm on push to: +# - main → stable (@latest) +# - beta → prerelease (@beta), e.g. 1.2.0-beta.1 on: push: branches: - main - - beta-boe + - beta jobs: test: @@ -40,6 +43,11 @@ jobs: name: Release needs: test runs-on: ubuntu-latest + permissions: + contents: write + issues: write + pull-requests: write + id-token: write steps: - name: Checkout code @@ -53,6 +61,7 @@ jobs: with: node-version: 22.x cache: 'npm' + registry-url: 'https://registry.npmjs.org/' - name: Install dependencies run: npm ci @@ -65,3 +74,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.releaserc.json b/.releaserc.json index 1d46820..88adc85 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -2,7 +2,7 @@ "branches": [ "main", { - "name": "beta-boe", + "name": "beta", "prerelease": "beta", "channel": "beta" } @@ -16,7 +16,12 @@ "changelogFile": "CHANGELOG.md" } ], - "@semantic-release/npm", + [ + "@semantic-release/npm", + { + "npmPublish": true + } + ], [ "@semantic-release/git", { From 5488f9c9f1f7f8d32133689e2e591628ddfe3fe5 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Fri, 31 Jul 2026 19:50:11 +0530 Subject: [PATCH 10/20] docs: refine README formatting and clarify command usage - Improved formatting for better readability, including consistent use of markdown syntax. - Clarified instructions for using the Obligation Registry and related commands. - Removed unnecessary whitespace to enhance overall document cleanliness. --- README.md | 60 ++++++------------------------------------------------- 1 file changed, 6 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 65370f5..c70dbc4 100644 --- a/README.md +++ b/README.md @@ -24,12 +24,10 @@ A comprehensive command-line interface for managing W3C Verifiable Credentials, This CLI leverages the TrustVC package: -- [`@trustvc/trustvc`](https://github.com/TrustVC/trustvc) — Core library for W3C credentials, OpenAttestation, token registries, Obligation Registry (BoE), and blockchain operations +- `[@trustvc/trustvc](https://github.com/TrustVC/trustvc)` — Core library for W3C credentials, OpenAttestation, token registries, Obligation Registry (BoE), and blockchain operations BoE on-chain helpers are imported from the **root** package (`mintObligationRegistry`, `acceptObligationRegistry`, …). Prefer those over any older `@trustvc/trustvc/obligation-registry` path. - - ## Table of Contents - [Features](#features) @@ -280,7 +278,7 @@ trustvc verify - **Document Store**: Deploy document store contracts and use `documentStoreIssue` and `documentStoreRevoke` to issue and revoke document hashes in deployed contracts. - **Transaction Cancel**: Cancel a pending transaction by replacing it with a 0-value transaction to yourself (same nonce, higher gas price). Supports specifying by transaction hash or by nonce and gas price. - **Title Escrow**: Provides comprehensive transferable records management including holder transfers, beneficiary nominations, endorsements, returns, and rejections using smart contracts. -- **Obligation Registry (BoE)**: Separate command trees for electronic Bill of Exchange on-chain flows — `obligation-registry` (deploy/mint) and `obligation-escrow` (accept/reject/discharge, transfers, return). Use **`trustvc verify`** for both ETR and BoE documents (ObligationRecords vs TransferableRecords is auto-detected). Do not use classic `token-registry` / `title-escrow` for obligation documents. See [Obligation Registry user guide](#obligation-registry-user-guide). +- **Obligation Registry (BoE)**: Separate command trees for electronic Bill of Exchange on-chain flows — `obligation-registry` (deploy/mint) and `obligation-escrow` (accept/reject/discharge, transfers, return). Use `trustvc verify` for both ETR and BoE documents (ObligationRecords vs TransferableRecords is auto-detected). Do not use classic `token-registry` / `title-escrow` for obligation documents. See [Obligation Registry user guide](#obligation-registry-user-guide). @@ -377,6 +375,8 @@ All title-escrow, obligation-registry, obligation-escrow, token registry, docume ### Detailed Command Reference + + #### transaction cancel Cancels a pending transaction by replacing it with a 0-value transaction to yourself using the same nonce and a higher gas price (replace-by-fee). This action is irreversible. @@ -424,8 +424,6 @@ trustvc transaction cancel \ **Note:** If the pending transaction uses EIP-1559 (maxFeePerGas / maxPriorityFeePerGas), it has no legacy `gasPrice`. In that case, specify the transaction by **nonce and gas price** and set a gas price (in wei) for the replacement. - - #### key-pair-generation Generates cryptographic key pairs for modern cryptosuites (ECDSA-SD-2023, BBS-2023). @@ -496,8 +494,6 @@ trustvc w3c-sign **Output:** Creates `signed_vc.json` with cryptographic proof. - - #### verify Verifies a W3C or OA document using the unified TrustVC verification pipeline. @@ -547,8 +543,6 @@ trustvc credential-status-create **Output:** Signed credential status list file. - - #### credential-status-update Updates an existing W3C credential status list to revoke or suspend credentials. @@ -569,8 +563,6 @@ trustvc credential-status-update **Output:** Updated credential status list file. - - #### oa-sign Signs OpenAttestation v2 or v3 documents with a private key. @@ -699,8 +691,6 @@ trustvc oa-decrypt Writes the decrypted Open Attestation document (raw OA v2/v3 or wrapped OA v2/v3) to the specified path. Fails if the key is wrong or the file is not a valid encrypted OA document. - - #### mint Mints a document hash (tokenId) to a token registry smart contract. @@ -828,8 +818,6 @@ trustvc document-store issue **Output:** Transaction receipt confirming the hash issuance. - - #### document-store revoke Revokes a document hash from a deployed document store. @@ -848,8 +836,6 @@ trustvc document-store revoke **Output:** Transaction receipt confirming the hash revocation. - - #### document-store grant-role Grants a role (ISSUER_ROLE, REVOKER_ROLE, or DEFAULT_ADMIN_ROLE) to an account in a deployed document store. @@ -1125,8 +1111,6 @@ trustvc title-escrow transfer-holder **Output:** Transaction receipt confirming holder transfer. - - #### title-escrow nominate-transfer-owner Nominates a new beneficiary (owner) for the transferable record. @@ -1155,8 +1139,6 @@ trustvc title-escrow nominate-transfer-owner **Output:** Transaction receipt confirming beneficiary nomination. - - #### title-escrow endorse-transfer-owner Endorses the change of beneficiary (owner) for the transferable record. @@ -1185,8 +1167,6 @@ trustvc title-escrow endorse-transfer-owner **Output:** Transaction receipt confirming beneficiary endorsement. - - #### title-escrow transfer-owner-holder Endorses the transfer of both beneficiary and holder to new addresses. @@ -1216,8 +1196,6 @@ trustvc title-escrow transfer-owner-holder **Output:** Transaction receipt confirming full ownership transfer. - - #### title-escrow return-to-issuer Returns the transferable record to the issuer. @@ -1245,8 +1223,6 @@ trustvc title-escrow return-to-issuer **Output:** Transaction receipt confirming document return. - - #### title-escrow accept-return-to-issuer Accepts a returned transferable record (issuer action). @@ -1274,8 +1250,6 @@ trustvc title-escrow accept-return-to-issuer **Output:** Transaction receipt confirming acceptance. - - #### title-escrow reject-return-to-issuer Rejects a returned transferable record (issuer action). @@ -1303,8 +1277,6 @@ trustvc title-escrow reject-return-to-issuer **Output:** Transaction receipt confirming rejection. - - #### title-escrow reject-transfer-holder Rejects a holder transfer request. @@ -1332,8 +1304,6 @@ trustvc title-escrow reject-transfer-holder **Output:** Transaction receipt confirming rejection. - - #### title-escrow reject-transfer-owner Rejects a beneficiary transfer request. @@ -1361,8 +1331,6 @@ trustvc title-escrow reject-transfer-owner **Output:** Transaction receipt confirming rejection. - - #### title-escrow reject-transfer-owner-holder Rejects a full ownership transfer (both beneficiary and holder). @@ -1390,8 +1358,6 @@ trustvc title-escrow reject-transfer-owner-holder **Output:** Transaction receipt confirming rejection. - - #### obligation-registry deploy Deploys an Obligation Registry (`TrustVCToken` + `ObligationEscrowFactory`) for electronic Bill of Exchange (BoE) flows. @@ -1415,15 +1381,13 @@ trustvc obligation-registry deploy **Output:** Deployed `ObligationEscrowFactory` and Obligation Registry addresses plus transaction receipt. Copy the Obligation Registry address into `credentialStatus.obligationRegistry` in your BoE document before signing and minting. - - #### obligation-registry mint Mints a BoE tokenId to an Obligation Registry and creates the linked ObligationEscrow. **Do not use** classic `mint` / `token-registry mint` for BoE documents. -**Before minting:** set `credentialStatus.obligationRegistry` to your deployed registry address, then sign the document with [`w3c-sign`](#w3c-sign). Mint only accepts a signed Verifiable Credential. +**Before minting:** set `credentialStatus.obligationRegistry` to your deployed registry address, then sign the document with `[w3c-sign](#w3c-sign)`. Mint only accepts a signed Verifiable Credential. **Usage:** @@ -1443,8 +1407,6 @@ trustvc obligation-registry mint **Output:** Transaction receipt confirming mint. - - #### obligation-escrow accept Accepts an obligation on the ObligationEscrow (drawee acceptance). @@ -1457,7 +1419,7 @@ trustvc obligation-escrow accept **Interactive Prompts:** -- Path to BoE / obligation document +- Path to BoE / obligation documentverify - *Network, obligationRegistry, token ID, and document ID are extracted from the document* - Wallet/private key option - Remark (optional) @@ -1465,8 +1427,6 @@ trustvc obligation-escrow accept **Output:** Transaction receipt confirming acceptance. - - #### obligation-escrow reject Rejects an obligation on the ObligationEscrow. @@ -1486,8 +1446,6 @@ trustvc obligation-escrow reject **Output:** Transaction receipt confirming rejection. - - #### obligation-escrow discharge Discharges an accepted obligation on the ObligationEscrow. @@ -1507,8 +1465,6 @@ trustvc obligation-escrow discharge **Output:** Transaction receipt confirming discharge. - - #### obligation-escrow status Reads on-chain obligation / escrow status for a BoE document (read-only; no wallet required for the status call itself beyond network connectivity). @@ -1527,8 +1483,6 @@ trustvc obligation-escrow status **Output:** Obligation and escrow status fields from the chain. - - #### obligation-escrow transfer-holder Transfers the holder of a BoE obligation record. Transfer, nominate, endorse, return, and reject variants mirror `title-escrow` but operate on ObligationEscrow via `obligation-escrow `. @@ -1553,8 +1507,6 @@ trustvc obligation-escrow transfer-holder **Output:** Transaction receipt confirming the escrow action. - - ## Configuration From dedcad6fe65d3c81d087e8452ca35ab7a94598f5 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Sat, 1 Aug 2026 00:38:07 +0530 Subject: [PATCH 11/20] chore: update dependencies and improve error handling in obligation escrow commands - Changed @trustvc/trustvc dependency to a local file reference for development. - Enhanced error handling in various obligation escrow commands to ensure proper exit codes on transaction failures. - Refactored command handlers to streamline transaction execution and error logging. - Updated README to clarify command usage and improve overall documentation quality. --- README.md | 14 ++-- src/commands/helpers.ts | 14 +++- .../accept-return-to-issuer.ts | 2 + src/commands/obligation-escrow/accept.ts | 2 + src/commands/obligation-escrow/discharge.ts | 2 + .../endorse-transfer-owner.ts | 2 + .../nominate-transfer-owner.ts | 53 ++++++--------- .../reject-return-to-issuer.ts | 2 + .../reject-transfer-holder.ts | 53 ++++++--------- .../reject-transfer-owner-holder.ts | 53 ++++++--------- .../reject-transfer-owner.ts | 2 + src/commands/obligation-escrow/reject.ts | 2 + .../obligation-escrow/return-to-issuer.ts | 2 + src/commands/obligation-escrow/runTx.ts | 4 +- src/commands/obligation-escrow/shared.ts | 20 ++++++ src/commands/obligation-escrow/status.ts | 43 +++++------- .../obligation-escrow/transfer-holder.ts | 2 + .../transfer-owner-holder.ts | 65 ++++++++----------- src/utils/cli-errors.ts | 44 ++++++++++--- src/utils/obligation-document.ts | 5 ++ tests/utils/contract-errors.test.ts | 11 ++++ 21 files changed, 217 insertions(+), 180 deletions(-) diff --git a/README.md b/README.md index c70dbc4..a312831 100644 --- a/README.md +++ b/README.md @@ -1467,7 +1467,7 @@ Transaction receipt confirming discharge. #### obligation-escrow status -Reads on-chain obligation / escrow status for a BoE document (read-only; no wallet required for the status call itself beyond network connectivity). +Reads on-chain obligation / escrow status for a BoE document. The CLI still prompts for a wallet or private key (same base inputs as other `obligation-escrow` commands) so it can attach a network provider via `getWalletOrSigner`. **Usage:** @@ -1478,7 +1478,9 @@ trustvc obligation-escrow status **Interactive Prompts:** - Path to BoE / obligation document - - *Network, obligationRegistry, and token ID are extracted from the document* +- Wallet/private key option +- Remark (optional) +- *Network, obligationRegistry, and token ID are extracted from the document* **Output:** Obligation and escrow status fields from the chain. @@ -1641,7 +1643,7 @@ User guide for electronic Bill of Exchange (BoE) / obligation flows with `trustv Classic transferable records (eBL / Token Registry + Title Escrow) use different commands. This section covers **only** Obligation Registry. -For library / SDK details, see the TrustVC README [§7c Obligation Registry (BoE)](https://github.com/TrustVC/trustvc#c-obligation-registry-boe). +For library / SDK details, see the TrustVC README [§7c Obligation Registry (BoE)](https://github.com/TrustVC/trustvc/blob/beta/README.md#c-obligation-registry-boe) (on the `beta` branch; not yet on `main`). SDK imports use the **root** package with the `*ObligationRegistry` suffix (no clash with classic ETR helpers): @@ -1692,7 +1694,7 @@ Using classic commands on a BoE document will fail or skip obligation checks. Us ### Typical workflow -``` +```text 1. Deploy Obligation Registry 2. Put the registry address into your BoE credential status 3. Sign the credential with `trustvc w3c-sign` @@ -1721,7 +1723,7 @@ Signing/building the VC can also be done with TrustVC library tools or your app **8. Return to issuer** — requires status **Rejected** or **Discharged** and **beneficiary == holder**; then issuer runs `reject-return-to-issuer` (restore) or `accept-return-to-issuer` (burn). -**8. Verify** — `trustvc verify` (auto-detects BoE vs ETR). +**9. Verify** — `trustvc verify` (auto-detects BoE vs ETR). ### Who can run what @@ -1735,7 +1737,7 @@ Signing/building the VC can also be done with TrustVC library tools or your app | Transfer / nominate / endorse | Current holder or beneficiary per Title Escrow–style rules | | `return-to-issuer` | Dual role (beneficiary == holder) | | `accept-return-to-issuer` / `reject-return-to-issuer` | Issuer | -| `obligation-escrow status` | Anyone with network access + document | +| `obligation-escrow status` | Anyone with the document + wallet/private key (read-only) | | `verify` | Anyone with the document (+ RPC when on-chain checks run) | diff --git a/src/commands/helpers.ts b/src/commands/helpers.ts index f92927f..24375a5 100644 --- a/src/commands/helpers.ts +++ b/src/commands/helpers.ts @@ -440,7 +440,12 @@ export const connectToObligationRegistry = async ({ try { signale.info(`Connecting to obligation registry at: ${address}`); const registry = new ethers.Contract(address, TrustVCToken__factory.abi, wallet as any); - if (!registry) { + const provider = wallet.provider; + if (!provider) { + throw new Error('Wallet provider is required to validate the obligation registry contract'); + } + const code = await provider.getCode(address); + if (!code || code === '0x') { throw new Error(`Failed to connect to obligation registry at address: ${address}`); } signale.success(`Successfully connected to obligation registry`); @@ -483,7 +488,12 @@ export const connectToObligationEscrow = async ({ signale.info(`Connecting to obligation escrow at: ${escrowAddress}`); const escrow = new ethers.Contract(escrowAddress, ObligationEscrow__factory.abi, wallet as any); - if (!escrow) { + const provider = wallet.provider; + if (!provider) { + throw new Error('Wallet provider is required to validate the obligation escrow contract'); + } + const code = await provider.getCode(escrowAddress); + if (!code || code === '0x') { throw new Error(`Failed to connect to obligation escrow at address: ${escrowAddress}`); } signale.success(`Successfully connected to obligation escrow`); diff --git a/src/commands/obligation-escrow/accept-return-to-issuer.ts b/src/commands/obligation-escrow/accept-return-to-issuer.ts index a4ec7f0..eefdc55 100644 --- a/src/commands/obligation-escrow/accept-return-to-issuer.ts +++ b/src/commands/obligation-escrow/accept-return-to-issuer.ts @@ -37,6 +37,7 @@ export const acceptReturnedHandler = async (args: BaseObligationEscrowCommand) = sdk: acceptReturnedObligationRegistry as any, sdkParams: { tokenId: args.tokenId, remarks: args.remark }, }); + if (!transaction) return; displayTransactionPrice( transaction as unknown as TransactionReceiptFees, args.network as NetworkCmdName, @@ -47,5 +48,6 @@ export const acceptReturnedHandler = async (args: BaseObligationEscrowCommand) = ); } catch (e) { error(getErrorMessage(e)); + process.exitCode = 1; } }; diff --git a/src/commands/obligation-escrow/accept.ts b/src/commands/obligation-escrow/accept.ts index d3b2c61..49adc7d 100644 --- a/src/commands/obligation-escrow/accept.ts +++ b/src/commands/obligation-escrow/accept.ts @@ -35,6 +35,7 @@ export const acceptHandler = async (args: BaseObligationEscrowCommand) => { sdk: acceptObligationRegistry as any, sdkParams: { remarks: args.remark }, }); + if (!transaction) return; displayTransactionPrice( transaction as unknown as TransactionReceiptFees, args.network as NetworkCmdName, @@ -45,5 +46,6 @@ export const acceptHandler = async (args: BaseObligationEscrowCommand) => { ); } catch (e) { error(getErrorMessage(e)); + process.exitCode = 1; } }; diff --git a/src/commands/obligation-escrow/discharge.ts b/src/commands/obligation-escrow/discharge.ts index 8ae5907..ae32702 100644 --- a/src/commands/obligation-escrow/discharge.ts +++ b/src/commands/obligation-escrow/discharge.ts @@ -36,6 +36,7 @@ export const dischargeHandler = async (args: BaseObligationEscrowCommand) => { sdk: dischargeObligationRegistry as any, sdkParams: { remarks: args.remark }, }); + if (!transaction) return; displayTransactionPrice( transaction as unknown as TransactionReceiptFees, args.network as NetworkCmdName, @@ -46,5 +47,6 @@ export const dischargeHandler = async (args: BaseObligationEscrowCommand) => { ); } catch (e) { error(getErrorMessage(e)); + process.exitCode = 1; } }; diff --git a/src/commands/obligation-escrow/endorse-transfer-owner.ts b/src/commands/obligation-escrow/endorse-transfer-owner.ts index d275c11..3166d5c 100644 --- a/src/commands/obligation-escrow/endorse-transfer-owner.ts +++ b/src/commands/obligation-escrow/endorse-transfer-owner.ts @@ -41,6 +41,7 @@ export const endorseHandler = async (args: ObligationEscrowNominateBeneficiaryCo sdk: transferBeneficiaryObligationRegistry as any, sdkParams: { newBeneficiaryAddress: args.newBeneficiary, remarks: args.remark }, }); + if (!transaction) return; displayTransactionPrice( transaction as unknown as TransactionReceiptFees, args.network as NetworkCmdName, @@ -51,5 +52,6 @@ export const endorseHandler = async (args: ObligationEscrowNominateBeneficiaryCo ); } catch (e) { error(getErrorMessage(e)); + process.exitCode = 1; } }; diff --git a/src/commands/obligation-escrow/nominate-transfer-owner.ts b/src/commands/obligation-escrow/nominate-transfer-owner.ts index 0d94a55..7a4397d 100644 --- a/src/commands/obligation-escrow/nominate-transfer-owner.ts +++ b/src/commands/obligation-escrow/nominate-transfer-owner.ts @@ -1,29 +1,21 @@ -import { error, info, success } from 'signale'; +import { info, success } from 'signale'; import { nominateObligationRegistry } from '@trustvc/trustvc'; import { ObligationEscrowNominateBeneficiaryCommand } from '../../types'; import { displayTransactionPrice, - getErrorMessage, getEtherscanAddress, NetworkCmdName, promptAddress, TransactionReceiptFees, } from '../../utils'; -import { promptBaseObligationEscrowInputs } from './shared'; +import { promptBaseObligationEscrowInputs, runObligationEscrowCommand } from './shared'; import { runObligationEscrowTx } from './runTx'; export const command = 'nominate-transfer-owner'; export const describe = 'Nominate a new beneficiary (owner) for a BoE obligation'; -export const handler = async (): Promise => { - try { - const answers = await promptForInputs(); - if (!answers) return; - await nominateHandler(answers); - } catch (err: unknown) { - error(err instanceof Error ? err.message : String(err)); - } -}; +export const handler = async (): Promise => + runObligationEscrowCommand(promptForInputs, nominateHandler); export const promptForInputs = async (): Promise => { const base = await promptBaseObligationEscrowInputs(); @@ -32,24 +24,21 @@ export const promptForInputs = async (): Promise { - try { - info(`Nominating beneficiary ${args.newBeneficiary} for obligation ${args.tokenId}`); - const transaction = await runObligationEscrowTx({ - args, - populate: ({ escrow }, encryptedRemark) => - escrow.nominate.populateTransaction(args.newBeneficiary, encryptedRemark), - sdk: nominateObligationRegistry as any, - sdkParams: { newBeneficiaryAddress: args.newBeneficiary, remarks: args.remark }, - }); - displayTransactionPrice( - transaction as unknown as TransactionReceiptFees, - args.network as NetworkCmdName, - ); - success(`Nominated ${args.newBeneficiary}`); - info( - `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, - ); - } catch (e) { - error(getErrorMessage(e)); - } + info(`Nominating beneficiary ${args.newBeneficiary} for obligation ${args.tokenId}`); + const transaction = await runObligationEscrowTx({ + args, + populate: ({ escrow }, encryptedRemark) => + escrow.nominate.populateTransaction(args.newBeneficiary, encryptedRemark), + sdk: nominateObligationRegistry as any, + sdkParams: { newBeneficiaryAddress: args.newBeneficiary, remarks: args.remark }, + }); + if (!transaction) return; + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Nominated ${args.newBeneficiary}`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); }; diff --git a/src/commands/obligation-escrow/reject-return-to-issuer.ts b/src/commands/obligation-escrow/reject-return-to-issuer.ts index 113d537..f810a53 100644 --- a/src/commands/obligation-escrow/reject-return-to-issuer.ts +++ b/src/commands/obligation-escrow/reject-return-to-issuer.ts @@ -37,6 +37,7 @@ export const rejectReturnedHandler = async (args: BaseObligationEscrowCommand) = sdk: rejectReturnedObligationRegistry as any, sdkParams: { tokenId: args.tokenId, remarks: args.remark }, }); + if (!transaction) return; displayTransactionPrice( transaction as unknown as TransactionReceiptFees, args.network as NetworkCmdName, @@ -47,5 +48,6 @@ export const rejectReturnedHandler = async (args: BaseObligationEscrowCommand) = ); } catch (e) { error(getErrorMessage(e)); + process.exitCode = 1; } }; diff --git a/src/commands/obligation-escrow/reject-transfer-holder.ts b/src/commands/obligation-escrow/reject-transfer-holder.ts index d4fc2ff..4226765 100644 --- a/src/commands/obligation-escrow/reject-transfer-holder.ts +++ b/src/commands/obligation-escrow/reject-transfer-holder.ts @@ -1,50 +1,39 @@ -import { error, info, success } from 'signale'; +import { info, success } from 'signale'; import { rejectTransferHolderObligationRegistry } from '@trustvc/trustvc'; import { BaseObligationEscrowCommand } from '../../types'; import { displayTransactionPrice, - getErrorMessage, getEtherscanAddress, NetworkCmdName, TransactionReceiptFees, } from '../../utils'; -import { promptBaseObligationEscrowInputs } from './shared'; +import { promptBaseObligationEscrowInputs, runObligationEscrowCommand } from './shared'; import { runObligationEscrowTx } from './runTx'; export const command = 'reject-transfer-holder'; export const describe = 'Reject a pending holder transfer on a BoE obligation'; -export const handler = async (): Promise => { - try { - const answers = await promptForInputs(); - if (!answers) return; - await rejectTransferHolderHandler(answers); - } catch (err: unknown) { - error(err instanceof Error ? err.message : String(err)); - } -}; +export const handler = async (): Promise => + runObligationEscrowCommand(promptForInputs, rejectTransferHolderHandler); export const promptForInputs = promptBaseObligationEscrowInputs; export const rejectTransferHolderHandler = async (args: BaseObligationEscrowCommand) => { - try { - info(`Rejecting holder transfer for obligation ${args.tokenId}`); - const transaction = await runObligationEscrowTx({ - args, - populate: ({ escrow }, encryptedRemark) => - escrow.rejectTransferHolder.populateTransaction(encryptedRemark), - sdk: rejectTransferHolderObligationRegistry as any, - sdkParams: { remarks: args.remark }, - }); - displayTransactionPrice( - transaction as unknown as TransactionReceiptFees, - args.network as NetworkCmdName, - ); - success(`Holder transfer rejected`); - info( - `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, - ); - } catch (e) { - error(getErrorMessage(e)); - } + info(`Rejecting holder transfer for obligation ${args.tokenId}`); + const transaction = await runObligationEscrowTx({ + args, + populate: ({ escrow }, encryptedRemark) => + escrow.rejectTransferHolder.populateTransaction(encryptedRemark), + sdk: rejectTransferHolderObligationRegistry as any, + sdkParams: { remarks: args.remark }, + }); + if (!transaction) return; + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Holder transfer rejected`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); }; diff --git a/src/commands/obligation-escrow/reject-transfer-owner-holder.ts b/src/commands/obligation-escrow/reject-transfer-owner-holder.ts index 85a0883..dc965c9 100644 --- a/src/commands/obligation-escrow/reject-transfer-owner-holder.ts +++ b/src/commands/obligation-escrow/reject-transfer-owner-holder.ts @@ -1,50 +1,39 @@ -import { error, info, success } from 'signale'; +import { info, success } from 'signale'; import { rejectTransferOwnersObligationRegistry } from '@trustvc/trustvc'; import { BaseObligationEscrowCommand } from '../../types'; import { displayTransactionPrice, - getErrorMessage, getEtherscanAddress, NetworkCmdName, TransactionReceiptFees, } from '../../utils'; -import { promptBaseObligationEscrowInputs } from './shared'; +import { promptBaseObligationEscrowInputs, runObligationEscrowCommand } from './shared'; import { runObligationEscrowTx } from './runTx'; export const command = 'reject-transfer-owner-holder'; export const describe = 'Reject a pending joint owner+holder transfer on a BoE obligation'; -export const handler = async (): Promise => { - try { - const answers = await promptForInputs(); - if (!answers) return; - await rejectTransferOwnersHandler(answers); - } catch (err: unknown) { - error(err instanceof Error ? err.message : String(err)); - } -}; +export const handler = async (): Promise => + runObligationEscrowCommand(promptForInputs, rejectTransferOwnersHandler); export const promptForInputs = promptBaseObligationEscrowInputs; export const rejectTransferOwnersHandler = async (args: BaseObligationEscrowCommand) => { - try { - info(`Rejecting owners transfer for obligation ${args.tokenId}`); - const transaction = await runObligationEscrowTx({ - args, - populate: ({ escrow }, encryptedRemark) => - escrow.rejectTransferOwners.populateTransaction(encryptedRemark), - sdk: rejectTransferOwnersObligationRegistry as any, - sdkParams: { remarks: args.remark }, - }); - displayTransactionPrice( - transaction as unknown as TransactionReceiptFees, - args.network as NetworkCmdName, - ); - success(`Owners transfer rejected`); - info( - `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, - ); - } catch (e) { - error(getErrorMessage(e)); - } + info(`Rejecting owners transfer for obligation ${args.tokenId}`); + const transaction = await runObligationEscrowTx({ + args, + populate: ({ escrow }, encryptedRemark) => + escrow.rejectTransferOwners.populateTransaction(encryptedRemark), + sdk: rejectTransferOwnersObligationRegistry as any, + sdkParams: { remarks: args.remark }, + }); + if (!transaction) return; + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Owners transfer rejected`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); }; diff --git a/src/commands/obligation-escrow/reject-transfer-owner.ts b/src/commands/obligation-escrow/reject-transfer-owner.ts index c6e4b99..0ecdbf0 100644 --- a/src/commands/obligation-escrow/reject-transfer-owner.ts +++ b/src/commands/obligation-escrow/reject-transfer-owner.ts @@ -36,6 +36,7 @@ export const rejectTransferOwnerHandler = async (args: BaseObligationEscrowComma sdk: rejectTransferBeneficiaryObligationRegistry as any, sdkParams: { remarks: args.remark }, }); + if (!transaction) return; displayTransactionPrice( transaction as unknown as TransactionReceiptFees, args.network as NetworkCmdName, @@ -46,5 +47,6 @@ export const rejectTransferOwnerHandler = async (args: BaseObligationEscrowComma ); } catch (e) { error(getErrorMessage(e)); + process.exitCode = 1; } }; diff --git a/src/commands/obligation-escrow/reject.ts b/src/commands/obligation-escrow/reject.ts index 6e5e5e2..40ea341 100644 --- a/src/commands/obligation-escrow/reject.ts +++ b/src/commands/obligation-escrow/reject.ts @@ -35,6 +35,7 @@ export const rejectHandler = async (args: BaseObligationEscrowCommand) => { sdk: rejectObligationRegistry as any, sdkParams: { remarks: args.remark }, }); + if (!transaction) return; displayTransactionPrice( transaction as unknown as TransactionReceiptFees, args.network as NetworkCmdName, @@ -45,5 +46,6 @@ export const rejectHandler = async (args: BaseObligationEscrowCommand) => { ); } catch (e) { error(getErrorMessage(e)); + process.exitCode = 1; } }; diff --git a/src/commands/obligation-escrow/return-to-issuer.ts b/src/commands/obligation-escrow/return-to-issuer.ts index 236dbc1..2f534f6 100644 --- a/src/commands/obligation-escrow/return-to-issuer.ts +++ b/src/commands/obligation-escrow/return-to-issuer.ts @@ -36,6 +36,7 @@ export const returnToIssuerHandler = async (args: BaseObligationEscrowCommand) = sdk: returnToIssuerObligationRegistry as any, sdkParams: { remarks: args.remark }, }); + if (!transaction) return; displayTransactionPrice( transaction as unknown as TransactionReceiptFees, args.network as NetworkCmdName, @@ -46,5 +47,6 @@ export const returnToIssuerHandler = async (args: BaseObligationEscrowCommand) = ); } catch (e) { error(getErrorMessage(e)); + process.exitCode = 1; } }; diff --git a/src/commands/obligation-escrow/runTx.ts b/src/commands/obligation-escrow/runTx.ts index fd5053a..d8218c1 100644 --- a/src/commands/obligation-escrow/runTx.ts +++ b/src/commands/obligation-escrow/runTx.ts @@ -35,7 +35,7 @@ export const runObligationEscrowTx = async (opts: { populate: PopulateFn; sdk: SdkCall; sdkParams: Record; -}): Promise => { +}): Promise => { const { args, populate, sdk, sdkParams } = opts; const { obligationRegistryAddress, tokenId, remark, encryptionKey, network, ...rest } = args; const wallet = await getWalletOrSigner({ network, ...rest }); @@ -60,7 +60,7 @@ export const runObligationEscrowTx = async (opts: { }); if (!shouldProceed) { - process.exit(0); + return null; } const contractOptions = { obligationRegistryAddress, tokenId }; diff --git a/src/commands/obligation-escrow/shared.ts b/src/commands/obligation-escrow/shared.ts index 15fd7f4..149100d 100644 --- a/src/commands/obligation-escrow/shared.ts +++ b/src/commands/obligation-escrow/shared.ts @@ -1,9 +1,11 @@ +import { error } from 'signale'; import { promptWalletSelection, promptAndReadDocument, promptRemark, extractObligationDocumentInfo, verifyDocumentSignature, + getErrorMessage, } from '../../utils'; import { BaseObligationEscrowCommand } from '../../types'; @@ -38,3 +40,21 @@ export const promptBaseObligationEscrowInputs = async (): Promise( + promptForInputs: () => Promise, + commandHandler: (args: TArgs) => Promise, +): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + await commandHandler(answers); + } catch (e) { + error(getErrorMessage(e)); + process.exitCode = 1; + } +}; diff --git a/src/commands/obligation-escrow/status.ts b/src/commands/obligation-escrow/status.ts index 2f3f7fd..ed1fff0 100644 --- a/src/commands/obligation-escrow/status.ts +++ b/src/commands/obligation-escrow/status.ts @@ -1,4 +1,4 @@ -import { error, info, success } from 'signale'; +import { info, success } from 'signale'; import { ObligationDocumentStatus, ObligationEscrowTerminationReason, @@ -7,8 +7,8 @@ import { isObligationRegistryRegistered, } from '@trustvc/trustvc'; import { BaseObligationEscrowCommand } from '../../types'; -import { getErrorMessage, getWalletOrSigner } from '../../utils'; -import { promptBaseObligationEscrowInputs } from './shared'; +import { getWalletOrSigner } from '../../utils'; +import { promptBaseObligationEscrowInputs, runObligationEscrowCommand } from './shared'; export const command = 'status'; export const describe = 'Read BoE obligation escrow status / registration / termination reason'; @@ -26,34 +26,23 @@ const REASON_LABEL: Record = { [ObligationEscrowTerminationReason.Discharged]: 'Discharged', }; -export const handler = async (): Promise => { - try { - const answers = await promptForInputs(); - if (!answers) return; - await statusHandler(answers); - } catch (err: unknown) { - error(err instanceof Error ? err.message : String(err)); - } -}; +export const handler = async (): Promise => + runObligationEscrowCommand(promptForInputs, statusHandler); export const promptForInputs = promptBaseObligationEscrowInputs; export const statusHandler = async (args: BaseObligationEscrowCommand) => { - try { - const { obligationRegistryAddress, tokenId, network, ...rest } = args; - const wallet = await getWalletOrSigner({ network, ...rest }); - // Escrow resolution uses contractOptions.tokenId (params.tokenId is API parity only). - const opts = { obligationRegistryAddress, tokenId }; + const { obligationRegistryAddress, tokenId, network, ...rest } = args; + const wallet = await getWalletOrSigner({ network, ...rest }); + // Escrow resolution uses contractOptions.tokenId (params.tokenId is API parity only). + const opts = { obligationRegistryAddress, tokenId }; - const status = await getObligationRegistryStatus(opts, wallet, { tokenId }); - const registered = await isObligationRegistryRegistered(opts, wallet, { tokenId }); - const reason = await getObligationEscrowTerminationReason(opts, wallet, { tokenId }); + const status = await getObligationRegistryStatus(opts, wallet, { tokenId }); + const registered = await isObligationRegistryRegistered(opts, wallet, { tokenId }); + const reason = await getObligationEscrowTerminationReason(opts, wallet, { tokenId }); - success(`Obligation ${tokenId} on ${obligationRegistryAddress}`); - info(` Status: ${STATUS_LABEL[status] ?? status} (${status})`); - info(` Registered: ${registered}`); - info(` Termination reason: ${REASON_LABEL[reason] ?? reason} (${reason})`); - } catch (e) { - error(getErrorMessage(e)); - } + success(`Obligation ${tokenId} on ${obligationRegistryAddress}`); + info(` Status: ${STATUS_LABEL[status] ?? status} (${status})`); + info(` Registered: ${registered}`); + info(` Termination reason: ${REASON_LABEL[reason] ?? reason} (${reason})`); }; diff --git a/src/commands/obligation-escrow/transfer-holder.ts b/src/commands/obligation-escrow/transfer-holder.ts index 74d7146..c3a1aa1 100644 --- a/src/commands/obligation-escrow/transfer-holder.ts +++ b/src/commands/obligation-escrow/transfer-holder.ts @@ -44,6 +44,7 @@ export const changeHolderHandler = async (args: ObligationEscrowTransferHolderCo sdk: transferHolderObligationRegistry as any, sdkParams: { holderAddress: args.newHolder, remarks: args.remark }, }); + if (!transaction) return; displayTransactionPrice( transaction as unknown as TransactionReceiptFees, args.network as NetworkCmdName, @@ -54,5 +55,6 @@ export const changeHolderHandler = async (args: ObligationEscrowTransferHolderCo ); } catch (e) { error(getErrorMessage(e)); + process.exitCode = 1; } }; diff --git a/src/commands/obligation-escrow/transfer-owner-holder.ts b/src/commands/obligation-escrow/transfer-owner-holder.ts index db39e7f..a37da7c 100644 --- a/src/commands/obligation-escrow/transfer-owner-holder.ts +++ b/src/commands/obligation-escrow/transfer-owner-holder.ts @@ -1,29 +1,21 @@ -import { error, info, success } from 'signale'; +import { info, success } from 'signale'; import { transferOwnersObligationRegistry } from '@trustvc/trustvc'; import { ObligationEscrowEndorseTransferOfOwnersCommand } from '../../types'; import { displayTransactionPrice, - getErrorMessage, getEtherscanAddress, NetworkCmdName, promptAddress, TransactionReceiptFees, } from '../../utils'; -import { promptBaseObligationEscrowInputs } from './shared'; +import { promptBaseObligationEscrowInputs, runObligationEscrowCommand } from './shared'; import { runObligationEscrowTx } from './runTx'; export const command = 'transfer-owner-holder'; export const describe = 'Transfer both beneficiary and holder of a BoE obligation'; -export const handler = async (): Promise => { - try { - const answers = await promptForInputs(); - if (!answers) return; - await transferOwnersHandler(answers); - } catch (err: unknown) { - error(err instanceof Error ? err.message : String(err)); - } -}; +export const handler = async (): Promise => + runObligationEscrowCommand(promptForInputs, transferOwnersHandler); export const promptForInputs = async (): Promise => { @@ -36,30 +28,27 @@ export const promptForInputs = export const transferOwnersHandler = async ( args: ObligationEscrowEndorseTransferOfOwnersCommand, ) => { - try { - info( - `Transferring owners of obligation ${args.tokenId} to owner ${args.newOwner} / holder ${args.newHolder}`, - ); - const transaction = await runObligationEscrowTx({ - args, - populate: ({ escrow }, encryptedRemark) => - escrow.transferOwners.populateTransaction(args.newOwner, args.newHolder, encryptedRemark), - sdk: transferOwnersObligationRegistry as any, - sdkParams: { - newBeneficiaryAddress: args.newOwner, - newHolderAddress: args.newHolder, - remarks: args.remark, - }, - }); - displayTransactionPrice( - transaction as unknown as TransactionReceiptFees, - args.network as NetworkCmdName, - ); - success(`Owners transferred`); - info( - `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, - ); - } catch (e) { - error(getErrorMessage(e)); - } + info( + `Transferring owners of obligation ${args.tokenId} to owner ${args.newOwner} / holder ${args.newHolder}`, + ); + const transaction = await runObligationEscrowTx({ + args, + populate: ({ escrow }, encryptedRemark) => + escrow.transferOwners.populateTransaction(args.newOwner, args.newHolder, encryptedRemark), + sdk: transferOwnersObligationRegistry as any, + sdkParams: { + newBeneficiaryAddress: args.newOwner, + newHolderAddress: args.newHolder, + remarks: args.remark, + }, + }); + if (!transaction) return; + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Owners transferred`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); }; diff --git a/src/utils/cli-errors.ts b/src/utils/cli-errors.ts index 358190e..28f29a5 100644 --- a/src/utils/cli-errors.ts +++ b/src/utils/cli-errors.ts @@ -76,6 +76,12 @@ const REVERT_SELECTOR_TO_NAME: Record = { const UNKNOWN_CUSTOM_ERROR = /unknown custom error/i; +/** Solidity custom errors are PascalCase identifiers (e.g. OwnerHolderMustDiffer). */ +const SOLIDITY_CUSTOM_ERROR_NAME = /^[A-Z][A-Za-z0-9_]*$/; + +const isKnownRevertName = (label: string): boolean => + label in KNOWN_REVERT_MESSAGES || Object.values(REVERT_SELECTOR_TO_NAME).includes(label); + type EthersCallException = { reason?: unknown; shortMessage?: unknown; @@ -101,6 +107,27 @@ const normalizeLabel = (raw: string): string | undefined => { return ident?.[1]; }; +/** Labels after generic "failed:" — only known reverts or Solidity-shaped names. */ +const acceptFailedSuffixLabel = (raw: string): string | undefined => { + const label = normalizeLabel(raw); + if (!label) return undefined; + if (isKnownRevertName(label) || SOLIDITY_CUSTOM_ERROR_NAME.test(label)) return label; + return undefined; +}; + +const labelFromWrappedMessage = (message: string): string | undefined => { + const fromContract = message.match(/Contract reverted with\s+([A-Za-z0-9_]+)/); + if (fromContract?.[1]) { + const label = normalizeLabel(fromContract[1]); + if (label) return label; + } + const fromFailed = message.match(/failed:\s*([A-Za-z0-9_]+)/); + if (fromFailed?.[1]) { + return acceptFailedSuffixLabel(fromFailed[1]); + } + return undefined; +}; + const collectPossibleData = (err: EthersCallException): string[] => { const values: unknown[] = [err.data, err.info?.error?.data, err.error?.data]; const out: string[] = []; @@ -132,10 +159,12 @@ export function extractContractRevertLabel(error: unknown): string | undefined { const err = asEthersError(error); if (!err) { if (error instanceof Error) { - const fromMessage = error.message.match( - /(?:execution reverted:\s*|Contract reverted with\s*|failed:\s*)([A-Za-z0-9_]+)/, - ); - return fromMessage?.[1] ? normalizeLabel(fromMessage[1]) : undefined; + const fromExec = error.message.match(/execution reverted:\s*([A-Za-z0-9_]+)/); + if (fromExec?.[1]) { + const label = normalizeLabel(fromExec[1]); + if (label) return label; + } + return labelFromWrappedMessage(error.message); } return undefined; } @@ -172,11 +201,8 @@ export function extractContractRevertLabel(error: unknown): string | undefined { const label = normalizeLabel(custom[1]); if (label) return label; } - const wrapped = err.message.match(/(?:Contract reverted with\s*|failed:\s*)([A-Za-z0-9_]+)/); - if (wrapped?.[1]) { - const label = normalizeLabel(wrapped[1]); - if (label) return label; - } + const wrapped = labelFromWrappedMessage(err.message); + if (wrapped) return wrapped; } return undefined; diff --git a/src/utils/obligation-document.ts b/src/utils/obligation-document.ts index c4c9ceb..8298af5 100644 --- a/src/utils/obligation-document.ts +++ b/src/utils/obligation-document.ts @@ -48,6 +48,11 @@ export const extractObligationDocumentInfo = async ( if (!chainId) { throw new Error('Document does not contain a valid chain ID'); } + if (!(chainId in SUPPORTED_CHAINS)) { + throw new Error( + `Unsupported chain ID in obligation document: ${chainId}. Use a BoE document on a supported network.`, + ); + } const network = SUPPORTED_CHAINS[chainId].name; const documentId = document.id || 'N/A'; diff --git a/tests/utils/contract-errors.test.ts b/tests/utils/contract-errors.test.ts index 45c6482..5712044 100644 --- a/tests/utils/contract-errors.test.ts +++ b/tests/utils/contract-errors.test.ts @@ -53,4 +53,15 @@ describe('contract revert error formatting', () => { ); expect(getErrorMessage(wrapped)).toMatch(/OwnerHolderMustDiffer:/); }); + + it('rejects generic failed: suffixes that are not contract reverts', () => { + const preprocessing = new Error('SDK preprocessing failed: badInput'); + expect(extractContractRevertLabel(preprocessing)).toBeUndefined(); + expect(isContractCallException(preprocessing)).toBe(false); + }); + + it('preserves Contract reverted with labels', () => { + const wrapped = new Error('Contract reverted with OwnerHolderMustDiffer'); + expect(extractContractRevertLabel(wrapped)).toBe('OwnerHolderMustDiffer'); + }); }); From 3390fcc516645201d8581023710d15b88530dd59 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Sat, 1 Aug 2026 00:43:45 +0530 Subject: [PATCH 12/20] Update .github/workflows/ci.yml Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9c6c0e..bcd3036 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,9 @@ name: CI # Same checks for PRs targeting main or beta (and for pushes to those branches). on: push: + branches: + - main + - beta pull_request: branches: - main From 4c3f853e747f5b28ddc67f042f2142ae9ece56a3 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Sat, 1 Aug 2026 00:44:20 +0530 Subject: [PATCH 13/20] Update src/utils/obligation-document.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/utils/obligation-document.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/utils/obligation-document.ts b/src/utils/obligation-document.ts index 8298af5..92743d7 100644 --- a/src/utils/obligation-document.ts +++ b/src/utils/obligation-document.ts @@ -39,6 +39,11 @@ export const extractObligationDocumentInfo = async ( ); } + if (!obligationRegistry) { + throw new Error('Document does not contain a valid obligationRegistry address'); + } + if (!tokenId) { + throw new Error('Document does not contain a valid token ID'); if (!obligationRegistry) { throw new Error('Document does not contain a valid obligationRegistry address'); } @@ -48,10 +53,8 @@ export const extractObligationDocumentInfo = async ( if (!chainId) { throw new Error('Document does not contain a valid chain ID'); } - if (!(chainId in SUPPORTED_CHAINS)) { - throw new Error( - `Unsupported chain ID in obligation document: ${chainId}. Use a BoE document on a supported network.`, - ); + if (!SUPPORTED_CHAINS[chainId]) { + throw new Error(`Document references unsupported chain ID: ${chainId}`); } const network = SUPPORTED_CHAINS[chainId].name; From ffe15447f67cf14c87561315e57450644e6b0d6a Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Sat, 1 Aug 2026 00:58:57 +0530 Subject: [PATCH 14/20] Refactor error handling in extractObligationDocumentInfo function - Removed redundant error checks for obligationRegistry and tokenId. - Enhanced error message for unsupported chain IDs to provide clearer guidance on using a valid BoE document. --- .../obligation-credential-subject.sample.json | 55 ------------------- src/utils/obligation-document.ts | 11 ++-- 2 files changed, 4 insertions(+), 62 deletions(-) delete mode 100644 samples/obligation-credential-subject.sample.json diff --git a/samples/obligation-credential-subject.sample.json b/samples/obligation-credential-subject.sample.json deleted file mode 100644 index 933ee8e..0000000 --- a/samples/obligation-credential-subject.sample.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "@context": [ - "https://www.w3.org/ns/credentials/v2", - "https://w3id.org/security/data-integrity/v2", - "https://trustvc.io/context/render-method-context-v2.json", - "https://trustvc.io/context/bill-of-exchange.json", - "https://trustvc.io/context/obligation-records-context.json", - "https://trustvc.io/context/qrcode-context.json" - ], - "renderMethod": [ - { - "type": "EMBEDDED_RENDERER", - "templateName": "BILL_OF_EXCHANGE", - "id": "http://localhost:9000" - } - ], - "credentialSubject": { - "electronicDocumentIdentifier": "urn:uuid:e6f4b2a1-9c3d-4e8f-a7b0-1d2e3f4a5b6c", - "referenceNumber": "BOE-2026-00147", - "amountInFigures": "128500.00", - "amountInWords": "One hundred twenty-eight thousand five hundred United States Dollars only", - "currencyCode": "USD", - "blDate": "2026-06-28", - "placeOfIssue": "Singapore", - "dateOfIssue": "2026-07-06", - "tenor": "At 90 days sight", - "payee": "Meridian Commodities Pte Ltd", - "drawnUnder": "Documentary Credit No. LC-DBS-2026-88341 / Invoice No. INV-2026-0456 / B/L No. BL-SIN-2026-0781", - "drawnUnderDate": "2026-06-15", - "issuedBy": "DBS Bank Ltd", - "drawee": { - "name": "Fairview Industries Inc.", - "address": "1201 Market Street, Suite 900, Wilmington, DE 19801, USA", - "authorisedSignatoryName": "James R. Carter", - "signature": "data:image/png;base64,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" - }, - "drawer": { - "name": "Meridian Commodities Pte Ltd", - "address": "8 Marina Boulevard, #24-01, Singapore 018981", - "authorisedSignatoryName": "Wei Ling Tan", - "signature": "data:image/png;base64,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" - } - }, - "type": ["VerifiableCredential"], - "credentialStatus": { - "type": "TransferableRecords", - "tokenNetwork": { - "chain": "Sepolia", - "chainId": 11155111 - }, - "obligationRegistry": "0xE8c7516BD4FC64DF039BAA586035dfaA57520BE4" - }, - "issuer": "did:web:didhost.vercel.app", - "validFrom": "2026-07-06T09:00:00Z" -} diff --git a/src/utils/obligation-document.ts b/src/utils/obligation-document.ts index 92743d7..8298af5 100644 --- a/src/utils/obligation-document.ts +++ b/src/utils/obligation-document.ts @@ -39,11 +39,6 @@ export const extractObligationDocumentInfo = async ( ); } - if (!obligationRegistry) { - throw new Error('Document does not contain a valid obligationRegistry address'); - } - if (!tokenId) { - throw new Error('Document does not contain a valid token ID'); if (!obligationRegistry) { throw new Error('Document does not contain a valid obligationRegistry address'); } @@ -53,8 +48,10 @@ export const extractObligationDocumentInfo = async ( if (!chainId) { throw new Error('Document does not contain a valid chain ID'); } - if (!SUPPORTED_CHAINS[chainId]) { - throw new Error(`Document references unsupported chain ID: ${chainId}`); + if (!(chainId in SUPPORTED_CHAINS)) { + throw new Error( + `Unsupported chain ID in obligation document: ${chainId}. Use a BoE document on a supported network.`, + ); } const network = SUPPORTED_CHAINS[chainId].name; From 6dad862cfd0b040e7e888406d6547b95a784f72d Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Sat, 1 Aug 2026 01:17:05 +0530 Subject: [PATCH 15/20] Update README and command descriptions for obligation-escrowd --- README.md | 92 ++++++++++++++++--- src/commands/obligation-escrow/discharge.ts | 3 +- src/commands/obligation-escrow/reject.ts | 2 +- .../obligation-escrow/return-to-issuer.ts | 2 +- 4 files changed, 83 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index a312831..afd3f57 100644 --- a/README.md +++ b/README.md @@ -1419,7 +1419,7 @@ trustvc obligation-escrow accept **Interactive Prompts:** -- Path to BoE / obligation documentverify +- Path to BoE / obligation document - *Network, obligationRegistry, token ID, and document ID are extracted from the document* - Wallet/private key option - Remark (optional) @@ -1429,7 +1429,7 @@ Transaction receipt confirming acceptance. #### obligation-escrow reject -Rejects an obligation on the ObligationEscrow. +Rejects an issued BoE obligation (Issued → Rejected). Burns the title (takes it out of circulation). **Usage:** @@ -1444,11 +1444,11 @@ trustvc obligation-escrow reject - Remark (optional) **Output:** -Transaction receipt confirming rejection. +Transaction receipt confirming rejection (and burn). #### obligation-escrow discharge -Discharges an accepted obligation on the ObligationEscrow. +Discharges an accepted BoE obligation (Accepted → Discharged). Burns the title. **Usage:** @@ -1463,7 +1463,7 @@ trustvc obligation-escrow discharge - Remark (optional) **Output:** -Transaction receipt confirming discharge. +Transaction receipt confirming discharge (and burn). #### obligation-escrow status @@ -1487,7 +1487,7 @@ Obligation and escrow status fields from the chain. #### obligation-escrow transfer-holder -Transfers the holder of a BoE obligation record. Transfer, nominate, endorse, return, and reject variants mirror `title-escrow` but operate on ObligationEscrow via `obligation-escrow `. +Transfers the holder of a BoE obligation record. Nominate, endorse, transfer-owner-holder, and reject-transfer variants mirror `title-escrow` but operate on ObligationEscrow via `obligation-escrow `. **Usage:** @@ -1495,7 +1495,6 @@ Transfers the holder of a BoE obligation record. Transfer, nominate, endorse, re trustvc obligation-escrow transfer-holder # Also: # nominate-transfer-owner, endorse-transfer-owner, transfer-owner-holder -# return-to-issuer, accept-return-to-issuer, reject-return-to-issuer # reject-transfer-holder, reject-transfer-owner, reject-transfer-owner-holder ``` @@ -1509,6 +1508,73 @@ trustvc obligation-escrow transfer-holder **Output:** Transaction receipt confirming the escrow action. +#### obligation-escrow return-to-issuer + +Returns the BoE obligation to the issuer. Same rules as classic ETR: the connected wallet must be both the current beneficiary (owner) and the current holder. + +**Who Can Execute:** +Both the **current holder** and **current beneficiary (owner)** must execute this together, or the entity that holds both roles. + +**Usage:** + +```sh +trustvc obligation-escrow return-to-issuer +``` + +**Interactive Prompts:** + +- Path to BoE / obligation document + - *Network, obligationRegistry, token ID, and document ID are extracted from the document* +- Wallet/private key option +- Remark (optional) + +**Output:** +Transaction receipt confirming return to issuer. + +#### obligation-escrow accept-return-to-issuer + +Accepts a returned BoE (issuer burn / shred). + +**Who Can Execute:** +Only the **issuer** (wallet with accepter role on the obligation registry). + +**Usage:** + +```sh +trustvc obligation-escrow accept-return-to-issuer +``` + +**Interactive Prompts:** + +- Path to BoE / obligation document +- Wallet/private key option +- Remark (optional) + +**Output:** +Transaction receipt confirming acceptance (burn). + +#### obligation-escrow reject-return-to-issuer + +Rejects a returned BoE and restores it to escrow (issuer restore). + +**Who Can Execute:** +Only the **issuer** (wallet with restorer role on the obligation registry). + +**Usage:** + +```sh +trustvc obligation-escrow reject-return-to-issuer +``` + +**Interactive Prompts:** + +- Path to BoE / obligation document +- Wallet/private key option +- Remark (optional) + +**Output:** +Transaction receipt confirming rejection (restore). + ## Configuration @@ -1699,7 +1765,7 @@ Using classic commands on a BoE document will fail or skip obligation checks. Us 2. Put the registry address into your BoE credential status 3. Sign the credential with `trustvc w3c-sign` 4. Mint (issuer wallet) -5. Accept or reject (holder) ──► optional transfers / discharge / return +5. Accept or reject (holder) — or transfer / discharge / return as needed 6. Verify with `trustvc verify` ``` @@ -1713,15 +1779,15 @@ Signing/building the VC can also be done with TrustVC library tools or your app **3. Mint** — `trustvc obligation-registry mint` with the signed BoE JSON from `w3c-sign` (registry, network, token ID, document ID are extracted). -**4. Accept or reject (holder)** — `trustvc obligation-escrow accept` or `reject` while **beneficiary ≠ holder**. Reject only sets status to Rejected; closing requires return-to-issuer afterward. +**4. Accept or reject (holder)** — `trustvc obligation-escrow accept` or `reject` while **beneficiary ≠ holder**. Accept moves Issued → Accepted. Reject moves Issued → Rejected and burns the title. **5. Status** — `trustvc obligation-escrow status` reads `Issued` / `Accepted` / `Rejected` / `Discharged`, registration, and termination reason. -**6. Transfers (optional)** — `obligation-escrow transfer-holder`, nominate/endorse/return variants mirror `title-escrow`. +**6. Transfers (optional)** — `obligation-escrow transfer-holder`, nominate/endorse/reject-transfer variants mirror `title-escrow`. -**7. Discharge (optional)** — `trustvc obligation-escrow discharge` by **beneficiary** after Accepted (sets Discharged; does not burn). +**7. Discharge (optional)** — `trustvc obligation-escrow discharge` by **beneficiary** after Accepted (sets Discharged and burns). -**8. Return to issuer** — requires status **Rejected** or **Discharged** and **beneficiary == holder**; then issuer runs `reject-return-to-issuer` (restore) or `accept-return-to-issuer` (burn). +**8. Return to issuer** — same as classic ETR: **beneficiary == holder**, then issuer runs `reject-return-to-issuer` (restore) or `accept-return-to-issuer` (burn). No Rejected/Discharged status requirement. **9. Verify** — `trustvc verify` (auto-detects BoE vs ETR). @@ -1735,7 +1801,7 @@ Signing/building the VC can also be done with TrustVC library tools or your app | `obligation-escrow accept` / `reject` | Holder (roles split) | | `obligation-escrow discharge` | Beneficiary | | Transfer / nominate / endorse | Current holder or beneficiary per Title Escrow–style rules | -| `return-to-issuer` | Dual role (beneficiary == holder) | +| `return-to-issuer` | Dual role (beneficiary == holder), same as ETR | | `accept-return-to-issuer` / `reject-return-to-issuer` | Issuer | | `obligation-escrow status` | Anyone with the document + wallet/private key (read-only) | | `verify` | Anyone with the document (+ RPC when on-chain checks run) | diff --git a/src/commands/obligation-escrow/discharge.ts b/src/commands/obligation-escrow/discharge.ts index ae32702..9539ddb 100644 --- a/src/commands/obligation-escrow/discharge.ts +++ b/src/commands/obligation-escrow/discharge.ts @@ -12,7 +12,8 @@ import { promptBaseObligationEscrowInputs } from './shared'; import { runObligationEscrowTx } from './runTx'; export const command = 'discharge'; -export const describe = 'Beneficiary discharges an accepted BoE obligation (terminal Discharged)'; +export const describe = + 'Beneficiary discharges an accepted BoE obligation (Accepted → Discharged, burns)'; export const handler = async (): Promise => { try { diff --git a/src/commands/obligation-escrow/reject.ts b/src/commands/obligation-escrow/reject.ts index 40ea341..1a956f5 100644 --- a/src/commands/obligation-escrow/reject.ts +++ b/src/commands/obligation-escrow/reject.ts @@ -12,7 +12,7 @@ import { promptBaseObligationEscrowInputs } from './shared'; import { runObligationEscrowTx } from './runTx'; export const command = 'reject'; -export const describe = 'Holder rejects an issued BoE obligation (terminal Rejected)'; +export const describe = 'Holder rejects an issued BoE obligation (Issued → Rejected, burns)'; export const handler = async (): Promise => { try { diff --git a/src/commands/obligation-escrow/return-to-issuer.ts b/src/commands/obligation-escrow/return-to-issuer.ts index 2f534f6..5dcd6b8 100644 --- a/src/commands/obligation-escrow/return-to-issuer.ts +++ b/src/commands/obligation-escrow/return-to-issuer.ts @@ -12,7 +12,7 @@ import { promptBaseObligationEscrowInputs } from './shared'; import { runObligationEscrowTx } from './runTx'; export const command = 'return-to-issuer'; -export const describe = 'Return a BoE obligation to the issuer (requires dual role)'; +export const describe = 'Return a BoE obligation to the issuer (dual role, same as classic ETR)'; export const handler = async (): Promise => { try { From a5534dcf3691e7ae9725a832fc8192836c875d1b Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Mon, 3 Aug 2026 12:50:35 +0530 Subject: [PATCH 16/20] Update @trustvc/trustvc dependency to version 2.16.0-beta.1 and add tests for verify obligation command --- package-lock.json | 8 +- package.json | 2 +- tests/commands/verify.obligation.test.ts | 131 +++++++++++++++++++++++ 3 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 tests/commands/verify.obligation.test.ts diff --git a/package-lock.json b/package-lock.json index 29c40c5..a694315 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@inquirer/prompts": "^5.3.8", - "@trustvc/trustvc": "2.15.0-beta.3", + "@trustvc/trustvc": "2.16.0-beta.1", "@types/yargs": "^17.0.32", "chalk": "^4.1.2", "dotenv": "^16.0.0", @@ -5548,9 +5548,9 @@ } }, "node_modules/@trustvc/trustvc": { - "version": "2.15.0-beta.3", - "resolved": "https://registry.npmjs.org/@trustvc/trustvc/-/trustvc-2.15.0-beta.3.tgz", - "integrity": "sha512-EFiYmXZulyWnq+g3tb/yrpANkggE76QjDILuNE/68ZWdh9cw8yUxR+hcrqkk8tbhapmgulzQjtTuuASnuD1xog==", + "version": "2.16.0-beta.1", + "resolved": "https://registry.npmjs.org/@trustvc/trustvc/-/trustvc-2.16.0-beta.1.tgz", + "integrity": "sha512-sXklxfioSrIV+ZyuYsyPy+WMbgOPcRweJTd2Tklg8ZdF/tQlb2yVHDTPLycSNI3YOilpRhKn9p0Y2hSJxTuRRw==", "license": "Apache-2.0", "dependencies": { "@tradetrust-tt/dnsprove": "^2.18.0", diff --git a/package.json b/package.json index 93c3156..a38b155 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ }, "dependencies": { "@inquirer/prompts": "^5.3.8", - "@trustvc/trustvc": "2.15.0-beta.3", + "@trustvc/trustvc": "2.16.0-beta.1", "@types/yargs": "^17.0.32", "chalk": "^4.1.2", "dotenv": "^16.0.0", diff --git a/tests/commands/verify.obligation.test.ts b/tests/commands/verify.obligation.test.ts new file mode 100644 index 0000000..cadc915 --- /dev/null +++ b/tests/commands/verify.obligation.test.ts @@ -0,0 +1,131 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { ObligationDocumentStatus, ObligationEscrowTerminationReason } from '@trustvc/trustvc'; +import { getObligationDocumentStatus, verify } from '../../src/commands/verify'; + +vi.mock('signale', () => ({ + default: { + success: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + info: vi.fn(), + }, + Signale: vi.fn().mockImplementation(() => ({ + await: vi.fn(), + success: vi.fn(), + })), +})); + +const verifyDocumentMock = vi.fn(); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + verifyDocument: (...args: unknown[]) => verifyDocumentMock(...args), + isObligationRecord: vi.fn(() => true), + isTransferableRecord: vi.fn(() => false), + isDocumentRevokable: vi.fn(() => false), + isWrappedV2Document: vi.fn(() => false), + isWrappedV3Document: vi.fn(() => false), + getChainId: vi.fn(() => 80002), + }; +}); + +vi.mock('../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getSupportedNetworkNameFromId: vi.fn(() => 'amoy'), + getSupportedNetwork: vi.fn(() => ({ + provider: () => ({ getNetwork: async () => ({ chainId: 80002 }) }), + })), + CaptureConsoleWarn: (fn: () => unknown) => ({ result: fn(), warnings: null }), + CaptureConsoleWarnAsync: async (fn: () => Promise) => ({ + result: await fn(), + warnings: null, + }), + }; +}); + +const shreddedObligationFragments = [ + { type: 'DOCUMENT_INTEGRITY', name: 'W3CSignatureIntegrity', status: 'VALID' }, + { + type: 'DOCUMENT_STATUS', + name: 'ObligationRecords', + status: 'VALID', + data: { + obligationRegistry: '0xRegistry', + // Reject/discharge/accept-return burns to 0xdEaD; SDK still reports minted (VALID), + // same as classic ETR shredded titles. + status: ObligationDocumentStatus.Rejected, + terminationReason: ObligationEscrowTerminationReason.Rejected, + }, + }, + { type: 'ISSUER_IDENTITY', name: 'W3CIssuerIdentity', status: 'VALID' }, +]; + +describe('verify obligation / BoE (shredded titles)', () => { + beforeEach(() => { + vi.clearAllMocks(); + verifyDocumentMock.mockResolvedValue(shreddedObligationFragments); + }); + + describe('getObligationDocumentStatus', () => { + it('returns registry status for VALID ObligationRecords after shred (rejected)', () => { + expect(getObligationDocumentStatus(shreddedObligationFragments as never)).toEqual({ + obligationRegistry: '0xRegistry', + status: ObligationDocumentStatus.Rejected, + terminationReason: ObligationEscrowTerminationReason.Rejected, + }); + }); + + it('returns registry status for VALID ObligationRecords after discharge shred', () => { + const fragments = [ + { + type: 'DOCUMENT_STATUS', + name: 'ObligationRecords', + status: 'VALID', + data: { + obligationRegistry: '0xRegistry', + status: ObligationDocumentStatus.Discharged, + terminationReason: ObligationEscrowTerminationReason.Discharged, + }, + }, + ]; + + expect(getObligationDocumentStatus(fragments as never)).toEqual({ + obligationRegistry: '0xRegistry', + status: ObligationDocumentStatus.Discharged, + terminationReason: ObligationEscrowTerminationReason.Discharged, + }); + }); + + it('returns null when ObligationRecords is INVALID (unminted / zero owner)', () => { + const fragments = [ + { + type: 'DOCUMENT_STATUS', + name: 'ObligationRecords', + status: 'INVALID', + reason: { message: 'Document has not been issued under token registry' }, + data: { obligationRegistry: '0xRegistry' }, + }, + ]; + + expect(getObligationDocumentStatus(fragments as never)).toBeNull(); + }); + }); + + describe('verify', () => { + it('treats shredded BoE as DOCUMENT_STATUS VALID and logs obligation status', async () => { + const signale = await import('signale'); + + await verify({ id: 'urn:uuid:shredded-boe' } as never); + + expect(verifyDocumentMock).toHaveBeenCalled(); + expect(signale.default.success).toHaveBeenCalledWith('DOCUMENT_STATUS: VALID'); + expect(signale.default.info).toHaveBeenCalledWith( + 'Obligation document status: registry=0xRegistry status=2 terminationReason=2', + ); + }); + }); +}); From 2c88be0620abfb9b371d728f08d340fb01cedef8 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Mon, 3 Aug 2026 13:46:41 +0530 Subject: [PATCH 17/20] Enhance obligation escrow command handlers and improve error handling - Refactored command handlers for various obligation escrow actions to utilize a shared execution function, reducing code duplication. - Improved error handling to ensure proper exit codes on transaction failures. - Updated README to reflect changes in command usage and prerequisites for Node.js version. --- README.md | 11 ++-- .../accept-return-to-issuer.ts | 13 +---- src/commands/obligation-escrow/accept.ts | 13 +---- src/commands/obligation-escrow/discharge.ts | 13 +---- .../endorse-transfer-owner.ts | 13 +---- .../reject-return-to-issuer.ts | 13 +---- .../reject-transfer-owner.ts | 13 +---- src/commands/obligation-escrow/reject.ts | 13 +---- .../obligation-escrow/return-to-issuer.ts | 13 +---- src/commands/obligation-escrow/runTx.ts | 4 ++ .../obligation-escrow/transfer-holder.ts | 13 +---- src/commands/obligation-registry/deploy.ts | 45 +++++++++++---- src/commands/obligation-registry/mint.ts | 12 +++- src/commands/verify.ts | 56 +++++++++++++++---- src/utils/cli-errors.ts | 22 +++----- src/utils/cli-options.ts | 7 ++- tests/commands/verify.test.ts | 37 +++++++++++- tests/utils/contract-errors.test.ts | 10 ++++ 18 files changed, 184 insertions(+), 137 deletions(-) diff --git a/README.md b/README.md index afd3f57..ec278d5 100644 --- a/README.md +++ b/README.md @@ -49,11 +49,11 @@ BoE on-chain helpers are imported from the **root** package (`mintObligationRegi ## Prerequisites -- **Node.js 22+** — Download from [nodejs.org](https://nodejs.org) or use [nvm](https://github.com/nvm-sh/nvm): +- **Node.js 22.19.5+** — Download from [nodejs.org](https://nodejs.org) or use [nvm](https://github.com/nvm-sh/nvm): ```sh -nvm install 22 -nvm use 22 +nvm install 22.19.5 +nvm use 22.19.5 ``` @@ -305,7 +305,6 @@ trustvc verify | | `[oa-decrypt](#oa-decrypt)` | Decrypt an OA document encrypted with oa-encrypt | | **Token Registry** | `[mint](#mint)` | Mint tokens to blockchain registries | | | `[token-registry deploy](#token-registry-deploy)` | Deploy token registry contracts | -| | `[mint](#mint)` | Mint tokens to blockchain registries | | | `token-registry mint` | Alternative: `mint` | | **Document Store** | `[document-store deploy](#document-store-deploy)` | Deploy document store contracts | | | `[document-store issue](#document-store-issue)` | Issue document hashes | @@ -1513,7 +1512,7 @@ Transaction receipt confirming the escrow action. Returns the BoE obligation to the issuer. Same rules as classic ETR: the connected wallet must be both the current beneficiary (owner) and the current holder. **Who Can Execute:** -Both the **current holder** and **current beneficiary (owner)** must execute this together, or the entity that holds both roles. +One wallet holding **both** the current beneficiary (owner) and current holder roles (`beneficiary == holder`). The CLI signs with a single wallet; there is no separate holder/beneficiary co-signature flow. **Usage:** @@ -1738,7 +1737,7 @@ You do **not** need to call the TypeScript SDK directly — the CLI wraps it wit ### Before you start 1. **Install the CLI** — `npm install -g @trustvc/trustvc-cli` or `npx @trustvc/trustvc-cli ` -2. **Node.js 22+** +2. **Node.js 22.19.5+** 3. **A wallet** — encrypted wallet file (recommended), private key, or key file 4. **A signed BoE document** — set `credentialStatus.obligationRegistry`, then sign with `trustvc w3c-sign` (not `tokenRegistry`) 5. **Network access** — select network in prompts, or set a custom RPC (e.g. `export SEPOLIA_RPC=…`) diff --git a/src/commands/obligation-escrow/accept-return-to-issuer.ts b/src/commands/obligation-escrow/accept-return-to-issuer.ts index eefdc55..fd2d57f 100644 --- a/src/commands/obligation-escrow/accept-return-to-issuer.ts +++ b/src/commands/obligation-escrow/accept-return-to-issuer.ts @@ -8,21 +8,14 @@ import { NetworkCmdName, TransactionReceiptFees, } from '../../utils'; -import { promptBaseObligationEscrowInputs } from './shared'; +import { promptBaseObligationEscrowInputs, runObligationEscrowCommand } from './shared'; import { runObligationEscrowTx } from './runTx'; export const command = 'accept-return-to-issuer'; export const describe = 'Issuer accepts a returned BoE obligation (burn / shred)'; -export const handler = async (): Promise => { - try { - const answers = await promptForInputs(); - if (!answers) return; - await acceptReturnedHandler(answers); - } catch (err: unknown) { - error(err instanceof Error ? err.message : String(err)); - } -}; +export const handler = async (): Promise => + runObligationEscrowCommand(promptForInputs, acceptReturnedHandler); export const promptForInputs = promptBaseObligationEscrowInputs; diff --git a/src/commands/obligation-escrow/accept.ts b/src/commands/obligation-escrow/accept.ts index 49adc7d..928b253 100644 --- a/src/commands/obligation-escrow/accept.ts +++ b/src/commands/obligation-escrow/accept.ts @@ -8,21 +8,14 @@ import { NetworkCmdName, TransactionReceiptFees, } from '../../utils'; -import { promptBaseObligationEscrowInputs } from './shared'; +import { promptBaseObligationEscrowInputs, runObligationEscrowCommand } from './shared'; import { runObligationEscrowTx } from './runTx'; export const command = 'accept'; export const describe = 'Holder accepts an issued BoE obligation (Issued → Accepted)'; -export const handler = async (): Promise => { - try { - const answers = await promptForInputs(); - if (!answers) return; - await acceptHandler(answers); - } catch (err: unknown) { - error(err instanceof Error ? err.message : String(err)); - } -}; +export const handler = async (): Promise => + runObligationEscrowCommand(promptForInputs, acceptHandler); export const promptForInputs = promptBaseObligationEscrowInputs; diff --git a/src/commands/obligation-escrow/discharge.ts b/src/commands/obligation-escrow/discharge.ts index 9539ddb..0f95297 100644 --- a/src/commands/obligation-escrow/discharge.ts +++ b/src/commands/obligation-escrow/discharge.ts @@ -8,22 +8,15 @@ import { NetworkCmdName, TransactionReceiptFees, } from '../../utils'; -import { promptBaseObligationEscrowInputs } from './shared'; +import { promptBaseObligationEscrowInputs, runObligationEscrowCommand } from './shared'; import { runObligationEscrowTx } from './runTx'; export const command = 'discharge'; export const describe = 'Beneficiary discharges an accepted BoE obligation (Accepted → Discharged, burns)'; -export const handler = async (): Promise => { - try { - const answers = await promptForInputs(); - if (!answers) return; - await dischargeHandler(answers); - } catch (err: unknown) { - error(err instanceof Error ? err.message : String(err)); - } -}; +export const handler = async (): Promise => + runObligationEscrowCommand(promptForInputs, dischargeHandler); export const promptForInputs = promptBaseObligationEscrowInputs; diff --git a/src/commands/obligation-escrow/endorse-transfer-owner.ts b/src/commands/obligation-escrow/endorse-transfer-owner.ts index 3166d5c..06e556b 100644 --- a/src/commands/obligation-escrow/endorse-transfer-owner.ts +++ b/src/commands/obligation-escrow/endorse-transfer-owner.ts @@ -9,21 +9,14 @@ import { promptAddress, TransactionReceiptFees, } from '../../utils'; -import { promptBaseObligationEscrowInputs } from './shared'; +import { promptBaseObligationEscrowInputs, runObligationEscrowCommand } from './shared'; import { runObligationEscrowTx } from './runTx'; export const command = 'endorse-transfer-owner'; export const describe = 'Endorse / transfer beneficiary (owner) of a BoE obligation'; -export const handler = async (): Promise => { - try { - const answers = await promptForInputs(); - if (!answers) return; - await endorseHandler(answers); - } catch (err: unknown) { - error(err instanceof Error ? err.message : String(err)); - } -}; +export const handler = async (): Promise => + runObligationEscrowCommand(promptForInputs, endorseHandler); export const promptForInputs = async (): Promise => { const base = await promptBaseObligationEscrowInputs(); diff --git a/src/commands/obligation-escrow/reject-return-to-issuer.ts b/src/commands/obligation-escrow/reject-return-to-issuer.ts index f810a53..18b0089 100644 --- a/src/commands/obligation-escrow/reject-return-to-issuer.ts +++ b/src/commands/obligation-escrow/reject-return-to-issuer.ts @@ -8,21 +8,14 @@ import { NetworkCmdName, TransactionReceiptFees, } from '../../utils'; -import { promptBaseObligationEscrowInputs } from './shared'; +import { promptBaseObligationEscrowInputs, runObligationEscrowCommand } from './shared'; import { runObligationEscrowTx } from './runTx'; export const command = 'reject-return-to-issuer'; export const describe = 'Issuer rejects a returned BoE obligation (restore to escrow)'; -export const handler = async (): Promise => { - try { - const answers = await promptForInputs(); - if (!answers) return; - await rejectReturnedHandler(answers); - } catch (err: unknown) { - error(err instanceof Error ? err.message : String(err)); - } -}; +export const handler = async (): Promise => + runObligationEscrowCommand(promptForInputs, rejectReturnedHandler); export const promptForInputs = promptBaseObligationEscrowInputs; diff --git a/src/commands/obligation-escrow/reject-transfer-owner.ts b/src/commands/obligation-escrow/reject-transfer-owner.ts index 0ecdbf0..38845d0 100644 --- a/src/commands/obligation-escrow/reject-transfer-owner.ts +++ b/src/commands/obligation-escrow/reject-transfer-owner.ts @@ -8,21 +8,14 @@ import { NetworkCmdName, TransactionReceiptFees, } from '../../utils'; -import { promptBaseObligationEscrowInputs } from './shared'; +import { promptBaseObligationEscrowInputs, runObligationEscrowCommand } from './shared'; import { runObligationEscrowTx } from './runTx'; export const command = 'reject-transfer-owner'; export const describe = 'Reject a pending beneficiary transfer on a BoE obligation'; -export const handler = async (): Promise => { - try { - const answers = await promptForInputs(); - if (!answers) return; - await rejectTransferOwnerHandler(answers); - } catch (err: unknown) { - error(err instanceof Error ? err.message : String(err)); - } -}; +export const handler = async (): Promise => + runObligationEscrowCommand(promptForInputs, rejectTransferOwnerHandler); export const promptForInputs = promptBaseObligationEscrowInputs; diff --git a/src/commands/obligation-escrow/reject.ts b/src/commands/obligation-escrow/reject.ts index 1a956f5..05270a2 100644 --- a/src/commands/obligation-escrow/reject.ts +++ b/src/commands/obligation-escrow/reject.ts @@ -8,21 +8,14 @@ import { NetworkCmdName, TransactionReceiptFees, } from '../../utils'; -import { promptBaseObligationEscrowInputs } from './shared'; +import { promptBaseObligationEscrowInputs, runObligationEscrowCommand } from './shared'; import { runObligationEscrowTx } from './runTx'; export const command = 'reject'; export const describe = 'Holder rejects an issued BoE obligation (Issued → Rejected, burns)'; -export const handler = async (): Promise => { - try { - const answers = await promptForInputs(); - if (!answers) return; - await rejectHandler(answers); - } catch (err: unknown) { - error(err instanceof Error ? err.message : String(err)); - } -}; +export const handler = async (): Promise => + runObligationEscrowCommand(promptForInputs, rejectHandler); export const promptForInputs = promptBaseObligationEscrowInputs; diff --git a/src/commands/obligation-escrow/return-to-issuer.ts b/src/commands/obligation-escrow/return-to-issuer.ts index 5dcd6b8..63f8257 100644 --- a/src/commands/obligation-escrow/return-to-issuer.ts +++ b/src/commands/obligation-escrow/return-to-issuer.ts @@ -8,21 +8,14 @@ import { NetworkCmdName, TransactionReceiptFees, } from '../../utils'; -import { promptBaseObligationEscrowInputs } from './shared'; +import { promptBaseObligationEscrowInputs, runObligationEscrowCommand } from './shared'; import { runObligationEscrowTx } from './runTx'; export const command = 'return-to-issuer'; export const describe = 'Return a BoE obligation to the issuer (dual role, same as classic ETR)'; -export const handler = async (): Promise => { - try { - const answers = await promptForInputs(); - if (!answers) return; - await returnToIssuerHandler(answers); - } catch (err: unknown) { - error(err instanceof Error ? err.message : String(err)); - } -}; +export const handler = async (): Promise => + runObligationEscrowCommand(promptForInputs, returnToIssuerHandler); export const promptForInputs = promptBaseObligationEscrowInputs; diff --git a/src/commands/obligation-escrow/runTx.ts b/src/commands/obligation-escrow/runTx.ts index d8218c1..5cb8cce 100644 --- a/src/commands/obligation-escrow/runTx.ts +++ b/src/commands/obligation-escrow/runTx.ts @@ -59,6 +59,10 @@ export const runObligationEscrowTx = async (opts: { }, }); + if (shouldProceed === null) { + process.exitCode = 1; + return null; + } if (!shouldProceed) { return null; } diff --git a/src/commands/obligation-escrow/transfer-holder.ts b/src/commands/obligation-escrow/transfer-holder.ts index c3a1aa1..c92cb41 100644 --- a/src/commands/obligation-escrow/transfer-holder.ts +++ b/src/commands/obligation-escrow/transfer-holder.ts @@ -9,21 +9,14 @@ import { promptAddress, TransactionReceiptFees, } from '../../utils'; -import { promptBaseObligationEscrowInputs } from './shared'; +import { promptBaseObligationEscrowInputs, runObligationEscrowCommand } from './shared'; import { runObligationEscrowTx } from './runTx'; export const command = 'transfer-holder'; export const describe = 'Transfer the holder of a BoE obligation record'; -export const handler = async (): Promise => { - try { - const answers = await promptForInputs(); - if (!answers) return; - await changeHolderHandler(answers); - } catch (err: unknown) { - error(err instanceof Error ? err.message : String(err)); - } -}; +export const handler = async (): Promise => + runObligationEscrowCommand(promptForInputs, changeHolderHandler); export const promptForInputs = async (): Promise => { const base = await promptBaseObligationEscrowInputs(); diff --git a/src/commands/obligation-registry/deploy.ts b/src/commands/obligation-registry/deploy.ts index 3c6cc55..5afe615 100644 --- a/src/commands/obligation-registry/deploy.ts +++ b/src/commands/obligation-registry/deploy.ts @@ -1,6 +1,7 @@ import { error, info, success } from 'signale'; -import { CHAIN_ID, deployObligationRegistry } from '@trustvc/trustvc'; +import { CHAIN_ID, deployObligationRegistry, v5Contracts } from '@trustvc/trustvc'; import { input, confirm } from '@inquirer/prompts'; +import { ethers } from 'ethers'; import { displayTransactionPrice, getErrorMessage, @@ -28,6 +29,7 @@ export const handler = async (): Promise => { await deployObligationRegistryContract(answers); } catch (err: unknown) { error(err instanceof Error ? err.message : String(err)); + process.exitCode = 1; } }; @@ -109,18 +111,38 @@ export const deployObligationRegistryContract = async ({ const wallet = await getWalletOrSigner({ network, ...rest }); const chainId = supportedNetwork[network as NetworkCmdName].networkId; - const shouldProceed = await performDryRunWithConfirmation({ - network, - getTransactionCallback: async () => ({ - to: await wallet.getAddress(), - data: '0x', - from: await wallet.getAddress(), - value: 0n, - }), - }); + // Dry-run only when the registry deploy tx can be populated (factory address known). + // When the factory is deployed first, skip estimation and report fees from the receipt. + let shouldProceed: boolean | null = true; + if (escrowFactoryAddress) { + shouldProceed = await performDryRunWithConfirmation({ + network, + getTransactionCallback: async () => { + const factory = new ethers.ContractFactory( + v5Contracts.TrustVCToken__factory.abi, + v5Contracts.TrustVCToken__factory.bytecode, + wallet as ethers.ContractRunner, + ); + const tx = await factory.getDeployTransaction( + registryName, + registrySymbol, + escrowFactoryAddress, + ); + return { ...tx, from: await wallet.getAddress() }; + }, + }); + } else { + info( + 'Skipping dry-run gas estimate: ObligationEscrowFactory will be deployed first. Fees will be shown from the receipt.', + ); + } + if (shouldProceed === null) { + process.exitCode = 1; + return null; + } if (!shouldProceed) { - process.exit(0); + return null; } info(`Deploying obligation registry ${registryName}`); @@ -145,5 +167,6 @@ export const deployObligationRegistryContract = async ({ return result.obligationRegistry; } catch (e) { error(getErrorMessage(e)); + process.exitCode = 1; } }; diff --git a/src/commands/obligation-registry/mint.ts b/src/commands/obligation-registry/mint.ts index 9997df1..7b4eb4d 100644 --- a/src/commands/obligation-registry/mint.ts +++ b/src/commands/obligation-registry/mint.ts @@ -33,6 +33,7 @@ export const handler = async (): Promise => { await mintObligationToken(answers); } catch (err: unknown) { error(err instanceof Error ? err.message : String(err)); + process.exitCode = 1; } }; @@ -82,6 +83,7 @@ export const mintObligationToken = async (args: ObligationRegistryMintCommand) = ...args, tokenId: addAddressPrefix(args.tokenId), }); + if (!transaction) return; displayTransactionPrice(transaction as any, args.network as NetworkCmdName); success( @@ -93,6 +95,7 @@ export const mintObligationToken = async (args: ObligationRegistryMintCommand) = return args.address; } catch (e) { error(getErrorMessage(e)); + process.exitCode = 1; } }; @@ -105,7 +108,7 @@ const mintToObligationRegistry = async ({ encryptionKey, network, ...rest -}: ObligationRegistryMintCommand): Promise => { +}: ObligationRegistryMintCommand): Promise => { const wallet = await getWalletOrSigner({ network, ...rest }); const shouldProceed = await performDryRunWithConfirmation({ @@ -123,8 +126,13 @@ const mintToObligationRegistry = async ({ }, }); + // null = definitive dry-run revert; false = user cancel — do not process.exit(0) + if (shouldProceed === null) { + process.exitCode = 1; + return null; + } if (!shouldProceed) { - process.exit(0); + return null; } let transaction; diff --git a/src/commands/verify.ts b/src/commands/verify.ts index 25d9084..2f1cdea 100644 --- a/src/commands/verify.ts +++ b/src/commands/verify.ts @@ -1,4 +1,5 @@ import { input } from '@inquirer/prompts'; +import { Argv } from 'yargs'; import { getSupportedNetwork, getSupportedNetworkNameFromId, @@ -6,6 +7,7 @@ import { CaptureConsoleWarnAsync, CaptureConsoleWarn, promptNetworkSelection, + supportedNetwork, } from '../utils'; import { getChainId, @@ -33,6 +35,10 @@ type ObligationDocumentStatusInfo = { terminationReason?: number; }; +type VerifyOptions = { + network?: string; +}; + /** Extract obligation registry info from a VALID ObligationRecords verify fragment. */ export const getObligationDocumentStatus = ( fragments: VerificationFragment[], @@ -57,12 +63,20 @@ export const getObligationDocumentStatus = ( export const command = 'verify'; export const describe = 'Verify a W3C or OpenAttestation document (ETR, BoE, or revocable VC)'; -export const handler = async () => { +export const builder = (yargs: Argv): Argv => + yargs.option('network', { + alias: 'n', + choices: Object.keys(supportedNetwork), + description: 'Network provider when document chain lookup fails (skips interactive selection)', + demandOption: false, + }); + +export const handler = async (argv: { network?: string }) => { try { const signedVC = await promptQuestions(); if (!signedVC) return; - await verify(signedVC); + await verify(signedVC, { network: argv.network }); } catch (err: unknown) { signale.error(err instanceof Error ? err.message : String(err)); } @@ -85,12 +99,12 @@ export const promptQuestions = async (): Promise => return signedVC; }; -export const verify = async (signedVC: SignedVerifiableCredential) => { +export const verify = async (signedVC: SignedVerifiableCredential, options: VerifyOptions = {}) => { const isOpenAttestation = isWrappedV2Document(signedVC) || isWrappedV3Document(signedVC); const { result, warnings } = isOpenAttestation - ? { result: await verifyOpenAttestationDocument(signedVC), warnings: null } - : await verifyW3CDocument(signedVC); + ? { result: await verifyOpenAttestationDocument(signedVC, options), warnings: null } + : await verifyW3CDocument(signedVC, options); if (warnings) { handleExpiredCredentialWarning(warnings); @@ -115,8 +129,31 @@ export const verify = async (signedVC: SignedVerifiableCredential) => { // ==== Helper Functions ==== +/** + * Resolve a network provider when document chain lookup fails. + * Prefers --network, then interactive prompt on TTY, otherwise no provider. + */ +const resolveFallbackProvider = async ( + networkOverride?: string, +): Promise => { + if (networkOverride) { + return getSupportedNetwork(networkOverride).provider() as unknown as V5Provider; + } + + if (process.stdin.isTTY) { + const networkName = await promptNetworkSelection(); + return getSupportedNetwork(networkName).provider() as unknown as V5Provider; + } + + signale.warn( + 'No network could be resolved from the document and this session is non-interactive. Verifying without a provider. Pass --network to select one.', + ); + return undefined; +}; + const verifyW3CDocument = async ( signedVC: SignedVerifiableCredential, + options: VerifyOptions = {}, ): Promise<{ result: VerificationFragment[]; warnings: unknown[][] }> => { signale.info('Verifying W3C document...'); @@ -146,8 +183,7 @@ const verifyW3CDocument = async ( signale.warn(`${err instanceof Error ? err.message : String(err)}`); } - const networkName = await promptNetworkSelection(); - const provider = getSupportedNetwork(networkName).provider() as unknown as V5Provider; + const provider = await resolveFallbackProvider(options.network); if (provider) { return await CaptureConsoleWarnAsync(() => verifyDocument(signedVC, { provider })); } @@ -158,6 +194,7 @@ const verifyW3CDocument = async ( const verifyOpenAttestationDocument = async ( signedVC: WrappedOrSignedOpenAttestationDocument, + options: VerifyOptions = {}, ): Promise => { signale.info('Verifying OpenAttestation document...'); @@ -168,10 +205,9 @@ const verifyOpenAttestationDocument = async ( // If the document is not transferable or revokable, verify directly if (!requiresNetwork) return await verifyDocument(signedVC); - // If chainId is not found, prompt for network selection + // If chainId is not found, prefer --network / TTY prompt / no-provider fallback if (requiresNetwork && !chainId) { - const networkName = await promptNetworkSelection(); - const provider = getSupportedNetwork(networkName).provider() as unknown as V5Provider; + const provider = await resolveFallbackProvider(options.network); if (provider) return await verifyDocument(signedVC, { provider }); } diff --git a/src/utils/cli-errors.ts b/src/utils/cli-errors.ts index 28f29a5..1693e6c 100644 --- a/src/utils/cli-errors.ts +++ b/src/utils/cli-errors.ts @@ -102,9 +102,12 @@ const normalizeLabel = (raw: string): string | undefined => { if (!cleaned || UNKNOWN_CUSTOM_ERROR.test(cleaned) || cleaned === '(unknown custom error)') { return undefined; } - // Drop leading junk like ": OwnerHolderMustDiffer" or parentheses wrappers - const ident = cleaned.match(/([A-Za-z][A-Za-z0-9_]*)/); - return ident?.[1]; + // Only accept a full-string identifier (e.g. OwnerHolderMustDiffer), not the first + // word of free-text reasons — those fall through to describeContractError. + if (/^[A-Za-z][A-Za-z0-9_]*$/.test(cleaned)) { + return cleaned; + } + return undefined; }; /** Labels after generic "failed:" — only known reverts or Solidity-shaped names. */ @@ -157,15 +160,9 @@ const labelFromRevertData = (err: EthersCallException): string | undefined => { export function extractContractRevertLabel(error: unknown): string | undefined { const err = asEthersError(error); + // Error instances are objects, so asEthersError always succeeds for them — + // free-text Error.message is handled below via err.message / labelFromWrappedMessage. if (!err) { - if (error instanceof Error) { - const fromExec = error.message.match(/execution reverted:\s*([A-Za-z0-9_]+)/); - if (fromExec?.[1]) { - const label = normalizeLabel(fromExec[1]); - if (label) return label; - } - return labelFromWrappedMessage(error.message); - } return undefined; } @@ -212,9 +209,6 @@ export function extractContractRevertLabel(error: unknown): string | undefined { export function isContractCallException(error: unknown): boolean { const err = asEthersError(error); if (!err) { - if (error instanceof Error && /Pre-check .* failed:/i.test(error.message)) { - return Boolean(extractContractRevertLabel(error)); - } return false; } if (err.code === 'CALL_EXCEPTION') return true; diff --git a/src/utils/cli-options.ts b/src/utils/cli-options.ts index 652cd1e..07ec98d 100644 --- a/src/utils/cli-options.ts +++ b/src/utils/cli-options.ts @@ -535,7 +535,8 @@ export const shouldRunDryRun = (network: string): boolean => { /** * Performs automatic dry run for specified networks with gas estimation and user confirmation - * Uses the existing dryRunMode function for comprehensive display + * Uses the existing dryRunMode function for comprehensive display. + * @returns `true` to proceed, `false` if the user cancelled, `null` if dry-run hit a definitive revert */ export const performDryRunWithConfirmation = async ({ network, @@ -543,7 +544,7 @@ export const performDryRunWithConfirmation = async ({ }: { network: string; getTransactionCallback: () => Promise; -}): Promise => { +}): Promise => { if (!shouldRunDryRun(network)) { return true; // Proceed without dry run for other networks } @@ -579,7 +580,7 @@ export const performDryRunWithConfirmation = async ({ if (isContractCallException(estimateError)) { error(`Transaction would revert: ${message}`); info('Transaction cancelled.'); - return false; + return null; } error(`Gas estimation failed: ${message}`); diff --git a/tests/commands/verify.test.ts b/tests/commands/verify.test.ts index ec7278d..e3c2d2c 100644 --- a/tests/commands/verify.test.ts +++ b/tests/commands/verify.test.ts @@ -1,7 +1,7 @@ import * as prompts from '@inquirer/prompts'; import fs from 'node:fs'; import path from 'node:path'; -import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { beforeEach, afterEach, describe, expect, it, MockedFunction, vi } from 'vitest'; import { promptQuestions, verify } from '../../src/commands/verify'; import { SignedVerifiableCredential } from '@trustvc/trustvc'; import { @@ -237,6 +237,12 @@ describe('verify', () => { signaleSuccessMock = (signale.default as any).success; signaleErrorMock = (signale.default as any).error; signaleWarnMock = (signale.default as any).warn; + // Fixture tests exercise the interactive fallback path. + Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true }); + }); + + afterEach(() => { + Object.defineProperty(process.stdin, 'isTTY', { value: undefined, configurable: true }); }); type TestCase = { @@ -331,5 +337,34 @@ describe('verify', () => { 'DOCUMENT_STATUS: ERROR - An error has occurred.', ); }); + + it('should not prompt for network in non-TTY when chain lookup fails', async () => { + Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true }); + const utils = await import('../../src/utils'); + const filePath = path.resolve( + process.cwd(), + 'tests/fixtures/verify/oa/3.0/oa_dns_txt_token_registry_no_network_field_stability_v3.json', + ); + const signedVC = utils.readJsonFile(filePath, 'document'); + + await expect(verify(signedVC)).resolves.toBeUndefined(); + + expect(prompts.select).not.toHaveBeenCalled(); + expect(signaleWarnMock).toHaveBeenCalledWith(expect.stringContaining('non-interactive')); + }); + + it('should prefer --network over prompting when chain lookup fails', async () => { + Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true }); + const utils = await import('../../src/utils'); + const filePath = path.resolve( + process.cwd(), + 'tests/fixtures/verify/oa/3.0/oa_dns_txt_token_registry_no_network_field_stability_v3.json', + ); + const signedVC = utils.readJsonFile(filePath, 'document'); + + await expect(verify(signedVC, { network: 'stability' })).resolves.toBeUndefined(); + + expect(prompts.select).not.toHaveBeenCalled(); + }); }); }); diff --git a/tests/utils/contract-errors.test.ts b/tests/utils/contract-errors.test.ts index 5712044..30154b8 100644 --- a/tests/utils/contract-errors.test.ts +++ b/tests/utils/contract-errors.test.ts @@ -64,4 +64,14 @@ describe('contract revert error formatting', () => { const wrapped = new Error('Contract reverted with OwnerHolderMustDiffer'); expect(extractContractRevertLabel(wrapped)).toBe('OwnerHolderMustDiffer'); }); + + it('does not extract a label from multi-word free-text reasons', () => { + const freeText = Object.assign(new Error('execution reverted'), { + code: 'CALL_EXCEPTION', + reason: 'insufficient funds for gas * price + value', + shortMessage: 'insufficient funds for gas * price + value', + }); + expect(extractContractRevertLabel(freeText)).toBeUndefined(); + expect(describeContractError(freeText)).toBe('insufficient funds for gas * price + value'); + }); }); From 54075d6927028dece6a2870d6777c66b94ae9d05 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Mon, 3 Aug 2026 13:54:05 +0530 Subject: [PATCH 18/20] Update src/commands/obligation-registry/mint.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/commands/obligation-registry/mint.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/obligation-registry/mint.ts b/src/commands/obligation-registry/mint.ts index 7b4eb4d..928aff7 100644 --- a/src/commands/obligation-registry/mint.ts +++ b/src/commands/obligation-registry/mint.ts @@ -132,7 +132,7 @@ const mintToObligationRegistry = async ({ return null; } if (!shouldProceed) { - return null; + throw new Error('Mint transaction was not submitted.'); } let transaction; From 87c7d0faa7102992bd028619416eb6f252a4a5a6 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Mon, 3 Aug 2026 13:54:41 +0530 Subject: [PATCH 19/20] Update src/commands/obligation-registry/mint.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/commands/obligation-registry/mint.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/commands/obligation-registry/mint.ts b/src/commands/obligation-registry/mint.ts index 928aff7..a8171d0 100644 --- a/src/commands/obligation-registry/mint.ts +++ b/src/commands/obligation-registry/mint.ts @@ -93,6 +93,7 @@ export const mintObligationToken = async (args: ObligationRegistryMintCommand) = `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, ); return args.address; + } catch (e) { } catch (e) { error(getErrorMessage(e)); process.exitCode = 1; From 8a81dc15f994dc10cb3da9c4999ae6c073364b43 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Mon, 3 Aug 2026 13:59:26 +0530 Subject: [PATCH 20/20] Refactor mintObligationToken error handling in mint.ts - Removed redundant error handling for mint transaction submission. - Updated error handling to return null instead of throwing an error when the transaction should not proceed. --- src/commands/obligation-registry/mint.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/commands/obligation-registry/mint.ts b/src/commands/obligation-registry/mint.ts index a8171d0..7b4eb4d 100644 --- a/src/commands/obligation-registry/mint.ts +++ b/src/commands/obligation-registry/mint.ts @@ -93,7 +93,6 @@ export const mintObligationToken = async (args: ObligationRegistryMintCommand) = `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, ); return args.address; - } catch (e) { } catch (e) { error(getErrorMessage(e)); process.exitCode = 1; @@ -133,7 +132,7 @@ const mintToObligationRegistry = async ({ return null; } if (!shouldProceed) { - throw new Error('Mint transaction was not submitted.'); + return null; } let transaction;