Template lifecycle
How to upload a template in one instruction or in several, resume an upload that stopped, and what you can and cannot do with a template afterwards. The code continues Getting started, and uses its send, creator and imports: in TypeScript also emptyMessage and a compiled template, and in Rust a payload.
A template is stored in its own account, at a PDA derived from the seeds ['template', creator, templateId]. creator is the uploader's address, and templateId is a number from 0 to 65,535 that the creator picks. Once a template is finalized, checked and locked, its account can never change.
Upload
CreateTemplate uploads a template, checks it, and finalizes it in one instruction, when the template fits in one transaction. A larger template is uploaded in pieces:
BeginTemplatecreates the account and records the template's total length and its SHA-256 hash.WriteTemplateChunkinstructions append the bytes. Each write must start exactly where the last one ended, at the byte count the account has recorded (written_len).FinalizeTemplatechecks that every byte arrived, that the bytes match the recorded hash, and that the template passes the program's checks. Then it locks the account.
Each instruction goes in a transaction of its own, which holds at most 1,232 bytes. In TypeScript, pass buildKitTemplateUploadPlan the transaction you send them in; it picks one instruction or pieces, and sizes each write to fit. In Rust you choose: a legacy transaction fits a CreateTemplate for a template of up to 960 bytes, and a write of up to 1,023 bytes. Limits has the sizes.
const plan = await buildKitTemplateUploadPlan({
compiled,
creator: creator.address,
templateId: 42,
// Size each instruction to fit the transactions `send` builds.
transactionMessage: await emptyMessage(creator),
});
for (const { kind, offset, instruction } of plan.instructions) {
await send(creator, [instruction]);
console.log(kind, offset ?? ''); // begin, write 0, write 1021, finalize
}use ballista_sdk::{
begin_template_instruction, finalize_template_instruction, template_hash,
write_template_chunk_instruction,
};
// Each instruction goes in its own legacy transaction, which fits a write of at most
// 1,023 bytes of template.
const CHUNK: usize = 1_000;
let creator_key = creator.pubkey();
let (template, _) = find_template_pda(&creator_key, 42);
let (length, hash) = (payload.len() as u32, template_hash(&payload));
let begin = begin_template_instruction(creator_key, 42, length, hash);
send(&creator, &[begin])?;
for (index, chunk) in payload.chunks(CHUNK).enumerate() {
let offset = (index * CHUNK) as u32;
let write = write_template_chunk_instruction(creator_key, template, offset, chunk);
send(&creator, &[write])?;
}
let finalize = finalize_template_instruction(creator_key, template);
send(&creator, &[finalize])?;Resume an interrupted upload
If an upload stops partway, the account keeps the bytes written so far and records how many. Send only the missing writes, then the finalize:
import { fetchEncodedAccount } from '@solana/kit';
import { buildKitResumeTemplateUploadPlan, getTemplateAddress } from '@jac0xb/ballista/kit';
// Plan only the writes the account is missing, then the finalize.
const [templateAddress] = await getTemplateAddress(creator.address, 43);
const stored = await fetchEncodedAccount(rpc, templateAddress);
if (!stored.exists) throw new Error('No upload to resume');
const resumed = await buildKitResumeTemplateUploadPlan({
compiled,
account: stored.data,
creator: creator.address,
templateId: 43,
transactionMessage: await emptyMessage(creator),
});
for (const { instruction } of resumed.instructions) {
await send(creator, [instruction]);
}use ballista_sdk::ballista_common::template::TemplateAccount;
// Write only the bytes the account doesn't have yet, then finalize.
let creator_key = creator.pubkey();
let (template, _) = find_template_pda(&creator_key, 42);
let data = rpc.get_account_data(&template)?;
let written = TemplateAccount::parse(&data)?.header().written_len();
for (index, chunk) in payload[written..].chunks(CHUNK).enumerate() {
let offset = (written + index * CHUNK) as u32;
let write = write_template_chunk_instruction(creator_key, template, offset, chunk);
send(&creator, &[write])?;
}
let finalize = finalize_template_instruction(creator_key, template);
send(&creator, &[finalize])?;Cancel or replace
An upload that has not been finalized can be cancelled with CancelTemplate (buildKitCancelTemplateInstruction in TypeScript, cancel_template_instruction in Rust). That closes the account and returns its rent deposit to the creator. A finalized template can never be changed or closed, and its rent deposit stays locked. To publish a revision, upload it under a new template ID; see Failure modes and recovery.