-
Notifications
You must be signed in to change notification settings - Fork 0
feat(cli): move secrets to OS keychain, update mach token metadata to github #172
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,26 +1,29 @@ | ||||||||||||||||||||
| use anyhow::{Context, Result}; | ||||||||||||||||||||
| use keyring::Entry; | ||||||||||||||||||||
| use serde::{Deserialize, Serialize}; | ||||||||||||||||||||
| use std::path::PathBuf; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[derive(Debug, Clone, Serialize, Deserialize, Default)] | ||||||||||||||||||||
| pub struct Config { | ||||||||||||||||||||
| pub api_url: String, | ||||||||||||||||||||
| /// Stored in OS keychain — field is None in the TOML file on disk. | ||||||||||||||||||||
| #[serde(skip_serializing_if = "Option::is_none")] | ||||||||||||||||||||
| pub api_key: Option<String>, | ||||||||||||||||||||
| pub email: Option<String>, | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /// Database connection URL (sqlite path or postgres/neon URL) | ||||||||||||||||||||
| /// Stored in OS keychain — field is None in the TOML file on disk. | ||||||||||||||||||||
| #[serde(skip_serializing_if = "Option::is_none")] | ||||||||||||||||||||
| pub db_url: Option<String>, | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /// Configured AI model providers | ||||||||||||||||||||
| /// Configured AI model providers (api_key fields stored in keychain) | ||||||||||||||||||||
| #[serde(skip_serializing_if = "Vec::is_empty", default)] | ||||||||||||||||||||
| pub model_providers: Vec<ModelProvider>, | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /// Node participation config — set when this machine has joined the compute network | ||||||||||||||||||||
| #[serde(skip_serializing_if = "Option::is_none")] | ||||||||||||||||||||
| pub node: Option<NodeConfig>, | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /// Cached tier from last successful auth (access/m1/m5/m10/teams/enterprise/beta) | ||||||||||||||||||||
| /// Cached tier from last successful auth | ||||||||||||||||||||
| #[serde(skip_serializing_if = "Option::is_none")] | ||||||||||||||||||||
| pub tier: Option<String>, | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -36,30 +39,55 @@ pub struct Config { | |||||||||||||||||||
| /// Stored when a machine joins the Maschina compute network. | ||||||||||||||||||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||||||||||||||||||||
| pub struct NodeConfig { | ||||||||||||||||||||
| /// UUID assigned by the API on registration | ||||||||||||||||||||
| pub node_id: String, | ||||||||||||||||||||
| /// Base64-encoded Ed25519 signing key (32 bytes) — used to sign execution receipts | ||||||||||||||||||||
| pub signing_key: String, | ||||||||||||||||||||
| /// URL of the local Python runtime this node will forward tasks to | ||||||||||||||||||||
| /// Base64-encoded Ed25519 signing key — stored in OS keychain, not on disk. | ||||||||||||||||||||
| #[serde(skip_serializing_if = "Option::is_none")] | ||||||||||||||||||||
| pub signing_key: Option<String>, | ||||||||||||||||||||
| pub runtime_url: String, | ||||||||||||||||||||
| /// NATS server URL for receiving tasks | ||||||||||||||||||||
| pub nats_url: String, | ||||||||||||||||||||
| /// Optional path to a CA certificate for TLS-enabled NATS connections | ||||||||||||||||||||
| #[serde(skip_serializing_if = "Option::is_none")] | ||||||||||||||||||||
| pub nats_ca_cert: Option<String>, | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||||||||||||||||||||
| pub struct ModelProvider { | ||||||||||||||||||||
| /// e.g. "anthropic", "openai", "ollama", "openrouter", "gemini", "mistral" | ||||||||||||||||||||
| pub name: String, | ||||||||||||||||||||
| /// Stored in OS keychain — field is None in the TOML file on disk. | ||||||||||||||||||||
| #[serde(skip_serializing_if = "Option::is_none")] | ||||||||||||||||||||
| pub api_key: Option<String>, | ||||||||||||||||||||
| /// For Ollama or custom OpenAI-compatible endpoints | ||||||||||||||||||||
| #[serde(skip_serializing_if = "Option::is_none")] | ||||||||||||||||||||
| pub base_url: Option<String>, | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // ─── Keychain helpers ───────────────────────────────────────────────────────── | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const KEYRING_SERVICE: &str = "maschina"; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| fn kr_key(profile: &str, field: &str) -> String { | ||||||||||||||||||||
| format!("{profile}/{field}") | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| fn kr_get(profile: &str, field: &str) -> Option<String> { | ||||||||||||||||||||
| Entry::new(KEYRING_SERVICE, &kr_key(profile, field)) | ||||||||||||||||||||
| .ok() | ||||||||||||||||||||
| .and_then(|e| e.get_password().ok()) | ||||||||||||||||||||
| .filter(|s| !s.is_empty()) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| fn kr_set(profile: &str, field: &str, value: &str) { | ||||||||||||||||||||
| if let Ok(entry) = Entry::new(KEYRING_SERVICE, &kr_key(profile, field)) { | ||||||||||||||||||||
| let _ = entry.set_password(value); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| fn kr_del(profile: &str, field: &str) { | ||||||||||||||||||||
| if let Ok(entry) = Entry::new(KEYRING_SERVICE, &kr_key(profile, field)) { | ||||||||||||||||||||
| let _ = entry.delete_credential(); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // ─── Config impl ────────────────────────────────────────────────────────────── | ||||||||||||||||||||
|
|
||||||||||||||||||||
| impl Config { | ||||||||||||||||||||
| pub fn default_api_url() -> String { | ||||||||||||||||||||
| "https://api.maschina.ai".to_string() | ||||||||||||||||||||
|
|
@@ -73,7 +101,7 @@ impl Config { | |||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /// ~/.config/maschina/<profile>.toml | ||||||||||||||||||||
| /// `~/.config/maschina/<profile>.toml` | ||||||||||||||||||||
| pub fn config_path(profile: &str) -> Result<PathBuf> { | ||||||||||||||||||||
| let base = dirs::config_dir() | ||||||||||||||||||||
| .or_else(dirs::home_dir) | ||||||||||||||||||||
|
|
@@ -86,36 +114,90 @@ pub fn config_path(profile: &str) -> Result<PathBuf> { | |||||||||||||||||||
| Ok(base.join("maschina").join(filename)) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /// Load config from disk and inject secrets from OS keychain. | ||||||||||||||||||||
| /// Falls back to any plaintext value already in the TOML (handles migration | ||||||||||||||||||||
| /// from older versions that stored secrets in the file). | ||||||||||||||||||||
| pub fn load(profile: &str) -> Result<Config> { | ||||||||||||||||||||
| let path = config_path(profile)?; | ||||||||||||||||||||
| if !path.exists() { | ||||||||||||||||||||
| return Ok(Config { | ||||||||||||||||||||
| let mut cfg = if path.exists() { | ||||||||||||||||||||
| let contents = std::fs::read_to_string(&path) | ||||||||||||||||||||
| .with_context(|| format!("failed to read {}", path.display()))?; | ||||||||||||||||||||
| let mut c: Config = toml::from_str(&contents).context("failed to parse config.toml")?; | ||||||||||||||||||||
| c.profile = profile.to_string(); | ||||||||||||||||||||
| c | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| Config { | ||||||||||||||||||||
| api_url: Config::default_api_url(), | ||||||||||||||||||||
| api_key: None, | ||||||||||||||||||||
| email: None, | ||||||||||||||||||||
| db_url: None, | ||||||||||||||||||||
| model_providers: vec![], | ||||||||||||||||||||
| node: None, | ||||||||||||||||||||
| tier: None, | ||||||||||||||||||||
| tui_theme: None, | ||||||||||||||||||||
| profile: profile.to_string(), | ||||||||||||||||||||
| }); | ||||||||||||||||||||
| ..Default::default() | ||||||||||||||||||||
| } | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Inject secrets from keychain; fall back to TOML value so old plaintext | ||||||||||||||||||||
| // configs continue to work until the next `save()` migrates them. | ||||||||||||||||||||
| cfg.api_key = kr_get(profile, "api_key").or(cfg.api_key); | ||||||||||||||||||||
| cfg.db_url = kr_get(profile, "db_url").or(cfg.db_url); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| for provider in &mut cfg.model_providers { | ||||||||||||||||||||
| let field = format!("provider/{}", provider.name); | ||||||||||||||||||||
| provider.api_key = kr_get(profile, &field).or(provider.api_key.take()); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if let Some(ref mut node) = cfg.node { | ||||||||||||||||||||
| node.signing_key = kr_get(profile, "node_signing_key").or(node.signing_key.take()); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| let contents = std::fs::read_to_string(&path) | ||||||||||||||||||||
| .with_context(|| format!("failed to read {}", path.display()))?; | ||||||||||||||||||||
| let mut cfg: Config = toml::from_str(&contents).context("failed to parse config.toml")?; | ||||||||||||||||||||
| cfg.profile = profile.to_string(); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Ok(cfg) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| pub fn save(config: &Config, profile: &str) -> Result<()> { | ||||||||||||||||||||
| /// Persist secrets to OS keychain, then write TOML with those fields stripped. | ||||||||||||||||||||
| pub fn save(cfg: &Config, profile: &str) -> Result<()> { | ||||||||||||||||||||
| // ── Write secrets to keychain ───────────────────────────────────────────── | ||||||||||||||||||||
| match &cfg.api_key { | ||||||||||||||||||||
| Some(v) if !v.is_empty() => kr_set(profile, "api_key", v), | ||||||||||||||||||||
| None => kr_del(profile, "api_key"), | ||||||||||||||||||||
| _ => {} | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
Comment on lines
+156
to
+160
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Empty string values don't delete stale keychain entries. The Consider treating empty strings the same as ♻️ Proposed fix match &cfg.api_key {
Some(v) if !v.is_empty() => kr_set(profile, "api_key", v),
- None => kr_del(profile, "api_key"),
- _ => {}
+ _ => kr_del(profile, "api_key"),
}Apply the same pattern to 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| match &cfg.db_url { | ||||||||||||||||||||
| Some(v) if !v.is_empty() => kr_set(profile, "db_url", v), | ||||||||||||||||||||
| None => kr_del(profile, "db_url"), | ||||||||||||||||||||
| _ => {} | ||||||||||||||||||||
| } | ||||||||||||||||||||
| for provider in &cfg.model_providers { | ||||||||||||||||||||
| let field = format!("provider/{}", provider.name); | ||||||||||||||||||||
| match &provider.api_key { | ||||||||||||||||||||
| Some(v) if !v.is_empty() => kr_set(profile, &field, v), | ||||||||||||||||||||
| None => kr_del(profile, &field), | ||||||||||||||||||||
| _ => {} | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| if let Some(ref node) = cfg.node { | ||||||||||||||||||||
| match &node.signing_key { | ||||||||||||||||||||
| Some(v) if !v.is_empty() => kr_set(profile, "node_signing_key", v), | ||||||||||||||||||||
| None => kr_del(profile, "node_signing_key"), | ||||||||||||||||||||
| _ => {} | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // ── Write TOML with secrets stripped ────────────────────────────────────── | ||||||||||||||||||||
| let mut on_disk = cfg.clone(); | ||||||||||||||||||||
| on_disk.api_key = None; | ||||||||||||||||||||
| on_disk.db_url = None; | ||||||||||||||||||||
| for p in &mut on_disk.model_providers { | ||||||||||||||||||||
| p.api_key = None; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| if let Some(ref mut node) = on_disk.node { | ||||||||||||||||||||
| node.signing_key = None; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let path = config_path(profile)?; | ||||||||||||||||||||
| if let Some(parent) = path.parent() { | ||||||||||||||||||||
| std::fs::create_dir_all(parent)?; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| let contents = toml::to_string_pretty(config).context("failed to serialize config")?; | ||||||||||||||||||||
| let contents = toml::to_string_pretty(&on_disk).context("failed to serialize config")?; | ||||||||||||||||||||
| std::fs::write(&path, &contents) | ||||||||||||||||||||
| .with_context(|| format!("failed to write {}", path.display()))?; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Ok(()) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,94 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * update-token-uri.ts | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Updates the on-chain metadata URI for the MACH Token-2022 mint. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Run this after the new metadata file is live on main (GitHub raw URL accessible). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Run: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * SOLANA_CLUSTER=devnet npx tsx scripts/update-token-uri.ts | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Prerequisites: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - MACH_MINT_ADDRESS in env (from setup-mach-token.ts output) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - Keypair at ~/.config/solana/id.json (or ANCHOR_WALLET) — must be update authority | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import fs from "node:fs"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import os from "node:os"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import path from "node:path"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { TOKEN_2022_PROGRAM_ID, createUpdateFieldInstruction } from "@solana/spl-token"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Connection, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Keypair, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PublicKey, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Transaction, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sendAndConfirmTransaction, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from "@solana/web3.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const CLUSTER = process.env.SOLANA_CLUSTER ?? "devnet"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const NEW_METADATA_URI = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "https://raw.githubusercontent.com/RustMunkey/maschina/main/assets/mach-token-metadata.json"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function getRpcUrl(): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (CLUSTER === "localnet") return "http://127.0.0.1:8899"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const apiKey = process.env.HELIUS_API_KEY; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (apiKey) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return CLUSTER === "mainnet-beta" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? `https://mainnet.helius-rpc.com/?api-key=${apiKey}` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : `https://devnet.helius-rpc.com/?api-key=${apiKey}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return CLUSTER === "mainnet-beta" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? "https://api.mainnet-beta.solana.com" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : "https://api.devnet.solana.com"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+32
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unrecognized cluster values silently fall back to devnet. If Suggested improvement+const VALID_CLUSTERS = ["localnet", "devnet", "mainnet-beta"] as const;
+type Cluster = (typeof VALID_CLUSTERS)[number];
+
function getRpcUrl(): string {
+ if (!VALID_CLUSTERS.includes(CLUSTER as Cluster)) {
+ console.warn(`Unknown cluster "${CLUSTER}", defaulting to devnet`);
+ }
if (CLUSTER === "localnet") return "http://127.0.0.1:8899";📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function loadKeypair(): Keypair { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const walletPath = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.env.ANCHOR_WALLET ?? path.join(os.homedir(), ".config", "solana", "id.json"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const bytes = JSON.parse(fs.readFileSync(walletPath, "utf-8")) as number[]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Keypair.fromSecretKey(Uint8Array.from(bytes)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function main() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const mintAddress = process.env.MACH_MINT_ADDRESS; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!mintAddress) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error("MACH_MINT_ADDRESS is required."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const connection = new Connection(getRpcUrl(), "confirmed"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const authority = loadKeypair(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const mint = new PublicKey(mintAddress); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`Cluster: ${CLUSTER}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`Mint: ${mintAddress}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`Authority: ${authority.publicKey.toBase58()}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`New URI: ${NEW_METADATA_URI}\n`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Token-2022 TokenMetadata extension — metadata lives at the mint address itself. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // updateField sets a single field by name; "uri" is the metadata URI. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ix = createUpdateFieldInstruction({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| programId: TOKEN_2022_PROGRAM_ID, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| metadata: mint, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| updateAuthority: authority.publicKey, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| field: "uri", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value: NEW_METADATA_URI, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const tx = new Transaction().add(ix); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tx.feePayer = authority.publicKey; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log("Sending updateField transaction..."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const sig = await sendAndConfirmTransaction(connection, tx, [authority], { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| commitment: "confirmed", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log("\nDone."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(` Signature: ${sig}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(` New URI: ${NEW_METADATA_URI}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| main().catch((err) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(err); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Silent failures in keychain writes may cause undetected secret loss.
Both
kr_setandkr_delsilently discard errors. If the keychain service is unavailable or the operation fails, the user won't know their secrets weren't persisted, potentially causing authentication failures on the nextload().Consider logging a warning or returning a
Resultso callers can decide how to handle failures.🛡️ Proposed fix to propagate errors
🤖 Prompt for AI Agents