Contract-controlled Hedera security-token demo with native HTS compliance, wallet actions, and a Cloudflare Workers frontend.
This repository demonstrates a regulated fungible token on Hedera Token Service (HTS).
The token is not an ERC-20 clone. It is a native HTS token with network-level KYC, freeze, wipe, pause, and supply controls. A small Solidity contract owns the demo authority for those HTS controls through Hedera's precompile, so visitors can connect a wallet and exercise the flow on testnet without any backend signing service.
The frontend is a static React app. There is no Worker API, no server-side private key, and no browser-bundled secret.
Demo sandbox notice
The faucet and compliance methods are intentionally public on testnet. This is a showcase repo: any visitor should be able to connect HashPack, grant/revoke KYC, freeze/unfreeze, wipe demo balances, pause/unpause, and claim demo tokens without asking for an admin key. A production token would put the same HTS authority behind issuer governance, multisig, policy checks, and audit controls.
| Area | Technology |
|---|---|
| Runtime | Node.js 22+ |
| Monorepo | npm workspaces |
| HTS package | TypeScript, Solidity, @hiero-ledger/sdk, solc, ethers |
| Contract library | @hiero-ledger/hiero-contracts |
| Frontend | React 19, Vite, Tailwind CSS v4, HashPack via Hedera WalletConnect |
| UI package | shadcn-style primitives in packages/ui |
| Tests | Vitest |
| Deployment | Hedera testnet, Cloudflare Workers |
- Contract-created HTS fungible token with regulated token keys.
- HashPack-signed token association.
- Public testnet KYC grant/revoke through a contract key.
- Public testnet freeze/unfreeze through a contract key.
- Public testnet regulatory wipe through a contract key.
- Public testnet pause/unpause through a contract key.
- Faucet claim flow that mints HTS supply and transfers demo tokens to the caller.
- Mirror Node reads for token state, transfer gate state, and holder ledger.
This template is built to be easy to evaluate live. The public methods are not an accident; they are the demo surface.
| Choice | Why it exists |
|---|---|
Public faucet claim() |
Lets a visitor mint a fixed amount of testnet RST after association and KYC. No backend signer is required. |
| Public KYC actions | Shows Hedera's native KYC gate directly from the UI. A visitor can grant and revoke eligibility to see transfer state change. |
| Public freeze actions | Demonstrates per-account regulatory controls without writing custom token transfer hooks. |
| Public wipe action | Demonstrates forced balance removal for testnet regulatory scenarios. |
| Public pause actions | Demonstrates token-wide transfer halt and resume through HTS pause key behavior. |
| Contract-held HTS keys | The Solidity controller owns the HTS role keys, so the frontend never stores a private admin key. |
| Static frontend | Cloudflare Workers hosts the demo as static assets. There is no worker entrypoint and no secret-bearing API. |
| HashPack-only wallet UX | The app uses native Hedera transaction signing instead of pretending the token is just another ERC-20. |
For production, the public methods should be replaced with access control and real issuer operations. The important point is that the same HTS primitives remain the core mechanism.
| Field | Value |
|---|---|
| Network | Hedera testnet |
| Token ID | 0.0.9960798 |
| Faucet contract ID | 0.0.9960797 |
| Claim amount | 100 RST |
| Manifest | packages/hts/deployments/testnet/regulated-faucet.json |
packages/hts/contracts/RegulatedHtsFaucet.sol is the demo controller. It creates one HTS token through the Hedera Token Service precompile, sets itself as the HTS role key for admin, KYC, freeze, wipe, supply, and pause, then exposes small public demo methods.
apps/web is a static dApp. It uses HashPack through Hedera WalletConnect for native Hedera transaction signing and Mirror Node REST for read-only state. Cloudflare hosts it as Worker static assets.
Association is deliberately wallet-signed. Hedera token association must be authorized by the holder account, so the frontend builds a native TokenAssociateTransaction and sends it to HashPack for approval.
Compliance actions are also wallet-signed Hedera transactions. The frontend builds ContractExecuteTransaction calls to the faucet contract, and HashPack signs/submits them from the connected account. The Solidity contract owns the HTS role keys, which is why public testnet users can trigger KYC, freeze, wipe, pause, and faucet actions without any server-held admin key.
Mirror Node is used for read-only state: token metadata, holder token relations, KYC status, freeze status, balances, transaction results, and the holder ledger. The ledger auto-refreshes every 45 seconds and also refetches after UI actions to handle Mirror Node indexing delay.
| Concept | In this repo |
|---|---|
| Token ID | The canonical Hedera token identifier, for example 0.0.9960798. This is what the UI shows. |
| Association | A holder must opt into an HTS token before receiving it. The holder signs this with HashPack. |
| KYC | A token-level eligibility flag. Transfers require KYC when the token has a KYC key. |
| Freeze | A per-account transfer lock for this token. |
| Wipe | A regulated removal of tokens from a holder balance. |
| Pause | A token-wide halt of transfers. |
| Treasury | HTS requires a treasury account. In this demo the contract is the treasury because it creates and controls the token. |
| Mirror Node | Hedera's indexed REST data source used by the frontend for display state. |
- Connect HashPack.
- Click
Associate token. - Click
Grant KYC. - Click
Claim 100 RST. - Transfers are now allowed while the holder has KYC, is not frozen, and the token is not paused.
- Try
Freeze,Unfreeze,Wipe,Pause,Unpause,Revoke KYC, andGrant KYCto see native HTS compliance controls reflected through Mirror Node.
| Action | What it does |
|---|---|
Associate token |
Sends a native TokenAssociateTransaction signed by the connected HashPack account. |
Grant KYC |
Calls the faucet controller to grant HTS KYC for a target holder. |
Claim 100 RST |
Mints demo supply and transfers it to the connected holder. |
Revoke KYC |
Removes the holder's transfer eligibility. |
Freeze holder |
Freezes one holder for this token. |
Unfreeze holder |
Clears the holder freeze flag. |
Wipe tokens |
Removes a chosen testnet balance amount from a holder. |
Pause token |
Pauses transfers for the whole HTS token. |
Unpause token |
Resumes transfers. |
Actions that target a holder accept a Hedera account ID such as 0.0.9959030. The frontend resolves the account's EVM alias internally when calling the Solidity precompile wrapper, but the user-facing UI stays Hedera-native.
npm install
npm run check
cp packages/hts/.env.example packages/hts/.env
cp apps/web/.env.example apps/web/.envFor contract deployment, fill packages/hts/.env with a funded Hedera testnet operator:
HEDERA_NETWORK=testnet
HEDERA_OPERATOR_ID=0.0.x
HEDERA_OPERATOR_KEY=...Deploy a new faucet-controlled HTS token:
npm run deploy:faucetThe script deploys the Solidity controller, creates the HTS token, writes packages/hts/deployments/<network>/regulated-faucet.json, and prints the VITE_* values for the frontend.
Set apps/web/.env:
VITE_HEDERA_NETWORK=testnet
VITE_HEDERA_TOKEN_ID=0.0.x
VITE_HTS_FAUCET_CONTRACT_ID=0.0.x
VITE_HTS_FAUCET_CLAIM_AMOUNT=10000
VITE_MIRROR_NODE_REST_URL=https://testnet.mirrornode.hedera.com
VITE_DEMO_HOLDER_ACCOUNT_ID=0.0.x
VITE_WALLETCONNECT_PROJECT_ID=c31d31a72f20e6ff1c43934de17d8664Run the frontend locally:
npm run dev:webOpen the printed Vite URL. The Connect HashPack button pairs with the HashPack browser extension and then sends native Hedera transactions to the wallet for user approval.
VITE_WALLETCONNECT_PROJECT_ID is a public WalletConnect/Reown project id used by the relay transport. It is not a private key and does not grant issuer authority.
Use this flow when presenting the project:
- Open the frontend and connect HashPack on Hedera testnet.
- Go to
Actions. - Run
Associate tokenfor the connected account. - Run
Grant KYCfor the same account. - Run
Claim 100 RST. - Go to
Overviewand watch the transfer gate and holder ledger update. - Run
Freeze holderand show that the transfer gate blocks the account. - Run
Unfreeze holder, thenRevoke KYC, thenGrant KYCagain. - Run
Pause tokenandUnpause tokento show token-wide controls. - Run
Wipe tokenswith a small amount to demonstrate regulated balance removal.
Mirror Node can take a few seconds to index new transactions. The frontend refreshes after actions and the holder ledger auto-refreshes every 45 seconds.
The faucet claim is one-time per wallet. After a wallet has claimed, the UI marks the faucet as claimed and blocks another claim for that wallet.
Static assets on Cloudflare Workers — no worker entrypoint, no server code.
For Cloudflare Workers Builds / static assets, use:
| Setting | Value |
|---|---|
| Root directory | repository root |
| Build command | npm run build |
| Deploy command | npm run deploy:web |
The web app is deployed as Worker static assets, matching the RWA frontend pattern. Do not use wrangler pages deploy in this project; that command targets the Pages API and can fail in Cloudflare Builds with a hidden build token that is not scoped for Pages direct upload.
For local or external CI direct upload, use Wrangler:
npm run build:web
npm run deploy:webCloudflare config lives in apps/web/wrangler.jsonc, uses assets.directory: "./dist", and deploys the hedera-regulated-token static-assets Worker.
Wrangler direct upload needs either an interactive wrangler login session or a Cloudflare Builds token with Workers deploy permissions.
Because the app is fully static, configuration is baked in at build time from VITE_* variables. Set the same public VITE_* variables in the Cloudflare project before building, and rebuild after changing any of them. No Cloudflare Worker secrets are required for the frontend; .dev.vars has no effect, as it only reaches server-side worker code.
| Command | Description |
|---|---|
npm run check |
Builds all packages and runs HTS tests |
npm run deploy:faucet |
Deploys the Solidity faucet controller and creates a new HTS token |
npm run dev:web |
Starts the Vite frontend |
npm run build:web |
Builds the static frontend |
npm run deploy:web |
Builds and deploys the frontend as Cloudflare static assets |
npm run token:info |
Reads token info through the SDK utilities |
.
├── apps/
│ └── web/ # Static React dApp
├── packages/
│ ├── hts/
│ │ ├── contracts/ # Solidity HTS controller
│ │ ├── deployments/ # Testnet deployment manifests
│ │ ├── scripts/ # Hedera deploy and utility scripts
│ │ ├── src/ # SDK helpers and compiler wrapper
│ │ └── tests/ # Vitest coverage
│ └── ui/ # Shared UI primitives
└── README.md
This is a public testnet demo. The faucet contract intentionally exposes role methods so anyone can exercise Hedera compliance behavior.
For production, do not expose KYC, freeze, wipe, pause, or mint methods publicly. Keep the HTS authority behind real issuer governance and audit the contract before mainnet use.
Before adapting this template for mainnet:
- Replace public role methods with issuer-only authorization.
- Put HTS role keys behind multisig, a governance contract, or a transfer-agent operations service.
- Separate faucet behavior from regulated issuance.
- Add policy checks for KYC, jurisdiction, transfer limits, and investor status.
- Add event indexing, monitoring, and operational runbooks.
- Audit the Solidity controller and deployment scripts.
- Use dedicated environments for testnet, staging, and mainnet.