From 55e926ad158e32a3983613e2d27ae5464febcdc9 Mon Sep 17 00:00:00 2001 From: Marc Nijdam Date: Thu, 13 Jun 2024 11:01:30 -0500 Subject: [PATCH] Fix asset to hotspot conversion (#373) * Fix asset to hotspot conversion Asset convertion to a Hotspot was relying on a metadata field that may take over 24 hours to get populated, and is only to be used for non critical purposes. This PR introduces a KeyToAsset cache that is used to cache asset accounts based on the asset key. The cache is then used for Asset->Hotspot conversion to get the entity key (back) * Fix onboarding update txn params The location passed into the onboarding server was not a quoted big number.. This fix removes serde_with as a dependency and fixes the issue in request parameter construction * kta module, naming cleanup asset_account renamed to kta, created separate module for kta functionality. kta module supports get_many to fetch many kta_keys * use Error::account_not_found convenience --- Cargo.lock | 9 -- helium-lib/Cargo.toml | 2 +- helium-lib/src/asset.rs | 97 ++++++++------ helium-lib/src/dao.rs | 17 ++- helium-lib/src/dc.rs | 2 +- helium-lib/src/hotspot.rs | 161 +++++++++++++++--------- helium-lib/src/kta.rs | 122 ++++++++++++++++++ helium-lib/src/lib.rs | 5 + helium-lib/src/onboarding.rs | 9 +- helium-lib/src/programs.rs | 3 - helium-lib/src/result.rs | 6 + helium-lib/src/reward.rs | 28 ++--- helium-wallet/src/cmd/router/balance.rs | 2 +- helium-wallet/src/main.rs | 1 + 14 files changed, 320 insertions(+), 144 deletions(-) create mode 100644 helium-lib/src/kta.rs diff --git a/Cargo.lock b/Cargo.lock index de90b86e..16688fa2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1742,7 +1742,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", - "serde", ] [[package]] @@ -2448,7 +2447,6 @@ dependencies = [ "rust_decimal", "serde", "serde_json", - "serde_with", "sha2 0.10.8", "solana-program", "solana-sdk", @@ -2737,7 +2735,6 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", - "serde", ] [[package]] @@ -4598,14 +4595,8 @@ version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07ff71d2c147a7b57362cead5e22f772cd52f6ab31cfcd9edcd7f6aeb2a0afbe" dependencies = [ - "base64 0.13.1", - "chrono", - "hex", - "indexmap 1.9.3", "serde", - "serde_json", "serde_with_macros", - "time", ] [[package]] diff --git a/helium-lib/Cargo.toml b/helium-lib/Cargo.toml index d97320f0..ea5b490a 100644 --- a/helium-lib/Cargo.toml +++ b/helium-lib/Cargo.toml @@ -36,7 +36,7 @@ solana-program = "*" solana-transaction-status = "*" serde = {workspace = true} serde_json = {workspace = true} -serde_with = "2" +# serde_with = "2" lazy_static = "1" rust_decimal = {workspace = true} helium-proto = {workspace= true} diff --git a/helium-lib/src/asset.rs b/helium-lib/src/asset.rs index 9ed1ecfb..25722e01 100644 --- a/helium-lib/src/asset.rs +++ b/helium-lib/src/asset.rs @@ -1,57 +1,40 @@ use crate::{ dao::Dao, - entity_key::AsEntityKey, - keypair::{serde_opt_pubkey, serde_pubkey, Keypair, Pubkey}, + entity_key::{self, AsEntityKey}, + keypair::{serde_opt_pubkey, serde_pubkey, Pubkey}, + kta, result::{DecodeError, Error, Result}, settings::{DasClient, DasSearchAssetsParams, Settings}, }; use helium_anchor_gen::helium_entity_manager; use itertools::Itertools; use serde::{Deserialize, Serialize}; -use solana_sdk::{bs58, signer::Signer}; -use std::{collections::HashMap, ops::Deref, result::Result as StdResult, str::FromStr}; - -pub async fn account_for_entity_key, E>( - client: &anchor_client::Client, - entity_key: &E, -) -> Result -where - E: AsEntityKey, -{ - let program = client.program(helium_entity_manager::id())?; - let asset_key = Dao::Hnt.key_to_asset_key(entity_key); - let asset_account = program - .account::(asset_key) - .await?; - Ok(asset_account) -} +use solana_sdk::bs58; +use std::{collections::HashMap, result::Result as StdResult, str::FromStr}; pub async fn for_entity_key(settings: &Settings, entity_key: &E) -> Result where E: AsEntityKey, { - let client = settings.mk_anchor_client(Keypair::void())?; - let asset_account = account_for_entity_key(&client, entity_key).await?; - get(settings, &asset_account).await + let kta = kta::for_entity_key(entity_key).await?; + for_kta(settings, &kta).await } -pub async fn get( +pub async fn for_kta( settings: &Settings, - asset_account: &helium_entity_manager::KeyToAssetV0, + kta: &helium_entity_manager::KeyToAssetV0, ) -> Result { let jsonrpc = settings.mk_jsonrpc_client()?; - let asset_responase: Asset = jsonrpc.get_asset(&asset_account.asset).await?; + let asset_responase: Asset = jsonrpc.get_asset(&kta.asset).await?; Ok(asset_responase) } -pub async fn get_with_proof( +pub async fn for_kta_with_proof( settings: &Settings, - asset_account: &helium_entity_manager::KeyToAssetV0, + kta: &helium_entity_manager::KeyToAssetV0, ) -> Result<(Asset, AssetProof)> { - let (asset, asset_proof) = futures::try_join!( - get(settings, asset_account), - proof::get(settings, asset_account) - )?; + let (asset, asset_proof) = + futures::try_join!(for_kta(settings, kta), proof::get(settings, kta))?; Ok((asset, asset_proof)) } @@ -80,11 +63,10 @@ pub mod proof { pub async fn get( settings: &Settings, - asset_account: &helium_entity_manager::KeyToAssetV0, + kta: &helium_entity_manager::KeyToAssetV0, ) -> Result { let jsonrpc = settings.mk_jsonrpc_client()?; - let asset_proof_response: AssetProof = - jsonrpc.get_asset_proof(&asset_account.asset).await?; + let asset_proof_response: AssetProof = jsonrpc.get_asset_proof(&kta.asset).await?; Ok(asset_proof_response) } @@ -93,9 +75,8 @@ pub mod proof { where E: AsEntityKey, { - let client = settings.mk_anchor_client(Keypair::void())?; - let asset_account = account_for_entity_key(&client, entity_key).await?; - get(settings, &asset_account).await + let kta = kta::for_entity_key(entity_key).await?; + get(settings, &kta).await } } @@ -136,10 +117,19 @@ pub struct Asset { #[serde(with = "serde_pubkey")] pub id: Pubkey, pub compression: AssetCompression, + pub creators: Vec, pub ownership: AssetOwnership, pub content: AssetContent, } +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct AssetCreator { + #[serde(with = "serde_pubkey")] + address: Pubkey, + share: u8, + verified: bool, +} + pub type Hash = [u8; 32]; #[derive(Debug, Deserialize, Serialize, Clone)] @@ -176,6 +166,37 @@ pub struct AssetProof { pub tree_id: Pubkey, } +impl Asset { + pub fn kta_key(&self) -> Result { + if let Some(creator) = self.creators.get(1) { + return Ok(creator.address); + } + let entity_key_str = self + .content + .json_uri + .path() + .strip_prefix('/') + .map(ToString::to_string) + .ok_or(DecodeError::other(format!( + "missing entity key in \"{}\"", + self.content.json_uri + )))?; + let key_serialization = + if ["IOT OPS", "CARRIER"].contains(&self.content.metadata.symbol.as_str()) { + helium_entity_manager::KeySerialization::UTF8 + } else { + helium_entity_manager::KeySerialization::B58 + }; + let entity_key = entity_key::from_string(entity_key_str, key_serialization)?; + let kta_key = Dao::Hnt.entity_key_to_kta_key(&entity_key); + Ok(kta_key) + } + + pub async fn get_kta(&self) -> Result { + kta::get(&self.kta_key()?).await + } +} + impl AssetProof { pub fn proof( &self, @@ -204,7 +225,7 @@ impl AssetProof { let canopy_heights = get_canopy_heights().await?; let height = canopy_heights .get(tree) - .ok_or_else(|| anchor_client::ClientError::AccountNotFound)?; + .ok_or_else(Error::account_not_found)?; self.proof(Some(*height)) } } diff --git a/helium-lib/src/dao.rs b/helium-lib/src/dao.rs index 49ac7713..560808b6 100644 --- a/helium-lib/src/dao.rs +++ b/helium-lib/src/dao.rs @@ -1,8 +1,5 @@ use crate::{ - entity_key::AsEntityKey, - keypair::Pubkey, - programs::{MPL_BUBBLEGUM_PROGRAM_ID, TOKEN_METADATA_PROGRAM_ID}, - result::Result, + entity_key::AsEntityKey, keypair::Pubkey, programs::TOKEN_METADATA_PROGRAM_ID, result::Result, token::Token, }; use helium_anchor_gen::{data_credits, helium_entity_manager, helium_sub_daos, lazy_distributor}; @@ -75,13 +72,13 @@ impl Dao { pub fn merkle_tree_authority(&self, merkle_tree: &Pubkey) -> Pubkey { let (tree_authority, _ta_bump) = - Pubkey::find_program_address(&[merkle_tree.as_ref()], &MPL_BUBBLEGUM_PROGRAM_ID); + Pubkey::find_program_address(&[merkle_tree.as_ref()], &mpl_bubblegum::ID); tree_authority } pub fn bubblegum_signer(&self) -> Pubkey { let (bubblegum_signer, _bs_bump) = - Pubkey::find_program_address(&[b"collection_cpi"], &MPL_BUBBLEGUM_PROGRAM_ID); + Pubkey::find_program_address(&[b"collection_cpi"], &mpl_bubblegum::ID); bubblegum_signer } @@ -93,7 +90,7 @@ impl Dao { key } - pub fn key_to_asset_key(&self, entity_key: &E) -> Pubkey { + pub fn entity_key_to_kta_key(&self, entity_key: &E) -> Pubkey { let hash = Sha256::digest(entity_key.as_entity_key()); let (key, _) = Pubkey::find_program_address( &[b"key_to_asset", self.key().as_ref(), hash.as_ref()], @@ -170,7 +167,7 @@ impl SubDao { key } - pub fn escrow_account_key(&self, delegated_dc_key: &Pubkey) -> Pubkey { + pub fn escrow_key(&self, delegated_dc_key: &Pubkey) -> Pubkey { let (key, _) = Pubkey::find_program_address( &[b"escrow_dc_account", delegated_dc_key.as_ref()], &data_credits::id(), @@ -217,12 +214,12 @@ impl SubDao { key } - pub fn asset_key_to_receipient_key(&self, asset_key: &Pubkey) -> Pubkey { + pub fn receipient_key_from_kta(&self, kta: &helium_entity_manager::KeyToAssetV0) -> Pubkey { let (key, _) = Pubkey::find_program_address( &[ b"recipient", self.lazy_distributor_key().as_ref(), - asset_key.as_ref(), + kta.asset.as_ref(), ], &lazy_distributor::id(), ); diff --git a/helium-lib/src/dc.rs b/helium-lib/src/dc.rs index 34f66bec..7060f0d5 100644 --- a/helium-lib/src/dc.rs +++ b/helium-lib/src/dc.rs @@ -91,7 +91,7 @@ pub async fn delegate + GetPubkey>( sub_dao: subdao.key(), owner: keypair.pubkey(), from_account: Token::Dc.associated_token_adress(&keypair.pubkey()), - escrow_account: subdao.escrow_account_key(&delegated_data_credits), + escrow_account: subdao.escrow_key(&delegated_data_credits), payer: keypair.pubkey(), associated_token_program: anchor_spl::associated_token::ID, token_program: anchor_spl::token::ID, diff --git a/helium-lib/src/hotspot.rs b/helium-lib/src/hotspot.rs index 23a16718..0d7fd091 100644 --- a/helium-lib/src/hotspot.rs +++ b/helium-lib/src/hotspot.rs @@ -4,9 +4,9 @@ use crate::{ entity_key::AsEntityKey, is_zero, keypair::{pubkey, serde_pubkey, GetPubkey, Keypair, Pubkey}, - onboarding, + kta, onboarding, priority_fee::{self, SetPriorityFees}, - programs::{MPL_BUBBLEGUM_PROGRAM_ID, SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, SPL_NOOP_PROGRAM_ID}, + programs::{SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, SPL_NOOP_PROGRAM_ID}, result::{DecodeError, EncodeError, Error, Result}, settings::{DasClient, DasSearchAssetsParams, Settings}, token::Token, @@ -21,34 +21,44 @@ use helium_anchor_gen::{ anchor_lang::{AnchorDeserialize, Discriminator, ToAccountMetas}, data_credits, helium_entity_manager, helium_sub_daos, }; +use itertools::Itertools; use rust_decimal::prelude::*; use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; use solana_program::instruction::AccountMeta; -use solana_sdk::{commitment_config::CommitmentConfig, signature::Signature, signer::Signer}; +use solana_sdk::{bs58, commitment_config::CommitmentConfig, signature::Signature, signer::Signer}; use std::{collections::HashMap, ops::Deref, result::Result as StdResult, str::FromStr}; pub const HOTSPOT_CREATOR: Pubkey = pubkey!("Fv5hf1Fg58htfC7YEXKNEfkpuogUUQDDTLgjGWxxv48H"); pub const ECC_VERIFIER: Pubkey = pubkey!("eccSAJM3tq7nQSpQTm8roxv4FPoipCkMsGizW2KBhqZ"); +pub fn key_from_kta(kta: helium_entity_manager::KeyToAssetV0) -> Result { + let key_str = match kta.key_serialization { + helium_entity_manager::KeySerialization::B58 => bs58::encode(kta.entity_key).into_string(), + helium_entity_manager::KeySerialization::UTF8 => String::from_utf8(kta.entity_key) + .map_err(|_| DecodeError::other("invalid entity key string"))?, + }; + Ok(helium_crypto::PublicKey::from_str(&key_str)?) +} + pub async fn for_owner(settings: &Settings, owner: &Pubkey) -> Result> { let assets = asset::for_owner(settings, &HOTSPOT_CREATOR, owner).await?; - assets - .into_iter() - .map(|asset| Hotspot::try_from(asset).map_err(Error::from)) - .collect::>>() + stream::iter(assets) + .map(|asset| async move { Hotspot::from_asset(asset).await }) + .buffered(5) + .try_collect::>() + .await } pub async fn search(client: &DasClient, params: DasSearchAssetsParams) -> Result { - let asset_page = asset::search(client, params).await?; - Ok(HotspotPage::try_from(asset_page)?) + asset::search(client, params) + .and_then(HotspotPage::from_asset_page) + .await } pub async fn get(settings: &Settings, hotspot_key: &helium_crypto::PublicKey) -> Result { - let client = settings.mk_anchor_client(Keypair::void())?; - let asset_account = asset::account_for_entity_key(&client, hotspot_key).await?; - let asset = asset::get(settings, &asset_account).await?; - Ok(asset.try_into()?) + let kta = kta::for_entity_key(hotspot_key).await?; + let asset = asset::for_kta(settings, &kta).await?; + Hotspot::from_asset(asset).await } pub async fn get_with_info( @@ -138,10 +148,12 @@ pub mod info { } #[derive(Serialize, Deserialize, Debug, Default)] - #[skip_serializing_none] pub struct HotspotInfoUpdateParams { + #[serde(skip_serializing_if = "Option::is_none")] pub before: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub until: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub limit: Option, } @@ -286,7 +298,7 @@ pub async fn direct_update + GetPubkey>( ) -> Result { fn mk_update_accounts( subdao: SubDao, - asset_account: &helium_entity_manager::KeyToAssetV0, + kta: &helium_entity_manager::KeyToAssetV0, asset: &asset::Asset, owner: &Pubkey, ) -> Vec { @@ -294,10 +306,10 @@ pub async fn direct_update + GetPubkey>( macro_rules! mk_update_info { ($name:ident, $info:ident) => { $name { - bubblegum_program: MPL_BUBBLEGUM_PROGRAM_ID, + bubblegum_program: mpl_bubblegum::ID, payer: owner.to_owned(), dc_fee_payer: owner.to_owned(), - $info: subdao.info_key(&asset_account.entity_key), + $info: subdao.info_key(&kta.entity_key), hotspot_owner: owner.to_owned(), merkle_tree: asset.compression.tree, tree_authority: Dao::Hnt.merkle_tree_authority(&asset.compression.tree), @@ -326,9 +338,9 @@ pub async fn direct_update + GetPubkey>( let program = anchor_client.program(helium_entity_manager::id())?; let solana_client = settings.mk_solana_client()?; - let asset_account = asset::account_for_entity_key(&anchor_client, hotspot).await?; - let (asset, asset_proof) = asset::get_with_proof(settings, &asset_account).await?; - let accounts = mk_update_accounts(update.subdao(), &asset_account, &asset, &program.payer()); + let kta = kta::for_entity_key(hotspot).await?; + let (asset, asset_proof) = asset::for_kta_with_proof(settings, &kta).await?; + let accounts = mk_update_accounts(update.subdao(), &kta, &asset, &program.payer()); let mut ixs = program .request() @@ -387,8 +399,8 @@ pub async fn transfer + GetPubkey>( let anchor_client = settings.mk_anchor_client(keypair.clone())?; let solana_client = settings.mk_solana_client()?; let program = anchor_client.program(mpl_bubblegum::ID)?; - let asset_account = asset::account_for_entity_key(&anchor_client, hotspot_key).await?; - let (asset, asset_proof) = asset::get_with_proof(settings, &asset_account).await?; + let kta = kta::for_entity_key(hotspot_key).await?; + let (asset, asset_proof) = asset::for_kta_with_proof(settings, &kta).await?; let leaf_delegate = asset.ownership.delegate.unwrap_or(asset.ownership.owner); let merkle_tree = asset_proof.tree_id; @@ -465,7 +477,7 @@ pub mod dataonly { rewardable_entity_config: SubDao::Iot.rewardable_entity_config_key(), data_only_config: data_only_config_key, dao: dao.key(), - key_to_asset: dao.key_to_asset_key(&entity_key), + key_to_asset: dao.entity_key_to_kta_key(&entity_key), sub_dao: SubDao::Iot.key(), dc_mint: *Token::Dc.mint(), dc: SubDao::dc_key(), @@ -485,8 +497,8 @@ pub mod dataonly { .account::(Dao::Hnt.dataonly_config_key()) .await?; - let asset_account = asset::account_for_entity_key(&anchor_client, hotspot_key).await?; - let (asset, asset_proof) = asset::get_with_proof(settings, &asset_account).await?; + let kta = kta::for_entity_key(hotspot_key).await?; + let (asset, asset_proof) = asset::for_kta_with_proof(settings, &kta).await?; let onboard_accounts = mk_accounts(config_account, program.payer(), hotspot_key); let mut ixs = program @@ -545,7 +557,7 @@ pub mod dataonly { data_only_config: dataonly_config_key, entity_creator: dao.entity_creator_key(), dao: dao.key(), - key_to_asset: dao.key_to_asset_key(&entity_key), + key_to_asset: dao.entity_key_to_kta_key(&entity_key), tree_authority: dao.merkle_tree_authority(&config_account.merkle_tree), recipient: owner, merkle_tree: config_account.merkle_tree, @@ -553,7 +565,7 @@ pub mod dataonly { bubblegum_signer: dao.bubblegum_signer(), token_metadata_program: TOKEN_METADATA_PROGRAM_ID, log_wrapper: SPL_NOOP_PROGRAM_ID, - bubblegum_program: MPL_BUBBLEGUM_PROGRAM_ID, + bubblegum_program: mpl_bubblegum::ID, compression_program: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, system_program: solana_sdk::system_program::id(), } @@ -668,18 +680,24 @@ pub struct HotspotPage { pub items: Vec, } -impl TryFrom for HotspotPage { - type Error = DecodeError; - fn try_from(value: asset::AssetPage) -> StdResult { +impl HotspotPage { + pub async fn from_asset_page(asset_page: asset::AssetPage) -> Result { + let kta_keys: Vec = asset_page + .items + .iter() + .map(asset::Asset::kta_key) + .try_collect()?; + let ktas = kta::get_many(&kta_keys).await?; + let items: Vec = ktas + .into_iter() + .zip(asset_page.items) + .map(|(kta, asset)| Hotspot::from_asset_with_kta(kta, asset)) + .try_collect()?; Ok(Self { - total: value.total, - limit: value.limit, - page: value.page, - items: value - .items - .into_iter() - .map(Hotspot::try_from) - .collect::, DecodeError>>()?, + total: asset_page.total, + limit: asset_page.limit, + page: asset_page.page, + items, }) } } @@ -709,6 +727,19 @@ impl Hotspot { info: None, } } + + pub async fn from_asset(asset: asset::Asset) -> Result { + let kta = asset.get_kta().await?; + Self::from_asset_with_kta(kta, asset) + } + + pub fn from_asset_with_kta( + kta: helium_entity_manager::KeyToAssetV0, + asset: asset::Asset, + ) -> Result { + let hotspot_key = key_from_kta(kta)?; + Ok(Self::with_hotspot_key(hotspot_key, asset.ownership.owner)) + } } #[derive(Serialize, Debug, Clone, Copy)] @@ -729,7 +760,7 @@ impl From for HotspotGeo { #[derive(Serialize, Debug, Clone, Copy)] pub struct HotspotLocation { - #[serde(with = "CellIndexHex")] + #[serde(with = "serde_cell_index")] location: h3o::CellIndex, geo: HotspotGeo, } @@ -769,6 +800,29 @@ impl HotspotLocation { } } +pub mod serde_cell_index { + use serde::de::{self, Deserialize}; + use std::str::FromStr; + + pub fn serialize( + value: &h3o::CellIndex, + serializer: S, + ) -> std::result::Result + where + S: serde::Serializer, + { + serializer.serialize_str(&value.to_string()) + } + + pub fn deserialize<'de, D>(deser: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + let str = String::deserialize(deser)?; + h3o::CellIndex::from_str(&str).map_err(|_| de::Error::custom("invalid h3 index")) + } +} + #[derive(Debug, Serialize, Clone)] #[serde(rename_all = "lowercase", untagged)] pub enum HotspotInfo { @@ -776,6 +830,7 @@ pub enum HotspotInfo { #[serde(skip_serializing_if = "Option::is_none")] asset: Option, mode: HotspotMode, + #[serde(skip_serializing_if = "Option::is_none")] gain: Option, #[serde(skip_serializing_if = "Option::is_none")] elevation: Option, @@ -808,27 +863,23 @@ pub struct CommittedHotspotInfoUpdate { #[derive(Debug, Serialize, Clone)] #[serde(rename_all = "lowercase", untagged)] -#[skip_serializing_none] pub enum HotspotInfoUpdate { Iot { + #[serde(skip_serializing_if = "Option::is_none")] gain: Option, + #[serde(skip_serializing_if = "Option::is_none")] elevation: Option, #[serde(flatten)] + #[serde(skip_serializing_if = "Option::is_none")] location: Option, }, Mobile { #[serde(flatten)] + #[serde(skip_serializing_if = "Option::is_none")] location: Option, }, } -serde_with::serde_conv!( - CellIndexHex, - h3o::CellIndex, - |index: &h3o::CellIndex| { index.to_string() }, - |value: &str| -> StdResult<_, h3o::error::InvalidCellIndex> { value.parse() } -); - impl HotspotInfoUpdate { pub fn subdao(&self) -> SubDao { match self { @@ -1026,17 +1077,3 @@ impl From for HotspotInfoUpda } } } - -impl TryFrom for Hotspot { - type Error = DecodeError; - fn try_from(value: asset::Asset) -> StdResult { - value - .content - .metadata - .get_attribute("ecc_compact") - .and_then(|v| v.as_str()) - .ok_or_else(|| DecodeError::other("no entity key found")) - .and_then(|str| helium_crypto::PublicKey::from_str(str).map_err(DecodeError::from)) - .map(|hotspot_key| Self::with_hotspot_key(hotspot_key, value.ownership.owner)) - } -} diff --git a/helium-lib/src/kta.rs b/helium-lib/src/kta.rs new file mode 100644 index 00000000..26cb9ab0 --- /dev/null +++ b/helium-lib/src/kta.rs @@ -0,0 +1,122 @@ +use crate::{ + dao::Dao, + entity_key::AsEntityKey, + keypair::{Keypair, Pubkey, VoidKeypair}, + result::{Error, Result}, + settings::Settings, +}; +use anchor_client::anchor_lang::AccountDeserialize; +use helium_anchor_gen::helium_entity_manager::{self, KeyToAssetV0}; +use itertools::Itertools; +use std::{ + collections::HashMap, + sync::{Arc, OnceLock, RwLock, RwLockReadGuard, RwLockWriteGuard}, +}; + +pub fn init(settings: &Settings) -> Result<()> { + let _ = CACHE.set(KtaCache::new(settings)?); + Ok(()) +} + +pub async fn get(kta_key: &Pubkey) -> Result { + let cache = CACHE.get().ok_or_else(Error::account_not_found)?; + cache.get(kta_key).await +} + +pub async fn get_many(kta_keys: &[Pubkey]) -> Result> { + let cache = CACHE.get().ok_or_else(Error::account_not_found)?; + cache.get_many(kta_keys).await +} + +pub async fn for_entity_key(entity_key: &E) -> Result +where + E: AsEntityKey, +{ + let kta_key = Dao::Hnt.entity_key_to_kta_key(entity_key); + get(&kta_key).await +} + +static CACHE: OnceLock = OnceLock::new(); + +type KtaCacheMap = HashMap; +struct KtaCache { + program: anchor_client::Program>, + cache: RwLock, +} + +impl KtaCache { + fn new(settings: &Settings) -> Result { + let anchor_client = settings.mk_anchor_client(Keypair::void())?; + let program = anchor_client.program(helium_entity_manager::id())?; + let cache = RwLock::new(KtaCacheMap::new()); + Ok(Self { program, cache }) + } + + fn cache_read(&self) -> RwLockReadGuard<'_, KtaCacheMap> { + self.cache.read().expect("cache read lock poisoned") + } + + fn cache_write(&self) -> RwLockWriteGuard<'_, KtaCacheMap> { + self.cache.write().expect("cache write lock poisoned") + } + + async fn get(&self, kta_key: &Pubkey) -> Result { + if let Some(account) = self.cache_read().get(kta_key) { + return Ok(account.clone()); + } + + let kta = self + .program + .account::(*kta_key) + .await?; + // NOTE: Holding lock across an await will not work with std::sync + // Since sync::RwLock is much faster than sync options we take the hit + // of multipl requests for the same kta_key before the key is found + self.cache_write().insert(*kta_key, kta.clone()); + Ok(kta) + } + + async fn get_many( + &self, + kta_keys: &[Pubkey], + ) -> Result> { + let missing_keys: Vec = { + let cache = self.cache_read(); + kta_keys + .iter() + .filter(|key| !cache.contains_key(key)) + .copied() + .collect() + }; + let mut missing_accounts = self + .program + .async_rpc() + .get_multiple_accounts(&missing_keys) + .await?; + { + let mut cache = self.cache_write(); + missing_keys + .into_iter() + .zip(missing_accounts.iter_mut()) + .map(|(key, maybe_account)| { + let Some(account) = maybe_account.as_mut() else { + return Err(Error::account_not_found()); + }; + helium_entity_manager::KeyToAssetV0::try_deserialize(&mut account.data.as_ref()) + .map_err(Error::from) + .map(|kta| (key, kta)) + }) + .map_ok(|(key, kta)| { + cache.insert(key, kta); + }) + .try_collect()?; + } + { + let cache = self.cache_read(); + kta_keys + .iter() + .map(|key| cache.get(key).cloned().ok_or(Error::account_not_found())) + .try_collect() + } + } +} diff --git a/helium-lib/src/lib.rs b/helium-lib/src/lib.rs index a1a9dc02..8d8e8d53 100644 --- a/helium-lib/src/lib.rs +++ b/helium-lib/src/lib.rs @@ -5,6 +5,7 @@ pub mod dc; pub mod entity_key; pub mod hotspot; pub mod keypair; +pub mod kta; pub mod onboarding; pub mod priority_fee; pub mod programs; @@ -35,3 +36,7 @@ where { value == &T::ZERO } + +pub fn init(settings: &settings::Settings) -> result::Result<()> { + kta::init(settings) +} diff --git a/helium-lib/src/onboarding.rs b/helium-lib/src/onboarding.rs index 74838da0..5ae4b67c 100644 --- a/helium-lib/src/onboarding.rs +++ b/helium-lib/src/onboarding.rs @@ -2,7 +2,6 @@ use crate::{hotspot::HotspotInfoUpdate, keypair}; use futures::TryFutureExt; use rust_decimal::prelude::*; use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use serde_with::serde_as; use std::marker::Send; pub struct Client { @@ -60,13 +59,11 @@ impl Client { ) -> Result { #[derive(Serialize)] #[serde(rename_all = "camelCase")] - #[serde_as] struct UpdateParams { entity_key: helium_crypto::PublicKey, #[serde(with = "keypair::serde_pubkey")] wallet: keypair::Pubkey, - #[serde_with(as = "Option")] - location: Option, + location: Option, gain: Option, elevation: Option, } @@ -74,7 +71,9 @@ impl Client { let params = UpdateParams { entity_key: hotspot.clone(), wallet: *signer, - location: update.location().map(Into::into), + location: update + .location() + .map(|location| u64::from(location).to_string()), gain: update.gain().and_then(|gain| gain.to_f64()), elevation: update.elevation().to_owned(), }; diff --git a/helium-lib/src/programs.rs b/helium-lib/src/programs.rs index b356f4eb..521bdf72 100644 --- a/helium-lib/src/programs.rs +++ b/helium-lib/src/programs.rs @@ -3,9 +3,6 @@ use crate::keypair::{pubkey, Pubkey}; pub const TOKEN_METADATA_PROGRAM_ID: Pubkey = pubkey!("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"); -pub const MPL_BUBBLEGUM_PROGRAM_ID: Pubkey = - pubkey!("BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY"); - pub const SPL_ACCOUNT_COMPRESSION_PROGRAM_ID: Pubkey = pubkey!("cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK"); diff --git a/helium-lib/src/result.rs b/helium-lib/src/result.rs index 4e170772..9620e28b 100644 --- a/helium-lib/src/result.rs +++ b/helium-lib/src/result.rs @@ -38,6 +38,12 @@ pub enum Error { Encode(#[from] EncodeError), } +impl Error { + pub fn account_not_found() -> Self { + anchor_client::ClientError::AccountNotFound.into() + } +} + #[derive(Debug, Error)] pub enum EncodeError { #[error("proto: {0}")] diff --git a/helium-lib/src/reward.rs b/helium-lib/src/reward.rs index 0fe12f28..c9833dec 100644 --- a/helium-lib/src/reward.rs +++ b/helium-lib/src/reward.rs @@ -3,6 +3,7 @@ use crate::{ dao::SubDao, entity_key::{self, AsEntityKey, KeySerialization}, keypair::{GetPubkey, Keypair, Pubkey}, + kta, programs::SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, result::{DecodeError, Error, Result}, settings::Settings, @@ -84,7 +85,7 @@ where let client = settings.mk_anchor_client(keypair.clone())?; let program = client.program(lazy_distributor::id())?; - let asset_account = asset::account_for_entity_key(&client, &entity_key).await?; + let kta = kta::for_entity_key(&entity_key).await?; let ld_account = lazy_distributor(settings, subdao).await?; let mut ixs: Vec = rewards @@ -97,7 +98,7 @@ where let accounts = lazy_distributor::accounts::SetCurrentRewardsV0 { lazy_distributor: subdao.lazy_distributor(), payer: program.payer(), - recipient: subdao.asset_key_to_receipient_key(&asset_account.asset), + recipient: subdao.receipient_key_from_kta(&kta), oracle: oracle_reward.oracle, system_program: solana_sdk::system_program::id(), }; @@ -113,7 +114,7 @@ where .flatten() .collect(); - let (asset, asset_proof) = asset::get_with_proof(settings, &asset_account).await?; + let (asset, asset_proof) = asset::for_kta_with_proof(settings, &kta).await?; let _args = lazy_distributor::DistributeCompressionRewardsArgsV0 { data_hash: asset.compression.data_hash, @@ -134,7 +135,7 @@ where circuit_breaker_program: circuit_breaker::id(), owner: asset.ownership.owner, circuit_breaker: lazy_distributor_circuit_breaker(&ld_account), - recipient: subdao.asset_key_to_receipient_key(&asset_account.asset), + recipient: subdao.receipient_key_from_kta(&kta), destination_account: subdao .token() .associated_token_adress(&asset.ownership.owner), @@ -233,11 +234,10 @@ pub async fn pending( let entity_key = entity_key::from_string(entity_key_string.clone(), entity_key_encoding)?; let client = settings.mk_anchor_client(Keypair::void())?; - let asset_account = asset::account_for_entity_key(&client, &entity_key).await?; - recipient::for_asset_account(&client, subdao, &asset_account) + let kta = kta::for_entity_key(&entity_key).await?; + recipient::for_kta(&client, subdao, &kta) .and_then(|maybe_recipient| async move { - maybe_recipient - .ok_or_else(|| anchor_client::ClientError::AccountNotFound.into()) + maybe_recipient.ok_or_else(Error::account_not_found) }) .map_ok(|recipient| { for_entity_key(&bulk_rewards, entity_key_string).map(|mut oracle_reward| { @@ -330,13 +330,13 @@ async fn bulk_from_oracle( pub mod recipient { use super::*; - pub async fn for_asset_account>( + pub async fn for_kta>( client: &anchor_client::Client, subdao: &SubDao, - asset_account: &helium_entity_manager::KeyToAssetV0, + kta: &helium_entity_manager::KeyToAssetV0, ) -> Result> { let program = client.program(lazy_distributor::id())?; - let recipient_key = subdao.asset_key_to_receipient_key(&asset_account.asset); + let recipient_key = subdao.receipient_key_from_kta(kta); match program .account::(recipient_key) .await @@ -358,8 +358,8 @@ pub mod recipient { { let client = settings.mk_anchor_client(keypair.clone())?; let program = client.program(lazy_distributor::id())?; - let asset_account = asset::account_for_entity_key(&client, entity_key).await?; - let (asset, asset_proof) = asset::get_with_proof(settings, &asset_account).await?; + let kta = kta::for_entity_key(entity_key).await?; + let (asset, asset_proof) = asset::for_kta_with_proof(settings, &kta).await?; let _args = lazy_distributor::InitializeCompressionRecipientArgsV0 { data_hash: asset.compression.data_hash, @@ -371,7 +371,7 @@ pub mod recipient { let accounts = lazy_distributor::accounts::InitializeCompressionRecipientV0 { payer: program.payer(), lazy_distributor: subdao.lazy_distributor(), - recipient: subdao.asset_key_to_receipient_key(&asset.id), + recipient: subdao.receipient_key_from_kta(&kta), merkle_tree: asset.compression.tree, owner: asset.ownership.owner, delegate: asset.ownership.owner, diff --git a/helium-wallet/src/cmd/router/balance.rs b/helium-wallet/src/cmd/router/balance.rs index 38b667ae..997fc9f3 100644 --- a/helium-wallet/src/cmd/router/balance.rs +++ b/helium-wallet/src/cmd/router/balance.rs @@ -14,7 +14,7 @@ impl Cmd { pub async fn run(&self, opts: Opts) -> Result { let settings = opts.try_into()?; let delegated_dc_key = self.subdao.delegated_dc_key(&self.router_key); - let escrow_key = self.subdao.escrow_account_key(&delegated_dc_key); + let escrow_key = self.subdao.escrow_key(&delegated_dc_key); let balance = token::balance_for_address(&settings, &escrow_key) .await? .map(|balance| balance.amount); diff --git a/helium-wallet/src/main.rs b/helium-wallet/src/main.rs index deed65c0..cd56b2b1 100644 --- a/helium-wallet/src/main.rs +++ b/helium-wallet/src/main.rs @@ -44,6 +44,7 @@ async fn main() -> Result { } async fn run(cli: Cli) -> Result { + helium_lib::init(&cli.opts.clone().try_into()?)?; match cli.cmd { Cmd::Info(cmd) => cmd.run(cli.opts).await, Cmd::Balance(cmd) => cmd.run(cli.opts).await,