From fe64c76fcddbec0342b47138066cfb94d4261c8e Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Wed, 12 Aug 2026 15:21:55 +0530 Subject: [PATCH 1/3] feat: add endorsement-chain command for obligation escrow - Introduced `trustvc obligation-escrow endorsement-chain` command to fetch the endorsement chain of a BoE, including transfers and status events. - Updated README to include new command details and usage instructions. - Enhanced `status` command to include escrow address and additional output information. - Refactored `connectToObligationEscrow` to utilize `getObligationEscrowAddress` for improved address resolution. - Added tests for the new endorsement-chain command and updated existing tests for status command to verify new functionality. --- README.md | 33 +++++-- src/commands/helpers.ts | 16 +++- .../obligation-escrow/endorsement-chain.ts | 60 ++++++++++++ src/commands/obligation-escrow/status.ts | 39 ++++++-- .../endorsement-chain.test.ts | 92 +++++++++++++++++++ .../commands/obligation-escrow/status.test.ts | 19 ++++ 6 files changed, 240 insertions(+), 19 deletions(-) create mode 100644 src/commands/obligation-escrow/endorsement-chain.ts create mode 100644 tests/commands/obligation-escrow/endorsement-chain.test.ts diff --git a/README.md b/README.md index e2acdd8..10e2176 100644 --- a/README.md +++ b/README.md @@ -229,6 +229,7 @@ trustvc obligation-escrow accept trustvc obligation-escrow reject trustvc obligation-escrow discharge trustvc obligation-escrow status +trustvc obligation-escrow endorsement-chain # Transfers (mirror title-escrow) trustvc obligation-escrow transfer-holder @@ -341,10 +342,11 @@ trustvc verify | | `[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 endorsement-chain](#obligation-escrow-endorsement-chain)` | Fetch endorsement chain (transfers + status events) | | | `[obligation-escrow transfer-holder](#obligation-escrow-transfer-holder)` | Transfer BoE holder | -| | `[obligation-escrow nominate-transfer-owner](#obligation-escrow-transfer-holder)` | Nominate BoE beneficiary | -| | `[obligation-escrow endorse-transfer-owner](#obligation-escrow-transfer-holder)` | Endorse BoE beneficiary change | -| | `[obligation-escrow transfer-owner-holder](#obligation-escrow-transfer-holder)` | Endorse full BoE ownership transfer | +| | `[obligation-escrow nominate-transfer-owner](#obligation-escrow-nominate-transfer-owner)` | Nominate BoE beneficiary | +| | `[obligation-escrow endorse-transfer-owner](#obligation-escrow-endorse-transfer-owner)` | Endorse BoE beneficiary change | +| | `[obligation-escrow transfer-owner-holder](#obligation-escrow-transfer-owner-holder)` | Endorse full BoE ownership transfer | | | `[obligation-escrow return-to-issuer](#obligation-escrow-return-to-issuer)` | Return BoE to issuer | | | `[obligation-escrow accept-return-to-issuer](#obligation-escrow-accept-return-to-issuer)` | Accept returned BoE | | | `[obligation-escrow reject-return-to-issuer](#obligation-escrow-reject-return-to-issuer)` | Reject returned BoE | @@ -359,7 +361,7 @@ trustvc verify ### Wallet/Private Key Options -Commands that submit transactions (title-escrow, obligation-registry, obligation-escrow write actions, token registry, document-store, and transaction) require a wallet or private key to sign. Read-only `obligation-escrow status` does not — it uses the network RPC/provider from the document (override with `{NETWORK}_RPC` if needed). You can provide your private key in one of the following ways: +Commands that submit transactions (title-escrow, obligation-registry, obligation-escrow write actions, token registry, document-store, and transaction) require a wallet or private key to sign. Read-only `obligation-escrow status` and `obligation-escrow endorsement-chain` do not — they use the network RPC/provider from the document (override with `{NETWORK}_RPC` if needed). You can provide your private key in one of the following ways: **Select wallet/private key option:** @@ -1480,7 +1482,25 @@ trustvc obligation-escrow status - *Network, obligationRegistry, and token ID are extracted from the document* **Output:** -Obligation and escrow status fields from the chain. +Obligation and escrow status fields from the chain (status, registered, termination reason, escrow address, owner, holder, nominee when available). + +#### obligation-escrow endorsement-chain + +Fetches the BoE endorsement chain (transfers + obligation status events) via the network Infura/RPC. Remarks are decrypted with the credential `id`. Read-only: no wallet required. + +**Usage:** + +```sh +trustvc obligation-escrow endorsement-chain +``` + +**Interactive Prompts:** + +- Path to BoE / obligation document +- *Network, obligationRegistry, token ID, and document `id` (encryption key) are extracted from the document* + +**Output:** +Chronological list of endorsement-chain events (type, block, owner/holder, remark, tx hash). #### obligation-escrow transfer-holder @@ -1778,7 +1798,7 @@ Signing/building the VC can also be done with TrustVC library tools or your app **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. +**5. Status / history** — `trustvc obligation-escrow status` reads `Issued` / `Accepted` / `Rejected` / `Discharged`, registration, termination reason, and escrow parties. `trustvc obligation-escrow endorsement-chain` prints the full on-chain history (transfers + status events). **6. Transfers (optional)** — `obligation-escrow transfer-holder`, nominate/endorse/reject-transfer variants mirror `title-escrow`. @@ -1802,6 +1822,7 @@ Signing/building the VC can also be done with TrustVC library tools or your app | `accept-return-to-issuer` | Connected wallet with registry **accepter** role (burn / shred) | | `reject-return-to-issuer` | Connected wallet with registry **restorer** role (restore) | | `obligation-escrow status` | Anyone with the document (read-only RPC; no signing key) | +| `obligation-escrow endorsement-chain` | Anyone with the document (read-only RPC; no signing key) | | `verify` | Anyone with the document (+ RPC when on-chain checks run) | diff --git a/src/commands/helpers.ts b/src/commands/helpers.ts index 432d769..7e7f108 100644 --- a/src/commands/helpers.ts +++ b/src/commands/helpers.ts @@ -8,6 +8,7 @@ import { v5SupportInterfaceIds, DocumentStore__factory, encrypt, + getObligationEscrowAddress, } from '@trustvc/trustvc'; // Internal utilities @@ -462,7 +463,8 @@ interface ConnectToObligationEscrowArgs { } /** - * Resolves ObligationEscrow via ownerOf(tokenId) on the obligation registry and connects. + * Resolves ObligationEscrow via getObligationEscrowAddress (handles inactive/burned titles + * via factory CREATE2) and connects — same as websites / demo dry-run resolution. */ export const connectToObligationEscrow = async ({ tokenId, @@ -470,11 +472,15 @@ export const connectToObligationEscrow = async ({ wallet, }: ConnectToObligationEscrowArgs) => { try { - signale.info(`Connecting to obligation registry at: ${address}`); - const registry = new ethers.Contract(address, TrustVCToken__factory.abi, wallet as any); + const provider = wallet.provider; + if (!provider) { + throw new Error('Provider is required to resolve obligation escrow address'); + } - signale.info(`Fetching obligation escrow address for tokenId: ${tokenId}`); - const escrowAddress = await registry.ownerOf(tokenId); + signale.info(`Resolving obligation escrow for tokenId: ${tokenId} on ${address}`); + const escrowAddress = await getObligationEscrowAddress(address, tokenId, provider as any, { + titleEscrowVersion: 'v5', + }); signale.info(`Obligation escrow address: ${escrowAddress}`); if (!escrowAddress || escrowAddress === ZeroAddress) { diff --git a/src/commands/obligation-escrow/endorsement-chain.ts b/src/commands/obligation-escrow/endorsement-chain.ts new file mode 100644 index 0000000..1a62a6f --- /dev/null +++ b/src/commands/obligation-escrow/endorsement-chain.ts @@ -0,0 +1,60 @@ +import { info, success } from 'signale'; +import { fetchEndorsementChain } from '@trustvc/trustvc'; +import { BaseObligationEscrowCommand } from '../../types'; +import { + extractObligationDocumentInfo, + getSupportedNetwork, + promptAndReadDocument, + verifyDocumentSignature, +} from '../../utils'; +import { runObligationEscrowCommand } from './shared'; + +export const command = 'endorsement-chain'; +export const describe = + 'Fetch BoE obligation endorsement chain (transfers + status events) via network Infura/RPC'; + +/** Read-only — document only; decrypt remarks with credential `id`. */ +export type ObligationEscrowEndorsementChainCommand = Pick< + BaseObligationEscrowCommand, + 'network' | 'obligationRegistryAddress' | 'tokenId' | 'encryptionKey' +>; + +export const handler = async (): Promise => + runObligationEscrowCommand(promptForInputs, endorsementChainHandler); + +export const promptForInputs = async (): Promise => { + const document = await promptAndReadDocument(); + await verifyDocumentSignature(document); + const { obligationRegistry, tokenId, network, documentId } = + await extractObligationDocumentInfo(document); + return { + network, + obligationRegistryAddress: obligationRegistry, + tokenId, + encryptionKey: documentId, + }; +}; + +export const endorsementChainHandler = async (args: ObligationEscrowEndorsementChainCommand) => { + const { obligationRegistryAddress, tokenId, network, encryptionKey } = args; + // Always use ChainInfo/Infura-style network RPC — never a wallet provider (MetaMask range caps). + const provider = getSupportedNetwork(network).provider(); + + info(`Fetching endorsement chain for ${tokenId} on ${obligationRegistryAddress} (${network})…`); + const chain = await fetchEndorsementChain( + obligationRegistryAddress, + tokenId, + provider as any, + encryptionKey, + ); + + success(`Endorsement chain (${chain.length} event${chain.length === 1 ? '' : 's'})`); + chain.forEach((event, index) => { + const when = event.timestamp ? new Date(event.timestamp).toISOString() : 'unknown-time'; + info(` ${index + 1}. [${event.type}] block=${event.blockNumber} @ ${when}`); + if (event.owner) info(` Owner: ${event.owner}`); + if (event.holder) info(` Holder: ${event.holder}`); + if (event.remark) info(` Remark: ${event.remark}`); + if (event.transactionHash) info(` Tx: ${event.transactionHash}`); + }); +}; diff --git a/src/commands/obligation-escrow/status.ts b/src/commands/obligation-escrow/status.ts index 19363de..70f5f2a 100644 --- a/src/commands/obligation-escrow/status.ts +++ b/src/commands/obligation-escrow/status.ts @@ -2,11 +2,13 @@ import { info, success } from 'signale'; import { ObligationDocumentStatus, ObligationEscrowTerminationReason, + getObligationEscrowAddress, getObligationEscrowTerminationReason, getObligationRegistryStatus, isObligationRegistryRegistered, + v5Contracts, } from '@trustvc/trustvc'; -import { VoidSigner, ZeroAddress } from 'ethers'; +import { Contract, VoidSigner, ZeroAddress } from 'ethers'; import { BaseObligationEscrowCommand } from '../../types'; import { extractObligationDocumentInfo, @@ -17,6 +19,8 @@ import { } from '../../utils'; import { runObligationEscrowCommand } from './shared'; +const { ObligationEscrow__factory } = v5Contracts; + export const command = 'status'; export const describe = 'Read BoE obligation escrow status / registration / termination reason'; @@ -60,16 +64,35 @@ export const statusHandler = async (args: ObligationEscrowStatusCommand) => { const readOnlySigner = new VoidSigner(ZeroAddress, provider); const opts = { obligationRegistryAddress, tokenId }; - const status = await getObligationRegistryStatus(opts, toSdkSigner(readOnlySigner), { tokenId }); - const registered = await isObligationRegistryRegistered(opts, toSdkSigner(readOnlySigner), { - tokenId, - }); - const reason = await getObligationEscrowTerminationReason(opts, toSdkSigner(readOnlySigner), { - tokenId, - }); + const [status, registered, reason, escrowAddress] = await Promise.all([ + getObligationRegistryStatus(opts, toSdkSigner(readOnlySigner), { tokenId }), + isObligationRegistryRegistered(opts, toSdkSigner(readOnlySigner), { tokenId }), + getObligationEscrowTerminationReason(opts, toSdkSigner(readOnlySigner), { tokenId }), + getObligationEscrowAddress(obligationRegistryAddress, tokenId, provider as any, { + titleEscrowVersion: 'v5', + }), + ]); + + let beneficiary = ''; + let holder = ''; + let nominee = ''; + try { + const escrow = new Contract(escrowAddress, ObligationEscrow__factory.abi, provider); + [beneficiary, holder, nominee] = await Promise.all([ + escrow.beneficiary(), + escrow.holder(), + escrow.nominee(), + ]); + } catch { + // Escrow may be inactive / not readable after shred — still print registry-level status. + } success(`Obligation ${tokenId} on ${obligationRegistryAddress}`); + info(` Escrow: ${escrowAddress}`); info(` Status: ${STATUS_LABEL[status] ?? status} (${status})`); info(` Registered: ${registered}`); info(` Termination reason: ${REASON_LABEL[reason] ?? reason} (${reason})`); + if (beneficiary) info(` Owner (beneficiary): ${beneficiary}`); + if (holder) info(` Holder: ${holder}`); + if (nominee && nominee !== ZeroAddress) info(` Nominee: ${nominee}`); }; diff --git a/tests/commands/obligation-escrow/endorsement-chain.test.ts b/tests/commands/obligation-escrow/endorsement-chain.test.ts new file mode 100644 index 0000000..7b6a8a5 --- /dev/null +++ b/tests/commands/obligation-escrow/endorsement-chain.test.ts @@ -0,0 +1,92 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { endorsementChainHandler } from '../../../src/commands/obligation-escrow/endorsement-chain'; +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, + fetchEndorsementChain: vi.fn().mockResolvedValue([ + { + type: 'INITIAL', + blockNumber: 1, + timestamp: 1_700_000_000_000, + owner: '0xOwner', + holder: '0xHolder', + remark: 'minted', + transactionHash: '0xtx', + }, + { + type: 'STATUS_INITIALIZED', + blockNumber: 1, + timestamp: 1_700_000_000_000, + owner: '0xOwner', + holder: '0xHolder', + remark: '', + transactionHash: '0xtx', + }, + ]), + }; +}); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getSupportedNetwork: vi.fn().mockReturnValue({ + provider: () => ({ mock: 'infura-provider' }), + networkId: 11155111, + }), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-escrow/endorsement-chain', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('fetches chain via network Infura/RPC using document id as keyId', async () => { + const trustvc = await import('@trustvc/trustvc'); + await endorsementChainHandler({ + network: NetworkCmdName.Sepolia, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + encryptionKey: 'urn:uuid:test-doc-id', + }); + + expect(trustvc.fetchEndorsementChain as MockedFunction).toHaveBeenCalledWith( + '0xRegistry', + '0x1', + { mock: 'infura-provider' }, + 'urn:uuid:test-doc-id', + ); + }); +}); diff --git a/tests/commands/obligation-escrow/status.test.ts b/tests/commands/obligation-escrow/status.test.ts index 0b948f9..75977a7 100644 --- a/tests/commands/obligation-escrow/status.test.ts +++ b/tests/commands/obligation-escrow/status.test.ts @@ -36,6 +36,19 @@ vi.mock('@trustvc/trustvc', async () => { getObligationRegistryStatus: vi.fn().mockResolvedValue(0), isObligationRegistryRegistered: vi.fn().mockResolvedValue(true), getObligationEscrowTerminationReason: vi.fn().mockResolvedValue(0), + getObligationEscrowAddress: vi.fn().mockResolvedValue('0xEscrow'), + }; +}); + +vi.mock('ethers', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + Contract: vi.fn().mockImplementation(() => ({ + beneficiary: vi.fn().mockResolvedValue('0xBeneficiary'), + holder: vi.fn().mockResolvedValue('0xHolder'), + nominee: vi.fn().mockResolvedValue(actual.ZeroAddress), + })), }; }); @@ -70,5 +83,11 @@ describe('obligation-escrow/status', () => { ); expect(trustvc.isObligationRegistryRegistered).toHaveBeenCalled(); expect(trustvc.getObligationEscrowTerminationReason).toHaveBeenCalled(); + expect(trustvc.getObligationEscrowAddress).toHaveBeenCalledWith( + '0xRegistry', + '0x1', + { mock: 'provider' }, + { titleEscrowVersion: 'v5' }, + ); }); }); From 0e532c025df4449cad6f36a27f64907817627011 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Fri, 14 Aug 2026 20:15:31 +0530 Subject: [PATCH 2/3] chore: update dependencies and refactor escrow address handling - Upgraded @trustvc/trustvc to version 2.16.0-beta.5 and @tradetrust-tt/token-registry to version 5.6.0-beta.3 in package.json and package-lock.json. - Refactored functions to use getTitleEscrowAddress instead of getObligationEscrowAddress for improved clarity in obligation escrow commands. - Enhanced logging in endorsement chain and status handlers to display owner and holder information more effectively. - Updated tests to reflect changes in address resolution and ensure accurate output verification. --- package-lock.json | 24 ++++----- package.json | 2 +- src/commands/helpers.ts | 6 +-- .../obligation-escrow/endorsement-chain.ts | 26 +++++++++- src/commands/obligation-escrow/status.ts | 26 ++++++---- .../endorsement-chain.test.ts | 25 +++++++--- .../commands/obligation-escrow/status.test.ts | 49 +++++++++++++++++-- 7 files changed, 122 insertions(+), 36 deletions(-) diff --git a/package-lock.json b/package-lock.json index ab99c43..1b4c9ca 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.16.0-beta.3", + "@trustvc/trustvc": "2.16.0-beta.5", "@types/yargs": "^17.0.32", "chalk": "^4.1.2", "dotenv": "^16.0.0", @@ -4308,9 +4308,9 @@ }, "node_modules/@tradetrust-tt/token-registry-v5": { "name": "@tradetrust-tt/token-registry", - "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==", + "version": "5.6.0-beta.3", + "resolved": "https://registry.npmjs.org/@tradetrust-tt/token-registry/-/token-registry-5.6.0-beta.3.tgz", + "integrity": "sha512-1YJDuuBN5P2mx94aVrhbQ3wXt0Kb9eqIPjabHeZCkFjH87ZV1rtdXTmy1T2Af8Y7zROCWAD1V/8JpCFqC3u/JA==", "license": "Apache-2.0", "dependencies": { "ethers": "^6.13.4" @@ -4667,9 +4667,9 @@ "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==", + "version": "1.1.0-beta.2", + "resolved": "https://registry.npmjs.org/@trustvc/eip7702/-/eip7702-1.1.0-beta.2.tgz", + "integrity": "sha512-yo82l4fDCE6ZuTPeuDxBSgcRxAUOmVm51y16dJQH4xpM2ZkEKPXfPsMjpiPa636uOEVan+e38v/IQfy2HPg9sA==", "license": "MIT", "dependencies": { "@account-abstraction/contracts": "^0.8.0", @@ -4677,19 +4677,19 @@ } }, "node_modules/@trustvc/trustvc": { - "version": "2.16.0-beta.3", - "resolved": "https://registry.npmjs.org/@trustvc/trustvc/-/trustvc-2.16.0-beta.3.tgz", - "integrity": "sha512-spuYfT78ZYY5Xg60yTrnDjbGsxwv5QNAyfEhWzp0mcBTRZRPJRiH8mVWTp4AZeAHO6ph9ZyOY0ZVl3f7/cBcvw==", + "version": "2.16.0-beta.5", + "resolved": "https://registry.npmjs.org/@trustvc/trustvc/-/trustvc-2.16.0-beta.5.tgz", + "integrity": "sha512-I2H2KT9eAPWkHB3lmqeii415vJQsE1Pi+TUD2kvFBtS21cEsNjgIflqIO2XP2ndzSyUu56ISbFEeV0f+3aI86g==", "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.6.0-beta.1", + "@tradetrust-tt/token-registry-v5": "npm:@tradetrust-tt/token-registry@^5.6.0-beta.3", "@tradetrust-tt/tradetrust": "^6.10.3", "@tradetrust-tt/tt-verify": "^9.7.5", "@trustvc/document-store": "^1.0.3", - "@trustvc/eip7702": "^1.0.0-beta.1", + "@trustvc/eip7702": "^1.1.0-beta.2", "@trustvc/w3c": "^2.4.2", "@trustvc/w3c-context": "^2.4.0", "@trustvc/w3c-credential-status": "^2.4.0", diff --git a/package.json b/package.json index f2637e4..3d6c414 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ }, "dependencies": { "@inquirer/prompts": "^5.3.8", - "@trustvc/trustvc": "2.16.0-beta.3", + "@trustvc/trustvc": "2.16.0-beta.5", "@types/yargs": "^17.0.32", "chalk": "^4.1.2", "dotenv": "^16.0.0", diff --git a/src/commands/helpers.ts b/src/commands/helpers.ts index 7e7f108..44a118c 100644 --- a/src/commands/helpers.ts +++ b/src/commands/helpers.ts @@ -8,7 +8,7 @@ import { v5SupportInterfaceIds, DocumentStore__factory, encrypt, - getObligationEscrowAddress, + getTitleEscrowAddress, } from '@trustvc/trustvc'; // Internal utilities @@ -463,7 +463,7 @@ interface ConnectToObligationEscrowArgs { } /** - * Resolves ObligationEscrow via getObligationEscrowAddress (handles inactive/burned titles + * Resolves ObligationEscrow via getTitleEscrowAddress (handles inactive/burned titles * via factory CREATE2) and connects — same as websites / demo dry-run resolution. */ export const connectToObligationEscrow = async ({ @@ -478,7 +478,7 @@ export const connectToObligationEscrow = async ({ } signale.info(`Resolving obligation escrow for tokenId: ${tokenId} on ${address}`); - const escrowAddress = await getObligationEscrowAddress(address, tokenId, provider as any, { + const escrowAddress = await getTitleEscrowAddress(address, tokenId, provider as any, { titleEscrowVersion: 'v5', }); signale.info(`Obligation escrow address: ${escrowAddress}`); diff --git a/src/commands/obligation-escrow/endorsement-chain.ts b/src/commands/obligation-escrow/endorsement-chain.ts index 1a62a6f..dd9495c 100644 --- a/src/commands/obligation-escrow/endorsement-chain.ts +++ b/src/commands/obligation-escrow/endorsement-chain.ts @@ -49,11 +49,33 @@ export const endorsementChainHandler = async (args: ObligationEscrowEndorsementC ); success(`Endorsement chain (${chain.length} event${chain.length === 1 ? '' : 's'})`); + let lastOwner = ''; + let lastHolder = ''; + const isZero = (value?: string) => !value || /^0x0{40}$/i.test(value); chain.forEach((event, index) => { + const isShred = + event.type === 'RETURN_TO_ISSUER_ACCEPTED' || event.type === 'SURRENDER_ACCEPTED'; + // eBoE shred keeps last owner/holder on the shred row. + const owner = isZero(event.owner) ? lastOwner : event.owner || lastOwner; + const holder = isZero(event.holder) ? lastHolder : event.holder || lastHolder; + if (!isZero(owner)) lastOwner = owner; + if (!isZero(holder)) lastHolder = holder; + if (isShred) { + lastOwner = ''; + lastHolder = ''; + } + const when = event.timestamp ? new Date(event.timestamp).toISOString() : 'unknown-time'; info(` ${index + 1}. [${event.type}] block=${event.blockNumber} @ ${when}`); - if (event.owner) info(` Owner: ${event.owner}`); - if (event.holder) info(` Holder: ${event.holder}`); + info(` Owner: ${owner || '-'}`); + info(` Holder: ${holder || '-'}`); + if (isShred && event.terminationReason && event.terminationReason !== 'None') { + const reasonLabel = + event.terminationReason === 'ReturnToIssuer' + ? 'Return to issuer' + : event.terminationReason; + info(` Reason: ${reasonLabel}`); + } if (event.remark) info(` Remark: ${event.remark}`); if (event.transactionHash) info(` Tx: ${event.transactionHash}`); }); diff --git a/src/commands/obligation-escrow/status.ts b/src/commands/obligation-escrow/status.ts index 70f5f2a..62ec956 100644 --- a/src/commands/obligation-escrow/status.ts +++ b/src/commands/obligation-escrow/status.ts @@ -2,7 +2,7 @@ import { info, success } from 'signale'; import { ObligationDocumentStatus, ObligationEscrowTerminationReason, - getObligationEscrowAddress, + getTitleEscrowAddress, getObligationEscrowTerminationReason, getObligationRegistryStatus, isObligationRegistryRegistered, @@ -68,21 +68,29 @@ export const statusHandler = async (args: ObligationEscrowStatusCommand) => { getObligationRegistryStatus(opts, toSdkSigner(readOnlySigner), { tokenId }), isObligationRegistryRegistered(opts, toSdkSigner(readOnlySigner), { tokenId }), getObligationEscrowTerminationReason(opts, toSdkSigner(readOnlySigner), { tokenId }), - getObligationEscrowAddress(obligationRegistryAddress, tokenId, provider as any, { + getTitleEscrowAddress(obligationRegistryAddress, tokenId, provider as any, { titleEscrowVersion: 'v5', }), ]); + const isZero = (value?: string) => !value || value === ZeroAddress; + let beneficiary = ''; let holder = ''; let nominee = ''; try { const escrow = new Contract(escrowAddress, ObligationEscrow__factory.abi, provider); - [beneficiary, holder, nominee] = await Promise.all([ - escrow.beneficiary(), - escrow.holder(), - escrow.nominee(), - ]); + const [currentBeneficiary, currentHolder, currentNominee, lastBeneficiary, lastHolder] = + await Promise.all([ + escrow.beneficiary(), + escrow.holder(), + escrow.nominee(), + escrow.lastBeneficiary(), + escrow.lastHolder(), + ]); + beneficiary = isZero(currentBeneficiary) ? lastBeneficiary : currentBeneficiary; + holder = isZero(currentHolder) ? lastHolder : currentHolder; + nominee = currentNominee; } catch { // Escrow may be inactive / not readable after shred — still print registry-level status. } @@ -92,7 +100,7 @@ export const statusHandler = async (args: ObligationEscrowStatusCommand) => { info(` Status: ${STATUS_LABEL[status] ?? status} (${status})`); info(` Registered: ${registered}`); info(` Termination reason: ${REASON_LABEL[reason] ?? reason} (${reason})`); - if (beneficiary) info(` Owner (beneficiary): ${beneficiary}`); - if (holder) info(` Holder: ${holder}`); + if (!isZero(beneficiary)) info(` Owner (beneficiary): ${beneficiary}`); + if (!isZero(holder)) info(` Holder: ${holder}`); if (nominee && nominee !== ZeroAddress) info(` Nominee: ${nominee}`); }; diff --git a/tests/commands/obligation-escrow/endorsement-chain.test.ts b/tests/commands/obligation-escrow/endorsement-chain.test.ts index 7b6a8a5..0aa814b 100644 --- a/tests/commands/obligation-escrow/endorsement-chain.test.ts +++ b/tests/commands/obligation-escrow/endorsement-chain.test.ts @@ -41,16 +41,17 @@ vi.mock('@trustvc/trustvc', async () => { owner: '0xOwner', holder: '0xHolder', remark: 'minted', - transactionHash: '0xtx', + transactionHash: '0xtx1', }, { - type: 'STATUS_INITIALIZED', - blockNumber: 1, - timestamp: 1_700_000_000_000, + type: 'RETURN_TO_ISSUER_ACCEPTED', + blockNumber: 2, + timestamp: 1_700_000_100_000, owner: '0xOwner', holder: '0xHolder', - remark: '', - transactionHash: '0xtx', + remark: 'burned', + transactionHash: '0xtx2', + terminationReason: 'Discharged', }, ]), }; @@ -75,6 +76,7 @@ describe('obligation-escrow/endorsement-chain', () => { it('fetches chain via network Infura/RPC using document id as keyId', async () => { const trustvc = await import('@trustvc/trustvc'); + const signale = await import('signale'); await endorsementChainHandler({ network: NetworkCmdName.Sepolia, obligationRegistryAddress: '0xRegistry', @@ -88,5 +90,16 @@ describe('obligation-escrow/endorsement-chain', () => { { mock: 'infura-provider' }, 'urn:uuid:test-doc-id', ); + + const infoMock = signale.info as MockedFunction; + const infoMessages = infoMock.mock.calls.map((args) => String(args[0])); + expect(infoMessages.some((msg) => msg.includes('Owner:') && msg.includes('0xOwner'))).toBe( + true, + ); + expect(infoMessages.some((msg) => msg.includes('Holder:') && msg.includes('0xHolder'))).toBe( + true, + ); + expect(infoMessages.some((msg) => msg.includes('Reason: Discharged'))).toBe(true); + expect(infoMessages.some((msg) => msg.includes('Remark: burned'))).toBe(true); }); }); diff --git a/tests/commands/obligation-escrow/status.test.ts b/tests/commands/obligation-escrow/status.test.ts index 75977a7..a300b24 100644 --- a/tests/commands/obligation-escrow/status.test.ts +++ b/tests/commands/obligation-escrow/status.test.ts @@ -1,6 +1,8 @@ import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; import { statusHandler } from '../../../src/commands/obligation-escrow/status'; import { NetworkCmdName } from '../../../src/utils'; +import { Contract, ZeroAddress } from 'ethers'; +import { info } from 'signale'; vi.mock('signale', async (importOriginal) => { const originalSignale = await importOriginal(); @@ -36,7 +38,7 @@ vi.mock('@trustvc/trustvc', async () => { getObligationRegistryStatus: vi.fn().mockResolvedValue(0), isObligationRegistryRegistered: vi.fn().mockResolvedValue(true), getObligationEscrowTerminationReason: vi.fn().mockResolvedValue(0), - getObligationEscrowAddress: vi.fn().mockResolvedValue('0xEscrow'), + getTitleEscrowAddress: vi.fn().mockResolvedValue('0xEscrow'), }; }); @@ -48,6 +50,8 @@ vi.mock('ethers', async (importOriginal) => { beneficiary: vi.fn().mockResolvedValue('0xBeneficiary'), holder: vi.fn().mockResolvedValue('0xHolder'), nominee: vi.fn().mockResolvedValue(actual.ZeroAddress), + lastBeneficiary: vi.fn().mockResolvedValue(actual.ZeroAddress), + lastHolder: vi.fn().mockResolvedValue(actual.ZeroAddress), })), }; }); @@ -65,8 +69,23 @@ vi.mock('../../../src/utils', async (importOriginal) => { }); describe('obligation-escrow/status', () => { - beforeEach(() => { + beforeEach(async () => { vi.clearAllMocks(); + const trustvc = await import('@trustvc/trustvc'); + vi.mocked(trustvc.getObligationRegistryStatus).mockResolvedValue(0); + vi.mocked(trustvc.isObligationRegistryRegistered).mockResolvedValue(true); + vi.mocked(trustvc.getObligationEscrowTerminationReason).mockResolvedValue(0); + vi.mocked(trustvc.getTitleEscrowAddress).mockResolvedValue('0xEscrow'); + vi.mocked(Contract).mockImplementation( + () => + ({ + beneficiary: vi.fn().mockResolvedValue('0xBeneficiary'), + holder: vi.fn().mockResolvedValue('0xHolder'), + nominee: vi.fn().mockResolvedValue(ZeroAddress), + lastBeneficiary: vi.fn().mockResolvedValue(ZeroAddress), + lastHolder: vi.fn().mockResolvedValue(ZeroAddress), + }) as any, + ); }); it('reads status via network provider without a wallet', async () => { @@ -83,11 +102,35 @@ describe('obligation-escrow/status', () => { ); expect(trustvc.isObligationRegistryRegistered).toHaveBeenCalled(); expect(trustvc.getObligationEscrowTerminationReason).toHaveBeenCalled(); - expect(trustvc.getObligationEscrowAddress).toHaveBeenCalledWith( + expect(trustvc.getTitleEscrowAddress).toHaveBeenCalledWith( '0xRegistry', '0x1', { mock: 'provider' }, { titleEscrowVersion: 'v5' }, ); + expect(info).toHaveBeenCalledWith(' Owner (beneficiary): 0xBeneficiary'); + expect(info).toHaveBeenCalledWith(' Holder: 0xHolder'); + }); + + it('prints lastBeneficiary and lastHolder when current parties are zero after shred', async () => { + vi.mocked(Contract).mockImplementation( + () => + ({ + beneficiary: vi.fn().mockResolvedValue(ZeroAddress), + holder: vi.fn().mockResolvedValue(ZeroAddress), + nominee: vi.fn().mockResolvedValue(ZeroAddress), + lastBeneficiary: vi.fn().mockResolvedValue('0xLastBeneficiary'), + lastHolder: vi.fn().mockResolvedValue('0xLastHolder'), + }) as any, + ); + + await statusHandler({ + network: NetworkCmdName.Amoy, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + }); + + expect(info).toHaveBeenCalledWith(' Owner (beneficiary): 0xLastBeneficiary'); + expect(info).toHaveBeenCalledWith(' Holder: 0xLastHolder'); }); }); From 59c166802a89bc95019215a90dae0c36f5d8290b Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Sun, 16 Aug 2026 23:05:08 +0530 Subject: [PATCH 3/3] fix: update verification output for BoE documents --- README.md | 4 ++-- src/commands/verify.ts | 12 ------------ tests/commands/verify.obligation.test.ts | 10 ++++++---- 3 files changed, 8 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 10e2176..87fe632 100644 --- a/README.md +++ b/README.md @@ -499,7 +499,7 @@ Creates `signed_vc.json` with cryptographic proof. 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. +Works for **classic ETR** (`tokenRegistry` → TransferableRecords fragment) and **BoE** (`obligationRegistry` → ObligationRecords fragment). Both report the same keys: `DOCUMENT_INTEGRITY`, `DOCUMENT_STATUS`, and `ISSUER_IDENTITY` with `VALID` / `SKIPPED` / `INVALID` / `ERROR`. For BoE, `DOCUMENT_STATUS` is the ObligationRecords fragment (minted on the obligation registry), not a registry address dump. **Usage:** @@ -513,7 +513,7 @@ trustvc verify - [If network required but no network detected]: Select network **Output:** -Verifies document integrity, status, and issuer identity. For BoE documents, logs obligation registry status when the ObligationRecords fragment is VALID. +Verifies document integrity, status, and issuer identity (`DOCUMENT_INTEGRITY`, `DOCUMENT_STATUS`, `ISSUER_IDENTITY`). **Supported Formats:** diff --git a/src/commands/verify.ts b/src/commands/verify.ts index dab56d5..f4245c9 100644 --- a/src/commands/verify.ts +++ b/src/commands/verify.ts @@ -113,18 +113,6 @@ export const verify = async (signedVC: SignedVerifiableCredential, options: Veri logResultStatus(getResultFromFragment(FragmentType.DOCUMENT_INTEGRITY, result)); logResultStatus(getResultFromFragment(FragmentType.DOCUMENT_STATUS, result)); logResultStatus(getResultFromFragment(FragmentType.ISSUER_IDENTITY, result)); - - const obligationStatus = getObligationDocumentStatus(result); - if (obligationStatus) { - 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(' ')}`); - } }; // ==== Helper Functions ==== diff --git a/tests/commands/verify.obligation.test.ts b/tests/commands/verify.obligation.test.ts index cadc915..82c91da 100644 --- a/tests/commands/verify.obligation.test.ts +++ b/tests/commands/verify.obligation.test.ts @@ -116,16 +116,18 @@ describe('verify obligation / BoE (shredded titles)', () => { }); describe('verify', () => { - it('treats shredded BoE as DOCUMENT_STATUS VALID and logs obligation status', async () => { + it('treats shredded BoE as DOCUMENT_STATUS VALID without dumping the registry address', 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', - ); + const infoMessages = (signale.default.info as unknown as { mock: { calls: unknown[][] } }).mock + .calls + .map((call) => String(call[0])); + expect(infoMessages.join('\n')).not.toContain('Obligation document status'); + expect(infoMessages.join('\n')).not.toContain('registry='); }); }); });