Only transaction amounts are encrypted. Sender and receiver addresses are not.
Overview
Inco offers confidential transaction capabilities where token amounts are encrypted, providing privacy for:- Confidential Transfers: Token amounts are encrypted. The sender, recipient addresses, and asset are visible, but the transfer amount remains private.
- Private Balance: User balances are stored encrypted and can only be decrypted by the account owner through cryptographic attestation.
Prerequisites
- Node.js 20+
- Next.js project
- pnpm package manager
- A Para API key from the Para Developer Portal
- WalletConnect project ID
Environment Setup
Create a.env file in the project root:
Copy
Ask AI
# Contract Addresses (Base Sepolia)
NEXT_PUBLIC_ERC20_CONTRACT_ADDRESS=0x2a7f20a455b35ea3cff416f71ddb30e0edf5c9fe
NEXT_PUBLIC_ENCRYPTED_ERC20_CONTRACT_ADDRESS=0x3b9cabcf20eda599c5df823f46eb2eabe99bda40
# Inco Configuration
NEXT_PUBLIC_INCO_ENV=demonet
# Para configuration
NEXT_PUBLIC_PARA_API_KEY=your-para-api-key
NEXT_PUBLIC_PARA_ENVIRONMENT=BETA
# WalletConnect (used by Para for external wallets)
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=your-walletconnect-project-id
Installation
Copy
Ask AI
pnpm install @getpara/react-sdk@^2.6.0 @inco/js@^0.7.7 wagmi@^2.19.5 viem@^2.44.4
Para Wallet Integration
1. Configuration Setup
Copy
Ask AI
// src/config/constants.ts
import { Environment } from "@getpara/react-sdk";
export const API_KEY = process.env.NEXT_PUBLIC_PARA_API_KEY ?? "";
export const ENVIRONMENT = (process.env.NEXT_PUBLIC_PARA_ENVIRONMENT as Environment) || Environment.BETA;
if (!API_KEY) {
throw new Error("Missing NEXT_PUBLIC_PARA_API_KEY environment variable");
}
2. Para Provider Setup
Copy
Ask AI
// src/context/para-provider.tsx
"use client";
import { useState } from "react";
import { ParaProvider as Provider } from "@getpara/react-sdk";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { cookieStorage, createStorage, http } from "wagmi";
import { baseSepolia as chain } from "wagmi/chains";
import { createPublicClient } from "viem";
import { API_KEY, ENVIRONMENT } from "@/config/constants";
const BASE_SEPOLIA_RPC_URL = "your-base-sepolia-rpc-url";
export const baseSepolia = {
...chain,
rpcUrls: {
default: {
http: [BASE_SEPOLIA_RPC_URL],
},
},
};
export const publicClient = createPublicClient({
chain: baseSepolia,
transport: http(BASE_SEPOLIA_RPC_URL),
});
export function ParaProvider({
children,
}: {
children: React.ReactNode;
}) {
const [queryClient] = useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>
<Provider
paraClientConfig={{
apiKey: API_KEY,
env: ENVIRONMENT,
}}
externalWalletConfig={{
wallets: ["METAMASK", "COINBASE", "WALLETCONNECT", "RAINBOW"],
evmConnector: {
config: {
chains: [baseSepolia],
transports: {
[chain.id]: http(BASE_SEPOLIA_RPC_URL),
},
storage: createStorage({
storage: cookieStorage,
}),
},
},
walletConnect: {
projectId:
process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID || "",
},
}}
config={{ appName: "Comfy" }}
paraModalConfig={{
authLayout: ["AUTH:FULL", "EXTERNAL:FULL"],
}}
>
{children}
</Provider>
</QueryClientProvider>
);
}
3. Root Layout Integration
Copy
Ask AI
// src/app/layout.tsx
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { ParaProvider } from "@/context/para-provider";
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className="antialiased">
<ParaProvider>{children}</ParaProvider>
</body>
</html>
);
}
4. Connect Wallet Component
Copy
Ask AI
// src/components/connect-wallet.tsx
"use client";
import { useModal } from "@getpara/react-sdk";
import { useAccount } from "wagmi";
export default function ConnectWallet() {
const { openModal } = useModal();
const { address, isConnected } = useAccount();
return (
<div>
{isConnected ? (
<p>Connected to {address}</p>
) : (
<button onClick={() => openModal()}>Connect Wallet</button>
)}
</div>
);
}
Core Features
1. Minting ERC20 Tokens
Copy
Ask AI
// src/components/mint-erc20.tsx
import React from "react";
import { parseUnits } from "viem";
import { useAccount, useWriteContract } from "wagmi";
import { publicClient } from "@/context/para-provider";
const MintERC20 = () => {
const { writeContractAsync } = useWriteContract();
const { address } = useAccount();
const amountWithDecimals = parseUnits("1000", 18);
const mintERC20 = async () => {
const txHash = await writeContractAsync({
address: process.env
.NEXT_PUBLIC_ERC20_CONTRACT_ADDRESS as `0x${string}`,
abi: [
{
inputs: [
{
internalType: "address",
name: "userAddress",
type: "address",
},
{
internalType: "uint256",
name: "amount",
type: "uint256",
},
],
name: "mint",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
],
functionName: "mint",
args: [address, amountWithDecimals],
});
await publicClient?.waitForTransactionReceipt({ hash: txHash });
};
return (
<div>
<h3>Mint ERC20</h3>
<button onClick={mintERC20}>Mint 1000 ERC20</button>
</div>
);
};
export default MintERC20;
2. Balance Checking
Copy
Ask AI
// src/components/encrypted-balance.tsx
import React, { useState } from 'react';
import { type Address } from 'viem';
import { useAccount, useBalance, useWalletClient } from 'wagmi';
import { getEncryptedBalance } from '@/lib/balance-utils';
import { publicClient } from '@/context/para-provider';
const BalanceDisplay = () => {
const { data: walletClient } = useWalletClient();
const { address } = useAccount();
const [encryptedBalance, setEncryptedBalance] = useState('0');
const ERC20Balance = useBalance({
address: address as Address,
token: process.env.NEXT_PUBLIC_ERC20_CONTRACT_ADDRESS as Address,
});
const handleFetchBalance = async () => {
if (address && publicClient && walletClient) {
const result = await getEncryptedBalance({
address,
publicClient,
walletClient,
encryptedERC20Address: process.env.NEXT_PUBLIC_ENCRYPTED_ERC20_CONTRACT_ADDRESS as Address,
incoEnv: process.env.NEXT_PUBLIC_INCO_ENV as 'testnet' | 'devnet',
});
if (result) setEncryptedBalance(result);
}
};
return (
<div>
<h3>ERC20 Balance</h3>
<p>{ERC20Balance?.data?.formatted} {ERC20Balance?.data?.symbol}</p>
<h3>Encrypted Balance</h3>
<p>{encryptedBalance} Tokens</p>
<button onClick={handleFetchBalance}>Fetch Encrypted Balance</button>
</div>
);
};
export default BalanceDisplay;
3. Wrapping Tokens (Deposit)
Copy
Ask AI
// src/components/wrap.tsx
'use client';
import { useAccount, useWalletClient } from 'wagmi';
import { type Address } from 'viem';
import { approveTokens, wrapTokens } from '@/lib/wrap-utils';
import { publicClient } from '@/context/para-provider';
export default function WrapTokens() {
const { data: walletClient } = useWalletClient();
const { address } = useAccount();
const handleWrap = async () => {
if (!walletClient || !address) return;
const amount = '0.01';
// Step 1: Approve tokens
const approveTx = await approveTokens({
walletClient,
address,
amount,
erc20Address: process.env.NEXT_PUBLIC_ERC20_CONTRACT_ADDRESS as Address,
encryptedERC20Address: process.env.NEXT_PUBLIC_ENCRYPTED_ERC20_CONTRACT_ADDRESS as Address,
});
await publicClient?.waitForTransactionReceipt({ hash: approveTx });
// Step 2: Wrap tokens
const wrapTx = await wrapTokens({
walletClient,
address,
amount,
encryptedERC20Address: process.env.NEXT_PUBLIC_ENCRYPTED_ERC20_CONTRACT_ADDRESS as Address,
});
await publicClient?.waitForTransactionReceipt({ hash: wrapTx });
};
return (
<div>
<h3>Wrap Tokens</h3>
<button onClick={handleWrap}>Wrap 0.01 Tokens</button>
</div>
);
}
4. Confidential Transfer
Copy
Ask AI
// src/components/confidential-transfer.tsx
import React, { useState } from "react";
import { Address } from "viem";
import { useAccount, useWalletClient } from "wagmi";
import { confidentialTransfer } from "@/lib/confidential-send";
import { publicClient } from "@/context/para-provider";
const ConfidentialTransfer = () => {
const [recipient, setRecipient] = useState<Address>("0x");
const { address } = useAccount();
const { data: walletClient } = useWalletClient();
const transferConfidentialERC20 = async () => {
const txHash = await confidentialTransfer({
amount: "0.01",
address: address,
recipient: recipient,
walletClient: walletClient,
publicClient: publicClient,
encryptedERC20Address:
process.env.NEXT_PUBLIC_ENCRYPTED_ERC20_CONTRACT_ADDRESS as Address,
incoEnv: "testnet",
});
await publicClient?.waitForTransactionReceipt({ hash: txHash });
};
return (
<div>
<input
type="text"
value={recipient}
onChange={(e) => setRecipient(e.target.value as Address)}
placeholder="Recipient address"
/>
<button onClick={transferConfidentialERC20}>
Transfer 0.01 confidential ERC20 to {recipient}
</button>
</div>
);
};
export default ConfidentialTransfer;
5. Unwrapping Tokens (Withdraw)
Copy
Ask AI
// src/components/unwrap.tsx
import { unwrapTokens } from "@/lib/unwrap-utils";
import React from "react";
import { useAccount, useWalletClient } from "wagmi";
import { publicClient } from "@/context/para-provider";
const Unwrap = () => {
const { data: walletClient } = useWalletClient();
const { address } = useAccount();
const handleUnwrap = async () => {
if (!walletClient || !address) return;
const unwrapTx = await unwrapTokens({
publicClient,
walletClient,
address,
amount: "0.01",
encryptedERC20Address:
process.env.NEXT_PUBLIC_ENCRYPTED_ERC20_CONTRACT_ADDRESS,
incoEnv: process.env.NEXT_PUBLIC_INCO_ENV,
});
};
return (
<div>
<h3>Unwrap</h3>
<button onClick={handleUnwrap}>Unwrap 0.01 Tokens</button>
</div>
);
};
export default Unwrap;
Utility Functions
Inco Lite Utilities
Copy
Ask AI
// src/lib/inco-lite.ts
import { Lightning, AttestedComputeSupportedOps } from '@inco/js/lite';
import { handleTypes } from '@inco/js';
import { baseSepolia } from 'viem/chains';
import { type PublicClient, type WalletClient, bytesToHex, pad, toHex } from 'viem';
type IncoEnv = 'testnet' | 'devnet';
async function getConfig(env: IncoEnv) {
return await Lightning.latest(env, baseSepolia.id);
}
export async function encryptValue({
value,
address,
contractAddress,
env,
}: {
value: bigint;
address: `0x${string}`;
contractAddress: `0x${string}`;
env: IncoEnv;
}): Promise<`0x${string}`> {
const inco = await getConfig(env);
const encrypted = await inco.encrypt(value, {
accountAddress: address,
dappAddress: contractAddress,
handleType: handleTypes.euint256,
});
return encrypted as `0x${string}`;
}
export async function decryptValue({
walletClient,
handle,
env,
}: {
walletClient: WalletClient;
handle: string;
env: IncoEnv;
}): Promise<bigint> {
const inco = await getConfig(env);
const decrypted = await inco.attestedDecrypt(walletClient, [handle]);
return decrypted[0].plaintext.value;
}
export async function attestedCompute({
walletClient,
lhsHandle,
op,
rhsPlaintext,
env,
}: {
walletClient: WalletClient;
lhsHandle: `0x${string}`;
op: AttestedComputeSupportedOps;
rhsPlaintext: bigint;
env: IncoEnv;
}) {
const inco = await getConfig(env);
const result = await inco.attestedCompute(
walletClient,
lhsHandle,
op,
rhsPlaintext
);
const plaintext = result.plaintext;
const attestation = {
handle: result.attestation.handle,
value: encodePlaintext(plaintext),
};
const signatures = result.attestation.covalidatorSignatures.map((sig) =>
bytesToHex(sig)
);
return { plaintext, attestation, signatures };
}
function encodePlaintext(value: unknown): `0x${string}` {
if (typeof value === 'boolean') {
return value ? `0x${'0'.repeat(63)}1` : `0x${'0'.repeat(64)}`;
}
if (typeof value === 'bigint') {
return pad(toHex(value), { size: 32 });
}
throw new Error('Unsupported plaintext type');
}
export async function getFee(env: IncoEnv, publicClient: PublicClient): Promise<bigint> {
const inco = await getConfig(env);
const fee = await publicClient.readContract({
address: inco.executorAddress,
abi: [
{
type: "function",
inputs: [],
name: "getFee",
outputs: [{ name: "", internalType: "uint256", type: "uint256" }],
stateMutability: "pure",
},
],
functionName: "getFee",
});
return fee;
}
Balance Utilities
Copy
Ask AI
// src/lib/balance-utils.ts
import { formatEther, type Address, type PublicClient, type WalletClient } from 'viem';
import { decryptValue } from './inco-lite';
import { baseSepolia } from '@/context/para-provider';
export const getEncryptedBalance = async ({
address,
publicClient,
walletClient,
encryptedERC20Address,
incoEnv,
}: {
address: Address;
publicClient: PublicClient;
walletClient: WalletClient;
encryptedERC20Address: Address;
incoEnv: 'testnet' | 'devnet';
}) => {
try {
if (!publicClient || !walletClient || !address) {
return '0';
}
const encryptedHandle = await publicClient.readContract({
address: encryptedERC20Address,
abi: [
{
inputs: [{ internalType: 'address', name: 'account', type: 'address' }],
name: 'balanceOf',
outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],
stateMutability: 'view',
type: 'function',
},
],
functionName: 'balanceOf',
args: [address],
});
if (encryptedHandle === '0x' + '0'.repeat(64)) {
return '0';
}
const decrypted = await decryptValue({
walletClient: { ...walletClient, chain: baseSepolia },
handle: encryptedHandle,
env: incoEnv,
});
return formatEther(decrypted);
} catch (error) {
console.error('Failed to fetch encrypted balance:', error);
return '0';
}
};
Wrap Utilities
Copy
Ask AI
// src/lib/wrap-utils.ts
import { parseEther, type Address, type WalletClient } from 'viem';
import { baseSepolia } from '@/context/para-provider';
export const approveTokens = async ({
walletClient,
address,
amount,
erc20Address,
encryptedERC20Address,
}: {
walletClient: WalletClient;
address: Address;
amount: string;
erc20Address: Address;
encryptedERC20Address: Address;
}) => {
const amountInWei = parseEther(amount);
return walletClient.writeContract({
address: erc20Address,
account: address,
chain: baseSepolia,
abi: [
{
inputs: [
{ internalType: 'address', name: 'spender', type: 'address' },
{ internalType: 'uint256', name: 'value', type: 'uint256' },
],
name: 'approve',
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
stateMutability: 'nonpayable',
type: 'function',
},
] as const,
functionName: 'approve',
args: [encryptedERC20Address, amountInWei],
});
};
export const wrapTokens = async ({
walletClient,
address,
amount,
encryptedERC20Address,
}: {
walletClient: WalletClient;
address: Address;
amount: string;
encryptedERC20Address: Address;
}) => {
const amountInWei = parseEther(amount);
return walletClient.writeContract({
address: encryptedERC20Address,
account: address,
chain: baseSepolia,
abi: [
{
inputs: [{ internalType: 'uint256', name: 'amount', type: 'uint256' }],
name: 'wrap',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
] as const,
functionName: 'wrap',
args: [amountInWei],
});
};
Confidential Transfer Utilities
Copy
Ask AI
// src/lib/confidential-send.ts
import {
type Address,
type PublicClient,
type WalletClient,
parseEther,
} from 'viem';
import { encryptValue, getFee } from './inco-lite';
import { baseSepolia } from '@/context/para-provider';
export const confidentialTransfer = async ({
amount,
address,
recipient,
walletClient,
publicClient,
encryptedERC20Address,
incoEnv,
}: {
amount: string;
address: Address;
recipient: Address;
publicClient: PublicClient;
walletClient: WalletClient;
encryptedERC20Address: Address;
incoEnv: 'testnet' | 'devnet';
}) => {
try {
const amountInWei = parseEther(amount);
const encryptedAmount = await encryptValue({
value: amountInWei,
address,
contractAddress: encryptedERC20Address,
env: incoEnv,
});
const fee = await getFee(incoEnv, publicClient);
return walletClient.writeContract({
address: encryptedERC20Address,
chain: baseSepolia,
abi: [
{
inputs: [
{ internalType: 'address', name: 'to', type: 'address' },
{ internalType: 'bytes', name: 'encryptedAmount', type: 'bytes' },
],
name: 'transfer',
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
stateMutability: 'payable',
type: 'function',
},
],
functionName: 'transfer',
args: [recipient, encryptedAmount],
account: address,
value: fee,
});
} catch (error) {
console.error('Error in confidential transfer:', error);
throw error;
}
};
Unwrap Utilities
Copy
Ask AI
// src/lib/unwrap-utils.ts
import {
type Address,
type PublicClient,
type WalletClient,
parseEther,
} from 'viem';
import { AttestedComputeSupportedOps } from '@inco/js/lite';
import { attestedCompute } from './inco-lite';
import { baseSepolia } from '@/context/para-provider';
export const unwrapTokens = async ({
publicClient,
walletClient,
address,
amount,
encryptedERC20Address,
incoEnv,
}: {
publicClient: PublicClient;
walletClient: WalletClient;
address: Address;
amount: string;
encryptedERC20Address: Address;
incoEnv: 'testnet' | 'devnet';
}) => {
try {
const amountInWei = parseEther(amount);
const balanceHandle = await publicClient.readContract({
address: encryptedERC20Address,
abi: [
{
inputs: [{ internalType: 'address', name: 'account', type: 'address' }],
name: 'balanceOf',
outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],
stateMutability: 'view',
type: 'function',
},
],
functionName: 'balanceOf',
args: [address],
});
const { attestation, signatures } = await attestedCompute({
walletClient: {...walletClient, chain: baseSepolia},
lhsHandle: balanceHandle as `0x${string}`,
op: AttestedComputeSupportedOps.Ge,
rhsPlaintext: amountInWei,
env: incoEnv,
});
const unwrapTx = await walletClient.writeContract({
address: encryptedERC20Address,
chain: baseSepolia,
abi: [
{
inputs: [
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
{
internalType: 'tuple',
name: 'enoughBalanceDecryptionAttestation',
components: [
{ internalType: 'bytes32', name: 'handle', type: 'bytes32' },
{ internalType: 'bytes32', name: 'value', type: 'bytes32' },
],
type: 'tuple',
},
{ internalType: 'bytes[]', name: 'signature', type: 'bytes[]' },
],
name: 'unwrap',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
],
functionName: 'unwrap',
args: [amountInWei, attestation, signatures],
account: address,
});
await publicClient.waitForTransactionReceipt({
hash: unwrapTx,
confirmations: 5,
});
return unwrapTx;
} catch (error) {
console.error('Unwrap error:', error);
throw error;
}
};