Skip to main content

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

StepProduct motionSDK
1Create plugincreateTenebraePluginInitialize
2UnlockPersist private unlock material into host storage (product Auth / client unlock)
3Read private balanceplugin.balance([assetId]) after indexer sync
4Deposit into TenebraeprepareShieldprovider.sendTransaction → wait for receipt
5Receive within TenebraeHost Auth material → createTransferLinkshare URL out of band (discovery only)
6Transfer within TenebraePaste URL → resolveTransferLinkSenderViewprepareTransferViaTransferLinkbroadcastPrivateOperation
7Withdraw from TenebraeprepareUnshield(asset, to) → public payout Address → broadcastPrivateOperation → wait
8Refreshbalance 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 resolveTransferLinkSenderViewprepareTransferViaTransferLink (orientation ticket stands in for live GET). Withdraw = public payout Address.

Next