Lifecycle
Run this flow on Ethereum Sepolia. For all deployments, see Stack.
Same host loop as Sandbox · Balance · Deposit · Receive · Send · Withdraw.
Deposit → private transfer → withdraw
| Step | Product motion | SDK |
|---|---|---|
| 1 | Create plugin | createTenebraePlugin — Initialize |
| 2 | Unlock | Persist private unlock material into host storage (product Auth / client unlock) |
| 3 | Read private balance | plugin.balance([assetId]) after indexer sync |
| 4 | Deposit into Tenebrae | prepareShield → provider.sendTransaction → wait for receipt |
| 5 | Receive within Tenebrae | Host Auth material → createTransferLink → share URL out of band (discovery only) |
| 6 | Transfer within Tenebrae | Paste URL → resolveTransferLinkSenderView → prepareTransferViaTransferLink → broadcastPrivateOperation |
| 7 | Withdraw from Tenebrae | prepareUnshield(asset, to) → public payout Address → broadcastPrivateOperation → wait |
| 8 | Refresh | balance again — host owns when to re-sync |
Transfer-link handoff (binding). createTransferLink returns a shareable discovery URL. The sender Host pastes that
URL, calls resolveTransferLinkSenderView (GET ticket from publicAppOrigin), then
prepareTransferViaTransferLink. Do not pass the URL string into
prepareTransfer(asset, to).
prepareTransfer(asset, to) remains the Kohaku Address verb for self-transfer only (to = connected Host
wallet). Cross-wallet private send uses resolveTransferLinkSenderView + prepareTransferViaTransferLink.
Withdraw. prepareUnshield(asset, to) takes a public payout Address (amount public on-chain).
How-tos: deposit · private transfer · withdraw · create transfer link.
const usdc: AssetId = { __type: 'erc20', contract: SEPOLIA_USDC };
const amount: AssetAmount = { asset: usdc, amount: 1_000_000n }; // 1 USDC × 10^6
await plugin.balance([usdc]);
// Deposit into Tenebrae — amount public on-chain
const shieldOp = await plugin.prepareShield(amount);
const shieldTx = await provider.sendTransaction(shieldOp);
await waitForTransactionReceipt(shieldTx);
// Receive (recipient Host) — URL encoding only; Host Auth owns hash derivation
const { transferLink } = await plugin.createTransferLink({
recipientAssetTransferLinkHashHex: RECIPIENT_ASSET_TRANSFER_LINK_HASH_HEX,
});
console.log(transferLink); // share out of band (QR / copy). Discovery only — not privacy.
// Transfer (sender Host) — paste transferLink → resolveTransferLinkSenderView → prepareTransferViaTransferLink
const { assetTransferLink } = await plugin.resolveTransferLinkSenderView({
transferLink,
});
const transferOp = await plugin.prepareTransferViaTransferLink({
asset: amount,
assetTransferLink,
});
const transferTx = await plugin.broadcastPrivateOperation(transferOp);
await waitForTransactionReceipt(transferTx);
// Withdraw from Tenebrae — public payout Address (amount public on-chain)
const unshieldOp = await plugin.prepareUnshield(amount, WALLET_ADDRESS_A);
const unshieldTx = await plugin.broadcastPrivateOperation(unshieldOp);
await waitForTransactionReceipt(unshieldTx);
await plugin.balance([usdc]);
createTenebraePlugin
→ unlock material in host storage
→ balance
→ prepareShield → sendTransaction → receipt → balance
→ createTransferLink → share URL out of band
→ paste URL → resolveTransferLinkSenderView → prepareTransferViaTransferLink → broadcast → receipt → balance
→ prepareUnshield(asset, payoutAddress) → broadcastPrivateOperation → receipt → balance
Sandbox note. Docs Prepare runs prepare / balance on the lab API. Sketch lines for sendTransaction and
broadcastPrivateOperation teach full host settle; they are not live spend on the docs origin. Send teaches
resolveTransferLinkSenderView → prepareTransferViaTransferLink (orientation ticket stands in for live GET).
Withdraw = public payout Address.