Browse Source

examples: Add dex-crank-relay

Armani Ferrante 4 năm trước cách đây
mục cha
commit
313d13e300

+ 2 - 0
examples/dex-crank-relay/Anchor.toml

@@ -0,0 +1,2 @@
+cluster = "devnet"
+wallet = "~/.config/solana/id.json"

+ 4 - 0
examples/dex-crank-relay/Cargo.toml

@@ -0,0 +1,4 @@
+[workspace]
+members = [
+    "programs/*"
+]

+ 13 - 0
examples/dex-crank-relay/migrations/deploy.js

@@ -0,0 +1,13 @@
+
+// Migrations are an early feature. Currently, they're nothing more than this
+// single deploy script that's invoked from the CLI, injecting a provider
+// configured from the workspace's Anchor.toml.
+
+const anchor = require("@project-serum/anchor");
+
+module.exports = async function (provider) {
+  // Configure client to use the provider.
+  anchor.setProvider(provider);
+
+  // Add your deploy script here.
+}

+ 22 - 0
examples/dex-crank-relay/programs/dex-crank-relay/Cargo.toml

@@ -0,0 +1,22 @@
+[package]
+name = "dex-crank-relay"
+version = "0.1.0"
+description = "Created with Anchor"
+edition = "2018"
+
+[lib]
+crate-type = ["cdylib", "lib"]
+name = "dex_crank_relay"
+
+[features]
+no-entrypoint = []
+no-idl = []
+cpi = ["no-entrypoint"]
+default = []
+
+[dependencies]
+anchor-lang = { git = "https://github.com/project-serum/anchor", features = ["derive"] }
+anchor-spl = { git = "https://github.com/project-serum/anchor" }
+registry = { path = "../../../lockup/programs/registry", features = ["cpi"] }
+serum_dex = { git = "https://github.com/project-serum/serum-dex", features = ["no-entrypoint"] }
+enumflags2 = "0.6.4"

+ 2 - 0
examples/dex-crank-relay/programs/dex-crank-relay/Xargo.toml

@@ -0,0 +1,2 @@
+[target.bpfel-unknown-unknown.dependencies.std]
+features = []

+ 312 - 0
examples/dex-crank-relay/programs/dex-crank-relay/src/lib.rs

@@ -0,0 +1,312 @@
+//! A relatively advanced example. If new to Anchor, it's recommended to start
+//! with other examples, first.
+//!
+//! dex-crank-relay is a proxy program that relays a `ConsumeEvents` instruction
+//! to the DEX, counts the number of events processed, and pays out a
+//! transaction fee as a function of `fee = fee_rate * num_events_consumed`.
+//!
+//! To be eligible for the reward, one must first own `stake_threshold` staking
+//! pool tokens on the configured staking "registrar".
+
+#![feature(proc_macro_hygiene)]
+
+use anchor_lang::prelude::*;
+use anchor_lang::solana_program;
+use anchor_lang::solana_program::instruction::Instruction;
+use anchor_spl::token::{self, TokenAccount, Transfer};
+use enumflags2::BitFlags;
+use registry::{Member, Registrar};
+use serum_dex::state::AccountFlag;
+
+#[program]
+pub mod dex_crank_relay {
+    use super::*;
+
+    pub fn create_reward(ctx: Context<CreateReward>, reward_bucket: RewardBucket) -> Result<()> {
+        (*ctx.accounts.reward_bucket) = reward_bucket;
+        Ok(())
+    }
+
+    pub fn set_stake_threshold(ctx: Context<Auth>, threshold: u64) -> Result<()> {
+        ctx.accounts.reward_bucket.stake_threshold = threshold;
+        Ok(())
+    }
+
+    pub fn set_fee_rate(ctx: Context<Auth>, fee_rate: u64) -> Result<()> {
+        ctx.accounts.reward_bucket.fee_rate = fee_rate;
+        Ok(())
+    }
+
+    pub fn set_authority(ctx: Context<Auth>, new_authority: Pubkey) -> Result<()> {
+        ctx.accounts.reward_bucket.authority = new_authority;
+        Ok(())
+    }
+
+    pub fn set_dex(ctx: Context<Auth>, new_dex_program: Pubkey) -> Result<()> {
+        ctx.accounts.reward_bucket.dex_program = new_dex_program;
+        Ok(())
+    }
+
+    pub fn set_registrar(ctx: Context<Auth>, new_registrar: Pubkey) -> Result<()> {
+        ctx.accounts.reward_bucket.registrar = new_registrar;
+        Ok(())
+    }
+
+    pub fn set_registry_program(ctx: Context<Auth>, new_registry_program: Pubkey) -> Result<()> {
+        ctx.accounts.reward_bucket.registry_program = new_registry_program;
+        Ok(())
+    }
+
+    pub fn migrate(ctx: Context<Migrate>) -> Result<()> {
+        let seeds = [
+            ctx.accounts.reward_bucket.to_account_info().key.as_ref(),
+            &[ctx.accounts.reward_bucket.nonce],
+        ];
+        let signer = &[&seeds[..]];
+        let cpi_ctx: CpiContext<Transfer> = (&*ctx.accounts).into();
+        token::transfer(cpi_ctx.with_signer(signer), ctx.accounts.vault.amount)?;
+        Ok(())
+    }
+
+    #[access_control(CrankRelay::accounts(&ctx))]
+    pub fn crank_relay<'a, 'b, 'c, 'info>(
+        ctx: Context<'a, 'b, 'c, 'info, CrankRelay<'info>>,
+        dex_data: Vec<u8>,
+    ) -> Result<()> {
+        if !is_staked(&ctx) {
+            return Err(ErrorCode::InsufficientStake.into());
+        }
+
+        // Event queue len before.
+        let before_event_count = event_q_len(
+            &ctx.accounts
+                .dex_event_q
+                .to_account_info()
+                .try_borrow_data()?,
+        );
+
+        // Invoke crank relay.
+        {
+            let dex_instruction = {
+                let relay_meta_accs = ctx
+                    .remaining_accounts
+                    .iter()
+                    .map(|acc_info| {
+                        if acc_info.is_writable {
+                            AccountMeta::new(*acc_info.key, acc_info.is_signer)
+                        } else {
+                            AccountMeta::new_readonly(*acc_info.key, acc_info.is_signer)
+                        }
+                    })
+                    .collect::<Vec<AccountMeta>>();
+                Instruction {
+                    program_id: *ctx.accounts.dex_program.key,
+                    accounts: relay_meta_accs,
+                    data: dex_data,
+                }
+            };
+            let mut relay_accs = vec![ctx.accounts.dex_program.clone()];
+            relay_accs.extend_from_slice(ctx.remaining_accounts);
+
+            solana_program::program::invoke(&dex_instruction, &relay_accs)?;
+        }
+
+        // Event queue len after.
+        let after_event_count = event_q_len(
+            &ctx.accounts
+                .dex_event_q
+                .to_account_info()
+                .try_borrow_data()?,
+        );
+
+        // Calculate crank fee.
+        let fee = {
+            assert!(before_event_count >= after_event_count);
+            let num_events = before_event_count - after_event_count;
+            let fee = num_events * ctx.accounts.reward_bucket.fee_rate;
+            if ctx.accounts.vault.amount < fee {
+                msg!("vault depleted");
+                return Ok(());
+            }
+            fee
+        };
+
+        // Pay out reward.
+        let seeds = [
+            ctx.accounts.reward_bucket.to_account_info().key.as_ref(),
+            &[ctx.accounts.reward_bucket.nonce],
+        ];
+        let signer = &[&seeds[..]];
+        let cpi_ctx: CpiContext<Transfer> = (&*ctx.accounts).into();
+        token::transfer(cpi_ctx.with_signer(signer), fee)?;
+
+        Ok(())
+    }
+}
+
+#[derive(Accounts)]
+pub struct CreateReward<'info> {
+    #[account(init)]
+    reward_bucket: ProgramAccount<'info, RewardBucket>,
+    rent: Sysvar<'info, Rent>,
+}
+
+#[derive(Accounts)]
+pub struct Auth<'info> {
+    #[account(mut, has_one = authority)]
+    reward_bucket: ProgramAccount<'info, RewardBucket>,
+    #[account(signer)]
+    authority: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct Migrate<'info> {
+    #[account(mut, has_one = vault, has_one = authority)]
+    reward_bucket: ProgramAccount<'info, RewardBucket>,
+    #[account(seeds = [
+        reward_bucket.to_account_info().key.as_ref(),
+        &[reward_bucket.nonce],
+    ])]
+    reward_bucket_signer: AccountInfo<'info>,
+    #[account(signer)]
+    authority: AccountInfo<'info>,
+    #[account(mut)]
+    vault: CpiAccount<'info, TokenAccount>,
+    #[account(mut)]
+    to: AccountInfo<'info>,
+    #[account("token_program.key == &token::ID")]
+    token_program: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct CrankRelay<'info> {
+    // Reward bucket.
+    #[account(
+        has_one = vault,
+        has_one = registrar,
+        has_one = registry_program,
+        has_one = dex_program,
+    )]
+    reward_bucket: ProgramAccount<'info, RewardBucket>,
+    #[account(
+        seeds = [
+            reward_bucket.to_account_info().key.as_ref(),
+            &[reward_bucket.nonce],
+        ]
+    )]
+    reward_bucket_signer: AccountInfo<'info>,
+    vault: CpiAccount<'info, TokenAccount>,
+
+    // Stake registry. Since they're CPI accounts, make sure to check owners
+    // so that we can avoid actually invoking the CPI and instead just read the
+    // accounts.
+    registry_program: AccountInfo<'info>,
+    #[account("registrar.to_account_info().owner == registry_program.key")]
+    registrar: CpiAccount<'info, Registrar>,
+    #[account(
+        belongs_to = registrar,
+        "member.to_account_info().owner == registry_program.key"
+    )]
+    member: CpiAccount<'info, Member>,
+    #[account("member_spt.to_account_info().key == &member.balances.spt")]
+    member_spt: CpiAccount<'info, TokenAccount>,
+    #[account("member_locked_spt.to_account_info().key == &member.balances_locked.spt")]
+    member_locked_spt: CpiAccount<'info, TokenAccount>,
+
+    // DEX.
+    #[account("dex_event_q.owner == dex_program.key")]
+    dex_event_q: AccountInfo<'info>,
+    dex_program: AccountInfo<'info>,
+
+    // Pay reward to.
+    #[account(mut)]
+    to: AccountInfo<'info>,
+    #[account("token_program.key == &token::ID")]
+    token_program: AccountInfo<'info>,
+}
+
+impl<'info> CrankRelay<'info> {
+    pub fn accounts(ctx: &Context<CrankRelay>) -> Result<()> {
+        let data = ctx.accounts.dex_event_q.try_borrow_data()?;
+
+        // b"serum" || account_flags;
+        let mut raw_flags = [0u8; 8];
+        raw_flags.copy_from_slice(&data[5..13]);
+        let account_flags = BitFlags::from_bits(u64::from_le_bytes(raw_flags))
+            .map_err(|_| ErrorCode::UnparseableAccountFlags)?;
+        if account_flags != (AccountFlag::Initialized | AccountFlag::EventQueue) {
+            return Err(ErrorCode::InvalidEventQueue.into());
+        }
+
+        Ok(())
+    }
+}
+
+#[account]
+pub struct RewardBucket {
+    vault: Pubkey,
+    nonce: u8,
+    registrar: Pubkey,
+    registry_program: Pubkey,
+    dex_program: Pubkey,
+    authority: Pubkey,
+    fee_rate: u64,
+    stake_threshold: u64,
+}
+
+fn is_staked(ctx: &Context<CrankRelay>) -> bool {
+    let total_staked = ctx.accounts.member_spt.amount + ctx.accounts.member_locked_spt.amount;
+    let stake_threshold = ctx.accounts.reward_bucket.stake_threshold;
+    if total_staked < stake_threshold {
+        return false;
+    }
+    true
+}
+
+#[error]
+pub enum ErrorCode {
+    #[msg("Please stake more to be eligible for crank transaction fees.")]
+    InsufficientStake,
+    #[msg("Event queue account does not have valid account flags.")]
+    InvalidEventQueue,
+    #[msg("Unable to parse DEX event queue account flags.")]
+    UnparseableAccountFlags,
+    Unknown,
+}
+
+impl<'a, 'b, 'c, 'info> From<&Migrate<'info>> for CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> {
+    fn from(accounts: &Migrate<'info>) -> CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> {
+        let cpi_accounts = Transfer {
+            from: accounts.vault.to_account_info().clone(),
+            to: accounts.to.to_account_info(),
+            authority: accounts.reward_bucket_signer.clone(),
+        };
+        let cpi_program = accounts.token_program.clone();
+        CpiContext::new(cpi_program, cpi_accounts)
+    }
+}
+
+impl<'a, 'b, 'c, 'info> From<&CrankRelay<'info>>
+    for CpiContext<'a, 'b, 'c, 'info, Transfer<'info>>
+{
+    fn from(accounts: &CrankRelay<'info>) -> CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> {
+        let cpi_accounts = Transfer {
+            from: accounts.vault.to_account_info().clone(),
+            to: accounts.to.to_account_info(),
+            authority: accounts.reward_bucket_signer.clone(),
+        };
+        let cpi_program = accounts.token_program.clone();
+        CpiContext::new(cpi_program, cpi_accounts)
+    }
+}
+
+// Returns the length of the Serum DEX event queue account represented by the
+// given `data`.
+fn event_q_len(data: &[u8]) -> u64 {
+    // b"serum" || account_flags || head.
+    let count_start = 5 + 8 + 8;
+    let count_end = count_start + 4;
+    let mut b = [0u8; 4];
+    b.copy_from_slice(&data[count_start..count_end]);
+    u32::from_le_bytes(b) as u64
+}

+ 14 - 0
examples/dex-crank-relay/tests/dex-crank-relay.js

@@ -0,0 +1,14 @@
+const anchor = require('@project-serum/anchor');
+
+describe('dex-crank-relay', () => {
+
+  // Configure the client to use the local cluster.
+  anchor.setProvider(anchor.Provider.env());
+
+  it('Is initialized!', async () => {
+    // Add your test here.
+    const program = anchor.workspace.DexCrankRelay;
+    const tx = await program.rpc.initialize();
+    console.log("Your transaction signature", tx);
+  });
+});