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 withBUBBLE_PROXY_URLif 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.mjsThe script signs the handle, POSTs to /encrypt-to-user, prints the encrypted payload, and decrypts it locally using your AES key.