From ad2b92811c0fcf35935b82aaea0d954b11586d57 Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Tue, 19 Mar 2024 08:17:08 -0700 Subject: [PATCH 01/12] WIP --- Cargo.lock | 2 + programs/dc-conversion-escrow/Cargo.toml | 30 +++ programs/dc-conversion-escrow/Xargo.toml | 2 + programs/dc-conversion-escrow/src/errors.rs | 17 ++ .../src/instructions/initialize_escrow_v0.rs | 65 ++++++ .../src/instructions/lend_v0.rs | 189 ++++++++++++++++++ .../src/instructions/mod.rs | 5 + programs/dc-conversion-escrow/src/lib.rs | 45 +++++ programs/dc-conversion-escrow/src/state.rs | 25 +++ 9 files changed, 380 insertions(+) create mode 100644 programs/dc-conversion-escrow/Cargo.toml create mode 100644 programs/dc-conversion-escrow/Xargo.toml create mode 100644 programs/dc-conversion-escrow/src/errors.rs create mode 100644 programs/dc-conversion-escrow/src/instructions/initialize_escrow_v0.rs create mode 100644 programs/dc-conversion-escrow/src/instructions/lend_v0.rs create mode 100644 programs/dc-conversion-escrow/src/instructions/mod.rs create mode 100644 programs/dc-conversion-escrow/src/lib.rs create mode 100644 programs/dc-conversion-escrow/src/state.rs diff --git a/Cargo.lock b/Cargo.lock index b3177850e..45e3e3f98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1360,7 +1360,9 @@ version = "0.1.0" dependencies = [ "anchor-lang", "anchor-spl", + "data-credits", "default-env", + "pyth-sdk-solana", "shared-utils", "solana-security-txt", ] diff --git a/programs/dc-conversion-escrow/Cargo.toml b/programs/dc-conversion-escrow/Cargo.toml new file mode 100644 index 000000000..91ea483e5 --- /dev/null +++ b/programs/dc-conversion-escrow/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "dc-conversion-escrow" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "dc_conversion_escrow" + +[features] +devnet = [] +no-genesis = [] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] + +[profile.release] +overflow-checks = true + +[dependencies] +anchor-lang = { workspace = true } +anchor-spl = { workspace = true } +shared-utils = { workspace = true } +solana-security-txt = { workspace = true } +data-credits = { path = "../data-credits", features = ["cpi"] } +default-env = { workspace = true } +pyth-sdk-solana = { version = "0.8.0" } diff --git a/programs/dc-conversion-escrow/Xargo.toml b/programs/dc-conversion-escrow/Xargo.toml new file mode 100644 index 000000000..475fb71ed --- /dev/null +++ b/programs/dc-conversion-escrow/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/programs/dc-conversion-escrow/src/errors.rs b/programs/dc-conversion-escrow/src/errors.rs new file mode 100644 index 000000000..b28d7ec75 --- /dev/null +++ b/programs/dc-conversion-escrow/src/errors.rs @@ -0,0 +1,17 @@ +use anchor_lang::prelude::*; + +#[error_code] +pub enum ErrorCode { + #[msg("Error loading Pyth data")] + PythError, + BadInstructionsAccount, + ProgramMismatch, + UnknownInstruction, + #[msg("Incorrect repayment destination")] + IncorrectDestination, + MissingRepay, + PythPriceNotFound, + ArithmeticError, + IncorrectDc, + HntAmountRequired, +} diff --git a/programs/dc-conversion-escrow/src/instructions/initialize_escrow_v0.rs b/programs/dc-conversion-escrow/src/instructions/initialize_escrow_v0.rs new file mode 100644 index 000000000..387e3c72a --- /dev/null +++ b/programs/dc-conversion-escrow/src/instructions/initialize_escrow_v0.rs @@ -0,0 +1,65 @@ +use crate::errors::ErrorCode; +use anchor_lang::prelude::*; +use anchor_spl::{ + associated_token::AssociatedToken, + token::{Mint, Token, TokenAccount}, +}; +use pyth_sdk_solana::load_price_feed_from_account_info; + +use crate::ConversionEscrowV0; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct InitializeEscrowArgsV0 { + pub slippage_bps: u16, +} + +#[derive(Accounts)] +pub struct InitializeEscrowV0<'info> { + #[account(mut)] + pub payer: Signer<'info>, + /// CHECK: The owner of this account. Can fully withdraw + pub owner: UncheckedAccount<'info>, + /// CHECK: The authority to issue dc conversions with funds from this account + pub convert_authority: UncheckedAccount<'info>, + #[account( + init, + payer = payer, + seeds = [b"conversion_escrow", mint.key().as_ref(), owner.key().as_ref()], + bump, + space = ConversionEscrowV0::INIT_SPACE + 60, + )] + pub conversion_escrow: Box>, + pub mint: Box>, + #[account( + init, + payer = payer, + associated_token::authority = conversion_escrow, + associated_token::mint = mint + )] + pub escrow: Box>, + pub system_program: Program<'info, System>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub oracle: AccountInfo<'info>, + pub token_program: Program<'info, Token>, +} + +pub fn handler(ctx: Context, args: InitializeEscrowArgsV0) -> Result<()> { + load_price_feed_from_account_info(&ctx.accounts.oracle).map_err(|e| { + msg!("Pyth error {}", e); + error!(ErrorCode::PythError) + })?; + + ctx + .accounts + .conversion_escrow + .set_inner(ConversionEscrowV0 { + oracle: ctx.accounts.oracle.key(), + escrow: ctx.accounts.escrow.key(), + mint: ctx.accounts.escrow.mint.key(), + slipage_bps: args.slippage_bps, + owner: ctx.accounts.owner.key(), + bump_seed: ctx.bumps["conversion_escrow"], + }); + + Ok(()) +} diff --git a/programs/dc-conversion-escrow/src/instructions/lend_v0.rs b/programs/dc-conversion-escrow/src/instructions/lend_v0.rs new file mode 100644 index 000000000..05a9dddb9 --- /dev/null +++ b/programs/dc-conversion-escrow/src/instructions/lend_v0.rs @@ -0,0 +1,189 @@ +use std::ops::Div; +use std::str::FromStr; + +use crate::errors::ErrorCode; +use crate::{escrow_seeds, ConversionEscrowV0}; +use anchor_lang::prelude::*; +use anchor_lang::solana_program::sysvar; +use anchor_lang::solana_program::sysvar::instructions::{ + load_current_index_checked, load_instruction_at_checked, +}; +use anchor_spl::mint; +use anchor_spl::token::{mint_to, Mint, MintTo, Token, TokenAccount}; +use data_credits::accounts::MintDataCreditsV0; +use data_credits::{DataCreditsV0, MintDataCreditsArgsV0, TESTING}; +use pyth_sdk_solana::load_price_feed_from_account_info; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct LendArgsV0 { + pub amount: u64, +} + +#[derive(Accounts)] +pub struct LendV0<'info> { + #[account( + has_one = mint, + has_one = oracle, + has_one = escrow, + )] + pub conversion_escrow: Box>, + pub escrow: Account<'info, TokenAccount>, + /// CHECK: Checked via pyth + pub oracle: UncheckedAccount<'info>, + pub mint: Box>, + #[account( + has_one = hnt_price_oracle, + // Ensure we're working with the canonical helium data credits + constraint = data_credits.dc_mint == Pubkey::from_str("dcuc8Amr83Wz27ZkQ2K9NS6r8zRpf1J6cvArEBDZDmm").unwrap() + )] + pub data_credits: Box>, + + /// CHECK: Checked by loading with pyth. Also double checked by the has_one on data credits instance. + pub hnt_price_oracle: AccountInfo<'info>, + + #[account( + token::mint = mint + )] + pub destination: Box>, + /// CHECK: check instructions account + #[account(address = sysvar::instructions::ID @ErrorCode::BadInstructionsAccount)] + pub instructions: UncheckedAccount<'info>, + pub token_program: Program<'info, Token>, +} + +pub fn handler(ctx: Context, args: LendArgsV0) -> Result<()> { + let ixs = ctx.accounts.instructions.to_account_info(); + + let price_feed = load_price_feed_from_account_info(&ctx.accounts.oracle).map_err(|e| { + msg!("Pyth error {}", e); + error!(ErrorCode::PythError) + })?; + + let current_time = Clock::get()?.unix_timestamp; + let price = price_feed + .get_ema_price_no_older_than(current_time, if TESTING { 6000000 } else { 10 * 60 }) + .ok_or_else(|| error!(ErrorCode::PythPriceNotFound))?; + // Remove the confidence from the price to use the most conservative price + // https://docs.pyth.network/price-feeds/solana-price-feeds/best-practices#confidence-intervals + let price_with_conf = price + .price + .checked_sub(i64::try_from(price.conf.checked_mul(2).unwrap()).unwrap()) + .unwrap(); + // Exponent is a negative number, likely -8 + // Since DC is 5 decimals, this is likely -8, we need to divide by 10^(-expo - 5) + let exponent_dec = 10_i64 + .checked_pow(u32::try_from(-price.expo - 5).unwrap()) + .ok_or_else(|| error!(ErrorCode::ArithmeticError))?; + + require_gt!(price_with_conf, 0); + let expected_dc: u64 = price_with_conf + .checked_div(exponent_dec) + .unwrap() + .try_into() + .unwrap(); + + let hnt_price_oracle = load_price_feed_from_account_info(&ctx.accounts.hnt_price_oracle) + .map_err(|e| { + msg!("Pyth error {}", e); + error!(ErrorCode::PythError) + })?; + + let hnt_price = hnt_price_oracle + .get_ema_price_no_older_than(current_time, if TESTING { 6000000 } else { 10 * 60 }) + .ok_or_else(|| error!(ErrorCode::PythPriceNotFound))?; + + require_gt!(hnt_price.price, 0); + // Remove the confidence from the price to use the most conservative price + // https://docs.pyth.network/price-feeds/solana-price-feeds/best-practices#confidence-intervals + let hnt_price_with_conf = hnt_price + .price + .checked_sub(i64::try_from(hnt_price.conf.checked_mul(2).unwrap()).unwrap()) + .unwrap(); + // dc_exponent = 5 since $1 = 10^5 DC + // expo is a negative number, i.e. normally -8 for 8 hnt decimals + // dc = (price * 10^expo) * (hnt_amount * 10^-hnt_decimals) * 10^dc_exponent + // dc = price * hnt_amount * 10^(expo - hnt_decimals + dc_exponent) + // dc = price * hnt_amount / 10^(hnt_decimals - expo - dc_exponent) + // hnt_amount = dc * 10^(hnt_decimals - expo - dc_exponent) / price + let exponent = i32::from(8) - hnt_price.expo - 5; + let decimals_factor = 10_u128 + .checked_pow(u32::try_from(exponent).unwrap()) + .ok_or_else(|| error!(ErrorCode::ArithmeticError))?; + + // make sure this isnt a cpi call + let current_index = load_current_index_checked(&ixs)? as usize; + // loop through instructions, looking for an equivalent mint dc to this borrow + let mut index = current_index + 1; // jupiter swap + let discriminator = get_function_hash("global", "mint_data_credits_v0"); + loop { + // get the next instruction, die if theres no more + if let Ok(ix) = load_instruction_at_checked(index, &ixs) { + if ix.program_id == data_credits::id() { + let ix_discriminator: [u8; 8] = ix.data[0..8] + .try_into() + .map_err(|_| ErrorCode::UnknownInstruction)?; + + // check if we have a toplevel repay toward the program authority + if ix_discriminator == discriminator { + require_keys_eq!( + ix.accounts[4].pubkey, + ctx.accounts.conversion_escrow.owner, + ErrorCode::IncorrectDestination + ); + require_keys_eq!( + ix.accounts[0].pubkey, + ctx.accounts.data_credits.key(), + ErrorCode::IncorrectDc + ); + + let mint_dc_args: MintDataCreditsArgsV0 = + MintDataCreditsArgsV0::deserialize(&mut ix.data.as_slice()).unwrap(); + let hnt_amount = mint_dc_args + .hnt_amount + .ok_or_else(|| error!(ErrorCode::HntAmountRequired))?; + let dc_amount_actual = u64::try_from( + u128::from(hnt_amount) + .checked_mul(u128::try_from(hnt_price_with_conf).unwrap()) + .ok_or_else(|| error!(ErrorCode::ArithmeticError))? + .div(decimals_factor), + ) + .map_err(|_| error!(ErrorCode::ArithmeticError))?; + let expected_dc_with_slippage = expected_dc + - (expected_dc * u64::from(ctx.accounts.conversion_escrow.slipage_bps / 10000)); + require_gt!(dc_amount_actual, expected_dc_with_slippage); + + break; + } + } + } else { + // no more instructions, so we're missing a repay + return Err(ErrorCode::MissingRepay.into()); + } + + index += 1 + } + + // Send the loan + mint_to( + CpiContext::new_with_signer( + ctx.accounts.token_program.to_account_info(), + MintTo { + mint: ctx.accounts.mint.to_account_info(), + to: ctx.accounts.destination.to_account_info(), + authority: ctx.accounts.escrow.to_account_info(), + }, + &[escrow_seeds!(ctx.accounts.conversion_escrow)], + ), + args.amount, + )?; + + Ok(()) +} + +pub fn get_function_hash(namespace: &str, name: &str) -> [u8; 8] { + let preimage = format!("{}:{}", namespace, name); + let mut sighash = [0u8; 8]; + sighash + .copy_from_slice(&anchor_lang::solana_program::hash::hash(preimage.as_bytes()).to_bytes()[..8]); + sighash +} diff --git a/programs/dc-conversion-escrow/src/instructions/mod.rs b/programs/dc-conversion-escrow/src/instructions/mod.rs new file mode 100644 index 000000000..57006d07f --- /dev/null +++ b/programs/dc-conversion-escrow/src/instructions/mod.rs @@ -0,0 +1,5 @@ +pub mod initialize_escrow_v0; +pub mod lend_v0; + +pub use initialize_escrow_v0::*; +pub use lend_v0::*; diff --git a/programs/dc-conversion-escrow/src/lib.rs b/programs/dc-conversion-escrow/src/lib.rs new file mode 100644 index 000000000..919a68172 --- /dev/null +++ b/programs/dc-conversion-escrow/src/lib.rs @@ -0,0 +1,45 @@ +use anchor_lang::prelude::*; +#[cfg(not(feature = "no-entrypoint"))] +use {default_env::default_env, solana_security_txt::security_txt}; + +declare_id!("fanqeMu3fw8R4LwKNbahPtYXJsyLL6NXyfe2BqzhfB6"); + +pub mod errors; +pub mod instructions; +pub mod state; + +pub use instructions::*; +pub use state::*; + +#[cfg(not(feature = "no-entrypoint"))] +security_txt! { + name: "DC Conversion Escrow", + project_url: "http://helium.com", + contacts: "email:hello@helium.foundation", + policy: "https://github.com/helium/helium-program-library/tree/master/SECURITY.md", + + + // Optional Fields + preferred_languages: "en", + source_code: "https://github.com/helium/helium-program-library/tree/master/programs/dc-conversion-escrow", + source_revision: default_env!("GITHUB_SHA", ""), + source_release: default_env!("GITHUB_REF_NAME", ""), + auditors: "Sec3" +} + +#[program] +pub mod dc_convesion_escrow { + use super::*; + + pub fn initialize_escrow_v0( + ctx: Context, + args: InitializeEscrowArgsV0, + ) -> Result<()> { + instructions::initialize_escrow_v0::handler(ctx, args) + } + + /// Lends funds to the lendee, assuming they repay in enough data credits to `owner` DC account + pub fn lend_v0(ctx: Context, args: LendArgsV0) -> Result<()> { + instructions::lend_v0::handler(ctx, args) + } +} diff --git a/programs/dc-conversion-escrow/src/state.rs b/programs/dc-conversion-escrow/src/state.rs new file mode 100644 index 000000000..b116291b9 --- /dev/null +++ b/programs/dc-conversion-escrow/src/state.rs @@ -0,0 +1,25 @@ +use anchor_lang::prelude::*; + +#[account] +#[derive(Default, InitSpace)] +pub struct ConversionEscrowV0 { + pub escrow: Pubkey, + pub mint: Pubkey, + /// How much slippage to allow from the oracle price + pub slipage_bps: u16, + pub oracle: Pubkey, + pub owner: Pubkey, + pub bump_seed: u8, +} + +#[macro_export] +macro_rules! escrow_seeds { + ( $escrow:expr ) => { + &[ + "conversion_escrow".as_bytes(), + $escrow.mint.as_ref(), + $escrow.owner.as_ref(), + &[$escrow.bump_seed], + ] + }; +} From fd458f6789250330c02180cfdc75e2baa3447645 Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Tue, 19 Mar 2024 14:25:37 -0700 Subject: [PATCH 02/12] Finish usdc to dc escrow program --- .github/workflows/tests.yaml | 1 + Anchor.toml | 2 + Cargo.lock | 2 +- packages/dc-conversion-escrow-sdk/.gitignore | 4 + .../dc-conversion-escrow-sdk/CHANGELOG.md | 160 ++ .../dc-conversion-escrow-sdk/package.json | 49 + .../dc-conversion-escrow-sdk/src/constants.ts | 3 + .../dc-conversion-escrow-sdk/src/index.ts | 29 + packages/dc-conversion-escrow-sdk/src/pdas.ts | 17 + .../dc-conversion-escrow-sdk/src/resolvers.ts | 15 + .../tsconfig.cjs.json | 7 + .../tsconfig.esm.json | 8 + .../dc-conversion-escrow-sdk/tsconfig.json | 17 + .../dc-conversion-escrow-sdk/yarn.deploy.lock | 2011 +++++++++++++++++ programs/dc-conversion-escrow/Cargo.toml | 2 +- .../src/instructions/initialize_escrow_v0.rs | 12 +- .../src/instructions/lend_v0.rs | 20 +- programs/dc-conversion-escrow/src/lib.rs | 4 +- programs/dc-conversion-escrow/src/state.rs | 2 + tests/dc-conversion-escrow.ts | 293 +++ tests/utils/fixtures.ts | 41 +- tsconfig.json | 3 + yarn.lock | 16 + 23 files changed, 2686 insertions(+), 32 deletions(-) create mode 100644 packages/dc-conversion-escrow-sdk/.gitignore create mode 100644 packages/dc-conversion-escrow-sdk/CHANGELOG.md create mode 100644 packages/dc-conversion-escrow-sdk/package.json create mode 100644 packages/dc-conversion-escrow-sdk/src/constants.ts create mode 100644 packages/dc-conversion-escrow-sdk/src/index.ts create mode 100644 packages/dc-conversion-escrow-sdk/src/pdas.ts create mode 100644 packages/dc-conversion-escrow-sdk/src/resolvers.ts create mode 100644 packages/dc-conversion-escrow-sdk/tsconfig.cjs.json create mode 100644 packages/dc-conversion-escrow-sdk/tsconfig.esm.json create mode 100644 packages/dc-conversion-escrow-sdk/tsconfig.json create mode 100644 packages/dc-conversion-escrow-sdk/yarn.deploy.lock create mode 100644 tests/dc-conversion-escrow.ts diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 84bfdfb90..657a022bc 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -149,6 +149,7 @@ jobs: - tests/voter-stake-registry.ts - tests/fanout.ts - tests/sus.ts + - tests/dc-conversion-escrow.ts steps: - uses: actions/checkout@v3 - uses: ./.github/actions/build-anchor/ diff --git a/Anchor.toml b/Anchor.toml index f2069dd9a..40f649edf 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -17,6 +17,7 @@ fanout = "fanqeMu3fw8R4LwKNbahPtYXJsyLL6NXyfe2BqzhfB6" mobile_entity_manager = "memMa1HG4odAFmUbGWfPwS1WWfK95k99F2YTkGvyxZr" hexboosting = "hexbnKYoA2GercNNhHUCCfrTRWrHjT6ujKPXTa5NPqJ" no_emit = "noEmmgLmQdk6DLiPV8CSwQv3qQDyGEhz9m5A4zhtByv" +dc_conversion_escrow = "dce4jeLBpfaFsNAKMAmVt5Py4E1R4mZcrVvMB5ejvGu" [workspace] members = [ @@ -34,6 +35,7 @@ members = [ "programs/mobile-entity-manager", "programs/hexboosting", "programs/no-emit", + "programs/dc-conversion-escrow", ] [registry] diff --git a/Cargo.lock b/Cargo.lock index 45e3e3f98..889b89917 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1356,7 +1356,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "dc-conversion-escrow" -version = "0.1.0" +version = "0.0.1" dependencies = [ "anchor-lang", "anchor-spl", diff --git a/packages/dc-conversion-escrow-sdk/.gitignore b/packages/dc-conversion-escrow-sdk/.gitignore new file mode 100644 index 000000000..445f244bc --- /dev/null +++ b/packages/dc-conversion-escrow-sdk/.gitignore @@ -0,0 +1,4 @@ +npm-debug.log +dist/ +tmp/ +./node_modules \ No newline at end of file diff --git a/packages/dc-conversion-escrow-sdk/CHANGELOG.md b/packages/dc-conversion-escrow-sdk/CHANGELOG.md new file mode 100644 index 000000000..ee59f464c --- /dev/null +++ b/packages/dc-conversion-escrow-sdk/CHANGELOG.md @@ -0,0 +1,160 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.6.41](https://github.com/helium/helium-program-libary/compare/v0.6.40...v0.6.41) (2024-03-18) + +**Note:** Version bump only for package @helium/hexboosting-sdk + + + + + +## [0.6.40](https://github.com/helium/helium-program-libary/compare/v0.6.39...v0.6.40) (2024-03-18) + +**Note:** Version bump only for package @helium/hexboosting-sdk + + + + + +## [0.6.39](https://github.com/helium/helium-program-libary/compare/v0.6.38...v0.6.39) (2024-03-13) + +**Note:** Version bump only for package @helium/hexboosting-sdk + + + + + +## [0.6.38](https://github.com/helium/helium-program-libary/compare/v0.6.37...v0.6.38) (2024-03-11) + +**Note:** Version bump only for package @helium/hexboosting-sdk + + + + + +## [0.6.37](https://github.com/helium/helium-program-libary/compare/v0.6.35...v0.6.37) (2024-03-11) + +**Note:** Version bump only for package @helium/hexboosting-sdk + + + + + +## [0.6.36](https://github.com/helium/helium-program-libary/compare/v0.6.27...v0.6.36) (2024-03-08) + +**Note:** Version bump only for package @helium/hexboosting-sdk + + + + + +## [0.6.35](https://github.com/helium/helium-program-libary/compare/v0.6.34...v0.6.35) (2024-03-04) + +**Note:** Version bump only for package @helium/hexboosting-sdk + + + + + +## [0.6.34](https://github.com/helium/helium-program-libary/compare/v0.6.33...v0.6.34) (2024-03-04) + +**Note:** Version bump only for package @helium/hexboosting-sdk + + + + + +## [0.6.33](https://github.com/helium/helium-program-libary/compare/v0.6.32...v0.6.33) (2024-03-01) + +**Note:** Version bump only for package @helium/hexboosting-sdk + + + + + +## [0.6.32](https://github.com/helium/helium-program-libary/compare/v0.6.31...v0.6.32) (2024-02-28) + +**Note:** Version bump only for package @helium/hexboosting-sdk + + + + + +## [0.6.31](https://github.com/helium/helium-program-libary/compare/v0.6.30...v0.6.31) (2024-02-28) + +**Note:** Version bump only for package @helium/hexboosting-sdk + + + + + +## [0.6.30](https://github.com/helium/helium-program-libary/compare/v0.6.29...v0.6.30) (2024-02-22) + +**Note:** Version bump only for package @helium/hexboosting-sdk + + + + + +## [0.6.29](https://github.com/helium/helium-program-libary/compare/v0.6.28...v0.6.29) (2024-02-06) + +**Note:** Version bump only for package @helium/hexboosting-sdk + + + + + +## [0.6.28](https://github.com/helium/helium-program-libary/compare/v0.6.27...v0.6.28) (2024-02-06) + +**Note:** Version bump only for package @helium/hexboosting-sdk + + + + + +## [0.6.27](https://github.com/helium/helium-program-libary/compare/v0.6.25...v0.6.27) (2024-02-01) + +**Note:** Version bump only for package @helium/hexboosting-sdk + + + + + +## [0.6.26](https://github.com/helium/helium-program-libary/compare/v0.6.5...v0.6.26) (2024-01-31) + +**Note:** Version bump only for package @helium/hexboosting-sdk + + + + + +## [0.6.25](https://github.com/helium/helium-program-libary/compare/v0.6.24...v0.6.25) (2024-01-29) + +**Note:** Version bump only for package @helium/hexboosting-sdk + + + + + +## [0.6.24](https://github.com/helium/helium-program-libary/compare/v0.6.23...v0.6.24) (2024-01-29) + +**Note:** Version bump only for package @helium/hexboosting-sdk + + + + + +## [0.6.23](https://github.com/helium/helium-program-libary/compare/v0.6.20...v0.6.23) (2024-01-20) + +**Note:** Version bump only for package @helium/hexboosting-sdk + + + + + +## [0.6.22](https://github.com/helium/helium-program-libary/compare/v0.6.5...v0.6.22) (2024-01-19) + +**Note:** Version bump only for package @helium/hexboosting-sdk diff --git a/packages/dc-conversion-escrow-sdk/package.json b/packages/dc-conversion-escrow-sdk/package.json new file mode 100644 index 000000000..ac6c7efd5 --- /dev/null +++ b/packages/dc-conversion-escrow-sdk/package.json @@ -0,0 +1,49 @@ +{ + "name": "@helium/dc-conversion-escrow-sdk", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "license": "Apache-2.0", + "version": "0.6.41", + "description": "Interface to the dc-conversion-escrow smart contract", + "repository": { + "type": "git", + "url": "https://github.com/helium/helium-program-libary" + }, + "main": "./lib/cjs/index.js", + "module": "./lib/esm/src/index.js", + "types": "./lib/types/src/index.d.ts", + "sideEffects": false, + "files": [ + "lib" + ], + "exports": { + "import": "./lib/esm/src/index.js", + "require": "./lib/cjs/index.js", + "types": "./lib/types/src/index.d.ts" + }, + "scripts": { + "format": "prettier --write \"src/**/*.{ts,tsx}\"", + "precommit": "npx git-format-staged -f 'prettier --ignore-unknown --stdin --stdin-filepath \"{}\"' .", + "clean": "npx shx mkdir -p lib && npx shx rm -rf lib", + "package": "npx shx mkdir -p lib/cjs lib/esm", + "prebuild": "npm run clean && npm run package" + }, + "dependencies": { + "@coral-xyz/anchor": "^0.28.0", + "@helium/anchor-resolvers": "^0.6.41", + "@helium/idls": "^0.6.41", + "bn.js": "^5.2.0", + "bs58": "^4.0.1" + }, + "devDependencies": { + "git-format-staged": "^2.1.3", + "ts-loader": "^9.2.3", + "ts-node": "^10.9.1", + "typescript": "^5.2.2" + }, + "keywords": [], + "author": "", + "gitHead": "5a8bf0b7b88e5934ef8d774e686f7c95804fbb8d" +} diff --git a/packages/dc-conversion-escrow-sdk/src/constants.ts b/packages/dc-conversion-escrow-sdk/src/constants.ts new file mode 100644 index 000000000..0f89f8d13 --- /dev/null +++ b/packages/dc-conversion-escrow-sdk/src/constants.ts @@ -0,0 +1,3 @@ +import { PublicKey } from "@solana/web3.js"; + +export const PROGRAM_ID = new PublicKey("dce4jeLBpfaFsNAKMAmVt5Py4E1R4mZcrVvMB5ejvGu"); diff --git a/packages/dc-conversion-escrow-sdk/src/index.ts b/packages/dc-conversion-escrow-sdk/src/index.ts new file mode 100644 index 000000000..d4c573551 --- /dev/null +++ b/packages/dc-conversion-escrow-sdk/src/index.ts @@ -0,0 +1,29 @@ +import { DcConversionEscrow } from "@helium/idls/lib/types/dc_conversion_escrow"; +import { AnchorProvider, Idl, Program } from "@coral-xyz/anchor"; +import { PublicKey } from "@solana/web3.js"; +import { PROGRAM_ID } from "./constants"; +import { dcConversionEscrowResolvers } from "./resolvers"; + + +export * from "./constants"; +export * from "./pdas"; + +export async function init( + provider: AnchorProvider, + programId: PublicKey = PROGRAM_ID, + idl?: Idl | null +): Promise> { + if (!idl) { + idl = await Program.fetchIdl(programId, provider); + } + + const dcConversionEscrow = new Program( + idl as DcConversionEscrow, + programId, + provider, + undefined, + () => dcConversionEscrowResolvers + ) as Program; + + return dcConversionEscrow; +} diff --git a/packages/dc-conversion-escrow-sdk/src/pdas.ts b/packages/dc-conversion-escrow-sdk/src/pdas.ts new file mode 100644 index 000000000..ac8609594 --- /dev/null +++ b/packages/dc-conversion-escrow-sdk/src/pdas.ts @@ -0,0 +1,17 @@ +import { PublicKey } from "@solana/web3.js"; +import { PROGRAM_ID } from "./constants"; + +export function conversionEscrowKey( + mint: PublicKey, + owner: PublicKey, + programId: PublicKey = PROGRAM_ID +) { + return PublicKey.findProgramAddressSync( + [ + Buffer.from("conversion_escrow", "utf-8"), + mint.toBuffer(), + owner.toBuffer(), + ], + programId + ); +} diff --git a/packages/dc-conversion-escrow-sdk/src/resolvers.ts b/packages/dc-conversion-escrow-sdk/src/resolvers.ts new file mode 100644 index 000000000..e70b62ce8 --- /dev/null +++ b/packages/dc-conversion-escrow-sdk/src/resolvers.ts @@ -0,0 +1,15 @@ +import { + ataResolver, + combineResolvers, + heliumCommonResolver +} from "@helium/anchor-resolvers"; + +export const dcConversionEscrowResolvers = combineResolvers( + heliumCommonResolver, + ataResolver({ + instruction: "initializeEscrowV0", + mint: "mint", + account: "escrow", + owner: "conversionEscrow", + }) +); diff --git a/packages/dc-conversion-escrow-sdk/tsconfig.cjs.json b/packages/dc-conversion-escrow-sdk/tsconfig.cjs.json new file mode 100644 index 000000000..5445b9909 --- /dev/null +++ b/packages/dc-conversion-escrow-sdk/tsconfig.cjs.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.cjs.json", + "include": ["src"], + "compilerOptions": { + "outDir": "lib/cjs" + } +} diff --git a/packages/dc-conversion-escrow-sdk/tsconfig.esm.json b/packages/dc-conversion-escrow-sdk/tsconfig.esm.json new file mode 100644 index 000000000..4b7ba456e --- /dev/null +++ b/packages/dc-conversion-escrow-sdk/tsconfig.esm.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.esm.json", + "include": ["src"], + "compilerOptions": { + "outDir": "lib/esm", + "declarationDir": "lib/types" + } +} diff --git a/packages/dc-conversion-escrow-sdk/tsconfig.json b/packages/dc-conversion-escrow-sdk/tsconfig.json new file mode 100644 index 000000000..9e1a89a8e --- /dev/null +++ b/packages/dc-conversion-escrow-sdk/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.root.json", + "references": [ + { + "path": "../idls" + }, + { + "path": "../anchor-resolvers" + }, + { + "path": "./tsconfig.cjs.json" + }, + { + "path": "./tsconfig.esm.json" + } + ] +} diff --git a/packages/dc-conversion-escrow-sdk/yarn.deploy.lock b/packages/dc-conversion-escrow-sdk/yarn.deploy.lock new file mode 100644 index 000000000..f3c21c9c9 --- /dev/null +++ b/packages/dc-conversion-escrow-sdk/yarn.deploy.lock @@ -0,0 +1,2011 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 6 + cacheKey: 8 + +"@babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.22.6": + version: 7.22.11 + resolution: "@babel/runtime@npm:7.22.11" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: a5cd6683a8fcdb8065cb1677f221e22f6c67ec8f15ad1d273b180b93ab3bd86c66da2c48f500d4e72d8d2cfa85ff4872a3f350e5aa3855630036af5da765c001 + languageName: node + linkType: hard + +"@coral-xyz/anchor@npm:^0.28.0": + version: 0.28.0 + resolution: "@coral-xyz/anchor@npm:0.28.0" + dependencies: + "@coral-xyz/borsh": "npm:^0.28.0" + "@solana/web3.js": "npm:^1.68.0" + base64-js: "npm:^1.5.1" + bn.js: "npm:^5.1.2" + bs58: "npm:^4.0.1" + buffer-layout: "npm:^1.2.2" + camelcase: "npm:^6.3.0" + cross-fetch: "npm:^3.1.5" + crypto-hash: "npm:^1.3.0" + eventemitter3: "npm:^4.0.7" + js-sha256: "npm:^0.9.0" + pako: "npm:^2.0.3" + snake-case: "npm:^3.0.4" + superstruct: "npm:^0.15.4" + toml: "npm:^3.0.0" + checksum: 58b3677b5b2ce2c779045184ce4a0ef696966a6a58f41c1c56f6f178db0491acecb6ec677ce0502f0b382a2c724f9c2860d82cc88601784d556d95fbeda415e5 + languageName: node + linkType: hard + +"@coral-xyz/borsh@npm:^0.28.0": + version: 0.28.0 + resolution: "@coral-xyz/borsh@npm:0.28.0" + dependencies: + bn.js: "npm:^5.1.2" + buffer-layout: "npm:^1.2.0" + peerDependencies: + "@solana/web3.js": ^1.68.0 + checksum: bc2b06b777f9ed43d3b886d2350826bd44d5b932c4fd4471af5956e8755236f5854938c890ee4986d88f61d1439e1d84e715c43dcb6dca4e76198c1ce8dc7a58 + languageName: node + linkType: hard + +"@cspotcode/source-map-support@npm:^0.8.0": + version: 0.8.1 + resolution: "@cspotcode/source-map-support@npm:0.8.1" + dependencies: + "@jridgewell/trace-mapping": "npm:0.3.9" + checksum: 5718f267085ed8edb3e7ef210137241775e607ee18b77d95aa5bd7514f47f5019aa2d82d96b3bf342ef7aa890a346fa1044532ff7cc3009e7d24fce3ce6200fa + languageName: node + linkType: hard + +"@helium/anchor-resolvers@^0.6.41": + version: 0.0.0-use.local + resolution: "@helium/anchor-resolvers@workspace:packages/anchor-resolvers" + dependencies: + "@solana/spl-token": ^0.3.8 + "@solana/web3.js": ^1.78.4 + git-format-staged: ^2.1.3 + ts-loader: ^9.2.3 + typescript: ^5.2.2 + peerDependencies: + "@coral-xyz/anchor": ^0.28.0 + languageName: unknown + linkType: soft + +"@helium/dc-conversion-escrow-sdk@workspace:.": + version: 0.0.0-use.local + resolution: "@helium/dc-conversion-escrow-sdk@workspace:." + dependencies: + "@coral-xyz/anchor": ^0.28.0 + "@helium/anchor-resolvers": ^0.6.41 + "@helium/idls": ^0.6.41 + bn.js: ^5.2.0 + bs58: ^4.0.1 + git-format-staged: ^2.1.3 + ts-loader: ^9.2.3 + ts-node: ^10.9.1 + typescript: ^5.2.2 + languageName: unknown + linkType: soft + +"@helium/idls@^0.6.41": + version: 0.0.0-use.local + resolution: "@helium/idls@workspace:packages/idls" + dependencies: + "@coral-xyz/anchor": ^0.28.0 + "@solana/web3.js": ^1.78.4 + bn.js: ^5.2.0 + borsh: ^0.7.0 + bs58: ^4.0.1 + ts-loader: ^9.2.3 + typescript: ^5.2.2 + languageName: unknown + linkType: soft + +"@isaacs/cliui@npm:^8.0.2": + version: 8.0.2 + resolution: "@isaacs/cliui@npm:8.0.2" + dependencies: + string-width: "npm:^5.1.2" + string-width-cjs: "npm:string-width@^4.2.0" + strip-ansi: "npm:^7.0.1" + strip-ansi-cjs: "npm:strip-ansi@^6.0.1" + wrap-ansi: "npm:^8.1.0" + wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" + checksum: 4a473b9b32a7d4d3cfb7a614226e555091ff0c5a29a1734c28c72a182c2f6699b26fc6b5c2131dfd841e86b185aea714c72201d7c98c2fba5f17709333a67aeb + languageName: node + linkType: hard + +"@jridgewell/resolve-uri@npm:^3.0.3": + version: 3.1.1 + resolution: "@jridgewell/resolve-uri@npm:3.1.1" + checksum: f5b441fe7900eab4f9155b3b93f9800a916257f4e8563afbcd3b5a5337b55e52bd8ae6735453b1b745457d9f6cdb16d74cd6220bbdd98cf153239e13f6cbb653 + languageName: node + linkType: hard + +"@jridgewell/sourcemap-codec@npm:^1.4.10": + version: 1.4.15 + resolution: "@jridgewell/sourcemap-codec@npm:1.4.15" + checksum: b881c7e503db3fc7f3c1f35a1dd2655a188cc51a3612d76efc8a6eb74728bef5606e6758ee77423e564092b4a518aba569bbb21c9bac5ab7a35b0c6ae7e344c8 + languageName: node + linkType: hard + +"@jridgewell/trace-mapping@npm:0.3.9": + version: 0.3.9 + resolution: "@jridgewell/trace-mapping@npm:0.3.9" + dependencies: + "@jridgewell/resolve-uri": "npm:^3.0.3" + "@jridgewell/sourcemap-codec": "npm:^1.4.10" + checksum: d89597752fd88d3f3480845691a05a44bd21faac18e2185b6f436c3b0fd0c5a859fbbd9aaa92050c4052caf325ad3e10e2e1d1b64327517471b7d51babc0ddef + languageName: node + linkType: hard + +"@noble/curves@npm:^1.0.0": + version: 1.2.0 + resolution: "@noble/curves@npm:1.2.0" + dependencies: + "@noble/hashes": "npm:1.3.2" + checksum: bb798d7a66d8e43789e93bc3c2ddff91a1e19fdb79a99b86cd98f1e5eff0ee2024a2672902c2576ef3577b6f282f3b5c778bebd55761ddbb30e36bf275e83dd0 + languageName: node + linkType: hard + +"@noble/hashes@npm:1.3.2, @noble/hashes@npm:^1.3.1": + version: 1.3.2 + resolution: "@noble/hashes@npm:1.3.2" + checksum: fe23536b436539d13f90e4b9be843cc63b1b17666a07634a2b1259dded6f490be3d050249e6af98076ea8f2ea0d56f578773c2197f2aa0eeaa5fba5bc18ba474 + languageName: node + linkType: hard + +"@npmcli/fs@npm:^3.1.0": + version: 3.1.0 + resolution: "@npmcli/fs@npm:3.1.0" + dependencies: + semver: "npm:^7.3.5" + checksum: a50a6818de5fc557d0b0e6f50ec780a7a02ab8ad07e5ac8b16bf519e0ad60a144ac64f97d05c443c3367235d337182e1d012bbac0eb8dbae8dc7b40b193efd0e + languageName: node + linkType: hard + +"@pkgjs/parseargs@npm:^0.11.0": + version: 0.11.0 + resolution: "@pkgjs/parseargs@npm:0.11.0" + checksum: 6ad6a00fc4f2f2cfc6bff76fb1d88b8ee20bc0601e18ebb01b6d4be583733a860239a521a7fbca73b612e66705078809483549d2b18f370eb346c5155c8e4a0f + languageName: node + linkType: hard + +"@solana/buffer-layout-utils@npm:^0.2.0": + version: 0.2.0 + resolution: "@solana/buffer-layout-utils@npm:0.2.0" + dependencies: + "@solana/buffer-layout": "npm:^4.0.0" + "@solana/web3.js": "npm:^1.32.0" + bigint-buffer: "npm:^1.1.5" + bignumber.js: "npm:^9.0.1" + checksum: 9284242245b18b49577195ba7548263850be865a4a2d183944fa01bb76382039db589aab8473698e9bb734b515ada9b4d70db0a72e341c5d567c59b83d6d0840 + languageName: node + linkType: hard + +"@solana/buffer-layout@npm:^4.0.0": + version: 4.0.1 + resolution: "@solana/buffer-layout@npm:4.0.1" + dependencies: + buffer: "npm:~6.0.3" + checksum: bf846888e813187243d4008a7a9f58b49d16cbd995b9d7f1b72898aa510ed77b1ce5e8468e7b2fd26dd81e557a4e74a666e21fccb95f123c1f740d41138bbacd + languageName: node + linkType: hard + +"@solana/spl-token@npm:^0.3.8": + version: 0.3.8 + resolution: "@solana/spl-token@npm:0.3.8" + dependencies: + "@solana/buffer-layout": "npm:^4.0.0" + "@solana/buffer-layout-utils": "npm:^0.2.0" + buffer: "npm:^6.0.3" + peerDependencies: + "@solana/web3.js": ^1.47.4 + checksum: 01f4f87112b0ad277701a3bcb8e03069b69449b92724b17959107686731082bfd3475b5f105e1e8f04badd2e810a43d5ef811744ced5178eea1232de8fd75147 + languageName: node + linkType: hard + +"@solana/web3.js@npm:^1.32.0, @solana/web3.js@npm:^1.68.0, @solana/web3.js@npm:^1.78.4": + version: 1.78.4 + resolution: "@solana/web3.js@npm:1.78.4" + dependencies: + "@babel/runtime": "npm:^7.22.6" + "@noble/curves": "npm:^1.0.0" + "@noble/hashes": "npm:^1.3.1" + "@solana/buffer-layout": "npm:^4.0.0" + agentkeepalive: "npm:^4.3.0" + bigint-buffer: "npm:^1.1.5" + bn.js: "npm:^5.2.1" + borsh: "npm:^0.7.0" + bs58: "npm:^4.0.1" + buffer: "npm:6.0.3" + fast-stable-stringify: "npm:^1.0.0" + jayson: "npm:^4.1.0" + node-fetch: "npm:^2.6.12" + rpc-websockets: "npm:^7.5.1" + superstruct: "npm:^0.14.2" + checksum: e1c44c6cbec87cdfd4d6d23b4241b746e14ed3a9ca73d596693758d91ac825cecf579345da3b0b7bb5e54b6794791bc0eac02cadf11f1ec79e859b6536f26f11 + languageName: node + linkType: hard + +"@tootallnate/once@npm:2": + version: 2.0.0 + resolution: "@tootallnate/once@npm:2.0.0" + checksum: ad87447820dd3f24825d2d947ebc03072b20a42bfc96cbafec16bff8bbda6c1a81fcb0be56d5b21968560c5359a0af4038a68ba150c3e1694fe4c109a063bed8 + languageName: node + linkType: hard + +"@tsconfig/node10@npm:^1.0.7": + version: 1.0.9 + resolution: "@tsconfig/node10@npm:1.0.9" + checksum: a33ae4dc2a621c0678ac8ac4bceb8e512ae75dac65417a2ad9b022d9b5411e863c4c198b6ba9ef659e14b9fb609bbec680841a2e84c1172df7a5ffcf076539df + languageName: node + linkType: hard + +"@tsconfig/node12@npm:^1.0.7": + version: 1.0.11 + resolution: "@tsconfig/node12@npm:1.0.11" + checksum: 5ce29a41b13e7897a58b8e2df11269c5395999e588b9a467386f99d1d26f6c77d1af2719e407621412520ea30517d718d5192a32403b8dfcc163bf33e40a338a + languageName: node + linkType: hard + +"@tsconfig/node14@npm:^1.0.0": + version: 1.0.3 + resolution: "@tsconfig/node14@npm:1.0.3" + checksum: 19275fe80c4c8d0ad0abed6a96dbf00642e88b220b090418609c4376e1cef81bf16237bf170ad1b341452feddb8115d8dd2e5acdfdea1b27422071163dc9ba9d + languageName: node + linkType: hard + +"@tsconfig/node16@npm:^1.0.2": + version: 1.0.4 + resolution: "@tsconfig/node16@npm:1.0.4" + checksum: 202319785901f942a6e1e476b872d421baec20cf09f4b266a1854060efbf78cde16a4d256e8bc949d31e6cd9a90f1e8ef8fb06af96a65e98338a2b6b0de0a0ff + languageName: node + linkType: hard + +"@types/connect@npm:^3.4.33": + version: 3.4.35 + resolution: "@types/connect@npm:3.4.35" + dependencies: + "@types/node": "npm:*" + checksum: fe81351470f2d3165e8b12ce33542eef89ea893e36dd62e8f7d72566dfb7e448376ae962f9f3ea888547ce8b55a40020ca0e01d637fab5d99567673084542641 + languageName: node + linkType: hard + +"@types/node@npm:*": + version: 20.5.7 + resolution: "@types/node@npm:20.5.7" + checksum: fc284c8e16ddc04569730d58e87eae349eb1c3dd9020cb79a1862d9d9add6f04e7367a236f3252db8db2572f90278e250f4cd43d27d264972b54394eaba1ed76 + languageName: node + linkType: hard + +"@types/node@npm:^12.12.54": + version: 12.20.55 + resolution: "@types/node@npm:12.20.55" + checksum: e4f86785f4092706e0d3b0edff8dca5a13b45627e4b36700acd8dfe6ad53db71928c8dee914d4276c7fd3b6ccd829aa919811c9eb708a2c8e4c6eb3701178c37 + languageName: node + linkType: hard + +"@types/ws@npm:^7.4.4": + version: 7.4.7 + resolution: "@types/ws@npm:7.4.7" + dependencies: + "@types/node": "npm:*" + checksum: b4c9b8ad209620c9b21e78314ce4ff07515c0cadab9af101c1651e7bfb992d7fd933bd8b9c99d110738fd6db523ed15f82f29f50b45510288da72e964dedb1a3 + languageName: node + linkType: hard + +"JSONStream@npm:^1.3.5": + version: 1.3.5 + resolution: "JSONStream@npm:1.3.5" + dependencies: + jsonparse: "npm:^1.2.0" + through: "npm:>=2.2.7 <3" + bin: + JSONStream: ./bin.js + checksum: 2605fa124260c61bad38bb65eba30d2f72216a78e94d0ab19b11b4e0327d572b8d530c0c9cc3b0764f727ad26d39e00bf7ebad57781ca6368394d73169c59e46 + languageName: node + linkType: hard + +"abbrev@npm:^1.0.0": + version: 1.1.1 + resolution: "abbrev@npm:1.1.1" + checksum: a4a97ec07d7ea112c517036882b2ac22f3109b7b19077dc656316d07d308438aac28e4d9746dc4d84bf6b1e75b4a7b0a5f3cb30592419f128ca9a8cee3bcfa17 + languageName: node + linkType: hard + +"acorn-walk@npm:^8.1.1": + version: 8.2.0 + resolution: "acorn-walk@npm:8.2.0" + checksum: 1715e76c01dd7b2d4ca472f9c58968516a4899378a63ad5b6c2d668bba8da21a71976c14ec5f5b75f887b6317c4ae0b897ab141c831d741dc76024d8745f1ad1 + languageName: node + linkType: hard + +"acorn@npm:^8.4.1": + version: 8.10.0 + resolution: "acorn@npm:8.10.0" + bin: + acorn: bin/acorn + checksum: 538ba38af0cc9e5ef983aee196c4b8b4d87c0c94532334fa7e065b2c8a1f85863467bb774231aae91613fcda5e68740c15d97b1967ae3394d20faddddd8af61d + languageName: node + linkType: hard + +"agent-base@npm:6, agent-base@npm:^6.0.2": + version: 6.0.2 + resolution: "agent-base@npm:6.0.2" + dependencies: + debug: "npm:4" + checksum: f52b6872cc96fd5f622071b71ef200e01c7c4c454ee68bc9accca90c98cfb39f2810e3e9aa330435835eedc8c23f4f8a15267f67c6e245d2b33757575bdac49d + languageName: node + linkType: hard + +"agentkeepalive@npm:^4.2.1, agentkeepalive@npm:^4.3.0": + version: 4.5.0 + resolution: "agentkeepalive@npm:4.5.0" + dependencies: + humanize-ms: "npm:^1.2.1" + checksum: 13278cd5b125e51eddd5079f04d6fe0914ac1b8b91c1f3db2c1822f99ac1a7457869068997784342fe455d59daaff22e14fb7b8c3da4e741896e7e31faf92481 + languageName: node + linkType: hard + +"aggregate-error@npm:^3.0.0": + version: 3.1.0 + resolution: "aggregate-error@npm:3.1.0" + dependencies: + clean-stack: "npm:^2.0.0" + indent-string: "npm:^4.0.0" + checksum: 1101a33f21baa27a2fa8e04b698271e64616b886795fd43c31068c07533c7b3facfcaf4e9e0cab3624bd88f729a592f1c901a1a229c9e490eafce411a8644b79 + languageName: node + linkType: hard + +"ansi-regex@npm:^5.0.1": + version: 5.0.1 + resolution: "ansi-regex@npm:5.0.1" + checksum: 2aa4bb54caf2d622f1afdad09441695af2a83aa3fe8b8afa581d205e57ed4261c183c4d3877cee25794443fde5876417d859c108078ab788d6af7e4fe52eb66b + languageName: node + linkType: hard + +"ansi-regex@npm:^6.0.1": + version: 6.0.1 + resolution: "ansi-regex@npm:6.0.1" + checksum: 1ff8b7667cded1de4fa2c9ae283e979fc87036864317da86a2e546725f96406746411d0d85e87a2d12fa5abd715d90006de7fa4fa0477c92321ad3b4c7d4e169 + languageName: node + linkType: hard + +"ansi-styles@npm:^4.0.0, ansi-styles@npm:^4.1.0": + version: 4.3.0 + resolution: "ansi-styles@npm:4.3.0" + dependencies: + color-convert: "npm:^2.0.1" + checksum: 513b44c3b2105dd14cc42a19271e80f386466c4be574bccf60b627432f9198571ebf4ab1e4c3ba17347658f4ee1711c163d574248c0c1cdc2d5917a0ad582ec4 + languageName: node + linkType: hard + +"ansi-styles@npm:^6.1.0": + version: 6.2.1 + resolution: "ansi-styles@npm:6.2.1" + checksum: ef940f2f0ced1a6347398da88a91da7930c33ecac3c77b72c5905f8b8fe402c52e6fde304ff5347f616e27a742da3f1dc76de98f6866c69251ad0b07a66776d9 + languageName: node + linkType: hard + +"aproba@npm:^1.0.3 || ^2.0.0": + version: 2.0.0 + resolution: "aproba@npm:2.0.0" + checksum: 5615cadcfb45289eea63f8afd064ab656006361020e1735112e346593856f87435e02d8dcc7ff0d11928bc7d425f27bc7c2a84f6c0b35ab0ff659c814c138a24 + languageName: node + linkType: hard + +"are-we-there-yet@npm:^3.0.0": + version: 3.0.1 + resolution: "are-we-there-yet@npm:3.0.1" + dependencies: + delegates: "npm:^1.0.0" + readable-stream: "npm:^3.6.0" + checksum: 52590c24860fa7173bedeb69a4c05fb573473e860197f618b9a28432ee4379049336727ae3a1f9c4cb083114601c1140cee578376164d0e651217a9843f9fe83 + languageName: node + linkType: hard + +"arg@npm:^4.1.0": + version: 4.1.3 + resolution: "arg@npm:4.1.3" + checksum: 544af8dd3f60546d3e4aff084d451b96961d2267d668670199692f8d054f0415d86fc5497d0e641e91546f0aa920e7c29e5250e99fc89f5552a34b5d93b77f43 + languageName: node + linkType: hard + +"balanced-match@npm:^1.0.0": + version: 1.0.2 + resolution: "balanced-match@npm:1.0.2" + checksum: 9706c088a283058a8a99e0bf91b0a2f75497f185980d9ffa8b304de1d9e58ebda7c72c07ebf01dadedaac5b2907b2c6f566f660d62bd336c3468e960403b9d65 + languageName: node + linkType: hard + +"base-x@npm:^3.0.2": + version: 3.0.9 + resolution: "base-x@npm:3.0.9" + dependencies: + safe-buffer: "npm:^5.0.1" + checksum: 957101d6fd09e1903e846fd8f69fd7e5e3e50254383e61ab667c725866bec54e5ece5ba49ce385128ae48f9ec93a26567d1d5ebb91f4d56ef4a9cc0d5a5481e8 + languageName: node + linkType: hard + +"base64-js@npm:^1.3.1, base64-js@npm:^1.5.1": + version: 1.5.1 + resolution: "base64-js@npm:1.5.1" + checksum: 669632eb3745404c2f822a18fc3a0122d2f9a7a13f7fb8b5823ee19d1d2ff9ee5b52c53367176ea4ad093c332fd5ab4bd0ebae5a8e27917a4105a4cfc86b1005 + languageName: node + linkType: hard + +"bigint-buffer@npm:^1.1.5": + version: 1.1.5 + resolution: "bigint-buffer@npm:1.1.5" + dependencies: + bindings: "npm:^1.3.0" + node-gyp: "npm:latest" + checksum: d010c9f57758bcdaccb435d88b483ffcc95fe8bbc6e7fb3a44fb5221f29c894ffaf4a3c5a4a530e0e7d6608203c2cde9b79ee4f2386cd6d4462d1070bc8c9f4e + languageName: node + linkType: hard + +"bignumber.js@npm:^9.0.1": + version: 9.1.2 + resolution: "bignumber.js@npm:9.1.2" + checksum: 582c03af77ec9cb0ebd682a373ee6c66475db94a4325f92299621d544aa4bd45cb45fd60001610e94aef8ae98a0905fa538241d9638d4422d57abbeeac6fadaf + languageName: node + linkType: hard + +"bindings@npm:^1.3.0": + version: 1.5.0 + resolution: "bindings@npm:1.5.0" + dependencies: + file-uri-to-path: "npm:1.0.0" + checksum: 65b6b48095717c2e6105a021a7da4ea435aa8d3d3cd085cb9e85bcb6e5773cf318c4745c3f7c504412855940b585bdf9b918236612a1c7a7942491de176f1ae7 + languageName: node + linkType: hard + +"bn.js@npm:^5.1.2, bn.js@npm:^5.2.0, bn.js@npm:^5.2.1": + version: 5.2.1 + resolution: "bn.js@npm:5.2.1" + checksum: 3dd8c8d38055fedfa95c1d5fc3c99f8dd547b36287b37768db0abab3c239711f88ff58d18d155dd8ad902b0b0cee973747b7ae20ea12a09473272b0201c9edd3 + languageName: node + linkType: hard + +"borsh@npm:^0.7.0": + version: 0.7.0 + resolution: "borsh@npm:0.7.0" + dependencies: + bn.js: "npm:^5.2.0" + bs58: "npm:^4.0.0" + text-encoding-utf-8: "npm:^1.0.2" + checksum: e98bfb5f7cfb820819c2870b884dac58dd4b4ce6a86c286c8fbf5c9ca582e73a8c6094df67e81a28c418ff07a309c6b118b2e27fdfea83fd92b8100c741da0b5 + languageName: node + linkType: hard + +"brace-expansion@npm:^1.1.7": + version: 1.1.11 + resolution: "brace-expansion@npm:1.1.11" + dependencies: + balanced-match: "npm:^1.0.0" + concat-map: "npm:0.0.1" + checksum: faf34a7bb0c3fcf4b59c7808bc5d2a96a40988addf2e7e09dfbb67a2251800e0d14cd2bfc1aa79174f2f5095c54ff27f46fb1289fe2d77dac755b5eb3434cc07 + languageName: node + linkType: hard + +"brace-expansion@npm:^2.0.1": + version: 2.0.1 + resolution: "brace-expansion@npm:2.0.1" + dependencies: + balanced-match: "npm:^1.0.0" + checksum: a61e7cd2e8a8505e9f0036b3b6108ba5e926b4b55089eeb5550cd04a471fe216c96d4fe7e4c7f995c728c554ae20ddfc4244cad10aef255e72b62930afd233d1 + languageName: node + linkType: hard + +"braces@npm:^3.0.2": + version: 3.0.2 + resolution: "braces@npm:3.0.2" + dependencies: + fill-range: "npm:^7.0.1" + checksum: e2a8e769a863f3d4ee887b5fe21f63193a891c68b612ddb4b68d82d1b5f3ff9073af066c343e9867a393fe4c2555dcb33e89b937195feb9c1613d259edfcd459 + languageName: node + linkType: hard + +"bs58@npm:^4.0.0, bs58@npm:^4.0.1": + version: 4.0.1 + resolution: "bs58@npm:4.0.1" + dependencies: + base-x: "npm:^3.0.2" + checksum: b3c5365bb9e0c561e1a82f1a2d809a1a692059fae016be233a6127ad2f50a6b986467c3a50669ce4c18929dcccb297c5909314dd347a25a68c21b68eb3e95ac2 + languageName: node + linkType: hard + +"buffer-layout@npm:^1.2.0, buffer-layout@npm:^1.2.2": + version: 1.2.2 + resolution: "buffer-layout@npm:1.2.2" + checksum: e5809ba275530bf4e52fd09558b7c2111fbda5b405124f581acf364261d9c154e271800271898cd40473f9bcbb42c31584efb04219bde549d3460ca4bafeaa07 + languageName: node + linkType: hard + +"buffer@npm:6.0.3, buffer@npm:^6.0.3, buffer@npm:~6.0.3": + version: 6.0.3 + resolution: "buffer@npm:6.0.3" + dependencies: + base64-js: "npm:^1.3.1" + ieee754: "npm:^1.2.1" + checksum: 5ad23293d9a731e4318e420025800b42bf0d264004c0286c8cc010af7a270c7a0f6522e84f54b9ad65cbd6db20b8badbfd8d2ebf4f80fa03dab093b89e68c3f9 + languageName: node + linkType: hard + +"bufferutil@npm:^4.0.1": + version: 4.0.7 + resolution: "bufferutil@npm:4.0.7" + dependencies: + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.3.0" + checksum: f75aa87e3d1b99b87a95f60a855e63f70af07b57fb8443e75a2ddfef2e47788d130fdd46e3a78fd7e0c10176082b26dfbed970c5b8632e1cc299cafa0e93ce45 + languageName: node + linkType: hard + +"cacache@npm:^17.0.0": + version: 17.1.4 + resolution: "cacache@npm:17.1.4" + dependencies: + "@npmcli/fs": "npm:^3.1.0" + fs-minipass: "npm:^3.0.0" + glob: "npm:^10.2.2" + lru-cache: "npm:^7.7.1" + minipass: "npm:^7.0.3" + minipass-collect: "npm:^1.0.2" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + p-map: "npm:^4.0.0" + ssri: "npm:^10.0.0" + tar: "npm:^6.1.11" + unique-filename: "npm:^3.0.0" + checksum: b7751df756656954a51201335addced8f63fc53266fa56392c9f5ae83c8d27debffb4458ac2d168a744a4517ec3f2163af05c20097f93d17bdc2dc8a385e14a6 + languageName: node + linkType: hard + +"camelcase@npm:^6.3.0": + version: 6.3.0 + resolution: "camelcase@npm:6.3.0" + checksum: 8c96818a9076434998511251dcb2761a94817ea17dbdc37f47ac080bd088fc62c7369429a19e2178b993497132c8cbcf5cc1f44ba963e76782ba469c0474938d + languageName: node + linkType: hard + +"chalk@npm:^4.1.0": + version: 4.1.2 + resolution: "chalk@npm:4.1.2" + dependencies: + ansi-styles: "npm:^4.1.0" + supports-color: "npm:^7.1.0" + checksum: fe75c9d5c76a7a98d45495b91b2172fa3b7a09e0cc9370e5c8feb1c567b85c4288e2b3fded7cfdd7359ac28d6b3844feb8b82b8686842e93d23c827c417e83fc + languageName: node + linkType: hard + +"chownr@npm:^2.0.0": + version: 2.0.0 + resolution: "chownr@npm:2.0.0" + checksum: c57cf9dd0791e2f18a5ee9c1a299ae6e801ff58fee96dc8bfd0dcb4738a6ce58dd252a3605b1c93c6418fe4f9d5093b28ffbf4d66648cb2a9c67eaef9679be2f + languageName: node + linkType: hard + +"clean-stack@npm:^2.0.0": + version: 2.2.0 + resolution: "clean-stack@npm:2.2.0" + checksum: 2ac8cd2b2f5ec986a3c743935ec85b07bc174d5421a5efc8017e1f146a1cf5f781ae962618f416352103b32c9cd7e203276e8c28241bbe946160cab16149fb68 + languageName: node + linkType: hard + +"color-convert@npm:^2.0.1": + version: 2.0.1 + resolution: "color-convert@npm:2.0.1" + dependencies: + color-name: "npm:~1.1.4" + checksum: 79e6bdb9fd479a205c71d89574fccfb22bd9053bd98c6c4d870d65c132e5e904e6034978e55b43d69fcaa7433af2016ee203ce76eeba9cfa554b373e7f7db336 + languageName: node + linkType: hard + +"color-name@npm:~1.1.4": + version: 1.1.4 + resolution: "color-name@npm:1.1.4" + checksum: b0445859521eb4021cd0fb0cc1a75cecf67fceecae89b63f62b201cca8d345baf8b952c966862a9d9a2632987d4f6581f0ec8d957dfacece86f0a7919316f610 + languageName: node + linkType: hard + +"color-support@npm:^1.1.3": + version: 1.1.3 + resolution: "color-support@npm:1.1.3" + bin: + color-support: bin.js + checksum: 9b7356817670b9a13a26ca5af1c21615463b500783b739b7634a0c2047c16cef4b2865d7576875c31c3cddf9dd621fa19285e628f20198b233a5cfdda6d0793b + languageName: node + linkType: hard + +"commander@npm:^2.20.3": + version: 2.20.3 + resolution: "commander@npm:2.20.3" + checksum: ab8c07884e42c3a8dbc5dd9592c606176c7eb5c1ca5ff274bcf907039b2c41de3626f684ea75ccf4d361ba004bbaff1f577d5384c155f3871e456bdf27becf9e + languageName: node + linkType: hard + +"concat-map@npm:0.0.1": + version: 0.0.1 + resolution: "concat-map@npm:0.0.1" + checksum: 902a9f5d8967a3e2faf138d5cb784b9979bad2e6db5357c5b21c568df4ebe62bcb15108af1b2253744844eb964fc023fbd9afbbbb6ddd0bcc204c6fb5b7bf3af + languageName: node + linkType: hard + +"console-control-strings@npm:^1.1.0": + version: 1.1.0 + resolution: "console-control-strings@npm:1.1.0" + checksum: 8755d76787f94e6cf79ce4666f0c5519906d7f5b02d4b884cf41e11dcd759ed69c57da0670afd9236d229a46e0f9cf519db0cd829c6dca820bb5a5c3def584ed + languageName: node + linkType: hard + +"create-require@npm:^1.1.0": + version: 1.1.1 + resolution: "create-require@npm:1.1.1" + checksum: a9a1503d4390d8b59ad86f4607de7870b39cad43d929813599a23714831e81c520bddf61bcdd1f8e30f05fd3a2b71ae8538e946eb2786dc65c2bbc520f692eff + languageName: node + linkType: hard + +"cross-fetch@npm:^3.1.5": + version: 3.1.8 + resolution: "cross-fetch@npm:3.1.8" + dependencies: + node-fetch: "npm:^2.6.12" + checksum: 78f993fa099eaaa041122ab037fe9503ecbbcb9daef234d1d2e0b9230a983f64d645d088c464e21a247b825a08dc444a6e7064adfa93536d3a9454b4745b3632 + languageName: node + linkType: hard + +"cross-spawn@npm:^7.0.0": + version: 7.0.3 + resolution: "cross-spawn@npm:7.0.3" + dependencies: + path-key: "npm:^3.1.0" + shebang-command: "npm:^2.0.0" + which: "npm:^2.0.1" + checksum: 671cc7c7288c3a8406f3c69a3ae2fc85555c04169e9d611def9a675635472614f1c0ed0ef80955d5b6d4e724f6ced67f0ad1bb006c2ea643488fcfef994d7f52 + languageName: node + linkType: hard + +"crypto-hash@npm:^1.3.0": + version: 1.3.0 + resolution: "crypto-hash@npm:1.3.0" + checksum: a3a507e0d2b18fbd2da8088a1c62d0c53c009a99bbfa6d851cac069734ffa546922fa51bdd776d006459701cdda873463e5059ece3431aca048fd99e7573d138 + languageName: node + linkType: hard + +"debug@npm:4, debug@npm:^4.3.3": + version: 4.3.4 + resolution: "debug@npm:4.3.4" + dependencies: + ms: "npm:2.1.2" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 3dbad3f94ea64f34431a9cbf0bafb61853eda57bff2880036153438f50fb5a84f27683ba0d8e5426bf41a8c6ff03879488120cf5b3a761e77953169c0600a708 + languageName: node + linkType: hard + +"delay@npm:^5.0.0": + version: 5.0.0 + resolution: "delay@npm:5.0.0" + checksum: 62f151151ecfde0d9afbb8a6be37a6d103c4cb24f35a20ef3fe56f920b0d0d0bb02bc9c0a3084d0179ef669ca332b91155f2ee4d9854622cd2cdba5fc95285f9 + languageName: node + linkType: hard + +"delegates@npm:^1.0.0": + version: 1.0.0 + resolution: "delegates@npm:1.0.0" + checksum: a51744d9b53c164ba9c0492471a1a2ffa0b6727451bdc89e31627fdf4adda9d51277cfcbfb20f0a6f08ccb3c436f341df3e92631a3440226d93a8971724771fd + languageName: node + linkType: hard + +"diff@npm:^4.0.1": + version: 4.0.2 + resolution: "diff@npm:4.0.2" + checksum: f2c09b0ce4e6b301c221addd83bf3f454c0bc00caa3dd837cf6c127d6edf7223aa2bbe3b688feea110b7f262adbfc845b757c44c8a9f8c0c5b15d8fa9ce9d20d + languageName: node + linkType: hard + +"dot-case@npm:^3.0.4": + version: 3.0.4 + resolution: "dot-case@npm:3.0.4" + dependencies: + no-case: "npm:^3.0.4" + tslib: "npm:^2.0.3" + checksum: a65e3519414856df0228b9f645332f974f2bf5433370f544a681122eab59e66038fc3349b4be1cdc47152779dac71a5864f1ccda2f745e767c46e9c6543b1169 + languageName: node + linkType: hard + +"eastasianwidth@npm:^0.2.0": + version: 0.2.0 + resolution: "eastasianwidth@npm:0.2.0" + checksum: 7d00d7cd8e49b9afa762a813faac332dee781932d6f2c848dc348939c4253f1d4564341b7af1d041853bc3f32c2ef141b58e0a4d9862c17a7f08f68df1e0f1ed + languageName: node + linkType: hard + +"emoji-regex@npm:^8.0.0": + version: 8.0.0 + resolution: "emoji-regex@npm:8.0.0" + checksum: d4c5c39d5a9868b5fa152f00cada8a936868fd3367f33f71be515ecee4c803132d11b31a6222b2571b1e5f7e13890156a94880345594d0ce7e3c9895f560f192 + languageName: node + linkType: hard + +"emoji-regex@npm:^9.2.2": + version: 9.2.2 + resolution: "emoji-regex@npm:9.2.2" + checksum: 8487182da74aabd810ac6d6f1994111dfc0e331b01271ae01ec1eb0ad7b5ecc2bbbbd2f053c05cb55a1ac30449527d819bbfbf0e3de1023db308cbcb47f86601 + languageName: node + linkType: hard + +"encoding@npm:^0.1.13": + version: 0.1.13 + resolution: "encoding@npm:0.1.13" + dependencies: + iconv-lite: "npm:^0.6.2" + checksum: bb98632f8ffa823996e508ce6a58ffcf5856330fde839ae42c9e1f436cc3b5cc651d4aeae72222916545428e54fd0f6aa8862fd8d25bdbcc4589f1e3f3715e7f + languageName: node + linkType: hard + +"enhanced-resolve@npm:^5.0.0": + version: 5.15.0 + resolution: "enhanced-resolve@npm:5.15.0" + dependencies: + graceful-fs: "npm:^4.2.4" + tapable: "npm:^2.2.0" + checksum: fbd8cdc9263be71cc737aa8a7d6c57b43d6aa38f6cc75dde6fcd3598a130cc465f979d2f4d01bb3bf475acb43817749c79f8eef9be048683602ca91ab52e4f11 + languageName: node + linkType: hard + +"env-paths@npm:^2.2.0": + version: 2.2.1 + resolution: "env-paths@npm:2.2.1" + checksum: 65b5df55a8bab92229ab2b40dad3b387fad24613263d103a97f91c9fe43ceb21965cd3392b1ccb5d77088021e525c4e0481adb309625d0cb94ade1d1fb8dc17e + languageName: node + linkType: hard + +"err-code@npm:^2.0.2": + version: 2.0.3 + resolution: "err-code@npm:2.0.3" + checksum: 8b7b1be20d2de12d2255c0bc2ca638b7af5171142693299416e6a9339bd7d88fc8d7707d913d78e0993176005405a236b066b45666b27b797252c771156ace54 + languageName: node + linkType: hard + +"es6-promise@npm:^4.0.3": + version: 4.2.8 + resolution: "es6-promise@npm:4.2.8" + checksum: 95614a88873611cb9165a85d36afa7268af5c03a378b35ca7bda9508e1d4f1f6f19a788d4bc755b3fd37c8ebba40782018e02034564ff24c9d6fa37e959ad57d + languageName: node + linkType: hard + +"es6-promisify@npm:^5.0.0": + version: 5.0.0 + resolution: "es6-promisify@npm:5.0.0" + dependencies: + es6-promise: "npm:^4.0.3" + checksum: fbed9d791598831413be84a5374eca8c24800ec71a16c1c528c43a98e2dadfb99331483d83ae6094ddb9b87e6f799a15d1553cebf756047e0865c753bc346b92 + languageName: node + linkType: hard + +"eventemitter3@npm:^4.0.7": + version: 4.0.7 + resolution: "eventemitter3@npm:4.0.7" + checksum: 1875311c42fcfe9c707b2712c32664a245629b42bb0a5a84439762dd0fd637fc54d078155ea83c2af9e0323c9ac13687e03cfba79b03af9f40c89b4960099374 + languageName: node + linkType: hard + +"exponential-backoff@npm:^3.1.1": + version: 3.1.1 + resolution: "exponential-backoff@npm:3.1.1" + checksum: 3d21519a4f8207c99f7457287291316306255a328770d320b401114ec8481986e4e467e854cb9914dd965e0a1ca810a23ccb559c642c88f4c7f55c55778a9b48 + languageName: node + linkType: hard + +"eyes@npm:^0.1.8": + version: 0.1.8 + resolution: "eyes@npm:0.1.8" + checksum: c31703a92bf36ba75ee8d379ee7985c24ee6149f3a6175f44cec7a05b178c38bce9836d3ca48c9acb0329a960ac2c4b2ead4e60cdd4fe6e8c92cad7cd6913687 + languageName: node + linkType: hard + +"fast-stable-stringify@npm:^1.0.0": + version: 1.0.0 + resolution: "fast-stable-stringify@npm:1.0.0" + checksum: ef1203d246a7e8ac15e2bfbda0a89fa375947bccf9f7910be0ea759856dbe8ea5024a0d8cc2cceabe18a9cb67e95927b78bb6173a3ae37ec55a518cf36e5244b + languageName: node + linkType: hard + +"file-uri-to-path@npm:1.0.0": + version: 1.0.0 + resolution: "file-uri-to-path@npm:1.0.0" + checksum: b648580bdd893a008c92c7ecc96c3ee57a5e7b6c4c18a9a09b44fb5d36d79146f8e442578bc0e173dc027adf3987e254ba1dfd6e3ec998b7c282873010502144 + languageName: node + linkType: hard + +"fill-range@npm:^7.0.1": + version: 7.0.1 + resolution: "fill-range@npm:7.0.1" + dependencies: + to-regex-range: "npm:^5.0.1" + checksum: cc283f4e65b504259e64fd969bcf4def4eb08d85565e906b7d36516e87819db52029a76b6363d0f02d0d532f0033c9603b9e2d943d56ee3b0d4f7ad3328ff917 + languageName: node + linkType: hard + +"foreground-child@npm:^3.1.0": + version: 3.1.1 + resolution: "foreground-child@npm:3.1.1" + dependencies: + cross-spawn: "npm:^7.0.0" + signal-exit: "npm:^4.0.1" + checksum: 139d270bc82dc9e6f8bc045fe2aae4001dc2472157044fdfad376d0a3457f77857fa883c1c8b21b491c6caade9a926a4bed3d3d2e8d3c9202b151a4cbbd0bcd5 + languageName: node + linkType: hard + +"fs-minipass@npm:^2.0.0": + version: 2.1.0 + resolution: "fs-minipass@npm:2.1.0" + dependencies: + minipass: "npm:^3.0.0" + checksum: 1b8d128dae2ac6cc94230cc5ead341ba3e0efaef82dab46a33d171c044caaa6ca001364178d42069b2809c35a1c3c35079a32107c770e9ffab3901b59af8c8b1 + languageName: node + linkType: hard + +"fs-minipass@npm:^3.0.0": + version: 3.0.3 + resolution: "fs-minipass@npm:3.0.3" + dependencies: + minipass: "npm:^7.0.3" + checksum: 8722a41109130851d979222d3ec88aabaceeaaf8f57b2a8f744ef8bd2d1ce95453b04a61daa0078822bc5cd21e008814f06fe6586f56fef511e71b8d2394d802 + languageName: node + linkType: hard + +"fs.realpath@npm:^1.0.0": + version: 1.0.0 + resolution: "fs.realpath@npm:1.0.0" + checksum: 99ddea01a7e75aa276c250a04eedeffe5662bce66c65c07164ad6264f9de18fb21be9433ead460e54cff20e31721c811f4fb5d70591799df5f85dce6d6746fd0 + languageName: node + linkType: hard + +"gauge@npm:^4.0.3": + version: 4.0.4 + resolution: "gauge@npm:4.0.4" + dependencies: + aproba: "npm:^1.0.3 || ^2.0.0" + color-support: "npm:^1.1.3" + console-control-strings: "npm:^1.1.0" + has-unicode: "npm:^2.0.1" + signal-exit: "npm:^3.0.7" + string-width: "npm:^4.2.3" + strip-ansi: "npm:^6.0.1" + wide-align: "npm:^1.1.5" + checksum: 788b6bfe52f1dd8e263cda800c26ac0ca2ff6de0b6eee2fe0d9e3abf15e149b651bd27bf5226be10e6e3edb5c4e5d5985a5a1a98137e7a892f75eff76467ad2d + languageName: node + linkType: hard + +"git-format-staged@npm:^2.1.3": + version: 2.1.3 + resolution: "git-format-staged@npm:2.1.3" + bin: + git-format-staged: git-format-staged + checksum: 749da68f0d9bf24db53b87a5f1613fc1a8790801d7c3ccb31d02b94d99f4cf2450126ef565f16adcc0649fbbf90dc44b4f009d4f99ff8a26921ba754bdb09b31 + languageName: node + linkType: hard + +"glob@npm:^10.2.2": + version: 10.3.3 + resolution: "glob@npm:10.3.3" + dependencies: + foreground-child: "npm:^3.1.0" + jackspeak: "npm:^2.0.3" + minimatch: "npm:^9.0.1" + minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" + path-scurry: "npm:^1.10.1" + bin: + glob: dist/cjs/src/bin.js + checksum: 29190d3291f422da0cb40b77a72fc8d2c51a36524e99b8bf412548b7676a6627489528b57250429612b6eec2e6fe7826d328451d3e694a9d15e575389308ec53 + languageName: node + linkType: hard + +"glob@npm:^7.1.3, glob@npm:^7.1.4": + version: 7.2.3 + resolution: "glob@npm:7.2.3" + dependencies: + fs.realpath: "npm:^1.0.0" + inflight: "npm:^1.0.4" + inherits: "npm:2" + minimatch: "npm:^3.1.1" + once: "npm:^1.3.0" + path-is-absolute: "npm:^1.0.0" + checksum: 29452e97b38fa704dabb1d1045350fb2467cf0277e155aa9ff7077e90ad81d1ea9d53d3ee63bd37c05b09a065e90f16aec4a65f5b8de401d1dac40bc5605d133 + languageName: node + linkType: hard + +"graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6": + version: 4.2.11 + resolution: "graceful-fs@npm:4.2.11" + checksum: ac85f94da92d8eb6b7f5a8b20ce65e43d66761c55ce85ac96df6865308390da45a8d3f0296dd3a663de65d30ba497bd46c696cc1e248c72b13d6d567138a4fc7 + languageName: node + linkType: hard + +"has-flag@npm:^4.0.0": + version: 4.0.0 + resolution: "has-flag@npm:4.0.0" + checksum: 261a1357037ead75e338156b1f9452c016a37dcd3283a972a30d9e4a87441ba372c8b81f818cd0fbcd9c0354b4ae7e18b9e1afa1971164aef6d18c2b6095a8ad + languageName: node + linkType: hard + +"has-unicode@npm:^2.0.1": + version: 2.0.1 + resolution: "has-unicode@npm:2.0.1" + checksum: 1eab07a7436512db0be40a710b29b5dc21fa04880b7f63c9980b706683127e3c1b57cb80ea96d47991bdae2dfe479604f6a1ba410106ee1046a41d1bd0814400 + languageName: node + linkType: hard + +"http-cache-semantics@npm:^4.1.1": + version: 4.1.1 + resolution: "http-cache-semantics@npm:4.1.1" + checksum: 83ac0bc60b17a3a36f9953e7be55e5c8f41acc61b22583060e8dedc9dd5e3607c823a88d0926f9150e571f90946835c7fe150732801010845c72cd8bbff1a236 + languageName: node + linkType: hard + +"http-proxy-agent@npm:^5.0.0": + version: 5.0.0 + resolution: "http-proxy-agent@npm:5.0.0" + dependencies: + "@tootallnate/once": "npm:2" + agent-base: "npm:6" + debug: "npm:4" + checksum: e2ee1ff1656a131953839b2a19cd1f3a52d97c25ba87bd2559af6ae87114abf60971e498021f9b73f9fd78aea8876d1fb0d4656aac8a03c6caa9fc175f22b786 + languageName: node + linkType: hard + +"https-proxy-agent@npm:^5.0.0": + version: 5.0.1 + resolution: "https-proxy-agent@npm:5.0.1" + dependencies: + agent-base: "npm:6" + debug: "npm:4" + checksum: 571fccdf38184f05943e12d37d6ce38197becdd69e58d03f43637f7fa1269cf303a7d228aa27e5b27bbd3af8f09fd938e1c91dcfefff2df7ba77c20ed8dfc765 + languageName: node + linkType: hard + +"humanize-ms@npm:^1.2.1": + version: 1.2.1 + resolution: "humanize-ms@npm:1.2.1" + dependencies: + ms: "npm:^2.0.0" + checksum: 9c7a74a2827f9294c009266c82031030eae811ca87b0da3dceb8d6071b9bde22c9f3daef0469c3c533cc67a97d8a167cd9fc0389350e5f415f61a79b171ded16 + languageName: node + linkType: hard + +"iconv-lite@npm:^0.6.2": + version: 0.6.3 + resolution: "iconv-lite@npm:0.6.3" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3.0.0" + checksum: 3f60d47a5c8fc3313317edfd29a00a692cc87a19cac0159e2ce711d0ebc9019064108323b5e493625e25594f11c6236647d8e256fbe7a58f4a3b33b89e6d30bf + languageName: node + linkType: hard + +"ieee754@npm:^1.2.1": + version: 1.2.1 + resolution: "ieee754@npm:1.2.1" + checksum: 5144c0c9815e54ada181d80a0b810221a253562422e7c6c3a60b1901154184f49326ec239d618c416c1c5945a2e197107aee8d986a3dd836b53dffefd99b5e7e + languageName: node + linkType: hard + +"imurmurhash@npm:^0.1.4": + version: 0.1.4 + resolution: "imurmurhash@npm:0.1.4" + checksum: 7cae75c8cd9a50f57dadd77482359f659eaebac0319dd9368bcd1714f55e65badd6929ca58569da2b6494ef13fdd5598cd700b1eba23f8b79c5f19d195a3ecf7 + languageName: node + linkType: hard + +"indent-string@npm:^4.0.0": + version: 4.0.0 + resolution: "indent-string@npm:4.0.0" + checksum: 824cfb9929d031dabf059bebfe08cf3137365e112019086ed3dcff6a0a7b698cb80cf67ccccde0e25b9e2d7527aa6cc1fed1ac490c752162496caba3e6699612 + languageName: node + linkType: hard + +"inflight@npm:^1.0.4": + version: 1.0.6 + resolution: "inflight@npm:1.0.6" + dependencies: + once: "npm:^1.3.0" + wrappy: "npm:1" + checksum: f4f76aa072ce19fae87ce1ef7d221e709afb59d445e05d47fba710e85470923a75de35bfae47da6de1b18afc3ce83d70facf44cfb0aff89f0a3f45c0a0244dfd + languageName: node + linkType: hard + +"inherits@npm:2, inherits@npm:^2.0.3": + version: 2.0.4 + resolution: "inherits@npm:2.0.4" + checksum: 4a48a733847879d6cf6691860a6b1e3f0f4754176e4d71494c41f3475553768b10f84b5ce1d40fbd0e34e6bfbb864ee35858ad4dd2cf31e02fc4a154b724d7f1 + languageName: node + linkType: hard + +"ip@npm:^2.0.0": + version: 2.0.0 + resolution: "ip@npm:2.0.0" + checksum: cfcfac6b873b701996d71ec82a7dd27ba92450afdb421e356f44044ed688df04567344c36cbacea7d01b1c39a4c732dc012570ebe9bebfb06f27314bca625349 + languageName: node + linkType: hard + +"is-fullwidth-code-point@npm:^3.0.0": + version: 3.0.0 + resolution: "is-fullwidth-code-point@npm:3.0.0" + checksum: 44a30c29457c7fb8f00297bce733f0a64cd22eca270f83e58c105e0d015e45c019491a4ab2faef91ab51d4738c670daff901c799f6a700e27f7314029e99e348 + languageName: node + linkType: hard + +"is-lambda@npm:^1.0.1": + version: 1.0.1 + resolution: "is-lambda@npm:1.0.1" + checksum: 93a32f01940220532e5948538699ad610d5924ac86093fcee83022252b363eb0cc99ba53ab084a04e4fb62bf7b5731f55496257a4c38adf87af9c4d352c71c35 + languageName: node + linkType: hard + +"is-number@npm:^7.0.0": + version: 7.0.0 + resolution: "is-number@npm:7.0.0" + checksum: 456ac6f8e0f3111ed34668a624e45315201dff921e5ac181f8ec24923b99e9f32ca1a194912dc79d539c97d33dba17dc635202ff0b2cf98326f608323276d27a + languageName: node + linkType: hard + +"isexe@npm:^2.0.0": + version: 2.0.0 + resolution: "isexe@npm:2.0.0" + checksum: 26bf6c5480dda5161c820c5b5c751ae1e766c587b1f951ea3fcfc973bafb7831ae5b54a31a69bd670220e42e99ec154475025a468eae58ea262f813fdc8d1c62 + languageName: node + linkType: hard + +"isomorphic-ws@npm:^4.0.1": + version: 4.0.1 + resolution: "isomorphic-ws@npm:4.0.1" + peerDependencies: + ws: "*" + checksum: d7190eadefdc28bdb93d67b5f0c603385aaf87724fa2974abb382ac1ec9756ed2cfb27065cbe76122879c2d452e2982bc4314317f3d6c737ddda6c047328771a + languageName: node + linkType: hard + +"jackspeak@npm:^2.0.3": + version: 2.3.1 + resolution: "jackspeak@npm:2.3.1" + dependencies: + "@isaacs/cliui": "npm:^8.0.2" + "@pkgjs/parseargs": "npm:^0.11.0" + dependenciesMeta: + "@pkgjs/parseargs": + optional: true + checksum: 34ea4d618d8d36ac104fe1053c85dfb6a63306cfe87e157ef42f18a7aa30027887370a4e163dd4993e45c6bf8a8ae003bf8476fdb8538e8ee5cd1938c27b15d0 + languageName: node + linkType: hard + +"jayson@npm:^4.1.0": + version: 4.1.0 + resolution: "jayson@npm:4.1.0" + dependencies: + "@types/connect": "npm:^3.4.33" + "@types/node": "npm:^12.12.54" + "@types/ws": "npm:^7.4.4" + JSONStream: "npm:^1.3.5" + commander: "npm:^2.20.3" + delay: "npm:^5.0.0" + es6-promisify: "npm:^5.0.0" + eyes: "npm:^0.1.8" + isomorphic-ws: "npm:^4.0.1" + json-stringify-safe: "npm:^5.0.1" + uuid: "npm:^8.3.2" + ws: "npm:^7.4.5" + bin: + jayson: bin/jayson.js + checksum: 86464322fbdc6db65d2bb4fc278cb6c86fad5c2a506065490d39459f09ba0d30f2b4fb740b33828a1424791419b6c8bd295dc54d361a4ad959bf70cc62b1ca7e + languageName: node + linkType: hard + +"js-sha256@npm:^0.9.0": + version: 0.9.0 + resolution: "js-sha256@npm:0.9.0" + checksum: ffad54b3373f81581e245866abfda50a62c483803a28176dd5c28fd2d313e0bdf830e77dac7ff8afd193c53031618920f3d98daf21cbbe80082753ab639c0365 + languageName: node + linkType: hard + +"json-stringify-safe@npm:^5.0.1": + version: 5.0.1 + resolution: "json-stringify-safe@npm:5.0.1" + checksum: 48ec0adad5280b8a96bb93f4563aa1667fd7a36334f79149abd42446d0989f2ddc58274b479f4819f1f00617957e6344c886c55d05a4e15ebb4ab931e4a6a8ee + languageName: node + linkType: hard + +"jsonparse@npm:^1.2.0": + version: 1.3.1 + resolution: "jsonparse@npm:1.3.1" + checksum: 6514a7be4674ebf407afca0eda3ba284b69b07f9958a8d3113ef1005f7ec610860c312be067e450c569aab8b89635e332cee3696789c750692bb60daba627f4d + languageName: node + linkType: hard + +"lower-case@npm:^2.0.2": + version: 2.0.2 + resolution: "lower-case@npm:2.0.2" + dependencies: + tslib: "npm:^2.0.3" + checksum: 83a0a5f159ad7614bee8bf976b96275f3954335a84fad2696927f609ddae902802c4f3312d86668722e668bef41400254807e1d3a7f2e8c3eede79691aa1f010 + languageName: node + linkType: hard + +"lru-cache@npm:^6.0.0": + version: 6.0.0 + resolution: "lru-cache@npm:6.0.0" + dependencies: + yallist: "npm:^4.0.0" + checksum: f97f499f898f23e4585742138a22f22526254fdba6d75d41a1c2526b3b6cc5747ef59c5612ba7375f42aca4f8461950e925ba08c991ead0651b4918b7c978297 + languageName: node + linkType: hard + +"lru-cache@npm:^7.7.1": + version: 7.18.3 + resolution: "lru-cache@npm:7.18.3" + checksum: e550d772384709deea3f141af34b6d4fa392e2e418c1498c078de0ee63670f1f46f5eee746e8ef7e69e1c895af0d4224e62ee33e66a543a14763b0f2e74c1356 + languageName: node + linkType: hard + +"lru-cache@npm:^9.1.1 || ^10.0.0": + version: 10.0.1 + resolution: "lru-cache@npm:10.0.1" + checksum: 06f8d0e1ceabd76bb6f644a26dbb0b4c471b79c7b514c13c6856113879b3bf369eb7b497dad4ff2b7e2636db202412394865b33c332100876d838ad1372f0181 + languageName: node + linkType: hard + +"make-error@npm:^1.1.1": + version: 1.3.6 + resolution: "make-error@npm:1.3.6" + checksum: b86e5e0e25f7f777b77fabd8e2cbf15737972869d852a22b7e73c17623928fccb826d8e46b9951501d3f20e51ad74ba8c59ed584f610526a48f8ccf88aaec402 + languageName: node + linkType: hard + +"make-fetch-happen@npm:^11.0.3": + version: 11.1.1 + resolution: "make-fetch-happen@npm:11.1.1" + dependencies: + agentkeepalive: "npm:^4.2.1" + cacache: "npm:^17.0.0" + http-cache-semantics: "npm:^4.1.1" + http-proxy-agent: "npm:^5.0.0" + https-proxy-agent: "npm:^5.0.0" + is-lambda: "npm:^1.0.1" + lru-cache: "npm:^7.7.1" + minipass: "npm:^5.0.0" + minipass-fetch: "npm:^3.0.0" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + negotiator: "npm:^0.6.3" + promise-retry: "npm:^2.0.1" + socks-proxy-agent: "npm:^7.0.0" + ssri: "npm:^10.0.0" + checksum: 7268bf274a0f6dcf0343829489a4506603ff34bd0649c12058753900b0eb29191dce5dba12680719a5d0a983d3e57810f594a12f3c18494e93a1fbc6348a4540 + languageName: node + linkType: hard + +"micromatch@npm:^4.0.0": + version: 4.0.5 + resolution: "micromatch@npm:4.0.5" + dependencies: + braces: "npm:^3.0.2" + picomatch: "npm:^2.3.1" + checksum: 02a17b671c06e8fefeeb6ef996119c1e597c942e632a21ef589154f23898c9c6a9858526246abb14f8bca6e77734aa9dcf65476fca47cedfb80d9577d52843fc + languageName: node + linkType: hard + +"minimatch@npm:^3.1.1": + version: 3.1.2 + resolution: "minimatch@npm:3.1.2" + dependencies: + brace-expansion: "npm:^1.1.7" + checksum: c154e566406683e7bcb746e000b84d74465b3a832c45d59912b9b55cd50dee66e5c4b1e5566dba26154040e51672f9aa450a9aef0c97cfc7336b78b7afb9540a + languageName: node + linkType: hard + +"minimatch@npm:^9.0.1": + version: 9.0.3 + resolution: "minimatch@npm:9.0.3" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: 253487976bf485b612f16bf57463520a14f512662e592e95c571afdab1442a6a6864b6c88f248ce6fc4ff0b6de04ac7aa6c8bb51e868e99d1d65eb0658a708b5 + languageName: node + linkType: hard + +"minipass-collect@npm:^1.0.2": + version: 1.0.2 + resolution: "minipass-collect@npm:1.0.2" + dependencies: + minipass: "npm:^3.0.0" + checksum: 14df761028f3e47293aee72888f2657695ec66bd7d09cae7ad558da30415fdc4752bbfee66287dcc6fd5e6a2fa3466d6c484dc1cbd986525d9393b9523d97f10 + languageName: node + linkType: hard + +"minipass-fetch@npm:^3.0.0": + version: 3.0.4 + resolution: "minipass-fetch@npm:3.0.4" + dependencies: + encoding: "npm:^0.1.13" + minipass: "npm:^7.0.3" + minipass-sized: "npm:^1.0.3" + minizlib: "npm:^2.1.2" + dependenciesMeta: + encoding: + optional: true + checksum: af7aad15d5c128ab1ebe52e043bdf7d62c3c6f0cecb9285b40d7b395e1375b45dcdfd40e63e93d26a0e8249c9efd5c325c65575aceee192883970ff8cb11364a + languageName: node + linkType: hard + +"minipass-flush@npm:^1.0.5": + version: 1.0.5 + resolution: "minipass-flush@npm:1.0.5" + dependencies: + minipass: "npm:^3.0.0" + checksum: 56269a0b22bad756a08a94b1ffc36b7c9c5de0735a4dd1ab2b06c066d795cfd1f0ac44a0fcae13eece5589b908ecddc867f04c745c7009be0b566421ea0944cf + languageName: node + linkType: hard + +"minipass-pipeline@npm:^1.2.4": + version: 1.2.4 + resolution: "minipass-pipeline@npm:1.2.4" + dependencies: + minipass: "npm:^3.0.0" + checksum: b14240dac0d29823c3d5911c286069e36d0b81173d7bdf07a7e4a91ecdef92cdff4baaf31ea3746f1c61e0957f652e641223970870e2353593f382112257971b + languageName: node + linkType: hard + +"minipass-sized@npm:^1.0.3": + version: 1.0.3 + resolution: "minipass-sized@npm:1.0.3" + dependencies: + minipass: "npm:^3.0.0" + checksum: 79076749fcacf21b5d16dd596d32c3b6bf4d6e62abb43868fac21674078505c8b15eaca4e47ed844985a4514854f917d78f588fcd029693709417d8f98b2bd60 + languageName: node + linkType: hard + +"minipass@npm:^3.0.0": + version: 3.3.6 + resolution: "minipass@npm:3.3.6" + dependencies: + yallist: "npm:^4.0.0" + checksum: a30d083c8054cee83cdcdc97f97e4641a3f58ae743970457b1489ce38ee1167b3aaf7d815cd39ec7a99b9c40397fd4f686e83750e73e652b21cb516f6d845e48 + languageName: node + linkType: hard + +"minipass@npm:^5.0.0": + version: 5.0.0 + resolution: "minipass@npm:5.0.0" + checksum: 425dab288738853fded43da3314a0b5c035844d6f3097a8e3b5b29b328da8f3c1af6fc70618b32c29ff906284cf6406b6841376f21caaadd0793c1d5a6a620ea + languageName: node + linkType: hard + +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.3": + version: 7.0.3 + resolution: "minipass@npm:7.0.3" + checksum: 6f1614f5b5b55568a46bca5fec0e7c46dac027691db27d0e1923a8192866903144cd962ac772c0e9f89b608ea818b702709c042bce98e190d258847d85461531 + languageName: node + linkType: hard + +"minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": + version: 2.1.2 + resolution: "minizlib@npm:2.1.2" + dependencies: + minipass: "npm:^3.0.0" + yallist: "npm:^4.0.0" + checksum: f1fdeac0b07cf8f30fcf12f4b586795b97be856edea22b5e9072707be51fc95d41487faec3f265b42973a304fe3a64acd91a44a3826a963e37b37bafde0212c3 + languageName: node + linkType: hard + +"mkdirp@npm:^1.0.3": + version: 1.0.4 + resolution: "mkdirp@npm:1.0.4" + bin: + mkdirp: bin/cmd.js + checksum: a96865108c6c3b1b8e1d5e9f11843de1e077e57737602de1b82030815f311be11f96f09cce59bd5b903d0b29834733e5313f9301e3ed6d6f6fba2eae0df4298f + languageName: node + linkType: hard + +"ms@npm:2.1.2": + version: 2.1.2 + resolution: "ms@npm:2.1.2" + checksum: 673cdb2c3133eb050c745908d8ce632ed2c02d85640e2edb3ace856a2266a813b30c613569bf3354fdf4ea7d1a1494add3bfa95e2713baa27d0c2c71fc44f58f + languageName: node + linkType: hard + +"ms@npm:^2.0.0": + version: 2.1.3 + resolution: "ms@npm:2.1.3" + checksum: aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d + languageName: node + linkType: hard + +"negotiator@npm:^0.6.3": + version: 0.6.3 + resolution: "negotiator@npm:0.6.3" + checksum: b8ffeb1e262eff7968fc90a2b6767b04cfd9842582a9d0ece0af7049537266e7b2506dfb1d107a32f06dd849ab2aea834d5830f7f4d0e5cb7d36e1ae55d021d9 + languageName: node + linkType: hard + +"no-case@npm:^3.0.4": + version: 3.0.4 + resolution: "no-case@npm:3.0.4" + dependencies: + lower-case: "npm:^2.0.2" + tslib: "npm:^2.0.3" + checksum: 0b2ebc113dfcf737d48dde49cfebf3ad2d82a8c3188e7100c6f375e30eafbef9e9124aadc3becef237b042fd5eb0aad2fd78669c20972d045bbe7fea8ba0be5c + languageName: node + linkType: hard + +"node-fetch@npm:^2.6.12": + version: 2.7.0 + resolution: "node-fetch@npm:2.7.0" + dependencies: + whatwg-url: "npm:^5.0.0" + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + checksum: d76d2f5edb451a3f05b15115ec89fc6be39de37c6089f1b6368df03b91e1633fd379a7e01b7ab05089a25034b2023d959b47e59759cb38d88341b2459e89d6e5 + languageName: node + linkType: hard + +"node-gyp-build@npm:^4.3.0": + version: 4.6.1 + resolution: "node-gyp-build@npm:4.6.1" + bin: + node-gyp-build: bin.js + node-gyp-build-optional: optional.js + node-gyp-build-test: build-test.js + checksum: c3676d337b36803bc7792e35bf7fdcda7cdcb7e289b8f9855a5535702a82498eb976842fefcf487258c58005ca32ce3d537fbed91280b04409161dcd7232a882 + languageName: node + linkType: hard + +"node-gyp@npm:latest": + version: 9.4.0 + resolution: "node-gyp@npm:9.4.0" + dependencies: + env-paths: "npm:^2.2.0" + exponential-backoff: "npm:^3.1.1" + glob: "npm:^7.1.4" + graceful-fs: "npm:^4.2.6" + make-fetch-happen: "npm:^11.0.3" + nopt: "npm:^6.0.0" + npmlog: "npm:^6.0.0" + rimraf: "npm:^3.0.2" + semver: "npm:^7.3.5" + tar: "npm:^6.1.2" + which: "npm:^2.0.2" + bin: + node-gyp: bin/node-gyp.js + checksum: 78b404e2e0639d64e145845f7f5a3cb20c0520cdaf6dda2f6e025e9b644077202ea7de1232396ba5bde3fee84cdc79604feebe6ba3ec84d464c85d407bb5da99 + languageName: node + linkType: hard + +"nopt@npm:^6.0.0": + version: 6.0.0 + resolution: "nopt@npm:6.0.0" + dependencies: + abbrev: "npm:^1.0.0" + bin: + nopt: bin/nopt.js + checksum: 82149371f8be0c4b9ec2f863cc6509a7fd0fa729929c009f3a58e4eb0c9e4cae9920e8f1f8eb46e7d032fec8fb01bede7f0f41a67eb3553b7b8e14fa53de1dac + languageName: node + linkType: hard + +"npmlog@npm:^6.0.0": + version: 6.0.2 + resolution: "npmlog@npm:6.0.2" + dependencies: + are-we-there-yet: "npm:^3.0.0" + console-control-strings: "npm:^1.1.0" + gauge: "npm:^4.0.3" + set-blocking: "npm:^2.0.0" + checksum: ae238cd264a1c3f22091cdd9e2b106f684297d3c184f1146984ecbe18aaa86343953f26b9520dedd1b1372bc0316905b736c1932d778dbeb1fcf5a1001390e2a + languageName: node + linkType: hard + +"once@npm:^1.3.0": + version: 1.4.0 + resolution: "once@npm:1.4.0" + dependencies: + wrappy: "npm:1" + checksum: cd0a88501333edd640d95f0d2700fbde6bff20b3d4d9bdc521bdd31af0656b5706570d6c6afe532045a20bb8dc0849f8332d6f2a416e0ba6d3d3b98806c7db68 + languageName: node + linkType: hard + +"p-map@npm:^4.0.0": + version: 4.0.0 + resolution: "p-map@npm:4.0.0" + dependencies: + aggregate-error: "npm:^3.0.0" + checksum: cb0ab21ec0f32ddffd31dfc250e3afa61e103ef43d957cc45497afe37513634589316de4eb88abdfd969fe6410c22c0b93ab24328833b8eb1ccc087fc0442a1c + languageName: node + linkType: hard + +"pako@npm:^2.0.3": + version: 2.1.0 + resolution: "pako@npm:2.1.0" + checksum: 71666548644c9a4d056bcaba849ca6fd7242c6cf1af0646d3346f3079a1c7f4a66ffec6f7369ee0dc88f61926c10d6ab05da3e1fca44b83551839e89edd75a3e + languageName: node + linkType: hard + +"path-is-absolute@npm:^1.0.0": + version: 1.0.1 + resolution: "path-is-absolute@npm:1.0.1" + checksum: 060840f92cf8effa293bcc1bea81281bd7d363731d214cbe5c227df207c34cd727430f70c6037b5159c8a870b9157cba65e775446b0ab06fd5ecc7e54615a3b8 + languageName: node + linkType: hard + +"path-key@npm:^3.1.0": + version: 3.1.1 + resolution: "path-key@npm:3.1.1" + checksum: 55cd7a9dd4b343412a8386a743f9c746ef196e57c823d90ca3ab917f90ab9f13dd0ded27252ba49dbdfcab2b091d998bc446f6220cd3cea65db407502a740020 + languageName: node + linkType: hard + +"path-scurry@npm:^1.10.1": + version: 1.10.1 + resolution: "path-scurry@npm:1.10.1" + dependencies: + lru-cache: "npm:^9.1.1 || ^10.0.0" + minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" + checksum: e2557cff3a8fb8bc07afdd6ab163a92587884f9969b05bbbaf6fe7379348bfb09af9ed292af12ed32398b15fb443e81692047b786d1eeb6d898a51eb17ed7d90 + languageName: node + linkType: hard + +"picomatch@npm:^2.3.1": + version: 2.3.1 + resolution: "picomatch@npm:2.3.1" + checksum: 050c865ce81119c4822c45d3c84f1ced46f93a0126febae20737bd05ca20589c564d6e9226977df859ed5e03dc73f02584a2b0faad36e896936238238b0446cf + languageName: node + linkType: hard + +"promise-retry@npm:^2.0.1": + version: 2.0.1 + resolution: "promise-retry@npm:2.0.1" + dependencies: + err-code: "npm:^2.0.2" + retry: "npm:^0.12.0" + checksum: f96a3f6d90b92b568a26f71e966cbbc0f63ab85ea6ff6c81284dc869b41510e6cdef99b6b65f9030f0db422bf7c96652a3fff9f2e8fb4a0f069d8f4430359429 + languageName: node + linkType: hard + +"readable-stream@npm:^3.6.0": + version: 3.6.2 + resolution: "readable-stream@npm:3.6.2" + dependencies: + inherits: "npm:^2.0.3" + string_decoder: "npm:^1.1.1" + util-deprecate: "npm:^1.0.1" + checksum: bdcbe6c22e846b6af075e32cf8f4751c2576238c5043169a1c221c92ee2878458a816a4ea33f4c67623c0b6827c8a400409bfb3cf0bf3381392d0b1dfb52ac8d + languageName: node + linkType: hard + +"regenerator-runtime@npm:^0.14.0": + version: 0.14.0 + resolution: "regenerator-runtime@npm:0.14.0" + checksum: 1c977ad82a82a4412e4f639d65d22be376d3ebdd30da2c003eeafdaaacd03fc00c2320f18120007ee700900979284fc78a9f00da7fb593f6e6eeebc673fba9a3 + languageName: node + linkType: hard + +"retry@npm:^0.12.0": + version: 0.12.0 + resolution: "retry@npm:0.12.0" + checksum: 623bd7d2e5119467ba66202d733ec3c2e2e26568074923bc0585b6b99db14f357e79bdedb63cab56cec47491c4a0da7e6021a7465ca6dc4f481d3898fdd3158c + languageName: node + linkType: hard + +"rimraf@npm:^3.0.2": + version: 3.0.2 + resolution: "rimraf@npm:3.0.2" + dependencies: + glob: "npm:^7.1.3" + bin: + rimraf: bin.js + checksum: 87f4164e396f0171b0a3386cc1877a817f572148ee13a7e113b238e48e8a9f2f31d009a92ec38a591ff1567d9662c6b67fd8818a2dbbaed74bc26a87a2a4a9a0 + languageName: node + linkType: hard + +"rpc-websockets@npm:^7.5.1": + version: 7.6.0 + resolution: "rpc-websockets@npm:7.6.0" + dependencies: + "@babel/runtime": "npm:^7.17.2" + bufferutil: "npm:^4.0.1" + eventemitter3: "npm:^4.0.7" + utf-8-validate: "npm:^5.0.2" + uuid: "npm:^8.3.2" + ws: "npm:^8.5.0" + dependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: af2b254f65985610bd354e8e13de07b5a36010b94672b0b5a9d226b9bb1b8b17d01c63221cad97263845888f3610e55867a32e4c0017dfb92fddf89417c4cb6c + languageName: node + linkType: hard + +"safe-buffer@npm:^5.0.1, safe-buffer@npm:~5.2.0": + version: 5.2.1 + resolution: "safe-buffer@npm:5.2.1" + checksum: b99c4b41fdd67a6aaf280fcd05e9ffb0813654894223afb78a31f14a19ad220bba8aba1cb14eddce1fcfb037155fe6de4e861784eb434f7d11ed58d1e70dd491 + languageName: node + linkType: hard + +"safer-buffer@npm:>= 2.1.2 < 3.0.0": + version: 2.1.2 + resolution: "safer-buffer@npm:2.1.2" + checksum: cab8f25ae6f1434abee8d80023d7e72b598cf1327164ddab31003c51215526801e40b66c5e65d658a0af1e9d6478cadcb4c745f4bd6751f97d8644786c0978b0 + languageName: node + linkType: hard + +"semver@npm:^7.3.4, semver@npm:^7.3.5": + version: 7.5.4 + resolution: "semver@npm:7.5.4" + dependencies: + lru-cache: "npm:^6.0.0" + bin: + semver: bin/semver.js + checksum: 12d8ad952fa353b0995bf180cdac205a4068b759a140e5d3c608317098b3575ac2f1e09182206bf2eb26120e1c0ed8fb92c48c592f6099680de56bb071423ca3 + languageName: node + linkType: hard + +"set-blocking@npm:^2.0.0": + version: 2.0.0 + resolution: "set-blocking@npm:2.0.0" + checksum: 6e65a05f7cf7ebdf8b7c75b101e18c0b7e3dff4940d480efed8aad3a36a4005140b660fa1d804cb8bce911cac290441dc728084a30504d3516ac2ff7ad607b02 + languageName: node + linkType: hard + +"shebang-command@npm:^2.0.0": + version: 2.0.0 + resolution: "shebang-command@npm:2.0.0" + dependencies: + shebang-regex: "npm:^3.0.0" + checksum: 6b52fe87271c12968f6a054e60f6bde5f0f3d2db483a1e5c3e12d657c488a15474121a1d55cd958f6df026a54374ec38a4a963988c213b7570e1d51575cea7fa + languageName: node + linkType: hard + +"shebang-regex@npm:^3.0.0": + version: 3.0.0 + resolution: "shebang-regex@npm:3.0.0" + checksum: 1a2bcae50de99034fcd92ad4212d8e01eedf52c7ec7830eedcf886622804fe36884278f2be8be0ea5fde3fd1c23911643a4e0f726c8685b61871c8908af01222 + languageName: node + linkType: hard + +"signal-exit@npm:^3.0.7": + version: 3.0.7 + resolution: "signal-exit@npm:3.0.7" + checksum: a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 + languageName: node + linkType: hard + +"signal-exit@npm:^4.0.1": + version: 4.1.0 + resolution: "signal-exit@npm:4.1.0" + checksum: 64c757b498cb8629ffa5f75485340594d2f8189e9b08700e69199069c8e3070fb3e255f7ab873c05dc0b3cec412aea7402e10a5990cb6a050bd33ba062a6c549 + languageName: node + linkType: hard + +"smart-buffer@npm:^4.2.0": + version: 4.2.0 + resolution: "smart-buffer@npm:4.2.0" + checksum: b5167a7142c1da704c0e3af85c402002b597081dd9575031a90b4f229ca5678e9a36e8a374f1814c8156a725d17008ae3bde63b92f9cfd132526379e580bec8b + languageName: node + linkType: hard + +"snake-case@npm:^3.0.4": + version: 3.0.4 + resolution: "snake-case@npm:3.0.4" + dependencies: + dot-case: "npm:^3.0.4" + tslib: "npm:^2.0.3" + checksum: 0a7a79900bbb36f8aaa922cf111702a3647ac6165736d5dc96d3ef367efc50465cac70c53cd172c382b022dac72ec91710608e5393de71f76d7142e6fd80e8a3 + languageName: node + linkType: hard + +"socks-proxy-agent@npm:^7.0.0": + version: 7.0.0 + resolution: "socks-proxy-agent@npm:7.0.0" + dependencies: + agent-base: "npm:^6.0.2" + debug: "npm:^4.3.3" + socks: "npm:^2.6.2" + checksum: 720554370154cbc979e2e9ce6a6ec6ced205d02757d8f5d93fe95adae454fc187a5cbfc6b022afab850a5ce9b4c7d73e0f98e381879cf45f66317a4895953846 + languageName: node + linkType: hard + +"socks@npm:^2.6.2": + version: 2.7.1 + resolution: "socks@npm:2.7.1" + dependencies: + ip: "npm:^2.0.0" + smart-buffer: "npm:^4.2.0" + checksum: 259d9e3e8e1c9809a7f5c32238c3d4d2a36b39b83851d0f573bfde5f21c4b1288417ce1af06af1452569cd1eb0841169afd4998f0e04ba04656f6b7f0e46d748 + languageName: node + linkType: hard + +"ssri@npm:^10.0.0": + version: 10.0.5 + resolution: "ssri@npm:10.0.5" + dependencies: + minipass: "npm:^7.0.3" + checksum: 0a31b65f21872dea1ed3f7c200d7bc1c1b91c15e419deca14f282508ba917cbb342c08a6814c7f68ca4ca4116dd1a85da2bbf39227480e50125a1ceffeecb750 + languageName: node + linkType: hard + +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.3": + version: 4.2.3 + resolution: "string-width@npm:4.2.3" + dependencies: + emoji-regex: "npm:^8.0.0" + is-fullwidth-code-point: "npm:^3.0.0" + strip-ansi: "npm:^6.0.1" + checksum: e52c10dc3fbfcd6c3a15f159f54a90024241d0f149cf8aed2982a2d801d2e64df0bf1dc351cf8e95c3319323f9f220c16e740b06faecd53e2462df1d2b5443fb + languageName: node + linkType: hard + +"string-width@npm:^5.0.1, string-width@npm:^5.1.2": + version: 5.1.2 + resolution: "string-width@npm:5.1.2" + dependencies: + eastasianwidth: "npm:^0.2.0" + emoji-regex: "npm:^9.2.2" + strip-ansi: "npm:^7.0.1" + checksum: 7369deaa29f21dda9a438686154b62c2c5f661f8dda60449088f9f980196f7908fc39fdd1803e3e01541970287cf5deae336798337e9319a7055af89dafa7193 + languageName: node + linkType: hard + +"string_decoder@npm:^1.1.1": + version: 1.3.0 + resolution: "string_decoder@npm:1.3.0" + dependencies: + safe-buffer: "npm:~5.2.0" + checksum: 8417646695a66e73aefc4420eb3b84cc9ffd89572861fe004e6aeb13c7bc00e2f616247505d2dbbef24247c372f70268f594af7126f43548565c68c117bdeb56 + languageName: node + linkType: hard + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": + version: 6.0.1 + resolution: "strip-ansi@npm:6.0.1" + dependencies: + ansi-regex: "npm:^5.0.1" + checksum: f3cd25890aef3ba6e1a74e20896c21a46f482e93df4a06567cebf2b57edabb15133f1f94e57434e0a958d61186087b1008e89c94875d019910a213181a14fc8c + languageName: node + linkType: hard + +"strip-ansi@npm:^7.0.1": + version: 7.1.0 + resolution: "strip-ansi@npm:7.1.0" + dependencies: + ansi-regex: "npm:^6.0.1" + checksum: 859c73fcf27869c22a4e4d8c6acfe690064659e84bef9458aa6d13719d09ca88dcfd40cbf31fd0be63518ea1a643fe070b4827d353e09533a5b0b9fd4553d64d + languageName: node + linkType: hard + +"superstruct@npm:^0.14.2": + version: 0.14.2 + resolution: "superstruct@npm:0.14.2" + checksum: c5c4840f432da82125b923ec45faca5113217e83ae416e314d80eae012b8bb603d2e745025d173450758d116348820bc7028157f8c9a72b6beae879f94b837c0 + languageName: node + linkType: hard + +"superstruct@npm:^0.15.4": + version: 0.15.5 + resolution: "superstruct@npm:0.15.5" + checksum: 6d1f5249fee789424b7178fa0a1ffb2ace629c5480c39505885bd8c0046a4ff8b267569a3442fa53b8c560a7ba6599cf3f8af94225aebeb2cf6023f7dd911050 + languageName: node + linkType: hard + +"supports-color@npm:^7.1.0": + version: 7.2.0 + resolution: "supports-color@npm:7.2.0" + dependencies: + has-flag: "npm:^4.0.0" + checksum: 3dda818de06ebbe5b9653e07842d9479f3555ebc77e9a0280caf5a14fb877ffee9ed57007c3b78f5a6324b8dbeec648d9e97a24e2ed9fdb81ddc69ea07100f4a + languageName: node + linkType: hard + +"tapable@npm:^2.2.0": + version: 2.2.1 + resolution: "tapable@npm:2.2.1" + checksum: 3b7a1b4d86fa940aad46d9e73d1e8739335efd4c48322cb37d073eb6f80f5281889bf0320c6d8ffcfa1a0dd5bfdbd0f9d037e252ef972aca595330538aac4d51 + languageName: node + linkType: hard + +"tar@npm:^6.1.11, tar@npm:^6.1.2": + version: 6.1.15 + resolution: "tar@npm:6.1.15" + dependencies: + chownr: "npm:^2.0.0" + fs-minipass: "npm:^2.0.0" + minipass: "npm:^5.0.0" + minizlib: "npm:^2.1.1" + mkdirp: "npm:^1.0.3" + yallist: "npm:^4.0.0" + checksum: f23832fceeba7578bf31907aac744ae21e74a66f4a17a9e94507acf460e48f6db598c7023882db33bab75b80e027c21f276d405e4a0322d58f51c7088d428268 + languageName: node + linkType: hard + +"text-encoding-utf-8@npm:^1.0.2": + version: 1.0.2 + resolution: "text-encoding-utf-8@npm:1.0.2" + checksum: ec4c15d50e738c5dba7327ad432ebf0725ec75d4d69c0bd55609254c5a3bc5341272d7003691084a0a73d60d981c8eb0e87603676fdb6f3fed60f4c9192309f9 + languageName: node + linkType: hard + +"through@npm:>=2.2.7 <3": + version: 2.3.8 + resolution: "through@npm:2.3.8" + checksum: a38c3e059853c494af95d50c072b83f8b676a9ba2818dcc5b108ef252230735c54e0185437618596c790bbba8fcdaef5b290405981ffa09dce67b1f1bf190cbd + languageName: node + linkType: hard + +"to-regex-range@npm:^5.0.1": + version: 5.0.1 + resolution: "to-regex-range@npm:5.0.1" + dependencies: + is-number: "npm:^7.0.0" + checksum: f76fa01b3d5be85db6a2a143e24df9f60dd047d151062d0ba3df62953f2f697b16fe5dad9b0ac6191c7efc7b1d9dcaa4b768174b7b29da89d4428e64bc0a20ed + languageName: node + linkType: hard + +"toml@npm:^3.0.0": + version: 3.0.0 + resolution: "toml@npm:3.0.0" + checksum: 5d7f1d8413ad7780e9bdecce8ea4c3f5130dd53b0a4f2e90b93340979a137739879d7b9ce2ce05c938b8cc828897fe9e95085197342a1377dd8850bf5125f15f + languageName: node + linkType: hard + +"tr46@npm:~0.0.3": + version: 0.0.3 + resolution: "tr46@npm:0.0.3" + checksum: 726321c5eaf41b5002e17ffbd1fb7245999a073e8979085dacd47c4b4e8068ff5777142fc6726d6ca1fd2ff16921b48788b87225cbc57c72636f6efa8efbffe3 + languageName: node + linkType: hard + +"ts-loader@npm:^9.2.3": + version: 9.4.4 + resolution: "ts-loader@npm:9.4.4" + dependencies: + chalk: "npm:^4.1.0" + enhanced-resolve: "npm:^5.0.0" + micromatch: "npm:^4.0.0" + semver: "npm:^7.3.4" + peerDependencies: + typescript: "*" + webpack: ^5.0.0 + checksum: 8e5e6b839b0edfa40d2156c880d88ccab58226894ea5978221bc48c7db3215e2e856bfd0093f148e925a2befc42d6c94cafa9a994a7da274541efaa916012b63 + languageName: node + linkType: hard + +"ts-node@npm:^10.9.1": + version: 10.9.1 + resolution: "ts-node@npm:10.9.1" + dependencies: + "@cspotcode/source-map-support": "npm:^0.8.0" + "@tsconfig/node10": "npm:^1.0.7" + "@tsconfig/node12": "npm:^1.0.7" + "@tsconfig/node14": "npm:^1.0.0" + "@tsconfig/node16": "npm:^1.0.2" + acorn: "npm:^8.4.1" + acorn-walk: "npm:^8.1.1" + arg: "npm:^4.1.0" + create-require: "npm:^1.1.0" + diff: "npm:^4.0.1" + make-error: "npm:^1.1.1" + v8-compile-cache-lib: "npm:^3.0.1" + yn: "npm:3.1.1" + peerDependencies: + "@swc/core": ">=1.2.50" + "@swc/wasm": ">=1.2.50" + "@types/node": "*" + typescript: ">=2.7" + peerDependenciesMeta: + "@swc/core": + optional: true + "@swc/wasm": + optional: true + bin: + ts-node: dist/bin.js + ts-node-cwd: dist/bin-cwd.js + ts-node-esm: dist/bin-esm.js + ts-node-script: dist/bin-script.js + ts-node-transpile-only: dist/bin-transpile.js + ts-script: dist/bin-script-deprecated.js + checksum: 090adff1302ab20bd3486e6b4799e90f97726ed39e02b39e566f8ab674fd5bd5f727f43615debbfc580d33c6d9d1c6b1b3ce7d8e3cca3e20530a145ffa232c35 + languageName: node + linkType: hard + +"tslib@npm:^2.0.3": + version: 2.6.2 + resolution: "tslib@npm:2.6.2" + checksum: 329ea56123005922f39642318e3d1f0f8265d1e7fcb92c633e0809521da75eeaca28d2cf96d7248229deb40e5c19adf408259f4b9640afd20d13aecc1430f3ad + languageName: node + linkType: hard + +"typescript@npm:^5.2.2": + version: 5.2.2 + resolution: "typescript@npm:5.2.2" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 7912821dac4d962d315c36800fe387cdc0a6298dba7ec171b350b4a6e988b51d7b8f051317786db1094bd7431d526b648aba7da8236607febb26cf5b871d2d3c + languageName: node + linkType: hard + +"typescript@patch:typescript@^5.2.2#~builtin": + version: 5.2.2 + resolution: "typescript@patch:typescript@npm%3A5.2.2#~builtin::version=5.2.2&hash=f3b441" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 0f4da2f15e6f1245e49db15801dbee52f2bbfb267e1c39225afdab5afee1a72839cd86000e65ee9d7e4dfaff12239d28beaf5ee431357fcced15fb08583d72ca + languageName: node + linkType: hard + +"unique-filename@npm:^3.0.0": + version: 3.0.0 + resolution: "unique-filename@npm:3.0.0" + dependencies: + unique-slug: "npm:^4.0.0" + checksum: 8e2f59b356cb2e54aab14ff98a51ac6c45781d15ceaab6d4f1c2228b780193dc70fae4463ce9e1df4479cb9d3304d7c2043a3fb905bdeca71cc7e8ce27e063df + languageName: node + linkType: hard + +"unique-slug@npm:^4.0.0": + version: 4.0.0 + resolution: "unique-slug@npm:4.0.0" + dependencies: + imurmurhash: "npm:^0.1.4" + checksum: 0884b58365af59f89739e6f71e3feacb5b1b41f2df2d842d0757933620e6de08eff347d27e9d499b43c40476cbaf7988638d3acb2ffbcb9d35fd035591adfd15 + languageName: node + linkType: hard + +"utf-8-validate@npm:^5.0.2": + version: 5.0.10 + resolution: "utf-8-validate@npm:5.0.10" + dependencies: + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.3.0" + checksum: 5579350a023c66a2326752b6c8804cc7b39dcd251bb088241da38db994b8d78352e388dcc24ad398ab98385ba3c5ffcadb6b5b14b2637e43f767869055e46ba6 + languageName: node + linkType: hard + +"util-deprecate@npm:^1.0.1": + version: 1.0.2 + resolution: "util-deprecate@npm:1.0.2" + checksum: 474acf1146cb2701fe3b074892217553dfcf9a031280919ba1b8d651a068c9b15d863b7303cb15bd00a862b498e6cf4ad7b4a08fb134edd5a6f7641681cb54a2 + languageName: node + linkType: hard + +"uuid@npm:^8.3.2": + version: 8.3.2 + resolution: "uuid@npm:8.3.2" + bin: + uuid: dist/bin/uuid + checksum: 5575a8a75c13120e2f10e6ddc801b2c7ed7d8f3c8ac22c7ed0c7b2ba6383ec0abda88c905085d630e251719e0777045ae3236f04c812184b7c765f63a70e58df + languageName: node + linkType: hard + +"v8-compile-cache-lib@npm:^3.0.1": + version: 3.0.1 + resolution: "v8-compile-cache-lib@npm:3.0.1" + checksum: 78089ad549e21bcdbfca10c08850022b22024cdcc2da9b168bcf5a73a6ed7bf01a9cebb9eac28e03cd23a684d81e0502797e88f3ccd27a32aeab1cfc44c39da0 + languageName: node + linkType: hard + +"webidl-conversions@npm:^3.0.0": + version: 3.0.1 + resolution: "webidl-conversions@npm:3.0.1" + checksum: c92a0a6ab95314bde9c32e1d0a6dfac83b578f8fa5f21e675bc2706ed6981bc26b7eb7e6a1fab158e5ce4adf9caa4a0aee49a52505d4d13c7be545f15021b17c + languageName: node + linkType: hard + +"whatwg-url@npm:^5.0.0": + version: 5.0.0 + resolution: "whatwg-url@npm:5.0.0" + dependencies: + tr46: "npm:~0.0.3" + webidl-conversions: "npm:^3.0.0" + checksum: b8daed4ad3356cc4899048a15b2c143a9aed0dfae1f611ebd55073310c7b910f522ad75d727346ad64203d7e6c79ef25eafd465f4d12775ca44b90fa82ed9e2c + languageName: node + linkType: hard + +"which@npm:^2.0.1, which@npm:^2.0.2": + version: 2.0.2 + resolution: "which@npm:2.0.2" + dependencies: + isexe: "npm:^2.0.0" + bin: + node-which: ./bin/node-which + checksum: 1a5c563d3c1b52d5f893c8b61afe11abc3bab4afac492e8da5bde69d550de701cf9806235f20a47b5c8fa8a1d6a9135841de2596535e998027a54589000e66d1 + languageName: node + linkType: hard + +"wide-align@npm:^1.1.5": + version: 1.1.5 + resolution: "wide-align@npm:1.1.5" + dependencies: + string-width: "npm:^1.0.2 || 2 || 3 || 4" + checksum: d5fc37cd561f9daee3c80e03b92ed3e84d80dde3365a8767263d03dacfc8fa06b065ffe1df00d8c2a09f731482fcacae745abfbb478d4af36d0a891fad4834d3 + languageName: node + linkType: hard + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version: 7.0.0 + resolution: "wrap-ansi@npm:7.0.0" + dependencies: + ansi-styles: "npm:^4.0.0" + string-width: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + checksum: a790b846fd4505de962ba728a21aaeda189b8ee1c7568ca5e817d85930e06ef8d1689d49dbf0e881e8ef84436af3a88bc49115c2e2788d841ff1b8b5b51a608b + languageName: node + linkType: hard + +"wrap-ansi@npm:^8.1.0": + version: 8.1.0 + resolution: "wrap-ansi@npm:8.1.0" + dependencies: + ansi-styles: "npm:^6.1.0" + string-width: "npm:^5.0.1" + strip-ansi: "npm:^7.0.1" + checksum: 371733296dc2d616900ce15a0049dca0ef67597d6394c57347ba334393599e800bab03c41d4d45221b6bc967b8c453ec3ae4749eff3894202d16800fdfe0e238 + languageName: node + linkType: hard + +"wrappy@npm:1": + version: 1.0.2 + resolution: "wrappy@npm:1.0.2" + checksum: 159da4805f7e84a3d003d8841557196034155008f817172d4e986bd591f74aa82aa7db55929a54222309e01079a65a92a9e6414da5a6aa4b01ee44a511ac3ee5 + languageName: node + linkType: hard + +"ws@npm:^7.4.5": + version: 7.5.9 + resolution: "ws@npm:7.5.9" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: c3c100a181b731f40b7f2fddf004aa023f79d64f489706a28bc23ff88e87f6a64b3c6651fbec3a84a53960b75159574d7a7385709847a62ddb7ad6af76f49138 + languageName: node + linkType: hard + +"ws@npm:^8.5.0": + version: 8.13.0 + resolution: "ws@npm:8.13.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 53e991bbf928faf5dc6efac9b8eb9ab6497c69feeb94f963d648b7a3530a720b19ec2e0ec037344257e05a4f35bd9ad04d9de6f289615ffb133282031b18c61c + languageName: node + linkType: hard + +"yallist@npm:^4.0.0": + version: 4.0.0 + resolution: "yallist@npm:4.0.0" + checksum: 343617202af32df2a15a3be36a5a8c0c8545208f3d3dfbc6bb7c3e3b7e8c6f8e7485432e4f3b88da3031a6e20afa7c711eded32ddfb122896ac5d914e75848d5 + languageName: node + linkType: hard + +"yn@npm:3.1.1": + version: 3.1.1 + resolution: "yn@npm:3.1.1" + checksum: 2c487b0e149e746ef48cda9f8bad10fc83693cd69d7f9dcd8be4214e985de33a29c9e24f3c0d6bcf2288427040a8947406ab27f7af67ee9456e6b84854f02dd6 + languageName: node + linkType: hard diff --git a/programs/dc-conversion-escrow/Cargo.toml b/programs/dc-conversion-escrow/Cargo.toml index 91ea483e5..6aed11a03 100644 --- a/programs/dc-conversion-escrow/Cargo.toml +++ b/programs/dc-conversion-escrow/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dc-conversion-escrow" -version = "0.1.0" +version = "0.0.1" description = "Created with Anchor" edition = "2021" diff --git a/programs/dc-conversion-escrow/src/instructions/initialize_escrow_v0.rs b/programs/dc-conversion-escrow/src/instructions/initialize_escrow_v0.rs index 387e3c72a..247c8f9e0 100644 --- a/programs/dc-conversion-escrow/src/instructions/initialize_escrow_v0.rs +++ b/programs/dc-conversion-escrow/src/instructions/initialize_escrow_v0.rs @@ -4,6 +4,7 @@ use anchor_spl::{ associated_token::AssociatedToken, token::{Mint, Token, TokenAccount}, }; +use data_credits::DataCreditsV0; use pyth_sdk_solana::load_price_feed_from_account_info; use crate::ConversionEscrowV0; @@ -15,16 +16,15 @@ pub struct InitializeEscrowArgsV0 { #[derive(Accounts)] pub struct InitializeEscrowV0<'info> { + pub data_credits: Box>, #[account(mut)] pub payer: Signer<'info>, /// CHECK: The owner of this account. Can fully withdraw - pub owner: UncheckedAccount<'info>, - /// CHECK: The authority to issue dc conversions with funds from this account - pub convert_authority: UncheckedAccount<'info>, + pub owner: Signer<'info>, #[account( init, payer = payer, - seeds = [b"conversion_escrow", mint.key().as_ref(), owner.key().as_ref()], + seeds = [b"conversion_escrow", data_credits.key().as_ref(), mint.key().as_ref(), owner.key().as_ref()], bump, space = ConversionEscrowV0::INIT_SPACE + 60, )] @@ -39,6 +39,7 @@ pub struct InitializeEscrowV0<'info> { pub escrow: Box>, pub system_program: Program<'info, System>, pub associated_token_program: Program<'info, AssociatedToken>, + /// CHECK: Checked with load_price_feed_from_account_info pub oracle: AccountInfo<'info>, pub token_program: Program<'info, Token>, } @@ -55,9 +56,10 @@ pub fn handler(ctx: Context, args: InitializeEscrowArgsV0) - .set_inner(ConversionEscrowV0 { oracle: ctx.accounts.oracle.key(), escrow: ctx.accounts.escrow.key(), - mint: ctx.accounts.escrow.mint.key(), + mint: ctx.accounts.mint.key(), slipage_bps: args.slippage_bps, owner: ctx.accounts.owner.key(), + data_credits: ctx.accounts.data_credits.key(), bump_seed: ctx.bumps["conversion_escrow"], }); diff --git a/programs/dc-conversion-escrow/src/instructions/lend_v0.rs b/programs/dc-conversion-escrow/src/instructions/lend_v0.rs index 05a9dddb9..45ad67ae9 100644 --- a/programs/dc-conversion-escrow/src/instructions/lend_v0.rs +++ b/programs/dc-conversion-escrow/src/instructions/lend_v0.rs @@ -1,5 +1,4 @@ use std::ops::Div; -use std::str::FromStr; use crate::errors::ErrorCode; use crate::{escrow_seeds, ConversionEscrowV0}; @@ -8,9 +7,7 @@ use anchor_lang::solana_program::sysvar; use anchor_lang::solana_program::sysvar::instructions::{ load_current_index_checked, load_instruction_at_checked, }; -use anchor_spl::mint; -use anchor_spl::token::{mint_to, Mint, MintTo, Token, TokenAccount}; -use data_credits::accounts::MintDataCreditsV0; +use anchor_spl::token::{transfer, Mint, Token, TokenAccount, Transfer}; use data_credits::{DataCreditsV0, MintDataCreditsArgsV0, TESTING}; use pyth_sdk_solana::load_price_feed_from_account_info; @@ -25,16 +22,16 @@ pub struct LendV0<'info> { has_one = mint, has_one = oracle, has_one = escrow, + has_one = data_credits, )] pub conversion_escrow: Box>, + #[account(mut)] pub escrow: Account<'info, TokenAccount>, /// CHECK: Checked via pyth pub oracle: UncheckedAccount<'info>, pub mint: Box>, #[account( has_one = hnt_price_oracle, - // Ensure we're working with the canonical helium data credits - constraint = data_credits.dc_mint == Pubkey::from_str("dcuc8Amr83Wz27ZkQ2K9NS6r8zRpf1J6cvArEBDZDmm").unwrap() )] pub data_credits: Box>, @@ -42,6 +39,7 @@ pub struct LendV0<'info> { pub hnt_price_oracle: AccountInfo<'info>, #[account( + mut, token::mint = mint )] pub destination: Box>, @@ -137,7 +135,7 @@ pub fn handler(ctx: Context, args: LendArgsV0) -> Result<()> { ); let mint_dc_args: MintDataCreditsArgsV0 = - MintDataCreditsArgsV0::deserialize(&mut ix.data.as_slice()).unwrap(); + MintDataCreditsArgsV0::deserialize(&mut &ix.data[8..]).unwrap(); let hnt_amount = mint_dc_args .hnt_amount .ok_or_else(|| error!(ErrorCode::HntAmountRequired))?; @@ -164,13 +162,13 @@ pub fn handler(ctx: Context, args: LendArgsV0) -> Result<()> { } // Send the loan - mint_to( + transfer( CpiContext::new_with_signer( ctx.accounts.token_program.to_account_info(), - MintTo { - mint: ctx.accounts.mint.to_account_info(), + Transfer { + from: ctx.accounts.escrow.to_account_info(), to: ctx.accounts.destination.to_account_info(), - authority: ctx.accounts.escrow.to_account_info(), + authority: ctx.accounts.conversion_escrow.to_account_info(), }, &[escrow_seeds!(ctx.accounts.conversion_escrow)], ), diff --git a/programs/dc-conversion-escrow/src/lib.rs b/programs/dc-conversion-escrow/src/lib.rs index 919a68172..dfc7568ab 100644 --- a/programs/dc-conversion-escrow/src/lib.rs +++ b/programs/dc-conversion-escrow/src/lib.rs @@ -2,7 +2,7 @@ use anchor_lang::prelude::*; #[cfg(not(feature = "no-entrypoint"))] use {default_env::default_env, solana_security_txt::security_txt}; -declare_id!("fanqeMu3fw8R4LwKNbahPtYXJsyLL6NXyfe2BqzhfB6"); +declare_id!("dce4jeLBpfaFsNAKMAmVt5Py4E1R4mZcrVvMB5ejvGu"); pub mod errors; pub mod instructions; @@ -28,7 +28,7 @@ security_txt! { } #[program] -pub mod dc_convesion_escrow { +pub mod dc_conversion_escrow { use super::*; pub fn initialize_escrow_v0( diff --git a/programs/dc-conversion-escrow/src/state.rs b/programs/dc-conversion-escrow/src/state.rs index b116291b9..8977aead4 100644 --- a/programs/dc-conversion-escrow/src/state.rs +++ b/programs/dc-conversion-escrow/src/state.rs @@ -9,6 +9,7 @@ pub struct ConversionEscrowV0 { pub slipage_bps: u16, pub oracle: Pubkey, pub owner: Pubkey, + pub data_credits: Pubkey, pub bump_seed: u8, } @@ -17,6 +18,7 @@ macro_rules! escrow_seeds { ( $escrow:expr ) => { &[ "conversion_escrow".as_bytes(), + $escrow.data_credits.as_ref(), $escrow.mint.as_ref(), $escrow.owner.as_ref(), &[$escrow.bump_seed], diff --git a/tests/dc-conversion-escrow.ts b/tests/dc-conversion-escrow.ts new file mode 100644 index 000000000..833e617e3 --- /dev/null +++ b/tests/dc-conversion-escrow.ts @@ -0,0 +1,293 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Program } from "@coral-xyz/anchor"; +import { VoterStakeRegistry } from "@helium/idls/lib/types/voter_stake_registry"; +import { createAtaAndMint, createMint, sendInstructions } from "@helium/spl-utils"; +import { parsePriceData } from "@pythnetwork/client"; +import { + createAssociatedTokenAccountIdempotentInstruction, + getAssociatedTokenAddressSync +} from "@solana/spl-token"; +import { Keypair, PublicKey } from "@solana/web3.js"; +import BN from "bn.js"; +import { expect } from "chai"; +import { ThresholdType } from "../packages/circuit-breaker-sdk/src"; +import { + dataCreditsKey, + init as initDc +} from "../packages/data-credits-sdk/src"; +import { PROGRAM_ID as DC_PROGRAM_ID } from "../packages/data-credits-sdk/src/constants"; +import { init } from "../packages/dc-conversion-escrow-sdk/src"; +import { PROGRAM_ID } from "../packages/dc-conversion-escrow-sdk/src/constants"; +import * as hsd from "../packages/helium-sub-daos-sdk/src"; +import { daoKey } from "../packages/helium-sub-daos-sdk/src"; +import { toBN, toNumber } from "../packages/spl-utils/src"; +import * as vsr from "../packages/voter-stake-registry-sdk/src"; +import { DataCredits } from "../target/types/data_credits"; +import { DcConversionEscrow } from "../target/types/dc_conversion_escrow"; +import { HeliumSubDaos } from "../target/types/helium_sub_daos"; +import { ensureDCIdl, ensureDcEscrowIdl, ensureHSDIdl, ensureVSRIdl } from "./utils/fixtures"; +import { initVsr } from "./utils/vsr"; + +const EPOCH_REWARDS = 100000000; + +describe("dc-conversion-escrow", () => { + anchor.setProvider(anchor.AnchorProvider.local("http://127.0.0.1:8899")); + + const mobileOracle = new PublicKey("JBaTytFv1CmGNkyNiLu16jFMXNZ49BGfy4bYAYZdkxg5") + const hntOracle = new PublicKey("7moA1i5vQUpfDwSpK6Pw9s56ahB7WFGidtbL2ujWrVvm") + let program: Program; + let dcProgram: Program; + let hsdProgram: Program; + let vsrProgram: Program; + let dcKey: PublicKey; + let mobileMint: PublicKey; + let hntMint: PublicKey; + let dcMint: PublicKey; + let startHntBal = 10000; + let startMobileEscrowBal = 100000000; + let hntDecimals = 8; + let mobileDecimals = 6; + let dcDecimals = 0; + const provider = anchor.getProvider() as anchor.AnchorProvider; + const me = provider.wallet.publicKey; + const hntHolder = Keypair.generate(); + + beforeEach(async () => { + program = await init( + provider, + PROGRAM_ID, + anchor.workspace.DcConversionEscrow.idl + ) + dcProgram = await initDc( + provider, + DC_PROGRAM_ID, + anchor.workspace.DataCredits.idl + ); + hsdProgram = await hsd.init( + provider, + hsd.PROGRAM_ID, + anchor.workspace.HeliumSubDaos.idl + ); + vsrProgram = await vsr.init( + provider, + vsr.PROGRAM_ID, + anchor.workspace.VoterStakeRegistry.idl + ); + await ensureVSRIdl(vsrProgram); + // fresh start + mobileMint = await createMint(provider, mobileDecimals, me, me); + hntMint = await createMint(provider, hntDecimals, me, me); + dcMint = await createMint(provider, dcDecimals, me, me); + await createAtaAndMint( + provider, + hntMint, + toBN(startHntBal, hntDecimals).toNumber(), + hntHolder.publicKey + ); + + const method = await dcProgram.methods + .initializeDataCreditsV0({ + authority: me, + config: { + windowSizeSeconds: new BN(60), + thresholdType: ThresholdType.Absolute as never, + threshold: new BN("10000000000000000000"), + }, + }) + .accounts({ + hntMint, + dcMint, + payer: me, + hntPriceOracle: new PublicKey( + "7moA1i5vQUpfDwSpK6Pw9s56ahB7WFGidtbL2ujWrVvm" + ), + }); + dcKey = (await method.pubkeys()).dataCredits!; + await method.rpc({ + skipPreflight: true, + }); + }); + + describe("with data credits and escrow", async () => { + let dao: PublicKey; + let conversionEscrow: PublicKey | undefined; + + beforeEach(async () => { + const registrar = ( + await initVsr( + vsrProgram, + provider, + provider.wallet.publicKey, + hntMint, + daoKey(hntMint)[0] + ) + ).registrar; + const method = await hsdProgram.methods + .initializeDaoV0({ + authority: me, + registrar, + netEmissionsCap: toBN(34.24, 8), + hstEmissionSchedule: [ + { + startUnixTime: new anchor.BN(0), + percent: 32, + }, + ], + emissionSchedule: [ + { + startUnixTime: new anchor.BN(0), + emissionsPerEpoch: new BN(EPOCH_REWARDS), + }, + ], + }) + .preInstructions([ + createAssociatedTokenAccountIdempotentInstruction( + me, + getAssociatedTokenAddressSync(hntMint, me), + me, + hntMint + ), + ]) + .accounts({ + dcMint, + hntMint, + hstPool: getAssociatedTokenAddressSync(hntMint, me), + }); + await ensureHSDIdl(hsdProgram); + await ensureDcEscrowIdl(program); + await ensureDCIdl(dcProgram); + + dao = (await method.pubkeys()).dao!; + if (!(await provider.connection.getAccountInfo(dao))) { + await method.rpc({ skipPreflight: true }); + } + console.log("start"); + + ({ + pubkeys: { conversionEscrow }, + } = await program.methods + .initializeEscrowV0({ + // Slippage can be super low since price isn't changing. + slippageBps: 1, + }) + .accounts({ + owner: me, + payer: me, + mint: mobileMint, + oracle: mobileOracle, + dataCredits: dataCreditsKey(dcMint)[0], + }) + .rpcAndKeys({ skipPreflight: true })); + console.log("done"); + + // Populate the escrow + await createAtaAndMint( + provider, + mobileMint, + toBN(startMobileEscrowBal, mobileDecimals).toNumber(), + conversionEscrow + ); + }); + + it("lends MOBILE to mint DC to owner", async () => { + // Lend enough MOBILE to `hntHolder` to get 40000 DC. HNT holder + // will then burn enough HNT to get that DC into `owner` + const pythDataMobile = (await provider.connection.getAccountInfo( + mobileOracle + ))!.data; + const priceMobile = parsePriceData(pythDataMobile); + + const mobileFloorValueInDc = Math.floor( + (priceMobile.emaPrice.value - priceMobile.emaConfidence!.value * 2) * + 10 ** 5 + ); + + const pythData = (await provider.connection.getAccountInfo(hntOracle))! + .data; + const priceHnt = parsePriceData(pythData); + + const hntFloorValueInDc = Math.floor( + (priceHnt.emaPrice.value - priceHnt.emaConfidence!.value * 2) * 10 ** 5 + ); + + const hntHolderMobileAta = getAssociatedTokenAddressSync( + mobileMint, + hntHolder.publicKey + ); + const mobileAmount = toBN(40000 / mobileFloorValueInDc, mobileDecimals); + + const instructions = [ + createAssociatedTokenAccountIdempotentInstruction( + me, + hntHolderMobileAta, + hntHolder.publicKey, + mobileMint + ), + createAssociatedTokenAccountIdempotentInstruction( + me, + getAssociatedTokenAddressSync(dcMint, me), + me, + dcMint + ), + await program.methods + .lendV0({ + // Goal: get 4000 DC + amount: mobileAmount, + }) + .accounts({ + conversionEscrow, + destination: hntHolderMobileAta, + }) + .instruction(), + await dcProgram.methods + .mintDataCreditsV0({ + hntAmount: toBN(40000 / hntFloorValueInDc, 8), + dcAmount: null, + }) + .accounts({ + dcMint, + owner: hntHolder.publicKey, + recipient: me, + }) + .instruction(), + ]; + + await sendInstructions(provider, instructions, [hntHolder]); + + const dcAta = await getAssociatedTokenAddressSync(dcMint, me); + const hntAta = await getAssociatedTokenAddressSync( + hntMint, + hntHolder.publicKey + ); + const mobileAta = await getAssociatedTokenAddressSync( + mobileMint, + conversionEscrow!, + true + ); + const dcBal = await provider.connection.getTokenAccountBalance(dcAta); + const mobileBal = await provider.connection.getTokenAccountBalance( + mobileAta + ); + const hntBal = await provider.connection.getTokenAccountBalance(hntAta); + expect(dcBal.value.uiAmount).to.eq(40000); + console.log("bal", mobileBal.value.uiAmount); + expect(mobileBal.value.uiAmount).to.eq( + // Ensure matching decimals amounts + toNumber( + toBN( + startMobileEscrowBal - toNumber(mobileAmount, mobileDecimals), + mobileDecimals + ), + mobileDecimals + ) + ); + expect(hntBal.value.uiAmount).to.eq( + // Ensure matching decimals amounts + toNumber( + toBN(startHntBal - 40000 / hntFloorValueInDc, hntDecimals), + hntDecimals + ) + ); + }); + }); +}); diff --git a/tests/utils/fixtures.ts b/tests/utils/fixtures.ts index 7038641c9..6b15871b4 100644 --- a/tests/utils/fixtures.ts +++ b/tests/utils/fixtures.ts @@ -23,6 +23,9 @@ import { HeliumSubDaos } from "../../target/types/helium_sub_daos"; import { LazyDistributor } from "../../target/types/lazy_distributor"; import { initTestDao, initTestSubdao } from "./daos"; import { random } from "./string"; +import { DcConversionEscrow } from "../../target/types/dc_conversion_escrow"; + +const ANCHOR_PATH = process.env.ANCHOR_PATH || 'anchor' // TODO: replace this with helium default uri once uploaded const DEFAULT_METADATA_URL = @@ -216,12 +219,12 @@ export const initTestMaker = async ( export async function ensureDCIdl(dcProgram: Program) { try { execSync( - `anchor idl init --filepath ${__dirname}/../../target/idl/data_credits.json ${dcProgram.programId}`, + `${ANCHOR_PATH} idl init --filepath ${__dirname}/../../target/idl/data_credits.json ${dcProgram.programId}`, { stdio: "inherit", shell: "/bin/bash" } ); } catch { execSync( - `anchor idl upgrade --filepath ${__dirname}/../../target/idl/data_credits.json ${dcProgram.programId}`, + `${ANCHOR_PATH} idl upgrade --filepath ${__dirname}/../../target/idl/data_credits.json ${dcProgram.programId}`, { stdio: "inherit", shell: "/bin/bash" } ); } @@ -230,12 +233,12 @@ export async function ensureDCIdl(dcProgram: Program) { export async function ensureMemIdl(memProgram: Program) { try { execSync( - `anchor idl init --filepath ${__dirname}/../../target/idl/mobile_entity_manager.json ${memProgram.programId}`, + `${ANCHOR_PATH} idl init --filepath ${__dirname}/../../target/idl/mobile_entity_manager.json ${memProgram.programId}`, { stdio: "inherit", shell: "/bin/bash" } ); } catch { execSync( - `anchor idl upgrade --filepath ${__dirname}/../../target/idl/mobile_entity_manager.json ${memProgram.programId}`, + `${ANCHOR_PATH} idl upgrade --filepath ${__dirname}/../../target/idl/mobile_entity_manager.json ${memProgram.programId}`, { stdio: "inherit", shell: "/bin/bash" } ); } @@ -244,12 +247,12 @@ export async function ensureMemIdl(memProgram: Program) { export async function ensureLDIdl(ldProgram: Program) { try { execSync( - `anchor idl init --filepath ${__dirname}/../../target/idl/lazy_distributor.json ${ldProgram.programId}`, + `${ANCHOR_PATH} idl init --filepath ${__dirname}/../../target/idl/lazy_distributor.json ${ldProgram.programId}`, { stdio: "inherit", shell: "/bin/bash" } ); } catch { execSync( - `anchor idl upgrade --filepath ${__dirname}/../../target/idl/lazy_distributor.json ${ldProgram.programId}`, + `${ANCHOR_PATH} idl upgrade --filepath ${__dirname}/../../target/idl/lazy_distributor.json ${ldProgram.programId}`, { stdio: "inherit", shell: "/bin/bash" } ); } @@ -258,12 +261,12 @@ export async function ensureLDIdl(ldProgram: Program) { export async function ensureHEMIdl(hemProgram: Program) { try { execSync( - `anchor idl init --filepath ${__dirname}/../../target/idl/helium_entity_manager.json ${hemProgram.programId}`, + `${ANCHOR_PATH} idl init --filepath ${__dirname}/../../target/idl/helium_entity_manager.json ${hemProgram.programId}`, { stdio: "inherit", shell: "/bin/bash" } ); } catch { execSync( - `anchor idl upgrade --filepath ${__dirname}/../../target/idl/helium_entity_manager.json ${hemProgram.programId}`, + `${ANCHOR_PATH} idl upgrade --filepath ${__dirname}/../../target/idl/helium_entity_manager.json ${hemProgram.programId}`, { stdio: "inherit", shell: "/bin/bash" } ); } @@ -272,12 +275,26 @@ export async function ensureHEMIdl(hemProgram: Program) { export async function ensureHSDIdl(hsdProgram: Program) { try { execSync( - `anchor idl init --filepath ${__dirname}/../../target/idl/helium_sub_daos.json ${hsdProgram.programId}`, + `${ANCHOR_PATH} idl init --filepath ${__dirname}/../../target/idl/helium_sub_daos.json ${hsdProgram.programId}`, + { stdio: "inherit", shell: "/bin/bash" } + ); + } catch { + execSync( + `${ANCHOR_PATH} idl upgrade --filepath ${__dirname}/../../target/idl/helium_sub_daos.json ${hsdProgram.programId}`, + { stdio: "inherit", shell: "/bin/bash" } + ); + } +} + +export async function ensureDcEscrowIdl(dcEscrowProgram: Program) { + try { + execSync( + `${ANCHOR_PATH} idl init --filepath ${__dirname}/../../target/idl/dc_conversion_escrow.json ${dcEscrowProgram.programId}`, { stdio: "inherit", shell: "/bin/bash" } ); } catch { execSync( - `anchor idl upgrade --filepath ${__dirname}/../../target/idl/helium_sub_daos.json ${hsdProgram.programId}`, + `${ANCHOR_PATH} idl upgrade --filepath ${__dirname}/../../target/idl/dc_conversion_escrow.json ${dcEscrowProgram.programId}`, { stdio: "inherit", shell: "/bin/bash" } ); } @@ -286,12 +303,12 @@ export async function ensureHSDIdl(hsdProgram: Program) { export async function ensureVSRIdl(vsrProgram: Program) { try { execSync( - `anchor idl init --filepath ${__dirname}/../../target/idl/voter_stake_registry.json ${vsrProgram.programId}`, + `${ANCHOR_PATH} idl init --filepath ${__dirname}/../../target/idl/voter_stake_registry.json ${vsrProgram.programId}`, { stdio: "inherit", shell: "/bin/bash" } ); } catch { execSync( - `anchor idl upgrade --filepath ${__dirname}/../../target/idl/voter_stake_registry.json ${vsrProgram.programId}`, + `${ANCHOR_PATH} idl upgrade --filepath ${__dirname}/../../target/idl/voter_stake_registry.json ${vsrProgram.programId}`, { stdio: "inherit", shell: "/bin/bash" } ); } diff --git a/tsconfig.json b/tsconfig.json index 4c1f31c15..95fab9220 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,9 @@ { "path": "./packages/sus" }, + { + "path": "./packages/dc-conversion-escrow-sdk" + }, { "path": "./packages/hexboosting-sdk" }, diff --git a/yarn.lock b/yarn.lock index 12cb21580..109780968 100644 --- a/yarn.lock +++ b/yarn.lock @@ -766,6 +766,22 @@ __metadata: languageName: unknown linkType: soft +"@helium/dc-conversion-escrow-sdk@workspace:packages/dc-conversion-escrow-sdk": + version: 0.0.0-use.local + resolution: "@helium/dc-conversion-escrow-sdk@workspace:packages/dc-conversion-escrow-sdk" + dependencies: + "@coral-xyz/anchor": ^0.28.0 + "@helium/anchor-resolvers": ^0.6.41 + "@helium/idls": ^0.6.41 + bn.js: ^5.2.0 + bs58: ^4.0.1 + git-format-staged: ^2.1.3 + ts-loader: ^9.2.3 + ts-node: ^10.9.1 + typescript: ^5.2.2 + languageName: unknown + linkType: soft + "@helium/distributor-oracle@^0.6.41, @helium/distributor-oracle@workspace:packages/distributor-oracle": version: 0.0.0-use.local resolution: "@helium/distributor-oracle@workspace:packages/distributor-oracle" From 602cc4b1b47ca51c0fcc08eab8626f6e999d125e Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Wed, 20 Mar 2024 11:00:46 -0700 Subject: [PATCH 03/12] V2 --- .github/workflows/tests.yaml | 2 +- Anchor.toml | 4 +- Cargo.lock | 26 +- .../.gitignore | 0 .../CHANGELOG.md | 0 .../package.json | 4 +- .../src/constants.ts | 0 .../src/index.ts | 16 +- .../src/pdas.ts | 0 .../src/resolvers.ts | 2 +- .../tsconfig.cjs.json | 0 .../tsconfig.esm.json | 0 .../tsconfig.json | 0 .../yarn.deploy.lock | 4 +- .../Cargo.toml | 6 +- .../Xargo.toml | 0 .../src/errors.rs | 0 .../src/instructions/initialize_escrow_v0.rs | 40 ++- .../src/instructions/lend_v0.rs | 178 +++++++++++ .../src/instructions/mod.rs | 0 .../src/lib.rs | 6 +- .../src/state.rs | 16 +- .../src/instructions/lend_v0.rs | 187 ----------- tests/conversion-escrow.ts | 181 +++++++++++ tests/dc-conversion-escrow.ts | 293 ------------------ tests/utils/fixtures.ts | 8 +- tsconfig.json | 2 +- yarn.lock | 32 +- 28 files changed, 450 insertions(+), 557 deletions(-) rename packages/{dc-conversion-escrow-sdk => conversion-escrow-sdk}/.gitignore (100%) rename packages/{dc-conversion-escrow-sdk => conversion-escrow-sdk}/CHANGELOG.md (100%) rename packages/{dc-conversion-escrow-sdk => conversion-escrow-sdk}/package.json (91%) rename packages/{dc-conversion-escrow-sdk => conversion-escrow-sdk}/src/constants.ts (100%) rename packages/{dc-conversion-escrow-sdk => conversion-escrow-sdk}/src/index.ts (53%) rename packages/{dc-conversion-escrow-sdk => conversion-escrow-sdk}/src/pdas.ts (100%) rename packages/{dc-conversion-escrow-sdk => conversion-escrow-sdk}/src/resolvers.ts (81%) rename packages/{dc-conversion-escrow-sdk => conversion-escrow-sdk}/tsconfig.cjs.json (100%) rename packages/{dc-conversion-escrow-sdk => conversion-escrow-sdk}/tsconfig.esm.json (100%) rename packages/{dc-conversion-escrow-sdk => conversion-escrow-sdk}/tsconfig.json (100%) rename packages/{dc-conversion-escrow-sdk => conversion-escrow-sdk}/yarn.deploy.lock (99%) rename programs/{dc-conversion-escrow => conversion-escrow}/Cargo.toml (80%) rename programs/{dc-conversion-escrow => conversion-escrow}/Xargo.toml (100%) rename programs/{dc-conversion-escrow => conversion-escrow}/src/errors.rs (100%) rename programs/{dc-conversion-escrow => conversion-escrow}/src/instructions/initialize_escrow_v0.rs (62%) create mode 100644 programs/conversion-escrow/src/instructions/lend_v0.rs rename programs/{dc-conversion-escrow => conversion-escrow}/src/instructions/mod.rs (100%) rename programs/{dc-conversion-escrow => conversion-escrow}/src/lib.rs (91%) rename programs/{dc-conversion-escrow => conversion-escrow}/src/state.rs (67%) delete mode 100644 programs/dc-conversion-escrow/src/instructions/lend_v0.rs create mode 100644 tests/conversion-escrow.ts delete mode 100644 tests/dc-conversion-escrow.ts diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 657a022bc..3680d6e5b 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -149,7 +149,7 @@ jobs: - tests/voter-stake-registry.ts - tests/fanout.ts - tests/sus.ts - - tests/dc-conversion-escrow.ts + - tests/conversion-escrow.ts steps: - uses: actions/checkout@v3 - uses: ./.github/actions/build-anchor/ diff --git a/Anchor.toml b/Anchor.toml index 40f649edf..fb47ac48e 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -17,7 +17,7 @@ fanout = "fanqeMu3fw8R4LwKNbahPtYXJsyLL6NXyfe2BqzhfB6" mobile_entity_manager = "memMa1HG4odAFmUbGWfPwS1WWfK95k99F2YTkGvyxZr" hexboosting = "hexbnKYoA2GercNNhHUCCfrTRWrHjT6ujKPXTa5NPqJ" no_emit = "noEmmgLmQdk6DLiPV8CSwQv3qQDyGEhz9m5A4zhtByv" -dc_conversion_escrow = "dce4jeLBpfaFsNAKMAmVt5Py4E1R4mZcrVvMB5ejvGu" +conversion_escrow = "dce4jeLBpfaFsNAKMAmVt5Py4E1R4mZcrVvMB5ejvGu" [workspace] members = [ @@ -35,7 +35,7 @@ members = [ "programs/mobile-entity-manager", "programs/hexboosting", "programs/no-emit", - "programs/dc-conversion-escrow", + "programs/conversion-escrow", ] [registry] diff --git a/Cargo.lock b/Cargo.lock index 889b89917..32db4cf29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1127,6 +1127,19 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" +[[package]] +name = "conversion-escrow" +version = "0.0.1" +dependencies = [ + "anchor-lang", + "anchor-spl", + "default-env", + "pyth-sdk-solana", + "shared-utils", + "solana-security-txt", + "spl-token", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -1354,19 +1367,6 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" -[[package]] -name = "dc-conversion-escrow" -version = "0.0.1" -dependencies = [ - "anchor-lang", - "anchor-spl", - "data-credits", - "default-env", - "pyth-sdk-solana", - "shared-utils", - "solana-security-txt", -] - [[package]] name = "default-env" version = "0.1.1" diff --git a/packages/dc-conversion-escrow-sdk/.gitignore b/packages/conversion-escrow-sdk/.gitignore similarity index 100% rename from packages/dc-conversion-escrow-sdk/.gitignore rename to packages/conversion-escrow-sdk/.gitignore diff --git a/packages/dc-conversion-escrow-sdk/CHANGELOG.md b/packages/conversion-escrow-sdk/CHANGELOG.md similarity index 100% rename from packages/dc-conversion-escrow-sdk/CHANGELOG.md rename to packages/conversion-escrow-sdk/CHANGELOG.md diff --git a/packages/dc-conversion-escrow-sdk/package.json b/packages/conversion-escrow-sdk/package.json similarity index 91% rename from packages/dc-conversion-escrow-sdk/package.json rename to packages/conversion-escrow-sdk/package.json index ac6c7efd5..775185296 100644 --- a/packages/dc-conversion-escrow-sdk/package.json +++ b/packages/conversion-escrow-sdk/package.json @@ -1,12 +1,12 @@ { - "name": "@helium/dc-conversion-escrow-sdk", + "name": "@helium/conversion-escrow-sdk", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" }, "license": "Apache-2.0", "version": "0.6.41", - "description": "Interface to the dc-conversion-escrow smart contract", + "description": "Interface to the conversion-escrow smart contract", "repository": { "type": "git", "url": "https://github.com/helium/helium-program-libary" diff --git a/packages/dc-conversion-escrow-sdk/src/constants.ts b/packages/conversion-escrow-sdk/src/constants.ts similarity index 100% rename from packages/dc-conversion-escrow-sdk/src/constants.ts rename to packages/conversion-escrow-sdk/src/constants.ts diff --git a/packages/dc-conversion-escrow-sdk/src/index.ts b/packages/conversion-escrow-sdk/src/index.ts similarity index 53% rename from packages/dc-conversion-escrow-sdk/src/index.ts rename to packages/conversion-escrow-sdk/src/index.ts index d4c573551..c0e3c10a1 100644 --- a/packages/dc-conversion-escrow-sdk/src/index.ts +++ b/packages/conversion-escrow-sdk/src/index.ts @@ -1,8 +1,8 @@ -import { DcConversionEscrow } from "@helium/idls/lib/types/dc_conversion_escrow"; +import { ConversionEscrow } from "@helium/idls/lib/types/conversion_escrow"; import { AnchorProvider, Idl, Program } from "@coral-xyz/anchor"; import { PublicKey } from "@solana/web3.js"; import { PROGRAM_ID } from "./constants"; -import { dcConversionEscrowResolvers } from "./resolvers"; +import { conversionEscrowResolvers } from "./resolvers"; export * from "./constants"; @@ -12,18 +12,18 @@ export async function init( provider: AnchorProvider, programId: PublicKey = PROGRAM_ID, idl?: Idl | null -): Promise> { +): Promise> { if (!idl) { idl = await Program.fetchIdl(programId, provider); } - const dcConversionEscrow = new Program( - idl as DcConversionEscrow, + const conversionEscrow = new Program( + idl as ConversionEscrow, programId, provider, undefined, - () => dcConversionEscrowResolvers - ) as Program; + () => conversionEscrowResolvers + ) as Program; - return dcConversionEscrow; + return conversionEscrow; } diff --git a/packages/dc-conversion-escrow-sdk/src/pdas.ts b/packages/conversion-escrow-sdk/src/pdas.ts similarity index 100% rename from packages/dc-conversion-escrow-sdk/src/pdas.ts rename to packages/conversion-escrow-sdk/src/pdas.ts diff --git a/packages/dc-conversion-escrow-sdk/src/resolvers.ts b/packages/conversion-escrow-sdk/src/resolvers.ts similarity index 81% rename from packages/dc-conversion-escrow-sdk/src/resolvers.ts rename to packages/conversion-escrow-sdk/src/resolvers.ts index e70b62ce8..a94300555 100644 --- a/packages/dc-conversion-escrow-sdk/src/resolvers.ts +++ b/packages/conversion-escrow-sdk/src/resolvers.ts @@ -4,7 +4,7 @@ import { heliumCommonResolver } from "@helium/anchor-resolvers"; -export const dcConversionEscrowResolvers = combineResolvers( +export const conversionEscrowResolvers = combineResolvers( heliumCommonResolver, ataResolver({ instruction: "initializeEscrowV0", diff --git a/packages/dc-conversion-escrow-sdk/tsconfig.cjs.json b/packages/conversion-escrow-sdk/tsconfig.cjs.json similarity index 100% rename from packages/dc-conversion-escrow-sdk/tsconfig.cjs.json rename to packages/conversion-escrow-sdk/tsconfig.cjs.json diff --git a/packages/dc-conversion-escrow-sdk/tsconfig.esm.json b/packages/conversion-escrow-sdk/tsconfig.esm.json similarity index 100% rename from packages/dc-conversion-escrow-sdk/tsconfig.esm.json rename to packages/conversion-escrow-sdk/tsconfig.esm.json diff --git a/packages/dc-conversion-escrow-sdk/tsconfig.json b/packages/conversion-escrow-sdk/tsconfig.json similarity index 100% rename from packages/dc-conversion-escrow-sdk/tsconfig.json rename to packages/conversion-escrow-sdk/tsconfig.json diff --git a/packages/dc-conversion-escrow-sdk/yarn.deploy.lock b/packages/conversion-escrow-sdk/yarn.deploy.lock similarity index 99% rename from packages/dc-conversion-escrow-sdk/yarn.deploy.lock rename to packages/conversion-escrow-sdk/yarn.deploy.lock index f3c21c9c9..13f8204a9 100644 --- a/packages/dc-conversion-escrow-sdk/yarn.deploy.lock +++ b/packages/conversion-escrow-sdk/yarn.deploy.lock @@ -72,9 +72,9 @@ __metadata: languageName: unknown linkType: soft -"@helium/dc-conversion-escrow-sdk@workspace:.": +"@helium/conversion-escrow-sdk@workspace:.": version: 0.0.0-use.local - resolution: "@helium/dc-conversion-escrow-sdk@workspace:." + resolution: "@helium/conversion-escrow-sdk@workspace:." dependencies: "@coral-xyz/anchor": ^0.28.0 "@helium/anchor-resolvers": ^0.6.41 diff --git a/programs/dc-conversion-escrow/Cargo.toml b/programs/conversion-escrow/Cargo.toml similarity index 80% rename from programs/dc-conversion-escrow/Cargo.toml rename to programs/conversion-escrow/Cargo.toml index 6aed11a03..7eb9291bd 100644 --- a/programs/dc-conversion-escrow/Cargo.toml +++ b/programs/conversion-escrow/Cargo.toml @@ -1,12 +1,12 @@ [package] -name = "dc-conversion-escrow" +name = "conversion-escrow" version = "0.0.1" description = "Created with Anchor" edition = "2021" [lib] crate-type = ["cdylib", "lib"] -name = "dc_conversion_escrow" +name = "conversion_escrow" [features] devnet = [] @@ -23,8 +23,8 @@ overflow-checks = true [dependencies] anchor-lang = { workspace = true } anchor-spl = { workspace = true } +spl-token = "3.5.0" shared-utils = { workspace = true } solana-security-txt = { workspace = true } -data-credits = { path = "../data-credits", features = ["cpi"] } default-env = { workspace = true } pyth-sdk-solana = { version = "0.8.0" } diff --git a/programs/dc-conversion-escrow/Xargo.toml b/programs/conversion-escrow/Xargo.toml similarity index 100% rename from programs/dc-conversion-escrow/Xargo.toml rename to programs/conversion-escrow/Xargo.toml diff --git a/programs/dc-conversion-escrow/src/errors.rs b/programs/conversion-escrow/src/errors.rs similarity index 100% rename from programs/dc-conversion-escrow/src/errors.rs rename to programs/conversion-escrow/src/errors.rs diff --git a/programs/dc-conversion-escrow/src/instructions/initialize_escrow_v0.rs b/programs/conversion-escrow/src/instructions/initialize_escrow_v0.rs similarity index 62% rename from programs/dc-conversion-escrow/src/instructions/initialize_escrow_v0.rs rename to programs/conversion-escrow/src/instructions/initialize_escrow_v0.rs index 247c8f9e0..190e6ccd2 100644 --- a/programs/dc-conversion-escrow/src/instructions/initialize_escrow_v0.rs +++ b/programs/conversion-escrow/src/instructions/initialize_escrow_v0.rs @@ -1,22 +1,29 @@ -use crate::errors::ErrorCode; +use crate::ConversionTargetV0; use anchor_lang::prelude::*; use anchor_spl::{ associated_token::AssociatedToken, token::{Mint, Token, TokenAccount}, }; -use data_credits::DataCreditsV0; -use pyth_sdk_solana::load_price_feed_from_account_info; use crate::ConversionEscrowV0; #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] pub struct InitializeEscrowArgsV0 { + pub oracle: Pubkey, + pub targets: Vec, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct ConversionTargetArgV0 { + pub mint: Pubkey, + pub oracle: Pubkey, + /// How much slippage to allow from the oracle price pub slippage_bps: u16, } #[derive(Accounts)] +#[instruction(args: InitializeEscrowArgsV0)] pub struct InitializeEscrowV0<'info> { - pub data_credits: Box>, #[account(mut)] pub payer: Signer<'info>, /// CHECK: The owner of this account. Can fully withdraw @@ -24,9 +31,9 @@ pub struct InitializeEscrowV0<'info> { #[account( init, payer = payer, - seeds = [b"conversion_escrow", data_credits.key().as_ref(), mint.key().as_ref(), owner.key().as_ref()], + seeds = [b"conversion_escrow", mint.key().as_ref(), owner.key().as_ref()], bump, - space = ConversionEscrowV0::INIT_SPACE + 60, + space = std::mem::size_of::() + 60 + std::mem::size_of::() * args.targets.len(), )] pub conversion_escrow: Box>, pub mint: Box>, @@ -39,27 +46,28 @@ pub struct InitializeEscrowV0<'info> { pub escrow: Box>, pub system_program: Program<'info, System>, pub associated_token_program: Program<'info, AssociatedToken>, - /// CHECK: Checked with load_price_feed_from_account_info - pub oracle: AccountInfo<'info>, pub token_program: Program<'info, Token>, } pub fn handler(ctx: Context, args: InitializeEscrowArgsV0) -> Result<()> { - load_price_feed_from_account_info(&ctx.accounts.oracle).map_err(|e| { - msg!("Pyth error {}", e); - error!(ErrorCode::PythError) - })?; - ctx .accounts .conversion_escrow .set_inner(ConversionEscrowV0 { - oracle: ctx.accounts.oracle.key(), + oracle: args.oracle, escrow: ctx.accounts.escrow.key(), mint: ctx.accounts.mint.key(), - slipage_bps: args.slippage_bps, + targets: args + .targets + .iter() + .map(|t| ConversionTargetV0 { + reserverd: [0; 8], + mint: t.mint, + oracle: t.oracle, + slipage_bps: t.slippage_bps, + }) + .collect(), owner: ctx.accounts.owner.key(), - data_credits: ctx.accounts.data_credits.key(), bump_seed: ctx.bumps["conversion_escrow"], }); diff --git a/programs/conversion-escrow/src/instructions/lend_v0.rs b/programs/conversion-escrow/src/instructions/lend_v0.rs new file mode 100644 index 000000000..b838d5036 --- /dev/null +++ b/programs/conversion-escrow/src/instructions/lend_v0.rs @@ -0,0 +1,178 @@ +use crate::errors::ErrorCode; +use crate::{escrow_seeds, ConversionEscrowV0}; +use anchor_lang::prelude::*; +use anchor_lang::solana_program::sysvar; +use anchor_lang::solana_program::sysvar::instructions::{ + load_current_index_checked, load_instruction_at_checked, +}; +use anchor_spl::token::{self, transfer, Mint, Token, TokenAccount, Transfer}; +use pyth_sdk_solana::load_price_feed_from_account_info; +use spl_token::instruction::TokenInstruction; + +pub const TESTING: bool = std::option_env!("TESTING").is_some(); + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct LendArgsV0 { + pub amount: u64, +} + +#[derive(Accounts)] +pub struct LendV0<'info> { + #[account( + has_one = mint, + has_one = oracle, + has_one = escrow, + )] + pub conversion_escrow: Box>, + #[account(mut)] + pub escrow: Account<'info, TokenAccount>, + /// CHECK: Checked via pyth + pub oracle: UncheckedAccount<'info>, + /// CHECK: Checked via pyth + #[account( + constraint = conversion_escrow.targets.iter().any(|t| t.oracle == target_oracle.key()) + )] + pub target_oracle: UncheckedAccount<'info>, + pub mint: Box>, + + #[account( + mut, + token::mint = mint + )] + pub destination: Box>, + #[account( + constraint = conversion_escrow.targets.iter().find(|t| t.oracle == target_oracle.key()).unwrap().mint == repay_account.mint, + constraint = repay_account.owner == conversion_escrow.owner + )] + pub repay_account: Box>, + /// CHECK: check instructions account + #[account(address = sysvar::instructions::ID @ErrorCode::BadInstructionsAccount)] + pub instructions: UncheckedAccount<'info>, + pub token_program: Program<'info, Token>, +} + +pub fn handler(ctx: Context, args: LendArgsV0) -> Result<()> { + let ixs = ctx.accounts.instructions.to_account_info(); + + let price_feed = load_price_feed_from_account_info(&ctx.accounts.oracle).map_err(|e| { + msg!("Pyth error {}", e); + error!(ErrorCode::PythError) + })?; + + let current_time = Clock::get()?.unix_timestamp; + let source_price = price_feed + .get_ema_price_no_older_than(current_time, if TESTING { 6000000 } else { 10 * 60 }) + .ok_or_else(|| error!(ErrorCode::PythPriceNotFound))?; + // Remove the confidence from the price to use the most conservative price + // https://docs.pyth.network/price-feeds/solana-price-feeds/best-practices#confidence-intervals + let source_price_with_conf = u64::try_from(source_price.price) + .unwrap() + .checked_sub(source_price.conf.checked_mul(2).unwrap()) + .unwrap(); + + require_gt!(source_price_with_conf, 0); + + let target_price_oracle = load_price_feed_from_account_info(&ctx.accounts.target_oracle) + .map_err(|e| { + msg!("Pyth error {}", e); + error!(ErrorCode::PythError) + })?; + + let target_price = target_price_oracle + .get_ema_price_no_older_than(current_time, if TESTING { 6000000 } else { 10 * 60 }) + .ok_or_else(|| error!(ErrorCode::PythPriceNotFound))?; + + require_gt!(target_price.price, 0); + + // Remove the confidence from the price to use the most conservative price + // https://docs.pyth.network/price-feeds/solana-price-feeds/best-practices#confidence-intervals + let target_price_with_conf = u64::try_from(target_price.price) + .unwrap() + .checked_sub(target_price.conf.checked_mul(2).unwrap()) + .unwrap(); + + // USD/Sorce divided by USD/Target gets us Target/Source, in other words how much target + // we expect to be repaid per source. + let target_per_source = source_price_with_conf + .checked_div(target_price_with_conf) + .unwrap(); + let expo_diff = source_price.expo - target_price.expo; + let expected_repayment_amount = if expo_diff > 0 { + // Target has more decimals than source, need to multiply + args + .amount + .checked_mul(10_u64.pow(u32::try_from(expo_diff.abs()).unwrap())) + .unwrap() + .checked_mul(target_per_source) + .unwrap() + } else if expo_diff < 0 { + // Target has less decimals than source, need to divide + args + .amount + .checked_mul(target_per_source) + .unwrap() + .checked_div(10_u64.pow(u32::try_from(expo_diff.abs()).unwrap())) + .unwrap() + } else { + // Same decimals + args.amount.checked_mul(target_per_source).unwrap() + }; + let target = ctx + .accounts + .conversion_escrow + .targets + .iter() + .find(|target| target.oracle == ctx.accounts.target_oracle.key()) + .unwrap(); + let expected_repayment_amount_with_slippage = expected_repayment_amount + - (expected_repayment_amount + .checked_mul(u64::from(target.slipage_bps)) + .unwrap() + .checked_div(10000) + .unwrap()); + + // make sure this isnt a cpi call + let current_index = load_current_index_checked(&ixs)? as usize; + // loop through instructions, looking for an equivalent mint dc to this borrow + let mut index = current_index + 1; // jupiter swap + loop { + // get the next instruction, die if theres no more + if let Ok(ix) = load_instruction_at_checked(index, &ixs) { + if ix.program_id == token::ID { + let transfer_data = match TokenInstruction::unpack(&ix.data)? { + TokenInstruction::Transfer { amount } => Some((amount, ix.accounts[1].pubkey)), + TokenInstruction::TransferChecked { amount, .. } => Some((amount, ix.accounts[2].pubkey)), + _ => None, + }; + + if let Some((amount, account)) = transfer_data { + if ctx.accounts.repay_account.key() == account { + require_gt!(amount, expected_repayment_amount_with_slippage); + break; + } + } + } + } else { + // no more instructions, so we're missing a repay + return Err(ErrorCode::MissingRepay.into()); + } + + index += 1 + } + + // Send the loan + transfer( + CpiContext::new_with_signer( + ctx.accounts.token_program.to_account_info(), + Transfer { + from: ctx.accounts.escrow.to_account_info(), + to: ctx.accounts.destination.to_account_info(), + authority: ctx.accounts.conversion_escrow.to_account_info(), + }, + &[escrow_seeds!(ctx.accounts.conversion_escrow)], + ), + args.amount, + )?; + + Ok(()) +} diff --git a/programs/dc-conversion-escrow/src/instructions/mod.rs b/programs/conversion-escrow/src/instructions/mod.rs similarity index 100% rename from programs/dc-conversion-escrow/src/instructions/mod.rs rename to programs/conversion-escrow/src/instructions/mod.rs diff --git a/programs/dc-conversion-escrow/src/lib.rs b/programs/conversion-escrow/src/lib.rs similarity index 91% rename from programs/dc-conversion-escrow/src/lib.rs rename to programs/conversion-escrow/src/lib.rs index dfc7568ab..f9583db81 100644 --- a/programs/dc-conversion-escrow/src/lib.rs +++ b/programs/conversion-escrow/src/lib.rs @@ -13,7 +13,7 @@ pub use state::*; #[cfg(not(feature = "no-entrypoint"))] security_txt! { - name: "DC Conversion Escrow", + name: "Conversion Escrow", project_url: "http://helium.com", contacts: "email:hello@helium.foundation", policy: "https://github.com/helium/helium-program-library/tree/master/SECURITY.md", @@ -21,14 +21,14 @@ security_txt! { // Optional Fields preferred_languages: "en", - source_code: "https://github.com/helium/helium-program-library/tree/master/programs/dc-conversion-escrow", + source_code: "https://github.com/helium/helium-program-library/tree/master/programs/conversion-escrow", source_revision: default_env!("GITHUB_SHA", ""), source_release: default_env!("GITHUB_REF_NAME", ""), auditors: "Sec3" } #[program] -pub mod dc_conversion_escrow { +pub mod conversion_escrow { use super::*; pub fn initialize_escrow_v0( diff --git a/programs/dc-conversion-escrow/src/state.rs b/programs/conversion-escrow/src/state.rs similarity index 67% rename from programs/dc-conversion-escrow/src/state.rs rename to programs/conversion-escrow/src/state.rs index 8977aead4..213009d96 100644 --- a/programs/dc-conversion-escrow/src/state.rs +++ b/programs/conversion-escrow/src/state.rs @@ -1,24 +1,30 @@ use anchor_lang::prelude::*; #[account] -#[derive(Default, InitSpace)] +#[derive(Default)] pub struct ConversionEscrowV0 { pub escrow: Pubkey, pub mint: Pubkey, - /// How much slippage to allow from the oracle price - pub slipage_bps: u16, pub oracle: Pubkey, pub owner: Pubkey, - pub data_credits: Pubkey, + pub targets: Vec, pub bump_seed: u8, } +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct ConversionTargetV0 { + pub mint: Pubkey, + pub oracle: Pubkey, + /// How much slippage to allow from the oracle price + pub slipage_bps: u16, + pub reserverd: [u64; 8], +} + #[macro_export] macro_rules! escrow_seeds { ( $escrow:expr ) => { &[ "conversion_escrow".as_bytes(), - $escrow.data_credits.as_ref(), $escrow.mint.as_ref(), $escrow.owner.as_ref(), &[$escrow.bump_seed], diff --git a/programs/dc-conversion-escrow/src/instructions/lend_v0.rs b/programs/dc-conversion-escrow/src/instructions/lend_v0.rs deleted file mode 100644 index 45ad67ae9..000000000 --- a/programs/dc-conversion-escrow/src/instructions/lend_v0.rs +++ /dev/null @@ -1,187 +0,0 @@ -use std::ops::Div; - -use crate::errors::ErrorCode; -use crate::{escrow_seeds, ConversionEscrowV0}; -use anchor_lang::prelude::*; -use anchor_lang::solana_program::sysvar; -use anchor_lang::solana_program::sysvar::instructions::{ - load_current_index_checked, load_instruction_at_checked, -}; -use anchor_spl::token::{transfer, Mint, Token, TokenAccount, Transfer}; -use data_credits::{DataCreditsV0, MintDataCreditsArgsV0, TESTING}; -use pyth_sdk_solana::load_price_feed_from_account_info; - -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] -pub struct LendArgsV0 { - pub amount: u64, -} - -#[derive(Accounts)] -pub struct LendV0<'info> { - #[account( - has_one = mint, - has_one = oracle, - has_one = escrow, - has_one = data_credits, - )] - pub conversion_escrow: Box>, - #[account(mut)] - pub escrow: Account<'info, TokenAccount>, - /// CHECK: Checked via pyth - pub oracle: UncheckedAccount<'info>, - pub mint: Box>, - #[account( - has_one = hnt_price_oracle, - )] - pub data_credits: Box>, - - /// CHECK: Checked by loading with pyth. Also double checked by the has_one on data credits instance. - pub hnt_price_oracle: AccountInfo<'info>, - - #[account( - mut, - token::mint = mint - )] - pub destination: Box>, - /// CHECK: check instructions account - #[account(address = sysvar::instructions::ID @ErrorCode::BadInstructionsAccount)] - pub instructions: UncheckedAccount<'info>, - pub token_program: Program<'info, Token>, -} - -pub fn handler(ctx: Context, args: LendArgsV0) -> Result<()> { - let ixs = ctx.accounts.instructions.to_account_info(); - - let price_feed = load_price_feed_from_account_info(&ctx.accounts.oracle).map_err(|e| { - msg!("Pyth error {}", e); - error!(ErrorCode::PythError) - })?; - - let current_time = Clock::get()?.unix_timestamp; - let price = price_feed - .get_ema_price_no_older_than(current_time, if TESTING { 6000000 } else { 10 * 60 }) - .ok_or_else(|| error!(ErrorCode::PythPriceNotFound))?; - // Remove the confidence from the price to use the most conservative price - // https://docs.pyth.network/price-feeds/solana-price-feeds/best-practices#confidence-intervals - let price_with_conf = price - .price - .checked_sub(i64::try_from(price.conf.checked_mul(2).unwrap()).unwrap()) - .unwrap(); - // Exponent is a negative number, likely -8 - // Since DC is 5 decimals, this is likely -8, we need to divide by 10^(-expo - 5) - let exponent_dec = 10_i64 - .checked_pow(u32::try_from(-price.expo - 5).unwrap()) - .ok_or_else(|| error!(ErrorCode::ArithmeticError))?; - - require_gt!(price_with_conf, 0); - let expected_dc: u64 = price_with_conf - .checked_div(exponent_dec) - .unwrap() - .try_into() - .unwrap(); - - let hnt_price_oracle = load_price_feed_from_account_info(&ctx.accounts.hnt_price_oracle) - .map_err(|e| { - msg!("Pyth error {}", e); - error!(ErrorCode::PythError) - })?; - - let hnt_price = hnt_price_oracle - .get_ema_price_no_older_than(current_time, if TESTING { 6000000 } else { 10 * 60 }) - .ok_or_else(|| error!(ErrorCode::PythPriceNotFound))?; - - require_gt!(hnt_price.price, 0); - // Remove the confidence from the price to use the most conservative price - // https://docs.pyth.network/price-feeds/solana-price-feeds/best-practices#confidence-intervals - let hnt_price_with_conf = hnt_price - .price - .checked_sub(i64::try_from(hnt_price.conf.checked_mul(2).unwrap()).unwrap()) - .unwrap(); - // dc_exponent = 5 since $1 = 10^5 DC - // expo is a negative number, i.e. normally -8 for 8 hnt decimals - // dc = (price * 10^expo) * (hnt_amount * 10^-hnt_decimals) * 10^dc_exponent - // dc = price * hnt_amount * 10^(expo - hnt_decimals + dc_exponent) - // dc = price * hnt_amount / 10^(hnt_decimals - expo - dc_exponent) - // hnt_amount = dc * 10^(hnt_decimals - expo - dc_exponent) / price - let exponent = i32::from(8) - hnt_price.expo - 5; - let decimals_factor = 10_u128 - .checked_pow(u32::try_from(exponent).unwrap()) - .ok_or_else(|| error!(ErrorCode::ArithmeticError))?; - - // make sure this isnt a cpi call - let current_index = load_current_index_checked(&ixs)? as usize; - // loop through instructions, looking for an equivalent mint dc to this borrow - let mut index = current_index + 1; // jupiter swap - let discriminator = get_function_hash("global", "mint_data_credits_v0"); - loop { - // get the next instruction, die if theres no more - if let Ok(ix) = load_instruction_at_checked(index, &ixs) { - if ix.program_id == data_credits::id() { - let ix_discriminator: [u8; 8] = ix.data[0..8] - .try_into() - .map_err(|_| ErrorCode::UnknownInstruction)?; - - // check if we have a toplevel repay toward the program authority - if ix_discriminator == discriminator { - require_keys_eq!( - ix.accounts[4].pubkey, - ctx.accounts.conversion_escrow.owner, - ErrorCode::IncorrectDestination - ); - require_keys_eq!( - ix.accounts[0].pubkey, - ctx.accounts.data_credits.key(), - ErrorCode::IncorrectDc - ); - - let mint_dc_args: MintDataCreditsArgsV0 = - MintDataCreditsArgsV0::deserialize(&mut &ix.data[8..]).unwrap(); - let hnt_amount = mint_dc_args - .hnt_amount - .ok_or_else(|| error!(ErrorCode::HntAmountRequired))?; - let dc_amount_actual = u64::try_from( - u128::from(hnt_amount) - .checked_mul(u128::try_from(hnt_price_with_conf).unwrap()) - .ok_or_else(|| error!(ErrorCode::ArithmeticError))? - .div(decimals_factor), - ) - .map_err(|_| error!(ErrorCode::ArithmeticError))?; - let expected_dc_with_slippage = expected_dc - - (expected_dc * u64::from(ctx.accounts.conversion_escrow.slipage_bps / 10000)); - require_gt!(dc_amount_actual, expected_dc_with_slippage); - - break; - } - } - } else { - // no more instructions, so we're missing a repay - return Err(ErrorCode::MissingRepay.into()); - } - - index += 1 - } - - // Send the loan - transfer( - CpiContext::new_with_signer( - ctx.accounts.token_program.to_account_info(), - Transfer { - from: ctx.accounts.escrow.to_account_info(), - to: ctx.accounts.destination.to_account_info(), - authority: ctx.accounts.conversion_escrow.to_account_info(), - }, - &[escrow_seeds!(ctx.accounts.conversion_escrow)], - ), - args.amount, - )?; - - Ok(()) -} - -pub fn get_function_hash(namespace: &str, name: &str) -> [u8; 8] { - let preimage = format!("{}:{}", namespace, name); - let mut sighash = [0u8; 8]; - sighash - .copy_from_slice(&anchor_lang::solana_program::hash::hash(preimage.as_bytes()).to_bytes()[..8]); - sighash -} diff --git a/tests/conversion-escrow.ts b/tests/conversion-escrow.ts new file mode 100644 index 000000000..271aa8c0f --- /dev/null +++ b/tests/conversion-escrow.ts @@ -0,0 +1,181 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Program } from "@coral-xyz/anchor"; +import { createAtaAndMint, createMint, sendInstructions, toBN, toNumber } from "@helium/spl-utils"; +import { parsePriceData } from "@pythnetwork/client"; +import { + createAssociatedTokenAccountIdempotentInstruction, + createTransferInstruction, + getAssociatedTokenAddressSync +} from "@solana/spl-token"; +import { Keypair, PublicKey } from "@solana/web3.js"; +import { expect } from "chai"; +import { init } from "../packages/conversion-escrow-sdk/src"; +import { PROGRAM_ID } from "../packages/conversion-escrow-sdk/src/constants"; +import { ConversionEscrow } from "../target/types/conversion_escrow"; +import { ensureDcEscrowIdl } from "./utils/fixtures"; + +describe("conversion-escrow", () => { + anchor.setProvider(anchor.AnchorProvider.local("http://127.0.0.1:8899")); + + const mobileOracle = new PublicKey("JBaTytFv1CmGNkyNiLu16jFMXNZ49BGfy4bYAYZdkxg5") + const hntOracle = new PublicKey("7moA1i5vQUpfDwSpK6Pw9s56ahB7WFGidtbL2ujWrVvm") + let program: Program; + let mobileMint: PublicKey; + let hntMint: PublicKey; + let startHntBal = 10000; + let startMobileEscrowBal = 100000000; + let hntDecimals = 8; + let mobileDecimals = 6; + const provider = anchor.getProvider() as anchor.AnchorProvider; + const me = provider.wallet.publicKey; + const hntHolder = Keypair.generate(); + + beforeEach(async () => { + program = await init( + provider, + PROGRAM_ID, + anchor.workspace.ConversionEscrow.idl + ) + // fresh start + mobileMint = await createMint(provider, mobileDecimals, me, me); + hntMint = await createMint(provider, hntDecimals, me, me); + await createAtaAndMint( + provider, + hntMint, + toBN(startHntBal, hntDecimals).toNumber(), + hntHolder.publicKey + ); + }); + + describe("with data credits and escrow", async () => { + let conversionEscrow: PublicKey | undefined; + + beforeEach(async () => { + await ensureDcEscrowIdl(program); + + ({ + pubkeys: { conversionEscrow }, + } = await program.methods + .initializeEscrowV0({ + oracle: mobileOracle, + // Slippage can be super low since price isn't changing. + targets: [ + { + mint: hntMint, + slippageBps: 1, + oracle: hntOracle, + }, + ], + }) + .accounts({ + owner: me, + payer: me, + mint: mobileMint, + }) + .rpcAndKeys({ skipPreflight: true })); + + // Populate the escrow + await createAtaAndMint( + provider, + mobileMint, + toBN(startMobileEscrowBal, mobileDecimals).toNumber(), + conversionEscrow + ); + }); + + it("lends MOBILE in exchange for HNT", async () => { + // Lend enough MOBILE to `hntHolder` to get 40000 DC. HNT holder + // will then burn enough HNT to get that DC into `owner` + const pythDataMobile = (await provider.connection.getAccountInfo( + mobileOracle + ))!.data; + const priceMobile = parsePriceData(pythDataMobile); + + const mobileFloorValue = + priceMobile.emaPrice.value - priceMobile.emaConfidence!.value * 2; + + const pythData = (await provider.connection.getAccountInfo(hntOracle))! + .data; + const priceHnt = parsePriceData(pythData); + + const hntFloorValue = + priceHnt.emaPrice.value - priceHnt.emaConfidence!.value * 2; + const hntPerMobile = mobileFloorValue / hntFloorValue + + const hntHolderMobileAta = getAssociatedTokenAddressSync( + mobileMint, + hntHolder.publicKey + ); + const hntHolderHntAta = getAssociatedTokenAddressSync( + hntMint, + hntHolder.publicKey + ); + const mobileAmount = new anchor.BN(5000) + const instructions = [ + createAssociatedTokenAccountIdempotentInstruction( + me, + hntHolderMobileAta, + hntHolder.publicKey, + mobileMint + ), + createAssociatedTokenAccountIdempotentInstruction( + me, + getAssociatedTokenAddressSync(hntMint, me), + me, + hntMint + ), + await program.methods + .lendV0({ + // Goal: get 5000 MOBILE worth of HNT + amount: mobileAmount, + }) + .accounts({ + conversionEscrow, + destination: hntHolderMobileAta, + targetOracle: hntOracle, + repayAccount: getAssociatedTokenAddressSync(hntMint, me), + }) + .instruction(), + createTransferInstruction( + hntHolderHntAta, + getAssociatedTokenAddressSync(hntMint, me), + hntHolder.publicKey, + BigInt(toBN(hntPerMobile * 5000, 8).toString()) + ), + ]; + + await sendInstructions(provider, instructions, [hntHolder]); + + const hntAta = await getAssociatedTokenAddressSync( + hntMint, + hntHolder.publicKey + ); + const mobileAta = await getAssociatedTokenAddressSync( + mobileMint, + conversionEscrow!, + true + ); + const mobileBal = await provider.connection.getTokenAccountBalance( + mobileAta + ); + const hntBal = await provider.connection.getTokenAccountBalance(hntAta); + expect(mobileBal.value.uiAmount).to.eq( + // Ensure matching decimals amounts + toNumber( + toBN( + startMobileEscrowBal - toNumber(mobileAmount, mobileDecimals), + mobileDecimals + ), + mobileDecimals + ) + ); + expect(hntBal.value.uiAmount).to.eq( + // Ensure matching decimals amounts + toNumber( + toBN(startHntBal - 5000 * hntPerMobile, hntDecimals), + hntDecimals + ) + ); + }); + }); +}); diff --git a/tests/dc-conversion-escrow.ts b/tests/dc-conversion-escrow.ts deleted file mode 100644 index 833e617e3..000000000 --- a/tests/dc-conversion-escrow.ts +++ /dev/null @@ -1,293 +0,0 @@ -import * as anchor from "@coral-xyz/anchor"; -import { Program } from "@coral-xyz/anchor"; -import { VoterStakeRegistry } from "@helium/idls/lib/types/voter_stake_registry"; -import { createAtaAndMint, createMint, sendInstructions } from "@helium/spl-utils"; -import { parsePriceData } from "@pythnetwork/client"; -import { - createAssociatedTokenAccountIdempotentInstruction, - getAssociatedTokenAddressSync -} from "@solana/spl-token"; -import { Keypair, PublicKey } from "@solana/web3.js"; -import BN from "bn.js"; -import { expect } from "chai"; -import { ThresholdType } from "../packages/circuit-breaker-sdk/src"; -import { - dataCreditsKey, - init as initDc -} from "../packages/data-credits-sdk/src"; -import { PROGRAM_ID as DC_PROGRAM_ID } from "../packages/data-credits-sdk/src/constants"; -import { init } from "../packages/dc-conversion-escrow-sdk/src"; -import { PROGRAM_ID } from "../packages/dc-conversion-escrow-sdk/src/constants"; -import * as hsd from "../packages/helium-sub-daos-sdk/src"; -import { daoKey } from "../packages/helium-sub-daos-sdk/src"; -import { toBN, toNumber } from "../packages/spl-utils/src"; -import * as vsr from "../packages/voter-stake-registry-sdk/src"; -import { DataCredits } from "../target/types/data_credits"; -import { DcConversionEscrow } from "../target/types/dc_conversion_escrow"; -import { HeliumSubDaos } from "../target/types/helium_sub_daos"; -import { ensureDCIdl, ensureDcEscrowIdl, ensureHSDIdl, ensureVSRIdl } from "./utils/fixtures"; -import { initVsr } from "./utils/vsr"; - -const EPOCH_REWARDS = 100000000; - -describe("dc-conversion-escrow", () => { - anchor.setProvider(anchor.AnchorProvider.local("http://127.0.0.1:8899")); - - const mobileOracle = new PublicKey("JBaTytFv1CmGNkyNiLu16jFMXNZ49BGfy4bYAYZdkxg5") - const hntOracle = new PublicKey("7moA1i5vQUpfDwSpK6Pw9s56ahB7WFGidtbL2ujWrVvm") - let program: Program; - let dcProgram: Program; - let hsdProgram: Program; - let vsrProgram: Program; - let dcKey: PublicKey; - let mobileMint: PublicKey; - let hntMint: PublicKey; - let dcMint: PublicKey; - let startHntBal = 10000; - let startMobileEscrowBal = 100000000; - let hntDecimals = 8; - let mobileDecimals = 6; - let dcDecimals = 0; - const provider = anchor.getProvider() as anchor.AnchorProvider; - const me = provider.wallet.publicKey; - const hntHolder = Keypair.generate(); - - beforeEach(async () => { - program = await init( - provider, - PROGRAM_ID, - anchor.workspace.DcConversionEscrow.idl - ) - dcProgram = await initDc( - provider, - DC_PROGRAM_ID, - anchor.workspace.DataCredits.idl - ); - hsdProgram = await hsd.init( - provider, - hsd.PROGRAM_ID, - anchor.workspace.HeliumSubDaos.idl - ); - vsrProgram = await vsr.init( - provider, - vsr.PROGRAM_ID, - anchor.workspace.VoterStakeRegistry.idl - ); - await ensureVSRIdl(vsrProgram); - // fresh start - mobileMint = await createMint(provider, mobileDecimals, me, me); - hntMint = await createMint(provider, hntDecimals, me, me); - dcMint = await createMint(provider, dcDecimals, me, me); - await createAtaAndMint( - provider, - hntMint, - toBN(startHntBal, hntDecimals).toNumber(), - hntHolder.publicKey - ); - - const method = await dcProgram.methods - .initializeDataCreditsV0({ - authority: me, - config: { - windowSizeSeconds: new BN(60), - thresholdType: ThresholdType.Absolute as never, - threshold: new BN("10000000000000000000"), - }, - }) - .accounts({ - hntMint, - dcMint, - payer: me, - hntPriceOracle: new PublicKey( - "7moA1i5vQUpfDwSpK6Pw9s56ahB7WFGidtbL2ujWrVvm" - ), - }); - dcKey = (await method.pubkeys()).dataCredits!; - await method.rpc({ - skipPreflight: true, - }); - }); - - describe("with data credits and escrow", async () => { - let dao: PublicKey; - let conversionEscrow: PublicKey | undefined; - - beforeEach(async () => { - const registrar = ( - await initVsr( - vsrProgram, - provider, - provider.wallet.publicKey, - hntMint, - daoKey(hntMint)[0] - ) - ).registrar; - const method = await hsdProgram.methods - .initializeDaoV0({ - authority: me, - registrar, - netEmissionsCap: toBN(34.24, 8), - hstEmissionSchedule: [ - { - startUnixTime: new anchor.BN(0), - percent: 32, - }, - ], - emissionSchedule: [ - { - startUnixTime: new anchor.BN(0), - emissionsPerEpoch: new BN(EPOCH_REWARDS), - }, - ], - }) - .preInstructions([ - createAssociatedTokenAccountIdempotentInstruction( - me, - getAssociatedTokenAddressSync(hntMint, me), - me, - hntMint - ), - ]) - .accounts({ - dcMint, - hntMint, - hstPool: getAssociatedTokenAddressSync(hntMint, me), - }); - await ensureHSDIdl(hsdProgram); - await ensureDcEscrowIdl(program); - await ensureDCIdl(dcProgram); - - dao = (await method.pubkeys()).dao!; - if (!(await provider.connection.getAccountInfo(dao))) { - await method.rpc({ skipPreflight: true }); - } - console.log("start"); - - ({ - pubkeys: { conversionEscrow }, - } = await program.methods - .initializeEscrowV0({ - // Slippage can be super low since price isn't changing. - slippageBps: 1, - }) - .accounts({ - owner: me, - payer: me, - mint: mobileMint, - oracle: mobileOracle, - dataCredits: dataCreditsKey(dcMint)[0], - }) - .rpcAndKeys({ skipPreflight: true })); - console.log("done"); - - // Populate the escrow - await createAtaAndMint( - provider, - mobileMint, - toBN(startMobileEscrowBal, mobileDecimals).toNumber(), - conversionEscrow - ); - }); - - it("lends MOBILE to mint DC to owner", async () => { - // Lend enough MOBILE to `hntHolder` to get 40000 DC. HNT holder - // will then burn enough HNT to get that DC into `owner` - const pythDataMobile = (await provider.connection.getAccountInfo( - mobileOracle - ))!.data; - const priceMobile = parsePriceData(pythDataMobile); - - const mobileFloorValueInDc = Math.floor( - (priceMobile.emaPrice.value - priceMobile.emaConfidence!.value * 2) * - 10 ** 5 - ); - - const pythData = (await provider.connection.getAccountInfo(hntOracle))! - .data; - const priceHnt = parsePriceData(pythData); - - const hntFloorValueInDc = Math.floor( - (priceHnt.emaPrice.value - priceHnt.emaConfidence!.value * 2) * 10 ** 5 - ); - - const hntHolderMobileAta = getAssociatedTokenAddressSync( - mobileMint, - hntHolder.publicKey - ); - const mobileAmount = toBN(40000 / mobileFloorValueInDc, mobileDecimals); - - const instructions = [ - createAssociatedTokenAccountIdempotentInstruction( - me, - hntHolderMobileAta, - hntHolder.publicKey, - mobileMint - ), - createAssociatedTokenAccountIdempotentInstruction( - me, - getAssociatedTokenAddressSync(dcMint, me), - me, - dcMint - ), - await program.methods - .lendV0({ - // Goal: get 4000 DC - amount: mobileAmount, - }) - .accounts({ - conversionEscrow, - destination: hntHolderMobileAta, - }) - .instruction(), - await dcProgram.methods - .mintDataCreditsV0({ - hntAmount: toBN(40000 / hntFloorValueInDc, 8), - dcAmount: null, - }) - .accounts({ - dcMint, - owner: hntHolder.publicKey, - recipient: me, - }) - .instruction(), - ]; - - await sendInstructions(provider, instructions, [hntHolder]); - - const dcAta = await getAssociatedTokenAddressSync(dcMint, me); - const hntAta = await getAssociatedTokenAddressSync( - hntMint, - hntHolder.publicKey - ); - const mobileAta = await getAssociatedTokenAddressSync( - mobileMint, - conversionEscrow!, - true - ); - const dcBal = await provider.connection.getTokenAccountBalance(dcAta); - const mobileBal = await provider.connection.getTokenAccountBalance( - mobileAta - ); - const hntBal = await provider.connection.getTokenAccountBalance(hntAta); - expect(dcBal.value.uiAmount).to.eq(40000); - console.log("bal", mobileBal.value.uiAmount); - expect(mobileBal.value.uiAmount).to.eq( - // Ensure matching decimals amounts - toNumber( - toBN( - startMobileEscrowBal - toNumber(mobileAmount, mobileDecimals), - mobileDecimals - ), - mobileDecimals - ) - ); - expect(hntBal.value.uiAmount).to.eq( - // Ensure matching decimals amounts - toNumber( - toBN(startHntBal - 40000 / hntFloorValueInDc, hntDecimals), - hntDecimals - ) - ); - }); - }); -}); diff --git a/tests/utils/fixtures.ts b/tests/utils/fixtures.ts index 6b15871b4..f32bda828 100644 --- a/tests/utils/fixtures.ts +++ b/tests/utils/fixtures.ts @@ -23,7 +23,7 @@ import { HeliumSubDaos } from "../../target/types/helium_sub_daos"; import { LazyDistributor } from "../../target/types/lazy_distributor"; import { initTestDao, initTestSubdao } from "./daos"; import { random } from "./string"; -import { DcConversionEscrow } from "../../target/types/dc_conversion_escrow"; +import { ConversionEscrow } from "../../target/types/conversion_escrow"; const ANCHOR_PATH = process.env.ANCHOR_PATH || 'anchor' @@ -286,15 +286,15 @@ export async function ensureHSDIdl(hsdProgram: Program) { } } -export async function ensureDcEscrowIdl(dcEscrowProgram: Program) { +export async function ensureDcEscrowIdl(dcEscrowProgram: Program) { try { execSync( - `${ANCHOR_PATH} idl init --filepath ${__dirname}/../../target/idl/dc_conversion_escrow.json ${dcEscrowProgram.programId}`, + `${ANCHOR_PATH} idl init --filepath ${__dirname}/../../target/idl/conversion_escrow.json ${dcEscrowProgram.programId}`, { stdio: "inherit", shell: "/bin/bash" } ); } catch { execSync( - `${ANCHOR_PATH} idl upgrade --filepath ${__dirname}/../../target/idl/dc_conversion_escrow.json ${dcEscrowProgram.programId}`, + `${ANCHOR_PATH} idl upgrade --filepath ${__dirname}/../../target/idl/conversion_escrow.json ${dcEscrowProgram.programId}`, { stdio: "inherit", shell: "/bin/bash" } ); } diff --git a/tsconfig.json b/tsconfig.json index 95fab9220..ef3d8cd43 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "path": "./packages/sus" }, { - "path": "./packages/dc-conversion-escrow-sdk" + "path": "./packages/conversion-escrow-sdk" }, { "path": "./packages/hexboosting-sdk" diff --git a/yarn.lock b/yarn.lock index ce4462ecf..14b5ad1a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -675,6 +675,22 @@ __metadata: languageName: unknown linkType: soft +"@helium/conversion-escrow-sdk@workspace:packages/conversion-escrow-sdk": + version: 0.0.0-use.local + resolution: "@helium/conversion-escrow-sdk@workspace:packages/conversion-escrow-sdk" + dependencies: + "@coral-xyz/anchor": ^0.28.0 + "@helium/anchor-resolvers": ^0.6.41 + "@helium/idls": ^0.6.41 + bn.js: ^5.2.0 + bs58: ^4.0.1 + git-format-staged: ^2.1.3 + ts-loader: ^9.2.3 + ts-node: ^10.9.1 + typescript: ^5.2.2 + languageName: unknown + linkType: soft + "@helium/crons@workspace:packages/crons": version: 0.0.0-use.local resolution: "@helium/crons@workspace:packages/crons" @@ -766,22 +782,6 @@ __metadata: languageName: unknown linkType: soft -"@helium/dc-conversion-escrow-sdk@workspace:packages/dc-conversion-escrow-sdk": - version: 0.0.0-use.local - resolution: "@helium/dc-conversion-escrow-sdk@workspace:packages/dc-conversion-escrow-sdk" - dependencies: - "@coral-xyz/anchor": ^0.28.0 - "@helium/anchor-resolvers": ^0.6.41 - "@helium/idls": ^0.6.41 - bn.js: ^5.2.0 - bs58: ^4.0.1 - git-format-staged: ^2.1.3 - ts-loader: ^9.2.3 - ts-node: ^10.9.1 - typescript: ^5.2.2 - languageName: unknown - linkType: soft - "@helium/distributor-oracle@^0.6.42, @helium/distributor-oracle@workspace:packages/distributor-oracle": version: 0.0.0-use.local resolution: "@helium/distributor-oracle@workspace:packages/distributor-oracle" From 7e31b419c23009dde2c6c5dd2aa2b37a4f0d08f5 Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Thu, 21 Mar 2024 15:26:55 -0700 Subject: [PATCH 04/12] WIP --- Cargo.lock | 11 +++++++++++ .../src/instructions/initialize_escrow_v0.rs | 3 +++ programs/conversion-escrow/src/state.rs | 1 + programs/mobile-entity-manager/src/state.rs | 17 +++++++++++++++++ 4 files changed, 32 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 32db4cf29..217cd1dae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3876,6 +3876,17 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slashing-conversion-escrow" +version = "0.0.1" +dependencies = [ + "anchor-lang", + "anchor-spl", + "default-env", + "shared-utils", + "solana-security-txt", +] + [[package]] name = "smallvec" version = "1.11.0" diff --git a/programs/conversion-escrow/src/instructions/initialize_escrow_v0.rs b/programs/conversion-escrow/src/instructions/initialize_escrow_v0.rs index 190e6ccd2..95cf21916 100644 --- a/programs/conversion-escrow/src/instructions/initialize_escrow_v0.rs +++ b/programs/conversion-escrow/src/instructions/initialize_escrow_v0.rs @@ -28,6 +28,8 @@ pub struct InitializeEscrowV0<'info> { pub payer: Signer<'info>, /// CHECK: The owner of this account. Can fully withdraw pub owner: Signer<'info>, + /// CHECK: The update authority + pub update_authority: AccountInfo<'info>, #[account( init, payer = payer, @@ -68,6 +70,7 @@ pub fn handler(ctx: Context, args: InitializeEscrowArgsV0) - }) .collect(), owner: ctx.accounts.owner.key(), + update_authority: ctx.accounts.update_authority.key(), bump_seed: ctx.bumps["conversion_escrow"], }); diff --git a/programs/conversion-escrow/src/state.rs b/programs/conversion-escrow/src/state.rs index 213009d96..3885296d5 100644 --- a/programs/conversion-escrow/src/state.rs +++ b/programs/conversion-escrow/src/state.rs @@ -7,6 +7,7 @@ pub struct ConversionEscrowV0 { pub mint: Pubkey, pub oracle: Pubkey, pub owner: Pubkey, + pub update_authority: Pubkey, pub targets: Vec, pub bump_seed: u8, } diff --git a/programs/mobile-entity-manager/src/state.rs b/programs/mobile-entity-manager/src/state.rs index 8629901f8..782422fe0 100644 --- a/programs/mobile-entity-manager/src/state.rs +++ b/programs/mobile-entity-manager/src/state.rs @@ -16,6 +16,23 @@ pub struct CarrierV0 { pub hexboost_authority: Pubkey, } +pub struct HotspotConfigV0 { + pub sub_dao: Pubkey, + pub staking_requirement: u64, +} + +pub struct HotspotVoucherV0 { + pub device_type: MobileDeviceTypeV0, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, Default, PartialEq)] +pub enum MobileDeviceTypeV0 { + #[default] + Cbrs, + WifiIndoor, + WifiOutdoor, +} + #[macro_export] macro_rules! carrier_seeds { ( $carrier:expr ) => { From a8e5b130bd5bf8cf9328f0a15b0b9c50ebb167d3 Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Mon, 25 Mar 2024 10:57:44 -0700 Subject: [PATCH 05/12] WIP --- .../slashing-conversion-escrow/Cargo.toml | 28 +++ .../slashing-conversion-escrow/Xargo.toml | 2 + .../slashing-conversion-escrow/src/errors.rs | 1 + .../initialize_slashing_escrow_v0.rs | 75 ++++++++ .../src/instructions/lend_v0.rs | 178 ++++++++++++++++++ .../src/instructions/mod.rs | 5 + .../slashing-conversion-escrow/src/lib.rs | 33 ++++ .../slashing-conversion-escrow/src/state.rs | 11 ++ 8 files changed, 333 insertions(+) create mode 100644 programs/slashing-conversion-escrow/Cargo.toml create mode 100644 programs/slashing-conversion-escrow/Xargo.toml create mode 100644 programs/slashing-conversion-escrow/src/errors.rs create mode 100644 programs/slashing-conversion-escrow/src/instructions/initialize_slashing_escrow_v0.rs create mode 100644 programs/slashing-conversion-escrow/src/instructions/lend_v0.rs create mode 100644 programs/slashing-conversion-escrow/src/instructions/mod.rs create mode 100644 programs/slashing-conversion-escrow/src/lib.rs create mode 100644 programs/slashing-conversion-escrow/src/state.rs diff --git a/programs/slashing-conversion-escrow/Cargo.toml b/programs/slashing-conversion-escrow/Cargo.toml new file mode 100644 index 000000000..d261c6b3a --- /dev/null +++ b/programs/slashing-conversion-escrow/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "slashing-conversion-escrow" +version = "0.0.1" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "slashing_coversion_escrow" + +[features] +devnet = [] +no-genesis = [] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] + +[profile.release] +overflow-checks = true + +[dependencies] +anchor-lang = { workspace = true } +anchor-spl = { workspace = true } +shared-utils = { workspace = true } +solana-security-txt = { workspace = true } +default-env = { workspace = true } diff --git a/programs/slashing-conversion-escrow/Xargo.toml b/programs/slashing-conversion-escrow/Xargo.toml new file mode 100644 index 000000000..475fb71ed --- /dev/null +++ b/programs/slashing-conversion-escrow/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/programs/slashing-conversion-escrow/src/errors.rs b/programs/slashing-conversion-escrow/src/errors.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/programs/slashing-conversion-escrow/src/errors.rs @@ -0,0 +1 @@ + diff --git a/programs/slashing-conversion-escrow/src/instructions/initialize_slashing_escrow_v0.rs b/programs/slashing-conversion-escrow/src/instructions/initialize_slashing_escrow_v0.rs new file mode 100644 index 000000000..190e6ccd2 --- /dev/null +++ b/programs/slashing-conversion-escrow/src/instructions/initialize_slashing_escrow_v0.rs @@ -0,0 +1,75 @@ +use crate::ConversionTargetV0; +use anchor_lang::prelude::*; +use anchor_spl::{ + associated_token::AssociatedToken, + token::{Mint, Token, TokenAccount}, +}; + +use crate::ConversionEscrowV0; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct InitializeEscrowArgsV0 { + pub oracle: Pubkey, + pub targets: Vec, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct ConversionTargetArgV0 { + pub mint: Pubkey, + pub oracle: Pubkey, + /// How much slippage to allow from the oracle price + pub slippage_bps: u16, +} + +#[derive(Accounts)] +#[instruction(args: InitializeEscrowArgsV0)] +pub struct InitializeEscrowV0<'info> { + #[account(mut)] + pub payer: Signer<'info>, + /// CHECK: The owner of this account. Can fully withdraw + pub owner: Signer<'info>, + #[account( + init, + payer = payer, + seeds = [b"conversion_escrow", mint.key().as_ref(), owner.key().as_ref()], + bump, + space = std::mem::size_of::() + 60 + std::mem::size_of::() * args.targets.len(), + )] + pub conversion_escrow: Box>, + pub mint: Box>, + #[account( + init, + payer = payer, + associated_token::authority = conversion_escrow, + associated_token::mint = mint + )] + pub escrow: Box>, + pub system_program: Program<'info, System>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub token_program: Program<'info, Token>, +} + +pub fn handler(ctx: Context, args: InitializeEscrowArgsV0) -> Result<()> { + ctx + .accounts + .conversion_escrow + .set_inner(ConversionEscrowV0 { + oracle: args.oracle, + escrow: ctx.accounts.escrow.key(), + mint: ctx.accounts.mint.key(), + targets: args + .targets + .iter() + .map(|t| ConversionTargetV0 { + reserverd: [0; 8], + mint: t.mint, + oracle: t.oracle, + slipage_bps: t.slippage_bps, + }) + .collect(), + owner: ctx.accounts.owner.key(), + bump_seed: ctx.bumps["conversion_escrow"], + }); + + Ok(()) +} diff --git a/programs/slashing-conversion-escrow/src/instructions/lend_v0.rs b/programs/slashing-conversion-escrow/src/instructions/lend_v0.rs new file mode 100644 index 000000000..b838d5036 --- /dev/null +++ b/programs/slashing-conversion-escrow/src/instructions/lend_v0.rs @@ -0,0 +1,178 @@ +use crate::errors::ErrorCode; +use crate::{escrow_seeds, ConversionEscrowV0}; +use anchor_lang::prelude::*; +use anchor_lang::solana_program::sysvar; +use anchor_lang::solana_program::sysvar::instructions::{ + load_current_index_checked, load_instruction_at_checked, +}; +use anchor_spl::token::{self, transfer, Mint, Token, TokenAccount, Transfer}; +use pyth_sdk_solana::load_price_feed_from_account_info; +use spl_token::instruction::TokenInstruction; + +pub const TESTING: bool = std::option_env!("TESTING").is_some(); + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct LendArgsV0 { + pub amount: u64, +} + +#[derive(Accounts)] +pub struct LendV0<'info> { + #[account( + has_one = mint, + has_one = oracle, + has_one = escrow, + )] + pub conversion_escrow: Box>, + #[account(mut)] + pub escrow: Account<'info, TokenAccount>, + /// CHECK: Checked via pyth + pub oracle: UncheckedAccount<'info>, + /// CHECK: Checked via pyth + #[account( + constraint = conversion_escrow.targets.iter().any(|t| t.oracle == target_oracle.key()) + )] + pub target_oracle: UncheckedAccount<'info>, + pub mint: Box>, + + #[account( + mut, + token::mint = mint + )] + pub destination: Box>, + #[account( + constraint = conversion_escrow.targets.iter().find(|t| t.oracle == target_oracle.key()).unwrap().mint == repay_account.mint, + constraint = repay_account.owner == conversion_escrow.owner + )] + pub repay_account: Box>, + /// CHECK: check instructions account + #[account(address = sysvar::instructions::ID @ErrorCode::BadInstructionsAccount)] + pub instructions: UncheckedAccount<'info>, + pub token_program: Program<'info, Token>, +} + +pub fn handler(ctx: Context, args: LendArgsV0) -> Result<()> { + let ixs = ctx.accounts.instructions.to_account_info(); + + let price_feed = load_price_feed_from_account_info(&ctx.accounts.oracle).map_err(|e| { + msg!("Pyth error {}", e); + error!(ErrorCode::PythError) + })?; + + let current_time = Clock::get()?.unix_timestamp; + let source_price = price_feed + .get_ema_price_no_older_than(current_time, if TESTING { 6000000 } else { 10 * 60 }) + .ok_or_else(|| error!(ErrorCode::PythPriceNotFound))?; + // Remove the confidence from the price to use the most conservative price + // https://docs.pyth.network/price-feeds/solana-price-feeds/best-practices#confidence-intervals + let source_price_with_conf = u64::try_from(source_price.price) + .unwrap() + .checked_sub(source_price.conf.checked_mul(2).unwrap()) + .unwrap(); + + require_gt!(source_price_with_conf, 0); + + let target_price_oracle = load_price_feed_from_account_info(&ctx.accounts.target_oracle) + .map_err(|e| { + msg!("Pyth error {}", e); + error!(ErrorCode::PythError) + })?; + + let target_price = target_price_oracle + .get_ema_price_no_older_than(current_time, if TESTING { 6000000 } else { 10 * 60 }) + .ok_or_else(|| error!(ErrorCode::PythPriceNotFound))?; + + require_gt!(target_price.price, 0); + + // Remove the confidence from the price to use the most conservative price + // https://docs.pyth.network/price-feeds/solana-price-feeds/best-practices#confidence-intervals + let target_price_with_conf = u64::try_from(target_price.price) + .unwrap() + .checked_sub(target_price.conf.checked_mul(2).unwrap()) + .unwrap(); + + // USD/Sorce divided by USD/Target gets us Target/Source, in other words how much target + // we expect to be repaid per source. + let target_per_source = source_price_with_conf + .checked_div(target_price_with_conf) + .unwrap(); + let expo_diff = source_price.expo - target_price.expo; + let expected_repayment_amount = if expo_diff > 0 { + // Target has more decimals than source, need to multiply + args + .amount + .checked_mul(10_u64.pow(u32::try_from(expo_diff.abs()).unwrap())) + .unwrap() + .checked_mul(target_per_source) + .unwrap() + } else if expo_diff < 0 { + // Target has less decimals than source, need to divide + args + .amount + .checked_mul(target_per_source) + .unwrap() + .checked_div(10_u64.pow(u32::try_from(expo_diff.abs()).unwrap())) + .unwrap() + } else { + // Same decimals + args.amount.checked_mul(target_per_source).unwrap() + }; + let target = ctx + .accounts + .conversion_escrow + .targets + .iter() + .find(|target| target.oracle == ctx.accounts.target_oracle.key()) + .unwrap(); + let expected_repayment_amount_with_slippage = expected_repayment_amount + - (expected_repayment_amount + .checked_mul(u64::from(target.slipage_bps)) + .unwrap() + .checked_div(10000) + .unwrap()); + + // make sure this isnt a cpi call + let current_index = load_current_index_checked(&ixs)? as usize; + // loop through instructions, looking for an equivalent mint dc to this borrow + let mut index = current_index + 1; // jupiter swap + loop { + // get the next instruction, die if theres no more + if let Ok(ix) = load_instruction_at_checked(index, &ixs) { + if ix.program_id == token::ID { + let transfer_data = match TokenInstruction::unpack(&ix.data)? { + TokenInstruction::Transfer { amount } => Some((amount, ix.accounts[1].pubkey)), + TokenInstruction::TransferChecked { amount, .. } => Some((amount, ix.accounts[2].pubkey)), + _ => None, + }; + + if let Some((amount, account)) = transfer_data { + if ctx.accounts.repay_account.key() == account { + require_gt!(amount, expected_repayment_amount_with_slippage); + break; + } + } + } + } else { + // no more instructions, so we're missing a repay + return Err(ErrorCode::MissingRepay.into()); + } + + index += 1 + } + + // Send the loan + transfer( + CpiContext::new_with_signer( + ctx.accounts.token_program.to_account_info(), + Transfer { + from: ctx.accounts.escrow.to_account_info(), + to: ctx.accounts.destination.to_account_info(), + authority: ctx.accounts.conversion_escrow.to_account_info(), + }, + &[escrow_seeds!(ctx.accounts.conversion_escrow)], + ), + args.amount, + )?; + + Ok(()) +} diff --git a/programs/slashing-conversion-escrow/src/instructions/mod.rs b/programs/slashing-conversion-escrow/src/instructions/mod.rs new file mode 100644 index 000000000..f68714218 --- /dev/null +++ b/programs/slashing-conversion-escrow/src/instructions/mod.rs @@ -0,0 +1,5 @@ +pub mod initialize_slashing_escrow_v0; +pub mod lend_v0; + +pub use initialize_slashing_escrow_v0::*; +pub use lend_v0::*; diff --git a/programs/slashing-conversion-escrow/src/lib.rs b/programs/slashing-conversion-escrow/src/lib.rs new file mode 100644 index 000000000..79b16d6ec --- /dev/null +++ b/programs/slashing-conversion-escrow/src/lib.rs @@ -0,0 +1,33 @@ +use anchor_lang::prelude::*; +#[cfg(not(feature = "no-entrypoint"))] +use {default_env::default_env, solana_security_txt::security_txt}; + +declare_id!("onboHxQSGJH2PCkFG3WPp5K3zWHFC57nBNW4dYFyxLo"); + +pub mod errors; +pub mod instructions; +pub mod state; + +pub use instructions::*; +pub use state::*; + +#[cfg(not(feature = "no-entrypoint"))] +security_txt! { + name: "Slashing Conversion Escrow", + project_url: "http://helium.com", + contacts: "email:hello@helium.foundation", + policy: "https://github.com/helium/helium-program-library/tree/master/SECURITY.md", + + + // Optional Fields + preferred_languages: "en", + source_code: "https://github.com/helium/helium-program-library/tree/master/programs/slashing-conversion-escrow", + source_revision: default_env!("GITHUB_SHA", ""), + source_release: default_env!("GITHUB_REF_NAME", ""), + auditors: "Sec3" +} + +#[program] +pub mod slashing_conversion_escrow { + use super::*; +} diff --git a/programs/slashing-conversion-escrow/src/state.rs b/programs/slashing-conversion-escrow/src/state.rs new file mode 100644 index 000000000..3fbbfa68b --- /dev/null +++ b/programs/slashing-conversion-escrow/src/state.rs @@ -0,0 +1,11 @@ +use anchor_lang::prelude::*; + +#[account] +#[derive(Default)] +pub struct SlashingConversionEscrowV0 { + pub main_conversion_escrow: Pubkey, + pub slashing_conversion_escrow: Pubkey, + /// 50% funds slashed to this account, other 50% on main conversion. + pub insurance_fund: Pubkey, + pub owner: Pubkey, +} From 656f0dea29ccbe949a286c911b36dc8826000c4c Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Thu, 28 Mar 2024 14:45:46 -0700 Subject: [PATCH 06/12] Finished mobile voucher with no onboard --- Anchor.toml | 6 +- Cargo.lock | 2 + .../src/heliumCommonResolver.ts | 4 + .../conversion-escrow-sdk/src/constants.ts | 2 +- .../helium-admin-cli/src/create-common-lut.ts | 157 +++++++++ .../helium-entity-manager-sdk/package.json | 1 + .../src/functions/payMobileVoucherDc.ts | 134 ++++++++ .../src/functions/payMobileVoucherMobile.ts | 130 ++++++++ .../src/functions/utils.ts | 13 + .../helium-entity-manager-sdk/src/index.ts | 2 + .../helium-entity-manager-sdk/src/pdas.ts | 38 +++ .../src/resolvers.ts | 42 ++- .../helium-entity-manager-sdk/tsconfig.json | 3 + packages/spl-utils/src/constants.ts | 21 +- packages/spl-utils/src/transaction.ts | 53 ++++ programs/conversion-escrow/src/errors.rs | 5 +- .../src/instructions/check_repay_v0.rs | 20 ++ .../src/instructions/initialize_escrow_v0.rs | 6 +- .../src/instructions/lend_v0.rs | 40 ++- .../conversion-escrow/src/instructions/mod.rs | 2 + programs/conversion-escrow/src/lib.rs | 6 +- programs/conversion-escrow/src/state.rs | 8 +- programs/helium-entity-manager/Cargo.toml | 2 + programs/helium-entity-manager/src/error.rs | 6 + .../initialize_maker_escrow_v0.rs | 91 ++++++ .../src/instructions/initialize_maker_v0.rs | 1 + .../src/instructions/maker_lend_v0.rs | 132 ++++++++ .../initialize_mobile_hotspot_voucher_v0.rs | 68 ++++ .../mobile_voucher_pay_dc_v0.rs | 158 ++++++++++ .../mobile_voucher_pay_mobile_v0.rs | 116 +++++++ .../mobile_voucher_verify_owner_v0.rs | 19 ++ .../src/instructions/mobile_voucher/mod.rs | 9 + .../src/instructions/mod.rs | 7 + programs/helium-entity-manager/src/lib.rs | 39 +++ programs/helium-entity-manager/src/state.rs | 43 +++ tests/conversion-escrow.ts | 12 +- tests/helium-entity-manager.ts | 298 +++++++++++++++++- tests/utils/fixtures.ts | 2 +- 38 files changed, 1660 insertions(+), 38 deletions(-) create mode 100644 packages/helium-admin-cli/src/create-common-lut.ts create mode 100644 packages/helium-entity-manager-sdk/src/functions/payMobileVoucherDc.ts create mode 100644 packages/helium-entity-manager-sdk/src/functions/payMobileVoucherMobile.ts create mode 100644 packages/helium-entity-manager-sdk/src/functions/utils.ts create mode 100644 programs/conversion-escrow/src/instructions/check_repay_v0.rs create mode 100644 programs/helium-entity-manager/src/instructions/initialize_maker_escrow_v0.rs create mode 100644 programs/helium-entity-manager/src/instructions/maker_lend_v0.rs create mode 100644 programs/helium-entity-manager/src/instructions/mobile_voucher/initialize_mobile_hotspot_voucher_v0.rs create mode 100644 programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_pay_dc_v0.rs create mode 100644 programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_pay_mobile_v0.rs create mode 100644 programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_verify_owner_v0.rs create mode 100644 programs/helium-entity-manager/src/instructions/mobile_voucher/mod.rs diff --git a/Anchor.toml b/Anchor.toml index fb47ac48e..4ac08d00c 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -17,7 +17,7 @@ fanout = "fanqeMu3fw8R4LwKNbahPtYXJsyLL6NXyfe2BqzhfB6" mobile_entity_manager = "memMa1HG4odAFmUbGWfPwS1WWfK95k99F2YTkGvyxZr" hexboosting = "hexbnKYoA2GercNNhHUCCfrTRWrHjT6ujKPXTa5NPqJ" no_emit = "noEmmgLmQdk6DLiPV8CSwQv3qQDyGEhz9m5A4zhtByv" -conversion_escrow = "dce4jeLBpfaFsNAKMAmVt5Py4E1R4mZcrVvMB5ejvGu" +conversion_escrow = "cnvEguKeWyyWnKxoQ9HwrzEVfztqKjwNmerDvxdHK9w" [workspace] members = [ @@ -85,6 +85,10 @@ address = "JBaTytFv1CmGNkyNiLu16jFMXNZ49BGfy4bYAYZdkxg5" # Mobile price oracle [[test.validator.clone]] address = "7moA1i5vQUpfDwSpK6Pw9s56ahB7WFGidtbL2ujWrVvm" +# Pyth usdc price oracle +[[test.validator.clone]] +address = "Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD" + # Squads multisig program [[test.validator.clone]] address = "SMPLecH534NA9acpos4G6x7uf3LWbCAwZQE9e8ZekMu" diff --git a/Cargo.lock b/Cargo.lock index 217cd1dae..fb0ea3e88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1959,6 +1959,8 @@ dependencies = [ "bs58 0.3.1", "bubblegum-cpi", "bytemuck", + "circuit-breaker", + "conversion-escrow", "data-credits", "default-env", "helium-sub-daos", diff --git a/packages/anchor-resolvers/src/heliumCommonResolver.ts b/packages/anchor-resolvers/src/heliumCommonResolver.ts index 1ec207865..07c9709b4 100644 --- a/packages/anchor-resolvers/src/heliumCommonResolver.ts +++ b/packages/anchor-resolvers/src/heliumCommonResolver.ts @@ -4,6 +4,10 @@ import { resolveIndividual } from "./individual"; export const heliumCommonResolver = resolveIndividual(async ({ path }) => { switch (path[path.length - 1]) { + case "conversionEscrowProgram": + return new PublicKey("cnvEguKeWyyWnKxoQ9HwrzEVfztqKjwNmerDvxdHK9w"); + case "circuitBreakerProgram": + return new PublicKey("circAbx64bbsscPbQzZAUvuXpHqrCe6fLMzc2uKXz9g"); case "dataCreditsProgram": return new PublicKey("credMBJhYFzfn7NxBMdU4aUqFggAjgztaCcv2Fo6fPT"); case "tokenMetadataProgram": diff --git a/packages/conversion-escrow-sdk/src/constants.ts b/packages/conversion-escrow-sdk/src/constants.ts index 0f89f8d13..140f72ed3 100644 --- a/packages/conversion-escrow-sdk/src/constants.ts +++ b/packages/conversion-escrow-sdk/src/constants.ts @@ -1,3 +1,3 @@ import { PublicKey } from "@solana/web3.js"; -export const PROGRAM_ID = new PublicKey("dce4jeLBpfaFsNAKMAmVt5Py4E1R4mZcrVvMB5ejvGu"); +export const PROGRAM_ID = new PublicKey("cnvEguKeWyyWnKxoQ9HwrzEVfztqKjwNmerDvxdHK9w"); diff --git a/packages/helium-admin-cli/src/create-common-lut.ts b/packages/helium-admin-cli/src/create-common-lut.ts new file mode 100644 index 000000000..0af3ab69f --- /dev/null +++ b/packages/helium-admin-cli/src/create-common-lut.ts @@ -0,0 +1,157 @@ +import * as anchor from "@coral-xyz/anchor"; +import { chunks, sendInstructions, truthy, withPriorityFees } from "@helium/spl-utils"; +import { + AddressLookupTableProgram, + PublicKey, + SystemProgram, +} from "@solana/web3.js"; +import Squads from "@sqds/sdk"; +import os from "os"; +import yargs from "yargs/yargs"; +import { loadKeypair, sendInstructionsOrSquads } from "./utils"; + +export async function run(args: any = process.argv) { + const yarg = yargs(args).options({ + wallet: { + alias: "k", + describe: "Anchor wallet keypair", + default: `${os.homedir()}/.config/solana/id.json`, + }, + url: { + alias: "u", + default: "http://127.0.0.1:8899", + describe: "The solana url", + }, + multisig: { + type: "string", + describe: + "Address of the squads multisig to be authority. If not provided, your wallet will be the authority", + }, + authorityIndex: { + type: "number", + describe: "Authority index for squads. Defaults to 1", + default: 1, + }, + }); + const argv = await yarg.argv; + process.env.ANCHOR_WALLET = argv.wallet; + process.env.ANCHOR_PROVIDER_URL = argv.url; + anchor.setProvider(anchor.AnchorProvider.local(argv.url)); + const provider = anchor.getProvider() as anchor.AnchorProvider; + const wallet = new anchor.Wallet(loadKeypair(argv.wallet)); + const squads = Squads.endpoint(process.env.ANCHOR_PROVIDER_URL, wallet, { + commitmentOrConfig: "finalized", + }); + let authority = provider.wallet.publicKey; + let multisig = argv.multisig ? new PublicKey(argv.multisig) : null; + if (multisig) { + authority = squads.getAuthorityPDA(multisig, argv.authorityIndex); + } + + const accounts = [ + // Programs + "1azyuavdMyvsivtNxPoz6SucD18eDHeXzFCUPq5XU7w", + "hdaoVTCqhfHHo75XdAMxBKdUqvq1i5bF23sisBqVgGR", + "credMBJhYFzfn7NxBMdU4aUqFggAjgztaCcv2Fo6fPT", + "hemjuPXBpNvggtaUnN1MwT3wrdhttKEfosTcc2P9Pg8", + "circAbx64bbsscPbQzZAUvuXpHqrCe6fLMzc2uKXz9g", + "treaf4wWBBty3fHdyBpo35Mz84M8k3heKXmjmi9vFt5", + "1atrmQs3eq1N2FEYWu6tyTXbCjP4uQwExpjtnhXtS8h", + "porcSnvH9pvcYPmQ65Y8qcZSRxQBiBBQX7UV5nmBegy", + "rorcfdX4h9m9swCKgcypaHJ8NGYVANBpmV9EHn3cYrF", + "hvsrNC3NKbcryqDs2DocYHZ9yPKEVzdSjQG6RVtK1s8", + "fanqeMu3fw8R4LwKNbahPtYXJsyLL6NXyfe2BqzhfB6", + "memMa1HG4odAFmUbGWfPwS1WWfK95k99F2YTkGvyxZr", + "hexbnKYoA2GercNNhHUCCfrTRWrHjT6ujKPXTa5NPqJ", + "noEmmgLmQdk6DLiPV8CSwQv3qQDyGEhz9m5A4zhtByv", + "cnvEguKeWyyWnKxoQ9HwrzEVfztqKjwNmerDvxdHK9w", + "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "11111111111111111111111111111111", + // Lazy distributor IOT + "37eiz5KzYwpAdLgrSh8GT1isKiJ6hcE5ET86dqaoCugL", + // Lazy dist Oracle + "orc1TYY5L4B4ZWDEMayTqu99ikPM9bQo9fqzoaCPP5Q", + // Oracle signer + "7WuVr8SGcZ4KxpHBEdRGVTeSwkhk1WGUXT7DEzvWpYu4", + // Lazy dist mobile + "GZtTp3AUo2AHdQe9BCJ6gXR9KqfruRvHnZ4QiJUALMcz", + // Hnt pyth + "7moA1i5vQUpfDwSpK6Pw9s56ahB7WFGidtbL2ujWrVvm", + // Mobile pyth + "JBaTytFv1CmGNkyNiLu16jFMXNZ49BGfy4bYAYZdkxg5", + // Usdc pyth + "Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD", + // Hnt + "hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux", + // dc + "dcuc8Amr83Wz27ZkQ2K9NS6r8zRpf1J6cvArEBDZDmm", + // Mobile + "mb1eu7TzEc71KxDpsmsKoucSSuuoGLv1drys1oP2jh6", + // Dao + "BQ3MCuTT5zVBhNfQ4SjMh3NPVhFy73MPV8rjfq5d1zie", + // Mobile subdao + "Gm9xDCJawDEKDrrQW6haw94gABaYzQwCq4ZQU8h8bd22", + // Iot subdao + "39Lw1RH6zt8AJvKn3BTxmUDofzduCM2J3kSaGDZ8L7Sk", + // Mobile Delegator pool + "71Y96vbVWYkoVQUgVsd8LSBRRDrgp5pf1sKznM5KuaA7", + // Mobile Delegator pool circuit breaker + "2cocTPZ7aRT62wTDGkosF98oo4iqCtkZnFdNHWqNZLuS", + // Iot delegator pool + "6fvj6rSwTeCkY7i45jYZYpZEhKmPRSTmA29hUDiMSFtU", + // Iot delegator pool circuit breaker + "6mNUqFAyLBkV88Nj6ctrcv66iJMHwzNzV8XFUwLmGerG", + // Mobile rewardable entity config + "EP1FNydbuKjGopwXVrVLR71NnC9YMRbTg9aHfk3KfMRj", + // Compression proram + "cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK", + // Mobile escrow circuit breaker + "4qGj88CX3McdTXEviEaqeP2pnZJxRTsZFWyU3Mrnbku4", + // Mobile escrow + "GD37bnQdGkDsjNqnVGr9qWTnQJSKMHbsiXX9tXLMUcaL", + // Iot escrow + "4po3YMfioHkNP4mL4N46UWJvBoQDS2HFjzGm1ifrUWuZ", + // Iot escrow circuit breaker + "5veMSa4ks66zydSaKSPMhV7H2eF88HvuKDArScNH9jaG", + // Hnt registrar + "BMnWRWZrWqb6JMKznaDqNxWaWAHoaTzVabM6Qwyh3WKz", + // Data credits + "D1LbvrJQ9K2WbGPMbM3Fnrf5PSsDH1TDpjqJdHuvs81n", + ].map((a) => { + return new PublicKey(a); + }); + + const slot = await provider.connection.getSlot(); + const [lookupTableInst, lookupTableAddress] = + AddressLookupTableProgram.createLookupTable({ + authority, + payer: authority, + recentSlot: slot, + }); + let isFirst = true; + for (const addresses of chunks(accounts, 20)) { + await sendInstructionsOrSquads({ + provider, + signers: [], + squads, + multisig: multisig!, + authorityIndex: argv.authorityIndex, + instructions: await withPriorityFees({ + connection: provider.connection, + computeUnits: 200000, + instructions: [ + isFirst ? lookupTableInst : undefined, + AddressLookupTableProgram.extendLookupTable({ + payer: authority, + authority, + lookupTable: lookupTableAddress, + addresses: addresses, + }), + ].filter(truthy), + }), + }); + isFirst = false; + } + console.log("lookup table address:", lookupTableAddress.toBase58()); +} diff --git a/packages/helium-entity-manager-sdk/package.json b/packages/helium-entity-manager-sdk/package.json index e5d88c24e..7f43aeec1 100644 --- a/packages/helium-entity-manager-sdk/package.json +++ b/packages/helium-entity-manager-sdk/package.json @@ -35,6 +35,7 @@ "@helium/address": "^4.10.2", "@helium/anchor-resolvers": "^0.6.42", "@helium/helium-sub-daos-sdk": "^0.6.42", + "@helium/conversion-excrow-sdk": "^0.6.42", "@helium/idls": "^0.6.42", "@helium/no-emit-sdk": "^0.6.42", "@helium/spl-utils": "^0.6.42", diff --git a/packages/helium-entity-manager-sdk/src/functions/payMobileVoucherDc.ts b/packages/helium-entity-manager-sdk/src/functions/payMobileVoucherDc.ts new file mode 100644 index 000000000..13e36a1f7 --- /dev/null +++ b/packages/helium-entity-manager-sdk/src/functions/payMobileVoucherDc.ts @@ -0,0 +1,134 @@ +import { AnchorProvider, Program } from "@coral-xyz/anchor"; +import { conversionEscrowKey } from "@helium/conversion-escrow-sdk"; +import { ConversionEscrow } from "@helium/idls/lib/types/conversion_escrow"; +import { HeliumEntityManager } from "@helium/idls/lib/types/helium_entity_manager"; +import { + HNT_MINT, + HNT_PYTH_PRICE_FEED, + USDC_MINT, + USDC_PYTH_PRICE_FEED, + toBN, + toNumber +} from "@helium/spl-utils"; +import { getAssociatedTokenAddressSync } from "@solana/spl-token"; +import { PublicKey } from "@solana/web3.js"; +import { deserializeInstruction } from "./utils"; + +export const JUPITER_URL = + process.env.JUPITER_URL || "https://quote-api.jup.ag/v6"; + +export async function payMobileVoucherDc({ + program, + ceProgram, + mobileHotspotVoucher, + verifiedOwner, + maker, + payer = (program.provider as AnchorProvider).wallet.publicKey, +}: { + program: Program; + ceProgram: Program; + mobileHotspotVoucher: PublicKey; + verifiedOwner?: PublicKey; + payer?: PublicKey; + maker?: PublicKey; +}) { + const voucher = await program.account.mobileHotspotVoucherV0.fetch( + mobileHotspotVoucher + ); + const rewardableEntityConfigAcc = + await program.account.rewardableEntityConfigV0.fetch( + voucher.rewardableEntityConfig + ); + const deviceType = voucher.deviceType; + const fees = + rewardableEntityConfigAcc.settings.mobileConfigV2?.feesByDevice.find( + (d) => Object.keys(d.deviceType)[0] === Object.keys(deviceType)[0] + )!; + const dcFeeUsd = toNumber(fees.dcOnboardingFee, 5); + + const quoteResponse = await ( + await fetch( + `${JUPITER_URL}/quote?inputMint=${USDC_MINT.toBase58()}&outputMint=${HNT_MINT.toBase58()}&amount=${toBN( + dcFeeUsd, + 6 + ).toString()}&slippageBps=50&onlyDirectRoutes=true` + ) + ).json(); + if (quoteResponse.error) { + throw new Error("Failed to get quote: " + quoteResponse.error); + } + const destination = getAssociatedTokenAddressSync( + HNT_MINT, + voucher.maker, + true + ); + const instructions = await ( + await fetch(`${JUPITER_URL}/swap-instructions`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + // quoteResponse from /quote api + quoteResponse, + userPublicKey: payer, + destinationTokenAccount: destination, + }), + }) + ).json(); + if (instructions.error) { + throw new Error("Failed to get swap instructions: " + instructions.error); + } + + const conversionEscrow = conversionEscrowKey(USDC_MINT, voucher.maker)[0]; + + const { + computeBudgetInstructions, // The necessary instructions to setup the compute budget. + swapInstruction: swapInstructionPayload, // The actual swap instruction. + addressLookupTableAddresses, // The lookup table addresses that you can use if you are using versioned transaction. + } = instructions; + + return { + instructions: [ + await program.methods + .makerLendV0({ + amount: toBN(dcFeeUsd, 6), + }) + .accounts({ + maker: voucher.maker, + targetOracle: HNT_PYTH_PRICE_FEED, + conversionEscrow, + oracle: USDC_PYTH_PRICE_FEED, + escrow: getAssociatedTokenAddressSync( + USDC_MINT, + conversionEscrow, + true + ), + destination: getAssociatedTokenAddressSync(USDC_MINT, payer), + repayAccount: destination, + usdcMint: USDC_MINT, + }) + .instruction(), + ...computeBudgetInstructions.map(deserializeInstruction), + deserializeInstruction(swapInstructionPayload), + await ceProgram.methods + .checkRepayV0() + .accounts({ + conversionEscrow, + repayAccount: destination, + }) + .instruction(), + await program.methods + .mobileVoucherPayDcV0() + .accounts({ + verifiedOwner, + mobileHotspotVoucher, + maker, + }) + .instruction(), + ], + addressLookupTableAddresses: addressLookupTableAddresses.map( + (a: any) => new PublicKey(a) + ), + }; +} diff --git a/packages/helium-entity-manager-sdk/src/functions/payMobileVoucherMobile.ts b/packages/helium-entity-manager-sdk/src/functions/payMobileVoucherMobile.ts new file mode 100644 index 000000000..4f54d47b0 --- /dev/null +++ b/packages/helium-entity-manager-sdk/src/functions/payMobileVoucherMobile.ts @@ -0,0 +1,130 @@ +import { AnchorProvider, Program } from "@coral-xyz/anchor"; +import { conversionEscrowKey } from "@helium/conversion-escrow-sdk"; +import { ConversionEscrow } from "@helium/idls/lib/types/conversion_escrow"; +import { HeliumEntityManager } from "@helium/idls/lib/types/helium_entity_manager"; +import { MOBILE_MINT, MOBILE_PYTH_PRICE_FEED, USDC_MINT, USDC_PYTH_PRICE_FEED, toBN, toNumber } from "@helium/spl-utils"; +import { getAssociatedTokenAddressSync } from "@solana/spl-token"; +import { + PublicKey +} from "@solana/web3.js"; +import { deserializeInstruction } from "./utils"; + +export const JUPITER_URL = + process.env.JUPITER_URL || "https://quote-api.jup.ag/v6"; + +export async function payMobileVoucherMobile({ + program, + ceProgram, + mobileHotspotVoucher, + verifiedOwner, + maker, + payer = (program.provider as AnchorProvider).wallet.publicKey, +}: { + program: Program; + ceProgram: Program; + mobileHotspotVoucher: PublicKey; + verifiedOwner?: PublicKey; + payer?: PublicKey; + maker?: PublicKey; +}) { + const voucher = await program.account.mobileHotspotVoucherV0.fetch( + mobileHotspotVoucher + ); + const rewardableEntityConfigAcc = + await program.account.rewardableEntityConfigV0.fetch( + voucher.rewardableEntityConfig + ); + const deviceType = voucher.deviceType; + const fees = + rewardableEntityConfigAcc.settings.mobileConfigV2?.feesByDevice.find( + (d) => Object.keys(d.deviceType)[0] === Object.keys(deviceType)[0] + )!; + const mobileFeeUsd = toNumber(fees.mobileOnboardingFeeUsd, 6); + + const quoteResponse = await ( + await fetch( + `${JUPITER_URL}/quote?inputMint=${USDC_MINT.toBase58()}&outputMint=${MOBILE_MINT.toBase58()}&amount=${toBN( + mobileFeeUsd, + 6 + ).toString()}&slippageBps=50&onlyDirectRoutes=true` + ) + ).json(); + if (quoteResponse.error) { + throw new Error("Failed to get quote: " + quoteResponse.error); + } + const destination = getAssociatedTokenAddressSync( + MOBILE_MINT, + voucher.maker, + true + ) + const instructions = await ( + await fetch(`${JUPITER_URL}/swap-instructions`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + // quoteResponse from /quote api + quoteResponse, + userPublicKey: payer, + destinationTokenAccount: destination, + }), + }) + ).json(); + if (instructions.error) { + throw new Error("Failed to get swap instructions: " + instructions.error); + } + + const conversionEscrow = conversionEscrowKey(USDC_MINT, voucher.maker)[0] + + const { + computeBudgetInstructions, // The necessary instructions to setup the compute budget. + swapInstruction: swapInstructionPayload, // The actual swap instruction. + addressLookupTableAddresses, // The lookup table addresses that you can use if you are using versioned transaction. + } = instructions; + + return { + instructions: [ + await program.methods + .makerLendV0({ + amount: toBN(mobileFeeUsd, 6), + }) + .accounts({ + maker: voucher.maker, + targetOracle: MOBILE_PYTH_PRICE_FEED, + conversionEscrow, + oracle: USDC_PYTH_PRICE_FEED, + escrow: getAssociatedTokenAddressSync( + USDC_MINT, + conversionEscrow, + true + ), + destination: getAssociatedTokenAddressSync(USDC_MINT, payer), + repayAccount: destination, + usdcMint: USDC_MINT, + }) + .instruction(), + ...computeBudgetInstructions.map(deserializeInstruction), + deserializeInstruction(swapInstructionPayload), + await ceProgram.methods + .checkRepayV0() + .accounts({ + conversionEscrow, + repayAccount: destination, + }) + .instruction(), + await program.methods + .mobileVoucherPayMobileV0() + .accounts({ + verifiedOwner, + mobileHotspotVoucher, + dntPrice: MOBILE_PYTH_PRICE_FEED, + maker, + }) + .instruction(), + ], + addressLookupTableAddresses: addressLookupTableAddresses.map( + (a: any) => new PublicKey(a) + ), + }; +} diff --git a/packages/helium-entity-manager-sdk/src/functions/utils.ts b/packages/helium-entity-manager-sdk/src/functions/utils.ts new file mode 100644 index 000000000..37c9c0485 --- /dev/null +++ b/packages/helium-entity-manager-sdk/src/functions/utils.ts @@ -0,0 +1,13 @@ +import { PublicKey, TransactionInstruction } from "@solana/web3.js"; + +export const deserializeInstruction = (instruction: any) => { + return new TransactionInstruction({ + programId: new PublicKey(instruction.programId), + keys: instruction.accounts.map((key: any) => ({ + pubkey: new PublicKey(key.pubkey), + isSigner: key.isSigner, + isWritable: key.isWritable, + })), + data: Buffer.from(instruction.data, "base64"), + }); +}; diff --git a/packages/helium-entity-manager-sdk/src/index.ts b/packages/helium-entity-manager-sdk/src/index.ts index c80e38332..2ed2cc21b 100644 --- a/packages/helium-entity-manager-sdk/src/index.ts +++ b/packages/helium-entity-manager-sdk/src/index.ts @@ -6,6 +6,8 @@ import { daoKey } from "@helium/helium-sub-daos-sdk"; export * from "./constants"; export { onboardIotHotspot } from "./functions/onboardIotHotspot"; export { onboardMobileHotspot } from "./functions/onboardMobileHotspot"; +export { payMobileVoucherMobile } from "./functions/payMobileVoucherMobile"; +export { payMobileVoucherDc } from "./functions/payMobileVoucherDc"; export { proofArgsAndAccounts } from "@helium/spl-utils"; export { updateIotMetadata } from "./functions/updateIotMetadata"; export { updateMobileMetadata } from "./functions/updateMobileMetadata"; diff --git a/packages/helium-entity-manager-sdk/src/pdas.ts b/packages/helium-entity-manager-sdk/src/pdas.ts index 2d3fb0b72..602a390b3 100644 --- a/packages/helium-entity-manager-sdk/src/pdas.ts +++ b/packages/helium-entity-manager-sdk/src/pdas.ts @@ -145,6 +145,44 @@ export const keyToAssetKey = ( return keyToAssetKeyRaw(dao, Buffer.from(hash, "hex"), programId); }; +export const mobileHotspotVoucherKeyRaw = ( + rewardableEntityConfig: PublicKey, + hashedEntityKey: Buffer, + programId: PublicKey = PROGRAM_ID +) => { + return PublicKey.findProgramAddressSync( + [ + Buffer.from("mobile_hotspot_voucher", "utf-8"), + rewardableEntityConfig.toBuffer(), + hashedEntityKey, + ], + programId + ); +}; + +export const mobileHotspotVoucherKey = ( + rewardableEntityConfig: PublicKey, + entityKey: Buffer | string, + encoding: BufferEncoding | "b58" = "b58", + programId: PublicKey = PROGRAM_ID +) => { + if (typeof entityKey === "string") { + if (encoding == "b58" || Address.isValid(entityKey)) { + entityKey = Buffer.from(bs58.decode(entityKey)); + } else { + entityKey = Buffer.from(entityKey, encoding); + } + } + const hash = sha256(entityKey); + + return mobileHotspotVoucherKeyRaw( + rewardableEntityConfig, + Buffer.from(hash, "hex"), + programId + ); +}; + + export const iotInfoKey = ( rewardableEntityConfig: PublicKey, entityKey: Buffer | string, diff --git a/packages/helium-entity-manager-sdk/src/resolvers.ts b/packages/helium-entity-manager-sdk/src/resolvers.ts index cbdfc9b77..0add36700 100644 --- a/packages/helium-entity-manager-sdk/src/resolvers.ts +++ b/packages/helium-entity-manager-sdk/src/resolvers.ts @@ -10,7 +10,7 @@ import { } from "@solana/spl-token"; import { PublicKey } from "@solana/web3.js"; import { init } from "./init"; -import { iotInfoKey, keyToAssetKey, mobileInfoKey, programApprovalKey } from "./pdas"; +import { iotInfoKey, keyToAssetKey, mobileHotspotVoucherKey, mobileInfoKey, programApprovalKey } from "./pdas"; import { notEmittedKey } from "@helium/no-emit-sdk"; export const heliumEntityManagerResolvers = combineResolvers( @@ -35,7 +35,10 @@ export const heliumEntityManagerResolvers = combineResolvers( }), resolveIndividual(async ({ path, provider }) => { if (path[path.length - 1] == "dntPrice") { - if (provider.connection.rpcEndpoint.includes("devnet") || provider.connection.rpcEndpoint.includes("test")) { + if ( + provider.connection.rpcEndpoint.includes("devnet") || + provider.connection.rpcEndpoint.includes("test") + ) { return new PublicKey("BmUdxoioVgoRTontomX8nBjWbnLevtxeuBYaLipP8GTQ"); } return new PublicKey("JBaTytFv1CmGNkyNiLu16jFMXNZ49BGfy4bYAYZdkxg5"); @@ -91,6 +94,17 @@ export const heliumEntityManagerResolvers = combineResolvers( Buffer.from("not_emitted", "utf8") ) )[0]; + } else if ( + path[path.length - 1] === "mobileHotspotVoucher" && + accounts.rewardableEntityConfig + ) { + return ( + await mobileHotspotVoucherKey( + accounts.rewardableEntityConfig as PublicKey, + args[args.length - 1].entityKey, + args[args.length - 1].keySerialization || "b58" + ) + )[0]; } }), resolveIndividual(async ({ path, args, provider, accounts, idlIx }) => { @@ -166,6 +180,12 @@ export const heliumEntityManagerResolvers = combineResolvers( mint: "hotspot", owner: "hotspotOwner", }), + ataResolver({ + instruction: "mobileVoucherPayDcV0", + mint: "dcMint", + account: "dcBurner", + owner: "maker", + }), ataResolver({ account: "dcBurner", mint: "dcMint", @@ -183,5 +203,23 @@ export const heliumEntityManagerResolvers = combineResolvers( account: "dntBurner", owner: "payer", }), + ataResolver({ + instruction: "mobileVoucherPayMobileV0", + mint: "dntMint", + account: "dntBurner", + owner: "maker", + }), + ataResolver({ + instruction: "initializeMakerEscrowV0", + mint: "mint", + account: "escrow", + owner: "conversionEscrow", + }), + ataResolver({ + instruction: "mobileVoucherPayDcV0", + mint: "hntMint", + account: "burner", + owner: "maker", + }), subDaoEpochInfoResolver ); diff --git a/packages/helium-entity-manager-sdk/tsconfig.json b/packages/helium-entity-manager-sdk/tsconfig.json index 5f0316873..9d229cf07 100644 --- a/packages/helium-entity-manager-sdk/tsconfig.json +++ b/packages/helium-entity-manager-sdk/tsconfig.json @@ -13,6 +13,9 @@ { "path": "../helium-sub-daos-sdk" }, + { + "path": "../conversion-escrow-sdk" + }, { "path": "../no-emit-sdk" }, diff --git a/packages/spl-utils/src/constants.ts b/packages/spl-utils/src/constants.ts index 2579cc2cf..8072f98e6 100644 --- a/packages/spl-utils/src/constants.ts +++ b/packages/spl-utils/src/constants.ts @@ -10,7 +10,24 @@ export const MOBILE_MINT = new PublicKey( export const IOT_MINT = new PublicKey("iotEVVZLEywoTn1QdwNPddxPWszn3zFhEot3MfL9fns"); -// TODO: Replace with actual HNT feed export const HNT_PYTH_PRICE_FEED = new PublicKey( - "6Eg8YdfFJQF2HHonzPUBSCCmyUEhrStg9VBLK957sBe6" + "7moA1i5vQUpfDwSpK6Pw9s56ahB7WFGidtbL2ujWrVvm" +); + +export const MOBILE_PYTH_PRICE_FEED = new PublicKey( + "JBaTytFv1CmGNkyNiLu16jFMXNZ49BGfy4bYAYZdkxg5" +); + +export const USDC_PYTH_PRICE_FEED = new PublicKey( + "Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD" +); + +export const USDC_MINT = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); + +export const HELIUM_COMMON_LUT_DEVNET = new PublicKey( + "FnqYkQ6ZKnVKdkvYCGsEeiP5qgGqVbcFUkGduy2ta4gA" +); + +export const HELIUM_COMMON_LUT = new PublicKey( + "43eY9L2spbM2b1MPDFFBStUiFGt29ziZ1nc1xbpzsfVt" ); diff --git a/packages/spl-utils/src/transaction.ts b/packages/spl-utils/src/transaction.ts index f0e4f155d..54e017bd4 100644 --- a/packages/spl-utils/src/transaction.ts +++ b/packages/spl-utils/src/transaction.ts @@ -1,5 +1,6 @@ import { AnchorProvider, Program, Provider } from "@coral-xyz/anchor"; import { + AddressLookupTableAccount, Commitment, ComputeBudgetProgram, Connection, @@ -13,7 +14,9 @@ import { SimulatedTransactionResponse, Transaction, TransactionInstruction, + TransactionMessage, TransactionSignature, + VersionedTransaction, VersionedTransactionResponse, } from "@solana/web3.js"; import bs58 from "bs58"; @@ -40,6 +43,56 @@ async function promiseAllInOrder( return ret; } + +export const getAddressLookupTableAccounts = async ( + connection: Connection, + keys: PublicKey[] +): Promise => { + const addressLookupTableAccountInfos = + await connection.getMultipleAccountsInfo( + keys.map((key) => new PublicKey(key)) + ); + + return addressLookupTableAccountInfos.reduce((acc, accountInfo, index) => { + const addressLookupTableAddress = keys[index]; + if (accountInfo) { + const addressLookupTableAccount = new AddressLookupTableAccount({ + key: addressLookupTableAddress, + state: AddressLookupTableAccount.deserialize(accountInfo.data), + }); + acc.push(addressLookupTableAccount); + } + + return acc; + }, new Array()); +}; + +export async function toVersionedTx({ + instructions, + addressLookupTableAddresses, + payer, + connection, +}: { + addressLookupTableAddresses: PublicKey[]; + instructions: TransactionInstruction[]; + payer: PublicKey; + connection: Connection; +}): Promise { + const addressLookupTableAccounts: AddressLookupTableAccount[] = []; + + addressLookupTableAccounts.push( + ...(await getAddressLookupTableAccounts(connection, addressLookupTableAddresses)) + ); + + const blockhash = (await connection.getLatestBlockhash()).blockhash; + const messageV0 = new TransactionMessage({ + payerKey: payer, + recentBlockhash: blockhash, + instructions + }).compileToV0Message(addressLookupTableAccounts); + return new VersionedTransaction(messageV0); +}; + export interface InstructionResult { instructions: TransactionInstruction[]; signers: Signer[]; diff --git a/programs/conversion-escrow/src/errors.rs b/programs/conversion-escrow/src/errors.rs index b28d7ec75..144887951 100644 --- a/programs/conversion-escrow/src/errors.rs +++ b/programs/conversion-escrow/src/errors.rs @@ -10,8 +10,9 @@ pub enum ErrorCode { #[msg("Incorrect repayment destination")] IncorrectDestination, MissingRepay, + InsufficientRepayAmount, PythPriceNotFound, ArithmeticError, - IncorrectDc, - HntAmountRequired, + IncorrectRepaymentMint, + IncorrectRepaymentOwner, } diff --git a/programs/conversion-escrow/src/instructions/check_repay_v0.rs b/programs/conversion-escrow/src/instructions/check_repay_v0.rs new file mode 100644 index 000000000..b8e2da782 --- /dev/null +++ b/programs/conversion-escrow/src/instructions/check_repay_v0.rs @@ -0,0 +1,20 @@ +use crate::{errors::ErrorCode, ConversionEscrowV0}; +use anchor_lang::prelude::*; +use anchor_spl::token::TokenAccount; + +#[derive(Accounts)] +pub struct CheckRepayV0<'info> { + pub conversion_escrow: Account<'info, ConversionEscrowV0>, + /// CHECK: This is checked by lend_v0 as the second account + pub repay_account: Box>, +} + +pub fn handler(ctx: Context) -> Result<()> { + require_gte!( + ctx.accounts.repay_account.amount, + ctx.accounts.conversion_escrow.temp_expected_repay + + ctx.accounts.conversion_escrow.temp_repay_balance, + ErrorCode::InsufficientRepayAmount + ); + Ok(()) +} diff --git a/programs/conversion-escrow/src/instructions/initialize_escrow_v0.rs b/programs/conversion-escrow/src/instructions/initialize_escrow_v0.rs index 95cf21916..926ab435c 100644 --- a/programs/conversion-escrow/src/instructions/initialize_escrow_v0.rs +++ b/programs/conversion-escrow/src/instructions/initialize_escrow_v0.rs @@ -63,15 +63,17 @@ pub fn handler(ctx: Context, args: InitializeEscrowArgsV0) - .targets .iter() .map(|t| ConversionTargetV0 { - reserverd: [0; 8], + reserved: [0; 8], mint: t.mint, oracle: t.oracle, - slipage_bps: t.slippage_bps, + slippage_bps: t.slippage_bps, }) .collect(), owner: ctx.accounts.owner.key(), update_authority: ctx.accounts.update_authority.key(), bump_seed: ctx.bumps["conversion_escrow"], + temp_repay_balance: 0, + temp_expected_repay: 0, }); Ok(()) diff --git a/programs/conversion-escrow/src/instructions/lend_v0.rs b/programs/conversion-escrow/src/instructions/lend_v0.rs index b838d5036..52a2649f6 100644 --- a/programs/conversion-escrow/src/instructions/lend_v0.rs +++ b/programs/conversion-escrow/src/instructions/lend_v0.rs @@ -41,8 +41,8 @@ pub struct LendV0<'info> { )] pub destination: Box>, #[account( - constraint = conversion_escrow.targets.iter().find(|t| t.oracle == target_oracle.key()).unwrap().mint == repay_account.mint, - constraint = repay_account.owner == conversion_escrow.owner + constraint = conversion_escrow.targets.iter().find(|t| t.oracle == target_oracle.key()).unwrap().mint == repay_account.mint @ ErrorCode::IncorrectRepaymentMint, + constraint = repay_account.owner == conversion_escrow.owner @ ErrorCode::IncorrectRepaymentOwner )] pub repay_account: Box>, /// CHECK: check instructions account @@ -126,30 +126,30 @@ pub fn handler(ctx: Context, args: LendArgsV0) -> Result<()> { .unwrap(); let expected_repayment_amount_with_slippage = expected_repayment_amount - (expected_repayment_amount - .checked_mul(u64::from(target.slipage_bps)) + .checked_mul(u64::from(target.slippage_bps)) .unwrap() .checked_div(10000) .unwrap()); - // make sure this isnt a cpi call + // Accounting for multiple flash loans in the same tx, just need to up the expected repay. Only set initial + // balance once. + if ctx.accounts.conversion_escrow.temp_repay_balance == 0 { + ctx.accounts.conversion_escrow.temp_repay_balance = ctx.accounts.repay_account.amount; + } + ctx.accounts.conversion_escrow.temp_expected_repay += expected_repayment_amount_with_slippage; + let current_index = load_current_index_checked(&ixs)? as usize; // loop through instructions, looking for an equivalent mint dc to this borrow let mut index = current_index + 1; // jupiter swap loop { // get the next instruction, die if theres no more if let Ok(ix) = load_instruction_at_checked(index, &ixs) { - if ix.program_id == token::ID { - let transfer_data = match TokenInstruction::unpack(&ix.data)? { - TokenInstruction::Transfer { amount } => Some((amount, ix.accounts[1].pubkey)), - TokenInstruction::TransferChecked { amount, .. } => Some((amount, ix.accounts[2].pubkey)), - _ => None, - }; - - if let Some((amount, account)) = transfer_data { - if ctx.accounts.repay_account.key() == account { - require_gt!(amount, expected_repayment_amount_with_slippage); - break; - } + if ix.program_id == crate::id() { + if ix.data[0..8] == get_function_hash("global", "check_repay_v0") + && ix.accounts[0].pubkey == ctx.accounts.conversion_escrow.key() + && ix.accounts[1].pubkey == ctx.accounts.repay_account.key() + { + break; } } } else { @@ -176,3 +176,11 @@ pub fn handler(ctx: Context, args: LendArgsV0) -> Result<()> { Ok(()) } + +pub fn get_function_hash(namespace: &str, name: &str) -> [u8; 8] { + let preimage = format!("{}:{}", namespace, name); + let mut sighash = [0u8; 8]; + sighash + .copy_from_slice(&anchor_lang::solana_program::hash::hash(preimage.as_bytes()).to_bytes()[..8]); + sighash +} diff --git a/programs/conversion-escrow/src/instructions/mod.rs b/programs/conversion-escrow/src/instructions/mod.rs index 57006d07f..81fe51a04 100644 --- a/programs/conversion-escrow/src/instructions/mod.rs +++ b/programs/conversion-escrow/src/instructions/mod.rs @@ -1,5 +1,7 @@ +pub mod check_repay_v0; pub mod initialize_escrow_v0; pub mod lend_v0; +pub use check_repay_v0::*; pub use initialize_escrow_v0::*; pub use lend_v0::*; diff --git a/programs/conversion-escrow/src/lib.rs b/programs/conversion-escrow/src/lib.rs index f9583db81..f27e2c5ee 100644 --- a/programs/conversion-escrow/src/lib.rs +++ b/programs/conversion-escrow/src/lib.rs @@ -2,7 +2,7 @@ use anchor_lang::prelude::*; #[cfg(not(feature = "no-entrypoint"))] use {default_env::default_env, solana_security_txt::security_txt}; -declare_id!("dce4jeLBpfaFsNAKMAmVt5Py4E1R4mZcrVvMB5ejvGu"); +declare_id!("cnvEguKeWyyWnKxoQ9HwrzEVfztqKjwNmerDvxdHK9w"); pub mod errors; pub mod instructions; @@ -42,4 +42,8 @@ pub mod conversion_escrow { pub fn lend_v0(ctx: Context, args: LendArgsV0) -> Result<()> { instructions::lend_v0::handler(ctx, args) } + + pub fn check_repay_v0(ctx: Context) -> Result<()> { + instructions::check_repay_v0::handler(ctx) + } } diff --git a/programs/conversion-escrow/src/state.rs b/programs/conversion-escrow/src/state.rs index 3885296d5..88509a4b7 100644 --- a/programs/conversion-escrow/src/state.rs +++ b/programs/conversion-escrow/src/state.rs @@ -9,6 +9,10 @@ pub struct ConversionEscrowV0 { pub owner: Pubkey, pub update_authority: Pubkey, pub targets: Vec, + /// Temporarily records the balance of the repay account to check if it has been repaid + pub temp_repay_balance: u64, + /// Temporarily records the amount we expect to be repaid + pub temp_expected_repay: u64, pub bump_seed: u8, } @@ -17,8 +21,8 @@ pub struct ConversionTargetV0 { pub mint: Pubkey, pub oracle: Pubkey, /// How much slippage to allow from the oracle price - pub slipage_bps: u16, - pub reserverd: [u64; 8], + pub slippage_bps: u16, + pub reserved: [u64; 8], } #[macro_export] diff --git a/programs/helium-entity-manager/Cargo.toml b/programs/helium-entity-manager/Cargo.toml index e755a8aff..cc158c4ab 100644 --- a/programs/helium-entity-manager/Cargo.toml +++ b/programs/helium-entity-manager/Cargo.toml @@ -36,3 +36,5 @@ solana-security-txt = { workspace = true } default-env = { workspace = true } no-emit = { workspace = true } pyth-sdk-solana = { version = "0.8.0" } +conversion-escrow = { path = "../conversion-escrow", features = ["cpi"] } +circuit-breaker = { workspace = true } diff --git a/programs/helium-entity-manager/src/error.rs b/programs/helium-entity-manager/src/error.rs index cd4de7636..121e75aff 100644 --- a/programs/helium-entity-manager/src/error.rs +++ b/programs/helium-entity-manager/src/error.rs @@ -56,4 +56,10 @@ pub enum ErrorCode { #[msg("Arithmetic error")] ArithmeticError, + + #[msg("Cannot accept a maker loan without an onboard")] + MissingOnboard, + + #[msg("Too much was borrowed from the maker for this txn")] + TooMuchBorrowed, } diff --git a/programs/helium-entity-manager/src/instructions/initialize_maker_escrow_v0.rs b/programs/helium-entity-manager/src/instructions/initialize_maker_escrow_v0.rs new file mode 100644 index 000000000..e465d81f1 --- /dev/null +++ b/programs/helium-entity-manager/src/instructions/initialize_maker_escrow_v0.rs @@ -0,0 +1,91 @@ +use anchor_lang::prelude::*; +use anchor_spl::{ + associated_token::AssociatedToken, + token::{Mint, Token, TokenAccount}, +}; +use conversion_escrow::{ + cpi::{accounts::InitializeEscrowV0, initialize_escrow_v0}, + program::ConversionEscrow, + ConversionTargetArgV0, InitializeEscrowArgsV0, +}; + +use crate::{maker_seeds, MakerV0}; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct InitializeMakerEscrowArgsV0 { + pub oracle: Pubkey, + pub targets: Vec, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct MConversionTargetArgV0 { + pub mint: Pubkey, + pub oracle: Pubkey, + /// How much slippage to allow from the oracle price + pub slippage_bps: u16, +} + +#[derive(Accounts)] +#[instruction(args: InitializeMakerEscrowArgsV0)] +pub struct InitializeMakerEscrowV0<'info> { + #[account( + has_one = update_authority, + )] + pub maker: Account<'info, MakerV0>, + pub update_authority: Signer<'info>, + #[account(mut)] + pub payer: Signer<'info>, + /// CHECK: Checked in CPI + #[account( + mut, + seeds = [b"conversion_escrow", mint.key().as_ref(), maker.key().as_ref()], + bump, + seeds::program = conversion_escrow_program.key(), + )] + pub conversion_escrow: UncheckedAccount<'info>, + pub mint: Box>, + /// CHECK: Checked in CPI + #[account(mut)] + pub escrow: UncheckedAccount<'info>, + pub system_program: Program<'info, System>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub token_program: Program<'info, Token>, + pub conversion_escrow_program: Program<'info, ConversionEscrow>, +} + +pub fn handler( + ctx: Context, + args: InitializeMakerEscrowArgsV0, +) -> Result<()> { + initialize_escrow_v0( + CpiContext::new_with_signer( + ctx.accounts.conversion_escrow_program.to_account_info(), + InitializeEscrowV0 { + payer: ctx.accounts.payer.to_account_info(), + owner: ctx.accounts.maker.to_account_info(), + update_authority: ctx.accounts.update_authority.to_account_info(), + conversion_escrow: ctx.accounts.conversion_escrow.to_account_info(), + mint: ctx.accounts.mint.to_account_info(), + escrow: ctx.accounts.escrow.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + associated_token_program: ctx.accounts.associated_token_program.to_account_info(), + token_program: ctx.accounts.token_program.to_account_info(), + }, + &[maker_seeds!(ctx.accounts.maker)], + ), + InitializeEscrowArgsV0 { + oracle: args.oracle, + targets: args + .targets + .iter() + .map(|t| ConversionTargetArgV0 { + mint: t.mint, + oracle: t.oracle, + slippage_bps: t.slippage_bps, + }) + .collect(), + }, + )?; + + Ok(()) +} diff --git a/programs/helium-entity-manager/src/instructions/initialize_maker_v0.rs b/programs/helium-entity-manager/src/instructions/initialize_maker_v0.rs index 49261272d..037eadaf1 100644 --- a/programs/helium-entity-manager/src/instructions/initialize_maker_v0.rs +++ b/programs/helium-entity-manager/src/instructions/initialize_maker_v0.rs @@ -166,6 +166,7 @@ pub fn handler(ctx: Context, args: InitializeMakerArgsV0) -> bump_seed: ctx.bumps["maker"], collection_bump_seed: ctx.bumps["collection"], dao: ctx.accounts.dao.key(), + expected_onboard_amount: 0, }); Ok(()) diff --git a/programs/helium-entity-manager/src/instructions/maker_lend_v0.rs b/programs/helium-entity-manager/src/instructions/maker_lend_v0.rs new file mode 100644 index 000000000..3679247ac --- /dev/null +++ b/programs/helium-entity-manager/src/instructions/maker_lend_v0.rs @@ -0,0 +1,132 @@ +/// Creates a loan from the maker to whoever is onboarding, +/// but only if they are doing a valid onboarding command later on. +use std::str::FromStr; + +use crate::{error::ErrorCode, maker_seeds}; +use crate::{state::*, TESTING}; +use anchor_lang::{ + prelude::*, + solana_program::sysvar::{ + self, + instructions::{load_current_index_checked, load_instruction_at_checked}, + }, +}; +use anchor_spl::token::{Mint, Token}; +use conversion_escrow::{ + cpi::{accounts::LendV0, lend_v0}, + program::ConversionEscrow, + ConversionEscrowV0, LendArgsV0, +}; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct MakerLendArgsV0 { + pub amount: u64, +} + +#[derive(Accounts)] +pub struct MakerLendV0<'info> { + #[account(mut)] + pub maker: Box>, + #[account( + constraint = TESTING || usdc_mint.key() == Pubkey::from_str("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v").unwrap() + )] + pub usdc_mint: Box>, + /// CHECK: Checked in cpi + pub target_oracle: UncheckedAccount<'info>, + /// CHECK: Checked in cpi + #[account(mut)] + pub escrow: AccountInfo<'info>, + /// CHECK: Checked in cpi + pub oracle: AccountInfo<'info>, + /// CHECK: Doesn't matter where it goes, so long as it is repaid + #[account(mut)] + pub destination: AccountInfo<'info>, + /// CHECK: Checked in CPI + #[account(mut)] + pub repay_account: AccountInfo<'info>, + #[account( + seeds = [b"conversion_escrow", usdc_mint.key().as_ref(), maker.key().as_ref()], + seeds::program = conversion_escrow_program, + bump = conversion_escrow.bump_seed, + has_one = escrow, + has_one = oracle, + )] + pub conversion_escrow: Box>, + pub conversion_escrow_program: Program<'info, ConversionEscrow>, + /// CHECK: check instructions account + #[account(address = sysvar::instructions::ID)] + pub instructions: UncheckedAccount<'info>, + pub token_program: Program<'info, Token>, +} + +pub fn handler(ctx: Context, args: MakerLendArgsV0) -> Result<()> { + let seeds = maker_seeds!(ctx.accounts.maker); + + let target = ctx + .accounts + .conversion_escrow + .targets + .iter() + .find(|target| target.oracle == ctx.accounts.target_oracle.key()) + .unwrap(); + let amount_with_slippage = args.amount + args.amount * u64::from(target.slippage_bps) / 10000; + lend_v0( + CpiContext::new_with_signer( + ctx.accounts.conversion_escrow_program.to_account_info(), + LendV0 { + conversion_escrow: ctx.accounts.conversion_escrow.to_account_info(), + escrow: ctx.accounts.escrow.to_account_info(), + mint: ctx.accounts.usdc_mint.to_account_info(), + oracle: ctx.accounts.oracle.to_account_info(), + target_oracle: ctx.accounts.target_oracle.to_account_info(), + destination: ctx.accounts.destination.to_account_info(), + repay_account: ctx.accounts.repay_account.to_account_info(), + instructions: ctx.accounts.instructions.to_account_info(), + token_program: ctx.accounts.token_program.to_account_info(), + }, + &[seeds], + ), + LendArgsV0 { + amount: amount_with_slippage, + }, + )?; + + let ixs = ctx.accounts.instructions.to_account_info(); + let current_index = load_current_index_checked(&ixs)? as usize; + // loop through instructions, looking for an onboard instruction + let mut index = current_index + 1; + let valid_instructions: Vec<[u8; 8]> = vec![ + get_function_hash("global", "mobile_voucher_pay_mobile_v0"), + get_function_hash("global", "mobile_voucher_pay_dc_v0"), + ]; + loop { + // get the next instruction, die if theres no more + if let Ok(ix) = load_instruction_at_checked(index, &ixs) { + if ix.program_id == crate::id() { + if valid_instructions.iter().any(|&i| i == ix.data[0..8]) { + // Maker is always the first account in the instruction + if ix.accounts[0].pubkey == ctx.accounts.maker.key() { + break; + } + } + } + } else { + // no more instructions, so we're missing an onboard + return Err(ErrorCode::MissingOnboard.into()); + } + + index += 1 + } + + ctx.accounts.maker.expected_onboard_amount = args.amount; + + Ok(()) +} + +pub fn get_function_hash(namespace: &str, name: &str) -> [u8; 8] { + let preimage = format!("{}:{}", namespace, name); + let mut sighash = [0u8; 8]; + sighash + .copy_from_slice(&anchor_lang::solana_program::hash::hash(preimage.as_bytes()).to_bytes()[..8]); + sighash +} diff --git a/programs/helium-entity-manager/src/instructions/mobile_voucher/initialize_mobile_hotspot_voucher_v0.rs b/programs/helium-entity-manager/src/instructions/mobile_voucher/initialize_mobile_hotspot_voucher_v0.rs new file mode 100644 index 000000000..dfa79ba4e --- /dev/null +++ b/programs/helium-entity-manager/src/instructions/mobile_voucher/initialize_mobile_hotspot_voucher_v0.rs @@ -0,0 +1,68 @@ +use crate::state::*; +use anchor_lang::prelude::*; +use anchor_lang::solana_program::hash::hash; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct InitializeMobileHotspotVoucherArgsV0 { + pub entity_key: Vec, + pub key_serialization: KeySerialization, + pub device_type: MobileDeviceTypeV0, +} + +#[derive(Accounts)] +#[instruction(args: InitializeMobileHotspotVoucherArgsV0)] +pub struct InitializeMobileHotspotVoucherV0<'info> { + #[account(mut)] + pub payer: Signer<'info>, + pub issuing_authority: Signer<'info>, + #[account( + constraint = rewardable_entity_config.settings.is_mobile(), + )] + pub rewardable_entity_config: Box>, + #[account( + seeds = ["maker_approval".as_bytes(), rewardable_entity_config.key().as_ref(), maker.key().as_ref()], + bump = maker_approval.bump_seed, + has_one = maker, + has_one = rewardable_entity_config, + )] + pub maker_approval: Box>, + #[account( + mut, + has_one = issuing_authority, + )] + pub maker: Box>, + #[account( + init, + payer = payer, + space = 8 + 60 + std::mem::size_of::() + args.entity_key.len(), + seeds = [ + "mobile_hotspot_voucher".as_bytes(), + rewardable_entity_config.key().as_ref(), + &hash(&args.entity_key[..]).to_bytes() + ], + bump, + )] + pub mobile_hotspot_voucher: Box>, + pub system_program: Program<'info, System>, +} + +pub fn handler( + ctx: Context, + args: InitializeMobileHotspotVoucherArgsV0, +) -> Result<()> { + ctx + .accounts + .mobile_hotspot_voucher + .set_inner(MobileHotspotVoucherV0 { + rewardable_entity_config: ctx.accounts.rewardable_entity_config.key(), + entity_key: args.entity_key, + bump_seed: ctx.bumps["mobile_hotspot_voucher"], + key_serialization: args.key_serialization, + device_type: args.device_type, + paid_mobile: false, + paid_dc: false, + maker: ctx.accounts.maker.key(), + verified_owner: ctx.accounts.issuing_authority.key(), + }); + Ok(()) +} diff --git a/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_pay_dc_v0.rs b/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_pay_dc_v0.rs new file mode 100644 index 000000000..911c01419 --- /dev/null +++ b/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_pay_dc_v0.rs @@ -0,0 +1,158 @@ +use crate::{error::ErrorCode, maker_seeds, state::*}; +use anchor_lang::prelude::*; +use anchor_spl::{ + associated_token::AssociatedToken, + token::{Mint, Token, TokenAccount}, +}; +use circuit_breaker::{CircuitBreaker, MintWindowedCircuitBreakerV0}; +use data_credits::{ + cpi::{ + accounts::{BurnCommonV0, BurnWithoutTrackingV0, MintDataCreditsV0}, + burn_without_tracking_v0, mint_data_credits_v0, + }, + program::DataCredits, + BurnWithoutTrackingArgsV0, DataCreditsV0, MintDataCreditsArgsV0, +}; +use helium_sub_daos::{DaoV0, SubDaoV0}; + +#[derive(Accounts)] +pub struct MobileVoucherPayDcV0<'info> { + #[account(mut)] + pub maker: Box>, + #[account(mut)] + pub payer: Signer<'info>, + #[account( + has_one = sub_dao, + constraint = rewardable_entity_config.settings.is_mobile(), + )] + pub rewardable_entity_config: Box>, + #[account( + mut, + has_one = rewardable_entity_config, + has_one = maker, + has_one = verified_owner, + )] + pub mobile_hotspot_voucher: Box>, + pub verified_owner: Signer<'info>, + #[account( + has_one = dao, + )] + pub sub_dao: Box>, + /// CHECK: Checked by loading with pyth. Also double checked by the has_one on data credits instance. + pub hnt_price_oracle: AccountInfo<'info>, + #[account( + has_one = dc_mint, + has_one = hnt_mint + )] + pub dao: Box>, + #[account(mut)] + pub dc_mint: Box>, + #[account(mut)] + pub hnt_mint: Box>, + #[account( + seeds=[ + "dc".as_bytes(), + dc_mint.key().as_ref() + ], + seeds::program = data_credits_program.key(), + bump = dc.data_credits_bump, + has_one = dc_mint, + has_one = hnt_price_oracle, + has_one = hnt_mint + )] + pub dc: Account<'info, DataCreditsV0>, + #[account( + mut, + associated_token::authority = maker, + associated_token::mint = hnt_mint, + )] + pub burner: Account<'info, TokenAccount>, + #[account( + init_if_needed, + payer = payer, + associated_token::mint = dc_mint, + associated_token::authority = maker, + )] + pub dc_burner: Box>, + #[account( + mut, + seeds = ["mint_windowed_breaker".as_bytes(), dc_mint.key().as_ref()], + seeds::program = circuit_breaker_program.key(), + bump = circuit_breaker.bump_seed + )] + pub circuit_breaker: Box>, + pub circuit_breaker_program: Program<'info, CircuitBreaker>, + pub token_program: Program<'info, Token>, + pub data_credits_program: Program<'info, DataCredits>, + pub system_program: Program<'info, System>, + pub associated_token_program: Program<'info, AssociatedToken>, +} + +pub fn handler(ctx: Context) -> Result<()> { + let fees = ctx + .accounts + .rewardable_entity_config + .settings + .mobile_device_fees(ctx.accounts.mobile_hotspot_voucher.device_type) + .ok_or(error!(ErrorCode::InvalidDeviceType))?; + + let dc_fee = fees.dc_onboarding_fee; + require_gte!( + // Extra decimal + dc_fee * 10, + ctx.accounts.maker.expected_onboard_amount, + ErrorCode::TooMuchBorrowed + ); + + if dc_fee > 0 { + mint_data_credits_v0( + CpiContext::new_with_signer( + ctx.accounts.data_credits_program.to_account_info(), + MintDataCreditsV0 { + data_credits: ctx.accounts.dc.to_account_info(), + hnt_price_oracle: ctx.accounts.hnt_price_oracle.to_account_info(), + burner: ctx.accounts.burner.to_account_info(), + recipient_token_account: ctx.accounts.dc_burner.to_account_info(), + recipient: ctx.accounts.maker.to_account_info(), + owner: ctx.accounts.maker.to_account_info(), + hnt_mint: ctx.accounts.hnt_mint.to_account_info(), + dc_mint: ctx.accounts.dc_mint.to_account_info(), + circuit_breaker: ctx.accounts.circuit_breaker.to_account_info(), + circuit_breaker_program: ctx.accounts.circuit_breaker_program.to_account_info(), + token_program: ctx.accounts.token_program.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + associated_token_program: ctx.accounts.associated_token_program.to_account_info(), + }, + &[maker_seeds!(ctx.accounts.maker)], + ), + MintDataCreditsArgsV0 { + dc_amount: Some(dc_fee), + hnt_amount: None, + }, + )?; + let cpi_accounts = BurnWithoutTrackingV0 { + burn_accounts: BurnCommonV0 { + data_credits: ctx.accounts.dc.to_account_info(), + burner: ctx.accounts.dc_burner.to_account_info(), + owner: ctx.accounts.maker.to_account_info(), + dc_mint: ctx.accounts.dc_mint.to_account_info(), + token_program: ctx.accounts.token_program.to_account_info(), + associated_token_program: ctx.accounts.associated_token_program.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + }, + }; + + burn_without_tracking_v0( + CpiContext::new_with_signer( + ctx.accounts.data_credits_program.to_account_info(), + cpi_accounts, + &[maker_seeds!(ctx.accounts.maker)], + ), + BurnWithoutTrackingArgsV0 { amount: dc_fee }, + )?; + } + + ctx.accounts.mobile_hotspot_voucher.paid_dc = true; + + Ok(()) +} diff --git a/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_pay_mobile_v0.rs b/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_pay_mobile_v0.rs new file mode 100644 index 000000000..7a5bf4279 --- /dev/null +++ b/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_pay_mobile_v0.rs @@ -0,0 +1,116 @@ +use std::str::FromStr; + +use crate::{error::ErrorCode, maker_seeds, state::*, TESTING}; +use anchor_lang::prelude::*; +use anchor_spl::{ + associated_token::AssociatedToken, + token::{burn, Burn, Mint, Token, TokenAccount}, +}; +use helium_sub_daos::SubDaoV0; +use pyth_sdk_solana::load_price_feed_from_account_info; + +#[cfg(feature = "devnet")] +const PRICE_ORACLE: &str = "BmUdxoioVgoRTontomX8nBjWbnLevtxeuBYaLipP8GTQ"; +#[cfg(not(feature = "devnet"))] +const PRICE_ORACLE: &str = "JBaTytFv1CmGNkyNiLu16jFMXNZ49BGfy4bYAYZdkxg5"; + +#[derive(Accounts)] +pub struct MobileVoucherPayMobileV0<'info> { + #[account(mut)] + pub maker: Box>, + #[account( + has_one = sub_dao, + constraint = rewardable_entity_config.settings.is_mobile(), + )] + pub rewardable_entity_config: Box>, + #[account( + mut, + has_one = rewardable_entity_config, + has_one = maker, + has_one = verified_owner, + )] + pub mobile_hotspot_voucher: Box>, + pub verified_owner: Signer<'info>, + #[account(mut)] + pub dnt_mint: Box>, + /// CHECK: Checked by loading with pyth. Also double checked by the has_one on data credits instance. + #[account( + address = Pubkey::from_str(PRICE_ORACLE).unwrap() + )] + pub dnt_price: AccountInfo<'info>, + #[account( + has_one = dnt_mint, + )] + pub sub_dao: Box>, + #[account( + mut, + associated_token::authority = maker, + associated_token::mint = dnt_mint + )] + pub dnt_burner: Account<'info, TokenAccount>, + pub token_program: Program<'info, Token>, + pub associated_token_program: Program<'info, AssociatedToken>, +} + +pub fn handler(ctx: Context) -> Result<()> { + let fees = ctx + .accounts + .rewardable_entity_config + .settings + .mobile_device_fees(ctx.accounts.mobile_hotspot_voucher.device_type) + .ok_or(error!(ErrorCode::InvalidDeviceType))?; + let dnt_fee = fees.mobile_onboarding_fee_usd; + require_gte!( + dnt_fee, + ctx.accounts.maker.expected_onboard_amount, + ErrorCode::TooMuchBorrowed + ); + + let mobile_price_oracle = + load_price_feed_from_account_info(&ctx.accounts.dnt_price).map_err(|e| { + msg!("Pyth error {}", e); + error!(ErrorCode::PythError) + })?; + let current_time = Clock::get()?.unix_timestamp; + let mobile_price = mobile_price_oracle + .get_ema_price_no_older_than(current_time, if TESTING { 6000000 } else { 10 * 60 }) + .ok_or_else(|| error!(ErrorCode::PythPriceNotFound))?; + // Remove the confidence from the price to use the most conservative price + // https://docs.pyth.network/price-feeds/solana-price-feeds/best-practices#confidence-intervals + let mobile_price_with_conf = mobile_price + .price + .checked_sub(i64::try_from(mobile_price.conf.checked_mul(2).unwrap()).unwrap()) + .unwrap(); + // Exponent is a negative number, likely -8 + // Since the price is multiplied by an extra 10^8, and we're dividing by that price, need to also multiply + // by the exponent + let exponent_dec = 10_u64 + .checked_pow(u32::try_from(-mobile_price.expo).unwrap()) + .ok_or_else(|| error!(ErrorCode::ArithmeticError))?; + + require_gt!(mobile_price_with_conf, 0); + let mobile_fee = dnt_fee + .checked_mul(exponent_dec) + .unwrap() + .checked_div(mobile_price_with_conf.try_into().unwrap()) + .unwrap(); + if mobile_fee > 0 { + let signer_seeds = maker_seeds!(ctx.accounts.maker); + burn( + CpiContext::new_with_signer( + ctx.accounts.token_program.to_account_info(), + Burn { + mint: ctx.accounts.dnt_mint.to_account_info(), + from: ctx.accounts.dnt_burner.to_account_info(), + authority: ctx.accounts.maker.to_account_info(), + }, + &[signer_seeds], + ), + mobile_fee, + )?; + } + + ctx.accounts.mobile_hotspot_voucher.paid_mobile = true; + + Ok(()) +} diff --git a/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_verify_owner_v0.rs b/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_verify_owner_v0.rs new file mode 100644 index 000000000..4cd658e71 --- /dev/null +++ b/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_verify_owner_v0.rs @@ -0,0 +1,19 @@ +use std::str::FromStr; + +use crate::{state::*, ECC_VERIFIER}; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct MobileVoucherVerifyOwnerV0<'info> { + #[account(address = Pubkey::from_str(ECC_VERIFIER).unwrap())] + pub ecc_verifier: Signer<'info>, + /// CHECK: Ecc verifier is what's telling us this is the owner + pub verified_owner: UncheckedAccount<'info>, + #[account(mut)] + pub mobile_hotspot_voucher: Box>, +} + +pub fn handler(ctx: Context) -> Result<()> { + ctx.accounts.mobile_hotspot_voucher.verified_owner = ctx.accounts.verified_owner.key(); + Ok(()) +} diff --git a/programs/helium-entity-manager/src/instructions/mobile_voucher/mod.rs b/programs/helium-entity-manager/src/instructions/mobile_voucher/mod.rs new file mode 100644 index 000000000..e30fac8ff --- /dev/null +++ b/programs/helium-entity-manager/src/instructions/mobile_voucher/mod.rs @@ -0,0 +1,9 @@ +pub mod initialize_mobile_hotspot_voucher_v0; +pub mod mobile_voucher_pay_dc_v0; +pub mod mobile_voucher_pay_mobile_v0; +pub mod mobile_voucher_verify_owner_v0; + +pub use initialize_mobile_hotspot_voucher_v0::*; +pub use mobile_voucher_pay_dc_v0::*; +pub use mobile_voucher_pay_mobile_v0::*; +pub use mobile_voucher_verify_owner_v0::*; diff --git a/programs/helium-entity-manager/src/instructions/mod.rs b/programs/helium-entity-manager/src/instructions/mod.rs index 4612a289c..8322d4097 100644 --- a/programs/helium-entity-manager/src/instructions/mod.rs +++ b/programs/helium-entity-manager/src/instructions/mod.rs @@ -1,6 +1,7 @@ pub mod approve_maker_v0; pub mod approve_program_v0; pub mod initialize_data_only_v0; +pub mod initialize_maker_escrow_v0; pub mod initialize_maker_v0; pub mod initialize_rewardable_entity_config_v0; pub mod issue_data_only_entity_v0; @@ -8,6 +9,8 @@ pub mod issue_entity_v0; pub mod issue_iot_operations_fund_v0; pub mod issue_not_emitted_entity_v0; pub mod issue_program_entity_v0; +pub mod maker_lend_v0; +pub mod mobile_voucher; pub mod onboard_data_only_iot_hotspot_v0; pub mod onboard_iot_hotspot_v0; pub mod onboard_mobile_hotspot_v0; @@ -27,6 +30,8 @@ pub mod update_rewardable_entity_config_v0; pub use approve_maker_v0::*; pub use approve_program_v0::*; pub use initialize_data_only_v0::*; +pub use initialize_data_only_v0::*; +pub use initialize_maker_escrow_v0::*; pub use initialize_maker_v0::*; pub use initialize_rewardable_entity_config_v0::*; pub use issue_data_only_entity_v0::*; @@ -34,6 +39,8 @@ pub use issue_entity_v0::*; pub use issue_iot_operations_fund_v0::*; pub use issue_not_emitted_entity_v0::*; pub use issue_program_entity_v0::*; +pub use maker_lend_v0::*; +pub use mobile_voucher::*; pub use onboard_data_only_iot_hotspot_v0::*; pub use onboard_iot_hotspot_v0::*; pub use onboard_mobile_hotspot_v0::*; diff --git a/programs/helium-entity-manager/src/lib.rs b/programs/helium-entity-manager/src/lib.rs index 5ae721158..23d01186a 100644 --- a/programs/helium-entity-manager/src/lib.rs +++ b/programs/helium-entity-manager/src/lib.rs @@ -180,4 +180,43 @@ pub mod helium_entity_manager { ) -> Result<()> { temp_standardize_entity::handler(ctx, args) } + + pub fn maker_lend_v0<'info>( + ctx: Context<'_, '_, '_, 'info, MakerLendV0<'info>>, + args: MakerLendArgsV0, + ) -> Result<()> { + maker_lend_v0::handler(ctx, args) + } + + pub fn mobile_voucher_pay_mobile_v0<'info>( + ctx: Context<'_, '_, '_, 'info, MobileVoucherPayMobileV0<'info>>, + ) -> Result<()> { + mobile_voucher_pay_mobile_v0::handler(ctx) + } + + pub fn initialize_mobile_hotspot_voucher_v0<'info>( + ctx: Context<'_, '_, '_, 'info, InitializeMobileHotspotVoucherV0<'info>>, + args: InitializeMobileHotspotVoucherArgsV0, + ) -> Result<()> { + initialize_mobile_hotspot_voucher_v0::handler(ctx, args) + } + + pub fn mobile_voucher_verify_owner_v0<'info>( + ctx: Context<'_, '_, '_, 'info, MobileVoucherVerifyOwnerV0<'info>>, + ) -> Result<()> { + mobile_voucher_verify_owner_v0::handler(ctx) + } + + pub fn initialize_maker_escrow_v0<'info>( + ctx: Context<'_, '_, '_, 'info, InitializeMakerEscrowV0<'info>>, + args: InitializeMakerEscrowArgsV0, + ) -> Result<()> { + initialize_maker_escrow_v0::handler(ctx, args) + } + + pub fn mobile_voucher_pay_dc_v0<'info>( + ctx: Context<'_, '_, '_, 'info, MobileVoucherPayDcV0<'info>>, + ) -> Result<()> { + mobile_voucher_pay_dc_v0::handler(ctx) + } } diff --git a/programs/helium-entity-manager/src/state.rs b/programs/helium-entity-manager/src/state.rs index 25b337130..84bec07a6 100644 --- a/programs/helium-entity-manager/src/state.rs +++ b/programs/helium-entity-manager/src/state.rs @@ -12,6 +12,32 @@ pub struct RewardableEntityConfigV0 { pub staking_requirement: u64, } +#[account] +#[derive(Default)] +pub struct MobileHotspotVoucherV0 { + pub maker: Pubkey, + pub rewardable_entity_config: Pubkey, + pub entity_key: Vec, + pub bump_seed: u8, + pub key_serialization: KeySerialization, + pub device_type: MobileDeviceTypeV0, + pub paid_mobile: bool, + pub paid_dc: bool, + /// Maker key if not verified. Otherwise the verified owner of the hotspot + /// through the sig verifier. + pub verified_owner: Pubkey, +} + +#[account] +#[derive(Default)] +pub struct IotHotspotVoucherV0 { + pub rewardable_entity_config: Pubkey, + pub entity_key: Vec, + pub bump_seed: u8, + pub key_serialization: KeySerialization, + pub paid_dc: bool, +} + #[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, Default, PartialEq)] pub enum MobileDeviceTypeV0 { #[default] @@ -144,6 +170,11 @@ pub struct MakerV0 { pub merkle_tree: Pubkey, pub collection_bump_seed: u8, pub dao: Pubkey, + // Outstanding loans in USDC. Used to check that the + // maker loan made is an appropriate amount during onboarding + // Given write locking on accounts, this should never be above what is actually + // used in the current tx + pub expected_onboard_amount: u64, } #[account] @@ -243,6 +274,18 @@ pub const MOBILE_HOTSPOT_INFO_SIZE: usize = 8 + 8 + // dc onboarding fee paid 60; // pad +#[macro_export] +macro_rules! maker_seeds { + ( $maker:expr ) => { + &[ + "maker".as_bytes(), + $maker.dao.as_ref(), + $maker.name.as_bytes(), + &[$maker.bump_seed], + ] + }; +} + #[macro_export] macro_rules! data_only_config_seeds { ( $data_only_config:expr ) => { diff --git a/tests/conversion-escrow.ts b/tests/conversion-escrow.ts index 271aa8c0f..96d186ab5 100644 --- a/tests/conversion-escrow.ts +++ b/tests/conversion-escrow.ts @@ -12,7 +12,7 @@ import { expect } from "chai"; import { init } from "../packages/conversion-escrow-sdk/src"; import { PROGRAM_ID } from "../packages/conversion-escrow-sdk/src/constants"; import { ConversionEscrow } from "../target/types/conversion_escrow"; -import { ensureDcEscrowIdl } from "./utils/fixtures"; +import { ensureConversionEscrowIdl as ensureConversionEscrowIdl } from "./utils/fixtures"; describe("conversion-escrow", () => { anchor.setProvider(anchor.AnchorProvider.local("http://127.0.0.1:8899")); @@ -51,7 +51,7 @@ describe("conversion-escrow", () => { let conversionEscrow: PublicKey | undefined; beforeEach(async () => { - await ensureDcEscrowIdl(program); + await ensureConversionEscrowIdl(program); ({ pubkeys: { conversionEscrow }, @@ -71,6 +71,7 @@ describe("conversion-escrow", () => { owner: me, payer: me, mint: mobileMint, + updateAuthority: me }) .rpcAndKeys({ skipPreflight: true })); @@ -142,6 +143,13 @@ describe("conversion-escrow", () => { hntHolder.publicKey, BigInt(toBN(hntPerMobile * 5000, 8).toString()) ), + await program.methods + .checkRepayV0() + .accounts({ + conversionEscrow, + repayAccount: getAssociatedTokenAddressSync(hntMint, me), + }) + .instruction(), ]; await sendInstructions(provider, instructions, [hntHolder]); diff --git a/tests/helium-entity-manager.ts b/tests/helium-entity-manager.ts index 1f4c9f96e..6f3790904 100644 --- a/tests/helium-entity-manager.ts +++ b/tests/helium-entity-manager.ts @@ -3,25 +3,37 @@ import { Program } from "@coral-xyz/anchor"; import { Keypair as HeliumKeypair } from "@helium/crypto"; import { init as initDataCredits } from "@helium/data-credits-sdk"; import { init as initHeliumSubDaos } from "@helium/helium-sub-daos-sdk"; +import { init as initConversionEscrow } from "@helium/conversion-escrow-sdk"; import { notEmittedKey, init as initBurn } from "@helium/no-emit-sdk"; import { Asset, AssetProof, + HELIUM_COMMON_LUT, + HNT_PYTH_PRICE_FEED, + MOBILE_PYTH_PRICE_FEED, + USDC_PYTH_PRICE_FEED, createAtaAndMint, createMint, createMintInstructions, proofArgsAndAccounts, sendInstructions, toBN, + toVersionedTx, } from "@helium/spl-utils"; import { AddGatewayV1 } from "@helium/transactions"; -import { getAssociatedTokenAddressSync, getAccount } from "@solana/spl-token"; +import { + createAssociatedTokenAccountIdempotentInstruction, + createTransferInstruction, + getAssociatedTokenAddressSync, + getAccount, +} from "@solana/spl-token"; import { ComputeBudgetProgram, Keypair, LAMPORTS_PER_SOL, PublicKey, SystemProgram, + Connection, } from "@solana/web3.js"; import chai from "chai"; import { @@ -47,6 +59,7 @@ import { initTestMaker, initTestRewardableEntityConfig, MAKER_STAKING_FEE, + ensureConversionEscrowIdl, } from "./utils/fixtures"; // @ts-ignore import bs58 from "bs58"; @@ -65,7 +78,10 @@ import { BN } from "bn.js"; import chaiAsPromised from "chai-as-promised"; import { createMockCompression } from "./utils/compression"; import { loadKeypair } from "./utils/solana"; -import { keyToAssetKey } from "@helium/helium-entity-manager-sdk"; +import { keyToAssetKey, payMobileVoucherDc, payMobileVoucherMobile } from "@helium/helium-entity-manager-sdk"; +import { program } from "@coral-xyz/anchor/dist/cjs/native/system"; +import { ConversionEscrow } from "../target/types/conversion_escrow"; +import { parsePriceData } from "@pythnetwork/client"; chai.use(chaiAsPromised); @@ -75,6 +91,7 @@ describe("helium-entity-manager", () => { let dcProgram: Program; let hsdProgram: Program; let hemProgram: Program; + let conversionEscrowProgram: Program; let noEmitProgram: Program; const provider = anchor.getProvider() as anchor.AnchorProvider; @@ -83,9 +100,16 @@ describe("helium-entity-manager", () => { let dao: PublicKey; let subDao: PublicKey; let dcMint: PublicKey; + let hntMint: PublicKey; + let dntMint: PublicKey; let activeDeviceAuthority: Keypair; beforeEach(async () => { + conversionEscrowProgram = await initConversionEscrow( + provider, + anchor.workspace.ConversionEscrow.programId, + anchor.workspace.ConversionEscrow.idl + ); dcProgram = await initDataCredits( provider, anchor.workspace.DataCredits.programId, @@ -115,17 +139,22 @@ describe("helium-entity-manager", () => { ); await ensureHEMIdl(hemProgram); - const dataCredits = await initTestDataCredits(dcProgram, provider); + const dataCredits = await initTestDataCredits( + dcProgram, + provider, + 100000000000 + ); dcMint = dataCredits.dcMint; - ({ dao } = await initTestDao( + ({ dao, mint: hntMint } = await initTestDao( hsdProgram, provider, 100, me, - dataCredits.dcMint + dataCredits.dcMint, + dataCredits.hntMint, )); activeDeviceAuthority = Keypair.generate(); - ({ subDao } = await initTestSubdao({ + ({ subDao, mint: dntMint } = await initTestSubdao({ hsdProgram, provider, authority: me, @@ -660,6 +689,263 @@ describe("helium-entity-manager", () => { await provider.connection.confirmTransaction(sig); }); + describe("with voucher", async () => { + let mobileHotspotVoucher: PublicKey | undefined; + let conversionEscrow: PublicKey | undefined; + let usdcMint: PublicKey; + const slippageBps = 5; + + beforeEach(async () => { + await ensureConversionEscrowIdl(conversionEscrowProgram); + usdcMint = await createMint(provider, 6, me, me); + + ({ + pubkeys: { conversionEscrow }, + } = await hemProgram.methods + .initializeMakerEscrowV0({ + oracle: USDC_PYTH_PRICE_FEED, + // Slippage can be super low since price isn't changing. + targets: [ + { + mint: dntMint, + slippageBps, + oracle: MOBILE_PYTH_PRICE_FEED, + }, + { + mint: hntMint, + slippageBps, + oracle: HNT_PYTH_PRICE_FEED, + }, + ], + }) + .accounts({ + maker, + payer: me, + mint: usdcMint, + updateAuthority: makerKeypair.publicKey, + }) + .signers([makerKeypair]) + .rpcAndKeys({ skipPreflight: true })); + + // Populate the escrow + await createAtaAndMint( + provider, + usdcMint, + toBN(1000000, 6).toNumber(), + conversionEscrow + ); + + ({ + pubkeys: { mobileHotspotVoucher }, + } = await hemProgram.methods + .initializeMobileHotspotVoucherV0({ + entityKey: Buffer.from(bs58.decode(ecc)), + keySerialization: { b58: {} }, + deviceType: { wifiOutdoor: {} }, + }) + .accounts({ + maker, + rewardableEntityConfig, + issuingAuthority: makerKeypair.publicKey, + }) + .signers([makerKeypair]) + .rpcAndKeys({ skipPreflight: true })); + await hemProgram.methods + .mobileVoucherVerifyOwnerV0() + .accounts({ + verifiedOwner: me, + eccVerifier: eccVerifier.publicKey, + mobileHotspotVoucher, + }) + .signers([eccVerifier]) + .rpc({ skipPreflight: true }); + }); + + it("allows paying mobile", async () => { + const makerDntAccount = getAssociatedTokenAddressSync(dntMint, maker, true); + const pythDataMobile = (await provider.connection.getAccountInfo( + MOBILE_PYTH_PRICE_FEED + ))!.data; + const priceMobile = parsePriceData(pythDataMobile); + + const usdPerMobile = + priceMobile.emaPrice.value - priceMobile.emaConfidence!.value * 2; + const mobileAmount = toBN(20 / usdPerMobile, 6); + const myUsdcAccount = getAssociatedTokenAddressSync(usdcMint, me); + const instructions = [ + createAssociatedTokenAccountIdempotentInstruction( + me, + myUsdcAccount, + me, + usdcMint + ), + await hemProgram.methods + .makerLendV0({ + amount: toBN(20, 6), + }) + .accounts({ + maker, + targetOracle: MOBILE_PYTH_PRICE_FEED, + conversionEscrow, + destination: myUsdcAccount, + repayAccount: makerDntAccount, + usdcMint, + }) + .instruction(), + // Repay + createAssociatedTokenAccountIdempotentInstruction( + me, + makerDntAccount, + maker, + dntMint + ), + createTransferInstruction( + getAssociatedTokenAddressSync(dntMint, me), + makerDntAccount, + me, + BigInt(mobileAmount.toString()) + ), + await conversionEscrowProgram.methods + .checkRepayV0() + .accounts({ + conversionEscrow, + repayAccount: makerDntAccount, + }) + .instruction(), + await hemProgram.methods + .mobileVoucherPayMobileV0() + .accounts({ + mobileHotspotVoucher, + dntPrice: MOBILE_PYTH_PRICE_FEED, + }) + .instruction(), + ]; + await sendInstructions(provider, instructions) + const voucher = await hemProgram.account.mobileHotspotVoucherV0.fetch(mobileHotspotVoucher!); + expect(voucher.paidMobile).to.be.true; + }) + + it("allows paying DC", async () => { + const makerHntAccount = getAssociatedTokenAddressSync( + hntMint, + maker, + true + ); + const myUsdcAccount = getAssociatedTokenAddressSync(usdcMint, me); + const instructions = [ + createAssociatedTokenAccountIdempotentInstruction( + me, + myUsdcAccount, + me, + usdcMint + ), + createAssociatedTokenAccountIdempotentInstruction( + me, + makerHntAccount, + maker, + hntMint + ), + await hemProgram.methods + .makerLendV0({ + amount: toBN(10, 6), + }) + .accounts({ + maker, + targetOracle: HNT_PYTH_PRICE_FEED, + conversionEscrow, + destination: myUsdcAccount, + repayAccount: makerHntAccount, + usdcMint, + }) + .instruction(), + // Repay in HNT + createTransferInstruction( + getAssociatedTokenAddressSync(hntMint, me), + makerHntAccount, + me, + BigInt(10000000000) + ), + await conversionEscrowProgram.methods + .checkRepayV0() + .accounts({ + conversionEscrow, + repayAccount: makerHntAccount, + }) + .instruction(), + await hemProgram.methods + .mobileVoucherPayDcV0() + .accounts({ + mobileHotspotVoucher, + }) + .instruction(), + ]; + await sendInstructions(provider, instructions); + const voucher = await hemProgram.account.mobileHotspotVoucherV0.fetch( + mobileHotspotVoucher! + ); + expect(voucher.paidDc).to.be.true; + }); + + it("fits a jupiter transaction in with flash lend mobile", async () => { + const { instructions, addressLookupTableAddresses } = + await payMobileVoucherMobile({ + program: hemProgram, + ceProgram: conversionEscrowProgram, + mobileHotspotVoucher: mobileHotspotVoucher!, + verifiedOwner: me, + maker, + payer: me, + }); + const tx = await toVersionedTx({ + instructions: [ + await hemProgram.methods + .mobileVoucherVerifyOwnerV0() + .accounts({ + verifiedOwner: me, + eccVerifier: eccVerifier.publicKey, + mobileHotspotVoucher, + }) + .instruction(), + ...instructions, + ], + addressLookupTableAddresses: [ + ...addressLookupTableAddresses, + HELIUM_COMMON_LUT, + ], + connection: new Connection("https://api.mainnet-beta.solana.com"), + payer: me, + }); + await tx.sign([eccVerifier]); + const signed = await provider.wallet.signTransaction(tx) + const serialized = signed.serialize(); + expect(serialized.length).to.be.lessThan(1232); + }) + + it("fits a jupiter transaction in with flash lend dc", async () => { + const { instructions, addressLookupTableAddresses } = + await payMobileVoucherDc({ + program: hemProgram, + ceProgram: conversionEscrowProgram, + mobileHotspotVoucher: mobileHotspotVoucher!, + verifiedOwner: me, + maker, + payer: me, + }); + const tx = await toVersionedTx({ + instructions, + addressLookupTableAddresses: [ + ...addressLookupTableAddresses, + HELIUM_COMMON_LUT, + ], + connection: new Connection("https://api.mainnet-beta.solana.com"), + payer: me, + }); + const signed = await provider.wallet.signTransaction(tx); + const serialized = signed.serialize(); + expect(serialized.length).to.be.lessThan(1232); + }); + }) + it("issues a mobile hotspot", async () => { await hemProgram.methods .issueEntityV0({ diff --git a/tests/utils/fixtures.ts b/tests/utils/fixtures.ts index f32bda828..9e9dbf26a 100644 --- a/tests/utils/fixtures.ts +++ b/tests/utils/fixtures.ts @@ -286,7 +286,7 @@ export async function ensureHSDIdl(hsdProgram: Program) { } } -export async function ensureDcEscrowIdl(dcEscrowProgram: Program) { +export async function ensureConversionEscrowIdl(dcEscrowProgram: Program) { try { execSync( `${ANCHOR_PATH} idl init --filepath ${__dirname}/../../target/idl/conversion_escrow.json ${dcEscrowProgram.programId}`, From 22a8e5316b512a17e2b7f8d11853b32d341b2546 Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Mon, 8 Apr 2024 17:00:16 -0500 Subject: [PATCH 07/12] Add issue/onboard endpoint for mobile --- packages/conversion-escrow-sdk/package.json | 6 +- .../conversion-escrow-sdk/yarn.deploy.lock | 8 +- packages/crons/yarn.deploy.lock | 17 ++ packages/distributor-oracle/yarn.deploy.lock | 17 ++ packages/entity-invalidator/yarn.deploy.lock | 17 ++ packages/helium-admin-cli/yarn.deploy.lock | 17 ++ .../helium-entity-manager-sdk/package.json | 2 +- .../src/resolvers.ts | 40 ++-- .../yarn.deploy.lock | 17 ++ packages/hotspot-utils/yarn.deploy.lock | 17 ++ packages/metadata-service/yarn.deploy.lock | 17 ++ packages/migration-service/yarn.deploy.lock | 17 ++ .../yarn.deploy.lock | 17 ++ packages/monitor-service/yarn.deploy.lock | 17 ++ packages/xnft-hotspot/yarn.deploy.lock | 17 ++ programs/helium-entity-manager/src/error.rs | 3 + .../src/instructions/issue_entity/common.rs | 187 ++++++++++++++++++ .../src/instructions/issue_entity/mod.rs | 3 + .../src/instructions/issue_entity_v0.rs | 7 +- .../initialize_mobile_hotspot_voucher_v0.rs | 1 + .../mobile_voucher/issue_mobile_hotspot_v0.rs | 80 ++++++++ .../mobile_voucher_pay_dc_v0.rs | 28 ++- .../src/instructions/mobile_voucher/mod.rs | 2 + .../src/instructions/mod.rs | 2 + programs/helium-entity-manager/src/lib.rs | 7 + programs/helium-entity-manager/src/state.rs | 4 +- tests/helium-entity-manager.ts | 151 +++++++++++++- yarn.lock | 11 +- 28 files changed, 695 insertions(+), 34 deletions(-) create mode 100644 programs/helium-entity-manager/src/instructions/issue_entity/common.rs create mode 100644 programs/helium-entity-manager/src/instructions/issue_entity/mod.rs create mode 100644 programs/helium-entity-manager/src/instructions/mobile_voucher/issue_mobile_hotspot_v0.rs diff --git a/packages/conversion-escrow-sdk/package.json b/packages/conversion-escrow-sdk/package.json index 775185296..de027e64c 100644 --- a/packages/conversion-escrow-sdk/package.json +++ b/packages/conversion-escrow-sdk/package.json @@ -5,7 +5,7 @@ "registry": "https://registry.npmjs.org/" }, "license": "Apache-2.0", - "version": "0.6.41", + "version": "0.6.42", "description": "Interface to the conversion-escrow smart contract", "repository": { "type": "git", @@ -32,8 +32,8 @@ }, "dependencies": { "@coral-xyz/anchor": "^0.28.0", - "@helium/anchor-resolvers": "^0.6.41", - "@helium/idls": "^0.6.41", + "@helium/anchor-resolvers": "^0.6.42", + "@helium/idls": "^0.6.42", "bn.js": "^5.2.0", "bs58": "^4.0.1" }, diff --git a/packages/conversion-escrow-sdk/yarn.deploy.lock b/packages/conversion-escrow-sdk/yarn.deploy.lock index 13f8204a9..14111d47e 100644 --- a/packages/conversion-escrow-sdk/yarn.deploy.lock +++ b/packages/conversion-escrow-sdk/yarn.deploy.lock @@ -58,7 +58,7 @@ __metadata: languageName: node linkType: hard -"@helium/anchor-resolvers@^0.6.41": +"@helium/anchor-resolvers@^0.6.42": version: 0.0.0-use.local resolution: "@helium/anchor-resolvers@workspace:packages/anchor-resolvers" dependencies: @@ -77,8 +77,8 @@ __metadata: resolution: "@helium/conversion-escrow-sdk@workspace:." dependencies: "@coral-xyz/anchor": ^0.28.0 - "@helium/anchor-resolvers": ^0.6.41 - "@helium/idls": ^0.6.41 + "@helium/anchor-resolvers": ^0.6.42 + "@helium/idls": ^0.6.42 bn.js: ^5.2.0 bs58: ^4.0.1 git-format-staged: ^2.1.3 @@ -88,7 +88,7 @@ __metadata: languageName: unknown linkType: soft -"@helium/idls@^0.6.41": +"@helium/idls@^0.6.42": version: 0.0.0-use.local resolution: "@helium/idls@workspace:packages/idls" dependencies: diff --git a/packages/crons/yarn.deploy.lock b/packages/crons/yarn.deploy.lock index 29581d3a4..7e769b4dc 100644 --- a/packages/crons/yarn.deploy.lock +++ b/packages/crons/yarn.deploy.lock @@ -166,6 +166,22 @@ __metadata: languageName: unknown linkType: soft +"@helium/conversion-escrow-sdk@^0.6.42": + version: 0.0.0-use.local + resolution: "@helium/conversion-escrow-sdk@workspace:packages/conversion-escrow-sdk" + dependencies: + "@coral-xyz/anchor": ^0.28.0 + "@helium/anchor-resolvers": ^0.6.42 + "@helium/idls": ^0.6.42 + bn.js: ^5.2.0 + bs58: ^4.0.1 + git-format-staged: ^2.1.3 + ts-loader: ^9.2.3 + ts-node: ^10.9.1 + typescript: ^5.2.2 + languageName: unknown + linkType: soft + "@helium/crons@workspace:.": version: 0.0.0-use.local resolution: "@helium/crons@workspace:." @@ -269,6 +285,7 @@ __metadata: "@coral-xyz/anchor": ^0.28.0 "@helium/address": ^4.10.2 "@helium/anchor-resolvers": ^0.6.42 + "@helium/conversion-escrow-sdk": ^0.6.42 "@helium/helium-sub-daos-sdk": ^0.6.42 "@helium/idls": ^0.6.42 "@helium/no-emit-sdk": ^0.6.42 diff --git a/packages/distributor-oracle/yarn.deploy.lock b/packages/distributor-oracle/yarn.deploy.lock index c7ff1c888..d1289b40e 100644 --- a/packages/distributor-oracle/yarn.deploy.lock +++ b/packages/distributor-oracle/yarn.deploy.lock @@ -166,6 +166,22 @@ __metadata: languageName: unknown linkType: soft +"@helium/conversion-escrow-sdk@^0.6.42": + version: 0.0.0-use.local + resolution: "@helium/conversion-escrow-sdk@workspace:packages/conversion-escrow-sdk" + dependencies: + "@coral-xyz/anchor": ^0.28.0 + "@helium/anchor-resolvers": ^0.6.42 + "@helium/idls": ^0.6.42 + bn.js: ^5.2.0 + bs58: ^4.0.1 + git-format-staged: ^2.1.3 + ts-loader: ^9.2.3 + ts-node: ^10.9.1 + typescript: ^5.2.2 + languageName: unknown + linkType: soft + "@helium/distributor-oracle@workspace:.": version: 0.0.0-use.local resolution: "@helium/distributor-oracle@workspace:." @@ -214,6 +230,7 @@ __metadata: "@coral-xyz/anchor": ^0.28.0 "@helium/address": ^4.10.2 "@helium/anchor-resolvers": ^0.6.42 + "@helium/conversion-escrow-sdk": ^0.6.42 "@helium/helium-sub-daos-sdk": ^0.6.42 "@helium/idls": ^0.6.42 "@helium/no-emit-sdk": ^0.6.42 diff --git a/packages/entity-invalidator/yarn.deploy.lock b/packages/entity-invalidator/yarn.deploy.lock index 5bb7660fb..93e5caabd 100644 --- a/packages/entity-invalidator/yarn.deploy.lock +++ b/packages/entity-invalidator/yarn.deploy.lock @@ -122,6 +122,22 @@ __metadata: languageName: unknown linkType: soft +"@helium/conversion-escrow-sdk@^0.6.42": + version: 0.0.0-use.local + resolution: "@helium/conversion-escrow-sdk@workspace:packages/conversion-escrow-sdk" + dependencies: + "@coral-xyz/anchor": ^0.28.0 + "@helium/anchor-resolvers": ^0.6.42 + "@helium/idls": ^0.6.42 + bn.js: ^5.2.0 + bs58: ^4.0.1 + git-format-staged: ^2.1.3 + ts-loader: ^9.2.3 + ts-node: ^10.9.1 + typescript: ^5.2.2 + languageName: unknown + linkType: soft + "@helium/entity-invalidator@workspace:.": version: 0.0.0-use.local resolution: "@helium/entity-invalidator@workspace:." @@ -160,6 +176,7 @@ __metadata: "@coral-xyz/anchor": ^0.28.0 "@helium/address": ^4.10.2 "@helium/anchor-resolvers": ^0.6.42 + "@helium/conversion-escrow-sdk": ^0.6.42 "@helium/helium-sub-daos-sdk": ^0.6.42 "@helium/idls": ^0.6.42 "@helium/no-emit-sdk": ^0.6.42 diff --git a/packages/helium-admin-cli/yarn.deploy.lock b/packages/helium-admin-cli/yarn.deploy.lock index 72bf8821f..8348803c3 100644 --- a/packages/helium-admin-cli/yarn.deploy.lock +++ b/packages/helium-admin-cli/yarn.deploy.lock @@ -201,6 +201,22 @@ __metadata: languageName: unknown linkType: soft +"@helium/conversion-escrow-sdk@^0.6.42": + version: 0.0.0-use.local + resolution: "@helium/conversion-escrow-sdk@workspace:packages/conversion-escrow-sdk" + dependencies: + "@coral-xyz/anchor": ^0.28.0 + "@helium/anchor-resolvers": ^0.6.42 + "@helium/idls": ^0.6.42 + bn.js: ^5.2.0 + bs58: ^4.0.1 + git-format-staged: ^2.1.3 + ts-loader: ^9.2.3 + ts-node: ^10.9.1 + typescript: ^5.2.2 + languageName: unknown + linkType: soft + "@helium/crypto@npm:^4.10.2": version: 4.10.2 resolution: "@helium/crypto@npm:4.10.2" @@ -333,6 +349,7 @@ __metadata: "@coral-xyz/anchor": ^0.28.0 "@helium/address": ^4.10.2 "@helium/anchor-resolvers": ^0.6.42 + "@helium/conversion-escrow-sdk": ^0.6.42 "@helium/helium-sub-daos-sdk": ^0.6.42 "@helium/idls": ^0.6.42 "@helium/no-emit-sdk": ^0.6.42 diff --git a/packages/helium-entity-manager-sdk/package.json b/packages/helium-entity-manager-sdk/package.json index 7f43aeec1..d2596a343 100644 --- a/packages/helium-entity-manager-sdk/package.json +++ b/packages/helium-entity-manager-sdk/package.json @@ -34,8 +34,8 @@ "@coral-xyz/anchor": "^0.28.0", "@helium/address": "^4.10.2", "@helium/anchor-resolvers": "^0.6.42", + "@helium/conversion-escrow-sdk": "^0.6.42", "@helium/helium-sub-daos-sdk": "^0.6.42", - "@helium/conversion-excrow-sdk": "^0.6.42", "@helium/idls": "^0.6.42", "@helium/no-emit-sdk": "^0.6.42", "@helium/spl-utils": "^0.6.42", diff --git a/packages/helium-entity-manager-sdk/src/resolvers.ts b/packages/helium-entity-manager-sdk/src/resolvers.ts index 0add36700..3448d20b5 100644 --- a/packages/helium-entity-manager-sdk/src/resolvers.ts +++ b/packages/helium-entity-manager-sdk/src/resolvers.ts @@ -12,6 +12,7 @@ import { PublicKey } from "@solana/web3.js"; import { init } from "./init"; import { iotInfoKey, keyToAssetKey, mobileHotspotVoucherKey, mobileInfoKey, programApprovalKey } from "./pdas"; import { notEmittedKey } from "@helium/no-emit-sdk"; +import { Accounts } from "@coral-xyz/anchor"; export const heliumEntityManagerResolvers = combineResolvers( heliumCommonResolver, @@ -59,15 +60,16 @@ export const heliumEntityManagerResolvers = combineResolvers( } }), resolveIndividual(async ({ idlIx, path, args, accounts }) => { + const dao = accounts.dao || (accounts.issueEntityCommon as Accounts)?.dao if ( path[path.length - 1] === "keyToAsset" && args[args.length - 1] && args[args.length - 1].entityKey && - accounts.dao + dao ) { return ( await keyToAssetKey( - accounts.dao as PublicKey, + dao as PublicKey, args[args.length - 1].entityKey, args[args.length - 1].encoding || "b58" ) @@ -107,10 +109,9 @@ export const heliumEntityManagerResolvers = combineResolvers( )[0]; } }), - resolveIndividual(async ({ path, args, provider, accounts, idlIx }) => { + resolveIndividual(async ({ path, provider, accounts }) => { if ( path[path.length - 1] === "iotInfo" && - args[args.length - 1].index && accounts.merkleTree && accounts.keyToAsset ) { @@ -137,21 +138,28 @@ export const heliumEntityManagerResolvers = combineResolvers( } }), resolveIndividual(async ({ path, args, provider, accounts }) => { + const merkleTree = + accounts.merkleTree || (accounts.issueEntityCommon as Accounts)?.merkleTree; + const keyToAsset = accounts.keyToAsset || (accounts.issueEntityCommon as Accounts)?.keyToAsset; if ( path[path.length - 1] === "mobileInfo" && - args[args.length - 1].index && - accounts.merkleTree && + merkleTree && + keyToAsset && accounts.rewardableEntityConfig ) { - // @ts-ignore - const program = await init(provider); - const keyToAssetAcc = await program.account.keyToAssetV0.fetch( - accounts.keyToAsset as PublicKey - ); + let entityKey = args[0].entityKey; + if (!entityKey) { + // @ts-ignore + const program = await init(provider); + const keyToAssetAcc = await program.account.keyToAssetV0.fetch( + keyToAsset as PublicKey + ); + entityKey = keyToAssetAcc.entityKey; + } return ( await mobileInfoKey( accounts.rewardableEntityConfig as PublicKey, - keyToAssetAcc.entityKey + entityKey ) )[0]; } @@ -223,3 +231,11 @@ export const heliumEntityManagerResolvers = combineResolvers( }), subDaoEpochInfoResolver ); + +function getParent(accounts: any, path: string[]) { + let parent = accounts; + for (let i = 0; i < path.length - 1; i++) { + parent = parent[path[i]]; + } + return parent; +} diff --git a/packages/helium-entity-manager-sdk/yarn.deploy.lock b/packages/helium-entity-manager-sdk/yarn.deploy.lock index b85c8e446..6a8853df9 100644 --- a/packages/helium-entity-manager-sdk/yarn.deploy.lock +++ b/packages/helium-entity-manager-sdk/yarn.deploy.lock @@ -122,6 +122,22 @@ __metadata: languageName: unknown linkType: soft +"@helium/conversion-escrow-sdk@^0.6.42": + version: 0.0.0-use.local + resolution: "@helium/conversion-escrow-sdk@workspace:packages/conversion-escrow-sdk" + dependencies: + "@coral-xyz/anchor": ^0.28.0 + "@helium/anchor-resolvers": ^0.6.42 + "@helium/idls": ^0.6.42 + bn.js: ^5.2.0 + bs58: ^4.0.1 + git-format-staged: ^2.1.3 + ts-loader: ^9.2.3 + ts-node: ^10.9.1 + typescript: ^5.2.2 + languageName: unknown + linkType: soft + "@helium/helium-entity-manager-sdk@workspace:.": version: 0.0.0-use.local resolution: "@helium/helium-entity-manager-sdk@workspace:." @@ -129,6 +145,7 @@ __metadata: "@coral-xyz/anchor": ^0.28.0 "@helium/address": ^4.10.2 "@helium/anchor-resolvers": ^0.6.42 + "@helium/conversion-escrow-sdk": ^0.6.42 "@helium/helium-sub-daos-sdk": ^0.6.42 "@helium/idls": ^0.6.42 "@helium/no-emit-sdk": ^0.6.42 diff --git a/packages/hotspot-utils/yarn.deploy.lock b/packages/hotspot-utils/yarn.deploy.lock index 1f164374f..287a38dcd 100644 --- a/packages/hotspot-utils/yarn.deploy.lock +++ b/packages/hotspot-utils/yarn.deploy.lock @@ -122,6 +122,22 @@ __metadata: languageName: unknown linkType: soft +"@helium/conversion-escrow-sdk@^0.6.42": + version: 0.0.0-use.local + resolution: "@helium/conversion-escrow-sdk@workspace:packages/conversion-escrow-sdk" + dependencies: + "@coral-xyz/anchor": ^0.28.0 + "@helium/anchor-resolvers": ^0.6.42 + "@helium/idls": ^0.6.42 + bn.js: ^5.2.0 + bs58: ^4.0.1 + git-format-staged: ^2.1.3 + ts-loader: ^9.2.3 + ts-node: ^10.9.1 + typescript: ^5.2.2 + languageName: unknown + linkType: soft + "@helium/helium-entity-manager-sdk@^0.6.42": version: 0.0.0-use.local resolution: "@helium/helium-entity-manager-sdk@workspace:packages/helium-entity-manager-sdk" @@ -129,6 +145,7 @@ __metadata: "@coral-xyz/anchor": ^0.28.0 "@helium/address": ^4.10.2 "@helium/anchor-resolvers": ^0.6.42 + "@helium/conversion-escrow-sdk": ^0.6.42 "@helium/helium-sub-daos-sdk": ^0.6.42 "@helium/idls": ^0.6.42 "@helium/no-emit-sdk": ^0.6.42 diff --git a/packages/metadata-service/yarn.deploy.lock b/packages/metadata-service/yarn.deploy.lock index 7436e0643..3221bd860 100644 --- a/packages/metadata-service/yarn.deploy.lock +++ b/packages/metadata-service/yarn.deploy.lock @@ -190,6 +190,22 @@ __metadata: languageName: unknown linkType: soft +"@helium/conversion-escrow-sdk@^0.6.42": + version: 0.0.0-use.local + resolution: "@helium/conversion-escrow-sdk@workspace:packages/conversion-escrow-sdk" + dependencies: + "@coral-xyz/anchor": ^0.28.0 + "@helium/anchor-resolvers": ^0.6.42 + "@helium/idls": ^0.6.42 + bn.js: ^5.2.0 + bs58: ^4.0.1 + git-format-staged: ^2.1.3 + ts-loader: ^9.2.3 + ts-node: ^10.9.1 + typescript: ^5.2.2 + languageName: unknown + linkType: soft + "@helium/data-credits-sdk@^0.6.42": version: 0.0.0-use.local resolution: "@helium/data-credits-sdk@workspace:packages/data-credits-sdk" @@ -217,6 +233,7 @@ __metadata: "@coral-xyz/anchor": ^0.28.0 "@helium/address": ^4.10.2 "@helium/anchor-resolvers": ^0.6.42 + "@helium/conversion-escrow-sdk": ^0.6.42 "@helium/helium-sub-daos-sdk": ^0.6.42 "@helium/idls": ^0.6.42 "@helium/no-emit-sdk": ^0.6.42 diff --git a/packages/migration-service/yarn.deploy.lock b/packages/migration-service/yarn.deploy.lock index 84a2fb742..f812270c8 100644 --- a/packages/migration-service/yarn.deploy.lock +++ b/packages/migration-service/yarn.deploy.lock @@ -211,6 +211,22 @@ __metadata: languageName: unknown linkType: soft +"@helium/conversion-escrow-sdk@^0.6.42": + version: 0.0.0-use.local + resolution: "@helium/conversion-escrow-sdk@workspace:packages/conversion-escrow-sdk" + dependencies: + "@coral-xyz/anchor": ^0.28.0 + "@helium/anchor-resolvers": ^0.6.42 + "@helium/idls": ^0.6.42 + bn.js: ^5.2.0 + bs58: ^4.0.1 + git-format-staged: ^2.1.3 + ts-loader: ^9.2.3 + ts-node: ^10.9.1 + typescript: ^5.2.2 + languageName: unknown + linkType: soft + "@helium/crypto@npm:^4.10.2": version: 4.10.2 resolution: "@helium/crypto@npm:4.10.2" @@ -290,6 +306,7 @@ __metadata: "@coral-xyz/anchor": ^0.28.0 "@helium/address": ^4.10.2 "@helium/anchor-resolvers": ^0.6.42 + "@helium/conversion-escrow-sdk": ^0.6.42 "@helium/helium-sub-daos-sdk": ^0.6.42 "@helium/idls": ^0.6.42 "@helium/no-emit-sdk": ^0.6.42 diff --git a/packages/mobile-entity-manager-sdk/yarn.deploy.lock b/packages/mobile-entity-manager-sdk/yarn.deploy.lock index 53141fea1..84c591e09 100644 --- a/packages/mobile-entity-manager-sdk/yarn.deploy.lock +++ b/packages/mobile-entity-manager-sdk/yarn.deploy.lock @@ -122,6 +122,22 @@ __metadata: languageName: unknown linkType: soft +"@helium/conversion-escrow-sdk@^0.6.42": + version: 0.0.0-use.local + resolution: "@helium/conversion-escrow-sdk@workspace:packages/conversion-escrow-sdk" + dependencies: + "@coral-xyz/anchor": ^0.28.0 + "@helium/anchor-resolvers": ^0.6.42 + "@helium/idls": ^0.6.42 + bn.js: ^5.2.0 + bs58: ^4.0.1 + git-format-staged: ^2.1.3 + ts-loader: ^9.2.3 + ts-node: ^10.9.1 + typescript: ^5.2.2 + languageName: unknown + linkType: soft + "@helium/helium-entity-manager-sdk@^0.6.42": version: 0.0.0-use.local resolution: "@helium/helium-entity-manager-sdk@workspace:packages/helium-entity-manager-sdk" @@ -129,6 +145,7 @@ __metadata: "@coral-xyz/anchor": ^0.28.0 "@helium/address": ^4.10.2 "@helium/anchor-resolvers": ^0.6.42 + "@helium/conversion-escrow-sdk": ^0.6.42 "@helium/helium-sub-daos-sdk": ^0.6.42 "@helium/idls": ^0.6.42 "@helium/no-emit-sdk": ^0.6.42 diff --git a/packages/monitor-service/yarn.deploy.lock b/packages/monitor-service/yarn.deploy.lock index 2a921927b..62dcaba80 100644 --- a/packages/monitor-service/yarn.deploy.lock +++ b/packages/monitor-service/yarn.deploy.lock @@ -156,6 +156,22 @@ __metadata: languageName: unknown linkType: soft +"@helium/conversion-escrow-sdk@^0.6.42": + version: 0.0.0-use.local + resolution: "@helium/conversion-escrow-sdk@workspace:packages/conversion-escrow-sdk" + dependencies: + "@coral-xyz/anchor": ^0.28.0 + "@helium/anchor-resolvers": ^0.6.42 + "@helium/idls": ^0.6.42 + bn.js: ^5.2.0 + bs58: ^4.0.1 + git-format-staged: ^2.1.3 + ts-loader: ^9.2.3 + ts-node: ^10.9.1 + typescript: ^5.2.2 + languageName: unknown + linkType: soft + "@helium/data-credits-sdk@^0.6.42": version: 0.0.0-use.local resolution: "@helium/data-credits-sdk@workspace:packages/data-credits-sdk" @@ -183,6 +199,7 @@ __metadata: "@coral-xyz/anchor": ^0.28.0 "@helium/address": ^4.10.2 "@helium/anchor-resolvers": ^0.6.42 + "@helium/conversion-escrow-sdk": ^0.6.42 "@helium/helium-sub-daos-sdk": ^0.6.42 "@helium/idls": ^0.6.42 "@helium/no-emit-sdk": ^0.6.42 diff --git a/packages/xnft-hotspot/yarn.deploy.lock b/packages/xnft-hotspot/yarn.deploy.lock index 563314e68..d62bc90cf 100644 --- a/packages/xnft-hotspot/yarn.deploy.lock +++ b/packages/xnft-hotspot/yarn.deploy.lock @@ -376,6 +376,22 @@ __metadata: languageName: unknown linkType: soft +"@helium/conversion-escrow-sdk@^0.6.42": + version: 0.0.0-use.local + resolution: "@helium/conversion-escrow-sdk@workspace:packages/conversion-escrow-sdk" + dependencies: + "@coral-xyz/anchor": ^0.28.0 + "@helium/anchor-resolvers": ^0.6.42 + "@helium/idls": ^0.6.42 + bn.js: ^5.2.0 + bs58: ^4.0.1 + git-format-staged: ^2.1.3 + ts-loader: ^9.2.3 + ts-node: ^10.9.1 + typescript: ^5.2.2 + languageName: unknown + linkType: soft + "@helium/distributor-oracle@^0.6.42": version: 0.0.0-use.local resolution: "@helium/distributor-oracle@workspace:packages/distributor-oracle" @@ -424,6 +440,7 @@ __metadata: "@coral-xyz/anchor": ^0.28.0 "@helium/address": ^4.10.2 "@helium/anchor-resolvers": ^0.6.42 + "@helium/conversion-escrow-sdk": ^0.6.42 "@helium/helium-sub-daos-sdk": ^0.6.42 "@helium/idls": ^0.6.42 "@helium/no-emit-sdk": ^0.6.42 diff --git a/programs/helium-entity-manager/src/error.rs b/programs/helium-entity-manager/src/error.rs index 121e75aff..59fad2f5a 100644 --- a/programs/helium-entity-manager/src/error.rs +++ b/programs/helium-entity-manager/src/error.rs @@ -62,4 +62,7 @@ pub enum ErrorCode { #[msg("Too much was borrowed from the maker for this txn")] TooMuchBorrowed, + + #[msg("Voucher was not paid for")] + VoucherNotPaid, } diff --git a/programs/helium-entity-manager/src/instructions/issue_entity/common.rs b/programs/helium-entity-manager/src/instructions/issue_entity/common.rs new file mode 100644 index 000000000..7d91ea02e --- /dev/null +++ b/programs/helium-entity-manager/src/instructions/issue_entity/common.rs @@ -0,0 +1,187 @@ +use std::cmp::min; +use std::collections::BTreeMap; + +use crate::{constants::ENTITY_METADATA_URL, error::ErrorCode}; +use crate::{key_to_asset_seeds, state::*}; +use account_compression_cpi::{program::SplAccountCompression, Noop}; +use anchor_lang::prelude::*; +use anchor_lang::solana_program::hash::hash; +use anchor_spl::token::Mint; +use angry_purple_tiger::AnimalName; +use bubblegum_cpi::{ + cpi::{accounts::MintToCollectionV1, mint_to_collection_v1}, + get_asset_id, + program::Bubblegum, + Collection, Creator, MetadataArgs, TokenProgramVersion, TokenStandard, TreeConfig, +}; +use helium_sub_daos::DaoV0; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct IssueEntityArgsV0 { + pub entity_key: Vec, +} + +#[derive(Accounts)] +pub struct IssueEntityCommonV0<'info> { + #[account(mut)] + pub payer: Signer<'info>, + pub collection: Box>, + /// CHECK: Handled by cpi + #[account( + mut, + seeds = ["metadata".as_bytes(), token_metadata_program.key().as_ref(), collection.key().as_ref()], + seeds::program = token_metadata_program.key(), + bump, + )] + pub collection_metadata: UncheckedAccount<'info>, + /// CHECK: Handled By cpi account + #[account( + seeds = ["metadata".as_bytes(), token_metadata_program.key().as_ref(), collection.key().as_ref(), "edition".as_bytes()], + seeds::program = token_metadata_program.key(), + bump, + )] + pub collection_master_edition: UncheckedAccount<'info>, + #[account( + mut, + has_one = collection, + has_one = merkle_tree, + has_one = dao, + )] + pub maker: Box>, + /// CHECK: Signs as a verified creator to make searching easier + #[account( + seeds = [b"entity_creator", dao.key().as_ref()], + bump, + )] + pub entity_creator: UncheckedAccount<'info>, + pub dao: Box>, + #[account( + mut, + seeds = [merkle_tree.key().as_ref()], + seeds::program = bubblegum_program.key(), + bump, + )] + pub tree_authority: Box>, + /// CHECK: Used in cpi + #[account(mut)] + pub merkle_tree: AccountInfo<'info>, + #[account( + seeds = ["collection_cpi".as_bytes()], + seeds::program = bubblegum_program.key(), + bump, + )] + /// CHECK: Used in cpi + pub bubblegum_signer: UncheckedAccount<'info>, + /// CHECK: Used as recipient wallet + pub recipient: AccountInfo<'info>, + + /// CHECK: Verified by constraint + #[account(address = mpl_token_metadata::ID)] + pub token_metadata_program: AccountInfo<'info>, + pub log_wrapper: Program<'info, Noop>, + pub bubblegum_program: Program<'info, Bubblegum>, + pub compression_program: Program<'info, SplAccountCompression>, + pub system_program: Program<'info, System>, +} + +impl<'info> IssueEntityCommonV0<'info> { + fn mint_to_collection_ctx(&self) -> CpiContext<'_, '_, '_, 'info, MintToCollectionV1<'info>> { + let cpi_accounts = MintToCollectionV1 { + tree_authority: self.tree_authority.to_account_info(), + leaf_delegate: self.recipient.to_account_info(), + leaf_owner: self.recipient.to_account_info(), + merkle_tree: self.merkle_tree.to_account_info(), + payer: self.payer.to_account_info(), + tree_delegate: self.maker.to_account_info(), + log_wrapper: self.log_wrapper.to_account_info(), + compression_program: self.compression_program.to_account_info(), + system_program: self.system_program.to_account_info(), + collection_authority: self.maker.to_account_info(), + collection_authority_record_pda: self.bubblegum_program.to_account_info(), + collection_mint: self.collection.to_account_info(), + collection_metadata: self.collection_metadata.to_account_info(), + edition_account: self.collection_master_edition.to_account_info(), + bubblegum_signer: self.bubblegum_signer.to_account_info(), + token_metadata_program: self.token_metadata_program.to_account_info(), + }; + CpiContext::new(self.bubblegum_program.to_account_info(), cpi_accounts) + } + + pub fn issue_entity( + &mut self, + bumps: BTreeMap, + key_to_asset: &mut Account<'info, KeyToAssetV0>, + args: IssueEntityArgsV0, + ) -> Result<()> { + let asset_id = get_asset_id(&self.merkle_tree.key(), self.tree_authority.num_minted); + key_to_asset.set_inner(KeyToAssetV0 { + asset: asset_id, + dao: self.dao.key(), + entity_key: args.entity_key.clone(), + bump_seed: bumps["key_to_asset"], + key_serialization: KeySerialization::B58, + }); + + let key_str = bs58::encode(args.entity_key).into_string(); + let animal_name: AnimalName = key_str + .parse() + .map_err(|_| error!(ErrorCode::InvalidEccCompact))?; + + let maker_seeds: &[&[&[u8]]] = &[&[ + b"maker", + self.maker.dao.as_ref(), + self.maker.name.as_bytes(), + &[self.maker.bump_seed], + ]]; + + let name = animal_name.to_string(); + let uri = format!("{}/v2/hotspot/{}", ENTITY_METADATA_URL, key_to_asset.key(),); + let metadata = MetadataArgs { + name: name[..min(name.len(), 32)].to_owned(), + symbol: String::from("HOTSPOT"), + uri, + collection: Some(Collection { + key: self.collection.key(), + verified: false, // Verified in cpi + }), + primary_sale_happened: true, + is_mutable: true, + edition_nonce: None, + token_standard: Some(TokenStandard::NonFungible), + uses: None, + token_program_version: TokenProgramVersion::Original, + creators: vec![ + Creator { + address: self.entity_creator.key(), + verified: true, + share: 100, + }, + Creator { + address: key_to_asset.key(), + verified: true, + share: 0, + }, + ], + seller_fee_basis_points: 0, + }; + let entity_creator_seeds: &[&[&[u8]]] = &[&[ + b"entity_creator", + self.dao.to_account_info().key.as_ref(), + &[bumps["entity_creator"]], + ]]; + let mut creator = self.entity_creator.to_account_info(); + creator.is_signer = true; + let mut key_to_asset_creator = key_to_asset.to_account_info(); + key_to_asset_creator.is_signer = true; + let key_to_asset_signer: &[&[u8]] = key_to_asset_seeds!(key_to_asset); + mint_to_collection_v1( + self + .mint_to_collection_ctx() + .with_remaining_accounts(vec![creator, key_to_asset_creator]) + .with_signer(&[maker_seeds[0], entity_creator_seeds[0], key_to_asset_signer]), + metadata, + )?; + + Ok(()) + } +} diff --git a/programs/helium-entity-manager/src/instructions/issue_entity/mod.rs b/programs/helium-entity-manager/src/instructions/issue_entity/mod.rs new file mode 100644 index 000000000..45987113c --- /dev/null +++ b/programs/helium-entity-manager/src/instructions/issue_entity/mod.rs @@ -0,0 +1,3 @@ +pub mod common; + +pub use common::*; diff --git a/programs/helium-entity-manager/src/instructions/issue_entity_v0.rs b/programs/helium-entity-manager/src/instructions/issue_entity_v0.rs index 3c1ca9030..cbe7ed078 100644 --- a/programs/helium-entity-manager/src/instructions/issue_entity_v0.rs +++ b/programs/helium-entity-manager/src/instructions/issue_entity_v0.rs @@ -2,7 +2,7 @@ use std::cmp::min; use std::str::FromStr; use crate::{constants::ENTITY_METADATA_URL, error::ErrorCode}; -use crate::{key_to_asset_seeds, state::*}; +use crate::{key_to_asset_seeds, state::*, IssueEntityArgsV0}; use account_compression_cpi::{program::SplAccountCompression, Noop}; use anchor_lang::prelude::*; use anchor_lang::solana_program::hash::hash; @@ -16,11 +16,6 @@ use bubblegum_cpi::{ }; use helium_sub_daos::DaoV0; -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] -pub struct IssueEntityArgsV0 { - pub entity_key: Vec, -} - pub const TESTING: bool = std::option_env!("TESTING").is_some(); pub const ECC_VERIFIER: &str = if TESTING { diff --git a/programs/helium-entity-manager/src/instructions/mobile_voucher/initialize_mobile_hotspot_voucher_v0.rs b/programs/helium-entity-manager/src/instructions/mobile_voucher/initialize_mobile_hotspot_voucher_v0.rs index dfa79ba4e..d9e888146 100644 --- a/programs/helium-entity-manager/src/instructions/mobile_voucher/initialize_mobile_hotspot_voucher_v0.rs +++ b/programs/helium-entity-manager/src/instructions/mobile_voucher/initialize_mobile_hotspot_voucher_v0.rs @@ -54,6 +54,7 @@ pub fn handler( .accounts .mobile_hotspot_voucher .set_inner(MobileHotspotVoucherV0 { + refund: ctx.accounts.payer.key(), rewardable_entity_config: ctx.accounts.rewardable_entity_config.key(), entity_key: args.entity_key, bump_seed: ctx.bumps["mobile_hotspot_voucher"], diff --git a/programs/helium-entity-manager/src/instructions/mobile_voucher/issue_mobile_hotspot_v0.rs b/programs/helium-entity-manager/src/instructions/mobile_voucher/issue_mobile_hotspot_v0.rs new file mode 100644 index 000000000..e25100a3f --- /dev/null +++ b/programs/helium-entity-manager/src/instructions/mobile_voucher/issue_mobile_hotspot_v0.rs @@ -0,0 +1,80 @@ +use crate::error::ErrorCode; +use crate::issue_entity::common::*; +use crate::state::*; +use crate::IssueEntityArgsV0; +use anchor_lang::prelude::*; +use anchor_lang::solana_program::hash::hash; +use helium_sub_daos::SubDaoV0; + +#[derive(Accounts)] +#[instruction(args: IssueEntityArgsV0)] +pub struct IssueMobileHotspotV0<'info> { + pub issue_entity_common: IssueEntityCommonV0<'info>, + #[account(mut)] + /// CHECK: Just getting refunded + pub refund: AccountInfo<'info>, + #[account( + mut, + has_one = verified_owner, + has_one = rewardable_entity_config, + has_one = refund, + close = refund, + constraint = voucher.maker == issue_entity_common.maker.key(), + constraint = voucher.paid_dc && voucher.paid_mobile @ ErrorCode::VoucherNotPaid, + seeds = [ + "mobile_hotspot_voucher".as_bytes(), + voucher.rewardable_entity_config.as_ref(), + &hash(&args.entity_key[..]).to_bytes() + ], + bump = voucher.bump_seed, + )] + pub voucher: Box>, + #[account( + has_one = sub_dao + )] + pub rewardable_entity_config: Box>, + #[account( + constraint = sub_dao.dao == issue_entity_common.dao.key(), + )] + pub sub_dao: Box>, + pub verified_owner: Signer<'info>, + #[account( + init, + payer = issue_entity_common.payer, + space = MOBILE_HOTSPOT_INFO_SIZE, + seeds = [ + b"mobile_info", + rewardable_entity_config.key().as_ref(), + &hash(&args.entity_key[..]).to_bytes() + ], + bump, + )] + pub mobile_info: Box>, + // This can't be in common accounts because there's a bug with anchor where you can't use #[instruction(...)] in those. + #[account( + init_if_needed, + payer = issue_entity_common.payer, + space = 8 + std::mem::size_of::() + 1 + args.entity_key.len(), + seeds = [ + "key_to_asset".as_bytes(), + issue_entity_common.dao.key().as_ref(), + &hash(&args.entity_key[..]).to_bytes() + ], + bump + )] + pub key_to_asset: Box>, + pub system_program: Program<'info, System>, +} + +pub fn handler(ctx: Context, args: IssueEntityArgsV0) -> Result<()> { + // Issue entity only if needed + if ctx.accounts.key_to_asset.dao == Pubkey::default() { + ctx.accounts.issue_entity_common.issue_entity( + ctx.bumps, + &mut ctx.accounts.key_to_asset, + args, + )?; + } + + Ok(()) +} diff --git a/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_pay_dc_v0.rs b/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_pay_dc_v0.rs index 911c01419..8c0d88282 100644 --- a/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_pay_dc_v0.rs +++ b/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_pay_dc_v0.rs @@ -13,7 +13,11 @@ use data_credits::{ program::DataCredits, BurnWithoutTrackingArgsV0, DataCreditsV0, MintDataCreditsArgsV0, }; -use helium_sub_daos::{DaoV0, SubDaoV0}; +use helium_sub_daos::{ + cpi::{accounts::TrackDcOnboardingFeesV0, track_dc_onboarding_fees_v0}, + program::HeliumSubDaos, + DaoV0, SubDaoV0, TrackDcOnboardingFeesArgsV0, +}; #[derive(Accounts)] pub struct MobileVoucherPayDcV0<'info> { @@ -35,6 +39,7 @@ pub struct MobileVoucherPayDcV0<'info> { pub mobile_hotspot_voucher: Box>, pub verified_owner: Signer<'info>, #[account( + mut, has_one = dao, )] pub sub_dao: Box>, @@ -86,6 +91,7 @@ pub struct MobileVoucherPayDcV0<'info> { pub data_credits_program: Program<'info, DataCredits>, pub system_program: Program<'info, System>, pub associated_token_program: Program<'info, AssociatedToken>, + pub helium_sub_daos_program: Program<'info, HeliumSubDaos>, } pub fn handler(ctx: Context) -> Result<()> { @@ -150,6 +156,26 @@ pub fn handler(ctx: Context) -> Result<()> { ), BurnWithoutTrackingArgsV0 { amount: dc_fee }, )?; + track_dc_onboarding_fees_v0( + CpiContext::new_with_signer( + ctx.accounts.helium_sub_daos_program.to_account_info(), + TrackDcOnboardingFeesV0 { + hem_auth: ctx.accounts.rewardable_entity_config.to_account_info(), + sub_dao: ctx.accounts.sub_dao.to_account_info(), + }, + &[&[ + "rewardable_entity_config".as_bytes(), + ctx.accounts.sub_dao.key().as_ref(), + ctx.accounts.rewardable_entity_config.symbol.as_bytes(), + &[ctx.accounts.rewardable_entity_config.bump_seed], + ]], + ), + TrackDcOnboardingFeesArgsV0 { + amount: fees.dc_onboarding_fee, + add: true, + symbol: ctx.accounts.rewardable_entity_config.symbol.clone(), + }, + )?; } ctx.accounts.mobile_hotspot_voucher.paid_dc = true; diff --git a/programs/helium-entity-manager/src/instructions/mobile_voucher/mod.rs b/programs/helium-entity-manager/src/instructions/mobile_voucher/mod.rs index e30fac8ff..1fb9830a6 100644 --- a/programs/helium-entity-manager/src/instructions/mobile_voucher/mod.rs +++ b/programs/helium-entity-manager/src/instructions/mobile_voucher/mod.rs @@ -1,9 +1,11 @@ pub mod initialize_mobile_hotspot_voucher_v0; +pub mod issue_mobile_hotspot_v0; pub mod mobile_voucher_pay_dc_v0; pub mod mobile_voucher_pay_mobile_v0; pub mod mobile_voucher_verify_owner_v0; pub use initialize_mobile_hotspot_voucher_v0::*; +pub use issue_mobile_hotspot_v0::*; pub use mobile_voucher_pay_dc_v0::*; pub use mobile_voucher_pay_mobile_v0::*; pub use mobile_voucher_verify_owner_v0::*; diff --git a/programs/helium-entity-manager/src/instructions/mod.rs b/programs/helium-entity-manager/src/instructions/mod.rs index 8322d4097..c95b28dc1 100644 --- a/programs/helium-entity-manager/src/instructions/mod.rs +++ b/programs/helium-entity-manager/src/instructions/mod.rs @@ -5,6 +5,7 @@ pub mod initialize_maker_escrow_v0; pub mod initialize_maker_v0; pub mod initialize_rewardable_entity_config_v0; pub mod issue_data_only_entity_v0; +pub mod issue_entity; pub mod issue_entity_v0; pub mod issue_iot_operations_fund_v0; pub mod issue_not_emitted_entity_v0; @@ -35,6 +36,7 @@ pub use initialize_maker_escrow_v0::*; pub use initialize_maker_v0::*; pub use initialize_rewardable_entity_config_v0::*; pub use issue_data_only_entity_v0::*; +pub use issue_entity::*; pub use issue_entity_v0::*; pub use issue_iot_operations_fund_v0::*; pub use issue_not_emitted_entity_v0::*; diff --git a/programs/helium-entity-manager/src/lib.rs b/programs/helium-entity-manager/src/lib.rs index 23d01186a..bd53c0302 100644 --- a/programs/helium-entity-manager/src/lib.rs +++ b/programs/helium-entity-manager/src/lib.rs @@ -219,4 +219,11 @@ pub mod helium_entity_manager { ) -> Result<()> { mobile_voucher_pay_dc_v0::handler(ctx) } + + pub fn issue_mobile_hotspot_v0<'info>( + ctx: Context<'_, '_, '_, 'info, IssueMobileHotspotV0<'info>>, + args: IssueEntityArgsV0, + ) -> Result<()> { + issue_mobile_hotspot_v0::handler(ctx, args) + } } diff --git a/programs/helium-entity-manager/src/state.rs b/programs/helium-entity-manager/src/state.rs index 84bec07a6..ba4c94754 100644 --- a/programs/helium-entity-manager/src/state.rs +++ b/programs/helium-entity-manager/src/state.rs @@ -18,7 +18,6 @@ pub struct MobileHotspotVoucherV0 { pub maker: Pubkey, pub rewardable_entity_config: Pubkey, pub entity_key: Vec, - pub bump_seed: u8, pub key_serialization: KeySerialization, pub device_type: MobileDeviceTypeV0, pub paid_mobile: bool, @@ -26,6 +25,9 @@ pub struct MobileHotspotVoucherV0 { /// Maker key if not verified. Otherwise the verified owner of the hotspot /// through the sig verifier. pub verified_owner: Pubkey, + /// The wallet that paid the rent for this, will be refunded + pub refund: Pubkey, + pub bump_seed: u8, } #[account] diff --git a/tests/helium-entity-manager.ts b/tests/helium-entity-manager.ts index 6f3790904..359e23ae3 100644 --- a/tests/helium-entity-manager.ts +++ b/tests/helium-entity-manager.ts @@ -78,7 +78,7 @@ import { BN } from "bn.js"; import chaiAsPromised from "chai-as-promised"; import { createMockCompression } from "./utils/compression"; import { loadKeypair } from "./utils/solana"; -import { keyToAssetKey, payMobileVoucherDc, payMobileVoucherMobile } from "@helium/helium-entity-manager-sdk"; +import { keyToAssetKey, mobileInfoKey, payMobileVoucherDc, payMobileVoucherMobile } from "@helium/helium-entity-manager-sdk"; import { program } from "@coral-xyz/anchor/dist/cjs/native/system"; import { ConversionEscrow } from "../target/types/conversion_escrow"; import { parsePriceData } from "@pythnetwork/client"; @@ -944,6 +944,155 @@ describe("helium-entity-manager", () => { const serialized = signed.serialize(); expect(serialized.length).to.be.lessThan(1232); }); + + describe("when fully paid", () => { + + beforeEach(async () => { + const makerDntAccount = getAssociatedTokenAddressSync( + dntMint, + maker, + true + ); + const pythDataMobile = (await provider.connection.getAccountInfo( + MOBILE_PYTH_PRICE_FEED + ))!.data; + const priceMobile = parsePriceData(pythDataMobile); + + const usdPerMobile = + priceMobile.emaPrice.value - priceMobile.emaConfidence!.value * 2; + const mobileAmount = toBN(20 / usdPerMobile, 6); + const myUsdcAccount = getAssociatedTokenAddressSync(usdcMint, me); + const instructions0 = [ + createAssociatedTokenAccountIdempotentInstruction( + me, + myUsdcAccount, + me, + usdcMint + ), + await hemProgram.methods + .makerLendV0({ + amount: toBN(20, 6), + }) + .accounts({ + maker, + targetOracle: MOBILE_PYTH_PRICE_FEED, + conversionEscrow, + destination: myUsdcAccount, + repayAccount: makerDntAccount, + usdcMint, + }) + .instruction(), + // Repay + createAssociatedTokenAccountIdempotentInstruction( + me, + makerDntAccount, + maker, + dntMint + ), + createTransferInstruction( + getAssociatedTokenAddressSync(dntMint, me), + makerDntAccount, + me, + BigInt(mobileAmount.toString()) + ), + await conversionEscrowProgram.methods + .checkRepayV0() + .accounts({ + conversionEscrow, + repayAccount: makerDntAccount, + }) + .instruction(), + await hemProgram.methods + .mobileVoucherPayMobileV0() + .accounts({ + mobileHotspotVoucher, + dntPrice: MOBILE_PYTH_PRICE_FEED, + }) + .instruction(), + ]; + await sendInstructions(provider, instructions0); + const makerHntAccount = getAssociatedTokenAddressSync( + hntMint, + maker, + true + ); + const instructions = [ + createAssociatedTokenAccountIdempotentInstruction( + me, + myUsdcAccount, + me, + usdcMint + ), + createAssociatedTokenAccountIdempotentInstruction( + me, + makerHntAccount, + maker, + hntMint + ), + await hemProgram.methods + .makerLendV0({ + amount: toBN(10, 6), + }) + .accounts({ + maker, + targetOracle: HNT_PYTH_PRICE_FEED, + conversionEscrow, + destination: myUsdcAccount, + repayAccount: makerHntAccount, + usdcMint, + }) + .instruction(), + // Repay in HNT + createTransferInstruction( + getAssociatedTokenAddressSync(hntMint, me), + makerHntAccount, + me, + BigInt(10000000000) + ), + await conversionEscrowProgram.methods + .checkRepayV0() + .accounts({ + conversionEscrow, + repayAccount: makerHntAccount, + }) + .instruction(), + await hemProgram.methods + .mobileVoucherPayDcV0() + .accounts({ + mobileHotspotVoucher, + }) + .instruction(), + ]; + await sendInstructions(provider, instructions); + }) + + it("issues a mobile hotspot", async () => { + const method = await hemProgram.methods + .issueMobileHotspotV0({ + entityKey: Buffer.from(bs58.decode(ecc)), + }) + .accounts({ + issueEntityCommon: { + maker, + recipient: me, + dao, + }, + mobileInfo: mobileInfoKey( + rewardableEntityConfig, + Buffer.from(bs58.decode(ecc)) + )[0], + voucher: mobileHotspotVoucher, + }); + await method.rpc({ skipPreflight: true }); + const { mobileInfo } = await method.pubkeys(); + + const mobileInfoAcc = + await hemProgram.account.mobileHotspotInfoV0.fetch(mobileInfo!); + expect(Boolean(mobileInfoAcc)).to.be.true; + const subDaoAcc = await hsdProgram.account.subDaoV0.fetch(subDao); + expect(subDaoAcc.dcOnboardingFeesPaid.toNumber()).to.be.eq(1000000); + }); + }) }) it("issues a mobile hotspot", async () => { diff --git a/yarn.lock b/yarn.lock index 14b5ad1a5..9017c01f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -621,7 +621,7 @@ __metadata: languageName: node linkType: hard -"@helium/anchor-resolvers@^0.6.41, @helium/anchor-resolvers@^0.6.42, @helium/anchor-resolvers@workspace:packages/anchor-resolvers": +"@helium/anchor-resolvers@^0.6.42, @helium/anchor-resolvers@workspace:packages/anchor-resolvers": version: 0.0.0-use.local resolution: "@helium/anchor-resolvers@workspace:packages/anchor-resolvers" dependencies: @@ -675,13 +675,13 @@ __metadata: languageName: unknown linkType: soft -"@helium/conversion-escrow-sdk@workspace:packages/conversion-escrow-sdk": +"@helium/conversion-escrow-sdk@^0.6.42, @helium/conversion-escrow-sdk@workspace:packages/conversion-escrow-sdk": version: 0.0.0-use.local resolution: "@helium/conversion-escrow-sdk@workspace:packages/conversion-escrow-sdk" dependencies: "@coral-xyz/anchor": ^0.28.0 - "@helium/anchor-resolvers": ^0.6.41 - "@helium/idls": ^0.6.41 + "@helium/anchor-resolvers": ^0.6.42 + "@helium/idls": ^0.6.42 bn.js: ^5.2.0 bs58: ^4.0.1 git-format-staged: ^2.1.3 @@ -968,6 +968,7 @@ __metadata: "@coral-xyz/anchor": ^0.28.0 "@helium/address": ^4.10.2 "@helium/anchor-resolvers": ^0.6.42 + "@helium/conversion-escrow-sdk": ^0.6.42 "@helium/helium-sub-daos-sdk": ^0.6.42 "@helium/idls": ^0.6.42 "@helium/no-emit-sdk": ^0.6.42 @@ -1076,7 +1077,7 @@ __metadata: languageName: unknown linkType: soft -"@helium/idls@^0.6.41, @helium/idls@^0.6.42, @helium/idls@workspace:packages/idls": +"@helium/idls@^0.6.42, @helium/idls@workspace:packages/idls": version: 0.0.0-use.local resolution: "@helium/idls@workspace:packages/idls" dependencies: From c3e45beb2485317be8f631c6a554493cfc21be3a Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Tue, 9 Apr 2024 17:07:02 -0500 Subject: [PATCH 08/12] Add ability to pay 0.02 sol via verify owner --- Anchor.toml | 4 + .../src/resolvers.ts | 11 + packages/spl-utils/src/constants.ts | 4 + programs/helium-entity-manager/src/error.rs | 2 + .../src/instructions/maker_lend_v0.rs | 8 +- .../mobile_voucher_pay_dc_v0.rs | 1 + .../mobile_voucher_pay_mobile_v0.rs | 1 + .../mobile_voucher_verify_owner_v0.rs | 37 +- tests/helium-entity-manager.ts | 443 +++++++++++------- 9 files changed, 327 insertions(+), 184 deletions(-) diff --git a/Anchor.toml b/Anchor.toml index 4ac08d00c..685cc2cbe 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -85,6 +85,10 @@ address = "JBaTytFv1CmGNkyNiLu16jFMXNZ49BGfy4bYAYZdkxg5" # Mobile price oracle [[test.validator.clone]] address = "7moA1i5vQUpfDwSpK6Pw9s56ahB7WFGidtbL2ujWrVvm" +# Sol pyth price +[[test.validator.clone]] +address = "H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4AQJEG" + # Pyth usdc price oracle [[test.validator.clone]] address = "Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD" diff --git a/packages/helium-entity-manager-sdk/src/resolvers.ts b/packages/helium-entity-manager-sdk/src/resolvers.ts index 3448d20b5..295a91aac 100644 --- a/packages/helium-entity-manager-sdk/src/resolvers.ts +++ b/packages/helium-entity-manager-sdk/src/resolvers.ts @@ -7,6 +7,7 @@ import { } from "@helium/anchor-resolvers"; import { getAssociatedTokenAddressSync, + NATIVE_MINT, } from "@solana/spl-token"; import { PublicKey } from "@solana/web3.js"; import { init } from "./init"; @@ -229,6 +230,16 @@ export const heliumEntityManagerResolvers = combineResolvers( account: "burner", owner: "maker", }), + resolveIndividual(async ({ path, accounts }) => { + if ((path[path.length - 1] === "makerWsol") && accounts.maker) { + return getAssociatedTokenAddressSync( + NATIVE_MINT, + accounts.maker as PublicKey, + true + ); + } + + }), subDaoEpochInfoResolver ); diff --git a/packages/spl-utils/src/constants.ts b/packages/spl-utils/src/constants.ts index 8072f98e6..206b7a69a 100644 --- a/packages/spl-utils/src/constants.ts +++ b/packages/spl-utils/src/constants.ts @@ -14,6 +14,10 @@ export const HNT_PYTH_PRICE_FEED = new PublicKey( "7moA1i5vQUpfDwSpK6Pw9s56ahB7WFGidtbL2ujWrVvm" ); +export const SOL_PYTH_PRICE_FEED = new PublicKey( + "H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4AQJEG" +); + export const MOBILE_PYTH_PRICE_FEED = new PublicKey( "JBaTytFv1CmGNkyNiLu16jFMXNZ49BGfy4bYAYZdkxg5" ); diff --git a/programs/helium-entity-manager/src/error.rs b/programs/helium-entity-manager/src/error.rs index 59fad2f5a..46f089b02 100644 --- a/programs/helium-entity-manager/src/error.rs +++ b/programs/helium-entity-manager/src/error.rs @@ -65,4 +65,6 @@ pub enum ErrorCode { #[msg("Voucher was not paid for")] VoucherNotPaid, + #[msg("Cannot do more than one maker lend at a time")] + LoanInProgress, } diff --git a/programs/helium-entity-manager/src/instructions/maker_lend_v0.rs b/programs/helium-entity-manager/src/instructions/maker_lend_v0.rs index 3679247ac..9bf1d0f25 100644 --- a/programs/helium-entity-manager/src/instructions/maker_lend_v0.rs +++ b/programs/helium-entity-manager/src/instructions/maker_lend_v0.rs @@ -25,7 +25,11 @@ pub struct MakerLendArgsV0 { #[derive(Accounts)] pub struct MakerLendV0<'info> { - #[account(mut)] + #[account( + mut, + // Ensure a loan isn't already in progress + constraint = maker.expected_onboard_amount == 0 @ ErrorCode::LoanInProgress, + )] pub maker: Box>, #[account( constraint = TESTING || usdc_mint.key() == Pubkey::from_str("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v").unwrap() @@ -98,7 +102,9 @@ pub fn handler(ctx: Context, args: MakerLendArgsV0) -> Result<()> { let valid_instructions: Vec<[u8; 8]> = vec![ get_function_hash("global", "mobile_voucher_pay_mobile_v0"), get_function_hash("global", "mobile_voucher_pay_dc_v0"), + get_function_hash("global", "mobile_voucher_verify_owner_v0"), ]; + loop { // get the next instruction, die if theres no more if let Ok(ix) = load_instruction_at_checked(index, &ixs) { diff --git a/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_pay_dc_v0.rs b/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_pay_dc_v0.rs index 8c0d88282..ae1833a4d 100644 --- a/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_pay_dc_v0.rs +++ b/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_pay_dc_v0.rs @@ -178,6 +178,7 @@ pub fn handler(ctx: Context) -> Result<()> { )?; } + ctx.accounts.maker.expected_onboard_amount = 0; ctx.accounts.mobile_hotspot_voucher.paid_dc = true; Ok(()) diff --git a/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_pay_mobile_v0.rs b/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_pay_mobile_v0.rs index 7a5bf4279..6791e0e57 100644 --- a/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_pay_mobile_v0.rs +++ b/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_pay_mobile_v0.rs @@ -110,6 +110,7 @@ pub fn handler(ctx: Context) -> Result<()> { )?; } + ctx.accounts.maker.expected_onboard_amount = 0; ctx.accounts.mobile_hotspot_voucher.paid_mobile = true; Ok(()) diff --git a/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_verify_owner_v0.rs b/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_verify_owner_v0.rs index 4cd658e71..effe68093 100644 --- a/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_verify_owner_v0.rs +++ b/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_verify_owner_v0.rs @@ -1,19 +1,52 @@ +/// Verify the owner and top them up with SOL if need be. use std::str::FromStr; -use crate::{state::*, ECC_VERIFIER}; +use crate::{error::ErrorCode, maker_seeds, state::*, ECC_VERIFIER}; use anchor_lang::prelude::*; +use anchor_spl::token::{close_account, CloseAccount, Token}; + +// 0.02 SOL minus the rent +const MIN_WALLET_LAMPORTS: u64 = 20_000_000; #[derive(Accounts)] pub struct MobileVoucherVerifyOwnerV0<'info> { + #[account(mut)] + pub maker: Box>, #[account(address = Pubkey::from_str(ECC_VERIFIER).unwrap())] pub ecc_verifier: Signer<'info>, /// CHECK: Ecc verifier is what's telling us this is the owner - pub verified_owner: UncheckedAccount<'info>, #[account(mut)] + pub verified_owner: UncheckedAccount<'info>, + #[account( + mut, + has_one = maker, + // Make sure this hasn't been verified yet + constraint = mobile_hotspot_voucher.verified_owner == maker.issuing_authority, + )] pub mobile_hotspot_voucher: Box>, + /// CHECK: Maker's wrapped sol account + pub maker_wsol: UncheckedAccount<'info>, + pub token_program: Program<'info, Token>, } pub fn handler(ctx: Context) -> Result<()> { ctx.accounts.mobile_hotspot_voucher.verified_owner = ctx.accounts.verified_owner.key(); + require_gte!( + MIN_WALLET_LAMPORTS, + ctx.accounts.maker.expected_onboard_amount, + ErrorCode::TooMuchBorrowed + ); + if ctx.accounts.verified_owner.lamports() < MIN_WALLET_LAMPORTS { + close_account(CpiContext::new_with_signer( + ctx.accounts.token_program.to_account_info(), + CloseAccount { + account: ctx.accounts.maker_wsol.to_account_info(), + destination: ctx.accounts.verified_owner.to_account_info(), + authority: ctx.accounts.maker.to_account_info(), + }, + &[maker_seeds!(ctx.accounts.maker)], + ))?; + } + ctx.accounts.maker.expected_onboard_amount = 0; Ok(()) } diff --git a/tests/helium-entity-manager.ts b/tests/helium-entity-manager.ts index 359e23ae3..105581ac1 100644 --- a/tests/helium-entity-manager.ts +++ b/tests/helium-entity-manager.ts @@ -11,6 +11,7 @@ import { HELIUM_COMMON_LUT, HNT_PYTH_PRICE_FEED, MOBILE_PYTH_PRICE_FEED, + SOL_PYTH_PRICE_FEED, USDC_PYTH_PRICE_FEED, createAtaAndMint, createMint, @@ -26,6 +27,7 @@ import { createTransferInstruction, getAssociatedTokenAddressSync, getAccount, + NATIVE_MINT, } from "@solana/spl-token"; import { ComputeBudgetProgram, @@ -716,6 +718,11 @@ describe("helium-entity-manager", () => { slippageBps, oracle: HNT_PYTH_PRICE_FEED, }, + { + mint: NATIVE_MINT, + slippageBps, + oracle: SOL_PYTH_PRICE_FEED, + } ], }) .accounts({ @@ -750,84 +757,13 @@ describe("helium-entity-manager", () => { }) .signers([makerKeypair]) .rpcAndKeys({ skipPreflight: true })); - await hemProgram.methods - .mobileVoucherVerifyOwnerV0() - .accounts({ - verifiedOwner: me, - eccVerifier: eccVerifier.publicKey, - mobileHotspotVoucher, - }) - .signers([eccVerifier]) - .rpc({ skipPreflight: true }); + }); - it("allows paying mobile", async () => { - const makerDntAccount = getAssociatedTokenAddressSync(dntMint, maker, true); - const pythDataMobile = (await provider.connection.getAccountInfo( - MOBILE_PYTH_PRICE_FEED - ))!.data; - const priceMobile = parsePriceData(pythDataMobile); - - const usdPerMobile = - priceMobile.emaPrice.value - priceMobile.emaConfidence!.value * 2; - const mobileAmount = toBN(20 / usdPerMobile, 6); - const myUsdcAccount = getAssociatedTokenAddressSync(usdcMint, me); - const instructions = [ - createAssociatedTokenAccountIdempotentInstruction( - me, - myUsdcAccount, - me, - usdcMint - ), - await hemProgram.methods - .makerLendV0({ - amount: toBN(20, 6), - }) - .accounts({ - maker, - targetOracle: MOBILE_PYTH_PRICE_FEED, - conversionEscrow, - destination: myUsdcAccount, - repayAccount: makerDntAccount, - usdcMint, - }) - .instruction(), - // Repay - createAssociatedTokenAccountIdempotentInstruction( - me, - makerDntAccount, - maker, - dntMint - ), - createTransferInstruction( - getAssociatedTokenAddressSync(dntMint, me), - makerDntAccount, - me, - BigInt(mobileAmount.toString()) - ), - await conversionEscrowProgram.methods - .checkRepayV0() - .accounts({ - conversionEscrow, - repayAccount: makerDntAccount, - }) - .instruction(), - await hemProgram.methods - .mobileVoucherPayMobileV0() - .accounts({ - mobileHotspotVoucher, - dntPrice: MOBILE_PYTH_PRICE_FEED, - }) - .instruction(), - ]; - await sendInstructions(provider, instructions) - const voucher = await hemProgram.account.mobileHotspotVoucherV0.fetch(mobileHotspotVoucher!); - expect(voucher.paidMobile).to.be.true; - }) - - it("allows paying DC", async () => { - const makerHntAccount = getAssociatedTokenAddressSync( - hntMint, + it("allows verifying owner while topping off sol", async () => { + const owner = Keypair.generate() + const makerWsol = getAssociatedTokenAddressSync( + NATIVE_MINT, maker, true ); @@ -841,113 +777,66 @@ describe("helium-entity-manager", () => { ), createAssociatedTokenAccountIdempotentInstruction( me, - makerHntAccount, + makerWsol, maker, - hntMint + NATIVE_MINT ), await hemProgram.methods .makerLendV0({ - amount: toBN(10, 6), + amount: toBN(0.02, 9), }) .accounts({ maker, - targetOracle: HNT_PYTH_PRICE_FEED, + targetOracle: SOL_PYTH_PRICE_FEED, conversionEscrow, destination: myUsdcAccount, - repayAccount: makerHntAccount, + repayAccount: makerWsol, usdcMint, }) .instruction(), - // Repay in HNT - createTransferInstruction( - getAssociatedTokenAddressSync(hntMint, me), - makerHntAccount, - me, - BigInt(10000000000) - ), + // Repay + SystemProgram.transfer({ + fromPubkey: me, + toPubkey: makerWsol, + lamports: 0.02 * LAMPORTS_PER_SOL, + }), await conversionEscrowProgram.methods .checkRepayV0() .accounts({ conversionEscrow, - repayAccount: makerHntAccount, + repayAccount: makerWsol, }) .instruction(), await hemProgram.methods - .mobileVoucherPayDcV0() + .mobileVoucherVerifyOwnerV0() .accounts({ + verifiedOwner: owner.publicKey, + eccVerifier: eccVerifier.publicKey, mobileHotspotVoucher, }) .instruction(), ]; - await sendInstructions(provider, instructions); + await sendInstructions(provider, instructions, [eccVerifier]); const voucher = await hemProgram.account.mobileHotspotVoucherV0.fetch( mobileHotspotVoucher! ); - expect(voucher.paidDc).to.be.true; + expect(voucher.verifiedOwner.toBase58()).to.eq(owner.publicKey.toBase58()); + const lamps = (await provider.connection.getAccountInfo(owner.publicKey))?.lamports + expect(lamps).to.be.gt(0.02 * LAMPORTS_PER_SOL) }); - - it("fits a jupiter transaction in with flash lend mobile", async () => { - const { instructions, addressLookupTableAddresses } = - await payMobileVoucherMobile({ - program: hemProgram, - ceProgram: conversionEscrowProgram, - mobileHotspotVoucher: mobileHotspotVoucher!, - verifiedOwner: me, - maker, - payer: me, - }); - const tx = await toVersionedTx({ - instructions: [ - await hemProgram.methods - .mobileVoucherVerifyOwnerV0() - .accounts({ - verifiedOwner: me, - eccVerifier: eccVerifier.publicKey, - mobileHotspotVoucher, - }) - .instruction(), - ...instructions, - ], - addressLookupTableAddresses: [ - ...addressLookupTableAddresses, - HELIUM_COMMON_LUT, - ], - connection: new Connection("https://api.mainnet-beta.solana.com"), - payer: me, - }); - await tx.sign([eccVerifier]); - const signed = await provider.wallet.signTransaction(tx) - const serialized = signed.serialize(); - expect(serialized.length).to.be.lessThan(1232); - }) - - it("fits a jupiter transaction in with flash lend dc", async () => { - const { instructions, addressLookupTableAddresses } = - await payMobileVoucherDc({ - program: hemProgram, - ceProgram: conversionEscrowProgram, - mobileHotspotVoucher: mobileHotspotVoucher!, - verifiedOwner: me, - maker, - payer: me, - }); - const tx = await toVersionedTx({ - instructions, - addressLookupTableAddresses: [ - ...addressLookupTableAddresses, - HELIUM_COMMON_LUT, - ], - connection: new Connection("https://api.mainnet-beta.solana.com"), - payer: me, - }); - const signed = await provider.wallet.signTransaction(tx); - const serialized = signed.serialize(); - expect(serialized.length).to.be.lessThan(1232); - }); - - describe("when fully paid", () => { - + describe("with verified owner", async () => { beforeEach(async () => { + await hemProgram.methods + .mobileVoucherVerifyOwnerV0() + .accounts({ + verifiedOwner: me, + eccVerifier: eccVerifier.publicKey, + mobileHotspotVoucher, + }) + .signers([eccVerifier]) + .rpc({ skipPreflight: true }); + }) + it("allows paying mobile", async () => { const makerDntAccount = getAssociatedTokenAddressSync( dntMint, maker, @@ -962,7 +851,7 @@ describe("helium-entity-manager", () => { priceMobile.emaPrice.value - priceMobile.emaConfidence!.value * 2; const mobileAmount = toBN(20 / usdPerMobile, 6); const myUsdcAccount = getAssociatedTokenAddressSync(usdcMint, me); - const instructions0 = [ + const instructions = [ createAssociatedTokenAccountIdempotentInstruction( me, myUsdcAccount, @@ -1010,12 +899,20 @@ describe("helium-entity-manager", () => { }) .instruction(), ]; - await sendInstructions(provider, instructions0); + await sendInstructions(provider, instructions); + const voucher = await hemProgram.account.mobileHotspotVoucherV0.fetch( + mobileHotspotVoucher! + ); + expect(voucher.paidMobile).to.be.true; + }); + + it("allows paying DC", async () => { const makerHntAccount = getAssociatedTokenAddressSync( hntMint, maker, true ); + const myUsdcAccount = getAssociatedTokenAddressSync(usdcMint, me); const instructions = [ createAssociatedTokenAccountIdempotentInstruction( me, @@ -1064,33 +961,217 @@ describe("helium-entity-manager", () => { .instruction(), ]; await sendInstructions(provider, instructions); - }) + const voucher = await hemProgram.account.mobileHotspotVoucherV0.fetch( + mobileHotspotVoucher! + ); + expect(voucher.paidDc).to.be.true; + }); - it("issues a mobile hotspot", async () => { - const method = await hemProgram.methods - .issueMobileHotspotV0({ - entityKey: Buffer.from(bs58.decode(ecc)), - }) - .accounts({ - issueEntityCommon: { - maker, - recipient: me, - dao, - }, - mobileInfo: mobileInfoKey( - rewardableEntityConfig, - Buffer.from(bs58.decode(ecc)) - )[0], - voucher: mobileHotspotVoucher, + it("fits a jupiter transaction in with flash lend mobile", async () => { + const { instructions, addressLookupTableAddresses } = + await payMobileVoucherMobile({ + program: hemProgram, + ceProgram: conversionEscrowProgram, + mobileHotspotVoucher: mobileHotspotVoucher!, + verifiedOwner: me, + maker, + payer: me, + }); + const tx = await toVersionedTx({ + instructions: [ + await hemProgram.methods + .mobileVoucherVerifyOwnerV0() + .accounts({ + verifiedOwner: me, + eccVerifier: eccVerifier.publicKey, + mobileHotspotVoucher, + }) + .instruction(), + ...instructions, + ], + addressLookupTableAddresses: [ + ...addressLookupTableAddresses, + HELIUM_COMMON_LUT, + ], + connection: new Connection("https://api.mainnet-beta.solana.com"), + payer: me, + }); + await tx.sign([eccVerifier]); + const signed = await provider.wallet.signTransaction(tx); + const serialized = signed.serialize(); + expect(serialized.length).to.be.lessThan(1232); + }); + + it("fits a jupiter transaction in with flash lend dc", async () => { + const { instructions, addressLookupTableAddresses } = + await payMobileVoucherDc({ + program: hemProgram, + ceProgram: conversionEscrowProgram, + mobileHotspotVoucher: mobileHotspotVoucher!, + verifiedOwner: me, + maker, + payer: me, }); - await method.rpc({ skipPreflight: true }); - const { mobileInfo } = await method.pubkeys(); - - const mobileInfoAcc = - await hemProgram.account.mobileHotspotInfoV0.fetch(mobileInfo!); - expect(Boolean(mobileInfoAcc)).to.be.true; - const subDaoAcc = await hsdProgram.account.subDaoV0.fetch(subDao); - expect(subDaoAcc.dcOnboardingFeesPaid.toNumber()).to.be.eq(1000000); + const tx = await toVersionedTx({ + instructions, + addressLookupTableAddresses: [ + ...addressLookupTableAddresses, + HELIUM_COMMON_LUT, + ], + connection: new Connection("https://api.mainnet-beta.solana.com"), + payer: me, + }); + const signed = await provider.wallet.signTransaction(tx); + const serialized = signed.serialize(); + expect(serialized.length).to.be.lessThan(1232); + }); + + describe("when fully paid", () => { + beforeEach(async () => { + const makerDntAccount = getAssociatedTokenAddressSync( + dntMint, + maker, + true + ); + const pythDataMobile = (await provider.connection.getAccountInfo( + MOBILE_PYTH_PRICE_FEED + ))!.data; + const priceMobile = parsePriceData(pythDataMobile); + + const usdPerMobile = + priceMobile.emaPrice.value - priceMobile.emaConfidence!.value * 2; + const mobileAmount = toBN(20 / usdPerMobile, 6); + const myUsdcAccount = getAssociatedTokenAddressSync(usdcMint, me); + const instructions0 = [ + createAssociatedTokenAccountIdempotentInstruction( + me, + myUsdcAccount, + me, + usdcMint + ), + await hemProgram.methods + .makerLendV0({ + amount: toBN(20, 6), + }) + .accounts({ + maker, + targetOracle: MOBILE_PYTH_PRICE_FEED, + conversionEscrow, + destination: myUsdcAccount, + repayAccount: makerDntAccount, + usdcMint, + }) + .instruction(), + // Repay + createAssociatedTokenAccountIdempotentInstruction( + me, + makerDntAccount, + maker, + dntMint + ), + createTransferInstruction( + getAssociatedTokenAddressSync(dntMint, me), + makerDntAccount, + me, + BigInt(mobileAmount.toString()) + ), + await conversionEscrowProgram.methods + .checkRepayV0() + .accounts({ + conversionEscrow, + repayAccount: makerDntAccount, + }) + .instruction(), + await hemProgram.methods + .mobileVoucherPayMobileV0() + .accounts({ + mobileHotspotVoucher, + dntPrice: MOBILE_PYTH_PRICE_FEED, + }) + .instruction(), + ]; + await sendInstructions(provider, instructions0); + const makerHntAccount = getAssociatedTokenAddressSync( + hntMint, + maker, + true + ); + const instructions = [ + createAssociatedTokenAccountIdempotentInstruction( + me, + myUsdcAccount, + me, + usdcMint + ), + createAssociatedTokenAccountIdempotentInstruction( + me, + makerHntAccount, + maker, + hntMint + ), + await hemProgram.methods + .makerLendV0({ + amount: toBN(10, 6), + }) + .accounts({ + maker, + targetOracle: HNT_PYTH_PRICE_FEED, + conversionEscrow, + destination: myUsdcAccount, + repayAccount: makerHntAccount, + usdcMint, + }) + .instruction(), + // Repay in HNT + createTransferInstruction( + getAssociatedTokenAddressSync(hntMint, me), + makerHntAccount, + me, + BigInt(10000000000) + ), + await conversionEscrowProgram.methods + .checkRepayV0() + .accounts({ + conversionEscrow, + repayAccount: makerHntAccount, + }) + .instruction(), + await hemProgram.methods + .mobileVoucherPayDcV0() + .accounts({ + mobileHotspotVoucher, + }) + .instruction(), + ]; + await sendInstructions(provider, instructions); + }); + + it("issues a mobile hotspot", async () => { + const method = await hemProgram.methods + .issueMobileHotspotV0({ + entityKey: Buffer.from(bs58.decode(ecc)), + }) + .accounts({ + issueEntityCommon: { + maker, + recipient: me, + dao, + }, + mobileInfo: mobileInfoKey( + rewardableEntityConfig, + Buffer.from(bs58.decode(ecc)) + )[0], + voucher: mobileHotspotVoucher, + }); + await method.rpc({ skipPreflight: true }); + const { mobileInfo } = await method.pubkeys(); + + const mobileInfoAcc = + await hemProgram.account.mobileHotspotInfoV0.fetch(mobileInfo!); + expect(Boolean(mobileInfoAcc)).to.be.true; + const subDaoAcc = await hsdProgram.account.subDaoV0.fetch(subDao); + expect(subDaoAcc.dcOnboardingFeesPaid.toNumber()).to.be.eq(1000000); + }); }); }) }) From c6235d8224d88f6b4cd804efafcb01c749d68c66 Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Wed, 10 Apr 2024 06:19:58 -0500 Subject: [PATCH 09/12] Add forceRequery option --- packages/distributor-oracle/src/client.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/distributor-oracle/src/client.ts b/packages/distributor-oracle/src/client.ts index aba04cc15..1beed4d4f 100644 --- a/packages/distributor-oracle/src/client.ts +++ b/packages/distributor-oracle/src/client.ts @@ -105,7 +105,8 @@ export async function getPendingRewards( lazyDistributor: PublicKey, dao: PublicKey, entityKeys: string[], - encoding: BufferEncoding | "b58" = "b58" + encoding: BufferEncoding | "b58" = "b58", + forceRequery = false ): Promise> { const oracleRewards = await getBulkRewards( program, @@ -129,7 +130,7 @@ export async function getPendingRewards( >("KeyToAssetV0", account.data), }), true, - false + forceRequery ); keyToAssets.forEach((kta, index) => { if (!kta?.info) { @@ -149,7 +150,9 @@ export async function getPendingRewards( info: program.coder.accounts.decode< IdlAccounts["recipientV0"] >("RecipientV0", account.data), - }) + }), + false, + forceRequery ); const withRecipients = recipients.map((recipient, index) => { return { From 30ff790901e67bd1c9193653ce4a9cc4ce17a262 Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Mon, 4 Nov 2024 10:53:27 -0800 Subject: [PATCH 10/12] More fixes --- Anchor.toml | 4 + packages/distributor-oracle/src/server.ts | 6 +- .../helium-admin-cli/src/create-common-lut.ts | 76 +------------- .../src/resolvers.ts | 9 -- packages/spl-utils/src/constants.ts | 98 ++++++++++++++++++- .../src/instructions/lend_v0.rs | 1 + .../src/instructions/maker_lend_v0.rs | 1 + .../mobile_voucher_verify_owner_v0.rs | 27 ++--- tests/conversion-escrow.ts | 3 +- tests/helium-entity-manager.ts | 76 +++++++------- 10 files changed, 167 insertions(+), 134 deletions(-) diff --git a/Anchor.toml b/Anchor.toml index b6dad45d3..7de0a709d 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -105,6 +105,10 @@ address = "DQ4C1tzvu28cwo1roN1Wm6TW35sfJEjLh517k3ZeWevx" # Mobile price oracle [[test.validator.clone]] address = "4DdmDswskDxXGpwHrXUfn2CNUm9rt21ac79GHNTN3J33" +# Helium Common LUT +[[test.validator.clone]] +address = "43eY9L2spbM2b1MPDFFBStUiFGt29ziZ1nc1xbpzsfVt" + # Sol pyth price [[test.validator.clone]] address = "7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE" diff --git a/packages/distributor-oracle/src/server.ts b/packages/distributor-oracle/src/server.ts index 00afc1285..067cac02d 100644 --- a/packages/distributor-oracle/src/server.ts +++ b/packages/distributor-oracle/src/server.ts @@ -384,15 +384,17 @@ export class OracleServer { if (!(programId.equals(LD_PID) || programId.equals(RO_PID))) { return { success: false, - message: "Invalid instructions in transaction", + message: `Invalid instructions in transaction: ${programId.toBase58()}`, }; } const data = Buffer.from(ix.data); let decoded: Instruction | null; if (programId.equals(LD_PID)) { + console.log("decoding LD", data) decoded = ( this.ldProgram.coder.instruction as BorshInstructionCoder ).decode(data); + console.log("decoded LD", decoded) } else { decoded = ( this.roProgram.coder.instruction as BorshInstructionCoder @@ -411,7 +413,7 @@ export class OracleServer { ) { return { success: false, - message: "Invalid instructions in transaction", + message: `Invalid instructions in transaction: name: ${decoded} ${programId.toBase58()}`, }; } diff --git a/packages/helium-admin-cli/src/create-common-lut.ts b/packages/helium-admin-cli/src/create-common-lut.ts index b0d729901..abba6b0f9 100644 --- a/packages/helium-admin-cli/src/create-common-lut.ts +++ b/packages/helium-admin-cli/src/create-common-lut.ts @@ -1,5 +1,5 @@ import * as anchor from "@coral-xyz/anchor"; -import { chunks, sendInstructions, truthy, withPriorityFees } from "@helium/spl-utils"; +import { chunks, LUT_ACCOUNTS, sendInstructions, truthy, withPriorityFees } from "@helium/spl-utils"; import { AddressLookupTableProgram, PublicKey, @@ -48,79 +48,7 @@ export async function run(args: any = process.argv) { authority = squads.getAuthorityPDA(multisig, argv.authorityIndex); } - const accounts = [ - // Programs - "1azyuavdMyvsivtNxPoz6SucD18eDHeXzFCUPq5XU7w", - "hdaoVTCqhfHHo75XdAMxBKdUqvq1i5bF23sisBqVgGR", - "credMBJhYFzfn7NxBMdU4aUqFggAjgztaCcv2Fo6fPT", - "hemjuPXBpNvggtaUnN1MwT3wrdhttKEfosTcc2P9Pg8", - "circAbx64bbsscPbQzZAUvuXpHqrCe6fLMzc2uKXz9g", - "treaf4wWBBty3fHdyBpo35Mz84M8k3heKXmjmi9vFt5", - "1atrmQs3eq1N2FEYWu6tyTXbCjP4uQwExpjtnhXtS8h", - "porcSnvH9pvcYPmQ65Y8qcZSRxQBiBBQX7UV5nmBegy", - "rorcfdX4h9m9swCKgcypaHJ8NGYVANBpmV9EHn3cYrF", - "hvsrNC3NKbcryqDs2DocYHZ9yPKEVzdSjQG6RVtK1s8", - "fanqeMu3fw8R4LwKNbahPtYXJsyLL6NXyfe2BqzhfB6", - "memMa1HG4odAFmUbGWfPwS1WWfK95k99F2YTkGvyxZr", - "hexbnKYoA2GercNNhHUCCfrTRWrHjT6ujKPXTa5NPqJ", - "noEmmgLmQdk6DLiPV8CSwQv3qQDyGEhz9m5A4zhtByv", - "cnvEguKeWyyWnKxoQ9HwrzEVfztqKjwNmerDvxdHK9w", - "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", - "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", - "11111111111111111111111111111111", - // Lazy distributor IOT - "37eiz5KzYwpAdLgrSh8GT1isKiJ6hcE5ET86dqaoCugL", - // Lazy dist Oracle - "orc1TYY5L4B4ZWDEMayTqu99ikPM9bQo9fqzoaCPP5Q", - // Oracle signer - "7WuVr8SGcZ4KxpHBEdRGVTeSwkhk1WGUXT7DEzvWpYu4", - // Lazy dist mobile - "GZtTp3AUo2AHdQe9BCJ6gXR9KqfruRvHnZ4QiJUALMcz", - // Hnt pyth - "4DdmDswskDxXGpwHrXUfn2CNUm9rt21ac79GHNTN3J33", - // Mobile pyth - "DQ4C1tzvu28cwo1roN1Wm6TW35sfJEjLh517k3ZeWevx", - // Usdc pyth - "Dpw1EAVrSB1ibxiDQyTAW6Zip3J4Btk2x4SgApQCeFbX", - // Hnt - "hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux", - // dc - "dcuc8Amr83Wz27ZkQ2K9NS6r8zRpf1J6cvArEBDZDmm", - // Mobile - "mb1eu7TzEc71KxDpsmsKoucSSuuoGLv1drys1oP2jh6", - // Dao - "BQ3MCuTT5zVBhNfQ4SjMh3NPVhFy73MPV8rjfq5d1zie", - // Mobile subdao - "Gm9xDCJawDEKDrrQW6haw94gABaYzQwCq4ZQU8h8bd22", - // Iot subdao - "39Lw1RH6zt8AJvKn3BTxmUDofzduCM2J3kSaGDZ8L7Sk", - // Mobile Delegator pool - "71Y96vbVWYkoVQUgVsd8LSBRRDrgp5pf1sKznM5KuaA7", - // Mobile Delegator pool circuit breaker - "2cocTPZ7aRT62wTDGkosF98oo4iqCtkZnFdNHWqNZLuS", - // Iot delegator pool - "6fvj6rSwTeCkY7i45jYZYpZEhKmPRSTmA29hUDiMSFtU", - // Iot delegator pool circuit breaker - "6mNUqFAyLBkV88Nj6ctrcv66iJMHwzNzV8XFUwLmGerG", - // Mobile rewardable entity config - "EP1FNydbuKjGopwXVrVLR71NnC9YMRbTg9aHfk3KfMRj", - // Compression proram - "cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK", - // Mobile escrow circuit breaker - "4qGj88CX3McdTXEviEaqeP2pnZJxRTsZFWyU3Mrnbku4", - // Mobile escrow - "GD37bnQdGkDsjNqnVGr9qWTnQJSKMHbsiXX9tXLMUcaL", - // Iot escrow - "4po3YMfioHkNP4mL4N46UWJvBoQDS2HFjzGm1ifrUWuZ", - // Iot escrow circuit breaker - "5veMSa4ks66zydSaKSPMhV7H2eF88HvuKDArScNH9jaG", - // Hnt registrar - "BMnWRWZrWqb6JMKznaDqNxWaWAHoaTzVabM6Qwyh3WKz", - // Data credits - "D1LbvrJQ9K2WbGPMbM3Fnrf5PSsDH1TDpjqJdHuvs81n", - ].map((a) => { - return new PublicKey(a); - }); + const accounts = LUT_ACCOUNTS; const slot = await provider.connection.getSlot(); const [lookupTableInst, lookupTableAddress] = diff --git a/packages/helium-entity-manager-sdk/src/resolvers.ts b/packages/helium-entity-manager-sdk/src/resolvers.ts index 072e62f61..1de1c86fa 100644 --- a/packages/helium-entity-manager-sdk/src/resolvers.ts +++ b/packages/helium-entity-manager-sdk/src/resolvers.ts @@ -224,15 +224,6 @@ export const heliumEntityManagerResolvers = combineResolvers( account: "burner", owner: "maker", }), - resolveIndividual(async ({ path, accounts }) => { - if ((path[path.length - 1] === "makerWsol") && accounts.maker) { - return getAssociatedTokenAddressSync( - NATIVE_MINT, - accounts.maker as PublicKey, - true - ); - } - }), ataResolver({ instruction: "onboardDataOnlyMobileHotspotV0", mint: "dntMint", diff --git a/packages/spl-utils/src/constants.ts b/packages/spl-utils/src/constants.ts index 3dd0181f2..4114e3065 100644 --- a/packages/spl-utils/src/constants.ts +++ b/packages/spl-utils/src/constants.ts @@ -35,7 +35,7 @@ export const HELIUM_COMMON_LUT_DEVNET = new PublicKey( ); export const HELIUM_COMMON_LUT = new PublicKey( - "4DdmDswskDxXGpwHrXUfn2CNUm9rt21ac79GHNTN3J33" + "43eY9L2spbM2b1MPDFFBStUiFGt29ziZ1nc1xbpzsfVt" ); export type Network = "hnt" | "mobile" | "iot"; @@ -49,4 +49,98 @@ export const realmNames: Record = { [HNT_MINT.toBase58()]: "Helium", [MOBILE_MINT.toBase58()]: "Helium MOBILE", [IOT_MINT.toBase58()]: "Helium IOT", -}; \ No newline at end of file +}; + +export const LUT_ACCOUNTS = [ + // Programs + "1azyuavdMyvsivtNxPoz6SucD18eDHeXzFCUPq5XU7w", + "hdaoVTCqhfHHo75XdAMxBKdUqvq1i5bF23sisBqVgGR", + "credMBJhYFzfn7NxBMdU4aUqFggAjgztaCcv2Fo6fPT", + "hemjuPXBpNvggtaUnN1MwT3wrdhttKEfosTcc2P9Pg8", + "circAbx64bbsscPbQzZAUvuXpHqrCe6fLMzc2uKXz9g", + "treaf4wWBBty3fHdyBpo35Mz84M8k3heKXmjmi9vFt5", + "1atrmQs3eq1N2FEYWu6tyTXbCjP4uQwExpjtnhXtS8h", + "porcSnvH9pvcYPmQ65Y8qcZSRxQBiBBQX7UV5nmBegy", + "rorcfdX4h9m9swCKgcypaHJ8NGYVANBpmV9EHn3cYrF", + "hvsrNC3NKbcryqDs2DocYHZ9yPKEVzdSjQG6RVtK1s8", + "fanqeMu3fw8R4LwKNbahPtYXJsyLL6NXyfe2BqzhfB6", + "memMa1HG4odAFmUbGWfPwS1WWfK95k99F2YTkGvyxZr", + "hexbnKYoA2GercNNhHUCCfrTRWrHjT6ujKPXTa5NPqJ", + "noEmmgLmQdk6DLiPV8CSwQv3qQDyGEhz9m5A4zhtByv", + "cnvEguKeWyyWnKxoQ9HwrzEVfztqKjwNmerDvxdHK9w", + "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "11111111111111111111111111111111", + // Lazy distributor IOT + "37eiz5KzYwpAdLgrSh8GT1isKiJ6hcE5ET86dqaoCugL", + // Lazy dist Oracle + "orc1TYY5L4B4ZWDEMayTqu99ikPM9bQo9fqzoaCPP5Q", + // Oracle signer + "7WuVr8SGcZ4KxpHBEdRGVTeSwkhk1WGUXT7DEzvWpYu4", + // Lazy dist mobile + "GZtTp3AUo2AHdQe9BCJ6gXR9KqfruRvHnZ4QiJUALMcz", + // Hnt pyth + "4DdmDswskDxXGpwHrXUfn2CNUm9rt21ac79GHNTN3J33", + // Mobile pyth + "DQ4C1tzvu28cwo1roN1Wm6TW35sfJEjLh517k3ZeWevx", + // Usdc pyth + "Dpw1EAVrSB1ibxiDQyTAW6Zip3J4Btk2x4SgApQCeFbX", + // Hnt + "hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux", + // dc + "dcuc8Amr83Wz27ZkQ2K9NS6r8zRpf1J6cvArEBDZDmm", + // Mobile + "mb1eu7TzEc71KxDpsmsKoucSSuuoGLv1drys1oP2jh6", + // Dao + "BQ3MCuTT5zVBhNfQ4SjMh3NPVhFy73MPV8rjfq5d1zie", + // Mobile subdao + "Gm9xDCJawDEKDrrQW6haw94gABaYzQwCq4ZQU8h8bd22", + // Iot subdao + "39Lw1RH6zt8AJvKn3BTxmUDofzduCM2J3kSaGDZ8L7Sk", + // Mobile Delegator pool + "71Y96vbVWYkoVQUgVsd8LSBRRDrgp5pf1sKznM5KuaA7", + // Mobile Delegator pool circuit breaker + "2cocTPZ7aRT62wTDGkosF98oo4iqCtkZnFdNHWqNZLuS", + // Iot delegator pool + "6fvj6rSwTeCkY7i45jYZYpZEhKmPRSTmA29hUDiMSFtU", + // Iot delegator pool circuit breaker + "6mNUqFAyLBkV88Nj6ctrcv66iJMHwzNzV8XFUwLmGerG", + // Mobile rewardable entity config + "EP1FNydbuKjGopwXVrVLR71NnC9YMRbTg9aHfk3KfMRj", + // Compression proram + "cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK", + // Mobile escrow circuit breaker + "4qGj88CX3McdTXEviEaqeP2pnZJxRTsZFWyU3Mrnbku4", + // Mobile escrow + "GD37bnQdGkDsjNqnVGr9qWTnQJSKMHbsiXX9tXLMUcaL", + // Iot escrow + "4po3YMfioHkNP4mL4N46UWJvBoQDS2HFjzGm1ifrUWuZ", + // Iot escrow circuit breaker + "5veMSa4ks66zydSaKSPMhV7H2eF88HvuKDArScNH9jaG", + // Hnt registrar + "BMnWRWZrWqb6JMKznaDqNxWaWAHoaTzVabM6Qwyh3WKz", + // Data credits + "D1LbvrJQ9K2WbGPMbM3Fnrf5PSsDH1TDpjqJdHuvs81n", + // HNT Proposal Config + "22SWTDZVj1L81SXfwbEeUmdZBFj23MFmER3Gv8BmxbBS", + // HNT state controller + "7Vrme34DXPH8ow4HEAatZKwZF9AR5vq8MZhA3CanMEbr", + // IOT proposal config + "7cvYwyj6k4NEPNoaCTUufDdGJqqB6ZBRf4t3TrSSUGrc", + // IOT State controller + "3eEnmZBiJems6ipPtdQS2UkJYfPqzvnDzhWQuTTN2ou5", + // IOT Registrar + "7ZZopN1mx6ECcb3YCG8dbxeLpA44xq4gzA1ETEiaLoeL", + // State controller program + "stcfiqW3fwD9QCd8Bqr1NBLrs7dftZHBQe7RiMMA4aM", + // Mobile proposal config + "5c9JxRCj4CwhZwaUyjvpb4JJbKW7xpvEFq3Rb2upkytc", + // Mobile registrar + "C4DWaps9bLiqy4e81wJ7VTQ6QR7C4MWvwsei3ZjsaDuW", + // Mobile state controller + "r11HAkEaPqkFHwDVewcmWSfRrMaLYcBLGquC2RBn3Xp", + // Instructions sysvar + "Sysvar1nstructions1111111111111111111111111", +].map((a) => { + return new PublicKey(a); +}); diff --git a/programs/conversion-escrow/src/instructions/lend_v0.rs b/programs/conversion-escrow/src/instructions/lend_v0.rs index 9e450eba5..0c08c7da7 100644 --- a/programs/conversion-escrow/src/instructions/lend_v0.rs +++ b/programs/conversion-escrow/src/instructions/lend_v0.rs @@ -22,6 +22,7 @@ pub struct LendArgsV0 { #[derive(Accounts)] pub struct LendV0<'info> { #[account( + mut, has_one = mint, has_one = oracle, has_one = escrow, diff --git a/programs/helium-entity-manager/src/instructions/maker_lend_v0.rs b/programs/helium-entity-manager/src/instructions/maker_lend_v0.rs index 4c15c007c..cfbded3ff 100644 --- a/programs/helium-entity-manager/src/instructions/maker_lend_v0.rs +++ b/programs/helium-entity-manager/src/instructions/maker_lend_v0.rs @@ -49,6 +49,7 @@ pub struct MakerLendV0<'info> { #[account(mut)] pub repay_account: AccountInfo<'info>, #[account( + mut, seeds = [b"conversion_escrow", usdc_mint.key().as_ref(), maker.key().as_ref()], seeds::program = conversion_escrow_program, bump = conversion_escrow.bump_seed, diff --git a/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_verify_owner_v0.rs b/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_verify_owner_v0.rs index effe68093..44032dc51 100644 --- a/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_verify_owner_v0.rs +++ b/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_verify_owner_v0.rs @@ -1,9 +1,10 @@ /// Verify the owner and top them up with SOL if need be. use std::str::FromStr; -use crate::{error::ErrorCode, maker_seeds, state::*, ECC_VERIFIER}; use anchor_lang::prelude::*; -use anchor_spl::token::{close_account, CloseAccount, Token}; +use anchor_spl::token::Token; + +use crate::{error::ErrorCode, state::*, ECC_VERIFIER}; // 0.02 SOL minus the rent const MIN_WALLET_LAMPORTS: u64 = 20_000_000; @@ -24,8 +25,6 @@ pub struct MobileVoucherVerifyOwnerV0<'info> { constraint = mobile_hotspot_voucher.verified_owner == maker.issuing_authority, )] pub mobile_hotspot_voucher: Box>, - /// CHECK: Maker's wrapped sol account - pub maker_wsol: UncheckedAccount<'info>, pub token_program: Program<'info, Token>, } @@ -37,15 +36,17 @@ pub fn handler(ctx: Context) -> Result<()> { ErrorCode::TooMuchBorrowed ); if ctx.accounts.verified_owner.lamports() < MIN_WALLET_LAMPORTS { - close_account(CpiContext::new_with_signer( - ctx.accounts.token_program.to_account_info(), - CloseAccount { - account: ctx.accounts.maker_wsol.to_account_info(), - destination: ctx.accounts.verified_owner.to_account_info(), - authority: ctx.accounts.maker.to_account_info(), - }, - &[maker_seeds!(ctx.accounts.maker)], - ))?; + let maker = ctx.accounts.maker.to_account_info(); + let mut maker_lamports = maker.try_borrow_mut_lamports()?; + let maker_data_len = ctx.accounts.maker.to_account_info().data_len(); + let rent = Rent::get()?.minimum_balance(maker_data_len); + let maker_spare_lamports = **maker_lamports - rent; + if maker_spare_lamports > MIN_WALLET_LAMPORTS { + **maker_lamports -= MIN_WALLET_LAMPORTS; + let owner_wallet = ctx.accounts.verified_owner.to_account_info(); + let mut wallet_lamports = owner_wallet.try_borrow_mut_lamports()?; + **wallet_lamports += MIN_WALLET_LAMPORTS; + } } ctx.accounts.maker.expected_onboard_amount = 0; Ok(()) diff --git a/tests/conversion-escrow.ts b/tests/conversion-escrow.ts index 052f743e3..e47a03aef 100644 --- a/tests/conversion-escrow.ts +++ b/tests/conversion-escrow.ts @@ -119,7 +119,7 @@ describe("conversion-escrow", () => { .sub(priceMobile.priceMessage.emaConf.mul(new anchor.BN(2))) .toNumber() * 10 ** priceHnt.priceMessage.exponent * - 10 ** 8; + 10 ** 6; const hntPerMobile = mobileFloorValue / hntFloorValue const hntHolderMobileAta = getAssociatedTokenAddressSync( @@ -130,6 +130,7 @@ describe("conversion-escrow", () => { hntMint, hntHolder.publicKey ); + console.log("mobile per hnt", 1 / hntPerMobile, mobileFloorValue, hntFloorValue); const mobileAmount = new anchor.BN(5000) const instructions = [ createAssociatedTokenAccountIdempotentInstruction( diff --git a/tests/helium-entity-manager.ts b/tests/helium-entity-manager.ts index 7ad6c9a19..8075a9716 100644 --- a/tests/helium-entity-manager.ts +++ b/tests/helium-entity-manager.ts @@ -10,9 +10,11 @@ import { AssetProof, HELIUM_COMMON_LUT, HNT_PYTH_PRICE_FEED, + LUT_ACCOUNTS, MOBILE_PYTH_PRICE_FEED, SOL_PYTH_PRICE_FEED, USDC_PYTH_PRICE_FEED, + chunks, createAtaAndMint, createMint, createMintInstructions, @@ -20,6 +22,8 @@ import { sendInstructions, toBN, toVersionedTx, + truthy, + withPriorityFees, } from "@helium/spl-utils"; import { PythSolanaReceiverProgram, @@ -40,6 +44,7 @@ import { PublicKey, SystemProgram, Connection, + AddressLookupTableProgram, } from "@solana/web3.js"; import chai from "chai"; import { @@ -927,11 +932,6 @@ describe("helium-entity-manager", () => { it("allows verifying owner while topping off sol", async () => { const owner = Keypair.generate(); - const makerWsol = getAssociatedTokenAddressSync( - NATIVE_MINT, - maker, - true - ); const myUsdcAccount = getAssociatedTokenAddressSync(usdcMint, me); const instructions = [ createAssociatedTokenAccountIdempotentInstruction( @@ -940,12 +940,6 @@ describe("helium-entity-manager", () => { me, usdcMint ), - createAssociatedTokenAccountIdempotentInstruction( - me, - makerWsol, - maker, - NATIVE_MINT - ), await hemProgram.methods .makerLendV0({ amount: toBN(0.02, 9), @@ -955,21 +949,21 @@ describe("helium-entity-manager", () => { targetOracle: SOL_PYTH_PRICE_FEED, conversionEscrow, destination: myUsdcAccount, - repayAccount: makerWsol, + repayAccount: maker, usdcMint, }) .instruction(), // Repay SystemProgram.transfer({ fromPubkey: me, - toPubkey: makerWsol, + toPubkey: maker, lamports: 0.02 * LAMPORTS_PER_SOL, }), await conversionEscrowProgram.methods .checkRepayV0() .accounts({ conversionEscrow, - repayAccount: makerWsol, + repayAccount: maker, }) .instruction(), await hemProgram.methods @@ -994,6 +988,7 @@ describe("helium-entity-manager", () => { expect(lamps).to.be.gt(0.02 * LAMPORTS_PER_SOL); }); describe("with verified owner", async () => { + let lut: PublicKey; beforeEach(async () => { await hemProgram.methods .mobileVoucherVerifyOwnerV0() @@ -1004,6 +999,31 @@ describe("helium-entity-manager", () => { }) .signers([eccVerifier]) .rpc({ skipPreflight: true }); + + const slot = await provider.connection.getSlot(); + const [lookupTableInst, lookupTableAddress] = + AddressLookupTableProgram.createLookupTable({ + authority: me, + payer: me, + recentSlot: slot, + }); + let isFirst = true; + for (const addresses of chunks([...LUT_ACCOUNTS, usdcMint, rewardableEntityConfig, dntMint, eccVerifier.publicKey], 20)) { + await sendInstructions( + provider, + [ + isFirst ? lookupTableInst : undefined, + AddressLookupTableProgram.extendLookupTable({ + payer: me, + authority: me, + lookupTable: lookupTableAddress, + addresses, + }), + ].filter(truthy) + ); + isFirst = false; + } + lut = lookupTableAddress; }); it("allows paying mobile", async () => { const makerDntAccount = getAssociatedTokenAddressSync( @@ -1018,8 +1038,7 @@ describe("helium-entity-manager", () => { pythDataMobile.priceMessage.emaPrice .sub(pythDataMobile.priceMessage.emaConf.mul(new anchor.BN(2))) .toNumber() * - 10 ** pythDataMobile.priceMessage.exponent * - 10 ** 6; + 10 ** pythDataMobile.priceMessage.exponent; const mobileAmount = toBN(20 / usdPerMobile, 6); const myUsdcAccount = getAssociatedTokenAddressSync(usdcMint, me); @@ -1150,27 +1169,18 @@ describe("helium-entity-manager", () => { payer: me, }); const tx = await toVersionedTx( - await populateMissingDraftInfo(provider.connection, { - instructions: [ - await hemProgram.methods - .mobileVoucherVerifyOwnerV0() - .accounts({ - verifiedOwner: me, - eccVerifier: eccVerifier.publicKey, - mobileHotspotVoucher, - }) - .instruction(), - ...instructions, - ], + { + instructions, addressLookupTableAddresses: [ ...addressLookupTableAddresses, - HELIUM_COMMON_LUT, + lut, ], feePayer: me, - }) + recentBlockhash: (await provider.connection.getLatestBlockhash()).blockhash, + } ); - await tx.sign([eccVerifier]); const signed = await provider.wallet.signTransaction(tx); + console.log("signed", signed) const serialized = signed.serialize(); expect(serialized.length).to.be.lessThan(1232); }); @@ -1191,6 +1201,7 @@ describe("helium-entity-manager", () => { addressLookupTableAddresses: [ ...addressLookupTableAddresses, HELIUM_COMMON_LUT, + lut, ], feePayer: me, }) @@ -1214,8 +1225,7 @@ describe("helium-entity-manager", () => { pythDataMobile.priceMessage.emaPrice .sub(pythDataMobile.priceMessage.emaConf.mul(new anchor.BN(2))) .toNumber() * - 10 ** pythDataMobile.priceMessage.exponent * - 10 ** 6; + 10 ** pythDataMobile.priceMessage.exponent; const mobileAmount = toBN(20 / usdPerMobile, 6); const myUsdcAccount = getAssociatedTokenAddressSync(usdcMint, me); const instructions0 = [ From 53205b18a3bca2791f79a030032ee925f32a81ce Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Fri, 8 Nov 2024 10:27:55 -0800 Subject: [PATCH 11/12] WIP: Topup working --- .../src/create-and-approve-makers.ts | 1 + packages/helium-admin-cli/src/create-maker.ts | 1 + .../src/functions/payMobileVoucherDc.ts | 134 ----- .../src/functions/payMobileVoucherMobile.ts | 130 ---- .../src/functions/topUpMaker.ts | 111 ++++ .../helium-entity-manager-sdk/src/index.ts | 3 +- packages/spl-utils/src/token.ts | 4 +- .../src/instructions/initialize_maker_v0.rs | 14 +- .../src/instructions/issue_entity/common.rs | 187 ------ .../src/instructions/issue_entity/mod.rs | 3 - .../src/instructions/issue_entity_v0.rs | 15 +- .../src/instructions/maker_lend_v0.rs | 71 +-- .../mobile_voucher/issue_mobile_hotspot_v0.rs | 80 --- .../mobile_voucher_pay_mobile_v0.rs | 125 ---- .../mobile_voucher_verify_owner_v0.rs | 53 -- .../src/instructions/mobile_voucher/mod.rs | 11 - .../src/instructions/mod.rs | 7 +- .../initialize_mobile_hotspot_voucher_v0.rs | 9 +- .../src/instructions/voucher/mod.rs | 7 + .../voucher/onboard_mobile_hotspot_v1.rs | 109 ++++ .../pay_mobile_voucher_v0.rs} | 161 +++-- programs/helium-entity-manager/src/lib.rs | 33 +- programs/helium-entity-manager/src/state.rs | 30 +- tests/helium-entity-manager.ts | 564 +++++------------- tests/utils/fixtures.ts | 8 +- 25 files changed, 550 insertions(+), 1321 deletions(-) delete mode 100644 packages/helium-entity-manager-sdk/src/functions/payMobileVoucherDc.ts delete mode 100644 packages/helium-entity-manager-sdk/src/functions/payMobileVoucherMobile.ts create mode 100644 packages/helium-entity-manager-sdk/src/functions/topUpMaker.ts delete mode 100644 programs/helium-entity-manager/src/instructions/issue_entity/common.rs delete mode 100644 programs/helium-entity-manager/src/instructions/issue_entity/mod.rs delete mode 100644 programs/helium-entity-manager/src/instructions/mobile_voucher/issue_mobile_hotspot_v0.rs delete mode 100644 programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_pay_mobile_v0.rs delete mode 100644 programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_verify_owner_v0.rs delete mode 100644 programs/helium-entity-manager/src/instructions/mobile_voucher/mod.rs rename programs/helium-entity-manager/src/instructions/{mobile_voucher => voucher}/initialize_mobile_hotspot_voucher_v0.rs (91%) create mode 100644 programs/helium-entity-manager/src/instructions/voucher/mod.rs create mode 100644 programs/helium-entity-manager/src/instructions/voucher/onboard_mobile_hotspot_v1.rs rename programs/helium-entity-manager/src/instructions/{mobile_voucher/mobile_voucher_pay_dc_v0.rs => voucher/pay_mobile_voucher_v0.rs} (52%) diff --git a/packages/helium-admin-cli/src/create-and-approve-makers.ts b/packages/helium-admin-cli/src/create-and-approve-makers.ts index da75865f4..2406ab098 100644 --- a/packages/helium-admin-cli/src/create-and-approve-makers.ts +++ b/packages/helium-admin-cli/src/create-and-approve-makers.ts @@ -217,6 +217,7 @@ export async function run(args: any = process.argv) { metadataUrl: 'todo', issuingAuthority: makerAuthority, updateAuthority, + topupAmounts: [], }) .accounts({ maker, diff --git a/packages/helium-admin-cli/src/create-maker.ts b/packages/helium-admin-cli/src/create-maker.ts index 0b070b33c..8b780d4ee 100644 --- a/packages/helium-admin-cli/src/create-maker.ts +++ b/packages/helium-admin-cli/src/create-maker.ts @@ -141,6 +141,7 @@ export async function run(args: any = process.argv) { issuingAuthority, // Temp, since we need to set maker tree updateAuthority: provider.wallet.publicKey, + topupAmounts: [], }) .accounts({ maker, diff --git a/packages/helium-entity-manager-sdk/src/functions/payMobileVoucherDc.ts b/packages/helium-entity-manager-sdk/src/functions/payMobileVoucherDc.ts deleted file mode 100644 index 13e36a1f7..000000000 --- a/packages/helium-entity-manager-sdk/src/functions/payMobileVoucherDc.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { AnchorProvider, Program } from "@coral-xyz/anchor"; -import { conversionEscrowKey } from "@helium/conversion-escrow-sdk"; -import { ConversionEscrow } from "@helium/idls/lib/types/conversion_escrow"; -import { HeliumEntityManager } from "@helium/idls/lib/types/helium_entity_manager"; -import { - HNT_MINT, - HNT_PYTH_PRICE_FEED, - USDC_MINT, - USDC_PYTH_PRICE_FEED, - toBN, - toNumber -} from "@helium/spl-utils"; -import { getAssociatedTokenAddressSync } from "@solana/spl-token"; -import { PublicKey } from "@solana/web3.js"; -import { deserializeInstruction } from "./utils"; - -export const JUPITER_URL = - process.env.JUPITER_URL || "https://quote-api.jup.ag/v6"; - -export async function payMobileVoucherDc({ - program, - ceProgram, - mobileHotspotVoucher, - verifiedOwner, - maker, - payer = (program.provider as AnchorProvider).wallet.publicKey, -}: { - program: Program; - ceProgram: Program; - mobileHotspotVoucher: PublicKey; - verifiedOwner?: PublicKey; - payer?: PublicKey; - maker?: PublicKey; -}) { - const voucher = await program.account.mobileHotspotVoucherV0.fetch( - mobileHotspotVoucher - ); - const rewardableEntityConfigAcc = - await program.account.rewardableEntityConfigV0.fetch( - voucher.rewardableEntityConfig - ); - const deviceType = voucher.deviceType; - const fees = - rewardableEntityConfigAcc.settings.mobileConfigV2?.feesByDevice.find( - (d) => Object.keys(d.deviceType)[0] === Object.keys(deviceType)[0] - )!; - const dcFeeUsd = toNumber(fees.dcOnboardingFee, 5); - - const quoteResponse = await ( - await fetch( - `${JUPITER_URL}/quote?inputMint=${USDC_MINT.toBase58()}&outputMint=${HNT_MINT.toBase58()}&amount=${toBN( - dcFeeUsd, - 6 - ).toString()}&slippageBps=50&onlyDirectRoutes=true` - ) - ).json(); - if (quoteResponse.error) { - throw new Error("Failed to get quote: " + quoteResponse.error); - } - const destination = getAssociatedTokenAddressSync( - HNT_MINT, - voucher.maker, - true - ); - const instructions = await ( - await fetch(`${JUPITER_URL}/swap-instructions`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - // quoteResponse from /quote api - quoteResponse, - userPublicKey: payer, - destinationTokenAccount: destination, - }), - }) - ).json(); - if (instructions.error) { - throw new Error("Failed to get swap instructions: " + instructions.error); - } - - const conversionEscrow = conversionEscrowKey(USDC_MINT, voucher.maker)[0]; - - const { - computeBudgetInstructions, // The necessary instructions to setup the compute budget. - swapInstruction: swapInstructionPayload, // The actual swap instruction. - addressLookupTableAddresses, // The lookup table addresses that you can use if you are using versioned transaction. - } = instructions; - - return { - instructions: [ - await program.methods - .makerLendV0({ - amount: toBN(dcFeeUsd, 6), - }) - .accounts({ - maker: voucher.maker, - targetOracle: HNT_PYTH_PRICE_FEED, - conversionEscrow, - oracle: USDC_PYTH_PRICE_FEED, - escrow: getAssociatedTokenAddressSync( - USDC_MINT, - conversionEscrow, - true - ), - destination: getAssociatedTokenAddressSync(USDC_MINT, payer), - repayAccount: destination, - usdcMint: USDC_MINT, - }) - .instruction(), - ...computeBudgetInstructions.map(deserializeInstruction), - deserializeInstruction(swapInstructionPayload), - await ceProgram.methods - .checkRepayV0() - .accounts({ - conversionEscrow, - repayAccount: destination, - }) - .instruction(), - await program.methods - .mobileVoucherPayDcV0() - .accounts({ - verifiedOwner, - mobileHotspotVoucher, - maker, - }) - .instruction(), - ], - addressLookupTableAddresses: addressLookupTableAddresses.map( - (a: any) => new PublicKey(a) - ), - }; -} diff --git a/packages/helium-entity-manager-sdk/src/functions/payMobileVoucherMobile.ts b/packages/helium-entity-manager-sdk/src/functions/payMobileVoucherMobile.ts deleted file mode 100644 index 4f54d47b0..000000000 --- a/packages/helium-entity-manager-sdk/src/functions/payMobileVoucherMobile.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { AnchorProvider, Program } from "@coral-xyz/anchor"; -import { conversionEscrowKey } from "@helium/conversion-escrow-sdk"; -import { ConversionEscrow } from "@helium/idls/lib/types/conversion_escrow"; -import { HeliumEntityManager } from "@helium/idls/lib/types/helium_entity_manager"; -import { MOBILE_MINT, MOBILE_PYTH_PRICE_FEED, USDC_MINT, USDC_PYTH_PRICE_FEED, toBN, toNumber } from "@helium/spl-utils"; -import { getAssociatedTokenAddressSync } from "@solana/spl-token"; -import { - PublicKey -} from "@solana/web3.js"; -import { deserializeInstruction } from "./utils"; - -export const JUPITER_URL = - process.env.JUPITER_URL || "https://quote-api.jup.ag/v6"; - -export async function payMobileVoucherMobile({ - program, - ceProgram, - mobileHotspotVoucher, - verifiedOwner, - maker, - payer = (program.provider as AnchorProvider).wallet.publicKey, -}: { - program: Program; - ceProgram: Program; - mobileHotspotVoucher: PublicKey; - verifiedOwner?: PublicKey; - payer?: PublicKey; - maker?: PublicKey; -}) { - const voucher = await program.account.mobileHotspotVoucherV0.fetch( - mobileHotspotVoucher - ); - const rewardableEntityConfigAcc = - await program.account.rewardableEntityConfigV0.fetch( - voucher.rewardableEntityConfig - ); - const deviceType = voucher.deviceType; - const fees = - rewardableEntityConfigAcc.settings.mobileConfigV2?.feesByDevice.find( - (d) => Object.keys(d.deviceType)[0] === Object.keys(deviceType)[0] - )!; - const mobileFeeUsd = toNumber(fees.mobileOnboardingFeeUsd, 6); - - const quoteResponse = await ( - await fetch( - `${JUPITER_URL}/quote?inputMint=${USDC_MINT.toBase58()}&outputMint=${MOBILE_MINT.toBase58()}&amount=${toBN( - mobileFeeUsd, - 6 - ).toString()}&slippageBps=50&onlyDirectRoutes=true` - ) - ).json(); - if (quoteResponse.error) { - throw new Error("Failed to get quote: " + quoteResponse.error); - } - const destination = getAssociatedTokenAddressSync( - MOBILE_MINT, - voucher.maker, - true - ) - const instructions = await ( - await fetch(`${JUPITER_URL}/swap-instructions`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - // quoteResponse from /quote api - quoteResponse, - userPublicKey: payer, - destinationTokenAccount: destination, - }), - }) - ).json(); - if (instructions.error) { - throw new Error("Failed to get swap instructions: " + instructions.error); - } - - const conversionEscrow = conversionEscrowKey(USDC_MINT, voucher.maker)[0] - - const { - computeBudgetInstructions, // The necessary instructions to setup the compute budget. - swapInstruction: swapInstructionPayload, // The actual swap instruction. - addressLookupTableAddresses, // The lookup table addresses that you can use if you are using versioned transaction. - } = instructions; - - return { - instructions: [ - await program.methods - .makerLendV0({ - amount: toBN(mobileFeeUsd, 6), - }) - .accounts({ - maker: voucher.maker, - targetOracle: MOBILE_PYTH_PRICE_FEED, - conversionEscrow, - oracle: USDC_PYTH_PRICE_FEED, - escrow: getAssociatedTokenAddressSync( - USDC_MINT, - conversionEscrow, - true - ), - destination: getAssociatedTokenAddressSync(USDC_MINT, payer), - repayAccount: destination, - usdcMint: USDC_MINT, - }) - .instruction(), - ...computeBudgetInstructions.map(deserializeInstruction), - deserializeInstruction(swapInstructionPayload), - await ceProgram.methods - .checkRepayV0() - .accounts({ - conversionEscrow, - repayAccount: destination, - }) - .instruction(), - await program.methods - .mobileVoucherPayMobileV0() - .accounts({ - verifiedOwner, - mobileHotspotVoucher, - dntPrice: MOBILE_PYTH_PRICE_FEED, - maker, - }) - .instruction(), - ], - addressLookupTableAddresses: addressLookupTableAddresses.map( - (a: any) => new PublicKey(a) - ), - }; -} diff --git a/packages/helium-entity-manager-sdk/src/functions/topUpMaker.ts b/packages/helium-entity-manager-sdk/src/functions/topUpMaker.ts new file mode 100644 index 000000000..89934eedd --- /dev/null +++ b/packages/helium-entity-manager-sdk/src/functions/topUpMaker.ts @@ -0,0 +1,111 @@ +import { AnchorProvider, Program } from "@coral-xyz/anchor"; +import { conversionEscrowKey } from "@helium/conversion-escrow-sdk"; +import { ConversionEscrow } from "@helium/idls/lib/types/conversion_escrow"; +import { HeliumEntityManager } from "@helium/idls/lib/types/helium_entity_manager"; +import { MOBILE_MINT, MOBILE_PYTH_PRICE_FEED, TransactionDraft, USDC_MINT, USDC_PYTH_PRICE_FEED, toBN, toNumber } from "@helium/spl-utils"; +import { getAssociatedTokenAddressSync } from "@solana/spl-token"; +import { + PublicKey +} from "@solana/web3.js"; +import { deserializeInstruction } from "./utils"; + +export const JUPITER_URL = + process.env.JUPITER_URL || "https://quote-api.jup.ag/v6"; + +export async function topUpMaker({ + program, + ceProgram, + maker, + usdcMint = USDC_MINT, + payer = (program.provider as AnchorProvider).wallet.publicKey, +}: { + program: Program; + ceProgram: Program; + payer?: PublicKey; + maker: PublicKey; + usdcMint?: PublicKey; +}) { + const makerAcc = await program.account.makerV0.fetch(maker); + return await Promise.all(makerAcc.topupAmounts.map(async (topupAmount) => { + const inputMint = USDC_MINT; + const outputMint = topupAmount.mint + +const quoteResponse = await( + await fetch( + `${JUPITER_URL}/quote?inputMint=${inputMint.toBase58()}&outputMint=${outputMint.toBase58()}&amount=${toBN( + topupAmount.sourceAmount, + 6 + ).toString()}&slippageBps=50&onlyDirectRoutes=true` + ) +).json(); +if (quoteResponse.error) { + throw new Error("Failed to get quote: " + quoteResponse.error); +} +const destination = getAssociatedTokenAddressSync( + inputMint, + maker, + true +); +const instructions = await( + await fetch(`${JUPITER_URL}/swap-instructions`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + // quoteResponse from /quote api + quoteResponse, + userPublicKey: payer, + destinationTokenAccount: destination, + }), + }) +).json(); +if (instructions.error) { + throw new Error("Failed to get swap instructions: " + instructions.error); +} + +const conversionEscrow = conversionEscrowKey(usdcMint, maker)[0]; +const conversionEscrowAcc = await ceProgram.account.conversionEscrowV0.fetch(conversionEscrow); +const oracle = conversionEscrowAcc.targets.find((t) => t.mint === outputMint)?.oracle; + +const { + computeBudgetInstructions, // The necessary instructions to setup the compute budget. + swapInstruction: swapInstructionPayload, // The actual swap instruction. + addressLookupTableAddresses, // The lookup table addresses that you can use if you are using versioned transaction. +} = instructions; + +return { + instructions: [ + await program.methods + .makerLendV0() + .accounts({ + maker, + targetOracle: MOBILE_PYTH_PRICE_FEED, + conversionEscrow, + oracle, + escrow: getAssociatedTokenAddressSync( + inputMint, + conversionEscrow, + true + ), + destination: getAssociatedTokenAddressSync(inputMint, payer), + repayAccount: destination, + sourceMint: inputMint, + }) + .instruction(), + ...computeBudgetInstructions.map(deserializeInstruction), + deserializeInstruction(swapInstructionPayload), + await ceProgram.methods + .checkRepayV0() + .accounts({ + conversionEscrow, + repayAccount: destination, + }) + .instruction(), + ], + addressLookupTableAddresses: addressLookupTableAddresses.map( + (a: any) => new PublicKey(a) + ), +}; + })); +} diff --git a/packages/helium-entity-manager-sdk/src/index.ts b/packages/helium-entity-manager-sdk/src/index.ts index 4572e5571..ad5a63b80 100644 --- a/packages/helium-entity-manager-sdk/src/index.ts +++ b/packages/helium-entity-manager-sdk/src/index.ts @@ -8,8 +8,7 @@ import { HeliumEntityManager } from "@helium/idls/lib/types/helium_entity_manage export * from "./constants"; export { onboardIotHotspot } from "./functions/onboardIotHotspot"; export { onboardMobileHotspot } from "./functions/onboardMobileHotspot"; -export { payMobileVoucherMobile } from "./functions/payMobileVoucherMobile"; -export { payMobileVoucherDc } from "./functions/payMobileVoucherDc"; +export { topUpMaker } from "./functions/topUpMaker"; export { proofArgsAndAccounts } from "@helium/spl-utils"; export { updateIotMetadata } from "./functions/updateIotMetadata"; export { updateMobileMetadata } from "./functions/updateMobileMetadata"; diff --git a/packages/spl-utils/src/token.ts b/packages/spl-utils/src/token.ts index 371797d53..932d24dd1 100644 --- a/packages/spl-utils/src/token.ts +++ b/packages/spl-utils/src/token.ts @@ -38,7 +38,9 @@ export async function mintTo( ) ); try { - await provider.sendAndConfirm(mintTx); + await provider.sendAndConfirm(mintTx, [], { + skipPreflight: true, + }); } catch (e: any) { console.log("Error", e, e.logs); if (e.logs) { diff --git a/programs/helium-entity-manager/src/instructions/initialize_maker_v0.rs b/programs/helium-entity-manager/src/instructions/initialize_maker_v0.rs index 037eadaf1..0f61ae981 100644 --- a/programs/helium-entity-manager/src/instructions/initialize_maker_v0.rs +++ b/programs/helium-entity-manager/src/instructions/initialize_maker_v0.rs @@ -1,5 +1,3 @@ -use crate::error::ErrorCode; -use crate::state::*; use anchor_lang::prelude::*; use anchor_spl::{ associated_token::AssociatedToken, @@ -7,10 +5,13 @@ use anchor_spl::{ }; use helium_sub_daos::DaoV0; use mpl_token_metadata::types::{CollectionDetails, DataV2}; -use shared_utils::token_metadata::{ - create_master_edition_v3, CreateMasterEditionV3, CreateMetadataAccountsV3, +use shared_utils::{ + create_metadata_accounts_v3, + token_metadata::{create_master_edition_v3, CreateMasterEditionV3, CreateMetadataAccountsV3}, + Metadata, }; -use shared_utils::{create_metadata_accounts_v3, Metadata}; + +use crate::{error::ErrorCode, state::*}; #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] pub struct InitializeMakerArgsV0 { @@ -18,6 +19,7 @@ pub struct InitializeMakerArgsV0 { pub issuing_authority: Pubkey, pub name: String, pub metadata_url: String, + pub topup_amounts: Vec, } #[derive(Accounts)] @@ -166,7 +168,7 @@ pub fn handler(ctx: Context, args: InitializeMakerArgsV0) -> bump_seed: ctx.bumps["maker"], collection_bump_seed: ctx.bumps["collection"], dao: ctx.accounts.dao.key(), - expected_onboard_amount: 0, + topup_amounts: args.topup_amounts, }); Ok(()) diff --git a/programs/helium-entity-manager/src/instructions/issue_entity/common.rs b/programs/helium-entity-manager/src/instructions/issue_entity/common.rs deleted file mode 100644 index 7d91ea02e..000000000 --- a/programs/helium-entity-manager/src/instructions/issue_entity/common.rs +++ /dev/null @@ -1,187 +0,0 @@ -use std::cmp::min; -use std::collections::BTreeMap; - -use crate::{constants::ENTITY_METADATA_URL, error::ErrorCode}; -use crate::{key_to_asset_seeds, state::*}; -use account_compression_cpi::{program::SplAccountCompression, Noop}; -use anchor_lang::prelude::*; -use anchor_lang::solana_program::hash::hash; -use anchor_spl::token::Mint; -use angry_purple_tiger::AnimalName; -use bubblegum_cpi::{ - cpi::{accounts::MintToCollectionV1, mint_to_collection_v1}, - get_asset_id, - program::Bubblegum, - Collection, Creator, MetadataArgs, TokenProgramVersion, TokenStandard, TreeConfig, -}; -use helium_sub_daos::DaoV0; - -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] -pub struct IssueEntityArgsV0 { - pub entity_key: Vec, -} - -#[derive(Accounts)] -pub struct IssueEntityCommonV0<'info> { - #[account(mut)] - pub payer: Signer<'info>, - pub collection: Box>, - /// CHECK: Handled by cpi - #[account( - mut, - seeds = ["metadata".as_bytes(), token_metadata_program.key().as_ref(), collection.key().as_ref()], - seeds::program = token_metadata_program.key(), - bump, - )] - pub collection_metadata: UncheckedAccount<'info>, - /// CHECK: Handled By cpi account - #[account( - seeds = ["metadata".as_bytes(), token_metadata_program.key().as_ref(), collection.key().as_ref(), "edition".as_bytes()], - seeds::program = token_metadata_program.key(), - bump, - )] - pub collection_master_edition: UncheckedAccount<'info>, - #[account( - mut, - has_one = collection, - has_one = merkle_tree, - has_one = dao, - )] - pub maker: Box>, - /// CHECK: Signs as a verified creator to make searching easier - #[account( - seeds = [b"entity_creator", dao.key().as_ref()], - bump, - )] - pub entity_creator: UncheckedAccount<'info>, - pub dao: Box>, - #[account( - mut, - seeds = [merkle_tree.key().as_ref()], - seeds::program = bubblegum_program.key(), - bump, - )] - pub tree_authority: Box>, - /// CHECK: Used in cpi - #[account(mut)] - pub merkle_tree: AccountInfo<'info>, - #[account( - seeds = ["collection_cpi".as_bytes()], - seeds::program = bubblegum_program.key(), - bump, - )] - /// CHECK: Used in cpi - pub bubblegum_signer: UncheckedAccount<'info>, - /// CHECK: Used as recipient wallet - pub recipient: AccountInfo<'info>, - - /// CHECK: Verified by constraint - #[account(address = mpl_token_metadata::ID)] - pub token_metadata_program: AccountInfo<'info>, - pub log_wrapper: Program<'info, Noop>, - pub bubblegum_program: Program<'info, Bubblegum>, - pub compression_program: Program<'info, SplAccountCompression>, - pub system_program: Program<'info, System>, -} - -impl<'info> IssueEntityCommonV0<'info> { - fn mint_to_collection_ctx(&self) -> CpiContext<'_, '_, '_, 'info, MintToCollectionV1<'info>> { - let cpi_accounts = MintToCollectionV1 { - tree_authority: self.tree_authority.to_account_info(), - leaf_delegate: self.recipient.to_account_info(), - leaf_owner: self.recipient.to_account_info(), - merkle_tree: self.merkle_tree.to_account_info(), - payer: self.payer.to_account_info(), - tree_delegate: self.maker.to_account_info(), - log_wrapper: self.log_wrapper.to_account_info(), - compression_program: self.compression_program.to_account_info(), - system_program: self.system_program.to_account_info(), - collection_authority: self.maker.to_account_info(), - collection_authority_record_pda: self.bubblegum_program.to_account_info(), - collection_mint: self.collection.to_account_info(), - collection_metadata: self.collection_metadata.to_account_info(), - edition_account: self.collection_master_edition.to_account_info(), - bubblegum_signer: self.bubblegum_signer.to_account_info(), - token_metadata_program: self.token_metadata_program.to_account_info(), - }; - CpiContext::new(self.bubblegum_program.to_account_info(), cpi_accounts) - } - - pub fn issue_entity( - &mut self, - bumps: BTreeMap, - key_to_asset: &mut Account<'info, KeyToAssetV0>, - args: IssueEntityArgsV0, - ) -> Result<()> { - let asset_id = get_asset_id(&self.merkle_tree.key(), self.tree_authority.num_minted); - key_to_asset.set_inner(KeyToAssetV0 { - asset: asset_id, - dao: self.dao.key(), - entity_key: args.entity_key.clone(), - bump_seed: bumps["key_to_asset"], - key_serialization: KeySerialization::B58, - }); - - let key_str = bs58::encode(args.entity_key).into_string(); - let animal_name: AnimalName = key_str - .parse() - .map_err(|_| error!(ErrorCode::InvalidEccCompact))?; - - let maker_seeds: &[&[&[u8]]] = &[&[ - b"maker", - self.maker.dao.as_ref(), - self.maker.name.as_bytes(), - &[self.maker.bump_seed], - ]]; - - let name = animal_name.to_string(); - let uri = format!("{}/v2/hotspot/{}", ENTITY_METADATA_URL, key_to_asset.key(),); - let metadata = MetadataArgs { - name: name[..min(name.len(), 32)].to_owned(), - symbol: String::from("HOTSPOT"), - uri, - collection: Some(Collection { - key: self.collection.key(), - verified: false, // Verified in cpi - }), - primary_sale_happened: true, - is_mutable: true, - edition_nonce: None, - token_standard: Some(TokenStandard::NonFungible), - uses: None, - token_program_version: TokenProgramVersion::Original, - creators: vec![ - Creator { - address: self.entity_creator.key(), - verified: true, - share: 100, - }, - Creator { - address: key_to_asset.key(), - verified: true, - share: 0, - }, - ], - seller_fee_basis_points: 0, - }; - let entity_creator_seeds: &[&[&[u8]]] = &[&[ - b"entity_creator", - self.dao.to_account_info().key.as_ref(), - &[bumps["entity_creator"]], - ]]; - let mut creator = self.entity_creator.to_account_info(); - creator.is_signer = true; - let mut key_to_asset_creator = key_to_asset.to_account_info(); - key_to_asset_creator.is_signer = true; - let key_to_asset_signer: &[&[u8]] = key_to_asset_seeds!(key_to_asset); - mint_to_collection_v1( - self - .mint_to_collection_ctx() - .with_remaining_accounts(vec![creator, key_to_asset_creator]) - .with_signer(&[maker_seeds[0], entity_creator_seeds[0], key_to_asset_signer]), - metadata, - )?; - - Ok(()) - } -} diff --git a/programs/helium-entity-manager/src/instructions/issue_entity/mod.rs b/programs/helium-entity-manager/src/instructions/issue_entity/mod.rs deleted file mode 100644 index 45987113c..000000000 --- a/programs/helium-entity-manager/src/instructions/issue_entity/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod common; - -pub use common::*; diff --git a/programs/helium-entity-manager/src/instructions/issue_entity_v0.rs b/programs/helium-entity-manager/src/instructions/issue_entity_v0.rs index cbe7ed078..b7c57af75 100644 --- a/programs/helium-entity-manager/src/instructions/issue_entity_v0.rs +++ b/programs/helium-entity-manager/src/instructions/issue_entity_v0.rs @@ -1,11 +1,7 @@ -use std::cmp::min; -use std::str::FromStr; +use std::{cmp::min, str::FromStr}; -use crate::{constants::ENTITY_METADATA_URL, error::ErrorCode}; -use crate::{key_to_asset_seeds, state::*, IssueEntityArgsV0}; use account_compression_cpi::{program::SplAccountCompression, Noop}; -use anchor_lang::prelude::*; -use anchor_lang::solana_program::hash::hash; +use anchor_lang::{prelude::*, solana_program::hash::hash}; use anchor_spl::token::Mint; use angry_purple_tiger::AnimalName; use bubblegum_cpi::{ @@ -16,6 +12,13 @@ use bubblegum_cpi::{ }; use helium_sub_daos::DaoV0; +use crate::{constants::ENTITY_METADATA_URL, error::ErrorCode, key_to_asset_seeds, state::*}; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct IssueEntityArgsV0 { + pub entity_key: Vec, +} + pub const TESTING: bool = std::option_env!("TESTING").is_some(); pub const ECC_VERIFIER: &str = if TESTING { diff --git a/programs/helium-entity-manager/src/instructions/maker_lend_v0.rs b/programs/helium-entity-manager/src/instructions/maker_lend_v0.rs index cfbded3ff..3d20ca936 100644 --- a/programs/helium-entity-manager/src/instructions/maker_lend_v0.rs +++ b/programs/helium-entity-manager/src/instructions/maker_lend_v0.rs @@ -4,10 +4,7 @@ use std::str::FromStr; use anchor_lang::{ prelude::*, - solana_program::sysvar::{ - self, - instructions::{load_current_index_checked, load_instruction_at_checked}, - }, + solana_program::sysvar::{self}, }; use anchor_spl::token::{Mint, Token}; use conversion_escrow::{ @@ -16,25 +13,13 @@ use conversion_escrow::{ ConversionEscrowV0, LendArgsV0, }; -use crate::{error::ErrorCode, maker_seeds, state::*, TESTING}; - -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] -pub struct MakerLendArgsV0 { - pub amount: u64, -} +use crate::{maker_seeds, state::*}; #[derive(Accounts)] pub struct MakerLendV0<'info> { - #[account( - mut, - // Ensure a loan isn't already in progress - constraint = maker.expected_onboard_amount == 0 @ ErrorCode::LoanInProgress, - )] + #[account(mut)] pub maker: Box>, - #[account( - constraint = TESTING || usdc_mint.key() == Pubkey::from_str("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v").unwrap() - )] - pub usdc_mint: Box>, + pub source_mint: Box>, /// CHECK: Checked in cpi pub target_oracle: UncheckedAccount<'info>, /// CHECK: Checked in cpi @@ -50,11 +35,12 @@ pub struct MakerLendV0<'info> { pub repay_account: AccountInfo<'info>, #[account( mut, - seeds = [b"conversion_escrow", usdc_mint.key().as_ref(), maker.key().as_ref()], + seeds = [b"conversion_escrow", source_mint.key().as_ref(), maker.key().as_ref()], seeds::program = conversion_escrow_program, bump = conversion_escrow.bump_seed, has_one = escrow, has_one = oracle, + constraint = conversion_escrow.owner == maker.key() )] pub conversion_escrow: Box>, pub conversion_escrow_program: Program<'info, ConversionEscrow>, @@ -64,7 +50,7 @@ pub struct MakerLendV0<'info> { pub token_program: Program<'info, Token>, } -pub fn handler(ctx: Context, args: MakerLendArgsV0) -> Result<()> { +pub fn handler(ctx: Context) -> Result<()> { let seeds = maker_seeds!(ctx.accounts.maker); let target = ctx @@ -74,7 +60,15 @@ pub fn handler(ctx: Context, args: MakerLendArgsV0) -> Result<()> { .iter() .find(|target| target.oracle == ctx.accounts.target_oracle.key()) .unwrap(); - let amount_with_slippage = args.amount + args.amount * u64::from(target.slippage_bps) / 10000; + let topup_amount = ctx + .accounts + .maker + .topup_amounts + .iter() + .find(|topup| topup.mint == target.mint) + .unwrap(); + let amount_with_slippage = topup_amount.source_amount + + topup_amount.source_amount * u64::from(target.slippage_bps) / 10000; lend_v0( CpiContext::new_with_signer( ctx.accounts.conversion_escrow_program.to_account_info(), @@ -82,7 +76,7 @@ pub fn handler(ctx: Context, args: MakerLendArgsV0) -> Result<()> { owner: ctx.accounts.maker.to_account_info(), conversion_escrow: ctx.accounts.conversion_escrow.to_account_info(), escrow: ctx.accounts.escrow.to_account_info(), - mint: ctx.accounts.usdc_mint.to_account_info(), + mint: ctx.accounts.source_mint.to_account_info(), oracle: ctx.accounts.oracle.to_account_info(), target_oracle: ctx.accounts.target_oracle.to_account_info(), destination: ctx.accounts.destination.to_account_info(), @@ -97,37 +91,6 @@ pub fn handler(ctx: Context, args: MakerLendArgsV0) -> Result<()> { }, )?; - let ixs = ctx.accounts.instructions.to_account_info(); - let current_index = load_current_index_checked(&ixs)? as usize; - // loop through instructions, looking for an onboard instruction - let mut index = current_index + 1; - let valid_instructions: Vec<[u8; 8]> = vec![ - get_function_hash("global", "mobile_voucher_pay_mobile_v0"), - get_function_hash("global", "mobile_voucher_pay_dc_v0"), - get_function_hash("global", "mobile_voucher_verify_owner_v0"), - ]; - - loop { - // get the next instruction, die if theres no more - if let Ok(ix) = load_instruction_at_checked(index, &ixs) { - if ix.program_id == crate::id() { - if valid_instructions.iter().any(|&i| i == ix.data[0..8]) { - // Maker is always the first account in the instruction - if ix.accounts[0].pubkey == ctx.accounts.maker.key() { - break; - } - } - } - } else { - // no more instructions, so we're missing an onboard - return Err(ErrorCode::MissingOnboard.into()); - } - - index += 1 - } - - ctx.accounts.maker.expected_onboard_amount = args.amount; - Ok(()) } diff --git a/programs/helium-entity-manager/src/instructions/mobile_voucher/issue_mobile_hotspot_v0.rs b/programs/helium-entity-manager/src/instructions/mobile_voucher/issue_mobile_hotspot_v0.rs deleted file mode 100644 index e25100a3f..000000000 --- a/programs/helium-entity-manager/src/instructions/mobile_voucher/issue_mobile_hotspot_v0.rs +++ /dev/null @@ -1,80 +0,0 @@ -use crate::error::ErrorCode; -use crate::issue_entity::common::*; -use crate::state::*; -use crate::IssueEntityArgsV0; -use anchor_lang::prelude::*; -use anchor_lang::solana_program::hash::hash; -use helium_sub_daos::SubDaoV0; - -#[derive(Accounts)] -#[instruction(args: IssueEntityArgsV0)] -pub struct IssueMobileHotspotV0<'info> { - pub issue_entity_common: IssueEntityCommonV0<'info>, - #[account(mut)] - /// CHECK: Just getting refunded - pub refund: AccountInfo<'info>, - #[account( - mut, - has_one = verified_owner, - has_one = rewardable_entity_config, - has_one = refund, - close = refund, - constraint = voucher.maker == issue_entity_common.maker.key(), - constraint = voucher.paid_dc && voucher.paid_mobile @ ErrorCode::VoucherNotPaid, - seeds = [ - "mobile_hotspot_voucher".as_bytes(), - voucher.rewardable_entity_config.as_ref(), - &hash(&args.entity_key[..]).to_bytes() - ], - bump = voucher.bump_seed, - )] - pub voucher: Box>, - #[account( - has_one = sub_dao - )] - pub rewardable_entity_config: Box>, - #[account( - constraint = sub_dao.dao == issue_entity_common.dao.key(), - )] - pub sub_dao: Box>, - pub verified_owner: Signer<'info>, - #[account( - init, - payer = issue_entity_common.payer, - space = MOBILE_HOTSPOT_INFO_SIZE, - seeds = [ - b"mobile_info", - rewardable_entity_config.key().as_ref(), - &hash(&args.entity_key[..]).to_bytes() - ], - bump, - )] - pub mobile_info: Box>, - // This can't be in common accounts because there's a bug with anchor where you can't use #[instruction(...)] in those. - #[account( - init_if_needed, - payer = issue_entity_common.payer, - space = 8 + std::mem::size_of::() + 1 + args.entity_key.len(), - seeds = [ - "key_to_asset".as_bytes(), - issue_entity_common.dao.key().as_ref(), - &hash(&args.entity_key[..]).to_bytes() - ], - bump - )] - pub key_to_asset: Box>, - pub system_program: Program<'info, System>, -} - -pub fn handler(ctx: Context, args: IssueEntityArgsV0) -> Result<()> { - // Issue entity only if needed - if ctx.accounts.key_to_asset.dao == Pubkey::default() { - ctx.accounts.issue_entity_common.issue_entity( - ctx.bumps, - &mut ctx.accounts.key_to_asset, - args, - )?; - } - - Ok(()) -} diff --git a/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_pay_mobile_v0.rs b/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_pay_mobile_v0.rs deleted file mode 100644 index a5d367207..000000000 --- a/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_pay_mobile_v0.rs +++ /dev/null @@ -1,125 +0,0 @@ -use std::str::FromStr; - -use anchor_lang::prelude::*; -use anchor_spl::{ - associated_token::AssociatedToken, - token::{burn, Burn, Mint, Token, TokenAccount}, -}; -use helium_sub_daos::SubDaoV0; -use pyth_solana_receiver_sdk::price_update::PriceUpdateV2; - -use crate::{error::ErrorCode, maker_seeds, state::*, TESTING}; - -const PRICE_ORACLE: &str = "DQ4C1tzvu28cwo1roN1Wm6TW35sfJEjLh517k3ZeWevx"; - -#[derive(Accounts)] -pub struct MobileVoucherPayMobileV0<'info> { - #[account(mut)] - pub maker: Box>, - #[account( - has_one = sub_dao, - constraint = rewardable_entity_config.settings.is_mobile(), - )] - pub rewardable_entity_config: Box>, - #[account( - mut, - has_one = rewardable_entity_config, - has_one = maker, - has_one = verified_owner, - )] - pub mobile_hotspot_voucher: Box>, - pub verified_owner: Signer<'info>, - #[account(mut)] - pub dnt_mint: Box>, - /// CHECK: Checked by loading with pyth. Also double checked by the has_one on data credits instance. - #[account( - address = Pubkey::from_str(PRICE_ORACLE).unwrap() - )] - pub dnt_price: Box>, - #[account( - has_one = dnt_mint, - )] - pub sub_dao: Box>, - #[account( - mut, - associated_token::authority = maker, - associated_token::mint = dnt_mint - )] - pub dnt_burner: Account<'info, TokenAccount>, - pub token_program: Program<'info, Token>, - pub associated_token_program: Program<'info, AssociatedToken>, -} - -pub fn handler(ctx: Context) -> Result<()> { - let fees = ctx - .accounts - .rewardable_entity_config - .settings - .mobile_device_fees(ctx.accounts.mobile_hotspot_voucher.device_type) - .ok_or(error!(ErrorCode::InvalidDeviceType))?; - let dnt_fee = fees.mobile_onboarding_fee_usd; - require_gte!( - dnt_fee, - ctx.accounts.maker.expected_onboard_amount, - ErrorCode::TooMuchBorrowed - ); - - let mobile_price_oracle = &mut ctx.accounts.dnt_price; - let current_time = Clock::get()?.unix_timestamp; - require_gte!( - mobile_price_oracle - .price_message - .publish_time - .saturating_add(if TESTING { 6000000 } else { 10 * 60 }.into()), - current_time, - ErrorCode::PythPriceFeedStale - ); - let mobile_price = mobile_price_oracle.price_message.ema_price; - // Remove the confidence from the price to use the most conservative price - // https://docs.pyth.network/price-feeds/solana-price-feeds/best-practices#confidence-intervals - let mobile_price_with_conf = mobile_price - .checked_sub( - i64::try_from( - mobile_price_oracle - .price_message - .ema_conf - .checked_mul(2) - .unwrap(), - ) - .unwrap(), - ) - .unwrap(); - // Exponent is a negative number, likely -8 - // Since the price is multiplied by an extra 10^8, and we're dividing by that price, need to also multiply - // by the exponent - let exponent_dec = 10_u64 - .checked_pow(u32::try_from(-mobile_price_oracle.price_message.exponent).unwrap()) - .ok_or_else(|| error!(ErrorCode::ArithmeticError))?; - - require_gt!(mobile_price_with_conf, 0); - let mobile_fee = dnt_fee - .checked_mul(exponent_dec) - .unwrap() - .checked_div(mobile_price_with_conf.try_into().unwrap()) - .unwrap(); - if mobile_fee > 0 { - let signer_seeds = maker_seeds!(ctx.accounts.maker); - burn( - CpiContext::new_with_signer( - ctx.accounts.token_program.to_account_info(), - Burn { - mint: ctx.accounts.dnt_mint.to_account_info(), - from: ctx.accounts.dnt_burner.to_account_info(), - authority: ctx.accounts.maker.to_account_info(), - }, - &[signer_seeds], - ), - mobile_fee, - )?; - } - - ctx.accounts.maker.expected_onboard_amount = 0; - ctx.accounts.mobile_hotspot_voucher.paid_mobile = true; - - Ok(()) -} diff --git a/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_verify_owner_v0.rs b/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_verify_owner_v0.rs deleted file mode 100644 index 44032dc51..000000000 --- a/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_verify_owner_v0.rs +++ /dev/null @@ -1,53 +0,0 @@ -/// Verify the owner and top them up with SOL if need be. -use std::str::FromStr; - -use anchor_lang::prelude::*; -use anchor_spl::token::Token; - -use crate::{error::ErrorCode, state::*, ECC_VERIFIER}; - -// 0.02 SOL minus the rent -const MIN_WALLET_LAMPORTS: u64 = 20_000_000; - -#[derive(Accounts)] -pub struct MobileVoucherVerifyOwnerV0<'info> { - #[account(mut)] - pub maker: Box>, - #[account(address = Pubkey::from_str(ECC_VERIFIER).unwrap())] - pub ecc_verifier: Signer<'info>, - /// CHECK: Ecc verifier is what's telling us this is the owner - #[account(mut)] - pub verified_owner: UncheckedAccount<'info>, - #[account( - mut, - has_one = maker, - // Make sure this hasn't been verified yet - constraint = mobile_hotspot_voucher.verified_owner == maker.issuing_authority, - )] - pub mobile_hotspot_voucher: Box>, - pub token_program: Program<'info, Token>, -} - -pub fn handler(ctx: Context) -> Result<()> { - ctx.accounts.mobile_hotspot_voucher.verified_owner = ctx.accounts.verified_owner.key(); - require_gte!( - MIN_WALLET_LAMPORTS, - ctx.accounts.maker.expected_onboard_amount, - ErrorCode::TooMuchBorrowed - ); - if ctx.accounts.verified_owner.lamports() < MIN_WALLET_LAMPORTS { - let maker = ctx.accounts.maker.to_account_info(); - let mut maker_lamports = maker.try_borrow_mut_lamports()?; - let maker_data_len = ctx.accounts.maker.to_account_info().data_len(); - let rent = Rent::get()?.minimum_balance(maker_data_len); - let maker_spare_lamports = **maker_lamports - rent; - if maker_spare_lamports > MIN_WALLET_LAMPORTS { - **maker_lamports -= MIN_WALLET_LAMPORTS; - let owner_wallet = ctx.accounts.verified_owner.to_account_info(); - let mut wallet_lamports = owner_wallet.try_borrow_mut_lamports()?; - **wallet_lamports += MIN_WALLET_LAMPORTS; - } - } - ctx.accounts.maker.expected_onboard_amount = 0; - Ok(()) -} diff --git a/programs/helium-entity-manager/src/instructions/mobile_voucher/mod.rs b/programs/helium-entity-manager/src/instructions/mobile_voucher/mod.rs deleted file mode 100644 index 1fb9830a6..000000000 --- a/programs/helium-entity-manager/src/instructions/mobile_voucher/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub mod initialize_mobile_hotspot_voucher_v0; -pub mod issue_mobile_hotspot_v0; -pub mod mobile_voucher_pay_dc_v0; -pub mod mobile_voucher_pay_mobile_v0; -pub mod mobile_voucher_verify_owner_v0; - -pub use initialize_mobile_hotspot_voucher_v0::*; -pub use issue_mobile_hotspot_v0::*; -pub use mobile_voucher_pay_dc_v0::*; -pub use mobile_voucher_pay_mobile_v0::*; -pub use mobile_voucher_verify_owner_v0::*; diff --git a/programs/helium-entity-manager/src/instructions/mod.rs b/programs/helium-entity-manager/src/instructions/mod.rs index d6e1b5c06..98008871e 100644 --- a/programs/helium-entity-manager/src/instructions/mod.rs +++ b/programs/helium-entity-manager/src/instructions/mod.rs @@ -5,13 +5,11 @@ pub mod initialize_maker_escrow_v0; pub mod initialize_maker_v0; pub mod initialize_rewardable_entity_config_v0; pub mod issue_data_only_entity_v0; -pub mod issue_entity; pub mod issue_entity_v0; pub mod issue_iot_operations_fund_v0; pub mod issue_not_emitted_entity_v0; pub mod issue_program_entity_v0; pub mod maker_lend_v0; -pub mod mobile_voucher; pub mod onboard_data_only_iot_hotspot_v0; pub mod onboard_data_only_mobile_hotspot_v0; pub mod onboard_iot_hotspot_v0; @@ -28,22 +26,20 @@ pub mod update_maker_tree_v0; pub mod update_maker_v0; pub mod update_mobile_info_v0; pub mod update_rewardable_entity_config_v0; +pub mod voucher; pub use approve_maker_v0::*; pub use approve_program_v0::*; pub use initialize_data_only_v0::*; -pub use initialize_data_only_v0::*; pub use initialize_maker_escrow_v0::*; pub use initialize_maker_v0::*; pub use initialize_rewardable_entity_config_v0::*; pub use issue_data_only_entity_v0::*; -pub use issue_entity::*; pub use issue_entity_v0::*; pub use issue_iot_operations_fund_v0::*; pub use issue_not_emitted_entity_v0::*; pub use issue_program_entity_v0::*; pub use maker_lend_v0::*; -pub use mobile_voucher::*; pub use onboard_data_only_iot_hotspot_v0::*; pub use onboard_data_only_mobile_hotspot_v0::*; pub use onboard_iot_hotspot_v0::*; @@ -60,3 +56,4 @@ pub use update_maker_tree_v0::*; pub use update_maker_v0::*; pub use update_mobile_info_v0::*; pub use update_rewardable_entity_config_v0::*; +pub use voucher::*; diff --git a/programs/helium-entity-manager/src/instructions/mobile_voucher/initialize_mobile_hotspot_voucher_v0.rs b/programs/helium-entity-manager/src/instructions/voucher/initialize_mobile_hotspot_voucher_v0.rs similarity index 91% rename from programs/helium-entity-manager/src/instructions/mobile_voucher/initialize_mobile_hotspot_voucher_v0.rs rename to programs/helium-entity-manager/src/instructions/voucher/initialize_mobile_hotspot_voucher_v0.rs index d9e888146..efb4a2085 100644 --- a/programs/helium-entity-manager/src/instructions/mobile_voucher/initialize_mobile_hotspot_voucher_v0.rs +++ b/programs/helium-entity-manager/src/instructions/voucher/initialize_mobile_hotspot_voucher_v0.rs @@ -1,6 +1,6 @@ +use anchor_lang::{prelude::*, solana_program::hash::hash}; + use crate::state::*; -use anchor_lang::prelude::*; -use anchor_lang::solana_program::hash::hash; #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] pub struct InitializeMobileHotspotVoucherArgsV0 { @@ -60,10 +60,9 @@ pub fn handler( bump_seed: ctx.bumps["mobile_hotspot_voucher"], key_serialization: args.key_serialization, device_type: args.device_type, - paid_mobile: false, - paid_dc: false, + paid: false, + dc_paid: 0, maker: ctx.accounts.maker.key(), - verified_owner: ctx.accounts.issuing_authority.key(), }); Ok(()) } diff --git a/programs/helium-entity-manager/src/instructions/voucher/mod.rs b/programs/helium-entity-manager/src/instructions/voucher/mod.rs new file mode 100644 index 000000000..5a1de4c9e --- /dev/null +++ b/programs/helium-entity-manager/src/instructions/voucher/mod.rs @@ -0,0 +1,7 @@ +pub mod initialize_mobile_hotspot_voucher_v0; +pub mod onboard_mobile_hotspot_v1; +pub mod pay_mobile_voucher_v0; + +pub use initialize_mobile_hotspot_voucher_v0::*; +pub use onboard_mobile_hotspot_v1::*; +pub use pay_mobile_voucher_v0::*; diff --git a/programs/helium-entity-manager/src/instructions/voucher/onboard_mobile_hotspot_v1.rs b/programs/helium-entity-manager/src/instructions/voucher/onboard_mobile_hotspot_v1.rs new file mode 100644 index 000000000..9c39814e5 --- /dev/null +++ b/programs/helium-entity-manager/src/instructions/voucher/onboard_mobile_hotspot_v1.rs @@ -0,0 +1,109 @@ +use account_compression_cpi::program::SplAccountCompression; +use anchor_lang::{prelude::*, solana_program::hash::hash}; +use bubblegum_cpi::get_asset_id; +use helium_sub_daos::DaoV0; +use shared_utils::{verify_compressed_nft, VerifyCompressedNftArgs}; + +use crate::{ + error::ErrorCode, KeyToAssetV0, MakerApprovalV0, MakerV0, MobileHotspotInfoV0, + MobileHotspotVoucherV0, RewardableEntityConfigV0, MOBILE_HOTSPOT_INFO_SIZE, +}; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct OnboardMobileHotspotArgsV1 { + pub data_hash: [u8; 32], + pub creator_hash: [u8; 32], + pub root: [u8; 32], + pub index: u32, +} + +#[derive(Accounts)] +#[instruction(args: OnboardMobileHotspotArgsV1)] +pub struct OnboardMobileHotspotV1<'info> { + #[account(mut)] + pub payer: Signer<'info>, + #[account(mut)] + /// CHECK: Just getting refunded + pub refund: AccountInfo<'info>, + #[account( + has_one = dao, + )] + pub maker: Box>, + #[account( + seeds = ["maker_approval".as_bytes(), rewardable_entity_config.key().as_ref(), maker.key().as_ref()], + bump = maker_approval.bump_seed, + has_one = maker, + has_one = rewardable_entity_config, + )] + pub maker_approval: Box>, + #[account( + mut, + has_one = rewardable_entity_config, + has_one = refund, + has_one = maker, + close = refund, + constraint = mobile_voucher.paid @ ErrorCode::VoucherNotPaid, + seeds = [ + "mobile_hotspot_voucher".as_bytes(), + mobile_voucher.rewardable_entity_config.as_ref(), + &hash(&key_to_asset.entity_key[..]).to_bytes() + ], + bump = mobile_voucher.bump_seed, + )] + pub mobile_voucher: Box>, + pub rewardable_entity_config: Box>, + #[account( + init, + payer = payer, + space = MOBILE_HOTSPOT_INFO_SIZE, + seeds = [ + b"mobile_info", + rewardable_entity_config.key().as_ref(), + &hash(&key_to_asset.entity_key[..]).to_bytes() + ], + bump, + )] + pub mobile_info: Box>, + pub dao: Box>, + #[account( + has_one = dao, + constraint = get_asset_id(&merkle_tree.key(), args.index.into()) == key_to_asset.asset + )] + pub key_to_asset: Box>, + /// CHECK: The merkle tree + pub merkle_tree: UncheckedAccount<'info>, + pub hotspot_owner: Signer<'info>, + pub compression_program: Program<'info, SplAccountCompression>, + pub system_program: Program<'info, System>, +} + +pub fn handler<'info>( + ctx: Context<'_, '_, '_, 'info, OnboardMobileHotspotV1<'info>>, + args: OnboardMobileHotspotArgsV1, +) -> Result<()> { + verify_compressed_nft(VerifyCompressedNftArgs { + data_hash: args.data_hash, + creator_hash: args.creator_hash, + root: args.root, + index: args.index, + compression_program: ctx.accounts.compression_program.to_account_info(), + merkle_tree: ctx.accounts.merkle_tree.to_account_info(), + owner: ctx.accounts.hotspot_owner.key(), + delegate: ctx.accounts.hotspot_owner.key(), + proof_accounts: ctx.remaining_accounts.to_vec(), + })?; + + ctx.accounts.mobile_info.set_inner(MobileHotspotInfoV0 { + asset: ctx.accounts.key_to_asset.asset, + bump_seed: ctx.bumps["mobile_info"], + location: None, + is_full_hotspot: true, + num_location_asserts: 0, + is_active: false, + dc_onboarding_fee_paid: ctx.accounts.mobile_voucher.dc_paid, + device_type: ctx.accounts.mobile_voucher.device_type, + deployment_info: None, + }); + + Ok(()) +} diff --git a/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_pay_dc_v0.rs b/programs/helium-entity-manager/src/instructions/voucher/pay_mobile_voucher_v0.rs similarity index 52% rename from programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_pay_dc_v0.rs rename to programs/helium-entity-manager/src/instructions/voucher/pay_mobile_voucher_v0.rs index ae1833a4d..4a6f93de4 100644 --- a/programs/helium-entity-manager/src/instructions/mobile_voucher/mobile_voucher_pay_dc_v0.rs +++ b/programs/helium-entity-manager/src/instructions/voucher/pay_mobile_voucher_v0.rs @@ -1,8 +1,9 @@ -use crate::{error::ErrorCode, maker_seeds, state::*}; +use std::str::FromStr; + use anchor_lang::prelude::*; use anchor_spl::{ associated_token::AssociatedToken, - token::{Mint, Token, TokenAccount}, + token::{burn, Burn, Mint, Token, TokenAccount}, }; use circuit_breaker::{CircuitBreaker, MintWindowedCircuitBreakerV0}; use data_credits::{ @@ -18,9 +19,14 @@ use helium_sub_daos::{ program::HeliumSubDaos, DaoV0, SubDaoV0, TrackDcOnboardingFeesArgsV0, }; +use pyth_solana_receiver_sdk::price_update::PriceUpdateV2; + +use crate::{error::ErrorCode, maker_seeds, state::*, TESTING}; + +const PRICE_ORACLE: &str = "DQ4C1tzvu28cwo1roN1Wm6TW35sfJEjLh517k3ZeWevx"; #[derive(Accounts)] -pub struct MobileVoucherPayDcV0<'info> { +pub struct PayMobileVoucherV0<'info> { #[account(mut)] pub maker: Box>, #[account(mut)] @@ -34,26 +40,30 @@ pub struct MobileVoucherPayDcV0<'info> { mut, has_one = rewardable_entity_config, has_one = maker, - has_one = verified_owner, )] pub mobile_hotspot_voucher: Box>, - pub verified_owner: Signer<'info>, #[account( mut, has_one = dao, + has_one = dnt_mint, )] pub sub_dao: Box>, - /// CHECK: Checked by loading with pyth. Also double checked by the has_one on data credits instance. - pub hnt_price_oracle: AccountInfo<'info>, #[account( has_one = dc_mint, - has_one = hnt_mint + has_one = hnt_mint, )] pub dao: Box>, #[account(mut)] pub dc_mint: Box>, #[account(mut)] + pub dnt_mint: Box>, + #[account(mut)] pub hnt_mint: Box>, + /// CHECK: Checked by loading with pyth. Also double checked by the has_one on data credits instance. + #[account( + address = Pubkey::from_str(PRICE_ORACLE).unwrap() + )] + pub dnt_price: Box>, #[account( seeds=[ "dc".as_bytes(), @@ -63,15 +73,16 @@ pub struct MobileVoucherPayDcV0<'info> { bump = dc.data_credits_bump, has_one = dc_mint, has_one = hnt_price_oracle, - has_one = hnt_mint )] pub dc: Account<'info, DataCreditsV0>, + pub hnt_price_oracle: Box>, #[account( - mut, - associated_token::authority = maker, + init_if_needed, + payer = payer, associated_token::mint = hnt_mint, + associated_token::authority = maker, )] - pub burner: Account<'info, TokenAccount>, + pub hnt_burner: Box>, #[account( init_if_needed, payer = payer, @@ -79,6 +90,14 @@ pub struct MobileVoucherPayDcV0<'info> { associated_token::authority = maker, )] pub dc_burner: Box>, + #[account( + init_if_needed, + payer = payer, + associated_token::authority = maker, + associated_token::mint = dnt_mint + )] + pub dnt_burner: Account<'info, TokenAccount>, + /// CHECK: Verified by cpi #[account( mut, seeds = ["mint_windowed_breaker".as_bytes(), dc_mint.key().as_ref()], @@ -94,7 +113,7 @@ pub struct MobileVoucherPayDcV0<'info> { pub helium_sub_daos_program: Program<'info, HeliumSubDaos>, } -pub fn handler(ctx: Context) -> Result<()> { +pub fn handler(ctx: Context) -> Result<()> { let fees = ctx .accounts .rewardable_entity_config @@ -103,39 +122,34 @@ pub fn handler(ctx: Context) -> Result<()> { .ok_or(error!(ErrorCode::InvalidDeviceType))?; let dc_fee = fees.dc_onboarding_fee; - require_gte!( - // Extra decimal - dc_fee * 10, - ctx.accounts.maker.expected_onboard_amount, - ErrorCode::TooMuchBorrowed - ); - if dc_fee > 0 { - mint_data_credits_v0( - CpiContext::new_with_signer( - ctx.accounts.data_credits_program.to_account_info(), - MintDataCreditsV0 { - data_credits: ctx.accounts.dc.to_account_info(), - hnt_price_oracle: ctx.accounts.hnt_price_oracle.to_account_info(), - burner: ctx.accounts.burner.to_account_info(), - recipient_token_account: ctx.accounts.dc_burner.to_account_info(), - recipient: ctx.accounts.maker.to_account_info(), - owner: ctx.accounts.maker.to_account_info(), - hnt_mint: ctx.accounts.hnt_mint.to_account_info(), - dc_mint: ctx.accounts.dc_mint.to_account_info(), - circuit_breaker: ctx.accounts.circuit_breaker.to_account_info(), - circuit_breaker_program: ctx.accounts.circuit_breaker_program.to_account_info(), - token_program: ctx.accounts.token_program.to_account_info(), - system_program: ctx.accounts.system_program.to_account_info(), - associated_token_program: ctx.accounts.associated_token_program.to_account_info(), + if ctx.accounts.dnt_burner.amount < dc_fee { + mint_data_credits_v0( + CpiContext::new_with_signer( + ctx.accounts.data_credits_program.to_account_info(), + MintDataCreditsV0 { + data_credits: ctx.accounts.dc.to_account_info(), + hnt_price_oracle: ctx.accounts.hnt_price_oracle.to_account_info(), + burner: ctx.accounts.hnt_burner.to_account_info(), + recipient_token_account: ctx.accounts.dc_burner.to_account_info(), + recipient: ctx.accounts.maker.to_account_info(), + owner: ctx.accounts.maker.to_account_info(), + hnt_mint: ctx.accounts.hnt_mint.to_account_info(), + dc_mint: ctx.accounts.dc_mint.to_account_info(), + circuit_breaker: ctx.accounts.circuit_breaker.to_account_info(), + circuit_breaker_program: ctx.accounts.circuit_breaker_program.to_account_info(), + token_program: ctx.accounts.token_program.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + associated_token_program: ctx.accounts.associated_token_program.to_account_info(), + }, + &[maker_seeds!(ctx.accounts.maker)], + ), + MintDataCreditsArgsV0 { + hnt_amount: None, + dc_amount: Some(ctx.accounts.dc_burner.amount - dc_fee), }, - &[maker_seeds!(ctx.accounts.maker)], - ), - MintDataCreditsArgsV0 { - dc_amount: Some(dc_fee), - hnt_amount: None, - }, - )?; + )?; + } let cpi_accounts = BurnWithoutTrackingV0 { burn_accounts: BurnCommonV0 { data_credits: ctx.accounts.dc.to_account_info(), @@ -178,8 +192,63 @@ pub fn handler(ctx: Context) -> Result<()> { )?; } - ctx.accounts.maker.expected_onboard_amount = 0; - ctx.accounts.mobile_hotspot_voucher.paid_dc = true; + let dnt_fee = fees.mobile_onboarding_fee_usd; + let mobile_price_oracle = &mut ctx.accounts.dnt_price; + let current_time = Clock::get()?.unix_timestamp; + require_gte!( + mobile_price_oracle + .price_message + .publish_time + .saturating_add(if TESTING { 6000000 } else { 10 * 60 }.into()), + current_time, + ErrorCode::PythPriceFeedStale + ); + let mobile_price = mobile_price_oracle.price_message.ema_price; + // Remove the confidence from the price to use the most conservative price + // https://docs.pyth.network/price-feeds/solana-price-feeds/best-practices#confidence-intervals + let mobile_price_with_conf = mobile_price + .checked_sub( + i64::try_from( + mobile_price_oracle + .price_message + .ema_conf + .checked_mul(2) + .unwrap(), + ) + .unwrap(), + ) + .unwrap(); + // Exponent is a negative number, likely -8 + // Since the price is multiplied by an extra 10^8, and we're dividing by that price, need to also multiply + // by the exponent + let exponent_dec = 10_u64 + .checked_pow(u32::try_from(-mobile_price_oracle.price_message.exponent).unwrap()) + .ok_or_else(|| error!(ErrorCode::ArithmeticError))?; + + require_gt!(mobile_price_with_conf, 0); + let mobile_fee = dnt_fee + .checked_mul(exponent_dec) + .unwrap() + .checked_div(mobile_price_with_conf.try_into().unwrap()) + .unwrap(); + if mobile_fee > 0 { + let signer_seeds = maker_seeds!(ctx.accounts.maker); + burn( + CpiContext::new_with_signer( + ctx.accounts.token_program.to_account_info(), + Burn { + mint: ctx.accounts.dnt_mint.to_account_info(), + from: ctx.accounts.dnt_burner.to_account_info(), + authority: ctx.accounts.maker.to_account_info(), + }, + &[signer_seeds], + ), + mobile_fee, + )?; + } + + ctx.accounts.mobile_hotspot_voucher.paid = true; + ctx.accounts.mobile_hotspot_voucher.dc_paid = dc_fee; Ok(()) } diff --git a/programs/helium-entity-manager/src/lib.rs b/programs/helium-entity-manager/src/lib.rs index a8c084c5d..560600617 100644 --- a/programs/helium-entity-manager/src/lib.rs +++ b/programs/helium-entity-manager/src/lib.rs @@ -180,17 +180,8 @@ pub mod helium_entity_manager { temp_standardize_entity::handler(ctx, args) } - pub fn maker_lend_v0<'info>( - ctx: Context<'_, '_, '_, 'info, MakerLendV0<'info>>, - args: MakerLendArgsV0, - ) -> Result<()> { - maker_lend_v0::handler(ctx, args) - } - - pub fn mobile_voucher_pay_mobile_v0<'info>( - ctx: Context<'_, '_, '_, 'info, MobileVoucherPayMobileV0<'info>>, - ) -> Result<()> { - mobile_voucher_pay_mobile_v0::handler(ctx) + pub fn maker_lend_v0<'info>(ctx: Context<'_, '_, '_, 'info, MakerLendV0<'info>>) -> Result<()> { + maker_lend_v0::handler(ctx) } pub fn initialize_mobile_hotspot_voucher_v0<'info>( @@ -200,12 +191,6 @@ pub mod helium_entity_manager { initialize_mobile_hotspot_voucher_v0::handler(ctx, args) } - pub fn mobile_voucher_verify_owner_v0<'info>( - ctx: Context<'_, '_, '_, 'info, MobileVoucherVerifyOwnerV0<'info>>, - ) -> Result<()> { - mobile_voucher_verify_owner_v0::handler(ctx) - } - pub fn initialize_maker_escrow_v0<'info>( ctx: Context<'_, '_, '_, 'info, InitializeMakerEscrowV0<'info>>, args: InitializeMakerEscrowArgsV0, @@ -213,17 +198,11 @@ pub mod helium_entity_manager { initialize_maker_escrow_v0::handler(ctx, args) } - pub fn mobile_voucher_pay_dc_v0<'info>( - ctx: Context<'_, '_, '_, 'info, MobileVoucherPayDcV0<'info>>, - ) -> Result<()> { - mobile_voucher_pay_dc_v0::handler(ctx) - } - - pub fn issue_mobile_hotspot_v0<'info>( - ctx: Context<'_, '_, '_, 'info, IssueMobileHotspotV0<'info>>, - args: IssueEntityArgsV0, + pub fn onboard_mobile_hotspot_v1<'info>( + ctx: Context<'_, '_, '_, 'info, OnboardMobileHotspotV1<'info>>, + args: OnboardMobileHotspotArgsV1, ) -> Result<()> { - issue_mobile_hotspot_v0::handler(ctx, args) + onboard_mobile_hotspot_v1::handler(ctx, args) } pub fn onboard_data_only_mobile_hotspot_v0<'info>( diff --git a/programs/helium-entity-manager/src/state.rs b/programs/helium-entity-manager/src/state.rs index cb44cc33c..f3a310e27 100644 --- a/programs/helium-entity-manager/src/state.rs +++ b/programs/helium-entity-manager/src/state.rs @@ -20,26 +20,13 @@ pub struct MobileHotspotVoucherV0 { pub entity_key: Vec, pub key_serialization: KeySerialization, pub device_type: MobileDeviceTypeV0, - pub paid_mobile: bool, - pub paid_dc: bool, - /// Maker key if not verified. Otherwise the verified owner of the hotspot - /// through the sig verifier. - pub verified_owner: Pubkey, + pub paid: bool, + pub dc_paid: u64, /// The wallet that paid the rent for this, will be refunded pub refund: Pubkey, pub bump_seed: u8, } -#[account] -#[derive(Default)] -pub struct IotHotspotVoucherV0 { - pub rewardable_entity_config: Pubkey, - pub entity_key: Vec, - pub bump_seed: u8, - pub key_serialization: KeySerialization, - pub paid_dc: bool, -} - #[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, Default, PartialEq)] pub enum MobileDeviceTypeV0 { #[default] @@ -173,11 +160,14 @@ pub struct MakerV0 { pub merkle_tree: Pubkey, pub collection_bump_seed: u8, pub dao: Pubkey, - // Outstanding loans in USDC. Used to check that the - // maker loan made is an appropriate amount during onboarding - // Given write locking on accounts, this should never be above what is actually - // used in the current tx - pub expected_onboard_amount: u64, + pub topup_amounts: Vec, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct TopupAmountV0 { + pub mint: Pubkey, + pub threshold: u64, + pub source_amount: u64, } #[account] diff --git a/tests/helium-entity-manager.ts b/tests/helium-entity-manager.ts index 8075a9716..b32e2fb27 100644 --- a/tests/helium-entity-manager.ts +++ b/tests/helium-entity-manager.ts @@ -18,6 +18,7 @@ import { createAtaAndMint, createMint, createMintInstructions, + mintTo, proofArgsAndAccounts, sendInstructions, toBN, @@ -93,12 +94,10 @@ import { ConversionEscrow } from "../target/types/conversion_escrow"; import { keyToAssetKey, mobileInfoKey, - payMobileVoucherDc, - payMobileVoucherMobile, + topUpMaker, } from "@helium/helium-entity-manager-sdk"; import { VoterStakeRegistry } from "../target/types/voter_stake_registry"; import { init as initVsr } from "../packages/voter-stake-registry-sdk/src"; -import { populateMissingDraftInfo } from "@helium/spl-utils"; chai.use(chaiAsPromised); @@ -122,6 +121,11 @@ describe("helium-entity-manager", () => { let hntMint: PublicKey; let dntMint: PublicKey; let activeDeviceAuthority: Keypair; + let topupAmounts: { + mint: PublicKey; + threshold: anchor.BN; + sourceAmount: anchor.BN; + }[]; beforeEach(async () => { pythProgram = new Program( @@ -193,6 +197,19 @@ describe("helium-entity-manager", () => { // Add some padding for onboards numTokens: MAKER_STAKING_FEE.mul(new BN(2)).add(new BN(100000000000)), })); + + topupAmounts = [ + { + mint: hntMint, + threshold: new BN(100000000), + sourceAmount: new BN(10000000), + }, + { + mint: dntMint, + threshold: new BN(1000000), + sourceAmount: new BN(10000000), + }, + ]; }); it("issues iot operations fund", async () => { @@ -645,7 +662,8 @@ describe("helium-entity-manager", () => { hemProgram, provider, rewardableEntityConfig, - dao + dao, + topupAmounts ); const account = await hemProgram.account.makerV0.fetch(maker); @@ -669,7 +687,8 @@ describe("helium-entity-manager", () => { hemProgram, provider, rewardableEntityConfig, - dao + dao, + topupAmounts ); const { @@ -783,10 +802,11 @@ describe("helium-entity-manager", () => { hemProgram, provider, rewardableEntityConfig, - dao + dao, + topupAmounts ); - await initTestMaker(hemProgram, provider, rewardableEntityConfig, dao); + await initTestMaker(hemProgram, provider, rewardableEntityConfig, dao, topupAmounts); await dcProgram.methods .mintDataCreditsV0({ @@ -930,436 +950,130 @@ describe("helium-entity-manager", () => { .rpcAndKeys({ skipPreflight: true })); }); - it("allows verifying owner while topping off sol", async () => { - const owner = Keypair.generate(); - const myUsdcAccount = getAssociatedTokenAddressSync(usdcMint, me); - const instructions = [ - createAssociatedTokenAccountIdempotentInstruction( - me, - myUsdcAccount, + it("fits top ups in a transaction", async () => { + const txDrafts = await topUpMaker({ + program: hemProgram, + ceProgram: conversionEscrowProgram, + maker, + payer: me, + }); + const recentBlockhash = (await provider.connection.getLatestBlockhash()) + .blockhash; + const txs = await Promise.all( + txDrafts.map((tx) => + toVersionedTx({ + ...tx, + feePayer: me, + recentBlockhash, + }) + ) + ); + for (const tx of txs) { + const signed = await provider.wallet.signTransaction(tx); + console.log("signed", signed); + const serialized = signed.serialize(); + expect(serialized.length).to.be.lessThan(1232); + } + }); + + it("lends USDC to allow top ups", async () => { + await createAtaAndMint( + provider, + usdcMint, + toBN(1000, 6).toNumber(), + maker + ); + const myAcc = await createAtaAndMint( + provider, + usdcMint, + toBN(1, 6).toNumber(), + me + ); + const mobileAcc = getAssociatedTokenAddressSync(dntMint, me); + const preBalance = await provider.connection.getTokenAccountBalance( + myAcc + ); + const priceMobile = await pythProgram.account.priceUpdateV2.fetch( + MOBILE_PYTH_PRICE_FEED + ); + + const mobileFloorValue = + priceMobile.priceMessage.emaPrice + .sub(priceMobile.priceMessage.emaConf.mul(new anchor.BN(2))) + .toNumber() * + 10 ** priceMobile.priceMessage.exponent; + console.log("mobileFloor", mobileFloorValue) + const { instruction, pubkeys } = await hemProgram.methods + .makerLendV0() + .accounts({ + maker, + sourceMint: usdcMint, + targetOracle: MOBILE_PYTH_PRICE_FEED, + conversionEscrow, + destination: getAssociatedTokenAddressSync(usdcMint, me), + repayAccount: getAssociatedTokenAddressSync(dntMint, maker, true), + }) + .prepare(); + + const ixs = [ + instruction, + createTransferInstruction( + mobileAcc, + pubkeys.repayAccount!, me, - usdcMint + BigInt(toBN(10 / mobileFloorValue, 6).toString()) ), - await hemProgram.methods - .makerLendV0({ - amount: toBN(0.02, 9), - }) - .accounts({ - maker, - targetOracle: SOL_PYTH_PRICE_FEED, - conversionEscrow, - destination: myUsdcAccount, - repayAccount: maker, - usdcMint, - }) - .instruction(), - // Repay - SystemProgram.transfer({ - fromPubkey: me, - toPubkey: maker, - lamports: 0.02 * LAMPORTS_PER_SOL, - }), await conversionEscrowProgram.methods .checkRepayV0() .accounts({ conversionEscrow, - repayAccount: maker, - }) - .instruction(), - await hemProgram.methods - .mobileVoucherVerifyOwnerV0() - .accounts({ - verifiedOwner: owner.publicKey, - eccVerifier: eccVerifier.publicKey, - mobileHotspotVoucher, + repayAccount: pubkeys.repayAccount, }) .instruction(), ]; - await sendInstructions(provider, instructions, [eccVerifier]); - const voucher = await hemProgram.account.mobileHotspotVoucherV0.fetch( - mobileHotspotVoucher! + + console.log("sending"); + await sendInstructions(provider, ixs); + console.log("sent"); + + const postBalance = await provider.connection.getTokenAccountBalance( + myAcc ); - expect(voucher.verifiedOwner.toBase58()).to.eq( - owner.publicKey.toBase58() + console.log(postBalance.value.uiAmount, + preBalance.value.uiAmount! + 10 * (1 + slippageBps / 10000) + ); + expect(postBalance.value.uiAmount).to.be.eq( + preBalance.value.uiAmount! + + Number((10 * (1 + slippageBps / 10000)).toFixed(3)) ); - const lamps = ( - await provider.connection.getAccountInfo(owner.publicKey) - )?.lamports; - expect(lamps).to.be.gt(0.02 * LAMPORTS_PER_SOL); }); - describe("with verified owner", async () => { - let lut: PublicKey; - beforeEach(async () => { - await hemProgram.methods - .mobileVoucherVerifyOwnerV0() - .accounts({ - verifiedOwner: me, - eccVerifier: eccVerifier.publicKey, - mobileHotspotVoucher, - }) - .signers([eccVerifier]) - .rpc({ skipPreflight: true }); - - const slot = await provider.connection.getSlot(); - const [lookupTableInst, lookupTableAddress] = - AddressLookupTableProgram.createLookupTable({ - authority: me, - payer: me, - recentSlot: slot, - }); - let isFirst = true; - for (const addresses of chunks([...LUT_ACCOUNTS, usdcMint, rewardableEntityConfig, dntMint, eccVerifier.publicKey], 20)) { - await sendInstructions( - provider, - [ - isFirst ? lookupTableInst : undefined, - AddressLookupTableProgram.extendLookupTable({ - payer: me, - authority: me, - lookupTable: lookupTableAddress, - addresses, - }), - ].filter(truthy) - ); - isFirst = false; - } - lut = lookupTableAddress; - }); - it("allows paying mobile", async () => { - const makerDntAccount = getAssociatedTokenAddressSync( - dntMint, - maker, - true - ); - const pythDataMobile = await pythProgram.account.priceUpdateV2.fetch( - MOBILE_PYTH_PRICE_FEED - ); - const usdPerMobile = - pythDataMobile.priceMessage.emaPrice - .sub(pythDataMobile.priceMessage.emaConf.mul(new anchor.BN(2))) - .toNumber() * - 10 ** pythDataMobile.priceMessage.exponent; - - const mobileAmount = toBN(20 / usdPerMobile, 6); - const myUsdcAccount = getAssociatedTokenAddressSync(usdcMint, me); - const instructions = [ - createAssociatedTokenAccountIdempotentInstruction( - me, - myUsdcAccount, - me, - usdcMint - ), - await hemProgram.methods - .makerLendV0({ - amount: toBN(20, 6), - }) - .accounts({ - maker, - targetOracle: MOBILE_PYTH_PRICE_FEED, - conversionEscrow, - destination: myUsdcAccount, - repayAccount: makerDntAccount, - usdcMint, - }) - .instruction(), - // Repay - createAssociatedTokenAccountIdempotentInstruction( - me, - makerDntAccount, - maker, - dntMint - ), - createTransferInstruction( - getAssociatedTokenAddressSync(dntMint, me), - makerDntAccount, - me, - BigInt(mobileAmount.toString()) - ), - await conversionEscrowProgram.methods - .checkRepayV0() - .accounts({ - conversionEscrow, - repayAccount: makerDntAccount, - }) - .instruction(), - await hemProgram.methods - .mobileVoucherPayMobileV0() - .accounts({ - mobileHotspotVoucher, - dntPrice: MOBILE_PYTH_PRICE_FEED, - }) - .instruction(), - ]; - await sendInstructions(provider, instructions); - const voucher = await hemProgram.account.mobileHotspotVoucherV0.fetch( - mobileHotspotVoucher! - ); - expect(voucher.paidMobile).to.be.true; - }); - - it("allows paying DC", async () => { - const makerHntAccount = getAssociatedTokenAddressSync( - hntMint, - maker, - true - ); - const myUsdcAccount = getAssociatedTokenAddressSync(usdcMint, me); - const instructions = [ - createAssociatedTokenAccountIdempotentInstruction( - me, - myUsdcAccount, - me, - usdcMint - ), - createAssociatedTokenAccountIdempotentInstruction( - me, - makerHntAccount, - maker, - hntMint - ), - await hemProgram.methods - .makerLendV0({ - amount: toBN(10, 6), - }) - .accounts({ - maker, - targetOracle: HNT_PYTH_PRICE_FEED, - conversionEscrow, - destination: myUsdcAccount, - repayAccount: makerHntAccount, - usdcMint, - }) - .instruction(), - // Repay in HNT - createTransferInstruction( - getAssociatedTokenAddressSync(hntMint, me), - makerHntAccount, - me, - BigInt(10000000000) - ), - await conversionEscrowProgram.methods - .checkRepayV0() - .accounts({ - conversionEscrow, - repayAccount: makerHntAccount, - }) - .instruction(), - await hemProgram.methods - .mobileVoucherPayDcV0() - .accounts({ - mobileHotspotVoucher, - }) - .instruction(), - ]; - await sendInstructions(provider, instructions); - const voucher = await hemProgram.account.mobileHotspotVoucherV0.fetch( - mobileHotspotVoucher! - ); - expect(voucher.paidDc).to.be.true; - }); - - it("fits a jupiter transaction in with flash lend mobile", async () => { - const { instructions, addressLookupTableAddresses } = - await payMobileVoucherMobile({ - program: hemProgram, - ceProgram: conversionEscrowProgram, - mobileHotspotVoucher: mobileHotspotVoucher!, - verifiedOwner: me, - maker, - payer: me, - }); - const tx = await toVersionedTx( - { - instructions, - addressLookupTableAddresses: [ - ...addressLookupTableAddresses, - lut, - ], - feePayer: me, - recentBlockhash: (await provider.connection.getLatestBlockhash()).blockhash, - } - ); - const signed = await provider.wallet.signTransaction(tx); - console.log("signed", signed) - const serialized = signed.serialize(); - expect(serialized.length).to.be.lessThan(1232); - }); - - it("fits a jupiter transaction in with flash lend dc", async () => { - const { instructions, addressLookupTableAddresses } = - await payMobileVoucherDc({ - program: hemProgram, - ceProgram: conversionEscrowProgram, - mobileHotspotVoucher: mobileHotspotVoucher!, - verifiedOwner: me, - maker, - payer: me, - }); - const tx = await toVersionedTx( - await populateMissingDraftInfo(provider.connection, { - instructions, - addressLookupTableAddresses: [ - ...addressLookupTableAddresses, - HELIUM_COMMON_LUT, - lut, - ], - feePayer: me, - }) - ); - const signed = await provider.wallet.signTransaction(tx); - const serialized = signed.serialize(); - expect(serialized.length).to.be.lessThan(1232); - }); - describe("when fully paid", () => { - beforeEach(async () => { - const makerDntAccount = getAssociatedTokenAddressSync( - dntMint, - maker, - true - ); - const pythDataMobile = await pythProgram.account.priceUpdateV2.fetch( - MOBILE_PYTH_PRICE_FEED - ); - const usdPerMobile = - pythDataMobile.priceMessage.emaPrice - .sub(pythDataMobile.priceMessage.emaConf.mul(new anchor.BN(2))) - .toNumber() * - 10 ** pythDataMobile.priceMessage.exponent; - const mobileAmount = toBN(20 / usdPerMobile, 6); - const myUsdcAccount = getAssociatedTokenAddressSync(usdcMint, me); - const instructions0 = [ - createAssociatedTokenAccountIdempotentInstruction( - me, - myUsdcAccount, - me, - usdcMint - ), - await hemProgram.methods - .makerLendV0({ - amount: toBN(20, 6), - }) - .accounts({ - maker, - targetOracle: MOBILE_PYTH_PRICE_FEED, - conversionEscrow, - destination: myUsdcAccount, - repayAccount: makerDntAccount, - usdcMint, - }) - .instruction(), - // Repay - createAssociatedTokenAccountIdempotentInstruction( - me, - makerDntAccount, - maker, - dntMint - ), - createTransferInstruction( - getAssociatedTokenAddressSync(dntMint, me), - makerDntAccount, - me, - BigInt(mobileAmount.toString()) - ), - await conversionEscrowProgram.methods - .checkRepayV0() - .accounts({ - conversionEscrow, - repayAccount: makerDntAccount, - }) - .instruction(), - await hemProgram.methods - .mobileVoucherPayMobileV0() - .accounts({ - mobileHotspotVoucher, - dntPrice: MOBILE_PYTH_PRICE_FEED, - }) - .instruction(), - ]; - await sendInstructions(provider, instructions0); - const makerHntAccount = getAssociatedTokenAddressSync( - hntMint, - maker, - true - ); - const instructions = [ - createAssociatedTokenAccountIdempotentInstruction( - me, - myUsdcAccount, - me, - usdcMint - ), - createAssociatedTokenAccountIdempotentInstruction( - me, - makerHntAccount, - maker, - hntMint - ), - await hemProgram.methods - .makerLendV0({ - amount: toBN(10, 6), - }) - .accounts({ - maker, - targetOracle: HNT_PYTH_PRICE_FEED, - conversionEscrow, - destination: myUsdcAccount, - repayAccount: makerHntAccount, - usdcMint, - }) - .instruction(), - // Repay in HNT - createTransferInstruction( - getAssociatedTokenAddressSync(hntMint, me), - makerHntAccount, - me, - BigInt(10000000000) - ), - await conversionEscrowProgram.methods - .checkRepayV0() - .accounts({ - conversionEscrow, - repayAccount: makerHntAccount, - }) - .instruction(), - await hemProgram.methods - .mobileVoucherPayDcV0() - .accounts({ - mobileHotspotVoucher, - }) - .instruction(), - ]; - await sendInstructions(provider, instructions); - }); - - it("issues a mobile hotspot", async () => { - const method = await hemProgram.methods - .issueMobileHotspotV0({ - entityKey: Buffer.from(bs58.decode(ecc)), - }) - .accounts({ - issueEntityCommon: { - maker, - recipient: me, - dao, - }, - mobileInfo: mobileInfoKey( - rewardableEntityConfig, - Buffer.from(bs58.decode(ecc)) - )[0], - voucher: mobileHotspotVoucher, - }); - await method.rpc({ skipPreflight: true }); - const { mobileInfo } = await method.pubkeys(); - - const mobileInfoAcc = - await hemProgram.account.mobileHotspotInfoV0.fetch(mobileInfo!); - expect(Boolean(mobileInfoAcc)).to.be.true; - const subDaoAcc = await hsdProgram.account.subDaoV0.fetch(subDao); - expect(subDaoAcc.dcOnboardingFeesPaid.toNumber()).to.be.eq(1000000); - }); - }); - }); + // it("issues a mobile hotspot", async () => { + // const method = await hemProgram.methods + // .onboardMobileHotspotV1({ + // entityKey: Buffer.from(bs58.decode(ecc)), + // }) + // .accounts({ + // issueEntityCommon: { + // maker, + // recipient: me, + // dao, + // }, + // mobileInfo: mobileInfoKey( + // rewardableEntityConfig, + // Buffer.from(bs58.decode(ecc)) + // )[0], + // voucher: mobileHotspotVoucher, + // }); + // await method.rpc({ skipPreflight: true }); + // const { mobileInfo } = await method.pubkeys(); + + // const mobileInfoAcc = + // await hemProgram.account.mobileHotspotInfoV0.fetch(mobileInfo!); + // expect(Boolean(mobileInfoAcc)).to.be.true; + // const subDaoAcc = await hsdProgram.account.subDaoV0.fetch(subDao); + // expect(subDaoAcc.dcOnboardingFeesPaid.toNumber()).to.be.eq(1000000); + // }); }); it("issues a mobile hotspot", async () => { @@ -1656,7 +1370,7 @@ describe("helium-entity-manager", () => { dao ); - await initTestMaker(hemProgram, provider, rewardableEntityConfig, dao); + await initTestMaker(hemProgram, provider, rewardableEntityConfig, dao, topupAmounts); await dcProgram.methods .mintDataCreditsV0({ diff --git a/tests/utils/fixtures.ts b/tests/utils/fixtures.ts index 35395e37f..17e83f8b6 100644 --- a/tests/utils/fixtures.ts +++ b/tests/utils/fixtures.ts @@ -123,7 +123,12 @@ export const initTestMaker = async ( program: Program, provider: anchor.AnchorProvider, rewardableEntityConfig: PublicKey, - dao: PublicKey + dao: PublicKey, + topupAmounts: { + mint: PublicKey; + threshold: anchor.BN; + sourceAmount: anchor.BN; + }[] = [] ): Promise<{ authority: PublicKey; makerKeypair: Keypair; @@ -157,6 +162,7 @@ export const initTestMaker = async ( issuingAuthority: makerKeypair.publicKey, name, metadataUrl: DEFAULT_METADATA_URL, + topupAmounts, }) .accounts({ dao, From abb1e4dfd560e371e2dab526c0717a56ffe3f13b Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Wed, 13 Nov 2024 13:34:45 -0800 Subject: [PATCH 12/12] Working new form of mobile voucher. Still not sure I like it --- packages/distributor-oracle/src/client.ts | 15 +- .../src/functions/onboardMobileHotspot.ts | 116 ++++++++-- .../src/resolvers.ts | 25 ++- .../instructions/voucher/issue_entity_v1.rs | 211 ++++++++++++++++++ .../src/instructions/voucher/mod.rs | 2 + .../voucher/onboard_mobile_hotspot_v1.rs | 7 +- .../voucher/pay_mobile_voucher_v0.rs | 51 ++++- programs/helium-entity-manager/src/lib.rs | 14 ++ tests/helium-entity-manager.ts | 186 ++++++++++----- 9 files changed, 526 insertions(+), 101 deletions(-) create mode 100644 programs/helium-entity-manager/src/instructions/voucher/issue_entity_v1.rs diff --git a/packages/distributor-oracle/src/client.ts b/packages/distributor-oracle/src/client.ts index f3923028c..5a13fa640 100644 --- a/packages/distributor-oracle/src/client.ts +++ b/packages/distributor-oracle/src/client.ts @@ -137,7 +137,7 @@ export async function getPendingRewards( >("KeyToAssetV0", account.data), }), true, - forceRequery + false ); keyToAssets.forEach((kta, index) => { if (!kta?.info) { @@ -277,18 +277,22 @@ export async function formBulkTransactions({ assetEndpoint || provider.connection.rpcEndpoint, assets ); + const willPay = (await axios.get( + `${lazyDistributorAcc.oracles[0].url}/will-pay-recipient` + )).data.willPay; let ixsPerAsset = await Promise.all( recipientAccs.map(async (recipientAcc, idx) => { if (!recipientAcc) { return [ - await ( + await( await initializeCompressionRecipient({ program: lazyDistributorProgram, assetId: assets![idx], lazyDistributor, assetEndpoint, owner: wallet, - payer, + // Temporarily set oracle as the payer to subsidize new HNT wallets. + payer: willPay ? lazyDistributorAcc.oracles[0].oracle : payer, getAssetFn: () => Promise.resolve(compressionAssetAccs![idx]), // cache result so we don't hit again getAssetProofFn: assetProofsById ? () => @@ -522,6 +526,9 @@ export async function formTransaction({ const recipientAcc = await lazyDistributorProgram.account.recipientV0.fetchNullable(recipient); if (!recipientAcc) { + const willPay = ( + await axios.get(`${lazyDistributorAcc.oracles[0].url}/will-pay-recipient`) + ).data.willPay; let initRecipientIx; if (assetAcc.compression.compressed) { initRecipientIx = await ( @@ -531,7 +538,7 @@ export async function formTransaction({ lazyDistributor, assetEndpoint, owner: wallet, - payer, + payer: willPay ? lazyDistributorAcc.oracles[0].oracle : payer, getAssetFn: () => Promise.resolve(assetAcc), // cache result so we don't hit again getAssetProofFn, }) diff --git a/packages/helium-entity-manager-sdk/src/functions/onboardMobileHotspot.ts b/packages/helium-entity-manager-sdk/src/functions/onboardMobileHotspot.ts index 963c24feb..3107b9f13 100644 --- a/packages/helium-entity-manager-sdk/src/functions/onboardMobileHotspot.ts +++ b/packages/helium-entity-manager-sdk/src/functions/onboardMobileHotspot.ts @@ -7,7 +7,7 @@ import { import { PublicKey } from "@solana/web3.js"; import BN from "bn.js"; import { keyToAssetForAsset } from "../helpers"; -import { mobileInfoKey } from "../pdas"; +import { mobileHotspotVoucherKey, mobileInfoKey } from "../pdas"; import { MobileDeploymentInfoV0 } from ".."; export async function onboardMobileHotspot({ @@ -51,28 +51,96 @@ export async function onboardMobileHotspot({ keyToAsset.entityKey ); const makerAcc = await program.account.makerV0.fetchNullable(maker); + const voucherK = mobileHotspotVoucherKey( + rewardableEntityConfig, + keyToAsset.entityKey + )[0]; + const voucher = await program.account.mobileHotspotVoucherV0.fetchNullable( + voucherK + ); + + if (voucher) { + return program.methods + .onboardMobileHotspotV1({ + ...args, + deploymentInfo: deploymentInfo as any, + }) + .preInstructions([ + await program.methods + .payMobileVoucherV0({ + ...args, + }) + .accounts({ + ...accounts, + payer, + rewardableEntityConfig, + hotspotOwner: owner, + maker, + dao, + mobileVoucher: voucherK, + keyToAsset: keyToAssetKey, + }) + .instruction(), + ]) + .postInstructions( + typeof location == "undefined" + ? [] + : [ + await program.methods + .updateMobileInfoV0({ + ...args, + location, + deploymentInfo: null, + }) + .accounts({ + ...accounts, + payer, + rewardableEntityConfig, + hotspotOwner: owner, + dao, - return program.methods - .onboardMobileHotspotV0({ - ...args, - location: typeof location == "undefined" ? null : location, - deviceType: { - [deviceType]: {}, - } as any, - deploymentInfo: deploymentInfo as any, - }) - .accounts({ - // hotspot: assetId, - ...accounts, - dcFeePayer, - payer, - rewardableEntityConfig, - hotspotOwner: owner, - mobileInfo: info, - maker, - dao, - issuingAuthority: makerAcc?.issuingAuthority, - keyToAsset: keyToAssetKey, - }) - .remainingAccounts(remainingAccounts); + mobileInfo: info, + }) + .instruction(), + ] + ) + .accounts({ + // hotspot: assetId, + ...accounts, + payer, + rewardableEntityConfig, + hotspotOwner: owner, + mobileInfo: info, + maker, + dao, + mobileVoucher: voucherK, + refund: voucher.refund, + keyToAsset: keyToAssetKey, + }) + .remainingAccounts(remainingAccounts); + } else { + return program.methods + .onboardMobileHotspotV0({ + ...args, + location: typeof location == "undefined" ? null : location, + deviceType: { + [deviceType]: {}, + } as any, + deploymentInfo: deploymentInfo as any, + }) + .accounts({ + // hotspot: assetId, + ...accounts, + dcFeePayer, + payer, + rewardableEntityConfig, + hotspotOwner: owner, + mobileInfo: info, + maker, + dao, + issuingAuthority: makerAcc?.issuingAuthority, + keyToAsset: keyToAssetKey, + }) + .remainingAccounts(remainingAccounts); + } } diff --git a/packages/helium-entity-manager-sdk/src/resolvers.ts b/packages/helium-entity-manager-sdk/src/resolvers.ts index 1de1c86fa..362644e3b 100644 --- a/packages/helium-entity-manager-sdk/src/resolvers.ts +++ b/packages/helium-entity-manager-sdk/src/resolvers.ts @@ -55,7 +55,7 @@ export const heliumEntityManagerResolvers = combineResolvers( } }), resolveIndividual(async ({ idlIx, path, args, accounts }) => { - const dao = accounts.dao || (accounts.issueEntityCommon as Accounts)?.dao + const dao = accounts.dao || (accounts.issueEntityCommon as Accounts)?.dao; if ( path[path.length - 1] === "keyToAsset" && args[args.length - 1] && @@ -134,8 +134,11 @@ export const heliumEntityManagerResolvers = combineResolvers( }), resolveIndividual(async ({ path, args, provider, accounts }) => { const merkleTree = - accounts.merkleTree || (accounts.issueEntityCommon as Accounts)?.merkleTree; - const keyToAsset = accounts.keyToAsset || (accounts.issueEntityCommon as Accounts)?.keyToAsset; + accounts.merkleTree || + (accounts.issueEntityCommon as Accounts)?.merkleTree; + const keyToAsset = + accounts.keyToAsset || + (accounts.issueEntityCommon as Accounts)?.keyToAsset; if ( path[path.length - 1] === "mobileInfo" && merkleTree && @@ -219,9 +222,21 @@ export const heliumEntityManagerResolvers = combineResolvers( owner: "conversionEscrow", }), ataResolver({ - instruction: "mobileVoucherPayDcV0", + instruction: "payMobileVoucherV0", mint: "hntMint", - account: "burner", + account: "hntBurner", + owner: "maker", + }), + ataResolver({ + instruction: "payMobileVoucherV0", + mint: "dcMint", + account: "dcBurner", + owner: "maker", + }), + ataResolver({ + instruction: "payMobileVoucherV0", + mint: "dntMint", + account: "dntBurner", owner: "maker", }), ataResolver({ diff --git a/programs/helium-entity-manager/src/instructions/voucher/issue_entity_v1.rs b/programs/helium-entity-manager/src/instructions/voucher/issue_entity_v1.rs new file mode 100644 index 000000000..21b583cea --- /dev/null +++ b/programs/helium-entity-manager/src/instructions/voucher/issue_entity_v1.rs @@ -0,0 +1,211 @@ +use std::{cmp::min, str::FromStr}; + +use account_compression_cpi::{program::SplAccountCompression, Noop}; +use anchor_lang::{prelude::*, solana_program::hash::hash}; +use anchor_spl::token::Mint; +use angry_purple_tiger::AnimalName; +use bubblegum_cpi::{ + cpi::{accounts::MintToCollectionV1, mint_to_collection_v1}, + get_asset_id, + program::Bubblegum, + Collection, Creator, MetadataArgs, TokenProgramVersion, TokenStandard, TreeConfig, +}; +use helium_sub_daos::DaoV0; + +use crate::{ + constants::ENTITY_METADATA_URL, error::ErrorCode, key_to_asset_seeds, state::*, + IssueEntityArgsV0, ECC_VERIFIER, +}; + +#[derive(Accounts)] +#[instruction(args: IssueEntityArgsV0)] +pub struct IssueEntityV1<'info> { + #[account(mut)] + pub payer: Signer<'info>, + #[account(address = Pubkey::from_str(ECC_VERIFIER).unwrap())] + pub ecc_verifier: Signer<'info>, + #[account( + has_one = maker, + seeds = [ + "mobile_hotspot_voucher".as_bytes(), + mobile_voucher.rewardable_entity_config.as_ref(), + &hash(&args.entity_key[..]).to_bytes() + ], + bump = mobile_voucher.bump_seed, + )] + pub mobile_voucher: Box>, + pub collection: Box>, + /// CHECK: Handled by cpi + #[account( + mut, + seeds = ["metadata".as_bytes(), token_metadata_program.key().as_ref(), collection.key().as_ref()], + seeds::program = token_metadata_program.key(), + bump, + )] + pub collection_metadata: UncheckedAccount<'info>, + /// CHECK: Handled By cpi account + #[account( + seeds = ["metadata".as_bytes(), token_metadata_program.key().as_ref(), collection.key().as_ref(), "edition".as_bytes()], + seeds::program = token_metadata_program.key(), + bump, + )] + pub collection_master_edition: UncheckedAccount<'info>, + #[account( + mut, + has_one = collection, + has_one = merkle_tree, + has_one = dao, + )] + pub maker: Box>, + /// CHECK: Signs as a verified creator to make searching easier + #[account( + seeds = [b"entity_creator", dao.key().as_ref()], + bump, + )] + pub entity_creator: UncheckedAccount<'info>, + pub dao: Box>, + #[account( + init, + payer = payer, + space = 8 + std::mem::size_of::() + 1 + args.entity_key.len(), + seeds = [ + "key_to_asset".as_bytes(), + dao.key().as_ref(), + &hash(&args.entity_key[..]).to_bytes() + ], + bump + )] + pub key_to_asset: Box>, + #[account( + mut, + seeds = [merkle_tree.key().as_ref()], + seeds::program = bubblegum_program.key(), + bump, + )] + pub tree_authority: Box>, + /// CHECK: Used in cpi + pub recipient: AccountInfo<'info>, + /// CHECK: Used in cpi + #[account(mut)] + pub merkle_tree: AccountInfo<'info>, + #[account( + seeds = ["collection_cpi".as_bytes()], + seeds::program = bubblegum_program.key(), + bump, + )] + /// CHECK: Used in cpi + pub bubblegum_signer: UncheckedAccount<'info>, + + /// CHECK: Verified by constraint + #[account(address = mpl_token_metadata::ID)] + pub token_metadata_program: AccountInfo<'info>, + pub log_wrapper: Program<'info, Noop>, + pub bubblegum_program: Program<'info, Bubblegum>, + pub compression_program: Program<'info, SplAccountCompression>, + pub system_program: Program<'info, System>, +} + +impl<'info> IssueEntityV1<'info> { + fn mint_to_collection_ctx(&self) -> CpiContext<'_, '_, '_, 'info, MintToCollectionV1<'info>> { + let cpi_accounts = MintToCollectionV1 { + tree_authority: self.tree_authority.to_account_info(), + leaf_delegate: self.recipient.to_account_info(), + leaf_owner: self.recipient.to_account_info(), + merkle_tree: self.merkle_tree.to_account_info(), + payer: self.payer.to_account_info(), + tree_delegate: self.maker.to_account_info(), + log_wrapper: self.log_wrapper.to_account_info(), + compression_program: self.compression_program.to_account_info(), + system_program: self.system_program.to_account_info(), + collection_authority: self.maker.to_account_info(), + collection_authority_record_pda: self.bubblegum_program.to_account_info(), + collection_mint: self.collection.to_account_info(), + collection_metadata: self.collection_metadata.to_account_info(), + edition_account: self.collection_master_edition.to_account_info(), + bubblegum_signer: self.bubblegum_signer.to_account_info(), + token_metadata_program: self.token_metadata_program.to_account_info(), + }; + CpiContext::new(self.bubblegum_program.to_account_info(), cpi_accounts) + } +} + +pub fn handler(ctx: Context, args: IssueEntityArgsV0) -> Result<()> { + let asset_id = get_asset_id( + &ctx.accounts.merkle_tree.key(), + ctx.accounts.tree_authority.num_minted, + ); + ctx.accounts.key_to_asset.set_inner(KeyToAssetV0 { + asset: asset_id, + dao: ctx.accounts.dao.key(), + entity_key: args.entity_key.clone(), + bump_seed: ctx.bumps["key_to_asset"], + key_serialization: KeySerialization::B58, + }); + + let key_str = bs58::encode(args.entity_key).into_string(); + let animal_name: AnimalName = key_str + .parse() + .map_err(|_| error!(ErrorCode::InvalidEccCompact))?; + + let maker_seeds: &[&[&[u8]]] = &[&[ + b"maker", + ctx.accounts.maker.dao.as_ref(), + ctx.accounts.maker.name.as_bytes(), + &[ctx.accounts.maker.bump_seed], + ]]; + + let name = animal_name.to_string(); + let uri = format!( + "{}/v2/hotspot/{}", + ENTITY_METADATA_URL, + ctx.accounts.key_to_asset.key(), + ); + let metadata = MetadataArgs { + name: name[..min(name.len(), 32)].to_owned(), + symbol: String::from("HOTSPOT"), + uri, + collection: Some(Collection { + key: ctx.accounts.collection.key(), + verified: false, // Verified in cpi + }), + primary_sale_happened: true, + is_mutable: true, + edition_nonce: None, + token_standard: Some(TokenStandard::NonFungible), + uses: None, + token_program_version: TokenProgramVersion::Original, + creators: vec![ + Creator { + address: ctx.accounts.entity_creator.key(), + verified: true, + share: 100, + }, + Creator { + address: ctx.accounts.key_to_asset.key(), + verified: true, + share: 0, + }, + ], + seller_fee_basis_points: 0, + }; + let entity_creator_seeds: &[&[&[u8]]] = &[&[ + b"entity_creator", + ctx.accounts.dao.to_account_info().key.as_ref(), + &[ctx.bumps["entity_creator"]], + ]]; + let mut creator = ctx.accounts.entity_creator.to_account_info(); + creator.is_signer = true; + let mut key_to_asset_creator = ctx.accounts.key_to_asset.to_account_info(); + key_to_asset_creator.is_signer = true; + let key_to_asset_signer: &[&[u8]] = key_to_asset_seeds!(ctx.accounts.key_to_asset); + mint_to_collection_v1( + ctx + .accounts + .mint_to_collection_ctx() + .with_remaining_accounts(vec![creator, key_to_asset_creator]) + .with_signer(&[maker_seeds[0], entity_creator_seeds[0], key_to_asset_signer]), + metadata, + )?; + + Ok(()) +} diff --git a/programs/helium-entity-manager/src/instructions/voucher/mod.rs b/programs/helium-entity-manager/src/instructions/voucher/mod.rs index 5a1de4c9e..8c2c185dc 100644 --- a/programs/helium-entity-manager/src/instructions/voucher/mod.rs +++ b/programs/helium-entity-manager/src/instructions/voucher/mod.rs @@ -1,7 +1,9 @@ pub mod initialize_mobile_hotspot_voucher_v0; +pub mod issue_entity_v1; pub mod onboard_mobile_hotspot_v1; pub mod pay_mobile_voucher_v0; pub use initialize_mobile_hotspot_voucher_v0::*; +pub use issue_entity_v1::*; pub use onboard_mobile_hotspot_v1::*; pub use pay_mobile_voucher_v0::*; diff --git a/programs/helium-entity-manager/src/instructions/voucher/onboard_mobile_hotspot_v1.rs b/programs/helium-entity-manager/src/instructions/voucher/onboard_mobile_hotspot_v1.rs index 9c39814e5..51a2fac83 100644 --- a/programs/helium-entity-manager/src/instructions/voucher/onboard_mobile_hotspot_v1.rs +++ b/programs/helium-entity-manager/src/instructions/voucher/onboard_mobile_hotspot_v1.rs @@ -5,8 +5,8 @@ use helium_sub_daos::DaoV0; use shared_utils::{verify_compressed_nft, VerifyCompressedNftArgs}; use crate::{ - error::ErrorCode, KeyToAssetV0, MakerApprovalV0, MakerV0, MobileHotspotInfoV0, - MobileHotspotVoucherV0, RewardableEntityConfigV0, MOBILE_HOTSPOT_INFO_SIZE, + error::ErrorCode, KeyToAssetV0, MakerApprovalV0, MakerV0, MobileDeploymentInfoV0, + MobileHotspotInfoV0, MobileHotspotVoucherV0, RewardableEntityConfigV0, MOBILE_HOTSPOT_INFO_SIZE, }; #[derive(AnchorSerialize, AnchorDeserialize, Clone)] @@ -15,6 +15,7 @@ pub struct OnboardMobileHotspotArgsV1 { pub creator_hash: [u8; 32], pub root: [u8; 32], pub index: u32, + pub deployment_info: Option, } #[derive(Accounts)] @@ -102,7 +103,7 @@ pub fn handler<'info>( is_active: false, dc_onboarding_fee_paid: ctx.accounts.mobile_voucher.dc_paid, device_type: ctx.accounts.mobile_voucher.device_type, - deployment_info: None, + deployment_info: args.deployment_info, }); Ok(()) diff --git a/programs/helium-entity-manager/src/instructions/voucher/pay_mobile_voucher_v0.rs b/programs/helium-entity-manager/src/instructions/voucher/pay_mobile_voucher_v0.rs index 4a6f93de4..bdb29edb1 100644 --- a/programs/helium-entity-manager/src/instructions/voucher/pay_mobile_voucher_v0.rs +++ b/programs/helium-entity-manager/src/instructions/voucher/pay_mobile_voucher_v0.rs @@ -1,10 +1,12 @@ use std::str::FromStr; +use account_compression_cpi::program::SplAccountCompression; use anchor_lang::prelude::*; use anchor_spl::{ associated_token::AssociatedToken, token::{burn, Burn, Mint, Token, TokenAccount}, }; +use bubblegum_cpi::get_asset_id; use circuit_breaker::{CircuitBreaker, MintWindowedCircuitBreakerV0}; use data_credits::{ cpi::{ @@ -20,17 +22,29 @@ use helium_sub_daos::{ DaoV0, SubDaoV0, TrackDcOnboardingFeesArgsV0, }; use pyth_solana_receiver_sdk::price_update::PriceUpdateV2; +use shared_utils::{verify_compressed_nft, VerifyCompressedNftArgs}; use crate::{error::ErrorCode, maker_seeds, state::*, TESTING}; const PRICE_ORACLE: &str = "DQ4C1tzvu28cwo1roN1Wm6TW35sfJEjLh517k3ZeWevx"; +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct PayMobileVoucherArgsV0 { + pub data_hash: [u8; 32], + pub creator_hash: [u8; 32], + pub root: [u8; 32], + pub index: u32, +} + #[derive(Accounts)] +#[instruction(args: PayMobileVoucherArgsV0)] pub struct PayMobileVoucherV0<'info> { #[account(mut)] pub maker: Box>, #[account(mut)] pub payer: Signer<'info>, + #[account(mut)] + pub hotspot_owner: Signer<'info>, #[account( has_one = sub_dao, constraint = rewardable_entity_config.settings.is_mobile(), @@ -41,7 +55,7 @@ pub struct PayMobileVoucherV0<'info> { has_one = rewardable_entity_config, has_one = maker, )] - pub mobile_hotspot_voucher: Box>, + pub mobile_voucher: Box>, #[account( mut, has_one = dao, @@ -105,25 +119,48 @@ pub struct PayMobileVoucherV0<'info> { bump = circuit_breaker.bump_seed )] pub circuit_breaker: Box>, + #[account( + has_one = dao, + constraint = get_asset_id(&merkle_tree.key(), args.index.into()) == key_to_asset.asset, + )] + pub key_to_asset: Box>, + /// CHECK: The merkle tree + pub merkle_tree: UncheckedAccount<'info>, pub circuit_breaker_program: Program<'info, CircuitBreaker>, pub token_program: Program<'info, Token>, pub data_credits_program: Program<'info, DataCredits>, + pub compression_program: Program<'info, SplAccountCompression>, pub system_program: Program<'info, System>, pub associated_token_program: Program<'info, AssociatedToken>, pub helium_sub_daos_program: Program<'info, HeliumSubDaos>, } -pub fn handler(ctx: Context) -> Result<()> { +pub fn handler<'info>( + ctx: Context<'_, '_, '_, 'info, PayMobileVoucherV0<'info>>, + args: PayMobileVoucherArgsV0, +) -> Result<()> { + verify_compressed_nft(VerifyCompressedNftArgs { + data_hash: args.data_hash, + creator_hash: args.creator_hash, + root: args.root, + index: args.index, + compression_program: ctx.accounts.compression_program.to_account_info(), + merkle_tree: ctx.accounts.merkle_tree.to_account_info(), + owner: ctx.accounts.hotspot_owner.key(), + delegate: ctx.accounts.hotspot_owner.key(), + proof_accounts: ctx.remaining_accounts.to_vec(), + })?; + let fees = ctx .accounts .rewardable_entity_config .settings - .mobile_device_fees(ctx.accounts.mobile_hotspot_voucher.device_type) + .mobile_device_fees(ctx.accounts.mobile_voucher.device_type) .ok_or(error!(ErrorCode::InvalidDeviceType))?; let dc_fee = fees.dc_onboarding_fee; if dc_fee > 0 { - if ctx.accounts.dnt_burner.amount < dc_fee { + if ctx.accounts.dc_burner.amount < dc_fee { mint_data_credits_v0( CpiContext::new_with_signer( ctx.accounts.data_credits_program.to_account_info(), @@ -146,7 +183,7 @@ pub fn handler(ctx: Context) -> Result<()> { ), MintDataCreditsArgsV0 { hnt_amount: None, - dc_amount: Some(ctx.accounts.dc_burner.amount - dc_fee), + dc_amount: Some(dc_fee - ctx.accounts.dc_burner.amount), }, )?; } @@ -247,8 +284,8 @@ pub fn handler(ctx: Context) -> Result<()> { )?; } - ctx.accounts.mobile_hotspot_voucher.paid = true; - ctx.accounts.mobile_hotspot_voucher.dc_paid = dc_fee; + ctx.accounts.mobile_voucher.paid = true; + ctx.accounts.mobile_voucher.dc_paid = dc_fee; Ok(()) } diff --git a/programs/helium-entity-manager/src/lib.rs b/programs/helium-entity-manager/src/lib.rs index 560600617..261e0b034 100644 --- a/programs/helium-entity-manager/src/lib.rs +++ b/programs/helium-entity-manager/src/lib.rs @@ -211,4 +211,18 @@ pub mod helium_entity_manager { ) -> Result<()> { onboard_data_only_mobile_hotspot_v0::handler(ctx, args) } + + pub fn pay_mobile_voucher_v0<'info>( + ctx: Context<'_, '_, '_, 'info, PayMobileVoucherV0<'info>>, + args: PayMobileVoucherArgsV0, + ) -> Result<()> { + pay_mobile_voucher_v0::handler(ctx, args) + } + + pub fn issue_entity_v1<'info>( + ctx: Context<'_, '_, '_, 'info, IssueEntityV1<'info>>, + args: IssueEntityArgsV0, + ) -> Result<()> { + issue_entity_v1::handler(ctx, args) + } } diff --git a/tests/helium-entity-manager.ts b/tests/helium-entity-manager.ts index b32e2fb27..ff1dd4338 100644 --- a/tests/helium-entity-manager.ts +++ b/tests/helium-entity-manager.ts @@ -1,42 +1,40 @@ import * as anchor from "@coral-xyz/anchor"; import { Program } from "@coral-xyz/anchor"; +import { init as initConversionEscrow } from "@helium/conversion-escrow-sdk"; import { Keypair as HeliumKeypair } from "@helium/crypto"; import { init as initDataCredits } from "@helium/data-credits-sdk"; import { init as initHeliumSubDaos } from "@helium/helium-sub-daos-sdk"; -import { init as initConversionEscrow } from "@helium/conversion-escrow-sdk"; -import { notEmittedKey, init as initBurn } from "@helium/no-emit-sdk"; +import { init as initBurn, notEmittedKey } from "@helium/no-emit-sdk"; import { Asset, AssetProof, HELIUM_COMMON_LUT, HNT_PYTH_PRICE_FEED, - LUT_ACCOUNTS, MOBILE_PYTH_PRICE_FEED, SOL_PYTH_PRICE_FEED, USDC_PYTH_PRICE_FEED, - chunks, + batchInstructionsToTxsWithPriorityFee, createAtaAndMint, createMint, createMintInstructions, - mintTo, + populateMissingDraftInfo, proofArgsAndAccounts, + sendAndConfirmWithRetry, sendInstructions, toBN, toVersionedTx, - truthy, - withPriorityFees, } from "@helium/spl-utils"; +import { AddGatewayV1 } from "@helium/transactions"; import { PythSolanaReceiverProgram, pythSolanaReceiverIdl, } from "@pythnetwork/pyth-solana-receiver"; -import { AddGatewayV1 } from "@helium/transactions"; import { - createAssociatedTokenAccountIdempotentInstruction, + NATIVE_MINT, createTransferInstruction, - getAssociatedTokenAddressSync, getAccount, - NATIVE_MINT, + getAssociatedTokenAddressSync, + createAssociatedTokenAccountIdempotentInstruction, } from "@solana/spl-token"; import { ComputeBudgetProgram, @@ -44,8 +42,6 @@ import { LAMPORTS_PER_SOL, PublicKey, SystemProgram, - Connection, - AddressLookupTableProgram, } from "@solana/web3.js"; import chai from "chai"; import { @@ -59,19 +55,19 @@ import { } from "../packages/helium-entity-manager-sdk/src"; import { DataCredits } from "../target/types/data_credits"; import { HeliumEntityManager } from "../target/types/helium_entity_manager"; -import { NoEmit } from "../target/types/no_emit"; import { HeliumSubDaos } from "../target/types/helium_sub_daos"; +import { NoEmit } from "../target/types/no_emit"; import { initTestDao, initTestSubdao } from "./utils/daos"; import { DC_FEE, + MAKER_STAKING_FEE, + ensureConversionEscrowIdl, ensureDCIdl, - ensureHSDIdl, ensureHEMIdl, + ensureHSDIdl, initTestDataCredits, initTestMaker, initTestRewardableEntityConfig, - MAKER_STAKING_FEE, - ensureConversionEscrowIdl, } from "./utils/fixtures"; // @ts-ignore import bs58 from "bs58"; @@ -81,6 +77,11 @@ import { helium } from "@helium/proto"; // @ts-ignore import axios from "axios"; +import { + keyToAssetKey, + mobileInfoKey, + topUpMaker, +} from "@helium/helium-entity-manager-sdk"; import { MerkleTree, SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, @@ -88,16 +89,11 @@ import { } from "@solana/spl-account-compression"; import { BN } from "bn.js"; import chaiAsPromised from "chai-as-promised"; -import { createMockCompression } from "./utils/compression"; -import { loadKeypair } from "./utils/solana"; +import { init as initVsr } from "../packages/voter-stake-registry-sdk/src"; import { ConversionEscrow } from "../target/types/conversion_escrow"; -import { - keyToAssetKey, - mobileInfoKey, - topUpMaker, -} from "@helium/helium-entity-manager-sdk"; import { VoterStakeRegistry } from "../target/types/voter_stake_registry"; -import { init as initVsr } from "../packages/voter-stake-registry-sdk/src"; +import { createMockCompression } from "./utils/compression"; +import { loadKeypair } from "./utils/solana"; chai.use(chaiAsPromised); @@ -806,7 +802,13 @@ describe("helium-entity-manager", () => { topupAmounts ); - await initTestMaker(hemProgram, provider, rewardableEntityConfig, dao, topupAmounts); + await initTestMaker( + hemProgram, + provider, + rewardableEntityConfig, + dao, + topupAmounts + ); await dcProgram.methods .mintDataCreditsV0({ @@ -1002,7 +1004,7 @@ describe("helium-entity-manager", () => { .sub(priceMobile.priceMessage.emaConf.mul(new anchor.BN(2))) .toNumber() * 10 ** priceMobile.priceMessage.exponent; - console.log("mobileFloor", mobileFloorValue) + console.log("mobileFloor", mobileFloorValue); const { instruction, pubkeys } = await hemProgram.methods .makerLendV0() .accounts({ @@ -1031,15 +1033,14 @@ describe("helium-entity-manager", () => { }) .instruction(), ]; - - console.log("sending"); + await sendInstructions(provider, ixs); - console.log("sent"); const postBalance = await provider.connection.getTokenAccountBalance( myAcc ); - console.log(postBalance.value.uiAmount, + console.log( + postBalance.value.uiAmount, preBalance.value.uiAmount! + 10 * (1 + slippageBps / 10000) ); expect(postBalance.value.uiAmount).to.be.eq( @@ -1048,32 +1049,95 @@ describe("helium-entity-manager", () => { ); }); - // it("issues a mobile hotspot", async () => { - // const method = await hemProgram.methods - // .onboardMobileHotspotV1({ - // entityKey: Buffer.from(bs58.decode(ecc)), - // }) - // .accounts({ - // issueEntityCommon: { - // maker, - // recipient: me, - // dao, - // }, - // mobileInfo: mobileInfoKey( - // rewardableEntityConfig, - // Buffer.from(bs58.decode(ecc)) - // )[0], - // voucher: mobileHotspotVoucher, - // }); - // await method.rpc({ skipPreflight: true }); - // const { mobileInfo } = await method.pubkeys(); - - // const mobileInfoAcc = - // await hemProgram.account.mobileHotspotInfoV0.fetch(mobileInfo!); - // expect(Boolean(mobileInfoAcc)).to.be.true; - // const subDaoAcc = await hsdProgram.account.subDaoV0.fetch(subDao); - // expect(subDaoAcc.dcOnboardingFeesPaid.toNumber()).to.be.eq(1000000); - // }); + it("issues a mobile hotspot", async () => { + await hemProgram.methods + .issueEntityV1({ + entityKey: Buffer.from(bs58.decode(ecc)), + }) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ units: 500000 }), + ]) + .accounts({ + maker, + recipient: hotspotOwner.publicKey, + mobileVoucher: mobileHotspotVoucher!, + dao, + eccVerifier: eccVerifier.publicKey, + }) + .signers([eccVerifier]) + .rpc({ skipPreflight: true }); + + const method = await onboardMobileHotspot({ + program: hemProgram, + assetId: hotspot, + maker, + dao, + rewardableEntityConfig, + getAssetFn, + getAssetProofFn, + deviceType: "wifiIndoor", + deploymentInfo: null, + }); + const txs = await batchInstructionsToTxsWithPriorityFee( + provider, + [ + createAssociatedTokenAccountIdempotentInstruction( + me, + getAssociatedTokenAddressSync(dntMint, maker, true), + maker, + dntMint + ), + createTransferInstruction( + getAssociatedTokenAddressSync(dntMint, me), + getAssociatedTokenAddressSync(dntMint, maker, true), + me, + toBN(1000, 8).toNumber() + ), + createAssociatedTokenAccountIdempotentInstruction( + me, + getAssociatedTokenAddressSync(hntMint, maker, true), + maker, + hntMint + ), + createTransferInstruction( + getAssociatedTokenAddressSync(hntMint, me), + getAssociatedTokenAddressSync(hntMint, maker, true), + me, + toBN(1000, 6).toNumber() + ), + ...(await method.transaction()).instructions, + ], + { + addressLookupTableAddresses: [HELIUM_COMMON_LUT], + computeScaleUp: 2, + } + ); + for (const draft of txs) { + const tx = toVersionedTx(draft); + try { + tx.sign([hotspotOwner]); + } catch (e) { + // ignore, one of these txs doesn't need hotspot owner to sign + } + await provider.wallet.signTransaction(tx); + await sendAndConfirmWithRetry( + provider.connection, + Buffer.from(tx.serialize()), + { + skipPreflight: true, + }, + "confirmed" + ); + } + + const { mobileInfo } = await method.pubkeys(); + + const mobileInfoAcc = + await hemProgram.account.mobileHotspotInfoV0.fetch(mobileInfo!); + expect(Boolean(mobileInfoAcc)).to.be.true; + const subDaoAcc = await hsdProgram.account.subDaoV0.fetch(subDao); + expect(subDaoAcc.dcOnboardingFeesPaid.toNumber()).to.be.eq(1000000); + }); }); it("issues a mobile hotspot", async () => { @@ -1370,7 +1434,13 @@ describe("helium-entity-manager", () => { dao ); - await initTestMaker(hemProgram, provider, rewardableEntityConfig, dao, topupAmounts); + await initTestMaker( + hemProgram, + provider, + rewardableEntityConfig, + dao, + topupAmounts + ); await dcProgram.methods .mintDataCreditsV0({