Skip to content

Commit

Permalink
Implement instruction buffer (#240)
Browse files Browse the repository at this point in the history
* StagedTXInstruction -> TXInstructionBuffer

* Scaffold instruction handlers

* Implement InitIxBuffer handler

* Implement writeTx

* Implement execut_ix

* Fixup

* Fixes

* Test write then execute

* Test write and execute multiple instructions

* close_ix_buffer

* Test close buffer

* Account validations

* clippy

* Update docs

* Add events

* Fix doc
  • Loading branch information
michaelhly authored Mar 1, 2022
1 parent fc4594c commit ec0cc48
Show file tree
Hide file tree
Showing 13 changed files with 438 additions and 22 deletions.
72 changes: 72 additions & 0 deletions programs/smart-wallet/src/instructions/buffer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//! Init or close an [InstructionBuffer].

use crate::*;

#[derive(Accounts)]
pub struct InitIxBuffer<'info> {
#[account(zero)]
pub buffer: Account<'info, InstructionBuffer>,
/// CHECK: Writer account that can write to the buffer.
pub writer: UncheckedAccount<'info>,
}

/// Emitted when a [InstructionBuffer] is initialized.
#[event]
pub struct InitBufferEvent {
/// The [InstructionBuffer::writer].
#[index]
pub writer: Pubkey,
/// The buffer.
pub buffer: Pubkey,
}

pub fn handle_init(ctx: Context<InitIxBuffer>) -> Result<()> {
let buffer = &mut ctx.accounts.buffer;
buffer.writer = ctx.accounts.writer.key();

emit!(InitBufferEvent {
writer: buffer.writer,
buffer: buffer.key()
});

Ok(())
}

impl<'info> Validate<'info> for InitIxBuffer<'info> {
fn validate(&self) -> Result<()> {
Ok(())
}
}

#[derive(Accounts)]
pub struct CloseIxBuffer<'info> {
#[account(mut, close = writer)]
pub buffer: Account<'info, InstructionBuffer>,
pub writer: Signer<'info>,
}

/// Emitted when an [InstructionBuffer] is closed.
#[event]
pub struct CloseBufferEvent {
/// The [InstructionBuffer::writer].
#[index]
pub writer: Pubkey,
/// The buffer.
pub buffer: Pubkey,
}

pub fn handle_close(ctx: Context<CloseIxBuffer>) -> Result<()> {
emit!(CloseBufferEvent {
writer: ctx.accounts.writer.key(),
buffer: ctx.accounts.buffer.key(),
});
Ok(())
}

impl<'info> Validate<'info> for CloseIxBuffer<'info> {
fn validate(&self) -> Result<()> {
assert_keys_eq!(self.writer.key(), self.buffer.writer);

Ok(())
}
}
28 changes: 28 additions & 0 deletions programs/smart-wallet/src/instructions/execute_ix.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//! Executes an instruction off of the [InstructionBuffer].

use anchor_lang::solana_program::program::invoke;

use crate::*;

#[derive(Accounts)]
pub struct ExecuteIx<'info> {
#[account(mut)]
pub buffer: Box<Account<'info, InstructionBuffer>>,
}

pub fn handler<'info>(ctx: Context<'_, '_, '_, 'info, ExecuteIx<'info>>) -> Result<()> {
let buffer = &mut ctx.accounts.buffer;
let ix = &buffer.staged_tx_instructions[buffer.exec_count as usize];
invoke(&ix.into(), ctx.remaining_accounts)?;
buffer.exec_count += 1;

Ok(())
}

impl<'info> Validate<'info> for ExecuteIx<'info> {
fn validate(&self) -> Result<()> {
invariant!(self.buffer.exec_count < self.buffer.staged_tx_instructions.len() as u8);

Ok(())
}
}
7 changes: 7 additions & 0 deletions programs/smart-wallet/src/instructions/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
pub mod buffer;
pub mod execute_ix;
pub mod write_ix;

pub use buffer::*;
pub use execute_ix::*;
pub use write_ix::*;
43 changes: 43 additions & 0 deletions programs/smart-wallet/src/instructions/write_ix.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//! Writes an instruction to the [InstructionBuffer].

use crate::*;

#[derive(Accounts)]
pub struct WriteIx<'info> {
#[account(mut)]
pub buffer: Box<Account<'info, InstructionBuffer>>,
pub writer: Signer<'info>,
}

/// Emitted when an instruction is written to the [InstructionBuffer].
#[event]
pub struct WriteIxEvent {
/// The [InstructionBuffer].
pub buffer: Pubkey,
/// The [InstructionBuffer::writer].
pub writer: Pubkey,
/// The program id of the instruction written.
pub program_id: Pubkey,
}

pub fn handler(ctx: Context<WriteIx>, ix: TXInstruction) -> Result<()> {
let buffer = &mut ctx.accounts.buffer;

emit!(WriteIxEvent {
buffer: buffer.key(),
writer: buffer.writer,
program_id: ix.program_id
});

buffer.staged_tx_instructions.push(ix);

Ok(())
}

impl<'info> Validate<'info> for WriteIx<'info> {
fn validate(&self) -> Result<()> {
assert_keys_eq!(self.writer.key(), self.buffer.writer);

Ok(())
}
}
22 changes: 22 additions & 0 deletions programs/smart-wallet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ use anchor_lang::solana_program;
use vipers::prelude::*;

mod events;
mod instructions;
mod state;
mod validators;

pub use events::*;
pub use instructions::*;
pub use state::*;

/// Number of seconds in a day.
Expand Down Expand Up @@ -386,6 +388,26 @@ pub mod smart_wallet {

Ok(())
}

#[access_control(ctx.accounts.validate())]
pub fn init_ix_buffer(ctx: Context<InitIxBuffer>) -> Result<()> {
instructions::buffer::handle_init(ctx)
}

#[access_control(ctx.accounts.validate())]
pub fn close_ix_buffer(ctx: Context<CloseIxBuffer>) -> Result<()> {
instructions::buffer::handle_close(ctx)
}

#[access_control(ctx.accounts.validate())]
pub fn execute_ix<'info>(ctx: Context<'_, '_, '_, 'info, ExecuteIx<'info>>) -> Result<()> {
instructions::execute_ix::handler(ctx)
}

#[access_control(ctx.accounts.validate())]
pub fn write_ix(ctx: Context<WriteIx>, ix: TXInstruction) -> Result<()> {
instructions::write_ix::handler(ctx, ix)
}
}

/// Accounts for [smart_wallet::create_smart_wallet].
Expand Down
26 changes: 8 additions & 18 deletions programs/smart-wallet/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,24 +194,14 @@ pub struct SubaccountInfo {
pub index: u64,
}

/// An account which holds the data of a single [TXInstruction].
/// Creating this allows an owner-invoker to execute a transaction
/// with a minimal transaction size.
/// An account which holds an array of TxInstructions to be executed.
#[account]
#[derive(Default, Debug, PartialEq)]
pub struct StagedTXInstruction {
/// The [SmartWallet] to execute this on.
pub smart_wallet: Pubkey,
/// The owner-invoker index.
pub index: u64,
/// Bump seed of the owner-invoker.
pub owner_invoker_bump: u8,

/// The owner which will execute the instruction.
pub owner: Pubkey,
/// Owner set sequence number.
pub owner_set_seqno: u32,

/// The instruction to execute.
pub ix: TXInstruction,
pub struct InstructionBuffer {
/// Execution count on this buffer.
pub exec_count: u8,
/// Key that can write to the buffer.
pub writer: Pubkey,
/// Staged instructions to be executed.
pub staged_tx_instructions: Vec<TXInstruction>,
}
2 changes: 2 additions & 0 deletions src/programs/smartWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type SmartWalletTypes = AnchorTypes<
{
smartWallet: SmartWalletData;
transaction: SmartWalletTransactionData;
instructionBuffer: InstructionBufferData;
subaccountInfo: SubaccountInfoData;
},
{
Expand All @@ -21,6 +22,7 @@ export type SmartWalletTypes = AnchorTypes<
type Accounts = SmartWalletTypes["Accounts"];
export type SmartWalletData = Accounts["SmartWallet"];
export type SmartWalletTransactionData = Accounts["Transaction"];
export type InstructionBufferData = Accounts["InstructionBuffer"];
export type SubaccountInfoData = Accounts["SubaccountInfo"];

export type SmartWalletInstruction = Omit<
Expand Down
8 changes: 8 additions & 0 deletions src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import mapValues from "lodash.mapvalues";

import type { Programs } from "./constants";
import { GOKI_ADDRESSES, GOKI_IDLS } from "./constants";
import { InstructionLoaderWrapper } from "./wrappers/instructionLoader";
import type { PendingSmartWallet } from "./wrappers/smartWallet";
import {
findOwnerInvokerAddress,
Expand All @@ -30,6 +31,13 @@ export class GokiSDK {
readonly programs: Programs
) {}

/**
* Wrapper for the instruction loader.
*/
get instructionLoader(): InstructionLoaderWrapper {
return new InstructionLoaderWrapper(this);
}

/**
* Creates a new instance of the SDK with the given keypair.
*/
Expand Down
110 changes: 110 additions & 0 deletions src/wrappers/instructionLoader/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { TransactionEnvelope } from "@saberhq/solana-contrib";
import type {
AccountMeta,
PublicKey,
TransactionInstruction,
} from "@solana/web3.js";
import { Keypair } from "@solana/web3.js";

import type { SmartWalletProgram } from "../../programs";
import type { InstructionBufferData } from "../../programs/smartWallet";
import type { GokiSDK } from "../../sdk";
import type { PendingBuffer } from "./types";

export class InstructionLoaderWrapper {
readonly program: SmartWalletProgram;

constructor(readonly sdk: GokiSDK) {
this.program = sdk.programs.SmartWallet;
}

/**
* loadBufferData
* @returns
*/
async loadBufferData(
bufferAccount: PublicKey
): Promise<InstructionBufferData> {
return await this.program.account.instructionBuffer.fetch(bufferAccount);
}

/**
* Initialize a loader buffer.
*/
async initBuffer(
bufferSize: number,
writer: PublicKey = this.sdk.provider.wallet.publicKey,
bufferAccount: Keypair = Keypair.generate()
): Promise<PendingBuffer> {
const tx = new TransactionEnvelope(
this.sdk.provider,
[
await this.program.account.instructionBuffer.createInstruction(
bufferAccount,
this.program.account.instructionBuffer.size + bufferSize
),
this.program.instruction.initIxBuffer({
accounts: {
buffer: bufferAccount.publicKey,
writer,
},
}),
],
[bufferAccount]
);

return {
tx,
bufferAccount: bufferAccount.publicKey,
};
}

closeBuffer(
bufferAccount: PublicKey,
writer: PublicKey = this.sdk.provider.wallet.publicKey
): TransactionEnvelope {
return new TransactionEnvelope(this.sdk.provider, [
this.program.instruction.closeIxBuffer({
accounts: {
buffer: bufferAccount,
writer,
},
}),
]);
}

/**
* Executes an instruction from the buffer.
*/
executeInstruction(
bufferAccount: PublicKey,
accountMetas: AccountMeta[]
): TransactionEnvelope {
return new TransactionEnvelope(this.sdk.provider, [
this.program.instruction.executeIx({
accounts: {
buffer: bufferAccount,
},
remainingAccounts: accountMetas,
}),
]);
}

/**
* Write an instruction to the buffer.
*/
writeInstruction(
ix: TransactionInstruction,
bufferAccount: PublicKey,
writer: PublicKey = this.sdk.provider.wallet.publicKey
): TransactionEnvelope {
return new TransactionEnvelope(this.sdk.provider, [
this.program.instruction.writeIx(ix, {
accounts: {
buffer: bufferAccount,
writer,
},
}),
]);
}
}
6 changes: 6 additions & 0 deletions src/wrappers/instructionLoader/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { PublicKey, TransactionEnvelope } from "@saberhq/solana-contrib";

export type PendingBuffer = {
tx: TransactionEnvelope;
bufferAccount: PublicKey;
};
2 changes: 1 addition & 1 deletion src/wrappers/smartWallet/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { PublicKey, TransactionInstruction } from "@solana/web3.js";
import type BN from "bn.js";

import type { SmartWalletData } from "../../programs";
import type { SmartWalletWrapper } from ".";
import type { SmartWalletWrapper } from "./index";

export type InitSmartWalletWrapperArgs = {
readonly bump: number;
Expand Down
Loading

0 comments on commit ec0cc48

Please sign in to comment.