diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc224b5..adca266 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/* @@ -41,4 +43,4 @@ jobs: - uses: reviewdog/action-eslint@v1 with: - github_token: ${{ secrets.GITHUB_TOKEN }} + github_token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 8cfaf56..c445e27 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 --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@21.2.0 - uses: JulienKode/pull-request-name-linter-action@v0.5.0 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d81567e..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 --legacy-peer-deps + - 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 3d38a6c..5e131ae 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -32,11 +32,12 @@ jobs: with: node-version: ${{ matrix.node-version }} cache: 'npm' - - run: npm ci --legacy-peer-deps + - run: npm ci --ignore-scripts - run: npm run test env: ANKR_API_KEY: ${{ secrets.ANKR_API_KEY }} POLYGONSCAN_API_KEY: ${{ secrets.POLYGONSCAN_API_KEY }} + AMOY_RPC: https://polygon-amoy-bor-rpc.publicnode.com test-build: name: Test Build @@ -58,5 +59,7 @@ jobs: with: node-version: ${{ matrix.node-version }} cache: 'npm' - - run: npm ci --legacy-peer-deps + - run: npm ci --ignore-scripts - run: npm run build + env: + NODE_OPTIONS: --max-old-space-size=8192 diff --git a/README.md b/README.md index 1b3e0d4..8774b1e 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) @@ -394,6 +396,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** @@ -746,8 +772,99 @@ 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` (same as Token Registry helpers). Functions use the `*ObligationRegistry` suffix (e.g. `mintObligationRegistry`) so they never clash with classic ETR exports (`mint`, `transferHolder`, …). + +**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** (from `@trustvc/trustvc`): + +| 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'; + +// 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 @@ -775,9 +892,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. @@ -789,9 +906,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` (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 e381883..72d92d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,16 +12,16 @@ "@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/eip7702": "^1.0.0-beta.1", - "@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", @@ -1323,59 +1323,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", @@ -1388,6 +1363,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", @@ -1479,9 +1470,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" @@ -1500,6 +1491,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", @@ -1517,15 +1592,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", @@ -3717,27 +3783,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" + "@noble/hashes": "1.8.0" }, "engines": { - "node": ">= 20.19.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/" @@ -6584,9 +6638,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" @@ -6865,24 +6919,24 @@ } }, "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", @@ -6899,13 +6953,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" }, @@ -6914,9 +6968,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", @@ -6931,46 +6985,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", @@ -6978,19 +6992,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", @@ -7006,46 +7021,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", @@ -7235,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", @@ -8093,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" } @@ -8278,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": { @@ -8508,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", @@ -8761,6 +8740,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", @@ -9046,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" @@ -9059,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": { @@ -9103,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", @@ -9142,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", @@ -9158,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" }, @@ -9171,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" } @@ -9181,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", @@ -9196,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" } @@ -9205,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" } @@ -9223,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" } @@ -9233,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" }, @@ -9246,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" } @@ -9293,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": { @@ -10130,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" @@ -11968,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" }, @@ -12254,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": { @@ -12753,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": { @@ -13113,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" @@ -13504,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", @@ -14815,9 +14783,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" @@ -15358,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": { @@ -16052,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" @@ -16138,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" }, @@ -19637,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" @@ -20207,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" @@ -20999,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" } @@ -22587,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", @@ -22936,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", @@ -22952,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" } @@ -22962,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" } @@ -23374,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", @@ -23603,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", @@ -23681,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", @@ -23720,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" } @@ -23736,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": { @@ -25143,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" @@ -25157,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" } @@ -25242,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 e8b9a11..b84a100 100644 --- a/package.json +++ b/package.json @@ -112,22 +112,27 @@ "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.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", - "@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", "ethers": "^5.8.0", "ethersV6": "npm:ethers@^6.14.4", "js-sha3": "^0.9.3", @@ -186,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" diff --git a/src/__tests__/core/documentBuilder.test.ts b/src/__tests__/core/documentBuilder.test.ts index 831e616..0c21c89 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://polygon-amoy-bor-rpc.publicnode.com'; + // ----------------------------- // 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.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 424c8af..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 = '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', () => { @@ -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__/e2e/fixtures/sample-boe-credential.ts b/src/__tests__/e2e/fixtures/sample-boe-credential.ts new file mode 100644 index 0000000..d7b1b5a --- /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: Number(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..bfb81fe --- /dev/null +++ b/src/__tests__/e2e/obligation-registry-functions/statusLifecycle.e2e.test.ts @@ -0,0 +1,286 @@ +import assert from 'node:assert/strict'; +import { network } from 'hardhat'; +import '@nomiclabs/hardhat-ethers'; +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'; + +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; + + 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 }, + ); + + assert.equal(status, ObligationDocumentStatus.Issued); + assert.equal(registered, true); + assert.equal(reason, ObligationEscrowTerminationReason.None); + }); + + it('E2: duplicate mint fails', async function () { + const tokenId = '2'; + + await mintObligationE2EToken(setup, tokenId, setup.holder.address, setup.beneficiary.address); + + await expectRejection( + mintObligationRegistry( + { obligationRegistryAddress: setup.obligationRegistry }, + setup.deployer, + { + beneficiaryAddress: setup.beneficiary.address, + holderAddress: setup.holder.address, + tokenId, + }, + setup.txOptions, + ), + ); + }); + + it('E3: accept fails when beneficiary == holder', async function () { + const tokenId = '3'; + + await mintObligationE2EToken(setup, tokenId, setup.holder.address, setup.holder.address); + + await expectRejection( + acceptObligationRegistry( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.holder, + {}, + setup.txOptions, + ), + /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 }, + ); + + assert.equal(status, ObligationDocumentStatus.Accepted); + }); + + it('E5: non-holder cannot accept', async function () { + const tokenId = '5'; + + await mintObligationE2EToken(setup, tokenId, setup.holder.address, setup.beneficiary.address); + + await expectRejection( + acceptObligationRegistry( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.beneficiary, + {}, + setup.txOptions, + ), + /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, + ); + + 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 () { + 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 }, + ); + + assert.equal(status, ObligationDocumentStatus.Discharged); + assert.equal(reason, ObligationEscrowTerminationReason.Discharged); + }); + + it('E8: discharge while Issued fails', async function () { + const tokenId = '8'; + + await mintObligationE2EToken(setup, tokenId, setup.holder.address, setup.beneficiary.address); + + await expectRejection( + dischargeObligationRegistry( + { obligationRegistryAddress: setup.obligationRegistry, tokenId }, + setup.beneficiary, + {}, + setup.txOptions, + ), + /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 }, + ); + + assert.equal(status, 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..bc6eb09 --- /dev/null +++ b/src/__tests__/e2e/obligation-registry-functions/transfer.e2e.test.ts @@ -0,0 +1,195 @@ +import { expect } from 'chai'; +import { network } from 'hardhat'; +import '@nomiclabs/hardhat-ethers'; +import '@nomicfoundation/hardhat-chai-matchers'; +import 'chai-as-promised'; +import { + acceptObligationRegistry, + nominateObligationRegistry, + transferBeneficiaryObligationRegistry, + transferHolderObligationRegistry, + transferOwnersObligationRegistry, +} from '../../../obligation-registry-functions'; +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 () { + 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.revertedWith(/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'); + + 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 () { + 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__/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/__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..939c250 --- /dev/null +++ b/src/__tests__/obligation-registry-functions/lifecycle.test.ts @@ -0,0 +1,117 @@ +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'; +import { ObligationContractOptions } from '../../obligation-registry-functions/types'; + +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 unknown 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, + } as ObligationContractOptions, + 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..62b0cb7 --- /dev/null +++ b/src/__tests__/obligation-registry-functions/mint.test.ts @@ -0,0 +1,148 @@ +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 unknown 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..25f8c83 --- /dev/null +++ b/src/__tests__/obligation-registry-functions/ownerOf.test.ts @@ -0,0 +1,43 @@ +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..3cd3cda --- /dev/null +++ b/src/__tests__/obligation-registry-functions/rejectTransfers.test.ts @@ -0,0 +1,89 @@ +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 unknown 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..1b8be4c --- /dev/null +++ b/src/__tests__/obligation-registry-functions/returnToken.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 * 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 unknown 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..3f82b72 --- /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 unknown 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..8183bbd --- /dev/null +++ b/src/__tests__/obligation-registry-functions/transfers.test.ts @@ -0,0 +1,122 @@ +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'; +import { ObligationContractOptions } from '../../obligation-registry-functions/types'; + +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 unknown 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'); + // 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, + } as ObligationContractOptions, + wallet, + { holderAddress: '0xNewHolder' }, + { chainId: mockChainId }, + ), + ).rejects.toThrow('Token ID is required'); + }); + }, +); 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/__tests__/verify/obligationRecordVerifier.utils.test.ts b/src/__tests__/verify/obligationRecordVerifier.utils.test.ts new file mode 100644 index 0000000..36c6a06 --- /dev/null +++ b/src/__tests__/verify/obligationRecordVerifier.utils.test.ts @@ -0,0 +1,122 @@ +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); + }); + + 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/core/documentBuilder.ts b/src/core/documentBuilder.ts index db7b5ff..44702c8 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 { + readonly chain: string; + readonly chainId: number; + readonly obligationRegistry: string; + readonly 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. @@ -87,11 +105,15 @@ 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 readonly documentType: string = 'w3c'; // Default to W3C + 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. + 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 /** @@ -148,6 +170,32 @@ 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.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( + '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 +251,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 +322,17 @@ export class DocumentBuilder { return config && typeof config.url === 'string' && typeof config.index === 'number'; } + private isObligationRecordsConfig( + config: Partial, + ): config is W3CObligationRecordsConfig { + return ( + 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) => { @@ -306,49 +368,89 @@ 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 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); + }, + ); + } + + 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. 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/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/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/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/index.ts b/src/index.ts index 03c66f9..5f373bf 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 './eip7702-functions'; export * from './core'; export * from './open-attestation'; diff --git a/src/obligation-registry-functions/deploy.ts b/src/obligation-registry-functions/deploy.ts new file mode 100644 index 0000000..5196af4 --- /dev/null +++ b/src/obligation-registry-functions/deploy.ts @@ -0,0 +1,171 @@ +import { v5Contracts } from '../token-registry-v5'; +import { getChainIdSafe, getTxOptions } 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 || undefined : 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..22b02d6 --- /dev/null +++ b/src/obligation-registry-functions/lifecycle.ts @@ -0,0 +1,7 @@ +import { createRemarkEscrowMethod } from './utils'; + +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 new file mode 100644 index 0000000..0a68689 --- /dev/null +++ b/src/obligation-registry-functions/mint.ts @@ -0,0 +1,29 @@ +import { v5SupportInterfaceIds } from '../token-registry-v5'; +import { Signer as SignerV6 } from 'ethersV6'; +import { Signer } from 'ethers'; +import { executeRegistryMethod, getEncryptedRemarks } from './utils'; +import { + MintObligationTokenOptions, + MintObligationTokenParams, + ObligationTransactionResponse, + TransactionOptions, +} from './types'; + +const mintObligationRegistry = async ( + contractOptions: MintObligationTokenOptions, + signer: Signer | SignerV6, + params: MintObligationTokenParams, + options: TransactionOptions, +): Promise => { + const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); + return executeRegistryMethod( + contractOptions.obligationRegistryAddress, + signer, + v5SupportInterfaceIds.TradeTrustTokenMintable, + 'mint', + [params.beneficiaryAddress, params.holderAddress, params.tokenId, encryptedRemarks], + options, + ); +}; + +export { mintObligationRegistry }; diff --git a/src/obligation-registry-functions/ownerOf.ts b/src/obligation-registry-functions/ownerOf.ts new file mode 100644 index 0000000..2cd7a5f --- /dev/null +++ b/src/obligation-registry-functions/ownerOf.ts @@ -0,0 +1,39 @@ +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 } from './types'; + +const ownerOfObligationRegistry = async ( + contractOptions: OwnerOfObligationTokenOptions, + signer: Signer | SignerV6, + params: OwnerOfObligationTokenParams, +): Promise => { + 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..a5d8875 --- /dev/null +++ b/src/obligation-registry-functions/rejectTransfers.ts @@ -0,0 +1,13 @@ +import { createRemarkEscrowMethod } from './utils'; + +const rejectTransferHolderObligationRegistry = createRemarkEscrowMethod('rejectTransferHolder'); +const rejectTransferBeneficiaryObligationRegistry = createRemarkEscrowMethod( + 'rejectTransferBeneficiary', +); +const rejectTransferOwnersObligationRegistry = createRemarkEscrowMethod('rejectTransferOwners'); + +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..a098633 --- /dev/null +++ b/src/obligation-registry-functions/returnToken.ts @@ -0,0 +1,54 @@ +import { v5SupportInterfaceIds } from '../token-registry-v5'; +import { Signer as SignerV6 } from 'ethersV6'; +import { Signer } from 'ethers'; +import { + AcceptReturnedObligationOptions, + AcceptReturnedObligationParams, + ObligationTransactionResponse, + RejectReturnedObligationOptions, + RejectReturnedObligationParams, + TransactionOptions, +} from './types'; +import { createRemarkEscrowMethod, executeRegistryMethod, getEncryptedRemarks } from './utils'; + +const returnToIssuerObligationRegistry = createRemarkEscrowMethod('returnToIssuer'); + +const acceptReturnedObligationRegistry = async ( + contractOptions: AcceptReturnedObligationOptions, + signer: Signer | SignerV6, + params: AcceptReturnedObligationParams, + options: TransactionOptions, +): Promise => { + const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); + return executeRegistryMethod( + contractOptions.obligationRegistryAddress, + signer, + v5SupportInterfaceIds.TradeTrustTokenBurnable, + 'burn', + [params.tokenId, encryptedRemarks], + options, + ); +}; + +const rejectReturnedObligationRegistry = async ( + contractOptions: RejectReturnedObligationOptions, + signer: Signer | SignerV6, + params: RejectReturnedObligationParams, + options: TransactionOptions, +): Promise => { + const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); + return executeRegistryMethod( + contractOptions.obligationRegistryAddress, + signer, + v5SupportInterfaceIds.TradeTrustTokenRestorable, + 'restore', + [params.tokenId, encryptedRemarks], + options, + ); +}; + +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..9abdccc --- /dev/null +++ b/src/obligation-registry-functions/status.ts @@ -0,0 +1,52 @@ +import { Signer as SignerV6 } from 'ethersV6'; +import { Signer } from 'ethers'; +import { + ObligationContractOptions, + ObligationDocumentStatus, + ObligationEscrowTerminationReason, + ObligationStatusReadOptions, + ObligationTokenIdParams, +} from './types'; +import { getEscrowContract } from './utils'; + +type EscrowViewReader = ( + contractOptions: ObligationContractOptions, + signer: Signer | SignerV6, + _params: ObligationTokenIdParams, + 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 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, + isObligationRegistryRegistered, + getObligationEscrowTerminationReason, +}; diff --git a/src/obligation-registry-functions/transfer.ts b/src/obligation-registry-functions/transfer.ts new file mode 100644 index 0000000..ca5b579 --- /dev/null +++ b/src/obligation-registry-functions/transfer.ts @@ -0,0 +1,83 @@ +import { Signer as SignerV6 } from 'ethersV6'; +import { Signer } from 'ethers'; +import { executeEscrowMethod, getEncryptedRemarks } from './utils'; +import { + NominateObligationParams, + ObligationContractOptions, + ObligationTransactionResponse, + TransactionOptions, + TransferObligationBeneficiaryParams, + TransferObligationHolderParams, + TransferObligationOwnersParams, +} from './types'; + +const transferHolderObligationRegistry = async ( + contractOptions: ObligationContractOptions, + signer: Signer | SignerV6, + params: TransferObligationHolderParams, + options: TransactionOptions, +): Promise => { + const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); + return executeEscrowMethod( + contractOptions, + signer, + 'transferHolder', + [params.holderAddress, encryptedRemarks], + options, + ); +}; + +const transferBeneficiaryObligationRegistry = async ( + contractOptions: ObligationContractOptions, + signer: Signer | SignerV6, + params: TransferObligationBeneficiaryParams, + options: TransactionOptions, +): Promise => { + const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); + return executeEscrowMethod( + contractOptions, + signer, + 'transferBeneficiary', + [params.newBeneficiaryAddress, encryptedRemarks], + options, + ); +}; + +const nominateObligationRegistry = async ( + contractOptions: ObligationContractOptions, + signer: Signer | SignerV6, + params: NominateObligationParams, + options: TransactionOptions, +): Promise => { + const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); + return executeEscrowMethod( + contractOptions, + signer, + 'nominate', + [params.newBeneficiaryAddress, encryptedRemarks], + options, + ); +}; + +const transferOwnersObligationRegistry = async ( + contractOptions: ObligationContractOptions, + signer: Signer | SignerV6, + params: TransferObligationOwnersParams, + options: TransactionOptions, +): Promise => { + const encryptedRemarks = getEncryptedRemarks(params.remarks, options.id); + return executeEscrowMethod( + contractOptions, + signer, + 'transferOwners', + [params.newBeneficiaryAddress, params.newHolderAddress, encryptedRemarks], + 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..6de5ac3 --- /dev/null +++ b/src/obligation-registry-functions/types.ts @@ -0,0 +1,124 @@ +import { CHAIN_ID } from '../utils'; +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; +} + +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..b7f2982 --- /dev/null +++ b/src/obligation-registry-functions/utils.ts @@ -0,0 +1,193 @@ +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 { getEthersContractFromProvider, isV6EthersProvider } from '../utils/ethers'; +import { + ObligationContractOptions, + ObligationTransactionResponse, + TransactionOptions, +} 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 === undefined || 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 => { + if (remarks && !id) { + throw new Error('An `id` is required to encrypt remarks'); + } + 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]; + }, +): 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); +}; + +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/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..4e3f4cf 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 => { @@ -28,15 +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; - } 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 new file mode 100644 index 0000000..0a0e24e --- /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?.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 | undefined => { + const credentialStatuses = [(document as SignedVerifiableCredential)?.credentialStatus].flat(); + + return credentialStatuses.find(isObligationRecordCredentialStatus); +}; + +export const isObligationRecord = ( + document: WrappedOrSignedOpenAttestationDocument | SignedVerifiableCredential, +): boolean => { + if (!isSignedDocument(document)) { + return false; + } + + const credentialStatuses = Array.isArray(document.credentialStatus) + ? document.credentialStatus + : [document.credentialStatus]; + + return credentialStatuses.some((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..c0b4478 --- /dev/null +++ b/src/verify/fragments/document-status/obligationRecords/obligationRecordVerifier.ts @@ -0,0 +1,123 @@ +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 { + 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'; +const name = OBLIGATION_RECORDS_TYPE; + +const verify: VerifierType['verify'] = async ( + document: DocumentsToVerify | SignedVerifiableCredential, + options: VerifierOptions, +) => { + const signedDocument = document as SignedVerifiableCredential; + const rawCredentialStatuses = Array.isArray(signedDocument?.credentialStatus) + ? signedDocument?.credentialStatus + : [signedDocument?.credentialStatus]; + const credentialStatuses = rawCredentialStatuses.filter(isObligationRecordCredentialStatus); + 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 + ], + ); + } + + const chainId = credentialStatus.tokenNetwork?.chainId; + if (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, + }); + }), + ); + + const result: ObligationRecordsResultFragment = { + name, + type, + status: 'INVALID' as const, + data: { + obligationRegistry: credentialStatuses?.[0]?.obligationRegistry, + }, + }; + + if (verificationResult.length > 0 && verificationResult.every(ValidTokenRegistryStatus.guard)) { + result.status = 'VALID' as const; + } else { + const invalidStatus = verificationResult.find( + (status): status is InvalidTokenRegistryStatus => !ValidTokenRegistryStatus.guard(status), + ); + result.reason = invalidStatus?.reason; + } + + return result; +}; + +const skip: VerifierType['skip'] = async () => + createObligationRecordsSkipFragment(name, '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]; + + return Boolean( + w3cVC.isSignedDocument(document) && + credentialStatuses.some((cs: w3cVC.CredentialStatus) => + isObligationRecordCredentialStatus(cs), + ), + ); +}; + +export const credentialStatusObligationRecordVerifier: VerifierType = { + skip, + test, + verify: async (...args): Promise => { + try { + return await verify(...args); + } catch (error: unknown) { + return toObligationRecordsErrorFragment(name, error); + } + }, +}; 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..5b480fa --- /dev/null +++ b/src/verify/fragments/document-status/obligationRecords/utils.ts @@ -0,0 +1,106 @@ +import { + ObligationEscrow__factory, + 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 = ( + 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, + provider, + chainId, +}: { + obligationRegistryAddress: string; + tokenId: string; + provider: providers.Provider; + chainId?: number | string; +}): Promise => { + if (chainId !== undefined) { + const network = await provider.getNetwork(); + const expectedChainId = Number(chainId); + // ethers v6 returns chainId as bigint + const actualChainId = Number(network.chainId); + + if (!Number.isFinite(expectedChainId) || actualChainId !== expectedChainId) { + return { + minted: false, + address: obligationRegistryAddress, + reason: { + code: OpenAttestationEthereumTokenRegistryStatusCode.UNRECOGNIZED_DOCUMENT, + codeString: + OpenAttestationEthereumTokenRegistryStatusCode[ + OpenAttestationEthereumTokenRegistryStatusCode.UNRECOGNIZED_DOCUMENT + ], + message: `Provider chain ID ${actualChainId} 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); + + 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. + 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 }; +}; 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', + }, + }; +}; diff --git a/src/verify/fragments/document-status/transferableRecords/transferableRecordVerifier.ts b/src/verify/fragments/document-status/transferableRecords/transferableRecordVerifier.ts index 424a4f7..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, @@ -110,7 +111,10 @@ 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 && !isObligationRecordCredentialStatus(cs), + ) ) { return true; } 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; diff --git a/src/verify/fragments/index.ts b/src/verify/fragments/index.ts index f82678d..3b313af 100644 --- a/src/verify/fragments/index.ts +++ b/src/verify/fragments/index.ts @@ -31,3 +31,8 @@ export { w3cSignatureIntegrity, ecdsaW3CSignatureIntegrity, }; + +export { + credentialStatusObligationRecordVerifier, + OBLIGATION_RECORDS_TYPE, +} from './document-status/obligationRecords/obligationRecordVerifier'; 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, ];