From dc3e73550b5564c33f0d54539e4a0f4ed34018b3 Mon Sep 17 00:00:00 2001 From: Emanuele Cesena Date: Tue, 21 May 2024 20:44:36 -0500 Subject: [PATCH] New FundAccount + FundModel (#94) --- anchor/Anchor.toml | 7 +- anchor/Cargo.lock | 23 + anchor/Cargo.toml | 1 + anchor/programs/glam/Cargo.toml | 1 + .../programs/glam/src/instructions/drift.rs | 40 +- .../glam/src/instructions/investor.rs | 52 +- .../programs/glam/src/instructions/manager.rs | 314 ++++------ .../glam/src/instructions/marinade.rs | 19 +- anchor/programs/glam/src/lib.rs | 18 +- anchor/programs/glam/src/state/accounts.rs | 70 ++- anchor/programs/glam/src/state/model/model.rs | 42 +- .../glam/src/state/model/openfunds.rs | 186 +++--- .../glam/src/state/openfunds/share_class.rs | 14 +- anchor/src/client.ts | 49 +- anchor/src/clientConfig.ts | 4 +- anchor/src/glamExports.ts | 20 +- anchor/src/models.ts | 112 ++-- anchor/target/idl/glam.json | 283 ++++++++- anchor/target/types/glam.ts | 566 ++++++++++++++++-- anchor/tests/glam_crud.spec.ts | 36 +- anchor/tests/glam_drift.spec.ts | 34 +- anchor/tests/glam_investor.spec.ts | 111 +--- anchor/tests/glam_openfunds.spec.ts | 176 ++++++ anchor/tests/glam_staking.spec.ts | 33 +- anchor/tests/setup.ts | 150 ++--- api/src/openfunds.ts | 135 +++-- api/src/validation.ts | 13 +- package.json | 1 + pnpm-lock.yaml | 3 + web/src/app/glam/glam-data-access.tsx | 72 +-- 30 files changed, 1724 insertions(+), 861 deletions(-) create mode 100644 anchor/tests/glam_openfunds.spec.ts diff --git a/anchor/Anchor.toml b/anchor/Anchor.toml index b4773bbc..97afc995 100644 --- a/anchor/Anchor.toml +++ b/anchor/Anchor.toml @@ -28,10 +28,11 @@ 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 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/drift.rs b/anchor/programs/glam/src/instructions/drift.rs index 84d19dbe..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,10 +17,10 @@ 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>, - /// CHECK: treasury account is the same as fund treasury - pub treasury: AccountInfo<'info>, + #[account(seeds = [b"treasury".as_ref(), fund.key().as_ref()], bump)] + pub treasury: SystemAccount<'info>, #[account(mut)] /// CHECK: checks are done inside cpi call @@ -52,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[..]]; @@ -113,10 +113,10 @@ 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>, - /// CHECK: treasury account is the same as fund treasury - pub treasury: AccountInfo<'info>, + #[account(seeds = [b"treasury".as_ref(), fund.key().as_ref()], bump)] + pub treasury: SystemAccount<'info>, #[account(mut)] /// CHECK: checks are done inside cpi call @@ -141,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[..]]; @@ -166,8 +166,10 @@ 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 treasury: Account<'info, Treasury>, + pub fund: Account<'info, FundAccount>, + + #[account(seeds = [b"treasury".as_ref(), fund.key().as_ref()], bump)] + pub treasury: SystemAccount<'info>, #[account(mut)] /// CHECK: checks are done inside cpi call @@ -203,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,7 +238,9 @@ 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>, + + #[account(seeds = [b"treasury".as_ref(), fund.key().as_ref()], bump)] + pub treasury: SystemAccount<'info>, #[account(mut)] /// CHECK: checks are done inside cpi call @@ -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,8 +311,10 @@ 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 treasury: Account<'info, Treasury>, + pub fund: Account<'info, FundAccount>, + + #[account(seeds = [b"treasury".as_ref(), fund.key().as_ref()], bump)] + pub treasury: SystemAccount<'info>, #[account(mut)] /// CHECK: checks are done inside cpi call @@ -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/investor.rs b/anchor/programs/glam/src/instructions/investor.rs index 42ddebc8..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,8 +325,8 @@ pub struct Redeem<'info> { #[account(mut)] pub signer: Signer<'info>, - /// CHECK: skip - pub treasury: AccountInfo<'info>, + #[account(seeds = [b"treasury".as_ref(), fund.key().as_ref()], bump)] + pub treasury: SystemAccount<'info>, // programs pub token_program: Program<'info, Token>, @@ -364,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). @@ -371,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 ); @@ -386,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 @@ -476,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( @@ -503,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; } @@ -534,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 ac62a175..6e3869f2 100644 --- a/anchor/programs/glam/src/instructions/manager.rs +++ b/anchor/programs/glam/src/instructions/manager.rs @@ -3,17 +3,19 @@ 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)] +#[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)] - pub fund: Box>, + #[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>, - /// 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>, @@ -23,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 // @@ -84,39 +59,71 @@ 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(); - - 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, - ); + 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(); + + // + // 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 + // + 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(()) } #[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 @@ -124,7 +131,10 @@ 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 openfunds: Box>, #[account(mut)] pub manager: Signer<'info>, @@ -135,11 +145,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 // @@ -148,10 +170,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], ]; @@ -160,7 +181,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!( @@ -212,9 +233,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, @@ -235,140 +256,33 @@ 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(()) } #[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>, } @@ -390,18 +304,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(()) @@ -410,7 +324,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 ac15caef..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,11 +222,10 @@ pub struct MarinadeDelayedUnstake<'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)] - 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())] @@ -262,11 +261,10 @@ pub struct MarinadeClaim<'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)] - 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())] @@ -293,11 +291,10 @@ pub struct MarinadeLiquidUnstake<'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)] - pub treasury: AccountInfo<'info>, + pub treasury: SystemAccount<'info>, /// CHECK: skip #[account(mut)] diff --git a/anchor/programs/glam/src/lib.rs b/anchor/programs/glam/src/lib.rs index aefc3dab..da281aad 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,25 +21,14 @@ 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>( 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/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/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..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 } @@ -75,6 +75,24 @@ 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 { + 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 @@ -94,19 +112,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 +132,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 +243,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 +263,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 +280,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.maximal_number_of_possible_decimals_amount, + // ShareClassFieldName::MaximalNumberOfPossibleDecimalsAmount, // ), // ( - // model.maximum_initialredemption_in_shares, - // ShareClassFieldName::MaximumInitialRedemptionInShares, + // 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 +316,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 +372,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)| { @@ -365,29 +390,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), - ( - pubkey2string(model.asset), - ShareClassFieldName::ShareClassCurrencyId, - ), - (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 52c89a4b..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 @@ -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/src/client.ts b/anchor/src/client.ts index 573d345c..03166704 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"]; @@ -62,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 ); @@ -171,6 +179,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,28 +189,44 @@ 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(); - //TODO: add instructions to "addShareClass" in the same tx - const txSig = ""; /*await this.program.methods + const shareClasses = fundModel.shareClasses; + fundModel.shareClasses = []; + + // console.log(fundModel); + + 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(); + 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/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/anchor/src/models.ts b/anchor/src/models.ts index e438afee..c3418325 100644 --- a/anchor/src/models.ts +++ b/anchor/src/models.ts @@ -1,18 +1,24 @@ import { IdlTypes } from "@coral-xyz/anchor"; import { Glam } from "./glamExports"; +import { setDefaultResultOrder } from "dns/promises"; 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 +27,13 @@ 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, + rawOpenfunds: new FundOpenfundsModel(obj) as FundOpenfundsModel }; return result; } @@ -41,7 +42,7 @@ export const FundModel = class { export type FundOpenfundsModel = IdlTypes["FundOpenfundsModel"]; export const FundOpenfundsModel = class { constructor(obj: any) { - const result: IdlTypes["FundOpenfundsModel"] = { + let partial: any = { fundDomicileAlpha2: null, legalFundNameIncludingUmbrella: null, fiscalYearEnd: null, @@ -57,8 +58,13 @@ export const FundOpenfundsModel = class { legalFundNameOnly: null, openEndedOrClosedEndedFundStructure: null, typeOfEuDirective: null, - ucitsVersion: null, - ...obj + ucitsVersion: null + }; + for (const key in partial) { + partial[key] = obj[key] || null; + } + let result: IdlTypes["FundOpenfundsModel"] = { + ...partial }; return result; } @@ -67,7 +73,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,10 +93,10 @@ export const ShareClassModel = class { asset: null, imageUri: null, isRawOpenfunds: null, - ...obj, - rawOpenfunds: obj.shareClassCurrency - ? (new ShareClassOpenfundsModel(obj) as ShareClassOpenfundsModel) - : null + ...partial, + rawOpenfunds: new ShareClassOpenfundsModel( + obj + ) as ShareClassOpenfundsModel }; return result; } @@ -88,27 +106,15 @@ export type ShareClassOpenfundsModel = IdlTypes["ShareClassOpenfundsModel"]; export const ShareClassOpenfundsModel = class { constructor(obj: any) { - const 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, @@ -116,15 +122,25 @@ 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 + 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 partial) { + partial[key] = obj[key] || null; + } + let result: IdlTypes["ShareClassOpenfundsModel"] = { + ...partial }; return result; } @@ -133,7 +149,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 +166,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/target/idl/glam.json b/anchor/target/idl/glam.json index e290a616..debb394c 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" } ] }, @@ -71,6 +60,11 @@ "isMut": true, "isSigner": false }, + { + "name": "openfunds", + "isMut": true, + "isSigner": false + }, { "name": "manager", "isMut": true, @@ -91,7 +85,7 @@ { "name": "shareClassMetadata", "type": { - "defined": "ShareClassMetadata" + "defined": "ShareClassModel" } } ] @@ -895,6 +889,16 @@ { "name": "engine", "type": "publicKey" + }, + { + "name": "params", + "type": { + "vec": { + "vec": { + "defined": "EngineField" + } + } + } } ] } @@ -1022,6 +1026,26 @@ } ], "types": [ + { + "name": "EngineField", + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "type": { + "defined": "EngineFieldName" + } + }, + { + "name": "value", + "type": { + "defined": "EngineFieldValue" + } + } + ] + } + }, { "name": "ShareClassMetadata", "type": { @@ -1385,15 +1409,15 @@ } }, { - "name": "fullShareClassName", + "name": "currencyOfMinimalSubscription", "type": { "option": "string" } }, { - "name": "hasPerformanceFee", + "name": "fullShareClassName", "type": { - "option": "bool" + "option": "string" } }, { @@ -1403,19 +1427,19 @@ } }, { - "name": "managementFeeApplied", + "name": "minimalInitialSubscriptionCategory", "type": { "option": "string" } }, { - "name": "managementFeeAppliedReferenceDate", + "name": "minimalInitialSubscriptionInAmount", "type": { "option": "string" } }, { - "name": "managementFeeMaximum", + "name": "minimalInitialSubscriptionInShares", "type": { "option": "string" } @@ -1462,6 +1486,18 @@ "option": "string" } }, + { + "name": "currencyOfMinimalOrMaximumRedemption", + "type": { + "option": "string" + } + }, + { + "name": "hasLockUpForRedemption", + "type": { + "option": "bool" + } + }, { "name": "isValidIsin", "type": { @@ -1481,25 +1517,49 @@ } }, { - "name": "managementFeeMinimum", + "name": "maximumInitialRedemptionInAmount", + "type": { + "option": "string" + } + }, + { + "name": "maximumInitialRedemptionInShares", + "type": { + "option": "string" + } + }, + { + "name": "minimalInitialRedemptionInAmount", + "type": { + "option": "string" + } + }, + { + "name": "minimalInitialRedemptionInShares", + "type": { + "option": "string" + } + }, + { + "name": "minimalRedemptionCategory", "type": { "option": "string" } }, { - "name": "maximalNumberOfPossibleDecimalsAmount", + "name": "shareClassDividendType", "type": { "option": "string" } }, { - "name": "maximalNumberOfPossibleDecimalsNav", + "name": "cusip", "type": { "option": "string" } }, { - "name": "maximalNumberOfPossibleDecimalsShares", + "name": "valor", "type": { "option": "string" } @@ -1699,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": { @@ -2667,10 +2885,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 b6239196..82156151 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" } ] }, @@ -71,6 +60,11 @@ export type Glam = { "isMut": true, "isSigner": false }, + { + "name": "openfunds", + "isMut": true, + "isSigner": false + }, { "name": "manager", "isMut": true, @@ -91,7 +85,7 @@ export type Glam = { { "name": "shareClassMetadata", "type": { - "defined": "ShareClassMetadata" + "defined": "ShareClassModel" } } ] @@ -895,6 +889,16 @@ export type Glam = { { "name": "engine", "type": "publicKey" + }, + { + "name": "params", + "type": { + "vec": { + "vec": { + "defined": "EngineField" + } + } + } } ] } @@ -1022,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": { @@ -1385,15 +1409,15 @@ export type Glam = { } }, { - "name": "fullShareClassName", + "name": "currencyOfMinimalSubscription", "type": { "option": "string" } }, { - "name": "hasPerformanceFee", + "name": "fullShareClassName", "type": { - "option": "bool" + "option": "string" } }, { @@ -1403,19 +1427,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" } @@ -1462,6 +1486,18 @@ export type Glam = { "option": "string" } }, + { + "name": "currencyOfMinimalOrMaximumRedemption", + "type": { + "option": "string" + } + }, + { + "name": "hasLockUpForRedemption", + "type": { + "option": "bool" + } + }, { "name": "isValidIsin", "type": { @@ -1481,25 +1517,49 @@ export type Glam = { } }, { - "name": "managementFeeMinimum", + "name": "maximumInitialRedemptionInAmount", + "type": { + "option": "string" + } + }, + { + "name": "maximumInitialRedemptionInShares", + "type": { + "option": "string" + } + }, + { + "name": "minimalInitialRedemptionInAmount", + "type": { + "option": "string" + } + }, + { + "name": "minimalInitialRedemptionInShares", + "type": { + "option": "string" + } + }, + { + "name": "minimalRedemptionCategory", "type": { "option": "string" } }, { - "name": "maximalNumberOfPossibleDecimalsAmount", + "name": "shareClassDividendType", "type": { "option": "string" } }, { - "name": "maximalNumberOfPossibleDecimalsNav", + "name": "cusip", "type": { "option": "string" } }, { - "name": "maximalNumberOfPossibleDecimalsShares", + "name": "valor", "type": { "option": "string" } @@ -1699,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": { @@ -2667,10 +2885,13 @@ export type Glam = { "name": "YearlySubscriptionDealingDays" }, { - "name": "FundId" + "name": "CUSIP" + }, + { + "name": "Valor" }, { - "name": "ShareClassCurrencyId" + "name": "FundId" }, { "name": "ImageUri" @@ -2856,6 +3077,11 @@ export const IDL: Glam = { "isMut": true, "isSigner": false }, + { + "name": "openfunds", + "isMut": true, + "isSigner": false + }, { "name": "treasury", "isMut": true, @@ -2874,26 +3100,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" } ] }, @@ -2910,6 +3120,11 @@ export const IDL: Glam = { "isMut": true, "isSigner": false }, + { + "name": "openfunds", + "isMut": true, + "isSigner": false + }, { "name": "manager", "isMut": true, @@ -2930,7 +3145,7 @@ export const IDL: Glam = { { "name": "shareClassMetadata", "type": { - "defined": "ShareClassMetadata" + "defined": "ShareClassModel" } } ] @@ -3734,6 +3949,16 @@ export const IDL: Glam = { { "name": "engine", "type": "publicKey" + }, + { + "name": "params", + "type": { + "vec": { + "vec": { + "defined": "EngineField" + } + } + } } ] } @@ -3861,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": { @@ -4224,15 +4469,15 @@ export const IDL: Glam = { } }, { - "name": "fullShareClassName", + "name": "currencyOfMinimalSubscription", "type": { "option": "string" } }, { - "name": "hasPerformanceFee", + "name": "fullShareClassName", "type": { - "option": "bool" + "option": "string" } }, { @@ -4242,19 +4487,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" } @@ -4301,6 +4546,18 @@ export const IDL: Glam = { "option": "string" } }, + { + "name": "currencyOfMinimalOrMaximumRedemption", + "type": { + "option": "string" + } + }, + { + "name": "hasLockUpForRedemption", + "type": { + "option": "bool" + } + }, { "name": "isValidIsin", "type": { @@ -4320,25 +4577,49 @@ export const IDL: Glam = { } }, { - "name": "managementFeeMinimum", + "name": "maximumInitialRedemptionInAmount", + "type": { + "option": "string" + } + }, + { + "name": "maximumInitialRedemptionInShares", + "type": { + "option": "string" + } + }, + { + "name": "minimalInitialRedemptionInAmount", + "type": { + "option": "string" + } + }, + { + "name": "minimalInitialRedemptionInShares", + "type": { + "option": "string" + } + }, + { + "name": "minimalRedemptionCategory", "type": { "option": "string" } }, { - "name": "maximalNumberOfPossibleDecimalsAmount", + "name": "shareClassDividendType", "type": { "option": "string" } }, { - "name": "maximalNumberOfPossibleDecimalsNav", + "name": "cusip", "type": { "option": "string" } }, { - "name": "maximalNumberOfPossibleDecimalsShares", + "name": "valor", "type": { "option": "string" } @@ -4538,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": { @@ -5506,10 +5945,13 @@ export const IDL: Glam = { "name": "YearlySubscriptionDealingDays" }, { - "name": "FundId" + "name": "CUSIP" + }, + { + "name": "Valor" }, { - "name": "ShareClassCurrencyId" + "name": "FundId" }, { "name": "ImageUri" diff --git a/anchor/tests/glam_crud.spec.ts b/anchor/tests/glam_crud.spec.ts index e62e6b93..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.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.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 () => { @@ -48,13 +42,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 +60,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..0b3c8d13 100644 --- a/anchor/tests/glam_drift.spec.ts +++ b/anchor/tests/glam_drift.spec.ts @@ -20,25 +20,21 @@ 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.fund.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); + 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); }); it("Drift initialize", async () => { @@ -187,20 +183,4 @@ describe("glam_drift", () => { } }, 30_000); */ - it("Close fund", async () => { - const fund = await program.account.fund.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.fund.fetchNullable(fundPDA); - expect(closedAccount).toBeNull(); - }); }); 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_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/glam_staking.spec.ts b/anchor/tests/glam_staking.spec.ts index 482886ff..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.fund.fetch(fundData.fundPDA); - // expect(fund.shareClassesLen).toEqual(1); - expect(fund.assets.length).toEqual(3); - expect(fund.symbol).toEqual("GTST"); - expect(fund.isActive).toEqual(true); + const fund = await program.account.fundAccount.fetch(fundData.fundPDA); + 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 65464a48..eee1864c 100644 --- a/anchor/tests/setup.ts +++ b/anchor/tests/setup.ts @@ -1,8 +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; @@ -14,76 +13,91 @@ export const sleep = async (ms: number) => { return new Promise((resolve) => setTimeout(resolve, ms)); }; -export const createFundForTest = async ( - name: string, - symbol: string, - manager: Wallet -) => { - 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) - }; +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 { - 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" }); + [txId, fundPDA] = await client.createFund({ + ...(fundTest || fundTestExample), + manager + }); console.log(`Fund ${fundPDA} initialized, txId: ${txId}`); - - 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, treasuryPDA, treasuryBump, sharePDA, shareBump }; + return { + fundPDA, + treasuryPDA: client.getTreasuryPDA(fundPDA), + sharePDA: client.getShareClassPDA(fundPDA, 0) + }; }; diff --git a/api/src/openfunds.ts b/api/src/openfunds.ts index 0fe076ec..7886505b 100644 --- a/api/src/openfunds.ts +++ b/api/src/openfunds.ts @@ -5,49 +5,103 @@ 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"; 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) => { + 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) + .map((row, i) => ({ + code: row[0], + field: cleanField(row[1]), + key: lodash.camelCase(row[1]), + tag: row[2], + template: row[4], + version: Number(row[5]) + })) + .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; }; 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], @@ -60,11 +114,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])) ) ]; }; @@ -83,13 +135,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); } }) ) @@ -99,17 +152,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/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; }; 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 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);