diff --git a/README.md b/README.md index 8774b1e..51862c3 100644 --- a/README.md +++ b/README.md @@ -418,7 +418,7 @@ const fragments = await verifyDocument(signedBoeVc, { isObligationRecord(signedBoeVc); // true when obligationRegistry is present ``` -After on-chain reject, discharge, or `acceptReturnedObligationRegistry`, verify returns **INVALID** (token burned). See [§7c](#c-obligation-registry-boe) for the on-chain SDK. +After on-chain **reject**, **discharge**, or **`acceptReturnedObligationRegistry`**, the token is burned (`0xdEaD`). Verify still treats the title as minted (same as classic ETR shredded titles); the website surfaces “Taken Out of Circulation”. See [§7c](#c-obligation-registry-boe) for the on-chain SDK. --- @@ -780,11 +780,23 @@ For more information on Token Registry and Title Escrow contracts **version v5** **Lifecycle** -Deploy factory + registry → Mint (Issued) → Accept (Accepted) / Reject (Rejected, auto-burn) - → Discharge (Discharged, auto-burn) | Transfers | Return to issuer → restore / burn -``` +1. Deploy factory + obligation registry +2. Mint → status **Issued** +3. Holder **accept** → **Accepted**, or holder **reject** → **Rejected** (burns / takes out of circulation) +4. Optional transfers / nominate / endorse (same pattern as Title Escrow) +5. Beneficiary **discharge** (from **Accepted**) → **Discharged** (burns) + +> [!NOTE] +> **Return to issuer** uses the same rules as classic ETR: the caller must be both beneficiary and holder; then the issuer runs **accept-return** (burn) or **reject-return** (restore). Status does **not** need to be Rejected or Discharged first. + +**Role rules** -**Role rules:** accept / reject / discharge require `beneficiary != holder`; `returnToIssuerObligationRegistry` requires dual role (`beneficiary == holder`). +| Action | Who | +|--------|-----| +| `accept` / `reject` | Holder, with `beneficiary != holder` | +| `discharge` | Beneficiary, with `beneficiary != holder` | +| `returnToIssuerObligationRegistry` | Dual role (`beneficiary == holder`) — same as classic ETR `returnToIssuer` | +| `acceptReturnedObligationRegistry` / `rejectReturnedObligationRegistry` | Issuer (registry accepter / restorer roles) | **Status enums** (from `@trustvc/trustvc`): diff --git a/package.json b/package.json index 99a2845..1f70626 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "build": "npm run clean && tsup", "clean": "rm -rf dist/", "precommit": "lint-staged", - "prepare": "husky", + "prepare": "npm run build && husky", "release": "semantic-release --parallel=1 --repositoryUrl=https://github.com/TrustVC/trustvc.git --verbose" }, "keywords": [ diff --git a/src/__tests__/verify/obligationRecordVerifier.utils.test.ts b/src/__tests__/verify/obligationRecordVerifier.utils.test.ts index 36c6a06..d0a01e1 100644 --- a/src/__tests__/verify/obligationRecordVerifier.utils.test.ts +++ b/src/__tests__/verify/obligationRecordVerifier.utils.test.ts @@ -4,9 +4,6 @@ import { OpenAttestationEthereumTokenRegistryStatusCode } from '@tradetrust-tt/t import { isTokenMintedOnObligationRegistry } from '../../verify/fragments/document-status/obligationRecords/utils'; const mockOwnerOf = vi.fn(); -const mockActive = vi.fn(); -const mockIsHoldingToken = vi.fn(); -const mockGetObligationEscrowAddress = vi.fn(); vi.mock('@tradetrust-tt/token-registry-v5/contracts', () => ({ TrustVCToken__factory: { @@ -14,16 +11,6 @@ vi.mock('@tradetrust-tt/token-registry-v5/contracts', () => ({ ownerOf: mockOwnerOf, })), }, - ObligationEscrow__factory: { - connect: vi.fn(() => ({ - active: mockActive, - isHoldingToken: mockIsHoldingToken, - })), - }, -})); - -vi.mock('../../core/endorsement-chain/obligation', () => ({ - getObligationEscrowAddress: (...args: unknown[]) => mockGetObligationEscrowAddress(...args), })); describe('isTokenMintedOnObligationRegistry', () => { @@ -35,13 +22,10 @@ describe('isTokenMintedOnObligationRegistry', () => { beforeEach(() => { vi.clearAllMocks(); - mockGetObligationEscrowAddress.mockResolvedValue('0xEscrow'); mockOwnerOf.mockResolvedValue('0xEscrow'); - mockActive.mockResolvedValue(true); - mockIsHoldingToken.mockResolvedValue(true); }); - it('returns minted when owner is set and title is live', async () => { + it('returns minted when owner is set (active title)', async () => { const result = await isTokenMintedOnObligationRegistry({ obligationRegistryAddress, tokenId, @@ -52,8 +36,8 @@ describe('isTokenMintedOnObligationRegistry', () => { expect(result).toEqual({ minted: true, address: obligationRegistryAddress }); }); - it('returns not minted when title is inactive or not holding', async () => { - mockIsHoldingToken.mockResolvedValue(false); + it('returns minted when owner is burn address (shredded, same as classic ETR)', async () => { + mockOwnerOf.mockResolvedValue('0x000000000000000000000000000000000000dEaD'); const result = await isTokenMintedOnObligationRegistry({ obligationRegistryAddress, @@ -62,13 +46,7 @@ describe('isTokenMintedOnObligationRegistry', () => { chainId: 80002, }); - expect(result.minted).toBe(false); - expect(result).toMatchObject({ - reason: { - code: OpenAttestationEthereumTokenRegistryStatusCode.DOCUMENT_NOT_MINTED, - message: expect.stringContaining('title is not active'), - }, - }); + expect(result).toEqual({ minted: true, address: obligationRegistryAddress }); }); it('returns not minted when owner is zero address', async () => { @@ -104,19 +82,5 @@ describe('isTokenMintedOnObligationRegistry', () => { message: 'Document has not been issued under token registry', }, }); - expect(mockGetObligationEscrowAddress).not.toHaveBeenCalled(); - }); - - it('rethrows failures from escrow address / active / isHoldingToken calls', async () => { - mockGetObligationEscrowAddress.mockRejectedValue(new Error('RPC failed resolving escrow')); - - await expect( - isTokenMintedOnObligationRegistry({ - obligationRegistryAddress, - tokenId, - provider, - chainId: 80002, - }), - ).rejects.toThrow('RPC failed resolving escrow'); }); }); diff --git a/src/verify/fragments/document-status/obligationRecords/utils.ts b/src/verify/fragments/document-status/obligationRecords/utils.ts index 5b480fa..cbd87c0 100644 --- a/src/verify/fragments/document-status/obligationRecords/utils.ts +++ b/src/verify/fragments/document-status/obligationRecords/utils.ts @@ -1,14 +1,10 @@ -import { - ObligationEscrow__factory, - TrustVCToken__factory, -} from '@tradetrust-tt/token-registry-v5/contracts'; +import { TrustVCToken__factory } from '@tradetrust-tt/token-registry-v5/contracts'; import { InvalidTokenRegistryStatus, OpenAttestationEthereumTokenRegistryStatusCode, ValidTokenRegistryStatus, } from '@tradetrust-tt/tt-verify'; import { constants, providers } from 'ethers'; -import { getObligationEscrowAddress } from '../../../../core/endorsement-chain/obligation'; import { decodeError, type EthersError } from '../transferableRecords/utils'; const notMintedReason = ( @@ -30,6 +26,7 @@ const notMintedReason = ( }, }); +// Same minted semantics as classic ETR: ownerOf !== AddressZero (includes burn 0xdEaD). export const isTokenMintedOnObligationRegistry = async ({ obligationRegistryAddress, tokenId, @@ -75,32 +72,12 @@ export const isTokenMintedOnObligationRegistry = async ({ if (!minted) { return notMintedReason(obligationRegistryAddress, tokenId); } + + return { minted: true, address: obligationRegistryAddress }; } catch (error: unknown) { // Only ownerOf absence / registry miss maps to DOCUMENT_NOT_MINTED. // CodedError (e.g. SERVER_ERROR) and unexpected reverts from decodeError propagate. const ethersError = error as EthersError; return notMintedReason(obligationRegistryAddress, tokenId, decodeError(ethersError)); } - - const obligationEscrowAddress = await getObligationEscrowAddress( - obligationRegistryAddress, - tokenId, - provider, - { titleEscrowVersion: 'v5' }, - ); - const obligationEscrow = ObligationEscrow__factory.connect(obligationEscrowAddress, provider); - const [active, isHoldingToken] = await Promise.all([ - obligationEscrow.active(), - obligationEscrow.isHoldingToken(), - ]); - - if (!active || !isHoldingToken) { - return notMintedReason( - obligationRegistryAddress, - tokenId, - `Document ${tokenId} title is not active under contract ${obligationRegistryAddress}`, - ); - } - - return { minted: true, address: obligationRegistryAddress }; };