diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 51da03e..84209c1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - beta-boe jobs: test: diff --git a/.releaserc.json b/.releaserc.json index d11fc16..1d46820 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -1,5 +1,12 @@ { - "branches": ["main"], + "branches": [ + "main", + { + "name": "beta-boe", + "prerelease": "beta", + "channel": "beta" + } + ], "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", diff --git a/README.md b/README.md index c846ef5..65370f5 100644 --- a/README.md +++ b/README.md @@ -12,16 +12,23 @@ A comprehensive command-line interface for managing W3C Verifiable Credentials, - ✅ **Token Registry**: Mint tokens to blockchain-based token registries - ✅ **Document Store**: Deploy and manage document store contracts - ✅ **Title Escrow**: Complete transferable records management (holder/beneficiary transfers) +- ✅ **Obligation Registry (BoE)**: Deploy/mint obligation registries, manage ObligationEscrow lifecycle, and verify BoE documents via a dedicated pipeline - ✅ **Credential Status**: Create and update W3C credential status lists - ✅ **W3C Standards**: Compliant with latest W3C DID and Verifiable Credentials specifications - ✅ **Multi-Network Support**: Ethereum, Polygon, XDC, Stability, and Astron networks - ✅ **Interactive CLI**: User-friendly prompts for all operations + + ## Powered By This CLI leverages the TrustVC package: -- [`@trustvc/trustvc`](https://github.com/TrustVC/trustvc) - Core library for W3C credentials, OpenAttestation, token registries, and blockchain operations +- [`@trustvc/trustvc`](https://github.com/TrustVC/trustvc) — Core library for W3C credentials, OpenAttestation, token registries, Obligation Registry (BoE), and blockchain operations + +BoE on-chain helpers are imported from the **root** package (`mintObligationRegistry`, `acceptObligationRegistry`, …). Prefer those over any older `@trustvc/trustvc/obligation-registry` path. + + ## Table of Contents @@ -38,6 +45,9 @@ This CLI leverages the TrustVC package: - [Setup](#setup) - [Project Structure](#project-structure) - [License](#license) +- [Obligation Registry user guide](#obligation-registry-user-guide) + + ## Prerequisites @@ -48,6 +58,8 @@ nvm install 22 nvm use 22 ``` + + ## Installation Install the CLI globally: @@ -62,8 +74,12 @@ Or run a single command without installing: npx @trustvc/trustvc-cli ``` + + ## Quick Start + + ### W3C Verifiable Credentials ```sh @@ -86,6 +102,8 @@ trustvc credential-status-create trustvc credential-status-update ``` + + ### OpenAttestation Documents ```sh @@ -108,6 +126,8 @@ trustvc oa-encrypt trustvc oa-decrypt ``` + + ### Wallet Management ```sh @@ -121,6 +141,8 @@ trustvc wallet encrypt trustvc wallet decrypt ``` + + ### Document Store ```sh @@ -143,6 +165,8 @@ trustvc document-store revoke-role trustvc document-store transfer-ownership ``` + + ### Transaction ```sh @@ -154,6 +178,8 @@ trustvc transaction cancel [options] # e.g. trustvc transaction cancel --transaction-hash 0x... --network sepolia --encrypted-wallet-path ./wallet.json ``` + + ### Token Registry & Title Escrow ```sh @@ -188,99 +214,155 @@ trustvc title-escrow reject-transfer-owner trustvc title-escrow reject-transfer-owner-holder ``` + + +### Obligation Registry & Escrow (BoE) + +```sh +# Deploy Obligation Registry (TrustVCToken + factory) +trustvc obligation-registry deploy + +# Mint a BoE tokenId to obligationRegistry (sign first with w3c-sign) +trustvc w3c-sign +trustvc obligation-registry mint + +# Lifecycle +trustvc obligation-escrow accept +trustvc obligation-escrow reject +trustvc obligation-escrow discharge +trustvc obligation-escrow status + +# Transfers (mirror title-escrow) +trustvc obligation-escrow transfer-holder +trustvc obligation-escrow nominate-transfer-owner +trustvc obligation-escrow endorse-transfer-owner +trustvc obligation-escrow transfer-owner-holder +trustvc obligation-escrow reject-transfer-holder +trustvc obligation-escrow reject-transfer-owner +trustvc obligation-escrow reject-transfer-owner-holder +trustvc obligation-escrow return-to-issuer +trustvc obligation-escrow accept-return-to-issuer +trustvc obligation-escrow reject-return-to-issuer + +# Verify BoE or ETR documents (unified pipeline — auto-detects ObligationRecords) +trustvc verify +``` + + + ## How It Works + + ### W3C Credentials - **Key Pair Generation**: Uses `generateKeyPair` from `@trustvc/trustvc` to create cryptographic key pairs supporting ECDSA-SD-2023 and BBS-2023 cryptosuites in Multikey format. - - **DID Creation**: Uses `issueDID` to generate did:web identifiers, allowing self-hosted DIDs as unique identifiers in decentralized systems. - - **Credential Signing**: Uses `signW3C` to sign verifiable credentials with did:web identifiers and modern cryptosuites. - - **Credential Verification**: Uses `verifyDocument` to verify W3C verifiable credentials. - - **Credential Status**: Provides commands to create and update W3C credential status lists for managing credential revocation and suspension. + + ### OpenAttestation - **Document Signing**: Uses `signOA` to cryptographically sign OpenAttestation v2 and v3 documents with private keys. - - **Document Verification**: Uses `verifyDocument` to verify OpenAttestation documents. - - **Document Wrapping**: Uses `wrapOA` to wrap OpenAttestation documents. - - **Document Unwrapping**: Uses `unwrapOA` to unwrap OpenAttestation documents. - - **Document Encryption**: Uses `oa-encrypt` to encrypt OA documents for safe sharing; use `oa-decrypt` with the same key to recover the document. + + ### Blockchain Operations - **Token Registry**: Deploy token registry contracts and mint document hashes (tokenIds) to blockchain-based token registries across multiple networks (Ethereum, Polygon, XDC, Stability, Astron). - - **Document Store**: Deploy document store contracts and use `documentStoreIssue` and `documentStoreRevoke` to issue and revoke document hashes in deployed contracts. - - **Transaction Cancel**: Cancel a pending transaction by replacing it with a 0-value transaction to yourself (same nonce, higher gas price). Supports specifying by transaction hash or by nonce and gas price. - - **Title Escrow**: Provides comprehensive transferable records management including holder transfers, beneficiary nominations, endorsements, returns, and rejections using smart contracts. +- **Obligation Registry (BoE)**: Separate command trees for electronic Bill of Exchange on-chain flows — `obligation-registry` (deploy/mint) and `obligation-escrow` (accept/reject/discharge, transfers, return). Use **`trustvc verify`** for both ETR and BoE documents (ObligationRecords vs TransferableRecords is auto-detected). Do not use classic `token-registry` / `title-escrow` for obligation documents. See [Obligation Registry user guide](#obligation-registry-user-guide). + + ## Commands + + ### Available Commands -| Category | Command | Description | -| ------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------- | -| **W3C Credentials** | [`key-pair-generation`](#key-pair-generation) | Generate cryptographic key pairs (ECDSA-SD-2023, BBS-2023) | -| | [`did-web`](#did-web) | Create did:web identifiers from key pairs | -| | [`w3c-sign`](#w3c-sign) | Sign W3C verifiable credentials | -| | [`verify`](#verify) | Verify W3C verifiable credentials | -| | [`credential-status-create`](#credential-status-create) | Create credential status lists | -| | [`credential-status-update`](#credential-status-update) | Update credential status (revoke/suspend) | -| **OpenAttestation** | [`oa-sign`](#oa-sign) | Sign OpenAttestation v2/v3 documents | -| | [`verify`](#verify) | Verify OpenAttestation documents | -| | [`oa-wrap`](#oa-wrap) | Wrap OpenAttestation documents | -| | [`oa-unwrap`](#oa-unwrap) | Unwrap OpenAttestation documents | -| | [`oa-encrypt`](#oa-encrypt) | Encrypt an OA document for safe sharing and storage | -| | [`oa-decrypt`](#oa-decrypt) | Decrypt an OA document encrypted with oa-encrypt | -| **Token Registry** | [`mint`](#mint) | Mint tokens to blockchain registries | -| | [`token-registry deploy`](#token-registry-deploy) | Deploy token registry contracts | -| | [`mint`](#mint) | Mint tokens to blockchain registries | -| | `token-registry mint` | Alternative: `mint` | -| **Document Store** | [`document-store deploy`](#document-store-deploy) | Deploy document store contracts | -| | [`document-store issue`](#document-store-issue) | Issue document hashes | -| | [`document-store revoke`](#document-store-revoke) | Revoke document hashes | -| | [`document-store grant-role`](#document-store-grant-role) | Grant roles to accounts | -| | [`document-store revoke-role`](#document-store-revoke-role) | Revoke roles from accounts | -| | [`document-store transfer-ownership`](#document-store-transfer-ownership) | Transfer document store ownership | -| **Transaction** | [`transaction cancel`](#transaction-cancel) | Cancel a pending transaction | -| **Wallet** | [`wallet create`](#wallet-create) | Create a new encrypted wallet file | -| | [`wallet encrypt`](#wallet-encrypt) | Encrypt a wallet using a private key | -| | [`wallet decrypt`](#wallet-decrypt) | Decrypt an encrypted wallet file | -| **Title Escrow** | [`transfer-holder`](#title-escrow-transfer-holder) | Transfer document holder | -| | `title-escrow transfer-holder` | Alternative: `transfer-holder` | -| | [`nominate-transfer-owner`](#title-escrow-nominate-transfer-owner) | Nominate new beneficiary | -| | `title-escrow nominate-transfer-owner` | Alternative: `nominate-transfer-owner` | -| | [`endorse-transfer-owner`](#title-escrow-endorse-transfer-owner) | Endorse beneficiary change | -| | `title-escrow endorse-transfer-owner` | Alternative: `endorse-transfer-owner` | -| | [`transfer-owner-holder`](#title-escrow-transfer-owner-holder) | Endorse full ownership transfer | -| | `title-escrow transfer-owner-holder` | Alternative: `transfer-owner-holder` | -| | [`return-to-issuer`](#title-escrow-return-to-issuer) | Return document to issuer | -| | `title-escrow return-to-issuer` | Alternative: `return-to-issuer` | -| | [`accept-return-to-issuer`](#title-escrow-accept-return-to-issuer) | Accept returned document | -| | `title-escrow accept-return-to-issuer` | Alternative: `accept-return-to-issuer` | -| | [`reject-return-to-issuer`](#title-escrow-reject-return-to-issuer) | Reject returned document | -| | `title-escrow reject-return-to-issuer` | Alternative: `reject-return-to-issuer` | -| | [`reject-transfer-holder`](#title-escrow-reject-transfer-holder) | Reject holder transfer | -| | `title-escrow reject-transfer-holder` | Alternative: `reject-transfer-holder` | -| | [`reject-transfer-owner`](#title-escrow-reject-transfer-owner) | Reject owner transfer | -| | [`reject-transfer-owner-holder`](#title-escrow-reject-transfer-owner-holder) | Reject full transfer | -| | `title-escrow reject-transfer-owner-holder` | Alternative: `reject-transfer-owner-holder` | + +| Category | Command | Description | +| -------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------- | +| **W3C Credentials** | `[key-pair-generation](#key-pair-generation)` | Generate cryptographic key pairs (ECDSA-SD-2023, BBS-2023) | +| | `[did-web](#did-web)` | Create did:web identifiers from key pairs | +| | `[w3c-sign](#w3c-sign)` | Sign W3C verifiable credentials | +| | `[verify](#verify)` | Verify W3C verifiable credentials | +| | `[credential-status-create](#credential-status-create)` | Create credential status lists | +| | `[credential-status-update](#credential-status-update)` | Update credential status (revoke/suspend) | +| **OpenAttestation** | `[oa-sign](#oa-sign)` | Sign OpenAttestation v2/v3 documents | +| | `[verify](#verify)` | Verify OpenAttestation documents | +| | `[oa-wrap](#oa-wrap)` | Wrap OpenAttestation documents | +| | `[oa-unwrap](#oa-unwrap)` | Unwrap OpenAttestation documents | +| | `[oa-encrypt](#oa-encrypt)` | Encrypt an OA document for safe sharing and storage | +| | `[oa-decrypt](#oa-decrypt)` | Decrypt an OA document encrypted with oa-encrypt | +| **Token Registry** | `[mint](#mint)` | Mint tokens to blockchain registries | +| | `[token-registry deploy](#token-registry-deploy)` | Deploy token registry contracts | +| | `[mint](#mint)` | Mint tokens to blockchain registries | +| | `token-registry mint` | Alternative: `mint` | +| **Document Store** | `[document-store deploy](#document-store-deploy)` | Deploy document store contracts | +| | `[document-store issue](#document-store-issue)` | Issue document hashes | +| | `[document-store revoke](#document-store-revoke)` | Revoke document hashes | +| | `[document-store grant-role](#document-store-grant-role)` | Grant roles to accounts | +| | `[document-store revoke-role](#document-store-revoke-role)` | Revoke roles from accounts | +| | `[document-store transfer-ownership](#document-store-transfer-ownership)` | Transfer document store ownership | +| **Transaction** | `[transaction cancel](#transaction-cancel)` | Cancel a pending transaction | +| **Wallet** | `[wallet create](#wallet-create)` | Create a new encrypted wallet file | +| | `[wallet encrypt](#wallet-encrypt)` | Encrypt a wallet using a private key | +| | `[wallet decrypt](#wallet-decrypt)` | Decrypt an encrypted wallet file | +| **Title Escrow** | `[transfer-holder](#title-escrow-transfer-holder)` | Transfer document holder | +| | `title-escrow transfer-holder` | Alternative: `transfer-holder` | +| | `[nominate-transfer-owner](#title-escrow-nominate-transfer-owner)` | Nominate new beneficiary | +| | `title-escrow nominate-transfer-owner` | Alternative: `nominate-transfer-owner` | +| | `[endorse-transfer-owner](#title-escrow-endorse-transfer-owner)` | Endorse beneficiary change | +| | `title-escrow endorse-transfer-owner` | Alternative: `endorse-transfer-owner` | +| | `[transfer-owner-holder](#title-escrow-transfer-owner-holder)` | Endorse full ownership transfer | +| | `title-escrow transfer-owner-holder` | Alternative: `transfer-owner-holder` | +| | `[return-to-issuer](#title-escrow-return-to-issuer)` | Return document to issuer | +| | `title-escrow return-to-issuer` | Alternative: `return-to-issuer` | +| | `[accept-return-to-issuer](#title-escrow-accept-return-to-issuer)` | Accept returned document | +| | `title-escrow accept-return-to-issuer` | Alternative: `accept-return-to-issuer` | +| | `[reject-return-to-issuer](#title-escrow-reject-return-to-issuer)` | Reject returned document | +| | `title-escrow reject-return-to-issuer` | Alternative: `reject-return-to-issuer` | +| | `[reject-transfer-holder](#title-escrow-reject-transfer-holder)` | Reject holder transfer | +| | `title-escrow reject-transfer-holder` | Alternative: `reject-transfer-holder` | +| | `[reject-transfer-owner](#title-escrow-reject-transfer-owner)` | Reject owner transfer | +| | `[reject-transfer-owner-holder](#title-escrow-reject-transfer-owner-holder)` | Reject full transfer | +| | `title-escrow reject-transfer-owner-holder` | Alternative: `reject-transfer-owner-holder` | +| **Obligation / BoE** | `[obligation-registry deploy](#obligation-registry-deploy)` | Deploy Obligation Registry | +| | `[obligation-registry mint](#obligation-registry-mint)` | Mint BoE token to obligationRegistry | +| | `[obligation-escrow accept](#obligation-escrow-accept)` | Accept obligation | +| | `[obligation-escrow reject](#obligation-escrow-reject)` | Reject obligation | +| | `[obligation-escrow discharge](#obligation-escrow-discharge)` | Discharge obligation | +| | `[obligation-escrow status](#obligation-escrow-status)` | Read obligation / escrow status | +| | `[obligation-escrow transfer-holder](#obligation-escrow-transfer-holder)` | Transfer BoE holder | +| | `obligation-escrow nominate-transfer-owner` | Nominate BoE beneficiary | +| | `obligation-escrow endorse-transfer-owner` | Endorse BoE beneficiary change | +| | `obligation-escrow transfer-owner-holder` | Endorse full BoE ownership transfer | +| | `obligation-escrow return-to-issuer` | Return BoE to issuer | +| | `obligation-escrow accept-return-to-issuer` | Accept returned BoE | +| | `obligation-escrow reject-return-to-issuer` | Reject returned BoE | +| | `obligation-escrow reject-transfer-*` | Reject BoE transfer requests | + + + --- + + ### Wallet/Private Key Options -All title-escrow, token registry, document-store, and transaction commands require a wallet or private key to sign transactions. You can provide your private key in one of the following ways: +All title-escrow, obligation-registry, obligation-escrow, token registry, document-store, and transaction commands require a wallet or private key to sign transactions. You can provide your private key in one of the following ways: **Select wallet/private key option:** @@ -291,10 +373,11 @@ All title-escrow, token registry, document-store, and transaction commands requi --- + + ### Detailed Command Reference -
-

transaction cancel

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

key-pair-generation

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

did-web

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

w3c-sign

+ +#### w3c-sign Signs a W3C verifiable credential using a did:web identifier. @@ -418,12 +496,13 @@ trustvc w3c-sign **Output:** Creates `signed_vc.json` with cryptographic proof. -
-
-

verify

-Verifies a W3C or OA document using respective verification methods. +#### verify + +Verifies a W3C or OA document using the unified TrustVC verification pipeline. + +Works for **classic ETR** (`tokenRegistry` → TransferableRecords fragment) and **BoE** (`obligationRegistry` → ObligationRecords fragment). When the document is an obligation record, the CLI also prints enriched on-chain status when available. **Usage:** @@ -437,18 +516,17 @@ trustvc verify - [If network required but no network detected]: Select network **Output:** -Verifies the document integrity, status, and issuer identity. +Verifies document integrity, status, and issuer identity. For BoE documents, logs obligation registry status when the ObligationRecords fragment is VALID. **Supported Formats:** -- W3C Verifiable Credential +- W3C Verifiable Credential (ETR, BoE, revocable VDs) - OpenAttestation v2 - OpenAttestation v3 -
-
-

credential-status-create

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

credential-status-update

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

oa-sign

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

oa-wrap

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

oa-unwrap

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

oa-encrypt

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

oa-decrypt

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

mint

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

token-registry deploy

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

token-registry mint

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

document-store deploy

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

document-store issue

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

document-store revoke

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

document-store grant-role

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

document-store revoke-role

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

document-store transfer-ownership

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

wallet create

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

wallet encrypt

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

wallet decrypt

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

title-escrow transfer-holder

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

title-escrow nominate-transfer-owner

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

title-escrow endorse-transfer-owner

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

title-escrow transfer-owner-holder

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

title-escrow return-to-issuer

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

title-escrow accept-return-to-issuer

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

title-escrow reject-return-to-issuer

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

title-escrow reject-transfer-holder

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

title-escrow reject-transfer-owner

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

title-escrow reject-transfer-owner-holder

+ +#### title-escrow reject-transfer-owner-holder Rejects a full ownership transfer (both beneficiary and holder). @@ -1333,17 +1383,182 @@ trustvc title-escrow reject-transfer-owner-holder **Interactive Prompts:** - Path to TT/JSON document file (or manual input) - - _Network, token registry address, token ID, and document ID are extracted from the document_ + - *Network, token registry address, token ID, and document ID are extracted from the document* - Wallet/private key option - Remark (optional, V5 registries only - will be encrypted with document ID as encryption key) **Output:** Transaction receipt confirming rejection. -
+ + +#### obligation-registry deploy + +Deploys an Obligation Registry (`TrustVCToken` + `ObligationEscrowFactory`) for electronic Bill of Exchange (BoE) flows. + +**Do not use** `token-registry deploy` for BoE documents. + +**Usage:** + +```sh +trustvc obligation-registry deploy +``` + +**Interactive Prompts:** + +- Network selection +- Registry name and symbol +- Optionally reuse an existing ObligationEscrowFactory address +- Wallet/private key option +- Dry-run confirmation before broadcasting + +**Output:** +Deployed `ObligationEscrowFactory` and Obligation Registry addresses plus transaction receipt. Copy the Obligation Registry address into `credentialStatus.obligationRegistry` in your BoE document before signing and minting. + + + +#### obligation-registry mint + +Mints a BoE tokenId to an Obligation Registry and creates the linked ObligationEscrow. + +**Do not use** classic `mint` / `token-registry mint` for BoE documents. + +**Before minting:** set `credentialStatus.obligationRegistry` to your deployed registry address, then sign the document with [`w3c-sign`](#w3c-sign). Mint only accepts a signed Verifiable Credential. + +**Usage:** + +```sh +trustvc w3c-sign +trustvc obligation-registry mint +``` + +**Interactive Prompts:** + +- Path to signed BoE / obligation document (output of `w3c-sign`) + - *Network, obligationRegistry address, token ID, and document ID are extracted from the document* +- Holder and beneficiary (drawer/drawee) addresses as required +- Wallet/private key option +- Dry-run confirmation before broadcasting + +**Output:** +Transaction receipt confirming mint. + + + +#### obligation-escrow accept + +Accepts an obligation on the ObligationEscrow (drawee acceptance). + +**Usage:** + +```sh +trustvc obligation-escrow accept +``` + +**Interactive Prompts:** + +- Path to BoE / obligation document + - *Network, obligationRegistry, token ID, and document ID are extracted from the document* +- Wallet/private key option +- Remark (optional) + +**Output:** +Transaction receipt confirming acceptance. + + + +#### obligation-escrow reject + +Rejects an obligation on the ObligationEscrow. + +**Usage:** + +```sh +trustvc obligation-escrow reject +``` + +**Interactive Prompts:** + +- Path to BoE / obligation document +- Wallet/private key option +- Remark (optional) + +**Output:** +Transaction receipt confirming rejection. + + + +#### obligation-escrow discharge + +Discharges an accepted obligation on the ObligationEscrow. + +**Usage:** + +```sh +trustvc obligation-escrow discharge +``` + +**Interactive Prompts:** + +- Path to BoE / obligation document +- Wallet/private key option +- Remark (optional) + +**Output:** +Transaction receipt confirming discharge. + + + +#### obligation-escrow status + +Reads on-chain obligation / escrow status for a BoE document (read-only; no wallet required for the status call itself beyond network connectivity). + +**Usage:** + +```sh +trustvc obligation-escrow status +``` + +**Interactive Prompts:** + +- Path to BoE / obligation document + - *Network, obligationRegistry, and token ID are extracted from the document* + +**Output:** +Obligation and escrow status fields from the chain. + + + +#### obligation-escrow transfer-holder + +Transfers the holder of a BoE obligation record. Transfer, nominate, endorse, return, and reject variants mirror `title-escrow` but operate on ObligationEscrow via `obligation-escrow `. + +**Usage:** + +```sh +trustvc obligation-escrow transfer-holder +# Also: +# nominate-transfer-owner, endorse-transfer-owner, transfer-owner-holder +# return-to-issuer, accept-return-to-issuer, reject-return-to-issuer +# reject-transfer-holder, reject-transfer-owner, reject-transfer-owner-holder +``` + +**Interactive Prompts:** + +- Path to BoE / obligation document +- New address(es) where applicable +- Wallet/private key option +- Remark (optional) + +**Output:** +Transaction receipt confirming the escrow action. + + ## Configuration + + ### Custom RPC Endpoints You can override the default RPC endpoints for any network by setting environment variables. The format is `{NETWORK_NAME}_RPC`. @@ -1381,6 +1596,8 @@ If no environment variable is set, the CLI will use the default RPC endpoint for ## Development + + ### Setup ```sh @@ -1397,6 +1614,8 @@ npm link npm test ``` + + ### Project Structure ``` @@ -1428,6 +1647,24 @@ src/commands/ │ ├── reject-transfer-holder.ts # Reject holder transfer │ ├── reject-transfer-owner.ts # Reject owner transfer │ └── reject-transfer-owner-holder.ts # Reject full transfer +├── obligation-registry/ +│ ├── deploy.ts # Deploy Obligation Registry +│ └── mint.ts # Mint BoE token to obligationRegistry +├── obligation-escrow/ +│ ├── accept.ts # Accept obligation +│ ├── reject.ts # Reject obligation +│ ├── discharge.ts # Discharge obligation +│ ├── status.ts # Read obligation / escrow status +│ ├── transfer-holder.ts # Transfer BoE holder +│ ├── nominate-transfer-owner.ts # Nominate BoE beneficiary +│ ├── endorse-transfer-owner.ts # Endorse BoE beneficiary change +│ ├── transfer-owner-holder.ts # Endorse full BoE transfer +│ ├── return-to-issuer.ts # Return BoE to issuer +│ ├── accept-return-to-issuer.ts # Accept returned BoE +│ ├── reject-return-to-issuer.ts # Reject returned BoE +│ ├── reject-transfer-holder.ts # Reject BoE holder transfer +│ ├── reject-transfer-owner.ts # Reject BoE owner transfer +│ └── reject-transfer-owner-holder.ts # Reject full BoE transfer ├── transaction/ │ └── cancel.ts # Cancel a pending transaction ├── wallet/ @@ -1444,6 +1681,125 @@ src/commands/ └── verify.ts # Verify W3C or OA document ``` + + +## Obligation Registry user guide + +User guide for electronic Bill of Exchange (BoE) / obligation flows with `trustvc-cli`. + +Classic transferable records (eBL / Token Registry + Title Escrow) use different commands. This section covers **only** Obligation Registry. + +For library / SDK details, see the TrustVC README [§7c Obligation Registry (BoE)](https://github.com/TrustVC/trustvc#c-obligation-registry-boe). + +SDK imports use the **root** package with the `*ObligationRegistry` suffix (no clash with classic ETR helpers): + +```ts +import { + mintObligationRegistry, + acceptObligationRegistry, + DocumentBuilder, + verifyDocument, +} from '@trustvc/trustvc'; +``` + +Build BoE credentials with `DocumentBuilder.obligationCredentialStatus({ obligationRegistry, chain, chainId, rpcProviderUrl })` (credential status uses `type: 'TransferableRecords'` plus an `obligationRegistry` field — not `tokenRegistry`). On-chain minting is separate via `obligation-registry mint` / `mintObligationRegistry`. + +### Who this is for + +Operators and integrators who: + +- Deploy an Obligation Registry on a supported network +- Mint a signed BoE credential on-chain +- Accept, reject, discharge, transfer, or return the obligation +- Verify a BoE document + +You do **not** need to call the TypeScript SDK directly — the CLI wraps it with interactive prompts. + +### Before you start + +1. **Install the CLI** — `npm install -g @trustvc/trustvc-cli` or `npx @trustvc/trustvc-cli ` +2. **Node.js 22+** +3. **A wallet** — encrypted wallet file (recommended), private key, or key file +4. **A signed BoE document** — set `credentialStatus.obligationRegistry`, then sign with `trustvc w3c-sign` (not `tokenRegistry`) +5. **Network access** — select network in prompts, or set a custom RPC (e.g. `export SEPOLIA_RPC=…`) + + + +### Classic vs Obligation — pick the right commands + + +| Task | Classic ETR | Obligation / BoE | +| --------------- | ------------------------------ | ---------------------------- | +| Deploy registry | `token-registry deploy` | `obligation-registry deploy` | +| Mint | `mint` / `token-registry mint` | `obligation-registry mint` | +| Escrow actions | `title-escrow …` | `obligation-escrow …` | +| Verify | `verify` (ETR and BoE) | `verify` (same command) | + + +Using classic commands on a BoE document will fail or skip obligation checks. Using obligation commands on a classic eBL document will fail extraction (missing `obligationRegistry`). + +### Typical workflow + +``` +1. Deploy Obligation Registry +2. Put the registry address into your BoE credential status +3. Sign the credential with `trustvc w3c-sign` +4. Mint (issuer wallet) +5. Accept or reject (holder) ──► optional transfers / discharge / return +6. Verify with `trustvc verify` +``` + +Signing/building the VC can also be done with TrustVC library tools or your app (`DocumentBuilder` + `obligationRegistry`). With the CLI, use `w3c-sign` before minting. + +### Step-by-step + +**1. Deploy** — `trustvc obligation-registry deploy` (network, name/symbol, optional factory reuse, wallet, dry-run). + +**2. Sign** — put the deployed registry into `credentialStatus.obligationRegistry`, then run `trustvc w3c-sign` on the unsigned BoE JSON. + +**3. Mint** — `trustvc obligation-registry mint` with the signed BoE JSON from `w3c-sign` (registry, network, token ID, document ID are extracted). + +**4. Accept or reject (holder)** — `trustvc obligation-escrow accept` or `reject` while **beneficiary ≠ holder**. Reject only sets status to Rejected; closing requires return-to-issuer afterward. + +**5. Status** — `trustvc obligation-escrow status` reads `Issued` / `Accepted` / `Rejected` / `Discharged`, registration, and termination reason. + +**6. Transfers (optional)** — `obligation-escrow transfer-holder`, nominate/endorse/return variants mirror `title-escrow`. + +**7. Discharge (optional)** — `trustvc obligation-escrow discharge` by **beneficiary** after Accepted (sets Discharged; does not burn). + +**8. Return to issuer** — requires status **Rejected** or **Discharged** and **beneficiary == holder**; then issuer runs `reject-return-to-issuer` (restore) or `accept-return-to-issuer` (burn). + +**8. Verify** — `trustvc verify` (auto-detects BoE vs ETR). + +### Who can run what + + +| Action | Typical role | +| ----------------------------------------------------- | ---------------------------------------------------------- | +| `obligation-registry deploy` | Deployer / issuer org | +| `obligation-registry mint` | Issuer (registry minter) | +| `obligation-escrow accept` / `reject` | Holder (roles split) | +| `obligation-escrow discharge` | Beneficiary | +| Transfer / nominate / endorse | Current holder or beneficiary per Title Escrow–style rules | +| `return-to-issuer` | Dual role (beneficiary == holder) | +| `accept-return-to-issuer` / `reject-return-to-issuer` | Issuer | +| `obligation-escrow status` | Anyone with network access + document | +| `verify` | Anyone with the document (+ RPC when on-chain checks run) | + + + + +### What the CLI reads from your document + +For mint, escrow, and related flows, the CLI extracts: + +- Network +- `obligationRegistry` address +- Token ID +- Document ID (remark encryption key when remarks are set) + +If extraction fails with a message about classic transferable records / `tokenRegistry`, you are using the wrong document type or command family. + ## License -Apache-2.0 +Apache-2.0 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 737cc65..29c40c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "@trustvc/trustvc-cli", - "version": "1.0.1", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@trustvc/trustvc-cli", - "version": "1.0.1", + "version": "1.1.0", "license": "Apache-2.0", "dependencies": { "@inquirer/prompts": "^5.3.8", - "@trustvc/trustvc": "^2.14.1", + "@trustvc/trustvc": "2.15.0-beta.3", "@types/yargs": "^17.0.32", "chalk": "^4.1.2", "dotenv": "^16.0.0", @@ -134,6 +134,16 @@ "ethers": "^5.8.0" } }, + "node_modules/@account-abstraction/contracts": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@account-abstraction/contracts/-/contracts-0.8.0.tgz", + "integrity": "sha512-8krPx/gpnoT+5xAroagVCbeA7FbUigMZWXFKKPm+oghyr29Dksssdx5sI7xGv9212i4JPaDDUGFk58dpuwVgHA==", + "license": "MIT", + "dependencies": { + "@openzeppelin/contracts": "^5.1.0", + "@uniswap/v3-periphery": "^1.4.3" + } + }, "node_modules/@actions/core": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.0.tgz", @@ -972,6 +982,81 @@ "node": ">=18" } }, + "node_modules/@digitalbazaar/ecdsa-rdfc-2019-cryptosuite": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@digitalbazaar/ecdsa-rdfc-2019-cryptosuite/-/ecdsa-rdfc-2019-cryptosuite-1.3.0.tgz", + "integrity": "sha512-Rhg++GnGWHJ29QyWTFW0tRqd/uGLADIsLVEq10zEIAY7D9ScoSLtyYzwZ/zBueBTXHXrtDUEL5SfrSvcKTLFyg==", + "license": "BSD-3-Clause", + "dependencies": { + "@digitalbazaar/ecdsa-multikey": "^1.6.0", + "jsonld": "^9.0.0", + "rdf-canonize": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@digitalbazaar/ecdsa-rdfc-2019-cryptosuite/node_modules/@digitalbazaar/http-client": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@digitalbazaar/http-client/-/http-client-4.3.0.tgz", + "integrity": "sha512-6lMpxpt9BOmqHKGs9Xm6DP4LlZTBFer/ZjHvP3FcW3IaUWYIWC7dw5RFZnvw4fP57kAVcm1dp3IF+Y50qhBvAw==", + "license": "BSD-3-Clause", + "dependencies": { + "ky": "^1.14.2", + "undici": "^6.23.0" + }, + "engines": { + "node": ">=18.0" + } + }, + "node_modules/@digitalbazaar/ecdsa-rdfc-2019-cryptosuite/node_modules/canonicalize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/canonicalize/-/canonicalize-2.1.0.tgz", + "integrity": "sha512-F705O3xrsUtgt98j7leetNhTWPe+5S72rlL5O4jA1pKqBVQ/dT1O1D6PFxmSXvc0SUOinWS57DKx0I3CHrXJHQ==", + "license": "Apache-2.0", + "bin": { + "canonicalize": "bin/canonicalize.js" + } + }, + "node_modules/@digitalbazaar/ecdsa-rdfc-2019-cryptosuite/node_modules/jsonld": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonld/-/jsonld-9.0.0.tgz", + "integrity": "sha512-pjMIdkXfC1T2wrX9B9i2uXhGdyCmgec3qgMht+TDj+S0qX3bjWMQUfL7NeqEhuRTi8G5ESzmL9uGlST7nzSEWg==", + "license": "BSD-3-Clause", + "dependencies": { + "@digitalbazaar/http-client": "^4.2.0", + "canonicalize": "^2.1.0", + "lru-cache": "^6.0.0", + "rdf-canonize": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@digitalbazaar/ecdsa-rdfc-2019-cryptosuite/node_modules/ky": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/ky/-/ky-1.14.3.tgz", + "integrity": "sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/ky?sponsor=1" + } + }, + "node_modules/@digitalbazaar/ecdsa-rdfc-2019-cryptosuite/node_modules/rdf-canonize": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/rdf-canonize/-/rdf-canonize-5.0.0.tgz", + "integrity": "sha512-g8OUrgMXAR9ys/ZuJVfBr05sPPoMA7nHIVs8VEvg9QwM5W4GR2qSFEEHjsyHF1eWlBaf8Ev40WNjQFQ+nJTO3w==", + "license": "BSD-3-Clause", + "dependencies": { + "setimmediate": "^1.0.5" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@digitalbazaar/ecdsa-sd-2023-cryptosuite": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/@digitalbazaar/ecdsa-sd-2023-cryptosuite/-/ecdsa-sd-2023-cryptosuite-3.4.1.tgz", @@ -2936,6 +3021,18 @@ "yarn": "1.x" } }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/curves": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.2.0.tgz", @@ -3144,6 +3241,12 @@ "@octokit/openapi-types": "^27.0.0" } }, + "node_modules/@openzeppelin/contracts": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.6.1.tgz", + "integrity": "sha512-Ly6SlsVJ3mj+b18W3R8gNufB7dTICT105fJhodGAGgyC2oqnBAhqSiNDJ8V8DLY05cCz81GLI0CU5vNYA1EC/w==", + "license": "MIT" + }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", @@ -3510,6 +3613,81 @@ "win32" ] }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", @@ -5004,9 +5182,9 @@ }, "node_modules/@tradetrust-tt/token-registry-v5": { "name": "@tradetrust-tt/token-registry", - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@tradetrust-tt/token-registry/-/token-registry-5.5.1.tgz", - "integrity": "sha512-yxOdjRLSI0fPptFbicYpnb/fn4Ngbi/LuGDCQKL0vb7PLCvhT8DLpiAFbFs0cc+QAb439lnZjhPg+UbWQHufRA==", + "version": "5.6.0-beta.1", + "resolved": "https://registry.npmjs.org/@tradetrust-tt/token-registry/-/token-registry-5.6.0-beta.1.tgz", + "integrity": "sha512-EtxZw/WnvGQMb/MB243eHy4ejSTN8bkV7I7Y9f19xk9v/mOojFL74C/tD1zjsZUUjiQi9rzLRYlzSUNbZBN2gg==", "license": "Apache-2.0", "dependencies": { "ethers": "^6.13.4" @@ -5359,30 +5537,43 @@ "integrity": "sha512-YIECQwcoreIfyTbol1/5u9CGK6mbg0Q0bSN2/Ks388zLus1IXELWK5EHYuyrFPbb9d8ajsvz7m+ySsMIY9574w==", "license": "Apache-2.0" }, + "node_modules/@trustvc/eip7702": { + "version": "1.0.0-beta.1", + "resolved": "https://registry.npmjs.org/@trustvc/eip7702/-/eip7702-1.0.0-beta.1.tgz", + "integrity": "sha512-cOGuZVLHd0+Uu7rZ/Dp2iF1VuNq+L+pWWGe+tPD/u74rSvCAzIc17dIk5HOwRBiC5awh1rXG46vLG8IRc5O01Q==", + "license": "MIT", + "dependencies": { + "@account-abstraction/contracts": "^0.8.0", + "@openzeppelin/contracts": "^5.6.1" + } + }, "node_modules/@trustvc/trustvc": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/@trustvc/trustvc/-/trustvc-2.14.1.tgz", - "integrity": "sha512-vGR6/s8nn69sl3Ntr/rE8+kg64RuqsRN2AeB0yeNOAUwTWEmQRk16cvB4g1xW7+CsLc5yI5sRuAqeAmbhHdV0g==", + "version": "2.15.0-beta.3", + "resolved": "https://registry.npmjs.org/@trustvc/trustvc/-/trustvc-2.15.0-beta.3.tgz", + "integrity": "sha512-EFiYmXZulyWnq+g3tb/yrpANkggE76QjDILuNE/68ZWdh9cw8yUxR+hcrqkk8tbhapmgulzQjtTuuASnuD1xog==", "license": "Apache-2.0", "dependencies": { "@tradetrust-tt/dnsprove": "^2.18.0", "@tradetrust-tt/ethers-aws-kms-signer": "^2.1.4", "@tradetrust-tt/token-registry-v4": "npm:@tradetrust-tt/token-registry@^4.16.0", - "@tradetrust-tt/token-registry-v5": "npm:@tradetrust-tt/token-registry@^5.5.0", + "@tradetrust-tt/token-registry-v5": "npm:@tradetrust-tt/token-registry@^5.6.0-beta.1", "@tradetrust-tt/tradetrust": "^6.10.3", "@tradetrust-tt/tt-verify": "^9.7.5", "@trustvc/document-store": "^1.0.3", - "@trustvc/w3c": "^2.2.0", - "@trustvc/w3c-context": "^2.2.0", - "@trustvc/w3c-credential-status": "^2.2.0", - "@trustvc/w3c-issuer": "^2.2.0", - "@trustvc/w3c-vc": "^2.2.0", + "@trustvc/eip7702": "^1.0.0-beta.1", + "@trustvc/w3c": "^2.4.1", + "@trustvc/w3c-context": "^2.4.0", + "@trustvc/w3c-credential-status": "^2.4.0", + "@trustvc/w3c-issuer": "^2.3.0", + "@trustvc/w3c-vc": "^2.4.1", "ethers": "^5.8.0", "ethersV6": "npm:ethers@^6.14.4", "js-sha3": "^0.9.3", "node-fetch": "^2.7.0", "node-forge": "^1.3.3", - "ts-chacha20": "^1.2.0" + "permissionless": "^0.3.6", + "ts-chacha20": "^1.2.0", + "viem": "^2.53.1" }, "engines": { "node": ">=20.0.0" @@ -5469,33 +5660,33 @@ } }, "node_modules/@trustvc/trustvc/node_modules/node-forge": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", - "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" } }, "node_modules/@trustvc/w3c": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@trustvc/w3c/-/w3c-2.2.0.tgz", - "integrity": "sha512-2WhAoYZW7JLt9H1OrT1qcwTL0g6LymMVg9uiZ3xxlaRgYzOmEqClN3Pq6apo1kRZTzJMHcKWECKADkHGa0Jgqw==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@trustvc/w3c/-/w3c-2.4.1.tgz", + "integrity": "sha512-yCyztSAbialRn9VYvXrHUE4v13q4g/BHUN4XUctzF5lrN3WsHKSgUpFUflocLdp1h5fxvJTbqyVoMSh5SDLwQw==", "license": "Apache-2.0", "dependencies": { - "@trustvc/w3c-context": "^2.2.0", - "@trustvc/w3c-credential-status": "^2.2.0", - "@trustvc/w3c-issuer": "^2.2.0", - "@trustvc/w3c-vc": "^2.2.0" + "@trustvc/w3c-context": "^2.4.0", + "@trustvc/w3c-credential-status": "^2.4.0", + "@trustvc/w3c-issuer": "^2.3.0", + "@trustvc/w3c-vc": "^2.4.1" }, "engines": { "node": ">=18.x" } }, "node_modules/@trustvc/w3c-context": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@trustvc/w3c-context/-/w3c-context-2.2.0.tgz", - "integrity": "sha512-p9mtIWZ1v1hhqiGLJ5Fu+2PK9ClIRsdo04vgCVC8BxhIjwUU7ZHb95sYF1E8Ay9pP2BRyFujBdoaYXHH8n5v4A==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@trustvc/w3c-context/-/w3c-context-2.4.0.tgz", + "integrity": "sha512-OCRfqZfTyEZ2Lpd5RPBl36DwvaKv3qV1PgZOEA9hrnEtHMWxwS5iSdv+xFYK8pZIoeheFZKymfgcB5LL7UBQRw==", "license": "Apache-2.0", "dependencies": { "did-resolver": "^4.1.0", @@ -5512,13 +5703,13 @@ "license": "Apache-2.0" }, "node_modules/@trustvc/w3c-credential-status": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@trustvc/w3c-credential-status/-/w3c-credential-status-2.2.0.tgz", - "integrity": "sha512-lfgnvAUSwdi5hWnuf+wqTkpPTYxmZyZ8kdzVPRQeKWqb0ysdWN+n32ROoNpQAFSPqYlSL0pLWuI/vg35WuhnEA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@trustvc/w3c-credential-status/-/w3c-credential-status-2.4.0.tgz", + "integrity": "sha512-fiMGtfOmq1x9e+vd89ZKqguYtmW2w2k2hxINkno+mDNHdt5CV8tfTH5EbL1fY0oPiuXM5SK+dV6iSBTIWGPFog==", "license": "Apache-2.0", "dependencies": { - "@trustvc/w3c-context": "^2.2.0", - "@trustvc/w3c-issuer": "^2.2.0", + "@trustvc/w3c-context": "^2.4.0", + "@trustvc/w3c-issuer": "^2.3.0", "base64url-universal": "^2.0.0", "pako": "^2.1.0" }, @@ -5527,9 +5718,9 @@ } }, "node_modules/@trustvc/w3c-issuer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@trustvc/w3c-issuer/-/w3c-issuer-2.2.0.tgz", - "integrity": "sha512-o5XWh52c3KeNqrrIpSvjPt+3zwZ/wwh2hlGOst6PZXVzS9nMab+jUwhs52d+HBhe2r8BL4Z81sdMGA8YAEnk6Q==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@trustvc/w3c-issuer/-/w3c-issuer-2.3.0.tgz", + "integrity": "sha512-J/Rlae2s/ihkF0q6OmBofmzdRjDI98ED9lRFIB6Uh2WaEl8eZP7+FoStip+t1Piy6ZbO/6MwaS0QCw/6N6viMA==", "license": "Apache-2.0", "dependencies": { "@digitalbazaar/bls12-381-multikey": "^2.1.0", @@ -5561,19 +5752,20 @@ } }, "node_modules/@trustvc/w3c-vc": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@trustvc/w3c-vc/-/w3c-vc-2.2.0.tgz", - "integrity": "sha512-QAfoEgNndi2X+V0Nz9nBGiUk4Ko0XUFVLn0BY6qJK8GHJndMMCEcZ14PlaHyKG8nIossQeKZbsBwnavm2jRFdg==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@trustvc/w3c-vc/-/w3c-vc-2.4.1.tgz", + "integrity": "sha512-f/pqBu70epEYVmtWvKvlhCd2F5QkV885rCcbfa9KZq9xYbOzeBueIF7P3gSdv3ILAiMCQTxvpL7hd8I8hj+M1A==", "license": "Apache-2.0", "dependencies": { "@digitalbazaar/bbs-2023-cryptosuite": "^2.0.1", "@digitalbazaar/bls12-381-multikey": "^2.1.0", "@digitalbazaar/data-integrity": "^2.5.0", "@digitalbazaar/ecdsa-multikey": "^1.8.0", + "@digitalbazaar/ecdsa-rdfc-2019-cryptosuite": "^1.3.0", "@digitalbazaar/ecdsa-sd-2023-cryptosuite": "^3.4.1", "@mattrglobal/jsonld-signatures-bbs": "^1.2.0", - "@trustvc/w3c-credential-status": "^2.2.0", - "@trustvc/w3c-issuer": "^2.2.0", + "@trustvc/w3c-credential-status": "^2.4.0", + "@trustvc/w3c-issuer": "^2.3.0", "base64url-universal": "^2.0.0", "cbor": "^9.0.2", "did-resolver": "^4.1.0", @@ -5906,6 +6098,55 @@ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "dev": true }, + "node_modules/@uniswap/lib": { + "version": "4.0.1-alpha", + "resolved": "https://registry.npmjs.org/@uniswap/lib/-/lib-4.0.1-alpha.tgz", + "integrity": "sha512-f6UIliwBbRsgVLxIaBANF6w09tYqc6Y/qXdsrbEmXHyFA7ILiKrIwRFXe1yOg8M3cksgVsO9N7yuL2DdCGQKBA==", + "license": "GPL-3.0-or-later", + "engines": { + "node": ">=10" + } + }, + "node_modules/@uniswap/v2-core": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@uniswap/v2-core/-/v2-core-1.0.1.tgz", + "integrity": "sha512-MtybtkUPSyysqLY2U210NBDeCHX+ltHt3oADGdjqoThZaFRDKwM6k1Nb3F0A3hk5hwuQvytFWhrWHOEq6nVJ8Q==", + "license": "GPL-3.0-or-later", + "engines": { + "node": ">=10" + } + }, + "node_modules/@uniswap/v3-core": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@uniswap/v3-core/-/v3-core-1.0.1.tgz", + "integrity": "sha512-7pVk4hEm00j9tc71Y9+ssYpO6ytkeI0y7WE9P6UcmNzhxPePwyAxImuhVsTqWK9YFvzgtvzJHi64pBl4jUzKMQ==", + "license": "BUSL-1.1", + "engines": { + "node": ">=10" + } + }, + "node_modules/@uniswap/v3-periphery": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@uniswap/v3-periphery/-/v3-periphery-1.4.4.tgz", + "integrity": "sha512-S4+m+wh8HbWSO3DKk4LwUCPZJTpCugIsHrWR86m/OrUyvSqGDTXKFfc2sMuGXCZrD1ZqO3rhQsKgdWg3Hbb2Kw==", + "license": "GPL-2.0-or-later", + "dependencies": { + "@openzeppelin/contracts": "3.4.2-solc-0.7", + "@uniswap/lib": "^4.0.1-alpha", + "@uniswap/v2-core": "^1.0.1", + "@uniswap/v3-core": "^1.0.0", + "base64-sol": "1.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@uniswap/v3-periphery/node_modules/@openzeppelin/contracts": { + "version": "3.4.2-solc-0.7", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-3.4.2-solc-0.7.tgz", + "integrity": "sha512-W6QmqgkADuFcTLzHL8vVoNBtkwjvQRpYIAom7KiUNoLKghyx3FgH0GBjt8NRvigV1ZmMOBllvE1By1C+bi8WpA==", + "license": "MIT" + }, "node_modules/@vitest/expect": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", @@ -6021,6 +6262,27 @@ "license": "ISC", "optional": true }, + "node_modules/abitype": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", + "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -6391,6 +6653,12 @@ "node": ">=14" } }, + "node_modules/base64-sol": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/base64-sol/-/base64-sol-1.0.1.tgz", + "integrity": "sha512-ld3cCNMeXt4uJXmLZBHFGMvVpK9KsLVEhPpFRXnvSVAqABKbuNZg/+dsq3NuM+wxFLb/UrVkz7m1ciWmkMfTbg==", + "license": "MIT" + }, "node_modules/base64url": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", @@ -6620,9 +6888,9 @@ } }, "node_modules/cborg": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/cborg/-/cborg-5.1.1.tgz", - "integrity": "sha512-BDbSRIp6XrQXkTc7g+DN0RB9RrDPTUfals2ecWUlt3juPLjbAvy/V72mJcXY0Ehu0Dq/3WpNCOCT68HUTbW+lw==", + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/cborg/-/cborg-5.1.11.tgz", + "integrity": "sha512-oc6Pzg/gkTobxHZNgMmny+G99dOeBMbAmnGHcZWMKtolxZBIVwfi0Pj0khxEtNU8HMFdbT5sK0HmtgecDUPP0A==", "license": "Apache-2.0", "bin": { "cborg": "lib/bin.js" @@ -8319,6 +8587,12 @@ "node": ">=6" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -10441,6 +10715,21 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -13835,6 +14124,69 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ox": { + "version": "0.14.33", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.33.tgz", + "integrity": "sha512-rooA/4o7bBof4Ge2VH/eovfNPb/AEEYyrNj03wggc55g5HZD8Pjs/OeWhttgjic3dDcqn0r29bDuvQEdTiUemQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.2.3", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/ox/node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT" + }, + "node_modules/ox/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ox/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/p-each-series": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-3.0.0.tgz", @@ -14076,6 +14428,21 @@ "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "license": "MIT" }, + "node_modules/permissionless": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/permissionless/-/permissionless-0.3.7.tgz", + "integrity": "sha512-r1UGWkVMqFzzJ0UcKEVHh6XvOm7+SfTvy7AoGA7ZlxpE96XR+zbtb+rRAA3DOQuq6/ifov5NvUyt1xf4zxbR0g==", + "license": "MIT", + "peerDependencies": { + "ox": "^0.11.3", + "viem": "^2.44.4" + }, + "peerDependenciesMeta": { + "ox": { + "optional": true + } + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -17012,6 +17379,84 @@ "extsprintf": "^1.2.0" } }, + "node_modules/viem": { + "version": "2.55.10", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.55.10.tgz", + "integrity": "sha512-Q9Ba+/ma81U2M5o5P2AQ7Ux8rTIwmCZvUcr8rKdQ22bV0IBFHllM2m5gWDP8hFaUN2nH2oW3QG44amRazflYNQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.9.1", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0", + "abitype": "1.2.3", + "isows": "1.0.7", + "ox": "0.14.33", + "ws": "8.21.0" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", diff --git a/package.json b/package.json index 34257d5..93c3156 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ }, "dependencies": { "@inquirer/prompts": "^5.3.8", - "@trustvc/trustvc": "^2.14.1", + "@trustvc/trustvc": "2.15.0-beta.3", "@types/yargs": "^17.0.32", "chalk": "^4.1.2", "dotenv": "^16.0.0", diff --git a/samples/obligation-credential-subject.sample.json b/samples/obligation-credential-subject.sample.json new file mode 100644 index 0000000..933ee8e --- /dev/null +++ b/samples/obligation-credential-subject.sample.json @@ -0,0 +1,55 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://w3id.org/security/data-integrity/v2", + "https://trustvc.io/context/render-method-context-v2.json", + "https://trustvc.io/context/bill-of-exchange.json", + "https://trustvc.io/context/obligation-records-context.json", + "https://trustvc.io/context/qrcode-context.json" + ], + "renderMethod": [ + { + "type": "EMBEDDED_RENDERER", + "templateName": "BILL_OF_EXCHANGE", + "id": "http://localhost:9000" + } + ], + "credentialSubject": { + "electronicDocumentIdentifier": "urn:uuid:e6f4b2a1-9c3d-4e8f-a7b0-1d2e3f4a5b6c", + "referenceNumber": "BOE-2026-00147", + "amountInFigures": "128500.00", + "amountInWords": "One hundred twenty-eight thousand five hundred United States Dollars only", + "currencyCode": "USD", + "blDate": "2026-06-28", + "placeOfIssue": "Singapore", + "dateOfIssue": "2026-07-06", + "tenor": "At 90 days sight", + "payee": "Meridian Commodities Pte Ltd", + "drawnUnder": "Documentary Credit No. LC-DBS-2026-88341 / Invoice No. INV-2026-0456 / B/L No. BL-SIN-2026-0781", + "drawnUnderDate": "2026-06-15", + "issuedBy": "DBS Bank Ltd", + "drawee": { + "name": "Fairview Industries Inc.", + "address": "1201 Market Street, Suite 900, Wilmington, DE 19801, USA", + "authorisedSignatoryName": "James R. Carter", + "signature": "data:image/png;base64,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" + }, + "drawer": { + "name": "Meridian Commodities Pte Ltd", + "address": "8 Marina Boulevard, #24-01, Singapore 018981", + "authorisedSignatoryName": "Wei Ling Tan", + "signature": "data:image/png;base64,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" + } + }, + "type": ["VerifiableCredential"], + "credentialStatus": { + "type": "TransferableRecords", + "tokenNetwork": { + "chain": "Sepolia", + "chainId": 11155111 + }, + "obligationRegistry": "0xE8c7516BD4FC64DF039BAA586035dfaA57520BE4" + }, + "issuer": "did:web:didhost.vercel.app", + "validFrom": "2026-07-06T09:00:00Z" +} diff --git a/src/commands/helpers.ts b/src/commands/helpers.ts index b11785e..f92927f 100644 --- a/src/commands/helpers.ts +++ b/src/commands/helpers.ts @@ -7,14 +7,19 @@ import { v4SupportInterfaceIds, v5SupportInterfaceIds, DocumentStore__factory, + encrypt, } from '@trustvc/trustvc'; -import { encrypt } from '@trustvc/trustvc'; // Internal utilities import { ConnectedSigner } from '../utils'; -// Contract factories from TrustVC v5 -const { TitleEscrow__factory, TradeTrustToken__factory, TDocDeployer__factory } = v5Contracts; +const { + TrustVCToken__factory, + ObligationEscrow__factory, + TitleEscrow__factory, + TradeTrustToken__factory, + TDocDeployer__factory, +} = v5Contracts; // Interface for connectToTokenRegistry function arguments interface ConnectToTokenRegistryArgs { @@ -27,6 +32,11 @@ interface ConnectToTDocDeployerContractArgs { wallet: Wallet | HDNodeWallet | ConnectedSigner | Signer; } +interface ConnectToObligationRegistryArgs { + address: string; + wallet: Wallet | HDNodeWallet | ConnectedSigner | Signer; +} + /** * Connects to a token registry contract instance. * @@ -419,3 +429,69 @@ export async function waitForTransaction(tx: any | string, provider: Provider, c throw new Error('Provider required for transaction hash'); } } + +/** + * Connects to a TrustVCToken (obligation registry) contract instance. + */ +export const connectToObligationRegistry = async ({ + address, + wallet, +}: ConnectToObligationRegistryArgs) => { + try { + signale.info(`Connecting to obligation registry at: ${address}`); + const registry = new ethers.Contract(address, TrustVCToken__factory.abi, wallet as any); + if (!registry) { + throw new Error(`Failed to connect to obligation registry at address: ${address}`); + } + signale.success(`Successfully connected to obligation registry`); + return registry; + } catch (error) { + signale.error( + `Error in connectToObligationRegistry: ${error instanceof Error ? error.message : String(error)}`, + ); + throw error; + } +}; + +interface ConnectToObligationEscrowArgs { + tokenId: string; + address: string; + wallet: Wallet | HDNodeWallet | ConnectedSigner | Signer; +} + +/** + * Resolves ObligationEscrow via ownerOf(tokenId) on the obligation registry and connects. + */ +export const connectToObligationEscrow = async ({ + tokenId, + address, + wallet, +}: ConnectToObligationEscrowArgs) => { + try { + signale.info(`Connecting to obligation registry at: ${address}`); + const registry = new ethers.Contract(address, TrustVCToken__factory.abi, wallet as any); + + signale.info(`Fetching obligation escrow address for tokenId: ${tokenId}`); + const escrowAddress = await registry.ownerOf(tokenId); + signale.info(`Obligation escrow address: ${escrowAddress}`); + + if (!escrowAddress || escrowAddress === ZeroAddress) { + throw new Error( + `Invalid obligation escrow address for tokenId: ${tokenId}. Address: ${escrowAddress}`, + ); + } + + signale.info(`Connecting to obligation escrow at: ${escrowAddress}`); + const escrow = new ethers.Contract(escrowAddress, ObligationEscrow__factory.abi, wallet as any); + if (!escrow) { + throw new Error(`Failed to connect to obligation escrow at address: ${escrowAddress}`); + } + signale.success(`Successfully connected to obligation escrow`); + return escrow; + } catch (error) { + signale.error( + `Error in connectToObligationEscrow: ${error instanceof Error ? error.message : String(error)}`, + ); + throw error; + } +}; diff --git a/src/commands/obligation-escrow/accept-return-to-issuer.ts b/src/commands/obligation-escrow/accept-return-to-issuer.ts new file mode 100644 index 0000000..a4ec7f0 --- /dev/null +++ b/src/commands/obligation-escrow/accept-return-to-issuer.ts @@ -0,0 +1,51 @@ +import { error, info, success } from 'signale'; +import { acceptReturnedObligationRegistry } from '@trustvc/trustvc'; +import { BaseObligationEscrowCommand } from '../../types'; +import { + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + TransactionReceiptFees, +} from '../../utils'; +import { promptBaseObligationEscrowInputs } from './shared'; +import { runObligationEscrowTx } from './runTx'; + +export const command = 'accept-return-to-issuer'; +export const describe = 'Issuer accepts a returned BoE obligation (burn / shred)'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + await acceptReturnedHandler(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = promptBaseObligationEscrowInputs; + +export const acceptReturnedHandler = async (args: BaseObligationEscrowCommand) => { + try { + info(`Accepting returned obligation ${args.tokenId}`); + const transaction = await runObligationEscrowTx({ + args, + // acceptReturned burns the title, which lives on the TrustVCToken registry, not the escrow. + populate: ({ registry }, encryptedRemark) => + registry.burn.populateTransaction(args.tokenId, encryptedRemark), + sdk: acceptReturnedObligationRegistry as any, + sdkParams: { tokenId: args.tokenId, remarks: args.remark }, + }); + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Return accepted (burned)`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-escrow/accept.ts b/src/commands/obligation-escrow/accept.ts new file mode 100644 index 0000000..d3b2c61 --- /dev/null +++ b/src/commands/obligation-escrow/accept.ts @@ -0,0 +1,49 @@ +import { error, info, success } from 'signale'; +import { acceptObligationRegistry } from '@trustvc/trustvc'; +import { BaseObligationEscrowCommand } from '../../types'; +import { + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + TransactionReceiptFees, +} from '../../utils'; +import { promptBaseObligationEscrowInputs } from './shared'; +import { runObligationEscrowTx } from './runTx'; + +export const command = 'accept'; +export const describe = 'Holder accepts an issued BoE obligation (Issued → Accepted)'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + await acceptHandler(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = promptBaseObligationEscrowInputs; + +export const acceptHandler = async (args: BaseObligationEscrowCommand) => { + try { + info(`Accepting obligation token ${args.tokenId} on ${args.obligationRegistryAddress}`); + const transaction = await runObligationEscrowTx({ + args, + populate: ({ escrow }, encryptedRemark) => escrow.accept.populateTransaction(encryptedRemark), + sdk: acceptObligationRegistry as any, + sdkParams: { remarks: args.remark }, + }); + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Obligation ${args.tokenId} accepted`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-escrow/discharge.ts b/src/commands/obligation-escrow/discharge.ts new file mode 100644 index 0000000..8ae5907 --- /dev/null +++ b/src/commands/obligation-escrow/discharge.ts @@ -0,0 +1,50 @@ +import { error, info, success } from 'signale'; +import { dischargeObligationRegistry } from '@trustvc/trustvc'; +import { BaseObligationEscrowCommand } from '../../types'; +import { + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + TransactionReceiptFees, +} from '../../utils'; +import { promptBaseObligationEscrowInputs } from './shared'; +import { runObligationEscrowTx } from './runTx'; + +export const command = 'discharge'; +export const describe = 'Beneficiary discharges an accepted BoE obligation (terminal Discharged)'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + await dischargeHandler(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = promptBaseObligationEscrowInputs; + +export const dischargeHandler = async (args: BaseObligationEscrowCommand) => { + try { + info(`Discharging obligation token ${args.tokenId} on ${args.obligationRegistryAddress}`); + const transaction = await runObligationEscrowTx({ + args, + populate: ({ escrow }, encryptedRemark) => + escrow.discharge.populateTransaction(encryptedRemark), + sdk: dischargeObligationRegistry as any, + sdkParams: { remarks: args.remark }, + }); + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Obligation ${args.tokenId} discharged`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-escrow/endorse-transfer-owner.ts b/src/commands/obligation-escrow/endorse-transfer-owner.ts new file mode 100644 index 0000000..d275c11 --- /dev/null +++ b/src/commands/obligation-escrow/endorse-transfer-owner.ts @@ -0,0 +1,55 @@ +import { error, info, success } from 'signale'; +import { transferBeneficiaryObligationRegistry } from '@trustvc/trustvc'; +import { ObligationEscrowNominateBeneficiaryCommand } from '../../types'; +import { + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + promptAddress, + TransactionReceiptFees, +} from '../../utils'; +import { promptBaseObligationEscrowInputs } from './shared'; +import { runObligationEscrowTx } from './runTx'; + +export const command = 'endorse-transfer-owner'; +export const describe = 'Endorse / transfer beneficiary (owner) of a BoE obligation'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + await endorseHandler(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = async (): Promise => { + const base = await promptBaseObligationEscrowInputs(); + const newBeneficiary = await promptAddress('new beneficiary', 'endorsed owner'); + return { ...base, newBeneficiary } as ObligationEscrowNominateBeneficiaryCommand; +}; + +export const endorseHandler = async (args: ObligationEscrowNominateBeneficiaryCommand) => { + try { + info(`Transferring beneficiary to ${args.newBeneficiary} for obligation ${args.tokenId}`); + const transaction = await runObligationEscrowTx({ + args, + populate: ({ escrow }, encryptedRemark) => + escrow.transferBeneficiary.populateTransaction(args.newBeneficiary, encryptedRemark), + sdk: transferBeneficiaryObligationRegistry as any, + sdkParams: { newBeneficiaryAddress: args.newBeneficiary, remarks: args.remark }, + }); + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Beneficiary transferred to ${args.newBeneficiary}`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-escrow/index.ts b/src/commands/obligation-escrow/index.ts new file mode 100644 index 0000000..28969ed --- /dev/null +++ b/src/commands/obligation-escrow/index.ts @@ -0,0 +1,11 @@ +import { Argv } from 'yargs'; + +export const command = 'obligation-escrow '; + +export const describe = + 'Invoke ObligationEscrow lifecycle / transfer functions for BoE obligation records'; + +export const builder = (yargs: Argv): Argv => + yargs.commandDir(__dirname, { extensions: ['ts', 'js'] }); + +export const handler = (): void => {}; diff --git a/src/commands/obligation-escrow/nominate-transfer-owner.ts b/src/commands/obligation-escrow/nominate-transfer-owner.ts new file mode 100644 index 0000000..0d94a55 --- /dev/null +++ b/src/commands/obligation-escrow/nominate-transfer-owner.ts @@ -0,0 +1,55 @@ +import { error, info, success } from 'signale'; +import { nominateObligationRegistry } from '@trustvc/trustvc'; +import { ObligationEscrowNominateBeneficiaryCommand } from '../../types'; +import { + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + promptAddress, + TransactionReceiptFees, +} from '../../utils'; +import { promptBaseObligationEscrowInputs } from './shared'; +import { runObligationEscrowTx } from './runTx'; + +export const command = 'nominate-transfer-owner'; +export const describe = 'Nominate a new beneficiary (owner) for a BoE obligation'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + await nominateHandler(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = async (): Promise => { + const base = await promptBaseObligationEscrowInputs(); + const newBeneficiary = await promptAddress('new beneficiary', 'nominee'); + return { ...base, newBeneficiary } as ObligationEscrowNominateBeneficiaryCommand; +}; + +export const nominateHandler = async (args: ObligationEscrowNominateBeneficiaryCommand) => { + try { + info(`Nominating beneficiary ${args.newBeneficiary} for obligation ${args.tokenId}`); + const transaction = await runObligationEscrowTx({ + args, + populate: ({ escrow }, encryptedRemark) => + escrow.nominate.populateTransaction(args.newBeneficiary, encryptedRemark), + sdk: nominateObligationRegistry as any, + sdkParams: { newBeneficiaryAddress: args.newBeneficiary, remarks: args.remark }, + }); + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Nominated ${args.newBeneficiary}`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-escrow/reject-return-to-issuer.ts b/src/commands/obligation-escrow/reject-return-to-issuer.ts new file mode 100644 index 0000000..113d537 --- /dev/null +++ b/src/commands/obligation-escrow/reject-return-to-issuer.ts @@ -0,0 +1,51 @@ +import { error, info, success } from 'signale'; +import { rejectReturnedObligationRegistry } from '@trustvc/trustvc'; +import { BaseObligationEscrowCommand } from '../../types'; +import { + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + TransactionReceiptFees, +} from '../../utils'; +import { promptBaseObligationEscrowInputs } from './shared'; +import { runObligationEscrowTx } from './runTx'; + +export const command = 'reject-return-to-issuer'; +export const describe = 'Issuer rejects a returned BoE obligation (restore to escrow)'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + await rejectReturnedHandler(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = promptBaseObligationEscrowInputs; + +export const rejectReturnedHandler = async (args: BaseObligationEscrowCommand) => { + try { + info(`Rejecting returned obligation ${args.tokenId} (restore)`); + const transaction = await runObligationEscrowTx({ + args, + // rejectReturned restores the title, which lives on the TrustVCToken registry, not the escrow. + populate: ({ registry }, encryptedRemark) => + registry.restore.populateTransaction(args.tokenId, encryptedRemark), + sdk: rejectReturnedObligationRegistry as any, + sdkParams: { tokenId: args.tokenId, remarks: args.remark }, + }); + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Return rejected (restored to escrow)`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-escrow/reject-transfer-holder.ts b/src/commands/obligation-escrow/reject-transfer-holder.ts new file mode 100644 index 0000000..d4fc2ff --- /dev/null +++ b/src/commands/obligation-escrow/reject-transfer-holder.ts @@ -0,0 +1,50 @@ +import { error, info, success } from 'signale'; +import { rejectTransferHolderObligationRegistry } from '@trustvc/trustvc'; +import { BaseObligationEscrowCommand } from '../../types'; +import { + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + TransactionReceiptFees, +} from '../../utils'; +import { promptBaseObligationEscrowInputs } from './shared'; +import { runObligationEscrowTx } from './runTx'; + +export const command = 'reject-transfer-holder'; +export const describe = 'Reject a pending holder transfer on a BoE obligation'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + await rejectTransferHolderHandler(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = promptBaseObligationEscrowInputs; + +export const rejectTransferHolderHandler = async (args: BaseObligationEscrowCommand) => { + try { + info(`Rejecting holder transfer for obligation ${args.tokenId}`); + const transaction = await runObligationEscrowTx({ + args, + populate: ({ escrow }, encryptedRemark) => + escrow.rejectTransferHolder.populateTransaction(encryptedRemark), + sdk: rejectTransferHolderObligationRegistry as any, + sdkParams: { remarks: args.remark }, + }); + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Holder transfer rejected`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-escrow/reject-transfer-owner-holder.ts b/src/commands/obligation-escrow/reject-transfer-owner-holder.ts new file mode 100644 index 0000000..85a0883 --- /dev/null +++ b/src/commands/obligation-escrow/reject-transfer-owner-holder.ts @@ -0,0 +1,50 @@ +import { error, info, success } from 'signale'; +import { rejectTransferOwnersObligationRegistry } from '@trustvc/trustvc'; +import { BaseObligationEscrowCommand } from '../../types'; +import { + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + TransactionReceiptFees, +} from '../../utils'; +import { promptBaseObligationEscrowInputs } from './shared'; +import { runObligationEscrowTx } from './runTx'; + +export const command = 'reject-transfer-owner-holder'; +export const describe = 'Reject a pending joint owner+holder transfer on a BoE obligation'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + await rejectTransferOwnersHandler(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = promptBaseObligationEscrowInputs; + +export const rejectTransferOwnersHandler = async (args: BaseObligationEscrowCommand) => { + try { + info(`Rejecting owners transfer for obligation ${args.tokenId}`); + const transaction = await runObligationEscrowTx({ + args, + populate: ({ escrow }, encryptedRemark) => + escrow.rejectTransferOwners.populateTransaction(encryptedRemark), + sdk: rejectTransferOwnersObligationRegistry as any, + sdkParams: { remarks: args.remark }, + }); + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Owners transfer rejected`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-escrow/reject-transfer-owner.ts b/src/commands/obligation-escrow/reject-transfer-owner.ts new file mode 100644 index 0000000..c6e4b99 --- /dev/null +++ b/src/commands/obligation-escrow/reject-transfer-owner.ts @@ -0,0 +1,50 @@ +import { error, info, success } from 'signale'; +import { rejectTransferBeneficiaryObligationRegistry } from '@trustvc/trustvc'; +import { BaseObligationEscrowCommand } from '../../types'; +import { + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + TransactionReceiptFees, +} from '../../utils'; +import { promptBaseObligationEscrowInputs } from './shared'; +import { runObligationEscrowTx } from './runTx'; + +export const command = 'reject-transfer-owner'; +export const describe = 'Reject a pending beneficiary transfer on a BoE obligation'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + await rejectTransferOwnerHandler(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = promptBaseObligationEscrowInputs; + +export const rejectTransferOwnerHandler = async (args: BaseObligationEscrowCommand) => { + try { + info(`Rejecting beneficiary transfer for obligation ${args.tokenId}`); + const transaction = await runObligationEscrowTx({ + args, + populate: ({ escrow }, encryptedRemark) => + escrow.rejectTransferBeneficiary.populateTransaction(encryptedRemark), + sdk: rejectTransferBeneficiaryObligationRegistry as any, + sdkParams: { remarks: args.remark }, + }); + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Beneficiary transfer rejected`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-escrow/reject.ts b/src/commands/obligation-escrow/reject.ts new file mode 100644 index 0000000..6e5e5e2 --- /dev/null +++ b/src/commands/obligation-escrow/reject.ts @@ -0,0 +1,49 @@ +import { error, info, success } from 'signale'; +import { rejectObligationRegistry } from '@trustvc/trustvc'; +import { BaseObligationEscrowCommand } from '../../types'; +import { + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + TransactionReceiptFees, +} from '../../utils'; +import { promptBaseObligationEscrowInputs } from './shared'; +import { runObligationEscrowTx } from './runTx'; + +export const command = 'reject'; +export const describe = 'Holder rejects an issued BoE obligation (terminal Rejected)'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + await rejectHandler(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = promptBaseObligationEscrowInputs; + +export const rejectHandler = async (args: BaseObligationEscrowCommand) => { + try { + info(`Rejecting obligation token ${args.tokenId} on ${args.obligationRegistryAddress}`); + const transaction = await runObligationEscrowTx({ + args, + populate: ({ escrow }, encryptedRemark) => escrow.reject.populateTransaction(encryptedRemark), + sdk: rejectObligationRegistry as any, + sdkParams: { remarks: args.remark }, + }); + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Obligation ${args.tokenId} rejected`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-escrow/return-to-issuer.ts b/src/commands/obligation-escrow/return-to-issuer.ts new file mode 100644 index 0000000..236dbc1 --- /dev/null +++ b/src/commands/obligation-escrow/return-to-issuer.ts @@ -0,0 +1,50 @@ +import { error, info, success } from 'signale'; +import { returnToIssuerObligationRegistry } from '@trustvc/trustvc'; +import { BaseObligationEscrowCommand } from '../../types'; +import { + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + TransactionReceiptFees, +} from '../../utils'; +import { promptBaseObligationEscrowInputs } from './shared'; +import { runObligationEscrowTx } from './runTx'; + +export const command = 'return-to-issuer'; +export const describe = 'Return a BoE obligation to the issuer (requires dual role)'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + await returnToIssuerHandler(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = promptBaseObligationEscrowInputs; + +export const returnToIssuerHandler = async (args: BaseObligationEscrowCommand) => { + try { + info(`Returning obligation ${args.tokenId} to issuer`); + const transaction = await runObligationEscrowTx({ + args, + populate: ({ escrow }, encryptedRemark) => + escrow.returnToIssuer.populateTransaction(encryptedRemark), + sdk: returnToIssuerObligationRegistry as any, + sdkParams: { remarks: args.remark }, + }); + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Returned to issuer`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-escrow/runTx.ts b/src/commands/obligation-escrow/runTx.ts new file mode 100644 index 0000000..fd5053a --- /dev/null +++ b/src/commands/obligation-escrow/runTx.ts @@ -0,0 +1,93 @@ +import signale from 'signale'; +import { BytesLike, TransactionReceipt } from 'ethers'; +import { CHAIN_ID } from '@trustvc/trustvc'; +import { + canEstimateGasPrice, + getGasFees, + getSupportedNetwork, + getWalletOrSigner, + performDryRunWithConfirmation, +} from '../../utils'; +import { + connectToObligationEscrow, + connectToObligationRegistry, + validateAndEncryptRemark, +} from '../helpers'; +import { BaseObligationEscrowCommand } from '../../types'; + +type PopulateFn = ( + contracts: { escrow: any; registry: any }, + encryptedRemark: BytesLike, +) => Promise; + +type SdkCall = ( + contractOptions: { obligationRegistryAddress: string; tokenId: string }, + wallet: any, + params: Record, + txOptions: Record, +) => Promise<{ hash: string; wait: () => Promise }>; + +/** + * Shared dry-run + gas + SDK send for obligation-escrow write methods that take remarks. + */ +export const runObligationEscrowTx = async (opts: { + args: BaseObligationEscrowCommand; + populate: PopulateFn; + sdk: SdkCall; + sdkParams: Record; +}): Promise => { + const { args, populate, sdk, sdkParams } = opts; + const { obligationRegistryAddress, tokenId, remark, encryptionKey, network, ...rest } = args; + const wallet = await getWalletOrSigner({ network, ...rest }); + const networkId = getSupportedNetwork(network).networkId; + + const shouldProceed = await performDryRunWithConfirmation({ + network, + getTransactionCallback: async () => { + const escrow = await connectToObligationEscrow({ + tokenId, + address: obligationRegistryAddress, + wallet, + }); + const registry = await connectToObligationRegistry({ + address: obligationRegistryAddress, + wallet, + }); + const encryptedRemark = validateAndEncryptRemark(remark, encryptionKey); + const tx = await populate({ escrow, registry }, encryptedRemark); + return { ...tx, from: await wallet.getAddress() }; + }, + }); + + if (!shouldProceed) { + process.exit(0); + } + + const contractOptions = { obligationRegistryAddress, tokenId }; + let transaction; + + if (canEstimateGasPrice(network)) { + if (!wallet.provider) { + throw new Error('Provider is required for gas estimation'); + } + const gasFees = await getGasFees({ provider: wallet.provider, ...rest }); + transaction = await sdk(contractOptions, wallet, sdkParams, { + chainId: networkId as unknown as CHAIN_ID, + maxFeePerGas: gasFees.maxFeePerGas?.toString(), + maxPriorityFeePerGas: gasFees.maxPriorityFeePerGas?.toString(), + id: encryptionKey, + }); + } else { + transaction = await sdk(contractOptions, wallet, sdkParams, { + chainId: networkId as unknown as CHAIN_ID, + id: encryptionKey, + }); + } + + signale.await(`Waiting for transaction ${transaction.hash} to be mined`); + const receipt = (await transaction.wait()) as unknown as TransactionReceipt; + if (!receipt) { + throw new Error('Transaction receipt not found'); + } + return receipt; +}; diff --git a/src/commands/obligation-escrow/shared.ts b/src/commands/obligation-escrow/shared.ts new file mode 100644 index 0000000..15fd7f4 --- /dev/null +++ b/src/commands/obligation-escrow/shared.ts @@ -0,0 +1,40 @@ +import { + promptWalletSelection, + promptAndReadDocument, + promptRemark, + extractObligationDocumentInfo, + verifyDocumentSignature, +} from '../../utils'; +import { BaseObligationEscrowCommand } from '../../types'; + +/** + * Shared prompt flow for obligation-escrow commands that only need document + wallet + remark. + */ +export const promptBaseObligationEscrowInputs = async (): Promise => { + const document = await promptAndReadDocument(); + await verifyDocumentSignature(document); + const { obligationRegistry, tokenId, network, documentId } = + await extractObligationDocumentInfo(document); + const { encryptedWalletPath, key, keyFile } = await promptWalletSelection(); + const remark = await promptRemark('v5'); + + const baseResult = { + network, + obligationRegistryAddress: obligationRegistry, + tokenId, + remark, + encryptionKey: documentId, + maxPriorityFeePerGasScale: 1, + }; + + if (encryptedWalletPath) { + return { ...baseResult, encryptedWalletPath } as BaseObligationEscrowCommand; + } + if (keyFile) { + return { ...baseResult, keyFile } as BaseObligationEscrowCommand; + } + if (key) { + return { ...baseResult, key } as BaseObligationEscrowCommand; + } + return baseResult as BaseObligationEscrowCommand; +}; diff --git a/src/commands/obligation-escrow/status.ts b/src/commands/obligation-escrow/status.ts new file mode 100644 index 0000000..2f3f7fd --- /dev/null +++ b/src/commands/obligation-escrow/status.ts @@ -0,0 +1,59 @@ +import { error, info, success } from 'signale'; +import { + ObligationDocumentStatus, + ObligationEscrowTerminationReason, + getObligationEscrowTerminationReason, + getObligationRegistryStatus, + isObligationRegistryRegistered, +} from '@trustvc/trustvc'; +import { BaseObligationEscrowCommand } from '../../types'; +import { getErrorMessage, getWalletOrSigner } from '../../utils'; +import { promptBaseObligationEscrowInputs } from './shared'; + +export const command = 'status'; +export const describe = 'Read BoE obligation escrow status / registration / termination reason'; + +const STATUS_LABEL: Record = { + [ObligationDocumentStatus.Issued]: 'Issued', + [ObligationDocumentStatus.Accepted]: 'Accepted', + [ObligationDocumentStatus.Rejected]: 'Rejected', + [ObligationDocumentStatus.Discharged]: 'Discharged', +}; +const REASON_LABEL: Record = { + [ObligationEscrowTerminationReason.None]: 'None', + [ObligationEscrowTerminationReason.ReturnToIssuer]: 'ReturnToIssuer', + [ObligationEscrowTerminationReason.Rejected]: 'Rejected', + [ObligationEscrowTerminationReason.Discharged]: 'Discharged', +}; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + await statusHandler(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = promptBaseObligationEscrowInputs; + +export const statusHandler = async (args: BaseObligationEscrowCommand) => { + try { + const { obligationRegistryAddress, tokenId, network, ...rest } = args; + const wallet = await getWalletOrSigner({ network, ...rest }); + // Escrow resolution uses contractOptions.tokenId (params.tokenId is API parity only). + const opts = { obligationRegistryAddress, tokenId }; + + const status = await getObligationRegistryStatus(opts, wallet, { tokenId }); + const registered = await isObligationRegistryRegistered(opts, wallet, { tokenId }); + const reason = await getObligationEscrowTerminationReason(opts, wallet, { tokenId }); + + success(`Obligation ${tokenId} on ${obligationRegistryAddress}`); + info(` Status: ${STATUS_LABEL[status] ?? status} (${status})`); + info(` Registered: ${registered}`); + info(` Termination reason: ${REASON_LABEL[reason] ?? reason} (${reason})`); + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-escrow/transfer-holder.ts b/src/commands/obligation-escrow/transfer-holder.ts new file mode 100644 index 0000000..74d7146 --- /dev/null +++ b/src/commands/obligation-escrow/transfer-holder.ts @@ -0,0 +1,58 @@ +import { error, info, success, warn } from 'signale'; +import { transferHolderObligationRegistry } from '@trustvc/trustvc'; +import { ObligationEscrowTransferHolderCommand } from '../../types'; +import { + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + promptAddress, + TransactionReceiptFees, +} from '../../utils'; +import { promptBaseObligationEscrowInputs } from './shared'; +import { runObligationEscrowTx } from './runTx'; + +export const command = 'transfer-holder'; +export const describe = 'Transfer the holder of a BoE obligation record'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + await changeHolderHandler(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = async (): Promise => { + const base = await promptBaseObligationEscrowInputs(); + const newHolder = await promptAddress('new holder', 'new holder'); + return { ...base, newHolder } as ObligationEscrowTransferHolderCommand; +}; + +export const changeHolderHandler = async (args: ObligationEscrowTransferHolderCommand) => { + try { + info( + `Changing holder of obligation ${args.tokenId} on ${args.obligationRegistryAddress} to ${args.newHolder}`, + ); + warn('Only the current holder can transfer the holder role.'); + const transaction = await runObligationEscrowTx({ + args, + populate: ({ escrow }, encryptedRemark) => + escrow.transferHolder.populateTransaction(args.newHolder, encryptedRemark), + sdk: transferHolderObligationRegistry as any, + sdkParams: { holderAddress: args.newHolder, remarks: args.remark }, + }); + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Holder changed to ${args.newHolder}`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-escrow/transfer-owner-holder.ts b/src/commands/obligation-escrow/transfer-owner-holder.ts new file mode 100644 index 0000000..db39e7f --- /dev/null +++ b/src/commands/obligation-escrow/transfer-owner-holder.ts @@ -0,0 +1,65 @@ +import { error, info, success } from 'signale'; +import { transferOwnersObligationRegistry } from '@trustvc/trustvc'; +import { ObligationEscrowEndorseTransferOfOwnersCommand } from '../../types'; +import { + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + promptAddress, + TransactionReceiptFees, +} from '../../utils'; +import { promptBaseObligationEscrowInputs } from './shared'; +import { runObligationEscrowTx } from './runTx'; + +export const command = 'transfer-owner-holder'; +export const describe = 'Transfer both beneficiary and holder of a BoE obligation'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + await transferOwnersHandler(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = + async (): Promise => { + const base = await promptBaseObligationEscrowInputs(); + const newOwner = await promptAddress('new beneficiary', 'new owner'); + const newHolder = await promptAddress('new holder', 'new holder'); + return { ...base, newOwner, newHolder } as ObligationEscrowEndorseTransferOfOwnersCommand; + }; + +export const transferOwnersHandler = async ( + args: ObligationEscrowEndorseTransferOfOwnersCommand, +) => { + try { + info( + `Transferring owners of obligation ${args.tokenId} to owner ${args.newOwner} / holder ${args.newHolder}`, + ); + const transaction = await runObligationEscrowTx({ + args, + populate: ({ escrow }, encryptedRemark) => + escrow.transferOwners.populateTransaction(args.newOwner, args.newHolder, encryptedRemark), + sdk: transferOwnersObligationRegistry as any, + sdkParams: { + newBeneficiaryAddress: args.newOwner, + newHolderAddress: args.newHolder, + remarks: args.remark, + }, + }); + displayTransactionPrice( + transaction as unknown as TransactionReceiptFees, + args.network as NetworkCmdName, + ); + success(`Owners transferred`); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-registry/deploy.ts b/src/commands/obligation-registry/deploy.ts new file mode 100644 index 0000000..3c6cc55 --- /dev/null +++ b/src/commands/obligation-registry/deploy.ts @@ -0,0 +1,149 @@ +import { error, info, success } from 'signale'; +import { CHAIN_ID, deployObligationRegistry } from '@trustvc/trustvc'; +import { input, confirm } from '@inquirer/prompts'; +import { + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + promptWalletSelection, + promptNetworkSelection, + getWalletOrSigner, + getSupportedNetwork, + performDryRunWithConfirmation, + promptAddress, + supportedNetwork, +} from '../../utils'; +import { TransactionReceipt } from 'ethers'; + +export const command = 'deploy'; + +export const describe = 'Deploys an Obligation Registry (TrustVCToken + ObligationEscrowFactory)'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + + await deployObligationRegistryContract(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +type DeployObligationRegistryCommand = { + registryName: string; + registrySymbol: string; + escrowFactoryAddress?: string; + network: string; + encryptedWalletPath?: string; + key?: string; + keyFile?: string; + maxPriorityFeePerGasScale: number; +}; + +export const promptForInputs = async (): Promise => { + const network = await promptNetworkSelection(); + + const registryName = await input({ + message: 'Enter the name of the obligation registry:', + required: true, + validate: (value: string) => { + if (!value || value.trim() === '') { + return 'Registry name is required'; + } + return true; + }, + }); + + const registrySymbol = await input({ + message: 'Enter the symbol of the obligation registry:', + required: true, + validate: (value: string) => { + if (!value || value.trim() === '') { + return 'Registry symbol is required'; + } + return true; + }, + }); + + const reuseFactory = await confirm({ + message: 'Reuse an existing ObligationEscrowFactory address?', + default: false, + }); + + const escrowFactoryAddress = reuseFactory + ? await promptAddress('ObligationEscrowFactory', 'optional', true) + : undefined; + + const { encryptedWalletPath, key, keyFile } = await promptWalletSelection(); + + const baseResult = { + network, + registryName, + registrySymbol, + escrowFactoryAddress, + maxPriorityFeePerGasScale: 1, + }; + + if (encryptedWalletPath) { + return { ...baseResult, encryptedWalletPath } as DeployObligationRegistryCommand; + } + if (keyFile) { + return { ...baseResult, keyFile } as DeployObligationRegistryCommand; + } + if (key) { + return { ...baseResult, key } as DeployObligationRegistryCommand; + } + return baseResult as DeployObligationRegistryCommand; +}; + +export const deployObligationRegistryContract = async ({ + network, + registryName, + registrySymbol, + escrowFactoryAddress, + ...rest +}: DeployObligationRegistryCommand) => { + try { + const wallet = await getWalletOrSigner({ network, ...rest }); + const chainId = supportedNetwork[network as NetworkCmdName].networkId; + + const shouldProceed = await performDryRunWithConfirmation({ + network, + getTransactionCallback: async () => ({ + to: await wallet.getAddress(), + data: '0x', + from: await wallet.getAddress(), + value: 0n, + }), + }); + + if (!shouldProceed) { + process.exit(0); + } + + info(`Deploying obligation registry ${registryName}`); + + const result = await deployObligationRegistry(registryName, registrySymbol, wallet, { + chainId: getSupportedNetwork(network).networkId as unknown as CHAIN_ID, + escrowFactoryAddress, + }); + + displayTransactionPrice(result.receipt as any, network as NetworkCmdName); + + const { hash } = result.receipt as unknown as TransactionReceipt; + success(`Obligation registry ${registryName} deployed at ${result.obligationRegistry}`); + info(`ObligationEscrowFactory: ${result.obligationEscrowFactoryAddress}`); + info(`Obligation Registry: ${result.obligationRegistry}`); + info( + `Use this Obligation Registry address as credentialStatus.obligationRegistry in your document before signing and minting.`, + ); + info(`Find more details at ${getEtherscanAddress({ network })}/tx/${hash}`); + info(`Chain ID used: ${chainId}`); + + return result.obligationRegistry; + } catch (e) { + error(getErrorMessage(e)); + } +}; diff --git a/src/commands/obligation-registry/index.ts b/src/commands/obligation-registry/index.ts new file mode 100644 index 0000000..0ac0d6c --- /dev/null +++ b/src/commands/obligation-registry/index.ts @@ -0,0 +1,11 @@ +import { Argv } from 'yargs'; + +export const command = 'obligation-registry '; + +export const describe = + 'Invoke a function over an Obligation Registry (BoE / TrustVCToken) smart contract'; + +export const builder = (yargs: Argv): Argv => + yargs.commandDir(__dirname, { extensions: ['ts', 'js'] }); + +export const handler = (): void => {}; diff --git a/src/commands/obligation-registry/mint.ts b/src/commands/obligation-registry/mint.ts new file mode 100644 index 0000000..9997df1 --- /dev/null +++ b/src/commands/obligation-registry/mint.ts @@ -0,0 +1,161 @@ +import signale, { error, info, success } from 'signale'; +import { ObligationRegistryMintCommand } from '../../types'; +import { + addAddressPrefix, + displayTransactionPrice, + getErrorMessage, + getEtherscanAddress, + NetworkCmdName, + promptWalletSelection, + getWalletOrSigner, + canEstimateGasPrice, + getGasFees, + extractObligationDocumentInfo, + promptAndReadDocument, + promptRemark, + promptAddress, + performDryRunWithConfirmation, + verifyDocumentSignature, +} from '../../utils'; +import { connectToObligationRegistry, validateAndEncryptRemark } from '../helpers'; +import { TransactionReceipt } from 'ethers'; +import { mintObligationRegistry } from '@trustvc/trustvc'; + +export const command = 'mint'; + +export const describe = 'Mint a tokenId to an Obligation Registry deployed on the blockchain'; + +export const handler = async (): Promise => { + try { + const answers = await promptForInputs(); + if (!answers) return; + + await mintObligationToken(answers); + } catch (err: unknown) { + error(err instanceof Error ? err.message : String(err)); + } +}; + +export const promptForInputs = async (): Promise => { + const document = await promptAndReadDocument(); + await verifyDocumentSignature(document); + + const { obligationRegistry, tokenId, network, documentId } = + await extractObligationDocumentInfo(document); + + const beneficiary = await promptAddress('beneficiary', 'initial recipient'); + const holder = await promptAddress('holder', 'initial holder'); + const { encryptedWalletPath, key, keyFile } = await promptWalletSelection(); + const remark = await promptRemark('v5'); + const encryptionKey = documentId; + + const baseResult = { + network, + address: obligationRegistry, + tokenId, + beneficiary, + holder, + remark, + encryptionKey, + maxPriorityFeePerGasScale: 1, + }; + + if (encryptedWalletPath) { + return { ...baseResult, encryptedWalletPath } as ObligationRegistryMintCommand; + } + if (keyFile) { + return { ...baseResult, keyFile } as ObligationRegistryMintCommand; + } + if (key) { + return { ...baseResult, key } as ObligationRegistryMintCommand; + } + return baseResult as ObligationRegistryMintCommand; +}; + +export const mintObligationToken = async (args: ObligationRegistryMintCommand) => { + try { + info( + `Issuing ${args.tokenId} to recipient ${args.beneficiary} and holder ${args.holder} in obligation registry ${args.address}`, + ); + + const transaction = await mintToObligationRegistry({ + ...args, + tokenId: addAddressPrefix(args.tokenId), + }); + + displayTransactionPrice(transaction as any, args.network as NetworkCmdName); + success( + `Token ${args.tokenId} minted on obligation registry ${args.address} (beneficiary ${args.beneficiary}, holder ${args.holder})`, + ); + info( + `Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transaction.hash}`, + ); + return args.address; + } catch (e) { + error(getErrorMessage(e)); + } +}; + +const mintToObligationRegistry = async ({ + address, + beneficiary, + holder, + tokenId, + remark, + encryptionKey, + network, + ...rest +}: ObligationRegistryMintCommand): Promise => { + const wallet = await getWalletOrSigner({ network, ...rest }); + + const shouldProceed = await performDryRunWithConfirmation({ + network, + getTransactionCallback: async () => { + const registry = await connectToObligationRegistry({ address, wallet }); + const encryptedRemark = validateAndEncryptRemark(remark, encryptionKey); + const tx = await registry.mint.populateTransaction( + beneficiary, + holder, + tokenId, + encryptedRemark, + ); + return { ...tx, from: await wallet.getAddress() }; + }, + }); + + if (!shouldProceed) { + process.exit(0); + } + + let transaction; + if (canEstimateGasPrice(network)) { + if (!wallet.provider) { + throw new Error('Provider is required for gas estimation'); + } + const gasFees = await getGasFees({ provider: wallet.provider, ...rest }); + transaction = await mintObligationRegistry( + { obligationRegistryAddress: address }, + wallet, + { beneficiaryAddress: beneficiary, holderAddress: holder, tokenId, remarks: remark }, + { + id: encryptionKey, + maxFeePerGas: gasFees.maxFeePerGas?.toString(), + maxPriorityFeePerGas: gasFees.maxPriorityFeePerGas?.toString(), + }, + ); + } else { + transaction = await mintObligationRegistry( + { obligationRegistryAddress: address }, + wallet, + { beneficiaryAddress: beneficiary, holderAddress: holder, tokenId, remarks: remark }, + { id: encryptionKey }, + ); + } + + signale.await(`Waiting for transaction ${transaction.hash} to be mined`); + const receipt = (await transaction.wait()) as unknown as TransactionReceipt; + if (!receipt) { + throw new Error('Transaction receipt not found'); + } + return receipt; +}; diff --git a/src/commands/verify.ts b/src/commands/verify.ts index 0220349..25d9084 100644 --- a/src/commands/verify.ts +++ b/src/commands/verify.ts @@ -11,6 +11,7 @@ import { getChainId, getDocumentData, isDocumentRevokable, + isObligationRecord, isTransferableRecord, isWrappedV2Document, isWrappedV3Document, @@ -24,8 +25,37 @@ import signale from 'signale'; import type { Provider as V5Provider } from '@ethersproject/providers'; import { FragmentType } from '../types'; +const OBLIGATION_RECORDS_FRAGMENT = 'ObligationRecords'; + +type ObligationDocumentStatusInfo = { + obligationRegistry: string; + status?: number; + terminationReason?: number; +}; + +/** Extract obligation registry info from a VALID ObligationRecords verify fragment. */ +export const getObligationDocumentStatus = ( + fragments: VerificationFragment[], +): ObligationDocumentStatusInfo | null => { + const fragment = fragments.find((f) => f.name === OBLIGATION_RECORDS_FRAGMENT); + if (!fragment || fragment.status !== 'VALID') return null; + + const data = ( + fragment as { + data?: { obligationRegistry?: string; status?: number; terminationReason?: number }; + } + ).data; + if (!data?.obligationRegistry) return null; + + return { + obligationRegistry: data.obligationRegistry, + status: data.status, + terminationReason: data.terminationReason, + }; +}; + export const command = 'verify'; -export const describe = 'Verify a document signed using w3c or OpenAttestation'; +export const describe = 'Verify a W3C or OpenAttestation document (ETR, BoE, or revocable VC)'; export const handler = async () => { try { @@ -69,6 +99,18 @@ export const verify = async (signedVC: SignedVerifiableCredential) => { logResultStatus(getResultFromFragment(FragmentType.DOCUMENT_INTEGRITY, result)); logResultStatus(getResultFromFragment(FragmentType.DOCUMENT_STATUS, result)); logResultStatus(getResultFromFragment(FragmentType.ISSUER_IDENTITY, result)); + + const obligationStatus = getObligationDocumentStatus(result); + if (obligationStatus) { + const parts = [`registry=${obligationStatus.obligationRegistry}`]; + if (obligationStatus.status !== undefined) { + parts.push(`status=${obligationStatus.status}`); + } + if (obligationStatus.terminationReason !== undefined) { + parts.push(`terminationReason=${obligationStatus.terminationReason}`); + } + signale.info(`Obligation document status: ${parts.join(' ')}`); + } }; // ==== Helper Functions ==== @@ -80,8 +122,13 @@ const verifyW3CDocument = async ( // To capture the console.warn from trustvc function const { result: isTransferable } = CaptureConsoleWarn(() => isTransferableRecord(signedVC)); + const isObligation = isObligationRecord(signedVC); const isRevokable = isDocumentRevokable(signedVC); - const requiresNetwork = isTransferable || isRevokable; + const requiresNetwork = isTransferable || isObligation || isRevokable; + + if (isObligation) { + signale.info('Verifying obligation / BoE document...'); + } // If the document is not transferable or revokable, verify directly // To capture the console.warn from trustvc function @@ -99,6 +146,12 @@ const verifyW3CDocument = async ( signale.warn(`${err instanceof Error ? err.message : String(err)}`); } + const networkName = await promptNetworkSelection(); + const provider = getSupportedNetwork(networkName).provider() as unknown as V5Provider; + if (provider) { + return await CaptureConsoleWarnAsync(() => verifyDocument(signedVC, { provider })); + } + // Fallback: Verify without provider return await CaptureConsoleWarnAsync(() => verifyDocument(signedVC)); }; diff --git a/src/types.ts b/src/types.ts index 9b58676..d15108d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -102,6 +102,39 @@ export type TitleEscrowNominateBeneficiaryCommand = BaseTitleEscrowCommand & { newBeneficiary: string; }; +export type ObligationRegistryMintCommand = NetworkOption & + Partial & + WalletOrSignerOption & + GasPriceScale & { + address: string; + beneficiary: string; + holder: string; + tokenId: string; + remark?: string; + encryptionKey?: string; + }; + +export type BaseObligationEscrowCommand = NetworkAndWalletSignerOption & + GasPriceScale & { + obligationRegistryAddress: string; + tokenId: string; + remark?: string; + encryptionKey?: string; + }; + +export type ObligationEscrowTransferHolderCommand = BaseObligationEscrowCommand & { + newHolder: string; +}; + +export type ObligationEscrowEndorseTransferOfOwnersCommand = BaseObligationEscrowCommand & { + newHolder: string; + newOwner: string; +}; + +export type ObligationEscrowNominateBeneficiaryCommand = BaseObligationEscrowCommand & { + newBeneficiary: string; +}; + export type EncryptWalletCommand = PrivateKeyOption & { outputFile: string; }; diff --git a/src/utils/cli-errors.ts b/src/utils/cli-errors.ts index 9a8a400..358190e 100644 --- a/src/utils/cli-errors.ts +++ b/src/utils/cli-errors.ts @@ -1,4 +1,7 @@ -// CLI error helpers: turn caught errors into clear messages for the user. +/** + * Shared helpers for decoding ethers CALL_EXCEPTION / custom errors into + * clear CLI messages (gas estimation often leaves revert.name unset). + */ export type ErrnoException = NodeJS.ErrnoException; @@ -11,6 +14,224 @@ export type CliErrorOptions = { const DEFAULT_PATH_PLACEHOLDER = 'the specified path'; +/** Known on-chain custom errors → actionable CLI guidance. */ +const KNOWN_REVERT_MESSAGES: Record = { + OwnerHolderMustDiffer: + 'Beneficiary (owner) and holder must be different wallets before accept/reject can run. Remint with different beneficiary and holder addresses.', + CallerNotBeneficiary: 'Connected wallet is not the beneficiary for this obligation.', + CallerNotHolder: 'Connected wallet is not the holder for this obligation.', + NotRegistered: "This token ID hasn't been minted on this registry yet.", + RemarkLengthExceeded: 'The remark is too long. Shorten it and try again.', + RegistryContractPaused: 'This registry is paused — unpause it before continuing.', + InactiveTitleEscrow: 'This obligation escrow is inactive.', + TitleEscrowNotHoldingToken: 'This escrow is not holding the token.', + DualRoleRejectionRequired: + 'Both beneficiary and holder roles are required to reject this transfer.', + TokenNotReturnedToIssuer: 'Token must be returned to issuer before this action.', + RecipientAlreadyHolder: 'Recipient is already the holder.', + InvalidNominee: 'The nominee address is invalid.', + InvalidTransferToZeroAddress: 'Cannot transfer to the zero address.', + NomineeAlreadyNominated: 'This nominee is already nominated.', + TargetNomineeAlreadyBeneficiary: 'The nominee is already the beneficiary.', + AlreadyRegistered: 'This token ID is already minted on this registry.', + TokenExists: 'This token ID already exists on this registry.', + AccessControlUnauthorizedAccount: 'Connected wallet is not authorized for this action.', + CallerNotMinter: 'Connected wallet does not hold MINTER_ROLE on this registry.', + InvalidStatusTransition: 'This action is not allowed from the current obligation status.', + EmptyReceivingData: 'Receiving data was empty.', + InvalidTokenId: 'The token ID is invalid.', + InvalidRegistry: 'The registry address is invalid.', +}; + +/** + * keccak256 selectors for no-arg / known signatures. Gas estimation often returns + * only `data: 0x` with shortMessage "execution reverted (unknown custom error)". + */ +const REVERT_SELECTOR_TO_NAME: Record = { + '0x7e288225': 'OwnerHolderMustDiffer', + '0xf52018ac': 'CallerNotBeneficiary', + '0x667b3fbf': 'CallerNotHolder', + '0xaba47339': 'NotRegistered', + '0x14086584': 'RemarkLengthExceeded', + '0xad49811c': 'RegistryContractPaused', + '0x7c339b19': 'InactiveTitleEscrow', + '0x2fd39523': 'TitleEscrowNotHoldingToken', + '0x8c90bf45': 'DualRoleRejectionRequired', + '0xd5475a0b': 'TokenNotReturnedToIssuer', + '0x702e6e62': 'RecipientAlreadyHolder', + '0x1c98cac3': 'InvalidNominee', + '0x2c75c45a': 'InvalidTransferToZeroAddress', + '0x98f83ce8': 'NomineeAlreadyNominated', + '0xdd9f921d': 'TargetNomineeAlreadyBeneficiary', + '0x3a81d6fc': 'AlreadyRegistered', + '0x55c7e8ba': 'TokenExists', + '0x10daa94f': 'EmptyReceivingData', + '0xe3962ba3': 'InvalidStatusTransition', + '0xed15e6cf': 'InvalidTokenId', + '0x540b9601': 'InvalidRegistry', + '0xbd805e91': 'InvalidTokenTransferToZeroAddressOwners', + '0xe2517d3f': 'AccessControlUnauthorizedAccount', + '0x5eee367a': 'CallerNotMinter', +}; + +const UNKNOWN_CUSTOM_ERROR = /unknown custom error/i; + +type EthersCallException = { + reason?: unknown; + shortMessage?: unknown; + message?: unknown; + revert?: { name?: unknown }; + errorName?: unknown; + code?: unknown; + data?: unknown; + info?: { error?: { data?: unknown; message?: unknown } }; + error?: { data?: unknown; message?: unknown }; +}; + +const asEthersError = (value: unknown): EthersCallException | null => + typeof value === 'object' && value !== null ? (value as EthersCallException) : null; + +const normalizeLabel = (raw: string): string | undefined => { + const cleaned = raw.trim().replace(/\(\)$/, ''); + if (!cleaned || UNKNOWN_CUSTOM_ERROR.test(cleaned) || cleaned === '(unknown custom error)') { + return undefined; + } + // Drop leading junk like ": OwnerHolderMustDiffer" or parentheses wrappers + const ident = cleaned.match(/([A-Za-z][A-Za-z0-9_]*)/); + return ident?.[1]; +}; + +const collectPossibleData = (err: EthersCallException): string[] => { + const values: unknown[] = [err.data, err.info?.error?.data, err.error?.data]; + const out: string[] = []; + for (const v of values) { + if (typeof v === 'string' && /^0x[0-9a-fA-F]+$/i.test(v)) { + out.push(v.toLowerCase()); + } else if (typeof v === 'object' && v !== null && 'data' in v) { + const nested = (v as { data?: unknown }).data; + if (typeof nested === 'string' && /^0x[0-9a-fA-F]+$/i.test(nested)) { + out.push(nested.toLowerCase()); + } + } + } + return out; +}; + +const labelFromRevertData = (err: EthersCallException): string | undefined => { + for (const data of collectPossibleData(err)) { + if (data === '0x') continue; + const selector = data.slice(0, 10); + if (REVERT_SELECTOR_TO_NAME[selector]) { + return REVERT_SELECTOR_TO_NAME[selector]; + } + } + return undefined; +}; + +export function extractContractRevertLabel(error: unknown): string | undefined { + const err = asEthersError(error); + if (!err) { + if (error instanceof Error) { + const fromMessage = error.message.match( + /(?:execution reverted:\s*|Contract reverted with\s*|failed:\s*)([A-Za-z0-9_]+)/, + ); + return fromMessage?.[1] ? normalizeLabel(fromMessage[1]) : undefined; + } + return undefined; + } + + const revertName = err.revert?.name ?? err.errorName; + if (typeof revertName === 'string') { + const label = normalizeLabel(revertName); + if (label) return label; + } + + if (typeof err.reason === 'string') { + const label = normalizeLabel(err.reason); + if (label) return label; + } + + // Prefer selector decoding before shortMessage — gas estimate often only has + // "execution reverted (unknown custom error)" + data: 0x7e288225 + const fromData = labelFromRevertData(err); + if (fromData) return fromData; + + if (typeof err.shortMessage === 'string' && err.shortMessage.trim()) { + if (!UNKNOWN_CUSTOM_ERROR.test(err.shortMessage)) { + const match = err.shortMessage.match(/execution reverted(?::\s*)?(.+)?/i); + if (match?.[1]) { + const label = normalizeLabel(match[1]); + if (label) return label; + } + } + } + + if (typeof err.message === 'string') { + const custom = err.message.match(/execution reverted:\s*([A-Za-z0-9_]+)/); + if (custom?.[1]) { + const label = normalizeLabel(custom[1]); + if (label) return label; + } + const wrapped = err.message.match(/(?:Contract reverted with\s*|failed:\s*)([A-Za-z0-9_]+)/); + if (wrapped?.[1]) { + const label = normalizeLabel(wrapped[1]); + if (label) return label; + } + } + + return undefined; +} + +/** True when the failure is a definitive on-chain revert (not a transient gas/RPC issue). */ +export function isContractCallException(error: unknown): boolean { + const err = asEthersError(error); + if (!err) { + if (error instanceof Error && /Pre-check .* failed:/i.test(error.message)) { + return Boolean(extractContractRevertLabel(error)); + } + return false; + } + if (err.code === 'CALL_EXCEPTION') return true; + if (err.revert?.name || err.errorName) return true; + if (labelFromRevertData(err)) return true; + return Boolean(extractContractRevertLabel(error)); +} + +/** Maps ethers / contract errors to a short user-facing message. */ +export function describeContractError(error: unknown): string { + const label = extractContractRevertLabel(error); + if (label && KNOWN_REVERT_MESSAGES[label]) { + return `${label}: ${KNOWN_REVERT_MESSAGES[label]}`; + } + if (label) { + return `Contract reverted with ${label}`; + } + + const err = asEthersError(error); + if (err) { + // Never surface the useless ethers placeholder as the final message + if ( + typeof err.shortMessage === 'string' && + err.shortMessage.trim() && + !UNKNOWN_CUSTOM_ERROR.test(err.shortMessage) + ) { + return err.shortMessage; + } + if (typeof err.reason === 'string' && err.reason.trim()) { + return err.reason; + } + if (typeof err.message === 'string' && err.message.trim()) { + return err.message.split('\n')[0]!; + } + } + + if (error instanceof Error && error.message) { + return error.message.split('\n')[0]!; + } + + return String(error); +} + export function isErrnoException(err: unknown): err is ErrnoException { return ( typeof err === 'object' && diff --git a/src/utils/cli-options.ts b/src/utils/cli-options.ts index 456bfba..652cd1e 100644 --- a/src/utils/cli-options.ts +++ b/src/utils/cli-options.ts @@ -12,7 +12,8 @@ import { readDocumentFile } from './file-io'; import { getTokenRegistryAddress, getTokenId, getChainId } from '@trustvc/trustvc'; import fs from 'fs'; import { getTokenRegistryVersion } from '../commands/helpers'; -import { getErrorMessage } from './index'; +import { getErrorMessage } from './formatting'; +import { isContractCallException } from './cli-errors'; import { dryRunMode } from './dryRun'; export interface NetworkOption { @@ -572,7 +573,16 @@ export const performDryRunWithConfirmation = async ({ info('\n✅ Proceeding with transaction...'); return true; } catch (estimateError) { - error(`Gas estimation failed: ${getErrorMessage(estimateError)}`); + const message = getErrorMessage(estimateError); + // Definitive on-chain reverts will never succeed if broadcast — abort instead of + // offering "proceed anyway" (which only dumps a larger callStatic stack next). + if (isContractCallException(estimateError)) { + error(`Transaction would revert: ${message}`); + info('Transaction cancelled.'); + return false; + } + + error(`Gas estimation failed: ${message}`); const proceedAnyway = await confirm({ message: 'Gas estimation failed. Do you want to proceed anyway?', default: false, diff --git a/src/utils/formatting.ts b/src/utils/formatting.ts index dd7faf9..cbdd83b 100644 --- a/src/utils/formatting.ts +++ b/src/utils/formatting.ts @@ -1,4 +1,5 @@ import chalk from 'chalk'; +import { describeContractError, isContractCallException } from './cli-errors'; export const addAddressPrefix = (address: string): string => address.startsWith('0x') ? address : `0x${address}`; @@ -33,11 +34,27 @@ const toErrorWithMessage = function (maybeError: unknown): ErrorWithMessage { export const extractErrorMessage = (error: unknown): string => toErrorWithMessage(error).message; export const getErrorMessage = function (error: unknown): string { + // Prefer structured ethers / custom-error decoding over raw `reason` (often null). + if (isContractCallException(error)) { + return describeContractError(error); + } + if (error instanceof Error) { - return 'reason' in error ? (error['reason'] as string) : error.message; - } else { - return extractErrorMessage(error); + const reason = + 'reason' in error && typeof (error as { reason?: unknown }).reason === 'string' + ? (error as { reason: string }).reason.trim() + : ''; + if (reason) return reason; + + // SDK may already wrap: "Pre-check for accept failed: OwnerHolderMustDiffer: ..." + if (/Pre-check .* failed:/i.test(error.message)) { + return describeContractError(error); + } + + return error.message.split('\n')[0]!; } + + return describeContractError(error) || extractErrorMessage(error); }; // Captures console.warn for a function to handle expected console.warn. diff --git a/src/utils/index.ts b/src/utils/index.ts index 081f499..3d83e77 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -30,3 +30,6 @@ export * from './dryRun'; // Document Verification export * from './document-verification'; + +// Obligation / BoE document helpers +export * from './obligation-document'; diff --git a/src/utils/obligation-document.ts b/src/utils/obligation-document.ts new file mode 100644 index 0000000..c4c9ceb --- /dev/null +++ b/src/utils/obligation-document.ts @@ -0,0 +1,68 @@ +import { info } from 'signale'; +import { + getObligationRegistryAddress, + getTokenId, + getChainId, + isObligationRecord, +} from '@trustvc/trustvc'; +import { SUPPORTED_CHAINS, CHAIN_ID } from './networks'; + +/** + * Extracts obligation / BoE document fields. Fails for classic ETR (tokenRegistry-only) docs. + */ +export const extractObligationDocumentInfo = async ( + document: any, +): Promise<{ + document: any; + obligationRegistry: string; + tokenId: string; + network: string; + documentId: string; +}> => { + if (!isObligationRecord(document)) { + throw new Error( + 'Document is not an obligation / BoE record. Use classic token-registry / title-escrow commands for TransferableRecords with tokenRegistry.', + ); + } + + let obligationRegistry: string | undefined; + let tokenId: string | undefined; + let chainId: CHAIN_ID | undefined; + + try { + obligationRegistry = getObligationRegistryAddress(document); + tokenId = getTokenId(document); + chainId = getChainId(document); + } catch (err) { + throw new Error( + `Failed to extract obligation document information: ${err instanceof Error ? err.message : String(err)}`, + ); + } + + if (!obligationRegistry) { + throw new Error('Document does not contain a valid obligationRegistry address'); + } + if (!tokenId) { + throw new Error('Document does not contain a valid token ID'); + } + if (!chainId) { + throw new Error('Document does not contain a valid chain ID'); + } + + const network = SUPPORTED_CHAINS[chainId].name; + const documentId = document.id || 'N/A'; + + info(`Extracted from obligation document:`); + info(` Network: ${network} (Chain ID: ${chainId})`); + info(` Obligation Registry: ${obligationRegistry}`); + info(` Token ID: ${tokenId}`); + info(` Document ID: ${documentId}`); + + return { + document, + obligationRegistry, + tokenId, + network, + documentId, + }; +}; diff --git a/tests/commands/obligation-escrow/accept-return-to-issuer.test.ts b/tests/commands/obligation-escrow/accept-return-to-issuer.test.ts new file mode 100644 index 0000000..b175b05 --- /dev/null +++ b/tests/commands/obligation-escrow/accept-return-to-issuer.test.ts @@ -0,0 +1,121 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { + acceptReturnedHandler, + promptForInputs, +} from '../../../src/commands/obligation-escrow/accept-return-to-issuer'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + acceptReturnedObligationRegistry: vi.fn(), + }; +}); + +vi.mock('../../../src/commands/obligation-escrow/shared', () => ({ + promptBaseObligationEscrowInputs: vi.fn().mockResolvedValue({ + network: 'amoy', + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + remark: 'ok', + encryptionKey: 'doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }), +})); + +const { burnPopulateTransaction, escrowAcceptReturned } = vi.hoisted(() => ({ + burnPopulateTransaction: vi.fn().mockResolvedValue({ to: '0xRegistry', data: '0xburn' }), + escrowAcceptReturned: vi.fn(), +})); + +vi.mock('../../../src/commands/helpers', () => ({ + connectToObligationEscrow: vi.fn().mockResolvedValue({ + // The escrow contract intentionally has no acceptReturned method: burning + // a returned title happens on the registry, not the escrow. + acceptReturned: escrowAcceptReturned, + }), + connectToObligationRegistry: vi.fn().mockResolvedValue({ + burn: { populateTransaction: burnPopulateTransaction }, + }), + validateAndEncryptRemark: vi.fn().mockReturnValue('0xencrypted'), +})); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getWalletOrSigner: vi.fn().mockResolvedValue({ + getAddress: vi.fn().mockResolvedValue('0xFrom'), + provider: {}, + }), + getSupportedNetwork: vi.fn().mockReturnValue({ networkId: 80002 }), + canEstimateGasPrice: vi.fn().mockReturnValue(false), + performDryRunWithConfirmation: vi.fn(async ({ getTransactionCallback }) => { + await getTransactionCallback(); + return true; + }), + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-escrow/accept-return-to-issuer', () => { + beforeEach(() => vi.clearAllMocks()); + + it('promptForInputs uses shared obligation prompts', async () => { + const result = await promptForInputs(); + expect(result.obligationRegistryAddress).toBe('0xRegistry'); + }); + + it('dry-run populates burn() on the registry contract, not the escrow', async () => { + const trustvc = await import('@trustvc/trustvc'); + (trustvc.acceptReturnedObligationRegistry as MockedFunction).mockResolvedValue({ + hash: '0xaccept-return', + wait: vi.fn().mockResolvedValue({ hash: '0xaccept-return' }), + }); + + await acceptReturnedHandler({ + network: NetworkCmdName.Amoy, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + remark: 'ok', + encryptionKey: 'doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + + expect(burnPopulateTransaction).toHaveBeenCalledWith('0x1', '0xencrypted'); + expect(escrowAcceptReturned).not.toHaveBeenCalled(); + expect(trustvc.acceptReturnedObligationRegistry).toHaveBeenCalled(); + }); +}); diff --git a/tests/commands/obligation-escrow/accept.test.ts b/tests/commands/obligation-escrow/accept.test.ts new file mode 100644 index 0000000..231bf3a --- /dev/null +++ b/tests/commands/obligation-escrow/accept.test.ts @@ -0,0 +1,85 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { acceptHandler, promptForInputs } from '../../../src/commands/obligation-escrow/accept'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + acceptObligationRegistry: vi.fn(), + }; +}); + +vi.mock('../../../src/commands/obligation-escrow/shared', () => ({ + promptBaseObligationEscrowInputs: vi.fn().mockResolvedValue({ + network: 'amoy', + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + remark: 'ok', + encryptionKey: 'doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }), +})); + +vi.mock('../../../src/commands/obligation-escrow/runTx', () => ({ + runObligationEscrowTx: vi.fn().mockResolvedValue({ hash: '0xaccept' }), +})); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-escrow/accept', () => { + beforeEach(() => vi.clearAllMocks()); + + it('promptForInputs uses shared obligation prompts', async () => { + const result = await promptForInputs(); + expect(result.obligationRegistryAddress).toBe('0xRegistry'); + }); + + it('acceptHandler runs SDK via runObligationEscrowTx', async () => { + const { runObligationEscrowTx } = await import('../../../src/commands/obligation-escrow/runTx'); + await acceptHandler({ + network: NetworkCmdName.Amoy, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + expect(runObligationEscrowTx as MockedFunction).toHaveBeenCalled(); + }); +}); diff --git a/tests/commands/obligation-escrow/discharge.test.ts b/tests/commands/obligation-escrow/discharge.test.ts new file mode 100644 index 0000000..5a0c010 --- /dev/null +++ b/tests/commands/obligation-escrow/discharge.test.ts @@ -0,0 +1,92 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { + dischargeHandler, + promptForInputs, +} from '../../../src/commands/obligation-escrow/discharge'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + dischargeObligationRegistry: vi.fn(), + }; +}); + +vi.mock('../../../src/commands/obligation-escrow/shared', () => ({ + promptBaseObligationEscrowInputs: vi.fn().mockResolvedValue({ + network: 'amoy', + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + remark: 'ok', + encryptionKey: 'doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }), +})); + +vi.mock('../../../src/commands/obligation-escrow/runTx', () => ({ + runObligationEscrowTx: vi.fn().mockResolvedValue({ hash: '0xdischarge' }), +})); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-escrow/discharge', () => { + beforeEach(() => vi.clearAllMocks()); + + it('promptForInputs uses shared obligation prompts', async () => { + const result = await promptForInputs(); + expect(result.obligationRegistryAddress).toBe('0xRegistry'); + }); + + it('dischargeHandler runs SDK via runObligationEscrowTx', async () => { + const { runObligationEscrowTx } = await import('../../../src/commands/obligation-escrow/runTx'); + await dischargeHandler({ + network: NetworkCmdName.Amoy, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + expect(runObligationEscrowTx as MockedFunction).toHaveBeenCalledWith( + expect.objectContaining({ + sdkParams: expect.objectContaining({ remarks: undefined }), + }), + ); + }); +}); diff --git a/tests/commands/obligation-escrow/endorse-transfer-owner.test.ts b/tests/commands/obligation-escrow/endorse-transfer-owner.test.ts new file mode 100644 index 0000000..deb6ba8 --- /dev/null +++ b/tests/commands/obligation-escrow/endorse-transfer-owner.test.ts @@ -0,0 +1,95 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { + endorseHandler, + promptForInputs, +} from '../../../src/commands/obligation-escrow/endorse-transfer-owner'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + transferBeneficiaryObligationRegistry: vi.fn(), + }; +}); + +vi.mock('../../../src/commands/obligation-escrow/shared', () => ({ + promptBaseObligationEscrowInputs: vi.fn().mockResolvedValue({ + network: 'amoy', + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + encryptionKey: 'doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }), +})); + +vi.mock('../../../src/commands/obligation-escrow/runTx', () => ({ + runObligationEscrowTx: vi.fn().mockResolvedValue({ hash: '0xendorse' }), +})); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + promptAddress: vi.fn().mockResolvedValue('0xEndorsee0000000000000000000000000000001'), + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-escrow/endorse-transfer-owner', () => { + beforeEach(() => vi.clearAllMocks()); + + it('promptForInputs includes newBeneficiary', async () => { + const result = await promptForInputs(); + expect(result.newBeneficiary).toBe('0xEndorsee0000000000000000000000000000001'); + }); + + it('endorseHandler calls runObligationEscrowTx with beneficiary params', async () => { + const { runObligationEscrowTx } = await import('../../../src/commands/obligation-escrow/runTx'); + await endorseHandler({ + network: NetworkCmdName.Amoy, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + newBeneficiary: '0xEndorsee0000000000000000000000000000001', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + expect(runObligationEscrowTx as MockedFunction).toHaveBeenCalledWith( + expect.objectContaining({ + sdkParams: expect.objectContaining({ + newBeneficiaryAddress: '0xEndorsee0000000000000000000000000000001', + }), + }), + ); + }); +}); diff --git a/tests/commands/obligation-escrow/nominate-transfer-owner.test.ts b/tests/commands/obligation-escrow/nominate-transfer-owner.test.ts new file mode 100644 index 0000000..a3da87a --- /dev/null +++ b/tests/commands/obligation-escrow/nominate-transfer-owner.test.ts @@ -0,0 +1,95 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { + nominateHandler, + promptForInputs, +} from '../../../src/commands/obligation-escrow/nominate-transfer-owner'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + nominateObligationRegistry: vi.fn(), + }; +}); + +vi.mock('../../../src/commands/obligation-escrow/shared', () => ({ + promptBaseObligationEscrowInputs: vi.fn().mockResolvedValue({ + network: 'amoy', + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + encryptionKey: 'doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }), +})); + +vi.mock('../../../src/commands/obligation-escrow/runTx', () => ({ + runObligationEscrowTx: vi.fn().mockResolvedValue({ hash: '0xnominate' }), +})); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + promptAddress: vi.fn().mockResolvedValue('0xNominee00000000000000000000000000000001'), + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-escrow/nominate-transfer-owner', () => { + beforeEach(() => vi.clearAllMocks()); + + it('promptForInputs includes newBeneficiary', async () => { + const result = await promptForInputs(); + expect(result.newBeneficiary).toBe('0xNominee00000000000000000000000000000001'); + }); + + it('nominateHandler calls runObligationEscrowTx with nominee params', async () => { + const { runObligationEscrowTx } = await import('../../../src/commands/obligation-escrow/runTx'); + await nominateHandler({ + network: NetworkCmdName.Amoy, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + newBeneficiary: '0xNominee00000000000000000000000000000001', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + expect(runObligationEscrowTx as MockedFunction).toHaveBeenCalledWith( + expect.objectContaining({ + sdkParams: expect.objectContaining({ + newBeneficiaryAddress: '0xNominee00000000000000000000000000000001', + }), + }), + ); + }); +}); diff --git a/tests/commands/obligation-escrow/reject-return-to-issuer.test.ts b/tests/commands/obligation-escrow/reject-return-to-issuer.test.ts new file mode 100644 index 0000000..83c6c2c --- /dev/null +++ b/tests/commands/obligation-escrow/reject-return-to-issuer.test.ts @@ -0,0 +1,121 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { + rejectReturnedHandler, + promptForInputs, +} from '../../../src/commands/obligation-escrow/reject-return-to-issuer'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + rejectReturnedObligationRegistry: vi.fn(), + }; +}); + +vi.mock('../../../src/commands/obligation-escrow/shared', () => ({ + promptBaseObligationEscrowInputs: vi.fn().mockResolvedValue({ + network: 'amoy', + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + remark: 'ok', + encryptionKey: 'doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }), +})); + +const { restorePopulateTransaction, escrowRejectReturned } = vi.hoisted(() => ({ + restorePopulateTransaction: vi.fn().mockResolvedValue({ to: '0xRegistry', data: '0xrestore' }), + escrowRejectReturned: vi.fn(), +})); + +vi.mock('../../../src/commands/helpers', () => ({ + connectToObligationEscrow: vi.fn().mockResolvedValue({ + // The escrow contract intentionally has no rejectReturned method: restoring + // a returned title happens on the registry, not the escrow. + rejectReturned: escrowRejectReturned, + }), + connectToObligationRegistry: vi.fn().mockResolvedValue({ + restore: { populateTransaction: restorePopulateTransaction }, + }), + validateAndEncryptRemark: vi.fn().mockReturnValue('0xencrypted'), +})); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getWalletOrSigner: vi.fn().mockResolvedValue({ + getAddress: vi.fn().mockResolvedValue('0xFrom'), + provider: {}, + }), + getSupportedNetwork: vi.fn().mockReturnValue({ networkId: 80002 }), + canEstimateGasPrice: vi.fn().mockReturnValue(false), + performDryRunWithConfirmation: vi.fn(async ({ getTransactionCallback }) => { + await getTransactionCallback(); + return true; + }), + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-escrow/reject-return-to-issuer', () => { + beforeEach(() => vi.clearAllMocks()); + + it('promptForInputs uses shared obligation prompts', async () => { + const result = await promptForInputs(); + expect(result.obligationRegistryAddress).toBe('0xRegistry'); + }); + + it('dry-run populates restore() on the registry contract, not the escrow', async () => { + const trustvc = await import('@trustvc/trustvc'); + (trustvc.rejectReturnedObligationRegistry as MockedFunction).mockResolvedValue({ + hash: '0xreject-return', + wait: vi.fn().mockResolvedValue({ hash: '0xreject-return' }), + }); + + await rejectReturnedHandler({ + network: NetworkCmdName.Amoy, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + remark: 'ok', + encryptionKey: 'doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + + expect(restorePopulateTransaction).toHaveBeenCalledWith('0x1', '0xencrypted'); + expect(escrowRejectReturned).not.toHaveBeenCalled(); + expect(trustvc.rejectReturnedObligationRegistry).toHaveBeenCalled(); + }); +}); diff --git a/tests/commands/obligation-escrow/reject-transfer-holder.test.ts b/tests/commands/obligation-escrow/reject-transfer-holder.test.ts new file mode 100644 index 0000000..e596fe2 --- /dev/null +++ b/tests/commands/obligation-escrow/reject-transfer-holder.test.ts @@ -0,0 +1,92 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { + rejectTransferHolderHandler, + promptForInputs, +} from '../../../src/commands/obligation-escrow/reject-transfer-holder'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + rejectTransferHolderObligationRegistry: vi.fn(), + }; +}); + +vi.mock('../../../src/commands/obligation-escrow/shared', () => ({ + promptBaseObligationEscrowInputs: vi.fn().mockResolvedValue({ + network: 'amoy', + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + remark: 'ok', + encryptionKey: 'doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }), +})); + +vi.mock('../../../src/commands/obligation-escrow/runTx', () => ({ + runObligationEscrowTx: vi.fn().mockResolvedValue({ hash: '0xreject-transfer-holder' }), +})); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-escrow/reject-transfer-holder', () => { + beforeEach(() => vi.clearAllMocks()); + + it('promptForInputs uses shared obligation prompts', async () => { + const result = await promptForInputs(); + expect(result.obligationRegistryAddress).toBe('0xRegistry'); + }); + + it('rejectTransferHolderHandler runs SDK via runObligationEscrowTx', async () => { + const { runObligationEscrowTx } = await import('../../../src/commands/obligation-escrow/runTx'); + await rejectTransferHolderHandler({ + network: NetworkCmdName.Amoy, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + expect(runObligationEscrowTx as MockedFunction).toHaveBeenCalledWith( + expect.objectContaining({ + sdkParams: expect.objectContaining({ remarks: undefined }), + }), + ); + }); +}); diff --git a/tests/commands/obligation-escrow/reject-transfer-owner-holder.test.ts b/tests/commands/obligation-escrow/reject-transfer-owner-holder.test.ts new file mode 100644 index 0000000..d01c1e2 --- /dev/null +++ b/tests/commands/obligation-escrow/reject-transfer-owner-holder.test.ts @@ -0,0 +1,92 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { + rejectTransferOwnersHandler, + promptForInputs, +} from '../../../src/commands/obligation-escrow/reject-transfer-owner-holder'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + rejectTransferOwnersObligationRegistry: vi.fn(), + }; +}); + +vi.mock('../../../src/commands/obligation-escrow/shared', () => ({ + promptBaseObligationEscrowInputs: vi.fn().mockResolvedValue({ + network: 'amoy', + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + remark: 'ok', + encryptionKey: 'doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }), +})); + +vi.mock('../../../src/commands/obligation-escrow/runTx', () => ({ + runObligationEscrowTx: vi.fn().mockResolvedValue({ hash: '0xreject-transfer-owner-holder' }), +})); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-escrow/reject-transfer-owner-holder', () => { + beforeEach(() => vi.clearAllMocks()); + + it('promptForInputs uses shared obligation prompts', async () => { + const result = await promptForInputs(); + expect(result.obligationRegistryAddress).toBe('0xRegistry'); + }); + + it('rejectTransferOwnersHandler runs SDK via runObligationEscrowTx', async () => { + const { runObligationEscrowTx } = await import('../../../src/commands/obligation-escrow/runTx'); + await rejectTransferOwnersHandler({ + network: NetworkCmdName.Amoy, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + expect(runObligationEscrowTx as MockedFunction).toHaveBeenCalledWith( + expect.objectContaining({ + sdkParams: expect.objectContaining({ remarks: undefined }), + }), + ); + }); +}); diff --git a/tests/commands/obligation-escrow/reject-transfer-owner.test.ts b/tests/commands/obligation-escrow/reject-transfer-owner.test.ts new file mode 100644 index 0000000..9ef97d3 --- /dev/null +++ b/tests/commands/obligation-escrow/reject-transfer-owner.test.ts @@ -0,0 +1,92 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { + rejectTransferOwnerHandler, + promptForInputs, +} from '../../../src/commands/obligation-escrow/reject-transfer-owner'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + rejectTransferBeneficiaryObligationRegistry: vi.fn(), + }; +}); + +vi.mock('../../../src/commands/obligation-escrow/shared', () => ({ + promptBaseObligationEscrowInputs: vi.fn().mockResolvedValue({ + network: 'amoy', + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + remark: 'ok', + encryptionKey: 'doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }), +})); + +vi.mock('../../../src/commands/obligation-escrow/runTx', () => ({ + runObligationEscrowTx: vi.fn().mockResolvedValue({ hash: '0xreject-transfer-owner' }), +})); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-escrow/reject-transfer-owner', () => { + beforeEach(() => vi.clearAllMocks()); + + it('promptForInputs uses shared obligation prompts', async () => { + const result = await promptForInputs(); + expect(result.obligationRegistryAddress).toBe('0xRegistry'); + }); + + it('rejectTransferOwnerHandler runs SDK via runObligationEscrowTx', async () => { + const { runObligationEscrowTx } = await import('../../../src/commands/obligation-escrow/runTx'); + await rejectTransferOwnerHandler({ + network: NetworkCmdName.Amoy, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + expect(runObligationEscrowTx as MockedFunction).toHaveBeenCalledWith( + expect.objectContaining({ + sdkParams: expect.objectContaining({ remarks: undefined }), + }), + ); + }); +}); diff --git a/tests/commands/obligation-escrow/reject.test.ts b/tests/commands/obligation-escrow/reject.test.ts new file mode 100644 index 0000000..90d0c07 --- /dev/null +++ b/tests/commands/obligation-escrow/reject.test.ts @@ -0,0 +1,89 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { rejectHandler, promptForInputs } from '../../../src/commands/obligation-escrow/reject'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + rejectObligationRegistry: vi.fn(), + }; +}); + +vi.mock('../../../src/commands/obligation-escrow/shared', () => ({ + promptBaseObligationEscrowInputs: vi.fn().mockResolvedValue({ + network: 'amoy', + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + remark: 'ok', + encryptionKey: 'doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }), +})); + +vi.mock('../../../src/commands/obligation-escrow/runTx', () => ({ + runObligationEscrowTx: vi.fn().mockResolvedValue({ hash: '0xreject' }), +})); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-escrow/reject', () => { + beforeEach(() => vi.clearAllMocks()); + + it('promptForInputs uses shared obligation prompts', async () => { + const result = await promptForInputs(); + expect(result.obligationRegistryAddress).toBe('0xRegistry'); + }); + + it('rejectHandler runs SDK via runObligationEscrowTx', async () => { + const { runObligationEscrowTx } = await import('../../../src/commands/obligation-escrow/runTx'); + await rejectHandler({ + network: NetworkCmdName.Amoy, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + expect(runObligationEscrowTx as MockedFunction).toHaveBeenCalledWith( + expect.objectContaining({ + sdkParams: expect.objectContaining({ remarks: undefined }), + }), + ); + }); +}); diff --git a/tests/commands/obligation-escrow/return-to-issuer.test.ts b/tests/commands/obligation-escrow/return-to-issuer.test.ts new file mode 100644 index 0000000..bd13c7b --- /dev/null +++ b/tests/commands/obligation-escrow/return-to-issuer.test.ts @@ -0,0 +1,68 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { returnToIssuerHandler } from '../../../src/commands/obligation-escrow/return-to-issuer'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + returnToIssuerObligationRegistry: vi.fn(), + }; +}); + +vi.mock('../../../src/commands/obligation-escrow/runTx', () => ({ + runObligationEscrowTx: vi.fn().mockResolvedValue({ hash: '0xreturn' }), +})); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-escrow/return-to-issuer', () => { + beforeEach(() => vi.clearAllMocks()); + + it('returnToIssuerHandler invokes runObligationEscrowTx', async () => { + const { runObligationEscrowTx } = await import('../../../src/commands/obligation-escrow/runTx'); + await returnToIssuerHandler({ + network: NetworkCmdName.Amoy, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + expect(runObligationEscrowTx as MockedFunction).toHaveBeenCalled(); + }); +}); diff --git a/tests/commands/obligation-escrow/status.test.ts b/tests/commands/obligation-escrow/status.test.ts new file mode 100644 index 0000000..e80f2cc --- /dev/null +++ b/tests/commands/obligation-escrow/status.test.ts @@ -0,0 +1,71 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { statusHandler } from '../../../src/commands/obligation-escrow/status'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + getObligationRegistryStatus: vi.fn().mockResolvedValue(0), + isObligationRegistryRegistered: vi.fn().mockResolvedValue(true), + getObligationEscrowTerminationReason: vi.fn().mockResolvedValue(0), + }; +}); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getWalletOrSigner: vi.fn().mockResolvedValue({ provider: {} }), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-escrow/status', () => { + beforeEach(() => vi.clearAllMocks()); + + it('reads status, registration, and termination reason', async () => { + const trustvc = await import('@trustvc/trustvc'); + await statusHandler({ + network: NetworkCmdName.Amoy, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + expect(trustvc.getObligationRegistryStatus as MockedFunction).toHaveBeenCalledWith( + { obligationRegistryAddress: '0xRegistry', tokenId: '0x1' }, + expect.anything(), + { tokenId: '0x1' }, + ); + expect(trustvc.isObligationRegistryRegistered).toHaveBeenCalled(); + expect(trustvc.getObligationEscrowTerminationReason).toHaveBeenCalled(); + }); +}); diff --git a/tests/commands/obligation-escrow/transfer-holder.test.ts b/tests/commands/obligation-escrow/transfer-holder.test.ts new file mode 100644 index 0000000..4a5c7f6 --- /dev/null +++ b/tests/commands/obligation-escrow/transfer-holder.test.ts @@ -0,0 +1,95 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { + changeHolderHandler, + promptForInputs, +} from '../../../src/commands/obligation-escrow/transfer-holder'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + transferHolderObligationRegistry: vi.fn(), + }; +}); + +vi.mock('../../../src/commands/obligation-escrow/shared', () => ({ + promptBaseObligationEscrowInputs: vi.fn().mockResolvedValue({ + network: 'amoy', + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + encryptionKey: 'doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }), +})); + +vi.mock('../../../src/commands/obligation-escrow/runTx', () => ({ + runObligationEscrowTx: vi.fn().mockResolvedValue({ hash: '0xtransfer' }), +})); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + promptAddress: vi.fn().mockResolvedValue('0xNewHolder000000000000000000000000000001'), + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-escrow/transfer-holder', () => { + beforeEach(() => vi.clearAllMocks()); + + it('promptForInputs includes newHolder', async () => { + const result = await promptForInputs(); + expect(result.newHolder).toBe('0xNewHolder000000000000000000000000000001'); + }); + + it('changeHolderHandler calls runObligationEscrowTx with holder params', async () => { + const { runObligationEscrowTx } = await import('../../../src/commands/obligation-escrow/runTx'); + await changeHolderHandler({ + network: NetworkCmdName.Amoy, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + newHolder: '0xNewHolder000000000000000000000000000001', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + expect(runObligationEscrowTx as MockedFunction).toHaveBeenCalledWith( + expect.objectContaining({ + sdkParams: expect.objectContaining({ + holderAddress: '0xNewHolder000000000000000000000000000001', + }), + }), + ); + }); +}); diff --git a/tests/commands/obligation-escrow/transfer-owner-holder.test.ts b/tests/commands/obligation-escrow/transfer-owner-holder.test.ts new file mode 100644 index 0000000..05f89fb --- /dev/null +++ b/tests/commands/obligation-escrow/transfer-owner-holder.test.ts @@ -0,0 +1,101 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { + transferOwnersHandler, + promptForInputs, +} from '../../../src/commands/obligation-escrow/transfer-owner-holder'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + transferOwnersObligationRegistry: vi.fn(), + }; +}); + +vi.mock('../../../src/commands/obligation-escrow/shared', () => ({ + promptBaseObligationEscrowInputs: vi.fn().mockResolvedValue({ + network: 'amoy', + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + encryptionKey: 'doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }), +})); + +vi.mock('../../../src/commands/obligation-escrow/runTx', () => ({ + runObligationEscrowTx: vi.fn().mockResolvedValue({ hash: '0xtransfer-owner-holder' }), +})); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + promptAddress: vi + .fn() + .mockResolvedValueOnce('0xNewOwner000000000000000000000000000001') + .mockResolvedValueOnce('0xNewHolder00000000000000000000000000001'), + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-escrow/transfer-owner-holder', () => { + beforeEach(() => vi.clearAllMocks()); + + it('promptForInputs includes newOwner and newHolder', async () => { + const result = await promptForInputs(); + expect(result.newOwner).toBe('0xNewOwner000000000000000000000000000001'); + expect(result.newHolder).toBe('0xNewHolder00000000000000000000000000001'); + }); + + it('transferOwnersHandler calls runObligationEscrowTx with owner+holder params', async () => { + const { runObligationEscrowTx } = await import('../../../src/commands/obligation-escrow/runTx'); + await transferOwnersHandler({ + network: NetworkCmdName.Amoy, + obligationRegistryAddress: '0xRegistry', + tokenId: '0x1', + newOwner: '0xNewOwner000000000000000000000000000001', + newHolder: '0xNewHolder00000000000000000000000000001', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + expect(runObligationEscrowTx as MockedFunction).toHaveBeenCalledWith( + expect.objectContaining({ + sdkParams: expect.objectContaining({ + newBeneficiaryAddress: '0xNewOwner000000000000000000000000000001', + newHolderAddress: '0xNewHolder00000000000000000000000000001', + }), + }), + ); + }); +}); diff --git a/tests/commands/obligation-registry/deploy.test.ts b/tests/commands/obligation-registry/deploy.test.ts new file mode 100644 index 0000000..11d1913 --- /dev/null +++ b/tests/commands/obligation-registry/deploy.test.ts @@ -0,0 +1,145 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { + handler, + deployObligationRegistryContract, + promptForInputs, +} from '../../../src/commands/obligation-registry/deploy'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@inquirer/prompts', () => ({ + select: vi.fn(), + input: vi.fn(), + confirm: vi.fn(), + password: vi.fn(), +})); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + deployObligationRegistry: vi.fn(), + }; +}); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + promptNetworkSelection: vi.fn().mockResolvedValue('amoy'), + promptWalletSelection: vi.fn().mockResolvedValue({ key: '0xabc' }), + promptAddress: vi.fn(), + getWalletOrSigner: vi.fn().mockResolvedValue({ + getAddress: vi.fn().mockResolvedValue('0xWallet'), + provider: {}, + }), + performDryRunWithConfirmation: vi.fn().mockResolvedValue(true), + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + getSupportedNetwork: vi.fn().mockReturnValue({ networkId: 80002 }), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + supportedNetwork: { + amoy: { networkId: 80002 }, + }, + }; +}); + +describe('obligation-registry/deploy', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('promptForInputs', () => { + it('collects registry name, symbol, and wallet', async () => { + const { input, confirm } = await import('@inquirer/prompts'); + (input as MockedFunction) + .mockResolvedValueOnce('My BoE Registry') + .mockResolvedValueOnce('BOE'); + (confirm as MockedFunction).mockResolvedValueOnce(false); + + const result = await promptForInputs(); + expect(result.registryName).toBe('My BoE Registry'); + expect(result.registrySymbol).toBe('BOE'); + expect(result.network).toBe(NetworkCmdName.Amoy); + expect(result.key).toBe('0xabc'); + }); + }); + + describe('deployObligationRegistryContract', () => { + it('deploys via SDK and returns registry address', async () => { + const trustvc = await import('@trustvc/trustvc'); + const deployMock = trustvc.deployObligationRegistry as MockedFunction; + deployMock.mockResolvedValue({ + receipt: { hash: '0xhash', logs: [] }, + obligationRegistry: '0xRegistry', + obligationEscrowFactoryAddress: '0xFactory', + }); + + const address = await deployObligationRegistryContract({ + network: NetworkCmdName.Amoy, + registryName: 'My BoE Registry', + registrySymbol: 'BOE', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + + expect(deployMock).toHaveBeenCalled(); + expect(address).toBe('0xRegistry'); + + const { success, info } = await import('signale'); + expect(success).toHaveBeenCalledWith( + 'Obligation registry My BoE Registry deployed at 0xRegistry', + ); + expect(info).toHaveBeenCalledWith('ObligationEscrowFactory: 0xFactory'); + expect(info).toHaveBeenCalledWith('Obligation Registry: 0xRegistry'); + expect(info).toHaveBeenCalledWith( + 'Use this Obligation Registry address as credentialStatus.obligationRegistry in your document before signing and minting.', + ); + }); + }); + + describe('handler', () => { + it('runs without throwing when prompts succeed', async () => { + const { input, confirm } = await import('@inquirer/prompts'); + (input as MockedFunction) + .mockResolvedValueOnce('My BoE Registry') + .mockResolvedValueOnce('BOE'); + (confirm as MockedFunction).mockResolvedValueOnce(false); + + const trustvc = await import('@trustvc/trustvc'); + (trustvc.deployObligationRegistry as MockedFunction).mockResolvedValue({ + receipt: { hash: '0xhash', logs: [] }, + obligationRegistry: '0xRegistry', + obligationEscrowFactoryAddress: '0xFactory', + }); + + await expect(handler()).resolves.toBeUndefined(); + }); + }); +}); diff --git a/tests/commands/obligation-registry/mint.test.ts b/tests/commands/obligation-registry/mint.test.ts new file mode 100644 index 0000000..02c41d9 --- /dev/null +++ b/tests/commands/obligation-registry/mint.test.ts @@ -0,0 +1,158 @@ +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import { + handler, + mintObligationToken, + promptForInputs, +} from '../../../src/commands/obligation-registry/mint'; +import { NetworkCmdName } from '../../../src/utils'; + +vi.mock('signale', async (importOriginal) => { + const originalSignale = await importOriginal(); + return { + ...originalSignale, + Signale: class MockSignale { + await = vi.fn(); + success = vi.fn(); + error = vi.fn(); + info = vi.fn(); + warn = vi.fn(); + constructor() {} + }, + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + await: vi.fn(), + default: { + await: vi.fn(), + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }, + }; +}); + +vi.mock('@inquirer/prompts', () => ({ + select: vi.fn(), + input: vi.fn(), + confirm: vi.fn(), + password: vi.fn(), +})); + +vi.mock('@trustvc/trustvc', async () => { + const actual = await vi.importActual('@trustvc/trustvc'); + return { + ...actual, + mintObligationRegistry: vi.fn(), + isObligationRecord: vi.fn(() => true), + getObligationRegistryAddress: vi.fn(() => '0x71D28767662cB233F887aD2Bb65d048d760bA694'), + getTokenId: vi.fn(() => '0x23f719b016c88ba1ef2e10c0718d7d0f0026b1dc6e219629f81e2f0f811c4e3e'), + getChainId: vi.fn(() => 80002), + }; +}); + +vi.mock('../../../src/commands/helpers', () => ({ + connectToObligationRegistry: vi.fn().mockResolvedValue({ + mint: { + populateTransaction: vi.fn().mockResolvedValue({ to: '0xRegistry', data: '0x' }), + }, + }), + validateAndEncryptRemark: vi.fn((r?: string) => r || '0x'), +})); + +vi.mock('../../../src/utils', async (importOriginal) => { + const actual = await importOriginal(); + const { default: obligationFixture } = + await import('../../fixtures/obligation/w3c-obligation-record.json'); + return { + ...actual, + promptAndReadDocument: vi.fn().mockResolvedValue(obligationFixture), + verifyDocumentSignature: vi.fn().mockResolvedValue(undefined), + extractObligationDocumentInfo: vi.fn().mockResolvedValue({ + document: obligationFixture, + obligationRegistry: '0x71D28767662cB233F887aD2Bb65d048d760bA694', + tokenId: '0x23f719b016c88ba1ef2e10c0718d7d0f0026b1dc6e219629f81e2f0f811c4e3e', + network: 'amoy', + documentId: 'urn:uuid:obligation-test-doc-id', + }), + promptAddress: vi.fn(), + promptWalletSelection: vi.fn().mockResolvedValue({ key: '0xabc' }), + promptRemark: vi.fn().mockResolvedValue('issued'), + getWalletOrSigner: vi.fn().mockResolvedValue({ + getAddress: vi.fn().mockResolvedValue('0xWallet'), + provider: {}, + }), + performDryRunWithConfirmation: vi.fn().mockResolvedValue(true), + canEstimateGasPrice: vi.fn().mockReturnValue(false), + displayTransactionPrice: vi.fn(), + getEtherscanAddress: vi.fn().mockReturnValue('https://amoy.polygonscan.com'), + addAddressPrefix: (v: string) => (v.startsWith('0x') ? v : `0x${v}`), + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), + }; +}); + +describe('obligation-registry/mint', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('promptForInputs builds mint command from obligation document', async () => { + const utils = await import('../../../src/utils'); + (utils.promptAddress as MockedFunction) + .mockResolvedValueOnce('0xBeneficiary000000000000000000000000000001') + .mockResolvedValueOnce('0xHolder0000000000000000000000000000000002'); + + const result = await promptForInputs(); + expect(result.address).toBe('0x71D28767662cB233F887aD2Bb65d048d760bA694'); + expect(result.beneficiary).toBe('0xBeneficiary000000000000000000000000000001'); + expect(result.holder).toBe('0xHolder0000000000000000000000000000000002'); + expect(result.remark).toBe('issued'); + }); + + it('mintObligationToken calls mintObligationRegistry', async () => { + const trustvc = await import('@trustvc/trustvc'); + const mintMock = trustvc.mintObligationRegistry as MockedFunction; + mintMock.mockResolvedValue({ + hash: '0xmint', + wait: vi.fn().mockResolvedValue({ hash: '0xmint' }), + }); + + await mintObligationToken({ + network: NetworkCmdName.Amoy, + address: '0x71D28767662cB233F887aD2Bb65d048d760bA694', + tokenId: '0x23f719b016c88ba1ef2e10c0718d7d0f0026b1dc6e219629f81e2f0f811c4e3e', + beneficiary: '0xBeneficiary000000000000000000000000000001', + holder: '0xHolder0000000000000000000000000000000002', + remark: 'issued', + encryptionKey: 'urn:uuid:obligation-test-doc-id', + key: '0xabc', + maxPriorityFeePerGasScale: 1, + }); + + expect(mintMock).toHaveBeenCalledWith( + { obligationRegistryAddress: '0x71D28767662cB233F887aD2Bb65d048d760bA694' }, + expect.anything(), + expect.objectContaining({ + beneficiaryAddress: '0xBeneficiary000000000000000000000000000001', + holderAddress: '0xHolder0000000000000000000000000000000002', + }), + expect.objectContaining({ id: 'urn:uuid:obligation-test-doc-id' }), + ); + }); + + it('handler completes mint flow', async () => { + const utils = await import('../../../src/utils'); + (utils.promptAddress as MockedFunction) + .mockResolvedValueOnce('0xBeneficiary000000000000000000000000000001') + .mockResolvedValueOnce('0xHolder0000000000000000000000000000000002'); + + const trustvc = await import('@trustvc/trustvc'); + (trustvc.mintObligationRegistry as MockedFunction).mockResolvedValue({ + hash: '0xmint', + wait: vi.fn().mockResolvedValue({ hash: '0xmint' }), + }); + + await expect(handler()).resolves.toBeUndefined(); + }); +}); diff --git a/tests/fixtures/obligation/w3c-obligation-record.json b/tests/fixtures/obligation/w3c-obligation-record.json new file mode 100644 index 0000000..3371dfb --- /dev/null +++ b/tests/fixtures/obligation/w3c-obligation-record.json @@ -0,0 +1,54 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://w3id.org/security/data-integrity/v2", + "https://trustvc.io/context/transferable-records-context.json", + "https://trustvc.io/context/bill-of-exchange.json" + ], + "type": ["VerifiableCredential"], + "issuer": "did:web:trustvc.github.io:did:1", + "id": "urn:uuid:obligation-test-doc-id", + "credentialStatus": { + "type": "TransferableRecords", + "tokenNetwork": { + "chain": "MATIC", + "chainId": "80002" + }, + "obligationRegistry": "0x71D28767662cB233F887aD2Bb65d048d760bA694", + "tokenId": "23f719b016c88ba1ef2e10c0718d7d0f0026b1dc6e219629f81e2f0f811c4e3e" + }, + "credentialSubject": { + "electronicDocumentIdentifier": "urn:uuid:obligation-test-doc-id", + "referenceNumber": "BOE-TEST-001", + "amountInFigures": "128500.00", + "amountInWords": "One hundred twenty-eight thousand five hundred United States Dollars only", + "currencyCode": "USD", + "blDate": "2026-06-28", + "placeOfIssue": "Singapore", + "dateOfIssue": "2026-07-06", + "tenor": "At 90 days sight", + "payee": "Meridian Commodities Pte Ltd", + "drawnUnder": "Documentary Credit No. LC-TEST-2026-88341 / Invoice No. INV-TEST-2026-0456 / B/L No. BL-TEST-2026-0781", + "drawnUnderDate": "2026-06-15", + "issuedBy": "DBS Bank Ltd", + "drawee": { + "name": "Fairview Industries Inc.", + "address": "1201 Market Street, Suite 900, Wilmington, DE 19801, USA", + "authorisedSignatoryName": "James R. Carter", + "signature": "" + }, + "drawer": { + "name": "Meridian Commodities Pte Ltd", + "address": "8 Marina Boulevard, #24-01, Singapore 018981", + "authorisedSignatoryName": "Wei Ling Tan", + "signature": "" + } + }, + "proof": { + "type": "DataIntegrityProof", + "cryptosuite": "ecdsa-sd-2023", + "proofPurpose": "assertionMethod", + "verificationMethod": "did:web:trustvc.github.io:did:1#multikey-1", + "proofValue": "uTEST" + } +} diff --git a/tests/utils/contract-errors.test.ts b/tests/utils/contract-errors.test.ts new file mode 100644 index 0000000..45c6482 --- /dev/null +++ b/tests/utils/contract-errors.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest'; +import { + describeContractError, + extractContractRevertLabel, + getErrorMessage, + isContractCallException, +} from '../../src/utils'; + +describe('contract revert error formatting', () => { + const ownerHolderError = Object.assign(new Error('execution reverted: OwnerHolderMustDiffer()'), { + code: 'CALL_EXCEPTION', + reason: null, + revert: { name: 'OwnerHolderMustDiffer', signature: 'OwnerHolderMustDiffer()', args: [] }, + shortMessage: 'execution reverted (unknown custom error)', + }); + + it('extracts OwnerHolderMustDiffer from ethers call exceptions', () => { + expect(extractContractRevertLabel(ownerHolderError)).toBe('OwnerHolderMustDiffer'); + expect(isContractCallException(ownerHolderError)).toBe(true); + }); + + it('maps OwnerHolderMustDiffer to an actionable message', () => { + expect(describeContractError(ownerHolderError)).toMatch( + /OwnerHolderMustDiffer: Beneficiary \(owner\) and holder must be different/, + ); + expect(getErrorMessage(ownerHolderError)).toMatch(/OwnerHolderMustDiffer:/); + }); + + it('does not return null when reason is null', () => { + expect(getErrorMessage(ownerHolderError)).not.toBe('null'); + expect(getErrorMessage(ownerHolderError)).toBeTruthy(); + }); + + it('decodes OwnerHolderMustDiffer from revert data when name is missing', () => { + // Typical gas-estimation shape: undecoded shortMessage + selector only + const gasEstimateError = Object.assign(new Error('execution reverted'), { + code: 'CALL_EXCEPTION', + reason: null, + data: '0x7e288225', + shortMessage: 'execution reverted (unknown custom error)', + }); + + expect(extractContractRevertLabel(gasEstimateError)).toBe('OwnerHolderMustDiffer'); + expect(getErrorMessage(gasEstimateError)).toMatch( + /OwnerHolderMustDiffer: Beneficiary \(owner\) and holder must be different/, + ); + expect(getErrorMessage(gasEstimateError)).not.toMatch(/unknown custom error/i); + }); + + it('rewrites SDK pre-check wrappers', () => { + const wrapped = new Error( + 'Pre-check for accept failed: OwnerHolderMustDiffer: Beneficiary (owner) and holder must be different wallets before accept/reject can run. Remint with different beneficiary and holder addresses.', + ); + expect(getErrorMessage(wrapped)).toMatch(/OwnerHolderMustDiffer:/); + }); +});