UNPKG

@wuwei-labs/srsly

Version:
216 lines 8.67 kB
/** * @purpose Thread account utilities for SRSLY * * Fetches Antegen thread state data. The thread type moved from a Codama * account to a plain type after the Anchor upgrade, so we manually fetch * and decode using the generated codec. */ import { getBase64Encoder, getAddressDecoder } from '@solana/kit'; import { getThreadDecoder } from '../generated/codama/types/thread'; import { fetchContractState } from '../generated/codama/accounts'; import { getRpcUrl } from '../utils/config'; import { createRpc } from '../utils/rpc'; import { deriveFiber } from '../pda/thread'; /** Anchor discriminator size (8 bytes) */ const DISCRIMINATOR_SIZE = 8; /** System program ID — contract.thread == this means no thread */ const SYSTEM_PROGRAM_ID = '11111111111111111111111111111111'; /** * Fetch and decode a raw Antegen thread account by its direct address. * * Used internally by instruction wrappers that already know the thread address. * * @param threadAddress - Direct address of the thread account * @param rpcUrl - Optional RPC URL override * @returns Decoded thread data */ export async function fetchThreadByAddress(threadAddress, rpcUrl) { const resolvedRpcUrl = rpcUrl || getRpcUrl(); const rpc = createRpc(resolvedRpcUrl); const addr = threadAddress; const acct = await rpc.getAccountInfo(addr, { encoding: 'base64' }).send(); const rawData = acct.value?.data; if (!rawData || typeof rawData === 'string') { throw new Error(`Thread account not found: ${threadAddress}`); } const base64Encoder = getBase64Encoder(); const bytes = base64Encoder.encode(rawData[0]); const data = getThreadDecoder().decode(bytes.subarray(DISCRIMINATOR_SIZE)); return { address: addr, data }; } /** * Fetch and decode an Antegen thread account by contract address. * * Looks up the contract's `thread` field, then fetches and decodes the * thread account. Returns null if no thread is configured on the contract. * * @param contractAddress - Address of the rental contract * @param rpcUrl - Optional RPC URL override * @returns Decoded thread data, or null if no thread configured */ export async function fetchThread(contractAddress, rpcUrl) { const resolvedRpcUrl = rpcUrl || getRpcUrl(); const rpc = createRpc(resolvedRpcUrl); const contract = await fetchContractState(rpc, contractAddress); const threadAddr = contract.data.thread; if (!threadAddr || threadAddr === SYSTEM_PROGRAM_ID) { return null; } return fetchThreadByAddress(threadAddr, resolvedRpcUrl); } // ============================================================================ // Fiber (compiled instruction) deserialization // ============================================================================ /** Expected discriminator for FiberState accounts */ const FIBER_DISCRIMINATOR = new Uint8Array([54, 11, 251, 60, 63, 197, 85, 36]); // --- Binary helpers --- function readU32LE(bytes, offset) { const v = bytes[offset] | (bytes[offset + 1] << 8) | (bytes[offset + 2] << 16) | (bytes[offset + 3] << 24); return [v >>> 0, offset + 4]; } function readI64LE(bytes, offset) { const view = new DataView(bytes.buffer, bytes.byteOffset + offset, 8); return [view.getBigInt64(0, true), offset + 8]; } function readU64LE(bytes, offset) { const view = new DataView(bytes.buffer, bytes.byteOffset + offset, 8); return [view.getBigUint64(0, true), offset + 8]; } function readAddress(bytes, offset) { const decoded = getAddressDecoder().decode(bytes.subarray(offset, offset + 32)); return [decoded, offset + 32]; } function toHex(bytes) { return Array.from(bytes) .map(b => b.toString(16).padStart(2, '0')) .join(''); } /** * Decompile a CompiledInstructionV0 byte blob into human-readable instructions. * * Layout: * num_ro_signers: u8, num_rw_signers: u8, num_rw: u8, * instructions: Vec<{ program_id_index: u8, accounts: Vec<u8>, data: Vec<u8> }>, * accounts: Vec<Pubkey> * * Account ordering: [rw_signers, ro_signers, rw_non_signers, ro_non_signers] */ function decompileCompiledInstruction(compiled) { let offset = 0; const numRoSigners = compiled[offset++]; const numRwSigners = compiled[offset++]; const numRw = compiled[offset++]; const totalSigners = numRwSigners + numRoSigners; // Read instructions vec let instrCount; [instrCount, offset] = readU32LE(compiled, offset); const rawInstrs = []; for (let i = 0; i < instrCount; i++) { const programIdIndex = compiled[offset++]; let acctLen; [acctLen, offset] = readU32LE(compiled, offset); const accountIndices = Array.from(compiled.subarray(offset, offset + acctLen)); offset += acctLen; let dataLen; [dataLen, offset] = readU32LE(compiled, offset); const data = compiled.slice(offset, offset + dataLen); offset += dataLen; rawInstrs.push({ programIdIndex, accountIndices, data }); } // Read accounts vec (deduplicated pubkey table) let accountsCount; [accountsCount, offset] = readU32LE(compiled, offset); const accountKeys = []; for (let i = 0; i < accountsCount; i++) { let addr; [addr, offset] = readAddress(compiled, offset); accountKeys.push(addr); } // Decompile each instruction using position-based signer/writable derivation const instructions = rawInstrs.map(instr => { const accounts = instr.accountIndices.map(idx => ({ address: accountKeys[idx], isSigner: idx < totalSigners, isWritable: idx < numRwSigners || (idx >= totalSigners && idx < totalSigners + numRw), })); return { programId: accountKeys[instr.programIdIndex], accounts, data: toHex(instr.data), }; }); return { instructions, numUniqueAccounts: accountsCount }; } /** * Fetch and decode a FiberState account by its direct address. */ export async function fetchFiberByAddress(fiberAddress, rpcUrl) { const resolvedRpcUrl = rpcUrl || getRpcUrl(); const rpc = createRpc(resolvedRpcUrl); const addr = fiberAddress; const acct = await rpc.getAccountInfo(addr, { encoding: 'base64' }).send(); const rawData = acct.value?.data; if (!rawData || typeof rawData === 'string') { throw new Error(`Fiber account not found: ${fiberAddress}`); } const bytes = new Uint8Array(getBase64Encoder().encode(rawData[0])); // Verify discriminator const disc = bytes.subarray(0, DISCRIMINATOR_SIZE); if (!FIBER_DISCRIMINATOR.every((b, i) => b === disc[i])) { throw new Error(`Invalid fiber discriminator`); } let offset = DISCRIMINATOR_SIZE; // thread: Pubkey (32 bytes) let thread; [thread, offset] = readAddress(bytes, offset); // compiledInstruction: Vec<u8> let ciLen; [ciLen, offset] = readU32LE(bytes, offset); const compiledBytes = bytes.slice(offset, offset + ciLen); offset += ciLen; const { instructions: compiledInstruction, numUniqueAccounts } = decompileCompiledInstruction(compiledBytes); // lastExecuted: i64 let lastExecuted; [lastExecuted, offset] = readI64LE(bytes, offset); // execCount: u64 let execCount; [execCount, offset] = readU64LE(bytes, offset); // priorityFee: u64 let priorityFee; [priorityFee, offset] = readU64LE(bytes, offset); return { address: addr, data: { thread, compiledInstruction, numUniqueAccounts, lastExecuted: Number(lastExecuted), execCount: Number(execCount), priorityFee: Number(priorityFee), }, }; } /** * Fetch and decode a FiberState by contract address and fiber index. * * Looks up the contract's thread, derives the fiber PDA, then fetches + decodes. * * @param contractAddress - Address of the rental contract * @param fiberIndex - Fiber index (0=controller, 1=close_rental, 2=activate_rental) * @param rpcUrl - Optional RPC URL override */ export async function fetchFiber(contractAddress, fiberIndex, rpcUrl) { const resolvedRpcUrl = rpcUrl || getRpcUrl(); const rpc = createRpc(resolvedRpcUrl); const contract = await fetchContractState(rpc, contractAddress); const threadAddr = contract.data.thread; if (!threadAddr || threadAddr === SYSTEM_PROGRAM_ID) { throw new Error(`Contract has no thread configured`); } const fiberAddr = await deriveFiber(threadAddr, fiberIndex); return fetchFiberByAddress(fiberAddr, resolvedRpcUrl); } //# sourceMappingURL=thread.js.map