- Introduction
- Installation
- Used by
- Check functions
- FP32 and FP64 math functions
InstructionsAccount
traitBorshSize
trait- Project structure
- Example
This repo is a collection of different utilities in use across various Bonfida projects. The repository has the following structure:
utils
: Mainbonfida-utils
utilities libraryautobindings
: CLI command to autogenerate Typescript or Python bindings for smart contracts written in the specific Bonfida styleautoproject
: CLI command to autogenerate an extensive template smart contractautodoc
: CLI command to generate a documentedinstruction.rs
filemacros
: Auxiliary crate containing macros in use by the mainbonfida-utils
librarycli
: CLI entrypoint for all tools
This repository is published on crates.io, in order to use it in your Solana programs add this to your Cargo.toml
file
bonfida-utils = "0.1"
Install the main bonfida cli
git clone https://github.com/Bonfida/bonfida-utils.git
cd bonfida-utils
cargo install --path cli
To automatically generate Javascript or Python bindings
cd /path/to/your/project
bonfida autobindings --help
To automatically generate a template smart contract
cd /path/to/project/parent
bonfida autoproject project-name
To automatically generate a documented instruction.rs
:
cd /path/to/your/project
bonfida autodoc
In order to generate instruction bindings automatically your project needs to follow a certain structure and derive certain traits that are detailed in the following sections.
bonfida-utils
contains safety verification functions:
check_account_key
check_account_owner
check_signer
bonfida-utils
contains some useful math functions for FP32 and FP64:
fp32_div
fp32_mul
ifp32_div
ifp32_mul
fp64_div
fp64_mul
The Accounts
struct needs to derive the InstructionsAccount
trait in order to automatically generate bindings in Rust and JS. In order to know which accounts are writable and/or signer you will have to specify constraints (cons
) for each account of the struct:
- For writable accounts:
#[cons(writable)]
- For signer accounts:
#[cons(signer)]
- For signer and writable accounts:
#[cons(signer, writable)]
For example
use bonfida_utils::{InstructionsAccount};
#[derive(InstructionsAccount)]
pub struct Accounts<'a, T> {
pub read_only_account: &'a T, // Read only account
#[cons(writable)] // This specifies that the account is writable
pub writable_account: &'a T,
#[cons(signer)] // This specifies that the account is sginer
pub signer_account: &'a T,
// Write the d
#[cons(signer, writable)] // This specifies that the account is sginer and writable
pub signer_and_writable_account: &'a T,
}
To specify accounts that are optional
pub struct Accounts<'a, T> {
// Writable account that is optional
#[cons(writable)]
pub referrer_account_opt: Option<&'a T>,
}
The struct used for the data of the instruction needs to derive the BorshSize
trait, for example let's take the following struct
#[derive(BorshSerialize, BorshDeserialize, BorshSize)]
pub struct Params {
pub position_type: PositionType,
pub market_index: u16,
pub max_base_qty: u64,
pub max_quote_qty: u64,
pub limit_price: u64,
pub match_limit: u64,
pub self_trade_behavior: u8,
pub order_type: OrderType,
pub number_of_markets: u8,
}
In the above example, BorshSize
should be derived for PositionType
and OrderType
as well. The derive macro can take care of this
for field-less enums :
#[derive(BorshSerialize, BorshDeserialize, BorshSize)]
pub enum OrderType {
Limit,
ImmediateOrCancel,
FillOrKill,
PostOnly
}
You might need to implement BorshSize
yourself for certain types (e.g an enum
with variants containing fields) :
#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq, FromPrimitive)]
pub enum ExampleEnum {
FirstVariant,
SecondVariant(u128),
}
impl BorshSize for ExampleEnum {
fn borsh_len(&self) -> usize {
match self {
Self::FirstVariant => 1,
Self::SecondVariant(n) => 1 + n.borsh_len()
}
}
}
π¨ The project structure is important:
- The Solana program must be in a folder called
program
- The JS bindings must be in a folder called
js
- The processor folder needs to contain instructions' logic in separate files. The name of each file needs to be snake case and match the name of the associated function in
instructions.rs
- The instruction enum of
instructions.rs
needs to have Pascal case names that match the snake case names of the files inprocessor
. This is detailed in the example below. - The instruction enum of
instructions.rs
needs to be the first enum to be defined in that file.
Let's have a look at a real life example.
The project structure is as follow
βββ program
β βββ instructions.rs
β βββ processor
β β βββ create_market.rs
βββ js
βββ src
βββ python
βββ src
β...
β... The rest is omitted
To simplify we will consider only one instruction create_market.rs
and only focus on processor
and instructions.rs
.
We can see from the project structure that create_market.rs
is located in the processor
directory, let's have a look at the contents of the file:
//! Creates a perp market
// The sentence above will be used to describe the instruction in the auto generated doc β οΈ
use bonfida_utils::{checks::check_signer, BorshSize, InstructionsAccount};
use borsh::{BorshDeserialize, BorshSerialize};
// Other imports are omitted
#[derive(InstructionsAccount)]
pub struct Accounts<'a, T> {
/// The market address
#[cons(writable)] // This specifies that the account is writable
pub market: &'a T,
/// The ecosystem address
#[cons(writable)]
pub ecosystem: &'a T,
/// The address of the Serum Core market
pub aob_orderbook: &'a T,
/// The address of the Serum Core event queue
pub aob_event_queue: &'a T,
/// The address of the Serum Core asks slab
pub aob_asks: &'a T,
/// The address of the Serum Core bids slab
pub aob_bids: &'a T,
/// The program ID of Serum Core
pub aob_program: &'a T,
/// The Pyth oracle address for this market
pub oracle: &'a T,
/// The market admin address
#[cons(signer)] // This specifies that the account is signer
pub admin: &'a T,
/// The market vault address
pub vault: &'a T,
}
// Constraints (cons) can be combined e.g
// #[cons(signer, writable)]
// pub some_account: &'a T
//
// Optional accounts are supported as well
// pub discount_account_opt: Option<&'a T>
// BorshSize might require custom impl e.g for enum
#[derive(BorshSerialize, BorshDeserialize, BorshSize)]
pub struct Params {
pub market_symbol: String,
pub signer_nonce: u8,
pub coin_decimals: u8,
pub quote_decimals: u8,
}
impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
pub fn parse(accounts: &'a [AccountInfo<'b>]) -> Result<Self, ProgramError> {
let accounts_iter = &mut accounts.iter();
let a = Accounts {
market: next_account_info(accounts_iter)?,
ecosystem: next_account_info(accounts_iter)?,
aob_orderbook: next_account_info(accounts_iter)?,
aob_event_queue: next_account_info(accounts_iter)?,
aob_asks: next_account_info(accounts_iter)?,
aob_bids: next_account_info(accounts_iter)?,
aob_program: next_account_info(accounts_iter)?,
oracle: next_account_info(accounts_iter)?,
admin: next_account_info(accounts_iter)?,
vault: next_account_info(accounts_iter)?,
};
// Account checks are omitted
Ok(a)
}
}
pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], params: Params) -> ProgramResult {
let accounts = Accounts::parse(accounts)?;
let Params {
market_symbol,
signer_nonce,
coin_decimals,
quote_decimals,
} = params;
// Instruction logic is omitted
Ok(())
}
We can see that create_market.rs
contains two important definitions:
Accounts
struct: itsInstructionsAccount
trait implementation should be derived, and the constraints for each account of the struct should be specifiedParams
struct: itsBorshSize
trait implementation should be derived
The instruction.rs
file can be autogenerated using cargo autodoc
use bonfida_utils::InstructionsAccount;
// Other imports are omitted
#[derive(BorshSerialize, BorshDeserialize)]
pub enum PerpInstruction {
/// Creates a new perps market
///
/// | Index | Writable | Signer | Description |
/// | --------------------------------------------------------------------- |
/// | 0 | β
| β | The market address |
/// | 1 | β
| β | The ecosystem address |
/// | 2 | β | β | The address of the Serum Core market |
/// | 3 | β | β | The address of the Serum Core event queue |
/// | 4 | β | β | The address of the Serum Core asks slab |
/// | 5 | β | β | The address of the Serum Core bids slab |
/// | 6 | β | β | The program ID of Serum Core |
/// | 7 | β | β | The Pyth oracle address for this market |
/// | 8 | β | β
| The market admin address |
/// | 9 | β | β | The market vault address |
CreateMarket,
///
/// ...
}
pub fn create_market(
accounts: create_market::Accounts<Pubkey>,
params: create_market::Params,
) -> Instruction {
accounts.get_instruction(crate::ID, PerpInstruction::CreateMarket as u8, params)
}
In order to generate Javascript instruction bindings run
cargo autobindings
This will generate a file named raw_instructions.ts
that contains all the instructions of your program
// This file is auto-generated. DO NOT EDIT
import BN from "bn.js";
import { Schema, serialize } from "borsh";
import { PublicKey, TransactionInstruction } from "@solana/web3.js";
export interface AccountKey {
pubkey: PublicKey;
isSigner: boolean;
isWritable: boolean;
}
export class createMarketInstruction {
tag: number;
marketSymbol: string;
signerNonce: number;
coinDecimals: number;
quoteDecimals: number;
static schema: Schema = new Map([
[
createMarketInstruction,
{
kind: "struct",
fields: [
["tag", "u8"],
["marketSymbol", "string"],
["signerNonce", "u8"],
["coinDecimals", "u8"],
["quoteDecimals", "u8"],
],
},
],
]);
constructor(obj: {
marketSymbol: string,
signerNonce: number,
coinDecimals: number,
quoteDecimals: number,
}) {
this.tag = 0;
this.marketSymbol = obj.marketSymbol;
this.signerNonce = obj.signerNonce;
this.coinDecimals = obj.coinDecimals;
this.quoteDecimals = obj.quoteDecimals;
}
serialize(): Uint8Array {
return serialize(createMarketInstruction.schema, this);
}
getInstruction(
programId: PublicKey,
market: PublicKey,
ecosystem: PublicKey,
aobOrderbook: PublicKey,
aobEventQueue: PublicKey,
aobAsks: PublicKey,
aobBids: PublicKey,
aobProgram: PublicKey,
oracle: PublicKey,
admin: PublicKey,
vault: PublicKey
): TransactionInstruction {
const data = Buffer.from(this.serialize());
let keys: AccountKey[] = [];
keys.push({
pubkey: market,
isSigner: false,
isWritable: true,
});
keys.push({
pubkey: ecosystem,
isSigner: false,
isWritable: true,
});
keys.push({
pubkey: aobOrderbook,
isSigner: false,
isWritable: false,
});
keys.push({
pubkey: aobEventQueue,
isSigner: false,
isWritable: false,
});
keys.push({
pubkey: aobAsks,
isSigner: false,
isWritable: false,
});
keys.push({
pubkey: aobBids,
isSigner: false,
isWritable: false,
});
keys.push({
pubkey: aobProgram,
isSigner: false,
isWritable: false,
});
keys.push({
pubkey: oracle,
isSigner: false,
isWritable: false,
});
keys.push({
pubkey: admin,
isSigner: true,
isWritable: false,
});
keys.push({
pubkey: vault,
isSigner: false,
isWritable: false,
});
return new TransactionInstruction({
keys,
programId,
data,
});
}
}
To generate Python bindings run
bonfida autobindings --target-language py
To run the autobindings tests you have to:
- Regenerate the js and python bindings to be sure they are up to date
- Run
yarn
in the js folder - Install ts-node with:
sudo npm install -g ts-node typescript '@types/node'
- From the
program
folder, runbonfida autobindings --test true