Skip to content

Commit

Permalink
pcli: add un-advertised support for importing Prax registry data (#4801)
Browse files Browse the repository at this point in the history
This makes a minimal set of changes to allow `pcli` to make use of the
Prax registry. In order to not pre-empt more comprehensive planning
about how such an integration should work, this PR just changes `pcli`
so that if the registry is placed in a `registry.json` file in the
`pcli` home directory, it will be imported into the view database on
startup.

(cherry picked from commit 5ed9813)
  • Loading branch information
hdevalence authored and conorsch committed Aug 12, 2024
1 parent 7fd39b6 commit 6c845bc
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 38 deletions.
9 changes: 9 additions & 0 deletions crates/bin/pcli/src/opt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,17 @@ impl Opt {
let path = self.home.join(crate::VIEW_FILE_NAME);
tracing::info!(%path, "using local view service");

let registry_path = self.home.join("registry.json");
// Check if the path exists or set it to nojne
let registry_path = if registry_path.exists() {
Some(registry_path)
} else {
None
};

let svc = ViewServer::load_or_initialize(
Some(path),
registry_path,
&config.full_viewing_key,
config.grpc_url.clone(),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ async fn app_can_sweep_a_collection_of_small_notes() -> anyhow::Result<()> {
// Spawn the client-side view server...
let view_server = {
penumbra_view::ViewServer::load_or_initialize(
None::<&camino::Utf8Path>,
None::<&camino::Utf8Path>,
&*test_keys::FULL_VIEWING_KEY,
grpc_url,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ async fn view_server_can_be_served_on_localhost() -> anyhow::Result<()> {
// Spawn the client-side view server...
let view_server = {
penumbra_view::ViewServer::load_or_initialize(
None::<&camino::Utf8Path>,
None::<&camino::Utf8Path>,
&*test_keys::FULL_VIEWING_KEY,
grpc_url,
Expand Down
16 changes: 10 additions & 6 deletions crates/core/asset/src/asset/denom_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub struct Metadata {
}

// These are constructed by the asset registry.
#[derive(Debug)]
pub(super) struct Inner {
// The Penumbra asset ID
id: Id,
Expand Down Expand Up @@ -317,16 +318,19 @@ impl Metadata {
if amount == 0u64.into() {
return self.default_unit();
}
let mut selected_index = 0;
let mut selected_exponent = 0;
for (unit_index, unit) in self.inner.units.iter().enumerate() {
let unit_amount = Amount::from(10u128.pow(unit.exponent as u32));
if amount >= unit_amount {
return Unit {
unit_index,
inner: self.inner.clone(),
};
if unit_amount <= amount && unit.exponent >= selected_exponent {
selected_index = unit_index;
selected_exponent = unit.exponent;
}
}
self.base_unit()
return Unit {
unit_index: selected_index,
inner: self.inner.clone(),
};
}

pub fn starts_with(&self, prefix: &str) -> bool {
Expand Down
21 changes: 11 additions & 10 deletions crates/view/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use tap::{Tap, TapFallible};
use tokio::sync::{watch, RwLock};
use tokio_stream::wrappers::WatchStream;
use tonic::{async_trait, transport::Channel, Request, Response, Status};
use tracing::instrument;
use tracing::{instrument, Instrument};
use url::Url;

use penumbra_asset::{asset, asset::Metadata, Value};
Expand Down Expand Up @@ -91,15 +91,9 @@ pub struct ViewServer {

impl ViewServer {
/// Convenience method that calls [`Storage::load_or_initialize`] and then [`Self::new`].
#[instrument(
skip_all,
fields(
path = ?storage_path.as_ref().map(|p| p.as_ref().as_str()),
url = %node,
)
)]
pub async fn load_or_initialize(
storage_path: Option<impl AsRef<Utf8Path>>,
registry_path: Option<impl AsRef<Utf8Path>>,
fvk: &FullViewingKey,
node: Url,
) -> anyhow::Result<Self> {
Expand All @@ -108,6 +102,10 @@ impl ViewServer {
.await?
.tap(|_| tracing::debug!("storage is ready"));

if let Some(registry_path) = registry_path {
storage.load_asset_metadata(registry_path).await?;
}

Self::new(storage, node)
.tap(|_| tracing::trace!("constructing view server"))
.await
Expand All @@ -121,22 +119,25 @@ impl ViewServer {
/// To create multiple [`ViewService`]s, clone the [`ViewService`] returned
/// by this method, rather than calling it multiple times. That way, each clone
/// will be backed by the same scanning task, rather than each spawning its own.
#[instrument(skip_all)]
pub async fn new(storage: Storage, node: Url) -> anyhow::Result<Self> {
let span = tracing::error_span!(parent: None, "view");
let channel = Channel::from_shared(node.to_string())
.with_context(|| "could not parse node URI")?
.connect()
.instrument(span.clone())
.await
.with_context(|| "could not connect to grpc server")
.tap_err(|error| tracing::error!(?error, "could not connect to grpc server"))?;

let (worker, state_commitment_tree, error_slot, sync_height_rx) =
Worker::new(storage.clone(), channel)
.instrument(span.clone())
.tap(|_| tracing::trace!("constructing view server worker"))
.await?
.tap(|_| tracing::debug!("constructed view server worker"));

tokio::spawn(worker.run()).tap(|_| tracing::debug!("spawned view server worker"));
tokio::spawn(worker.run().instrument(span))
.tap(|_| tracing::debug!("spawned view server worker"));

Ok(Self {
storage,
Expand Down
66 changes: 45 additions & 21 deletions crates/view/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,36 @@ impl Storage {
.await?
}

/// Loads asset metadata from a JSON file and use to update the database.
pub async fn load_asset_metadata(
&self,
registry_path: impl AsRef<Utf8Path>,
) -> anyhow::Result<()> {
tracing::debug!(registry_path = ?registry_path.as_ref(), "loading asset metadata");
let registry_path = registry_path.as_ref();
// Parse into a serde_json::Value first so we can get the bits we care about
let mut registry_json: serde_json::Value = serde_json::from_str(
std::fs::read_to_string(registry_path)
.context("failed to read file")?
.as_str(),
)
.context("failed to parse JSON")?;

let registry: BTreeMap<String, Metadata> = serde_json::value::from_value(
registry_json
.get_mut("assetById")
.ok_or_else(|| anyhow::anyhow!("missing assetById"))?
.take(),
)
.context("could not parse asset registry")?;

for metadata in registry.into_values() {
self.record_asset(metadata).await?;
}

Ok(())
}

/// Query for account balance by address
pub async fn balances(
&self,
Expand Down Expand Up @@ -793,14 +823,10 @@ impl Storage {

spawn_blocking(move || {
pool.get()?
.prepare_cached("SELECT * FROM assets")?
.prepare_cached("SELECT metadata FROM assets")?
.query_and_then([], |row| {
let _asset_id: Vec<u8> = row.get("asset_id")?;
let denom: String = row.get("denom")?;

let denom_metadata = asset::REGISTRY
.parse_denom(&denom)
.ok_or_else(|| anyhow::anyhow!("invalid denomination {}", denom))?;
let metadata_json = row.get::<_, String>("metadata")?;
let denom_metadata = serde_json::from_str(&metadata_json)?;

anyhow::Ok(denom_metadata)
})?
Expand All @@ -816,13 +842,10 @@ impl Storage {

spawn_blocking(move || {
pool.get()?
.prepare_cached("SELECT * FROM assets WHERE asset_id = ?1")?
.prepare_cached("SELECT metadata FROM assets WHERE asset_id = ?1")?
.query_and_then([id], |row| {
let _asset_id: Vec<u8> = row.get("asset_id")?;
let denom: String = row.get("denom")?;
let denom_metadata = asset::REGISTRY
.parse_denom(&denom)
.ok_or_else(|| anyhow::anyhow!("invalid denomination {}", denom))?;
let metadata_json = row.get::<_, String>("metadata")?;
let denom_metadata = serde_json::from_str(&metadata_json)?;
anyhow::Ok(denom_metadata)
})?
.next()
Expand All @@ -840,13 +863,10 @@ impl Storage {

spawn_blocking(move || {
pool.get()?
.prepare_cached("SELECT * FROM assets WHERE denom LIKE ?1 ESCAPE '\\'")?
.prepare_cached("SELECT metadata FROM assets WHERE denom LIKE ?1 ESCAPE '\\'")?
.query_and_then([pattern], |row| {
let _asset_id: Vec<u8> = row.get("asset_id")?;
let denom: String = row.get("denom")?;
let denom_metadata = asset::REGISTRY
.parse_denom(&denom)
.ok_or_else(|| anyhow::anyhow!("invalid denomination {}", denom))?;
let metadata_json = row.get::<_, String>("metadata")?;
let denom_metadata = serde_json::from_str(&metadata_json)?;
anyhow::Ok(denom_metadata)
})?
.collect()
Expand Down Expand Up @@ -1030,17 +1050,21 @@ impl Storage {
}).await?
}

#[tracing::instrument(skip(self))]
pub async fn record_asset(&self, asset: Metadata) -> anyhow::Result<()> {
tracing::debug!(?asset);

let asset_id = asset.id().to_bytes().to_vec();
let denom = asset.base_denom().denom;
let metadata_json = serde_json::to_string(&asset)?;

let pool = self.pool.clone();

spawn_blocking(move || {
pool.get()?
.execute(
"INSERT OR IGNORE INTO assets (asset_id, denom) VALUES (?1, ?2)",
(asset_id, denom),
"INSERT OR REPLACE INTO assets (asset_id, denom, metadata) VALUES (?1, ?2, ?3)",
(asset_id, denom, metadata_json),
)
.map_err(anyhow::Error::from)
})
Expand Down
3 changes: 2 additions & 1 deletion crates/view/src/storage/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ CREATE TABLE sync_height (height BIGINT NOT NULL);
-- used for storing a cache of known assets
CREATE TABLE assets (
asset_id BLOB PRIMARY KEY NOT NULL,
denom TEXT NOT NULL
denom TEXT NOT NULL,
metadata TEXT NOT NULL
);

-- the shape information about the sct
Expand Down

0 comments on commit 6c845bc

Please sign in to comment.