#![cfg(feature = "builder")]
extern crate alloc;
use alloc::vec::Vec;
use hopper_schema::{InstructionDescriptor, ProgramManifest};
#[derive(Debug, Clone, Copy)]
pub struct AccountMeta {
pub pubkey: [u8; 32],
pub is_signer: bool,
pub is_writable: bool,
}
#[derive(Debug, Clone)]
pub struct BuiltInstruction {
pub program_id: [u8; 32],
pub data: Vec<u8>,
pub accounts: Vec<AccountMeta>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BuildError {
UnknownInstruction,
ArgCountMismatch {
expected: usize,
got: usize,
},
ArgSizeMismatch {
index: usize,
expected: u16,
got: usize,
},
AccountCountMismatch {
expected: usize,
got: usize,
},
}
#[derive(Debug)]
pub struct InstructionBuilder<'a> {
program_id: [u8; 32],
ix: &'a InstructionDescriptor,
args: Vec<&'a [u8]>,
accounts: Vec<[u8; 32]>,
}
impl<'a> InstructionBuilder<'a> {
pub fn new(
manifest: &'a ProgramManifest,
program_id: [u8; 32],
ix_name: &str,
) -> Result<Self, BuildError> {
let ix = find_instruction(manifest, ix_name).ok_or(BuildError::UnknownInstruction)?;
Ok(Self {
program_id,
ix,
args: Vec::with_capacity(ix.args.len()),
accounts: Vec::with_capacity(ix.accounts.len()),
})
}
pub fn arg(mut self, bytes: &'a [u8]) -> Self {
self.args.push(bytes);
self
}
pub fn account(mut self, pubkey: [u8; 32]) -> Self {
self.accounts.push(pubkey);
self
}
pub fn build(self) -> Result<BuiltInstruction, BuildError> {
if self.args.len() != self.ix.args.len() {
return Err(BuildError::ArgCountMismatch {
expected: self.ix.args.len(),
got: self.args.len(),
});
}
if self.accounts.len() != self.ix.accounts.len() {
return Err(BuildError::AccountCountMismatch {
expected: self.ix.accounts.len(),
got: self.accounts.len(),
});
}
let mut data_len: usize = 1;
let mut i = 0;
while i < self.args.len() {
let want = self.ix.args[i].size as usize;
if self.args[i].len() != want {
return Err(BuildError::ArgSizeMismatch {
index: i,
expected: self.ix.args[i].size,
got: self.args[i].len(),
});
}
data_len += want;
i += 1;
}
let mut data = Vec::with_capacity(data_len);
data.push(self.ix.tag);
for a in &self.args {
data.extend_from_slice(a);
}
let mut metas = Vec::with_capacity(self.accounts.len());
let mut i = 0;
while i < self.accounts.len() {
let entry = &self.ix.accounts[i];
metas.push(AccountMeta {
pubkey: self.accounts[i],
is_signer: entry.signer,
is_writable: entry.writable,
});
i += 1;
}
Ok(BuiltInstruction {
program_id: self.program_id,
data,
accounts: metas,
})
}
}
fn find_instruction<'a>(m: &'a ProgramManifest, name: &str) -> Option<&'a InstructionDescriptor> {
let mut i = 0;
while i < m.instructions.len() {
if m.instructions[i].name == name {
return Some(&m.instructions[i]);
}
i += 1;
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use hopper_schema::{AccountEntry, ArgDescriptor, InstructionDescriptor, ProgramManifest};
fn sample_manifest() -> ProgramManifest {
static ARGS: [ArgDescriptor; 2] = [
ArgDescriptor {
name: "amount",
canonical_type: "u64",
size: 8,
},
ArgDescriptor {
name: "bump",
canonical_type: "u8",
size: 1,
},
];
static ACCTS: [AccountEntry; 2] = [
AccountEntry {
name: "vault",
writable: true,
signer: false,
layout_ref: "Vault",
},
AccountEntry {
name: "authority",
writable: false,
signer: true,
layout_ref: "",
},
];
static IX: [InstructionDescriptor; 1] = [InstructionDescriptor {
name: "deposit",
tag: 3,
args: &ARGS,
accounts: &ACCTS,
capabilities: &[],
policy_pack: "",
receipt_expected: true,
}];
ProgramManifest {
name: "test",
version: "0",
description: "",
layouts: &[],
layout_metadata: &[],
instructions: &IX,
events: &[],
policies: &[],
compatibility_pairs: &[],
tooling_hints: &[],
contexts: &[],
}
}
#[test]
fn builds_a_valid_ix() {
let m = sample_manifest();
let amount = 42u64.to_le_bytes();
let bump = [254u8];
let vault = [1u8; 32];
let auth = [2u8; 32];
let ix = InstructionBuilder::new(&m, [9u8; 32], "deposit")
.unwrap()
.arg(&amount)
.arg(&bump)
.account(vault)
.account(auth)
.build()
.unwrap();
assert_eq!(ix.program_id, [9u8; 32]);
assert_eq!(ix.data.len(), 1 + 8 + 1);
assert_eq!(ix.data[0], 3);
assert_eq!(ix.accounts.len(), 2);
assert!(ix.accounts[0].is_writable);
assert!(ix.accounts[1].is_signer);
}
#[test]
fn rejects_mismatched_arg_size() {
let m = sample_manifest();
let amount_wrong = [1u8; 4];
let bump = [0u8; 1];
let err = InstructionBuilder::new(&m, [0u8; 32], "deposit")
.unwrap()
.arg(&amount_wrong)
.arg(&bump)
.account([0u8; 32])
.account([0u8; 32])
.build()
.unwrap_err();
assert!(matches!(
err,
BuildError::ArgSizeMismatch {
index: 0,
expected: 8,
got: 4
}
));
}
#[test]
fn rejects_unknown_instruction() {
let m = sample_manifest();
let err = InstructionBuilder::new(&m, [0u8; 32], "withdraw").unwrap_err();
assert_eq!(err, BuildError::UnknownInstruction);
}
}