Skip to main content

Attested Reveal

Attested Reveal allows you to decrypt encrypted handles and display the plaintext values off-chain (e.g., show a user’s balance in your UI) without any on-chain transaction.

Import

import { decrypt } from '@inco/solana-sdk/attested-decrypt';
import { hexToBuffer } from '@inco/solana-sdk/utils';

Basic Usage

import { decrypt } from '@inco/solana-sdk/attested-decrypt';

// Decrypt handles and get plaintext values (requires wallet signature)
const result = await decrypt(['handle1', 'handle2'], {
  address: wallet.publicKey,
  signMessage: wallet.signMessage,
});

// Use plaintexts for off-chain display
console.log(result.plaintexts);  // ['150', '50']
Attested Reveal uses the same decrypt() function as Attested Decrypt. The difference is that you only use result.plaintexts for display, without building an on-chain verification transaction.

How It Works

  1. Send encrypted handles to the covalidator
  2. Covalidator decrypts and returns plaintext values with signatures
  3. Display the plaintext values in your UI

Example: Reveal Arithmetic Result

import { encryptValue } from '@inco/solana-sdk/encryption';
import { decrypt } from '@inco/solana-sdk/attested-decrypt';
import { hexToBuffer } from '@inco/solana-sdk/utils';

// Step 1: Perform encrypted operation on-chain
const encryptedA = await encryptValue(BigInt(100));
const encryptedB = await encryptValue(BigInt(50));

const tx = await program.methods
  .testAddition(hexToBuffer(encryptedA), hexToBuffer(encryptedB), 0)
  .accounts({
    authority: wallet.publicKey,
  })
  .rpc();

// Step 2: Get the result handle from transaction logs
const resultHandle = await getHandleFromTx(tx, "Addition result handle:");

// Step 3: Decrypt and display (off-chain only)
// signMessage can be from wallet adapter (e.g., Phantom) or tweetnacl for testing

const result = await decrypt([resultHandle], {
  address: wallet.publicKey,
  signMessage: wallet.signMessage,  // or: async (msg) => nacl.sign.detached(msg, keypair.secretKey)
});

console.log("Result:", result.plaintexts[0]);  // "150"

Example: Reveal Multiple Values

// Get handles from multiple operations
const addHandle = await getHandleFromTx(addTx, "Addition result handle:");
const subHandle = await getHandleFromTx(subTx, "Subtraction result handle:");

// Decrypt multiple handles at once (requires wallet signature)
const result = await decrypt([addHandle, subHandle], {
  address: wallet.publicKey,
  signMessage: wallet.signMessage,
});

console.log("100 + 50 =", result.plaintexts[0]);  // "150"
console.log("100 - 50 =", result.plaintexts[1]);  // "50"

Example: Reveal Comparison Result

const encryptedA = await encryptValue(BigInt(100));
const encryptedB = await encryptValue(BigInt(50));

const tx = await program.methods
  .testComparison(hexToBuffer(encryptedA), hexToBuffer(encryptedB), 0)
  .accounts({
    authority: wallet.publicKey,
  })
  .rpc();

const resultHandle = await getHandleFromTx(tx, "Comparison (A >= B) result handle:");

const result = await decrypt([resultHandle], {
  address: wallet.publicKey,
  signMessage: wallet.signMessage,
});

// Boolean results: "1" = true, "0" = false
const isGreaterOrEqual = result.plaintexts[0] === "1";
console.log("100 >= 50:", isGreaterOrEqual);  // true

Example: Reveal e_select Result

const encryptedA = await encryptValue(BigInt(100));
const encryptedB = await encryptValue(BigInt(50));
const encryptedIfTrue = await encryptValue(BigInt(999));
const encryptedIfFalse = await encryptValue(BigInt(0));

const tx = await program.methods
  .testEselect(
    hexToBuffer(encryptedA),
    hexToBuffer(encryptedB),
    hexToBuffer(encryptedIfTrue),
    hexToBuffer(encryptedIfFalse),
    0
  )
  .accounts({
    authority: wallet.publicKey,
  })
  .rpc();

const resultHandle = await getHandleFromTx(tx, "Select result handle:");

const result = await decrypt([resultHandle], {
  address: wallet.publicKey,
  signMessage: wallet.signMessage,
});

console.log("select(100>=50, 999, 0) =", result.plaintexts[0]);  // "999"

Client-Side Integration

import { useWallet } from '@solana/wallet-adapter-react';
import { decrypt } from '@inco/solana-sdk/attested-decrypt';
import { useState } from 'react';

function RevealValue({ handle }: { handle: string }) {
  const wallet = useWallet();
  const [value, setValue] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const reveal = async () => {
    if (!wallet.publicKey || !wallet.signMessage) {
      setError('Please connect your wallet');
      return;
    }

    setLoading(true);
    setError(null);
    try {
      const result = await decrypt([handle], {
        address: wallet.publicKey,
        signMessage: wallet.signMessage,
      });
      setValue(result.plaintexts[0]);
    } catch (err) {
      setError('Failed to reveal value');
      console.error('Reveal failed:', err);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="p-4 border rounded-lg">
      {value !== null ? (
        <p className="text-2xl font-bold">{value}</p>
      ) : (
        <p className="text-gray-500">Value hidden</p>
      )}

      <button
        onClick={reveal}
        disabled={loading}
        className="mt-4 px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50"
      >
        {loading ? 'Revealing...' : 'Reveal Value'}
      </button>

      {error && <p className="mt-2 text-red-500">{error}</p>}
    </div>
  );
}

Getting Handles

There are two ways to get encrypted handles for decryption:

Option 1: From Transaction Logs

When your program emits handles in logs after an operation:
async function getHandleFromTx(
  txSignature: string,
  logPrefix: string
): Promise<string> {
  // Wait for transaction to be confirmed
  await new Promise(resolve => setTimeout(resolve, 2000));

  const txDetails = await connection.getTransaction(txSignature, {
    commitment: "confirmed",
    maxSupportedTransactionVersion: 0,
  });

  const logs = txDetails?.meta?.logMessages || [];

  for (const log of logs) {
    if (log.includes(logPrefix)) {
      const match = log.match(/(\d+)/);
      if (match) return match[1];
    }
  }

  throw new Error(`Handle not found in logs for prefix: ${logPrefix}`);
}

// Usage
const handle = await getHandleFromTx(txSignature, "Addition result handle:");
const result = await decrypt([handle], {
  address: wallet.publicKey,
  signMessage: wallet.signMessage,
});

Option 2: From On-Chain Account (e.g., Token Balance PDA)

When the encrypted handle is stored in an account (like a confidential token balance):
import { PublicKey } from '@solana/web3.js';

// Derive the token account PDA
const [tokenAccountPda] = PublicKey.findProgramAddressSync(
  [
    Buffer.from("token_account"),
    mintPubkey.toBuffer(),
    ownerPubkey.toBuffer(),
  ],
  INCO_TOKEN_PROGRAM_ID
);

// Fetch the account data
const accountInfo = await connection.getAccountInfo(tokenAccountPda);

if (!accountInfo) {
  throw new Error("Token account not found");
}

// Parse the account data to extract the encrypted balance handle
// The offset depends on your account structure
const accountData = program.coder.accounts.decode(
  "TokenAccount",
  accountInfo.data
);

// Get the encrypted balance handle (stored as u128 or similar)
const balanceHandle = accountData.encryptedBalance.toString();

// Decrypt the balance
const result = await decrypt([balanceHandle], {
  address: wallet.publicKey,
  signMessage: wallet.signMessage,
});
console.log("Decrypted balance:", result.plaintexts[0]);

Example: Reveal Confidential Token Balance

import { decrypt } from '@inco/solana-sdk/attested-decrypt';
import { PublicKey } from '@solana/web3.js';

async function revealTokenBalance(
  connection: Connection,
  mint: PublicKey,
  owner: PublicKey,
  signMessage: (message: Uint8Array) => Promise<Uint8Array>
): Promise<string> {
  // Derive Associated Token Account (ATA) PDA
  const [ata] = PublicKey.findProgramAddressSync(
    [
      owner.toBuffer(),
      INCO_TOKEN_PROGRAM_ID.toBuffer(),
      mint.toBuffer(),
    ],
    ASSOCIATED_TOKEN_PROGRAM_ID
  );

  // Fetch and parse account
  const accountInfo = await connection.getAccountInfo(ata);
  if (!accountInfo) {
    throw new Error("Token account not found");
  }

  const tokenAccount = program.coder.accounts.decode(
    "IncoTokenAccount",
    accountInfo.data
  );

  // Extract encrypted balance handle
  const balanceHandle = tokenAccount.amount.toString();

  // Decrypt (wallet signature required)
  const result = await decrypt([balanceHandle], {
    address: owner,
    signMessage, // Pass signMessage function from wallet
  });
  return result.plaintexts[0];
}

// Usage
const balance = await revealTokenBalance(
  connection,
  mintPubkey,
  wallet.publicKey,
  wallet.signMessage
);
console.log("Your balance:", balance);

API Reference

decrypt(handles, options)

Decrypts encrypted handles and returns plaintext values. Parameters:
  • handles: string[] - Array of encrypted handles as decimal strings (max 10)
  • options: DecryptOptions - Wallet authentication options
Handles must be passed as decimal string representations (e.g., "123456789012345678901234567890"), not hex strings. When reading a handle from an on-chain account (stored as u128), convert it to a decimal string using .toString().
Returns: Promise<DecryptResult>

Types

DecryptOptions

interface DecryptOptions {
  address: PublicKey | string;  // Wallet public key (base58 string or PublicKey)
  signMessage: (message: Uint8Array) => Promise<Uint8Array>;  // Message signing function
}
The signMessage function can come from:
  • Wallet adapter (e.g., Phantom): wallet.signMessage
  • tweetnacl for testing: async (msg) => nacl.sign.detached(msg, keypair.secretKey)
For complete examples, see the lightning-rod-solana tests.

DecryptResult

interface DecryptResult {
  plaintexts: string[];      // Decrypted values as strings
  handles: string[];         // Original handles
  signatures: string[];      // Ed25519 signatures (base58)
  ed25519Instructions: TransactionInstruction[];  // For on-chain verification (not used in Reveal)
}

Error Handling

import { decrypt, AttestedDecryptError } from '@inco/solana-sdk/attested-decrypt';

try {
  const result = await decrypt(handles, {
    address: wallet.publicKey,
    signMessage: wallet.signMessage,
  });
} catch (error) {
  if (error instanceof AttestedDecryptError) {
    console.error('Reveal failed:', error.message);
  }
}

Errors

Error MessageCauseSolution
No handles provided for decryptionEmpty handles array []Pass at least one handle
Maximum 10 handles per transactionMore than 10 handlesSplit into multiple calls
Covalidator API request failed: <details>Covalidator errorCheck network; handle may be invalid
Covalidator returned empty plaintextMissing plaintextHandle may be invalid or not found