Store IDL at deterministic account address (#49)

This commit is contained in:
Armani Ferrante 2021-01-29 06:19:00 -08:00
parent 345d25a583
commit 772edde073
No known key found for this signature in database
GPG Key ID: D597A80BCF8E12B7
16 changed files with 613 additions and 67 deletions

3
Cargo.lock generated
View File

@ -101,10 +101,12 @@ dependencies = [
name = "anchor-cli"
version = "0.1.0"
dependencies = [
"anchor-lang",
"anchor-syn",
"anyhow",
"clap 3.0.0-beta.2",
"dirs",
"flate2",
"heck",
"serde",
"serde_json",
@ -112,6 +114,7 @@ dependencies = [
"serum-common",
"shellexpand",
"solana-client",
"solana-program",
"solana-sdk",
"syn 1.0.57",
"toml",

View File

@ -8,12 +8,11 @@ edition = "2018"
name = "anchor"
path = "src/main.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = "3.0.0-beta.1"
anyhow = "1.0.32"
syn = { version = "1.0.54", features = ["full", "extra-traits"] }
anchor-lang = { path = "../" }
anchor-syn = { path = "../syn", features = ["idl"] }
serde_json = "1.0"
shellexpand = "2.1.0"
@ -21,7 +20,9 @@ serde_yaml = "0.8"
toml = "0.5.8"
serde = { version = "1.0", features = ["derive"] }
solana-sdk = "1.5.0"
solana-program = "1.5.0"
solana-client = "1.4.4"
serum-common = { git = "https://github.com/project-serum/serum-dex", features = ["client"] }
dirs = "3.0"
heck = "0.3.1"
heck = "0.3.1"
flate2 = "1.0.19"

View File

@ -148,7 +148,7 @@ pub fn extract_lib_name(path: impl AsRef<Path>) -> Result<String> {
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Program {
pub lib_name: String,
pub path: PathBuf,

View File

@ -1,10 +1,20 @@
use crate::config::{find_cargo_toml, read_all_programs, Config, Program};
use anchor_lang::idl::IdlAccount;
use anchor_lang::{AnchorDeserialize, AnchorSerialize};
use anchor_syn::idl::Idl;
use anyhow::Result;
use anyhow::{anyhow, Result};
use clap::Clap;
use flate2::read::ZlibDecoder;
use flate2::write::ZlibEncoder;
use flate2::Compression;
use serde::{Deserialize, Serialize};
use solana_client::rpc_client::RpcClient;
use solana_client::rpc_config::RpcSendTransactionConfig;
use solana_program::instruction::{AccountMeta, Instruction};
use solana_sdk::commitment_config::CommitmentConfig;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::Signer;
use solana_sdk::transaction::Transaction;
use std::fs::{self, File};
use std::io::prelude::*;
use std::path::{Path, PathBuf};
@ -34,8 +44,39 @@ pub enum Command {
Test,
/// Creates a new program.
New { name: String },
/// Outputs an interface definition file.
/// Commands for interact with interface definitions.
Idl {
#[clap(subcommand)]
subcmd: IdlCommand,
},
/// Deploys the workspace, creates IDL accounts, and runs the migration
/// script.
Deploy {
#[clap(short, long)]
url: Option<String>,
#[clap(short, long)]
keypair: Option<String>,
},
/// Runs the deploy migration script.
Migrate {
#[clap(short, long)]
url: String,
},
/// Not yet implemented. Please use `solana program deploy` command to
/// upgrade your program.
Upgrade {},
}
#[derive(Debug, Clap)]
pub enum IdlCommand {
/// Initializes a program's IDL account. Can only be run once.
Init {
program_id: Pubkey,
#[clap(short, long)]
filepath: String,
},
/// Parses an IDL from source.
Parse {
/// Path to the program's interface definition.
#[clap(short, long)]
file: String,
@ -43,16 +84,13 @@ pub enum Command {
#[clap(short, long)]
out: Option<String>,
},
/// Deploys the workspace to the configured cluster.
Deploy {
/// Fetches an IDL for the given program from a cluster.
Fetch {
program_id: Pubkey,
/// Output file for the idl (stdout if not specified).
#[clap(short, long)]
url: Option<String>,
#[clap(short, long)]
keypair: Option<String>,
out: Option<String>,
},
/// Not yet implemented. Please use `solana program deploy` command to
/// upgrade your program.
Upgrade {},
}
fn main() -> Result<()> {
@ -63,17 +101,13 @@ fn main() -> Result<()> {
Command::Build { idl } => build(idl),
Command::Test => test(),
Command::New { name } => new(name),
Command::Idl { file, out } => {
if out.is_none() {
return idl(file, None);
}
idl(file, Some(&PathBuf::from(out.unwrap())))
}
Command::Idl { subcmd } => idl(subcmd),
Command::Deploy { url, keypair } => deploy(url, keypair),
Command::Upgrade {} => {
println!("This command is not yet implemented. Please use `solana program deploy`.");
Ok(())
}
Command::Migrate { url } => migrate(&url),
}
}
@ -176,7 +210,7 @@ fn build_ws(
Some(idl) => Some(PathBuf::from(idl)),
None => {
let cfg_parent = match cfg_path.parent() {
None => return Err(anyhow::anyhow!("Invalid Anchor.toml")),
None => return Err(anyhow!("Invalid Anchor.toml")),
Some(parent) => parent,
};
fs::create_dir_all(cfg_parent.join("target/idl"))?;
@ -191,10 +225,7 @@ fn build_ws(
fn build_all(_cfg: Config, cfg_path: PathBuf, idl_out: Option<PathBuf>) -> Result<()> {
match cfg_path.parent() {
None => Err(anyhow::anyhow!(
"Invalid Anchor.toml at {}",
cfg_path.display()
)),
None => Err(anyhow!("Invalid Anchor.toml at {}", cfg_path.display())),
Some(parent) => {
let files = fs::read_dir(parent.join("programs"))?;
for f in files {
@ -219,7 +250,7 @@ fn build_cwd(idl_out: Option<String>) -> Result<()> {
// Runs the build command outside of a workspace.
fn _build_cwd(cargo_toml: PathBuf, idl_out: Option<PathBuf>) -> Result<()> {
match cargo_toml.parent() {
None => return Err(anyhow::anyhow!("Unable to find parent")),
None => return Err(anyhow!("Unable to find parent")),
Some(p) => std::env::set_current_dir(&p)?,
};
@ -241,12 +272,29 @@ fn _build_cwd(cargo_toml: PathBuf, idl_out: Option<PathBuf>) -> Result<()> {
Some(o) => PathBuf::from(&o.join(&idl.name).with_extension("json")),
};
write_idl(&idl, Some(&out))
write_idl(&idl, OutFile::File(out))
}
fn idl(file: String, out: Option<&Path>) -> Result<()> {
let idl = extract_idl(&file)?;
write_idl(&idl, out)
// Fetches an IDL for the given program_id.
fn fetch_idl(program_id: Pubkey) -> Result<Idl> {
let cfg = Config::discover()?.expect("Inside a workspace").0;
let client = RpcClient::new(cfg.cluster.url().to_string());
let idl_addr = IdlAccount::address(&program_id);
let account = client
.get_account_with_commitment(&idl_addr, CommitmentConfig::recent())?
.value
.map_or(Err(anyhow!("Account not found")), Ok)?;
// Cut off account discriminator.
let mut d: &[u8] = &account.data[8..];
let idl_account: IdlAccount = AnchorDeserialize::deserialize(&mut d)?;
let mut z = ZlibDecoder::new(&idl_account.data[..]);
let mut s = Vec::new();
z.read_to_end(&mut s)?;
serde_json::from_slice(&s[..]).map_err(Into::into)
}
fn extract_idl(file: &str) -> Result<Idl> {
@ -254,15 +302,61 @@ fn extract_idl(file: &str) -> Result<Idl> {
anchor_syn::parser::file::parse(&*file)
}
fn write_idl(idl: &Idl, out: Option<&Path>) -> Result<()> {
fn idl(subcmd: IdlCommand) -> Result<()> {
match subcmd {
IdlCommand::Init {
program_id,
filepath,
} => idl_init(program_id, filepath),
IdlCommand::Parse { file, out } => idl_parse(file, out),
IdlCommand::Fetch { program_id, out } => idl_fetch(program_id, out),
}
}
fn idl_init(program_id: Pubkey, idl_filepath: String) -> Result<()> {
let cfg = Config::discover()?.expect("Inside a workspace").0;
let keypair = cfg.wallet.to_string();
let bytes = std::fs::read(idl_filepath)?;
let idl: Idl = serde_json::from_reader(&*bytes)?;
let idl_address = create_idl_account(&cfg, &keypair, &program_id, &idl)?;
println!("Idl account created: {:?}", idl_address);
Ok(())
}
fn idl_parse(file: String, out: Option<String>) -> Result<()> {
let idl = extract_idl(&file)?;
let out = match out {
None => OutFile::Stdout,
Some(out) => OutFile::File(PathBuf::from(out)),
};
write_idl(&idl, out)
}
fn idl_fetch(program_id: Pubkey, out: Option<String>) -> Result<()> {
let idl = fetch_idl(program_id)?;
let out = match out {
None => OutFile::Stdout,
Some(out) => OutFile::File(PathBuf::from(out)),
};
write_idl(&idl, out)
}
fn write_idl(idl: &Idl, out: OutFile) -> Result<()> {
let idl_json = serde_json::to_string_pretty(idl)?;
match out.as_ref() {
None => println!("{}", idl_json),
Some(out) => std::fs::write(out, idl_json)?,
match out {
OutFile::Stdout => println!("{}", idl_json),
OutFile::File(out) => std::fs::write(out, idl_json)?,
};
Ok(())
}
enum OutFile {
Stdout,
File(PathBuf),
}
// Builds, deploys, and tests all workspace programs in a single command.
fn test() -> Result<()> {
// Switch directories to top level workspace.
@ -297,7 +391,7 @@ fn test() -> Result<()> {
let idl_out = PathBuf::from("target/idl")
.join(&idl.name)
.with_extension("json");
write_idl(&idl, Some(&idl_out))?;
write_idl(&idl, OutFile::File(idl_out))?;
}
// Run the tests.
@ -386,6 +480,10 @@ fn deploy(url: Option<String>, keypair: Option<String>) -> Result<()> {
// Add metadata to all IDLs.
for (program, address) in deployment {
// Store the IDL on chain.
let idl_address = create_idl_account(&cfg, &keypair, &address, &program.idl)?;
println!("IDL account created: {}", idl_address.to_string());
// Add metadata to the IDL.
let mut idl = program.idl;
idl.metadata = Some(serde_json::to_value(IdlTestMetadata {
@ -396,18 +494,130 @@ fn deploy(url: Option<String>, keypair: Option<String>) -> Result<()> {
let idl_out = PathBuf::from("target/idl")
.join(&idl.name)
.with_extension("json");
write_idl(&idl, Some(&idl_out))?;
write_idl(&idl, OutFile::File(idl_out))?;
println!("Deployed {} at {}", idl.name, address.to_string());
}
run_hosted_deploy(&url)?;
// Run migration script.
migrate(&url)?;
Ok(())
}
fn run_hosted_deploy(url: &str) -> Result<()> {
println!("Running deploy script");
fn create_idl_account(
cfg: &Config,
keypair_path: &str,
program_id: &Pubkey,
idl: &Idl,
) -> Result<Pubkey> {
// Misc.
let idl_address = IdlAccount::address(program_id);
let keypair = solana_sdk::signature::read_keypair_file(keypair_path)
.map_err(|_| anyhow!("Unable to read keypair file"))?;
let client = RpcClient::new(cfg.cluster.url().to_string());
// Serialize and compress the idl.
let idl_data = {
let json_bytes = serde_json::to_vec(idl)?;
let mut e = ZlibEncoder::new(Vec::new(), Compression::default());
e.write_all(&json_bytes)?;
e.finish()?
};
// Run `Create instruction.
{
let data = serialize_idl_ix(anchor_lang::idl::IdlInstruction::Create {
data_len: (idl_data.len() as u64) * 2, // Double for future growth.
})?;
let program_signer = Pubkey::find_program_address(&[], program_id).0;
let accounts = vec![
AccountMeta::new_readonly(keypair.pubkey(), true),
AccountMeta::new(idl_address, false),
AccountMeta::new_readonly(program_signer, false),
AccountMeta::new_readonly(solana_program::system_program::ID, false),
AccountMeta::new_readonly(*program_id, false),
AccountMeta::new_readonly(solana_program::sysvar::rent::ID, false),
];
let ix = Instruction {
program_id: *program_id,
accounts,
data,
};
let (recent_hash, _fee_calc) = client.get_recent_blockhash()?;
let tx = Transaction::new_signed_with_payer(
&[ix],
Some(&keypair.pubkey()),
&[&keypair],
recent_hash,
);
client.send_and_confirm_transaction_with_spinner_and_config(
&tx,
CommitmentConfig::single(),
RpcSendTransactionConfig {
skip_preflight: true,
..RpcSendTransactionConfig::default()
},
)?;
}
// Write the idl to the account buffer, chopping up the IDL into pieces
// and sending multiple transactions in the event the IDL doesn't fit into
// a single transaction.
{
const MAX_WRITE_SIZE: usize = 1000;
let mut offset = 0;
while offset < idl_data.len() {
// Instruction data.
let data = {
let start = offset;
let end = std::cmp::min(offset + MAX_WRITE_SIZE, idl_data.len());
serialize_idl_ix(anchor_lang::idl::IdlInstruction::Write {
data: idl_data[start..end].to_vec(),
})?
};
// Instruction accounts.
let accounts = vec![
AccountMeta::new(idl_address, false),
AccountMeta::new_readonly(keypair.pubkey(), true),
];
// Instruction.
let ix = Instruction {
program_id: *program_id,
accounts,
data,
};
// Send transaction.
let (recent_hash, _fee_calc) = client.get_recent_blockhash()?;
let tx = Transaction::new_signed_with_payer(
&[ix],
Some(&keypair.pubkey()),
&[&keypair],
recent_hash,
);
client.send_and_confirm_transaction_with_spinner_and_config(
&tx,
CommitmentConfig::single(),
RpcSendTransactionConfig {
skip_preflight: true,
..RpcSendTransactionConfig::default()
},
)?;
offset += MAX_WRITE_SIZE;
}
}
Ok(idl_address)
}
fn serialize_idl_ix(ix_inner: anchor_lang::idl::IdlInstruction) -> Result<Vec<u8>> {
let mut data = anchor_lang::idl::IDL_IX_TAG.to_le_bytes().to_vec();
data.append(&mut ix_inner.try_to_vec()?);
Ok(data)
}
fn migrate(url: &str) -> Result<()> {
println!("Running migration deploy script");
let cur_dir = std::env::current_dir()?;
let module_path = format!("{}/migrations/deploy.js", cur_dir.display());
@ -463,7 +673,7 @@ fn deploy_ws(url: &str, keypair: &str) -> Result<Vec<(Program, Pubkey)>> {
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(rename_all = "PascalCase")]
pub struct DeployStdout {
program_id: String,
}

View File

@ -24,7 +24,9 @@ name = "{1}"
[features]
no-entrypoint = []
no-idl = []
cpi = ["no-entrypoint"]
default = []
[dependencies]
anchor-lang = {{ git = "https://github.com/project-serum/anchor", features = ["derive"] }}

View File

@ -1,4 +1,4 @@
use crate::{Accounts, AccountsExit, ToAccountInfo, ToAccountInfos, ToAccountMetas};
use crate::{Accounts, AccountsExit, AccountsInit, ToAccountInfo, ToAccountInfos, ToAccountMetas};
use solana_program::account_info::AccountInfo;
use solana_program::entrypoint::ProgramResult;
use solana_program::instruction::AccountMeta;
@ -19,6 +19,31 @@ impl<'info> Accounts<'info> for AccountInfo<'info> {
}
}
impl<'info> AccountsInit<'info> for AccountInfo<'info> {
fn try_accounts_init(
_program_id: &Pubkey,
accounts: &mut &[AccountInfo<'info>],
) -> Result<Self, ProgramError> {
if accounts.len() == 0 {
return Err(ProgramError::NotEnoughAccountKeys);
}
let account = &accounts[0];
*accounts = &accounts[1..];
// The discriminator should be zero, since we're initializing.
let data: &[u8] = &account.try_borrow_data()?;
let mut disc_bytes = [0u8; 8];
disc_bytes.copy_from_slice(&data[..8]);
let discriminator = u64::from_le_bytes(disc_bytes);
if discriminator != 0 {
return Err(ProgramError::InvalidAccountData);
}
Ok(account.clone())
}
}
impl<'info> ToAccountMetas for AccountInfo<'info> {
fn to_account_metas(&self, is_signer: Option<bool>) -> Vec<AccountMeta> {
let is_signer = is_signer.unwrap_or(self.is_signer);

View File

@ -5,13 +5,27 @@ use solana_program::sysvar::rent::Rent;
// Needed for the `Accounts` macro.
use crate as anchor_lang;
// The Ctor accounts that can be used to create any account within the program
// itself (instead of creating the account on the client).
//
// This is used to create accounts at deterministic addresses, as a function of
// nothing but a program ID--for example, to create state global program
// structs and program IDL accounts.
#[derive(Accounts)]
pub struct Ctor<'info> {
// Payer of the transaction.
pub from: AccountInfo<'info>,
// The deterministically defined "state" account being created via
// `create_account_with_seed`.
#[account(mut)]
pub to: AccountInfo<'info>,
// The program-derived-address signing off on the account creation.
// Seeds = &[] + bump seed.
pub base: AccountInfo<'info>,
// The system program.
pub system_program: AccountInfo<'info>,
// The program whose state is being constructed.
pub program: AccountInfo<'info>,
// Rent sysvar.
pub rent: Sysvar<'info, Rent>,
}

73
src/idl.rs Normal file
View File

@ -0,0 +1,73 @@
//! idl.rs defines the instructions and account state used to store a
//! program's IDL.
//!
//! Note that the transaction to store the IDL can be larger than the max
//! transaction size. As a reuslt, the transaction must be broken up into
//! several pieces and stored into the IDL account with multiple transactions
//! via the `Write` instruction to continuously append to the account's IDL data
//! buffer.
//!
//! To upgrade the IDL, first invoke the `Clear` instruction to reset the data.
//! And invoke `Write` once more. To eliminate the ability to change the IDL,
//! set the authority to a key for which you can't sign, e.g., the zero address
//! or the system program ID, or compile the program with the "no-idl" feature
//! and upgrade the program with the upgradeable BPF loader.
use crate::prelude::*;
use solana_program::pubkey::Pubkey;
// Needed for the `Accounts` macro.
use crate as anchor_lang;
// The first 8 bytes of an instruction to create or modify the IDL account. This
// instruction is defined outside the main program's instruction enum, so that
// the enum variant tags can align with function source order.
//
// Sha256(anchor:idl)[..8];
pub const IDL_IX_TAG: u64 = 0x0a69e9a778bcf440;
#[derive(AnchorSerialize, AnchorDeserialize)]
pub enum IdlInstruction {
// One time initializer for creating the program's idl account.
Create { data_len: u64 },
// Appends to the end of the idl account data.
Write { data: Vec<u8> },
// Clear's the IdlInstruction data. Used to update the IDL.
Clear,
// Sets a new authority on the IdlAccount.
SetAuthority { new_authority: Pubkey },
}
// Accounts for the Create instuction.
pub type IdlCreateAccounts<'info> = crate::ctor::Ctor<'info>;
// Accounts for Idl instructions.
#[derive(Accounts)]
pub struct IdlAccounts<'info> {
#[account(mut, has_one = authority)]
pub idl: ProgramAccount<'info, IdlAccount>,
#[account(signer)]
pub authority: AccountInfo<'info>,
}
// The account holding a program's IDL. This is stored on chain so that clients
// can fetch it and generate a client with nothing but a program's ID.
#[account]
#[derive(Debug)]
pub struct IdlAccount {
// Address that can modify the IDL.
pub authority: Pubkey,
// Compressed idl bytes.
pub data: Vec<u8>,
}
impl IdlAccount {
pub fn address(program_id: &Pubkey) -> Pubkey {
let program_signer = Pubkey::find_program_address(&[], program_id).0;
Pubkey::create_with_seed(&program_signer, IdlAccount::seed(), program_id)
.expect("Seed is always valid")
}
pub fn seed() -> &'static str {
"anchor:idl"
}
}

View File

@ -33,6 +33,7 @@ mod context;
mod cpi_account;
mod ctor;
mod error;
pub mod idl;
mod program_account;
mod state;
mod sysvar;

View File

@ -15,17 +15,17 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
let name = &s.ident;
let ty = &s.raw_field.ty;
quote! {
let #name: #ty = Accounts::try_accounts(program_id, accounts)?;
let #name: #ty = anchor_lang::Accounts::try_accounts(program_id, accounts)?;
}
}
AccountField::Field(f) => {
let name = f.typed_ident();
match f.is_init {
false => quote! {
let #name = Accounts::try_accounts(program_id, accounts)?;
let #name = anchor_lang::Accounts::try_accounts(program_id, accounts)?;
},
true => quote! {
let #name = AccountsInit::try_accounts_init(program_id, accounts)?;
let #name = anchor_lang::AccountsInit::try_accounts_init(program_id, accounts)?;
},
}
}
@ -217,7 +217,7 @@ pub fn generate_constraint_belongs_to(
// todo: would be nice if target could be an account info object.
quote! {
if &#ident.#target != #target.to_account_info().key {
return Err(ProgramError::Custom(1)); // todo: error codes
return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(1)); // todo: error codes
}
}
}
@ -237,7 +237,7 @@ pub fn generate_constraint_signer(f: &Field, _c: &ConstraintSigner) -> proc_macr
// This check will be performed on the other end of the invocation.
if cfg!(not(feature = "cpi")) {
if !#info.is_signer {
return Err(ProgramError::MissingRequiredSignature);
return Err(anchor_lang::solana_program::program_error::ProgramError::MissingRequiredSignature);
}
}
}
@ -247,7 +247,7 @@ pub fn generate_constraint_literal(c: &ConstraintLiteral) -> proc_macro2::TokenS
let tokens = &c.tokens;
quote! {
if !(#tokens) {
return Err(ProgramError::Custom(1)); // todo: error codes
return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(1)); // todo: error codes
}
}
}
@ -263,7 +263,7 @@ pub fn generate_constraint_owner(f: &Field, c: &ConstraintOwner) -> proc_macro2:
ConstraintOwner::Skip => quote! {},
ConstraintOwner::Program => quote! {
if #info.owner != program_id {
return Err(ProgramError::Custom(1)); // todo: error codes
return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(1)); // todo: error codes
}
},
}
@ -283,7 +283,7 @@ pub fn generate_constraint_rent_exempt(
ConstraintRentExempt::Skip => quote! {},
ConstraintRentExempt::Enforce => quote! {
if !rent.is_exempt(#info.lamports(), #info.try_data_len()?) {
return Err(ProgramError::Custom(2)); // todo: error codes
return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(2)); // todo: error codes
}
},
}
@ -296,9 +296,9 @@ pub fn generate_constraint_seeds(f: &Field, c: &ConstraintSeeds) -> proc_macro2:
let program_signer = Pubkey::create_program_address(
&#seeds,
program_id,
).map_err(|_| ProgramError::Custom(1))?; // todo
).map_err(|_| anchor_lang::solana_program::program_error::ProgramError::Custom(1))?; // todo
if #name.to_account_info().key != &program_signer {
return Err(ProgramError::Custom(1)); // todo
return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(1)); // todo
}
}
}

View File

@ -21,6 +21,13 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream {
anchor_lang::solana_program::entrypoint!(entry);
#[cfg(not(feature = "no-entrypoint"))]
fn entry(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult {
if cfg!(not(feature = "no-idl")) {
if instruction_data.len() >= 8 {
if anchor_lang::idl::IDL_IX_TAG.to_le_bytes() == instruction_data[..8] {
return __private::__idl(program_id, accounts, &instruction_data[8..]);
}
}
}
let mut data: &[u8] = instruction_data;
let ix = __private::instruction::#instruction_name::deserialize(&mut data)
.map_err(|_| ProgramError::Custom(1))?; // todo: error code
@ -114,6 +121,136 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
// so.
pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStream {
let program_name = &program.name;
let non_inlined_idl: proc_macro2::TokenStream = {
quote! {
// Entry for all IDL related instructions. Use the "no-idl" feature
// to eliminate this code, for example, if one wants to make the
// IDL no longer mutable or if one doesn't want to store the IDL
// on chain.
#[inline(never)]
#[cfg(not(feature = "no-idl"))]
pub fn __idl(program_id: &Pubkey, accounts: &[AccountInfo], idl_ix_data: &[u8]) -> ProgramResult {
let mut accounts = accounts;
let mut data: &[u8] = idl_ix_data;
let ix = anchor_lang::idl::IdlInstruction::deserialize(&mut data)
.map_err(|_| ProgramError::Custom(1))?; // todo
match ix {
anchor_lang::idl::IdlInstruction::Create { data_len } => {
let mut accounts = anchor_lang::idl::IdlCreateAccounts::try_accounts(program_id, &mut accounts)?;
__idl_create_account(program_id, &mut accounts, data_len)?;
accounts.exit(program_id)?;
},
anchor_lang::idl::IdlInstruction::Write { data } => {
let mut accounts = anchor_lang::idl::IdlAccounts::try_accounts(program_id, &mut accounts)?;
__idl_write(program_id, &mut accounts, data)?;
accounts.exit(program_id)?;
},
anchor_lang::idl::IdlInstruction::Clear => {
let mut accounts = anchor_lang::idl::IdlAccounts::try_accounts(program_id, &mut accounts)?;
__idl_clear(program_id, &mut accounts)?;
accounts.exit(program_id)?;
},
anchor_lang::idl::IdlInstruction::SetAuthority { new_authority } => {
let mut accounts = anchor_lang::idl::IdlAccounts::try_accounts(program_id, &mut accounts)?;
__idl_set_authority(program_id, &mut accounts, new_authority)?;
accounts.exit(program_id)?;
}
}
Ok(())
}
// One time IDL account initializer. Will faill on subsequent
// invocations.
#[inline(never)]
pub fn __idl_create_account(
program_id: &Pubkey,
accounts: &mut anchor_lang::idl::IdlCreateAccounts,
data_len: u64,
) -> ProgramResult {
// Create the IDL's account.
let from = accounts.from.key;
let (base, nonce) = Pubkey::find_program_address(&[], accounts.program.key);
let seed = anchor_lang::idl::IdlAccount::seed();
let owner = accounts.program.key;
let to = Pubkey::create_with_seed(&base, seed, owner).unwrap();
// Space: account discriminator || authority pubkey || vec len || vec data
let space = 8 + 32 + 4 + data_len as usize;
let lamports = accounts.rent.minimum_balance(space);
let seeds = &[&[nonce][..]];
let ix = anchor_lang::solana_program::system_instruction::create_account_with_seed(
from,
&to,
&base,
seed,
lamports,
space as u64,
owner,
);
anchor_lang::solana_program::program::invoke_signed(
&ix,
&[
accounts.from.clone(),
accounts.to.clone(),
accounts.base.clone(),
accounts.system_program.clone(),
],
&[seeds],
)?;
// Deserialize the newly created account.
let mut idl_account = {
let mut account_data = accounts.to.try_borrow_data()?;
let mut account_data_slice: &[u8] = &account_data;
anchor_lang::idl::IdlAccount::try_deserialize_unchecked(
&mut account_data_slice,
)?
};
// Set the authority.
idl_account.authority = *accounts.from.key;
// Store the new account data.
let mut data = accounts.to.try_borrow_mut_data()?;
let dst: &mut [u8] = &mut data;
let mut cursor = std::io::Cursor::new(dst);
idl_account.try_serialize(&mut cursor)?;
Ok(())
}
#[inline(never)]
pub fn __idl_write(
program_id: &Pubkey,
accounts: &mut anchor_lang::idl::IdlAccounts,
idl_data: Vec<u8>,
) -> ProgramResult {
let mut idl = &mut accounts.idl;
idl.data.extend(idl_data);
Ok(())
}
#[inline(never)]
pub fn __idl_clear(
program_id: &Pubkey,
accounts: &mut anchor_lang::idl::IdlAccounts,
) -> ProgramResult {
accounts.idl.data = vec![];
Ok(())
}
#[inline(never)]
pub fn __idl_set_authority(
program_id: &Pubkey,
accounts: &mut anchor_lang::idl::IdlAccounts,
new_authority: Pubkey,
) -> ProgramResult {
accounts.idl.authority = new_authority;
Ok(())
}
}
};
let non_inlined_ctor: proc_macro2::TokenStream = match &program.state {
None => quote! {},
Some(state) => {
@ -123,6 +260,8 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
let mod_name = &program.name;
let anchor_ident = &state.ctor_anchor;
quote! {
// One time state account initializer. Will faill on subsequent
// invocations.
#[inline(never)]
pub fn __ctor(program_id: &Pubkey, accounts: &[AccountInfo], #(#ctor_typed_args),*) -> ProgramResult {
let mut remaining_accounts: &[AccountInfo] = accounts;
@ -277,6 +416,7 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
.collect();
quote! {
#non_inlined_idl
#non_inlined_ctor
#(#non_inlined_state_handlers)*
#(#non_inlined_handlers)*

View File

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Idl {
pub version: String,
pub name: String,
@ -17,7 +17,7 @@ pub struct Idl {
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdlState {
#[serde(rename = "struct")]
pub strct: IdlTypeDef,
@ -26,7 +26,7 @@ pub struct IdlState {
pub type IdlStateMethod = IdlInstruction;
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdlInstruction {
pub name: String,
pub accounts: Vec<IdlAccountItem>,
@ -34,14 +34,14 @@ pub struct IdlInstruction {
}
// A single struct deriving `Accounts`.
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IdlAccounts {
pub name: String,
pub accounts: Vec<IdlAccountItem>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum IdlAccountItem {
IdlAccount(IdlAccount),
@ -49,7 +49,7 @@ pub enum IdlAccountItem {
}
// A single field in the accounts struct.
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IdlAccount {
pub name: String,
@ -57,42 +57,42 @@ pub struct IdlAccount {
pub is_signer: bool,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdlField {
pub name: String,
#[serde(rename = "type")]
pub ty: IdlType,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdlTypeDef {
pub name: String,
#[serde(rename = "type")]
pub ty: IdlTypeDefTy,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase", tag = "kind")]
pub enum IdlTypeDefTy {
Struct { fields: Vec<IdlField> },
Enum { variants: Vec<EnumVariant> },
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnumVariant {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub fields: Option<EnumFields>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum EnumFields {
Named(Vec<IdlField>),
Tuple(Vec<IdlType>),
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum IdlType {
Bool,
@ -112,7 +112,7 @@ pub enum IdlType {
Vec(Box<IdlType>),
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdlTypePublicKey;
impl std::str::FromStr for IdlType {
@ -162,7 +162,7 @@ impl std::str::FromStr for IdlType {
}
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdlErrorCode {
pub code: u32,
pub name: String,

View File

@ -27,13 +27,15 @@
"@solana/web3.js": "^0.90.4",
"@types/bn.js": "^4.11.6",
"@types/bs58": "^4.0.1",
"@types/pako": "^1.0.1",
"bn.js": "^5.1.2",
"bs58": "^4.0.1",
"buffer-layout": "^1.2.0",
"camelcase": "^5.3.1",
"crypto-hash": "^1.3.0",
"eventemitter3": "^4.0.7",
"find": "^0.3.0"
"find": "^0.3.0",
"pako": "^2.0.3"
},
"devDependencies": {
"@commitlint/cli": "^8.2.0",

View File

@ -1,3 +1,6 @@
import { PublicKey } from "@solana/web3.js";
import * as borsh from "@project-serum/borsh";
export type Idl = {
version: string;
name: string;
@ -101,3 +104,35 @@ type IdlErrorCode = {
name: string;
msg?: string;
};
// Deterministic IDL address as a function of the program id.
export async function idlAddress(programId: PublicKey): Promise<PublicKey> {
const base = (await PublicKey.findProgramAddress([], programId))[0];
return await PublicKey.createWithSeed(base, seed(), programId);
}
// Seed for generating the idlAddress.
export function seed(): string {
return "anchor:idl";
}
// The on-chain account of the IDL.
export interface IdlProgramAccount {
authority: PublicKey;
data: Buffer;
}
const IDL_ACCOUNT_LAYOUT: borsh.Layout<IdlProgramAccount> = borsh.struct([
borsh.publicKey("authority"),
borsh.vecU8("data"),
]);
export function decodeIdlAccount(data: Buffer): IdlProgramAccount {
return IDL_ACCOUNT_LAYOUT.decode(data);
}
export function encodeIdlAccount(acc: IdlProgramAccount): Buffer {
const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
const len = IDL_ACCOUNT_LAYOUT.encode(acc, buffer);
return buffer.slice(0, len);
}

View File

@ -1,7 +1,8 @@
import { PublicKey } from "@solana/web3.js";
import { inflate } from "pako";
import Provider from "./provider";
import { RpcFactory } from "./rpc";
import { Idl } from "./idl";
import { Idl, idlAddress, decodeIdlAccount } from "./idl";
import Coder from "./coder";
import { Rpcs, Ixs, Txs, Accounts, State } from "./rpc";
import { getProvider } from "./";
@ -78,4 +79,33 @@ export class Program {
this.coder = coder;
this.state = state;
}
/**
* Generates a Program client by fetching the IDL from chain.
*/
public static async at(programId: PublicKey, provider?: Provider) {
const idl = await Program.fetchIdl(programId, provider);
return new Program(idl, programId, provider);
}
/**
* Fetches an idl from the blockchain.
*/
public static async fetchIdl(programId: PublicKey, provider?: Provider) {
provider = provider ?? getProvider();
const address = await idlAddress(programId);
const accountInfo = await provider.connection.getAccountInfo(address);
// Chop off account discriminator.
let idlAccount = decodeIdlAccount(accountInfo.data.slice(8));
const inflatedIdl = inflate(idlAccount.data);
return JSON.parse(decodeUtf8(inflatedIdl));
}
}
function decodeUtf8(array: Uint8Array): string {
const decoder =
typeof TextDecoder === "undefined"
? new (require("util").TextDecoder)("utf-8") // Node.
: new TextDecoder("utf-8"); // Browser.
return decoder.decode(array);
}

View File

@ -829,6 +829,11 @@
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
"@types/pako@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/pako/-/pako-1.0.1.tgz#33b237f3c9aff44d0f82fe63acffa4a365ef4a61"
integrity sha512-GdZbRSJ3Cv5fiwT6I0SQ3ckeN2PWNqxd26W9Z2fCK1tGrrasGy4puvNFtnddqH9UJFMQYXxEuuB7B8UK+LLwSg==
"@types/parse-json@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
@ -4510,6 +4515,11 @@ p-try@^2.0.0:
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
pako@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/pako/-/pako-2.0.3.tgz#cdf475e31b678565251406de9e759196a0ea7a43"
integrity sha512-WjR1hOeg+kki3ZIOjaf4b5WVcay1jaliKSYiEaB1XzwhMQZJxRdQRv0V31EKBYlxb4T7SK3hjfc/jxyU64BoSw==
parent-module@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"