import { readFileSync } from "fs";
import {
Fireblocks,
TransactionOperation,
TransactionStateEnum,
TransferPeerPathType,
type CreateTransactionResponse,
type FireblocksResponse,
type PublicKeyInformation,
type TransactionRequest,
type TransactionResponse,
} from "@fireblocks/ts-sdk";
import {
Connection,
PublicKey,
SystemProgram,
Transaction,
} from "@solana/web3.js";
type TransactionResponseWithProgramCall = TransactionResponse & {
signedProgramCallData?: string;
};
const PRIMARY_TRANSACTION_STATUS_SIGNED = "SIGNED" as const;
function requiredEnv(name: string): string {
const v = process.env[name];
if (!v) {
throw new Error(`Missing required environment variable: ${name}`);
}
return v;
}
function hexToBuffer(hexString: string): Buffer {
const cleanHex = hexString.startsWith("0x")
? hexString.slice(2)
: hexString;
return Buffer.from(cleanHex, "hex");
}
function assertPublicKeyHex(info: PublicKeyInformation, label: string): string {
const pk = info.publicKey;
if (!pk) {
throw new Error(`Fireblocks response missing publicKey (${label})`);
}
return pk;
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function main(): Promise<void> {
const apiSecret = readFileSync(
requiredEnv("FIREBLOCKS_SECRET_KEY_PATH"),
"utf8"
);
const apiKey = requiredEnv("FIREBLOCKS_API_KEY");
const basePath =
process.env.FIREBLOCKS_BASE_PATH ?? "https://api.fireblocks.io/v1";
const fireblocks = new Fireblocks({
apiKey,
basePath,
secretKey: apiSecret,
});
const vaultAccountIdA = requiredEnv("FIREBLOCKS_VAULT_A");
const vaultAccountIdB = requiredEnv("FIREBLOCKS_VAULT_B");
const solanaRpc =
process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com";
const pubkeyA: FireblocksResponse<PublicKeyInformation> =
await fireblocks.vaults.getPublicKeyInfoForAddress({
vaultAccountId: vaultAccountIdA,
assetId: "SOL",
change: 0,
addressIndex: 0,
});
const pubkeyB: FireblocksResponse<PublicKeyInformation> =
await fireblocks.vaults.getPublicKeyInfoForAddress({
vaultAccountId: vaultAccountIdB,
assetId: "SOL",
change: 0,
addressIndex: 0,
});
const pubkeyA_buffer = hexToBuffer(assertPublicKeyHex(pubkeyA.data, "vault A"));
const pubkeyB_buffer = hexToBuffer(assertPublicKeyHex(pubkeyB.data, "vault B"));
const connection = new Connection(solanaRpc, "confirmed");
const pubkeyASolana = new PublicKey(pubkeyA_buffer);
const pubkeyBSolana = new PublicKey(pubkeyB_buffer);
const transaction = new Transaction();
const transferInstruction = SystemProgram.transfer({
fromPubkey: pubkeyASolana,
toPubkey: pubkeyBSolana,
lamports: 100_000,
});
transferInstruction.keys.push({
pubkey: pubkeyBSolana,
isSigner: true,
isWritable: true,
});
transaction.add(transferInstruction);
transaction.feePayer = pubkeyASolana;
const { blockhash } = await connection.getLatestBlockhash();
transaction.recentBlockhash = blockhash;
console.log("Blockhash: ", blockhash);
const serializedTx = transaction
.serialize({ requireAllSignatures: false })
.toString("base64");
console.log("Serialized TX: ", serializedTx);
const signOnlyRequest: TransactionRequest = {
operation: TransactionOperation.ProgramCall,
assetId: "SOL",
source: { type: TransferPeerPathType.VaultAccount, id: vaultAccountIdA },
extraParameters: {
programCallData: serializedTx,
signOnly: true,
useDurableNonce: true,
},
};
const createRes: FireblocksResponse<CreateTransactionResponse> =
await fireblocks.transactions.createTransaction({
transactionRequest: signOnlyRequest,
});
const initialTxId = createRes.data.id;
if (!initialTxId) {
throw new Error("createTransaction did not return a transaction id");
}
let signedPoll: FireblocksResponse<TransactionResponseWithProgramCall> | null =
null;
for (let i = 0; i < 20; i++) {
await sleep(5000);
signedPoll = await fireblocks.transactions.getTransaction({
txId: initialTxId,
});
const status = signedPoll.data.status;
console.log(`tx ${initialTxId} status: ${status}`);
if (status === PRIMARY_TRANSACTION_STATUS_SIGNED) {
break;
}
if (
status === TransactionStateEnum.Failed ||
status === TransactionStateEnum.Blocked ||
i === 19
) {
process.exit(1);
}
}
if (!signedPoll) {
throw new Error("Polling did not load transaction state");
}
console.log(JSON.stringify(signedPoll.data));
const signedData = signedPoll.data.signedProgramCallData;
if (!signedData) {
throw new Error(
"Expected signedProgramCallData on SIGNED PROGRAM_CALL transaction"
);
}
const broadcastRequest: TransactionRequest = {
operation: TransactionOperation.ProgramCall,
assetId: "SOL",
source: { type: TransferPeerPathType.VaultAccount, id: vaultAccountIdB },
extraParameters: {
programCallData: signedData,
},
};
const tx2 = await fireblocks.transactions.createTransaction({
transactionRequest: broadcastRequest,
});
console.log("Tx2: ", tx2);
}
main().catch((err: unknown) => {
console.error(err);
process.exit(1);
});