Skip to main content
This guide demonstrates how to create a seamless private payment flow using Para SDK with Inco’s confidential wrapper. The app allows users to deposit, check balances, send confidential transfers, and withdraw with encrypted amounts.
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:
# 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
To obtain values of NEXT_PUBLIC_PARA_API_KEY and NEXT_PUBLIC_PARA_ENVIRONMENT visit Para.

Installation

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

// 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

// 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

// 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

// 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

// 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

// 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)

// 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

// 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)

// 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

// 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

// 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

// 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

// 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

// 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;
    }
  };

Conclusion

With Para, setting up secure, user-friendly access to Inco Confidential Wrapper contracts is fast and easy.