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
- Send encrypted handles to the covalidator
- Covalidator decrypts and returns plaintext values with signatures
- 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 Message | Cause | Solution |
|---|
No handles provided for decryption | Empty handles array [] | Pass at least one handle |
Maximum 10 handles per transaction | More than 10 handles | Split into multiple calls |
Covalidator API request failed: <details> | Covalidator error | Check network; handle may be invalid |
Covalidator returned empty plaintext | Missing plaintext | Handle may be invalid or not found |