Custom Tx Manager
Build your own transaction manager service with @revibase/transaction-manager and an Ed25519 keypair.
2
Generate an Ed25519 keypair
node -e '
(async () => {
const toBase58 = (bytes) => {
const alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
let digits = [0];
for (const b of bytes) {
let carry = b;
for (let j = 0; j < digits.length; ++j) {
carry += digits[j] << 8;
digits[j] = carry % 58;
carry = (carry / 58) | 0;
}
while (carry) {
digits.push(carry % 58);
carry = (carry / 58) | 0;
}
}
return digits.reverse().map((d) => alphabet[d]).join("");
};
const keyPair = await crypto.subtle.generateKey(
{ name: "Ed25519" },
true,
["sign", "verify"]
);
const [pubRaw, privJwk] = await Promise.all([
crypto.subtle.exportKey("raw", keyPair.publicKey),
crypto.subtle.exportKey("jwk", keyPair.privateKey),
]);
console.log({
publicKey: toBase58(new Uint8Array(pubRaw)),
privateKey: JSON.stringify(privJwk),
});
})();
'3
Backend callback (/api/transactionManager/sign)
/api/transactionManager/sign)Backend examples
import { verifyTransaction } from "@revibase/transaction-manager";
import { createSolanaRpc, getBase58Encoder } from "gill";
import { enforcePolicies } from "@/lib/policy";
const rpc = createSolanaRpc(process.env.RPC_URL ?? "https://api.mainnet-beta.solana.com");
const transactionManagerConfig = {
publicKey: process.env.TX_MANAGER_PUBLIC_KEY!, // base58
url: process.env.TX_MANAGER_URL!, // public HTTPS URL of this endpoint
};
export async function POST(req: Request) {
try {
const { publicKey, payload } = (await req.json()) as {
publicKey: string;
payload: unknown[];
};
if (publicKey !== transactionManagerConfig.publicKey) {
return Response.json({ error: "Invalid transaction manager public key" }, { status: 400 });
}
const jwk = JSON.parse(process.env.TX_MANAGER_PRIVATE_KEY!);
const privateKey = await crypto.subtle.importKey(
"jwk",
jwk,
{ name: "Ed25519" },
false,
["sign"],
);
const signatures: string[] = [];
for (const payloadItem of payload) {
const { transactionMessage, verificationResults } = await verifyTransaction(
rpc,
transactionManagerConfig,
payloadItem,
);
await enforcePolicies(verificationResults);
const signatureBytes = await crypto.subtle.sign(
{ name: "Ed25519" },
privateKey,
transactionMessage,
);
signatures.push(getBase58Encoder().encode(signatureBytes));
}
return Response.json({ signatures });
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
return Response.json({ error: msg }, { status: 500 });
}
}4
Policy checks
import type { VerificationResults } from "@revibase/transaction-manager";
import {
SYSTEM_PROGRAM_ADDRESS,
identifySystemInstruction,
parseTransferSolInstruction,
parseTransferSolWithSeedInstruction,
SystemInstruction,
} from "gill";
const ALLOWED_ORIGINS = new Set(["https://app.revibase.com"]);
const MAX_TRANSFER_LAMPORTS = 1_000_000_000n; // 1 SOL
export async function enforcePolicies(results: VerificationResults) {
const { signers, instructions } = results;
for (const signer of signers) {
const origin = signer.client?.origin;
if (origin && !ALLOWED_ORIGINS.has(origin)) {
throw new Error("Unauthorized app origin");
}
}
for (const ix of instructions) {
if (ix.programAddress !== SYSTEM_PROGRAM_ADDRESS) {
throw new Error("Unauthorized program");
}
const ixKind = identifySystemInstruction(ix.data);
if (
ixKind !== SystemInstruction.TransferSol &&
ixKind !== SystemInstruction.TransferSolWithSeed
) {
throw new Error("Unauthorized instruction");
}
const parsed =
ixKind === SystemInstruction.TransferSol
? parseTransferSolInstruction(ix)
: parseTransferSolWithSeedInstruction(ix);
if (parsed.data.amount > MAX_TRANSFER_LAMPORTS) {
throw new Error("Transfer limit exceeded");
}
}
}Last updated





