Skip to main content

HCS-12: HashLinks SDK

Build interactive, decentralized experiences on Hedera without smart contracts. HashLinks combines WebAssembly logic, Gutenberg UI blocks, and a composition layer to create powerful applications with minimal fees.


HashLinks enables you to:

  • Execute business logic deterministically through WebAssembly modules
  • Create rich UI components using WordPress Gutenberg blocks
  • Compose applications by binding actions to blocks via assemblies
  • Store everything on-chain using Hedera Consensus Service topics
  • Run anywhere with both server-side and browser-based clients

Quick StartDirect link to Quick Start

InstallationDirect link to Installation

npm install @hashgraphonline/standards-sdk

Basic SetupDirect link to Basic Setup

import { HCS12Client, NetworkType, Logger } from '@hashgraphonline/standards-sdk';

// Server-side client with private key
const client = new HCS12Client({
network: NetworkType.TESTNET,
mirrorNode: 'https://testnet.mirrornode.hedera.com',
logger: new Logger({ module: 'HCS12' }),
hcs12: {
operatorId: '0.0.123456',
operatorPrivateKey: 'your-private-key'
}
});

// Initialize all registries
await client.initializeRegistries();

Browser SetupDirect link to Browser Setup

import { HCS12BrowserClient, NetworkType } from '@hashgraphonline/standards-sdk';

// Browser client with HashConnect wallet
const browserClient = new HCS12BrowserClient({
network: NetworkType.TESTNET,
mirrorNode: 'https://testnet.mirrornode.hedera.com',
hwc: hashinalsWalletConnectSDK, // Your HashConnect instance
});

API Surface & ModulesDirect link to API Surface & Modules

The HCS-12 SDK module exports the following:

  • Clients
    • HCS12BaseClient (low-level core), HCS12Client (server), HCS12BrowserClient (browser)
  • Composition & UI
    • builders (ActionBuilder, BlockBuilder, AssemblyBuilder)
    • rendering (template engine, block renderer, state/resource managers)
  • Registries & Types
    • registries (discovery/catalogue helpers)
    • types (canonical type definitions)
  • Validation & WASM
    • validation (zod schemas + helpers, WasmValidator)
    • wasm (utilities for working with WASM content)
  • constants

See the dedicated pages in this section for deeper coverage (actions, blocks, assembly, rendering, registries, wasm, security). The validation page documents schemas and the WasmValidator contract.


Core ArchitectureDirect link to Core Architecture

HashLinks consists of three main components that work together:

1. Actions (WebAssembly Logic)Direct link to 1. Actions (WebAssembly Logic)

What they do:

  • Execute deterministic business logic
  • Process transactions without smart contract fees
  • Validate parameters and return results
  • Run in sandboxed environments for security
// Action module interface
interface WasmInterface {
INFO(): string; // Module metadata
POST(...): Promise<string>; // State-changing operations
GET(...): Promise<string>; // Read-only operations
}

2. Blocks (UI Components)Direct link to 2. Blocks (UI Components)

What they do:

  • Define user interface elements
  • Store templates separately for flexibility
  • Support nested composition
  • Bind to actions for interactivity
// Block structure
const block = {
name: 'hashlink/counter',
template_t_id: '0.0.789', // Template stored via HCS-1
attributes: {
count: { type: 'number', default: 0 }
}
};

3. Assemblies (Composition Layer)Direct link to 3. Assemblies (Composition Layer)

What they do:

  • Combine blocks and actions into applications
  • Map action aliases to topic IDs
  • Configure block attributes
  • Enable complex layouts through composition
// Assembly composition
const assembly = {
blocks: [blockId],
actions: { increment: actionId },
attributes: { count: 10 }
};

Validation & WASM ContractDirect link to Validation & WASM Contract

Use the validation helpers to verify registrations and messages before submission:

import {
validateActionRegistration,
validateBlockRegistration,
validateAssemblyMessage,
validateAssemblyRegistration,
validateHashLinksRegistration,
safeValidate,
} from '@hashgraphonline/standards-sdk/hcs-12';

const { valid, errors } = validateBlockRegistration(blockDef);

To ensure compatibility, WASM modules must export at least:

  • INFO() → string with module metadata (name/version/capabilities)
  • POST(...) → state-changing entrypoint
  • GET(...) → read-only entrypoint

The WasmValidator introspects the module and checks required exports and signatures. See the “WASM” and “Validation” pages for details.


Let's create a simple counter application step by step:

Step 1: Create the Action ModuleDirect link to Step 1: Create the Action Module

import { ActionBuilder } from '@hashgraphonline/standards-sdk';

// First, inscribe your WASM module
const wasmBuffer = await fs.readFile('./counter.wasm');
const wasmTopicId = await client.inscribeFile(
wasmBuffer,
'counter.wasm',
{ compress: true }
);

// Build action registration
const actionBuilder = new ActionBuilder(logger)
.setWasmTopicId(wasmTopicId)
.setModuleInfo({
name: 'counter-actions',
version: '1.0.0',
hashlinks_version: '1.0.0',
actions: [
{ name: 'increment', input: 'CounterInput', output: 'CounterOutput' },
{ name: 'decrement', input: 'CounterInput', output: 'CounterOutput' },
{ name: 'reset', input: 'Empty', output: 'CounterOutput' }
],
capabilities: []
});

// Register the action
const actionRegistration = await client.registerAction(actionBuilder);
console.log('Action registered:', actionRegistration.topicId);

Step 2: Create the BlockDirect link to Step 2: Create the Block

import { BlockBuilder } from '@hashgraphonline/standards-sdk';

// Define the block template
const template = `
<div class="counter-block" data-block-id="{{blockId}}">
<h3>{{attributes.label}}</h3>
<div class="counter-display">
<span class="count">{{attributes.count}}</span>
</div>
<div class="counter-controls">
<button data-action-click="{{actions.increment}}">+</button>
<button data-action-click="{{actions.decrement}}">-</button>
<button data-action-click="{{actions.reset}}">Reset</button>
</div>
</div>
`;

// Build block definition
const blockBuilder = new BlockBuilder()
.setName('hashlink/counter')
.setTitle('Counter Block')
.setCategory('interactive')
.addAttribute('count', 'number', 0)
.addAttribute('step', 'number', 1)
.addAttribute('label', 'string', 'Counter')
.setTemplate(Buffer.from(template))
.addAction('increment', actionRegistration.topicId)
.addAction('decrement', actionRegistration.topicId)
.addAction('reset', actionRegistration.topicId);

// Register the block
const blockRegistration = await client.registerBlock(blockBuilder);
console.log('Block registered:', blockRegistration.topicId);

Step 3: Create the AssemblyDirect link to Step 3: Create the Assembly

import { AssemblyBuilder } from '@hashgraphonline/standards-sdk';

// Create assembly that combines block and actions
const assemblyBuilder = new AssemblyBuilder(logger)
.setName('counter-app')
.setVersion('1.0.0')
.setDescription('Simple counter application')
.addBlock(blockRegistration.topicId, {
attributes: {
count: 0,
step: 1,
label: 'My Counter'
},
actions: {
increment: actionRegistration.topicId,
decrement: actionRegistration.topicId,
reset: actionRegistration.topicId
}
});

// Create the assembly
const assemblyTopicId = await client.createAssembly(assemblyBuilder);
console.log('Assembly created:', assemblyTopicId);
// Load and render the assembly
const assembly = await client.loadAssembly(assemblyTopicId);
const renderer = new BlockRenderer(
logger,
client.gutenbergBridge,
client.assemblyEngine,
client.blockStateManager
);

// Render to DOM element
const container = document.getElementById('app');
const result = await renderer.render(assembly.blocks[0], {
container,
assembly,
network: NetworkType.TESTNET
});

// The counter is now live and interactive!

Key ConceptsDirect link to Key Concepts

Registry SystemDirect link to Registry System

What it does:

  • Maintains discoverable directories of components
  • Enables searching and filtering
  • Tracks versions and updates
  • Provides public access to HashLinks
// Create a registry topic
const registryTopicId = await client.createRegistryTopic(
'action', // Registry type: 'action', 'block', 'assembly', or 'hashlinks'
adminKey, // Optional admin key for updates
submitKey // Optional submit key for submissions
);

Format: hcs://[protocol]/[topic-id]

// Examples of HashLink URLs
'hcs://1/0.0.123456' // HCS-1 inscription
'hcs://12/0.0.789012' // HCS-12 assembly
'hcs://20/0.0.345678' // HCS-20 token

Security ModelDirect link to Security Model

Built-in protections:

  • WASM Sandboxing - Isolated execution environment
  • Capability System - Explicit permission requirements
  • Signature Verification - Cryptographic validation
  • Template Sanitization - XSS prevention
  • Validation Rules - Parameter checking

Client TypesDirect link to Client Types

HCS12Client (Server-side)Direct link to HCS12Client (Server-side)

Use when you:

  • Have access to private keys
  • Need full transaction capabilities
  • Run backend services or CLI tools
  • Require admin operations

HCS12BrowserClient (Browser)Direct link to HCS12BrowserClient (Browser)

Use when you:

  • Build web applications
  • Integrate with wallets via HashConnect
  • Need user transaction signing
  • Create interactive DApps

HCS12BaseClient (Abstract)Direct link to HCS12BaseClient (Abstract)

Shared functionality:

  • Registry management
  • Mirror node queries
  • Template rendering
  • Block state management

Factory MethodsDirect link to Factory Methods

The SDK provides convenient factory methods for common patterns:

Display BlocksDirect link to Display Blocks

const displayBlock = BlockBuilder.createDisplayBlock(
'hashlink/stats',
'Statistics Display'
)
.addAttribute('title', 'string', 'Stats')
.addAttribute('data', 'array', [])
.setTemplate(statsTemplate)
.build();

Interactive BlocksDirect link to Interactive Blocks

const interactiveBlock = BlockBuilder.createInteractiveBlock(
'hashlink/form',
'Contact Form'
)
.addAttribute('submitUrl', 'string', '')
.addAction('submit', submitActionId)
.setTemplate(formTemplate)
.build();

Container BlocksDirect link to Container Blocks

const containerBlock = BlockBuilder.createContainerBlock(
'hashlink/layout',
'Grid Layout'
)
.addAttribute('columns', 'number', 2)
.addAttribute('gap', 'string', '1rem')
.setTemplate(gridTemplate)
.build();

Advanced FeaturesDirect link to Advanced Features

Nested CompositionDirect link to Nested Composition

Blocks can contain other blocks through HashLink references:

<!-- Container template with nested blocks -->
<div class="container">
<div data-hashlink="hcs://12/0.0.123456"></div>
<div data-hashlink="hcs://12/0.0.789012"></div>
</div>

State ManagementDirect link to State Management

The SDK includes a built-in state manager:

const stateManager = client.blockStateManager;

// Subscribe to state changes
stateManager.subscribe('counter-1', (state) => {
console.log('Counter updated:', state.count);
});

// Update state after action execution
stateManager.updateState('counter-1', { count: 42 });

Performance OptimizationDirect link to Performance Optimization

Caching strategies:

  • Assembly definitions cached in memory
  • WASM modules cached after first load
  • Templates precompiled for faster rendering
  • Mirror node responses cached with TTL

Error HandlingDirect link to Error Handling

The SDK provides comprehensive error handling:

try {
const result = await client.executeAction({
topicId: '0.0.123456',
action: 'transfer',
params: { amount: 100, to: '0.0.789' }
});

if (result.success) {
console.log('Success:', result.data);
} else {
console.error('Action failed:', result.error);
}
} catch (error) {
if (error.code === 'VALIDATION_ERROR') {
console.error('Invalid parameters:', error.details);
} else if (error.code === 'NETWORK_ERROR') {
console.error('Network issue:', error.message);
} else {
console.error('Unexpected error:', error);
}
}

Best PracticesDirect link to Best Practices

DevelopmentDirect link to Development

  • Use builders for all component creation
  • Validate early with schema validation
  • Handle errors with explicit fallbacks
  • Test locally before mainnet deployment
  • Version modules to enable upgradability

ProductionDirect link to Production

  • Cache to reduce network calls
  • Validate WASM modules before execution
  • Monitor performance with metrics
  • Implement rate limiting for actions
  • Use compression for large templates

SecurityDirect link to Security

  • Sanitize templates to prevent XSS
  • Validate parameters with schemas
  • Check capabilities before execution
  • Verify signatures for authentication
  • Audit actions for compliance

Common IssuesDirect link to Common Issues

Issue: "Module not found: fs"Direct link to Issue: "Module not found: fs"

Solution: Use the browser client in web environments:

// Wrong: Server client in browser
import { HCS12Client } from '@hashgraphonline/standards-sdk';

// Correct: Browser client for web
import { HCS12BrowserClient } from '@hashgraphonline/standards-sdk';

Issue: "WASM execution failed"Direct link to Issue: "WASM execution failed"

Solution: Ensure the module implements the required interface:

// Check module exports
const validator = new WasmValidator();
const report = await validator.validate(wasmBuffer);
if (!report.valid) {
console.error('Missing exports:', report.missingExports);
}

Issue: "Template rendering error"Direct link to Issue: "Template rendering error"

Solution: Validate template syntax:

// Precompile to catch errors early
try {
await templateEngine.precompile('my-template', templateHtml);
} catch (error) {
console.error('Template syntax error:', error.message);
}

Next StepsDirect link to Next Steps

Explore the complete SDK documentation:

  • Actions - Deep dive into WebAssembly modules
  • Blocks - Master UI component creation
  • Assembly - Learn composition patterns
  • Registries - Understand discovery systems
  • Rendering - Explore the rendering pipeline
  • Security - Implement secure HashLinks
  • WASM - WebAssembly execution details

ResourcesDirect link to Resources