From 5650b08f4b4539229a530ba870ab67a75a82a984 Mon Sep 17 00:00:00 2001 From: Emanuele Cesena Date: Thu, 9 May 2024 16:59:52 -0700 Subject: [PATCH 1/9] treasury: system account --- anchor/programs/glam/src/instructions/drift.rs | 14 +++++++------- anchor/programs/glam/src/instructions/investor.rs | 3 +-- anchor/programs/glam/src/instructions/manager.rs | 3 +-- anchor/programs/glam/src/instructions/marinade.rs | 9 +++------ 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/anchor/programs/glam/src/instructions/drift.rs b/anchor/programs/glam/src/instructions/drift.rs index 84d19dbe..6e4c2884 100644 --- a/anchor/programs/glam/src/instructions/drift.rs +++ b/anchor/programs/glam/src/instructions/drift.rs @@ -19,8 +19,7 @@ pub struct DriftInitialize<'info> { #[account(has_one = manager @ ManagerError::NotAuthorizedError)] pub fund: Account<'info, Fund>, - /// CHECK: treasury account is the same as fund treasury - pub treasury: AccountInfo<'info>, + pub treasury: SystemAccount<'info>, #[account(mut)] /// CHECK: checks are done inside cpi call @@ -115,8 +114,7 @@ pub struct DriftUpdate<'info> { #[account(has_one = manager @ ManagerError::NotAuthorizedError)] pub fund: Account<'info, Fund>, - /// CHECK: treasury account is the same as fund treasury - pub treasury: AccountInfo<'info>, + pub treasury: SystemAccount<'info>, #[account(mut)] /// CHECK: checks are done inside cpi call @@ -167,7 +165,8 @@ pub fn drift_update_delegated_trader_handler( pub struct DriftDeposit<'info> { #[account(has_one = manager @ ManagerError::NotAuthorizedError)] pub fund: Account<'info, Fund>, - pub treasury: Account<'info, Treasury>, + + pub treasury: SystemAccount<'info>, #[account(mut)] /// CHECK: checks are done inside cpi call @@ -236,7 +235,8 @@ pub fn drift_deposit_handler<'c: 'info, 'info>( pub struct DriftWithdraw<'info> { #[account(has_one = manager @ ManagerError::NotAuthorizedError)] pub fund: Account<'info, Fund>, - pub treasury: Account<'info, Treasury>, + + pub treasury: SystemAccount<'info>, #[account(mut)] /// CHECK: checks are done inside cpi call @@ -308,7 +308,7 @@ pub fn drift_withdraw_handler<'c: 'info, 'info>( pub struct DriftClose<'info> { #[account(has_one = manager @ ManagerError::NotAuthorizedError)] pub fund: Account<'info, Fund>, - pub treasury: Account<'info, Treasury>, + pub treasury: SystemAccount<'info>, #[account(mut)] /// CHECK: checks are done inside cpi call diff --git a/anchor/programs/glam/src/instructions/investor.rs b/anchor/programs/glam/src/instructions/investor.rs index 42ddebc8..8909531a 100644 --- a/anchor/programs/glam/src/instructions/investor.rs +++ b/anchor/programs/glam/src/instructions/investor.rs @@ -315,8 +315,7 @@ pub struct Redeem<'info> { #[account(mut)] pub signer: Signer<'info>, - /// CHECK: skip - pub treasury: AccountInfo<'info>, + pub treasury: SystemAccount<'info>, // programs pub token_program: Program<'info, Token>, diff --git a/anchor/programs/glam/src/instructions/manager.rs b/anchor/programs/glam/src/instructions/manager.rs index ac62a175..b3985dbb 100644 --- a/anchor/programs/glam/src/instructions/manager.rs +++ b/anchor/programs/glam/src/instructions/manager.rs @@ -11,9 +11,8 @@ pub struct InitializeFund<'info> { #[account(init, seeds = [b"fund".as_ref(), manager.key().as_ref(), name.as_ref()], bump, payer = manager, space = 8 + Fund::INIT_SIZE + ShareClassMetadata::INIT_SIZE)] pub fund: Box>, - /// CHECK: we'll create the account #[account(mut, seeds = [b"treasury".as_ref(), fund.key().as_ref()], bump)] - pub treasury: AccountInfo<'info>, + pub treasury: SystemAccount<'info>, #[account(mut)] pub manager: Signer<'info>, diff --git a/anchor/programs/glam/src/instructions/marinade.rs b/anchor/programs/glam/src/instructions/marinade.rs index ac15caef..591ac025 100644 --- a/anchor/programs/glam/src/instructions/marinade.rs +++ b/anchor/programs/glam/src/instructions/marinade.rs @@ -224,9 +224,8 @@ pub struct MarinadeDelayedUnstake<'info> { #[account(has_one = manager, has_one = treasury)] pub fund: Box>, - /// CHECK: skip #[account(mut, seeds = [b"treasury".as_ref(), fund.key().as_ref()], bump)] - pub treasury: AccountInfo<'info>, + pub treasury: SystemAccount<'info>, /// CHECK: skip // #[account(init_if_needed, seeds = [b"ticket"], bump, payer = signer, space = 88, owner = marinade_program.key())] @@ -264,9 +263,8 @@ pub struct MarinadeClaim<'info> { #[account(has_one = manager, has_one = treasury)] pub fund: Box>, - /// CHECK: skip #[account(mut, seeds = [b"treasury".as_ref(), fund.key().as_ref()], bump)] - pub treasury: AccountInfo<'info>, + pub treasury: SystemAccount<'info>, /// CHECK: skip // #[account(init_if_needed, seeds = [b"ticket"], bump, payer = signer, space = 88, owner = marinade_program.key())] @@ -295,9 +293,8 @@ pub struct MarinadeLiquidUnstake<'info> { #[account(has_one = manager, has_one = treasury)] pub fund: Box>, - /// CHECK: skip #[account(mut, seeds = [b"treasury".as_ref(), fund.key().as_ref()], bump)] - pub treasury: AccountInfo<'info>, + pub treasury: SystemAccount<'info>, /// CHECK: skip #[account(mut)] From 4f5442b03dac396cdd158df8aabd1b17d098287d Mon Sep 17 00:00:00 2001 From: Emanuele Cesena Date: Thu, 9 May 2024 17:26:17 -0700 Subject: [PATCH 2/9] v0.2: Fund -> FundAccount --- anchor/Anchor.toml | 6 +-- .../programs/glam/src/instructions/drift.rs | 26 ++++++----- .../programs/glam/src/instructions/manager.rs | 43 ++++++++++--------- .../glam/src/instructions/marinade.rs | 10 ++--- anchor/tests/glam_crud.spec.ts | 18 ++++---- anchor/tests/glam_drift.spec.ts | 14 +++--- anchor/tests/glam_staking.spec.ts | 8 ++-- 7 files changed, 69 insertions(+), 56 deletions(-) diff --git a/anchor/Anchor.toml b/anchor/Anchor.toml index b4773bbc..4ace2780 100644 --- a/anchor/Anchor.toml +++ b/anchor/Anchor.toml @@ -28,10 +28,10 @@ cluster = "localnet" wallet = "~/.config/solana/id.json" [scripts] +#test = "../node_modules/.bin/nx run anchor:jest --verbose --testPathPattern tests/ --testNamePattern glam_crud" +#test = "../node_modules/.bin/nx run anchor:jest --verbose --testPathPattern tests/ --testNamePattern glam_investor" +#test = "../node_modules/.bin/nx run anchor:jest --verbose --testPathPattern tests/ --testNamePattern glam_drift" test = "../node_modules/.bin/nx run anchor:jest --verbose --testPathPattern tests/ --testNamePattern glam_staking" -# test = "../node_modules/.bin/nx run anchor:jest --verbose --testPathPattern tests/ --testNamePattern glam_investor" -# test = "../node_modules/.bin/nx run anchor:jest --verbose --testPathPattern tests/ --testNamePattern glam_crud" -# test = "../node_modules/.bin/nx run anchor:jest --verbose --testPathPattern tests/ --testNamePattern glam_drift" #test = "../node_modules/.bin/nx run anchor:jest --verbose --testPathPattern tests/ --testNamePattern devnet" [test] diff --git a/anchor/programs/glam/src/instructions/drift.rs b/anchor/programs/glam/src/instructions/drift.rs index 6e4c2884..487bcee8 100644 --- a/anchor/programs/glam/src/instructions/drift.rs +++ b/anchor/programs/glam/src/instructions/drift.rs @@ -3,7 +3,7 @@ use anchor_spl::token::Token; use anchor_spl::token_interface::TokenAccount; use crate::error::ManagerError; -use crate::state::fund::*; +use crate::state::*; use drift::cpi::accounts::{ DeleteUser, Deposit, InitializeUser, InitializeUserStats, UpdateUserDelegate, Withdraw, @@ -17,8 +17,9 @@ use drift::State; #[derive(Accounts)] pub struct DriftInitialize<'info> { #[account(has_one = manager @ ManagerError::NotAuthorizedError)] - pub fund: Account<'info, Fund>, + pub fund: Account<'info, FundAccount>, + #[account(seeds = [b"treasury".as_ref(), fund.key().as_ref()], bump)] pub treasury: SystemAccount<'info>, #[account(mut)] @@ -51,7 +52,7 @@ pub fn drift_initialize_handler( let seeds = &[ "treasury".as_bytes(), fund_key.as_ref(), - &[ctx.accounts.fund.bump_treasury], + &[ctx.bumps.treasury], ]; let signer_seeds = &[&seeds[..]]; @@ -112,8 +113,9 @@ pub fn drift_initialize_handler( #[derive(Accounts)] pub struct DriftUpdate<'info> { #[account(has_one = manager @ ManagerError::NotAuthorizedError)] - pub fund: Account<'info, Fund>, + pub fund: Account<'info, FundAccount>, + #[account(seeds = [b"treasury".as_ref(), fund.key().as_ref()], bump)] pub treasury: SystemAccount<'info>, #[account(mut)] @@ -139,7 +141,7 @@ pub fn drift_update_delegated_trader_handler( let seeds = &[ "treasury".as_bytes(), fund_key.as_ref(), - &[ctx.accounts.fund.bump_treasury], + &[ctx.bumps.treasury], ]; let signer_seeds = &[&seeds[..]]; @@ -164,8 +166,9 @@ pub fn drift_update_delegated_trader_handler( #[derive(Accounts)] pub struct DriftDeposit<'info> { #[account(has_one = manager @ ManagerError::NotAuthorizedError)] - pub fund: Account<'info, Fund>, + pub fund: Account<'info, FundAccount>, + #[account(seeds = [b"treasury".as_ref(), fund.key().as_ref()], bump)] pub treasury: SystemAccount<'info>, #[account(mut)] @@ -202,7 +205,7 @@ pub fn drift_deposit_handler<'c: 'info, 'info>( let seeds = &[ "treasury".as_bytes(), fund_key.as_ref(), - &[ctx.accounts.fund.bump_treasury], + &[ctx.bumps.treasury], ]; let signer_seeds = &[&seeds[..]]; @@ -236,6 +239,7 @@ pub struct DriftWithdraw<'info> { #[account(has_one = manager @ ManagerError::NotAuthorizedError)] pub fund: Account<'info, Fund>, + #[account(seeds = [b"treasury".as_ref(), fund.key().as_ref()], bump)] pub treasury: SystemAccount<'info>, #[account(mut)] @@ -274,7 +278,7 @@ pub fn drift_withdraw_handler<'c: 'info, 'info>( let seeds = &[ "treasury".as_bytes(), fund_key.as_ref(), - &[ctx.accounts.fund.bump_treasury], + &[ctx.bumps.treasury], ]; let signer_seeds = &[&seeds[..]]; @@ -307,7 +311,9 @@ pub fn drift_withdraw_handler<'c: 'info, 'info>( #[derive(Accounts)] pub struct DriftClose<'info> { #[account(has_one = manager @ ManagerError::NotAuthorizedError)] - pub fund: Account<'info, Fund>, + pub fund: Account<'info, FundAccount>, + + #[account(seeds = [b"treasury".as_ref(), fund.key().as_ref()], bump)] pub treasury: SystemAccount<'info>, #[account(mut)] @@ -336,7 +342,7 @@ pub fn drift_close_handler(ctx: Context) -> Result<()> { let seeds = &[ "treasury".as_bytes(), fund_key.as_ref(), - &[ctx.accounts.fund.bump_treasury], + &[ctx.bumps.treasury], ]; let signer_seeds = &[&seeds[..]]; diff --git a/anchor/programs/glam/src/instructions/manager.rs b/anchor/programs/glam/src/instructions/manager.rs index b3985dbb..c02e018a 100644 --- a/anchor/programs/glam/src/instructions/manager.rs +++ b/anchor/programs/glam/src/instructions/manager.rs @@ -3,13 +3,13 @@ use anchor_spl::{token_2022, token_interface::Token2022}; use spl_token_2022::{extension::ExtensionType, state::Mint as StateMint}; use crate::error::ManagerError; -use crate::state::fund::*; +use crate::state::*; #[derive(Accounts)] #[instruction(name: String)] pub struct InitializeFund<'info> { #[account(init, seeds = [b"fund".as_ref(), manager.key().as_ref(), name.as_ref()], bump, payer = manager, space = 8 + Fund::INIT_SIZE + ShareClassMetadata::INIT_SIZE)] - pub fund: Box>, + pub fund: Box>, #[account(mut, seeds = [b"treasury".as_ref(), fund.key().as_ref()], bump)] pub treasury: SystemAccount<'info>, @@ -89,7 +89,10 @@ pub fn initialize_fund_handler<'c: 'info, 'info>( .map(|a| a.key()) .collect(); - fund.init( + fund.manager = ctx.accounts.manager.key(); + fund.treasury = ctx.accounts.treasury.key(); + + /*fund.init( fund_name, fund_symbol, fund_uri, @@ -101,7 +104,7 @@ pub fn initialize_fund_handler<'c: 'info, 'info>( ctx.bumps.treasury, Clock::get()?.unix_timestamp, activate, - ); + );*/ msg!("Fund created: {}", ctx.accounts.fund.key()); Ok(()) @@ -123,7 +126,7 @@ pub struct AddShareClass<'info> { pub share_class_mint: AccountInfo<'info>, #[account(mut, has_one = manager @ ManagerError::NotAuthorizedError)] - pub fund: Account<'info, Fund>, + pub fund: Account<'info, FundAccount>, #[account(mut)] pub manager: Signer<'info>, @@ -138,7 +141,7 @@ pub fn add_share_class_handler<'c: 'info, 'info>( ) -> Result<()> { let fund = &mut ctx.accounts.fund; fund.share_classes.push(ctx.accounts.share_class_mint.key()); - fund.share_classes_bumps.push(ctx.bumps.share_class_mint); + // fund.share_classes_bumps.push(ctx.bumps.share_class_mint); // // Initialize share class mint and metadata // @@ -367,7 +370,7 @@ pub fn add_share_class_handler<'c: 'info, 'info>( #[derive(Accounts)] pub struct UpdateFund<'info> { #[account(mut, has_one = manager @ ManagerError::NotAuthorizedError)] - fund: Account<'info, Fund>, + fund: Account<'info, FundAccount>, #[account(mut)] manager: Signer<'info>, } @@ -389,18 +392,18 @@ pub fn update_fund_handler<'c: 'info, 'info>( require!(uri.as_bytes().len() <= 100, ManagerError::InvalidFundName); fund.uri = uri; } - if let Some(activate) = activate { - fund.is_active = activate; - } - if let Some(asset_weights) = asset_weights { - require!( - asset_weights.len() == fund.assets_weights.len(), - ManagerError::InvalidAssetsLen - ); - for (i, &w) in asset_weights.iter().enumerate() { - fund.assets_weights[i] = w; - } - } + // if let Some(activate) = activate { + // fund.is_active = activate; + // } + // if let Some(asset_weights) = asset_weights { + // require!( + // asset_weights.len() == fund.assets_weights.len(), + // ManagerError::InvalidAssetsLen + // ); + // for (i, &w) in asset_weights.iter().enumerate() { + // fund.assets_weights[i] = w; + // } + // } msg!("Fund updated: {}", ctx.accounts.fund.key()); Ok(()) @@ -409,7 +412,7 @@ pub fn update_fund_handler<'c: 'info, 'info>( #[derive(Accounts)] pub struct CloseFund<'info> { #[account(mut, close = manager, has_one = manager @ ManagerError::NotAuthorizedError)] - fund: Account<'info, Fund>, + fund: Account<'info, FundAccount>, #[account(mut)] manager: Signer<'info>, } diff --git a/anchor/programs/glam/src/instructions/marinade.rs b/anchor/programs/glam/src/instructions/marinade.rs index 591ac025..a663a7f9 100644 --- a/anchor/programs/glam/src/instructions/marinade.rs +++ b/anchor/programs/glam/src/instructions/marinade.rs @@ -9,7 +9,7 @@ use marinade::program::MarinadeFinance; use marinade::State as MarinadeState; use marinade::TicketAccountData; -use crate::Fund; +use crate::state::*; pub fn marinade_deposit<'c: 'info, 'info>( ctx: Context, @@ -169,7 +169,7 @@ pub struct MarinadeDeposit<'info> { pub manager: Signer<'info>, #[account(has_one = manager, has_one = treasury)] - pub fund: Box>, + pub fund: Box>, /// CHECK: skip #[account(mut, seeds = [b"treasury".as_ref(), fund.key().as_ref()], bump)] @@ -222,7 +222,7 @@ pub struct MarinadeDelayedUnstake<'info> { pub manager: Signer<'info>, #[account(has_one = manager, has_one = treasury)] - pub fund: Box>, + pub fund: Box>, #[account(mut, seeds = [b"treasury".as_ref(), fund.key().as_ref()], bump)] pub treasury: SystemAccount<'info>, @@ -261,7 +261,7 @@ pub struct MarinadeClaim<'info> { pub manager: Signer<'info>, #[account(has_one = manager, has_one = treasury)] - pub fund: Box>, + pub fund: Box>, #[account(mut, seeds = [b"treasury".as_ref(), fund.key().as_ref()], bump)] pub treasury: SystemAccount<'info>, @@ -291,7 +291,7 @@ pub struct MarinadeLiquidUnstake<'info> { pub manager: Signer<'info>, #[account(has_one = manager, has_one = treasury)] - pub fund: Box>, + pub fund: Box>, #[account(mut, seeds = [b"treasury".as_ref(), fund.key().as_ref()], bump)] pub treasury: SystemAccount<'info>, diff --git a/anchor/tests/glam_crud.spec.ts b/anchor/tests/glam_crud.spec.ts index e62e6b93..a2a0128c 100644 --- a/anchor/tests/glam_crud.spec.ts +++ b/anchor/tests/glam_crud.spec.ts @@ -26,11 +26,11 @@ describe("glam_crud", () => { sharePDA = fundData.sharePDA; shareBump = fundData.shareBump; - const fund = await program.account.fund.fetch(fundPDA); + const fund = await program.account.fundAccount.fetch(fundPDA); expect(fund.shareClasses.length).toEqual(1); - expect(fund.assets.length).toEqual(3); - expect(fund.symbol).toEqual("GBTC"); - expect(fund.isActive).toEqual(true); + // expect(fund.assets.length).toEqual(3); + // expect(fund.symbol).toEqual("GBTC"); + // expect(fund.isActive).toEqual(true); const metadata = await getTokenMetadata(provider.connection, sharePDA); const { image_uri } = Object.fromEntries(metadata!.additionalMetadata); @@ -48,13 +48,13 @@ describe("glam_crud", () => { manager: manager.publicKey }) .rpc({ commitment }); - const fund = await program.account.fund.fetch(fundPDA); + const fund = await program.account.fundAccount.fetch(fundPDA); expect(fund.name).toEqual(newFundName); - expect(fund.isActive).toEqual(false); + // expect(fund.isActive).toEqual(false); }); it("Close fund", async () => { - const fund = await program.account.fund.fetchNullable(fundPDA); + const fund = await program.account.fundAccount.fetchNullable(fundPDA); expect(fund).not.toBeNull(); await program.methods @@ -66,7 +66,9 @@ describe("glam_crud", () => { .rpc({ commitment }); // The account should no longer exist, returning null. - const closedAccount = await program.account.fund.fetchNullable(fundPDA); + const closedAccount = await program.account.fundAccount.fetchNullable( + fundPDA + ); expect(closedAccount).toBeNull(); }); }); diff --git a/anchor/tests/glam_drift.spec.ts b/anchor/tests/glam_drift.spec.ts index b6e8eed1..356fa6c4 100644 --- a/anchor/tests/glam_drift.spec.ts +++ b/anchor/tests/glam_drift.spec.ts @@ -33,12 +33,12 @@ describe("glam_drift", () => { }); it("Initialize fund", async () => { - const fund = await program.account.fund.fetch(fundPDA); + const fund = await program.account.fundAccount.fetch(fundPDA); // console.log(fund); // expect(fund.shareClassesLen).toEqual(1); - expect(fund.assets.length).toEqual(3); - expect(fund.symbol).toEqual("GBTC"); - expect(fund.isActive).toEqual(true); + // expect(fund.assets.length).toEqual(3); + // expect(fund.symbol).toEqual("GBTC"); + // expect(fund.isActive).toEqual(true); }); it("Drift initialize", async () => { @@ -188,7 +188,7 @@ describe("glam_drift", () => { }, 30_000); */ it("Close fund", async () => { - const fund = await program.account.fund.fetchNullable(fundPDA); + const fund = await program.account.fundAccount.fetchNullable(fundPDA); expect(fund).not.toBeNull(); await program.methods @@ -200,7 +200,9 @@ describe("glam_drift", () => { .rpc({ commitment }); // The account should no longer exist, returning null. - const closedAccount = await program.account.fund.fetchNullable(fundPDA); + const closedAccount = await program.account.fundAccount.fetchNullable( + fundPDA + ); expect(closedAccount).toBeNull(); }); }); diff --git a/anchor/tests/glam_staking.spec.ts b/anchor/tests/glam_staking.spec.ts index 482886ff..28f6b225 100644 --- a/anchor/tests/glam_staking.spec.ts +++ b/anchor/tests/glam_staking.spec.ts @@ -45,11 +45,11 @@ describe("glam_staking", () => { sharePDA = fundData.sharePDA; shareBump = fundData.shareBump; - const fund = await program.account.fund.fetch(fundData.fundPDA); + const fund = await program.account.fundAccount.fetch(fundData.fundPDA); // expect(fund.shareClassesLen).toEqual(1); - expect(fund.assets.length).toEqual(3); - expect(fund.symbol).toEqual("GTST"); - expect(fund.isActive).toEqual(true); + // expect(fund.assets.length).toEqual(3); + // expect(fund.symbol).toEqual("GTST"); + // expect(fund.isActive).toEqual(true); // air drop to treasury and delay 1s for confirmation await provider.connection.requestAirdrop(fundTreasuryPDA, 100_000_000_000); From 591e1ef77c8d2b8e59e1b9c7779ba71653890b32 Mon Sep 17 00:00:00 2001 From: Emanuele Cesena Date: Thu, 9 May 2024 18:24:30 -0700 Subject: [PATCH 3/9] v0.2: FundAccount + Openfunds data --- .../programs/glam/src/instructions/manager.rs | 90 ++++++------ anchor/programs/glam/src/lib.rs | 16 +-- anchor/src/client.ts | 10 +- anchor/target/idl/glam.json | 25 +--- anchor/target/types/glam.ts | 50 ++----- anchor/tests/glam_drift.spec.ts | 18 --- anchor/tests/setup.ts | 128 ++++++++++++++---- api/src/openfunds.ts | 7 +- api/src/validation.ts | 13 +- 9 files changed, 180 insertions(+), 177 deletions(-) diff --git a/anchor/programs/glam/src/instructions/manager.rs b/anchor/programs/glam/src/instructions/manager.rs index c02e018a..0e89d9fe 100644 --- a/anchor/programs/glam/src/instructions/manager.rs +++ b/anchor/programs/glam/src/instructions/manager.rs @@ -6,11 +6,14 @@ use crate::error::ManagerError; use crate::state::*; #[derive(Accounts)] -#[instruction(name: String)] +#[instruction(fund_model: FundModel)] pub struct InitializeFund<'info> { - #[account(init, seeds = [b"fund".as_ref(), manager.key().as_ref(), name.as_ref()], bump, payer = manager, space = 8 + Fund::INIT_SIZE + ShareClassMetadata::INIT_SIZE)] + #[account(init, seeds = [b"fund".as_ref(), manager.key().as_ref(), fund_model.created.as_ref().unwrap().key.as_ref()], bump, payer = manager, space = 8 + FundAccount::INIT_SIZE)] pub fund: Box>, + #[account(init, seeds = [b"openfunds".as_ref(), fund.key().as_ref()], bump, payer = manager, space = FundMetadataAccount::INIT_SIZE)] + pub openfunds: Box>, + #[account(mut, seeds = [b"treasury".as_ref(), fund.key().as_ref()], bump)] pub treasury: SystemAccount<'info>, @@ -22,35 +25,8 @@ pub struct InitializeFund<'info> { pub fn initialize_fund_handler<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, InitializeFund<'info>>, - fund_name: String, - fund_symbol: String, - fund_uri: String, - asset_weights: Vec, - activate: bool, + fund_model: FundModel, ) -> Result<()> { - // - // Validate the input - // - require!( - fund_name.as_bytes().len() <= MAX_FUND_NAME, - ManagerError::InvalidFundName - ); - require!( - fund_symbol.as_bytes().len() <= MAX_FUND_SYMBOL, - ManagerError::InvalidFundSymbol - ); - require!( - fund_uri.as_bytes().len() <= MAX_FUND_URI, - ManagerError::InvalidFundUri - ); - - let assets_len = ctx.remaining_accounts.len(); - require!(assets_len <= MAX_ASSETS, ManagerError::InvalidAssetsLen); - require!( - asset_weights.len() == assets_len, - ManagerError::InvalidAssetsLen - ); - // // Create the treasury account // @@ -83,28 +59,42 @@ pub fn initialize_fund_handler<'c: 'info, 'info>( // Initialize the fund // let fund = &mut ctx.accounts.fund; - let asset_mints: Vec = ctx - .remaining_accounts - .into_iter() - .map(|a| a.key()) - .collect(); + let treasury = &mut ctx.accounts.treasury; + let openfunds = &mut ctx.accounts.openfunds; + + let model = fund_model.clone(); + if let Some(fund_name) = model.name { + require!( + fund_name.len() < MAX_FUND_NAME, + ManagerError::InvalidFundName + ); + fund.name = fund_name; + } + if let Some(fund_uri) = model.uri { + require!(fund_uri.len() < MAX_FUND_URI, ManagerError::InvalidFundUri); + fund.uri = fund_uri; + } + if let Some(openfunds_uri) = model.openfunds_uri { + require!( + openfunds_uri.len() < MAX_FUND_URI, + ManagerError::InvalidFundUri + ); + fund.openfunds_uri = openfunds_uri; + } + fund.treasury = treasury.key(); + fund.openfunds = openfunds.key(); fund.manager = ctx.accounts.manager.key(); - fund.treasury = ctx.accounts.treasury.key(); - - /*fund.init( - fund_name, - fund_symbol, - fund_uri, - ctx.accounts.manager.key(), - ctx.accounts.treasury.key(), - asset_mints, - asset_weights, - ctx.bumps.fund, - ctx.bumps.treasury, - Clock::get()?.unix_timestamp, - activate, - );*/ + + // + // Initialize openfunds + // + let openfunds_metadata = FundMetadataAccount::from(fund_model); + openfunds.fund_pubkey = fund.key(); + openfunds.company = openfunds_metadata.company; + openfunds.fund = openfunds_metadata.fund; + openfunds.share_classes = openfunds_metadata.share_classes; + openfunds.fund_managers = openfunds_metadata.fund_managers; msg!("Fund created: {}", ctx.accounts.fund.key()); Ok(()) diff --git a/anchor/programs/glam/src/lib.rs b/anchor/programs/glam/src/lib.rs index aefc3dab..7a69af5d 100644 --- a/anchor/programs/glam/src/lib.rs +++ b/anchor/programs/glam/src/lib.rs @@ -8,6 +8,7 @@ use anchor_lang::prelude::*; use crate::instructions::*; pub use constants::*; pub use state::fund::*; +pub use state::model::*; declare_id!("Gco1pcjxCMYjKJjSNJ7mKV7qezeUTE7arXJgy7PAPNRc"); @@ -20,20 +21,9 @@ pub mod glam { pub fn initialize<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, InitializeFund<'info>>, - fund_name: String, - fund_symbol: String, - fund_uri: String, - asset_weights: Vec, - activate: bool, + fund: FundModel, ) -> Result<()> { - manager::initialize_fund_handler( - ctx, - fund_name, - fund_symbol, - fund_uri, - asset_weights, - activate, - ) + manager::initialize_fund_handler(ctx, fund) } pub fn add_share_class<'c: 'info, 'info>( diff --git a/anchor/src/client.ts b/anchor/src/client.ts index 573d345c..13635a34 100644 --- a/anchor/src/client.ts +++ b/anchor/src/client.ts @@ -183,25 +183,23 @@ export class GlamClient { const fundModel = this.enrichFundModelInitialize(fund); const fundPDA = this.getFundPDA(fundModel); const treasury = this.getTreasuryPDA(fundPDA); - const share = this.getShareClassPDA(fundPDA, 0); + // const share = this.getShareClassPDA(fundPDA, 0); const openfunds = this.getOpenfundsPDA(fundPDA); const manager = this.getManager(); //TODO: add instructions to "addShareClass" in the same tx - const txSig = ""; /*await this.program.methods + const txSig = await this.program.methods .initialize(fundModel) .accounts({ fund: fundPDA, treasury, openfunds, - share, - manager, - tokenProgram: TOKEN_2022_PROGRAM_ID + manager }) .preInstructions([ ComputeBudgetProgram.setComputeUnitLimit({ units: 500_000 }) ]) - .rpc();*/ + .rpc(); return [txSig, fundPDA]; } diff --git a/anchor/target/idl/glam.json b/anchor/target/idl/glam.json index e290a616..f796638d 100644 --- a/anchor/target/idl/glam.json +++ b/anchor/target/idl/glam.json @@ -17,6 +17,11 @@ "isMut": true, "isSigner": false }, + { + "name": "openfunds", + "isMut": true, + "isSigner": false + }, { "name": "treasury", "isMut": true, @@ -35,26 +40,10 @@ ], "args": [ { - "name": "fundName", - "type": "string" - }, - { - "name": "fundSymbol", - "type": "string" - }, - { - "name": "fundUri", - "type": "string" - }, - { - "name": "assetWeights", + "name": "fund", "type": { - "vec": "u32" + "defined": "FundModel" } - }, - { - "name": "activate", - "type": "bool" } ] }, diff --git a/anchor/target/types/glam.ts b/anchor/target/types/glam.ts index b6239196..bc4c7f51 100644 --- a/anchor/target/types/glam.ts +++ b/anchor/target/types/glam.ts @@ -17,6 +17,11 @@ export type Glam = { "isMut": true, "isSigner": false }, + { + "name": "openfunds", + "isMut": true, + "isSigner": false + }, { "name": "treasury", "isMut": true, @@ -35,26 +40,10 @@ export type Glam = { ], "args": [ { - "name": "fundName", - "type": "string" - }, - { - "name": "fundSymbol", - "type": "string" - }, - { - "name": "fundUri", - "type": "string" - }, - { - "name": "assetWeights", + "name": "fund", "type": { - "vec": "u32" + "defined": "FundModel" } - }, - { - "name": "activate", - "type": "bool" } ] }, @@ -2856,6 +2845,11 @@ export const IDL: Glam = { "isMut": true, "isSigner": false }, + { + "name": "openfunds", + "isMut": true, + "isSigner": false + }, { "name": "treasury", "isMut": true, @@ -2874,26 +2868,10 @@ export const IDL: Glam = { ], "args": [ { - "name": "fundName", - "type": "string" - }, - { - "name": "fundSymbol", - "type": "string" - }, - { - "name": "fundUri", - "type": "string" - }, - { - "name": "assetWeights", + "name": "fund", "type": { - "vec": "u32" + "defined": "FundModel" } - }, - { - "name": "activate", - "type": "bool" } ] }, diff --git a/anchor/tests/glam_drift.spec.ts b/anchor/tests/glam_drift.spec.ts index 356fa6c4..fc95e898 100644 --- a/anchor/tests/glam_drift.spec.ts +++ b/anchor/tests/glam_drift.spec.ts @@ -187,22 +187,4 @@ describe("glam_drift", () => { } }, 30_000); */ - it("Close fund", async () => { - const fund = await program.account.fundAccount.fetchNullable(fundPDA); - expect(fund).not.toBeNull(); - - await program.methods - .close() - .accounts({ - fund: fundPDA, - manager: manager.publicKey - }) - .rpc({ commitment }); - - // The account should no longer exist, returning null. - const closedAccount = await program.account.fundAccount.fetchNullable( - fundPDA - ); - expect(closedAccount).toBeNull(); - }); }); diff --git a/anchor/tests/setup.ts b/anchor/tests/setup.ts index 65464a48..e803dc7d 100644 --- a/anchor/tests/setup.ts +++ b/anchor/tests/setup.ts @@ -1,5 +1,6 @@ import { Program, Wallet, workspace } from "@coral-xyz/anchor"; import { ComputeBudgetProgram, PublicKey } from "@solana/web3.js"; +import { GlamClient } from "../src"; import { Glam } from "../target/types/glam"; import { getMetadataUri, getImageUri, getFundUri } from "../src/offchain"; import { TOKEN_2022_PROGRAM_ID } from "@solana/spl-token"; // Fix import warning in VSCode @@ -19,15 +20,87 @@ export const createFundForTest = async ( symbol: string, manager: Wallet ) => { - const [fundPDA, fundBump] = PublicKey.findProgramAddressSync( - [Buffer.from("fund"), manager.publicKey.toBuffer(), Buffer.from(name)], - program.programId - ); + const client = new GlamClient(); + let txId, fundPDA; + try { + [txId, fundPDA] = await client.createFund({ + shareClasses: [ + { + // Glam Token + symbol: "GBS", + name: "Glam Investment Fund BTC-SOL", + asset: usdc, + // Openfund Share Class + isin: "XS1082172823", + shareClassCurrency: "USDC", + fullShareClassName: null, // auto + hasPerformanceFee: false, + hasSubscriptionFeeInFavourOfDistributor: false, + investmentStatus: "open", //TODO: auto + shareClassDistributionPolicy: "accumulating", //TODO: auto + shareClassExtension: "", + shareClassLaunchDate: null, // auto + shareClassLifecycle: "active", //TODO: auto + // launchPrice: null, + // launchPriceCurrency: null, + // launchPriceDate: null, + hasAppliedSubscriptionFeeInFavourOfFund: false, + hasAppliedRedemptionFeeInFavourOfFund: false, + hasLockUpForRedemption: false, + hasRedemptionFeeInFavourOfDistributor: false, + isValidISIN: false + // lockUpComment: null, + // lockUpPeriodInDays: null, + // roundingMethodForPrices: null, + // roundingMethodForRedemptionInAmount: null, + // roundingMethodForRedemptionInShares: null, + // roundingMethodForSubscriptionInAmount: null, + // roundingMethodForSubscriptionInShares: null, + } + ], + // Glam + isEnabled: true, + assets: [usdc, btc, eth], + assetsWeights: [0, 60, 40], + // Openfund (Fund) + fundDomicileAlpha2: "XS", + legalFundNameIncludingUmbrella: null, // auto + fiscalYearEnd: "12-31", + fundCurrency: null, // auto + fundLaunchDate: null, // auto + investmentObjective: "demo", + // investmentObjective: + // "The Glam Investment Fund seeks to reflect generally the performance of the price of Bitcoin and Solana.", + isFundOfFunds: false, + isPassiveFund: true, //TODO: auto + legalForm: "other", + openEndedOrClosedEndedFundStructure: "open-ended fund", //TODO: auto + // Openfund Company (simplified) + company: { + name: "Glam Systems", + email: "hello@glam.systems", + website: "https://glam.systems" + }, + // Openfund Manager (simplified) + manager: { + name: "0x0ece.sol" + } + }); + console.log(`Fund ${fundPDA} initialized, txId: ${txId}`); + } catch (e) { + console.error(e); + throw e; + } - const [treasuryPDA, treasuryBump] = PublicKey.findProgramAddressSync( - [Buffer.from("treasury"), fundPDA.toBuffer()], - program.programId - ); + // const [fundPDA, fundBump] = PublicKey.findProgramAddressSync( + // [Buffer.from("fund"), manager.publicKey.toBuffer(), Buffer.from(name)], + // program.programId + // ); + + // const [treasuryPDA, treasuryBump] = PublicKey.findProgramAddressSync( + // [Buffer.from("treasury"), fundPDA.toBuffer()], + // program.programId + // ); const shareClassSymbol = `${symbol}.A`; const [sharePDA, shareBump] = PublicKey.findProgramAddressSync( @@ -52,22 +125,22 @@ export const createFundForTest = async ( }; try { - let txId = await program.methods - .initialize(name, symbol, getFundUri(fundPDA), [10, 50, 40], true) - .accounts({ - fund: fundPDA, - treasury: treasuryPDA, - manager: manager.publicKey - }) - .remainingAccounts([ - { pubkey: usdc, isSigner: false, isWritable: false }, - { pubkey: btc, isSigner: false, isWritable: false }, - { pubkey: eth, isSigner: false, isWritable: false } - ]) - .rpc({ commitment: "confirmed" }); - console.log(`Fund ${fundPDA} initialized, txId: ${txId}`); + // let txId = await program.methods + // .initialize(name, symbol, getFundUri(fundPDA), [10, 50, 40], true) + // .accounts({ + // fund: fundPDA, + // treasury: treasuryPDA, + // manager: manager.publicKey + // }) + // .remainingAccounts([ + // { pubkey: usdc, isSigner: false, isWritable: false }, + // { pubkey: btc, isSigner: false, isWritable: false }, + // { pubkey: eth, isSigner: false, isWritable: false } + // ]) + // .rpc({ commitment: "confirmed" }); + // console.log(`Fund ${fundPDA} initialized, txId: ${txId}`); - txId = await program.methods + const txId = await program.methods .addShareClass(shareClassMetadata) .accounts({ fund: fundPDA, @@ -85,5 +158,12 @@ export const createFundForTest = async ( throw e; } - return { fundPDA, fundBump, treasuryPDA, treasuryBump, sharePDA, shareBump }; + return { + fundPDA, + fundBump: null, + treasuryPDA: client.getTreasuryPDA(fundPDA), + treasuryBump: null, + sharePDA, + shareBump + }; }; diff --git a/api/src/openfunds.ts b/api/src/openfunds.ts index 0fe076ec..b488a6de 100644 --- a/api/src/openfunds.ts +++ b/api/src/openfunds.ts @@ -83,13 +83,14 @@ export const openfunds = async (funds, template, format, client, res) => { // validate key const key = validatePubkey(fund); if (!key) { - reject(rejectErr); + return reject(rejectErr); } // fetch fund try { - resolve(await client.fetchFund(key)); + return resolve(await client.fetchFund(key)); } catch (err) { - reject(rejectErr); + console.error(err); + return reject(rejectErr); } }) ) diff --git a/api/src/validation.ts b/api/src/validation.ts index c9014101..6f073fdd 100644 --- a/api/src/validation.ts +++ b/api/src/validation.ts @@ -1,17 +1,12 @@ import { base58 } from "@scure/base"; +import { PublicKey } from "@solana/web3.js"; -export const validatePubkey = (pubkey) => { - if (pubkey.length > 50) { - return false; - } +export const validatePubkey = (pubkey: string) => { let key; try { - key = base58.decode(pubkey); - if (key.length != 32) { - return false; - } + key = new PublicKey(pubkey); } catch (_e) { - return false; + return undefined; } return key; }; From ad4ca941cf324d25ab0f463027a7ff478621706e Mon Sep 17 00:00:00 2001 From: Emanuele Cesena Date: Sat, 11 May 2024 11:26:56 -0700 Subject: [PATCH 4/9] v0.2: openfunds templates --- anchor/programs/glam/src/state/model/model.rs | 42 ++--- .../glam/src/state/model/openfunds.rs | 147 +++++++++--------- .../glam/src/state/openfunds/share_class.rs | 12 +- anchor/target/idl/glam.json | 63 ++++++-- anchor/target/types/glam.ts | 126 ++++++++++++--- api/src/openfunds.ts | 114 ++++++++++---- package.json | 1 + pnpm-lock.yaml | 3 + 8 files changed, 342 insertions(+), 166 deletions(-) diff --git a/anchor/programs/glam/src/state/model/model.rs b/anchor/programs/glam/src/state/model/model.rs index b5296643..7b099f2d 100644 --- a/anchor/programs/glam/src/state/model/model.rs +++ b/anchor/programs/glam/src/state/model/model.rs @@ -87,18 +87,18 @@ pub struct ShareClassOpenfundsModel { // Core // pub applied_subscription_fee_in_favour_of_distributor: Option, // pub applied_subscription_fee_in_favour_of_distributor_reference_date: Option, - // pub currency_of_minimal_subscription: Option, + pub currency_of_minimal_subscription: Option, pub full_share_class_name: Option, - pub has_performance_fee: Option, + // pub has_performance_fee: Option, // pub has_subscription_fee_in_favour_of_distributor: Option, pub investment_status: Option, - pub management_fee_applied: Option, - pub management_fee_applied_reference_date: Option, - pub management_fee_maximum: Option, + // pub management_fee_applied: Option, + // pub management_fee_applied_reference_date: Option, + // pub management_fee_maximum: Option, // pub maximum_subscription_fee_in_favour_of_distributor: Option, - // pub minimal_initial_subscription_category: Option, - // pub minimal_initial_subscription_in_amount: Option, - // pub minimal_initial_subscription_in_shares: Option, + pub minimal_initial_subscription_category: Option, + pub minimal_initial_subscription_in_amount: Option, + pub minimal_initial_subscription_in_shares: Option, // pub minimal_subsequent_subscription_category: Option, // pub minimal_subsequent_subscription_in_amount: Option, // pub minimal_subsequent_subscription_in_shares: Option, @@ -122,28 +122,28 @@ pub struct ShareClassOpenfundsModel { // Full // pub applied_redemption_fee_in_favour_of_distributor: Option, // pub applied_redemption_fee_in_favour_of_distributor_reference_date: Option, - // pub currency_of_minimal_or_maximum_redemption: Option, + pub currency_of_minimal_or_maximum_redemption: Option, // pub cut_off_date_offset_for_redemption: Option, // pub cut_off_date_offset_for_subscription: Option, // pub cut_off_time_for_redemption: Option, // pub cut_off_time_for_subscription: Option, - // pub has_lock_up_for_redemption: Option, + pub has_lock_up_for_redemption: Option, // pub has_redemption_fee_in_favour_of_distributor: Option, pub is_valid_isin: Option, pub lock_up_comment: Option, pub lock_up_period_in_days: Option, - pub management_fee_minimum: Option, - pub maximal_number_of_possible_decimals_amount: Option, - pub maximal_number_of_possible_decimals_nav: Option, - pub maximal_number_of_possible_decimals_shares: Option, - // pub maximum_initialredemption_in_amount: Option, - // pub maximum_initialredemption_in_shares: Option, + // pub management_fee_minimum: Option, + // pub maximal_number_of_possible_decimals_amount: Option, + // pub maximal_number_of_possible_decimals_nav: Option, + // pub maximal_number_of_possible_decimals_shares: Option, + pub maximum_initial_redemption_in_amount: Option, + pub maximum_initial_redemption_in_shares: Option, // pub maximum_redemption_fee_in_favour_of_distributor: Option, // pub maximum_subsequent_redemption_in_amount: Option, // pub maximum_subsequent_redemption_in_shares: Option, - // pub minimal_initial_redemption_in_amount: Option, - // pub minimal_initial_redemption_in_shares: Option, - // pub minimal_redemption_category: Option, + pub minimal_initial_redemption_in_amount: Option, + pub minimal_initial_redemption_in_shares: Option, + pub minimal_redemption_category: Option, // pub minimal_subsequent_redemption_in_amount: Option, // pub minimal_subsequent_redemption_in_shares: Option, // pub minimum_redemption_fee_in_favour_of_distributor: Option, @@ -155,6 +155,10 @@ pub struct ShareClassOpenfundsModel { // pub rounding_method_for_redemption_in_shares: Option, // pub rounding_method_for_subscription_in_amount: Option, // pub rounding_method_for_subscription_in_shares: Option, + pub share_class_dividend_type: Option, + // Full | Country + pub cusip: Option, + pub valor: Option, } // Company diff --git a/anchor/programs/glam/src/state/model/openfunds.rs b/anchor/programs/glam/src/state/model/openfunds.rs index 128b2e29..ea47450e 100644 --- a/anchor/programs/glam/src/state/model/openfunds.rs +++ b/anchor/programs/glam/src/state/model/openfunds.rs @@ -94,19 +94,19 @@ impl From<&ShareClassModel> for Vec { // model.applied_subscription_fee_in_favour_of_distributor_reference_date, // ShareClassFieldName::AppliedSubscriptionFeeInFavourOfDistributorReferenceDate, // ), - // ( - // model.currency_of_minimal_subscription, - // ShareClassFieldName::CurrencyOfMinimalSubscription, - // ), ( - model.full_share_class_name, - ShareClassFieldName::FullShareClassName, + model.currency_of_minimal_subscription, + ShareClassFieldName::CurrencyOfMinimalSubscription, ), ( - bool2string(model.has_performance_fee), - ShareClassFieldName::HasPerformanceFee, + model.full_share_class_name, + ShareClassFieldName::FullShareClassName, ), // ( + // bool2string(model.has_performance_fee), + // ShareClassFieldName::HasPerformanceFee, + // ), + // ( // bool2string(model.has_subscription_fee_in_favour_of_distributor), // ShareClassFieldName::HasSubscriptionFeeInFavourOfDistributor, // ), @@ -114,34 +114,34 @@ impl From<&ShareClassModel> for Vec { model.investment_status, ShareClassFieldName::InvestmentStatus, ), - ( - model.management_fee_applied, - ShareClassFieldName::ManagementFeeApplied, - ), - ( - model.management_fee_applied_reference_date, - ShareClassFieldName::ManagementFeeAppliedReferenceDate, - ), - ( - model.management_fee_maximum, - ShareClassFieldName::ManagementFeeMaximum, - ), // ( - // model.maximum_subscription_fee_in_favour_of_distributor, - // ShareClassFieldName::MaximumSubscriptionFeeInFavourOfDistributor, + // model.management_fee_applied, + // ShareClassFieldName::ManagementFeeApplied, // ), // ( - // model.minimal_initial_subscription_category, - // ShareClassFieldName::MinimalInitialSubscriptionCategory, + // model.management_fee_applied_reference_date, + // ShareClassFieldName::ManagementFeeAppliedReferenceDate, // ), // ( - // model.minimal_initial_subscription_in_amount, - // ShareClassFieldName::MinimalInitialSubscriptionInAmount, + // model.management_fee_maximum, + // ShareClassFieldName::ManagementFeeMaximum, // ), // ( - // model.minimal_initial_subscription_in_shares, - // ShareClassFieldName::MinimalInitialSubscriptionInShares, + // model.maximum_subscription_fee_in_favour_of_distributor, + // ShareClassFieldName::MaximumSubscriptionFeeInFavourOfDistributor, // ), + ( + model.minimal_initial_subscription_category, + ShareClassFieldName::MinimalInitialSubscriptionCategory, + ), + ( + model.minimal_initial_subscription_in_amount, + ShareClassFieldName::MinimalInitialSubscriptionInAmount, + ), + ( + model.minimal_initial_subscription_in_shares, + ShareClassFieldName::MinimalInitialSubscriptionInShares, + ), // ( // model.minimal_subsequent_subscription_category, // ShareClassFieldName::MinimalSubsequentSubscriptionCategory, @@ -225,10 +225,10 @@ impl From<&ShareClassModel> for Vec { // model.applied_redemption_fee_in_favour_of_distributor_reference_date, // ShareClassFieldName::AppliedRedemptionFeeInFavourOfDistributorReferenceDate, // ), - // ( - // model.currency_of_minimal_or_maximum_redemption, - // ShareClassFieldName::CurrencyOfMinimalOrMaximumRedemption, - // ), + ( + model.currency_of_minimal_or_maximum_redemption, + ShareClassFieldName::CurrencyOfMinimalOrMaximumRedemption, + ), // ( // model.cut_off_date_offset_for_redemption, // ShareClassFieldName::CutOffDateOffsetForRedemption, @@ -245,10 +245,10 @@ impl From<&ShareClassModel> for Vec { // model.cut_off_time_for_subscription, // ShareClassFieldName::CutOffTimeForSubscription, // ), - // ( - // bool2string(model.has_lock_up_for_redemption), - // ShareClassFieldName::HasLockUpForRedemption, - // ), + ( + bool2string(model.has_lock_up_for_redemption), + ShareClassFieldName::HasLockUpForRedemption, + ), // ( // bool2string(model.has_redemption_fee_in_favour_of_distributor), // ShareClassFieldName::HasRedemptionFeeInFavourOfDistributor, @@ -262,31 +262,31 @@ impl From<&ShareClassModel> for Vec { model.lock_up_period_in_days, ShareClassFieldName::LockUpPeriodInDays, ), - ( - model.management_fee_minimum, - ShareClassFieldName::ManagementFeeMinimum, - ), - ( - model.maximal_number_of_possible_decimals_amount, - ShareClassFieldName::MaximalNumberOfPossibleDecimalsAmount, - ), - ( - model.maximal_number_of_possible_decimals_nav, - ShareClassFieldName::MaximalNumberOfPossibleDecimalsNAV, - ), - ( - model.maximal_number_of_possible_decimals_shares, - ShareClassFieldName::MaximalNumberOfPossibleDecimalsShares, - ), // ( - // model.maximum_initialredemption_in_amount, - // ShareClassFieldName::MaximumInitialRedemptionInAmount, + // model.management_fee_minimum, + // ShareClassFieldName::ManagementFeeMinimum, // ), // ( - // model.maximum_initialredemption_in_shares, - // ShareClassFieldName::MaximumInitialRedemptionInShares, + // model.maximal_number_of_possible_decimals_amount, + // ShareClassFieldName::MaximalNumberOfPossibleDecimalsAmount, // ), // ( + // model.maximal_number_of_possible_decimals_nav, + // ShareClassFieldName::MaximalNumberOfPossibleDecimalsNAV, + // ), + // ( + // model.maximal_number_of_possible_decimals_shares, + // ShareClassFieldName::MaximalNumberOfPossibleDecimalsShares, + // ), + ( + model.maximum_initial_redemption_in_amount, + ShareClassFieldName::MaximumInitialRedemptionInAmount, + ), + ( + model.maximum_initial_redemption_in_shares, + ShareClassFieldName::MaximumInitialRedemptionInShares, + ), + // ( // model.maximum_redemption_fee_in_favour_of_distributor, // ShareClassFieldName::MaximumRedemptionFeeInFavourOfDistributor, // ), @@ -298,18 +298,18 @@ impl From<&ShareClassModel> for Vec { // model.maximum_subsequent_redemption_in_shares, // ShareClassFieldName::MaximumSubsequentRedemptionInShares, // ), - // ( - // model.minimal_initial_redemption_in_amount, - // ShareClassFieldName::MinimalInitialRedemptionInAmount, - // ), - // ( - // model.minimal_initial_redemption_in_shares, - // ShareClassFieldName::MinimalInitialRedemptionInShares, - // ), - // ( - // model.minimal_redemption_category, - // ShareClassFieldName::MinimalRedemptionCategory, - // ), + ( + model.minimal_initial_redemption_in_amount, + ShareClassFieldName::MinimalInitialRedemptionInAmount, + ), + ( + model.minimal_initial_redemption_in_shares, + ShareClassFieldName::MinimalInitialRedemptionInShares, + ), + ( + model.minimal_redemption_category, + ShareClassFieldName::MinimalRedemptionCategory, + ), // ( // model.minimal_subsequent_redemption_in_amount, // ShareClassFieldName::MinimalSubsequentRedemptionInAmount, @@ -354,6 +354,13 @@ impl From<&ShareClassModel> for Vec { // model.rounding_method_for_subscription_in_shares, // ShareClassFieldName::RoundingMethodForSubscriptionInShares, // ), + ( + model.share_class_dividend_type, + ShareClassFieldName::ShareClassDividendType, + ), + // Full | Country + (model.cusip, ShareClassFieldName::CUSIP), + (model.valor, ShareClassFieldName::Valor), ] .iter() .for_each(|(value, field)| { @@ -372,10 +379,6 @@ impl From<&ShareClassModel> for Vec { //TODO let v: Vec<(Option, ShareClassFieldName)> = vec![ (pubkey2string(model.fund_id), ShareClassFieldName::FundId), - ( - pubkey2string(model.asset), - ShareClassFieldName::ShareClassCurrencyId, - ), (model.image_uri, ShareClassFieldName::ImageUri), ]; v.iter().for_each(|(value, field)| { diff --git a/anchor/programs/glam/src/state/openfunds/share_class.rs b/anchor/programs/glam/src/state/openfunds/share_class.rs index 52c89a4b..0c26c03d 100644 --- a/anchor/programs/glam/src/state/openfunds/share_class.rs +++ b/anchor/programs/glam/src/state/openfunds/share_class.rs @@ -51,7 +51,7 @@ pub enum ShareClassFieldName { ShareClassExtension, // impl ShareClassLaunchDate, // impl ShareClassLifecycle, // impl - SRRI, + SRRI, // impl TERExcludingPerformanceFee, TERExcludingPerformanceFeeDate, TERIncludingPerformanceFee, @@ -220,7 +220,7 @@ pub enum ShareClassFieldName { SettlementPeriodForSubscription, SettlementPeriodForSwitchIn, SettlementPeriodForSwitchOut, - ShareClassDividendType, + ShareClassDividendType, // impl SingleRegisterAccountRestrictions, SubscriptionPeriodEndDate, SubscriptionPeriodStartDate, @@ -236,10 +236,12 @@ pub enum ShareClassFieldName { WeeklySubscriptionDealingDays, YearlyRedemptionDealingDays, YearlySubscriptionDealingDays, + // Full | Country + CUSIP, // impl + Valor, // impl // Glam Extensions - FundId, // impl - ShareClassCurrencyId, // impl - ImageUri, // impl + FundId, // impl + ImageUri, // impl } #[derive(AnchorDeserialize, AnchorSerialize, Clone, Debug)] diff --git a/anchor/target/idl/glam.json b/anchor/target/idl/glam.json index f796638d..fa685003 100644 --- a/anchor/target/idl/glam.json +++ b/anchor/target/idl/glam.json @@ -1374,15 +1374,15 @@ } }, { - "name": "fullShareClassName", + "name": "currencyOfMinimalSubscription", "type": { "option": "string" } }, { - "name": "hasPerformanceFee", + "name": "fullShareClassName", "type": { - "option": "bool" + "option": "string" } }, { @@ -1392,19 +1392,19 @@ } }, { - "name": "managementFeeApplied", + "name": "minimalInitialSubscriptionCategory", "type": { "option": "string" } }, { - "name": "managementFeeAppliedReferenceDate", + "name": "minimalInitialSubscriptionInAmount", "type": { "option": "string" } }, { - "name": "managementFeeMaximum", + "name": "minimalInitialSubscriptionInShares", "type": { "option": "string" } @@ -1451,6 +1451,18 @@ "option": "string" } }, + { + "name": "currencyOfMinimalOrMaximumRedemption", + "type": { + "option": "string" + } + }, + { + "name": "hasLockUpForRedemption", + "type": { + "option": "bool" + } + }, { "name": "isValidIsin", "type": { @@ -1470,25 +1482,49 @@ } }, { - "name": "managementFeeMinimum", + "name": "maximumInitialRedemptionInAmount", + "type": { + "option": "string" + } + }, + { + "name": "maximumInitialRedemptionInShares", "type": { "option": "string" } }, { - "name": "maximalNumberOfPossibleDecimalsAmount", + "name": "minimalInitialRedemptionInAmount", "type": { "option": "string" } }, { - "name": "maximalNumberOfPossibleDecimalsNav", + "name": "minimalInitialRedemptionInShares", "type": { "option": "string" } }, { - "name": "maximalNumberOfPossibleDecimalsShares", + "name": "minimalRedemptionCategory", + "type": { + "option": "string" + } + }, + { + "name": "shareClassDividendType", + "type": { + "option": "string" + } + }, + { + "name": "cusip", + "type": { + "option": "string" + } + }, + { + "name": "valor", "type": { "option": "string" } @@ -2656,10 +2692,13 @@ "name": "YearlySubscriptionDealingDays" }, { - "name": "FundId" + "name": "CUSIP" + }, + { + "name": "Valor" }, { - "name": "ShareClassCurrencyId" + "name": "FundId" }, { "name": "ImageUri" diff --git a/anchor/target/types/glam.ts b/anchor/target/types/glam.ts index bc4c7f51..a040f63f 100644 --- a/anchor/target/types/glam.ts +++ b/anchor/target/types/glam.ts @@ -1374,15 +1374,15 @@ export type Glam = { } }, { - "name": "fullShareClassName", + "name": "currencyOfMinimalSubscription", "type": { "option": "string" } }, { - "name": "hasPerformanceFee", + "name": "fullShareClassName", "type": { - "option": "bool" + "option": "string" } }, { @@ -1392,19 +1392,19 @@ export type Glam = { } }, { - "name": "managementFeeApplied", + "name": "minimalInitialSubscriptionCategory", "type": { "option": "string" } }, { - "name": "managementFeeAppliedReferenceDate", + "name": "minimalInitialSubscriptionInAmount", "type": { "option": "string" } }, { - "name": "managementFeeMaximum", + "name": "minimalInitialSubscriptionInShares", "type": { "option": "string" } @@ -1451,6 +1451,18 @@ export type Glam = { "option": "string" } }, + { + "name": "currencyOfMinimalOrMaximumRedemption", + "type": { + "option": "string" + } + }, + { + "name": "hasLockUpForRedemption", + "type": { + "option": "bool" + } + }, { "name": "isValidIsin", "type": { @@ -1470,25 +1482,49 @@ export type Glam = { } }, { - "name": "managementFeeMinimum", + "name": "maximumInitialRedemptionInAmount", + "type": { + "option": "string" + } + }, + { + "name": "maximumInitialRedemptionInShares", "type": { "option": "string" } }, { - "name": "maximalNumberOfPossibleDecimalsAmount", + "name": "minimalInitialRedemptionInAmount", "type": { "option": "string" } }, { - "name": "maximalNumberOfPossibleDecimalsNav", + "name": "minimalInitialRedemptionInShares", "type": { "option": "string" } }, { - "name": "maximalNumberOfPossibleDecimalsShares", + "name": "minimalRedemptionCategory", + "type": { + "option": "string" + } + }, + { + "name": "shareClassDividendType", + "type": { + "option": "string" + } + }, + { + "name": "cusip", + "type": { + "option": "string" + } + }, + { + "name": "valor", "type": { "option": "string" } @@ -2656,10 +2692,13 @@ export type Glam = { "name": "YearlySubscriptionDealingDays" }, { - "name": "FundId" + "name": "CUSIP" + }, + { + "name": "Valor" }, { - "name": "ShareClassCurrencyId" + "name": "FundId" }, { "name": "ImageUri" @@ -4202,15 +4241,15 @@ export const IDL: Glam = { } }, { - "name": "fullShareClassName", + "name": "currencyOfMinimalSubscription", "type": { "option": "string" } }, { - "name": "hasPerformanceFee", + "name": "fullShareClassName", "type": { - "option": "bool" + "option": "string" } }, { @@ -4220,19 +4259,19 @@ export const IDL: Glam = { } }, { - "name": "managementFeeApplied", + "name": "minimalInitialSubscriptionCategory", "type": { "option": "string" } }, { - "name": "managementFeeAppliedReferenceDate", + "name": "minimalInitialSubscriptionInAmount", "type": { "option": "string" } }, { - "name": "managementFeeMaximum", + "name": "minimalInitialSubscriptionInShares", "type": { "option": "string" } @@ -4279,6 +4318,18 @@ export const IDL: Glam = { "option": "string" } }, + { + "name": "currencyOfMinimalOrMaximumRedemption", + "type": { + "option": "string" + } + }, + { + "name": "hasLockUpForRedemption", + "type": { + "option": "bool" + } + }, { "name": "isValidIsin", "type": { @@ -4298,25 +4349,49 @@ export const IDL: Glam = { } }, { - "name": "managementFeeMinimum", + "name": "maximumInitialRedemptionInAmount", + "type": { + "option": "string" + } + }, + { + "name": "maximumInitialRedemptionInShares", "type": { "option": "string" } }, { - "name": "maximalNumberOfPossibleDecimalsAmount", + "name": "minimalInitialRedemptionInAmount", "type": { "option": "string" } }, { - "name": "maximalNumberOfPossibleDecimalsNav", + "name": "minimalInitialRedemptionInShares", "type": { "option": "string" } }, { - "name": "maximalNumberOfPossibleDecimalsShares", + "name": "minimalRedemptionCategory", + "type": { + "option": "string" + } + }, + { + "name": "shareClassDividendType", + "type": { + "option": "string" + } + }, + { + "name": "cusip", + "type": { + "option": "string" + } + }, + { + "name": "valor", "type": { "option": "string" } @@ -5484,10 +5559,13 @@ export const IDL: Glam = { "name": "YearlySubscriptionDealingDays" }, { - "name": "FundId" + "name": "CUSIP" + }, + { + "name": "Valor" }, { - "name": "ShareClassCurrencyId" + "name": "FundId" }, { "name": "ImageUri" diff --git a/api/src/openfunds.ts b/api/src/openfunds.ts index b488a6de..098e49af 100644 --- a/api/src/openfunds.ts +++ b/api/src/openfunds.ts @@ -5,43 +5,85 @@ import * as ExcelJS from "exceljs"; import * as util from "util"; +import * as lodash from "lodash"; import { write, writeToBuffer } from "@fast-csv/format"; import { parseString } from "@fast-csv/parse"; import { validatePubkey } from "./validation"; -const openfundsKeyFromField = (f) => { - const words = f.field.replace("-", "").split(" "); - return [words[0].toLowerCase(), ...words.splice(1)].join(""); -}; - const openfundsGetTemplate = async (template) => { - // https://docs.google.com/spreadsheets/d/1PQFTn1iV90OkZqzdvwaOgTceltGnsEJxuxiCTdo_5Yo/edit#gid=0 const templateUrl = - "https://docs.google.com/spreadsheets/d/e/2PACX-1vSH59SKOZv_mrXjfBUKCqK75sGj-yIXSLOkw4MMMnxMVSCZFodvOTfvTIRrymeMAOG2EBTnG5eN_ImV/pub?output=csv"; + "https://docs.google.com/spreadsheets/d/e/2PACX-1vRieRstFVBGCkV0rVYwNmypV5nMdFtJcqP7TtzIgAC8JA9lXHePI4oXO04aaC7EcLc0f6bt9MW6tJw7/pub?output=csv"; const res = await fetch(templateUrl); - const templateCsv = await new Promise(async (resolve, reject) => { - let templateCsv = []; - parseString(await res.text()) - .on("data", (row) => { - templateCsv.push(row); - }) - .on("end", (rowCount: number) => { - resolve(templateCsv); - }); - }); - const codes = templateCsv[0].slice(1).filter((x) => !!x); - const fields = templateCsv[1].slice(1).filter((x) => !!x); - const tags = templateCsv[2].slice(1).filter((x) => !!x); - const templates = templateCsv[3].slice(1).filter((x) => !!x); - const templateMap = codes.map((code, i) => ({ - code, - field: fields[i], - tag: tags[i], - template: templates[i] - })); - // .filter((obj) => obj.template == "basic"); + const templateCsv: Array> = await new Promise( + async (resolve, reject) => { + let templateCsv = []; + parseString(await res.text()) + .on("data", (row) => { + templateCsv.push(row); + }) + .on("end", (rowCount: number) => { + resolve(templateCsv); + }); + } + ); + + let allowedTags = []; + let glamFields = true; + let filterTemplate = "complete"; + switch (template) { + case "basic": + case "glam-basic": + filterTemplate = "basic"; + break; + case "of-basic": + filterTemplate = "basic"; + glamFields = false; + break; + case "of-complete": + glamFields = false; + break; + case "of-essential": + allowedTags = ["essential"]; + // glamFields = false; + break; + case "of-core": + allowedTags = ["essential", "core"]; + // glamFields = false; + break; + case "of-additional": + allowedTags = ["essential", "core", "additional"]; + // glamFields = false; + break; + } + + const cleanField = (field) => + field + .split(" ") + .map((word) => + [word[0].toUpperCase(), ...word.split("").splice(1)].join("") + ) + .join(" "); + + const templateMap = templateCsv + .slice(1) + .map((row, i) => ({ + code: row[0], + field: cleanField(row[1]), + key: lodash.camelCase(row[1]), + tag: row[2], + template: row[8], + version: Number(row[9]) + })) + .filter((obj) => obj.version === 1) + .filter((obj) => + filterTemplate === "complete" ? true : obj.template === filterTemplate + ) + .filter((obj) => + allowedTags.length === 0 ? true : allowedTags.indexOf(obj.tag) >= 0 + ) + .filter((obj) => (glamFields ? true : obj.tag !== "glam")); // console.log(templateMap); return templateMap; }; @@ -60,11 +102,9 @@ const openfundsApplyCsvTemplate = async (models, template) => { return [ fields.map((f) => f.code), fields.map((f) => f.field), - // fields.map((f) => openfundsKeyFromField(f)), // row with keys, just to debug + // fields.map((f) => f.key), // row with keys, just to debug ...models.flatMap((m) => - openfundsCsvRows(m).map((row) => - fields.map((f) => row[openfundsKeyFromField(f)]) - ) + openfundsCsvRows(m).map((row) => fields.map((f) => row[f.key])) ) ]; }; @@ -100,17 +140,23 @@ export const openfunds = async (funds, template, format, client, res) => { } console.log(util.inspect(models, false, null)); + let actualTemplate = template; + if (template === "auto") { + actualTemplate = models.every((m) => m.shareClasses.length <= 1) + ? "basic" + : "complete"; + } switch (format.toLowerCase()) { case "csv": { - const csv = await openfundsApplyCsvTemplate(models, template); + const csv = await openfundsApplyCsvTemplate(models, actualTemplate); res.setHeader("content-type", "text/csv"); return res.send(await writeToBuffer(csv)); break; } case "xls": case "xlsx": { - const csv = await openfundsApplyCsvTemplate(models, template); + const csv = await openfundsApplyCsvTemplate(models, actualTemplate); const workbook = new ExcelJS.Workbook(); const worksheet = await workbook.csv.read(write(csv)); res.setHeader( diff --git a/package.json b/package.json index 65d7dc76..8a360416 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "exceljs": "^4.4.0", "express": "^4.18.1", "jotai": "2.5.1", + "lodash": "^4.17.21", "react": "18.2.0", "react-dom": "18.2.0", "react-hook-form": "^7.51.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a6fb9b3a..cc1cf5e9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -137,6 +137,9 @@ importers: jotai: specifier: 2.5.1 version: 2.5.1(@types/react@18.3.1)(react@18.2.0) + lodash: + specifier: ^4.17.21 + version: 4.17.21 react: specifier: 18.2.0 version: 18.2.0 From 258ff04ab18be1e3351805647fff86b3a9fbe7f5 Mon Sep 17 00:00:00 2001 From: Emanuele Cesena Date: Sat, 11 May 2024 17:29:43 -0700 Subject: [PATCH 5/9] v0.2: clean model --- anchor/Anchor.toml | 3 +- anchor/src/client.ts | 23 +++- anchor/src/models.ts | 121 ++++++++++++------- anchor/tests/glam_openfunds.spec.ts | 176 ++++++++++++++++++++++++++++ anchor/tests/setup.ts | 8 +- api/src/openfunds.ts | 32 +++-- 6 files changed, 299 insertions(+), 64 deletions(-) create mode 100644 anchor/tests/glam_openfunds.spec.ts diff --git a/anchor/Anchor.toml b/anchor/Anchor.toml index 4ace2780..a0a9e9c6 100644 --- a/anchor/Anchor.toml +++ b/anchor/Anchor.toml @@ -31,7 +31,8 @@ wallet = "~/.config/solana/id.json" #test = "../node_modules/.bin/nx run anchor:jest --verbose --testPathPattern tests/ --testNamePattern glam_crud" #test = "../node_modules/.bin/nx run anchor:jest --verbose --testPathPattern tests/ --testNamePattern glam_investor" #test = "../node_modules/.bin/nx run anchor:jest --verbose --testPathPattern tests/ --testNamePattern glam_drift" -test = "../node_modules/.bin/nx run anchor:jest --verbose --testPathPattern tests/ --testNamePattern glam_staking" +#test = "../node_modules/.bin/nx run anchor:jest --verbose --testPathPattern tests/ --testNamePattern glam_staking" +test = "../node_modules/.bin/nx run anchor:jest --verbose --testPathPattern tests/ --testNamePattern glam_openfunds" #test = "../node_modules/.bin/nx run anchor:jest --verbose --testPathPattern tests/ --testNamePattern devnet" [test] diff --git a/anchor/src/client.ts b/anchor/src/client.ts index 13635a34..476bb789 100644 --- a/anchor/src/client.ts +++ b/anchor/src/client.ts @@ -1,4 +1,5 @@ import * as anchor from "@coral-xyz/anchor"; +import * as util from "util"; import { BN, Program, IdlAccounts, IdlTypes } from "@coral-xyz/anchor"; import { ComputeBudgetProgram, @@ -17,6 +18,7 @@ import { import { Glam, GlamIDL, GlamProgram, getGlamProgramId } from "./glamExports"; import { GlamClientConfig } from "./clientConfig"; import { FundModel, FundOpenfundsModel } from "./models"; +import { kMaxLength } from "buffer"; type FundAccount = IdlAccounts["fundAccount"]; type FundMetadataAccount = IdlAccounts["fundMetadataAccount"]; @@ -171,6 +173,7 @@ export class GlamClient { } const sharePDA = this.getShareClassPDA(fundPDA, i); + shareClass.uri = `https://api.glam.systems/metadata/${sharePDA}`; shareClass.imageUri = `https://api.glam.systems/image/${sharePDA}.png`; }); @@ -180,13 +183,17 @@ export class GlamClient { public async createFund( fund: any ): Promise<[TransactionSignature, PublicKey]> { - const fundModel = this.enrichFundModelInitialize(fund); + let fundModel = this.enrichFundModelInitialize(fund); const fundPDA = this.getFundPDA(fundModel); const treasury = this.getTreasuryPDA(fundPDA); // const share = this.getShareClassPDA(fundPDA, 0); const openfunds = this.getOpenfundsPDA(fundPDA); const manager = this.getManager(); + const shareClasses = fundModel.shareClasses; + fundModel.shareClasses = []; + console.log(util.inspect(shareClasses, false, null)); + //TODO: add instructions to "addShareClass" in the same tx const txSig = await this.program.methods .initialize(fundModel) @@ -196,10 +203,18 @@ export class GlamClient { openfunds, manager }) - .preInstructions([ - ComputeBudgetProgram.setComputeUnitLimit({ units: 500_000 }) - ]) .rpc(); + // shareClasses.forEach(async (shareClass, j) => { + // await this.program.methods + // .addShareClass(shareClass) + // .accounts({ + // fund: fundPDA, + // treasury, + // openfunds, + // manager + // }) + // .rpc(); + // }); return [txSig, fundPDA]; } diff --git a/anchor/src/models.ts b/anchor/src/models.ts index e438afee..26211bbc 100644 --- a/anchor/src/models.ts +++ b/anchor/src/models.ts @@ -4,15 +4,20 @@ import { Glam } from "./glamExports"; export type FundModel = IdlTypes["FundModel"]; export const FundModel = class { constructor(obj: any) { - let result: IdlTypes["FundModel"] = { + let partial: any = { id: null, name: null, uri: null, - openfundUri: null, + openfundsUri: null, isEnabled: null, created: null, - isRawOpenfunds: null, - ...obj, + isRawOpenfunds: null + }; + for (const key in partial) { + partial[key] = obj[key] || null; + } + let result: IdlTypes["FundModel"] = { + ...partial, assets: obj.assets || [], assetsWeights: obj.assetsWeights || [], shareClasses: obj.shareClasses @@ -21,18 +26,15 @@ export const FundModel = class { new ShareClassModel(shareClass) as ShareClassModel ) : [], - // company: obj.company - // ? (new CompanyModel(obj.company) as CompanyModel) - // : null, - // manager: obj.manager - // ? (new ManagerModel(obj.manager) as ManagerModel) - // : null, - // rawOpenfunds: obj.fundDomicileAlpha2 - // ? (new FundOpenfundsModel(obj) as FundOpenfundsModel) - // : null - company: new CompanyModel(obj.company || {}), - manager: new ManagerModel(obj.manager || {}), - rawOpenfunds: new FundOpenfundsModel(obj) + company: obj.company + ? (new CompanyModel(obj.company) as CompanyModel) + : null, + manager: obj.manager + ? (new ManagerModel(obj.manager) as ManagerModel) + : null, + // company: new CompanyModel(obj.company) as CompanyModel, + // manager: new ManagerModel(obj.manager) as ManagerModel, + rawOpenfunds: new FundOpenfundsModel(obj) as FundOpenfundsModel }; return result; } @@ -41,7 +43,7 @@ export const FundModel = class { export type FundOpenfundsModel = IdlTypes["FundOpenfundsModel"]; export const FundOpenfundsModel = class { constructor(obj: any) { - const result: IdlTypes["FundOpenfundsModel"] = { + let result: IdlTypes["FundOpenfundsModel"] = { fundDomicileAlpha2: null, legalFundNameIncludingUmbrella: null, fiscalYearEnd: null, @@ -57,9 +59,11 @@ export const FundOpenfundsModel = class { legalFundNameOnly: null, openEndedOrClosedEndedFundStructure: null, typeOfEuDirective: null, - ucitsVersion: null, - ...obj + ucitsVersion: null }; + for (const key in result) { + result[key] = obj[key] || null; + } return result; } }; @@ -67,7 +71,19 @@ export const FundOpenfundsModel = class { export type ShareClassModel = IdlTypes["ShareClassModel"]; export const ShareClassModel = class { constructor(obj: any) { - const result: IdlTypes["ShareClassModel"] = { + let partial: any = { + symbol: null, + name: null, + uri: null, + fundId: null, + asset: null, + imageUri: null, + isRawOpenfunds: null + }; + for (const key in partial) { + partial[key] = obj[key] || null; + } + let result: IdlTypes["ShareClassModel"] = { symbol: null, name: null, uri: null, @@ -75,7 +91,7 @@ export const ShareClassModel = class { asset: null, imageUri: null, isRawOpenfunds: null, - ...obj, + ...partial, rawOpenfunds: obj.shareClassCurrency ? (new ShareClassOpenfundsModel(obj) as ShareClassOpenfundsModel) : null @@ -88,27 +104,27 @@ export type ShareClassOpenfundsModel = IdlTypes["ShareClassOpenfundsModel"]; export const ShareClassOpenfundsModel = class { constructor(obj: any) { - const result: IdlTypes["ShareClassOpenfundsModel"] = { + let result: IdlTypes["ShareClassOpenfundsModel"] = { isin: null, shareClassCurrency: null, - appliedSubscriptionFeeInFavourOfDistributor: null, - appliedSubscriptionFeeInFavourOfDistributorReferenceDate: null, + // appliedSubscriptionFeeInFavourOfDistributor: null, + // appliedSubscriptionFeeInFavourOfDistributorReferenceDate: null, currencyOfMinimalSubscription: null, fullShareClassName: null, - hasPerformanceFee: null, - hasSubscriptionFeeInFavourOfDistributor: null, + // hasPerformanceFee: null, + // hasSubscriptionFeeInFavourOfDistributor: null, investmentStatus: null, - managementFeeApplied: null, - managementFeeAppliedReferenceDate: null, - managementFeeMaximum: null, - maximumSubscriptionFeeInFavourOfDistributor: null, + // managementFeeApplied: null, + // managementFeeAppliedReferenceDate: null, + // managementFeeMaximum: null, + // maximumSubscriptionFeeInFavourOfDistributor: null, minimalInitialSubscriptionCategory: null, minimalInitialSubscriptionInAmount: null, minimalInitialSubscriptionInShares: null, - minimalSubsequentSubscriptionCategory: null, - minimalSubsequentSubscriptionInAmount: null, - minimalSubsequentSubscriptionInShares: null, - minimumSubscriptionFeeInFavourOfDistributor: null, + // minimalSubsequentSubscriptionCategory: null, + // minimalSubsequentSubscriptionInAmount: null, + // minimalSubsequentSubscriptionInShares: null, + // minimumSubscriptionFeeInFavourOfDistributor: null, shareClassDistributionPolicy: null, shareClassExtension: null, shareClassLaunchDate: null, @@ -116,16 +132,31 @@ export const ShareClassOpenfundsModel = class { launchPrice: null, launchPriceCurrency: null, launchPriceDate: null, - hasAppliedSubscriptionFeeInFavourOfFund: null, - appliedSubscriptionFeeInFavourOfFund: null, - appliedSubscriptionFeeInFavourOfFundReferenceDate: null, - maximumSubscriptionFeeInFavourOfFund: null, - hasAppliedRedemptionFeeInFavourOfFund: null, - appliedRedemptionFeeInFavourOfFund: null, - appliedRedemptionFeeInFavourOfFundReferenceDate: null, - maximumRedemptionFeeInFavourOfFund: null, - ...obj + // hasAppliedSubscriptionFeeInFavourOfFund: null, + // appliedSubscriptionFeeInFavourOfFund: null, + // appliedSubscriptionFeeInFavourOfFundReferenceDate: null, + // maximumSubscriptionFeeInFavourOfFund: null, + // hasAppliedRedemptionFeeInFavourOfFund: null, + // appliedRedemptionFeeInFavourOfFund: null, + // appliedRedemptionFeeInFavourOfFundReferenceDate: null, + // maximumRedemptionFeeInFavourOfFund: null, + currencyOfMinimalOrMaximumRedemption: null, + hasLockUpForRedemption: null, + isValidIsin: null, + lockUpComment: null, + lockUpPeriodInDays: null, + maximumInitialRedemptionInAmount: null, + maximumInitialRedemptionInShares: null, + minimalInitialRedemptionInAmount: null, + minimalInitialRedemptionInShares: null, + minimalRedemptionCategory: null, + shareClassDividendType: null, + cusip: null, + valor: null }; + for (const key in result) { + result[key] = obj[key] || null; + } return result; } }; @@ -133,7 +164,7 @@ export const ShareClassOpenfundsModel = class { export type CompanyModel = IdlTypes["CompanyModel"]; export const CompanyModel = class { constructor(obj: any) { - const result: IdlTypes["CompanyModel"] = { + let result: IdlTypes["CompanyModel"] = { // alias name = fundGroupName fundGroupName: obj.fundGroupName || obj.name || null, // alias email = emailAddressOfManCo @@ -150,7 +181,7 @@ export const CompanyModel = class { export type ManagerModel = IdlTypes["ManagerModel"]; export const ManagerModel = class { constructor(obj: any) { - const result: IdlTypes["ManagerModel"] = { + let result: IdlTypes["ManagerModel"] = { // alias name = portfolioManagerName portfolioManagerName: obj.portfolioManagerName || obj.name || null, pubkey: obj.pubkey || null, diff --git a/anchor/tests/glam_openfunds.spec.ts b/anchor/tests/glam_openfunds.spec.ts new file mode 100644 index 00000000..33ba77f3 --- /dev/null +++ b/anchor/tests/glam_openfunds.spec.ts @@ -0,0 +1,176 @@ +import * as anchor from "@coral-xyz/anchor"; +import { PublicKey } from "@solana/web3.js"; +import { GlamClient } from "../src"; + +describe("glam_openfunds", () => { + const client = new GlamClient(); + + const manager = client.getManager(); + + const usdc = new PublicKey("8zGuJQqwhZafTah7Uc7Z4tXRnguqkn5KLFAP8oV6PHe2"); + const wsol = new PublicKey("So11111111111111111111111111111111111111112"); + const btc = new PublicKey("3BZPwbcqB5kKScF3TEXxwNfx5ipV13kbRVDvfVp5c6fv"); + + // fund1: 1 share class + implicit fields + const fund1 = { + shareClasses: [ + { + // Glam Token + name: "Glam Investment Fund BTC-SOL", + symbol: "GBS", + asset: usdc, + // Glam + permanentDelegate: manager, + lockUpTime: 40 * 24 * 60 * 60, + requiresMemoOnTransfer: true, + // Openfunds Share Class + fullShareClassName: null, // auto + isin: "XS1082172823", + cusip: "demo", + valor: "demo", + shareClassCurrency: "USDC", + shareClassLifecycle: "active", + investmentStatus: "open", + shareClassDistributionPolicy: "accumulating", + shareClassLaunchDate: null, // auto + minimalInitialSubscriptionCategory: "amount", + minimalInitialSubscriptionInShares: 0, + minimalInitialSubscriptionInAmount: 1_000, + currencyOfMinimalSubscription: "USDC", + minimalRedemptionCategory: "shares", + minimalInitialRedemptionInShares: 1, + maximumInitialRedemptionInShares: 1_000, + minimalInitialRedemptionInAmount: 0, + maximumInitialRedemptionInAmount: null, + currencyOfMinimalOrMaximumRedemption: "USDC", + shareClassDividendType: "both", + srri: 4, + hasLockUpForRedemption: true, //TODO: auto + lockUpComment: "demo", + lockUpPeriodInDays: 40, //TODO: auto + launchPrice: null, // auto + launchPriceCurrency: null, // auto + launchPriceDate: null // auto + } + ], + // Glam + isEnabled: true, + assets: [usdc, btc, wsol], + assetsWeights: [0, 60, 40], + // Openfunds (Fund) + fundDomicileAlpha2: "XS", + legalFundNameIncludingUmbrella: null, // auto + fundLaunchDate: null, // auto + investmentObjective: "demo", + fundCurrency: "USDC", + openEndedOrClosedEndedFundStructure: "open-ended fund", + fiscalYearEnd: "12-31", + legalForm: "other", + // Openfunds Company (simplified) + company: { + name: "Glam Systems", + email: "hello@glam.systems", + website: "https://glam.systems", + manCo: "Glam Management", + domicileOfManCo: "CH" + }, + // Openfunds Manager (simplified) + manager: { + name: "0x0ece.sol" + } + }; + + // fund1b: 1 share class, all fields explicit + const fund1b = { + shareClasses: [ + { + // Glam Token + name: "Glam Investment Fund BTC-SOL", + symbol: "GBS", + asset: usdc, + // Glam + permanentDelegate: manager, + lockUpTime: 40 * 24 * 60 * 60, + requiresMemoOnTransfer: true, + // Openfunds Share Class + fullShareClassName: "Glam Investment Fund BTC-SOL", + isin: "XS1082172823", + cusip: "demo", + valor: "demo", + shareClassCurrency: "USDC", + shareClassLifecycle: "active", + investmentStatus: "open", + shareClassDistributionPolicy: "accumulating", + shareClassLaunchDate: new Date().toISOString().split("T")[0], + minimalInitialSubscriptionCategory: "amount", + minimalInitialSubscriptionInShares: "0", + minimalInitialSubscriptionInAmount: "1000", + currencyOfMinimalSubscription: "USDC", + minimalRedemptionCategory: "shares", + minimalInitialRedemptionInShares: "1", + maximumInitialRedemptionInShares: "1000", + minimalInitialRedemptionInAmount: "0", + maximumInitialRedemptionInAmount: null, + currencyOfMinimalOrMaximumRedemption: "USDC", + shareClassDividendType: "both", + srri: "4", + hasLockUpForRedemption: true, + lockUpComment: "demo", + lockUpPeriodInDays: "40", + launchPrice: "100", + launchPriceCurrency: "USD", + launchPriceDate: new Date().toISOString().split("T")[0] + } + ], + // Glam + isEnabled: true, + assets: [usdc, btc, wsol], + assetsWeights: [0, 60, 40], + // Openfunds (Fund) + fundDomicileAlpha2: "XS", + legalFundNameIncludingUmbrella: "Glam Investment Fund BTC-SOL (b)", + fundLaunchDate: new Date().toISOString().split("T")[0], + investmentObjective: "demo", + fundCurrency: "USDC", + openEndedOrClosedEndedFundStructure: "open-ended fund", + fiscalYearEnd: "12-31", + legalForm: "other", + // Openfunds Company (simplified) + company: { + fundGroupName: "Glam Systems", + manCo: "Glam Management", + domicileOfManCo: "CH", + emailAddressOfManCo: "hello@glam.systems", + fundWebsiteOfManCo: "https://glam.systems" + }, + // Openfunds Manager (simplified) + manager: { + portfolioManagerName: "0x0ece.sol" + } + }; + + // fund2: 2 share classes + //TODO + + // it("Initialize fund with 1 share class", async () => { + // let txId, fundPDA; + // try { + // [txId, fundPDA] = await client.createFund(fund1); + // console.log(`Fund ${fundPDA} initialized, txId: ${txId}`); + // } catch (e) { + // console.error(e); + // throw e; + // } + // }); + + it("Initialize fund with 1 share class (b)", async () => { + let txId, fundPDA; + try { + [txId, fundPDA] = await client.createFund(fund1b); + console.log(`Fund ${fundPDA} initialized, txId: ${txId}`); + } catch (e) { + console.error(e); + throw e; + } + }); +}); diff --git a/anchor/tests/setup.ts b/anchor/tests/setup.ts index e803dc7d..461e23ed 100644 --- a/anchor/tests/setup.ts +++ b/anchor/tests/setup.ts @@ -30,7 +30,7 @@ export const createFundForTest = async ( symbol: "GBS", name: "Glam Investment Fund BTC-SOL", asset: usdc, - // Openfund Share Class + // Openfunds Share Class isin: "XS1082172823", shareClassCurrency: "USDC", fullShareClassName: null, // auto @@ -62,7 +62,7 @@ export const createFundForTest = async ( isEnabled: true, assets: [usdc, btc, eth], assetsWeights: [0, 60, 40], - // Openfund (Fund) + // Openfunds (Fund) fundDomicileAlpha2: "XS", legalFundNameIncludingUmbrella: null, // auto fiscalYearEnd: "12-31", @@ -75,13 +75,13 @@ export const createFundForTest = async ( isPassiveFund: true, //TODO: auto legalForm: "other", openEndedOrClosedEndedFundStructure: "open-ended fund", //TODO: auto - // Openfund Company (simplified) + // Openfunds Company (simplified) company: { name: "Glam Systems", email: "hello@glam.systems", website: "https://glam.systems" }, - // Openfund Manager (simplified) + // Openfunds Manager (simplified) manager: { name: "0x0ece.sol" } diff --git a/api/src/openfunds.ts b/api/src/openfunds.ts index 098e49af..7886505b 100644 --- a/api/src/openfunds.ts +++ b/api/src/openfunds.ts @@ -6,6 +6,7 @@ import * as ExcelJS from "exceljs"; import * as util from "util"; import * as lodash from "lodash"; +import { ShareClassModel } from "@glam/anchor"; import { write, writeToBuffer } from "@fast-csv/format"; import { parseString } from "@fast-csv/parse"; @@ -58,13 +59,20 @@ const openfundsGetTemplate = async (template) => { break; } - const cleanField = (field) => - field - .split(" ") - .map((word) => - [word[0].toUpperCase(), ...word.split("").splice(1)].join("") - ) - .join(" "); + const cleanField = (field) => { + let val = ""; + try { + val = field + .split(" ") + .map((word) => + [word[0].toUpperCase(), ...word.split("").splice(1)].join("") + ) + .join(" "); + } catch (e) { + console.log("field", field); + } + return val; + }; const templateMap = templateCsv .slice(1) @@ -73,8 +81,8 @@ const openfundsGetTemplate = async (template) => { field: cleanField(row[1]), key: lodash.camelCase(row[1]), tag: row[2], - template: row[8], - version: Number(row[9]) + template: row[4], + version: Number(row[5]) })) .filter((obj) => obj.version === 1) .filter((obj) => @@ -89,7 +97,11 @@ const openfundsGetTemplate = async (template) => { }; const openfundsCsvRows = (model) => { - return model.shareClasses.map((shareClass) => ({ + return ( + model.shareClasses.length > 0 + ? model.shareClasses + : [new ShareClassModel({})] + ).map((shareClass) => ({ ...model, ...model.company, ...model.fundManagers[0], From 79143af08ea373d7e4b98c3dd16e0fbae46ade49 Mon Sep 17 00:00:00 2001 From: Emanuele Cesena Date: Sat, 11 May 2024 19:13:29 -0700 Subject: [PATCH 6/9] v0.2: add share classes --- anchor/Anchor.toml | 4 +- anchor/Cargo.lock | 23 +++ anchor/Cargo.toml | 1 + anchor/programs/glam/Cargo.toml | 1 + .../programs/glam/src/instructions/manager.rs | 170 ++++-------------- anchor/programs/glam/src/lib.rs | 2 +- .../glam/src/state/model/openfunds.rs | 38 ++-- .../glam/src/state/openfunds/share_class.rs | 2 +- anchor/src/client.ts | 36 ++-- anchor/src/models.ts | 47 ++--- anchor/target/idl/glam.json | 7 +- anchor/target/types/glam.ts | 14 +- anchor/tests/setup.ts | 148 +++++---------- 13 files changed, 184 insertions(+), 309 deletions(-) diff --git a/anchor/Anchor.toml b/anchor/Anchor.toml index a0a9e9c6..97afc995 100644 --- a/anchor/Anchor.toml +++ b/anchor/Anchor.toml @@ -31,8 +31,8 @@ wallet = "~/.config/solana/id.json" #test = "../node_modules/.bin/nx run anchor:jest --verbose --testPathPattern tests/ --testNamePattern glam_crud" #test = "../node_modules/.bin/nx run anchor:jest --verbose --testPathPattern tests/ --testNamePattern glam_investor" #test = "../node_modules/.bin/nx run anchor:jest --verbose --testPathPattern tests/ --testNamePattern glam_drift" -#test = "../node_modules/.bin/nx run anchor:jest --verbose --testPathPattern tests/ --testNamePattern glam_staking" -test = "../node_modules/.bin/nx run anchor:jest --verbose --testPathPattern tests/ --testNamePattern glam_openfunds" +test = "../node_modules/.bin/nx run anchor:jest --verbose --testPathPattern tests/ --testNamePattern glam_staking" +#test = "../node_modules/.bin/nx run anchor:jest --verbose --testPathPattern tests/ --testNamePattern glam_openfunds" #test = "../node_modules/.bin/nx run anchor:jest --verbose --testPathPattern tests/ --testNamePattern devnet" [test] diff --git a/anchor/Cargo.lock b/anchor/Cargo.lock index 3a995c7b..54f85730 100644 --- a/anchor/Cargo.lock +++ b/anchor/Cargo.lock @@ -1115,6 +1115,7 @@ dependencies = [ "spl-token-2022 3.0.2", "spl-token-metadata-interface 0.3.3", "spl-transfer-hook-interface 0.6.3", + "strum", ] [[package]] @@ -2696,6 +2697,28 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.59", +] + [[package]] name = "subtle" version = "2.4.1" diff --git a/anchor/Cargo.toml b/anchor/Cargo.toml index 85bb2bf6..c425662e 100644 --- a/anchor/Cargo.toml +++ b/anchor/Cargo.toml @@ -23,6 +23,7 @@ spl-associated-token-account = "3.0.2" spl-token-metadata-interface = "0.3.3" spl-transfer-hook-interface = "0.6.3" pyth-sdk-solana = "0.10.1" +strum = { version = "0.26", features = ["derive"] } drift = { path = "./deps/drift" } marinade = { path = "./deps/marinade" } diff --git a/anchor/programs/glam/Cargo.toml b/anchor/programs/glam/Cargo.toml index d17893cc..36315969 100644 --- a/anchor/programs/glam/Cargo.toml +++ b/anchor/programs/glam/Cargo.toml @@ -28,6 +28,7 @@ spl-associated-token-account = { workspace = true } spl-token-metadata-interface = { workspace = true } spl-transfer-hook-interface = { workspace = true } pyth-sdk-solana = { workspace = true } +strum = { workspace = true } drift = { workspace = true } marinade = { workspace = true } diff --git a/anchor/programs/glam/src/instructions/manager.rs b/anchor/programs/glam/src/instructions/manager.rs index 0e89d9fe..80ab98f1 100644 --- a/anchor/programs/glam/src/instructions/manager.rs +++ b/anchor/programs/glam/src/instructions/manager.rs @@ -101,14 +101,13 @@ pub fn initialize_fund_handler<'c: 'info, 'info>( } #[derive(Accounts)] -#[instruction(share_class_metadata: ShareClassMetadata)] pub struct AddShareClass<'info> { /// CHECK: we'll create the account later on with metadata #[account( mut, seeds = [ b"share".as_ref(), - share_class_metadata.symbol.as_ref(), + &[fund.share_classes.len() as u8], fund.key().as_ref() ], bump @@ -118,6 +117,9 @@ pub struct AddShareClass<'info> { #[account(mut, has_one = manager @ ManagerError::NotAuthorizedError)] pub fund: Account<'info, FundAccount>, + #[account(mut)] + pub openfunds: Box>, + #[account(mut)] pub manager: Signer<'info>, @@ -127,11 +129,23 @@ pub struct AddShareClass<'info> { pub fn add_share_class_handler<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, AddShareClass<'info>>, - share_class_metadata: ShareClassMetadata, + share_class_metadata: ShareClassModel, ) -> Result<()> { + // + // Add share class to fund + // let fund = &mut ctx.accounts.fund; + let fund_key = fund.key(); + let share_class_idx = fund.share_classes.len() as u8; fund.share_classes.push(ctx.accounts.share_class_mint.key()); - // fund.share_classes_bumps.push(ctx.bumps.share_class_mint); + + let openfunds_metadata = Vec::::from(&share_class_metadata); + // + // Add share class to openfunds + // + let openfunds = &mut ctx.accounts.openfunds; + openfunds.share_classes.push(openfunds_metadata.clone()); + // // Initialize share class mint and metadata // @@ -140,10 +154,9 @@ pub fn add_share_class_handler<'c: 'info, 'info>( let share_mint_authority = ctx.accounts.share_class_mint.to_account_info(); let share_metadata_authority = ctx.accounts.share_class_mint.to_account_info(); - let fund_key = ctx.accounts.fund.key(); let seeds = &[ "share".as_bytes(), - share_class_metadata.symbol.as_ref(), + &[share_class_idx], fund_key.as_ref(), &[ctx.bumps.share_class_mint], ]; @@ -152,7 +165,7 @@ pub fn add_share_class_handler<'c: 'info, 'info>( let space = ExtensionType::try_calculate_account_len::(&[ExtensionType::MetadataPointer]) .unwrap(); - let metadata_space = ShareClassMetadata::INIT_SIZE; + let metadata_space = 2048; let lamports_required = (Rent::get()?).minimum_balance(space + metadata_space); msg!( @@ -204,9 +217,9 @@ pub fn add_share_class_handler<'c: 'info, 'info>( &share_metadata_authority.key(), &share_mint.key(), &share_mint_authority.key(), - share_class_metadata.name.clone(), - share_class_metadata.symbol.clone(), - share_class_metadata.uri.clone(), + share_class_metadata.name.unwrap(), + share_class_metadata.symbol.unwrap(), + share_class_metadata.uri.unwrap(), ); solana_program::program::invoke_signed( &init_token_metadata_ix, @@ -227,133 +240,26 @@ pub fn add_share_class_handler<'c: 'info, 'info>( &spl_token_2022::id(), &share_metadata.key(), &share_metadata_authority.key(), - spl_token_metadata_interface::state::Field::Key("fund_id".to_string()), + spl_token_metadata_interface::state::Field::Key("FundId".to_string()), fund_key.to_string(), ), &[share_mint.clone(), share_mint_authority.clone()], signer_seeds, )?; - solana_program::program::invoke_signed( - &spl_token_metadata_interface::instruction::update_field( - &spl_token_2022::id(), - &share_metadata.key(), - &share_metadata_authority.key(), - spl_token_metadata_interface::state::Field::Key("share_class_asset".to_string()), - share_class_metadata.share_class_asset, - ), - &[share_mint.clone(), share_mint_authority.clone()], - signer_seeds, - )?; - solana_program::program::invoke_signed( - &spl_token_metadata_interface::instruction::update_field( - &spl_token_2022::id(), - &share_metadata.key(), - &share_metadata_authority.key(), - spl_token_metadata_interface::state::Field::Key("share_class_asset_id".to_string()), - share_class_metadata.share_class_asset_id.to_string(), - ), - &[share_mint.clone(), share_mint_authority.clone()], - signer_seeds, - )?; - solana_program::program::invoke_signed( - &spl_token_metadata_interface::instruction::update_field( - &spl_token_2022::id(), - &share_metadata.key(), - &share_metadata_authority.key(), - spl_token_metadata_interface::state::Field::Key("isin".to_string()), - share_class_metadata.isin, - ), - &[share_mint.clone(), share_mint_authority.clone()], - signer_seeds, - )?; - solana_program::program::invoke_signed( - &spl_token_metadata_interface::instruction::update_field( - &spl_token_2022::id(), - &share_metadata.key(), - &share_metadata_authority.key(), - spl_token_metadata_interface::state::Field::Key("status".to_string()), - share_class_metadata.status, - ), - &[share_mint.clone(), share_mint_authority.clone()], - signer_seeds, - )?; - solana_program::program::invoke_signed( - &spl_token_metadata_interface::instruction::update_field( - &spl_token_2022::id(), - &share_metadata.key(), - &share_metadata_authority.key(), - spl_token_metadata_interface::state::Field::Key("fee_management".to_string()), - share_class_metadata.fee_management.to_string(), - ), - &[share_mint.clone(), share_mint_authority.clone()], - signer_seeds, - )?; - solana_program::program::invoke_signed( - &spl_token_metadata_interface::instruction::update_field( - &spl_token_2022::id(), - &share_metadata.key(), - &share_metadata_authority.key(), - spl_token_metadata_interface::state::Field::Key("fee_performance".to_string()), - share_class_metadata.fee_performance.to_string(), - ), - &[share_mint.clone(), share_mint_authority.clone()], - signer_seeds, - )?; - solana_program::program::invoke_signed( - &spl_token_metadata_interface::instruction::update_field( - &spl_token_2022::id(), - &share_metadata.key(), - &share_metadata_authority.key(), - spl_token_metadata_interface::state::Field::Key("policy_distribution".to_string()), - share_class_metadata.policy_distribution, - ), - &[share_mint.clone(), share_mint_authority.clone()], - signer_seeds, - )?; - solana_program::program::invoke_signed( - &spl_token_metadata_interface::instruction::update_field( - &spl_token_2022::id(), - &share_metadata.key(), - &share_metadata_authority.key(), - spl_token_metadata_interface::state::Field::Key("extension".to_string()), - share_class_metadata.extension, - ), - &[share_mint.clone(), share_mint_authority.clone()], - signer_seeds, - )?; - solana_program::program::invoke_signed( - &spl_token_metadata_interface::instruction::update_field( - &spl_token_2022::id(), - &share_metadata.key(), - &share_metadata_authority.key(), - spl_token_metadata_interface::state::Field::Key("launch_date".to_string()), - share_class_metadata.launch_date, - ), - &[share_mint.clone(), share_mint_authority.clone()], - signer_seeds, - )?; - solana_program::program::invoke_signed( - &spl_token_metadata_interface::instruction::update_field( - &spl_token_2022::id(), - &share_metadata.key(), - &share_metadata_authority.key(), - spl_token_metadata_interface::state::Field::Key("lifecycle".to_string()), - share_class_metadata.lifecycle, - ), - &[share_mint.clone(), share_mint_authority.clone()], - signer_seeds, - )?; - solana_program::program::invoke_signed( - &spl_token_metadata_interface::instruction::update_field( - &spl_token_2022::id(), - &share_metadata.key(), - &share_metadata_authority.key(), - spl_token_metadata_interface::state::Field::Key("image_uri".to_string()), - share_class_metadata.image_uri, - ), - &[share_mint.clone(), share_mint_authority.clone()], - signer_seeds, - )?; + let _ = openfunds_metadata.iter().take(10).try_for_each(|field| { + solana_program::program::invoke_signed( + &spl_token_metadata_interface::instruction::update_field( + &spl_token_2022::id(), + &share_metadata.key(), + &share_metadata_authority.key(), + spl_token_metadata_interface::state::Field::Key(field.name.to_string()), + field.clone().value, + ), + &[share_mint.clone(), share_mint_authority.clone()], + signer_seeds, + ) + }); + Ok(()) } diff --git a/anchor/programs/glam/src/lib.rs b/anchor/programs/glam/src/lib.rs index 7a69af5d..da281aad 100644 --- a/anchor/programs/glam/src/lib.rs +++ b/anchor/programs/glam/src/lib.rs @@ -28,7 +28,7 @@ pub mod glam { pub fn add_share_class<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, AddShareClass<'info>>, - share_class_metadata: ShareClassMetadata, + share_class_metadata: ShareClassModel, ) -> Result<()> { manager::add_share_class_handler(ctx, share_class_metadata) } diff --git a/anchor/programs/glam/src/state/model/openfunds.rs b/anchor/programs/glam/src/state/model/openfunds.rs index ea47450e..2e200007 100644 --- a/anchor/programs/glam/src/state/model/openfunds.rs +++ b/anchor/programs/glam/src/state/model/openfunds.rs @@ -75,6 +75,25 @@ impl From for Vec { impl From<&ShareClassModel> for Vec { fn from(model: &ShareClassModel) -> Self { let mut res = vec![]; + // Derived fields + let is_raw_openfunds = model.is_raw_openfunds.unwrap_or(false); + let model = model.clone(); + if !is_raw_openfunds { + //TODO + let v: Vec<(Option, ShareClassFieldName)> = vec![ + (pubkey2string(model.fund_id), ShareClassFieldName::FundId), + (model.image_uri, ShareClassFieldName::ImageUri), + ]; + v.iter().for_each(|(value, field)| { + if let Some(value) = value { + let value = value.to_string(); + res.push(ShareClassField { + name: field.clone(), + value: value.clone(), + }) + } + }); + } // Raw Openfund fields if let Some(model) = model.raw_openfunds.clone() { //TODO @@ -372,25 +391,6 @@ impl From<&ShareClassModel> for Vec { } }); } - // Derived fields - let is_raw_openfunds = model.is_raw_openfunds.unwrap_or(false); - let model = model.clone(); - if !is_raw_openfunds { - //TODO - let v: Vec<(Option, ShareClassFieldName)> = vec![ - (pubkey2string(model.fund_id), ShareClassFieldName::FundId), - (model.image_uri, ShareClassFieldName::ImageUri), - ]; - v.iter().for_each(|(value, field)| { - if let Some(value) = value { - let value = value.to_string(); - res.push(ShareClassField { - name: field.clone(), - value: value.clone(), - }) - } - }); - } res } } diff --git a/anchor/programs/glam/src/state/openfunds/share_class.rs b/anchor/programs/glam/src/state/openfunds/share_class.rs index 0c26c03d..b45cf8d1 100644 --- a/anchor/programs/glam/src/state/openfunds/share_class.rs +++ b/anchor/programs/glam/src/state/openfunds/share_class.rs @@ -2,7 +2,7 @@ use anchor_lang::prelude::*; // Openfunds v2.0 Share Class -#[derive(AnchorDeserialize, AnchorSerialize, Clone, Debug)] +#[derive(AnchorDeserialize, AnchorSerialize, Clone, Debug, strum::Display)] pub enum ShareClassFieldName { // Essential ISIN, // impl diff --git a/anchor/src/client.ts b/anchor/src/client.ts index 476bb789..f5bf8668 100644 --- a/anchor/src/client.ts +++ b/anchor/src/client.ts @@ -1,5 +1,5 @@ import * as anchor from "@coral-xyz/anchor"; -import * as util from "util"; +// import * as util from "util"; import { BN, Program, IdlAccounts, IdlTypes } from "@coral-xyz/anchor"; import { ComputeBudgetProgram, @@ -186,15 +186,14 @@ export class GlamClient { let fundModel = this.enrichFundModelInitialize(fund); const fundPDA = this.getFundPDA(fundModel); const treasury = this.getTreasuryPDA(fundPDA); - // const share = this.getShareClassPDA(fundPDA, 0); const openfunds = this.getOpenfundsPDA(fundPDA); const manager = this.getManager(); const shareClasses = fundModel.shareClasses; fundModel.shareClasses = []; - console.log(util.inspect(shareClasses, false, null)); - //TODO: add instructions to "addShareClass" in the same tx + console.log(shareClasses); + const txSig = await this.program.methods .initialize(fundModel) .accounts({ @@ -204,17 +203,24 @@ export class GlamClient { manager }) .rpc(); - // shareClasses.forEach(async (shareClass, j) => { - // await this.program.methods - // .addShareClass(shareClass) - // .accounts({ - // fund: fundPDA, - // treasury, - // openfunds, - // manager - // }) - // .rpc(); - // }); + await Promise.all( + shareClasses.map(async (shareClass, j) => { + const shareClassMint = this.getShareClassPDA(fundPDA, j); + return await this.program.methods + .addShareClass(shareClass) + .accounts({ + fund: fundPDA, + shareClassMint, + openfunds, + manager, + tokenProgram: TOKEN_2022_PROGRAM_ID + }) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ units: 500_000 }) + ]) + .rpc(); + }) + ); return [txSig, fundPDA]; } diff --git a/anchor/src/models.ts b/anchor/src/models.ts index 26211bbc..c3418325 100644 --- a/anchor/src/models.ts +++ b/anchor/src/models.ts @@ -1,5 +1,6 @@ import { IdlTypes } from "@coral-xyz/anchor"; import { Glam } from "./glamExports"; +import { setDefaultResultOrder } from "dns/promises"; export type FundModel = IdlTypes["FundModel"]; export const FundModel = class { @@ -32,8 +33,6 @@ export const FundModel = class { manager: obj.manager ? (new ManagerModel(obj.manager) as ManagerModel) : null, - // company: new CompanyModel(obj.company) as CompanyModel, - // manager: new ManagerModel(obj.manager) as ManagerModel, rawOpenfunds: new FundOpenfundsModel(obj) as FundOpenfundsModel }; return result; @@ -43,7 +42,7 @@ export const FundModel = class { export type FundOpenfundsModel = IdlTypes["FundOpenfundsModel"]; export const FundOpenfundsModel = class { constructor(obj: any) { - let result: IdlTypes["FundOpenfundsModel"] = { + let partial: any = { fundDomicileAlpha2: null, legalFundNameIncludingUmbrella: null, fiscalYearEnd: null, @@ -61,9 +60,12 @@ export const FundOpenfundsModel = class { typeOfEuDirective: null, ucitsVersion: null }; - for (const key in result) { - result[key] = obj[key] || null; + for (const key in partial) { + partial[key] = obj[key] || null; } + let result: IdlTypes["FundOpenfundsModel"] = { + ...partial + }; return result; } }; @@ -92,9 +94,9 @@ export const ShareClassModel = class { imageUri: null, isRawOpenfunds: null, ...partial, - rawOpenfunds: obj.shareClassCurrency - ? (new ShareClassOpenfundsModel(obj) as ShareClassOpenfundsModel) - : null + rawOpenfunds: new ShareClassOpenfundsModel( + obj + ) as ShareClassOpenfundsModel }; return result; } @@ -104,27 +106,15 @@ export type ShareClassOpenfundsModel = IdlTypes["ShareClassOpenfundsModel"]; export const ShareClassOpenfundsModel = class { constructor(obj: any) { - let result: IdlTypes["ShareClassOpenfundsModel"] = { + let partial: any = { isin: null, shareClassCurrency: null, - // appliedSubscriptionFeeInFavourOfDistributor: null, - // appliedSubscriptionFeeInFavourOfDistributorReferenceDate: null, currencyOfMinimalSubscription: null, fullShareClassName: null, - // hasPerformanceFee: null, - // hasSubscriptionFeeInFavourOfDistributor: null, investmentStatus: null, - // managementFeeApplied: null, - // managementFeeAppliedReferenceDate: null, - // managementFeeMaximum: null, - // maximumSubscriptionFeeInFavourOfDistributor: null, minimalInitialSubscriptionCategory: null, minimalInitialSubscriptionInAmount: null, minimalInitialSubscriptionInShares: null, - // minimalSubsequentSubscriptionCategory: null, - // minimalSubsequentSubscriptionInAmount: null, - // minimalSubsequentSubscriptionInShares: null, - // minimumSubscriptionFeeInFavourOfDistributor: null, shareClassDistributionPolicy: null, shareClassExtension: null, shareClassLaunchDate: null, @@ -132,14 +122,6 @@ export const ShareClassOpenfundsModel = class { launchPrice: null, launchPriceCurrency: null, launchPriceDate: null, - // hasAppliedSubscriptionFeeInFavourOfFund: null, - // appliedSubscriptionFeeInFavourOfFund: null, - // appliedSubscriptionFeeInFavourOfFundReferenceDate: null, - // maximumSubscriptionFeeInFavourOfFund: null, - // hasAppliedRedemptionFeeInFavourOfFund: null, - // appliedRedemptionFeeInFavourOfFund: null, - // appliedRedemptionFeeInFavourOfFundReferenceDate: null, - // maximumRedemptionFeeInFavourOfFund: null, currencyOfMinimalOrMaximumRedemption: null, hasLockUpForRedemption: null, isValidIsin: null, @@ -154,9 +136,12 @@ export const ShareClassOpenfundsModel = class { cusip: null, valor: null }; - for (const key in result) { - result[key] = obj[key] || null; + for (const key in partial) { + partial[key] = obj[key] || null; } + let result: IdlTypes["ShareClassOpenfundsModel"] = { + ...partial + }; return result; } }; diff --git a/anchor/target/idl/glam.json b/anchor/target/idl/glam.json index fa685003..914df0a4 100644 --- a/anchor/target/idl/glam.json +++ b/anchor/target/idl/glam.json @@ -60,6 +60,11 @@ "isMut": true, "isSigner": false }, + { + "name": "openfunds", + "isMut": true, + "isSigner": false + }, { "name": "manager", "isMut": true, @@ -80,7 +85,7 @@ { "name": "shareClassMetadata", "type": { - "defined": "ShareClassMetadata" + "defined": "ShareClassModel" } } ] diff --git a/anchor/target/types/glam.ts b/anchor/target/types/glam.ts index a040f63f..70409e84 100644 --- a/anchor/target/types/glam.ts +++ b/anchor/target/types/glam.ts @@ -60,6 +60,11 @@ export type Glam = { "isMut": true, "isSigner": false }, + { + "name": "openfunds", + "isMut": true, + "isSigner": false + }, { "name": "manager", "isMut": true, @@ -80,7 +85,7 @@ export type Glam = { { "name": "shareClassMetadata", "type": { - "defined": "ShareClassMetadata" + "defined": "ShareClassModel" } } ] @@ -2927,6 +2932,11 @@ export const IDL: Glam = { "isMut": true, "isSigner": false }, + { + "name": "openfunds", + "isMut": true, + "isSigner": false + }, { "name": "manager", "isMut": true, @@ -2947,7 +2957,7 @@ export const IDL: Glam = { { "name": "shareClassMetadata", "type": { - "defined": "ShareClassMetadata" + "defined": "ShareClassModel" } } ] diff --git a/anchor/tests/setup.ts b/anchor/tests/setup.ts index 461e23ed..f427518f 100644 --- a/anchor/tests/setup.ts +++ b/anchor/tests/setup.ts @@ -27,35 +27,41 @@ export const createFundForTest = async ( shareClasses: [ { // Glam Token - symbol: "GBS", name: "Glam Investment Fund BTC-SOL", + symbol: "GBS", asset: usdc, + // Glam + permanentDelegate: manager, + lockUpTime: 40 * 24 * 60 * 60, + requiresMemoOnTransfer: true, // Openfunds Share Class + fullShareClassName: "Glam Investment Fund BTC-SOL", isin: "XS1082172823", + cusip: "demo", + valor: "demo", shareClassCurrency: "USDC", - fullShareClassName: null, // auto - hasPerformanceFee: false, - hasSubscriptionFeeInFavourOfDistributor: false, - investmentStatus: "open", //TODO: auto - shareClassDistributionPolicy: "accumulating", //TODO: auto - shareClassExtension: "", - shareClassLaunchDate: null, // auto - shareClassLifecycle: "active", //TODO: auto - // launchPrice: null, - // launchPriceCurrency: null, - // launchPriceDate: null, - hasAppliedSubscriptionFeeInFavourOfFund: false, - hasAppliedRedemptionFeeInFavourOfFund: false, - hasLockUpForRedemption: false, - hasRedemptionFeeInFavourOfDistributor: false, - isValidISIN: false - // lockUpComment: null, - // lockUpPeriodInDays: null, - // roundingMethodForPrices: null, - // roundingMethodForRedemptionInAmount: null, - // roundingMethodForRedemptionInShares: null, - // roundingMethodForSubscriptionInAmount: null, - // roundingMethodForSubscriptionInShares: null, + shareClassLifecycle: "active", + investmentStatus: "open", + shareClassDistributionPolicy: "accumulating", + shareClassLaunchDate: new Date().toISOString().split("T")[0], + minimalInitialSubscriptionCategory: "amount", + minimalInitialSubscriptionInShares: "0", + minimalInitialSubscriptionInAmount: "1000", + currencyOfMinimalSubscription: "USDC", + minimalRedemptionCategory: "shares", + minimalInitialRedemptionInShares: "1", + maximumInitialRedemptionInShares: "1000", + minimalInitialRedemptionInAmount: "0", + maximumInitialRedemptionInAmount: null, + currencyOfMinimalOrMaximumRedemption: "USDC", + shareClassDividendType: "both", + srri: "4", + hasLockUpForRedemption: true, + lockUpComment: "demo", + lockUpPeriodInDays: "40", + launchPrice: "100", + launchPriceCurrency: "USD", + launchPriceDate: new Date().toISOString().split("T")[0] } ], // Glam @@ -64,26 +70,24 @@ export const createFundForTest = async ( assetsWeights: [0, 60, 40], // Openfunds (Fund) fundDomicileAlpha2: "XS", - legalFundNameIncludingUmbrella: null, // auto - fiscalYearEnd: "12-31", - fundCurrency: null, // auto - fundLaunchDate: null, // auto + legalFundNameIncludingUmbrella: "Glam Investment Fund BTC-SOL (b)", + fundLaunchDate: new Date().toISOString().split("T")[0], investmentObjective: "demo", - // investmentObjective: - // "The Glam Investment Fund seeks to reflect generally the performance of the price of Bitcoin and Solana.", - isFundOfFunds: false, - isPassiveFund: true, //TODO: auto + fundCurrency: "USDC", + openEndedOrClosedEndedFundStructure: "open-ended fund", + fiscalYearEnd: "12-31", legalForm: "other", - openEndedOrClosedEndedFundStructure: "open-ended fund", //TODO: auto // Openfunds Company (simplified) company: { - name: "Glam Systems", - email: "hello@glam.systems", - website: "https://glam.systems" + fundGroupName: "Glam Systems", + manCo: "Glam Management", + domicileOfManCo: "CH", + emailAddressOfManCo: "hello@glam.systems", + fundWebsiteOfManCo: "https://glam.systems" }, // Openfunds Manager (simplified) manager: { - name: "0x0ece.sol" + portfolioManagerName: "0x0ece.sol" } }); console.log(`Fund ${fundPDA} initialized, txId: ${txId}`); @@ -92,78 +96,12 @@ export const createFundForTest = async ( throw e; } - // const [fundPDA, fundBump] = PublicKey.findProgramAddressSync( - // [Buffer.from("fund"), manager.publicKey.toBuffer(), Buffer.from(name)], - // program.programId - // ); - - // const [treasuryPDA, treasuryBump] = PublicKey.findProgramAddressSync( - // [Buffer.from("treasury"), fundPDA.toBuffer()], - // program.programId - // ); - - const shareClassSymbol = `${symbol}.A`; - const [sharePDA, shareBump] = PublicKey.findProgramAddressSync( - [Buffer.from("share"), Buffer.from(shareClassSymbol), fundPDA.toBuffer()], - program.programId - ); - const shareClassMetadata = { - name: `${name} - A Share`, - symbol: shareClassSymbol, - uri: getMetadataUri(sharePDA), - shareClassAsset: "USDC", - shareClassAssetId: usdc, - isin: "XS1082172823", - status: "open", - feeManagement: 15_000, // 1_000_000 * 0.015, - feePerformance: 100_000, // 1_000_000 * 0.1, - policyDistribution: "accumulating", - extension: "", - launchDate: "2024-04-01", - lifecycle: "active", - imageUri: getImageUri(sharePDA) - }; - - try { - // let txId = await program.methods - // .initialize(name, symbol, getFundUri(fundPDA), [10, 50, 40], true) - // .accounts({ - // fund: fundPDA, - // treasury: treasuryPDA, - // manager: manager.publicKey - // }) - // .remainingAccounts([ - // { pubkey: usdc, isSigner: false, isWritable: false }, - // { pubkey: btc, isSigner: false, isWritable: false }, - // { pubkey: eth, isSigner: false, isWritable: false } - // ]) - // .rpc({ commitment: "confirmed" }); - // console.log(`Fund ${fundPDA} initialized, txId: ${txId}`); - - const txId = await program.methods - .addShareClass(shareClassMetadata) - .accounts({ - fund: fundPDA, - shareClassMint: sharePDA, - manager: manager.publicKey, - tokenProgram: TOKEN_2022_PROGRAM_ID - }) - .preInstructions([ - ComputeBudgetProgram.setComputeUnitLimit({ units: 500_000 }) - ]) - .rpc({ commitment: "confirmed" }); - console.log(`Share class ${sharePDA} added, txId: ${txId}`); - } catch (e) { - console.error(e); - throw e; - } - return { fundPDA, fundBump: null, treasuryPDA: client.getTreasuryPDA(fundPDA), treasuryBump: null, - sharePDA, - shareBump + sharePDA: client.getShareClassPDA(fundPDA, 0), + shareBump: null }; }; From c7048a8761536c3cec30d0cd8f31bc2c5fcf7cad Mon Sep 17 00:00:00 2001 From: Emanuele Cesena Date: Sat, 18 May 2024 09:49:35 -0500 Subject: [PATCH 7/9] v0.2: investor subscribe/redeem --- .../glam/src/instructions/investor.rs | 49 ++- .../programs/glam/src/instructions/manager.rs | 16 + anchor/programs/glam/src/state/accounts.rs | 70 +++- anchor/src/client.ts | 10 +- anchor/target/idl/glam.json | 188 +++++++++ anchor/target/types/glam.ts | 376 ++++++++++++++++++ anchor/tests/glam_crud.spec.ts | 22 +- anchor/tests/glam_drift.spec.ts | 10 +- anchor/tests/glam_investor.spec.ts | 111 ++---- anchor/tests/glam_staking.spec.ts | 25 +- anchor/tests/setup.ts | 152 ++++--- 11 files changed, 812 insertions(+), 217 deletions(-) diff --git a/anchor/programs/glam/src/instructions/investor.rs b/anchor/programs/glam/src/instructions/investor.rs index 8909531a..dc2e8276 100644 --- a/anchor/programs/glam/src/instructions/investor.rs +++ b/anchor/programs/glam/src/instructions/investor.rs @@ -9,7 +9,7 @@ use pyth_sdk_solana::state::SolanaPriceAccount; use pyth_sdk_solana::Price; use crate::error::{FundError, InvestorError}; -use crate::state::fund::*; +use crate::state::*; //TODO(security): check that treasury belongs to the fund @@ -69,11 +69,17 @@ fn check_pricing_account(_asset: &str, _pricing_account: &str) -> bool { #[derive(Accounts)] pub struct Subscribe<'info> { - pub fund: Box>, + pub fund: Box>, // the shares to mint - #[account(mut, mint::authority = share_class, mint::token_program = token_2022_program)] + #[account(mut, seeds = [ + b"share".as_ref(), + &[0u8], //TODO: add share_class_idx to instruction + fund.key().as_ref() + ], + bump, mint::authority = share_class, mint::token_program = token_2022_program)] pub share_class: Box>, // mint + #[account( init_if_needed, payer = signer, @@ -109,7 +115,10 @@ pub fn subscribe_handler<'c: 'info, 'info>( skip_state: bool, ) -> Result<()> { let fund = &ctx.accounts.fund; - require!(fund.is_active, InvestorError::FundNotActive); + require!(fund.is_enabled(), InvestorError::FundNotActive); + + let assets = fund.assets().unwrap(); + // msg!("assets: {:?}", assets); if fund.share_classes.len() > 1 { // we need to define how to split the total amount into share classes @@ -117,8 +126,8 @@ pub fn subscribe_handler<'c: 'info, 'info>( } require!(fund.share_classes.len() > 0, FundError::NoShareClassInFund); - msg!("fund.share_class[0]: {}", fund.share_classes[0]); - msg!("expected share class: {}", ctx.accounts.share_class.key()); + // msg!("fund.share_class[0]: {}", fund.share_classes[0]); + // msg!("expected share class: {}", ctx.accounts.share_class.key()); require!( fund.share_classes[0] == ctx.accounts.share_class.key(), @@ -127,7 +136,8 @@ pub fn subscribe_handler<'c: 'info, 'info>( let asset_info = ctx.accounts.asset.to_account_info(); let asset_key = asset_info.key(); - let asset_idx = fund.assets.iter().position(|&asset| asset == asset_key); + let asset_idx = assets.iter().position(|&asset| asset == asset_key); + // msg!("asset={:?} idx={:?}", asset_key, asset_idx); require!(asset_idx.is_some(), InvestorError::InvalidAssetSubscribe); let asset_idx = asset_idx.unwrap(); @@ -153,7 +163,7 @@ pub fn subscribe_handler<'c: 'info, 'info>( // the assets should be the fund.assets, including the base asset, // and in the correct order. require!( - ctx.remaining_accounts.len() == 2 * fund.assets.len(), + ctx.remaining_accounts.len() == 2 * assets.len(), InvestorError::InvalidAssetSubscribe ); @@ -276,9 +286,9 @@ pub fn subscribe_handler<'c: 'info, 'info>( let fund_key = ctx.accounts.fund.key(); let seeds = &[ "share".as_bytes(), - share_class_symbol.as_bytes(), + &[0u8], fund_key.as_ref(), - &[ctx.accounts.fund.share_classes_bumps[0]], + &[ctx.bumps.share_class], ]; let signer_seeds = &[&seeds[..]]; mint_to( @@ -303,7 +313,7 @@ pub fn subscribe_handler<'c: 'info, 'info>( #[derive(Accounts)] pub struct Redeem<'info> { - pub fund: Account<'info, Fund>, + pub fund: Account<'info, FundAccount>, // the shares to burn #[account(mut, mint::authority = share_class, mint::token_program = token_2022_program)] @@ -315,6 +325,7 @@ pub struct Redeem<'info> { #[account(mut)] pub signer: Signer<'info>, + #[account(seeds = [b"treasury".as_ref(), fund.key().as_ref()], bump)] pub treasury: SystemAccount<'info>, // programs @@ -363,6 +374,9 @@ pub fn redeem_handler<'c: 'info, 'info>( let signer = &ctx.accounts.signer; let treasury = &ctx.accounts.treasury; + let assets = fund.assets().unwrap(); + let assets_weights = fund.assets_weights().unwrap(); + // if we skip the redeem state, we attempt to do in_kind redeem, // i.e. transfer to the user a % of each asset in the fund (assuming // the fund is balanced, if it's not the redeem may fail). @@ -370,7 +384,7 @@ pub fn redeem_handler<'c: 'info, 'info>( // the assets should be the fund.assets, including the base asset, // and in the correct order. require!( - ctx.remaining_accounts.len() == 4 * fund.assets.len(), + ctx.remaining_accounts.len() == 4 * assets.len(), InvestorError::InvalidAssetsRedeem ); @@ -385,10 +399,7 @@ pub fn redeem_handler<'c: 'info, 'info>( .expect("invalid treasury account"); let pricing_account = &accounts[3]; - require!( - asset.key() == fund.assets[i], - InvestorError::InvalidAssetsRedeem - ); + require!(asset.key() == assets[i], InvestorError::InvalidAssetsRedeem); require!( signer_asset_ata.owner == signer.key(), InvestorError::InvalidAssetsRedeem @@ -475,7 +486,7 @@ pub fn redeem_handler<'c: 'info, 'info>( // value_to_redeem.price, // value_to_redeem.expo // ); - let total_weight: u32 = fund.assets_weights.iter().sum(); + let total_weight: u32 = assets_weights.iter().sum(); burn( CpiContext::new( @@ -502,7 +513,7 @@ pub fn redeem_handler<'c: 'info, 'info>( ((value.price as u128 * 10u128.pow(att.asset_decimals as u32)) / att.asset_price.price as u128) as u64 } else { - let weight: u32 = fund.assets_weights[i]; + let weight: u32 = assets_weights[i]; if weight == 0 { continue; } @@ -533,7 +544,7 @@ pub fn redeem_handler<'c: 'info, 'info>( let seeds = &[ "treasury".as_bytes(), fund_key.as_ref(), - &[ctx.accounts.fund.bump_treasury], + &[ctx.bumps.treasury], ]; let signer_seeds = &[&seeds[..]]; // msg!( diff --git a/anchor/programs/glam/src/instructions/manager.rs b/anchor/programs/glam/src/instructions/manager.rs index 80ab98f1..6e3869f2 100644 --- a/anchor/programs/glam/src/instructions/manager.rs +++ b/anchor/programs/glam/src/instructions/manager.rs @@ -86,6 +86,22 @@ pub fn initialize_fund_handler<'c: 'info, 'info>( fund.openfunds = openfunds.key(); fund.manager = ctx.accounts.manager.key(); + // + // Set engine params + // + fund.params = vec![vec![ + EngineField { + name: EngineFieldName::Assets, + value: EngineFieldValue::VecPubkey { val: model.assets }, + }, + EngineField { + name: EngineFieldName::AssetsWeights, + value: EngineFieldValue::VecU32 { + val: model.assets_weights, + }, + }, + ]]; + // // Initialize openfunds // diff --git a/anchor/programs/glam/src/state/accounts.rs b/anchor/programs/glam/src/state/accounts.rs index 42a30002..2878de75 100644 --- a/anchor/programs/glam/src/state/accounts.rs +++ b/anchor/programs/glam/src/state/accounts.rs @@ -3,6 +3,40 @@ use anchor_lang::prelude::*; use super::model::*; use super::openfunds::*; +#[derive(AnchorDeserialize, AnchorSerialize, Clone, Debug)] +pub enum EngineFieldName { + TimeCreated, + IsEnabled, + Assets, + AssetsWeights, +} + +#[derive(AnchorDeserialize, AnchorSerialize, Clone, Debug)] +pub enum EngineFieldValue { + // openfunds + Boolean { val: bool }, + Date { val: String }, // YYYY-MM-DD + Double { val: i64 }, + Integer { val: i32 }, + String { val: String }, + Time { val: String }, // hh:mm (24 hour) + // more types + U8 { val: u8 }, + U64 { val: u64 }, + Pubkey { val: Pubkey }, + Percentage { val: u32 }, // 100% = 1_000_000 + URI { val: String }, + Timestamp { val: i64 }, + VecPubkey { val: Vec }, + VecU32 { val: Vec }, +} + +#[derive(AnchorDeserialize, AnchorSerialize, Clone, Debug)] +pub struct EngineField { + pub name: EngineFieldName, + pub value: EngineFieldValue, +} + #[account] pub struct FundAccount { pub name: String, @@ -13,10 +47,44 @@ pub struct FundAccount { pub openfunds_uri: String, pub manager: Pubkey, pub engine: Pubkey, - // pub params: Vec>, // params[0]: EngineFundParams, ... + pub params: Vec>, // params[0]: EngineFundParams, ... } impl FundAccount { pub const INIT_SIZE: usize = 1024; + + pub fn is_enabled(&self) -> bool { + return true; + } + + pub fn assets(&self) -> Option<&Vec> { + for EngineField { name, value } in &self.params[0] { + match name { + EngineFieldName::Assets => { + return match value { + EngineFieldValue::VecPubkey { val: v } => Some(v), + _ => None, + }; + } + _ => { /* ignore */ } + } + } + return None; + } + + pub fn assets_weights(&self) -> Option<&Vec> { + for EngineField { name, value } in &self.params[0] { + match name { + EngineFieldName::AssetsWeights => { + return match value { + EngineFieldValue::VecU32 { val: v } => Some(v), + _ => None, + }; + } + _ => { /* ignore */ } + } + } + return None; + } } #[account] diff --git a/anchor/src/client.ts b/anchor/src/client.ts index f5bf8668..03166704 100644 --- a/anchor/src/client.ts +++ b/anchor/src/client.ts @@ -64,12 +64,18 @@ export class GlamClient { } getFundPDA(fundModel: FundModel): PublicKey { + const createdKey = fundModel?.created?.key || [ + ...Buffer.from( + anchor.utils.sha256.hash(this.getFundName(fundModel)) + ).slice(0, 8) + ]; + const manager = this.getManager(); const [pda, _bump] = PublicKey.findProgramAddressSync( [ anchor.utils.bytes.utf8.encode("fund"), manager.toBuffer(), - Uint8Array.from(fundModel?.created?.key || []) + Uint8Array.from(createdKey) ], this.programId ); @@ -192,7 +198,7 @@ export class GlamClient { const shareClasses = fundModel.shareClasses; fundModel.shareClasses = []; - console.log(shareClasses); + // console.log(fundModel); const txSig = await this.program.methods .initialize(fundModel) diff --git a/anchor/target/idl/glam.json b/anchor/target/idl/glam.json index 914df0a4..debb394c 100644 --- a/anchor/target/idl/glam.json +++ b/anchor/target/idl/glam.json @@ -889,6 +889,16 @@ { "name": "engine", "type": "publicKey" + }, + { + "name": "params", + "type": { + "vec": { + "vec": { + "defined": "EngineField" + } + } + } } ] } @@ -1016,6 +1026,26 @@ } ], "types": [ + { + "name": "EngineField", + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "type": { + "defined": "EngineFieldName" + } + }, + { + "name": "value", + "type": { + "defined": "EngineFieldValue" + } + } + ] + } + }, { "name": "ShareClassMetadata", "type": { @@ -1729,6 +1759,164 @@ ] } }, + { + "name": "EngineFieldName", + "type": { + "kind": "enum", + "variants": [ + { + "name": "TimeCreated" + }, + { + "name": "IsEnabled" + }, + { + "name": "Assets" + }, + { + "name": "AssetsWeights" + } + ] + } + }, + { + "name": "EngineFieldValue", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Boolean", + "fields": [ + { + "name": "val", + "type": "bool" + } + ] + }, + { + "name": "Date", + "fields": [ + { + "name": "val", + "type": "string" + } + ] + }, + { + "name": "Double", + "fields": [ + { + "name": "val", + "type": "i64" + } + ] + }, + { + "name": "Integer", + "fields": [ + { + "name": "val", + "type": "i32" + } + ] + }, + { + "name": "String", + "fields": [ + { + "name": "val", + "type": "string" + } + ] + }, + { + "name": "Time", + "fields": [ + { + "name": "val", + "type": "string" + } + ] + }, + { + "name": "U8", + "fields": [ + { + "name": "val", + "type": "u8" + } + ] + }, + { + "name": "U64", + "fields": [ + { + "name": "val", + "type": "u64" + } + ] + }, + { + "name": "Pubkey", + "fields": [ + { + "name": "val", + "type": "publicKey" + } + ] + }, + { + "name": "Percentage", + "fields": [ + { + "name": "val", + "type": "u32" + } + ] + }, + { + "name": "URI", + "fields": [ + { + "name": "val", + "type": "string" + } + ] + }, + { + "name": "Timestamp", + "fields": [ + { + "name": "val", + "type": "i64" + } + ] + }, + { + "name": "VecPubkey", + "fields": [ + { + "name": "val", + "type": { + "vec": "publicKey" + } + } + ] + }, + { + "name": "VecU32", + "fields": [ + { + "name": "val", + "type": { + "vec": "u32" + } + } + ] + } + ] + } + }, { "name": "ManagerKind", "type": { diff --git a/anchor/target/types/glam.ts b/anchor/target/types/glam.ts index 70409e84..82156151 100644 --- a/anchor/target/types/glam.ts +++ b/anchor/target/types/glam.ts @@ -889,6 +889,16 @@ export type Glam = { { "name": "engine", "type": "publicKey" + }, + { + "name": "params", + "type": { + "vec": { + "vec": { + "defined": "EngineField" + } + } + } } ] } @@ -1016,6 +1026,26 @@ export type Glam = { } ], "types": [ + { + "name": "EngineField", + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "type": { + "defined": "EngineFieldName" + } + }, + { + "name": "value", + "type": { + "defined": "EngineFieldValue" + } + } + ] + } + }, { "name": "ShareClassMetadata", "type": { @@ -1729,6 +1759,164 @@ export type Glam = { ] } }, + { + "name": "EngineFieldName", + "type": { + "kind": "enum", + "variants": [ + { + "name": "TimeCreated" + }, + { + "name": "IsEnabled" + }, + { + "name": "Assets" + }, + { + "name": "AssetsWeights" + } + ] + } + }, + { + "name": "EngineFieldValue", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Boolean", + "fields": [ + { + "name": "val", + "type": "bool" + } + ] + }, + { + "name": "Date", + "fields": [ + { + "name": "val", + "type": "string" + } + ] + }, + { + "name": "Double", + "fields": [ + { + "name": "val", + "type": "i64" + } + ] + }, + { + "name": "Integer", + "fields": [ + { + "name": "val", + "type": "i32" + } + ] + }, + { + "name": "String", + "fields": [ + { + "name": "val", + "type": "string" + } + ] + }, + { + "name": "Time", + "fields": [ + { + "name": "val", + "type": "string" + } + ] + }, + { + "name": "U8", + "fields": [ + { + "name": "val", + "type": "u8" + } + ] + }, + { + "name": "U64", + "fields": [ + { + "name": "val", + "type": "u64" + } + ] + }, + { + "name": "Pubkey", + "fields": [ + { + "name": "val", + "type": "publicKey" + } + ] + }, + { + "name": "Percentage", + "fields": [ + { + "name": "val", + "type": "u32" + } + ] + }, + { + "name": "URI", + "fields": [ + { + "name": "val", + "type": "string" + } + ] + }, + { + "name": "Timestamp", + "fields": [ + { + "name": "val", + "type": "i64" + } + ] + }, + { + "name": "VecPubkey", + "fields": [ + { + "name": "val", + "type": { + "vec": "publicKey" + } + } + ] + }, + { + "name": "VecU32", + "fields": [ + { + "name": "val", + "type": { + "vec": "u32" + } + } + ] + } + ] + } + }, { "name": "ManagerKind", "type": { @@ -3761,6 +3949,16 @@ export const IDL: Glam = { { "name": "engine", "type": "publicKey" + }, + { + "name": "params", + "type": { + "vec": { + "vec": { + "defined": "EngineField" + } + } + } } ] } @@ -3888,6 +4086,26 @@ export const IDL: Glam = { } ], "types": [ + { + "name": "EngineField", + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "type": { + "defined": "EngineFieldName" + } + }, + { + "name": "value", + "type": { + "defined": "EngineFieldValue" + } + } + ] + } + }, { "name": "ShareClassMetadata", "type": { @@ -4601,6 +4819,164 @@ export const IDL: Glam = { ] } }, + { + "name": "EngineFieldName", + "type": { + "kind": "enum", + "variants": [ + { + "name": "TimeCreated" + }, + { + "name": "IsEnabled" + }, + { + "name": "Assets" + }, + { + "name": "AssetsWeights" + } + ] + } + }, + { + "name": "EngineFieldValue", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Boolean", + "fields": [ + { + "name": "val", + "type": "bool" + } + ] + }, + { + "name": "Date", + "fields": [ + { + "name": "val", + "type": "string" + } + ] + }, + { + "name": "Double", + "fields": [ + { + "name": "val", + "type": "i64" + } + ] + }, + { + "name": "Integer", + "fields": [ + { + "name": "val", + "type": "i32" + } + ] + }, + { + "name": "String", + "fields": [ + { + "name": "val", + "type": "string" + } + ] + }, + { + "name": "Time", + "fields": [ + { + "name": "val", + "type": "string" + } + ] + }, + { + "name": "U8", + "fields": [ + { + "name": "val", + "type": "u8" + } + ] + }, + { + "name": "U64", + "fields": [ + { + "name": "val", + "type": "u64" + } + ] + }, + { + "name": "Pubkey", + "fields": [ + { + "name": "val", + "type": "publicKey" + } + ] + }, + { + "name": "Percentage", + "fields": [ + { + "name": "val", + "type": "u32" + } + ] + }, + { + "name": "URI", + "fields": [ + { + "name": "val", + "type": "string" + } + ] + }, + { + "name": "Timestamp", + "fields": [ + { + "name": "val", + "type": "i64" + } + ] + }, + { + "name": "VecPubkey", + "fields": [ + { + "name": "val", + "type": { + "vec": "publicKey" + } + } + ] + }, + { + "name": "VecU32", + "fields": [ + { + "name": "val", + "type": { + "vec": "u32" + } + } + ] + } + ] + } + }, { "name": "ManagerKind", "type": { diff --git a/anchor/tests/glam_crud.spec.ts b/anchor/tests/glam_crud.spec.ts index a2a0128c..d1ab885f 100644 --- a/anchor/tests/glam_crud.spec.ts +++ b/anchor/tests/glam_crud.spec.ts @@ -15,28 +15,22 @@ describe("glam_crud", () => { const program = anchor.workspace.Glam as Program; const commitment = "confirmed"; - let fundPDA, fundBump, treasuryPDA, treasuryBump, sharePDA, shareBump; + let fundPDA; it("Initialize fund", async () => { - const fundData = await createFundForTest("Glam Fund BTC", "GBTC", manager); + const fundData = await createFundForTest(); fundPDA = fundData.fundPDA; - fundBump = fundData.fundBump; - treasuryPDA = fundData.treasuryPDA; - treasuryBump = fundData.treasuryBump; - sharePDA = fundData.sharePDA; - shareBump = fundData.shareBump; const fund = await program.account.fundAccount.fetch(fundPDA); expect(fund.shareClasses.length).toEqual(1); // expect(fund.assets.length).toEqual(3); - // expect(fund.symbol).toEqual("GBTC"); - // expect(fund.isActive).toEqual(true); + // expect(fund.isEnabled).toEqual(true); - const metadata = await getTokenMetadata(provider.connection, sharePDA); - const { image_uri } = Object.fromEntries(metadata!.additionalMetadata); - expect(metadata?.symbol).toEqual("GBTC.A"); - expect(metadata?.uri).toEqual(getMetadataUri(sharePDA)); - expect(image_uri).toEqual(getImageUri(sharePDA)); + // const metadata = await getTokenMetadata(provider.connection, sharePDA); + // const { image_uri } = Object.fromEntries(metadata!.additionalMetadata); + // expect(metadata?.symbol).toEqual("GBTC.A"); + // expect(metadata?.uri).toEqual(getMetadataUri(sharePDA)); + // expect(image_uri).toEqual(getImageUri(sharePDA)); }); it("Update fund", async () => { diff --git a/anchor/tests/glam_drift.spec.ts b/anchor/tests/glam_drift.spec.ts index fc95e898..0b3c8d13 100644 --- a/anchor/tests/glam_drift.spec.ts +++ b/anchor/tests/glam_drift.spec.ts @@ -20,22 +20,18 @@ describe("glam_drift", () => { const program = anchor.workspace.Glam as Program; const commitment = "confirmed"; - let fundPDA, fundBump, treasuryPDA, treasuryBump, sharePDA, shareBump; + let fundPDA, treasuryPDA, sharePDA; it("Create fund", async () => { - const fundData = await createFundForTest("Glam Fund BTC", "GBTC", manager); + const fundData = await createFundForTest(); fundPDA = fundData.fundPDA; - fundBump = fundData.fundBump; treasuryPDA = fundData.treasuryPDA; - treasuryBump = fundData.treasuryBump; sharePDA = fundData.sharePDA; - shareBump = fundData.shareBump; }); it("Initialize fund", async () => { const fund = await program.account.fundAccount.fetch(fundPDA); - // console.log(fund); - // expect(fund.shareClassesLen).toEqual(1); + expect(fund.shareClasses.length).toEqual(1); // expect(fund.assets.length).toEqual(3); // expect(fund.symbol).toEqual("GBTC"); // expect(fund.isActive).toEqual(true); diff --git a/anchor/tests/glam_investor.spec.ts b/anchor/tests/glam_investor.spec.ts index db00110d..14c78319 100644 --- a/anchor/tests/glam_investor.spec.ts +++ b/anchor/tests/glam_investor.spec.ts @@ -21,20 +21,12 @@ import { createTransferCheckedInstruction } from "@solana/spl-token"; +import { fundTestExample, createFundForTest } from "./setup"; import { Glam } from "../target/types/glam"; +import { GlamClient } from "../src"; import { getFundUri, getImageUri, getMetadataUri } from "../src/offchain"; describe("glam_investor", () => { - // Configure the client to use the local cluster. - const provider = anchor.AnchorProvider.env(); - anchor.setProvider(provider); - const connection = provider.connection; - const program = anchor.workspace.Glam as Program; - const commitment = "confirmed"; - - const manager = provider.wallet as anchor.Wallet; - console.log("Manager:", manager.publicKey); - const userKeypairs = [ Keypair.generate(), // mock user 0 Keypair.generate(), // ... @@ -54,42 +46,26 @@ describe("glam_investor", () => { const btc = tokenKeypairs[2]; // 9 decimals, token2022 const BTC_TOKEN_PROGRAM_ID = TOKEN_2022_PROGRAM_ID; - const fundName = "Investment fund"; - const fundSymbol = "IFD"; - const [fundPDA, fundBump] = PublicKey.findProgramAddressSync( - [Buffer.from("fund"), manager.publicKey.toBuffer(), Buffer.from(fundName)], - program.programId - ); - const fundUri = getFundUri(fundPDA); + const client = new GlamClient(); + const fundExample = { + ...fundTestExample, + name: "Glam Investment", + assets: [usdc.publicKey, btc.publicKey, eth.publicKey] + } as any; + const shareClassSymbol = fundExample.shareClasses[0].symbol; + const fundPDA = client.getFundPDA(fundExample); + const treasuryPDA = client.getTreasuryPDA(fundPDA); + const sharePDA = client.getShareClassPDA(fundPDA, 0); - // share class - const shareClassSymbol = `${fundSymbol}.A`; - const [sharePDA, shareBump] = PublicKey.findProgramAddressSync( - [Buffer.from("share"), Buffer.from(shareClassSymbol), fundPDA.toBuffer()], - program.programId - ); - const shareClassMetadata = { - name: fundName, - symbol: shareClassSymbol, - uri: getMetadataUri(sharePDA), - shareClassAsset: "USDC", - shareClassAssetId: usdc.publicKey, - isin: "XS1082172823", - status: "open", - feeManagement: 15000, // 1_000_000 * 0.015, - feePerformance: 100000, // 1_000_000 * 0.1, - policyDistribution: "accumulating", - extension: "", - launchDate: "2024-04-01", - lifecycle: "active", - imageUri: getImageUri(sharePDA) - }; - - // treasury - const [treasuryPDA, treasuryBump] = PublicKey.findProgramAddressSync( - [Buffer.from("treasury"), fundPDA.toBuffer()], - program.programId - ); + // Configure the client to use the local cluster. + const provider = anchor.AnchorProvider.env(); + anchor.setProvider(provider); + const connection = provider.connection; + const program = anchor.workspace.Glam as Program; + const commitment = "confirmed"; + + const manager = provider.wallet as anchor.Wallet; + console.log("Manager:", manager.publicKey); const treasuryUsdcAta = getAssociatedTokenAddressSync( usdc.publicKey, @@ -282,42 +258,14 @@ describe("glam_investor", () => { // // create fund // - let txId = await program.methods - .initialize(fundName, fundSymbol, fundUri, [0, 60, 40], true) - .accounts({ - fund: fundPDA, - treasury: treasuryPDA, - manager: manager.publicKey - }) - .remainingAccounts([ - { pubkey: usdc.publicKey, isSigner: false, isWritable: false }, - { pubkey: btc.publicKey, isSigner: false, isWritable: false }, - { pubkey: eth.publicKey, isSigner: false, isWritable: false } - ]) - .preInstructions([ - ComputeBudgetProgram.setComputeUnitLimit({ units: 500_000 }) - ]) - .rpc({ commitment }); - - txId = await program.methods - .addShareClass(shareClassMetadata) - .accounts({ - fund: fundPDA, - shareClassMint: sharePDA, - manager: manager.publicKey, - tokenProgram: TOKEN_2022_PROGRAM_ID - }) - .preInstructions([ - ComputeBudgetProgram.setComputeUnitLimit({ units: 500_000 }) - ]) - .rpc({ commitment: "confirmed" }); + const fundData = await createFundForTest(fundExample); } catch (e) { console.error(e); throw e; } }, /* timeout */ 15_000); - afterAll(async () => { + /*afterAll(async () => { await program.methods .close() .accounts({ @@ -329,15 +277,12 @@ describe("glam_investor", () => { // The account should no longer exist, returning null. const closedAccount = await program.account.fund.fetchNullable(fundPDA); expect(closedAccount).toBeNull(); - }); + });*/ it("Fund created", async () => { try { - const fund = await program.account.fund.fetch(fundPDA); + const fund = await program.account.fundAccount.fetch(fundPDA); expect(fund.shareClasses[0]).toEqual(sharePDA); - expect(fund.name).toEqual(fundName); - expect(fund.symbol).toEqual(fundSymbol); - expect(fund.isActive).toEqual(true); } catch (e) { console.error(e); } @@ -465,8 +410,10 @@ describe("glam_investor", () => { .rpc({ commitment }); } catch (e) { console.error(e); - expect(e.message).toContain("Share class not allowed to subscribe"); - expect(e.message).toContain("Error Code: InvalidShareClass"); + expect(e.message).toContain("A seeds constraint was violated"); + expect(e.message).toContain("Error Code: ConstraintSeeds"); + // expect(e.message).toContain("Share class not allowed to subscribe"); + // expect(e.message).toContain("Error Code: InvalidShareClass"); } }); diff --git a/anchor/tests/glam_staking.spec.ts b/anchor/tests/glam_staking.spec.ts index 28f6b225..22df4c23 100644 --- a/anchor/tests/glam_staking.spec.ts +++ b/anchor/tests/glam_staking.spec.ts @@ -14,7 +14,7 @@ describe("glam_staking", () => { const manager = provider.wallet as anchor.Wallet; const program = anchor.workspace.Glam as Program; - let fundPDA, fundBump, fundTreasuryPDA, fundTreasuryBump, sharePDA, shareBump; + let fundPDA, treasuryPDA, sharePDA; // marinade setup const marinadeProgram = new PublicKey( @@ -37,22 +37,19 @@ describe("glam_staking", () => { }); it("Create fund", async () => { - const fundData = await createFundForTest("Glam Fund TEST", "GTST", manager); + const fundData = await createFundForTest(); fundPDA = fundData.fundPDA; - fundBump = fundData.fundBump; - fundTreasuryPDA = fundData.treasuryPDA; - fundTreasuryBump = fundData.treasuryBump; + treasuryPDA = fundData.treasuryPDA; sharePDA = fundData.sharePDA; - shareBump = fundData.shareBump; const fund = await program.account.fundAccount.fetch(fundData.fundPDA); - // expect(fund.shareClassesLen).toEqual(1); + expect(fund.shareClasses.length).toEqual(1); // expect(fund.assets.length).toEqual(3); // expect(fund.symbol).toEqual("GTST"); // expect(fund.isActive).toEqual(true); // air drop to treasury and delay 1s for confirmation - await provider.connection.requestAirdrop(fundTreasuryPDA, 100_000_000_000); + await provider.connection.requestAirdrop(treasuryPDA, 100_000_000_000); await sleep(1000); }); @@ -62,7 +59,7 @@ describe("glam_staking", () => { await getOrCreateAssociatedTokenAccount( provider, marinadeState.mSolMintAddress, - fundTreasuryPDA + treasuryPDA ) ).associatedTokenAccountAddress; @@ -78,7 +75,7 @@ describe("glam_staking", () => { liqPoolMsolLegAuthority: await marinadeState.mSolLegAuthority(), liqPoolSolLegPda: await marinadeState.solLeg(), mintTo: treasurymSolAta, - treasury: fundTreasuryPDA, + treasury: treasuryPDA, fund: fundPDA, marinadeProgram }) @@ -96,14 +93,14 @@ describe("glam_staking", () => { .marinadeLiquidUnstake(new anchor.BN(1e9)) .accounts({ manager: manager.publicKey, - treasury: fundTreasuryPDA, + treasury: treasuryPDA, fund: fundPDA, marinadeState: marinadeState.marinadeStateAddress, msolMint: marinadeState.mSolMintAddress, liqPoolSolLegPda: await marinadeState.solLeg(), liqPoolMsolLeg: marinadeState.mSolLeg, getMsolFrom: treasurymSolAta, - getMsolFromAuthority: fundTreasuryPDA, + getMsolFromAuthority: treasuryPDA, treasuryMsolAccount: marinadeTreasuryMsol, marinadeProgram }) @@ -129,7 +126,7 @@ describe("glam_staking", () => { .accounts({ manager: manager.publicKey, fund: fundPDA, - treasury: fundTreasuryPDA, + treasury: treasuryPDA, ticket: ticketPda, msolMint: marinadeState.mSolMintAddress, burnMsolFrom: treasurymSolAta, @@ -157,7 +154,7 @@ describe("glam_staking", () => { .accounts({ manager: manager.publicKey, fund: fundPDA, - treasury: fundTreasuryPDA, + treasury: treasuryPDA, ticket: ticketPda, marinadeState: marinadeState.marinadeStateAddress, reservePda: await marinadeState.reserveAddress(), diff --git a/anchor/tests/setup.ts b/anchor/tests/setup.ts index f427518f..eee1864c 100644 --- a/anchor/tests/setup.ts +++ b/anchor/tests/setup.ts @@ -1,9 +1,7 @@ -import { Program, Wallet, workspace } from "@coral-xyz/anchor"; -import { ComputeBudgetProgram, PublicKey } from "@solana/web3.js"; +import { Program, workspace } from "@coral-xyz/anchor"; +import { PublicKey } from "@solana/web3.js"; import { GlamClient } from "../src"; import { Glam } from "../target/types/glam"; -import { getMetadataUri, getImageUri, getFundUri } from "../src/offchain"; -import { TOKEN_2022_PROGRAM_ID } from "@solana/spl-token"; // Fix import warning in VSCode const program = workspace.Glam as Program; @@ -15,80 +13,81 @@ export const sleep = async (ms: number) => { return new Promise((resolve) => setTimeout(resolve, ms)); }; -export const createFundForTest = async ( - name: string, - symbol: string, - manager: Wallet -) => { +export const fundTestExample = { + shareClasses: [ + { + // Glam Token + name: "Glam Investment Fund BTC-SOL", + symbol: "GBS", + asset: usdc, + // Glam + lockUpTime: 40 * 24 * 60 * 60, + requiresMemoOnTransfer: true, + // Openfunds Share Class + fullShareClassName: "Glam Investment Fund BTC-SOL", + isin: "XS1082172823", + cusip: "demo", + valor: "demo", + shareClassCurrency: "USDC", + shareClassLifecycle: "active", + investmentStatus: "open", + shareClassDistributionPolicy: "accumulating", + shareClassLaunchDate: new Date().toISOString().split("T")[0], + minimalInitialSubscriptionCategory: "amount", + minimalInitialSubscriptionInShares: "0", + minimalInitialSubscriptionInAmount: "1000", + currencyOfMinimalSubscription: "USDC", + minimalRedemptionCategory: "shares", + minimalInitialRedemptionInShares: "1", + maximumInitialRedemptionInShares: "1000", + minimalInitialRedemptionInAmount: "0", + maximumInitialRedemptionInAmount: null, + currencyOfMinimalOrMaximumRedemption: "USDC", + shareClassDividendType: "both", + srri: "4", + hasLockUpForRedemption: true, + lockUpComment: "demo", + lockUpPeriodInDays: "40", + launchPrice: "100", + launchPriceCurrency: "USD", + launchPriceDate: new Date().toISOString().split("T")[0] + } + ], + // Glam + isEnabled: true, + assets: [usdc, btc, eth], + assetsWeights: [0, 60, 40], + // Openfunds (Fund) + fundDomicileAlpha2: "XS", + legalFundNameIncludingUmbrella: "Glam Investment Fund BTC-SOL", + fundLaunchDate: new Date().toISOString().split("T")[0], + investmentObjective: "demo", + fundCurrency: "USDC", + openEndedOrClosedEndedFundStructure: "open-ended fund", + fiscalYearEnd: "12-31", + legalForm: "other", + // Openfunds Company (simplified) + company: { + fundGroupName: "Glam Systems", + manCo: "Glam Management", + domicileOfManCo: "CH", + emailAddressOfManCo: "hello@glam.systems", + fundWebsiteOfManCo: "https://glam.systems" + }, + // Openfunds Manager (simplified) + manager: { + portfolioManagerName: "0x0ece.sol" + } +}; + +export const createFundForTest = async (fundTest?: any) => { const client = new GlamClient(); + const manager = client.getManager(); let txId, fundPDA; try { [txId, fundPDA] = await client.createFund({ - shareClasses: [ - { - // Glam Token - name: "Glam Investment Fund BTC-SOL", - symbol: "GBS", - asset: usdc, - // Glam - permanentDelegate: manager, - lockUpTime: 40 * 24 * 60 * 60, - requiresMemoOnTransfer: true, - // Openfunds Share Class - fullShareClassName: "Glam Investment Fund BTC-SOL", - isin: "XS1082172823", - cusip: "demo", - valor: "demo", - shareClassCurrency: "USDC", - shareClassLifecycle: "active", - investmentStatus: "open", - shareClassDistributionPolicy: "accumulating", - shareClassLaunchDate: new Date().toISOString().split("T")[0], - minimalInitialSubscriptionCategory: "amount", - minimalInitialSubscriptionInShares: "0", - minimalInitialSubscriptionInAmount: "1000", - currencyOfMinimalSubscription: "USDC", - minimalRedemptionCategory: "shares", - minimalInitialRedemptionInShares: "1", - maximumInitialRedemptionInShares: "1000", - minimalInitialRedemptionInAmount: "0", - maximumInitialRedemptionInAmount: null, - currencyOfMinimalOrMaximumRedemption: "USDC", - shareClassDividendType: "both", - srri: "4", - hasLockUpForRedemption: true, - lockUpComment: "demo", - lockUpPeriodInDays: "40", - launchPrice: "100", - launchPriceCurrency: "USD", - launchPriceDate: new Date().toISOString().split("T")[0] - } - ], - // Glam - isEnabled: true, - assets: [usdc, btc, eth], - assetsWeights: [0, 60, 40], - // Openfunds (Fund) - fundDomicileAlpha2: "XS", - legalFundNameIncludingUmbrella: "Glam Investment Fund BTC-SOL (b)", - fundLaunchDate: new Date().toISOString().split("T")[0], - investmentObjective: "demo", - fundCurrency: "USDC", - openEndedOrClosedEndedFundStructure: "open-ended fund", - fiscalYearEnd: "12-31", - legalForm: "other", - // Openfunds Company (simplified) - company: { - fundGroupName: "Glam Systems", - manCo: "Glam Management", - domicileOfManCo: "CH", - emailAddressOfManCo: "hello@glam.systems", - fundWebsiteOfManCo: "https://glam.systems" - }, - // Openfunds Manager (simplified) - manager: { - portfolioManagerName: "0x0ece.sol" - } + ...(fundTest || fundTestExample), + manager }); console.log(`Fund ${fundPDA} initialized, txId: ${txId}`); } catch (e) { @@ -98,10 +97,7 @@ export const createFundForTest = async ( return { fundPDA, - fundBump: null, treasuryPDA: client.getTreasuryPDA(fundPDA), - treasuryBump: null, - sharePDA: client.getShareClassPDA(fundPDA, 0), - shareBump: null + sharePDA: client.getShareClassPDA(fundPDA, 0) }; }; From 308a6c85bce3eba86eb50b89773d65983cd6fe7e Mon Sep 17 00:00:00 2001 From: Emanuele Cesena Date: Sun, 19 May 2024 09:37:01 -0500 Subject: [PATCH 8/9] v0.2: fix web build --- anchor/src/clientConfig.ts | 4 +- anchor/src/glamExports.ts | 20 ++++---- web/src/app/glam/glam-data-access.tsx | 72 ++++++++------------------- 3 files changed, 34 insertions(+), 62 deletions(-) diff --git a/anchor/src/clientConfig.ts b/anchor/src/clientConfig.ts index beb1234f..33aa930c 100644 --- a/anchor/src/clientConfig.ts +++ b/anchor/src/clientConfig.ts @@ -1,7 +1,9 @@ import { Provider } from "@coral-xyz/anchor"; import { Cluster } from "@solana/web3.js"; +export type ClusterOrCustom = Cluster | "custom"; + export type GlamClientConfig = { provider?: Provider; - cluster?: Cluster; + cluster?: ClusterOrCustom; }; diff --git a/anchor/src/glamExports.ts b/anchor/src/glamExports.ts index 94e345ed..fed4f62d 100644 --- a/anchor/src/glamExports.ts +++ b/anchor/src/glamExports.ts @@ -1,8 +1,10 @@ // Here we export some useful types and functions for interacting with the Anchor program. -import { Cluster, PublicKey } from '@solana/web3.js'; -import { Program } from '@coral-xyz/anchor'; -import { IDL as GlamIDL } from '../target/types/glam'; -import type { Glam } from '../target/types/glam'; +import { Cluster, PublicKey } from "@solana/web3.js"; +import { Program } from "@coral-xyz/anchor"; +import { IDL as GlamIDL } from "../target/types/glam"; +import type { Glam } from "../target/types/glam"; + +import type { ClusterOrCustom } from "./clientConfig"; // anchor 0.30 // import GlamIDLUntyped from '../target/idl/glam.json'; @@ -13,15 +15,15 @@ export type GlamProgram = Program; // After updating your program ID (e.g. after running `anchor keys sync`) update the value below. export const GLAM_PROGRAM_ID = new PublicKey( - 'Gco1pcjxCMYjKJjSNJ7mKV7qezeUTE7arXJgy7PAPNRc' + "Gco1pcjxCMYjKJjSNJ7mKV7qezeUTE7arXJgy7PAPNRc" ); // This is a helper function to get the program ID for the Glam program depending on the cluster. -export function getGlamProgramId(cluster: Cluster) { +export function getGlamProgramId(cluster: ClusterOrCustom) { switch (cluster) { - case 'devnet': - case 'testnet': - case 'mainnet-beta': + case "devnet": + case "testnet": + case "mainnet-beta": // You only need to update this if you deploy your program on one of these clusters. return GLAM_PROGRAM_ID; default: diff --git a/web/src/app/glam/glam-data-access.tsx b/web/src/app/glam/glam-data-access.tsx index 8c3d0805..5a1807fc 100644 --- a/web/src/app/glam/glam-data-access.tsx +++ b/web/src/app/glam/glam-data-access.tsx @@ -22,6 +22,7 @@ import { getDriftSignerPublicKey } from "@drift-labs/sdk"; import { + GlamClient, GlamIDL, getFundUri, getGlamProgramId, @@ -41,8 +42,14 @@ import { useTransactionToast } from "../ui/ui-layout"; export function useGlamProgram() { const { connection } = useConnection(); const { cluster } = useCluster(); - const transactionToast = useTransactionToast(); const provider = useAnchorProvider(); + + const client = new GlamClient({ + provider, + cluster: cluster.network + }); + + const transactionToast = useTransactionToast(); const programId = useMemo( () => getGlamProgramId(cluster.network as Cluster), [cluster] @@ -97,57 +104,18 @@ export function useGlamProgram() { assetsStructure: number[]; shareClassMetadata: ShareClassMetadata; }) => { - const [fundPDA, fundBump] = PublicKey.findProgramAddressSync( - [Buffer.from("fund"), manager.toBuffer(), Buffer.from(fundName)], - program.programId - ); - const fundUri = getFundUri(fundPDA); - - const [treasuryPDA, treasuryBump] = PublicKey.findProgramAddressSync( - [Buffer.from("treasury"), fundPDA.toBuffer()], - program.programId - ); - - const [sharePDA, shareBump] = PublicKey.findProgramAddressSync( - [ - Buffer.from("share"), - Buffer.from(shareClassMetadata.symbol), - fundPDA.toBuffer() - ], - program.programId - ); - - shareClassMetadata.uri = getMetadataUri(sharePDA); - shareClassMetadata.imageUri = getImageUri(sharePDA); - - const remainingAccounts: Array = assets.map((a) => ({ - pubkey: new PublicKey(a), - isSigner: false, - isWritable: false - })); - - await program.methods - .initialize(fundName, fundSymbol, fundUri, assetsStructure, true) - .accounts({ - fund: fundPDA, - treasury: treasuryPDA, - manager: manager - }) - .remainingAccounts(remainingAccounts) - .rpc({ commitment: "confirmed" }); - - return program.methods - .addShareClass(shareClassMetadata) - .accounts({ - fund: fundPDA, - shareClassMint: sharePDA, - manager: manager, - tokenProgram: TOKEN_2022_PROGRAM_ID - }) - .preInstructions([ - ComputeBudgetProgram.setComputeUnitLimit({ units: 500_000 }) - ]) - .rpc({ commitment: "confirmed" }); + const fundModel = { + name: fundName, + assets, + assetsWeights: assetsStructure, + shareClass: [ + { + ...shareClassMetadata + } + ] + }; + const [txId, fundPDA] = await client.createFund(fundModel); + return txId; }, onSuccess: (tx) => { console.log(tx); From cd2a54b080e58a15d84d170a1620d11e1d1ddad9 Mon Sep 17 00:00:00 2001 From: Emanuele Cesena Date: Tue, 21 May 2024 20:42:07 -0500 Subject: [PATCH 9/9] Update openfunds.rs --- anchor/programs/glam/src/state/model/openfunds.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/anchor/programs/glam/src/state/model/openfunds.rs b/anchor/programs/glam/src/state/model/openfunds.rs index 2e200007..17b1489d 100644 --- a/anchor/programs/glam/src/state/model/openfunds.rs +++ b/anchor/programs/glam/src/state/model/openfunds.rs @@ -64,7 +64,7 @@ impl From for Vec { // Derived fields let is_raw_openfunds = model.is_raw_openfunds.unwrap_or(false); if !is_raw_openfunds { - //TODO + //TODO: add Glam extension fields } res } @@ -79,7 +79,6 @@ impl From<&ShareClassModel> for Vec { let is_raw_openfunds = model.is_raw_openfunds.unwrap_or(false); let model = model.clone(); if !is_raw_openfunds { - //TODO let v: Vec<(Option, ShareClassFieldName)> = vec![ (pubkey2string(model.fund_id), ShareClassFieldName::FundId), (model.image_uri, ShareClassFieldName::ImageUri),