@hauska-sdk/vda
v0.1.0
Published
CNS Protocol VDA SDK - Verified Digital Assets management
Downloads
165
Maintainers
Readme
@hauska-sdk/vda
CNS Protocol VDA SDK - Verified Digital Assets management with cross-spoke search and access control.
Installation
npm install @hauska-sdk/vdaQuick Start
import { VDASDK } from "@hauska-sdk/vda";
import { PostgreSQLStorageAdapter } from "@hauska-sdk/adapters-storage-postgres";
import { WalletManager } from "@hauska-sdk/wallet";
import { Pool } from "pg";
// Initialize SDK
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
const sdk = new VDASDK({
storageAdapter: new PostgreSQLStorageAdapter({
pool,
autoMigrate: true,
}),
walletManager: new WalletManager(),
logHook: (level, message, data) => {
console.log(`[${level}] ${message}`, data);
},
});
// Mint a VDA
const vda = await sdk.mint({
assetType: "deed",
address: "123 Main St, Schertz, TX 78154",
ownerWallet: "0x...",
spoke: "real-estate",
});
// Create an access pass
const accessPass = await sdk.createAccessPass({
grantorWallet: "0x...",
recipientWallet: "0x...",
accessibleVDAs: [vda.id],
permissions: ["view", "download"],
expiry: Date.now() + 3600000, // 1 hour
spoke: "real-estate",
address: "123 Main St",
});
// Verify ownership
const verification = await sdk.verifyOwnership(vda.id, "0x...");
if (verification.hasAccess) {
// Grant access to resource
}Features
- ✅ VDA Minting - Create verified digital assets with universal metadata
- ✅ Access Pass Management - Create, revoke, and verify time-bound access passes
- ✅ Ownership Verification - Check direct ownership and access pass permissions
- ✅ Ownership Transfer - Transfer VDA ownership between wallets
- ✅ Cross-Spoke Search - Search VDAs across all spokes by universal metadata keys
- ✅ Automatic Wallet Creation - Seamless wallet management
- ✅ Logging Hooks - Optional monitoring and debugging support
Universal Metadata Schema
VDAs use a universal metadata schema that enables cross-spoke search. At least one universal metadata key is required:
Universal Metadata Keys
address- Physical address (911 format: "123 Main St, Schertz, TX 78154")- Used by: real-estate, architect, municipal spokes
- Enables cross-spoke property search
legalDesc- Legal description (e.g., "Lot 5, Block 3, Schertz Heights")- Used by: real-estate, architect, municipal spokes
- Alternative to address for property identification
patientId- Patient identifier- Used by: healthcare spoke
- Enables patient record aggregation across systems
api14- API well number (format: "42-123-12345-00-00")- Used by: oil-gas spoke
- Standard identifier for oil & gas wells
Spoke Types
real-estate- Real estate properties, deeds, titleshealthcare- Patient records, medical documentsoil-gas- Well logs, permits, leasesarchitect- Blueprints, building plansmunicipal- Permits, licenses, city records
Asset Types
deed- Property deedblueprint- Architectural blueprintpermit- Building or operational permitcontract- Legal contracthealth-record- Medical recordwell-log- Oil & gas well logaccess-pass- Time-bound access passinvoice- Invoice document
API Reference
VDASDK
Main SDK class that provides all VDA functionality.
Constructor
new VDASDK(config: VDASDKConfig)Configuration:
interface VDASDKConfig {
storageAdapter: VDAStorageAdapter; // Required
walletManager?: WalletManager; // Optional, auto-created if not provided
logHook?: LogHook; // Optional logging hook
}Methods
mint(params: MintVDAParams): Promise<VDA>
Mint a new VDA.
const vda = await sdk.mint({
assetType: "deed",
address: "123 Main St, Schertz, TX 78154",
ownerWallet: "0x...", // Optional if userId/password provided
spoke: "real-estate",
// Optional: userId, password for auto-wallet creation
});getVDA(vdaId: string): Promise<VDA | null>
Get a VDA by ID.
const vda = await sdk.getVDA("vda-id");listVDAsByOwner(wallet: string): Promise<VDA[]>
List all VDAs owned by a wallet.
const vdas = await sdk.listVDAsByOwner("0x...");createAccessPass(params: CreateAccessPassParams): Promise<VDA>
Create an access pass VDA.
const accessPass = await sdk.createAccessPass({
grantorWallet: "0x...", // Must own all accessible VDAs
recipientWallet: "0x...", // Optional if userId/password provided
accessibleVDAs: ["vda-1", "vda-2"],
permissions: ["view", "download"],
expiry: Date.now() + 3600000, // Must be in the future
spoke: "real-estate",
address: "123 Main St", // Universal metadata
});revokeAccessPass(accessPassId: string, grantorWallet: string): Promise<VDA>
Revoke an access pass.
const revoked = await sdk.revokeAccessPass(
accessPassId,
grantorWallet
);verifyOwnership(vdaId: string, wallet: string, requiredPermissions?: string[]): Promise<OwnershipVerificationResult>
Verify ownership of a VDA.
const result = await sdk.verifyOwnership(
vdaId,
wallet,
["view", "download"] // Optional permissions to check
);
if (result.hasAccess) {
console.log(`Access type: ${result.accessType}`); // "direct" or "access-pass"
if (result.accessPass) {
console.log(`Access via pass: ${result.accessPass.id}`);
}
}transferOwnership(params: TransferOwnershipParams): Promise<VDA>
Transfer ownership of a VDA.
const updatedVDA = await sdk.transferOwnership({
vdaId: "vda-id",
currentOwner: "0x...", // Must match current owner
newOwner: "0x...",
});searchByAddress(address: string, options?: SearchOptions): Promise<SearchResult>
Search VDAs by address (supports partial matching).
const result = await sdk.searchByAddress("Main St", {
limit: 10,
offset: 0,
});
console.log(`Found ${result.total} VDAs`);
console.log(`Showing ${result.results.length} results`);searchByPatientId(patientId: string, options?: SearchOptions): Promise<SearchResult>
Search VDAs by patient ID (exact match).
const result = await sdk.searchByPatientId("patient-123", {
limit: 20,
offset: 0,
});searchByAPI14(api14: string, options?: SearchOptions): Promise<SearchResult>
Search VDAs by API14 (exact match).
const result = await sdk.searchByAPI14("42-123-12345-00-00");searchUniversal(query: string, options?: SearchOptions): Promise<SearchResult>
Search VDAs across all spokes by any universal metadata key. Automatically detects query type.
// Automatically detects API14 format
const result1 = await sdk.searchUniversal("42-123-12345-00-00");
// Tries patient ID first, falls back to address
const result2 = await sdk.searchUniversal("patient-123");
// Searches by address
const result3 = await sdk.searchUniversal("123 Main St");Access Pass Usage Guide
Access passes are time-bound, revocable permissions that grant access to VDAs. They are themselves VDAs with assetType: "access-pass".
Creating Access Passes
// Create an access pass for a data room
const dataRoomVDA = await sdk.mint({
assetType: "deed",
address: "123 Main St",
ownerWallet: propertyOwnerWallet,
spoke: "real-estate",
});
const accessPass = await sdk.createAccessPass({
grantorWallet: propertyOwnerWallet, // Must own dataRoomVDA
recipientWallet: buyerWallet,
accessibleVDAs: [dataRoomVDA.id],
permissions: ["view", "download"], // What the recipient can do
expiry: Date.now() + 7 * 24 * 3600000, // 7 days
spoke: "real-estate",
address: "123 Main St",
});Verifying Access
// Check if wallet has access to a VDA
const verification = await sdk.verifyOwnership(
dataRoomVDA.id,
buyerWallet,
["view"] // Required permission
);
if (verification.hasAccess) {
if (verification.accessType === "direct") {
console.log("User owns this VDA");
} else if (verification.accessType === "access-pass") {
console.log("User has access via access pass");
console.log(`Access pass expires: ${verification.accessPass?.metadata.expiry}`);
}
} else {
console.log(`Access denied: ${verification.reason}`);
}Revoking Access Passes
// Revoke an access pass before it expires
const revoked = await sdk.revokeAccessPass(
accessPass.id,
propertyOwnerWallet // Must be the grantor
);
// Access is immediately revoked
const verification = await sdk.verifyOwnership(
dataRoomVDA.id,
buyerWallet
);
// verification.hasAccess === falseAccess Pass Permissions
view- Can view the VDA and associated documentsdownload- Can download documentswrite- Can modify the VDAannotate- Can add annotations
Code Examples
Real Estate: Property Listing
// Mint a property deed
const property = await sdk.mint({
assetType: "deed",
address: "123 Main St, Schertz, TX 78154",
legalDesc: "Lot 5, Block 3, Schertz Heights",
ownerWallet: ownerWallet,
spoke: "real-estate",
ipfsCid: "Qm...", // Link to property documents on IPFS
});
// Create a data room access pass for potential buyers
const dataRoomPass = await sdk.createAccessPass({
grantorWallet: ownerWallet,
recipientWallet: buyerWallet,
accessibleVDAs: [property.id],
permissions: ["view", "download"],
expiry: Date.now() + 30 * 24 * 3600000, // 30 days
spoke: "real-estate",
address: "123 Main St, Schertz, TX 78154",
});Healthcare: Patient Record Management
// Mint a patient record
const patientRecord = await sdk.mint({
assetType: "health-record",
patientId: "PATIENT-12345",
ownerWallet: patientWallet,
spoke: "healthcare",
ipfsCid: "Qm...", // Encrypted medical records
});
// Grant doctor access
const doctorAccess = await sdk.createAccessPass({
grantorWallet: patientWallet,
recipientWallet: doctorWallet,
accessibleVDAs: [patientRecord.id],
permissions: ["view", "annotate"],
expiry: Date.now() + 24 * 3600000, // 24 hours
spoke: "healthcare",
patientId: "PATIENT-12345",
});
// Search all records for a patient
const allRecords = await sdk.searchByPatientId("PATIENT-12345");Oil & Gas: Well Log Management
// Mint a well log
const wellLog = await sdk.mint({
assetType: "well-log",
api14: "42-123-12345-00-00",
ownerWallet: operatorWallet,
spoke: "oil-gas",
ipfsCid: "Qm...", // Well log data
});
// Grant regulator access
const regulatorAccess = await sdk.createAccessPass({
grantorWallet: operatorWallet,
recipientWallet: regulatorWallet,
accessibleVDAs: [wellLog.id],
permissions: ["view"],
expiry: Date.now() + 365 * 24 * 3600000, // 1 year
spoke: "oil-gas",
api14: "42-123-12345-00-00",
});
// Search by API14
const wellData = await sdk.searchByAPI14("42-123-12345-00-00");Cross-Spoke Search
// Search for all assets at a specific address across all spokes
const allAssets = await sdk.searchByAddress("123 Main St, Schertz, TX 78154");
// Results may include:
// - Real estate deeds
// - Architectural blueprints
// - Municipal permits
// - All from different spokes but same addressExpress.js Integration
import express from "express";
import { VDASDK } from "@hauska-sdk/vda";
import { PostgreSQLStorageAdapter } from "@hauska-sdk/adapters-storage-postgres";
const app = express();
const sdk = new VDASDK({
storageAdapter: new PostgreSQLStorageAdapter({
pool: new Pool({ connectionString: process.env.DATABASE_URL }),
autoMigrate: true,
}),
});
// Mint a VDA
app.post("/api/vdas", async (req, res) => {
try {
const vda = await sdk.mint({
assetType: req.body.assetType,
address: req.body.address,
ownerWallet: req.body.ownerWallet,
spoke: req.body.spoke,
});
res.json(vda);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Verify access before serving document
app.get("/api/documents/:vdaId", async (req, res) => {
const wallet = req.headers["x-wallet-address"] as string;
const verification = await sdk.verifyOwnership(
req.params.vdaId,
wallet,
["view"]
);
if (!verification.hasAccess) {
return res.status(403).json({ error: verification.reason });
}
// Fetch and serve document from IPFS
// ...
});
// Create access pass
app.post("/api/access-passes", async (req, res) => {
try {
const accessPass = await sdk.createAccessPass({
grantorWallet: req.body.grantorWallet,
recipientWallet: req.body.recipientWallet,
accessibleVDAs: req.body.accessibleVDAs,
permissions: req.body.permissions,
expiry: req.body.expiry,
spoke: req.body.spoke,
address: req.body.address,
});
res.json(accessPass);
} catch (error) {
res.status(400).json({ error: error.message });
}
});Troubleshooting
Common Issues
"Invalid VDA metadata: At least one universal metadata key is required"
Problem: VDA is missing required universal metadata (address, legalDesc, patientId, or api14).
Solution: Provide at least one universal metadata key:
// ❌ Missing universal metadata
await sdk.mint({
assetType: "deed",
ownerWallet: "0x...",
spoke: "real-estate",
});
// ✅ Include address or legalDesc
await sdk.mint({
assetType: "deed",
address: "123 Main St",
ownerWallet: "0x...",
spoke: "real-estate",
});"Grantor does not own the following VDAs"
Problem: Trying to create an access pass for VDAs the grantor doesn't own.
Solution: Verify grantor owns all accessible VDAs:
// Check ownership first
const verification = await sdk.verifyOwnership(
vdaId,
grantorWallet
);
if (verification.hasAccess && verification.accessType === "direct") {
// Grantor owns it, can create access pass
}"Expiry must be in the future"
Problem: Access pass expiry is set to a past timestamp.
Solution: Ensure expiry is in the future:
// ❌ Past expiry
expiry: Date.now() - 1000
// ✅ Future expiry
expiry: Date.now() + 3600000 // 1 hour from now"Access pass is already revoked"
Problem: Trying to revoke an already-revoked access pass.
Solution: Check revocation status before revoking:
const accessPass = await sdk.getVDA(accessPassId);
if (accessPass?.metadata.revoked) {
// Already revoked
return;
}
await sdk.revokeAccessPass(accessPassId, grantorWallet);Search returns no results
Problem: Search query doesn't match stored addresses.
Solution:
- For address search, use partial matching (e.g., "Main St" instead of full address)
- Ensure addresses are normalized (case-insensitive)
- Check that VDAs were indexed correctly
// ✅ Partial matching works
const results = await sdk.searchByAddress("Main St");
// ✅ Exact match also works
const results = await sdk.searchByAddress("123 Main St, Schertz, TX 78154");"VDA not found" after transfer
Problem: VDA ID doesn't exist or was deleted.
Solution: Verify VDA exists before transfer:
const vda = await sdk.getVDA(vdaId);
if (!vda) {
throw new Error("VDA not found");
}
await sdk.transferOwnership({ vdaId, currentOwner, newOwner });Performance Tips
Use pagination for large result sets:
const result = await sdk.searchByAddress("Main St", { limit: 50, offset: 0, });Index frequently searched fields: The PostgreSQL adapter automatically creates indexes on
address,patient_id,api14, andowner_wallet.Batch operations: When creating multiple VDAs, use
Promise.all()for parallel execution:const vdas = await Promise.all( addresses.map(addr => sdk.mint({ address: addr, ... })) );
Storage Adapters
PostgreSQL (Production)
import { PostgreSQLStorageAdapter } from "@hauska-sdk/adapters-storage-postgres";
import { Pool } from "pg";
const adapter = new PostgreSQLStorageAdapter({
pool: new Pool({
connectionString: process.env.DATABASE_URL,
}),
autoMigrate: true, // Automatically creates tables
});Mock (Testing)
import { MockStorageAdapter } from "@hauska-sdk/adapters-storage-mock";
const adapter = new MockStorageAdapter();License
MIT
