First commit
Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
commit
8df4979297
|
@ -0,0 +1,12 @@
|
|||
node_modules
|
||||
dist
|
||||
ts/**/*.js
|
||||
|
||||
.anchor
|
||||
target
|
||||
test-ledger
|
||||
|
||||
.DS_Store
|
||||
|
||||
**/*.rs.bk
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
.anchor
|
||||
.DS_Store
|
||||
target
|
||||
node_modules
|
||||
dist
|
||||
build
|
||||
test-ledger
|
|
@ -0,0 +1,16 @@
|
|||
[features]
|
||||
seeds = true
|
||||
skip-lint = false
|
||||
|
||||
[programs.localnet]
|
||||
mango_v3_reimbursement = "m3roABq4Ta3sGyFRLdY4LH1KN16zBtg586gJ3UxoBzb"
|
||||
|
||||
[registry]
|
||||
url = "https://api.apr.dev"
|
||||
|
||||
[provider]
|
||||
cluster = "localnet"
|
||||
wallet = "/Users/mc/.config/solana/id.json"
|
||||
|
||||
[scripts]
|
||||
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,13 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"programs/*"
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
overflow-checks = true
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
[profile.release.build-override]
|
||||
opt-level = 3
|
||||
incremental = false
|
||||
codegen-units = 1
|
|
@ -0,0 +1,38 @@
|
|||
Flow
|
||||
|
||||
- Write to table
|
||||
- Create group
|
||||
- Create vaults
|
||||
- Token Transfer to vaults outside the context of the program
|
||||
- UI creates Reimbursement account
|
||||
- UI packs tx with ixs which contain
|
||||
** create token account OR a create idempotent ATA
|
||||
** reimburse to above token account OR ATA
|
||||
|
||||
# mango-v3-reimbursement
|
||||
|
||||
This repository contains code that allows users of mango v3 to receive re-imbursement after the emergency shutdown in Slot 154,889,307.
|
||||
|
||||
## Deployment
|
||||
|
||||
First calculate the re-imbursement from a recent solana snapshot, we used Slot 154,856,403:
|
||||
|
||||
- download the [solana snapshot](https://drive.google.com/file/d/1nYJjW0n2pSpAOwf7kUR_p-Cj2PpS3kcn/view?usp=sharing)
|
||||
- download the [deposits & withdrawals](https://docs.google.com/spreadsheets/d/1DwtllQeCw3j9-DjNFgxSk_Gl_L8405W9ExjeqvKVjds/edit#gid=0) since snapshot as tsv
|
||||
- compile PR #208 on v3 and the following two commands to generate a csv as well as a binary buffer
|
||||
|
||||
```
|
||||
cargo --bin cli equity-from-snapshot \
|
||||
--snapshot snapshot-154856403-HJj8oytGGRnbUoFdojewzBWB3FrTBaTJdx1v4g63oUWc.sqlite \
|
||||
--late-changes deposits_withdraws.tsv \
|
||||
--program mv3ekLzLbnVPNxjSKvqBpU3ZeZXPQdEC3bp5MDEBG68 \
|
||||
--group 98pjRuQjK3qA6gXts96PqZT4Ze5QmnCmt3QYjhbUSPue \
|
||||
--outtype csv --outfile reimbursement.csv
|
||||
|
||||
cargo --bin cli equity-from-snapshot \
|
||||
--snapshot snapshot-154856403-HJj8oytGGRnbUoFdojewzBWB3FrTBaTJdx1v4g63oUWc.sqlite \
|
||||
--late-changes deposits_withdraws.tsv \
|
||||
--program mv3ekLzLbnVPNxjSKvqBpU3ZeZXPQdEC3bp5MDEBG68 \
|
||||
--group 98pjRuQjK3qA6gXts96PqZT4Ze5QmnCmt3QYjhbUSPue \
|
||||
--outtype bin --outfile reimbursement.bin
|
||||
```
|
|
@ -0,0 +1,21 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Anchor works purely on a token level and does not know that the index types
|
||||
# are just type aliases for a primitive type. This hack replaces them with the
|
||||
# primitive in the idl json and types ts file.
|
||||
for pair_str in \
|
||||
"TokenIndex u16" \
|
||||
"Serum3MarketIndex u16" \
|
||||
"PerpMarketIndex u16" \
|
||||
"usize u64" \
|
||||
"NodeHandle u32" \
|
||||
; do
|
||||
pair=( $pair_str );
|
||||
perl -0777 -pi -e "s/\{\s*\"defined\":\s*\"${pair[0]}\"\s*\}/\"${pair[1]}\"/g" \
|
||||
target/idl/mango_v3_reimbursement.json target/types/mango_v3_reimbursement.ts;
|
||||
done
|
||||
|
||||
# Anchor puts all enums in the IDL, independent of visibility. And then it
|
||||
# errors on enums that have tuple variants. This hack drops these from the idl.
|
||||
perl -0777 -pi -e 's/ *{\s*"name": "NodeRef(?<nested>(?:[^{}[\]]+|\{(?&nested)\}|\[(?&nested)\])*)\},\n//g' \
|
||||
target/idl/mango_v3_reimbursement.json target/types/mango_v3_reimbursement.ts;
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "mango_v3_reimbursement_lib",
|
||||
"license": "MIT",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
|
||||
"lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blockworks-foundation/mango-client": "^3.6.20",
|
||||
"@project-serum/anchor": "^0.25.0",
|
||||
"@solana/spl-token": "^0.3.5",
|
||||
"@solana/web3.js": "^1.63.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"@project-serum/anchor/@solana/web3.js": "1.63.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bn.js": "^5.1.0",
|
||||
"@types/chai": "^4.3.0",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"chai": "^4.3.4",
|
||||
"mocha": "^9.0.3",
|
||||
"prettier": "^2.6.2",
|
||||
"ts-mocha": "^10.0.0",
|
||||
"typescript": "^4.3.5"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
[package]
|
||||
name = "mango-v3-reimbursement"
|
||||
version = "0.1.0"
|
||||
description = "Created with Anchor"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
name = "mango_v3_reimbursement"
|
||||
|
||||
[features]
|
||||
no-entrypoint = []
|
||||
no-idl = []
|
||||
no-log-ix-name = []
|
||||
cpi = ["no-entrypoint"]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
anchor-lang = { path = "../../../mango-v4/anchor/lang" }
|
||||
anchor-spl = { path = "../../../mango-v4/anchor/spl" }
|
||||
solana-program = "~1.10.29"
|
||||
static_assertions = "1.1"
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -0,0 +1,42 @@
|
|||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::token::Token;
|
||||
|
||||
use crate::state::Group;
|
||||
use crate::Error;
|
||||
|
||||
#[derive(Accounts)]
|
||||
#[instruction(group_num: u32)]
|
||||
pub struct CreateGroup<'info> {
|
||||
#[account(
|
||||
init,
|
||||
seeds = [b"Group".as_ref(), &group_num.to_le_bytes()],
|
||||
bump,
|
||||
payer = payer,
|
||||
space = 8 + std::mem::size_of::<Group>(),
|
||||
)]
|
||||
pub group: AccountLoader<'info, Group>,
|
||||
|
||||
#[account(mut)]
|
||||
pub payer: Signer<'info>,
|
||||
|
||||
pub authority: Signer<'info>,
|
||||
|
||||
pub token_program: Program<'info, Token>,
|
||||
pub system_program: Program<'info, System>,
|
||||
pub rent: Sysvar<'info, Rent>,
|
||||
}
|
||||
|
||||
pub fn handle_create_group(
|
||||
ctx: Context<CreateGroup>,
|
||||
group_num: u32,
|
||||
table: Pubkey,
|
||||
claim_transfer_destination: Pubkey,
|
||||
) -> Result<()> {
|
||||
let mut group = ctx.accounts.group.load_init()?;
|
||||
group.group_num = group_num;
|
||||
group.table = table;
|
||||
group.claim_transfer_destination = claim_transfer_destination;
|
||||
group.authority = ctx.accounts.authority.key();
|
||||
group.bump = *ctx.bumps.get("group").ok_or(Error::SomeError)?;
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::token::Token;
|
||||
|
||||
use crate::state::{Group, ReimbursementAccount};
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct CreateReimbursementAccount<'info> {
|
||||
pub group: AccountLoader<'info, Group>,
|
||||
|
||||
#[account(
|
||||
init,
|
||||
seeds = [b"ReimbursementAccount".as_ref(), group.key().as_ref(), mango_account_owner.key().as_ref()],
|
||||
bump,
|
||||
payer = payer,
|
||||
space = 8 + std::mem::size_of::<ReimbursementAccount>(),
|
||||
)]
|
||||
pub reimbursement_account: AccountLoader<'info, ReimbursementAccount>,
|
||||
|
||||
pub mango_account_owner: Signer<'info>,
|
||||
|
||||
#[account(mut)]
|
||||
pub payer: Signer<'info>,
|
||||
|
||||
pub token_program: Program<'info, Token>,
|
||||
pub system_program: Program<'info, System>,
|
||||
pub rent: Sysvar<'info, Rent>,
|
||||
}
|
||||
|
||||
pub fn handle_create_reimbursement_account(
|
||||
_ctx: Context<CreateReimbursementAccount>,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
use crate::Error;
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::token::{Mint, Token, TokenAccount};
|
||||
|
||||
use crate::state::Group;
|
||||
|
||||
#[derive(Accounts)]
|
||||
#[instruction(token_index: usize, mint_decimals: u8)]
|
||||
pub struct CreateVault<'info> {
|
||||
#[account (
|
||||
mut,
|
||||
has_one = authority,
|
||||
constraint = !group.load()?.has_reimbursement_started()
|
||||
)]
|
||||
pub group: AccountLoader<'info, Group>,
|
||||
|
||||
pub authority: Signer<'info>,
|
||||
|
||||
#[account(
|
||||
init,
|
||||
seeds = [b"Vault".as_ref(), group.key().as_ref(), &token_index.to_le_bytes()],
|
||||
bump,
|
||||
token::authority = group,
|
||||
token::mint = mint,
|
||||
payer = payer
|
||||
)]
|
||||
pub vault: Account<'info, TokenAccount>,
|
||||
|
||||
#[account(
|
||||
init,
|
||||
seeds = [b"Mint".as_ref(), group.key().as_ref(), &token_index.to_le_bytes()],
|
||||
bump,
|
||||
mint::authority = group,
|
||||
mint::decimals = mint_decimals,
|
||||
payer = payer
|
||||
)]
|
||||
pub claim_mint: Account<'info, Mint>,
|
||||
|
||||
pub mint: Account<'info, Mint>,
|
||||
|
||||
#[account(mut)]
|
||||
pub payer: Signer<'info>,
|
||||
|
||||
pub token_program: Program<'info, Token>,
|
||||
pub system_program: Program<'info, System>,
|
||||
pub rent: Sysvar<'info, Rent>,
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
pub fn handle_create_vault(
|
||||
ctx: Context<CreateVault>,
|
||||
token_index: usize,
|
||||
mint_decimals: u8,
|
||||
) -> Result<()> {
|
||||
require!(token_index < 16usize, Error::SomeError);
|
||||
|
||||
let mint = &ctx.accounts.mint;
|
||||
require_eq!(mint_decimals, mint.decimals);
|
||||
|
||||
let mut group = ctx.accounts.group.load_mut()?;
|
||||
require_eq!(group.vaults[token_index], Pubkey::default());
|
||||
require_eq!(group.claim_mints[token_index], Pubkey::default());
|
||||
require_eq!(group.mints[token_index], Pubkey::default());
|
||||
group.vaults[token_index] = ctx.accounts.vault.key();
|
||||
group.claim_mints[token_index] = ctx.accounts.claim_mint.key();
|
||||
group.mints[token_index] = ctx.accounts.mint.key();
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::token::Token;
|
||||
|
||||
use crate::state::Group;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct EditGroup<'info> {
|
||||
#[account(
|
||||
mut,
|
||||
has_one = authority,
|
||||
constraint = !group.load()?.has_reimbursement_started()
|
||||
)]
|
||||
pub group: AccountLoader<'info, Group>,
|
||||
|
||||
pub authority: Signer<'info>,
|
||||
|
||||
pub token_program: Program<'info, Token>,
|
||||
pub system_program: Program<'info, System>,
|
||||
pub rent: Sysvar<'info, Rent>,
|
||||
}
|
||||
|
||||
// TODO: remove once we go live, create_group also supports taking table,
|
||||
// this is just a backup for testing without requiring a new group everytime
|
||||
pub fn handle_edit_group(ctx: Context<EditGroup>, table: Pubkey) -> Result<()> {
|
||||
let mut group = ctx.accounts.group.load_mut()?;
|
||||
group.table = table;
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
pub use create_group::*;
|
||||
pub use create_reimbursement_account::*;
|
||||
pub use create_vault::*;
|
||||
pub use edit_group::*;
|
||||
pub use reimburse::*;
|
||||
pub use start_reimbursement::*;
|
||||
|
||||
mod create_group;
|
||||
mod create_reimbursement_account;
|
||||
mod create_vault;
|
||||
mod edit_group;
|
||||
mod reimburse;
|
||||
mod start_reimbursement;
|
|
@ -0,0 +1,129 @@
|
|||
use anchor_lang::{__private::bytemuck, prelude::*};
|
||||
use anchor_spl::token::{self, Mint, Token, TokenAccount};
|
||||
|
||||
use crate::state::{Group, ReimbursementAccount, Table};
|
||||
|
||||
#[derive(Accounts)]
|
||||
#[instruction(token_index: usize)]
|
||||
pub struct Reimburse<'info> {
|
||||
#[account (
|
||||
constraint = group.load()?.has_reimbursement_started()
|
||||
)]
|
||||
pub group: AccountLoader<'info, Group>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
constraint = group.load()?.vaults[token_index] == vault.key()
|
||||
)]
|
||||
pub vault: Account<'info, TokenAccount>,
|
||||
|
||||
#[account(
|
||||
constraint = group.load()?.mints[token_index] == mint.key()
|
||||
)]
|
||||
pub mint: Box<Account<'info, Mint>>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
constraint = token_account.owner == mango_account_owner.key()
|
||||
)]
|
||||
pub token_account: Box<Account<'info, TokenAccount>>,
|
||||
#[account(
|
||||
mut,
|
||||
// TODO: enable after testing is done
|
||||
// constraint = !reimbursement_account.load()?.reimbursed(token_index),
|
||||
// constraint = !reimbursement_account.load()?.claim_transferred(token_index),
|
||||
)]
|
||||
pub reimbursement_account: AccountLoader<'info, ReimbursementAccount>,
|
||||
pub mango_account_owner: Signer<'info>,
|
||||
|
||||
#[account(mut)]
|
||||
pub claim_mint_token_account: Box<Account<'info, TokenAccount>>,
|
||||
#[account(
|
||||
mut,
|
||||
constraint = group.load()?.claim_mints[token_index] == claim_mint.key()
|
||||
)]
|
||||
pub claim_mint: Box<Account<'info, Mint>>,
|
||||
|
||||
/// CHECK:
|
||||
pub table: UncheckedAccount<'info>,
|
||||
|
||||
pub token_program: Program<'info, Token>,
|
||||
pub system_program: Program<'info, System>,
|
||||
pub rent: Sysvar<'info, Rent>,
|
||||
}
|
||||
|
||||
pub fn handle_reimburse<'key, 'accounts, 'remaining, 'info>(
|
||||
ctx: Context<'key, 'accounts, 'remaining, 'info, Reimburse<'info>>,
|
||||
index_into_table: usize,
|
||||
token_index: usize,
|
||||
transfer_claim: bool,
|
||||
) -> Result<()> {
|
||||
let group = ctx.accounts.group.load()?;
|
||||
|
||||
// Verify entry in reimbursement table
|
||||
let table_ai = &ctx.accounts.table;
|
||||
let data = table_ai.try_borrow_data()?;
|
||||
let table: &Table = bytemuck::from_bytes::<Table>(&data[40..]);
|
||||
require_eq!(
|
||||
table.rows[index_into_table].owner,
|
||||
ctx.accounts.mango_account_owner.key()
|
||||
);
|
||||
|
||||
// Verify reimbursement_account
|
||||
let mut reimbursement_account = ctx.accounts.reimbursement_account.load_mut()?;
|
||||
let (pda_address, _) = Pubkey::find_program_address(
|
||||
&[
|
||||
b"ReimbursementAccount".as_ref(),
|
||||
ctx.accounts.group.key().as_ref(),
|
||||
ctx.accounts.mango_account_owner.key().as_ref(),
|
||||
],
|
||||
&crate::id(),
|
||||
);
|
||||
require_eq!(pda_address, ctx.accounts.reimbursement_account.key());
|
||||
|
||||
token::transfer(
|
||||
{
|
||||
let accounts = token::Transfer {
|
||||
from: ctx.accounts.vault.to_account_info(),
|
||||
to: ctx.accounts.token_account.to_account_info(),
|
||||
authority: ctx.accounts.group.to_account_info(),
|
||||
};
|
||||
CpiContext::new(ctx.accounts.token_program.to_account_info(), accounts).with_signer(&[
|
||||
&[
|
||||
b"Group".as_ref(),
|
||||
&group.group_num.to_le_bytes(),
|
||||
&[group.bump],
|
||||
],
|
||||
])
|
||||
},
|
||||
table.rows[index_into_table].balances[token_index],
|
||||
)?;
|
||||
reimbursement_account.mark_reimbursed(token_index);
|
||||
|
||||
if transfer_claim {
|
||||
require_eq!(
|
||||
ctx.accounts.claim_mint_token_account.owner,
|
||||
group.claim_transfer_destination
|
||||
);
|
||||
token::mint_to(
|
||||
{
|
||||
let accounts = token::MintTo {
|
||||
mint: ctx.accounts.claim_mint.to_account_info(),
|
||||
to: ctx.accounts.claim_mint_token_account.to_account_info(),
|
||||
authority: ctx.accounts.group.to_account_info(),
|
||||
};
|
||||
CpiContext::new(ctx.accounts.token_program.to_account_info(), accounts).with_signer(
|
||||
&[&[
|
||||
b"Group".as_ref(),
|
||||
&group.group_num.to_le_bytes(),
|
||||
&[group.bump],
|
||||
]],
|
||||
)
|
||||
},
|
||||
table.rows[index_into_table].balances[token_index],
|
||||
)?;
|
||||
reimbursement_account.mark_claim_transferred(token_index);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::token::Token;
|
||||
|
||||
use crate::state::Group;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct StartReimbursement<'info> {
|
||||
#[account(
|
||||
mut,
|
||||
has_one = authority,
|
||||
constraint = !group.load()?.has_reimbursement_started()
|
||||
)]
|
||||
pub group: AccountLoader<'info, Group>,
|
||||
|
||||
pub authority: Signer<'info>,
|
||||
|
||||
pub token_program: Program<'info, Token>,
|
||||
pub system_program: Program<'info, System>,
|
||||
pub rent: Sysvar<'info, Rent>,
|
||||
}
|
||||
|
||||
// TODO: do we also want to have a end/freeze reimbursement?
|
||||
pub fn handle_start_reimbursement(ctx: Context<StartReimbursement>) -> Result<()> {
|
||||
let mut group = ctx.accounts.group.load_mut()?;
|
||||
group.reimbursement_started = 1;
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
pub mod instructions;
|
||||
pub mod state;
|
||||
|
||||
use instructions::*;
|
||||
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
declare_id!("m3roABq4Ta3sGyFRLdY4LH1KN16zBtg586gJ3UxoBzb");
|
||||
|
||||
#[program]
|
||||
pub mod mango_v3_reimbursement {
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn create_group(
|
||||
ctx: Context<CreateGroup>,
|
||||
group_num: u32,
|
||||
table: Pubkey,
|
||||
claim_transfer_destination: Pubkey,
|
||||
) -> Result<()> {
|
||||
handle_create_group(ctx, group_num, table, claim_transfer_destination)
|
||||
}
|
||||
|
||||
pub fn edit_group(ctx: Context<EditGroup>, table: Pubkey) -> Result<()> {
|
||||
handle_edit_group(ctx, table)
|
||||
}
|
||||
|
||||
pub fn create_vault(
|
||||
ctx: Context<CreateVault>,
|
||||
token_index: usize,
|
||||
mint_decimals: u8,
|
||||
) -> Result<()> {
|
||||
handle_create_vault(ctx, token_index, mint_decimals)
|
||||
}
|
||||
|
||||
pub fn create_reimbursement_account(ctx: Context<CreateReimbursementAccount>) -> Result<()> {
|
||||
handle_create_reimbursement_account(ctx)
|
||||
}
|
||||
|
||||
pub fn start_reimbursement(ctx: Context<StartReimbursement>) -> Result<()> {
|
||||
handle_start_reimbursement(ctx)
|
||||
}
|
||||
|
||||
pub fn reimburse<'key, 'accounts, 'remaining, 'info>(
|
||||
ctx: Context<'key, 'accounts, 'remaining, 'info, Reimburse<'info>>,
|
||||
index_into_table: usize,
|
||||
token_index: usize,
|
||||
transfer_claim: bool,
|
||||
) -> Result<()> {
|
||||
handle_reimburse(ctx, index_into_table, token_index, transfer_claim)
|
||||
}
|
||||
}
|
||||
|
||||
#[error_code]
|
||||
pub enum Error {
|
||||
SomeError,
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
use std::mem::size_of;
|
||||
|
||||
pub use anchor_lang::prelude::*;
|
||||
use static_assertions::const_assert_eq;
|
||||
|
||||
#[account(zero_copy)]
|
||||
pub struct Group {
|
||||
pub group_num: u32,
|
||||
pub table: Pubkey,
|
||||
pub claim_transfer_destination: Pubkey,
|
||||
pub authority: Pubkey,
|
||||
pub vaults: [Pubkey; 16],
|
||||
pub claim_mints: [Pubkey; 16],
|
||||
pub mints: [Pubkey; 16],
|
||||
pub reimbursement_started: u8,
|
||||
pub bump: u8,
|
||||
pub padding: [u8; 2],
|
||||
}
|
||||
const_assert_eq!(
|
||||
size_of::<Group>(),
|
||||
4 + 32 + 32 + 32 + 32 * 16 + 32 * 16 + 32 * 16 + 4
|
||||
);
|
||||
const_assert_eq!(size_of::<Group>() % 8, 0);
|
||||
|
||||
impl Group {
|
||||
pub fn has_reimbursement_started(&self) -> bool {
|
||||
self.reimbursement_started == 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
pub use group::*;
|
||||
pub use reimbursement_account::*;
|
||||
pub use table::*;
|
||||
|
||||
mod group;
|
||||
mod reimbursement_account;
|
||||
mod table;
|
|
@ -0,0 +1,31 @@
|
|||
use std::mem::size_of;
|
||||
|
||||
use anchor_lang::prelude::*;
|
||||
use static_assertions::const_assert_eq;
|
||||
|
||||
#[account(zero_copy)]
|
||||
pub struct ReimbursementAccount {
|
||||
pub done: u16,
|
||||
pub claim_transferred: u16,
|
||||
pub padding: [u8; 4],
|
||||
}
|
||||
const_assert_eq!(size_of::<ReimbursementAccount>(), 8);
|
||||
const_assert_eq!(size_of::<ReimbursementAccount>() % 8, 0);
|
||||
|
||||
impl ReimbursementAccount {
|
||||
pub fn reimbursed(&self, token_index: usize) -> bool {
|
||||
self.done & (1 << token_index) == 1
|
||||
}
|
||||
|
||||
pub fn mark_reimbursed(&mut self, token_index: usize) {
|
||||
self.done |= 1 << token_index
|
||||
}
|
||||
|
||||
pub fn claim_transferred(&self, token_index: usize) -> bool {
|
||||
self.done & (1 << token_index) == 1
|
||||
}
|
||||
|
||||
pub fn mark_claim_transferred(&mut self, token_index: usize) {
|
||||
self.claim_transferred |= 1 << token_index
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
use std::mem::size_of;
|
||||
|
||||
use anchor_lang::{__private::bytemuck, prelude::*};
|
||||
use static_assertions::const_assert_eq;
|
||||
|
||||
#[account(zero_copy)]
|
||||
pub struct Table {
|
||||
pub rows: [Row; 1],
|
||||
}
|
||||
const_assert_eq!(size_of::<Table>(), (32 + 8 * 16) * 1);
|
||||
const_assert_eq!(size_of::<Table>() % 8, 0);
|
||||
|
||||
#[derive(Copy, Clone, AnchorSerialize, AnchorDeserialize)]
|
||||
#[repr(C)]
|
||||
pub struct Row {
|
||||
pub owner: Pubkey,
|
||||
pub balances: [u64; 16],
|
||||
}
|
||||
const_assert_eq!(size_of::<Row>(), 32 + 8 * 16);
|
||||
const_assert_eq!(size_of::<Row>() % 8, 0);
|
|
@ -0,0 +1,12 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -ex pipefail
|
||||
|
||||
WALLET_WITH_FUNDS=~/.config/solana/mango-devnet.json
|
||||
PROGRAM_ID=m3roABq4Ta3sGyFRLdY4LH1KN16zBtg586gJ3UxoBzb
|
||||
cargo run --manifest-path ../mango-v4/anchor/cli/Cargo.toml -- build
|
||||
./idl-fixup.sh
|
||||
cp -v ./target/types/mango_v3_reimbursement.ts ./ts/client/src/mango_v3_reimbursement.ts
|
||||
yarn tsc
|
||||
solana --url https://mango.devnet.rpcpool.com program deploy --program-id $PROGRAM_ID -k $WALLET_WITH_FUNDS target/deploy/mango_v3_reimbursement.so --skip-fee-check
|
||||
cargo run --manifest-path ../mango-v4/anchor/cli/Cargo.toml -- idl upgrade --provider.cluster https://mango.devnet.rpcpool.com --provider.wallet $WALLET_WITH_FUNDS --filepath target/idl/mango_v3_reimbursement.json $PROGRAM_ID
|
|
@ -0,0 +1,30 @@
|
|||
import { Program, ProgramAccount, Provider } from "@project-serum/anchor";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import { MangoV3Reimbursement, IDL } from "./mango_v3_reimbursement";
|
||||
|
||||
export const ID = new PublicKey("m3roABq4Ta3sGyFRLdY4LH1KN16zBtg586gJ3UxoBzb");
|
||||
|
||||
export class MangoV3ReimbursementClient {
|
||||
public program: Program<MangoV3Reimbursement>;
|
||||
constructor(provider: Provider) {
|
||||
this.program = new Program<MangoV3Reimbursement>(
|
||||
IDL as MangoV3Reimbursement,
|
||||
ID,
|
||||
provider
|
||||
);
|
||||
}
|
||||
|
||||
public async decodeTable(group) {
|
||||
const ai = await this.program.provider.connection.getAccountInfo(
|
||||
group.account.table
|
||||
);
|
||||
|
||||
if (!ai) {
|
||||
throw new Error(`Table ai cannot be undefined!`);
|
||||
}
|
||||
|
||||
return (this.program as any)._coder.accounts.accountLayouts
|
||||
.get("table")
|
||||
.decode(ai.data.subarray(40));
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from "./client";
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,261 @@
|
|||
import {
|
||||
Cluster,
|
||||
Config,
|
||||
MangoClient,
|
||||
} from "@blockworks-foundation/mango-client";
|
||||
import { AnchorProvider, Provider, Wallet } from "@project-serum/anchor";
|
||||
import {
|
||||
Connection,
|
||||
Keypair,
|
||||
PublicKey,
|
||||
SystemProgram,
|
||||
TransactionInstruction,
|
||||
} from "@solana/web3.js";
|
||||
import {
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
getAssociatedTokenAddress,
|
||||
getMint,
|
||||
TOKEN_PROGRAM_ID,
|
||||
} from "@solana/spl-token";
|
||||
import { MangoV3ReimbursementClient } from "./client";
|
||||
import BN from "bn.js";
|
||||
import fs from "fs";
|
||||
|
||||
/// Env
|
||||
const CLUSTER_URL =
|
||||
process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL;
|
||||
const PAYER_KEYPAIR =
|
||||
process.env.PAYER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR;
|
||||
const GROUP_NUM = Number(process.env.GROUP_NUM || 5);
|
||||
const CLUSTER: Cluster =
|
||||
(process.env.CLUSTER_OVERRIDE as Cluster) || "mainnet-beta";
|
||||
const MANGO_V3_CLUSTER: Cluster =
|
||||
(process.env.MANGO_V3_CLUSTER_OVERRIDE as Cluster) || "mainnet";
|
||||
const MANGO_V3_GROUP_NAME: Cluster =
|
||||
(process.env.MANGO_V3_GROUP_NAME_OVERRIDE as Cluster) || "mainnet.1";
|
||||
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const connection = new Connection(CLUSTER_URL!, options);
|
||||
|
||||
// Mango v3 client setup
|
||||
const config = Config.ids();
|
||||
const groupIds = config.getGroup(MANGO_V3_CLUSTER, MANGO_V3_GROUP_NAME);
|
||||
if (!groupIds) {
|
||||
throw new Error(`Group ${MANGO_V3_GROUP_NAME} not found`);
|
||||
}
|
||||
const mangoProgramId = groupIds.mangoProgramId;
|
||||
const mangoGroupKey = groupIds.publicKey;
|
||||
const mangoV3Client = new MangoClient(connection, mangoProgramId);
|
||||
|
||||
Error.stackTraceLimit = 1000;
|
||||
|
||||
export async function createAssociatedTokenAccountIdempotentInstruction(
|
||||
payer: PublicKey,
|
||||
owner: PublicKey,
|
||||
mint: PublicKey,
|
||||
ata: PublicKey
|
||||
): Promise<TransactionInstruction> {
|
||||
return new TransactionInstruction({
|
||||
keys: [
|
||||
{ pubkey: payer, isSigner: true, isWritable: true },
|
||||
{ pubkey: ata, isSigner: false, isWritable: true },
|
||||
{ pubkey: owner, isSigner: false, isWritable: false },
|
||||
{ pubkey: mint, isSigner: false, isWritable: false },
|
||||
{
|
||||
pubkey: SystemProgram.programId,
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
},
|
||||
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
|
||||
],
|
||||
programId: ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
data: Buffer.from([0x1]),
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Client setup
|
||||
let sig;
|
||||
const admin = Keypair.fromSecretKey(
|
||||
Buffer.from(JSON.parse(fs.readFileSync(PAYER_KEYPAIR!, "utf-8")))
|
||||
);
|
||||
const adminWallet = new Wallet(admin);
|
||||
const provider = new AnchorProvider(connection, adminWallet, options);
|
||||
const mangoV3ReimbursementClient = new MangoV3ReimbursementClient(provider);
|
||||
|
||||
// Create group if not already
|
||||
if (
|
||||
!(await mangoV3ReimbursementClient.program.account.group.all()).find(
|
||||
(group) => group.account.groupNum === GROUP_NUM
|
||||
)
|
||||
) {
|
||||
const sig = await mangoV3ReimbursementClient.program.methods
|
||||
.createGroup(
|
||||
GROUP_NUM,
|
||||
new PublicKey("tab26VnfeLLkhVUa87mt5EWHA5PAbrVL1NCuSvZUSvc"),
|
||||
new PublicKey("mdcXrm2NkzXYvHNcKXzCLXT58R4UN8Rzd1uzD4h8338")
|
||||
)
|
||||
.accounts({
|
||||
payer: (mangoV3ReimbursementClient.program.provider as AnchorProvider)
|
||||
.wallet.publicKey,
|
||||
authority: (
|
||||
mangoV3ReimbursementClient.program.provider as AnchorProvider
|
||||
).wallet.publicKey,
|
||||
})
|
||||
.rpc();
|
||||
console.log(
|
||||
`created group, sig https://explorer.solana.com/tx/${
|
||||
sig + (CLUSTER === "devnet" ? "?cluster=devnet" : "")
|
||||
}`
|
||||
);
|
||||
}
|
||||
let group = (
|
||||
await mangoV3ReimbursementClient.program.account.group.all()
|
||||
).find((group) => group.account.groupNum === GROUP_NUM);
|
||||
|
||||
// // Edit group, set new table
|
||||
// await mangoV3ReimbursementClient.program.methods
|
||||
// .editGroup(new PublicKey("tabWqAkVwFcPGJTmEaik9KSbcDqRJRH4d39oyBrRzCn"))
|
||||
// .accounts({
|
||||
// group: group?.publicKey,
|
||||
// authority: (mangoV3ReimbursementClient.program.provider as AnchorProvider)
|
||||
// .wallet.publicKey,
|
||||
// })
|
||||
// .rpc();
|
||||
|
||||
// Reload table
|
||||
group = (await mangoV3ReimbursementClient.program.account.group.all()).find(
|
||||
(group) => group.account.groupNum === GROUP_NUM
|
||||
);
|
||||
|
||||
// Create vaults
|
||||
for (const [index, tokenInfo] of (
|
||||
await mangoV3Client.getMangoGroup(mangoGroupKey)
|
||||
).tokens.entries()!) {
|
||||
// If token for index is set in v3
|
||||
if (tokenInfo.mint.equals(PublicKey.default)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If token is still active in v3
|
||||
if (tokenInfo.oracleInactive === true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If vault has not already been created
|
||||
if (!group?.account.vaults[index].equals(PublicKey.default)) {
|
||||
continue;
|
||||
}
|
||||
const mint = await getMint(connection, tokenInfo.mint);
|
||||
const sig = await mangoV3ReimbursementClient.program.methods
|
||||
.createVault(new BN(index), mint.decimals)
|
||||
.accounts({
|
||||
group: (group as any).publicKey,
|
||||
mint: tokenInfo.mint,
|
||||
payer: (mangoV3ReimbursementClient.program.provider as AnchorProvider)
|
||||
.wallet.publicKey,
|
||||
})
|
||||
.rpc();
|
||||
console.log(
|
||||
`setup vault, sig https://explorer.solana.com/tx/${
|
||||
sig + (CLUSTER === "devnet" ? "?cluster=devnet" : "")
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
// Set start reimbursement flag to true
|
||||
if (group?.account.reimbursementStarted === 0) {
|
||||
sig = await mangoV3ReimbursementClient.program.methods
|
||||
.startReimbursement()
|
||||
.accounts({
|
||||
group: (group as any).publicKey,
|
||||
authority: (
|
||||
mangoV3ReimbursementClient.program.provider as AnchorProvider
|
||||
).wallet.publicKey,
|
||||
})
|
||||
.rpc();
|
||||
console.log(
|
||||
`start reimbursement, sig https://explorer.solana.com/tx/${
|
||||
sig + (CLUSTER === "devnet" ? "?cluster=devnet" : "")
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
// Create reimbursement account if not already created
|
||||
const reimbursementAccount = (
|
||||
await PublicKey.findProgramAddress(
|
||||
[
|
||||
Buffer.from("ReimbursementAccount"),
|
||||
group?.publicKey.toBuffer()!,
|
||||
admin.publicKey.toBuffer(),
|
||||
],
|
||||
mangoV3ReimbursementClient.program.programId
|
||||
)
|
||||
)[0];
|
||||
if (!(await connection.getAccountInfo(reimbursementAccount))) {
|
||||
sig = await mangoV3ReimbursementClient.program.methods
|
||||
.createReimbursementAccount()
|
||||
.accounts({
|
||||
group: (group as any).publicKey,
|
||||
mangoAccountOwner: admin.publicKey,
|
||||
payer: (mangoV3ReimbursementClient.program.provider as AnchorProvider)
|
||||
.wallet.publicKey,
|
||||
})
|
||||
.rpc({ skipPreflight: true });
|
||||
console.log(
|
||||
`created reimbursement account for ${
|
||||
admin.publicKey
|
||||
}, sig https://explorer.solana.com/tx/${
|
||||
sig + (CLUSTER === "devnet" ? "?cluster=devnet" : "")
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
// Table decoding example
|
||||
const table = await mangoV3ReimbursementClient.decodeTable(group);
|
||||
const balancesForUser = table.rows.find((row) =>
|
||||
row.owner.equals(admin.publicKey)
|
||||
).balances;
|
||||
|
||||
// Example - reimburse for first token
|
||||
sig = await mangoV3ReimbursementClient.program.methods
|
||||
.reimburse(new BN(0), new BN(0), true)
|
||||
.accounts({
|
||||
group: (group as any).publicKey,
|
||||
vault: group?.account.vaults[0],
|
||||
tokenAccount: await getAssociatedTokenAddress(
|
||||
group?.account.mints[0]!,
|
||||
admin.publicKey
|
||||
),
|
||||
mint: group?.account.mints[0],
|
||||
reimbursementAccount,
|
||||
claimMint: group?.account.claimMints[0],
|
||||
claimMintTokenAccount: await getAssociatedTokenAddress(
|
||||
group?.account.claimMints[0]!,
|
||||
group?.account.claimTransferDestination!
|
||||
),
|
||||
mangoAccountOwner: admin.publicKey,
|
||||
table: group?.account.table,
|
||||
})
|
||||
.preInstructions([
|
||||
await createAssociatedTokenAccountIdempotentInstruction(
|
||||
(mangoV3ReimbursementClient.program.provider as AnchorProvider).wallet
|
||||
.publicKey,
|
||||
group?.account.claimTransferDestination!,
|
||||
group?.account.claimMints[0]!,
|
||||
await getAssociatedTokenAddress(
|
||||
group?.account.claimMints[0]!,
|
||||
group?.account.claimTransferDestination!,
|
||||
true
|
||||
)
|
||||
),
|
||||
])
|
||||
.rpc();
|
||||
console.log(
|
||||
`reimbursing ${admin.publicKey}, sig https://explorer.solana.com/tx/${
|
||||
sig + (CLUSTER === "devnet" ? "?cluster=devnet" : "")
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
"lib": [
|
||||
"es2019"
|
||||
],
|
||||
"outDir": "./dist",
|
||||
"resolveJsonModule": true,
|
||||
"noImplicitAny": false,
|
||||
"sourceMap": true,
|
||||
"skipLibCheck": true,
|
||||
"target": "es2019",
|
||||
"strictNullChecks": true
|
||||
},
|
||||
"include": [
|
||||
"ts/client/src",
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
]
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -e pipefail
|
||||
cargo run --manifest-path ../mango-v4/anchor/cli/Cargo.toml -- build
|
||||
./idl-fixup.sh
|
||||
cp -v ./target/types/mango_v3_reimbursement.ts ./ts/client/src/mango_v3_reimbursement.ts
|
||||
yarn tsc
|
Loading…
Reference in New Issue