{"_id":"@credorelabs/credore-smart-contracts","_rev":"4-217d73d65bfeaf0c63e70532e129bf74","name":"@credorelabs/credore-smart-contracts","dist-tags":{"latest":"1.0.4"},"versions":{"1.0.0":{"name":"@credorelabs/credore-smart-contracts","version":"1.0.0","license":"UNLICENSED","_id":"@credorelabs/credore-smart-contracts@1.0.0","maintainers":[{"name":"lmahanand","email":"lingraj@credore.xyz"}],"homepage":"https://github.com/credorelabs/credore-smart-contracts#readme","bugs":{"url":"https://github.com/credorelabs/credore-smart-contracts/issues"},"dist":{"shasum":"beb287200239bc6f287eaa695973304fdab8305e","tarball":"https://registry.npmjs.org/@credorelabs/credore-smart-contracts/-/credore-smart-contracts-1.0.0.tgz","fileCount":6,"integrity":"sha512-lwP3dKdOFntEvuUB4ifLTSDDpnpFOP9qm8vI3AFYA3f2wCOPzFXUWwWWiXWhslZHh7Km7IteF8QQiVp/Ikc61g==","signatures":[{"sig":"MEUCIQDO6H/9IguRfGSmZhsvCruE2T0fdbRBu66s1ZCJ+LOe2gIgfeDkhPGY/8J0oRYloNmSWFILZPOUF08KVKwY+4jzieM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":8508},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","gitHead":"18129ea833155d64a0f4b79378ee686089f54aee","scripts":{"build":"yarn compile && yarn build:ts","compile":"hardhat build","build:ts":"tsc -p tsconfig.build.json","prepublish":"yarn build"},"_npmUser":{"name":"lmahanand","email":"lingraj@credore.xyz"},"repository":{"url":"git+https://github.com/credorelabs/credore-smart-contracts.git","type":"git"},"_npmVersion":"10.9.4","description":"Smart contracts for title documents","directories":{},"_nodeVersion":"22.22.0","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"chai":"^5.0.0","mocha":"^11.7.5","ethers":"^6.16.0","hardhat":"^3.1.12","ts-node":"^10.9.2","typechain":"^8.3.2","typescript":"^5.9.3","@types/chai":"^5.2.3","@types/node":"^25.5.0","@types/mocha":"^10.0.10","@typechain/ethers-v6":"^0.5.1","@nomicfoundation/hardhat-mocha":"^3.0.13","@nomicfoundation/ignition-core":"^3.0.9","@nomicfoundation/hardhat-ethers":"^4.0.6","@nomicfoundation/hardhat-verify":"^3.0.12","@nomicfoundation/hardhat-ignition":"^3.0.9","@nomicfoundation/hardhat-keystore":"^3.0.5","@nomicfoundation/hardhat-typechain":"^3.0.4","@nomicfoundation/hardhat-ignition-ethers":"^3.0.9","@nomicfoundation/hardhat-network-helpers":"^3.0.4","@nomicfoundation/hardhat-ethers-chai-matchers":"^3.0.3","@nomicfoundation/hardhat-toolbox-mocha-ethers":"^3.0.0"},"_npmOperationalInternal":{"tmp":"tmp/credore-smart-contracts_1.0.0_1773823462490_0.29294902043823745","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@credorelabs/credore-smart-contracts","version":"1.0.1","license":"UNLICENSED","_id":"@credorelabs/credore-smart-contracts@1.0.1","maintainers":[{"name":"lmahanand","email":"lingraj@credore.xyz"}],"homepage":"https://github.com/credorelabs/credore-smart-contracts#readme","bugs":{"url":"https://github.com/credorelabs/credore-smart-contracts/issues"},"dist":{"shasum":"9f33d79642b6c37e696034f7e0030998975afc7b","tarball":"https://registry.npmjs.org/@credorelabs/credore-smart-contracts/-/credore-smart-contracts-1.0.1.tgz","fileCount":102,"integrity":"sha512-7MM8DX2+Qs4HBcZeSNCWcWZOTbDn1Kj4ZYM3haY7hiIqFAy+v4s/Hk3XouSXdDcVbVjCzUz3H3G2A/UOpomoFA==","signatures":[{"sig":"MEUCIQDDIwL2fh8nSuRx4P/EJ5bCZlVIgEnpyEjzFykrQ/hFFgIgZOFvp8IQDeZPCmQqaE3nhkTuD8hB7HQYm4CdFrrid8w=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1398392},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","gitHead":"e6a123656ef38eec9b079cd673c8ed6637c083e4","scripts":{"lint":"eslint .","node":"hardhat node","test":"hardhat test","build":"yarn compile && yarn build:ts","compile":"hardhat build","build:ts":"tsc -p tsconfig.build.json","lint:fix":"eslint . --fix","lint:src":"eslint src/","scenario":"hardhat run scripts/titleflowv2e2eScenario.ts --network localhost","test:e2e":"hardhat run scripts/e2eTest.ts --network localhost","lint:test":"eslint test/","prepublish":"yarn build","deploy:local":"hardhat run scripts/localDeploy.ts --network localhost"},"_npmUser":{"name":"lmahanand","email":"lingraj@credore.xyz"},"repository":{"url":"git+https://github.com/credorelabs/credore-smart-contracts.git","type":"git"},"_npmVersion":"10.9.4","description":"Smart contracts for title documents","directories":{},"_nodeVersion":"22.22.0","dependencies":{"@openzeppelin/contracts":"5.6.1","@tradetrust-tt/token-registry":"^5.5.1","@openzeppelin/contracts-upgradeable":"5.6.1"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"chai":"^5.0.0","mocha":"^11.7.5","eslint":"^10.0.3","ethers":"^6.16.0","globals":"^17.4.0","hardhat":"^3.1.12","ts-node":"^10.9.2","typechain":"^8.3.2","@eslint/js":"^10.0.1","typescript":"^5.9.3","@types/chai":"^5.2.3","@types/node":"^25.5.0","@types/mocha":"^10.0.10","typescript-eslint":"^8.57.1","@typechain/ethers-v6":"^0.5.1","eslint-plugin-chai-friendly":"^1.1.1","@nomicfoundation/hardhat-mocha":"^3.0.13","@nomicfoundation/ignition-core":"^3.0.9","@nomicfoundation/hardhat-ethers":"^4.0.6","@nomicfoundation/hardhat-verify":"^3.0.12","@nomicfoundation/hardhat-ignition":"^3.0.9","@nomicfoundation/hardhat-keystore":"^3.0.5","@nomicfoundation/hardhat-typechain":"^3.0.4","@nomicfoundation/hardhat-ignition-ethers":"^3.0.9","@nomicfoundation/hardhat-network-helpers":"^3.0.4","@nomicfoundation/hardhat-ethers-chai-matchers":"^3.0.3","@nomicfoundation/hardhat-toolbox-mocha-ethers":"^3.0.0"},"_npmOperationalInternal":{"tmp":"tmp/credore-smart-contracts_1.0.1_1773936846553_0.5619541830900916","host":"s3://npm-registry-packages-npm-production"}},"1.0.3":{"name":"@credorelabs/credore-smart-contracts","version":"1.0.3","license":"UNLICENSED","_id":"@credorelabs/credore-smart-contracts@1.0.3","maintainers":[{"name":"lmahanand","email":"lingraj@credore.xyz"}],"homepage":"https://github.com/credorelabs/credore-smart-contracts#readme","bugs":{"url":"https://github.com/credorelabs/credore-smart-contracts/issues"},"dist":{"shasum":"2cb003e40ddd67089ef902c57d9c02748cddbad7","tarball":"https://registry.npmjs.org/@credorelabs/credore-smart-contracts/-/credore-smart-contracts-1.0.3.tgz","fileCount":245,"integrity":"sha512-Dt3AgsQ2Z8zO2Rw36kQwXA1JOsjTPDxBirjmpmt9/4FGyIt/vbhatF08j6dvMvZqhGfv5OBHXX6e/gmfOHzKxQ==","signatures":[{"sig":"MEUCIBWFiDEvC8B21ZIa1vVFb88BlbwvI74/Cz6zX78A+qBXAiEA8jYxwyCjOWURkuuHo5T/ZCVFkUEF0nGPxnoL5jXUJBY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":4424871},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","gitHead":"9931b283c3ac3cd984bb429c627e733627037d68","scripts":{"lint":"eslint .","node":"hardhat node","size":"hardhat build && hardhat run scripts/check-sizes.ts --no-compile","test":"hardhat test","build":"yarn compile && yarn build:ts","compile":"hardhat build","build:ts":"tsc -p tsconfig.build.json","lint:fix":"eslint . --fix","lint:src":"eslint src/","lint:test":"eslint test/","prepublish":"yarn build","scenario:ef":"hardhat run scripts/Exportfactoring.ts --network localhost","deploy:local":"hardhat run scripts/deploy.ts --network localhost","publish:public":"yarn npm publish --access public","scenario:external-endorse":"hardhat run scripts/Exportfactoringendorse.ts --network localhost","scenario:external-surrender":"hardhat run scripts/ExternalEBLSurrender.ts --network localhost"},"_npmUser":{"name":"lmahanand","email":"lingraj@credore.xyz"},"repository":{"url":"git+https://github.com/credorelabs/credore-smart-contracts.git","type":"git"},"_npmVersion":"10.9.4","description":"Smart contracts for title documents : TitleFlow, TitleFlowFactory, TitleFlowRegistry, TitleFlowRelayFacet","directories":{},"resolutions":{"serialize-javascript":"^7.0.4"},"_nodeVersion":"22.22.0","dependencies":{"@openzeppelin/contracts":"5.6.1","@tradetrust-tt/token-registry":"^5.5.1","@openzeppelin/contracts-upgradeable":"5.6.1"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"chai":"^5.0.0","mocha":"^11.7.5","dotenv":"^17.3.1","eslint":"^10.0.3","ethers":"^6.16.0","globals":"^17.4.0","hardhat":"^3.1.12","ts-node":"^10.9.2","typechain":"^8.3.2","@eslint/js":"^10.0.1","typescript":"^5.9.3","@types/chai":"^5.2.3","@types/node":"^25.5.0","@types/mocha":"^10.0.10","typescript-eslint":"^8.57.1","@typechain/ethers-v6":"^0.5.1","eslint-plugin-chai-friendly":"^1.1.1","@nomicfoundation/hardhat-mocha":"^3.0.13","@nomicfoundation/ignition-core":"^3.0.9","@nomicfoundation/hardhat-ethers":"^4.0.6","@nomicfoundation/hardhat-verify":"^3.0.12","@nomicfoundation/hardhat-ignition":"^3.0.9","@nomicfoundation/hardhat-keystore":"^3.0.5","@nomicfoundation/hardhat-typechain":"^3.0.4","@nomicfoundation/hardhat-ignition-ethers":"^3.0.9","@nomicfoundation/hardhat-network-helpers":"^3.0.4","@nomicfoundation/hardhat-ethers-chai-matchers":"^3.0.3","@nomicfoundation/hardhat-toolbox-mocha-ethers":"^3.0.0"},"_npmOperationalInternal":{"tmp":"tmp/credore-smart-contracts_1.0.3_1774174338566_0.12445717471501205","host":"s3://npm-registry-packages-npm-production"}},"1.0.4":{"name":"@credorelabs/credore-smart-contracts","version":"1.0.4","license":"UNLICENSED","description":"Smart contracts for title documents : TitleFlow, TitleFlowFactory, TitleFlowRegistry, TitleFlowRelayFacet","type":"module","main":"dist/index.js","types":"dist/index.d.ts","publishConfig":{"registry":"https://registry.npmjs.org","access":"public"},"scripts":{"compile":"hardhat compile","build:ts":"tsc -p tsconfig.build.json","build":"yarn compile && yarn build:ts","prepublishOnly":"yarn build","test":"hardhat test","lint":"eslint .","lint:fix":"eslint . --fix","node":"hardhat node","deploy:local":"hardhat run scripts/deploy.ts --network localhost","scenario:ef":"hardhat run scripts/Exportfactoring.ts --network localhost","scenario:external-endorse":"hardhat run scripts/Exportfactoringendorse.ts --network localhost","scenario:external-surrender":"hardhat run scripts/ExternalEBLSurrender.ts --network localhost","publish:public":"npm publish --access public","size":"hardhat compile && hardhat run scripts/check-sizes.ts --no-compile"},"repository":{"type":"git","url":"git+https://github.com/credorelabs/credore-smart-contracts.git"},"devDependencies":{"@eslint/js":"^10.0.1","@nomicfoundation/hardhat-ethers":"^4.0.6","@nomicfoundation/hardhat-ethers-chai-matchers":"^3.0.3","@nomicfoundation/hardhat-ignition":"^3.0.9","@nomicfoundation/hardhat-ignition-ethers":"^3.0.9","@nomicfoundation/hardhat-keystore":"^3.0.5","@nomicfoundation/hardhat-mocha":"^3.0.13","@nomicfoundation/hardhat-network-helpers":"^3.0.4","@nomicfoundation/hardhat-toolbox-mocha-ethers":"^3.0.0","@nomicfoundation/hardhat-typechain":"^3.0.4","@nomicfoundation/hardhat-verify":"^3.0.12","@nomicfoundation/ignition-core":"^3.0.9","@typechain/ethers-v6":"^0.5.1","@types/chai":"^5.2.3","@types/mocha":"^10.0.10","@types/node":"^25.5.0","chai":"^5.0.0","dotenv":"^17.3.1","eslint":"^10.0.3","eslint-plugin-chai-friendly":"^1.1.1","ethers":"^6.16.0","globals":"^17.4.0","hardhat":"^3.1.12","mocha":"^11.7.5","ts-node":"^10.9.2","typechain":"^8.3.2","typescript":"^5.9.3","typescript-eslint":"^8.57.1"},"dependencies":{"@openzeppelin/contracts":"5.6.1","@openzeppelin/contracts-upgradeable":"5.6.1","@tradetrust-tt/token-registry":"^5.5.1"},"resolutions":{"serialize-javascript":"^7.0.4"},"_id":"@credorelabs/credore-smart-contracts@1.0.4","gitHead":"8e75ddd6e13db3abaafc952d6f66133b5988a7aa","bugs":{"url":"https://github.com/credorelabs/credore-smart-contracts/issues"},"homepage":"https://github.com/credorelabs/credore-smart-contracts#readme","_nodeVersion":"22.22.0","_npmVersion":"10.9.4","dist":{"integrity":"sha512-OrH8to9ulWUgcIBwVxpiPqckJso7m7BLECu1HPsx7Ih0W9jjgQsVtZtnk6CfRPqd31Emc6fq96tsIMdsMWV8SA==","shasum":"a526ec5d3beca8727904213d522aa67dd7af1ae2","tarball":"https://registry.npmjs.org/@credorelabs/credore-smart-contracts/-/credore-smart-contracts-1.0.4.tgz","fileCount":298,"unpackedSize":5637715,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIBRSX7egCFIxDywE/vX+MHEiPN/CPaW2YcUI/Zt9HA/YAiEAmEqcgJuEMwog2S2eEQB58XDYoQf4oD4FKo+7qOfuJf0="}]},"_npmUser":{"name":"lmahanand","email":"lingraj@credore.xyz"},"directories":{},"maintainers":[{"name":"lmahanand","email":"lingraj@credore.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/credore-smart-contracts_1.0.4_1775130913113_0.5228902495519285"},"_hasShrinkwrap":false}},"time":{"created":"2026-03-18T08:44:22.347Z","modified":"2026-04-02T11:55:13.496Z","1.0.0":"2026-03-18T08:44:22.641Z","1.0.1":"2026-03-19T16:14:06.713Z","1.0.3":"2026-03-22T10:12:18.755Z","1.0.4":"2026-04-02T11:55:13.357Z"},"bugs":{"url":"https://github.com/credorelabs/credore-smart-contracts/issues"},"license":"UNLICENSED","homepage":"https://github.com/credorelabs/credore-smart-contracts#readme","repository":{"type":"git","url":"git+https://github.com/credorelabs/credore-smart-contracts.git"},"description":"Smart contracts for title documents : TitleFlow, TitleFlowFactory, TitleFlowRegistry, TitleFlowRelayFacet","maintainers":[{"name":"lmahanand","email":"lingraj@credore.xyz"}],"readme":"# @credorelabs/credore-smart-contracts\n\n> Gasless EIP-712 smart contract library for managing electronic Bills of Lading (eBL) title escrow operations on TradeTrust-compliant blockchain infrastructure.\n\n[![npm version](https://img.shields.io/npm/v/@credorelabs/credore-smart-contracts)](https://www.npmjs.com/package/@credorelabs/credore-smart-contracts)\n[![License: UNLICENSED](https://img.shields.io/badge/license-UNLICENSED-red.svg)](./LICENSE)\n\n---\n\n## Table of Contents\n\n- [Overview](#overview)\n- [Architecture](#architecture)\n- [Prerequisites](#prerequisites)\n- [Installation](#installation)\n- [Key Hierarchy](#key-hierarchy)\n- [Quick Start](#quick-start)\n- [End-to-End Integration Guide](#end-to-end-integration-guide)\n  - [Phase 1 — Deploy the System](#phase-1--deploy-the-system)\n  - [Phase 2 — Create a TitleFlow Instance](#phase-2--create-a-titleflow-instance)\n  - [Phase 3 — Mint Document Token](#phase-3--mint-document-token)\n  - [Phase 4 — Register Escrow](#phase-4--register-escrow)\n  - [Phase 5 — Nominate](#phase-5--nominate)\n  - [Phase 6 — Transfer Beneficiary](#phase-6--transfer-beneficiary)\n  - [Phase 7 — Reject Transfer of Beneficiary](#phase-7--reject-transfer-of-beneficiary)\n  - [Phase 8 — Transfer Holder](#phase-8--transfer-holder)\n  - [Phase 9 — Reject Transfer of Holder](#phase-9--reject-transfer-of-holder)\n  - [Phase 10 — Transfer Owners](#phase-10--transfer-owners)\n  - [Phase 11 — Reject Transfer of Owners](#phase-11--reject-transfer-of-owners)\n  - [Phase 12 — Return to Issuer](#phase-12--return-to-issuer)\n  - [Phase 13 — Shred](#phase-13--shred)\n  - [Phase 14 — Lock for External Transfer (PINT Outbound)](#phase-14--lock-for-external-transfer-pint-outbound)\n  - [Phase 15 — Restore from External (PINT Inbound)](#phase-15--restore-from-external-pint-inbound)\n  - [Phase 16 — Endorse for External Transfer](#phase-16--endorse-for-external-transfer)\n  - [Phase 17 — Restore Endorse (PINT Beneficiary Restore)](#phase-17--restore-endorse-pint-beneficiary-restore)\n  - [Phase 18 — Surrender for External Transfer](#phase-18--surrender-for-external-transfer)\n- [Key Management](#key-management)\n  - [Planned Owner Rotation](#planned-owner-rotation)\n  - [Planned Attorney Rotation](#planned-attorney-rotation)\n  - [Emergency Recovery (Guardian)](#emergency-recovery-guardian)\n  - [Add / Remove Backup Attorney](#add--remove-backup-attorney)\n  - [Relayer Management](#relayer-management)\n- [Pause Mechanism](#pause-mechanism)\n- [API Reference](#api-reference)\n  - [Contracts and Types](#contracts-and-types)\n  - [Role Constants](#role-constants)\n  - [Enums](#enums)\n  - [EIP-712 Type Definitions](#eip-712-type-definitions)\n  - [TypeScript Interfaces](#typescript-interfaces)\n  - [Helpers](#helpers)\n  - [Deployments](#deployments)\n- [Local Development](#local-development)\n- [Deployment](#deployment)\n- [Error Reference](#error-reference)\n\n---\n\n## Overview\n\nThis library provides smart contract bindings, EIP-712 signing utilities, and TypeScript types for the Credore title escrow system. It sits on top of the TradeTrust token registry and adds a **gasless operation layer** - the document owner never pays gas or touches the blockchain directly. All on-chain operations are submitted by an attorney or relayer on the owner's behalf, authorised by an off-chain EIP-712 signature.\n\n```\nOwner (HSM)          Attorney (Backend)        Blockchain\n-------------        ------------------        ----------\nSign EIP-712    -->  Verify + Submit tx   -->  TitleFlow\n(offline)            (pays gas)                (on-chain)\n```\n\n---\n\n## Architecture\n\n```\n@tradetrust-tt/token-registry          @credorelabs/credore-smart-contracts\n-----------------------------          ------------------------------------\nTitleEscrowFactory                     TitleFlowFactory\nTradeTrustToken (ERC-721 registry)       └── TitleFlow (clone per owner)\nTitleEscrow (per document)                     ├── registerEscrow()\n  ├── beneficiary  <── TitleFlow               ├── nominate()\n  └── holder       <── TitleFlow               ├── transferBeneficiary()\n                                               ├── rejectTransferBeneficiary()\n                                               ├── transferHolder()\n                                               ├── rejectTransferHolder()\n                                               ├── transferOwners()\n                                               ├── rejectTransferOwners()\n                                               ├── returnToIssuer()\n                                               ├── shred()\n                                               ├── lockForExternalTransfer()\n                                               ├── restoreFromExternal()\n                                               ├── endorseForExternalTransfer()\n                                               ├── restoreEndorse()\n                                               └── surrenderForExternalTransfer()\n\nTitleFlowRegistry                      Global registry of all TitleFlow instances\n```\n\n**TitleFlow is both the on-chain beneficiary and holder of every TitleEscrow it manages.** The real-world owner controls it exclusively via EIP-712 signatures from an HSM.\n\n---\n\n## Prerequisites\n\n| Requirement | Version |\n|---|---|\n| Node.js | >= 22.x |\n| ethers.js | ^6.x |\n| @tradetrust-tt/token-registry | ^5.5.1 |\n\n---\n\n## Installation\n\n```bash\nnpm install @credorelabs/credore-smart-contracts @tradetrust-tt/token-registry ethers\n# or\nyarn add @credorelabs/credore-smart-contracts @tradetrust-tt/token-registry ethers\n```\n\n---\n\n## Key Hierarchy\n\nEvery TitleFlow contract is initialised with five key holders.\n\n```\nGUARDIAN (cold HSM  CRO / Legal)\n├── Emergency rotate owner key          guardianRecoverOwner()\n├── Emergency rotate attorney key       guardianRotateAttorney()\n├── Emergency rotate relayer key        guardianRotateRelayer()\n├── Add / remove backup attorneys       addBackupAttorney() / removeAttorney()\n└── Unpause the contract                unpause()  (DEFAULT_ADMIN_ROLE)\n\nOWNER (cold HSM  document owner)\n└── Signs all EIP-712 messages offline\n    (never submits transactions, never pays gas)\n\nATTORNEY  PRIMARY (hot/warm HSM  attorney backend)\n├── Submits all daily transactions on-chain    (ATTORNEY_ADMIN_ROLE)\n├── Pays gas for all operations\n└── Can pause the contract\n\nATTORNEY  BACKUP (warm HSM  standby)\n└── Identical permissions to primary — activates immediately if primary fails\n\nRELAYER (automated service — PINT interop)\n└── Submits restoreFromExternal() and restoreEndorse()   (RELAYER_ROLE)\n```\n\n---\n\n## Quick Start\n\n```typescript\nimport {\n  TitleFlow__factory,\n  TitleFlowFactory__factory,\n  TitleFlowRegistry__factory,\n  ACTION_TYPES,\n  LIFECYCLE_TYPES,\n  ActionType,\n  Lifecycle,\n  titleFlowDomain,\n  ATTORNEY_ADMIN_ROLE,\n  RELAYER_ROLE,\n  GUARDIAN_ROLE,\n  FACTORY_ADMIN_ROLE,\n} from \"@credorelabs/credore-smart-contracts\";\n\nimport { ethers } from \"ethers\";\n\nconst provider      = new ethers.JsonRpcProvider(\"https://your-rpc-url\");\nconst attorneyWallet = new ethers.Wallet(process.env.ATTORNEY_PRIVATE_KEY!, provider);\nconst ownerWallet    = new ethers.Wallet(process.env.OWNER_PRIVATE_KEY!,   provider);\n\n// Attach to deployed TitleFlow\nconst titleFlow = TitleFlow__factory.connect(TITLE_FLOW_ADDRESS, attorneyWallet);\n\n// Build EIP-712 domain for this TitleFlow\nconst { chainId } = await provider.getNetwork();\nconst domain      = titleFlowDomain(chainId, TITLE_FLOW_ADDRESS);\n```\n\n---\n\n## End-to-End Integration Guide\n\n### Phase 1 — Deploy the System\n\nDeploy all four contracts in order. Use the provided deployment script:\n\n```bash\n# Set env vars first (see .env.example)\nyarn hardhat run scripts/deploy.ts --network sepolia\nyarn hardhat run scripts/deploy.ts --network polygon\nyarn hardhat run scripts/deploy.ts --network mainnet\n```\n\nAddresses are saved to `deployments/<network>.json` and exported from the package:\n\n```typescript\nimport { sepoliaDeployments, polygonDeployments, mainnetDeployments } from \"@credorelabs/credore-smart-contracts\";\n\nconst factoryAddress   = sepoliaDeployments.contracts.TitleFlowFactory;\nconst registryAddress  = sepoliaDeployments.contracts.TitleFlowRegistry;\n```\n\nOr load at runtime:\n\n```typescript\nimport { loadDeployment } from \"@credorelabs/credore-smart-contracts\";\n\nconst dep = await loadDeployment(\"sepolia\");\nconst factoryAddress = dep?.contracts.TitleFlowFactory;\n```\n\n---\n\n### Phase 2 — Create a TitleFlow Instance\n\nEach document owner gets their own dedicated TitleFlow contract deployed as a gas-efficient CREATE2 clone.\n\n```typescript\nimport { TitleFlowFactory__factory } from \"@credorelabs/credore-smart-contracts\";\n\nconst factory = TitleFlowFactory__factory.connect(factoryAddress, attorneyWallet);\n\n// Predict address before deploying (deterministic)\nconst predictedAddress = await factory.predictAddress(\n  primaryAttorneyAddress,\n  backupAttorneyAddress,\n  guardianAddress,\n  ownerAddress,\n  relayerAddress,\n);\nconsole.log(\"Predicted TitleFlow address:\", predictedAddress);\n\n// Deploy the clone (requires FACTORY_ADMIN_ROLE)\nconst tx = await factory.create(\n  primaryAttorneyAddress,\n  backupAttorneyAddress,\n  guardianAddress,\n  ownerAddress,\n  relayerAddress,\n);\nconst receipt = await tx.wait();\n\n// Parse TitleFlowCreated event to get the actual address\nconst event = receipt?.logs\n  .map(l => { try { return factory.interface.parseLog(l as any); } catch { return null; } })\n  .find(e => e?.name === \"TitleFlowCreated\");\n\nconst titleFlowAddress = event?.args.titleFlow;\nconsole.log(\"TitleFlow deployed at:\", titleFlowAddress);\n```\n\n---\n\n### Phase 3 — Mint Document Token\n\nMint the eBL document token on the TradeTrust registry. **Both beneficiary and holder must be set to the TitleFlow address.**\n\n```typescript\nimport { TradeTrustToken__factory } from \"@tradetrust-tt/token-registry/contracts\";\n\nconst registry = TradeTrustToken__factory.connect(registryAddress, attorneyWallet);\n\n// The document hash uniquely identifies this eBL — use as tokenId\nconst tokenId = ethers.keccak256(ethers.toUtf8Bytes(\"BL-2025-001-UNIQUE-REF\"));\n\nawait (await registry.mint(\n  titleFlowAddress,  // beneficiary — TitleFlow, NOT the real owner\n  titleFlowAddress,  // holder      — TitleFlow, NOT the real owner\n  tokenId,\n  ethers.toUtf8Bytes(\"Initial issuance — Credore eBL Platform\"),\n)).wait();\n\n// The TitleEscrow address is the ownerOf the NFT token\nconst titleEscrowAddress = await registry.ownerOf(BigInt(tokenId));\nconsole.log(\"TitleEscrow:\", titleEscrowAddress);\n```\n\n---\n\n### Phase 4 — Register Escrow\n\nAfter minting, the TitleEscrow must be registered with TitleFlow. The **owner signs off-chain**, the **attorney submits on-chain**.\n\n```typescript\nimport { TitleFlow__factory, LIFECYCLE_TYPES, titleFlowDomain } from \"@credorelabs/credore-smart-contracts\";\n\nconst titleFlow    = TitleFlow__factory.connect(titleFlowAddress, attorneyWallet);\nconst domain       = titleFlowDomain(chainId, titleFlowAddress);\nconst ownerAddress = await ownerWallet.getAddress();\n\nconst nonce       = await titleFlow.nonce(titleEscrowAddress, ownerAddress);\nconst exportNonce = await titleFlow.exportNonce(titleEscrowAddress);\nconst deadline    = BigInt(Math.floor(Date.now() / 1000) + 3600);\n\n// Owner signs off-chain (HSM in production)\nconst signature = await ownerWallet.signTypedData(domain, LIFECYCLE_TYPES, {\n  titleEscrow:           titleEscrowAddress,\n  pintRef:               ethers.ZeroHash,\n  nonce,\n  deadline,\n  exportDocumentHash:    ethers.ZeroHash,\n  exportEnvelopeHash:    ethers.ZeroHash,\n  lastBeneficiary:       ethers.ZeroAddress,\n  newHolder:             ethers.ZeroAddress,\n  destinationPlatformId: ethers.ZeroHash,\n  exportNonce,\n});\n\n// Attorney submits on-chain\nawait (await titleFlow.registerEscrow(\n  titleEscrowAddress,\n  nonce,\n  deadline,\n  exportNonce,\n  signature,\n)).wait();\n\nconst lifecycle = await titleFlow.lifecycle(titleEscrowAddress);\nconsole.log(\"Escrow registered — lifecycle:\", lifecycle.toString()); // 1 = ACTIVE\n```\n\n---\n\n### Phase 5 — Nominate\n\nNominate a new beneficiary. Required before `transferBeneficiary` when holder ≠ beneficiary.\n\n```typescript\nimport { ACTION_TYPES, ActionType } from \"@credorelabs/credore-smart-contracts\";\n\nconst nonce    = await titleFlow.nonce(titleEscrowAddress, ownerAddress);\nconst deadline = BigInt(Math.floor(Date.now() / 1000) + 3600);\nconst remarks  = \"Nominating new beneficiary\";\n\n// currentNominee must be non-zero — pass the existing nominee or any non-zero address\nconst currentNominee = await escrow.nominee();\nconst newNominee     = \"0xNEW_NOMINEE_ADDRESS\";\n\n// Owner signs off-chain\nconst signature = await ownerWallet.signTypedData(domain, ACTION_TYPES, {\n  titleEscrow:    titleEscrowAddress,\n  beneficiary:    ethers.ZeroAddress,\n  holder:         ethers.ZeroAddress,\n  nominee:        currentNominee,\n  newBeneficiary: ethers.ZeroAddress,\n  newHolder:      ethers.ZeroAddress,\n  newNominee:     newNominee,\n  remarkHash:     ethers.keccak256(ethers.toUtf8Bytes(remarks)),\n  nonce,\n  action:         ActionType.Nominate,\n  deadline,\n});\n\n// Attorney submits\nawait (await titleFlow.nominate(\n  titleEscrowAddress,\n  currentNominee,\n  newNominee,\n  ethers.toUtf8Bytes(remarks),\n  nonce,\n  deadline,\n  signature,\n)).wait();\n\nconsole.log(\"Nominee set to:\", await escrow.nominee());\n```\n\n---\n\n### Phase 6 — Transfer Beneficiary\n\nTransfer the beneficiary role to the nominated address. `nominate()` must be called first when holder ≠ beneficiary.\n\n```typescript\nconst beneficiary = await escrow.beneficiary();\nconst holder      = await escrow.holder();\nconst nominee     = await escrow.nominee(); // must be non-zero\nconst nonce       = await titleFlow.nonce(titleEscrowAddress, ownerAddress);\nconst deadline    = BigInt(Math.floor(Date.now() / 1000) + 3600);\nconst remarks     = \"Transferring beneficiary rights\";\n\nconst signature = await ownerWallet.signTypedData(domain, ACTION_TYPES, {\n  titleEscrow:    titleEscrowAddress,\n  beneficiary,\n  holder,\n  nominee,\n  newBeneficiary: nominee, // transfer to current nominee\n  newHolder:      ethers.ZeroAddress,\n  newNominee:     ethers.ZeroAddress,\n  remarkHash:     ethers.keccak256(ethers.toUtf8Bytes(remarks)),\n  nonce,\n  action:         ActionType.BeneficiaryTransfer,\n  deadline,\n});\n\nawait (await titleFlow.transferBeneficiary(\n  titleEscrowAddress,\n  holder,\n  beneficiary,\n  nominee,        // newBeneficiary\n  nominee,        // nominee\n  ethers.toUtf8Bytes(remarks),\n  nonce,\n  deadline,\n  signature,\n)).wait();\n\nconsole.log(\"Beneficiary transferred to:\", await escrow.beneficiary());\n```\n\n---\n\n### Phase 7 — Reject Transfer of Beneficiary\n\nRevert the beneficiary back to the previous address. Only valid when holder ≠ beneficiary — use `rejectTransferOwners` if they are the same party.\n\n```typescript\nconst nonce    = await titleFlow.nonce(titleEscrowAddress, ownerAddress);\nconst deadline = BigInt(Math.floor(Date.now() / 1000) + 3600);\nconst remarks  = \"Rejecting beneficiary transfer — incorrect party\";\n\nconst signature = await ownerWallet.signTypedData(domain, ACTION_TYPES, {\n  titleEscrow:    titleEscrowAddress,\n  beneficiary:    ethers.ZeroAddress,\n  holder:         ethers.ZeroAddress,\n  nominee:        ethers.ZeroAddress,\n  newBeneficiary: ethers.ZeroAddress,\n  newHolder:      ethers.ZeroAddress,\n  newNominee:     ethers.ZeroAddress,\n  remarkHash:     ethers.keccak256(ethers.toUtf8Bytes(remarks)),\n  nonce,\n  action:         ActionType.RejectBeneficiary,\n  deadline,\n});\n\nawait (await titleFlow.rejectTransferBeneficiary(\n  titleEscrowAddress,\n  ethers.toUtf8Bytes(remarks),\n  nonce,\n  deadline,\n  signature,\n)).wait();\n\nconsole.log(\"Beneficiary reverted to:\", await escrow.beneficiary());\n```\n\n---\n\n### Phase 8 — Transfer Holder\n\nTransfer the holder role independently of the beneficiary.\n\n```typescript\nconst beneficiary    = await escrow.beneficiary();\nconst holder         = await escrow.holder();\nconst newHolder      = \"0xNEW_HOLDER_ADDRESS\";\nconst nonce          = await titleFlow.nonce(titleEscrowAddress, ownerAddress);\nconst deadline       = BigInt(Math.floor(Date.now() / 1000) + 3600);\nconst remarks        = \"Transferring holder rights\";\n\nconst signature = await ownerWallet.signTypedData(domain, ACTION_TYPES, {\n  titleEscrow:    titleEscrowAddress,\n  beneficiary,\n  holder,\n  nominee:        ethers.ZeroAddress,\n  newBeneficiary: ethers.ZeroAddress,\n  newHolder,\n  newNominee:     ethers.ZeroAddress,\n  remarkHash:     ethers.keccak256(ethers.toUtf8Bytes(remarks)),\n  nonce,\n  action:         ActionType.HolderTransfer,\n  deadline,\n});\n\nawait (await titleFlow.transferHolder(\n  titleEscrowAddress,\n  holder,\n  beneficiary,\n  newHolder,\n  ethers.toUtf8Bytes(remarks),\n  nonce,\n  deadline,\n  signature,\n)).wait();\n\nconsole.log(\"Holder transferred to:\", await escrow.holder());\n```\n\n---\n\n### Phase 9 — Reject Transfer of Holder\n\nRevert the holder to the previous address. Only valid when holder ≠ beneficiary.\n\n```typescript\nconst nonce    = await titleFlow.nonce(titleEscrowAddress, ownerAddress);\nconst deadline = BigInt(Math.floor(Date.now() / 1000) + 3600);\nconst remarks  = \"Rejecting holder transfer — wrong party nominated\";\n\nconst signature = await ownerWallet.signTypedData(domain, ACTION_TYPES, {\n  titleEscrow:    titleEscrowAddress,\n  beneficiary:    ethers.ZeroAddress,\n  holder:         ethers.ZeroAddress,\n  nominee:        ethers.ZeroAddress,\n  newBeneficiary: ethers.ZeroAddress,\n  newHolder:      ethers.ZeroAddress,\n  newNominee:     ethers.ZeroAddress,\n  remarkHash:     ethers.keccak256(ethers.toUtf8Bytes(remarks)),\n  nonce,\n  action:         ActionType.RejectHolder,\n  deadline,\n});\n\nawait (await titleFlow.rejectTransferHolder(\n  titleEscrowAddress,\n  ethers.toUtf8Bytes(remarks),\n  nonce,\n  deadline,\n  signature,\n)).wait();\n\nconsole.log(\"Holder reverted to:\", await escrow.holder());\n```\n\n---\n\n### Phase 10 — Transfer Owners\n\nTransfer both beneficiary and holder in a single transaction.\n\n```typescript\nconst newOwner = \"0xNEW_OWNER_ADDRESS\";\nconst nonce    = await titleFlow.nonce(titleEscrowAddress, ownerAddress);\nconst deadline = BigInt(Math.floor(Date.now() / 1000) + 3600);\nconst remarks  = \"Full ownership transfer\";\n\nconst signature = await ownerWallet.signTypedData(domain, ACTION_TYPES, {\n  titleEscrow:    titleEscrowAddress,\n  beneficiary:    ethers.ZeroAddress,\n  holder:         ethers.ZeroAddress,\n  nominee:        ethers.ZeroAddress,\n  newBeneficiary: ethers.ZeroAddress,\n  newHolder:      newOwner,\n  newNominee:     newOwner,\n  remarkHash:     ethers.keccak256(ethers.toUtf8Bytes(remarks)),\n  nonce,\n  action:         ActionType.OwnersTransfer,\n  deadline,\n});\n\nawait (await titleFlow.transferOwners(\n  titleEscrowAddress,\n  newOwner,   // nominee (new beneficiary)\n  newOwner,   // newHolder\n  ethers.toUtf8Bytes(remarks),\n  nonce,\n  deadline,\n  signature,\n)).wait();\n\nconsole.log(\"beneficiary:\", await escrow.beneficiary()); // newOwner\nconsole.log(\"holder:     \", await escrow.holder());      // newOwner\n```\n\n---\n\n### Phase 11 — Reject Transfer of Owners\n\nRevert both beneficiary and holder simultaneously. Use when the same party holds both roles.\n\n```typescript\nconst nonce    = await titleFlow.nonce(titleEscrowAddress, ownerAddress);\nconst deadline = BigInt(Math.floor(Date.now() / 1000) + 3600);\nconst remarks  = \"Rejecting full ownership transfer — error in counterparty\";\n\nconst signature = await ownerWallet.signTypedData(domain, ACTION_TYPES, {\n  titleEscrow:    titleEscrowAddress,\n  beneficiary:    ethers.ZeroAddress,\n  holder:         ethers.ZeroAddress,\n  nominee:        ethers.ZeroAddress,\n  newBeneficiary: ethers.ZeroAddress,\n  newHolder:      ethers.ZeroAddress,\n  newNominee:     ethers.ZeroAddress,\n  remarkHash:     ethers.keccak256(ethers.toUtf8Bytes(remarks)),\n  nonce,\n  action:         ActionType.RejectOwners,\n  deadline,\n});\n\nawait (await titleFlow.rejectTransferOwners(\n  titleEscrowAddress,\n  ethers.toUtf8Bytes(remarks),\n  nonce,\n  deadline,\n  signature,\n)).wait();\n\nconsole.log(\"beneficiary reverted to:\", await escrow.beneficiary());\nconsole.log(\"holder reverted to:     \", await escrow.holder());\n```\n\n---\n\n### Phase 12 — Return to Issuer\n\nReturn the eBL to the issuing Token Registry. After returning, the registry admin burns the token.\n\n```typescript\nconst nonce    = await titleFlow.nonce(titleEscrowAddress, ownerAddress);\nconst deadline = BigInt(Math.floor(Date.now() / 1000) + 3600);\nconst remarks  = \"Returning eBL to issuer — transaction complete\";\n\nconst signature = await ownerWallet.signTypedData(domain, ACTION_TYPES, {\n  titleEscrow:    titleEscrowAddress,\n  beneficiary:    ethers.ZeroAddress,\n  holder:         ethers.ZeroAddress,\n  nominee:        ethers.ZeroAddress,\n  newBeneficiary: ethers.ZeroAddress,\n  newHolder:      ethers.ZeroAddress,\n  newNominee:     ethers.ZeroAddress,\n  remarkHash:     ethers.keccak256(ethers.toUtf8Bytes(remarks)),\n  nonce,\n  action:         ActionType.ReturnToIssuer,\n  deadline,\n});\n\nawait (await titleFlow.returnToIssuer(\n  titleEscrowAddress,\n  ethers.toUtf8Bytes(remarks),\n  nonce,\n  deadline,\n  signature,\n)).wait();\n\nconsole.log(\"eBL returned to issuer\");\n\n// Registry admin burns the document (requires ACCEPTER_ROLE)\nconst registry = TradeTrustToken__factory.connect(registryAddress, adminWallet);\nawait (await registry.burn(\n  BigInt(tokenId),\n  ethers.toUtf8Bytes(\"Accepting returned eBL\"),\n)).wait();\n```\n\n---\n\n### Phase 13 — Shred\n\nPermanently destroy the eBL document.\n\n```typescript\nconst nonce    = await titleFlow.nonce(titleEscrowAddress, ownerAddress);\nconst deadline = BigInt(Math.floor(Date.now() / 1000) + 3600);\nconst remarks  = \"Shredding eBL — goods delivered\";\n\nconst signature = await ownerWallet.signTypedData(domain, ACTION_TYPES, {\n  titleEscrow:    titleEscrowAddress,\n  beneficiary:    ethers.ZeroAddress,\n  holder:         ethers.ZeroAddress,\n  nominee:        ethers.ZeroAddress,\n  newBeneficiary: ethers.ZeroAddress,\n  newHolder:      ethers.ZeroAddress,\n  newNominee:     ethers.ZeroAddress,\n  remarkHash:     ethers.keccak256(ethers.toUtf8Bytes(remarks)),\n  nonce,\n  action:         ActionType.Shred,\n  deadline,\n});\n\nawait (await titleFlow.shred(\n  titleEscrowAddress,\n  ethers.toUtf8Bytes(remarks),\n  nonce,\n  deadline,\n  signature,\n)).wait();\n\nconsole.log(\"eBL shredded — lifecycle:\", await titleFlow.lifecycle(titleEscrowAddress)); // 4 = DESTROYED\n```\n\n---\n\n### Phase 14 — Lock for External Transfer (PINT Outbound)\n\nLock the eBL for transfer to an external PINT platform. Transfers the holder to the relayer. Lifecycle becomes `LOCKED_EXTERNAL` — all relay operations are blocked until restored.\n\nThe EIP-712 struct uses `newHolder = TitleFlow.relayer` (the stored relayer address). Sign with `newHolder = relayerAddress` to match what the contract reconstructs.\n\n```typescript\nconst pintRef             = ethers.keccak256(ethers.toUtf8Bytes(\"PINT-REF-2025-001\"));\nconst exportDocumentHash  = ethers.keccak256(ethers.toUtf8Bytes(\"EXPORT-DOC-HASH\"));\nconst exportEnvelopeHash  = ethers.keccak256(ethers.toUtf8Bytes(\"EXPORT-ENV-HASH\"));\nconst destinationPlatform = ethers.keccak256(ethers.toUtf8Bytes(\"EXTERNAL-PLATFORM-ID\"));\nconst relayerAddress      = await titleFlow.relayer(); // stored relayer on TitleFlow\n\nconst lastBeneficiary = await escrow.beneficiary();\nconst nonce           = await titleFlow.nonce(titleEscrowAddress, ownerAddress);\nconst exportNonce     = await titleFlow.exportNonce(titleEscrowAddress);\nconst deadline        = BigInt(Math.floor(Date.now() / 1000) + 3600);\n\n// Owner signs off-chain — newHolder MUST equal TitleFlow.relayer\nconst signature = await ownerWallet.signTypedData(domain, LIFECYCLE_TYPES, {\n  titleEscrow:           titleEscrowAddress,\n  pintRef,\n  nonce,\n  deadline,\n  exportDocumentHash,\n  exportEnvelopeHash,\n  lastBeneficiary,\n  newHolder:             relayerAddress,  // must equal TitleFlow.relayer on-chain\n  destinationPlatformId: destinationPlatform,\n  exportNonce,\n});\n\n// Attorney submits\nawait (await titleFlow.lockForExternalTransfer(\n  titleEscrowAddress,\n  pintRef,\n  nonce,\n  deadline,\n  exportDocumentHash,\n  exportEnvelopeHash,\n  lastBeneficiary,\n  destinationPlatform,\n  exportNonce,\n  signature,\n)).wait();\n\n// After lock: holder = relayer, beneficiary = TitleFlow (unchanged)\nconsole.log(\"Locked — holder:\", await escrow.holder()); // relayerAddress\nconsole.log(\"lifecycle:\", await titleFlow.lifecycle(titleEscrowAddress)); // 2 = LOCKED_EXTERNAL\n```\n\n---\n\n### Phase 15 — Restore from External (PINT Inbound)\n\nRestore an eBL from a PINT inbound transfer. Because `restoreFromExternal` calls `transferOwners` via `_forwardCall`, TitleFlow must be the holder at restore time. The relayer must first return the holder role directly on TitleEscrow.\n\n```typescript\nimport { TitleEscrow__factory } from \"@tradetrust-tt/token-registry/contracts\";\n\n// Step A: Relayer returns holder to TitleFlow directly on TitleEscrow\n// (relayer is the current on-chain holder after lock)\nconst escrowAsRelayer = TitleEscrow__factory.connect(titleEscrowAddress, relayerWallet);\nawait (await escrowAsRelayer.transferHolder(\n  titleFlowAddress,\n  ethers.toUtf8Bytes(\"PINT restore — return holder to TitleFlow\"),\n)).wait();\n\n// Step B: Owner signs restoreFromExternal — relayer submits (RELAYER_ROLE required)\nconst newHolder   = \"0xNEW_HOLDER_ADDRESS\";\nconst nonce       = await titleFlow.nonce(titleEscrowAddress, ownerAddress);\nconst exportNonce = await titleFlow.exportNonce(titleEscrowAddress);\nconst deadline    = BigInt(Math.floor(Date.now() / 1000) + 3600);\n\nconst signature = await ownerWallet.signTypedData(domain, LIFECYCLE_TYPES, {\n  titleEscrow:           titleEscrowAddress,\n  pintRef,                          // same pintRef used in lock\n  nonce,\n  deadline,\n  exportDocumentHash,               // same hashes used in lock\n  exportEnvelopeHash,\n  lastBeneficiary:       ethers.ZeroAddress,\n  newHolder,\n  destinationPlatformId: ethers.ZeroHash,\n  exportNonce,\n});\n\nawait (await titleFlow.connect(relayerWallet).restoreFromExternal(\n  titleEscrowAddress,\n  pintRef,\n  nonce,\n  deadline,\n  exportDocumentHash,\n  exportEnvelopeHash,\n  newHolder,\n  exportNonce,\n  signature,\n)).wait();\n\nconsole.log(\"Restored — lifecycle:\", await titleFlow.lifecycle(titleEscrowAddress)); // 1 = ACTIVE\nconsole.log(\"holder:\", await escrow.holder()); // newHolder\n```\n\n---\n\n### Phase 16 — Endorse for External Transfer\n\nTransfer only the **beneficial title** (beneficiary) to the relayer for cross-platform endorsement. Holder remains as TitleFlow. The EIP-712 struct uses `newHolder = TitleFlow.relayer`.\n\n```typescript\nconst lastBeneficiary = await escrow.beneficiary();\nconst nonce           = await titleFlow.nonce(titleEscrowAddress, ownerAddress);\nconst exportNonce     = await titleFlow.exportNonce(titleEscrowAddress);\nconst deadline        = BigInt(Math.floor(Date.now() / 1000) + 3600);\n\nconst signature = await ownerWallet.signTypedData(domain, LIFECYCLE_TYPES, {\n  titleEscrow:           titleEscrowAddress,\n  pintRef,\n  nonce,\n  deadline,\n  exportDocumentHash,\n  exportEnvelopeHash,\n  lastBeneficiary,\n  newHolder:             relayerAddress,  // must equal TitleFlow.relayer\n  destinationPlatformId: destinationPlatform,\n  exportNonce,\n});\n\nawait (await titleFlow.endorseForExternalTransfer(\n  titleEscrowAddress,\n  pintRef,\n  nonce,\n  deadline,\n  exportDocumentHash,\n  exportEnvelopeHash,\n  lastBeneficiary,\n  destinationPlatform,\n  exportNonce,\n  signature,\n)).wait();\n\n// After endorse: beneficiary = relayer, holder = TitleFlow (unchanged)\nconsole.log(\"Endorsed — beneficiary:\", await escrow.beneficiary()); // relayerAddress\n```\n\n---\n\n### Phase 17 — Restore Endorse (PINT Beneficiary Restore)\n\nRestore beneficial title after `endorseForExternalTransfer`. The relayer is the current beneficiary after the endorse.\n\n**Note:** `restoreEndorse` calls `nominate` internally. The relayer must pre-nominate the new beneficiary directly on TitleEscrow before calling `restoreEndorse`, so that the internal nominate is skipped (nominee is already set). See `TitleFlowLifecycle_restoreEndorse.sol` for the modified implementation.\n\n```typescript\n// Step A: Relayer nominates new beneficiary directly on TitleEscrow\n// (relayer = beneficiary after endorse ✓)\nconst escrowAsRelayer = TitleEscrow__factory.connect(titleEscrowAddress, relayerWallet);\nawait (await escrowAsRelayer.nominate(\n  newBeneficiaryAddress,\n  ethers.toUtf8Bytes(\"Restore endorse — nominate new beneficiary\"),\n)).wait();\n\n// Step B: Owner signs restoreEndorse — relayer submits (RELAYER_ROLE required)\n// newHolder field in the EIP-712 struct carries the newBeneficiary value\nconst nonce       = await titleFlow.nonce(titleEscrowAddress, ownerAddress);\nconst exportNonce = await titleFlow.exportNonce(titleEscrowAddress);\nconst deadline    = BigInt(Math.floor(Date.now() / 1000) + 3600);\n\nconst signature = await ownerWallet.signTypedData(domain, LIFECYCLE_TYPES, {\n  titleEscrow:           titleEscrowAddress,\n  pintRef,\n  nonce,\n  deadline,\n  exportDocumentHash,\n  exportEnvelopeHash,\n  lastBeneficiary:       ethers.ZeroAddress,\n  newHolder:             newBeneficiaryAddress, // = newBeneficiary in restoreEndorse\n  destinationPlatformId: ethers.ZeroHash,\n  exportNonce,\n});\n\nawait (await titleFlow.connect(relayerWallet).restoreEndorse(\n  titleEscrowAddress,\n  pintRef,\n  nonce,\n  deadline,\n  exportDocumentHash,\n  exportEnvelopeHash,\n  newBeneficiaryAddress,\n  exportNonce,\n  signature,\n)).wait();\n\nconsole.log(\"Endorse restored — beneficiary:\", await escrow.beneficiary()); // newBeneficiaryAddress\nconsole.log(\"lifecycle:\", await titleFlow.lifecycle(titleEscrowAddress));    // 1 = ACTIVE\n```\n\n---\n\n### Phase 18 — Surrender for External Transfer\n\nTransfer both beneficiary and holder to the relayer for cross-platform surrender. Use for PINT outbound when full ownership must transfer. After surrender, the relayer calls `returnToIssuer` directly and burns the token.\n\n```typescript\nconst lastBeneficiary = await escrow.beneficiary();\nconst nonce           = await titleFlow.nonce(titleEscrowAddress, ownerAddress);\nconst exportNonce     = await titleFlow.exportNonce(titleEscrowAddress);\nconst deadline        = BigInt(Math.floor(Date.now() / 1000) + 3600);\nconst forAmendment    = false; // true = surrender for amendment, false = surrender for delivery\n\nconst signature = await ownerWallet.signTypedData(domain, LIFECYCLE_TYPES, {\n  titleEscrow:           titleEscrowAddress,\n  pintRef,\n  nonce,\n  deadline,\n  exportDocumentHash,\n  exportEnvelopeHash,\n  lastBeneficiary,\n  newHolder:             relayerAddress,  // must equal TitleFlow.relayer\n  destinationPlatformId: destinationPlatform,\n  exportNonce,\n});\n\nawait (await titleFlow.surrenderForExternalTransfer(\n  titleEscrowAddress,\n  pintRef,\n  nonce,\n  deadline,\n  exportDocumentHash,\n  exportEnvelopeHash,\n  lastBeneficiary,\n  destinationPlatform,\n  exportNonce,\n  forAmendment,\n  signature,\n)).wait();\n\n// After surrender: beneficiary = relayer, holder = relayer\n// Relayer can now call returnToIssuer + burn directly on TitleEscrow\nconst escrowAsRelayer = TitleEscrow__factory.connect(titleEscrowAddress, relayerWallet);\nawait (await escrowAsRelayer.returnToIssuer(\n  ethers.toUtf8Bytes(\"Surrender — returning to issuer\"),\n)).wait();\n\nconst registry = TradeTrustToken__factory.connect(registryAddress, relayerWallet);\nawait (await registry.burn(\n  BigInt(tokenId),\n  ethers.toUtf8Bytes(\"Surrender accepted — eBL destroyed\"),\n)).wait();\n\nconsole.log(\"lifecycle:\", await titleFlow.lifecycle(titleEscrowAddress)); // 4 = DESTROYED\n```\n\n---\n\n## Key Management\n\n### Planned Owner Rotation\n\nEnforces a 48-hour timelock — the new owner must accept after the delay.\n\n```typescript\n// Attorney proposes (starts 48hr timelock)\nawait (await titleFlow.connect(attorneyWallet).proposeOwnerRotation(newOwnerAddress)).wait();\n\n// After 48 hours, new owner accepts from the new HSM\nawait (await titleFlow.connect(newOwnerWallet).acceptOwnerRotation()).wait();\nconsole.log(\"Owner rotated to:\", await titleFlow.owner());\n```\n\n### Planned Attorney Rotation\n\n```typescript\n// Current attorney proposes\nawait (await titleFlow.connect(attorneyWallet).proposeAttorney(newAttorneyAddress)).wait();\n\n// New attorney accepts — role transferred atomically\nawait (await titleFlow.connect(newAttorneyWallet).acceptAttorney()).wait();\nconsole.log(\"Attorney rotated to:\", await titleFlow.attorney());\n```\n\n### Emergency Recovery (Guardian)\n\nBypasses all timelocks. Requires `GUARDIAN_ROLE`.\n\n```typescript\n// Emergency owner recovery\nawait (await titleFlow.connect(guardianWallet).guardianRecoverOwner(\n  newOwnerAddress,\n  \"Primary HSM hardware failure — emergency recovery\",\n)).wait();\n\n// Emergency attorney rotation\nawait (await titleFlow.connect(guardianWallet).guardianRotateAttorney(\n  newAttorneyAddress,\n  \"Attorney infrastructure outage — switching to backup\",\n)).wait();\n\n// Emergency relayer rotation\nawait (await titleFlow.connect(guardianWallet).guardianRotateRelayer(\n  newRelayerAddress,\n  \"PINT service migration — rotating relayer\",\n)).wait();\n```\n\n### Add / Remove Backup Attorney\n\n```typescript\nimport { ATTORNEY_ADMIN_ROLE } from \"@credorelabs/credore-smart-contracts\";\n\n// Guardian adds backup (DEFAULT_ADMIN_ROLE required)\nawait (await titleFlow.connect(guardianWallet).addBackupAttorney(backupAddress)).wait();\n\nconst hasRole = await titleFlow.hasRole(ATTORNEY_ADMIN_ROLE, backupAddress);\nconsole.log(\"Backup attorney active:\", hasRole); // true\n\n// Guardian removes backup\nawait (await titleFlow.connect(guardianWallet).removeAttorney(backupAddress)).wait();\n```\n\n### Relayer Management\n\n```typescript\n// Active relayer rotates to new address (RELAYER_ROLE required)\nawait (await titleFlow.connect(relayerWallet).setRelayer(newRelayerAddress)).wait();\n\n// Guardian adds a backup relayer (DEFAULT_ADMIN_ROLE)\nawait (await titleFlow.connect(guardianWallet).addBackupRelayer(backupRelayerAddress)).wait();\n\n// Guardian removes a relayer\nawait (await titleFlow.connect(guardianWallet).removeRelayer(oldRelayerAddress)).wait();\n```\n\n---\n\n## Pause Mechanism\n\n```typescript\n// Attorney pauses (ATTORNEY_ADMIN_ROLE)\nawait (await titleFlow.connect(attorneyWallet).pause(\n  \"Regulatory freeze — all operations suspended\",\n)).wait();\nconsole.log(\"Paused:\", await titleFlow.paused()); // true\n\n// Guardian unpauses (DEFAULT_ADMIN_ROLE)\nawait (await titleFlow.connect(guardianWallet).unpause()).wait();\nconsole.log(\"Paused:\", await titleFlow.paused()); // false\n```\n\n---\n\n## API Reference\n\n### Contracts and Types\n\n```typescript\nimport {\n  // TypeChain contract types\n  TitleFlow__factory,\n  TitleFlowFactory__factory,\n  TitleFlowRegistry__factory,\n  TitleFlowRelayFacet__factory,\n\n  // Interface types (type-only imports)\n  type TitleFlow,\n  type TitleFlowFactory,\n  type TitleFlowRegistry,\n  type TitleFlowRelayFacet,\n  type ITitleFlowAdmin,\n  type ITitleFlowLifecycle,\n  type ITitleFlowRelay,\n  type ITitleFlowStorage,\n  type ITitleFlowFactory,\n  type ITitleFlowRegistry,\n} from \"@credorelabs/credore-smart-contracts\";\n```\n\n### Role Constants\n\nPre-computed `keccak256` role hashes for use with `hasRole()` and event filtering.\n\n```typescript\nimport {\n  ATTORNEY_ADMIN_ROLE,  // keccak256(\"ATTORNEY_ADMIN_ROLE\")\n  RELAYER_ROLE,         // keccak256(\"RELAYER_ROLE\")\n  GUARDIAN_ROLE,        // keccak256(\"GUARDIAN_ROLE\")\n  FACTORY_ADMIN_ROLE,   // keccak256(\"FACTORY_ADMIN_ROLE\")\n  REGISTRAR_ROLE,       // keccak256(\"REGISTRAR_ROLE\")\n} from \"@credorelabs/credore-smart-contracts\";\n\nconst isAttorney = await titleFlow.hasRole(ATTORNEY_ADMIN_ROLE, address);\n```\n\n### Enums\n\n```typescript\nimport { ActionType, Lifecycle } from \"@credorelabs/credore-smart-contracts\";\n\n// ActionType — used in ACTION_TYPES EIP-712 signatures\nActionType.Nominate            // 0\nActionType.BeneficiaryTransfer // 1\nActionType.HolderTransfer      // 2\nActionType.OwnersTransfer      // 3\nActionType.RejectBeneficiary   // 4\nActionType.RejectHolder        // 5\nActionType.RejectOwners        // 6\nActionType.ReturnToIssuer      // 7\nActionType.Shred               // 8\nActionType.RejectSurrender     // 9\n\n// Lifecycle — returned by titleFlow.lifecycle(escrowAddress)\nLifecycle.UNREGISTERED    // 0\nLifecycle.ACTIVE          // 1\nLifecycle.LOCKED_EXTERNAL // 2\nLifecycle.SURRENDERED     // 3\nLifecycle.DESTROYED       // 4\n```\n\n### EIP-712 Type Definitions\n\n```typescript\nimport { ACTION_TYPES, LIFECYCLE_TYPES, titleFlowDomain } from \"@credorelabs/credore-smart-contracts\";\n\n// Build domain for a deployed TitleFlow instance\nconst domain = titleFlowDomain(chainId, titleFlowAddress);\n// { name: \"TitleFlow\", version: \"1\", chainId, verifyingContract: titleFlowAddress }\n\n// Use with signer.signTypedData()\nconst sig = await ownerWallet.signTypedData(domain, ACTION_TYPES, value);\nconst sig = await ownerWallet.signTypedData(domain, LIFECYCLE_TYPES, value);\n```\n\n`ACTION_TYPES` is used for: `nominate`, `transferBeneficiary`, `transferHolder`, `transferOwners`, all `rejectTransfer*`, `returnToIssuer`, `shred`.\n\n`LIFECYCLE_TYPES` is used for: `registerEscrow`, `lockForExternalTransfer`, `endorseForExternalTransfer`, `surrenderForExternalTransfer`, `restoreFromExternal`, `restoreEndorse`.\n\n### TypeScript Interfaces\n\n```typescript\nimport type {\n  ActionSignParams,      // Parameters for ACTION_TYPES signing\n  LifecycleSignParams,   // Parameters for LIFECYCLE_TYPES signing\n  TitleFlowRecord,       // Struct from TitleFlowRegistry.getRecord()\n  ExternalLockData,      // Struct from _externalLocks (after lockForExternalTransfer)\n  EndorseLockData,       // Struct from _endorseLocks (after endorseForExternalTransfer)\n  SurrenderLockData,     // Struct from _surrenderLocks (after surrenderForExternalTransfer)\n  DeploymentRecord,      // Shape of deployments/<network>.json\n} from \"@credorelabs/credore-smart-contracts\";\n```\n\n### Helpers\n\n```typescript\nimport {\n  titleFlowDomain,   // Build EIP-712 domain object\n  loadDeployment,    // Runtime deployment loader (returns null if not deployed)\n} from \"@credorelabs/credore-smart-contracts\";\n\n// Load deployment at runtime (won't crash if file doesn't exist)\nconst dep = await loadDeployment(\"sepolia\"); // \"mainnet\" | \"sepolia\" | \"polygon\" | \"amoy\"\nif (dep) {\n  console.log(\"TitleFlowFactory:\", dep.contracts.TitleFlowFactory);\n}\n```\n\n### Deployments\n\n```typescript\nimport {\n  mainnetDeployments,\n  sepoliaDeployments,\n  polygonDeployments,\n  amoyDeployments,\n} from \"@credorelabs/credore-smart-contracts\";\n\n// Each object is typed as DeploymentRecord\nconst factory  = sepoliaDeployments.contracts.TitleFlowFactory;\nconst registry = sepoliaDeployments.contracts.TitleFlowRegistry;\nconst chainId  = sepoliaDeployments.chainId;\n```\n\n---\n\n## Local Development\n\n### Prerequisites\n\nInstall [Anvil](https://book.getfoundry.sh/anvil/) (Foundry toolkit):\n\n```bash\ncurl -L https://foundry.paradigm.xyz | bash && foundryup\n```\n\n### Start local node\n\n```bash\nanvil \\\n  --chain-id 31337 \\\n  --port 8545 \\\n  --accounts 20 \\\n  --balance 10000 \\\n  --gas-limit 30000000 \\\n  --code-size-limit 100000\n```\n\n### Deploy contracts locally\n\n```bash\n# Copy env template and fill in Anvil test account addresses\ncp .env.example .env\n\nyarn hardhat run scripts/deploy.ts --network localhost\n```\n\nAddresses saved to `deployments/localhost.json`.\n\n### Run tests\n\n```bash\nyarn hardhat test\n```\n\n---\n\n## Deployment\n\n```bash\n# Testnet\nyarn hardhat run scripts/deploy.ts --network sepolia\nyarn hardhat run scripts/deploy.ts --network amoy\n\n# Mainnet\nyarn hardhat run scripts/deploy.ts --network polygon\nyarn hardhat run scripts/deploy.ts --network mainnet\n```\n\nRequired `.env` variables — see `.env.example`:\n\n| Variable | Description |\n|---|---|\n| `DEPLOYER_PRIVATE_KEY` | Account paying deployment gas |\n| `GUARDIAN_PRIVATE_KEY` | Guardian account (for `setFactory` wiring on live networks) |\n| `GUARDIAN_ADDRESS` | Guardian EOA address embedded in contracts |\n| `FACTORY_ADMIN_ADDRESS` | Factory admin EOA embedded in contracts |\n| `ALCHEMY_KEY` | Alchemy API key (or set per-network RPC URLs) |\n| `ETHERSCAN_API_KEY` | For Ethereum contract verification |\n| `POLYGONSCAN_API_KEY` | For Polygon contract verification |\n\n## Deployment Addresses\n\n| Network  | TitleFlowFactory | TitleFlowRegistry | Deployed |\n|---|---|---|---|\n| Mainnet  | — | — | — |\n| Polygon  | — | — | — |\n| Sepolia  | — | — | — |\n| Amoy     | — | — | — |\n\n---\n\n## Error Reference\n\n| Error | Cause | Fix |\n|---|---|---|\n| `InvalidSigner` | EIP-712 signature does not recover to the registered owner | Verify `domain`, `ACTION_TYPES`/`LIFECYCLE_TYPES`, and all field values match exactly what the contract reconstructs |\n| `InvalidNonce` | Nonce already consumed or skipped | Read fresh nonce with `titleFlow.nonce(escrow, owner)` before every signing operation |\n| `SignatureExpired` | `block.timestamp > deadline` | Build deadline from on-chain block time, not `Date.now()` — especially important in tests after time manipulation |\n| `InvalidEscrow` | Escrow not registered or address is zero | Call `registerEscrow` before any title operations |\n| `AlreadyRegistered` | `registerEscrow` called twice | Check `titleFlow.registeredEscrow(escrowAddr)` before registering |\n| `InvalidState` | Operation not allowed in current lifecycle state | Check `titleFlow.lifecycle(escrowAddr)` — must be `ACTIVE` (1) for relay ops; only lifecycle functions work in `LOCKED_EXTERNAL` (2) |\n| `InvalidPintReference` | `pintRef` is zero or does not match locked value | Never pass `ZeroHash` as pintRef for lock/restore operations |\n| `InvalidExportNonce` | `exportNonce` does not match on-chain value | Read fresh `titleFlow.exportNonce(escrowAddr)` before each lifecycle call |\n| `InvalidSignatureLength` | Signature is not exactly 65 bytes | Use `signer.signTypedData()` (EIP-712), not `signer.signMessage()` |\n| `EnforcedPause` | Contract is paused | Wait for guardian to unpause; check `titleFlow.paused()` before submitting |\n| `AccessControlUnauthorizedAccount` | Caller lacks required role | Attorney for daily ops; relayer for `restoreFromExternal`/`restoreEndorse`; guardian for emergency |\n| `CallerNotBeneficiary` | TitleEscrow operation requires `msg.sender == beneficiary` but TitleFlow proxy is not | Check `escrow.beneficiary()` — TitleFlow must still be the beneficiary |\n| `CallerNotHolder` | TitleEscrow operation requires `msg.sender == holder` but TitleFlow proxy is not | For direct escrow calls (relayer), check that the relayer is the current holder |\n| `DualRoleRejectionRequired` | Used `rejectTransferBeneficiary` or `rejectTransferHolder` when the same address holds both roles | Use `rejectTransferOwners` when beneficiary and holder are the same address |\n| `InvalidTransferToZeroAddress` | `prevBeneficiary` or `prevHolder` is zero during rejection | Rejection requires a prior transfer to revert — ensure `transferBeneficiary`/`transferHolder`/`transferOwners` was performed first |\n| `TargetNomineeAlreadyBeneficiary` | `nominate()` called with `_nominee == beneficiary` | Nominee must differ from current beneficiary |\n| `NomineeAlreadyNominated` | `nominate()` called with same nominee already set | Clear nominee first or use a different address |\n| `TokenNotReturnedToIssuer` | `shred()` called while escrow still holds the token | Call `returnToIssuer` first, then burn via `tokenRegistry.burn()` |\n\n---\n\n## Lifecycle State Machine\n\n```\n          registerEscrow()\nUNREGISTERED ──────────────────> ACTIVE\n                                    │\n           ┌────────────────────────┤\n           │  nominate()            │\n           │  transferBeneficiary() │\n           │  rejectTransfer*()     │\n           │  transferHolder()      │\n           │  transferOwners()      │\n           └────────────────────────┤\n                                    │\n           returnToIssuer() ────────┼──> token owned by registry\n                                    │    registry.burn() → NFT → 0xdead\n                                    │\n           shred() ─────────────────┼──> DESTROYED (4)\n                                    │    lifecycle set by TitleFlowRelayFacet\n                                    │\n           lockForExternalTransfer()│\n           endorseForExternalTransfer()\n           surrenderForExternalTransfer()\n                                    ▼\n                            LOCKED_EXTERNAL (2)\n                         (relay ops blocked)\n                                    │\n           restoreFromExternal() ───┤  (after relayer.transferHolder back to TitleFlow)\n           restoreEndorse() ────────┤  (after relayer.nominate directly on TitleEscrow)\n           rejectSurrender() ───────┘  (attorney rejects surrender)\n                                    │\n                                    ▼\n                                 ACTIVE (1)\n```\n\n---\n\n## License\n\nUNLICENSED — proprietary software. All rights reserved by Credore (Trustless Private Limited).\n\n## Copyright & Legal Notice\n\nCopyright © 2026 Credore (Trustless Private Limited). All rights reserved.\n\nThis software and associated documentation files (the \"Software\") are proprietary and confidential.\nNo license is granted to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software.\nAny use requires explicit written permission from Credore (Trustless Private Limited).\n\nFor licensing inquiries: [info@credore.xyz](mailto:info@credore.xyz)\n","readmeFilename":"README.md"}