For the complete documentation index, see llms.txt. This page is also available as Markdown.

Tutorial: private Adder on Avalanche Fuji

This walkthrough is the primitive-only path: your host-chain contract calls PodLib helpers (the SDK surface for MpcLib-style primitives) and never deploys custom Solidity on COTI. If you are unsure whether that is enough for your product, read Tutorials: building Privacy on Avalanche (PoD) dApps first.

This guide shows how to build a minimal Privacy on Demand dApp that adds two encrypted integers on COTI and stores the encrypted sum on your EVM contract. It follows the same ideas as the SDK’s MpcAdder.sol example, extended with Avalanche Fuji routing presets and request correlation suitable for a real UI.

For background on async flows and fees, see Async private operations, How do PoA fees work?, and the SDK’s Fees, gas, and oracle page.

Writing a PoD example

In this example we will do the following:

  1. Use one built-in executor operation from PodLib (the sample below uses add256) so you do not write a custom COTI contract yet.

  2. Submit a payable request that forwards msg.value and splits out callbackFeeLocalWei for the return leg (two-way Inbox message).

  3. Implement a success callback that decodes abi.encode(ctUint256) and stores the ciphertext.

  4. Wire onDefaultMpcError.selector so failed remote runs surface through the SDK’s default error path (and emit ErrorRemoteCall from PodUser).

After that works, you harden for production: per-user request ownership, explicit pending / completed / failed state, fee estimation via the Inbox, and tests for under-funded sends. The SDK’s Examples with description lists what the shipped MpcAdder omits on purpose.

Prerequisites

Complete Getting started on Avalanche Fuji (Day 0) first (wallet on 43113, test AVAX, Hardhat/Foundry Fuji network, SnowScan smoke-test).

  • Solidity toolchain (Foundry or Hardhat) targeting Avalanche Fuji C-Chain (where the SDK’s PodUserFuji Inbox is deployed).

  • Node.js 18+ for scripts and fetch used by encryption helpers.

  • Fuji AVAX for deployment and for msg.value on each add call (plus gas).

  • User onboarding so your client can obtain an account AES key for decryption (see the SDK’s TypeScript integration and Onboarding / account AES key docs).

Always confirm Inbox, COTI chain id, and MPC executor against PodUserFuji.sol / PodNetworkConstants.sol in your installed @coti-io/coti-contracts package; constants can change between releases.

Step 1: Install packages

@coti-io/pod-sdk is TypeScript only. Solidity imports come from @coti-io/coti-contracts.

Step 2: Create the PrivateAdder contract

Save as PrivateAdder.sol. The contract:

  • Inherits PodLib and PodUserFuji (Fuji Inbox + COTI Testnet routing are set in the PodUserFuji constructor — do not call setInbox / configureCoti again).

  • Calls add256 with the caller’s encrypted inputs and your callback selector.

  • Resolves requestId in the callback the same way as the SDK’s Getting started example.

Notes:

  • onDefaultMpcError is implemented on PodLibBase and forwards failures to ErrorRemoteCall on PodUser. Your UI can listen for that event to mark a request failed.

  • addCallback must stay onlyInbox so random accounts cannot forge results.

Step 3: Compile and deploy on Avalanche Fuji

Configure remappings / paths so @coti-io/coti-contracts resolves (Hardhat or Foundry), then compile and deploy PrivateAdder to Avalanche Fuji C-Chain. Record the deployed address for scripts.

Step 4: Budget msg.value and callbackFeeLocalWei

Two-way Inbox traffic needs enough native token to cover outbound execution and the callback. Use the deployed Inbox’s calculateTwoWayFeeRequiredInLocalToken (or your operator’s runbook) to pick msg.value and callbackFeeLocalWei. Undershooting typically leaves the request stuck or failing.

Step 5: Encrypt the two summands (TypeScript)

CotiPodCrypto.encrypt calls the PoD encryption service. For Fuji-style test usage, pass "testnet" as the network key (see coti-pod-crypto.ts in the SDK: testnet maps to the COTI testnet encryption endpoint).

Use DataType.itUint256 when you build itUint256 calldata yourself (for example with ethers.Contract). If you use PodContract.encryptAndCallMethod in the next step, you can skip manual encryption: pass plaintext numeric strings and DataType.itUint256 in each PodMethodArgument, and the SDK encrypts before encoding the transaction.

Step 6: Submit the add transaction (PodContract, fees, extractRequestIds)

PodContract wraps your ethers.Contract: it **estimateFee**s against the Inbox, maps PodMethodArgument values (including encryptAndCallMethod encryption for it* types), injects the callBackFee into the slot marked isCallBackFee: true, sends value: totalFee on payable functions, and exposes extractRequestIds(txHash) to read requestId values from MessageSent logs on the Inbox (reliable across layouts where parsing logs from the app contract alone is brittle).

Tune forwardGasLimit, callBackGasLimit, and callBackDataSize from measurements on your contract; estimateFee requires callBackGasLimit and callBackDataSize together or neither. For callMethod instead of encryptAndCallMethod, supply JSON ciphertext strings for it* arguments (what the encryption service returns), for example when the browser already called CotiPodCrypto.encrypt.

Alternative (raw ethers.Contract) — If you are not using PodContract, call add with toitUint256(encA), toitUint256(encB), callbackFeeLocalWei, and { value: totalWei }, then still use extractRequestIds for correlation:

Private addition is asynchronous: the sum appears only after the Inbox invokes addCallback in a later transaction. Poll statusByRequest(requestId) or wait for AddCompleted.

Step 7: Read the encrypted sum and decrypt locally

After status is Completed, read sumByRequest(requestId). The value is ctUint256 (ciphertext), not plaintext.

CotiPodCrypto.decrypt delegates to @coti-io/coti-sdk-typescript and expects a scalar ciphertext as a hex string for Uint64, plus the user’s AES key (see SDK source coti-pod-crypto.ts).

Step 8: Sanity checks and next steps

  • Callback decode must stay (ctUint256) — changing the executor op or COTI-side behavior without updating the decode tuple will corrupt storage reads.

  • Type lane — This contract uses add256 with itUint256 / ctUint256 on chain. CotiPodCrypto.decrypt still takes a DataType for the scalar decode; keep DataType.Uint64 (or Uint256, etc.) aligned with how your app and onboarding produce the ciphertext for this flow, per your installed SDK.

  • Production: add tests for non-Inbox callers on addCallback, under-funded msg.value, and decrypt failures; follow the first production checklist in Getting started.

Tutorial: private Adder on Avalanche Fuji

Tutorial: custom privacy logic with PoD

Last updated

Was this helpful?