Docs / SDK & Integration

SDK & Integration

Step-by-step code for integrating with ChainEstate contracts using Viem and the iExec Nox handle SDK. Transactions go through window.ethereum directly (no wagmi write hooks) to avoid MetaMask RPC rate-limits. See app/properties/[id]/page.tsx and app/market/page.tsx for production usage.

Important: MetaMask must be connected to Arbitrum Sepolia (chain ID 421614). If you see RPC endpoint errors, switch to a healthy Sepolia RPC provider or set NEXT_PUBLIC_RPC_URLto a reliable Arbitrum Sepolia RPC endpoint.

Nox JavaScript SDK

@iexec-nox/handle Overview

The Nox JavaScript SDK provides a simple, secure interface for encrypting data and managing handles with the iExec Nox protocol. It works with both Ethers.js and Viem, supports account abstraction via ERC-4337 smart accounts, and keeps sensitive data encrypted without requiring direct cryptographic handling in your app.

Key Features

  • Easy integration with Viem and Ethers.js
  • Type-safe TypeScript support
  • Gasless encryption via EIP-712 signatures
  • Automatic handle and proof management
  • Secure off-chain data protection using Intel TDX

Core Concepts

  • Handles are 32-byte opaque references to encrypted off-chain data.
  • Handle proofs authenticate the handle using TEE-signed EIP-712 payloads.
  • Access control is enforced on-chain so only authorized wallets can decrypt handles.
  • Single-use handles and proofs must be re-generated for each confidential transaction.

Contract Addresses

NameAddress (Arb Sepolia)Role
USDT (Mock)0x9a822B9A…2d8501ERC-20 stablecoin β€” 6 decimals
PropertyRegistry0xCdBCA38E…C00d5eFactory + holder registry
SecondaryMarket0x77836405…424cfaFixed-price P2P listing marketplace
RentDistributor0x80E0e5f6…f72609Monthly USDT distribution
CESTToken0xC6c08db8…78190DERC20Votes governance token
PEARL-DXB-0010x853D51fB…21a641PropertyToken (example)

Import from app/lib/contracts.ts β€” ADDRESSES, ERC20_ABI, PROPERTY_TOKEN_ABI, SECONDARY_MARKET_ABI.

Code Reference

1. Install & Setup

bash

Install Viem and the iExec Nox handle SDK. Wagmi is optional β€” all transaction flows use window.ethereum directly to avoid MetaMask rate-limiting.

bash
# Core dependencies
npm install viem @tanstack/react-query

# iExec Nox handle client (ERC-7984 encryption)
npm install @iexec-nox/handle

2. Create Nox Handle Client

typescript

Create a viem WalletClient from window.ethereum, then pass it to createViemHandleClient. This client signs EIP-712 messages for the Intel TDX TEE gateway β€” no gas, no transactions.

typescript
import { createWalletClient, custom } from 'viem'
import { arbitrumSepolia } from 'viem/chains'
import { createViemHandleClient } from '@iexec-nox/handle'

// Build inside an async function (after wallet is connected)
const eth = window.ethereum  // MetaMask EIP-1193 provider
const viemWallet = createWalletClient({
  chain: arbitrumSepolia,
  transport: custom(eth),
})

// createViemHandleClient is async β€” must await
const handleClient = await createViemHandleClient(viemWallet)

3. Encrypt a Token Amount

typescript

Call encryptInput with 3 positional args: value, Solidity type, and the contract address that will consume the handle. Gasless β€” signs an EIP-712 message. Returns { handle, handleProof }.

typescript
// encryptInput takes 3 positional args β€” NOT an object
const { handle, handleProof } = await handleClient.encryptInput(
  100n,                              // value: number | bigint
  'uint256',                         // Solidity type string
  '0x853D51fB...' as `0x${string}`  // PropertyToken contract address
)

// handle      β†’ bytes32  (opaque encrypted pointer to "100")
// handleProof β†’ bytes    (Intel TDX TEE attestation)
// Both are single-use β€” re-encrypt before each purchaseTokens call

4. Buy Flow β€” purchaseTokens

typescript

Full 3-step buy flow using window.ethereum directly. Manual receipt polling (every 3 s) avoids MetaMask's rate-limit triggered by viem's waitForTransactionReceipt.

typescript
import { createWalletClient, custom, encodeFunctionData } from 'viem'
import { arbitrumSepolia } from 'viem/chains'
import { createViemHandleClient } from '@iexec-nox/handle'
import { ADDRESSES, ERC20_ABI, PROPERTY_TOKEN_ABI } from '@/app/lib/contracts'

const eth = window.ethereum

// Manual receipt polling β€” avoids MetaMask RPC rate-limits
async function waitForReceipt(hash: string) {
  for (let i = 0; i < 40; i++) {
    await new Promise(r => setTimeout(r, 3000))
    const r = await eth.request({ method: 'eth_getTransactionReceipt', params: [hash] })
    if (r) return r  // r.status === '0x1' success, '0x0' reverted
  }
  throw new Error('Not confirmed after 2 min')
}

// Step 1 β€” Encrypt via Nox TEE (gasless EIP-712 sign)
const viemWallet = createWalletClient({ chain: arbitrumSepolia, transport: custom(eth) })
const handleClient = await createViemHandleClient(viemWallet)
const { handle, handleProof } = await handleClient.encryptInput(
  tokenAmount, 'uint256', propertyTokenAddress as `0x${string}`
)

// Step 2 β€” Approve payment token (USDT / USDC / CEST)
// USDT + USDC: 6 decimals ($1.00 = 1_000_000). CEST: 18 decimals ($0.04/CEST).
// Example: USDT path (default)
const payToken = ADDRESSES.usdt           // or ADDRESSES.usdc, or ADDRESSES.cestToken
const payAmount = BigInt(tokenAmount) * 1_000_000n  // USDT/USDC: 6 dec
// CEST path: BigInt(Math.round(tokenAmount / 0.04)) * 10n**18n
const approveData = encodeFunctionData({
  abi: ERC20_ABI, functionName: 'approve',
  args: [propertyTokenAddress, payAmount],
})
const approveTx = await eth.request({ method: 'eth_sendTransaction',
  params: [{ from: address, to: payToken, data: approveData, gas: '0x13880' }] })
const approveRx = await waitForReceipt(approveTx as string)
if (approveRx.status === '0x0') throw new Error('Approval reverted')

// Step 3 β€” purchaseTokens (encrypted balance stored on-chain)
const buyData = encodeFunctionData({
  abi: PROPERTY_TOKEN_ABI, functionName: 'purchaseTokens',
  args: [handle as `0x${string}`, handleProof as `0x${string}`, BigInt(tokenAmount)],
})
const purchaseTx = await eth.request({ method: 'eth_sendTransaction',
  params: [{ from: address, to: propertyTokenAddress, data: buyData, gas: '0x493E0' }] })
const purchaseRx = await waitForReceipt(purchaseTx as string)
if (purchaseRx.status === '0x0') throw new Error('Purchase reverted')

5. Sell Flow β€” grantOperator + createListing

typescript

Sellers first grant SecondaryMarket as an operator (with expiry), then create a public listing. Token amounts in the listing are public; on-chain balances remain encrypted.

typescript
import { encodeFunctionData } from 'viem'
import { ADDRESSES, PROPERTY_TOKEN_ABI, SECONDARY_MARKET_ABI } from '@/app/lib/contracts'

// Step 1 β€” Grant SecondaryMarket as operator (7-day expiry)
const expiry = BigInt(Math.floor(Date.now() / 1000) + 7 * 24 * 3600)
const grantData = encodeFunctionData({
  abi: PROPERTY_TOKEN_ABI, functionName: 'grantOperator',
  args: [ADDRESSES.secondaryMarket, expiry],
})
const grantTx = await eth.request({ method: 'eth_sendTransaction',
  params: [{ from: address, to: propertyTokenAddress, data: grantData, gas: '0x30D40' }] })
const grantRx = await waitForReceipt(grantTx as string)
if (grantRx.status === '0x0') throw new Error('grantOperator reverted')

// Step 2 β€” Create listing (pricePerToken in USDT 6 decimals: $1.025 = 1_025_000)
const pricePerTokenUsdt = BigInt(Math.round(parseFloat(sellPrice) * 1_000_000))
const listData = encodeFunctionData({
  abi: SECONDARY_MARKET_ABI, functionName: 'createListing',
  args: [
    propertyTokenAddress as `0x${string}`,  // address tokenContract
    BigInt(propertyId),                        // uint256 propertyId
    BigInt(tokenAmount),                       // uint256 tokenAmount
    pricePerTokenUsdt,                         // uint256 pricePerToken
  ],
})
const listTx = await eth.request({ method: 'eth_sendTransaction',
  params: [{ from: address, to: ADDRESSES.secondaryMarket, data: listData, gas: '0x493E0' }] })
const listRx = await waitForReceipt(listTx as string)
if (listRx.status === '0x0') throw new Error('createListing reverted')
// listingId is emitted in the tx logs β€” query via getLogs or Arbiscan

6. Buy Secondary β€” executeBuy

typescript

To buy an existing listing: approve USDT for the total cost, then call executeBuy. SecondaryMarket calls confidentialTransferFrom internally β€” token amounts move encrypted.

typescript
import { encodeFunctionData } from 'viem'
import { ADDRESSES, ERC20_ABI, SECONDARY_MARKET_ABI } from '@/app/lib/contracts'

// totalCost = tokenAmount Γ— pricePerToken (USDT 6 dec, BigInt)
const totalUsdt = listing.tokenAmount * listing.pricePerToken

// Step 1 β€” Approve USDT to SecondaryMarket
const approveData = encodeFunctionData({
  abi: ERC20_ABI, functionName: 'approve',
  args: [ADDRESSES.secondaryMarket, totalUsdt],
})
const approveTx = await eth.request({ method: 'eth_sendTransaction',
  params: [{ from: address, to: ADDRESSES.usdt, data: approveData, gas: '0x13880' }] })
const approveRx = await waitForReceipt(approveTx as string)
if (approveRx.status === '0x0') throw new Error('USDT approval reverted')

// Step 2 β€” Execute buy (0.5% fee deducted)
const buyData = encodeFunctionData({
  abi: SECONDARY_MARKET_ABI, functionName: 'executeBuy',
  args: [BigInt(listingId)],
})
const buyTx = await eth.request({ method: 'eth_sendTransaction',
  params: [{ from: address, to: ADDRESSES.secondaryMarket, data: buyData, gas: '0x61A80' }] })
const buyRx = await waitForReceipt(buyTx as string)
if (buyRx.status === '0x0') throw new Error('executeBuy reverted')

7. Direct Transfer β€” grantOperator to Recipient

typescript

Transfer tokens directly to another wallet using the operator pattern. Grant the recipient as a temporary operator, then they call transferFrom. No listing required β€” this is a private wallet-to-wallet transfer.

typescript
import { encodeFunctionData } from 'viem'
import { PROPERTY_TOKEN_ABI } from '@/app/lib/contracts'
import { PROPERTIES } from '@/app/lib/propertiesData'

const eth = window.ethereum
const address = '0xYOUR_WALLET'

// Resolve property contract address
const property = PROPERTIES.find(p => p.id === 'pearl-dxb-001')
const propertyTokenAddress = property.contractAddress

// Step 1 β€” Grant recipient as operator (7-day window)
const expiry = BigInt(Math.floor(Date.now() / 1000) + 7 * 24 * 3600)
const grantData = encodeFunctionData({
  abi: PROPERTY_TOKEN_ABI, functionName: 'grantOperator',
  args: [recipientAddress as `0x${string}`, expiry],
})
const grantTx = await eth.request({ method: 'eth_sendTransaction',
  params: [{ from: address, to: propertyTokenAddress, data: grantData, gasPrice }] })

// The recipient wallet can now call confidentialTransferFrom for up to 7 days.
// Transfer amounts are encrypted β€” no observer knows how much moved.
// Revoke early by re-calling grantOperator with expiry = 0.

8. Governance β€” castVote

typescript

Cast a vote on a ConfidentialGovernance proposal. Voting is gated by PropertyToken holder status (not CEST). Options: 0 = For, 1 = Against, 2 = Abstain.

typescript
import { encodeFunctionData } from 'viem'
import { ADDRESSES, GOVERNANCE_ABI } from '@/app/lib/contracts'

const eth = window.ethereum
const address = '0xYOUR_WALLET'

// proposalId from ConfidentialGovernance (uint256)
// option: 0 = For, 1 = Against, 2 = Abstain
const voteData = encodeFunctionData({
  abi: GOVERNANCE_ABI, functionName: 'castVote',
  args: [BigInt(proposalId), 0],  // 0 = Vote For
})

const voteTx = await eth.request({ method: 'eth_sendTransaction',
  params: [{ from: address, to: ADDRESSES.confidentialGovernance, data: voteData, gasPrice }] })
const voteRx = await waitForReceipt(voteTx as string)
if (voteRx.status === '0x0') throw new Error('castVote reverted β€” not a holder or proposal closed')

// IMPORTANT:
// - Voting is by PropertyToken holder status (1 holder = 1 vote)
// - CEST tokens do NOT determine voting power in ConfidentialGovernance
// - CEST ERC20Votes is reserved for future DAO delegation (Phase 2)
// - Proposals are created via createProposal(propertyId, proposalType, description)

9. Read a Listing

typescript

Use createPublicClient with custom(window.ethereum) to read contract state β€” no external RPC needed. All listing fields are public; only PropertyToken balances are encrypted.

typescript
import { createPublicClient, custom } from 'viem'
import { arbitrumSepolia } from 'viem/chains'
import { ADDRESSES, SECONDARY_MARKET_ABI } from '@/app/lib/contracts'

const publicClient = createPublicClient({
  chain: arbitrumSepolia,
  transport: custom(window.ethereum),
})

const listing = await publicClient.readContract({
  address: ADDRESSES.secondaryMarket,
  abi: SECONDARY_MARKET_ABI,
  functionName: 'listings',
  args: [BigInt(listingId)],
})

// listing β†’ {
//   listingId:    bigint,
//   seller:       `0x${string}`,
//   tokenContract:`0x${string}`,
//   propertyId:   bigint,
//   tokenAmount:  bigint,
//   pricePerToken:bigint,   // USDT 6 decimals
//   listedAt:     bigint,   // unix timestamp
//   active:       boolean,
// }

ABI Reference

PROPERTY_TOKEN_ABI
FunctionSignatureReturnsAccessNote
purchaseTokens(bytes32 handle, bytes handleProof, uint256 clearAmount)voidpublicCore buy β€” Nox TEE required
grantOperator(address operator, uint256 expiry)voidholderGrant SecondaryMarket as operator
pricePerToken()uint256viewUSDT 6 dec β€” all 5 = 1_000_000
propertyId()uint256viewID in PropertyRegistry (1–5)
SECONDARY_MARKET_ABI
FunctionSignatureReturnsAccessNote
createListing(address tokenContract, uint256 propertyId, uint256 tokenAmount, uint256 pricePerToken)uint256 listingIdpublicgrantOperator first
executeBuy(uint256 listingId)voidpublicapprove USDT first
cancelListing(uint256 listingId)voidsellerAny time while active
listings(uint256)ListingviewFull listing struct
GOVERNANCE_ABI
FunctionSignatureReturnsAccessNote
castVote(uint256 proposalId, uint8 option)voidholder0=For 1=Against 2=Abstain β€” holder-gated
createProposal(uint256 propertyId, uint8 proposalType, string description)uint256holderReturns proposalId
finalizeProposal(uint256 proposalId)voidpublicTallies votes after deadline
ERC20_ABI (USDT / CEST)
FunctionSignatureReturnsAccessNote
approve(address spender, uint256 amount)boolnonpayableRequired before buy/executeBuy
allowance(address owner, address spender)uint256viewCheck before tx to avoid unnecessary approval
balanceOf(address account)uint256viewUSDT: 6 dec. CEST: 18 dec.
transfer(address to, uint256 amount)boolnonpayableDirect transfer (no approval needed)

Key Patterns & Gotchas

USDT has 6 decimals

  • Β·$1.00 = 1_000_000 (uint256)
  • Β·$1.025 = 1_025_000
  • Β·Always use BigInt: BigInt(Math.round(price * 1e6))
  • Β·CEST uses 18 decimals (standard ERC20)

BigInt TypeScript

  • Β·tsconfig: "target": "ES2020" required
  • Β·Literal syntax: 1_000_000n is ES2020+
  • Β·Wagmi args[] must be bigint, not number
  • Β·BigInt(x) for runtime conversion

MetaMask RPC rate-limits

  • Β·viem's waitForTransactionReceipt polls every ~4 s β€” triggers MetaMask rate-limit
  • Β·Fix: poll eth_getTransactionReceipt manually every 3 s via window.ethereum
  • Β·MetaMask also blocks eth_call via custom() transport β€” wrap reads in try/catch
  • Β·receipt.status is raw hex: "0x1" success, "0x0" reverted (not "success"/"reverted")

Nox handle lifetime

  • Β·handle + handleProof are one-time use per tx
  • Β·Re-encrypt before each purchaseTokens call
  • Β·handleProof is TEE attestation β€” cannot be reused
  • Β·Encrypt must happen after wallet is connected

operator grant expiry

  • Β·grantOperator expiry = unix timestamp (seconds)
  • Β·7-day: Date.now()/1000 + 7*24*3600
  • Β·If listing active past expiry, executeBuy reverts
  • Β·Cancel listing before expiry if not selling

ERC-7984 privacy rules

  • Β·Transfers never revert on insufficient balance
  • Β·Events never emit token amounts
  • Β·balanceOf(address) returns euint256 β€” unreadable
  • Β·Only TEE Handle Gateway can decrypt (Intel TDX)