First commit

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
microwavedcola1 2022-10-15 09:47:53 +02:00
commit 8df4979297
30 changed files with 4834 additions and 0 deletions

12
.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
node_modules
dist
ts/**/*.js
.anchor
target
test-ledger
.DS_Store
**/*.rs.bk

8
.prettierignore Normal file
View File

@ -0,0 +1,8 @@
.anchor
.DS_Store
target
node_modules
dist
build
test-ledger

16
Anchor.toml Normal file
View File

@ -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"

1460
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

13
Cargo.toml Normal file
View File

@ -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

38
README.md Normal file
View File

@ -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
```

21
idl-fixup.sh Executable file
View File

@ -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;

29
package.json Normal file
View File

@ -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"
}
}

View File

@ -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"

View File

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

View File

@ -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(())
}

View File

@ -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(())
}

View File

@ -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(())
}

View File

@ -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(())
}

View File

@ -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;

View File

@ -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(())
}

View File

@ -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(())
}

View File

@ -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,
}

View File

@ -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
}
}

View File

@ -0,0 +1,7 @@
pub use group::*;
pub use reimbursement_account::*;
pub use table::*;
mod group;
mod reimbursement_account;
mod table;

View File

@ -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
}
}

View File

@ -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);

12
release-to-devnet.sh Executable file
View File

@ -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

30
ts/client/src/client.ts Normal file
View File

@ -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));
}
}

1
ts/client/src/index.ts Normal file
View File

@ -0,0 +1 @@
export * from "./client";

File diff suppressed because it is too large Load Diff

261
ts/client/src/setup.ts Normal file
View File

@ -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();

22
tsconfig.json Normal file
View File

@ -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",
]
}

7
update-local-idl.sh Executable file
View File

@ -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

1363
yarn.lock Normal file

File diff suppressed because it is too large Load Diff