Soda Labs
Soda Labs Docs
User InterfaceHttp proxy

Encrypt to user

A copy-paste example that signs a handle, calls the encrypt-to-user endpoint, and decrypts the response.

This is a copy-paste example that signs a handle, calls /encrypt-to-user, and decrypts the MPC response with your AES key.

Quick start

  • Requirements: Node 18+, a wallet private key, an AES user key from onboarding, the handle you want to decrypt, default chain 84532 (Base Sepolia). Override the proxy with BUBBLE_PROXY_URL if needed.

  • Install dependencies (same as onboarding):

    npm install soda-bubble-sdk ethers ethereumjs-util

Example script (encrypt_to_user.mjs)

import { decrypt } from "soda-bubble-sdk";
import { Wallet, getBytes } from "ethers";

const PRIVATE_KEY = process.env.PRIVATE_KEY;
const USER_AES_KEY = process.env.USER_AES_KEY; // hex from onboarding
const HANDLE = process.env.HANDLE; // bigint as decimal or hex (e.g. 0xabc...)
if (!PRIVATE_KEY || !USER_AES_KEY || !HANDLE) throw new Error("Set PRIVATE_KEY, USER_AES_KEY, and HANDLE env vars");

const CHAIN_ID = Number(process.env.CHAIN_ID || 84532);
const PROXY_URL = process.env.BUBBLE_PROXY_URL || "https://proxy2.bubble.sodalabs.net";
const wallet = new Wallet(PRIVATE_KEY);

const parseHandle = (value) => {
  const asString = String(value);
  return asString.startsWith("0x") ? BigInt(asString) : BigInt(asString);
};

const handleToBytes = (h) => {
  const hex = "0x" + h.toString(16).padStart(64, "0");
  return getBytes(hex);
};

const decryptOutput = (outputB64, userKeyHex) => {
  const encrypted = Buffer.from(outputB64, "base64");
  const keyBytes = new Uint8Array(Buffer.from(userKeyHex, "hex"));

  if (encrypted.length === 32) {
    const cipher = new Uint8Array(encrypted.slice(0, 16));
    const r = new Uint8Array(encrypted.slice(16, 32));
    return decrypt(keyBytes, r, cipher);
  }
  if (encrypted.length === 64) {
    const cipher1 = new Uint8Array(encrypted.slice(0, 16));
    const r1 = new Uint8Array(encrypted.slice(16, 32));
    const cipher2 = new Uint8Array(encrypted.slice(32, 48));
    const r2 = new Uint8Array(encrypted.slice(48, 64));
    return decrypt(keyBytes, r1, cipher1, r2, cipher2);
  }
  throw new Error(`Unexpected output length: ${encrypted.length}`);
};

const toBigInt = (bytes) => {
  let result = 0n;
  for (const b of bytes) result = (result << 8n) | BigInt(b);
  return result;
};

const main = async () => {
  const handleBigInt = parseHandle(HANDLE);
  const handleBytes = handleToBytes(handleBigInt);

  // Sign handle bytes
  const signature = await wallet.signMessage(handleBytes);

  // Call /encrypt-to-user
  const body = {
    handle: Buffer.from(handleBytes).toString("base64"),
    chain_id: CHAIN_ID,
    user_signature: Buffer.from(getBytes(signature)).toString("base64"),
  };

  const res = await fetch(`${PROXY_URL}/encrypt-to-user`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new Error(await res.text());
  const { output } = await res.json();

  console.log("Encrypted output (base64):", output);

  // Optional decrypt
  const decryptedBytes = decryptOutput(output, USER_AES_KEY);
  const decryptedValue = toBigInt(decryptedBytes);
  console.log("Decrypted value (bigint):", decryptedValue.toString());
};

main().catch(err => {
  console.error("Encrypt-to-user failed:", err);
  process.exit(1);
});

Run it:

PRIVATE_KEY=0xabc... USER_AES_KEY=deadbeef... HANDLE=12345 CHAIN_ID=84532 node encrypt_to_user.mjs

The script signs the handle, POSTs to /encrypt-to-user, prints the encrypted payload, and decrypts it locally using your AES key.

On this page