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
Import from app/lib/contracts.ts β ADDRESSES, ERC20_ABI, PROPERTY_TOKEN_ABI, SECONDARY_MARKET_ABI.
Code Reference
1. Install & Setup
bashInstall Viem and the iExec Nox handle SDK. Wagmi is optional β all transaction flows use window.ethereum directly to avoid MetaMask rate-limiting.
# Core dependencies
npm install viem @tanstack/react-query
# iExec Nox handle client (ERC-7984 encryption)
npm install @iexec-nox/handle2. Create Nox Handle Client
typescriptCreate 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.
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
typescriptCall 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 }.
// 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 call4. Buy Flow β purchaseTokens
typescriptFull 3-step buy flow using window.ethereum directly. Manual receipt polling (every 3 s) avoids MetaMask's rate-limit triggered by viem's waitForTransactionReceipt.
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
typescriptSellers 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.
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 Arbiscan6. Buy Secondary β executeBuy
typescriptTo buy an existing listing: approve USDT for the total cost, then call executeBuy. SecondaryMarket calls confidentialTransferFrom internally β token amounts move encrypted.
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
typescriptTransfer 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.
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
typescriptCast a vote on a ConfidentialGovernance proposal. Voting is gated by PropertyToken holder status (not CEST). Options: 0 = For, 1 = Against, 2 = Abstain.
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
typescriptUse createPublicClient with custom(window.ethereum) to read contract state β no external RPC needed. All listing fields are public; only PropertyToken balances are encrypted.
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_ABIpurchaseTokens(bytes32 handle, bytes handleProof, uint256 clearAmount)voidpublicCore buy β Nox TEE requiredgrantOperator(address operator, uint256 expiry)voidholderGrant SecondaryMarket as operatorpricePerToken()uint256viewUSDT 6 dec β all 5 = 1_000_000propertyId()uint256viewID in PropertyRegistry (1β5)SECONDARY_MARKET_ABIcreateListing(address tokenContract, uint256 propertyId, uint256 tokenAmount, uint256 pricePerToken)uint256 listingIdpublicgrantOperator firstexecuteBuy(uint256 listingId)voidpublicapprove USDT firstcancelListing(uint256 listingId)voidsellerAny time while activelistings(uint256)ListingviewFull listing structGOVERNANCE_ABIcastVote(uint256 proposalId, uint8 option)voidholder0=For 1=Against 2=Abstain β holder-gatedcreateProposal(uint256 propertyId, uint8 proposalType, string description)uint256holderReturns proposalIdfinalizeProposal(uint256 proposalId)voidpublicTallies votes after deadlineERC20_ABI (USDT / CEST)approve(address spender, uint256 amount)boolnonpayableRequired before buy/executeBuyallowance(address owner, address spender)uint256viewCheck before tx to avoid unnecessary approvalbalanceOf(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)