@torrens/property-sdk
v0.1.2
Published
Shared, runtime-agnostic SDK for Torrens apps + demos: ABIs, tier-aware metadata resolution, formatting, address config, and signer-worker helpers.
Readme
@torrens/property-sdk
Shared, runtime-agnostic SDK for every Torrens surface — the owner/admin
apps, the six demos, and the Cloudflare-Worker consumers (regulator-mcp, the
curator). This is Foundation F1 of the Experience programme
(docs/ROADMAP_EXPERIENCE.md §1) and the package the demo consolidation
(docs/DEMOS_CONSOLIDATION.md §3/§5) is built on.
It kills the duplicated dptMetadata.js, format.js and config/contracts.js
copied into every app/demo, and replaces the old monolithic IPFS fetch with the
tier-aware schema-3.0 resolver (public tier + role-gated restricted/PII
access-link fetch).
Hard constraint: runtime-agnostic
No fs, no Node-only viem client builders. All network IO goes through
fetch() (injectable for tests/Workers). Works unchanged in Cloudflare
Workers, Node 20+, and the browser. Formatting uses viem's pure formatUnits
and Intl.
Exports
ABIs (@torrens/property-sdk or @torrens/property-sdk/abis)
- Core, re-exported from
@torrens/contracts(single source of truth — the deployed bytecode is byte-identical, so the ABI must be too):dptAbi,landRegistryAbi,applicationRegistryAbi,ownerApprovalAbi,coOwnerRegistryAbi,tokenV2Abi. - Generic:
erc20Abi(incl. demofaucet/mint). - Demo-specific (copied verbatim from each demo's
config/contracts):mortgagePositionAbi(+mortgagePositionV2Abiwith payment-routing),remortgageSettlementAbi,rmbsVaultAbi,rmbsIssuerAbi,rmbsDistributionAbi,torrensFinVaultAbi,secondaryTransferMarketAbi,pexaDinUsdcSettlementAbi. - Enums/labels:
MORTGAGE_STATUS,MORTGAGE_STATUS_LABELS,MORTGAGE_CASE_TYPE,RMBS_POOL_STATUS,SECONDARY_LISTING_STATUS,PEXA_READINESS_FIELDS.
This is the single home for demo ABIs — resolving the §6 double-maintenance risk.
Tier-aware metadata
import {
fetchDptMetadata, // PUBLIC tier only — non-personal facts
fetchRestrictedProperty, // role-gated: full address/coords via access link
fetchApplicationMetadata,// private PII doc via access link (or public URL)
uploadApplicationMetadata,
cidFromUri,
DEFAULT_IPFS_GATEWAYS,
TORRENS_HOSTED_PINATA_GATEWAY,
} from '@torrens/property-sdk'
// Public tier — best-effort, never throws, returns null on total failure.
const pub = await fetchDptMetadata(tokenUri, { cache })
// Restricted tier — pre-3.0 tokens (no pointer) resolve to null with NO
// network call; otherwise mint a short-lived access link via the signer worker.
const restricted = await fetchRestrictedProperty(pub, {
accessLinkUrl: SIGNER_ACCESS_LINK_URL,
})Public/restricted types are wrapped from @torrens/dpt-metadata
(PublicDptMetadataV3, RestrictedPropertyDoc) — not duplicated. The role gate
lives in the signer worker behind accessLinkUrl; the SDK just relays.
Signer-worker helpers
getSignedPinataUrl, uploadViaSignedUrl, getPrivateAccessLink,
fetchPrivateJson. URLs are passed in by the caller (no import.meta.env), so
the helpers stay runtime-agnostic. CORS allowlist on the worker must include
every consuming origin (DEMOS_CONSOLIDATION §6).
Formatting
formatFin, formatTFin, formatUsdc, formatBps, shortAddress,
formatDate, parseFin, parseUsdc; FIN_DECIMALS=18, TFIN_DECIMALS=18,
DIN_DECIMALS=18, USDC_DECIMALS=6.
Deployment addresses
DeploymentAddresses, getDeploymentAddresses(chainId) (built-in §2 Base
Sepolia book), loadDeploymentAddresses(url, fetch?) (HTTP, no fs),
defineDeploymentAddresses(obj) (validate a bundler-imported JSON),
DEFAULT_CHAIN_ID=84532.
Attestation reads (Programme-1 P3)
createAttestationReader({ client, easAddress?, issuerRegistryAddress?, chainId?, fromBlock? })
returns an AttestationReader backed by EAS + IssuerRegistry (Base Sepolia
addresses default from the address book). Runtime-agnostic — fetch()/viem
only, no fs, no eas-sdk — so it runs in the browser apps, Node, and the
Cloudflare-Worker consumers. client is any viem PublicClient (or the narrow
ChainReader shape).
| Method | What it does |
|---|---|
| getAttestation(uid) | EAS.getAttestation + decodes the Torrens envelope (subjectRef, tier, commitment, issuedAt, cid); null if absent. |
| latestValid(subjectRef, schemaUid, opts?) | Resolves the current attestation: newest, non-revoked, non-expired, and (if opts.maxAge) fresh. Returns { attestation, asOf, isFresh, isRevoked } so callers distinguish "unrevoked" from "stale" (ROADMAP §7). |
| verifyCommitment(att, contentBytes) | Recomputes sha256(content) and compares to the on-chain commitment → Tier-1 tamper-check (ADR-035). |
| isIssuerAuthorised(attester, schemaUid) | IssuerRegistry.isAuthorised. |
Event-scan vs indexer. EAS has no native "latest valid for subject+claim"
read (ADR-037 / §D7). latestValid ships a reference Attested-event scan
(eventScanSource) that works with no extra infra — but a production
deployment should back it with an indexer / subgraph. Pass opts.source
(an AttestationSource) to swap the scan for an indexer with identical
freshness/revocation semantics.
How consumers adopt it
| Consumer | Replaces with the SDK |
|---|---|
| owner-app / admin-app | lib/format.ts, lib/dptMetadata, lib/restrictedProperty, lib/pinataAccessLinks, ad-hoc ABIs |
| mortgage / remortgage / rmbs / secondary demos | lib/dptMetadata.js, lib/format.js, config/contracts.{js,ts} |
| regulator-mcp / curator (Workers) | the same metadata/ABI/address surface, no fs |
The apps' React hooks (useRestrictedProperty, useOwnerNames) stay in the
apps and wrap these framework-free primitives.
Develop
This is a source-only package (exports → src/index.ts), like the other
packages/*. There is no build step; consumers import the TypeScript directly.
# Typecheck (from the repo root):
pnpm exec tsc -p packages/property-sdk/tsconfig.json --noEmit
# Tests run via the root Vitest config (globs packages/**/*.test.ts):
pnpm test:frontend # whole frontend suite
pnpm exec vitest run packages/property-sdk # just this package