feat(profiles): chains as data, shipped in the binary and overridable by file
ci / gate (push) Successful in 6m18s
ci / gate (push) Successful in 6m18s
`wallet-core/src/profiles.toml` carries what runtime metadata does not: genesis hash, SS58 prefix, symbol and decimals, coin type, the endpoint list, block time, and the rule for when the FIPS 204 signing context applies. Three profiles ship: Quantus mainnet at spec 152 with QTC, Planck with PLK (in the wallet only to prove the multi-chain UX), and a local dev node with an empty genesis hash that means "do not check". Endpoints are the names decided on #20 and #53, one per chain; until those exist an override file points a dev run at the public nodes. The override file is the same shape with every field optional: a known id has fields replaced, an unknown id must be complete and is appended. A mainnet endpoint must be wss, a genesis hash must be 66 characters or empty, a context at most 255 bytes; anything else is refused with the profile named. Adding a fourth chain is a TOML entry and no Rust, which the test does. `signing_context_for(profile, spec)` and `genesis_matches(profile, hash)` are the two pure functions the adapter (#19, #20) will call at connect. The app loads `<app data>/profiles.toml` or WALLET_PROFILES and exposes `chains_list`. Closes #18 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW
This commit is contained in:
Generated
+2
@@ -4485,7 +4485,9 @@ dependencies = [
|
||||
"async-trait",
|
||||
"blake2",
|
||||
"bs58",
|
||||
"serde",
|
||||
"thiserror 2.0.18",
|
||||
"toml 0.8.2",
|
||||
"wallet-entities",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -18,7 +18,7 @@ use wallet_data::mnemonic;
|
||||
use wallet_entities::{
|
||||
AppInfo, CreationChallenge, CreationStart, SignatureScheme, WalletError, WalletSummary,
|
||||
};
|
||||
use wallet_entities::{SessionAccountInfo, SessionStatusInfo};
|
||||
use wallet_entities::{ChainProfile, SessionAccountInfo, SessionStatusInfo};
|
||||
|
||||
use crate::state::{AppState, PendingCreation};
|
||||
|
||||
@@ -216,3 +216,10 @@ pub fn session_status(state: State<'_, AppState>) -> Result<SessionStatusInfo, W
|
||||
locks_in_seconds: st.locks_in,
|
||||
})
|
||||
}
|
||||
|
||||
/// The chains this build knows, shipped profiles first, then any the
|
||||
/// operator's override file added.
|
||||
#[tauri::command]
|
||||
pub fn chains_list(state: State<'_, AppState>) -> Result<Vec<ChainProfile>, WalletError> {
|
||||
Ok(state.profiles.iter().cloned().collect())
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ pub fn run() {
|
||||
commands::unlock,
|
||||
commands::lock,
|
||||
commands::session_status,
|
||||
commands::chains_list,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("the Tauri runtime failed to start");
|
||||
|
||||
@@ -7,6 +7,7 @@ use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
|
||||
use wallet_core::backup::BackupChallenge;
|
||||
use wallet_core::profiles::ProfileRegistry;
|
||||
use wallet_core::session::Session;
|
||||
use wallet_data::keystore::FileKeystore;
|
||||
use wallet_entities::SignatureScheme;
|
||||
@@ -27,6 +28,7 @@ pub struct AppState {
|
||||
pub keystore: FileKeystore,
|
||||
pub pending_creation: Mutex<Option<PendingCreation>>,
|
||||
pub session: Session,
|
||||
pub profiles: ProfileRegistry,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
@@ -40,10 +42,27 @@ impl AppState {
|
||||
.ok_or_else(|| anyhow::anyhow!("no app-data directory on this platform"))?,
|
||||
};
|
||||
tracing::info!(dir = %dir.display(), "keystore directory");
|
||||
|
||||
// Shipped profiles, then the operator's override file if there is
|
||||
// one: `<app data>/profiles.toml`, or whatever WALLET_PROFILES says.
|
||||
let override_path = match std::env::var_os("WALLET_PROFILES") {
|
||||
Some(p) => Some(std::path::PathBuf::from(p)),
|
||||
None => FileKeystore::default_dir()
|
||||
.map(|d| d.parent().map(|p| p.join("profiles.toml")).unwrap_or(d)),
|
||||
};
|
||||
let mut profiles = ProfileRegistry::shipped()?;
|
||||
if let Some(path) = override_path {
|
||||
if path.exists() {
|
||||
let text = std::fs::read_to_string(&path)?;
|
||||
profiles = profiles.with_override(&text)?;
|
||||
tracing::info!(path = %path.display(), "applied profile overrides");
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
keystore: FileKeystore::in_dir(dir),
|
||||
pending_creation: Mutex::new(None),
|
||||
session: Session::new(DEFAULT_IDLE_LOCK),
|
||||
profiles,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,3 +15,5 @@ thiserror.workspace = true
|
||||
zeroize.workspace = true
|
||||
blake2.workspace = true
|
||||
bs58.workspace = true
|
||||
serde.workspace = true
|
||||
toml.workspace = true
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
pub mod backup;
|
||||
pub mod paths;
|
||||
pub mod ports;
|
||||
pub mod profiles;
|
||||
pub mod session;
|
||||
pub mod ss58;
|
||||
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
//! Chain profiles: the per-chain facts that runtime metadata does not
|
||||
//! carry. The shipped set is `profiles.toml` in this crate; an operator
|
||||
//! override file of the same shape, found by the app, replaces fields by
|
||||
//! profile id and may add profiles. Everything here is pure: the app reads
|
||||
//! the override file and hands the text in.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::Deserialize;
|
||||
use wallet_entities::{ChainId, ChainKind, ChainProfile, SigningContextRule};
|
||||
|
||||
const SHIPPED: &str = include_str!("profiles.toml");
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ProfileError {
|
||||
#[error("profiles: {0}")]
|
||||
Parse(String),
|
||||
#[error("profile {id}: {why}")]
|
||||
Invalid { id: String, why: String },
|
||||
#[error("override for unknown profile {0} must be complete")]
|
||||
IncompleteOverride(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ProfilesFile {
|
||||
#[serde(default)]
|
||||
profile: Vec<ChainProfile>,
|
||||
}
|
||||
|
||||
/// An override entry: every field optional, matched by id.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct OverrideFile {
|
||||
#[serde(default)]
|
||||
profile: Vec<ProfileOverride>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ProfileOverride {
|
||||
id: String,
|
||||
display_name: Option<String>,
|
||||
kind: Option<ChainKind>,
|
||||
genesis_hash: Option<String>,
|
||||
ss58_prefix: Option<u16>,
|
||||
token_symbol: Option<String>,
|
||||
token_decimals: Option<u8>,
|
||||
coin_type: Option<u32>,
|
||||
endpoints: Option<Vec<String>>,
|
||||
block_time_ms: Option<u32>,
|
||||
signing_context: Option<SigningContextRule>,
|
||||
}
|
||||
|
||||
/// The set of chains this build knows, in file order.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProfileRegistry {
|
||||
profiles: BTreeMap<String, ChainProfile>,
|
||||
order: Vec<String>,
|
||||
}
|
||||
|
||||
impl ProfileRegistry {
|
||||
/// The shipped profiles alone.
|
||||
pub fn shipped() -> Result<Self, ProfileError> {
|
||||
Self::from_toml(SHIPPED)
|
||||
}
|
||||
|
||||
pub fn from_toml(text: &str) -> Result<Self, ProfileError> {
|
||||
let file: ProfilesFile =
|
||||
toml::from_str(text).map_err(|e| ProfileError::Parse(e.to_string()))?;
|
||||
let mut profiles = BTreeMap::new();
|
||||
let mut order = vec![];
|
||||
for p in file.profile {
|
||||
validate(&p)?;
|
||||
if profiles.insert(p.id.0.clone(), p.clone()).is_some() {
|
||||
return Err(ProfileError::Invalid {
|
||||
id: p.id.0,
|
||||
why: "duplicate id".into(),
|
||||
});
|
||||
}
|
||||
order.push(p.id.0);
|
||||
}
|
||||
Ok(Self { profiles, order })
|
||||
}
|
||||
|
||||
/// Apply an operator override file. A known id has any subset of fields
|
||||
/// replaced; an unknown id must be a complete profile, and is appended.
|
||||
pub fn with_override(mut self, text: &str) -> Result<Self, ProfileError> {
|
||||
let file: OverrideFile =
|
||||
toml::from_str(text).map_err(|e| ProfileError::Parse(e.to_string()))?;
|
||||
for o in file.profile {
|
||||
match self.profiles.get_mut(&o.id) {
|
||||
Some(p) => {
|
||||
if let Some(v) = o.display_name {
|
||||
p.display_name = v;
|
||||
}
|
||||
if let Some(v) = o.kind {
|
||||
p.kind = v;
|
||||
}
|
||||
if let Some(v) = o.genesis_hash {
|
||||
p.genesis_hash = v;
|
||||
}
|
||||
if let Some(v) = o.ss58_prefix {
|
||||
p.ss58_prefix = v;
|
||||
}
|
||||
if let Some(v) = o.token_symbol {
|
||||
p.token_symbol = v;
|
||||
}
|
||||
if let Some(v) = o.token_decimals {
|
||||
p.token_decimals = v;
|
||||
}
|
||||
if let Some(v) = o.coin_type {
|
||||
p.coin_type = v;
|
||||
}
|
||||
if let Some(v) = o.endpoints {
|
||||
p.endpoints = v;
|
||||
}
|
||||
if let Some(v) = o.block_time_ms {
|
||||
p.block_time_ms = v;
|
||||
}
|
||||
if let Some(v) = o.signing_context {
|
||||
p.signing_context = v;
|
||||
}
|
||||
validate(p)?;
|
||||
}
|
||||
None => {
|
||||
let complete = (|| {
|
||||
Some(ChainProfile {
|
||||
id: ChainId(o.id.clone()),
|
||||
display_name: o.display_name?,
|
||||
kind: o.kind?,
|
||||
genesis_hash: o.genesis_hash?,
|
||||
ss58_prefix: o.ss58_prefix?,
|
||||
token_symbol: o.token_symbol?,
|
||||
token_decimals: o.token_decimals?,
|
||||
coin_type: o.coin_type?,
|
||||
endpoints: o.endpoints?,
|
||||
block_time_ms: o.block_time_ms?,
|
||||
signing_context: o.signing_context?,
|
||||
})
|
||||
})()
|
||||
.ok_or_else(|| ProfileError::IncompleteOverride(o.id.clone()))?;
|
||||
validate(&complete)?;
|
||||
self.order.push(o.id.clone());
|
||||
self.profiles.insert(o.id, complete);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn get(&self, id: &ChainId) -> Option<&ChainProfile> {
|
||||
self.profiles.get(&id.0)
|
||||
}
|
||||
|
||||
/// Profiles in file order: shipped first, then any added by override.
|
||||
pub fn iter(&self) -> impl Iterator<Item = &ChainProfile> {
|
||||
self.order.iter().filter_map(|id| self.profiles.get(id))
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.profiles.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.profiles.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
fn validate(p: &ChainProfile) -> Result<(), ProfileError> {
|
||||
let bad = |why: &str| {
|
||||
Err(ProfileError::Invalid {
|
||||
id: p.id.0.clone(),
|
||||
why: why.into(),
|
||||
})
|
||||
};
|
||||
if p.id.0.is_empty()
|
||||
|| !p
|
||||
.id
|
||||
.0
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
|
||||
{
|
||||
return bad("id must be lowercase ascii, digits and hyphens");
|
||||
}
|
||||
if !p.genesis_hash.is_empty()
|
||||
&& (p.genesis_hash.len() != 66 || !p.genesis_hash.starts_with("0x"))
|
||||
{
|
||||
return bad("genesis_hash must be 0x plus 64 hex digits, or empty to skip the check");
|
||||
}
|
||||
if p.endpoints.is_empty() {
|
||||
return bad("at least one endpoint");
|
||||
}
|
||||
if !p
|
||||
.endpoints
|
||||
.iter()
|
||||
.all(|e| e.starts_with("wss://") || e.starts_with("ws://"))
|
||||
{
|
||||
return bad("endpoints must be ws:// or wss:// URLs");
|
||||
}
|
||||
if p.kind == ChainKind::Mainnet && !p.endpoints.iter().all(|e| e.starts_with("wss://")) {
|
||||
return bad("a mainnet endpoint must be wss://");
|
||||
}
|
||||
if p.token_decimals > 30 {
|
||||
return bad("token_decimals is implausible");
|
||||
}
|
||||
if p.signing_context.context.len() > 255 {
|
||||
return bad("a FIPS 204 context is at most 255 bytes");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The FIPS 204 context to sign under for a given runtime spec version:
|
||||
/// empty below the profile's minimum, the profile's string from it.
|
||||
pub fn signing_context_for(profile: &ChainProfile, spec_version: u32) -> &[u8] {
|
||||
if spec_version >= profile.signing_context.min_spec {
|
||||
profile.signing_context.context.as_bytes()
|
||||
} else {
|
||||
b""
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a node reporting `observed_genesis` is the chain this profile
|
||||
/// describes. An empty profile hash means "do not check" (dev nodes).
|
||||
pub fn genesis_matches(profile: &ChainProfile, observed_genesis: &str) -> bool {
|
||||
profile.genesis_hash.is_empty() || profile.genesis_hash.eq_ignore_ascii_case(observed_genesis)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn shipped_profiles_load_and_are_as_decided() {
|
||||
let r = ProfileRegistry::shipped().unwrap();
|
||||
let ids: Vec<&str> = r.iter().map(|p| p.id.0.as_str()).collect();
|
||||
assert_eq!(ids, ["quantus", "planck", "dev"]);
|
||||
let q = r.get(&ChainId("quantus".into())).unwrap();
|
||||
assert_eq!(q.endpoints, ["wss://quantus.blackbeard.observer"]);
|
||||
assert_eq!(q.token_symbol, "QTC");
|
||||
assert_eq!(q.ss58_prefix, 189);
|
||||
assert_eq!(q.coin_type, 189_189);
|
||||
assert_eq!(signing_context_for(q, 152), b"QUANTUS_EXTRINSIC");
|
||||
assert_eq!(signing_context_for(q, 147), b"");
|
||||
assert!(genesis_matches(
|
||||
q,
|
||||
"0xFB5487C0BE6AE4ADE2D41D16E50465129861636C2B8D61FA94D7A19631626FBA"
|
||||
));
|
||||
assert!(!genesis_matches(
|
||||
q,
|
||||
"0x4901bf5c57fd3f9e726af399c763de6670dbdb115a91c0237e173f16eef65e72"
|
||||
));
|
||||
let dev = r.get(&ChainId("dev".into())).unwrap();
|
||||
assert!(genesis_matches(dev, "0xanything"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_override_replaces_fields_by_id_and_adds_complete_profiles() {
|
||||
let r = ProfileRegistry::shipped()
|
||||
.unwrap()
|
||||
.with_override(
|
||||
r#"
|
||||
[[profile]]
|
||||
id = "planck"
|
||||
endpoints = ["wss://a1-planck.quantus.cat", "wss://a2-planck.quantus.cat"]
|
||||
|
||||
[[profile]]
|
||||
id = "fourth"
|
||||
display_name = "A fourth chain"
|
||||
kind = "testnet"
|
||||
genesis_hash = ""
|
||||
ss58_prefix = 42
|
||||
token_symbol = "FOUR"
|
||||
token_decimals = 10
|
||||
coin_type = 4
|
||||
endpoints = ["wss://fourth.example"]
|
||||
block_time_ms = 6000
|
||||
signing_context = { min_spec = 0, context = "" }
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(r.len(), 4);
|
||||
assert_eq!(r.get(&ChainId("planck".into())).unwrap().endpoints.len(), 2);
|
||||
assert_eq!(
|
||||
r.get(&ChainId("planck".into())).unwrap().token_symbol,
|
||||
"PLK"
|
||||
);
|
||||
let fourth = r.get(&ChainId("fourth".into())).unwrap();
|
||||
assert_eq!(signing_context_for(fourth, 1), b"");
|
||||
assert_eq!(r.iter().last().unwrap().id.0, "fourth");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_incomplete_new_profile_or_a_bad_field_is_refused() {
|
||||
let base = ProfileRegistry::shipped().unwrap();
|
||||
assert!(matches!(
|
||||
base.clone()
|
||||
.with_override("[[profile]]\nid = \"x\"\ndisplay_name = \"X\"\n"),
|
||||
Err(ProfileError::IncompleteOverride(_))
|
||||
));
|
||||
assert!(matches!(
|
||||
base.clone()
|
||||
.with_override("[[profile]]\nid = \"quantus\"\nendpoints = [\"ws://insecure\"]\n"),
|
||||
Err(ProfileError::Invalid { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
base.with_override("[[profile]]\nid = \"quantus\"\ngenesis_hash = \"0x12\"\n"),
|
||||
Err(ProfileError::Invalid { .. })
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
# Chain profiles: everything the wallet needs to know about a chain that is
|
||||
# not in its runtime metadata. Adding a chain is one entry here and no Rust.
|
||||
#
|
||||
# Endpoints are our own names (issue #53): one per chain, TLS on oolon,
|
||||
# no auth, throttled. An operator override file in the app-data directory
|
||||
# (`profiles.toml`, same shape) replaces any field of any profile by id and
|
||||
# may add profiles; that is how a dev run points at a local node.
|
||||
|
||||
[[profile]]
|
||||
id = "quantus"
|
||||
display_name = "Quantus"
|
||||
kind = "mainnet"
|
||||
genesis_hash = "0xfb5487c0be6ae4ade2d41d16e50465129861636c2b8d61fa94d7a19631626fba"
|
||||
ss58_prefix = 189
|
||||
token_symbol = "QTC"
|
||||
token_decimals = 12
|
||||
coin_type = 189189
|
||||
endpoints = ["wss://quantus.blackbeard.observer"]
|
||||
block_time_ms = 12000
|
||||
# From spec 148 the runtime verifies extrinsics under this FIPS 204 context;
|
||||
# earlier specs use none. Mainnet launched at 152.
|
||||
signing_context = { min_spec = 148, context = "QUANTUS_EXTRINSIC" }
|
||||
|
||||
[[profile]]
|
||||
id = "planck"
|
||||
display_name = "Planck"
|
||||
kind = "testnet"
|
||||
genesis_hash = "0x4901bf5c57fd3f9e726af399c763de6670dbdb115a91c0237e173f16eef65e72"
|
||||
ss58_prefix = 189
|
||||
token_symbol = "PLK"
|
||||
token_decimals = 12
|
||||
coin_type = 189189
|
||||
endpoints = ["wss://planck.blackbeard.observer"]
|
||||
block_time_ms = 6000
|
||||
signing_context = { min_spec = 148, context = "QUANTUS_EXTRINSIC" }
|
||||
|
||||
[[profile]]
|
||||
id = "dev"
|
||||
display_name = "Local dev node"
|
||||
kind = "dev"
|
||||
# `quantus-node --dev` regenerates its genesis; an empty hash means "do not
|
||||
# check", which is acceptable for a chain that holds nothing.
|
||||
genesis_hash = ""
|
||||
ss58_prefix = 189
|
||||
token_symbol = "DEV"
|
||||
token_decimals = 12
|
||||
coin_type = 189189
|
||||
endpoints = ["ws://127.0.0.1:9944"]
|
||||
block_time_ms = 12000
|
||||
signing_context = { min_spec = 148, context = "QUANTUS_EXTRINSIC" }
|
||||
@@ -16,6 +16,47 @@ use ts_rs::TS;
|
||||
#[ts(export)]
|
||||
pub struct ChainId(pub String);
|
||||
|
||||
/// What a chain is for. The UI labels testnets and never offers a dev chain
|
||||
/// outside a dev build.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[ts(export)]
|
||||
pub enum ChainKind {
|
||||
Mainnet,
|
||||
Testnet,
|
||||
Dev,
|
||||
}
|
||||
|
||||
/// When a runtime spec version starts verifying signatures under a FIPS 204
|
||||
/// context string. Below `min_spec` the context is empty.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
pub struct SigningContextRule {
|
||||
pub min_spec: u32,
|
||||
pub context: String,
|
||||
}
|
||||
|
||||
/// Everything about a chain that runtime metadata does not say. Shipped as
|
||||
/// TOML in `wallet-core`, overridable by an operator file.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
pub struct ChainProfile {
|
||||
pub id: ChainId,
|
||||
pub display_name: String,
|
||||
pub kind: ChainKind,
|
||||
/// `0x` plus 64 hex digits, or empty to skip the check (dev nodes).
|
||||
pub genesis_hash: String,
|
||||
pub ss58_prefix: u16,
|
||||
pub token_symbol: String,
|
||||
pub token_decimals: u8,
|
||||
/// BIP44 coin type for account derivation.
|
||||
pub coin_type: u32,
|
||||
/// WebSocket JSON-RPC endpoints, tried in order.
|
||||
pub endpoints: Vec<String>,
|
||||
pub block_time_ms: u32,
|
||||
pub signing_context: SigningContextRule,
|
||||
}
|
||||
|
||||
/// What kind of thing an asset is on its chain.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, TS)]
|
||||
#[serde(tag = "kind", content = "id", rename_all = "snake_case")]
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* What a chain is for. The UI labels testnets and never offers a dev chain
|
||||
* outside a dev build.
|
||||
*/
|
||||
export type ChainKind = "mainnet" | "testnet" | "dev";
|
||||
@@ -0,0 +1,22 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ChainId } from "./ChainId";
|
||||
import type { ChainKind } from "./ChainKind";
|
||||
import type { SigningContextRule } from "./SigningContextRule";
|
||||
|
||||
/**
|
||||
* Everything about a chain that runtime metadata does not say. Shipped as
|
||||
* TOML in `wallet-core`, overridable by an operator file.
|
||||
*/
|
||||
export type ChainProfile = { id: ChainId, display_name: string, kind: ChainKind,
|
||||
/**
|
||||
* `0x` plus 64 hex digits, or empty to skip the check (dev nodes).
|
||||
*/
|
||||
genesis_hash: string, ss58_prefix: number, token_symbol: string, token_decimals: number,
|
||||
/**
|
||||
* BIP44 coin type for account derivation.
|
||||
*/
|
||||
coin_type: number,
|
||||
/**
|
||||
* WebSocket JSON-RPC endpoints, tried in order.
|
||||
*/
|
||||
endpoints: Array<string>, block_time_ms: number, signing_context: SigningContextRule, };
|
||||
@@ -0,0 +1,7 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* When a runtime spec version starts verifying signatures under a FIPS 204
|
||||
* context string. Below `min_spec` the context is empty.
|
||||
*/
|
||||
export type SigningContextRule = { min_spec: number, context: string, };
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import type { AppInfo } from './generated/AppInfo'
|
||||
import type { ChainProfile } from './generated/ChainProfile'
|
||||
import type { CreationStart } from './generated/CreationStart'
|
||||
import type { SessionStatusInfo } from './generated/SessionStatusInfo'
|
||||
import type { SignatureScheme } from './generated/SignatureScheme'
|
||||
@@ -102,3 +103,7 @@ export function lock(): Promise<void> {
|
||||
export function sessionStatus(): Promise<SessionStatusInfo> {
|
||||
return command<SessionStatusInfo>('session_status')
|
||||
}
|
||||
|
||||
export function chainsList(): Promise<ChainProfile[]> {
|
||||
return command<ChainProfile[]>('chains_list')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user