From b63ca014bb213f99bf3f13347b559935d93ca57f Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Thu, 30 Jul 2026 15:32:31 +0530 Subject: [PATCH 01/42] feat: implement obligation registry functions and enhance document builder --- README.md | 153 +++++++- package-lock.json | 346 +++++++++--------- package.json | 17 +- .../e2e/fixtures/sample-boe-credential.ts | 103 ++++++ .../obligation-registry-functions/fixtures.ts | 101 +++++ .../statusLifecycle.e2e.test.ts | 263 +++++++++++++ .../transfer.e2e.test.ts | 179 +++++++++ src/__tests__/e2e/utils.ts | 30 ++ .../deploy.test.ts | 78 ++++ .../obligation-registry-functions/fixtures.ts | 129 +++++++ .../lifecycle.test.ts | 112 ++++++ .../mint.test.ts | 146 ++++++++ .../ownerOf.test.ts | 44 +++ .../rejectTransfers.test.ts | 87 +++++ .../returnToken.test.ts | 108 ++++++ .../status.test.ts | 93 +++++ .../transfers.test.ts | 110 ++++++ src/core/documentBuilder.ts | 100 ++++- src/core/endorsement-chain/index.ts | 1 + src/core/endorsement-chain/obligation.ts | 33 ++ src/obligation-registry-functions/deploy.ts | 172 +++++++++ src/obligation-registry-functions/index.ts | 10 + .../lifecycle.ts | 66 ++++ src/obligation-registry-functions/mint.ts | 64 ++++ src/obligation-registry-functions/ownerOf.ts | 46 +++ .../rejectTransfers.ts | 73 ++++ .../returnToken.ts | 136 +++++++ src/obligation-registry-functions/status.ts | 60 +++ src/obligation-registry-functions/transfer.ts | 92 +++++ src/obligation-registry-functions/types.ts | 121 ++++++ src/obligation-registry-functions/utils.ts | 118 ++++++ src/token-registry-v5/contracts.ts | 6 + src/utils/documents/index.ts | 7 +- src/utils/documents/obligation.ts | 48 +++ .../obligationRecordVerifier.ts | 161 ++++++++ .../obligationRecordVerifier.types.ts | 31 ++ .../obligationRecords/utils.ts | 92 +++++ .../transferableRecordVerifier.ts | 8 +- src/verify/fragments/index.ts | 6 + src/verify/verify.ts | 3 + 40 files changed, 3354 insertions(+), 199 deletions(-) create mode 100644 src/__tests__/e2e/fixtures/sample-boe-credential.ts create mode 100644 src/__tests__/e2e/obligation-registry-functions/fixtures.ts create mode 100644 src/__tests__/e2e/obligation-registry-functions/statusLifecycle.e2e.test.ts create mode 100644 src/__tests__/e2e/obligation-registry-functions/transfer.e2e.test.ts create mode 100644 src/__tests__/obligation-registry-functions/deploy.test.ts create mode 100644 src/__tests__/obligation-registry-functions/fixtures.ts create mode 100644 src/__tests__/obligation-registry-functions/lifecycle.test.ts create mode 100644 src/__tests__/obligation-registry-functions/mint.test.ts create mode 100644 src/__tests__/obligation-registry-functions/ownerOf.test.ts create mode 100644 src/__tests__/obligation-registry-functions/rejectTransfers.test.ts create mode 100644 src/__tests__/obligation-registry-functions/returnToken.test.ts create mode 100644 src/__tests__/obligation-registry-functions/status.test.ts create mode 100644 src/__tests__/obligation-registry-functions/transfers.test.ts create mode 100644 src/core/endorsement-chain/obligation.ts create mode 100644 src/obligation-registry-functions/deploy.ts create mode 100644 src/obligation-registry-functions/index.ts create mode 100644 src/obligation-registry-functions/lifecycle.ts create mode 100644 src/obligation-registry-functions/mint.ts create mode 100644 src/obligation-registry-functions/ownerOf.ts create mode 100644 src/obligation-registry-functions/rejectTransfers.ts create mode 100644 src/obligation-registry-functions/returnToken.ts create mode 100644 src/obligation-registry-functions/status.ts create mode 100644 src/obligation-registry-functions/transfer.ts create mode 100644 src/obligation-registry-functions/types.ts create mode 100644 src/obligation-registry-functions/utils.ts create mode 100644 src/utils/documents/obligation.ts create mode 100644 src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.ts create mode 100644 src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.types.ts create mode 100644 src/verify/fragments/document-status/obligationRecords/utils.ts diff --git a/README.md b/README.md index fc2b368..41c71b8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## About -TrustVC is a comprehensive wrapper library designed to simplify the signing and verification processes for TrustVC W3C [Verifiable Credentials (VC)](https://github.com/TrustVC/w3c) and OpenAttestation Verifiable Documents (VD), including OpenCert Verifiable Documents, adhering to the W3C [VC](https://www.w3.org/TR/vc-data-model/) Data Model v2.0 (W3C Standard). It ensures compatibility and interoperability for Verifiable Credentials while supporting OpenAttestation [Verifiable Documents (VD)](https://github.com/Open-Attestation/open-attestation) v6.9.5. TrustVC seamlessly integrates functionalities for handling W3C Verifiable Credentials and OpenAttestation Verifiable Documents, leveraging existing TradeTrust libraries and smart contracts for [Token Registry](https://github.com/TradeTrust/token-registry) (V4 and V5). For W3C credentials it supports both [`did:web`](https://w3c-ccg.github.io/did-method-web/) (**recommended for production**; host a DID document on a domain you control, which acts as the trust anchor) and [`did:key`](https://w3c-ccg.github.io/did-key-spec/) (self-certifying, no hosting required; best for ad-hoc or ephemeral issuers, but requires an out-of-band trust binding) issuers across the `ecdsa-sd-2023` and `bbs-2023` cryptosuites. Additionally, it includes essential utility functions for strings, networks, and chains, making it a versatile tool for developers working with decentralized identity and verifiable data solutions. +TrustVC is a comprehensive wrapper library designed to simplify the signing and verification processes for TrustVC W3C [Verifiable Credentials (VC)](https://github.com/TrustVC/w3c) and OpenAttestation Verifiable Documents (VD), including OpenCert Verifiable Documents, adhering to the W3C [VC](https://www.w3.org/TR/vc-data-model/) Data Model v2.0 (W3C Standard). It ensures compatibility and interoperability for Verifiable Credentials while supporting OpenAttestation [Verifiable Documents (VD)](https://github.com/Open-Attestation/open-attestation) v6.9.5. TrustVC seamlessly integrates functionalities for handling W3C Verifiable Credentials and OpenAttestation Verifiable Documents, leveraging existing TradeTrust libraries and smart contracts for [Token Registry](https://github.com/TradeTrust/token-registry) (V4 and V5) and for **Obligation Registry** (electronic Bill of Exchange / BoE via `TrustVCToken` + `ObligationEscrow`). For W3C credentials it supports both [`did:web`](https://w3c-ccg.github.io/did-method-web/) (**recommended for production**; host a DID document on a domain you control, which acts as the trust anchor) and [`did:key`](https://w3c-ccg.github.io/did-key-spec/) (self-certifying, no hosting required; best for ad-hoc or ephemeral issuers, but requires an out-of-band trust binding) issuers across the `ecdsa-sd-2023` and `bbs-2023` cryptosuites. Additionally, it includes essential utility functions for strings, networks, and chains, making it a versatile tool for developers working with decentralized identity and verifiable data solutions. ## Table of Contents @@ -18,6 +18,7 @@ TrustVC is a comprehensive wrapper library designed to simplify the signing and - [b) TrustVC W3C Signing (signW3C)](#b-trustvc-w3c-signing-signw3c) - [3. **Deriving (Selective Disclosure)**](#3-deriving-selective-disclosure) - [4. **Verifying**](#4-verifying) + - [Obligation / BoE (`verifyDocument`)](#obligation--boe-verifydocument) - [5. **Encryption**](#5-encryption) - [6. **Decryption**](#6-decryption) - [7. **TradeTrust Token Registry**](#7-tradetrust-token-registry) @@ -25,6 +26,7 @@ TrustVC is a comprehensive wrapper library designed to simplify the signing and - [TradeTrustToken](#tradetrusttoken) - [a) Token Registry v4](#a-token-registry-v4) - [b) Token Registry V5](#b-token-registry-v5) + - [c) Obligation Registry (BoE)](#c-obligation-registry-boe) - [8. **Document Builder**](#8-document-builder) - [9. **Document Store**](#9-document-store) - [10. **Transaction Cancel**](#10-transaction-cancel) @@ -385,6 +387,30 @@ const signedDocument = { const resultFragments = await verifyDocument(signedDocument); ``` +#### Obligation / BoE (`verifyDocument`) + +Use the same `verifyDocument` entry point for Bill of Exchange credentials. The pipeline routes by `credentialStatus`: + +| Fragment | When it runs | +|----------|----------------| +| `TransferableRecords` | Classic ETR (`credentialStatus.tokenRegistry`) | +| `ObligationRecords` | BoE (`credentialStatus.obligationRegistry`) | + +Classic ETR documents get `ObligationRecords` **SKIPPED**; BoE documents get `TransferableRecords` **SKIPPED**. Pass `rpcProviderUrl` or `provider` for on-chain checks. + +```ts +import { verifyDocument, isObligationRecord } from '@trustvc/trustvc'; + +const fragments = await verifyDocument(signedBoeVc, { + rpcProviderUrl: 'https://rpc-amoy.polygon.technology', +}); + +// ObligationRecords fragment: VALID | INVALID | SKIPPED +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. + --- ### 5. **Encryption** @@ -737,8 +763,100 @@ function rejectTransferOwners(bytes calldata _remark) external; For more information on Token Registry and Title Escrow contracts **version v5**, please visit the readme of [TradeTrust Token Registry V5](https://github.com/TradeTrust/token-registry/blob/master/README.md) +#### c) Obligation Registry (BoE) + +> **New:** Obligation Registry supports electronic Bill of Exchange (BoE). It mirrors the classic Transferable Records pattern using **`TrustVCToken`** + **`ObligationEscrow`** (v5 only — no Obligation v4 path). +> +> Import on-chain helpers from `@trustvc/trustvc/obligation-registry-functions`. Functions use the `*ObligationRegistry` suffix (e.g. `mintObligationRegistry`) so they never clash with classic ETR exports on `@trustvc/trustvc`. + +**Lifecycle** + +``` +Deploy factory + registry → Mint (Issued) → Accept (Accepted) / Reject (Rejected, auto-burn) + → Discharge (Discharged, auto-burn) | Transfers | Return to issuer → restore / burn +``` + +**Role rules:** accept / reject / discharge require `beneficiary != holder`; `returnToIssuerObligationRegistry` requires dual role (`beneficiary == holder`). + +**Status enums** (`@trustvc/trustvc/obligation-registry-functions`): + +| Enum | Values | +|------|--------| +| `ObligationDocumentStatus` | `Issued=0`, `Accepted=1`, `Rejected=2`, `Discharged=3` | +| `ObligationEscrowTerminationReason` | `None=0`, `ReturnToIssuer=1`, `Rejected=2`, `Discharged=3` | + +```ts +import { + deployObligationEscrowFactory, + deployObligationRegistry, + mintObligationRegistry, + acceptObligationRegistry, + rejectObligationRegistry, + dischargeObligationRegistry, + nominateObligationRegistry, + transferHolderObligationRegistry, + transferBeneficiaryObligationRegistry, + transferOwnersObligationRegistry, + rejectTransferHolderObligationRegistry, + rejectTransferBeneficiaryObligationRegistry, + rejectTransferOwnersObligationRegistry, + returnToIssuerObligationRegistry, + acceptReturnedObligationRegistry, + rejectReturnedObligationRegistry, + getObligationRegistryStatus, + getObligationEscrowTerminationReason, + ownerOfObligationRegistry, +} from '@trustvc/trustvc/obligation-registry-functions'; + +// Deploy +const { obligationRegistry, obligationEscrowFactoryAddress } = + await deployObligationRegistry('My BoE Registry', 'BOE', signer, { chainId }); + +// Mint (separate from W3C DocumentBuilder sign) +await ( + await mintObligationRegistry( + { obligationRegistryAddress: obligationRegistry }, + issuerSigner, + { beneficiaryAddress, holderAddress, tokenId: '1', remarks: 'issued' }, + { chainId, id: encryptionKeyId }, + ) +).wait(); + +// Holder accepts +await ( + await acceptObligationRegistry( + { obligationRegistryAddress: obligationRegistry, tokenId: '1' }, + holderSigner, + { remarks: 'accepted' }, + { chainId, id: encryptionKeyId }, + ) +).wait(); +``` + +Escrow calls accept `{ obligationRegistryAddress, tokenId }` or `{ obligationEscrowAddress }`. Remarks are encrypted when `options.id` is set (same as Token Registry v5). + +**Endorsement chain** — pass the `TrustVCToken` address to existing helpers: + +```ts +import { fetchEndorsementChain, fetchObligationEndorsementChain } from '@trustvc/trustvc'; + +const chain = await fetchEndorsementChain(obligationRegistry, tokenId, provider); +// or: fetchObligationEndorsementChain(obligationRegistry, tokenId, provider, { encryptionId }) +``` + +**Low-level contracts** (`@trustvc/trustvc/token-registry-v5/contracts`): + +```ts +import { v5Contracts } from '@trustvc/trustvc'; + +const token = v5Contracts.TrustVCToken__factory.connect(obligationRegistry, signer); +const escrow = v5Contracts.ObligationEscrow__factory.connect(escrowAddress, signer); +``` + +Unit tests: `src/__tests__/obligation-registry-functions/`. E2E: `npm run test:e2e` (see `src/__tests__/e2e/README.md`). + ### 8. **Document Builder** -> The `DocumentBuilder` class helps build and manage W3C Verifiable Credentials (VCs) with credential status features, implementing the **W3C VC Data Model 2.0** specification. It supports creating documents with two types of credential statuses: `transferableRecords` and `verifiableDocument`. It can sign the document using modern cryptographic signature schemes including **ECDSA-SD-2023** (default) and **BBS-2023**, verify its signature, and serialize the document to a JSON format. Additionally, it allows for configuration of document rendering methods and expiration dates. +> The `DocumentBuilder` class helps build and manage W3C Verifiable Credentials (VCs) with credential status features, implementing the **W3C VC Data Model 2.0** specification. It supports creating documents with credential statuses for classic transferable records (`tokenRegistry`), obligation records (`obligationRegistry`), and revocable verifiable documents. It can sign the document using modern cryptographic signature schemes including **ECDSA-SD-2023** (default) and **BBS-2023**, verify its signature, and serialize the document to a JSON format. Additionally, it allows for configuration of document rendering methods and expiration dates. #### Usage @@ -766,9 +884,9 @@ builder.credentialSubject({ ``` ##### Configure Credential Status -You can configure the credential status as either `transferableRecords` or `verifiableDocument`. +You can configure the credential status as `transferableRecords`, `obligationRecords`, or `verifiableDocument`. -**Transferable Records** +**Transferable Records (classic Token Registry)** ```ts builder.credentialStatus({ // Refers to the supported network. @@ -780,9 +898,32 @@ builder.credentialStatus({ }); ``` +Verify with `verifyDocument` (TransferableRecords fragment). + +**Obligation Records (BoE / Obligation Registry)** + +```ts +import { DocumentBuilder } from '@trustvc/trustvc'; + +const boeBuilder = new DocumentBuilder({ + '@context': 'https://trustvc.io/context/bill-of-exchange.json', +}).credentialSubject({ + electronicDocumentIdentifier: 'urn:uuid:e6f4b2a1-9c3d-4e8f-a7b0-1d2e3f4a5b6c', +}); + +boeBuilder.obligationCredentialStatus({ + chain: 'amoy', + chainId: 80002, + obligationRegistry: '0x1234567890abcdef...', + rpcProviderUrl: 'https://rpc-amoy.polygon.technology', +}); +``` + +This sets `credentialStatus.type` to `TransferableRecords` with an `obligationRegistry` field (not `tokenRegistry`). On-chain minting is separate — use `mintObligationRegistry` from `@trustvc/trustvc/obligation-registry-functions` (see [§7c](#c-obligation-registry-boe)). Verify with `verifyDocument` (ObligationRecords fragment) — see [§4](#obligation--boe-verifydocument). + > ⚠️ **Disclaimer:** -> This builder **does not mint** documents on-chain. If you're using `transferableRecords`, you'll need to mint the document. -> [See the minting guide here](https://docs.tradetrust.io/docs/how-tos/credential-status#2-minting-the-credential) +> These builders **do not mint** on-chain. Mint separately via `mint` (ETR) or `mintObligationRegistry` (BoE). +> Classic ETR: [TradeTrust minting guide](https://docs.tradetrust.io/docs/how-tos/credential-status#2-minting-the-credential). **Verifiable Document** diff --git a/package-lock.json b/package-lock.json index 9d0b104..9c12c01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,15 +12,15 @@ "@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/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", @@ -1310,59 +1310,34 @@ "node": ">=18" } }, - "node_modules/@digitalbazaar/bbs-2023-cryptosuite/node_modules/@digitalbazaar/bls12-381-multikey": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@digitalbazaar/bls12-381-multikey/-/bls12-381-multikey-2.1.0.tgz", - "integrity": "sha512-JelU85fNhvHl2/mqRdmrtrE2ZQJ0//+UwI0l/YFmvsOr6YN2GuKPzdkfXjpm7f3UvnBqz5f8QKFTb9mVa7mVVg==", + "node_modules/@digitalbazaar/bbs-signatures": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@digitalbazaar/bbs-signatures/-/bbs-signatures-3.1.0.tgz", + "integrity": "sha512-wx86l/PFOaRcoLBPmzwpF9Oo4uYJrm4uq/B1rHX5OHD15NakmUINfRr8NAGDG356GeTBDGZkMsBFgNj1x0dc+g==", "license": "BSD-3-Clause", "dependencies": { - "@digitalbazaar/bbs-signatures": "^3.0.0", - "@noble/curves": "^1.3.0", - "base58-universal": "^2.0.0", - "base64url-universal": "^2.0.0", - "cborg": "^4.2.0" + "@noble/curves": "^2.2.0", + "@noble/hashes": "^2.2.0" }, "engines": { "node": ">=18" } }, - "node_modules/@digitalbazaar/bbs-2023-cryptosuite/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==", + "node_modules/@digitalbazaar/bbs-signatures/node_modules/@noble/curves": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.2.0.tgz", + "integrity": "sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ==", "license": "MIT", "dependencies": { - "@noble/hashes": "1.8.0" + "@noble/hashes": "2.2.0" }, "engines": { - "node": "^14.21.3 || >=16" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" } }, - "node_modules/@digitalbazaar/bbs-2023-cryptosuite/node_modules/cborg": { - "version": "4.5.8", - "resolved": "https://registry.npmjs.org/cborg/-/cborg-4.5.8.tgz", - "integrity": "sha512-6/viltD51JklRhq4L7jC3zgy6gryuG5xfZ3kzpE+PravtyeQLeQmCYLREhQH7pWENg5pY4Yu/XCd6a7dKScVlw==", - "license": "Apache-2.0", - "bin": { - "cborg": "lib/bin.js" - } - }, - "node_modules/@digitalbazaar/bbs-signatures": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@digitalbazaar/bbs-signatures/-/bbs-signatures-3.1.0.tgz", - "integrity": "sha512-wx86l/PFOaRcoLBPmzwpF9Oo4uYJrm4uq/B1rHX5OHD15NakmUINfRr8NAGDG356GeTBDGZkMsBFgNj1x0dc+g==", - "license": "BSD-3-Clause", - "dependencies": { - "@noble/curves": "^2.2.0", - "@noble/hashes": "^2.2.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@digitalbazaar/bbs-signatures/node_modules/@noble/hashes": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", @@ -1375,6 +1350,22 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@digitalbazaar/bls12-381-multikey": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@digitalbazaar/bls12-381-multikey/-/bls12-381-multikey-2.1.0.tgz", + "integrity": "sha512-JelU85fNhvHl2/mqRdmrtrE2ZQJ0//+UwI0l/YFmvsOr6YN2GuKPzdkfXjpm7f3UvnBqz5f8QKFTb9mVa7mVVg==", + "license": "BSD-3-Clause", + "dependencies": { + "@digitalbazaar/bbs-signatures": "^3.0.0", + "@noble/curves": "^1.3.0", + "base58-universal": "^2.0.0", + "base64url-universal": "^2.0.0", + "cborg": "^4.2.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@digitalbazaar/data-integrity": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@digitalbazaar/data-integrity/-/data-integrity-2.5.0.tgz", @@ -1466,9 +1457,9 @@ } }, "node_modules/@digitalbazaar/di-sd-primitives/node_modules/undici": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.26.0.tgz", - "integrity": "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==", + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz", + "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==", "license": "MIT", "engines": { "node": ">=18.17" @@ -1487,6 +1478,90 @@ "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-rdfc-2019-cryptosuite/node_modules/undici": { + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz", + "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "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", @@ -1504,15 +1579,6 @@ "node": ">=18" } }, - "node_modules/@digitalbazaar/ecdsa-sd-2023-cryptosuite/node_modules/cborg": { - "version": "4.5.8", - "resolved": "https://registry.npmjs.org/cborg/-/cborg-4.5.8.tgz", - "integrity": "sha512-6/viltD51JklRhq4L7jC3zgy6gryuG5xfZ3kzpE+PravtyeQLeQmCYLREhQH7pWENg5pY4Yu/XCd6a7dKScVlw==", - "license": "Apache-2.0", - "bin": { - "cborg": "lib/bin.js" - } - }, "node_modules/@digitalbazaar/http-client": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/@digitalbazaar/http-client/-/http-client-3.4.1.tgz", @@ -3692,27 +3758,15 @@ } }, "node_modules/@noble/curves": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.2.0.tgz", - "integrity": "sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ==", + "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": "2.2.0" - }, - "engines": { - "node": ">= 20.19.0" + "@noble/hashes": "1.8.0" }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/curves/node_modules/@noble/hashes": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", - "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", - "license": "MIT", "engines": { - "node": ">= 20.19.0" + "node": "^14.21.3 || >=16" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -6560,9 +6614,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" @@ -6831,24 +6885,24 @@ "license": "Apache-2.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", @@ -6865,13 +6919,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" }, @@ -6880,9 +6934,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", @@ -6897,46 +6951,6 @@ "node": ">=18.x" } }, - "node_modules/@trustvc/w3c-issuer/node_modules/@digitalbazaar/bls12-381-multikey": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@digitalbazaar/bls12-381-multikey/-/bls12-381-multikey-2.1.0.tgz", - "integrity": "sha512-JelU85fNhvHl2/mqRdmrtrE2ZQJ0//+UwI0l/YFmvsOr6YN2GuKPzdkfXjpm7f3UvnBqz5f8QKFTb9mVa7mVVg==", - "license": "BSD-3-Clause", - "dependencies": { - "@digitalbazaar/bbs-signatures": "^3.0.0", - "@noble/curves": "^1.3.0", - "base58-universal": "^2.0.0", - "base64url-universal": "^2.0.0", - "cborg": "^4.2.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@trustvc/w3c-issuer/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/@trustvc/w3c-issuer/node_modules/cborg": { - "version": "4.5.8", - "resolved": "https://registry.npmjs.org/cborg/-/cborg-4.5.8.tgz", - "integrity": "sha512-6/viltD51JklRhq4L7jC3zgy6gryuG5xfZ3kzpE+PravtyeQLeQmCYLREhQH7pWENg5pY4Yu/XCd6a7dKScVlw==", - "license": "Apache-2.0", - "bin": { - "cborg": "lib/bin.js" - } - }, "node_modules/@trustvc/w3c-issuer/node_modules/did-resolver": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/did-resolver/-/did-resolver-4.1.0.tgz", @@ -6944,19 +6958,20 @@ "license": "Apache-2.0" }, "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", @@ -6972,46 +6987,6 @@ "jsonld": "^6.0.0" } }, - "node_modules/@trustvc/w3c-vc/node_modules/@digitalbazaar/bls12-381-multikey": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@digitalbazaar/bls12-381-multikey/-/bls12-381-multikey-2.1.0.tgz", - "integrity": "sha512-JelU85fNhvHl2/mqRdmrtrE2ZQJ0//+UwI0l/YFmvsOr6YN2GuKPzdkfXjpm7f3UvnBqz5f8QKFTb9mVa7mVVg==", - "license": "BSD-3-Clause", - "dependencies": { - "@digitalbazaar/bbs-signatures": "^3.0.0", - "@noble/curves": "^1.3.0", - "base58-universal": "^2.0.0", - "base64url-universal": "^2.0.0", - "cborg": "^4.2.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@trustvc/w3c-vc/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/@trustvc/w3c-vc/node_modules/cborg": { - "version": "4.5.8", - "resolved": "https://registry.npmjs.org/cborg/-/cborg-4.5.8.tgz", - "integrity": "sha512-6/viltD51JklRhq4L7jC3zgy6gryuG5xfZ3kzpE+PravtyeQLeQmCYLREhQH7pWENg5pY4Yu/XCd6a7dKScVlw==", - "license": "Apache-2.0", - "bin": { - "cborg": "lib/bin.js" - } - }, "node_modules/@trustvc/w3c-vc/node_modules/did-resolver": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/did-resolver/-/did-resolver-4.1.0.tgz", @@ -8676,6 +8651,15 @@ "node": ">=16" } }, + "node_modules/cborg": { + "version": "4.5.8", + "resolved": "https://registry.npmjs.org/cborg/-/cborg-4.5.8.tgz", + "integrity": "sha512-6/viltD51JklRhq4L7jC3zgy6gryuG5xfZ3kzpE+PravtyeQLeQmCYLREhQH7pWENg5pY4Yu/XCd6a7dKScVlw==", + "license": "Apache-2.0", + "bin": { + "cborg": "lib/bin.js" + } + }, "node_modules/chai": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", @@ -14696,9 +14680,9 @@ } }, "node_modules/jsonld-signatures/node_modules/undici": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.26.0.tgz", - "integrity": "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==", + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz", + "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==", "license": "MIT", "engines": { "node": ">=18.17" diff --git a/package.json b/package.json index 1886eb0..dbcb1cd 100644 --- a/package.json +++ b/package.json @@ -112,21 +112,26 @@ "types": "./dist/types/w3c/credential-status/index.d.ts", "import": "./dist/esm/w3c/credential-status/index.js", "require": "./dist/cjs/w3c/credential-status/index.js" + }, + "./obligation-registry-functions": { + "types": "./dist/types/obligation-registry-functions/index.d.ts", + "import": "./dist/esm/obligation-registry-functions/index.js", + "require": "./dist/cjs/obligation-registry-functions/index.js" } }, "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/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", diff --git a/src/__tests__/e2e/fixtures/sample-boe-credential.ts b/src/__tests__/e2e/fixtures/sample-boe-credential.ts new file mode 100644 index 0000000..904980b --- /dev/null +++ b/src/__tests__/e2e/fixtures/sample-boe-credential.ts @@ -0,0 +1,103 @@ +import { DocumentBuilder } from '../../../core/documentBuilder'; +import { CHAIN_ID } from '../../../utils'; +import { PrivateKeyPair, VerificationType } from '@trustvc/w3c-issuer'; + +/** Same did:web issuer as W3C transferable-record fixtures — resolves via trustvc.github.io DNS. */ +export const SAMPLE_BOE_ISSUER = 'did:web:trustvc.github.io:did:1' as const; + +export const SAMPLE_BOE_VERIFICATION_METHOD = `${SAMPLE_BOE_ISSUER}#multikey-1` as const; + +/** + * ECDSA-SD-2023 test key for did:web:trustvc.github.io:did:1. + * Matches documentBuilder.test.ts / W3C fixture signing material. + */ +export const SAMPLE_BOE_SIGNING_KEY: PrivateKeyPair = { + '@context': 'https://w3id.org/security/multikey/v1', + id: SAMPLE_BOE_VERIFICATION_METHOD, + type: VerificationType.Multikey, + controller: SAMPLE_BOE_ISSUER, + publicKeyMultibase: 'zDnaemDNwi4G5eTzGfRooFFu5Kns3be6yfyVNtiaMhWkZbwtc', + secretKeyMultibase: 'z42tmUXTVn3n9BihE6NhdMpvVBTnFTgmb6fw18o5Ud6puhRW', +}; + +/** Bill of Exchange credential subject aligned with https://trustvc.io/context/bill-of-exchange.json */ +export const SAMPLE_BOE_CREDENTIAL_SUBJECT = { + electronicDocumentIdentifier: 'urn:uuid:e6f4b2a1-9c3d-4e8f-a7b0-1d2e3f4a5b6c', + referenceNumber: 'BOE-E2E-00147', + amountInFigures: '100000.00', + amountInWords: 'One hundred thousand 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-E2E-2026-88341 / Invoice No. INV-E2E-2026-0456 / B/L No. BL-E2E-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: '', + }, + drawer: { + name: 'Meridian Commodities Pte Ltd', + address: '8 Marina Boulevard, #24-01, Singapore 018981', + authorisedSignatoryName: 'Wei Ling Tan', + signature: '', + }, +}; + +export const SAMPLE_BOE_CONTEXT = 'https://trustvc.io/context/bill-of-exchange.json'; + +export const SAMPLE_BOE_ENCRYPTION_ID = 'e2e-boe-encryption-key'; + +// DocumentBuilder.obligationCredentialStatus() config for Hardhat local network. +export const createSampleBoeCredentialStatusConfig = (obligationRegistry: string) => ({ + chain: 'local', + chainId: CHAIN_ID.local, + obligationRegistry, + rpcProviderUrl: 'http://127.0.0.1:8545', +}); + +// Unsigned W3C VC shell — use with DocumentBuilder before sign. +export const createSampleBoeDocumentShell = () => ({ + '@context': ['https://www.w3.org/ns/credentials/v2', SAMPLE_BOE_CONTEXT], + type: ['VerifiableCredential'], + issuer: SAMPLE_BOE_ISSUER, + credentialSubject: SAMPLE_BOE_CREDENTIAL_SUBJECT, +}); + +// Pre-configured DocumentBuilder for BoE e2e / verify tests. +export const createSampleBoeDocumentBuilder = (obligationRegistry?: string) => { + const builder = new DocumentBuilder({ + '@context': SAMPLE_BOE_CONTEXT, + }) + .credentialSubject(SAMPLE_BOE_CREDENTIAL_SUBJECT) + .renderMethod({ + type: 'EMBEDDED_RENDERER', + templateName: 'BILL_OF_EXCHANGE', + id: 'https://generic-templates.tradetrust.io', + }) + .expirationDate('2029-12-03T12:19:52Z'); + + if (obligationRegistry) { + builder.obligationCredentialStatus(createSampleBoeCredentialStatusConfig(obligationRegistry)); + } + + return builder; +}; + +// Sign a BoE VC with the shared did:web test key (passes DOCUMENT_INTEGRITY / DNS-DID checks). +export const signSampleBoeDocument = async (obligationRegistry?: string) => { + const builder = createSampleBoeDocumentBuilder(obligationRegistry); + return builder.sign(SAMPLE_BOE_SIGNING_KEY, 'ecdsa-sd-2023'); +}; + +// Default on-chain tx options for obligation e2e on Hardhat. +export const createSampleBoeTxOptions = (encryptionId = SAMPLE_BOE_ENCRYPTION_ID) => ({ + chainId: CHAIN_ID.local, + id: encryptionId, +}); diff --git a/src/__tests__/e2e/obligation-registry-functions/fixtures.ts b/src/__tests__/e2e/obligation-registry-functions/fixtures.ts new file mode 100644 index 0000000..8e00497 --- /dev/null +++ b/src/__tests__/e2e/obligation-registry-functions/fixtures.ts @@ -0,0 +1,101 @@ +import { ethers as ethersV6 } from 'ethersV6'; +import { ethers } from 'ethers'; +import { CHAIN_ID } from '../../../utils'; +import { + deployObligationRegistry, + mintObligationRegistry, +} from '../../../obligation-registry-functions'; +import type { TransactionOptions } from '../../../obligation-registry-functions/types'; +import { getObligationEscrowAddress } from '../../../core'; +import { getSignersV5, getSignersV6, providerV5, providerV6 } from '../fixtures'; +import { createSampleBoeTxOptions } from '../fixtures/sample-boe-credential'; + +export type ObligationE2EProvider = { + Provider: typeof providerV5 | typeof providerV6; + ethersVersion: 'v5' | 'v6'; +}; + +export const obligationE2EProviders: ObligationE2EProvider[] = [ + { Provider: providerV5, ethersVersion: 'v5' }, + { Provider: providerV6, ethersVersion: 'v6' }, +]; + +export type ObligationE2ESigner = ethers.Wallet | ethersV6.Wallet; + +export interface ObligationE2ESetup { + deployer: ObligationE2ESigner; + holder: ObligationE2ESigner; + beneficiary: ObligationE2ESigner; + other: ObligationE2ESigner; + obligationRegistry: string; + obligationEscrowFactoryAddress: string; + txOptions: TransactionOptions; + ethersVersion: 'v5' | 'v6'; + provider: typeof providerV5 | typeof providerV6; +} + +export const createObligationE2ESigners = async ( + ethersVersion: 'v5' | 'v6', + count = 6, +): Promise => { + return ethersVersion === 'v5' ? getSignersV5(count) : getSignersV6(count); +}; + +export const deployObligationE2ERegistry = async ( + deployer: ObligationE2ESigner, +): Promise<{ obligationRegistry: string; obligationEscrowFactoryAddress: string }> => { + return deployObligationRegistry('E2E BoE Registry', 'BOE', deployer, { + chainId: CHAIN_ID.local, + }); +}; + +export const mintObligationE2EToken = async ( + setup: Pick, + tokenId: string | number, + holderAddress: string, + beneficiaryAddress: string, + remarks?: string, +) => { + const tx = await mintObligationRegistry( + { obligationRegistryAddress: setup.obligationRegistry }, + setup.deployer, + { + beneficiaryAddress, + holderAddress, + tokenId, + remarks, + }, + setup.txOptions, + ); + await tx.wait(); +}; + +export const buildObligationE2ESetup = ( + ethersVersion: 'v5' | 'v6', + signers: ObligationE2ESigner[], + obligationRegistry: string, + obligationEscrowFactoryAddress: string, +): ObligationE2ESetup => { + const [deployer, holder, beneficiary, other] = signers; + + return { + deployer, + holder, + beneficiary, + other, + obligationRegistry, + obligationEscrowFactoryAddress, + txOptions: createSampleBoeTxOptions(), + ethersVersion, + provider: ethersVersion === 'v5' ? providerV5 : providerV6, + }; +}; + +export const getObligationE2EEscrowAddress = async ( + setup: ObligationE2ESetup, + tokenId: string | number, +): Promise => { + return getObligationEscrowAddress(setup.obligationRegistry, String(tokenId), setup.provider, { + titleEscrowVersion: 'v5', + }); +}; diff --git a/src/__tests__/e2e/obligation-registry-functions/statusLifecycle.e2e.test.ts b/src/__tests__/e2e/obligation-registry-functions/statusLifecycle.e2e.test.ts new file mode 100644 index 0000000..4a4bc9e --- /dev/null +++ b/src/__tests__/e2e/obligation-registry-functions/statusLifecycle.e2e.test.ts @@ -0,0 +1,263 @@ +import { expect } from 'chai'; +import { network } from 'hardhat'; +import '@nomiclabs/hardhat-ethers'; +import '@nomicfoundation/hardhat-chai-matchers'; +import { + acceptObligationRegistry, + rejectObligationRegistry, + dischargeObligationRegistry, + getObligationRegistryStatus, + getObligationEscrowTerminationReason, + isObligationRegistryRegistered, + mintObligationRegistry, + ObligationDocumentStatus, + ObligationEscrowTerminationReason, +} from '../../../obligation-registry-functions'; +import { createObligationContract } from '../utils'; +import { + buildObligationE2ESetup, + createObligationE2ESigners, + deployObligationE2ERegistry, + getObligationE2EEscrowAddress, + mintObligationE2EToken, + obligationE2EProviders, + type ObligationE2ESetup, +} from './fixtures'; + +obligationE2EProviders.forEach(({ ethersVersion }) => { + describe(`Obligation status lifecycle E2E (ethers ${ethersVersion})`, function () { + let setup: ObligationE2ESetup; + + before(async function () { + await network.provider.send('evm_setAutomine', [true]); + await network.provider.send('hardhat_reset'); + + const signers = await createObligationE2ESigners(ethersVersion, 6); + const deployed = await deployObligationE2ERegistry(signers[0]); + setup = buildObligationE2ESetup( + ethersVersion, + signers, + deployed.obligationRegistry, + deployed.obligationEscrowFactoryAddress, + ); + }); + + it('E1: mint → Issued, isRegistered true, terminationReason None', async function () { + const tokenId = '1'; + + await mintObligationE2EToken( + setup, + tokenId, + setup.holder.address, + setup.beneficiary.address, + 'issued', + ); + + const status = await getObligationRegistryStatus( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.deployer, + { tokenId }, + ); + const registered = await isObligationRegistryRegistered( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.deployer, + { tokenId }, + ); + const reason = await getObligationEscrowTerminationReason( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.deployer, + { tokenId }, + ); + + expect(status).to.equal(ObligationDocumentStatus.Issued); + expect(registered).to.equal(true); + expect(reason).to.equal(ObligationEscrowTerminationReason.None); + }); + + it('E2: duplicate mint fails', async function () { + const tokenId = '2'; + + await mintObligationE2EToken(setup, tokenId, setup.holder.address, setup.beneficiary.address); + + await expect( + mintObligationRegistry( + { obligationRegistryAddress: setup.obligationRegistry }, + setup.deployer, + { + beneficiaryAddress: setup.beneficiary.address, + holderAddress: setup.holder.address, + tokenId, + }, + setup.txOptions, + ), + ).to.be.rejected; + }); + + it('E3: accept fails when beneficiary == holder', async function () { + const tokenId = '3'; + + await mintObligationE2EToken(setup, tokenId, setup.holder.address, setup.holder.address); + + await expect( + acceptObligationRegistry( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.holder, + {}, + setup.txOptions, + ), + ).to.be.rejectedWith(/accept failed/); + }); + + it('E4: holder accept transitions Issued → Accepted', async function () { + const tokenId = '4'; + + await mintObligationE2EToken(setup, tokenId, setup.holder.address, setup.beneficiary.address); + + await ( + await acceptObligationRegistry( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.holder, + { remarks: 'accepted' }, + setup.txOptions, + ) + ).wait(); + + const status = await getObligationRegistryStatus( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.deployer, + { tokenId }, + ); + + expect(status).to.equal(ObligationDocumentStatus.Accepted); + }); + + it('E5: non-holder cannot accept', async function () { + const tokenId = '5'; + + await mintObligationE2EToken(setup, tokenId, setup.holder.address, setup.beneficiary.address); + + await expect( + acceptObligationRegistry( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.beneficiary, + {}, + setup.txOptions, + ), + ).to.be.rejectedWith(/accept failed/); + }); + + it('E6: holder reject → Rejected + terminationReason Rejected + inactive', async function () { + const tokenId = '6'; + + await mintObligationE2EToken(setup, tokenId, setup.holder.address, setup.beneficiary.address); + + await ( + await rejectObligationRegistry( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.holder, + { remarks: 'rejected' }, + setup.txOptions, + ) + ).wait(); + + const status = await getObligationRegistryStatus( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.deployer, + { tokenId }, + ); + const reason = await getObligationEscrowTerminationReason( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.deployer, + { tokenId }, + ); + const escrowAddress = await getObligationE2EEscrowAddress(setup, tokenId); + const escrow = createObligationContract( + escrowAddress, + 'ObligationEscrow', + ethersVersion, + setup.deployer, + ); + + expect(status).to.equal(ObligationDocumentStatus.Rejected); + expect(reason).to.equal(ObligationEscrowTerminationReason.Rejected); + expect(await escrow.active()).to.equal(false); + }); + + it('E7: beneficiary discharge → Discharged + terminationReason Discharged', async function () { + const tokenId = '7'; + + await mintObligationE2EToken(setup, tokenId, setup.holder.address, setup.beneficiary.address); + await ( + await acceptObligationRegistry( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.holder, + {}, + setup.txOptions, + ) + ).wait(); + + await ( + await dischargeObligationRegistry( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.beneficiary, + { remarks: 'discharged' }, + setup.txOptions, + ) + ).wait(); + + const status = await getObligationRegistryStatus( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.deployer, + { tokenId }, + ); + const reason = await getObligationEscrowTerminationReason( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.deployer, + { tokenId }, + ); + + expect(status).to.equal(ObligationDocumentStatus.Discharged); + expect(reason).to.equal(ObligationEscrowTerminationReason.Discharged); + }); + + it('E8: discharge while Issued fails', async function () { + const tokenId = '8'; + + await mintObligationE2EToken(setup, tokenId, setup.holder.address, setup.beneficiary.address); + + await expect( + dischargeObligationRegistry( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.beneficiary, + {}, + setup.txOptions, + ), + ).to.be.rejectedWith(/discharge failed/); + }); + + it('E9: mint with empty remarks / no encryption id', async function () { + const tokenId = '9'; + + await ( + await mintObligationRegistry( + { obligationRegistryAddress: setup.obligationRegistry }, + setup.deployer, + { + beneficiaryAddress: setup.beneficiary.address, + holderAddress: setup.holder.address, + tokenId, + }, + { chainId: setup.txOptions.chainId }, + ) + ).wait(); + + const status = await getObligationRegistryStatus( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.deployer, + { tokenId }, + ); + + expect(status).to.equal(ObligationDocumentStatus.Issued); + }); + }); +}); diff --git a/src/__tests__/e2e/obligation-registry-functions/transfer.e2e.test.ts b/src/__tests__/e2e/obligation-registry-functions/transfer.e2e.test.ts new file mode 100644 index 0000000..974332c --- /dev/null +++ b/src/__tests__/e2e/obligation-registry-functions/transfer.e2e.test.ts @@ -0,0 +1,179 @@ +import { expect } from 'chai'; +import { network } from 'hardhat'; +import '@nomiclabs/hardhat-ethers'; +import '@nomicfoundation/hardhat-chai-matchers'; +import { + acceptObligationRegistry, + nominateObligationRegistry, + transferBeneficiaryObligationRegistry, + transferHolderObligationRegistry, + transferOwnersObligationRegistry, +} from '../../../obligation-registry-functions'; +import { + buildObligationE2ESetup, + createObligationE2ESigners, + deployObligationE2ERegistry, + mintObligationE2EToken, + obligationE2EProviders, + type ObligationE2ESetup, +} from './fixtures'; + +obligationE2EProviders.forEach(({ ethersVersion }) => { + describe(`Obligation transfer E2E (ethers ${ethersVersion})`, function () { + let setup: ObligationE2ESetup; + + before(async function () { + await network.provider.send('evm_setAutomine', [true]); + await network.provider.send('hardhat_reset'); + + const signers = await createObligationE2ESigners(ethersVersion, 6); + const deployed = await deployObligationE2ERegistry(signers[0]); + setup = buildObligationE2ESetup( + ethersVersion, + signers, + deployed.obligationRegistry, + deployed.obligationEscrowFactoryAddress, + ); + }); + + it('E10: transferHolder after accept', async function () { + const tokenId = '10'; + + await mintObligationE2EToken(setup, tokenId, setup.holder.address, setup.beneficiary.address); + await ( + await acceptObligationRegistry( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.holder, + {}, + setup.txOptions, + ) + ).wait(); + + const tx = await transferHolderObligationRegistry( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.holder, + { holderAddress: setup.other.address, remarks: 'transfer holder' }, + setup.txOptions, + ); + await tx.wait(); + + expect(tx.hash).to.be.a('string'); + }); + + it('E11: non-holder cannot transferHolder', async function () { + const tokenId = '11'; + + await mintObligationE2EToken(setup, tokenId, setup.holder.address, setup.beneficiary.address); + await ( + await acceptObligationRegistry( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.holder, + {}, + setup.txOptions, + ) + ).wait(); + + await expect( + transferHolderObligationRegistry( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.beneficiary, + { holderAddress: setup.other.address }, + setup.txOptions, + ), + ).to.be.rejectedWith(/transferHolder failed/); + }); + + it('E12: nominate + transferBeneficiary', async function () { + const tokenId = '12'; + + await mintObligationE2EToken(setup, tokenId, setup.holder.address, setup.beneficiary.address); + await ( + await acceptObligationRegistry( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.holder, + {}, + setup.txOptions, + ) + ).wait(); + + await ( + await nominateObligationRegistry( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.beneficiary, + { newBeneficiaryAddress: setup.other.address }, + setup.txOptions, + ) + ).wait(); + + const tx = await transferBeneficiaryObligationRegistry( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.holder, + { newBeneficiaryAddress: setup.other.address }, + setup.txOptions, + ); + await tx.wait(); + + expect(tx.hash).to.be.a('string'); + }); + + it('E13: non-beneficiary cannot nominate', async function () { + const tokenId = '13'; + + await mintObligationE2EToken(setup, tokenId, setup.holder.address, setup.beneficiary.address); + await ( + await acceptObligationRegistry( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.holder, + {}, + setup.txOptions, + ) + ).wait(); + + await expect( + nominateObligationRegistry( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.holder, + { newBeneficiaryAddress: setup.other.address }, + setup.txOptions, + ), + ).to.be.rejectedWith(/nominate failed/); + }); + + it('E14: dual-role transferOwners on Issued (no accept)', async function () { + const tokenId = '14'; + + await mintObligationE2EToken(setup, tokenId, setup.holder.address, setup.holder.address); + + const tx = await transferOwnersObligationRegistry( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.holder, + { + newHolderAddress: setup.other.address, + newBeneficiaryAddress: setup.beneficiary.address, + }, + setup.txOptions, + ); + await tx.wait(); + + expect(tx.hash).to.be.a('string'); + }); + + it('E15: transferOwners fails when not dual-role', async function () { + const tokenId = '15'; + + await mintObligationE2EToken(setup, tokenId, setup.holder.address, setup.beneficiary.address); + + await expect( + transferOwnersObligationRegistry( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.holder, + { + newHolderAddress: setup.other.address, + newBeneficiaryAddress: setup.beneficiary.address, + }, + setup.txOptions, + ), + ).to.be.rejectedWith(/transferOwners failed/); + }); + }); +}); diff --git a/src/__tests__/e2e/utils.ts b/src/__tests__/e2e/utils.ts index 060d04c..e884cf7 100644 --- a/src/__tests__/e2e/utils.ts +++ b/src/__tests__/e2e/utils.ts @@ -36,3 +36,33 @@ export const createContract = ( ? new ethers.Contract(address, abi, signer as Signer) : new ethersV6.Contract(address, abi, signer as ContractRunner); }; + +export function getObligationContractFactory( + contractName: 'TrustVCToken' | 'ObligationEscrowFactory', + ethersVersion: 'v5' | 'v6', + owner: Signer | ContractRunner, +) { + const Factory = ethersVersion === 'v5' ? ethers.ContractFactory : ethersV6.ContractFactory; + const signer = ethersVersion === 'v5' ? (owner as Signer) : (owner as ContractRunner); + + return new Factory( + v5Contracts[`${contractName}__factory`].abi, + v5Contracts[`${contractName}__factory`].bytecode, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); +} + +export const createObligationContract = ( + address: string, + contractName: 'ObligationEscrow' | 'TrustVCToken', + ethersVersion: 'v5' | 'v6', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer: any, +) => { + const abi = v5Contracts[`${contractName}__factory`].abi; + + return ethersVersion === 'v5' + ? new ethers.Contract(address, abi, signer as Signer) + : new ethersV6.Contract(address, abi, signer as ContractRunner); +}; diff --git a/src/__tests__/obligation-registry-functions/deploy.test.ts b/src/__tests__/obligation-registry-functions/deploy.test.ts new file mode 100644 index 0000000..39c85b3 --- /dev/null +++ b/src/__tests__/obligation-registry-functions/deploy.test.ts @@ -0,0 +1,78 @@ +import './fixtures.js'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { + deployObligationEscrowFactory, + deployObligationRegistry, +} from '../../obligation-registry-functions'; +import { getEthersContractFactoryFromProvider, isV6EthersProvider } from '../../utils/ethers'; +import { CHAIN_ID } from '../../utils'; + +describe('deploy obligation registry', () => { + const mockChainId = CHAIN_ID.sepolia; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('deployObligationEscrowFactory with ethers v6', async () => { + const mockContract = { + getAddress: vi.fn().mockResolvedValue('0xEscrowFactory'), + deploymentTransaction: vi.fn().mockReturnValue({ + wait: vi.fn().mockResolvedValue({ transactionHash: 'factory_tx' }), + }), + }; + const mockFactory = { + deploy: vi.fn().mockResolvedValue(mockContract), + }; + + const providerV6Mock: any = { + getNetwork: vi.fn().mockResolvedValue({ chainId: mockChainId }), + }; + const wallet: any = { provider: providerV6Mock }; + + vi.mocked(isV6EthersProvider).mockReturnValue(true); + vi.mocked(getEthersContractFactoryFromProvider).mockReturnValue( + vi.fn().mockReturnValue(mockFactory) as any, + ); + + const result = await deployObligationEscrowFactory(wallet, { chainId: mockChainId }); + + expect(result.obligationEscrowFactoryAddress).toBe('0xEscrowFactory'); + expect(mockFactory.deploy).toHaveBeenCalled(); + }); + + it('deployObligationRegistry with existing escrow factory (ethers v6)', async () => { + const mockContract = { + getAddress: vi.fn().mockResolvedValue('0xObligationRegistry'), + deploymentTransaction: vi.fn().mockReturnValue({ + wait: vi.fn().mockResolvedValue({ transactionHash: 'registry_tx' }), + }), + }; + const mockFactory = { + deploy: vi.fn().mockResolvedValue(mockContract), + }; + + const providerV6Mock: any = { + getNetwork: vi.fn().mockResolvedValue({ chainId: mockChainId }), + }; + const wallet: any = { provider: providerV6Mock }; + + vi.mocked(isV6EthersProvider).mockReturnValue(true); + vi.mocked(getEthersContractFactoryFromProvider).mockReturnValue( + vi.fn().mockReturnValue(mockFactory) as any, + ); + + const result = await deployObligationRegistry('BoE', 'BOE', wallet, { + chainId: mockChainId, + escrowFactoryAddress: '0xExistingFactory', + }); + + expect(result.obligationRegistry).toBe('0xObligationRegistry'); + expect(result.obligationEscrowFactoryAddress).toBe('0xExistingFactory'); + expect(mockFactory.deploy).toHaveBeenCalledWith('BoE', 'BOE', '0xExistingFactory', {}); + }); + + it('throws when provider is missing', async () => { + await expect(deployObligationEscrowFactory({} as any)).rejects.toThrow('Provider is required'); + }); +}); diff --git a/src/__tests__/obligation-registry-functions/fixtures.ts b/src/__tests__/obligation-registry-functions/fixtures.ts new file mode 100644 index 0000000..8f446bb --- /dev/null +++ b/src/__tests__/obligation-registry-functions/fixtures.ts @@ -0,0 +1,129 @@ +import { vi } from 'vitest'; +import { ethers as ethersV5 } from 'ethers'; +import { JsonRpcProvider as JsonRpcProviderV6 } from 'ethersV6'; +import * as originalModule from '../../utils/ethers'; +import * as tokenRegistryFunctions from '../../token-registry-functions/utils'; + +export const MOCK_OBLIGATION_REGISTRY_ADDRESS = '0xObligationRegistryContract'; +export const MOCK_OBLIGATION_ESCROW_ADDRESS = '0xObligationEscrowContract'; +export const MOCK_OWNER_ADDRESS = '0xowner'; + +vi.mock('../../token-registry-functions/utils', async (importOriginal) => { + const original = (await importOriginal()) as typeof tokenRegistryFunctions; + return { + ...original, + getChainIdSafe: vi.fn().mockResolvedValue(1), + getTxOptions: vi.fn().mockResolvedValue({}), + }; +}); + +vi.mock('../../utils/ethers', async (importOriginal) => { + const original = (await importOriginal()) as typeof originalModule; + + const MockContractConstructor = vi.fn((address: string) => { + if (address === MOCK_OBLIGATION_REGISTRY_ADDRESS) { + return mockTrustVCTokenContract; + } + return mockObligationEscrowContract; + }); + + return { + ...original, + getEthersContractFromProvider: vi.fn(() => MockContractConstructor), + getEthersContractFactoryFromProvider: vi.fn(() => vi.fn()), + isV6EthersProvider: vi.fn().mockImplementation(original.isV6EthersProvider), + }; +}); + +vi.mock('../../core', () => ({ + encrypt: vi.fn(() => 'encrypted_remarks'), + getObligationEscrowAddress: vi.fn(), + checkSupportsInterface: vi.fn(), +})); + +vi.mock('../../token-registry-v5', async (importOriginal) => { + const original = (await importOriginal()) as Record; + return { + ...original, + v5Contracts: { + TrustVCToken__factory: { + abi: 'TrustVCToken', + bytecode: '0x60806040', + }, + ObligationEscrow__factory: { + abi: 'ObligationEscrow', + bytecode: '0x60806040', + }, + ObligationEscrowFactory__factory: { + abi: 'ObligationEscrowFactory', + bytecode: '0x60806040', + }, + }, + v5SupportInterfaceIds: { + TradeTrustTokenMintable: '0xTradeTrustTokenMintableIdV5', + TradeTrustTokenRestorable: '0xTradeTrustTokenRestorableIdV5', + TradeTrustTokenBurnable: '0xTradeTrustTokenBurnableIdV5', + SBT: '0xSBTIdV5', + }, + }; +}); + +const assignTxMethod = (hash: string) => + Object.assign( + vi.fn(() => Promise.resolve(hash)), + { + staticCall: vi.fn(() => Promise.resolve(true)), + }, + ); + +export const mockTrustVCTokenContract = { + callStatic: { + burn: vi.fn(), + restore: vi.fn(), + mint: vi.fn(), + }, + mint: assignTxMethod('mint_tx_hash'), + burn: assignTxMethod('burn_tx_hash'), + restore: assignTxMethod('restore_tx_hash'), + ownerOf: vi.fn(() => Promise.resolve(MOCK_OWNER_ADDRESS)), +}; + +export const mockObligationEscrowContract = { + callStatic: { + accept: vi.fn(), + reject: vi.fn(), + discharge: vi.fn(), + transferHolder: vi.fn(), + transferBeneficiary: vi.fn(), + transferOwners: vi.fn(), + nominate: vi.fn(), + rejectTransferHolder: vi.fn(), + rejectTransferBeneficiary: vi.fn(), + rejectTransferOwners: vi.fn(), + returnToIssuer: vi.fn(), + }, + accept: assignTxMethod('accept_tx_hash'), + reject: assignTxMethod('reject_tx_hash'), + discharge: assignTxMethod('discharge_tx_hash'), + transferHolder: assignTxMethod('transfer_holder_tx_hash'), + transferBeneficiary: assignTxMethod('transfer_beneficiary_tx_hash'), + transferOwners: assignTxMethod('transfer_owners_tx_hash'), + nominate: assignTxMethod('nominate_tx_hash'), + rejectTransferHolder: assignTxMethod('reject_transfer_holder_tx_hash'), + rejectTransferBeneficiary: assignTxMethod('reject_transfer_beneficiary_tx_hash'), + rejectTransferOwners: assignTxMethod('reject_transfer_owners_tx_hash'), + returnToIssuer: assignTxMethod('return_to_issuer_tx_hash'), + status: vi.fn(() => Promise.resolve(1)), + isRegistered: vi.fn(() => Promise.resolve(true)), + terminationReason: vi.fn(() => Promise.resolve(0)), +}; + +export const PRIVATE_KEY = '0x59c6995e998f97a5a004497e5f1ebce0c16828d44b3f8d0bfa3a89d271d5b6b9'; + +export const providerV5 = new ethersV5.providers.JsonRpcProvider(); +vi.spyOn(providerV5, 'getNetwork').mockResolvedValue({ + name: 'mainnet', + chainId: 1, +}); + +export const providerV6 = new JsonRpcProviderV6(); diff --git a/src/__tests__/obligation-registry-functions/lifecycle.test.ts b/src/__tests__/obligation-registry-functions/lifecycle.test.ts new file mode 100644 index 0000000..ff31efe --- /dev/null +++ b/src/__tests__/obligation-registry-functions/lifecycle.test.ts @@ -0,0 +1,112 @@ +import './fixtures.js'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { Wallet as WalletV5 } from 'ethers'; +import { Network, Wallet as WalletV6 } from 'ethersV6'; +import { + acceptObligationRegistry, + rejectObligationRegistry, + dischargeObligationRegistry, +} from '../../obligation-registry-functions'; +import { + MOCK_OBLIGATION_ESCROW_ADDRESS, + MOCK_OBLIGATION_REGISTRY_ADDRESS, + mockObligationEscrowContract, + PRIVATE_KEY, + providerV5, + providerV6, +} from './fixtures.js'; +import * as coreModule from '../../core'; +import { CHAIN_ID } from '../../utils'; + +const providers = [ + { Provider: providerV5, ethersVersion: 'v5' as const }, + { Provider: providerV6, ethersVersion: 'v6' as const }, +]; + +describe.each(providers)( + 'obligation lifecycle (ethers $ethersVersion)', + ({ Provider, ethersVersion }) => { + const mockChainId = CHAIN_ID.local; + let wallet: WalletV5 | WalletV6; + + beforeEach(() => { + vi.clearAllMocks(); + if (ethersVersion === 'v5') { + wallet = new WalletV5(PRIVATE_KEY, Provider as any); + vi.spyOn(wallet, 'getChainId').mockResolvedValue(mockChainId as unknown as number); + } else { + wallet = new WalletV6(PRIVATE_KEY, Provider as any); + vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ chainId: mockChainId } as Network); + } + + vi.spyOn(coreModule, 'getObligationEscrowAddress').mockResolvedValue( + MOCK_OBLIGATION_ESCROW_ADDRESS, + ); + mockObligationEscrowContract.callStatic.accept.mockResolvedValue(true); + mockObligationEscrowContract.callStatic.reject.mockResolvedValue(true); + mockObligationEscrowContract.callStatic.discharge.mockResolvedValue(true); + mockObligationEscrowContract.accept.staticCall.mockResolvedValue(true); + mockObligationEscrowContract.reject.staticCall.mockResolvedValue(true); + mockObligationEscrowContract.discharge.staticCall.mockResolvedValue(true); + }); + + it('acceptObligationRegistry with remarks', async () => { + const result = await acceptObligationRegistry( + { obligationRegistryAddress: MOCK_OBLIGATION_REGISTRY_ADDRESS, tokenId: '1' }, + wallet, + { remarks: 'accepted' }, + { chainId: mockChainId, id: 'encryption-id' }, + ); + + expect(result).toEqual('accept_tx_hash'); + expect(coreModule.encrypt).toHaveBeenCalledWith('accepted', 'encryption-id'); + }); + + it('rejectObligationRegistry without remarks', async () => { + const result = await rejectObligationRegistry( + { obligationEscrowAddress: MOCK_OBLIGATION_ESCROW_ADDRESS }, + wallet, + {}, + { chainId: mockChainId }, + ); + + expect(result).toEqual('reject_tx_hash'); + }); + + it('dischargeObligationRegistry without remarks', async () => { + const result = await dischargeObligationRegistry( + { obligationRegistryAddress: MOCK_OBLIGATION_REGISTRY_ADDRESS, tokenId: '1' }, + wallet, + {}, + { chainId: mockChainId }, + ); + + expect(result).toEqual('discharge_tx_hash'); + }); + + it('throws when escrow cannot be resolved', async () => { + await expect( + acceptObligationRegistry( + { obligationRegistryAddress: MOCK_OBLIGATION_REGISTRY_ADDRESS }, + wallet, + {}, + { chainId: mockChainId }, + ), + ).rejects.toThrow('Token ID is required'); + }); + + it('throws when callStatic fails', async () => { + mockObligationEscrowContract.callStatic.accept.mockRejectedValue(new Error('fail')); + mockObligationEscrowContract.accept.staticCall.mockRejectedValue(new Error('fail')); + + await expect( + acceptObligationRegistry( + { obligationEscrowAddress: MOCK_OBLIGATION_ESCROW_ADDRESS }, + wallet, + {}, + { chainId: mockChainId }, + ), + ).rejects.toThrow('Pre-check (callStatic) for accept failed'); + }); + }, +); diff --git a/src/__tests__/obligation-registry-functions/mint.test.ts b/src/__tests__/obligation-registry-functions/mint.test.ts new file mode 100644 index 0000000..e147b85 --- /dev/null +++ b/src/__tests__/obligation-registry-functions/mint.test.ts @@ -0,0 +1,146 @@ +import './fixtures.js'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { Wallet as WalletV5 } from 'ethers'; +import { Network, Wallet as WalletV6 } from 'ethersV6'; +import * as coreModule from '../../core'; +import { mintObligationRegistry } from '../../obligation-registry-functions'; +import { + MOCK_OBLIGATION_REGISTRY_ADDRESS, + mockTrustVCTokenContract, + PRIVATE_KEY, + providerV5, + providerV6, +} from './fixtures.js'; +import { getEthersContractFromProvider } from '../../utils/ethers'; +import { CHAIN_ID } from '../../utils'; + +const providers = [ + { Provider: providerV5, ethersVersion: 'v5' as const }, + { Provider: providerV6, ethersVersion: 'v6' as const }, +]; + +describe.each(providers)( + 'mintObligationRegistry (ethers $ethersVersion)', + ({ Provider, ethersVersion }) => { + const mockChainId = CHAIN_ID.local; + let wallet: WalletV5 | WalletV6; + + beforeEach(() => { + vi.clearAllMocks(); + if (ethersVersion === 'v5') { + wallet = new WalletV5(PRIVATE_KEY, Provider as any); + vi.spyOn(wallet, 'getChainId').mockResolvedValue(mockChainId as unknown as number); + } else { + wallet = new WalletV6(PRIVATE_KEY, Provider as any); + vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ chainId: mockChainId } as Network); + } + + vi.mocked(getEthersContractFromProvider).mockReturnValue( + vi.fn(() => mockTrustVCTokenContract) as any, + ); + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(true); + mockTrustVCTokenContract.callStatic.mint.mockResolvedValue(true); + mockTrustVCTokenContract.mint.staticCall.mockResolvedValue(true); + }); + + it('mints with remarks', async () => { + const result = await mintObligationRegistry( + { obligationRegistryAddress: MOCK_OBLIGATION_REGISTRY_ADDRESS }, + wallet, + { + beneficiaryAddress: '0xBeneficiary', + holderAddress: '0xHolder', + tokenId: '1', + remarks: 'issued', + }, + { chainId: mockChainId, id: 'encryption-id' }, + ); + + expect(result).toEqual('mint_tx_hash'); + expect(coreModule.encrypt).toHaveBeenCalledWith('issued', 'encryption-id'); + }); + + it('mints without remarks', async () => { + const result = await mintObligationRegistry( + { obligationRegistryAddress: MOCK_OBLIGATION_REGISTRY_ADDRESS }, + wallet, + { + beneficiaryAddress: '0xBeneficiary', + holderAddress: '0xHolder', + tokenId: '1', + }, + { chainId: mockChainId }, + ); + + expect(result).toEqual('mint_tx_hash'); + expect(coreModule.encrypt).not.toHaveBeenCalled(); + }); + + it('throws when callStatic fails', async () => { + mockTrustVCTokenContract.callStatic.mint.mockRejectedValue(new Error('fail')); + mockTrustVCTokenContract.mint.staticCall.mockRejectedValue(new Error('fail')); + + await expect( + mintObligationRegistry( + { obligationRegistryAddress: MOCK_OBLIGATION_REGISTRY_ADDRESS }, + wallet, + { + beneficiaryAddress: '0xBeneficiary', + holderAddress: '0xHolder', + tokenId: '1', + }, + { chainId: mockChainId }, + ), + ).rejects.toThrow('Pre-check (callStatic) for mint failed'); + }); + + it('throws when obligation registry address is missing', async () => { + await expect( + mintObligationRegistry( + { obligationRegistryAddress: '' }, + wallet, + { + beneficiaryAddress: '0xBeneficiary', + holderAddress: '0xHolder', + tokenId: '1', + }, + { chainId: mockChainId }, + ), + ).rejects.toThrow('Obligation registry address is required'); + }); + + it('throws when provider is missing', async () => { + const signerWithoutProvider = new WalletV5(PRIVATE_KEY); + + await expect( + mintObligationRegistry( + { obligationRegistryAddress: MOCK_OBLIGATION_REGISTRY_ADDRESS }, + signerWithoutProvider, + { + beneficiaryAddress: '0xBeneficiary', + holderAddress: '0xHolder', + tokenId: '1', + }, + { chainId: mockChainId }, + ), + ).rejects.toThrow('Provider is required'); + }); + + it('throws when registry is unsupported', async () => { + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(false); + + await expect( + mintObligationRegistry( + { obligationRegistryAddress: MOCK_OBLIGATION_REGISTRY_ADDRESS }, + wallet, + { + beneficiaryAddress: '0xBeneficiary', + holderAddress: '0xHolder', + tokenId: '1', + }, + { chainId: mockChainId }, + ), + ).rejects.toThrow('Only TrustVCToken obligation registry is supported'); + }); + }, +); diff --git a/src/__tests__/obligation-registry-functions/ownerOf.test.ts b/src/__tests__/obligation-registry-functions/ownerOf.test.ts new file mode 100644 index 0000000..50c4b60 --- /dev/null +++ b/src/__tests__/obligation-registry-functions/ownerOf.test.ts @@ -0,0 +1,44 @@ +import './fixtures.js'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { Wallet as WalletV5 } from 'ethers'; +import { ownerOfObligationRegistry } from '../../obligation-registry-functions'; +import { + MOCK_OBLIGATION_REGISTRY_ADDRESS, + MOCK_OWNER_ADDRESS, + mockTrustVCTokenContract, + PRIVATE_KEY, + providerV5, +} from './fixtures.js'; +import * as coreModule from '../../core'; +import { getEthersContractFromProvider } from '../../utils/ethers'; + +describe('ownerOfObligationRegistry', () => { + let wallet: WalletV5; + + beforeEach(() => { + vi.clearAllMocks(); + wallet = new WalletV5(PRIVATE_KEY, providerV5 as any); + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(true); + vi.mocked(getEthersContractFromProvider).mockReturnValue( + vi.fn(() => mockTrustVCTokenContract) as any, + ); + mockTrustVCTokenContract.ownerOf.mockResolvedValue(MOCK_OWNER_ADDRESS); + }); + + it('returns owner address', async () => { + const owner = await ownerOfObligationRegistry( + { obligationRegistryAddress: MOCK_OBLIGATION_REGISTRY_ADDRESS }, + wallet, + { tokenId: '1' }, + {}, + ); + + expect(owner).toBe(MOCK_OWNER_ADDRESS); + }); + + it('throws when registry address is missing', async () => { + await expect( + ownerOfObligationRegistry({ obligationRegistryAddress: '' }, wallet, { tokenId: '1' }, {}), + ).rejects.toThrow('Obligation registry address is required'); + }); +}); diff --git a/src/__tests__/obligation-registry-functions/rejectTransfers.test.ts b/src/__tests__/obligation-registry-functions/rejectTransfers.test.ts new file mode 100644 index 0000000..f20acea --- /dev/null +++ b/src/__tests__/obligation-registry-functions/rejectTransfers.test.ts @@ -0,0 +1,87 @@ +import './fixtures.js'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { Wallet as WalletV5 } from 'ethers'; +import { Network, Wallet as WalletV6 } from 'ethersV6'; +import { + rejectTransferHolderObligationRegistry, + rejectTransferBeneficiaryObligationRegistry, + rejectTransferOwnersObligationRegistry, +} from '../../obligation-registry-functions'; +import { + MOCK_OBLIGATION_ESCROW_ADDRESS, + MOCK_OBLIGATION_REGISTRY_ADDRESS, + mockObligationEscrowContract, + PRIVATE_KEY, + providerV5, + providerV6, +} from './fixtures.js'; +import * as coreModule from '../../core'; +import { CHAIN_ID } from '../../utils'; + +const providers = [ + { Provider: providerV5, ethersVersion: 'v5' as const }, + { Provider: providerV6, ethersVersion: 'v6' as const }, +]; + +describe.each(providers)( + 'obligation reject transfers (ethers $ethersVersion)', + ({ Provider, ethersVersion }) => { + const mockChainId = CHAIN_ID.local; + let wallet: WalletV5 | WalletV6; + + beforeEach(() => { + vi.clearAllMocks(); + if (ethersVersion === 'v5') { + wallet = new WalletV5(PRIVATE_KEY, Provider as any); + vi.spyOn(wallet, 'getChainId').mockResolvedValue(mockChainId as unknown as number); + } else { + wallet = new WalletV6(PRIVATE_KEY, Provider as any); + vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ chainId: mockChainId } as Network); + } + + vi.spyOn(coreModule, 'getObligationEscrowAddress').mockResolvedValue( + MOCK_OBLIGATION_ESCROW_ADDRESS, + ); + mockObligationEscrowContract.callStatic.rejectTransferHolder.mockResolvedValue(true); + mockObligationEscrowContract.callStatic.rejectTransferBeneficiary.mockResolvedValue(true); + mockObligationEscrowContract.callStatic.rejectTransferOwners.mockResolvedValue(true); + mockObligationEscrowContract.rejectTransferHolder.staticCall.mockResolvedValue(true); + mockObligationEscrowContract.rejectTransferBeneficiary.staticCall.mockResolvedValue(true); + mockObligationEscrowContract.rejectTransferOwners.staticCall.mockResolvedValue(true); + }); + + it('rejectTransferHolderObligationRegistry with remarks', async () => { + const result = await rejectTransferHolderObligationRegistry( + { obligationEscrowAddress: MOCK_OBLIGATION_ESCROW_ADDRESS }, + wallet, + { remarks: 'reject' }, + { chainId: mockChainId, id: 'encryption-id' }, + ); + + expect(result).toEqual('reject_transfer_holder_tx_hash'); + }); + + it('rejectTransferBeneficiaryObligationRegistry resolves escrow', async () => { + const result = await rejectTransferBeneficiaryObligationRegistry( + { obligationRegistryAddress: MOCK_OBLIGATION_REGISTRY_ADDRESS, tokenId: '1' }, + wallet, + {}, + { chainId: mockChainId }, + ); + + expect(result).toEqual('reject_transfer_beneficiary_tx_hash'); + expect(coreModule.getObligationEscrowAddress).toHaveBeenCalled(); + }); + + it('rejectTransferOwnersObligationRegistry without remarks', async () => { + const result = await rejectTransferOwnersObligationRegistry( + { obligationEscrowAddress: MOCK_OBLIGATION_ESCROW_ADDRESS }, + wallet, + {}, + { chainId: mockChainId }, + ); + + expect(result).toEqual('reject_transfer_owners_tx_hash'); + }); + }, +); diff --git a/src/__tests__/obligation-registry-functions/returnToken.test.ts b/src/__tests__/obligation-registry-functions/returnToken.test.ts new file mode 100644 index 0000000..752c857 --- /dev/null +++ b/src/__tests__/obligation-registry-functions/returnToken.test.ts @@ -0,0 +1,108 @@ +import './fixtures.js'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { Wallet as WalletV5 } from 'ethers'; +import { Network, Wallet as WalletV6 } from 'ethersV6'; +import * as coreModule from '../../core'; +import { + returnToIssuerObligationRegistry, + acceptReturnedObligationRegistry, + rejectReturnedObligationRegistry, +} from '../../obligation-registry-functions'; +import { + MOCK_OBLIGATION_ESCROW_ADDRESS, + MOCK_OBLIGATION_REGISTRY_ADDRESS, + mockObligationEscrowContract, + mockTrustVCTokenContract, + PRIVATE_KEY, + providerV5, + providerV6, +} from './fixtures.js'; +import { getEthersContractFromProvider } from '../../utils/ethers'; +import { CHAIN_ID } from '../../utils'; + +const providers = [ + { Provider: providerV5, ethersVersion: 'v5' as const }, + { Provider: providerV6, ethersVersion: 'v6' as const }, +]; + +describe.each(providers)( + 'obligation return token (ethers $ethersVersion)', + ({ Provider, ethersVersion }) => { + const mockChainId = CHAIN_ID.local; + let wallet: WalletV5 | WalletV6; + + beforeEach(() => { + vi.clearAllMocks(); + if (ethersVersion === 'v5') { + wallet = new WalletV5(PRIVATE_KEY, Provider as any); + vi.spyOn(wallet, 'getChainId').mockResolvedValue(mockChainId as unknown as number); + } else { + wallet = new WalletV6(PRIVATE_KEY, Provider as any); + vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ chainId: mockChainId } as Network); + } + + vi.spyOn(coreModule, 'getObligationEscrowAddress').mockResolvedValue( + MOCK_OBLIGATION_ESCROW_ADDRESS, + ); + vi.spyOn(coreModule, 'checkSupportsInterface').mockResolvedValue(true); + mockObligationEscrowContract.callStatic.returnToIssuer.mockResolvedValue(true); + mockObligationEscrowContract.returnToIssuer.staticCall.mockResolvedValue(true); + mockTrustVCTokenContract.callStatic.burn.mockResolvedValue(true); + mockTrustVCTokenContract.callStatic.restore.mockResolvedValue(true); + mockTrustVCTokenContract.burn.staticCall.mockResolvedValue(true); + mockTrustVCTokenContract.restore.staticCall.mockResolvedValue(true); + + vi.mocked(getEthersContractFromProvider).mockReturnValue( + vi.fn((address: string) => + address === MOCK_OBLIGATION_REGISTRY_ADDRESS + ? mockTrustVCTokenContract + : mockObligationEscrowContract, + ) as any, + ); + }); + + it('returnToIssuerObligationRegistry with remarks', async () => { + const result = await returnToIssuerObligationRegistry( + { obligationEscrowAddress: MOCK_OBLIGATION_ESCROW_ADDRESS }, + wallet, + { remarks: 'return' }, + { chainId: mockChainId, id: 'encryption-id' }, + ); + + expect(result).toEqual('return_to_issuer_tx_hash'); + }); + + it('acceptReturnedObligationRegistry burns token', async () => { + const result = await acceptReturnedObligationRegistry( + { obligationRegistryAddress: MOCK_OBLIGATION_REGISTRY_ADDRESS }, + wallet, + { tokenId: '1', remarks: 'burn' }, + { chainId: mockChainId, id: 'encryption-id' }, + ); + + expect(result).toEqual('burn_tx_hash'); + }); + + it('rejectReturnedObligationRegistry restores token', async () => { + const result = await rejectReturnedObligationRegistry( + { obligationRegistryAddress: MOCK_OBLIGATION_REGISTRY_ADDRESS }, + wallet, + { tokenId: '1' }, + { chainId: mockChainId }, + ); + + expect(result).toEqual('restore_tx_hash'); + }); + + it('throws when registry address is missing for acceptReturned', async () => { + await expect( + acceptReturnedObligationRegistry( + { obligationRegistryAddress: '' }, + wallet, + { tokenId: '1' }, + { chainId: mockChainId }, + ), + ).rejects.toThrow('Obligation registry address is required'); + }); + }, +); diff --git a/src/__tests__/obligation-registry-functions/status.test.ts b/src/__tests__/obligation-registry-functions/status.test.ts new file mode 100644 index 0000000..f34e22c --- /dev/null +++ b/src/__tests__/obligation-registry-functions/status.test.ts @@ -0,0 +1,93 @@ +import './fixtures.js'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { Wallet as WalletV5 } from 'ethers'; +import { Network, Wallet as WalletV6 } from 'ethersV6'; +import { + getObligationRegistryStatus, + isObligationRegistryRegistered, + getObligationEscrowTerminationReason, + ObligationDocumentStatus, + ObligationEscrowTerminationReason, +} from '../../obligation-registry-functions'; +import { + MOCK_OBLIGATION_ESCROW_ADDRESS, + MOCK_OBLIGATION_REGISTRY_ADDRESS, + mockObligationEscrowContract, + PRIVATE_KEY, + providerV5, + providerV6, +} from './fixtures.js'; +import * as coreModule from '../../core'; + +const providers = [ + { Provider: providerV5, ethersVersion: 'v5' as const }, + { Provider: providerV6, ethersVersion: 'v6' as const }, +]; + +describe.each(providers)( + 'obligation status reads (ethers $ethersVersion)', + ({ Provider, ethersVersion }) => { + let wallet: WalletV5 | WalletV6; + + beforeEach(() => { + vi.clearAllMocks(); + if (ethersVersion === 'v5') { + wallet = new WalletV5(PRIVATE_KEY, Provider as any); + } else { + wallet = new WalletV6(PRIVATE_KEY, Provider as any); + vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ chainId: 1 } as Network); + } + + vi.spyOn(coreModule, 'getObligationEscrowAddress').mockResolvedValue( + MOCK_OBLIGATION_ESCROW_ADDRESS, + ); + mockObligationEscrowContract.status.mockResolvedValue(ObligationDocumentStatus.Accepted); + mockObligationEscrowContract.isRegistered.mockResolvedValue(true); + mockObligationEscrowContract.terminationReason.mockResolvedValue( + ObligationEscrowTerminationReason.None, + ); + }); + + it('getObligationRegistryStatus returns enum value', async () => { + const status = await getObligationRegistryStatus( + { obligationRegistryAddress: MOCK_OBLIGATION_REGISTRY_ADDRESS, tokenId: '1' }, + wallet, + { tokenId: '1' }, + ); + + expect(status).toBe(ObligationDocumentStatus.Accepted); + }); + + it('isObligationRegistryRegistered returns boolean', async () => { + const registered = await isObligationRegistryRegistered( + { obligationEscrowAddress: MOCK_OBLIGATION_ESCROW_ADDRESS }, + wallet, + { tokenId: '1' }, + ); + + expect(registered).toBe(true); + }); + + it('getObligationEscrowTerminationReason returns enum value', async () => { + const reason = await getObligationEscrowTerminationReason( + { obligationRegistryAddress: MOCK_OBLIGATION_REGISTRY_ADDRESS, tokenId: '1' }, + wallet, + { tokenId: '1' }, + ); + + expect(reason).toBe(ObligationEscrowTerminationReason.None); + }); + + it('throws when provider is missing', async () => { + const signerWithoutProvider = new WalletV5(PRIVATE_KEY); + + await expect( + getObligationRegistryStatus( + { obligationEscrowAddress: MOCK_OBLIGATION_ESCROW_ADDRESS }, + signerWithoutProvider, + { tokenId: '1' }, + ), + ).rejects.toThrow('Provider is required'); + }); + }, +); diff --git a/src/__tests__/obligation-registry-functions/transfers.test.ts b/src/__tests__/obligation-registry-functions/transfers.test.ts new file mode 100644 index 0000000..ab12132 --- /dev/null +++ b/src/__tests__/obligation-registry-functions/transfers.test.ts @@ -0,0 +1,110 @@ +import './fixtures.js'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { Wallet as WalletV5 } from 'ethers'; +import { Network, Wallet as WalletV6 } from 'ethersV6'; +import { + nominateObligationRegistry, + transferHolderObligationRegistry, + transferBeneficiaryObligationRegistry, + transferOwnersObligationRegistry, +} from '../../obligation-registry-functions'; +import { + MOCK_OBLIGATION_ESCROW_ADDRESS, + MOCK_OBLIGATION_REGISTRY_ADDRESS, + mockObligationEscrowContract, + PRIVATE_KEY, + providerV5, + providerV6, +} from './fixtures.js'; +import * as coreModule from '../../core'; +import { CHAIN_ID } from '../../utils'; + +const providers = [ + { Provider: providerV5, ethersVersion: 'v5' as const }, + { Provider: providerV6, ethersVersion: 'v6' as const }, +]; + +describe.each(providers)( + 'obligation transfers (ethers $ethersVersion)', + ({ Provider, ethersVersion }) => { + const mockChainId = CHAIN_ID.local; + let wallet: WalletV5 | WalletV6; + + beforeEach(() => { + vi.clearAllMocks(); + if (ethersVersion === 'v5') { + wallet = new WalletV5(PRIVATE_KEY, Provider as any); + vi.spyOn(wallet, 'getChainId').mockResolvedValue(mockChainId as unknown as number); + } else { + wallet = new WalletV6(PRIVATE_KEY, Provider as any); + vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ chainId: mockChainId } as Network); + } + + vi.spyOn(coreModule, 'getObligationEscrowAddress').mockResolvedValue( + MOCK_OBLIGATION_ESCROW_ADDRESS, + ); + Object.values(mockObligationEscrowContract.callStatic).forEach((fn) => + (fn as ReturnType).mockResolvedValue(true), + ); + ['nominate', 'transferHolder', 'transferBeneficiary', 'transferOwners'].forEach((method) => { + (mockObligationEscrowContract as any)[method].staticCall.mockResolvedValue(true); + }); + }); + + it('nominateObligationRegistry with remarks', async () => { + const result = await nominateObligationRegistry( + { obligationEscrowAddress: MOCK_OBLIGATION_ESCROW_ADDRESS }, + wallet, + { newBeneficiaryAddress: '0xNewBeneficiary', remarks: 'nominate' }, + { chainId: mockChainId, id: 'encryption-id' }, + ); + + expect(result).toEqual('nominate_tx_hash'); + }); + + it('transferHolderObligationRegistry resolves escrow from registry + tokenId', async () => { + const result = await transferHolderObligationRegistry( + { obligationRegistryAddress: MOCK_OBLIGATION_REGISTRY_ADDRESS, tokenId: '1' }, + wallet, + { holderAddress: '0xNewHolder' }, + { chainId: mockChainId }, + ); + + expect(result).toEqual('transfer_holder_tx_hash'); + expect(coreModule.getObligationEscrowAddress).toHaveBeenCalled(); + }); + + it('transferBeneficiaryObligationRegistry without remarks', async () => { + const result = await transferBeneficiaryObligationRegistry( + { obligationEscrowAddress: MOCK_OBLIGATION_ESCROW_ADDRESS }, + wallet, + { newBeneficiaryAddress: '0xNewBeneficiary' }, + { chainId: mockChainId }, + ); + + expect(result).toEqual('transfer_beneficiary_tx_hash'); + }); + + it('transferOwnersObligationRegistry without remarks', async () => { + const result = await transferOwnersObligationRegistry( + { obligationEscrowAddress: MOCK_OBLIGATION_ESCROW_ADDRESS }, + wallet, + { newHolderAddress: '0xNewHolder', newBeneficiaryAddress: '0xNewBeneficiary' }, + { chainId: mockChainId }, + ); + + expect(result).toEqual('transfer_owners_tx_hash'); + }); + + it('throws when registry and tokenId are missing', async () => { + await expect( + transferHolderObligationRegistry( + { obligationRegistryAddress: MOCK_OBLIGATION_REGISTRY_ADDRESS }, + wallet, + { holderAddress: '0xNewHolder' }, + { chainId: mockChainId }, + ), + ).rejects.toThrow('Token ID is required'); + }); + }, +); diff --git a/src/core/documentBuilder.ts b/src/core/documentBuilder.ts index db7b5ff..770b49c 100644 --- a/src/core/documentBuilder.ts +++ b/src/core/documentBuilder.ts @@ -1,6 +1,10 @@ import { PrivateKeyPair } from '@trustvc/w3c-issuer'; import { deriveW3C, signW3C, verifyW3CSignature } from '../w3c'; -import { assertCredentialStatus, assertTransferableRecords } from '@trustvc/w3c-credential-status'; +import { + assertCredentialStatus, + assertObligationRecords, + assertTransferableRecords, +} from '@trustvc/w3c-credential-status'; import { CredentialStatus, CryptoSuiteName, @@ -51,6 +55,20 @@ export interface W3CTransferableRecordsConfig { rpcProviderUrl: string; } +/** + * Configuration for W3C Obligation Records (Bill of Exchange), including blockchain details and obligation registry information. + * @property {string} chain - The name of the blockchain network (e.g., "Ethereum", "Polygon"). + * @property {number} chainId - The unique identifier of the blockchain network. + * @property {string} obligationRegistry - The smart contract address of the obligation registry (TrustVCToken). + * @property {string} rpcProviderUrl - The RPC endpoint URL for interacting with the blockchain. + */ +export interface W3CObligationRecordsConfig { + chain: string; + chainId: number; + obligationRegistry: string; + rpcProviderUrl: string; +} + /** * Configuration for the rendering method used in a Verifiable Credential document. * @property {string} id - A unique identifier for the rendering method, typically a URL or URI. @@ -88,7 +106,11 @@ export interface SignOptions { export class DocumentBuilder { private document: Partial; // Holds the document to be built and signed. private documentType: string = 'w3c'; // Default to W3C - private selectedStatusType: 'transferableRecords' | 'verifiableDocument' | null = null; // Tracks selected status type. + private selectedStatusType: + | 'transferableRecords' + | 'obligationRecords' + | 'verifiableDocument' + | null = null; // Tracks selected status type. private statusConfig: Partial = {}; // Configuration for the credential status. private rpcProviderUrl: string; // Holds the RPC provider URL for verifying token registry. private requiredFields: string[] = ['credentialSubject']; // Required fields that must be present in the document. @@ -148,6 +170,28 @@ export class DocumentBuilder { return this; } + // Configures the credential status for obligation records (Bill of Exchange). + obligationCredentialStatus(config: W3CObligationRecordsConfig) { + if (this.isSigned) throw new Error('Configuration Error: Document is already signed.'); + + if (!this.isObligationRecordsConfig(config)) { + throw new Error( + 'Configuration Error: Missing required fields for obligation credential status.', + ); + } + + this.selectedStatusType = 'obligationRecords'; + this.statusConfig = { + type: 'TransferableRecords', + tokenNetwork: { chain: config.chain, chainId: config.chainId }, + obligationRegistry: config.obligationRegistry, + }; + this.rpcProviderUrl = config.rpcProviderUrl; + this.addContext(TR_CONTEXT_URL); + + return this; + } + // Sets the expiration date of the document. expirationDate(date: string | Date) { if (this.isSigned) throw new Error('Configuration Error: Document is already signed.'); @@ -203,6 +247,9 @@ export class DocumentBuilder { } else if (this.selectedStatusType === 'transferableRecords') { assertTransferableRecords(this.document.credentialStatus, 'sign'); await this.verifyTokenRegistry(); // Verify that the token registry supports the required interface. + } else if (this.selectedStatusType === 'obligationRecords') { + assertObligationRecords(this.document.credentialStatus, 'sign'); + await this.verifyObligationRegistry(); } this.document.issuer = this.document.issuer || privateKey.id.split('#')[0]; // Set the issuer of the document. @@ -271,6 +318,18 @@ export class DocumentBuilder { return config && typeof config.url === 'string' && typeof config.index === 'number'; } + private isObligationRecordsConfig( + config: Partial, + ): config is W3CObligationRecordsConfig { + return ( + config && + typeof config.obligationRegistry === 'string' && + typeof config.chain === 'string' && + typeof config.chainId === 'number' && + typeof config.rpcProviderUrl === 'string' + ); + } + // Private helper method to validate that the required fields are present in the input document. private validateRequiredFields(input: Partial) { this.requiredFields.forEach((field) => { @@ -340,15 +399,48 @@ export class DocumentBuilder { } } + private async verifyObligationRegistry() { + const chainId = this.document.credentialStatus.tokenNetwork + .chainId as keyof typeof SUPPORTED_CHAINS; + if (!(chainId in SUPPORTED_CHAINS)) { + throw new Error(`Unsupported Chain: Chain ID ${chainId} is not supported.`); + } + + try { + const provider = new ethers.providers.JsonRpcProvider(this.rpcProviderUrl); + const isSupported = await this.supportsInterface( + v5Contracts.TrustVCToken__factory, + constantsV5.contractInterfaceId.TradeTrustTokenMintable, + provider, + this.statusConfig.obligationRegistry, + ); + if (!isSupported) { + throw new Error('Obligation registry version is not supported.'); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + if (error.message === 'Obligation registry version is not supported.') { + throw error; + } else { + throw new Error( + `Network Error: Unable to verify obligation registry. Please check the RPC URL or obligation registry address.`, + ); + } + } + } + // Private helper method to check if a contract supports a specific interface ID. private async supportsInterface( contractFactory: | typeof v4Contracts.TradeTrustToken__factory - | typeof v5Contracts.TradeTrustToken__factory, + | typeof v5Contracts.TradeTrustToken__factory + | typeof v5Contracts.TrustVCToken__factory, interfaceId: string, provider: ethers.providers.JsonRpcProvider, + contractAddress?: string, ) { - const contract = contractFactory.connect(this.statusConfig.tokenRegistry, provider); + const address = contractAddress ?? this.statusConfig.tokenRegistry; + const contract = contractFactory.connect(address, provider); return contract.supportsInterface(interfaceId); } } diff --git a/src/core/endorsement-chain/index.ts b/src/core/endorsement-chain/index.ts index 8c2457c..c362bb6 100644 --- a/src/core/endorsement-chain/index.ts +++ b/src/core/endorsement-chain/index.ts @@ -1,6 +1,7 @@ export * from './fetchEscrowTransfer'; export * from './fetchTokenTransfer'; export * from './helpers'; +export * from './obligation'; export * from './retrieveEndorsementChain'; export * from './types'; export * from './useEndorsementChain'; diff --git a/src/core/endorsement-chain/obligation.ts b/src/core/endorsement-chain/obligation.ts new file mode 100644 index 0000000..4cc378e --- /dev/null +++ b/src/core/endorsement-chain/obligation.ts @@ -0,0 +1,33 @@ +import { Provider } from '@ethersproject/abstract-provider'; +import { ethers as ethersV6 } from 'ethersV6'; +import { EndorsementChain } from './types'; +import { fetchEndorsementChain, getTitleEscrowAddress } from './useEndorsementChain'; + +export const getObligationEscrowAddress = async ( + obligationRegistryAddress: string, + tokenId: string, + provider: Provider | ethersV6.Provider, + options?: { + titleEscrowVersion?: 'v4' | 'v5'; + }, +): Promise => { + return getTitleEscrowAddress(obligationRegistryAddress, tokenId, provider, options); +}; + +export const fetchObligationEndorsementChain = async ( + obligationRegistryAddress: string, + tokenId: string, + provider: Provider | ethersV6.Provider, + options?: { + encryptionId?: string; + obligationEscrowAddress?: string; + }, +): Promise => { + return fetchEndorsementChain( + obligationRegistryAddress, + tokenId, + provider, + options?.encryptionId, + options?.obligationEscrowAddress, + ); +}; diff --git a/src/obligation-registry-functions/deploy.ts b/src/obligation-registry-functions/deploy.ts new file mode 100644 index 0000000..d195b5b --- /dev/null +++ b/src/obligation-registry-functions/deploy.ts @@ -0,0 +1,172 @@ +import { v5Contracts } from '../token-registry-v5'; +import { getTxOptions } from '../token-registry-functions/utils'; +import { getChainIdSafe } from '../token-registry-functions/utils'; +import { CHAIN_ID } from '../utils'; +import { getEthersContractFactoryFromProvider, isV6EthersProvider } from '../utils/ethers'; +import { + ContractFactory as ContractFactoryV6, + ContractTransactionReceipt as ContractReceiptV6, + Signer as SignerV6, +} from 'ethersV6'; +import { + ContractFactory as ContractFactoryV5, + ContractReceipt as ContractReceiptV5, + Signer as SignerV5, +} from 'ethers'; + +export type ObligationDeployOptions = { + chainId?: CHAIN_ID; + maxFeePerGas?: Parameters[2]; + maxPriorityFeePerGas?: Parameters[3]; +}; + +export type DeployObligationEscrowFactoryResult = { + obligationEscrowFactoryAddress: string; + receipt: ContractReceiptV5 | ContractReceiptV6; +}; + +export type DeployObligationRegistryOptions = ObligationDeployOptions & { + escrowFactoryAddress?: string; +}; + +export type DeployObligationRegistryResult = { + obligationRegistry: string; + obligationEscrowFactoryAddress: string; + receipt: ContractReceiptV5 | ContractReceiptV6; +}; + +const getDeployedAddress = ( + receipt: ContractReceiptV5 | ContractReceiptV6, + fallbackAddress?: string, +): string => { + if (fallbackAddress) { + return fallbackAddress; + } + + const contractAddress = + 'contractAddress' in receipt && receipt.contractAddress ? receipt.contractAddress : undefined; + + if (!contractAddress) { + throw new Error('Unable to resolve deployed contract address from receipt'); + } + + return contractAddress; +}; + +export const deployObligationEscrowFactory = async ( + signer: SignerV5 | SignerV6, + options: ObligationDeployOptions = {}, +): Promise => { + if (!signer.provider) { + throw new Error('Provider is required'); + } + + const chainId = options.chainId ?? ((await getChainIdSafe(signer)) as unknown as CHAIN_ID); + const txOptions = await getTxOptions( + signer, + chainId, + options.maxFeePerGas, + options.maxPriorityFeePerGas, + ); + + const ContractFactory = getEthersContractFactoryFromProvider(signer.provider); + const factory = new ContractFactory( + v5Contracts.ObligationEscrowFactory__factory.abi, + v5Contracts.ObligationEscrowFactory__factory.bytecode, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); + + if (isV6EthersProvider(signer.provider)) { + const contract = await (factory as ContractFactoryV6).deploy(txOptions); + const receipt = await contract.deploymentTransaction()?.wait(); + if (!receipt) { + throw new Error('Failed to deploy ObligationEscrowFactory'); + } + + return { + obligationEscrowFactoryAddress: await contract.getAddress(), + receipt, + }; + } + + const contract = await (factory as ContractFactoryV5).deploy(txOptions); + const receipt = await contract.deployTransaction.wait(); + if (!receipt) { + throw new Error('Failed to deploy ObligationEscrowFactory'); + } + + return { + obligationEscrowFactoryAddress: contract.address, + receipt, + }; +}; + +export const deployObligationRegistry = async ( + registryName: string, + registrySymbol: string, + signer: SignerV5 | SignerV6, + options: DeployObligationRegistryOptions = {}, +): Promise => { + if (!signer.provider) { + throw new Error('Provider is required'); + } + + let obligationEscrowFactoryAddress = options.escrowFactoryAddress; + if (!obligationEscrowFactoryAddress) { + const deployedFactory = await deployObligationEscrowFactory(signer, options); + obligationEscrowFactoryAddress = deployedFactory.obligationEscrowFactoryAddress; + } + + const chainId = options.chainId ?? ((await getChainIdSafe(signer)) as unknown as CHAIN_ID); + const txOptions = await getTxOptions( + signer, + chainId, + options.maxFeePerGas, + options.maxPriorityFeePerGas, + ); + + const ContractFactory = getEthersContractFactoryFromProvider(signer.provider); + const factory = new ContractFactory( + v5Contracts.TrustVCToken__factory.abi, + v5Contracts.TrustVCToken__factory.bytecode, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); + + if (isV6EthersProvider(signer.provider)) { + const contract = await (factory as ContractFactoryV6).deploy( + registryName, + registrySymbol, + obligationEscrowFactoryAddress, + txOptions, + ); + const receipt = await contract.deploymentTransaction()?.wait(); + if (!receipt) { + throw new Error('Failed to deploy TrustVCToken obligation registry'); + } + + return { + obligationRegistry: await contract.getAddress(), + obligationEscrowFactoryAddress, + receipt, + }; + } + + const contract = await (factory as ContractFactoryV5).deploy( + registryName, + registrySymbol, + obligationEscrowFactoryAddress, + txOptions, + ); + const receipt = await contract.deployTransaction.wait(); + if (!receipt) { + throw new Error('Failed to deploy TrustVCToken obligation registry'); + } + + return { + obligationRegistry: getDeployedAddress(receipt, contract.address), + obligationEscrowFactoryAddress, + receipt, + }; +}; diff --git a/src/obligation-registry-functions/index.ts b/src/obligation-registry-functions/index.ts new file mode 100644 index 0000000..bd88895 --- /dev/null +++ b/src/obligation-registry-functions/index.ts @@ -0,0 +1,10 @@ +export * from './types'; +export * from './utils'; +export * from './deploy'; +export * from './mint'; +export * from './ownerOf'; +export * from './transfer'; +export * from './rejectTransfers'; +export * from './returnToken'; +export * from './lifecycle'; +export * from './status'; diff --git a/src/obligation-registry-functions/lifecycle.ts b/src/obligation-registry-functions/lifecycle.ts new file mode 100644 index 0000000..bf84d5c --- /dev/null +++ b/src/obligation-registry-functions/lifecycle.ts @@ -0,0 +1,66 @@ +import { Signer as SignerV6 } from 'ethersV6'; +import { ContractTransaction, Signer } from 'ethers'; +import { + getEncryptedRemarks, + getObligationEscrowContract, + resolveObligationEscrowAddress, + runStaticCall, + sendTransaction, +} from './utils'; +import { ObligationContractOptions, ObligationRemarkParams, TransactionOptions } from './types'; + +const acceptObligationRegistry = async ( + contractOptions: ObligationContractOptions, + signer: Signer | SignerV6, + params: ObligationRemarkParams, + options: TransactionOptions, +): Promise => { + if (!signer.provider) throw new Error('Provider is required'); + + const obligationEscrowAddress = await resolveObligationEscrowAddress(contractOptions, signer); + const obligationEscrowContract = getObligationEscrowContract(obligationEscrowAddress, signer); + const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); + + await runStaticCall(obligationEscrowContract, 'accept', [encryptedRemarks], signer.provider); + return sendTransaction(obligationEscrowContract, 'accept', [encryptedRemarks], signer, options); +}; + +const rejectObligationRegistry = async ( + contractOptions: ObligationContractOptions, + signer: Signer | SignerV6, + params: ObligationRemarkParams, + options: TransactionOptions, +): Promise => { + if (!signer.provider) throw new Error('Provider is required'); + + const obligationEscrowAddress = await resolveObligationEscrowAddress(contractOptions, signer); + const obligationEscrowContract = getObligationEscrowContract(obligationEscrowAddress, signer); + const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); + + await runStaticCall(obligationEscrowContract, 'reject', [encryptedRemarks], signer.provider); + return sendTransaction(obligationEscrowContract, 'reject', [encryptedRemarks], signer, options); +}; + +const dischargeObligationRegistry = async ( + contractOptions: ObligationContractOptions, + signer: Signer | SignerV6, + params: ObligationRemarkParams, + options: TransactionOptions, +): Promise => { + if (!signer.provider) throw new Error('Provider is required'); + + const obligationEscrowAddress = await resolveObligationEscrowAddress(contractOptions, signer); + const obligationEscrowContract = getObligationEscrowContract(obligationEscrowAddress, signer); + const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); + + await runStaticCall(obligationEscrowContract, 'discharge', [encryptedRemarks], signer.provider); + return sendTransaction( + obligationEscrowContract, + 'discharge', + [encryptedRemarks], + signer, + options, + ); +}; + +export { acceptObligationRegistry, rejectObligationRegistry, dischargeObligationRegistry }; diff --git a/src/obligation-registry-functions/mint.ts b/src/obligation-registry-functions/mint.ts new file mode 100644 index 0000000..5392fda --- /dev/null +++ b/src/obligation-registry-functions/mint.ts @@ -0,0 +1,64 @@ +import { checkSupportsInterface, encrypt } from '../core'; +import { v5Contracts, v5SupportInterfaceIds } from '../token-registry-v5'; +import { Signer as SignerV6, Contract as ContractV6 } from 'ethersV6'; +import { Contract as ContractV5, ContractTransaction, Signer } from 'ethers'; +import { getEthersContractFromProvider, isV6EthersProvider } from '../utils/ethers'; +import { getTxOptions } from './utils'; +import { MintObligationTokenOptions, MintObligationTokenParams, TransactionOptions } from './types'; + +const mintObligationRegistry = async ( + contractOptions: MintObligationTokenOptions, + signer: Signer | SignerV6, + params: MintObligationTokenParams, + options: TransactionOptions, +): Promise => { + const { obligationRegistryAddress } = contractOptions; + const { chainId, maxFeePerGas, maxPriorityFeePerGas } = options; + + if (!obligationRegistryAddress) throw new Error('Obligation registry address is required'); + if (!signer.provider) throw new Error('Provider is required'); + + const isSupported = await checkSupportsInterface( + obligationRegistryAddress, + v5SupportInterfaceIds.TradeTrustTokenMintable, + signer.provider, + ); + if (!isSupported) { + throw new Error('Only TrustVCToken obligation registry is supported'); + } + + const { beneficiaryAddress, holderAddress, tokenId, remarks } = params; + const Contract = getEthersContractFromProvider(signer.provider); + const obligationRegistryContract = new Contract( + obligationRegistryAddress, + v5Contracts.TrustVCToken__factory.abi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ) as ContractV5 | ContractV6; + + const encryptedRemarks = remarks ? `0x${encrypt(remarks, options.id ?? '')}` : '0x'; + + try { + const isV6 = isV6EthersProvider(signer.provider); + const args = [beneficiaryAddress, holderAddress, tokenId, encryptedRemarks]; + if (isV6) { + await (obligationRegistryContract as ContractV6).mint.staticCall(...args); + } else { + await (obligationRegistryContract as ContractV5).callStatic.mint(...args); + } + } catch (error) { + console.error('callStatic failed:', error); + throw new Error('Pre-check (callStatic) for mint failed'); + } + + const txOptions = await getTxOptions(signer, chainId, maxFeePerGas, maxPriorityFeePerGas); + return obligationRegistryContract.mint( + beneficiaryAddress, + holderAddress, + tokenId, + encryptedRemarks, + txOptions, + ); +}; + +export { mintObligationRegistry }; diff --git a/src/obligation-registry-functions/ownerOf.ts b/src/obligation-registry-functions/ownerOf.ts new file mode 100644 index 0000000..272965e --- /dev/null +++ b/src/obligation-registry-functions/ownerOf.ts @@ -0,0 +1,46 @@ +import { checkSupportsInterface } from '../core'; +import { v5Contracts, v5SupportInterfaceIds } from '../token-registry-v5'; +import { Signer as SignerV6, Contract as ContractV6 } from 'ethersV6'; +import { Contract as ContractV5, Signer } from 'ethers'; +import { getEthersContractFromProvider } from '../utils/ethers'; +import { + OwnerOfObligationTokenOptions, + OwnerOfObligationTokenParams, + TransactionOptions, +} from './types'; + +const ownerOfObligationRegistry = async ( + contractOptions: OwnerOfObligationTokenOptions, + signer: Signer | SignerV6, + params: OwnerOfObligationTokenParams, + // Kept for API parity with other obligation-registry functions. + options: TransactionOptions, +): Promise => { + void options; + const { obligationRegistryAddress } = contractOptions; + const { tokenId } = params; + + if (!obligationRegistryAddress) throw new Error('Obligation registry address is required'); + if (!signer.provider) throw new Error('Provider is required'); + + const isSupported = await checkSupportsInterface( + obligationRegistryAddress, + v5SupportInterfaceIds.SBT, + signer.provider, + ); + if (!isSupported) { + throw new Error('Only TrustVCToken obligation registry is supported'); + } + + const Contract = getEthersContractFromProvider(signer.provider); + const obligationRegistryContract = new Contract( + obligationRegistryAddress, + v5Contracts.TrustVCToken__factory.abi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ) as ContractV5 | ContractV6; + + return obligationRegistryContract.ownerOf(tokenId); +}; + +export { ownerOfObligationRegistry }; diff --git a/src/obligation-registry-functions/rejectTransfers.ts b/src/obligation-registry-functions/rejectTransfers.ts new file mode 100644 index 0000000..bd7d35b --- /dev/null +++ b/src/obligation-registry-functions/rejectTransfers.ts @@ -0,0 +1,73 @@ +import { Signer as SignerV6 } from 'ethersV6'; +import { ContractTransaction, Signer } from 'ethers'; +import { + getEncryptedRemarks, + getObligationEscrowContract, + resolveObligationEscrowAddress, + runStaticCall, + sendTransaction, +} from './utils'; +import { ObligationContractOptions, ObligationRemarkParams, TransactionOptions } from './types'; + +const rejectTransferHolderObligationRegistry = async ( + contractOptions: ObligationContractOptions, + signer: Signer | SignerV6, + params: ObligationRemarkParams, + options: TransactionOptions, +): Promise => { + if (!signer.provider) throw new Error('Provider is required'); + + const obligationEscrowAddress = await resolveObligationEscrowAddress(contractOptions, signer); + const obligationEscrowContract = getObligationEscrowContract(obligationEscrowAddress, signer); + const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); + const args = [encryptedRemarks]; + + await runStaticCall(obligationEscrowContract, 'rejectTransferHolder', args, signer.provider); + return sendTransaction(obligationEscrowContract, 'rejectTransferHolder', args, signer, options); +}; + +const rejectTransferBeneficiaryObligationRegistry = async ( + contractOptions: ObligationContractOptions, + signer: Signer | SignerV6, + params: ObligationRemarkParams, + options: TransactionOptions, +): Promise => { + if (!signer.provider) throw new Error('Provider is required'); + + const obligationEscrowAddress = await resolveObligationEscrowAddress(contractOptions, signer); + const obligationEscrowContract = getObligationEscrowContract(obligationEscrowAddress, signer); + const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); + const args = [encryptedRemarks]; + + await runStaticCall(obligationEscrowContract, 'rejectTransferBeneficiary', args, signer.provider); + return sendTransaction( + obligationEscrowContract, + 'rejectTransferBeneficiary', + args, + signer, + options, + ); +}; + +const rejectTransferOwnersObligationRegistry = async ( + contractOptions: ObligationContractOptions, + signer: Signer | SignerV6, + params: ObligationRemarkParams, + options: TransactionOptions, +): Promise => { + if (!signer.provider) throw new Error('Provider is required'); + + const obligationEscrowAddress = await resolveObligationEscrowAddress(contractOptions, signer); + const obligationEscrowContract = getObligationEscrowContract(obligationEscrowAddress, signer); + const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); + const args = [encryptedRemarks]; + + await runStaticCall(obligationEscrowContract, 'rejectTransferOwners', args, signer.provider); + return sendTransaction(obligationEscrowContract, 'rejectTransferOwners', args, signer, options); +}; + +export { + rejectTransferHolderObligationRegistry, + rejectTransferBeneficiaryObligationRegistry, + rejectTransferOwnersObligationRegistry, +}; diff --git a/src/obligation-registry-functions/returnToken.ts b/src/obligation-registry-functions/returnToken.ts new file mode 100644 index 0000000..fe7f4b0 --- /dev/null +++ b/src/obligation-registry-functions/returnToken.ts @@ -0,0 +1,136 @@ +import { checkSupportsInterface, encrypt } from '../core'; +import { v5SupportInterfaceIds } from '../token-registry-v5'; +import { Signer as SignerV6, Contract as ContractV6 } from 'ethersV6'; +import { Contract as ContractV5, ContractTransaction, Signer } from 'ethers'; +import { isV6EthersProvider } from '../utils/ethers'; +import { + AcceptReturnedObligationOptions, + AcceptReturnedObligationParams, + ObligationContractOptions, + ObligationRemarkParams, + RejectReturnedObligationOptions, + RejectReturnedObligationParams, + TransactionOptions, +} from './types'; +import { + getEncryptedRemarks, + getObligationEscrowContract, + getObligationRegistryContract, + getTxOptions, + resolveObligationEscrowAddress, + runStaticCall, + sendTransaction, +} from './utils'; + +const returnToIssuerObligationRegistry = async ( + contractOptions: ObligationContractOptions, + signer: Signer | SignerV6, + params: ObligationRemarkParams, + options: TransactionOptions, +): Promise => { + if (!signer.provider) throw new Error('Provider is required'); + + const obligationEscrowAddress = await resolveObligationEscrowAddress(contractOptions, signer); + const obligationEscrowContract = getObligationEscrowContract(obligationEscrowAddress, signer); + const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); + const args = [encryptedRemarks]; + + await runStaticCall(obligationEscrowContract, 'returnToIssuer', args, signer.provider); + return sendTransaction(obligationEscrowContract, 'returnToIssuer', args, signer, options); +}; + +const acceptReturnedObligationRegistry = async ( + contractOptions: AcceptReturnedObligationOptions, + signer: Signer | SignerV6, + params: AcceptReturnedObligationParams, + options: TransactionOptions, +): Promise => { + const { obligationRegistryAddress } = contractOptions; + const { chainId, maxFeePerGas, maxPriorityFeePerGas } = options; + + if (!obligationRegistryAddress) throw new Error('Obligation registry address is required'); + if (!signer.provider) throw new Error('Provider is required'); + + const isSupported = await checkSupportsInterface( + obligationRegistryAddress, + v5SupportInterfaceIds.TradeTrustTokenBurnable, + signer.provider, + ); + if (!isSupported) { + throw new Error('Only TrustVCToken obligation registry is supported'); + } + + const { tokenId, remarks } = params; + const obligationRegistryContract = getObligationRegistryContract( + obligationRegistryAddress, + signer, + ); + const encryptedRemarks = remarks ? `0x${encrypt(remarks, options.id ?? '')}` : '0x'; + + try { + const isV6 = isV6EthersProvider(signer.provider); + const args = [tokenId, encryptedRemarks]; + if (isV6) { + await (obligationRegistryContract as ContractV6).burn.staticCall(...args); + } else { + await (obligationRegistryContract as ContractV5).callStatic.burn(...args); + } + } catch (error) { + console.error('callStatic failed:', error); + throw new Error('Pre-check (callStatic) for acceptReturned failed'); + } + + const txOptions = await getTxOptions(signer, chainId, maxFeePerGas, maxPriorityFeePerGas); + return obligationRegistryContract.burn(tokenId, encryptedRemarks, txOptions); +}; + +const rejectReturnedObligationRegistry = async ( + contractOptions: RejectReturnedObligationOptions, + signer: Signer | SignerV6, + params: RejectReturnedObligationParams, + options: TransactionOptions, +): Promise => { + const { obligationRegistryAddress } = contractOptions; + const { chainId, maxFeePerGas, maxPriorityFeePerGas } = options; + + if (!obligationRegistryAddress) throw new Error('Obligation registry address is required'); + if (!signer.provider) throw new Error('Provider is required'); + + const isSupported = await checkSupportsInterface( + obligationRegistryAddress, + v5SupportInterfaceIds.TradeTrustTokenRestorable, + signer.provider, + ); + if (!isSupported) { + throw new Error('Only TrustVCToken obligation registry is supported'); + } + + const { tokenId, remarks } = params; + const obligationRegistryContract = getObligationRegistryContract( + obligationRegistryAddress, + signer, + ); + const encryptedRemarks = remarks ? `0x${encrypt(remarks, options.id ?? '')}` : '0x'; + + try { + const isV6 = isV6EthersProvider(signer.provider); + const args = [tokenId, encryptedRemarks]; + if (isV6) { + await (obligationRegistryContract as ContractV6).restore.staticCall(...args); + } else { + await (obligationRegistryContract as ContractV5).callStatic.restore(...args); + } + } catch (error) { + console.error('callStatic failed:', error); + throw new Error('Pre-check (callStatic) for rejectReturned failed'); + } + + const txOptions = await getTxOptions(signer, chainId, maxFeePerGas, maxPriorityFeePerGas); + return obligationRegistryContract.restore(tokenId, encryptedRemarks, txOptions); +}; + +export { + returnToIssuerObligationRegistry, + acceptReturnedObligationRegistry, + rejectReturnedObligationRegistry, +}; diff --git a/src/obligation-registry-functions/status.ts b/src/obligation-registry-functions/status.ts new file mode 100644 index 0000000..6e55601 --- /dev/null +++ b/src/obligation-registry-functions/status.ts @@ -0,0 +1,60 @@ +import { Signer as SignerV6 } from 'ethersV6'; +import { Signer } from 'ethers'; +import { + ObligationContractOptions, + ObligationDocumentStatus, + ObligationEscrowTerminationReason, + ObligationStatusReadOptions, + ObligationTokenIdParams, +} from './types'; +import { getObligationEscrowContract, resolveObligationEscrowAddress } from './utils'; + +const getObligationRegistryStatus = async ( + contractOptions: ObligationContractOptions, + signer: Signer | SignerV6, + _params: ObligationTokenIdParams, + options: ObligationStatusReadOptions = {}, +): Promise => { + if (!signer.provider) throw new Error('Provider is required'); + + const obligationEscrowAddress = await resolveObligationEscrowAddress(contractOptions, signer); + const obligationEscrowContract = getObligationEscrowContract(obligationEscrowAddress, signer); + const status = await obligationEscrowContract.status({ blockTag: options.blockTag }); + + return Number(status) as ObligationDocumentStatus; +}; + +const isObligationRegistryRegistered = async ( + contractOptions: ObligationContractOptions, + signer: Signer | SignerV6, + _params: ObligationTokenIdParams, + options: ObligationStatusReadOptions = {}, +): Promise => { + if (!signer.provider) throw new Error('Provider is required'); + + const obligationEscrowAddress = await resolveObligationEscrowAddress(contractOptions, signer); + const obligationEscrowContract = getObligationEscrowContract(obligationEscrowAddress, signer); + + return obligationEscrowContract.isRegistered({ blockTag: options.blockTag }); +}; + +const getObligationEscrowTerminationReason = async ( + contractOptions: ObligationContractOptions, + signer: Signer | SignerV6, + _params: ObligationTokenIdParams, + options: ObligationStatusReadOptions = {}, +): Promise => { + if (!signer.provider) throw new Error('Provider is required'); + + const obligationEscrowAddress = await resolveObligationEscrowAddress(contractOptions, signer); + const obligationEscrowContract = getObligationEscrowContract(obligationEscrowAddress, signer); + const reason = await obligationEscrowContract.terminationReason({ blockTag: options.blockTag }); + + return Number(reason) as ObligationEscrowTerminationReason; +}; + +export { + getObligationRegistryStatus, + isObligationRegistryRegistered, + getObligationEscrowTerminationReason, +}; diff --git a/src/obligation-registry-functions/transfer.ts b/src/obligation-registry-functions/transfer.ts new file mode 100644 index 0000000..15d3422 --- /dev/null +++ b/src/obligation-registry-functions/transfer.ts @@ -0,0 +1,92 @@ +import { Signer as SignerV6 } from 'ethersV6'; +import { ContractTransaction, Signer } from 'ethers'; +import { + getEncryptedRemarks, + getObligationEscrowContract, + resolveObligationEscrowAddress, + runStaticCall, + sendTransaction, +} from './utils'; +import { + NominateObligationParams, + ObligationContractOptions, + TransactionOptions, + TransferObligationBeneficiaryParams, + TransferObligationHolderParams, + TransferObligationOwnersParams, +} from './types'; + +const transferHolderObligationRegistry = async ( + contractOptions: ObligationContractOptions, + signer: Signer | SignerV6, + params: TransferObligationHolderParams, + options: TransactionOptions, +): Promise => { + if (!signer.provider) throw new Error('Provider is required'); + + const obligationEscrowAddress = await resolveObligationEscrowAddress(contractOptions, signer); + const obligationEscrowContract = getObligationEscrowContract(obligationEscrowAddress, signer); + const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); + const args = [params.holderAddress, encryptedRemarks]; + + await runStaticCall(obligationEscrowContract, 'transferHolder', args, signer.provider); + return sendTransaction(obligationEscrowContract, 'transferHolder', args, signer, options); +}; + +const transferBeneficiaryObligationRegistry = async ( + contractOptions: ObligationContractOptions, + signer: Signer | SignerV6, + params: TransferObligationBeneficiaryParams, + options: TransactionOptions, +): Promise => { + if (!signer.provider) throw new Error('Provider is required'); + + const obligationEscrowAddress = await resolveObligationEscrowAddress(contractOptions, signer); + const obligationEscrowContract = getObligationEscrowContract(obligationEscrowAddress, signer); + const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); + const args = [params.newBeneficiaryAddress, encryptedRemarks]; + + await runStaticCall(obligationEscrowContract, 'transferBeneficiary', args, signer.provider); + return sendTransaction(obligationEscrowContract, 'transferBeneficiary', args, signer, options); +}; + +const nominateObligationRegistry = async ( + contractOptions: ObligationContractOptions, + signer: Signer | SignerV6, + params: NominateObligationParams, + options: TransactionOptions, +): Promise => { + if (!signer.provider) throw new Error('Provider is required'); + + const obligationEscrowAddress = await resolveObligationEscrowAddress(contractOptions, signer); + const obligationEscrowContract = getObligationEscrowContract(obligationEscrowAddress, signer); + const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); + const args = [params.newBeneficiaryAddress, encryptedRemarks]; + + await runStaticCall(obligationEscrowContract, 'nominate', args, signer.provider); + return sendTransaction(obligationEscrowContract, 'nominate', args, signer, options); +}; + +const transferOwnersObligationRegistry = async ( + contractOptions: ObligationContractOptions, + signer: Signer | SignerV6, + params: TransferObligationOwnersParams, + options: TransactionOptions, +): Promise => { + if (!signer.provider) throw new Error('Provider is required'); + + const obligationEscrowAddress = await resolveObligationEscrowAddress(contractOptions, signer); + const obligationEscrowContract = getObligationEscrowContract(obligationEscrowAddress, signer); + const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); + const args = [params.newHolderAddress, params.newBeneficiaryAddress, encryptedRemarks]; + + await runStaticCall(obligationEscrowContract, 'transferOwners', args, signer.provider); + return sendTransaction(obligationEscrowContract, 'transferOwners', args, signer, options); +}; + +export { + transferHolderObligationRegistry, + transferBeneficiaryObligationRegistry, + nominateObligationRegistry, + transferOwnersObligationRegistry, +}; diff --git a/src/obligation-registry-functions/types.ts b/src/obligation-registry-functions/types.ts new file mode 100644 index 0000000..a8d3867 --- /dev/null +++ b/src/obligation-registry-functions/types.ts @@ -0,0 +1,121 @@ +import { CHAIN_ID } from '../utils'; +import { BigNumber, providers as providersV5 } from 'ethers'; +import { BigNumberish, Provider as ProviderV6 } from 'ethersV6'; + +export interface GasPriceScale { + maxPriorityFeePerGasScale: number; +} + +export interface GasOption extends GasPriceScale { + dryRun: boolean; +} + +export type GasValue = BigNumber | BigNumberish | string | number; + +export interface ObligationRemarkParams { + remarks?: string; +} + +export interface MintObligationTokenParams { + beneficiaryAddress: string; + holderAddress: string; + tokenId: string | number; + remarks?: string; +} + +export interface OwnerOfObligationTokenParams { + tokenId: string | number; +} + +export interface ObligationTokenIdParams { + tokenId: string | number; +} + +export interface AcceptReturnedObligationParams { + tokenId: string | number; + remarks?: string; +} + +export interface RejectReturnedObligationParams { + tokenId: string | number; + remarks?: string; +} + +export interface TransactionOptions { + chainId?: CHAIN_ID; + maxFeePerGas?: BigNumberish | string | number | BigNumber; + maxPriorityFeePerGas?: BigNumberish | string | number | BigNumber; + id?: string; +} + +export type ObligationContractOptions = + | { + obligationEscrowAddress: string; + obligationRegistryAddress?: string; + tokenId?: string | number; + } + | { + obligationEscrowAddress?: undefined; + obligationRegistryAddress: string; + tokenId: string | number; + }; + +export type MintObligationTokenOptions = { + obligationRegistryAddress: string; +}; + +export type OwnerOfObligationTokenOptions = { + obligationRegistryAddress: string; +}; + +export type AcceptReturnedObligationOptions = { + obligationRegistryAddress: string; +}; + +export type RejectReturnedObligationOptions = { + obligationRegistryAddress: string; +}; + +export type ObligationStatusReadOptions = { + blockTag?: number | string; +}; + +export enum ObligationDocumentStatus { + Issued = 0, + Accepted = 1, + Rejected = 2, + Discharged = 3, +} + +export enum ObligationEscrowTerminationReason { + None = 0, + ReturnToIssuer = 1, + Rejected = 2, + Discharged = 3, +} + +export interface TransferObligationHolderParams { + holderAddress: string; + remarks?: string; +} + +export interface TransferObligationBeneficiaryParams { + newBeneficiaryAddress: string; + remarks?: string; +} + +export interface NominateObligationParams { + newBeneficiaryAddress: string; + remarks?: string; +} + +export interface TransferObligationOwnersParams { + newHolderAddress: string; + newBeneficiaryAddress: string; + remarks?: string; +} + +export interface ProviderInfo { + Provider: providersV5.Provider | ProviderV6; + ethersVersion: 'v5' | 'v6'; +} diff --git a/src/obligation-registry-functions/utils.ts b/src/obligation-registry-functions/utils.ts new file mode 100644 index 0000000..e7d2415 --- /dev/null +++ b/src/obligation-registry-functions/utils.ts @@ -0,0 +1,118 @@ +import { encrypt, getObligationEscrowAddress } from '../core'; +import { v5Contracts } from '../token-registry-v5'; +import { getTxOptions } from '../token-registry-functions/utils'; +import { Signer as SignerV6, Contract as ContractV6 } from 'ethersV6'; +import { Contract as ContractV5, Signer } from 'ethers'; +import { getEthersContractFromProvider, isV6EthersProvider } from '../utils/ethers'; +import { ObligationContractOptions } from './types'; + +export const getObligationRegistryContract = ( + obligationRegistryAddress: string, + signerOrProvider: Signer | SignerV6, +): ContractV5 | ContractV6 => { + const provider = signerOrProvider.provider; + if (!provider) { + throw new Error('Provider is required'); + } + + const Contract = getEthersContractFromProvider(provider); + return new Contract( + obligationRegistryAddress, + v5Contracts.TrustVCToken__factory.abi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signerOrProvider as any, + ); +}; + +export const resolveObligationEscrowAddress = async ( + contractOptions: ObligationContractOptions, + signer: Signer | SignerV6, +): Promise => { + let { obligationEscrowAddress } = contractOptions; + + if (!obligationEscrowAddress) { + const { obligationRegistryAddress, tokenId } = contractOptions; + if (!obligationRegistryAddress) { + throw new Error('Obligation registry address is required'); + } + if (!tokenId) { + throw new Error('Token ID is required'); + } + if (!signer.provider) { + throw new Error('Provider is required'); + } + + obligationEscrowAddress = await getObligationEscrowAddress( + obligationRegistryAddress, + tokenId as string, + signer.provider, + { titleEscrowVersion: 'v5' }, + ); + } + + if (!obligationEscrowAddress) { + throw new Error('Obligation escrow address is required'); + } + + return obligationEscrowAddress; +}; + +export const getObligationEscrowContract = ( + obligationEscrowAddress: string, + signer: Signer | SignerV6, +): ContractV5 | ContractV6 => { + const Contract = getEthersContractFromProvider(signer.provider!); + return new Contract( + obligationEscrowAddress, + v5Contracts.ObligationEscrow__factory.abi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); +}; + +export const getEncryptedRemarks = ( + remarks: string | undefined, + id: string | undefined, +): string => { + return remarks && id ? `0x${encrypt(remarks, id)}` : '0x'; +}; + +export const runStaticCall = async ( + contract: ContractV5 | ContractV6, + method: string, + args: unknown[], + provider: Signer['provider'] | SignerV6['provider'], +): Promise => { + try { + if (isV6EthersProvider(provider)) { + await (contract as ContractV6)[method].staticCall(...args); + } else { + await (contract as ContractV5).callStatic[method](...args); + } + } catch (error) { + console.error('callStatic failed:', error); + throw new Error(`Pre-check (callStatic) for ${method} failed`); + } +}; + +export const sendTransaction = async ( + contract: ContractV5 | ContractV6, + method: string, + args: unknown[], + signer: Signer | SignerV6, + options: { + chainId?: Parameters[1]; + maxFeePerGas?: Parameters[2]; + maxPriorityFeePerGas?: Parameters[3]; + }, +) => { + const txOptions = await getTxOptions( + signer, + options.chainId, + options.maxFeePerGas, + options.maxPriorityFeePerGas, + ); + return (contract as ContractV5)[method](...args, txOptions); +}; + +export { getTxOptions }; diff --git a/src/token-registry-v5/contracts.ts b/src/token-registry-v5/contracts.ts index 549fb32..500af60 100644 --- a/src/token-registry-v5/contracts.ts +++ b/src/token-registry-v5/contracts.ts @@ -4,6 +4,9 @@ export { TitleEscrow__factory, TradeTrustToken__factory, TradeTrustTokenStandard__factory, + TrustVCToken__factory, + ObligationEscrow__factory, + ObligationEscrowFactory__factory, ERC1967Utils__factory as ERC1967__factory, } from '@tradetrust-tt/token-registry-v5/contracts'; export type { @@ -12,5 +15,8 @@ export type { TitleEscrowFactory, TradeTrustToken, TradeTrustTokenStandard, + TrustVCToken, + ObligationEscrow, + ObligationEscrowFactory, ERC1967Utils as ERC1967, } from '@tradetrust-tt/token-registry-v5/contracts'; diff --git a/src/utils/documents/index.ts b/src/utils/documents/index.ts index 3ffb72f..14ac557 100644 --- a/src/utils/documents/index.ts +++ b/src/utils/documents/index.ts @@ -14,9 +14,12 @@ import { TRANSFERABLE_RECORDS_TYPE } from '../../verify/fragments'; import { TransferableRecordsCredentialStatus } from '../../w3c/credential-status'; import { isDerived, isSignedDocument, SignedVerifiableCredential } from '../../w3c/vc'; import { CHAIN_ID } from '../supportedChains'; +import { isObligationRecordCredentialStatus } from './obligation'; export type WrappedOrSignedOpenAttestationDocument = WrappedDocument; +export * from './obligation'; + export const getTransferableRecordsCredentialStatus = ( document: unknown, ): TransferableRecordsCredentialStatus => { @@ -31,7 +34,9 @@ export const isTransferableRecord = ( let isTransferableAssetVal: boolean = false; if (isSignedDocument(document)) { const credentialStatus = getTransferableRecordsCredentialStatus(document); - isTransferableAssetVal = credentialStatus?.type === TRANSFERABLE_RECORDS_TYPE; + isTransferableAssetVal = + credentialStatus?.type === TRANSFERABLE_RECORDS_TYPE && + !isObligationRecordCredentialStatus(credentialStatus); } else { isTransferableAssetVal = isTransferableAsset(document); } diff --git a/src/utils/documents/obligation.ts b/src/utils/documents/obligation.ts new file mode 100644 index 0000000..555af41 --- /dev/null +++ b/src/utils/documents/obligation.ts @@ -0,0 +1,48 @@ +import { TRANSFERABLE_RECORDS_TYPE } from '../../verify/fragments'; +import { ObligationRecordsCredentialStatus } from '@trustvc/w3c-credential-status'; +import { CredentialStatus, isSignedDocument, SignedVerifiableCredential } from '../../w3c/vc'; +import { WrappedOrSignedOpenAttestationDocument } from './index'; + +export const isObligationRecordCredentialStatus = ( + credentialStatus: CredentialStatus | undefined, +): credentialStatus is ObligationRecordsCredentialStatus => { + if (!credentialStatus || credentialStatus.type !== TRANSFERABLE_RECORDS_TYPE) { + return false; + } + + const obligationRegistry = (credentialStatus as ObligationRecordsCredentialStatus) + .obligationRegistry; + return typeof obligationRegistry === 'string' && obligationRegistry.length > 0; +}; + +export const getObligationRecordsCredentialStatus = ( + document: unknown, +): ObligationRecordsCredentialStatus => { + return [ + (document as SignedVerifiableCredential)?.credentialStatus, + ].flat()?.[0] as ObligationRecordsCredentialStatus; +}; + +export const isObligationRecord = ( + document: WrappedOrSignedOpenAttestationDocument | SignedVerifiableCredential, +): boolean => { + if (!isSignedDocument(document)) { + return false; + } + + const credentialStatuses = Array.isArray(document.credentialStatus) + ? document.credentialStatus + : [document.credentialStatus]; + + return credentialStatuses.every((cs) => isObligationRecordCredentialStatus(cs)); +}; + +export const getObligationRegistryAddress = ( + document: WrappedOrSignedOpenAttestationDocument | SignedVerifiableCredential, +): string | undefined => { + if (!isSignedDocument(document)) { + return undefined; + } + + return getObligationRecordsCredentialStatus(document)?.obligationRegistry; +}; diff --git a/src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.ts b/src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.ts new file mode 100644 index 0000000..166bdd2 --- /dev/null +++ b/src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.ts @@ -0,0 +1,161 @@ +import { + CodedError, + DocumentsToVerify, + InvalidTokenRegistryStatus, + OpenAttestationEthereumTokenRegistryStatusCode, + ValidTokenRegistryStatus, + VerifierOptions, +} from '@tradetrust-tt/tt-verify'; +import { ObligationRecordsCredentialStatus } from '@trustvc/w3c-credential-status'; +import * as w3cVC from '@trustvc/w3c-vc'; +import { SignedVerifiableCredential } from '@trustvc/w3c-vc'; +import { isObligationRecordCredentialStatus } from '../../../../utils/documents/obligation'; +import { + ObligationRecordsErrorFragment, + ObligationRecordsResultFragment, + ObligationRecordsVerificationFragment, + VerifierType, +} from './obligationRecordVerifier.types'; +import { isTokenMintedOnObligationRegistry } from './utils'; + +export const OBLIGATION_RECORDS_TYPE = 'ObligationRecords'; +const type = 'DOCUMENT_STATUS'; +const name = OBLIGATION_RECORDS_TYPE; + +const verify: VerifierType['verify'] = async ( + document: DocumentsToVerify | SignedVerifiableCredential, + options: VerifierOptions, +) => { + const signedDocument = document as SignedVerifiableCredential; + const credentialStatuses = ( + Array.isArray(signedDocument?.credentialStatus) + ? signedDocument?.credentialStatus + : [signedDocument?.credentialStatus] + ) as ObligationRecordsCredentialStatus[]; + const { provider } = options; + + const verificationResult = await Promise.all( + credentialStatuses.map(async (credentialStatus: ObligationRecordsCredentialStatus) => { + const tokenId = '0x' + credentialStatus.tokenId; + + if (!credentialStatus?.obligationRegistry) { + throw new CodedError( + "Document's credentialStatus does not have obligationRegistry", + OpenAttestationEthereumTokenRegistryStatusCode.UNRECOGNIZED_DOCUMENT, + OpenAttestationEthereumTokenRegistryStatusCode[ + OpenAttestationEthereumTokenRegistryStatusCode.UNRECOGNIZED_DOCUMENT + ], + ); + } + + if ( + !credentialStatus?.tokenNetwork || + credentialStatus?.tokenNetwork?.chainId === undefined + ) { + throw new CodedError( + "Document's credentialStatus does not have tokenNetwork.chainId", + OpenAttestationEthereumTokenRegistryStatusCode.UNRECOGNIZED_DOCUMENT, + OpenAttestationEthereumTokenRegistryStatusCode[ + OpenAttestationEthereumTokenRegistryStatusCode.UNRECOGNIZED_DOCUMENT + ], + ); + } + + return isTokenMintedOnObligationRegistry({ + obligationRegistryAddress: credentialStatus.obligationRegistry, + tokenId, + provider, + chainId: credentialStatus.tokenNetwork.chainId, + }); + }), + ); + + const result: ObligationRecordsResultFragment = { + name, + type, + status: 'INVALID' as const, + data: { + obligationRegistry: credentialStatuses?.[0]?.obligationRegistry, + }, + }; + + if (verificationResult.every(ValidTokenRegistryStatus.guard)) { + result.status = 'VALID' as const; + } else { + result.reason = (verificationResult as InvalidTokenRegistryStatus[])?.[0]?.reason; + } + + return result; +}; + +const skip: VerifierType['skip'] = async () => { + return { + status: 'SKIPPED', + type, + name, + reason: { + code: OpenAttestationEthereumTokenRegistryStatusCode.SKIPPED, + codeString: + OpenAttestationEthereumTokenRegistryStatusCode[ + OpenAttestationEthereumTokenRegistryStatusCode.SKIPPED + ], + message: `Document does not have ObligationRecords status`, + }, + }; +}; + +const test: VerifierType['test'] = ( + document: DocumentsToVerify | SignedVerifiableCredential, +): boolean => { + const doc = document as SignedVerifiableCredential; + const credentialStatuses = Array.isArray(doc?.credentialStatus) + ? doc?.credentialStatus + : [doc?.credentialStatus]; + + if ( + w3cVC.isSignedDocument(document) && + credentialStatuses.every((cs: w3cVC.CredentialStatus) => isObligationRecordCredentialStatus(cs)) + ) { + return true; + } + + return false; +}; + +export const credentialStatusObligationRecordVerifier: VerifierType = { + skip, + test, + verify: async (...args): Promise => { + try { + return await verify(...args); + } catch (e: unknown) { + if (e instanceof CodedError) { + const err: ObligationRecordsErrorFragment = { + name, + type, + status: 'ERROR' as const, + reason: { + code: e.code, + codeString: e.codeString, + message: e.message, + }, + }; + return err; + } + + return { + name, + type, + status: 'ERROR' as const, + reason: { + code: OpenAttestationEthereumTokenRegistryStatusCode.UNEXPECTED_ERROR, + codeString: + OpenAttestationEthereumTokenRegistryStatusCode[ + OpenAttestationEthereumTokenRegistryStatusCode.UNEXPECTED_ERROR + ], + message: e instanceof Error ? e.message : 'An unexpected error occurred', + }, + }; + } + }, +}; diff --git a/src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.types.ts b/src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.types.ts new file mode 100644 index 0000000..37cb1e8 --- /dev/null +++ b/src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.types.ts @@ -0,0 +1,31 @@ +import { + ErrorVerificationFragment, + OpenAttestationEthereumTokenRegistryStatusCode, + VerificationFragment, + Verifier, +} from '@tradetrust-tt/tt-verify'; + +export type ObligationRecordsErrorReason = { + code: OpenAttestationEthereumTokenRegistryStatusCode; + codeString: string; + message: string; +}; + +export type ObligationRecordsResultFragment = VerificationFragment & { + status: 'VALID' | 'INVALID'; + data: { + obligationRegistry: string; + }; + reason?: ObligationRecordsErrorReason; +}; + +export type ObligationRecordsErrorFragment = Omit, 'data'> & { + data?: never; + reason: ObligationRecordsErrorReason; +}; + +export type ObligationRecordsVerificationFragment = + | ObligationRecordsResultFragment + | ObligationRecordsErrorFragment; + +export type VerifierType = Verifier; diff --git a/src/verify/fragments/document-status/obligationRecords/utils.ts b/src/verify/fragments/document-status/obligationRecords/utils.ts new file mode 100644 index 0000000..04fc8e2 --- /dev/null +++ b/src/verify/fragments/document-status/obligationRecords/utils.ts @@ -0,0 +1,92 @@ +import { TrustVCToken__factory } from '@tradetrust-tt/token-registry-v5/contracts'; +import { + CodedError, + InvalidTokenRegistryStatus, + OpenAttestationEthereumTokenRegistryStatusCode, + ValidTokenRegistryStatus, +} from '@tradetrust-tt/tt-verify'; +import { constants, errors, providers } from 'ethers'; +import { decodeError } from '../transferableRecords/utils'; + +type EthersError = { + message?: string; + data?: string; + method?: string; + reason?: string; + code?: errors; +}; + +export const isTokenMintedOnObligationRegistry = async ({ + obligationRegistryAddress, + tokenId, + provider, + chainId, +}: { + obligationRegistryAddress: string; + tokenId: string; + provider: providers.Provider; + chainId?: number | string; +}): Promise => { + if (chainId !== undefined) { + const network = await provider.getNetwork(); + const expectedChainId = typeof chainId === 'string' ? parseInt(chainId, 10) : Number(chainId); + + if (!Number.isFinite(expectedChainId) || network.chainId !== expectedChainId) { + return { + minted: false, + address: obligationRegistryAddress, + reason: { + code: OpenAttestationEthereumTokenRegistryStatusCode.UNRECOGNIZED_DOCUMENT, + codeString: + OpenAttestationEthereumTokenRegistryStatusCode[ + OpenAttestationEthereumTokenRegistryStatusCode.UNRECOGNIZED_DOCUMENT + ], + message: `Provider chain ID ${network.chainId} does not match credentialStatus.tokenNetwork.chainId ${expectedChainId}`, + }, + }; + } + } + + try { + const obligationRegistryContract = TrustVCToken__factory.connect( + obligationRegistryAddress, + provider, + ); + const minted = await obligationRegistryContract + .ownerOf(tokenId) + .then((owner: string) => owner !== constants.AddressZero); + + return minted + ? { minted, address: obligationRegistryAddress } + : { + minted, + address: obligationRegistryAddress, + reason: { + code: OpenAttestationEthereumTokenRegistryStatusCode.DOCUMENT_NOT_MINTED, + codeString: + OpenAttestationEthereumTokenRegistryStatusCode[ + OpenAttestationEthereumTokenRegistryStatusCode.DOCUMENT_NOT_MINTED + ], + message: `Document ${tokenId} has not been issued under contract ${obligationRegistryAddress}`, + }, + }; + } catch (error: unknown) { + const decodedMessage = decodeError(error as EthersError); + if (error instanceof CodedError) { + throw error; + } + + return { + minted: false, + address: obligationRegistryAddress, + reason: { + message: decodedMessage, + code: OpenAttestationEthereumTokenRegistryStatusCode.DOCUMENT_NOT_MINTED, + codeString: + OpenAttestationEthereumTokenRegistryStatusCode[ + OpenAttestationEthereumTokenRegistryStatusCode.DOCUMENT_NOT_MINTED + ], + }, + }; + } +}; diff --git a/src/verify/fragments/document-status/transferableRecords/transferableRecordVerifier.ts b/src/verify/fragments/document-status/transferableRecords/transferableRecordVerifier.ts index 424a4f7..b90b58f 100644 --- a/src/verify/fragments/document-status/transferableRecords/transferableRecordVerifier.ts +++ b/src/verify/fragments/document-status/transferableRecords/transferableRecordVerifier.ts @@ -110,7 +110,13 @@ const test: VerifierType['test'] = ( if ( w3cVC.isSignedDocument(document) && - credentialStatuses.every((cs: w3cVC.CredentialStatus) => cs?.type === TRANSFERABLE_RECORDS_TYPE) + credentialStatuses.every( + (cs: w3cVC.CredentialStatus) => + cs?.type === TRANSFERABLE_RECORDS_TYPE && + !!(cs as TransferableRecordsCredentialStatus).tokenRegistry && + !(cs as TransferableRecordsCredentialStatus & { obligationRegistry?: string }) + .obligationRegistry, + ) ) { return true; } diff --git a/src/verify/fragments/index.ts b/src/verify/fragments/index.ts index f82678d..c73082a 100644 --- a/src/verify/fragments/index.ts +++ b/src/verify/fragments/index.ts @@ -12,13 +12,19 @@ import { credentialStatusTransferableRecordVerifier, TRANSFERABLE_RECORDS_TYPE, } from './document-status/transferableRecords/transferableRecordVerifier'; +import { + credentialStatusObligationRecordVerifier, + OBLIGATION_RECORDS_TYPE, +} from './document-status/obligationRecords/obligationRecordVerifier'; import { w3cCredentialStatus } from './document-status/w3cCredentialStatus'; import { w3cIssuerIdentity } from './issuer-identity/w3cIssuerIdentity'; import { w3cEmptyCredentialStatus } from './document-status/w3cEmptyCredentialStatus'; export { TRANSFERABLE_RECORDS_TYPE, + OBLIGATION_RECORDS_TYPE, credentialStatusTransferableRecordVerifier, + credentialStatusObligationRecordVerifier, openAttestationDidSignedDocumentStatus, openAttestationDnsDidIdentityProof, openAttestationDnsTxtIdentityProof, diff --git a/src/verify/verify.ts b/src/verify/verify.ts index 1e32536..995de3a 100644 --- a/src/verify/verify.ts +++ b/src/verify/verify.ts @@ -32,6 +32,7 @@ import type { import { w3cSignatureIntegrity } from './fragments/document-integrity/w3cSignatureIntegrity'; import { ecdsaW3CSignatureIntegrity } from './fragments/document-integrity/ecdsaW3CSignatureIntegrity'; import { credentialStatusTransferableRecordVerifier } from './fragments/document-status/transferableRecords/transferableRecordVerifier'; +import { credentialStatusObligationRecordVerifier } from './fragments/document-status/obligationRecords/obligationRecordVerifier'; import { w3cCredentialStatus } from './fragments/document-status/w3cCredentialStatus'; import { w3cIssuerIdentity } from './fragments/issuer-identity/w3cIssuerIdentity'; import { w3cEmptyCredentialStatus } from './fragments'; @@ -50,6 +51,7 @@ const verifiers = { w3cCredentialStatus, w3cEmptyCredentialStatus, credentialStatusTransferableRecordVerifier, + credentialStatusObligationRecordVerifier, }, issuerIdentity: { openAttestationDnsDidIdentityProof, @@ -66,6 +68,7 @@ const w3cVerifiers: Verifier[] = [ bbs2023W3CSignatureIntegrity, w3cCredentialStatus, credentialStatusTransferableRecordVerifier, + credentialStatusObligationRecordVerifier, w3cEmptyCredentialStatus, w3cIssuerIdentity, ]; From da8f982f3099e8a5bcddf672d6cd1040800a16c4 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Thu, 30 Jul 2026 15:40:08 +0530 Subject: [PATCH 02/42] fix: update provider URL to use environment variable for Amoy RPC --- src/__tests__/core/verify.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/core/verify.test.ts b/src/__tests__/core/verify.test.ts index 424c8af..1de01fa 100644 --- a/src/__tests__/core/verify.test.ts +++ b/src/__tests__/core/verify.test.ts @@ -18,7 +18,7 @@ import { import { W3CCredentialStatusCode } from '../../verify/fragments/document-status/w3cCredentialStatus'; import { openAttestationDidSignedDocumentStatus } from '@tradetrust-tt/tt-verify'; -const providerUrl = 'https://rpc-amoy.polygon.technology'; +const providerUrl = process.env.AMOY_RPC || 'https://rpc-amoy.polygon.technology'; describe.concurrent('W3C verify', () => { describe.concurrent('W3C_VERIFIABLE_DOCUMENT', () => { From 3dde46fcbacafd59d613169efd01f5d90ec39bf4 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Thu, 30 Jul 2026 15:46:24 +0530 Subject: [PATCH 03/42] chore: add AMOY_RPC environment variable for Polygon Amoy RPC integration --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 13416db..33663e1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -37,6 +37,7 @@ jobs: env: ANKR_API_KEY: ${{ secrets.ANKR_API_KEY }} POLYGONSCAN_API_KEY: ${{ secrets.POLYGONSCAN_API_KEY }} + AMOY_RPC: ${{ secrets.AMOY_RPC }} test-build: name: Test Build From 348df073c06ba4c42a4e127e82e0aa426de42fc0 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Thu, 30 Jul 2026 15:47:19 +0530 Subject: [PATCH 04/42] Update src/__tests__/e2e/obligation-registry-functions/transfer.e2e.test.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../e2e/obligation-registry-functions/transfer.e2e.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/e2e/obligation-registry-functions/transfer.e2e.test.ts b/src/__tests__/e2e/obligation-registry-functions/transfer.e2e.test.ts index 974332c..e326032 100644 --- a/src/__tests__/e2e/obligation-registry-functions/transfer.e2e.test.ts +++ b/src/__tests__/e2e/obligation-registry-functions/transfer.e2e.test.ts @@ -80,7 +80,7 @@ obligationE2EProviders.forEach(({ ethersVersion }) => { { holderAddress: setup.other.address }, setup.txOptions, ), - ).to.be.rejectedWith(/transferHolder failed/); + ).to.be.revertedWith(/transferHolder failed/); }); it('E12: nominate + transferBeneficiary', async function () { From b855d8fdf23dacb661fe7c4309a3531ea948514c Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Thu, 30 Jul 2026 15:47:30 +0530 Subject: [PATCH 05/42] Update src/obligation-registry-functions/ownerOf.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/obligation-registry-functions/ownerOf.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/obligation-registry-functions/ownerOf.ts b/src/obligation-registry-functions/ownerOf.ts index 272965e..7b03f9b 100644 --- a/src/obligation-registry-functions/ownerOf.ts +++ b/src/obligation-registry-functions/ownerOf.ts @@ -14,9 +14,9 @@ const ownerOfObligationRegistry = async ( signer: Signer | SignerV6, params: OwnerOfObligationTokenParams, // Kept for API parity with other obligation-registry functions. - options: TransactionOptions, + _options: TransactionOptions, ): Promise => { - void options; + const { obligationRegistryAddress } = contractOptions; const { obligationRegistryAddress } = contractOptions; const { tokenId } = params; From a4ebe10a948e9e318548b97417e1fd74a4aa74b6 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Thu, 30 Jul 2026 15:48:17 +0530 Subject: [PATCH 06/42] Update src/obligation-registry-functions/utils.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/obligation-registry-functions/utils.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/obligation-registry-functions/utils.ts b/src/obligation-registry-functions/utils.ts index e7d2415..3907f82 100644 --- a/src/obligation-registry-functions/utils.ts +++ b/src/obligation-registry-functions/utils.ts @@ -74,6 +74,9 @@ export const getEncryptedRemarks = ( remarks: string | undefined, id: string | undefined, ): string => { + if (remarks && !id) { + throw new Error('An `id` is required to encrypt remarks'); + } return remarks && id ? `0x${encrypt(remarks, id)}` : '0x'; }; From 994b08811b5a6f467dba47734791c531fb581ab7 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Thu, 30 Jul 2026 15:48:30 +0530 Subject: [PATCH 07/42] Update src/verify/fragments/document-status/obligationRecords/utils.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../fragments/document-status/obligationRecords/utils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/verify/fragments/document-status/obligationRecords/utils.ts b/src/verify/fragments/document-status/obligationRecords/utils.ts index 04fc8e2..a3c0ce7 100644 --- a/src/verify/fragments/document-status/obligationRecords/utils.ts +++ b/src/verify/fragments/document-status/obligationRecords/utils.ts @@ -71,10 +71,13 @@ export const isTokenMintedOnObligationRegistry = async ({ }, }; } catch (error: unknown) { - const decodedMessage = decodeError(error as EthersError); if (error instanceof CodedError) { throw error; } + if ((error as EthersError).code !== errors.CALL_EXCEPTION) { + throw error; + } + const decodedMessage = decodeError(error as EthersError); return { minted: false, From 8a5af28a8cddedfb7d1323b6f9c7ee1b588cb778 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Thu, 30 Jul 2026 15:50:04 +0530 Subject: [PATCH 08/42] refactor: streamline mint and returnToken functions by replacing --- src/obligation-registry-functions/mint.ts | 22 +++------- .../returnToken.ts | 43 +++++-------------- 2 files changed, 17 insertions(+), 48 deletions(-) diff --git a/src/obligation-registry-functions/mint.ts b/src/obligation-registry-functions/mint.ts index 5392fda..352ad9d 100644 --- a/src/obligation-registry-functions/mint.ts +++ b/src/obligation-registry-functions/mint.ts @@ -1,9 +1,9 @@ -import { checkSupportsInterface, encrypt } from '../core'; +import { checkSupportsInterface } from '../core'; import { v5Contracts, v5SupportInterfaceIds } from '../token-registry-v5'; import { Signer as SignerV6, Contract as ContractV6 } from 'ethersV6'; import { Contract as ContractV5, ContractTransaction, Signer } from 'ethers'; -import { getEthersContractFromProvider, isV6EthersProvider } from '../utils/ethers'; -import { getTxOptions } from './utils'; +import { getEthersContractFromProvider } from '../utils/ethers'; +import { getEncryptedRemarks, getTxOptions, runStaticCall } from './utils'; import { MintObligationTokenOptions, MintObligationTokenParams, TransactionOptions } from './types'; const mintObligationRegistry = async ( @@ -36,20 +36,10 @@ const mintObligationRegistry = async ( signer as any, ) as ContractV5 | ContractV6; - const encryptedRemarks = remarks ? `0x${encrypt(remarks, options.id ?? '')}` : '0x'; + const encryptedRemarks = getEncryptedRemarks(remarks, options.id); + const args = [beneficiaryAddress, holderAddress, tokenId, encryptedRemarks]; - try { - const isV6 = isV6EthersProvider(signer.provider); - const args = [beneficiaryAddress, holderAddress, tokenId, encryptedRemarks]; - if (isV6) { - await (obligationRegistryContract as ContractV6).mint.staticCall(...args); - } else { - await (obligationRegistryContract as ContractV5).callStatic.mint(...args); - } - } catch (error) { - console.error('callStatic failed:', error); - throw new Error('Pre-check (callStatic) for mint failed'); - } + await runStaticCall(obligationRegistryContract, 'mint', args, signer.provider); const txOptions = await getTxOptions(signer, chainId, maxFeePerGas, maxPriorityFeePerGas); return obligationRegistryContract.mint( diff --git a/src/obligation-registry-functions/returnToken.ts b/src/obligation-registry-functions/returnToken.ts index fe7f4b0..3eb19da 100644 --- a/src/obligation-registry-functions/returnToken.ts +++ b/src/obligation-registry-functions/returnToken.ts @@ -1,8 +1,7 @@ -import { checkSupportsInterface, encrypt } from '../core'; +import { checkSupportsInterface } from '../core'; import { v5SupportInterfaceIds } from '../token-registry-v5'; -import { Signer as SignerV6, Contract as ContractV6 } from 'ethersV6'; -import { Contract as ContractV5, ContractTransaction, Signer } from 'ethers'; -import { isV6EthersProvider } from '../utils/ethers'; +import { Signer as SignerV6 } from 'ethersV6'; +import { ContractTransaction, Signer } from 'ethers'; import { AcceptReturnedObligationOptions, AcceptReturnedObligationParams, @@ -65,20 +64,10 @@ const acceptReturnedObligationRegistry = async ( obligationRegistryAddress, signer, ); - const encryptedRemarks = remarks ? `0x${encrypt(remarks, options.id ?? '')}` : '0x'; - - try { - const isV6 = isV6EthersProvider(signer.provider); - const args = [tokenId, encryptedRemarks]; - if (isV6) { - await (obligationRegistryContract as ContractV6).burn.staticCall(...args); - } else { - await (obligationRegistryContract as ContractV5).callStatic.burn(...args); - } - } catch (error) { - console.error('callStatic failed:', error); - throw new Error('Pre-check (callStatic) for acceptReturned failed'); - } + const encryptedRemarks = getEncryptedRemarks(remarks, options.id); + const args = [tokenId, encryptedRemarks]; + + await runStaticCall(obligationRegistryContract, 'burn', args, signer.provider); const txOptions = await getTxOptions(signer, chainId, maxFeePerGas, maxPriorityFeePerGas); return obligationRegistryContract.burn(tokenId, encryptedRemarks, txOptions); @@ -110,20 +99,10 @@ const rejectReturnedObligationRegistry = async ( obligationRegistryAddress, signer, ); - const encryptedRemarks = remarks ? `0x${encrypt(remarks, options.id ?? '')}` : '0x'; - - try { - const isV6 = isV6EthersProvider(signer.provider); - const args = [tokenId, encryptedRemarks]; - if (isV6) { - await (obligationRegistryContract as ContractV6).restore.staticCall(...args); - } else { - await (obligationRegistryContract as ContractV5).callStatic.restore(...args); - } - } catch (error) { - console.error('callStatic failed:', error); - throw new Error('Pre-check (callStatic) for rejectReturned failed'); - } + const encryptedRemarks = getEncryptedRemarks(remarks, options.id); + const args = [tokenId, encryptedRemarks]; + + await runStaticCall(obligationRegistryContract, 'restore', args, signer.provider); const txOptions = await getTxOptions(signer, chainId, maxFeePerGas, maxPriorityFeePerGas); return obligationRegistryContract.restore(tokenId, encryptedRemarks, txOptions); From 95a931eef7cc46aa79bfad906410788ce7ec4704 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Thu, 30 Jul 2026 15:50:16 +0530 Subject: [PATCH 09/42] fix: improve token ID validation in resolveObligationEscrowAddress function --- src/obligation-registry-functions/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/obligation-registry-functions/utils.ts b/src/obligation-registry-functions/utils.ts index 3907f82..bc00f4d 100644 --- a/src/obligation-registry-functions/utils.ts +++ b/src/obligation-registry-functions/utils.ts @@ -35,7 +35,7 @@ export const resolveObligationEscrowAddress = async ( if (!obligationRegistryAddress) { throw new Error('Obligation registry address is required'); } - if (!tokenId) { + if (tokenId === undefined || tokenId === '') { throw new Error('Token ID is required'); } if (!signer.provider) { From 334f84099aec1e821b71d53bbd065784744e0bb9 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Thu, 30 Jul 2026 15:51:35 +0530 Subject: [PATCH 10/42] refactor: remove redundant variable declaration in ownerOf function --- src/__tests__/core/documentBuilder.test.ts | 18 +++++++++-------- src/__tests__/core/verify.test.ts | 20 +++++++++++++++++++ .../ownerOf.test.ts | 3 +-- src/obligation-registry-functions/ownerOf.ts | 9 +-------- .../transferableRecordVerifier.ts | 6 ++---- 5 files changed, 34 insertions(+), 22 deletions(-) diff --git a/src/__tests__/core/documentBuilder.test.ts b/src/__tests__/core/documentBuilder.test.ts index 831e616..2891579 100644 --- a/src/__tests__/core/documentBuilder.test.ts +++ b/src/__tests__/core/documentBuilder.test.ts @@ -7,6 +7,8 @@ import { VerificationType, } from '@trustvc/w3c-issuer'; +const amoyRpcUrl = process.env.AMOY_RPC || 'https://rpc-amoy.polygon.technology'; + // ----------------------------- // Note: Dummy/test cryptographic key pairs for local development and CI/CD. // Used for signing/verifying credentials only. Not for production. Do not control funds. @@ -112,7 +114,7 @@ describe('DocumentBuilder data model 2.0 using ECDSA', () => { chain: 'amoy', chainId: 80002, tokenRegistry: '0x71D28767662cB233F887aD2Bb65d048d760bA694', - rpcProviderUrl: 'https://rpc-amoy.polygon.technology', + rpcProviderUrl: amoyRpcUrl, }), ).toThrow('Configuration Error: Document is already signed.'); }); @@ -124,7 +126,7 @@ describe('DocumentBuilder data model 2.0 using ECDSA', () => { chain: 'amoy', chainId: 80002, tokenRegistry: '0x71D28767662cB233F887aD2Bb65d048d760bA694', - rpcProviderUrl: 'https://rpc-amoy.polygon.technology', + rpcProviderUrl: amoyRpcUrl, }); expect(documentBuilder).toBeDefined(); }); @@ -143,7 +145,7 @@ describe('DocumentBuilder data model 2.0 using ECDSA', () => { chain: 'amoy', chainId: 80002, tokenRegistry: '0x71D28767662cB233F887aD2Bb65d048d760bA694', - rpcProviderUrl: 'https://rpc-amoy.polygon.technology', + rpcProviderUrl: amoyRpcUrl, url: 'https://trustvc.github.io/did/credentials/statuslist/1', index: 10, }), @@ -167,7 +169,7 @@ describe('DocumentBuilder data model 2.0 using ECDSA', () => { chain: 'amoy', chainId: 80002, tokenRegistry: '0x71D28767662cB233F887aD2Bb65d048d760bA694', - rpcProviderUrl: 'https://rpc-amoy.polygon.technology', + rpcProviderUrl: amoyRpcUrl, }); const signedDocument = await documentBuilder.sign(ECDSAtestPrivateKey); expect(signedDocument).toBeDefined(); @@ -195,7 +197,7 @@ describe('DocumentBuilder data model 2.0 using ECDSA', () => { chain: 'amoy', chainId: 80002, tokenRegistry: '0x71D28767662cB233F887aD2Bb65d048d760bA694', - rpcProviderUrl: 'https://rpc-amoy.polygon.technology', + rpcProviderUrl: amoyRpcUrl, }); const signedDocument = await documentBuilder.sign(bbs2023KeyPair, CryptoSuite.Bbs2023); expect(signedDocument).toBeDefined(); @@ -246,7 +248,7 @@ describe('DocumentBuilder data model 2.0 using ECDSA', () => { chain: 'amoy', chainId: 80002, tokenRegistry: '0x71D28767662cB233F887aD2Bb65d048d760bA694', - rpcProviderUrl: 'https://rpc-amoy.polygon.technology', + rpcProviderUrl: amoyRpcUrl, }); const signedDocument = await documentBuilder.sign(ECDSAtestPrivateKey); expect(signedDocument).toBeDefined(); @@ -260,7 +262,7 @@ describe('DocumentBuilder data model 2.0 using ECDSA', () => { chain: 'amoy', chainId: 80002, tokenRegistry: '0x71D28767662cB233F887aD2Bb65d048d760bA694', - rpcProviderUrl: 'https://rpc-amoy.polygon.technology', + rpcProviderUrl: amoyRpcUrl, }); const signedDocument = await documentBuilder.sign(bbs2023KeyPair, CryptoSuite.Bbs2023); expect(signedDocument).toBeDefined(); @@ -274,7 +276,7 @@ describe('DocumentBuilder data model 2.0 using ECDSA', () => { chain: 'unknown-chain', chainId: 999999, // Invalid chainId tokenRegistry: '0x71D28767662cB233F887aD2Bb65d048d760bA694', - rpcProviderUrl: 'https://rpc-amoy.polygon.technology', + rpcProviderUrl: amoyRpcUrl, }); await expect(documentBuilder.sign(ECDSAtestPrivateKey)).rejects.toThrow( 'Unsupported Chain: Chain ID 999999 is not supported.', diff --git a/src/__tests__/core/verify.test.ts b/src/__tests__/core/verify.test.ts index 1de01fa..c9c791e 100644 --- a/src/__tests__/core/verify.test.ts +++ b/src/__tests__/core/verify.test.ts @@ -72,6 +72,16 @@ describe.concurrent('W3C verify', () => { "status": "SKIPPED", "type": "DOCUMENT_STATUS", }, + { + "name": "ObligationRecords", + "reason": { + "code": 4, + "codeString": "SKIPPED", + "message": "Document does not have ObligationRecords status", + }, + "status": "SKIPPED", + "type": "DOCUMENT_STATUS", + }, { "name": "W3CEmptyCredentialStatus", "reason": { @@ -341,6 +351,16 @@ describe.concurrent('W3C verify', () => { "status": "VALID", "type": "DOCUMENT_STATUS", }, + { + "name": "ObligationRecords", + "reason": { + "code": 4, + "codeString": "SKIPPED", + "message": "Document does not have ObligationRecords status", + }, + "status": "SKIPPED", + "type": "DOCUMENT_STATUS", + }, { "name": "W3CEmptyCredentialStatus", "reason": { diff --git a/src/__tests__/obligation-registry-functions/ownerOf.test.ts b/src/__tests__/obligation-registry-functions/ownerOf.test.ts index 50c4b60..25f8c83 100644 --- a/src/__tests__/obligation-registry-functions/ownerOf.test.ts +++ b/src/__tests__/obligation-registry-functions/ownerOf.test.ts @@ -30,7 +30,6 @@ describe('ownerOfObligationRegistry', () => { { obligationRegistryAddress: MOCK_OBLIGATION_REGISTRY_ADDRESS }, wallet, { tokenId: '1' }, - {}, ); expect(owner).toBe(MOCK_OWNER_ADDRESS); @@ -38,7 +37,7 @@ describe('ownerOfObligationRegistry', () => { it('throws when registry address is missing', async () => { await expect( - ownerOfObligationRegistry({ obligationRegistryAddress: '' }, wallet, { tokenId: '1' }, {}), + ownerOfObligationRegistry({ obligationRegistryAddress: '' }, wallet, { tokenId: '1' }), ).rejects.toThrow('Obligation registry address is required'); }); }); diff --git a/src/obligation-registry-functions/ownerOf.ts b/src/obligation-registry-functions/ownerOf.ts index 7b03f9b..2cd7a5f 100644 --- a/src/obligation-registry-functions/ownerOf.ts +++ b/src/obligation-registry-functions/ownerOf.ts @@ -3,20 +3,13 @@ import { v5Contracts, v5SupportInterfaceIds } from '../token-registry-v5'; import { Signer as SignerV6, Contract as ContractV6 } from 'ethersV6'; import { Contract as ContractV5, Signer } from 'ethers'; import { getEthersContractFromProvider } from '../utils/ethers'; -import { - OwnerOfObligationTokenOptions, - OwnerOfObligationTokenParams, - TransactionOptions, -} from './types'; +import { OwnerOfObligationTokenOptions, OwnerOfObligationTokenParams } from './types'; const ownerOfObligationRegistry = async ( contractOptions: OwnerOfObligationTokenOptions, signer: Signer | SignerV6, params: OwnerOfObligationTokenParams, - // Kept for API parity with other obligation-registry functions. - _options: TransactionOptions, ): Promise => { - const { obligationRegistryAddress } = contractOptions; const { obligationRegistryAddress } = contractOptions; const { tokenId } = params; diff --git a/src/verify/fragments/document-status/transferableRecords/transferableRecordVerifier.ts b/src/verify/fragments/document-status/transferableRecords/transferableRecordVerifier.ts index b90b58f..f7fe71f 100644 --- a/src/verify/fragments/document-status/transferableRecords/transferableRecordVerifier.ts +++ b/src/verify/fragments/document-status/transferableRecords/transferableRecordVerifier.ts @@ -9,6 +9,7 @@ import { import { TransferableRecordsCredentialStatus } from '@trustvc/w3c-credential-status'; import * as w3cVC from '@trustvc/w3c-vc'; import { SignedVerifiableCredential } from '@trustvc/w3c-vc'; +import { isObligationRecordCredentialStatus } from '../../../../utils/documents/obligation'; import { TransferableRecordsErrorFragment, TransferableRecordsResultFragment, @@ -112,10 +113,7 @@ const test: VerifierType['test'] = ( w3cVC.isSignedDocument(document) && credentialStatuses.every( (cs: w3cVC.CredentialStatus) => - cs?.type === TRANSFERABLE_RECORDS_TYPE && - !!(cs as TransferableRecordsCredentialStatus).tokenRegistry && - !(cs as TransferableRecordsCredentialStatus & { obligationRegistry?: string }) - .obligationRegistry, + cs?.type === TRANSFERABLE_RECORDS_TYPE && !isObligationRecordCredentialStatus(cs), ) ) { return true; From 2650f6ebb0f286fb0876c4802a9d38fd44c5a56b Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Thu, 30 Jul 2026 15:53:14 +0530 Subject: [PATCH 11/42] Update README.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 41c71b8..3bccf08 100644 --- a/README.md +++ b/README.md @@ -771,7 +771,6 @@ 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 ``` From 70af72446efeb488f17a1c8cfe4ef5ed411b230d Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Thu, 30 Jul 2026 16:05:58 +0530 Subject: [PATCH 12/42] chore: update AMOY_RPC URL in tests and workflows for consistency --- .github/workflows/tests.yml | 2 +- src/__tests__/core/documentBuilder.test.ts | 2 +- src/__tests__/core/verify.amoy.test.ts | 2 +- src/__tests__/core/verify.test.ts | 2 +- src/obligation-registry-functions/status.ts | 3 +++ 5 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 33663e1..502c6c6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -37,7 +37,7 @@ jobs: env: ANKR_API_KEY: ${{ secrets.ANKR_API_KEY }} POLYGONSCAN_API_KEY: ${{ secrets.POLYGONSCAN_API_KEY }} - AMOY_RPC: ${{ secrets.AMOY_RPC }} + AMOY_RPC: https://polygon-amoy-bor-rpc.publicnode.com test-build: name: Test Build diff --git a/src/__tests__/core/documentBuilder.test.ts b/src/__tests__/core/documentBuilder.test.ts index 2891579..0c21c89 100644 --- a/src/__tests__/core/documentBuilder.test.ts +++ b/src/__tests__/core/documentBuilder.test.ts @@ -7,7 +7,7 @@ import { VerificationType, } from '@trustvc/w3c-issuer'; -const amoyRpcUrl = process.env.AMOY_RPC || 'https://rpc-amoy.polygon.technology'; +const amoyRpcUrl = process.env.AMOY_RPC || 'https://polygon-amoy-bor-rpc.publicnode.com'; // ----------------------------- // Note: Dummy/test cryptographic key pairs for local development and CI/CD. diff --git a/src/__tests__/core/verify.amoy.test.ts b/src/__tests__/core/verify.amoy.test.ts index 4152650..55c0986 100644 --- a/src/__tests__/core/verify.amoy.test.ts +++ b/src/__tests__/core/verify.amoy.test.ts @@ -9,7 +9,7 @@ import { w3cTransferableRecordMintedTests, } from './verify.polygon-network.helpers'; -const AMOY_RPC_URL = process.env.AMOY_RPC || 'https://rpc-amoy.polygon.technology/'; +const AMOY_RPC_URL = process.env.AMOY_RPC || 'https://polygon-amoy-bor-rpc.publicnode.com'; describe('Polygon Amoy (testnet) network support', () => { describe('CHAIN_ID and SUPPORTED_CHAINS', () => { diff --git a/src/__tests__/core/verify.test.ts b/src/__tests__/core/verify.test.ts index c9c791e..c2edd56 100644 --- a/src/__tests__/core/verify.test.ts +++ b/src/__tests__/core/verify.test.ts @@ -18,7 +18,7 @@ import { import { W3CCredentialStatusCode } from '../../verify/fragments/document-status/w3cCredentialStatus'; import { openAttestationDidSignedDocumentStatus } from '@tradetrust-tt/tt-verify'; -const providerUrl = process.env.AMOY_RPC || 'https://rpc-amoy.polygon.technology'; +const providerUrl = process.env.AMOY_RPC || 'https://polygon-amoy-bor-rpc.publicnode.com'; describe.concurrent('W3C verify', () => { describe.concurrent('W3C_VERIFIABLE_DOCUMENT', () => { diff --git a/src/obligation-registry-functions/status.ts b/src/obligation-registry-functions/status.ts index 6e55601..0b69a61 100644 --- a/src/obligation-registry-functions/status.ts +++ b/src/obligation-registry-functions/status.ts @@ -12,6 +12,7 @@ import { getObligationEscrowContract, resolveObligationEscrowAddress } from './u const getObligationRegistryStatus = async ( contractOptions: ObligationContractOptions, signer: Signer | SignerV6, + // Unused; escrow resolution uses contractOptions.tokenId (API parity). _params: ObligationTokenIdParams, options: ObligationStatusReadOptions = {}, ): Promise => { @@ -27,6 +28,7 @@ const getObligationRegistryStatus = async ( const isObligationRegistryRegistered = async ( contractOptions: ObligationContractOptions, signer: Signer | SignerV6, + // Unused; escrow resolution uses contractOptions.tokenId (API parity). _params: ObligationTokenIdParams, options: ObligationStatusReadOptions = {}, ): Promise => { @@ -41,6 +43,7 @@ const isObligationRegistryRegistered = async ( const getObligationEscrowTerminationReason = async ( contractOptions: ObligationContractOptions, signer: Signer | SignerV6, + // Unused; escrow resolution uses contractOptions.tokenId (API parity). _params: ObligationTokenIdParams, options: ObligationStatusReadOptions = {}, ): Promise => { From 4b2e604517a304d89074753b8e8c7cec53a428b9 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Thu, 30 Jul 2026 16:28:35 +0530 Subject: [PATCH 13/42] docs: update README to reflect changes in obligation registry imports --- README.md | 8 ++++---- src/index.ts | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3bccf08..4406e1e 100644 --- a/README.md +++ b/README.md @@ -767,7 +767,7 @@ For more information on Token Registry and Title Escrow contracts **version v5** > **New:** Obligation Registry supports electronic Bill of Exchange (BoE). It mirrors the classic Transferable Records pattern using **`TrustVCToken`** + **`ObligationEscrow`** (v5 only — no Obligation v4 path). > -> Import on-chain helpers from `@trustvc/trustvc/obligation-registry-functions`. Functions use the `*ObligationRegistry` suffix (e.g. `mintObligationRegistry`) so they never clash with classic ETR exports on `@trustvc/trustvc`. +> Import on-chain helpers from `@trustvc/trustvc` (same as Token Registry helpers). Functions use the `*ObligationRegistry` suffix (e.g. `mintObligationRegistry`) so they never clash with classic ETR exports (`mint`, `transferHolder`, …). **Lifecycle** @@ -777,7 +777,7 @@ Deploy factory + registry → Mint (Issued) → Accept (Accepted) / Reject (Reje **Role rules:** accept / reject / discharge require `beneficiary != holder`; `returnToIssuerObligationRegistry` requires dual role (`beneficiary == holder`). -**Status enums** (`@trustvc/trustvc/obligation-registry-functions`): +**Status enums** (from `@trustvc/trustvc`): | Enum | Values | |------|--------| @@ -805,7 +805,7 @@ import { getObligationRegistryStatus, getObligationEscrowTerminationReason, ownerOfObligationRegistry, -} from '@trustvc/trustvc/obligation-registry-functions'; +} from '@trustvc/trustvc'; // Deploy const { obligationRegistry, obligationEscrowFactoryAddress } = @@ -918,7 +918,7 @@ boeBuilder.obligationCredentialStatus({ }); ``` -This sets `credentialStatus.type` to `TransferableRecords` with an `obligationRegistry` field (not `tokenRegistry`). On-chain minting is separate — use `mintObligationRegistry` from `@trustvc/trustvc/obligation-registry-functions` (see [§7c](#c-obligation-registry-boe)). Verify with `verifyDocument` (ObligationRecords fragment) — see [§4](#obligation--boe-verifydocument). +This sets `credentialStatus.type` to `TransferableRecords` with an `obligationRegistry` field (not `tokenRegistry`). On-chain minting is separate — use `mintObligationRegistry` from `@trustvc/trustvc` (see [§7c](#c-obligation-registry-boe)). Verify with `verifyDocument` (ObligationRecords fragment) — see [§4](#obligation--boe-verifydocument). > ⚠️ **Disclaimer:** > These builders **do not mint** on-chain. Mint separately via `mint` (ETR) or `mintObligationRegistry` (BoE). diff --git a/src/index.ts b/src/index.ts index 8a66ac0..c700560 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,6 +34,7 @@ import { cancelTransaction } from './transaction'; export type { TypedContractMethod } from './token-registry-v5/typedContractMethod'; export type { CancelTransactionSigner } from './transaction'; export * from './token-registry-functions'; +export * from './obligation-registry-functions'; export * from './core'; export * from './open-attestation'; export * from './verify'; From abfff65c42c29fcf9371df97c185013ab6685ce1 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Thu, 30 Jul 2026 16:37:16 +0530 Subject: [PATCH 14/42] refactor: simplify obligation registry functions by utilizing createRemarkEscrowMethod --- .../lifecycle.ts | 67 +------------ src/obligation-registry-functions/mint.ts | 52 +++------- .../rejectTransfers.ts | 72 ++------------ .../returnToken.ts | 94 ++++--------------- src/obligation-registry-functions/status.ts | 73 ++++++-------- src/obligation-registry-functions/transfer.ts | 68 ++++++-------- src/obligation-registry-functions/utils.ts | 71 +++++++++++++- .../obligationRecordVerifier.ts | 64 +++---------- .../obligationRecords/verifierHelpers.ts | 53 +++++++++++ 9 files changed, 231 insertions(+), 383 deletions(-) create mode 100644 src/verify/fragments/document-status/obligationRecords/verifierHelpers.ts diff --git a/src/obligation-registry-functions/lifecycle.ts b/src/obligation-registry-functions/lifecycle.ts index bf84d5c..22b02d6 100644 --- a/src/obligation-registry-functions/lifecycle.ts +++ b/src/obligation-registry-functions/lifecycle.ts @@ -1,66 +1,7 @@ -import { Signer as SignerV6 } from 'ethersV6'; -import { ContractTransaction, Signer } from 'ethers'; -import { - getEncryptedRemarks, - getObligationEscrowContract, - resolveObligationEscrowAddress, - runStaticCall, - sendTransaction, -} from './utils'; -import { ObligationContractOptions, ObligationRemarkParams, TransactionOptions } from './types'; +import { createRemarkEscrowMethod } from './utils'; -const acceptObligationRegistry = async ( - contractOptions: ObligationContractOptions, - signer: Signer | SignerV6, - params: ObligationRemarkParams, - options: TransactionOptions, -): Promise => { - if (!signer.provider) throw new Error('Provider is required'); - - const obligationEscrowAddress = await resolveObligationEscrowAddress(contractOptions, signer); - const obligationEscrowContract = getObligationEscrowContract(obligationEscrowAddress, signer); - const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); - - await runStaticCall(obligationEscrowContract, 'accept', [encryptedRemarks], signer.provider); - return sendTransaction(obligationEscrowContract, 'accept', [encryptedRemarks], signer, options); -}; - -const rejectObligationRegistry = async ( - contractOptions: ObligationContractOptions, - signer: Signer | SignerV6, - params: ObligationRemarkParams, - options: TransactionOptions, -): Promise => { - if (!signer.provider) throw new Error('Provider is required'); - - const obligationEscrowAddress = await resolveObligationEscrowAddress(contractOptions, signer); - const obligationEscrowContract = getObligationEscrowContract(obligationEscrowAddress, signer); - const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); - - await runStaticCall(obligationEscrowContract, 'reject', [encryptedRemarks], signer.provider); - return sendTransaction(obligationEscrowContract, 'reject', [encryptedRemarks], signer, options); -}; - -const dischargeObligationRegistry = async ( - contractOptions: ObligationContractOptions, - signer: Signer | SignerV6, - params: ObligationRemarkParams, - options: TransactionOptions, -): Promise => { - if (!signer.provider) throw new Error('Provider is required'); - - const obligationEscrowAddress = await resolveObligationEscrowAddress(contractOptions, signer); - const obligationEscrowContract = getObligationEscrowContract(obligationEscrowAddress, signer); - const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); - - await runStaticCall(obligationEscrowContract, 'discharge', [encryptedRemarks], signer.provider); - return sendTransaction( - obligationEscrowContract, - 'discharge', - [encryptedRemarks], - signer, - options, - ); -}; +const acceptObligationRegistry = createRemarkEscrowMethod('accept'); +const rejectObligationRegistry = createRemarkEscrowMethod('reject'); +const dischargeObligationRegistry = createRemarkEscrowMethod('discharge'); export { acceptObligationRegistry, rejectObligationRegistry, dischargeObligationRegistry }; diff --git a/src/obligation-registry-functions/mint.ts b/src/obligation-registry-functions/mint.ts index 352ad9d..189fb4e 100644 --- a/src/obligation-registry-functions/mint.ts +++ b/src/obligation-registry-functions/mint.ts @@ -1,9 +1,7 @@ -import { checkSupportsInterface } from '../core'; -import { v5Contracts, v5SupportInterfaceIds } from '../token-registry-v5'; -import { Signer as SignerV6, Contract as ContractV6 } from 'ethersV6'; -import { Contract as ContractV5, ContractTransaction, Signer } from 'ethers'; -import { getEthersContractFromProvider } from '../utils/ethers'; -import { getEncryptedRemarks, getTxOptions, runStaticCall } from './utils'; +import { v5SupportInterfaceIds } from '../token-registry-v5'; +import { Signer as SignerV6 } from 'ethersV6'; +import { ContractTransaction, Signer } from 'ethers'; +import { executeRegistryMethod, getEncryptedRemarks } from './utils'; import { MintObligationTokenOptions, MintObligationTokenParams, TransactionOptions } from './types'; const mintObligationRegistry = async ( @@ -12,42 +10,14 @@ const mintObligationRegistry = async ( params: MintObligationTokenParams, options: TransactionOptions, ): Promise => { - const { obligationRegistryAddress } = contractOptions; - const { chainId, maxFeePerGas, maxPriorityFeePerGas } = options; - - if (!obligationRegistryAddress) throw new Error('Obligation registry address is required'); - if (!signer.provider) throw new Error('Provider is required'); - - const isSupported = await checkSupportsInterface( - obligationRegistryAddress, + const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); + return executeRegistryMethod( + contractOptions.obligationRegistryAddress, + signer, v5SupportInterfaceIds.TradeTrustTokenMintable, - signer.provider, - ); - if (!isSupported) { - throw new Error('Only TrustVCToken obligation registry is supported'); - } - - const { beneficiaryAddress, holderAddress, tokenId, remarks } = params; - const Contract = getEthersContractFromProvider(signer.provider); - const obligationRegistryContract = new Contract( - obligationRegistryAddress, - v5Contracts.TrustVCToken__factory.abi, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - signer as any, - ) as ContractV5 | ContractV6; - - const encryptedRemarks = getEncryptedRemarks(remarks, options.id); - const args = [beneficiaryAddress, holderAddress, tokenId, encryptedRemarks]; - - await runStaticCall(obligationRegistryContract, 'mint', args, signer.provider); - - const txOptions = await getTxOptions(signer, chainId, maxFeePerGas, maxPriorityFeePerGas); - return obligationRegistryContract.mint( - beneficiaryAddress, - holderAddress, - tokenId, - encryptedRemarks, - txOptions, + 'mint', + [params.beneficiaryAddress, params.holderAddress, params.tokenId, encryptedRemarks], + options, ); }; diff --git a/src/obligation-registry-functions/rejectTransfers.ts b/src/obligation-registry-functions/rejectTransfers.ts index bd7d35b..a5d8875 100644 --- a/src/obligation-registry-functions/rejectTransfers.ts +++ b/src/obligation-registry-functions/rejectTransfers.ts @@ -1,70 +1,10 @@ -import { Signer as SignerV6 } from 'ethersV6'; -import { ContractTransaction, Signer } from 'ethers'; -import { - getEncryptedRemarks, - getObligationEscrowContract, - resolveObligationEscrowAddress, - runStaticCall, - sendTransaction, -} from './utils'; -import { ObligationContractOptions, ObligationRemarkParams, TransactionOptions } from './types'; +import { createRemarkEscrowMethod } from './utils'; -const rejectTransferHolderObligationRegistry = async ( - contractOptions: ObligationContractOptions, - signer: Signer | SignerV6, - params: ObligationRemarkParams, - options: TransactionOptions, -): Promise => { - if (!signer.provider) throw new Error('Provider is required'); - - const obligationEscrowAddress = await resolveObligationEscrowAddress(contractOptions, signer); - const obligationEscrowContract = getObligationEscrowContract(obligationEscrowAddress, signer); - const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); - const args = [encryptedRemarks]; - - await runStaticCall(obligationEscrowContract, 'rejectTransferHolder', args, signer.provider); - return sendTransaction(obligationEscrowContract, 'rejectTransferHolder', args, signer, options); -}; - -const rejectTransferBeneficiaryObligationRegistry = async ( - contractOptions: ObligationContractOptions, - signer: Signer | SignerV6, - params: ObligationRemarkParams, - options: TransactionOptions, -): Promise => { - if (!signer.provider) throw new Error('Provider is required'); - - const obligationEscrowAddress = await resolveObligationEscrowAddress(contractOptions, signer); - const obligationEscrowContract = getObligationEscrowContract(obligationEscrowAddress, signer); - const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); - const args = [encryptedRemarks]; - - await runStaticCall(obligationEscrowContract, 'rejectTransferBeneficiary', args, signer.provider); - return sendTransaction( - obligationEscrowContract, - 'rejectTransferBeneficiary', - args, - signer, - options, - ); -}; - -const rejectTransferOwnersObligationRegistry = async ( - contractOptions: ObligationContractOptions, - signer: Signer | SignerV6, - params: ObligationRemarkParams, - options: TransactionOptions, -): Promise => { - if (!signer.provider) throw new Error('Provider is required'); - - const obligationEscrowAddress = await resolveObligationEscrowAddress(contractOptions, signer); - const obligationEscrowContract = getObligationEscrowContract(obligationEscrowAddress, signer); - const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); - const args = [encryptedRemarks]; - - await runStaticCall(obligationEscrowContract, 'rejectTransferOwners', args, signer.provider); - return sendTransaction(obligationEscrowContract, 'rejectTransferOwners', args, signer, options); -}; +const rejectTransferHolderObligationRegistry = createRemarkEscrowMethod('rejectTransferHolder'); +const rejectTransferBeneficiaryObligationRegistry = createRemarkEscrowMethod( + 'rejectTransferBeneficiary', +); +const rejectTransferOwnersObligationRegistry = createRemarkEscrowMethod('rejectTransferOwners'); export { rejectTransferHolderObligationRegistry, diff --git a/src/obligation-registry-functions/returnToken.ts b/src/obligation-registry-functions/returnToken.ts index 3eb19da..9f35928 100644 --- a/src/obligation-registry-functions/returnToken.ts +++ b/src/obligation-registry-functions/returnToken.ts @@ -1,42 +1,16 @@ -import { checkSupportsInterface } from '../core'; import { v5SupportInterfaceIds } from '../token-registry-v5'; import { Signer as SignerV6 } from 'ethersV6'; import { ContractTransaction, Signer } from 'ethers'; import { AcceptReturnedObligationOptions, AcceptReturnedObligationParams, - ObligationContractOptions, - ObligationRemarkParams, RejectReturnedObligationOptions, RejectReturnedObligationParams, TransactionOptions, } from './types'; -import { - getEncryptedRemarks, - getObligationEscrowContract, - getObligationRegistryContract, - getTxOptions, - resolveObligationEscrowAddress, - runStaticCall, - sendTransaction, -} from './utils'; +import { createRemarkEscrowMethod, executeRegistryMethod, getEncryptedRemarks } from './utils'; -const returnToIssuerObligationRegistry = async ( - contractOptions: ObligationContractOptions, - signer: Signer | SignerV6, - params: ObligationRemarkParams, - options: TransactionOptions, -): Promise => { - if (!signer.provider) throw new Error('Provider is required'); - - const obligationEscrowAddress = await resolveObligationEscrowAddress(contractOptions, signer); - const obligationEscrowContract = getObligationEscrowContract(obligationEscrowAddress, signer); - const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); - const args = [encryptedRemarks]; - - await runStaticCall(obligationEscrowContract, 'returnToIssuer', args, signer.provider); - return sendTransaction(obligationEscrowContract, 'returnToIssuer', args, signer, options); -}; +const returnToIssuerObligationRegistry = createRemarkEscrowMethod('returnToIssuer'); const acceptReturnedObligationRegistry = async ( contractOptions: AcceptReturnedObligationOptions, @@ -44,33 +18,15 @@ const acceptReturnedObligationRegistry = async ( params: AcceptReturnedObligationParams, options: TransactionOptions, ): Promise => { - const { obligationRegistryAddress } = contractOptions; - const { chainId, maxFeePerGas, maxPriorityFeePerGas } = options; - - if (!obligationRegistryAddress) throw new Error('Obligation registry address is required'); - if (!signer.provider) throw new Error('Provider is required'); - - const isSupported = await checkSupportsInterface( - obligationRegistryAddress, - v5SupportInterfaceIds.TradeTrustTokenBurnable, - signer.provider, - ); - if (!isSupported) { - throw new Error('Only TrustVCToken obligation registry is supported'); - } - - const { tokenId, remarks } = params; - const obligationRegistryContract = getObligationRegistryContract( - obligationRegistryAddress, + const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); + return executeRegistryMethod( + contractOptions.obligationRegistryAddress, signer, + v5SupportInterfaceIds.TradeTrustTokenBurnable, + 'burn', + [params.tokenId, encryptedRemarks], + options, ); - const encryptedRemarks = getEncryptedRemarks(remarks, options.id); - const args = [tokenId, encryptedRemarks]; - - await runStaticCall(obligationRegistryContract, 'burn', args, signer.provider); - - const txOptions = await getTxOptions(signer, chainId, maxFeePerGas, maxPriorityFeePerGas); - return obligationRegistryContract.burn(tokenId, encryptedRemarks, txOptions); }; const rejectReturnedObligationRegistry = async ( @@ -79,33 +35,15 @@ const rejectReturnedObligationRegistry = async ( params: RejectReturnedObligationParams, options: TransactionOptions, ): Promise => { - const { obligationRegistryAddress } = contractOptions; - const { chainId, maxFeePerGas, maxPriorityFeePerGas } = options; - - if (!obligationRegistryAddress) throw new Error('Obligation registry address is required'); - if (!signer.provider) throw new Error('Provider is required'); - - const isSupported = await checkSupportsInterface( - obligationRegistryAddress, - v5SupportInterfaceIds.TradeTrustTokenRestorable, - signer.provider, - ); - if (!isSupported) { - throw new Error('Only TrustVCToken obligation registry is supported'); - } - - const { tokenId, remarks } = params; - const obligationRegistryContract = getObligationRegistryContract( - obligationRegistryAddress, + const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); + return executeRegistryMethod( + contractOptions.obligationRegistryAddress, signer, + v5SupportInterfaceIds.TradeTrustTokenRestorable, + 'restore', + [params.tokenId, encryptedRemarks], + options, ); - const encryptedRemarks = getEncryptedRemarks(remarks, options.id); - const args = [tokenId, encryptedRemarks]; - - await runStaticCall(obligationRegistryContract, 'restore', args, signer.provider); - - const txOptions = await getTxOptions(signer, chainId, maxFeePerGas, maxPriorityFeePerGas); - return obligationRegistryContract.restore(tokenId, encryptedRemarks, txOptions); }; export { diff --git a/src/obligation-registry-functions/status.ts b/src/obligation-registry-functions/status.ts index 0b69a61..9abdccc 100644 --- a/src/obligation-registry-functions/status.ts +++ b/src/obligation-registry-functions/status.ts @@ -7,54 +7,43 @@ import { ObligationStatusReadOptions, ObligationTokenIdParams, } from './types'; -import { getObligationEscrowContract, resolveObligationEscrowAddress } from './utils'; +import { getEscrowContract } from './utils'; -const getObligationRegistryStatus = async ( +type EscrowViewReader = ( contractOptions: ObligationContractOptions, signer: Signer | SignerV6, - // Unused; escrow resolution uses contractOptions.tokenId (API parity). _params: ObligationTokenIdParams, - options: ObligationStatusReadOptions = {}, -): Promise => { - if (!signer.provider) throw new Error('Provider is required'); - - const obligationEscrowAddress = await resolveObligationEscrowAddress(contractOptions, signer); - const obligationEscrowContract = getObligationEscrowContract(obligationEscrowAddress, signer); - const status = await obligationEscrowContract.status({ blockTag: options.blockTag }); - - return Number(status) as ObligationDocumentStatus; + options?: ObligationStatusReadOptions, +) => Promise; + +const createEscrowViewReader = ( + read: ( + contract: Awaited>, + options: ObligationStatusReadOptions, + ) => Promise, +): EscrowViewReader => { + return async (contractOptions, signer, _params, options = {}) => { + // Escrow resolution uses contractOptions.tokenId (API parity with _params). + const contract = await getEscrowContract(contractOptions, signer); + return read(contract, options); + }; }; -const isObligationRegistryRegistered = async ( - contractOptions: ObligationContractOptions, - signer: Signer | SignerV6, - // Unused; escrow resolution uses contractOptions.tokenId (API parity). - _params: ObligationTokenIdParams, - options: ObligationStatusReadOptions = {}, -): Promise => { - if (!signer.provider) throw new Error('Provider is required'); - - const obligationEscrowAddress = await resolveObligationEscrowAddress(contractOptions, signer); - const obligationEscrowContract = getObligationEscrowContract(obligationEscrowAddress, signer); - - return obligationEscrowContract.isRegistered({ blockTag: options.blockTag }); -}; - -const getObligationEscrowTerminationReason = async ( - contractOptions: ObligationContractOptions, - signer: Signer | SignerV6, - // Unused; escrow resolution uses contractOptions.tokenId (API parity). - _params: ObligationTokenIdParams, - options: ObligationStatusReadOptions = {}, -): Promise => { - if (!signer.provider) throw new Error('Provider is required'); - - const obligationEscrowAddress = await resolveObligationEscrowAddress(contractOptions, signer); - const obligationEscrowContract = getObligationEscrowContract(obligationEscrowAddress, signer); - const reason = await obligationEscrowContract.terminationReason({ blockTag: options.blockTag }); - - return Number(reason) as ObligationEscrowTerminationReason; -}; +const getObligationRegistryStatus = createEscrowViewReader( + async (contract, options) => + Number(await contract.status({ blockTag: options.blockTag })) as ObligationDocumentStatus, +); + +const isObligationRegistryRegistered = createEscrowViewReader((contract, options) => + contract.isRegistered({ blockTag: options.blockTag }), +); + +const getObligationEscrowTerminationReason = createEscrowViewReader( + async (contract, options) => + Number( + await contract.terminationReason({ blockTag: options.blockTag }), + ) as ObligationEscrowTerminationReason, +); export { getObligationRegistryStatus, diff --git a/src/obligation-registry-functions/transfer.ts b/src/obligation-registry-functions/transfer.ts index 15d3422..429ce48 100644 --- a/src/obligation-registry-functions/transfer.ts +++ b/src/obligation-registry-functions/transfer.ts @@ -1,12 +1,6 @@ import { Signer as SignerV6 } from 'ethersV6'; import { ContractTransaction, Signer } from 'ethers'; -import { - getEncryptedRemarks, - getObligationEscrowContract, - resolveObligationEscrowAddress, - runStaticCall, - sendTransaction, -} from './utils'; +import { executeEscrowMethod, getEncryptedRemarks } from './utils'; import { NominateObligationParams, ObligationContractOptions, @@ -22,15 +16,14 @@ const transferHolderObligationRegistry = async ( params: TransferObligationHolderParams, options: TransactionOptions, ): Promise => { - if (!signer.provider) throw new Error('Provider is required'); - - const obligationEscrowAddress = await resolveObligationEscrowAddress(contractOptions, signer); - const obligationEscrowContract = getObligationEscrowContract(obligationEscrowAddress, signer); const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); - const args = [params.holderAddress, encryptedRemarks]; - - await runStaticCall(obligationEscrowContract, 'transferHolder', args, signer.provider); - return sendTransaction(obligationEscrowContract, 'transferHolder', args, signer, options); + return executeEscrowMethod( + contractOptions, + signer, + 'transferHolder', + [params.holderAddress, encryptedRemarks], + options, + ); }; const transferBeneficiaryObligationRegistry = async ( @@ -39,15 +32,14 @@ const transferBeneficiaryObligationRegistry = async ( params: TransferObligationBeneficiaryParams, options: TransactionOptions, ): Promise => { - if (!signer.provider) throw new Error('Provider is required'); - - const obligationEscrowAddress = await resolveObligationEscrowAddress(contractOptions, signer); - const obligationEscrowContract = getObligationEscrowContract(obligationEscrowAddress, signer); const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); - const args = [params.newBeneficiaryAddress, encryptedRemarks]; - - await runStaticCall(obligationEscrowContract, 'transferBeneficiary', args, signer.provider); - return sendTransaction(obligationEscrowContract, 'transferBeneficiary', args, signer, options); + return executeEscrowMethod( + contractOptions, + signer, + 'transferBeneficiary', + [params.newBeneficiaryAddress, encryptedRemarks], + options, + ); }; const nominateObligationRegistry = async ( @@ -56,15 +48,14 @@ const nominateObligationRegistry = async ( params: NominateObligationParams, options: TransactionOptions, ): Promise => { - if (!signer.provider) throw new Error('Provider is required'); - - const obligationEscrowAddress = await resolveObligationEscrowAddress(contractOptions, signer); - const obligationEscrowContract = getObligationEscrowContract(obligationEscrowAddress, signer); const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); - const args = [params.newBeneficiaryAddress, encryptedRemarks]; - - await runStaticCall(obligationEscrowContract, 'nominate', args, signer.provider); - return sendTransaction(obligationEscrowContract, 'nominate', args, signer, options); + return executeEscrowMethod( + contractOptions, + signer, + 'nominate', + [params.newBeneficiaryAddress, encryptedRemarks], + options, + ); }; const transferOwnersObligationRegistry = async ( @@ -73,15 +64,14 @@ const transferOwnersObligationRegistry = async ( params: TransferObligationOwnersParams, options: TransactionOptions, ): Promise => { - if (!signer.provider) throw new Error('Provider is required'); - - const obligationEscrowAddress = await resolveObligationEscrowAddress(contractOptions, signer); - const obligationEscrowContract = getObligationEscrowContract(obligationEscrowAddress, signer); const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); - const args = [params.newHolderAddress, params.newBeneficiaryAddress, encryptedRemarks]; - - await runStaticCall(obligationEscrowContract, 'transferOwners', args, signer.provider); - return sendTransaction(obligationEscrowContract, 'transferOwners', args, signer, options); + return executeEscrowMethod( + contractOptions, + signer, + 'transferOwners', + [params.newHolderAddress, params.newBeneficiaryAddress, encryptedRemarks], + options, + ); }; export { diff --git a/src/obligation-registry-functions/utils.ts b/src/obligation-registry-functions/utils.ts index bc00f4d..4ba1c4d 100644 --- a/src/obligation-registry-functions/utils.ts +++ b/src/obligation-registry-functions/utils.ts @@ -1,10 +1,10 @@ -import { encrypt, getObligationEscrowAddress } from '../core'; +import { encrypt, getObligationEscrowAddress, checkSupportsInterface } from '../core'; import { v5Contracts } from '../token-registry-v5'; import { getTxOptions } from '../token-registry-functions/utils'; import { Signer as SignerV6, Contract as ContractV6 } from 'ethersV6'; -import { Contract as ContractV5, Signer } from 'ethers'; +import { Contract as ContractV5, ContractTransaction, Signer } from 'ethers'; import { getEthersContractFromProvider, isV6EthersProvider } from '../utils/ethers'; -import { ObligationContractOptions } from './types'; +import { ObligationContractOptions, TransactionOptions } from './types'; export const getObligationRegistryContract = ( obligationRegistryAddress: string, @@ -118,4 +118,69 @@ export const sendTransaction = async ( return (contract as ContractV5)[method](...args, txOptions); }; +export const getEscrowContract = async ( + contractOptions: ObligationContractOptions, + signer: Signer | SignerV6, +): Promise => { + if (!signer.provider) throw new Error('Provider is required'); + const address = await resolveObligationEscrowAddress(contractOptions, signer); + return getObligationEscrowContract(address, signer); +}; + +export const executeEscrowMethod = async ( + contractOptions: ObligationContractOptions, + signer: Signer | SignerV6, + method: string, + args: unknown[], + options: TransactionOptions, +): Promise => { + const obligationEscrowContract = await getEscrowContract(contractOptions, signer); + await runStaticCall(obligationEscrowContract, method, args, signer.provider); + return sendTransaction(obligationEscrowContract, method, args, signer, options); +}; + +export const createRemarkEscrowMethod = + (method: string) => + ( + contractOptions: ObligationContractOptions, + signer: Signer | SignerV6, + params: { remarks?: string }, + options: TransactionOptions, + ): Promise => + executeEscrowMethod( + contractOptions, + signer, + method, + [getEncryptedRemarks(params.remarks, options.id)], + options, + ); + +export const executeRegistryMethod = async ( + obligationRegistryAddress: string, + signer: Signer | SignerV6, + interfaceId: string, + method: string, + args: unknown[], + options: TransactionOptions, +): Promise => { + if (!obligationRegistryAddress) throw new Error('Obligation registry address is required'); + if (!signer.provider) throw new Error('Provider is required'); + + const isSupported = await checkSupportsInterface( + obligationRegistryAddress, + interfaceId, + signer.provider, + ); + if (!isSupported) { + throw new Error('Only TrustVCToken obligation registry is supported'); + } + + const obligationRegistryContract = getObligationRegistryContract( + obligationRegistryAddress, + signer, + ); + await runStaticCall(obligationRegistryContract, method, args, signer.provider); + return sendTransaction(obligationRegistryContract, method, args, signer, options); +}; + export { getTxOptions }; diff --git a/src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.ts b/src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.ts index 166bdd2..bde5732 100644 --- a/src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.ts +++ b/src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.ts @@ -11,12 +11,15 @@ import * as w3cVC from '@trustvc/w3c-vc'; import { SignedVerifiableCredential } from '@trustvc/w3c-vc'; import { isObligationRecordCredentialStatus } from '../../../../utils/documents/obligation'; import { - ObligationRecordsErrorFragment, ObligationRecordsResultFragment, ObligationRecordsVerificationFragment, VerifierType, } from './obligationRecordVerifier.types'; import { isTokenMintedOnObligationRegistry } from './utils'; +import { + createObligationRecordsSkipFragment, + toObligationRecordsErrorFragment, +} from './verifierHelpers'; export const OBLIGATION_RECORDS_TYPE = 'ObligationRecords'; const type = 'DOCUMENT_STATUS'; @@ -88,21 +91,8 @@ const verify: VerifierType['verify'] = async ( return result; }; -const skip: VerifierType['skip'] = async () => { - return { - status: 'SKIPPED', - type, - name, - reason: { - code: OpenAttestationEthereumTokenRegistryStatusCode.SKIPPED, - codeString: - OpenAttestationEthereumTokenRegistryStatusCode[ - OpenAttestationEthereumTokenRegistryStatusCode.SKIPPED - ], - message: `Document does not have ObligationRecords status`, - }, - }; -}; +const skip: VerifierType['skip'] = async () => + createObligationRecordsSkipFragment(name, 'Document does not have ObligationRecords status'); const test: VerifierType['test'] = ( document: DocumentsToVerify | SignedVerifiableCredential, @@ -112,14 +102,12 @@ const test: VerifierType['test'] = ( ? doc?.credentialStatus : [doc?.credentialStatus]; - if ( + return Boolean( w3cVC.isSignedDocument(document) && - credentialStatuses.every((cs: w3cVC.CredentialStatus) => isObligationRecordCredentialStatus(cs)) - ) { - return true; - } - - return false; + credentialStatuses.every((cs: w3cVC.CredentialStatus) => + isObligationRecordCredentialStatus(cs), + ), + ); }; export const credentialStatusObligationRecordVerifier: VerifierType = { @@ -128,34 +116,8 @@ export const credentialStatusObligationRecordVerifier: VerifierType = { verify: async (...args): Promise => { try { return await verify(...args); - } catch (e: unknown) { - if (e instanceof CodedError) { - const err: ObligationRecordsErrorFragment = { - name, - type, - status: 'ERROR' as const, - reason: { - code: e.code, - codeString: e.codeString, - message: e.message, - }, - }; - return err; - } - - return { - name, - type, - status: 'ERROR' as const, - reason: { - code: OpenAttestationEthereumTokenRegistryStatusCode.UNEXPECTED_ERROR, - codeString: - OpenAttestationEthereumTokenRegistryStatusCode[ - OpenAttestationEthereumTokenRegistryStatusCode.UNEXPECTED_ERROR - ], - message: e instanceof Error ? e.message : 'An unexpected error occurred', - }, - }; + } catch (error: unknown) { + return toObligationRecordsErrorFragment(name, error); } }, }; diff --git a/src/verify/fragments/document-status/obligationRecords/verifierHelpers.ts b/src/verify/fragments/document-status/obligationRecords/verifierHelpers.ts new file mode 100644 index 0000000..439c8a6 --- /dev/null +++ b/src/verify/fragments/document-status/obligationRecords/verifierHelpers.ts @@ -0,0 +1,53 @@ +import { + CodedError, + OpenAttestationEthereumTokenRegistryStatusCode, +} from '@tradetrust-tt/tt-verify'; +import { ObligationRecordsErrorFragment } from './obligationRecordVerifier.types'; + +const DOCUMENT_STATUS_TYPE = 'DOCUMENT_STATUS' as const; + +export const createObligationRecordsSkipFragment = (name: string, message: string) => ({ + status: 'SKIPPED' as const, + type: DOCUMENT_STATUS_TYPE, + name, + reason: { + code: OpenAttestationEthereumTokenRegistryStatusCode.SKIPPED, + codeString: + OpenAttestationEthereumTokenRegistryStatusCode[ + OpenAttestationEthereumTokenRegistryStatusCode.SKIPPED + ], + message, + }, +}); + +export const toObligationRecordsErrorFragment = ( + name: string, + error: unknown, +): ObligationRecordsErrorFragment => { + if (error instanceof CodedError) { + return { + name, + type: DOCUMENT_STATUS_TYPE, + status: 'ERROR' as const, + reason: { + code: error.code, + codeString: error.codeString, + message: error.message, + }, + }; + } + + return { + name, + type: DOCUMENT_STATUS_TYPE, + status: 'ERROR' as const, + reason: { + code: OpenAttestationEthereumTokenRegistryStatusCode.UNEXPECTED_ERROR, + codeString: + OpenAttestationEthereumTokenRegistryStatusCode[ + OpenAttestationEthereumTokenRegistryStatusCode.UNEXPECTED_ERROR + ], + message: error instanceof Error ? error.message : 'An unexpected error occurred', + }, + }; +}; From 2f87b30158a2b3bba77aafb41e393c0ec66b0071 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Thu, 30 Jul 2026 16:44:29 +0530 Subject: [PATCH 15/42] refactor: enhance null safety and streamline code in documentBuilder and obligation record verifier --- src/core/documentBuilder.ts | 9 ++++----- src/obligation-registry-functions/deploy.ts | 5 ++--- src/utils/documents/obligation.ts | 2 +- .../obligationRecords/obligationRecordVerifier.ts | 8 +++----- .../document-status/obligationRecords/utils.ts | 3 ++- src/verify/fragments/index.ts | 11 +++++------ 6 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/core/documentBuilder.ts b/src/core/documentBuilder.ts index 770b49c..eb210cb 100644 --- a/src/core/documentBuilder.ts +++ b/src/core/documentBuilder.ts @@ -322,11 +322,10 @@ export class DocumentBuilder { config: Partial, ): config is W3CObligationRecordsConfig { return ( - config && - typeof config.obligationRegistry === 'string' && - typeof config.chain === 'string' && - typeof config.chainId === 'number' && - typeof config.rpcProviderUrl === 'string' + typeof config?.obligationRegistry === 'string' && + typeof config?.chain === 'string' && + typeof config?.chainId === 'number' && + typeof config?.rpcProviderUrl === 'string' ); } diff --git a/src/obligation-registry-functions/deploy.ts b/src/obligation-registry-functions/deploy.ts index d195b5b..5196af4 100644 --- a/src/obligation-registry-functions/deploy.ts +++ b/src/obligation-registry-functions/deploy.ts @@ -1,6 +1,5 @@ import { v5Contracts } from '../token-registry-v5'; -import { getTxOptions } from '../token-registry-functions/utils'; -import { getChainIdSafe } from '../token-registry-functions/utils'; +import { getChainIdSafe, getTxOptions } from '../token-registry-functions/utils'; import { CHAIN_ID } from '../utils'; import { getEthersContractFactoryFromProvider, isV6EthersProvider } from '../utils/ethers'; import { @@ -44,7 +43,7 @@ const getDeployedAddress = ( } const contractAddress = - 'contractAddress' in receipt && receipt.contractAddress ? receipt.contractAddress : undefined; + 'contractAddress' in receipt ? receipt.contractAddress || undefined : undefined; if (!contractAddress) { throw new Error('Unable to resolve deployed contract address from receipt'); diff --git a/src/utils/documents/obligation.ts b/src/utils/documents/obligation.ts index 555af41..b8ee189 100644 --- a/src/utils/documents/obligation.ts +++ b/src/utils/documents/obligation.ts @@ -6,7 +6,7 @@ import { WrappedOrSignedOpenAttestationDocument } from './index'; export const isObligationRecordCredentialStatus = ( credentialStatus: CredentialStatus | undefined, ): credentialStatus is ObligationRecordsCredentialStatus => { - if (!credentialStatus || credentialStatus.type !== TRANSFERABLE_RECORDS_TYPE) { + if (credentialStatus?.type !== TRANSFERABLE_RECORDS_TYPE) { return false; } diff --git a/src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.ts b/src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.ts index bde5732..4e707b6 100644 --- a/src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.ts +++ b/src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.ts @@ -51,10 +51,8 @@ const verify: VerifierType['verify'] = async ( ); } - if ( - !credentialStatus?.tokenNetwork || - credentialStatus?.tokenNetwork?.chainId === undefined - ) { + const chainId = credentialStatus.tokenNetwork?.chainId; + if (chainId === undefined) { throw new CodedError( "Document's credentialStatus does not have tokenNetwork.chainId", OpenAttestationEthereumTokenRegistryStatusCode.UNRECOGNIZED_DOCUMENT, @@ -68,7 +66,7 @@ const verify: VerifierType['verify'] = async ( obligationRegistryAddress: credentialStatus.obligationRegistry, tokenId, provider, - chainId: credentialStatus.tokenNetwork.chainId, + chainId, }); }), ); diff --git a/src/verify/fragments/document-status/obligationRecords/utils.ts b/src/verify/fragments/document-status/obligationRecords/utils.ts index a3c0ce7..dbf7a10 100644 --- a/src/verify/fragments/document-status/obligationRecords/utils.ts +++ b/src/verify/fragments/document-status/obligationRecords/utils.ts @@ -29,7 +29,8 @@ export const isTokenMintedOnObligationRegistry = async ({ }): Promise => { if (chainId !== undefined) { const network = await provider.getNetwork(); - const expectedChainId = typeof chainId === 'string' ? parseInt(chainId, 10) : Number(chainId); + const expectedChainId = + typeof chainId === 'string' ? Number.parseInt(chainId, 10) : Number(chainId); if (!Number.isFinite(expectedChainId) || network.chainId !== expectedChainId) { return { diff --git a/src/verify/fragments/index.ts b/src/verify/fragments/index.ts index c73082a..3b313af 100644 --- a/src/verify/fragments/index.ts +++ b/src/verify/fragments/index.ts @@ -12,19 +12,13 @@ import { credentialStatusTransferableRecordVerifier, TRANSFERABLE_RECORDS_TYPE, } from './document-status/transferableRecords/transferableRecordVerifier'; -import { - credentialStatusObligationRecordVerifier, - OBLIGATION_RECORDS_TYPE, -} from './document-status/obligationRecords/obligationRecordVerifier'; import { w3cCredentialStatus } from './document-status/w3cCredentialStatus'; import { w3cIssuerIdentity } from './issuer-identity/w3cIssuerIdentity'; import { w3cEmptyCredentialStatus } from './document-status/w3cEmptyCredentialStatus'; export { TRANSFERABLE_RECORDS_TYPE, - OBLIGATION_RECORDS_TYPE, credentialStatusTransferableRecordVerifier, - credentialStatusObligationRecordVerifier, openAttestationDidSignedDocumentStatus, openAttestationDnsDidIdentityProof, openAttestationDnsTxtIdentityProof, @@ -37,3 +31,8 @@ export { w3cSignatureIntegrity, ecdsaW3CSignatureIntegrity, }; + +export { + credentialStatusObligationRecordVerifier, + OBLIGATION_RECORDS_TYPE, +} from './document-status/obligationRecords/obligationRecordVerifier'; From cca932cdfd359edd9854ea8aace0ae14c8a409d7 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Thu, 30 Jul 2026 16:55:07 +0530 Subject: [PATCH 16/42] refactor: make properties readonly in W3CObligationRecordsConfig --- src/core/documentBuilder.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/core/documentBuilder.ts b/src/core/documentBuilder.ts index eb210cb..4767dd0 100644 --- a/src/core/documentBuilder.ts +++ b/src/core/documentBuilder.ts @@ -63,10 +63,10 @@ export interface W3CTransferableRecordsConfig { * @property {string} rpcProviderUrl - The RPC endpoint URL for interacting with the blockchain. */ export interface W3CObligationRecordsConfig { - chain: string; - chainId: number; - obligationRegistry: string; - rpcProviderUrl: string; + readonly chain: string; + readonly chainId: number; + readonly obligationRegistry: string; + readonly rpcProviderUrl: string; } /** @@ -105,7 +105,7 @@ export interface SignOptions { */ export class DocumentBuilder { private document: Partial; // Holds the document to be built and signed. - private documentType: string = 'w3c'; // Default to W3C + private readonly documentType: string = 'w3c'; // Default to W3C private selectedStatusType: | 'transferableRecords' | 'obligationRecords' @@ -113,7 +113,7 @@ export class DocumentBuilder { | null = null; // Tracks selected status type. private statusConfig: Partial = {}; // Configuration for the credential status. private rpcProviderUrl: string; // Holds the RPC provider URL for verifying token registry. - private requiredFields: string[] = ['credentialSubject']; // Required fields that must be present in the document. + private readonly requiredFields: string[] = ['credentialSubject']; // Required fields that must be present in the document. private isSigned: boolean = false; // Tracks if a document is signed private isDerived: boolean = false; // Tracks if a document is derived /** From caa4d6770853bd96d19c690d1c4560da93b04fbb Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Thu, 30 Jul 2026 17:09:16 +0530 Subject: [PATCH 17/42] refactor: update chain ID comparison logic and improve error handling in obligation registry utility --- .../obligationRecords/utils.ts | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/verify/fragments/document-status/obligationRecords/utils.ts b/src/verify/fragments/document-status/obligationRecords/utils.ts index dbf7a10..010c51e 100644 --- a/src/verify/fragments/document-status/obligationRecords/utils.ts +++ b/src/verify/fragments/document-status/obligationRecords/utils.ts @@ -5,17 +5,9 @@ import { OpenAttestationEthereumTokenRegistryStatusCode, ValidTokenRegistryStatus, } from '@tradetrust-tt/tt-verify'; -import { constants, errors, providers } from 'ethers'; +import { constants, providers } from 'ethers'; import { decodeError } from '../transferableRecords/utils'; -type EthersError = { - message?: string; - data?: string; - method?: string; - reason?: string; - code?: errors; -}; - export const isTokenMintedOnObligationRegistry = async ({ obligationRegistryAddress, tokenId, @@ -31,8 +23,10 @@ export const isTokenMintedOnObligationRegistry = async ({ const network = await provider.getNetwork(); const expectedChainId = typeof chainId === 'string' ? Number.parseInt(chainId, 10) : Number(chainId); + // ethers v6 returns chainId as bigint + const actualChainId = Number(network.chainId); - if (!Number.isFinite(expectedChainId) || network.chainId !== expectedChainId) { + if (!Number.isFinite(expectedChainId) || actualChainId !== expectedChainId) { return { minted: false, address: obligationRegistryAddress, @@ -42,7 +36,7 @@ export const isTokenMintedOnObligationRegistry = async ({ OpenAttestationEthereumTokenRegistryStatusCode[ OpenAttestationEthereumTokenRegistryStatusCode.UNRECOGNIZED_DOCUMENT ], - message: `Provider chain ID ${network.chainId} does not match credentialStatus.tokenNetwork.chainId ${expectedChainId}`, + message: `Provider chain ID ${actualChainId} does not match credentialStatus.tokenNetwork.chainId ${expectedChainId}`, }, }; } @@ -72,19 +66,23 @@ export const isTokenMintedOnObligationRegistry = async ({ }, }; } catch (error: unknown) { - if (error instanceof CodedError) { - throw error; - } - if ((error as EthersError).code !== errors.CALL_EXCEPTION) { - throw error; + // Same shape as transferableRecords: map ownerOf failures to DOCUMENT_NOT_MINTED via decodeError. + // If decodeError rethrows (e.g. ethers v6 BAD_DATA with hex-only revert data), still treat as not minted. + let message: string; + try { + message = decodeError(error); + } catch (decodedError) { + if (decodedError instanceof CodedError) { + throw decodedError; + } + message = `Document ${tokenId} has not been issued under contract ${obligationRegistryAddress}`; } - const decodedMessage = decodeError(error as EthersError); return { minted: false, address: obligationRegistryAddress, reason: { - message: decodedMessage, + message, code: OpenAttestationEthereumTokenRegistryStatusCode.DOCUMENT_NOT_MINTED, codeString: OpenAttestationEthereumTokenRegistryStatusCode[ From 02de09f0db97270fa3f7356f54bdc29d9a367b74 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Thu, 30 Jul 2026 17:26:05 +0530 Subject: [PATCH 18/42] refactor: add ObligationEscrow support to fetchEscrowTransfers and update event handling --- src/__tests__/fixtures/endorsement-chain.ts | 2 +- .../endorsement-chain/fetchEscrowTransfer.ts | 84 +++++++++++++++++-- src/core/endorsement-chain/helpers.ts | 4 + src/core/endorsement-chain/types.ts | 6 +- .../endorsement-chain/useEndorsementChain.ts | 21 ++++- .../obligationRecords/utils.ts | 3 +- 6 files changed, 104 insertions(+), 16 deletions(-) diff --git a/src/__tests__/fixtures/endorsement-chain.ts b/src/__tests__/fixtures/endorsement-chain.ts index 4e9b0a9..3c9087b 100644 --- a/src/__tests__/fixtures/endorsement-chain.ts +++ b/src/__tests__/fixtures/endorsement-chain.ts @@ -3836,7 +3836,7 @@ export const testCases = [ description: 'Invalid Token Registry Version - Amoy', tokenRegistryAddress: '0x96cc41e7007dee20eb409586e2e8206d5053219b', tokenId: '0xd97a8af5c38157b95c558b7801862f4b53171149926d76d0c5b2b279016eed0a', - expectedError: 'Only Token Registry V4/V5 is supported', + expectedError: 'Only Token Registry V4/V5 or Obligation Registry is supported', timeout: 180_000, }, ]; diff --git a/src/core/endorsement-chain/fetchEscrowTransfer.ts b/src/core/endorsement-chain/fetchEscrowTransfer.ts index 6b822b8..0b58669 100644 --- a/src/core/endorsement-chain/fetchEscrowTransfer.ts +++ b/src/core/endorsement-chain/fetchEscrowTransfer.ts @@ -7,6 +7,7 @@ import { import { TitleEscrow__factory as TitleEscrowFactoryV5, TitleEscrow as TitleEscrowV5, + ObligationEscrow__factory, } from '../../token-registry-v5/contracts'; import { getEthersContractFromProvider } from '../../utils/ethers'; import { @@ -51,12 +52,34 @@ export const fetchEscrowTransfersV5 = async ( // eslint-disable-next-line @typescript-eslint/no-explicit-any provider as any, ); - const holderChangeLogsDeferred = await fetchAllTransfers( - titleEscrowContract, - titleEscrowAddress, - tokenRegistryAddress, + return fetchAllTransfers(titleEscrowContract, titleEscrowAddress, tokenRegistryAddress); +}; + +/** + * ObligationEscrow shares Title Escrow V5 transfer events and adds status lifecycle events. + * @param {Provider | ethersV6.Provider} provider - Ethers provider + * @param {string} obligationEscrowAddress - ObligationEscrow contract address + * @param {string} [obligationRegistryAddress] - Obligation registry (TrustVCToken) address + * @returns {Promise} - Transfer and status events + */ +export const fetchEscrowTransfersObligation = async ( + provider: Provider | ethersV6.Provider, + obligationEscrowAddress: string, + obligationRegistryAddress?: string, +): Promise => { + const Contract = getEthersContractFromProvider(provider); + const obligationEscrowContract = new Contract( + obligationEscrowAddress, + ObligationEscrow__factory.abi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + provider as any, + ); + return fetchAllTransfers( + obligationEscrowContract, + obligationEscrowAddress, + obligationRegistryAddress, + true, ); - return holderChangeLogsDeferred; }; const getParsedLogs = ( @@ -111,16 +134,18 @@ const fetchHolderTransfers = async ( }; /** - * Retrieve all V5 events - * @param {TitleEscrowV5} titleEscrowContract - Contract; Updated type to Contract to make it competible with ethers v5 - * @param {string} titleEscrowAddress - Title escrow address - * @param {string} tokenRegistryAddress - Token registry address + * Retrieve all V5 / ObligationEscrow events + * @param {ethers.Contract | ethersV6.Contract} titleEscrowContract - Escrow contract + * @param {string} titleEscrowAddress - Escrow address + * @param {string} tokenRegistryAddress - Registry address + * @param {boolean} includeObligationStatus - When true, also collect ObligationEscrow status events * @returns {Promise<(TitleEscrowTransferEvent | TokenTransferEvent)[]>} - Array of events */ const fetchAllTransfers = async ( titleEscrowContract: ethers.Contract | ethersV6.Contract, titleEscrowAddress?: string, tokenRegistryAddress?: string, + includeObligationStatus = false, ): Promise<(TitleEscrowTransferEvent | TokenTransferEvent)[]> => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const allFilters: any[] = [ @@ -134,6 +159,15 @@ const fetchAllTransfers = async ( titleEscrowContract.filters.RejectTransferHolder, titleEscrowContract.filters.Shred, ]; + + if (includeObligationStatus) { + allFilters.push( + titleEscrowContract.filters.StatusInitialized, + titleEscrowContract.filters.StatusAccepted, + titleEscrowContract.filters.StatusRejected, + titleEscrowContract.filters.StatusDischarged, + ); + } // eslint-disable-next-line @typescript-eslint/no-explicit-any const allLogs: any = await Promise.all( allFilters.map(async (filter) => { @@ -237,6 +271,38 @@ const fetchAllTransfers = async ( transactionIndex: event.transactionIndex, remark: event.args?.remark, } as TokenTransferEvent; + } else if (event?.name === 'StatusInitialized') { + return { + type: 'STATUS_INITIALIZED', + blockNumber: event.blockNumber, + transactionHash: event.transactionHash, + transactionIndex: event.transactionIndex, + remark: event.args?.remark, + } as TitleEscrowTransferEvent; + } else if (event?.name === 'StatusAccepted') { + return { + type: 'STATUS_ACCEPTED', + blockNumber: event.blockNumber, + transactionHash: event.transactionHash, + transactionIndex: event.transactionIndex, + remark: event.args?.remark, + } as TitleEscrowTransferEvent; + } else if (event?.name === 'StatusRejected') { + return { + type: 'STATUS_REJECTED', + blockNumber: event.blockNumber, + transactionHash: event.transactionHash, + transactionIndex: event.transactionIndex, + remark: event.args?.remark, + } as TitleEscrowTransferEvent; + } else if (event?.name === 'StatusDischarged') { + return { + type: 'STATUS_DISCHARGED', + blockNumber: event.blockNumber, + transactionHash: event.transactionHash, + transactionIndex: event.transactionIndex, + remark: event.args?.remark, + } as TitleEscrowTransferEvent; } return undefined; diff --git a/src/core/endorsement-chain/helpers.ts b/src/core/endorsement-chain/helpers.ts index e59e406..ee68312 100644 --- a/src/core/endorsement-chain/helpers.ts +++ b/src/core/endorsement-chain/helpers.ts @@ -87,6 +87,10 @@ const identifyEventTypeFromLogs = (groupedEvents: TransferBaseEvent[]): Transfer for (const event of groupedEvents) { if ( [ + 'STATUS_INITIALIZED', + 'STATUS_ACCEPTED', + 'STATUS_REJECTED', + 'STATUS_DISCHARGED', 'INITIAL', 'RETURNED_TO_ISSUER', 'RETURN_TO_ISSUER_ACCEPTED', diff --git a/src/core/endorsement-chain/types.ts b/src/core/endorsement-chain/types.ts index e13d430..c1a5184 100644 --- a/src/core/endorsement-chain/types.ts +++ b/src/core/endorsement-chain/types.ts @@ -46,7 +46,11 @@ export type TitleEscrowTransferEventType = | 'TRANSFER_OWNERS' | 'REJECT_TRANSFER_BENEFICIARY' // V5 | 'REJECT_TRANSFER_HOLDER' // V5 - | 'REJECT_TRANSFER_OWNERS'; // V5 + | 'REJECT_TRANSFER_OWNERS' // V5 + | 'STATUS_INITIALIZED' // ObligationEscrow + | 'STATUS_ACCEPTED' // ObligationEscrow + | 'STATUS_REJECTED' // ObligationEscrow + | 'STATUS_DISCHARGED'; // ObligationEscrow export interface TokenTransferEvent extends TransferBaseEvent { type: TokenTransferEventType; diff --git a/src/core/endorsement-chain/useEndorsementChain.ts b/src/core/endorsement-chain/useEndorsementChain.ts index 5d8695d..4ffde00 100644 --- a/src/core/endorsement-chain/useEndorsementChain.ts +++ b/src/core/endorsement-chain/useEndorsementChain.ts @@ -7,6 +7,7 @@ import { decrypt } from '../decrypt'; import { fetchEscrowTransfersV4, fetchEscrowTransfersV5, + fetchEscrowTransfersObligation, } from '../endorsement-chain/fetchEscrowTransfer'; import { fetchTokenTransfers } from '../endorsement-chain/fetchTokenTransfer'; import { mergeTransfersV4, mergeTransfersV5 } from '../endorsement-chain/helpers'; @@ -19,6 +20,8 @@ export const TitleEscrowInterface = { V5: supportInterfaceIdsV5.TitleEscrow, }; +export const ObligationEscrowInterface = supportInterfaceIdsV5.ObligationEscrow; + // Helper to fetch Title Escrow Factory Address const getTitleEscrowFactoryAddress = async ( tokenRegistryAddress: string, @@ -206,7 +209,7 @@ export const fetchEndorsementChain = async ( const resolvedTitleEscrowAddress = titleEscrowAddress ?? (await getTitleEscrowAddress(tokenRegistryAddress, tokenId, provider)); - const [isV4, isV5] = await Promise.all([ + const [isV4, isV5, isObligation] = await Promise.all([ isTitleEscrowVersion({ titleEscrowAddress: resolvedTitleEscrowAddress, versionInterface: TitleEscrowInterface.V4, @@ -217,10 +220,15 @@ export const fetchEndorsementChain = async ( versionInterface: TitleEscrowInterface.V5, provider, }), + isTitleEscrowVersion({ + titleEscrowAddress: resolvedTitleEscrowAddress, + versionInterface: ObligationEscrowInterface, + provider, + }), ]); - if (!isV4 && !isV5) { - throw new Error('Only Token Registry V4/V5 is supported'); + if (!isV4 && !isV5 && !isObligation) { + throw new Error('Only Token Registry V4/V5 or Obligation Registry is supported'); } let transferEvents: TransferBaseEvent[] = []; @@ -232,6 +240,13 @@ export const fetchEndorsementChain = async ( ]); transferEvents = mergeTransfersV4([...titleEscrowLogs, ...tokenLogs]); + } else if (isObligation) { + const obligationEscrowLogs = await fetchEscrowTransfersObligation( + provider, + resolvedTitleEscrowAddress, + tokenRegistryAddress, + ); + transferEvents = mergeTransfersV5(obligationEscrowLogs); } else if (isV5) { const titleEscrowLogs = await fetchEscrowTransfersV5( provider, diff --git a/src/verify/fragments/document-status/obligationRecords/utils.ts b/src/verify/fragments/document-status/obligationRecords/utils.ts index 010c51e..47a9f6f 100644 --- a/src/verify/fragments/document-status/obligationRecords/utils.ts +++ b/src/verify/fragments/document-status/obligationRecords/utils.ts @@ -21,8 +21,7 @@ export const isTokenMintedOnObligationRegistry = async ({ }): Promise => { if (chainId !== undefined) { const network = await provider.getNetwork(); - const expectedChainId = - typeof chainId === 'string' ? Number.parseInt(chainId, 10) : Number(chainId); + const expectedChainId = Number(chainId); // ethers v6 returns chainId as bigint const actualChainId = Number(network.chainId); From b32f6791e71eb6aed393b5df29c3ce777282ea5f Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Thu, 30 Jul 2026 17:35:52 +0530 Subject: [PATCH 19/42] Update README.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4406e1e..c34bd80 100644 --- a/README.md +++ b/README.md @@ -918,7 +918,7 @@ boeBuilder.obligationCredentialStatus({ }); ``` -This sets `credentialStatus.type` to `TransferableRecords` with an `obligationRegistry` field (not `tokenRegistry`). On-chain minting is separate — use `mintObligationRegistry` from `@trustvc/trustvc` (see [§7c](#c-obligation-registry-boe)). Verify with `verifyDocument` (ObligationRecords fragment) — see [§4](#obligation--boe-verifydocument). +This sets `credentialStatus.type` to `ObligationRecords` with an `obligationRegistry` field (not `tokenRegistry`). On-chain minting is separate — use `mintObligationRegistry` from `@trustvc/trustvc` (see [§7c](`#c-obligation-registry-boe`)). Verify with `verifyDocument` (ObligationRecords fragment) — see [§4](`#obligation--boe-verifydocument`). > ⚠️ **Disclaimer:** > These builders **do not mint** on-chain. Mint separately via `mint` (ETR) or `mintObligationRegistry` (BoE). From c22441a6a8109b453a0e9440336b6289ea264efd Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Thu, 30 Jul 2026 18:23:36 +0530 Subject: [PATCH 20/42] refactor: update chain ID handling and improve test structure in obligation registry functions --- .../e2e/fixtures/sample-boe-credential.ts | 2 +- .../statusLifecycle.e2e.test.ts | 1 + .../transfer.e2e.test.ts | 16 ++++++++++++++++ .../lifecycle.test.ts | 9 +++++++-- .../obligation-registry-functions/mint.test.ts | 4 +++- .../rejectTransfers.test.ts | 4 +++- .../returnToken.test.ts | 4 +++- .../obligation-registry-functions/status.test.ts | 2 +- .../transfers.test.ts | 16 ++++++++++++++-- src/obligation-registry-functions/transfer.ts | 2 +- 10 files changed, 50 insertions(+), 10 deletions(-) diff --git a/src/__tests__/e2e/fixtures/sample-boe-credential.ts b/src/__tests__/e2e/fixtures/sample-boe-credential.ts index 904980b..d7b1b5a 100644 --- a/src/__tests__/e2e/fixtures/sample-boe-credential.ts +++ b/src/__tests__/e2e/fixtures/sample-boe-credential.ts @@ -57,7 +57,7 @@ export const SAMPLE_BOE_ENCRYPTION_ID = 'e2e-boe-encryption-key'; // DocumentBuilder.obligationCredentialStatus() config for Hardhat local network. export const createSampleBoeCredentialStatusConfig = (obligationRegistry: string) => ({ chain: 'local', - chainId: CHAIN_ID.local, + chainId: Number(CHAIN_ID.local), obligationRegistry, rpcProviderUrl: 'http://127.0.0.1:8545', }); diff --git a/src/__tests__/e2e/obligation-registry-functions/statusLifecycle.e2e.test.ts b/src/__tests__/e2e/obligation-registry-functions/statusLifecycle.e2e.test.ts index 4a4bc9e..5fc7bec 100644 --- a/src/__tests__/e2e/obligation-registry-functions/statusLifecycle.e2e.test.ts +++ b/src/__tests__/e2e/obligation-registry-functions/statusLifecycle.e2e.test.ts @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { network } from 'hardhat'; import '@nomiclabs/hardhat-ethers'; import '@nomicfoundation/hardhat-chai-matchers'; +import 'chai-as-promised'; import { acceptObligationRegistry, rejectObligationRegistry, diff --git a/src/__tests__/e2e/obligation-registry-functions/transfer.e2e.test.ts b/src/__tests__/e2e/obligation-registry-functions/transfer.e2e.test.ts index e326032..bc6eb09 100644 --- a/src/__tests__/e2e/obligation-registry-functions/transfer.e2e.test.ts +++ b/src/__tests__/e2e/obligation-registry-functions/transfer.e2e.test.ts @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { network } from 'hardhat'; import '@nomiclabs/hardhat-ethers'; import '@nomicfoundation/hardhat-chai-matchers'; +import 'chai-as-promised'; import { acceptObligationRegistry, nominateObligationRegistry, @@ -13,10 +14,12 @@ import { buildObligationE2ESetup, createObligationE2ESigners, deployObligationE2ERegistry, + getObligationE2EEscrowAddress, mintObligationE2EToken, obligationE2EProviders, type ObligationE2ESetup, } from './fixtures'; +import { createObligationContract } from '../utils'; obligationE2EProviders.forEach(({ ethersVersion }) => { describe(`Obligation transfer E2E (ethers ${ethersVersion})`, function () { @@ -156,6 +159,19 @@ obligationE2EProviders.forEach(({ ethersVersion }) => { await tx.wait(); expect(tx.hash).to.be.a('string'); + + const escrowAddress = await getObligationE2EEscrowAddress(setup, tokenId); + const escrow = createObligationContract( + escrowAddress, + 'ObligationEscrow', + ethersVersion, + setup.deployer, + ); + + // transferOwners must apply newHolderAddress to the holder role and + // newBeneficiaryAddress to the beneficiary role, not swapped. + expect(await escrow.holder()).to.equal(setup.other.address); + expect(await escrow.beneficiary()).to.equal(setup.beneficiary.address); }); it('E15: transferOwners fails when not dual-role', async function () { diff --git a/src/__tests__/obligation-registry-functions/lifecycle.test.ts b/src/__tests__/obligation-registry-functions/lifecycle.test.ts index ff31efe..939c250 100644 --- a/src/__tests__/obligation-registry-functions/lifecycle.test.ts +++ b/src/__tests__/obligation-registry-functions/lifecycle.test.ts @@ -17,6 +17,7 @@ import { } from './fixtures.js'; import * as coreModule from '../../core'; import { CHAIN_ID } from '../../utils'; +import { ObligationContractOptions } from '../../obligation-registry-functions/types'; const providers = [ { Provider: providerV5, ethersVersion: 'v5' as const }, @@ -36,7 +37,9 @@ describe.each(providers)( vi.spyOn(wallet, 'getChainId').mockResolvedValue(mockChainId as unknown as number); } else { wallet = new WalletV6(PRIVATE_KEY, Provider as any); - vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ chainId: mockChainId } as Network); + vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ + chainId: mockChainId, + } as unknown as Network); } vi.spyOn(coreModule, 'getObligationEscrowAddress').mockResolvedValue( @@ -87,7 +90,9 @@ describe.each(providers)( it('throws when escrow cannot be resolved', async () => { await expect( acceptObligationRegistry( - { obligationRegistryAddress: MOCK_OBLIGATION_REGISTRY_ADDRESS }, + { + obligationRegistryAddress: MOCK_OBLIGATION_REGISTRY_ADDRESS, + } as ObligationContractOptions, wallet, {}, { chainId: mockChainId }, diff --git a/src/__tests__/obligation-registry-functions/mint.test.ts b/src/__tests__/obligation-registry-functions/mint.test.ts index e147b85..62b0cb7 100644 --- a/src/__tests__/obligation-registry-functions/mint.test.ts +++ b/src/__tests__/obligation-registry-functions/mint.test.ts @@ -32,7 +32,9 @@ describe.each(providers)( vi.spyOn(wallet, 'getChainId').mockResolvedValue(mockChainId as unknown as number); } else { wallet = new WalletV6(PRIVATE_KEY, Provider as any); - vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ chainId: mockChainId } as Network); + vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ + chainId: mockChainId, + } as unknown as Network); } vi.mocked(getEthersContractFromProvider).mockReturnValue( diff --git a/src/__tests__/obligation-registry-functions/rejectTransfers.test.ts b/src/__tests__/obligation-registry-functions/rejectTransfers.test.ts index f20acea..3cd3cda 100644 --- a/src/__tests__/obligation-registry-functions/rejectTransfers.test.ts +++ b/src/__tests__/obligation-registry-functions/rejectTransfers.test.ts @@ -36,7 +36,9 @@ describe.each(providers)( vi.spyOn(wallet, 'getChainId').mockResolvedValue(mockChainId as unknown as number); } else { wallet = new WalletV6(PRIVATE_KEY, Provider as any); - vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ chainId: mockChainId } as Network); + vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ + chainId: mockChainId, + } as unknown as Network); } vi.spyOn(coreModule, 'getObligationEscrowAddress').mockResolvedValue( diff --git a/src/__tests__/obligation-registry-functions/returnToken.test.ts b/src/__tests__/obligation-registry-functions/returnToken.test.ts index 752c857..1b8be4c 100644 --- a/src/__tests__/obligation-registry-functions/returnToken.test.ts +++ b/src/__tests__/obligation-registry-functions/returnToken.test.ts @@ -38,7 +38,9 @@ describe.each(providers)( vi.spyOn(wallet, 'getChainId').mockResolvedValue(mockChainId as unknown as number); } else { wallet = new WalletV6(PRIVATE_KEY, Provider as any); - vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ chainId: mockChainId } as Network); + vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ + chainId: mockChainId, + } as unknown as Network); } vi.spyOn(coreModule, 'getObligationEscrowAddress').mockResolvedValue( diff --git a/src/__tests__/obligation-registry-functions/status.test.ts b/src/__tests__/obligation-registry-functions/status.test.ts index f34e22c..3f82b72 100644 --- a/src/__tests__/obligation-registry-functions/status.test.ts +++ b/src/__tests__/obligation-registry-functions/status.test.ts @@ -35,7 +35,7 @@ describe.each(providers)( wallet = new WalletV5(PRIVATE_KEY, Provider as any); } else { wallet = new WalletV6(PRIVATE_KEY, Provider as any); - vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ chainId: 1 } as Network); + vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ chainId: 1 } as unknown as Network); } vi.spyOn(coreModule, 'getObligationEscrowAddress').mockResolvedValue( diff --git a/src/__tests__/obligation-registry-functions/transfers.test.ts b/src/__tests__/obligation-registry-functions/transfers.test.ts index ab12132..8183bbd 100644 --- a/src/__tests__/obligation-registry-functions/transfers.test.ts +++ b/src/__tests__/obligation-registry-functions/transfers.test.ts @@ -18,6 +18,7 @@ import { } from './fixtures.js'; import * as coreModule from '../../core'; import { CHAIN_ID } from '../../utils'; +import { ObligationContractOptions } from '../../obligation-registry-functions/types'; const providers = [ { Provider: providerV5, ethersVersion: 'v5' as const }, @@ -37,7 +38,9 @@ describe.each(providers)( vi.spyOn(wallet, 'getChainId').mockResolvedValue(mockChainId as unknown as number); } else { wallet = new WalletV6(PRIVATE_KEY, Provider as any); - vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ chainId: mockChainId } as Network); + vi.spyOn(Provider, 'getNetwork').mockResolvedValue({ + chainId: mockChainId, + } as unknown as Network); } vi.spyOn(coreModule, 'getObligationEscrowAddress').mockResolvedValue( @@ -94,12 +97,21 @@ describe.each(providers)( ); expect(result).toEqual('transfer_owners_tx_hash'); + // Contract signature is transferOwners(newBeneficiary, newHolder, remark) - beneficiary first. + expect(mockObligationEscrowContract.transferOwners).toHaveBeenCalledWith( + '0xNewBeneficiary', + '0xNewHolder', + '0x', + {}, + ); }); it('throws when registry and tokenId are missing', async () => { await expect( transferHolderObligationRegistry( - { obligationRegistryAddress: MOCK_OBLIGATION_REGISTRY_ADDRESS }, + { + obligationRegistryAddress: MOCK_OBLIGATION_REGISTRY_ADDRESS, + } as ObligationContractOptions, wallet, { holderAddress: '0xNewHolder' }, { chainId: mockChainId }, diff --git a/src/obligation-registry-functions/transfer.ts b/src/obligation-registry-functions/transfer.ts index 429ce48..ea8da01 100644 --- a/src/obligation-registry-functions/transfer.ts +++ b/src/obligation-registry-functions/transfer.ts @@ -69,7 +69,7 @@ const transferOwnersObligationRegistry = async ( contractOptions, signer, 'transferOwners', - [params.newHolderAddress, params.newBeneficiaryAddress, encryptedRemarks], + [params.newBeneficiaryAddress, params.newHolderAddress, encryptedRemarks], options, ); }; From 491e732892b905033553059bb8f1d893bb660afb Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Thu, 30 Jul 2026 19:24:31 +0530 Subject: [PATCH 21/42] refactor: enhance obligation registry utility with notMintedReason function --- .../obligationRecordVerifier.utils.test.ts | 86 +++++++++++++++++++ .../obligationRecords/utils.ts | 77 +++++++++++------ 2 files changed, 136 insertions(+), 27 deletions(-) create mode 100644 src/__tests__/verify/obligationRecordVerifier.utils.test.ts diff --git a/src/__tests__/verify/obligationRecordVerifier.utils.test.ts b/src/__tests__/verify/obligationRecordVerifier.utils.test.ts new file mode 100644 index 0000000..5088bd4 --- /dev/null +++ b/src/__tests__/verify/obligationRecordVerifier.utils.test.ts @@ -0,0 +1,86 @@ +import { describe, expect, it, vi, beforeEach } from 'vitest'; +import { constants } from 'ethers'; +import { OpenAttestationEthereumTokenRegistryStatusCode } from '@tradetrust-tt/tt-verify'; +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: { + connect: vi.fn(() => ({ + ownerOf: mockOwnerOf, + })), + }, + ObligationEscrow__factory: { + connect: vi.fn(() => ({ + active: mockActive, + isHoldingToken: mockIsHoldingToken, + })), + }, +})); + +vi.mock('../../core/endorsement-chain/obligation', () => ({ + getObligationEscrowAddress: (...args: unknown[]) => mockGetObligationEscrowAddress(...args), +})); + +describe('isTokenMintedOnObligationRegistry', () => { + const obligationRegistryAddress = '0xRegistry'; + const tokenId = '0x1'; + const provider = { + getNetwork: vi.fn().mockResolvedValue({ chainId: 80002 }), + } as never; + + 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 () => { + const result = await isTokenMintedOnObligationRegistry({ + obligationRegistryAddress, + tokenId, + provider, + chainId: 80002, + }); + + expect(result).toEqual({ minted: true, address: obligationRegistryAddress }); + }); + + it('returns not minted when title is inactive or not holding', async () => { + mockIsHoldingToken.mockResolvedValue(false); + + const result = await isTokenMintedOnObligationRegistry({ + obligationRegistryAddress, + tokenId, + provider, + chainId: 80002, + }); + + expect(result.minted).toBe(false); + expect(result).toMatchObject({ + reason: { + code: OpenAttestationEthereumTokenRegistryStatusCode.DOCUMENT_NOT_MINTED, + message: expect.stringContaining('title is not active'), + }, + }); + }); + + it('returns not minted when owner is zero address', async () => { + mockOwnerOf.mockResolvedValue(constants.AddressZero); + + const result = await isTokenMintedOnObligationRegistry({ + obligationRegistryAddress, + tokenId, + provider, + chainId: 80002, + }); + + expect(result.minted).toBe(false); + }); +}); diff --git a/src/verify/fragments/document-status/obligationRecords/utils.ts b/src/verify/fragments/document-status/obligationRecords/utils.ts index 47a9f6f..878f33a 100644 --- a/src/verify/fragments/document-status/obligationRecords/utils.ts +++ b/src/verify/fragments/document-status/obligationRecords/utils.ts @@ -1,4 +1,7 @@ -import { TrustVCToken__factory } from '@tradetrust-tt/token-registry-v5/contracts'; +import { + ObligationEscrow__factory, + TrustVCToken__factory, +} from '@tradetrust-tt/token-registry-v5/contracts'; import { CodedError, InvalidTokenRegistryStatus, @@ -6,8 +9,28 @@ import { ValidTokenRegistryStatus, } from '@tradetrust-tt/tt-verify'; import { constants, providers } from 'ethers'; +import { getObligationEscrowAddress } from '../../../../core/endorsement-chain/obligation'; import { decodeError } from '../transferableRecords/utils'; +const notMintedReason = ( + obligationRegistryAddress: string, + tokenId: string, + message?: string, +): InvalidTokenRegistryStatus => ({ + minted: false, + address: obligationRegistryAddress, + reason: { + code: OpenAttestationEthereumTokenRegistryStatusCode.DOCUMENT_NOT_MINTED, + codeString: + OpenAttestationEthereumTokenRegistryStatusCode[ + OpenAttestationEthereumTokenRegistryStatusCode.DOCUMENT_NOT_MINTED + ], + message: + message ?? + `Document ${tokenId} has not been issued under contract ${obligationRegistryAddress}`, + }, +}); + export const isTokenMintedOnObligationRegistry = async ({ obligationRegistryAddress, tokenId, @@ -50,20 +73,31 @@ export const isTokenMintedOnObligationRegistry = async ({ .ownerOf(tokenId) .then((owner: string) => owner !== constants.AddressZero); - return minted - ? { minted, address: obligationRegistryAddress } - : { - minted, - address: obligationRegistryAddress, - reason: { - code: OpenAttestationEthereumTokenRegistryStatusCode.DOCUMENT_NOT_MINTED, - codeString: - OpenAttestationEthereumTokenRegistryStatusCode[ - OpenAttestationEthereumTokenRegistryStatusCode.DOCUMENT_NOT_MINTED - ], - message: `Document ${tokenId} has not been issued under contract ${obligationRegistryAddress}`, - }, - }; + if (!minted) { + return notMintedReason(obligationRegistryAddress, tokenId); + } + + 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 }; } catch (error: unknown) { // Same shape as transferableRecords: map ownerOf failures to DOCUMENT_NOT_MINTED via decodeError. // If decodeError rethrows (e.g. ethers v6 BAD_DATA with hex-only revert data), still treat as not minted. @@ -77,17 +111,6 @@ export const isTokenMintedOnObligationRegistry = async ({ message = `Document ${tokenId} has not been issued under contract ${obligationRegistryAddress}`; } - return { - minted: false, - address: obligationRegistryAddress, - reason: { - message, - code: OpenAttestationEthereumTokenRegistryStatusCode.DOCUMENT_NOT_MINTED, - codeString: - OpenAttestationEthereumTokenRegistryStatusCode[ - OpenAttestationEthereumTokenRegistryStatusCode.DOCUMENT_NOT_MINTED - ], - }, - }; + return notMintedReason(obligationRegistryAddress, tokenId, message); } }; From bab0a76973902c62df751002d80eba683e501461 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Thu, 30 Jul 2026 19:28:58 +0530 Subject: [PATCH 22/42] chore: trigger ci From a4c3c26e1da369295294e3da7aa849460a5b593e Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Thu, 30 Jul 2026 22:02:12 +0530 Subject: [PATCH 23/42] feat: add beta-boe prerelease configuration to release settings --- release.config.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/release.config.js b/release.config.js index fff85de..d074829 100644 --- a/release.config.js +++ b/release.config.js @@ -9,6 +9,11 @@ module.exports = { name: 'v1', prerelease: 'alpha', }, + { + name: 'beta-boe', + prerelease: 'beta', + channel: 'beta', + }, ], github: true, changelog: true, From c613ba77c220582564599c760d237493565de8a4 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Thu, 30 Jul 2026 22:08:11 +0530 Subject: [PATCH 24/42] refactor: replace chai assertions with node:assert in obligation registry tests --- .../statusLifecycle.e2e.test.ts | 64 +++++++++++++------ .../obligationRecordVerifier.utils.test.ts | 36 +++++++++++ .../obligationRecords/utils.ts | 55 +++++++--------- 3 files changed, 101 insertions(+), 54 deletions(-) diff --git a/src/__tests__/e2e/obligation-registry-functions/statusLifecycle.e2e.test.ts b/src/__tests__/e2e/obligation-registry-functions/statusLifecycle.e2e.test.ts index 5fc7bec..bfb81fe 100644 --- a/src/__tests__/e2e/obligation-registry-functions/statusLifecycle.e2e.test.ts +++ b/src/__tests__/e2e/obligation-registry-functions/statusLifecycle.e2e.test.ts @@ -1,8 +1,6 @@ -import { expect } from 'chai'; +import assert from 'node:assert/strict'; import { network } from 'hardhat'; import '@nomiclabs/hardhat-ethers'; -import '@nomicfoundation/hardhat-chai-matchers'; -import 'chai-as-promised'; import { acceptObligationRegistry, rejectObligationRegistry, @@ -25,6 +23,27 @@ import { type ObligationE2ESetup, } from './fixtures'; +async function expectRejection( + promise: Promise, + messageIncludes?: string | RegExp, +): Promise { + try { + await promise; + assert.fail('Expected promise to reject'); + } catch (error: unknown) { + if (error instanceof assert.AssertionError && error.message === 'Expected promise to reject') { + throw error; + } + if (messageIncludes === undefined) return; + const message = error instanceof Error ? error.message : String(error); + if (typeof messageIncludes === 'string') { + assert.match(message, new RegExp(messageIncludes)); + } else { + assert.match(message, messageIncludes); + } + } +} + obligationE2EProviders.forEach(({ ethersVersion }) => { describe(`Obligation status lifecycle E2E (ethers ${ethersVersion})`, function () { let setup: ObligationE2ESetup; @@ -70,9 +89,9 @@ obligationE2EProviders.forEach(({ ethersVersion }) => { { tokenId }, ); - expect(status).to.equal(ObligationDocumentStatus.Issued); - expect(registered).to.equal(true); - expect(reason).to.equal(ObligationEscrowTerminationReason.None); + assert.equal(status, ObligationDocumentStatus.Issued); + assert.equal(registered, true); + assert.equal(reason, ObligationEscrowTerminationReason.None); }); it('E2: duplicate mint fails', async function () { @@ -80,7 +99,7 @@ obligationE2EProviders.forEach(({ ethersVersion }) => { await mintObligationE2EToken(setup, tokenId, setup.holder.address, setup.beneficiary.address); - await expect( + await expectRejection( mintObligationRegistry( { obligationRegistryAddress: setup.obligationRegistry }, setup.deployer, @@ -91,7 +110,7 @@ obligationE2EProviders.forEach(({ ethersVersion }) => { }, setup.txOptions, ), - ).to.be.rejected; + ); }); it('E3: accept fails when beneficiary == holder', async function () { @@ -99,14 +118,15 @@ obligationE2EProviders.forEach(({ ethersVersion }) => { await mintObligationE2EToken(setup, tokenId, setup.holder.address, setup.holder.address); - await expect( + await expectRejection( acceptObligationRegistry( { obligationRegistryAddress: setup.obligationRegistry, tokenId }, setup.holder, {}, setup.txOptions, ), - ).to.be.rejectedWith(/accept failed/); + /accept failed/, + ); }); it('E4: holder accept transitions Issued → Accepted', async function () { @@ -129,7 +149,7 @@ obligationE2EProviders.forEach(({ ethersVersion }) => { { tokenId }, ); - expect(status).to.equal(ObligationDocumentStatus.Accepted); + assert.equal(status, ObligationDocumentStatus.Accepted); }); it('E5: non-holder cannot accept', async function () { @@ -137,14 +157,15 @@ obligationE2EProviders.forEach(({ ethersVersion }) => { await mintObligationE2EToken(setup, tokenId, setup.holder.address, setup.beneficiary.address); - await expect( + await expectRejection( acceptObligationRegistry( { obligationRegistryAddress: setup.obligationRegistry, tokenId }, setup.beneficiary, {}, setup.txOptions, ), - ).to.be.rejectedWith(/accept failed/); + /accept failed/, + ); }); it('E6: holder reject → Rejected + terminationReason Rejected + inactive', async function () { @@ -179,9 +200,9 @@ obligationE2EProviders.forEach(({ ethersVersion }) => { setup.deployer, ); - expect(status).to.equal(ObligationDocumentStatus.Rejected); - expect(reason).to.equal(ObligationEscrowTerminationReason.Rejected); - expect(await escrow.active()).to.equal(false); + assert.equal(status, ObligationDocumentStatus.Rejected); + assert.equal(reason, ObligationEscrowTerminationReason.Rejected); + assert.equal(await escrow.active(), false); }); it('E7: beneficiary discharge → Discharged + terminationReason Discharged', async function () { @@ -217,8 +238,8 @@ obligationE2EProviders.forEach(({ ethersVersion }) => { { tokenId }, ); - expect(status).to.equal(ObligationDocumentStatus.Discharged); - expect(reason).to.equal(ObligationEscrowTerminationReason.Discharged); + assert.equal(status, ObligationDocumentStatus.Discharged); + assert.equal(reason, ObligationEscrowTerminationReason.Discharged); }); it('E8: discharge while Issued fails', async function () { @@ -226,14 +247,15 @@ obligationE2EProviders.forEach(({ ethersVersion }) => { await mintObligationE2EToken(setup, tokenId, setup.holder.address, setup.beneficiary.address); - await expect( + await expectRejection( dischargeObligationRegistry( { obligationRegistryAddress: setup.obligationRegistry, tokenId }, setup.beneficiary, {}, setup.txOptions, ), - ).to.be.rejectedWith(/discharge failed/); + /discharge failed/, + ); }); it('E9: mint with empty remarks / no encryption id', async function () { @@ -258,7 +280,7 @@ obligationE2EProviders.forEach(({ ethersVersion }) => { { tokenId }, ); - expect(status).to.equal(ObligationDocumentStatus.Issued); + assert.equal(status, ObligationDocumentStatus.Issued); }); }); }); diff --git a/src/__tests__/verify/obligationRecordVerifier.utils.test.ts b/src/__tests__/verify/obligationRecordVerifier.utils.test.ts index 5088bd4..36c6a06 100644 --- a/src/__tests__/verify/obligationRecordVerifier.utils.test.ts +++ b/src/__tests__/verify/obligationRecordVerifier.utils.test.ts @@ -83,4 +83,40 @@ describe('isTokenMintedOnObligationRegistry', () => { expect(result.minted).toBe(false); }); + + it('maps ownerOf absence revert to DOCUMENT_NOT_MINTED via decodeError', async () => { + mockOwnerOf.mockRejectedValue({ + message: 'owner query for nonexistent token', + code: 'CALL_EXCEPTION', + }); + + const result = await isTokenMintedOnObligationRegistry({ + obligationRegistryAddress, + tokenId, + provider, + chainId: 80002, + }); + + expect(result).toMatchObject({ + minted: false, + reason: { + code: OpenAttestationEthereumTokenRegistryStatusCode.DOCUMENT_NOT_MINTED, + 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 878f33a..5797084 100644 --- a/src/verify/fragments/document-status/obligationRecords/utils.ts +++ b/src/verify/fragments/document-status/obligationRecords/utils.ts @@ -3,7 +3,6 @@ import { TrustVCToken__factory, } from '@tradetrust-tt/token-registry-v5/contracts'; import { - CodedError, InvalidTokenRegistryStatus, OpenAttestationEthereumTokenRegistryStatusCode, ValidTokenRegistryStatus, @@ -76,41 +75,31 @@ export const isTokenMintedOnObligationRegistry = async ({ if (!minted) { return notMintedReason(obligationRegistryAddress, tokenId); } + } catch (error: unknown) { + // Only ownerOf absence / registry miss maps to DOCUMENT_NOT_MINTED. + // CodedError (e.g. SERVER_ERROR) and unexpected reverts from decodeError propagate. + return notMintedReason(obligationRegistryAddress, tokenId, decodeError(error)); + } + + 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(), + ]); - const obligationEscrowAddress = await getObligationEscrowAddress( + if (!active || !isHoldingToken) { + return notMintedReason( obligationRegistryAddress, tokenId, - provider, - { titleEscrowVersion: 'v5' }, + `Document ${tokenId} title is not active under contract ${obligationRegistryAddress}`, ); - 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 }; - } catch (error: unknown) { - // Same shape as transferableRecords: map ownerOf failures to DOCUMENT_NOT_MINTED via decodeError. - // If decodeError rethrows (e.g. ethers v6 BAD_DATA with hex-only revert data), still treat as not minted. - let message: string; - try { - message = decodeError(error); - } catch (decodedError) { - if (decodedError instanceof CodedError) { - throw decodedError; - } - message = `Document ${tokenId} has not been issued under contract ${obligationRegistryAddress}`; - } - - return notMintedReason(obligationRegistryAddress, tokenId, message); } + + return { minted: true, address: obligationRegistryAddress }; }; From 47e38b9cfaad2c73ac3e482b388442b700cf5edd Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Thu, 30 Jul 2026 22:22:57 +0530 Subject: [PATCH 25/42] chore: trigger review From c63751da1c059e45c5ee21db523df09220d448ab Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Thu, 30 Jul 2026 22:24:41 +0530 Subject: [PATCH 26/42] refactor: enhance error handling in obligation records utilities by introducing EthersError type --- .../fragments/document-status/obligationRecords/utils.ts | 5 +++-- .../fragments/document-status/transferableRecords/utils.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/verify/fragments/document-status/obligationRecords/utils.ts b/src/verify/fragments/document-status/obligationRecords/utils.ts index 5797084..5b480fa 100644 --- a/src/verify/fragments/document-status/obligationRecords/utils.ts +++ b/src/verify/fragments/document-status/obligationRecords/utils.ts @@ -9,7 +9,7 @@ import { } from '@tradetrust-tt/tt-verify'; import { constants, providers } from 'ethers'; import { getObligationEscrowAddress } from '../../../../core/endorsement-chain/obligation'; -import { decodeError } from '../transferableRecords/utils'; +import { decodeError, type EthersError } from '../transferableRecords/utils'; const notMintedReason = ( obligationRegistryAddress: string, @@ -78,7 +78,8 @@ export const isTokenMintedOnObligationRegistry = async ({ } catch (error: unknown) { // Only ownerOf absence / registry miss maps to DOCUMENT_NOT_MINTED. // CodedError (e.g. SERVER_ERROR) and unexpected reverts from decodeError propagate. - return notMintedReason(obligationRegistryAddress, tokenId, decodeError(error)); + const ethersError = error as EthersError; + return notMintedReason(obligationRegistryAddress, tokenId, decodeError(ethersError)); } const obligationEscrowAddress = await getObligationEscrowAddress( diff --git a/src/verify/fragments/document-status/transferableRecords/utils.ts b/src/verify/fragments/document-status/transferableRecords/utils.ts index cdd5bd5..3e0c240 100644 --- a/src/verify/fragments/document-status/transferableRecords/utils.ts +++ b/src/verify/fragments/document-status/transferableRecords/utils.ts @@ -8,7 +8,7 @@ import { import { constants, errors, providers } from 'ethers'; // TODO: Remove and replace with ethers v6 ERROR interfaces https://github.com/ethers-io/ethers.js/blob/v6.13.4/src.ts/utils/errors.ts#L156 -type EthersError = { +export type EthersError = { message?: string; data?: string; method?: string; From 696fe71b8cef7bf44812af0a5b9e4c765a7e0e1d Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Thu, 30 Jul 2026 22:54:42 +0530 Subject: [PATCH 27/42] docs: fix markdown links in README for obligation records section --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c34bd80..00a390c 100644 --- a/README.md +++ b/README.md @@ -918,7 +918,7 @@ boeBuilder.obligationCredentialStatus({ }); ``` -This sets `credentialStatus.type` to `ObligationRecords` with an `obligationRegistry` field (not `tokenRegistry`). On-chain minting is separate — use `mintObligationRegistry` from `@trustvc/trustvc` (see [§7c](`#c-obligation-registry-boe`)). Verify with `verifyDocument` (ObligationRecords fragment) — see [§4](`#obligation--boe-verifydocument`). +This sets `credentialStatus.type` to `ObligationRecords` with an `obligationRegistry` field (not `tokenRegistry`). On-chain minting is separate — use `mintObligationRegistry` from `@trustvc/trustvc` (see [§7c](#c-obligation-registry-boe)). Verify with `verifyDocument` (ObligationRecords fragment) — see [§4](#obligation--boe-verifydocument). > ⚠️ **Disclaimer:** > These builders **do not mint** on-chain. Mint separately via `mint` (ETR) or `mintObligationRegistry` (BoE). From 0f37c7c2d9799709bb0428b1a6310d23364e59ea Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Fri, 31 Jul 2026 10:12:21 +0530 Subject: [PATCH 28/42] chore: remove beta-boe prerelease configuration from release settings --- release.config.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/release.config.js b/release.config.js index d074829..fff85de 100644 --- a/release.config.js +++ b/release.config.js @@ -9,11 +9,6 @@ module.exports = { name: 'v1', prerelease: 'alpha', }, - { - name: 'beta-boe', - prerelease: 'beta', - channel: 'beta', - }, ], github: true, changelog: true, From c8ea6b32c59a25c818cc539f3a1f51c67388ba0b Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Fri, 31 Jul 2026 11:24:43 +0530 Subject: [PATCH 29/42] docs: update README to reflect change from ObligationRecords --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b5abe1c..8774b1e 100644 --- a/README.md +++ b/README.md @@ -927,7 +927,7 @@ boeBuilder.obligationCredentialStatus({ }); ``` -This sets `credentialStatus.type` to `ObligationRecords` with an `obligationRegistry` field (not `tokenRegistry`). On-chain minting is separate — use `mintObligationRegistry` from `@trustvc/trustvc` (see [§7c](#c-obligation-registry-boe)). Verify with `verifyDocument` (ObligationRecords fragment) — see [§4](#obligation--boe-verifydocument). +This sets `credentialStatus.type` to `TransferableRecords` with an `obligationRegistry` field (not `tokenRegistry`). On-chain minting is separate — use `mintObligationRegistry` from `@trustvc/trustvc` (see [§7c](#c-obligation-registry-boe)). Verify with `verifyDocument` (ObligationRecords fragment) — see [§4](#obligation--boe-verifydocument). > ⚠️ **Disclaimer:** > These builders **do not mint** on-chain. Mint separately via `mint` (ETR) or `mintObligationRegistry` (BoE). From d0a17a92a0c864dbaeadeb0a2f1a86af592f8cc6 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Fri, 31 Jul 2026 12:25:14 +0530 Subject: [PATCH 30/42] ci: add beta and alpha branches to CI workflow triggers --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc224b5..bbbff94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,8 @@ on: types: ['opened', 'edited', 'reopened', 'synchronize'] branches: - main + - beta + - alpha - dev - feat/* - feature/* From e721272cefe3c399f21e5cb81b94aa741d8ea50b Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Fri, 31 Jul 2026 12:45:50 +0530 Subject: [PATCH 31/42] ci: update workflows to use Node.js 20 and enhance npm installation process --- .github/workflows/ci.yml | 9 ++ .github/workflows/tests.yml | 2 + src/__tests__/utils/documents/index.test.ts | 35 +++++++ src/core/documentBuilder.ts | 99 ++++++++++--------- src/obligation-registry-functions/mint.ts | 11 ++- .../returnToken.ts | 7 +- src/obligation-registry-functions/transfer.ts | 11 ++- src/obligation-registry-functions/types.ts | 7 +- src/obligation-registry-functions/utils.ts | 19 ++-- src/utils/documents/index.ts | 16 +-- src/utils/documents/obligation.ts | 10 +- .../obligationRecordVerifier.ts | 18 ++-- 12 files changed, 158 insertions(+), 86 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bbbff94..b7f5d67 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,15 @@ jobs: ref: ${{ github.head_ref }} fetch-depth: 0 + - name: Use Node.js 20 + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + + - run: npm ci --legacy-peer-deps + - uses: reviewdog/action-eslint@v1 with: github_token: ${{ secrets.GITHUB_TOKEN }} + eslint_flags: '.' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 062d8b6..413fd70 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -61,3 +61,5 @@ jobs: cache: 'npm' - run: npm ci --legacy-peer-deps - run: npm run build + env: + NODE_OPTIONS: --max-old-space-size=8192 diff --git a/src/__tests__/utils/documents/index.test.ts b/src/__tests__/utils/documents/index.test.ts index 5ee7f71..0f7c29a 100644 --- a/src/__tests__/utils/documents/index.test.ts +++ b/src/__tests__/utils/documents/index.test.ts @@ -3,6 +3,7 @@ import { getChainId, getTokenId, getTokenRegistryAddress, + isObligationRecord, isTransferableRecord, } from '../../../utils'; import { @@ -45,6 +46,40 @@ describe.concurrent('documents', () => { const transferableRecord = isTransferableRecord(WRAPPED_DOCUMENT_DNS_DID_V3); expect(transferableRecord).toBe(false); }); + + it('isTransferableRecord - mixed transferable + obligation statuses is false', () => { + const mixed = { + ...W3C_TRANSFERABLE_RECORD, + credentialStatus: [ + W3C_TRANSFERABLE_RECORD.credentialStatus, + { + type: 'TransferableRecords', + tokenNetwork: { chain: 'MATIC', chainId: 80002 }, + obligationRegistry: '0x71D28767662cB233F887aD2Bb65d048d760bA694', + tokenId: '23f719b016c88ba1ef2e10c0718d7d0f0026b1dc6e219629f81e2f0f811c4e3e', + }, + ], + }; + expect(isTransferableRecord(mixed as typeof W3C_TRANSFERABLE_RECORD)).toBe(false); + }); + }); + + describe.concurrent('isObligationRecord', () => { + it('isObligationRecord - true when any status is obligation', () => { + const mixed = { + ...W3C_TRANSFERABLE_RECORD, + credentialStatus: [ + W3C_TRANSFERABLE_RECORD.credentialStatus, + { + type: 'TransferableRecords', + tokenNetwork: { chain: 'MATIC', chainId: 80002 }, + obligationRegistry: '0x71D28767662cB233F887aD2Bb65d048d760bA694', + tokenId: '23f719b016c88ba1ef2e10c0718d7d0f0026b1dc6e219629f81e2f0f811c4e3e', + }, + ], + }; + expect(isObligationRecord(mixed as typeof W3C_TRANSFERABLE_RECORD)).toBe(true); + }); }); describe.concurrent('getTokenRegistryAddress', () => { diff --git a/src/core/documentBuilder.ts b/src/core/documentBuilder.ts index 4767dd0..099bdf0 100644 --- a/src/core/documentBuilder.ts +++ b/src/core/documentBuilder.ts @@ -364,68 +364,75 @@ export class DocumentBuilder { } } - // Private helper method to verify that the token registry supports the required interface for transferable records. - private async verifyTokenRegistry() { + private assertSupportedChain(): void { const chainId = this.document.credentialStatus.tokenNetwork .chainId as keyof typeof SUPPORTED_CHAINS; if (!(chainId in SUPPORTED_CHAINS)) { throw new Error(`Unsupported Chain: Chain ID ${chainId} is not supported.`); } + } + private async withRegistryNetworkError( + unsupportedRegistryError: string, + networkError: string, + verify: () => Promise, + ): Promise { try { - const provider = new ethers.providers.JsonRpcProvider(this.rpcProviderUrl); - const isV4Supported = await this.supportsInterface( - v4Contracts.TradeTrustToken__factory, - constantsV4.contractInterfaceId.TradeTrustTokenMintable, - provider, - ); - const isV5Supported = await this.supportsInterface( - v5Contracts.TradeTrustToken__factory, - constantsV5.contractInterfaceId.TradeTrustTokenMintable, - provider, - ); - if (!isV4Supported && !isV5Supported) - throw new Error('Token registry version is not supported.'); + await verify(); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { - if (error.message === 'Token registry version is not supported.') { + if (error.message === unsupportedRegistryError) { throw error; - } else { - throw new Error( - `Network Error: Unable to verify token registry. Please check the RPC URL or token registry address.`, - ); } + throw new Error(networkError); } } - private async verifyObligationRegistry() { - const chainId = this.document.credentialStatus.tokenNetwork - .chainId as keyof typeof SUPPORTED_CHAINS; - if (!(chainId in SUPPORTED_CHAINS)) { - throw new Error(`Unsupported Chain: Chain ID ${chainId} is not supported.`); - } + // Private helper method to verify that the token registry supports the required interface for transferable records. + private async verifyTokenRegistry() { + this.assertSupportedChain(); + + const unsupportedRegistryError = 'Token registry version is not supported.'; + await this.withRegistryNetworkError( + unsupportedRegistryError, + 'Network Error: Unable to verify token registry. Please check the RPC URL or token registry address.', + async () => { + const provider = new ethers.providers.JsonRpcProvider(this.rpcProviderUrl); + const isV4Supported = await this.supportsInterface( + v4Contracts.TradeTrustToken__factory, + constantsV4.contractInterfaceId.TradeTrustTokenMintable, + provider, + ); + const isV5Supported = await this.supportsInterface( + v5Contracts.TradeTrustToken__factory, + constantsV5.contractInterfaceId.TradeTrustTokenMintable, + provider, + ); + if (!isV4Supported && !isV5Supported) throw new Error(unsupportedRegistryError); + }, + ); + } - try { - const provider = new ethers.providers.JsonRpcProvider(this.rpcProviderUrl); - const isSupported = await this.supportsInterface( - v5Contracts.TrustVCToken__factory, - constantsV5.contractInterfaceId.TradeTrustTokenMintable, - provider, - this.statusConfig.obligationRegistry, - ); - if (!isSupported) { - throw new Error('Obligation registry version is not supported.'); - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { - if (error.message === 'Obligation registry version is not supported.') { - throw error; - } else { - throw new Error( - `Network Error: Unable to verify obligation registry. Please check the RPC URL or obligation registry address.`, + private async verifyObligationRegistry() { + this.assertSupportedChain(); + + const unsupportedRegistryError = 'Obligation registry version is not supported.'; + await this.withRegistryNetworkError( + unsupportedRegistryError, + 'Network Error: Unable to verify obligation registry. Please check the RPC URL or obligation registry address.', + async () => { + const provider = new ethers.providers.JsonRpcProvider(this.rpcProviderUrl); + const isSupported = await this.supportsInterface( + v5Contracts.TrustVCToken__factory, + constantsV5.contractInterfaceId.TradeTrustTokenMintable, + provider, + this.statusConfig.obligationRegistry, ); - } - } + if (!isSupported) { + throw new Error(unsupportedRegistryError); + } + }, + ); } // Private helper method to check if a contract supports a specific interface ID. diff --git a/src/obligation-registry-functions/mint.ts b/src/obligation-registry-functions/mint.ts index 189fb4e..0a68689 100644 --- a/src/obligation-registry-functions/mint.ts +++ b/src/obligation-registry-functions/mint.ts @@ -1,15 +1,20 @@ import { v5SupportInterfaceIds } from '../token-registry-v5'; import { Signer as SignerV6 } from 'ethersV6'; -import { ContractTransaction, Signer } from 'ethers'; +import { Signer } from 'ethers'; import { executeRegistryMethod, getEncryptedRemarks } from './utils'; -import { MintObligationTokenOptions, MintObligationTokenParams, TransactionOptions } from './types'; +import { + MintObligationTokenOptions, + MintObligationTokenParams, + ObligationTransactionResponse, + TransactionOptions, +} from './types'; const mintObligationRegistry = async ( contractOptions: MintObligationTokenOptions, signer: Signer | SignerV6, params: MintObligationTokenParams, options: TransactionOptions, -): Promise => { +): Promise => { const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); return executeRegistryMethod( contractOptions.obligationRegistryAddress, diff --git a/src/obligation-registry-functions/returnToken.ts b/src/obligation-registry-functions/returnToken.ts index 9f35928..a098633 100644 --- a/src/obligation-registry-functions/returnToken.ts +++ b/src/obligation-registry-functions/returnToken.ts @@ -1,9 +1,10 @@ import { v5SupportInterfaceIds } from '../token-registry-v5'; import { Signer as SignerV6 } from 'ethersV6'; -import { ContractTransaction, Signer } from 'ethers'; +import { Signer } from 'ethers'; import { AcceptReturnedObligationOptions, AcceptReturnedObligationParams, + ObligationTransactionResponse, RejectReturnedObligationOptions, RejectReturnedObligationParams, TransactionOptions, @@ -17,7 +18,7 @@ const acceptReturnedObligationRegistry = async ( signer: Signer | SignerV6, params: AcceptReturnedObligationParams, options: TransactionOptions, -): Promise => { +): Promise => { const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); return executeRegistryMethod( contractOptions.obligationRegistryAddress, @@ -34,7 +35,7 @@ const rejectReturnedObligationRegistry = async ( signer: Signer | SignerV6, params: RejectReturnedObligationParams, options: TransactionOptions, -): Promise => { +): Promise => { const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); return executeRegistryMethod( contractOptions.obligationRegistryAddress, diff --git a/src/obligation-registry-functions/transfer.ts b/src/obligation-registry-functions/transfer.ts index ea8da01..ca5b579 100644 --- a/src/obligation-registry-functions/transfer.ts +++ b/src/obligation-registry-functions/transfer.ts @@ -1,9 +1,10 @@ import { Signer as SignerV6 } from 'ethersV6'; -import { ContractTransaction, Signer } from 'ethers'; +import { Signer } from 'ethers'; import { executeEscrowMethod, getEncryptedRemarks } from './utils'; import { NominateObligationParams, ObligationContractOptions, + ObligationTransactionResponse, TransactionOptions, TransferObligationBeneficiaryParams, TransferObligationHolderParams, @@ -15,7 +16,7 @@ const transferHolderObligationRegistry = async ( signer: Signer | SignerV6, params: TransferObligationHolderParams, options: TransactionOptions, -): Promise => { +): Promise => { const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); return executeEscrowMethod( contractOptions, @@ -31,7 +32,7 @@ const transferBeneficiaryObligationRegistry = async ( signer: Signer | SignerV6, params: TransferObligationBeneficiaryParams, options: TransactionOptions, -): Promise => { +): Promise => { const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); return executeEscrowMethod( contractOptions, @@ -47,7 +48,7 @@ const nominateObligationRegistry = async ( signer: Signer | SignerV6, params: NominateObligationParams, options: TransactionOptions, -): Promise => { +): Promise => { const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); return executeEscrowMethod( contractOptions, @@ -63,7 +64,7 @@ const transferOwnersObligationRegistry = async ( signer: Signer | SignerV6, params: TransferObligationOwnersParams, options: TransactionOptions, -): Promise => { +): Promise => { const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); return executeEscrowMethod( contractOptions, diff --git a/src/obligation-registry-functions/types.ts b/src/obligation-registry-functions/types.ts index a8d3867..6de5ac3 100644 --- a/src/obligation-registry-functions/types.ts +++ b/src/obligation-registry-functions/types.ts @@ -1,6 +1,9 @@ import { CHAIN_ID } from '../utils'; -import { BigNumber, providers as providersV5 } from 'ethers'; -import { BigNumberish, Provider as ProviderV6 } from 'ethersV6'; +import { BigNumber, providers as providersV5, ContractTransaction } from 'ethers'; +import { BigNumberish, Provider as ProviderV6, ContractTransactionResponse } from 'ethersV6'; + +/** Transaction response from obligation write helpers — ethers v5 or v6 depending on the signer. */ +export type ObligationTransactionResponse = ContractTransaction | ContractTransactionResponse; export interface GasPriceScale { maxPriorityFeePerGasScale: number; diff --git a/src/obligation-registry-functions/utils.ts b/src/obligation-registry-functions/utils.ts index 4ba1c4d..b7f2982 100644 --- a/src/obligation-registry-functions/utils.ts +++ b/src/obligation-registry-functions/utils.ts @@ -2,9 +2,13 @@ import { encrypt, getObligationEscrowAddress, checkSupportsInterface } from '../ import { v5Contracts } from '../token-registry-v5'; import { getTxOptions } from '../token-registry-functions/utils'; import { Signer as SignerV6, Contract as ContractV6 } from 'ethersV6'; -import { Contract as ContractV5, ContractTransaction, Signer } from 'ethers'; +import { Contract as ContractV5, Signer } from 'ethers'; import { getEthersContractFromProvider, isV6EthersProvider } from '../utils/ethers'; -import { ObligationContractOptions, TransactionOptions } from './types'; +import { + ObligationContractOptions, + ObligationTransactionResponse, + TransactionOptions, +} from './types'; export const getObligationRegistryContract = ( obligationRegistryAddress: string, @@ -108,13 +112,16 @@ export const sendTransaction = async ( maxFeePerGas?: Parameters[2]; maxPriorityFeePerGas?: Parameters[3]; }, -) => { +): Promise => { const txOptions = await getTxOptions( signer, options.chainId, options.maxFeePerGas, options.maxPriorityFeePerGas, ); + if (isV6EthersProvider(signer.provider)) { + return (contract as ContractV6)[method](...args, txOptions); + } return (contract as ContractV5)[method](...args, txOptions); }; @@ -133,7 +140,7 @@ export const executeEscrowMethod = async ( method: string, args: unknown[], options: TransactionOptions, -): Promise => { +): Promise => { const obligationEscrowContract = await getEscrowContract(contractOptions, signer); await runStaticCall(obligationEscrowContract, method, args, signer.provider); return sendTransaction(obligationEscrowContract, method, args, signer, options); @@ -146,7 +153,7 @@ export const createRemarkEscrowMethod = signer: Signer | SignerV6, params: { remarks?: string }, options: TransactionOptions, - ): Promise => + ): Promise => executeEscrowMethod( contractOptions, signer, @@ -162,7 +169,7 @@ export const executeRegistryMethod = async ( method: string, args: unknown[], options: TransactionOptions, -): Promise => { +): Promise => { if (!obligationRegistryAddress) throw new Error('Obligation registry address is required'); if (!signer.provider) throw new Error('Provider is required'); diff --git a/src/utils/documents/index.ts b/src/utils/documents/index.ts index 14ac557..4e3f4cf 100644 --- a/src/utils/documents/index.ts +++ b/src/utils/documents/index.ts @@ -31,17 +31,17 @@ export const getTransferableRecordsCredentialStatus = ( export const isTransferableRecord = ( document: WrappedOrSignedOpenAttestationDocument | SignedVerifiableCredential, ): boolean => { - let isTransferableAssetVal: boolean = false; if (isSignedDocument(document)) { - const credentialStatus = getTransferableRecordsCredentialStatus(document); - isTransferableAssetVal = - credentialStatus?.type === TRANSFERABLE_RECORDS_TYPE && - !isObligationRecordCredentialStatus(credentialStatus); - } else { - isTransferableAssetVal = isTransferableAsset(document); + const credentialStatuses = Array.isArray(document.credentialStatus) + ? document.credentialStatus + : [document.credentialStatus]; + + return credentialStatuses.every( + (cs) => cs?.type === TRANSFERABLE_RECORDS_TYPE && !isObligationRecordCredentialStatus(cs), + ); } - return isTransferableAssetVal; + return isTransferableAsset(document); }; export const getTokenRegistryAddress = ( diff --git a/src/utils/documents/obligation.ts b/src/utils/documents/obligation.ts index b8ee189..0a0e24e 100644 --- a/src/utils/documents/obligation.ts +++ b/src/utils/documents/obligation.ts @@ -17,10 +17,10 @@ export const isObligationRecordCredentialStatus = ( export const getObligationRecordsCredentialStatus = ( document: unknown, -): ObligationRecordsCredentialStatus => { - return [ - (document as SignedVerifiableCredential)?.credentialStatus, - ].flat()?.[0] as ObligationRecordsCredentialStatus; +): ObligationRecordsCredentialStatus | undefined => { + const credentialStatuses = [(document as SignedVerifiableCredential)?.credentialStatus].flat(); + + return credentialStatuses.find(isObligationRecordCredentialStatus); }; export const isObligationRecord = ( @@ -34,7 +34,7 @@ export const isObligationRecord = ( ? document.credentialStatus : [document.credentialStatus]; - return credentialStatuses.every((cs) => isObligationRecordCredentialStatus(cs)); + return credentialStatuses.some((cs) => isObligationRecordCredentialStatus(cs)); }; export const getObligationRegistryAddress = ( diff --git a/src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.ts b/src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.ts index 4e707b6..c0b4478 100644 --- a/src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.ts +++ b/src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.ts @@ -30,11 +30,10 @@ const verify: VerifierType['verify'] = async ( options: VerifierOptions, ) => { const signedDocument = document as SignedVerifiableCredential; - const credentialStatuses = ( - Array.isArray(signedDocument?.credentialStatus) - ? signedDocument?.credentialStatus - : [signedDocument?.credentialStatus] - ) as ObligationRecordsCredentialStatus[]; + const rawCredentialStatuses = Array.isArray(signedDocument?.credentialStatus) + ? signedDocument?.credentialStatus + : [signedDocument?.credentialStatus]; + const credentialStatuses = rawCredentialStatuses.filter(isObligationRecordCredentialStatus); const { provider } = options; const verificationResult = await Promise.all( @@ -80,10 +79,13 @@ const verify: VerifierType['verify'] = async ( }, }; - if (verificationResult.every(ValidTokenRegistryStatus.guard)) { + if (verificationResult.length > 0 && verificationResult.every(ValidTokenRegistryStatus.guard)) { result.status = 'VALID' as const; } else { - result.reason = (verificationResult as InvalidTokenRegistryStatus[])?.[0]?.reason; + const invalidStatus = verificationResult.find( + (status): status is InvalidTokenRegistryStatus => !ValidTokenRegistryStatus.guard(status), + ); + result.reason = invalidStatus?.reason; } return result; @@ -102,7 +104,7 @@ const test: VerifierType['test'] = ( return Boolean( w3cVC.isSignedDocument(document) && - credentialStatuses.every((cs: w3cVC.CredentialStatus) => + credentialStatuses.some((cs: w3cVC.CredentialStatus) => isObligationRecordCredentialStatus(cs), ), ); From 982abe4724f9d550ab4356c3281828acefa9ced3 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Fri, 31 Jul 2026 12:46:32 +0530 Subject: [PATCH 32/42] Update src/core/documentBuilder.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/core/documentBuilder.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/documentBuilder.ts b/src/core/documentBuilder.ts index 099bdf0..44702c8 100644 --- a/src/core/documentBuilder.ts +++ b/src/core/documentBuilder.ts @@ -173,6 +173,11 @@ export class DocumentBuilder { // Configures the credential status for obligation records (Bill of Exchange). obligationCredentialStatus(config: W3CObligationRecordsConfig) { if (this.isSigned) throw new Error('Configuration Error: Document is already signed.'); + if (this.selectedStatusType && this.selectedStatusType !== 'obligationRecords') { + throw new Error( + 'Configuration Error: Do not mix obligation records with other credential status types.', + ); + } if (!this.isObligationRecordsConfig(config)) { throw new Error( @@ -191,7 +196,6 @@ export class DocumentBuilder { return this; } - // Sets the expiration date of the document. expirationDate(date: string | Date) { if (this.isSigned) throw new Error('Configuration Error: Document is already signed.'); From 687eaf9c8a59c83bc0a94b119368f7356913c48b Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Fri, 31 Jul 2026 12:49:18 +0530 Subject: [PATCH 33/42] Update src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.types.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../obligationRecords/obligationRecordVerifier.types.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.types.ts b/src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.types.ts index 37cb1e8..56bc7c1 100644 --- a/src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.types.ts +++ b/src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.types.ts @@ -25,7 +25,6 @@ export type ObligationRecordsErrorFragment = Omit; From fdb6d214f9384aec1d403723469d5d3106df4d50 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Fri, 31 Jul 2026 12:51:18 +0530 Subject: [PATCH 34/42] refactor: adjust type definitions in obligationRecordVerifier to improve clarity --- .../obligationRecords/obligationRecordVerifier.types.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.types.ts b/src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.types.ts index 56bc7c1..37cb1e8 100644 --- a/src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.types.ts +++ b/src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.types.ts @@ -25,6 +25,7 @@ export type ObligationRecordsErrorFragment = Omit; From 812d4f2942ae2e59937b1700f6da131dd7446988 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Fri, 31 Jul 2026 13:40:22 +0530 Subject: [PATCH 35/42] ci: streamline npm installation in workflows by removing legacy-peer-deps flag --- .github/workflows/ci.yml | 8 -------- .github/workflows/linters.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/tests.yml | 4 ++-- 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7f5d67..e7e0ed8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,14 +41,6 @@ jobs: ref: ${{ github.head_ref }} fetch-depth: 0 - - name: Use Node.js 20 - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'npm' - - - run: npm ci --legacy-peer-deps - - uses: reviewdog/action-eslint@v1 with: github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 8cfaf56..2e0036e 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -27,7 +27,7 @@ jobs: with: node-version: ${{ matrix.node-version }} cache: 'npm' - - run: npm ci --legacy-peer-deps + - run: npm ci - run: npm run lint commit-lint: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d81567e..2f76ced 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 20 - - run: npm ci --legacy-peer-deps + - run: npm ci - run: npm run test publish-npm: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 413fd70..a42a1fd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -32,7 +32,7 @@ jobs: with: node-version: ${{ matrix.node-version }} cache: 'npm' - - run: npm ci --legacy-peer-deps + - run: npm ci - run: npm run test env: ANKR_API_KEY: ${{ secrets.ANKR_API_KEY }} @@ -59,7 +59,7 @@ jobs: with: node-version: ${{ matrix.node-version }} cache: 'npm' - - run: npm ci --legacy-peer-deps + - run: npm ci - run: npm run build env: NODE_OPTIONS: --max-old-space-size=8192 From 63c64c2dd7892513c8700cd6a2d7b8b9b5da01c0 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Fri, 31 Jul 2026 13:41:18 +0530 Subject: [PATCH 36/42] ci: remove unnecessary eslint_flags from CI workflow configuration --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e7e0ed8..adca266 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,5 +43,4 @@ jobs: - uses: reviewdog/action-eslint@v1 with: - github_token: ${{ secrets.GITHUB_TOKEN }} - eslint_flags: '.' + github_token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From 645a151bd05f40aa8af7968f6eb39f2dd07f12cb Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Fri, 31 Jul 2026 13:48:03 +0530 Subject: [PATCH 37/42] chore: update package dependencies and streamline npm installation in workflows --- .github/workflows/linters.yml | 4 +- .github/workflows/publish.yml | 4 +- .github/workflows/tests.yml | 4 +- package-lock.json | 223 ++++++++++++++++++++++++++++------ package.json | 3 +- 5 files changed, 191 insertions(+), 47 deletions(-) diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 2e0036e..1bf8857 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -27,7 +27,7 @@ jobs: with: node-version: ${{ matrix.node-version }} cache: 'npm' - - run: npm ci + - run: npm ci --ignore-scripts - run: npm run lint commit-lint: @@ -41,5 +41,5 @@ jobs: fetch-depth: 0 - name: Install Commit Lint Dependencies - run: npm install --force @commitlint/config-conventional@v17 + run: npm install --ignore-scripts --force @commitlint/config-conventional@v17 - uses: JulienKode/pull-request-name-linter-action@v0.5.0 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2f76ced..5e87b8c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 20 - - run: npm ci + - run: npm ci --ignore-scripts - run: npm run test publish-npm: @@ -45,7 +45,7 @@ jobs: with: node-version: 20 registry-url: https://registry.npmjs.org/ - - run: npm ci --legacy-peer-deps + - run: npm ci --ignore-scripts - run: npm run build - run: npm run release env: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a42a1fd..5e131ae 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -32,7 +32,7 @@ jobs: with: node-version: ${{ matrix.node-version }} cache: 'npm' - - run: npm ci + - run: npm ci --ignore-scripts - run: npm run test env: ANKR_API_KEY: ${{ secrets.ANKR_API_KEY }} @@ -59,7 +59,7 @@ jobs: with: node-version: ${{ matrix.node-version }} cache: 'npm' - - run: npm ci + - run: npm ci --ignore-scripts - run: npm run build env: NODE_OPTIONS: --max-old-space-size=8192 diff --git a/package-lock.json b/package-lock.json index a1b71bc..72d92d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,12 +16,12 @@ "@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/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", - "@trustvc/eip7702": "^1.0.0-beta.1", "ethers": "^5.8.0", "ethersV6": "npm:ethers@^6.14.4", "js-sha3": "^0.9.3", @@ -7210,6 +7210,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/prettier": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", + "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", + "license": "MIT", + "peer": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.48.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz", @@ -8068,7 +8075,6 @@ "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", "license": "MIT", - "optional": true, "engines": { "node": ">=6" } @@ -8253,7 +8259,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "devOptional": true, "license": "MIT" }, "node_modules/base-x": { @@ -8483,7 +8488,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "devOptional": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -9030,7 +9034,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "devOptional": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -9043,7 +9046,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "devOptional": true, "license": "MIT" }, "node_modules/color-support": { @@ -9087,7 +9089,6 @@ "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", "license": "MIT", - "optional": true, "dependencies": { "array-back": "^3.1.0", "find-replace": "^3.0.0", @@ -9126,7 +9127,6 @@ "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.3.tgz", "integrity": "sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==", "license": "MIT", - "optional": true, "dependencies": { "array-back": "^4.0.2", "chalk": "^2.4.2", @@ -9142,7 +9142,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "license": "MIT", - "optional": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -9155,7 +9154,6 @@ "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", "license": "MIT", - "optional": true, "engines": { "node": ">=8" } @@ -9165,7 +9163,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "license": "MIT", - "optional": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -9180,7 +9177,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "license": "MIT", - "optional": true, "dependencies": { "color-name": "1.1.3" } @@ -9189,15 +9185,13 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/command-line-usage/node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "license": "MIT", - "optional": true, "engines": { "node": ">=0.8.0" } @@ -9207,7 +9201,6 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "license": "MIT", - "optional": true, "engines": { "node": ">=4" } @@ -9217,7 +9210,6 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "license": "MIT", - "optional": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -9230,7 +9222,6 @@ "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", "license": "MIT", - "optional": true, "engines": { "node": ">=8" } @@ -9277,7 +9268,6 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "devOptional": true, "license": "MIT" }, "node_modules/concurrently": { @@ -10114,7 +10104,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=4.0.0" @@ -11952,7 +11941,6 @@ "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", "license": "MIT", - "optional": true, "dependencies": { "array-back": "^3.0.1" }, @@ -12238,7 +12226,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "devOptional": true, "license": "ISC" }, "node_modules/fsevents": { @@ -12737,7 +12724,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -13097,7 +13083,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -13488,7 +13473,6 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "devOptional": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -15342,7 +15326,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "devOptional": true, "license": "MIT" }, "node_modules/lodash.capitalize": { @@ -16036,7 +16019,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "devOptional": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -16122,7 +16104,6 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "license": "MIT", - "optional": true, "bin": { "mkdirp": "bin/cmd.js" }, @@ -19621,7 +19602,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "devOptional": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -20191,7 +20171,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -20983,7 +20962,6 @@ "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==", "license": "MIT", - "optional": true, "engines": { "node": ">=6" } @@ -22571,6 +22549,13 @@ "node": ">=0.6.19" } }, + "node_modules/string-format": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string-format/-/string-format-2.0.0.tgz", + "integrity": "sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==", + "license": "WTFPL OR MIT", + "peer": true + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -22920,7 +22905,6 @@ "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.2.tgz", "integrity": "sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==", "license": "MIT", - "optional": true, "dependencies": { "array-back": "^4.0.1", "deep-extend": "~0.6.0", @@ -22936,7 +22920,6 @@ "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", "license": "MIT", - "optional": true, "engines": { "node": ">=8" } @@ -22946,7 +22929,6 @@ "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", "license": "MIT", - "optional": true, "engines": { "node": ">=8" } @@ -23358,6 +23340,68 @@ "integrity": "sha512-PTyPoWYHc2we8P2NTn5hpYG211popWbkjiw+k63xqjeMrx9pPtXSclz9F3fu0Tpr+vfR1xxcQFwsEkh1cXMLqw==", "license": "MIT" }, + "node_modules/ts-command-line-args": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/ts-command-line-args/-/ts-command-line-args-2.5.1.tgz", + "integrity": "sha512-H69ZwTw3rFHb5WYpQya40YAX2/w7Ut75uUECbgBIsLmM+BNuYnxsltfyyLMxy6sEeKxgijLTnQtLd0nKd6+IYw==", + "license": "ISC", + "peer": true, + "dependencies": { + "chalk": "^4.1.0", + "command-line-args": "^5.1.1", + "command-line-usage": "^6.1.0", + "string-format": "^2.0.0" + }, + "bin": { + "write-markdown": "dist/write-markdown.js" + } + }, + "node_modules/ts-command-line-args/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ts-command-line-args/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ts-command-line-args/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ts-essentials": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-7.0.3.tgz", @@ -23587,6 +23631,111 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typechain": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/typechain/-/typechain-8.3.2.tgz", + "integrity": "sha512-x/sQYr5w9K7yv3es7jo4KTX05CLxOf7TRWwoHlrjRh8H82G64g+k7VuWPJlgMo6qrjfCulOdfBjiaDtmhFYD/Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prettier": "^2.1.1", + "debug": "^4.3.1", + "fs-extra": "^7.0.0", + "glob": "7.1.7", + "js-sha3": "^0.8.0", + "lodash": "^4.17.15", + "mkdirp": "^1.0.4", + "prettier": "^2.3.1", + "ts-command-line-args": "^2.2.0", + "ts-essentials": "^7.0.1" + }, + "bin": { + "typechain": "dist/cli/cli.js" + }, + "peerDependencies": { + "typescript": ">=4.3.0" + } + }, + "node_modules/typechain/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/typechain/node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typechain/node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "license": "MIT", + "peer": true + }, + "node_modules/typechain/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "license": "MIT", + "peer": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/typechain/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "license": "MIT", + "peer": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/typechain/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -23665,7 +23814,6 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -23704,7 +23852,6 @@ "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", "license": "MIT", - "optional": true, "engines": { "node": ">=8" } @@ -23720,7 +23867,6 @@ "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "dev": true, "license": "BSD-2-Clause", "optional": true, "bin": { @@ -25127,7 +25273,6 @@ "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz", "integrity": "sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==", "license": "MIT", - "optional": true, "dependencies": { "reduce-flatten": "^2.0.0", "typical": "^5.2.0" @@ -25141,7 +25286,6 @@ "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", "license": "MIT", - "optional": true, "engines": { "node": ">=8" } @@ -25226,7 +25370,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "devOptional": true, "license": "ISC" }, "node_modules/ws": { diff --git a/package.json b/package.json index 3776df5..b84a100 100644 --- a/package.json +++ b/package.json @@ -191,7 +191,8 @@ }, "overrides": { "ethers": "^5.8.0", - "@digitalbazaar/bls12-381-multikey": "~2.1.0" + "@digitalbazaar/bls12-381-multikey": "~2.1.0", + "ox": "0.14.29" }, "peerDependencies": { "ethers": "^5.8.0" From 811b19fc3e89d170a8eb3eb52bdabe8d6d06a672 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Fri, 31 Jul 2026 13:57:12 +0530 Subject: [PATCH 38/42] chore: update commitlint dependency to version 21.2.0 in workflows --- .github/workflows/linters.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 1bf8857..c445e27 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -41,5 +41,5 @@ jobs: fetch-depth: 0 - name: Install Commit Lint Dependencies - run: npm install --ignore-scripts --force @commitlint/config-conventional@v17 + run: npm install --ignore-scripts --force @commitlint/config-conventional@21.2.0 - uses: JulienKode/pull-request-name-linter-action@v0.5.0 From 7a847366001096f433e485a1ca5ca2a73349108d Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Fri, 31 Jul 2026 14:20:37 +0530 Subject: [PATCH 39/42] ci: increase Node.js memory limit in publish workflow to improve build stability --- .github/workflows/publish.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5e87b8c..500a053 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -47,8 +47,11 @@ jobs: registry-url: https://registry.npmjs.org/ - run: npm ci --ignore-scripts - run: npm run build + env: + NODE_OPTIONS: --max-old-space-size=8192 - run: npm run release env: GH_TOKEN: ${{ secrets.GH_TOKEN }} NODE_AUTH_TOKEN: ${{secrets.npm_token}} + NODE_OPTIONS: --max-old-space-size=8192 # NPM_CONFIG_PROVENANCE: true From 53d2969e4f42bb4f9e455b5c4364eeeb3d878f43 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Fri, 31 Jul 2026 20:38:45 +0530 Subject: [PATCH 40/42] refactor: simplify obligation record verifier tests by removing unused mocks --- .../obligationRecordVerifier.utils.test.ts | 44 ++----------------- .../obligationRecords/utils.ts | 31 ++----------- 2 files changed, 8 insertions(+), 67 deletions(-) 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 }; }; From 2e24dd13ecb1ea3974f2643a09f9bd8b10d7ad92 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Fri, 31 Jul 2026 22:44:27 +0530 Subject: [PATCH 41/42] fix: build package during prepare for GitHub installs Co-authored-by: Cursor --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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": [ From c9730e3164b45334a300961819dccbb4350deac7 Mon Sep 17 00:00:00 2001 From: manishdex25 Date: Sat, 1 Aug 2026 01:16:34 +0530 Subject: [PATCH 42/42] docs: update README to clarify token lifecycle and role rules in obligation registry --- README.md | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) 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`):