githubEdit

Tutorial: private Adder on Sepolia

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 Demand (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.solarrow-up-right example, extended with Sepolia 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 oraclearrow-up-right 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 descriptionarrow-up-right lists what the shipped MpcAdder omits on purpose.

Prerequisites

  • Solidity toolchain (Foundry or Hardhat) targeting Ethereum Sepolia (where the SDK’s PodUserSepolia Inbox is deployed).

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

  • Sepolia ETH 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 integrationarrow-up-right and Onboarding / account AES keyarrow-up-right docs).

Always confirm Inbox, COTI chain id, and MPC executor against the version of PodUserSepolia.sol in your installed @coti/pod-sdk package; constants can change between releases.

Step 1: Install the SDK

Step 2: Create the PrivateAdder contract

Save as PrivateAdder.sol. The contract:

  • Inherits PodLib and PodUserSepolia (Sepolia defaults for Inbox and COTI routing).

  • 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 startedarrow-up-right 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 Sepolia

Configure remappings so @coti/pod-sdk resolves (Hardhat paths, Foundry remappings.txt, etc.), then compile and deploy PrivateAdder to Ethereum Sepolia. 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 Sepolia-style test usage, pass "testnet" as the network key (see coti-pod-crypto.tsarrow-up-right 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)

PodContractarrow-up-right 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.tsarrow-up-right).

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 checklistarrow-up-right in Getting started.

Tutorial: private Adder on Sepolia

Tutorial: custom privacy logic with PoD

Last updated

Was this helpful?