@wuwei-labs/srsly
Version:
TypeScript SDK for SRSLY
216 lines • 8.67 kB
JavaScript
/**
* @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