Zum Hauptinhalt springen
Echtzeit-Radar & Feeds
Alle RSS Feeds ➔
👥 Community & Social
•
IT Security NachrichtenAccount recovery best practices for users(27.09.2026 um 08:31 Uhr)
•••
IT NachrichtenToday’s NYT Mini Crossword Answers for Tuesday, Sept. 29(29.09.2026 um 04:18 Uhr)
••
AI & KI NachrichtenOpenAI axes next model citing safety issues(29.09.2026 um 02:48 Uhr)
•••••
IT Security NachrichtenAccount recovery best practices for users(27.09.2026 um 08:31 Uhr)
•••
IT NachrichtenToday’s NYT Mini Crossword Answers for Tuesday, Sept. 29(29.09.2026 um 04:18 Uhr)
••
AI & KI NachrichtenOpenAI axes next model citing safety issues(29.09.2026 um 02:48 Uhr)
••••
Intelligence View
⚡ tsecurity.de Intelligence

[Tutorial] Building an Unshielded Token dApp with UI

Building an unshielded token DApp with a working frontend. 📁 Full Source Code: midnight-apps/unshielded-token We will keep it simple and use React VITE Ts as it is compatible with most of midnight packages, so the first thing we do is se…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

Building an unshielded token DApp with a working frontend.



📁 Full Source Code: midnight-apps/unshielded-token



We will keep it simple and use React VITE Ts as it is compatible with most of midnight packages, so the first thing we do is setup a basic wallet connection and for this we will also make sure to use the latest version of Dapp Connector V4



Be sure to have a package.json that's compatible





You can follow this official midnight network wallet connection guide on how to setup a basic wallet connection for reference check useWallet.ts how i handled the edge cases (I'll do a deep dive on wallet in a later post)



With the frontend ready to connect, i had some work to do on the contract side. Here are three core circuits i used to handle the native mint for the unshielded token vault lifecycle.



Natively Minting a Stablecoin into the vault with mintUnshieldedToken



We use a padded string for the domain to define the token standard in this case "stablecoin:usd"




export circuit mintToContract(amount: Uint<64>): Bytes<32> {
const domain = pad(32, "stablecoin:usd");
const color = mintUnshieldedToken(
disclose(domain),
disclose(amount),
left<ContractAddress, UserAddress>(kernel.self())
);
totalSupply = totalSupply + disclose(amount) as Uint<64>;
return color;
}







Note : We have to cast amount Uint<64> when updating totalSupply




Transferring with sendUnshielded from vault



To move tokens, sendTouser requires us to reconstruct the color using the same domain and contract's address (kernel.self())




export circuit sendToUser(amount: Uint<64>, userAddr: UserAddress): [] {
const domain = pad(32, "stablecoin:usd");
const color = tokenType(disclose(domain), kernel.self());
sendUnshielded(
color,
disclose(amount) as Uint<128>,
right<ContractAddress, UserAddress>(disclose(userAddr))
);
}






Depositing into vault with receiveUnshielded



For receiveTokens circuit you need to be careful with bit sizes unlike the mint function receiveUnshielded strictly requires a Uint<128> for amount




export circuit receiveTokens(amount: Uint<128>): [] {
const domain = pad(32, "stablecoin:usd");
const color = tokenType(disclose(domain), kernel.self());
receiveUnshielded(color, disclose(amount));
}






See full contract code Contract.compact









Compiling the Contract



Now we will have to compile the contract so we can use its artifacts in the frontend [Verifier/Prover, ZKir..].



First install compact dev tools




curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh






Then compile




compact compile contracts/Contract.compact contracts/managed/stablecoin







Note : SKIP this step if you want to clone my repo because if you generate new keys you would need to redeploy again, as old keys in this path would no longer be usable for frontend unless you download them from github again (In case you wanted to use my contract) i have already deployed my own contract on Prepod network db5d7cb3ed5ab23217abedb86831f6f5b23a9179e91e48dab88d819ef41b8e6d




If you do decide to recompile and redeploy run:




MNEMONIC="24 secret seed phrase from lace or 1am" npx tsx scripts/go.ts












Frontend Integration



So now we have a ready contract deployed on Prepod, the next steps is to integrate to the frontend and as you can see in the screenshot the features we want to cover are:




  1. Contract operations : Mint Tokens into vault, Send tokens from vault to an address, Deposit tokens into the vault.

  2. Statistics : Total Supply, Contract (Vault) Balance, Wallet Balance

  3. User Wallet : Basic OP such as doing basic transfer from your own wallet to another wallet and displaying your receiving address and balance.











1. Contract operations



Before we do anything, we need to make sure that Contract Providers are fully setup





  • privateStateProvider has levelPrivateStateProvider for persistent localstorage


  • publicDataProvider Used for onchain read state on the indexer


  • zkConfigProvider Loads FetchZkConfigProvider <- Compiled Verifiers..


  • proofProvider responsible for generating Zero-Knowledge proofs on your proof server


  • walletProvider handles balanceTx via connectedApi.balanceUnsealedTransaction


  • midnightProvider Submits tx via connectedApi.submitTransaction




Note : in this tutorial i rebuilt the providers in every function, in a production env initialize them once and reuse them across all operations.




The function below covers the full lifecycle minting into the vault contract, we can call await mintToContract(BigInt(amount)) in our UI to execute it.




export async function mintToContract(
connectedApi: ConnectedAPI,
coinPublicKey: string,
shieldedAddresses: { shieldedEncryptionPublicKey: string },
amount: bigint,
onSuccess: (txId: string) => void,
onError: (err: string) => void
): Promise<void> {
try {
console.log('[Mint] === Starting mintToContract ===');
console.log('[Mint] Amount:', amount.toString());
console.log('[Mint] Contract:', CONTRACT_ADDRESS);

const mods = await getModules();
const { indexerModule, FetchZkConfigProvider, levelModule, CompiledContract, ledger, proofModule } = mods;

const indexerPublicDataProvider = indexerModule.indexerPublicDataProvider;
const levelPrivateStateProvider = levelModule.levelPrivateStateProvider;
const zkConfigProvider = new FetchZkConfigProvider(window.location.origin + CONTRACT_PATH, fetch.bind(window));
const proofProvider = proofModule.httpClientProofProvider(PROOF_SERVER, zkConfigProvider);

const providers: any = {
privateStateProvider: levelPrivateStateProvider({
midnightDbName: 'midnight-stablecoin-db',
privateStateStoreName: STORE_NAME,
accountId: coinPublicKey,
privateStoragePasswordProvider: () => STORAGE_PASSWORD,
}),
publicDataProvider: indexerPublicDataProvider(INDEXER_HTTP, INDEXER_WS),
zkConfigProvider,
proofProvider,
walletProvider: {
getCoinPublicKey: () => coinPublicKey,
getEncryptionPublicKey: () => shieldedAddresses.shieldedEncryptionPublicKey,
async balanceTx(tx: any) {
const serialized = uint8ArrayToHex(tx.serialize());
const result = await connectedApi.balanceUnsealedTransaction(serialized);
const bytes = hexToUint8Array(result.tx);
return ledger.Transaction.deserialize('signature', 'proof', 'binding', bytes);
},
},
midnightProvider: {
submitTx: async (tx: any): Promise<string> => {
const serialized = uint8ArrayToHex(tx.serialize());
console.log('[Mint] Calling submitTransaction, hex length:', serialized.length);
await connectedApi.submitTransaction(serialized);
return tx.identifiers()[0];
},
},
};

const [{ findDeployedContract }] = await Promise.all([
import('@midnight-ntwrk/midnight-js-contracts'),
]);

const contractModule = await import(CONTRACT_PATH + '/contract/index.js');
const compiledContract = CompiledContract.make('stablecoin', contractModule.Contract).pipe(
CompiledContract.withVacantWitnesses,
CompiledContract.withCompiledFileAssets(CONTRACT_PATH)
);

const contract: any = await findDeployedContract(providers, {
contractAddress: CONTRACT_ADDRESS,
compiledContract,
privateStateId: 'stablecoinState',
initialPrivateState: {},
});

const currentState = contract?.state?.();
console.log('[Mint] Current contract state ledger:', JSON.stringify(currentState?.ledger, null, 2));

console.log('[Mint] Calling contract.callTx.mintToContract...');
const txData = await contract.callTx.mintToContract(amount);
console.log('[Mint] SUCCESS, txId:', txData.public.txId);
console.log('[Mint] Color returned:', txData.private.result);
onSuccess(txData.public.txId);
} catch (err) {
console.error('[Mint] Error:', err);
onError(err instanceof Error ? err.message : String(err));
}
}






When you call




const txData = await contract.callTx.mintToContract(amount);






Each key on callTx corresponds to an exported circuit in our compact circuit which in this case is




export circuit mintToContract(amount: Uint<64>): Bytes<32>






Which uses mintUnshieldedToken called from the inside




Note : The proof generation might take some time before popup appears, in my case im using local proof server at port 6300 so it's fast.






So now we've successfully minted some tokens into our vault contract, we now need to send the tokens from the vault into our own address.



First things first, we must handle how user addresses are encoded so we created a helper function for this it parses Bech32m string and decodes to unshielded address and then returns Raw Bytes because sendTouser circuit expects Bytes<32> field




export async function encodeUserAddress(bech32Address: string): Promise<Uint8Array> {
const mods = await getModules();
const { addressModule } = mods;
const { MidnightBech32m, UnshieldedAddress } = addressModule;

try {
const parsed = MidnightBech32m.parse(bech32Address);
const decoded: any = parsed.decode(UnshieldedAddress, 'preprod');
return decoded.data;
} catch (e) {
console.error('[encodeUserAddress] Error:', e);
throw new Error('Invalid address format');
}
}






Now this function takes user input and runs it through our helper function encodeUserAddress(recipient) and most importantly calls await store.contractSend(params..) calls our circuit sendToUser which has sendUnshielded inside.




  const handleSend = async () => {
if (!amount || !recipient || !connectedApi) return;

const recipientBytes = await encodeUserAddress(recipient);
const store = useWalletStore.getState();
const shieldedAddresses = await connectedApi.getShieldedAddresses();
const coinPublicKey = shieldedAddresses.shieldedCoinPublicKey;

await store.contractSend(
connectedApi,
coinPublicKey,
shieldedAddresses,
BigInt(amount),
recipientBytes,
(txId: string) => {
useWalletStore.getState().setTransactionHash(txId);
useWalletStore.getState().loadWalletState();
},
(errMsg: string) => {
useWalletStore.getState().setError(errMsg);
}
);
};








Now we can try to deposit Our stablecoin token into the vault using receiveUnshielded.



In our frontend we have handleReceive, it functions in a similar way to handleSend .. store.receiveTokens(params..) calls our exported circuit receiveTokens(amount: Uint<128>) which has receiveUnshielded(color, disclose(amount)) inside




   const handleReceive = async () => {
if (!amount || !connectedApi) return;

const store = useWalletStore.getState();
const shieldedAddresses = await connectedApi.getShieldedAddresses();
const coinPublicKey = shieldedAddresses.shieldedCoinPublicKey;

await store.receiveTokens(
connectedApi,
coinPublicKey,
shieldedAddresses,
BigInt(amount),
(txId: string) => {
useWalletStore.getState().setTransactionHash(txId);
useWalletStore.getState().loadWalletState();
},
(errMsg: string) => {
useWalletStore.getState().setError(errMsg);
}
);
};







Note : I know you are seeing me use getShieldedAddresses() Let me explain why. It is the most convenient way to retrieve both keys in one call as it returns shieldedAddress, shieldedCoinPublicKey, and shieldedEncryptionPublicKey.










2. Statistics



The vault contract has a state called Balance, it returns a set of Tokens Balances and what we did here was to iterate through balances array to find how many tokens with our token ID <-- For a token ID to appear you need to execute a mint operation.




export async function getContractBalance(): Promise<bigint> {
try {
const mods = await getModules();
const { indexerModule } = mods;
const indexerPublicDataProvider = indexerModule.indexerPublicDataProvider;
const provider = indexerPublicDataProvider(INDEXER_HTTP, INDEXER_WS);

const contractState = await provider.queryContractState(CONTRACT_ADDRESS);
console.log('[getContractBalance] Contract state balance:', contractState?.balance);

if (!contractState?.balance) return 0n;

for (const [key, value] of contractState.balance.entries()) {
console.log('[getContractBalance] Key:', key, 'Value:', value.toString());
if (key && typeof key === 'object' && 'raw' in key && key.raw === STABLECOIN_TOKEN) {
console.log('[getContractBalance] Found balance:', value.toString());
return value;
}
}

return 0n;
} catch (err) {
console.error('[getContractBalance] Error:', err);
return 0n;
}
}






Now we need to get User's stablecoin balance, we first get connectedApi.getUnshieldedBalances() to get all user wallet balances and then filter the results with balances[STABLECOIN_TOKEN]




export async function getUserStablecoinBalance(connectedApi: ConnectedAPI): Promise<bigint> {
try {
const balances = await connectedApi.getUnshieldedBalances();
const stablecoinBalance = balances[STABLECOIN_TOKEN];
return stablecoinBalance || 0n;
} catch (err) {
console.error('[getUserStablecoinBalance] Error:', err);
return 0n;
}
}






In order to retrieve totalSupply we created a function getContractState() it runs through provider.queryContractState(CONTRACT_ADDRESS); to return the data we need such as totalSupply.




export async function getContractState(): Promise<ContractState> {
try {
const mods = await getModules();
const { indexerModule } = mods;

const indexerPublicDataProvider = indexerModule.indexerPublicDataProvider;
const provider = indexerPublicDataProvider(INDEXER_HTTP, INDEXER_WS);

const contractState = await provider.queryContractState(CONTRACT_ADDRESS);
if (!contractState) {
console.log('[ContractState] No contract state found');
return { totalSupply: 0n, totalBurned: 0n };
}

const contractModule = await import(CONTRACT_PATH + '/contract/index.js');
const ledgerState = contractModule.ledger(contractState.data);

console.log('[ContractState] Ledger totalSupply:', ledgerState.totalSupply.toString());
console.log('[ContractState] Ledger totalBurned:', ledgerState.totalBurned.toString());

return {
totalSupply: ledgerState.totalSupply,
totalBurned: ledgerState.totalBurned,
};
} catch (err) {
console.error('[ContractState] Error:', err);
console.error('[ContractState] Error message:', err instanceof Error ? err.message : String(err));
console.error('[ContractState] Error stack:', err instanceof Error ? err.stack : '');
return { totalSupply: 0n, totalBurned: 0n };
}
}









3. Wallet operations



For displaying user receiving addresses and Stablecoin balance (Read Section 2 for in depth details on how to get user Stablecoin balance)




const unshieldedAddress = await connectedApi.getUnshieldedAddress();
const unshieldedBalances = await connectedApi.getUnshieldedBalances();









import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { useWalletStore } from '../hooks/useWallet';
import { getUserStablecoinBalance } from '../hooks/wallet/services/contractCalls';

export function WalletInfoPage() {
const { connectedApi, addresses } = useWalletStore();
const [balance, setBalance] = useState<bigint | null>(null);
const [copied, setCopied] = useState<string | null>(null);

useEffect(() => {
if (!connectedApi) return;

const fetchBalance = async () => {
const bal = await getUserStablecoinBalance(connectedApi);
setBalance(bal);
};

fetchBalance();
const interval = setInterval(fetchBalance, 15000);
return () => clearInterval(interval);
}, [connectedApi]);

const handleCopy = async (text: string, field: string) => {
await navigator.clipboard.writeText(text);
setCopied(field);
setTimeout(() => setCopied(null), 2000);
};

const formatBalance = (bal: bigint | null): string => {
if (bal === null) return '—';
return bal.toLocaleString();
};

const formatAddress = (addr: string): string => {
if (!addr) return '—';
return addr.length > 24 ? `${addr.slice(0, 12)}...${addr.slice(-12)}` : addr;
};








Now we move to sending the stablecoin token between user wallets, handlesend (different from contractSend one) looks like




const handleSend = async () => {
if (!amount || !recipient) return;
await sendStablecoin(recipient, BigInt(amount));
};






This is much simpler than contract operations because sendStablecoin is available at the wallet level not a circuit call, it uses Dapp Connector V4 API to initiate the transfer and prompt the user.




      await makeTransfer(
connectedApi,
recipient,
amount,
async () => {
setTransactionHash('transfer-success');
await loadWalletState();
},
(err) => {
if (err === 'Disconnected') {
useWalletStore.getState().resetConnection();
setError('Wallet disconnected. Please reconnect.');
} else {
setError(err);
}
}
);






Key differences from contractSend

































contractSend makeTransfer
Funds source Vault funds User funds
Mechanism handleSend Dapp Connector makeTransfer
Address Encode Requires encoding -> Bytes<32>
Passes Bech32m directly
Zk Proofs Required for circuit execution Handled by Wallet







When to use unshielded vs shielded tokens and the privacy tradeoffs




























Unshielded Shielded
Privacy Mechanism None, completely transparent blockchain transactions Zero-Knowledge proofs (Zswap)
Legal Compliance Can be audited for AML Requires keys for selective disclosure
Use cases Compliant Stablecoins.. as required by regulators Confidential transfers..





Why did I choose UnShielded for my Stablecoin?




  1. Regulatory Compliance : Stablecoin issuers typically need to demonstrate full traceability of supply and transfers due to AML (Anti-money laundering) Regulations.


  2. Verifiability : Vault demonstrate a native mint functionality of this Stablecoin and it contains a public state totalSupply thats publicly readable in which regulators can monitor.


  3. Exchange Listings : Many exchanges like Binance.. have decided to delist XMR for this reason, Regulators put legal pressure on Binance to force a delist while tokens like NIGHT did manage to get listed because it is Unshielded precisely







When would i choose Shielded over UnShielded?




  1. Private tokenized securities : where transfers are confidential While specific properties like voting rights remain verifiable.


  2. Regulated industries requiring data minimization : in healthcare industries, frameworks like GDPR, CCPA, and HIPAA requires minimal data disclosure and shielded tokens ensure sensitive information stays in local storage while Zero-knowledge proofs can still confirm eligibility and compliance.


  3. forward secrecy : Even if encryptions keys are compromised in the future, shielded transactions remains private and this is something unshielded transactions cannot offer.





The bottom line : Midnight is multi-modal it does not enforce anything like XMR enforces privacy and BTC enforces transparency. It is permissionless, You can use whatever works for your project.


2. Cyber Threat Intelligence & Forensik

IoC Intelligence (1 Indikatoren)
db5d7cb3ed5ab23217abedb86831f6f5b23a9179e91e48dab88d819ef41b8e6d
CTI Threat Relationship Graph2 Knoten / 1 Relationen
CVE / Incident Software MITRE ATT&CK CWE Weakness IoC
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten [Tutorial] Building an Unshielded Token dApp with UI

Thematisch verwandte Begriffe: Tutorial, Building, Unshielded, Token · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

💬 Kommentare werden geladen…
Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-102265 | PyJWT is a Python implementation of JSON Web Token standards. From 2.13…
Advisory →
tsecurity.de Icon
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag