diff --git a/Cargo.lock b/Cargo.lock index 8f54390..9b8ea92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2607,8 +2607,8 @@ dependencies = [ "serde_json", "sip7", "spacedb", + "spaces_nums", "spaces_protocol", - "spaces_ptr", "spaces_testutil", "spaces_wallet", "tabled", @@ -2618,27 +2618,28 @@ dependencies = [ ] [[package]] -name = "spaces_protocol" -version = "0.0.7" +name = "spaces_nums" +version = "0.1.0" dependencies = [ + "bech32", "bitcoin", "borsh", "borsh_utils", + "hex", + "log", "serde", - "serde_json", + "spaces_protocol", ] [[package]] -name = "spaces_ptr" -version = "0.1.0" +name = "spaces_protocol" +version = "0.0.7" dependencies = [ - "bech32", "bitcoin", "borsh", "borsh_utils", - "hex", "serde", - "spaces_protocol", + "serde_json", ] [[package]] @@ -2670,8 +2671,8 @@ dependencies = [ "secp256k1", "serde", "serde_json", + "spaces_nums", "spaces_protocol", - "spaces_ptr", "tempfile", ] diff --git a/Cargo.toml b/Cargo.toml index ec5d9e9..333596b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["borsh_utils", "client", "protocol", "testutil", "wallet", "ptr", "sip7"] +members = ["borsh_utils", "client", "protocol", "testutil", "wallet", "nums", "sip7"] [workspace.dependencies] anyhow = "1.0" diff --git a/SUBSPACES.md b/SUBSPACES.md index ac53a60..3a78844 100644 --- a/SUBSPACES.md +++ b/SUBSPACES.md @@ -86,7 +86,7 @@ You can create an on-chain identifier that only the controller of the script pub $ space-cli createptr 5120d3c3196cb3ed7fa79c882ed62f8e5942e546130d5ae5983da67dbb6c9bdd2e79 ``` -This command creates a UTXO with the same script pubkey and "mints" a space pointer (sptr) derived from it: +This command creates a UTXO with the same script pubkey and "mints" a num id derived from it: ``` sptr13thcluavwywaktvv466wr6hykf7x5avg49hgdh7w8hh8chsqvwcskmtxpd diff --git a/client/Cargo.toml b/client/Cargo.toml index 545fd51..0a9224e 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -17,7 +17,7 @@ path = "src/lib.rs" [dependencies] spaces_wallet = { path = "../wallet" } spaces_protocol = { path = "../protocol", features = ["std"] } -spaces_ptr = { path = "../ptr", features = ["std"] } +spaces_nums= { path = "../nums", features = ["std"] } sip7 = { path = "../sip7", features = ["serde"] } spacedb = { workspace = true } borsh_utils = { path = "../borsh_utils" } diff --git a/client/src/bin/space-cli.rs b/client/src/bin/space-cli.rs index 4d4b2ec..62b75b3 100644 --- a/client/src/bin/space-cli.rs +++ b/client/src/bin/space-cli.rs @@ -17,7 +17,7 @@ use spaces_client::{ auth::{auth_token_from_cookie, auth_token_from_creds, http_client_with_auth}, config::{default_cookie_path, default_spaces_rpc_port, ExtendedNetwork}, format::{ - print_error_rpc_response, print_list_bidouts, print_list_ptrs_response, + print_error_rpc_response, print_list_bidouts, print_list_nums_response, print_list_spaces_response, print_list_transactions, print_list_unspent, print_list_wallets, print_server_info, print_wallet_balance_response, print_wallet_info, print_wallet_response, Format, @@ -28,12 +28,11 @@ use spaces_client::{ }, wallets::{AddressKind, WalletResponse}, }; -use spaces_client::rpc::{CommitParams, CreatePtrParams, DelegateParams, SetFallbackParams}; +use spaces_client::rpc::{AuthorizeParams, CommitParams, CreateNumParams, DelegateParams, SetFallbackParams}; use spaces_client::store::Sha256; use spaces_protocol::bitcoin::{Amount, FeeRate, OutPoint, Txid}; use spaces_protocol::slabel::SLabel; -use spaces_ptr::snumeric::SNumeric; -use spaces_ptr::sptr::Sptr; +use spaces_nums::num_id::NumId; use spaces_wallet::{bitcoin::secp256k1::schnorr::Signature, export::WalletExport, Listing}; use spaces_wallet::bitcoin::hashes::sha256; use spaces_wallet::bitcoin::ScriptBuf; @@ -145,30 +144,32 @@ enum Commands { /// The space name space: String, }, - /// Create a new ptr - #[command(name = "createptr")] - CreatePtr { - /// The script public key as hex string - spk: String, + /// Create a new num + #[command(name = "createnum")] + CreateNum { + /// Optional script public key as hex string. + /// If omitted, a unique address is generated automatically. + #[arg(long)] + bind_spk: Option, #[arg(long, short)] fee_rate: Option, }, - /// Get ptr info - #[command(name = "getptr")] - GetPtr { - /// The sha256 hash of the spk or the spk itself prefixed with hex: - spk: String, + /// Get num info + #[command(name = "getnum")] + GetNum { + /// Space name, numeric, or num id + subject: Subject, }, - /// Transfer ownership of spaces and/or PTRs to the given name or address + /// Transfer ownership of spaces and/or nums to the given name or address #[command( name = "transfer", - override_usage = "space-cli transfer [SPACES-OR-PTRS]... --to " + override_usage = "space-cli transfer [SPACES-OR-NUMS]... --to " )] Transfer { - /// Spaces (e.g., @bitcoin) and/or PTRs (e.g., sptr1...) to send + /// Spaces (e.g., @bitcoin) and/or nums (e.g., num1... or #800000-3) to send #[arg(display_order = 0)] - spaces: Vec, + spaces: Vec, /// Recipient space name or address #[arg(long, display_order = 1)] to: String, @@ -186,11 +187,11 @@ enum Commands { #[arg(long, short)] fee_rate: Option, }, - /// Initialize a space for operation of off-chain subspaces + /// Initialize a space or numeric for operation of off-chain subspaces #[command(name = "delegate")] Delegate { - /// The space to delegate - space: String, + /// Space name, numeric, or num id + subject: Subject, /// Fee rate to use in sat/vB #[arg(long, short)] fee_rate: Option, @@ -198,8 +199,8 @@ enum Commands { /// Commit a new root #[command(name = "commit")] Commit { - /// The space to apply new root - space: String, + /// Space name, numeric, or num id + subject: Subject, /// The new state root root: sha256::Hash, /// Fee rate to use in sat/vB @@ -209,18 +210,18 @@ enum Commands { /// Rollback the last pending commitment #[command(name = "rollback")] Rollback { - /// The space to rollback - space: String, + /// Space name, numeric, or num id + subject: Subject, /// Fee rate to use in sat/vB #[arg(long, short)] fee_rate: Option, }, - /// Delegate operation of a space to someone else + /// Authorize someone else to operate a space or numeric #[command(name = "authorize")] Authorize { - /// Space to authorize + /// Space name, numeric, or num id #[arg(display_order = 0)] - space: String, + subject: Subject, /// Recipient space name or address (must be a space address) #[arg(long, display_order = 1)] to: String, @@ -228,21 +229,23 @@ enum Commands { #[arg(long, short)] fee_rate: Option, }, - /// Get the current space a sptr is responsible for + /// Get the current space a num id is responsible for #[command(name = "getdelegator")] GetDelegator { - /// The sptr or numeric identifier (e.g., sptr1... or #800000-3) - sptr: String, + /// A num id (e.g., num1...) or numeric (e.g., #800000-3) + subject: Subject, }, - /// Get the current sptr responsible for a space + /// Get the current num id responsible for a space or numeric #[command(name = "getdelegation")] GetDelegation { - space: SLabel, + /// Space name, numeric, or num id + subject: Subject, }, - /// Get a commitment for a space + /// Get a commitment for a space or numeric #[command(name = "getcommitment")] GetCommitment { - space: SLabel, + /// Space name, numeric, or num id + subject: Subject, // If no specific root, the most recent commitment will be fetched root: Option, }, @@ -335,9 +338,9 @@ enum Commands { /// The OutPoint outpoint: OutPoint, }, - /// Get a ptrout - #[command(name = "getptrout")] - GetPtrOut { + /// Get a num output + #[command(name = "getnumout")] + GetNumOut { /// The OutPoint outpoint: OutPoint, }, @@ -350,7 +353,7 @@ enum Commands { #[arg(default_value = "0")] target_interval: usize, }, - /// Set on-chain fallback record data for a space/sptr/numeric. + /// Set on-chain fallback record data for a space or num. /// /// Records can be specified as key=value flags, raw base64, or JSON from stdin. /// @@ -360,8 +363,8 @@ enum Commands { /// echo '[{"type":"txt","key":"btc","value":"bc1q..."}]' | space-cli setfallback @alice --stdin #[command(name = "setfallback")] SetFallback { - /// Space name, SPTR, or numeric identifier - subject: String, + /// Space name, numeric, or num id + subject: Subject, /// Add a TXT record (key=value, can be repeated) #[arg(long = "txt", value_name = "KEY=VALUE")] txt_records: Vec, @@ -378,11 +381,11 @@ enum Commands { #[arg(long, short)] fee_rate: Option, }, - /// Get on-chain fallback record data for a space/sptr/numeric. + /// Get on-chain fallback record data for a space or num. #[command(name = "getfallback")] GetFallback { - /// Space name, SPTR, or numeric identifier - subject: String, + /// Space name, numeric, or num id + subject: Subject, }, /// List last transactions #[command(name = "listtransactions")] @@ -396,9 +399,9 @@ enum Commands { /// still in auction with a winning bid #[command(name = "listspaces")] ListSpaces, - /// List PTRs owned by wallet - #[command(name = "listptrs")] - ListPtrs, + /// List nums owned by wallet + #[command(name = "listnums")] + ListNums, /// List unspent auction outputs i.e. outputs that can be /// auctioned off in the bidding process #[command(name = "listbidouts")] @@ -414,6 +417,14 @@ enum Commands { /// compatible with most bitcoin wallets #[command(name = "getnewaddress")] GetCoinAddress, + /// Increment the address index and return the next address. + /// Useful when you need a guaranteed fresh address + #[command(name = "walletincrementaddress")] + IncrementAddress { + /// The kind of address to increment (coin or space) + #[arg(value_enum, default_value = "coin")] + kind: AddressKind, + }, } struct SpaceCli { @@ -511,23 +522,6 @@ fn normalize_space(space: &str) -> String { } } -/// Parse a string as a space name, sptr, or numeric identifier -fn parse_subject(s: &str) -> anyhow::Result { - if s.starts_with("sptr1") { - Sptr::from_str(s) - .map(Subject::Ptr) - .map_err(|e| anyhow!("Invalid sptr: {}", e)) - } else if s.starts_with('#') { - SNumeric::from_str(s) - .map(Subject::Numeric) - .map_err(|e| anyhow!("Invalid numeric: {}", e)) - } else { - SLabel::from_str(s) - .map(Subject::Space) - .map_err(|e| anyhow!("Invalid space name: {}", e)) - } -} - #[tokio::main] async fn main() -> anyhow::Result<()> { let (cli, args) = SpaceCli::configure().await?; @@ -697,7 +691,7 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client Commands::Renew { spaces, fee_rate } => { let spaces: Vec<_> = spaces.into_iter().map(|s| { let normalized = normalize_space(&s); - Subject::Space(SLabel::from_str(&normalized).expect("valid space")) + Subject::Label(SLabel::from_str(&normalized).expect("valid space")) }).collect(); cli.send_request( Some(RpcWalletRequest::Transfer(TransferSpacesParams { @@ -716,22 +710,6 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client to, fee_rate, } => { - // Parse spaces, PTRs, and numerics into Subject - let spaces: Result, _> = spaces.into_iter().map(|s| { - if s.starts_with("sptr1") { - Sptr::from_str(&s).map(Subject::Ptr) - .map_err(|e| ClientError::Custom(format!("Invalid SPTR '{}': {}", s, e))) - } else if s.starts_with('#') { - SNumeric::from_str(&s).map(Subject::Numeric) - .map_err(|e| ClientError::Custom(format!("Invalid numeric '{}': {}", s, e))) - } else { - let normalized = normalize_space(&s); - SLabel::from_str(&normalized).map(Subject::Space) - .map_err(|e| ClientError::Custom(format!("Invalid space '{}': {}", s, e))) - } - }).collect(); - let spaces = spaces?; - cli.send_request( Some(RpcWalletRequest::Transfer(TransferSpacesParams { spaces, @@ -761,7 +739,7 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client .await? } Commands::SetFallback { - subject: subject_str, + subject, txt_records, blob_records, raw, @@ -805,8 +783,6 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client )); }; - let subject = parse_subject(&subject_str) - .map_err(|e| ClientError::Custom(format!("Invalid subject: {}", e)))?; cli.send_request( Some(RpcWalletRequest::SetFallback(SetFallbackParams { subject, data })), None, @@ -816,10 +792,8 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client .await?; } Commands::GetFallback { - subject: subject_str, + subject, } => { - let subject = parse_subject(&subject_str) - .map_err(|e| ClientError::Custom(format!("Invalid subject: {}", e)))?; let response = cli.client.get_fallback(subject).await?; println!("{}", serde_json::to_string_pretty(&response)?); } @@ -843,9 +817,9 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client let spaces = cli.client.wallet_list_spaces(&cli.wallet).await?; print_list_spaces_response(tip.tip.height, spaces, cli.format); } - Commands::ListPtrs => { - let ptrs = cli.client.wallet_list_ptrs(&cli.wallet).await?; - print_list_ptrs_response(ptrs, cli.format); + Commands::ListNums => { + let nums = cli.client.wallet_list_nums(&cli.wallet).await?; + print_list_nums_response(nums, cli.format); } Commands::Balance => { let balance = cli.client.wallet_get_balance(&cli.wallet).await?; @@ -865,6 +839,13 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client .await?; println!("{}", response); } + Commands::IncrementAddress { kind } => { + let response = cli + .client + .wallet_increment_address(&cli.wallet, kind) + .await?; + println!("{}", response); + } Commands::BumpFee { txid, fee_rate } => { let fee_rate = FeeRate::from_sat_per_vb(fee_rate).expect("valid fee rate"); let response = cli @@ -942,15 +923,23 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client cli.client.verify_listing(listing).await?; println!("{} Listing verified", "✓".color(Color::Green)); } - Commands::CreatePtr { spk, fee_rate } => { - let spk = ScriptBuf::from(hex::decode(spk) - .map_err(|_| ClientError::Custom("Invalid spk hex".to_string()))?); - - let sptr = Sptr::from_spk::(spk.clone()); - println!("Creating sptr: {}", sptr); + Commands::CreateNum { bind_spk, fee_rate } => { + let spk = match bind_spk { + Some(hex) => { + let spk = ScriptBuf::from(hex::decode(hex) + .map_err(|_| ClientError::Custom("Invalid spk hex".to_string()))?); + let num_id = NumId::from_spk::(spk.clone()); + println!("Creating num id: {}", num_id); + Some(spk) + } + None => { + println!("Creating num with auto-generated address"); + None + } + }; cli.send_request( - Some(RpcWalletRequest::CreatePtr(CreatePtrParams { - spk: hex::encode(spk.as_bytes()), + Some(RpcWalletRequest::CreateNum(CreateNumParams { + bind_spk: spk, })), None, fee_rate, @@ -958,64 +947,39 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client ) .await? } - Commands::GetPtr { spk } => { - let subject = parse_subject(&spk) - .map_err(|e| ClientError::Custom(format!("input error: {}", e)))?; - let ptr = cli + Commands::GetNum { subject } => { + let num = cli .client - .get_ptr(subject) + .get_num(subject) .await .map_err(|e| ClientError::Custom(e.to_string()))?; - println!("{}", serde_json::to_string(&ptr).expect("result")); + println!("{}", serde_json::to_string(&num).expect("result")); } - Commands::GetPtrOut { outpoint } => { - let ptrout = cli + Commands::GetNumOut { outpoint } => { + let numout = cli .client - .get_ptrout(outpoint) + .get_numout(outpoint) .await .map_err(|e| ClientError::Custom(e.to_string()))?; - println!("{}", serde_json::to_string(&ptrout).expect("result")); + println!("{}", serde_json::to_string(&numout).expect("result")); } - Commands::Delegate { space, fee_rate } => { - let space_info = match cli.client.get_space(&space).await? { - Some(space_info) => space_info, - None => return Err(ClientError::Custom("no such space".to_string())) - }; - let commitments_tip = cli.client.get_commitment( - space_info.spaceout.space.as_ref().expect("space").name.clone(), - None - ).await?; - if commitments_tip.is_some() { - return Err(ClientError::Custom("space is already delegated".to_string())); - } - - println!("Delegating space {}", space); + Commands::Delegate { subject, fee_rate } => { cli.send_request( Some(RpcWalletRequest::Delegate(DelegateParams { - space: space_info.spaceout.space.as_ref().expect("space").name.clone(), + subject, })), None, fee_rate, false, ) .await?; - println!("Space delegation should be complete once tx is confirmed"); + println!("Delegation should be complete once tx is confirmed"); } - Commands::Commit { space, root, fee_rate } => { - let space_info = match cli.client.get_space(&space).await? { - Some(space_info) => space_info, - None => return Err(ClientError::Custom("no such space".to_string())) - }; - - let label = space_info.spaceout.space.as_ref().expect("space").name.clone(); - let delegation = cli.client.get_delegation(label.clone()).await?; - if delegation.is_none() { - return Err(ClientError::Custom("space is not operational - use operate @ first.".to_string())); - } + Commands::Commit { subject, root, fee_rate } => { cli.send_request( Some(RpcWalletRequest::Commit(CommitParams { - space: label.clone(), + subject, root: Some(root), })), None, @@ -1024,20 +988,10 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client ) .await?; } - Commands::Rollback { space, fee_rate } => { - let space_info = match cli.client.get_space(&space).await? { - Some(space_info) => space_info, - None => return Err(ClientError::Custom("no such space".to_string())) - }; - - let label = space_info.spaceout.space.as_ref().expect("space").name.clone(); - let delegation = cli.client.get_delegation(label.clone()).await?; - if delegation.is_none() { - return Err(ClientError::Custom("space is not delegated - use delegate @ first.".to_string())); - } + Commands::Rollback { subject, fee_rate } => { cli.send_request( Some(RpcWalletRequest::Commit(CommitParams { - space: label.clone(), + subject, root: None, })), None, @@ -1047,24 +1001,11 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client .await?; println!("Rollback transaction sent"); } - Commands::Authorize { space, to, fee_rate } => { - let space_info = match cli.client.get_space(&space).await? { - Some(space_info) => space_info, - None => return Err(ClientError::Custom("no such space".to_string())) - }; - - let label = space_info.spaceout.space.as_ref().expect("space").name.clone(); - let delegation = cli.client.get_delegation(label.clone()).await?; - if delegation.is_none() { - return Err(ClientError::Custom("space is not delegated - use delegate @ first.".to_string())); - } - let delegation = delegation.unwrap(); - + Commands::Authorize { subject, to, fee_rate } => { cli.send_request( - Some(RpcWalletRequest::Transfer(TransferSpacesParams { - spaces: vec![Subject::Ptr(delegation)], - to: Some(to), - data: None, + Some(RpcWalletRequest::Authorize(AuthorizeParams { + subject, + to, })), None, fee_rate, @@ -1072,9 +1013,7 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client ) .await?; } - Commands::GetDelegator { sptr } => { - let subject = parse_subject(&sptr) - .map_err(|e| ClientError::Custom(format!("Invalid subject: {}", e)))?; + Commands::GetDelegator { subject } => { let delegator = cli .client .get_delegator(subject) @@ -1082,21 +1021,21 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client .map_err(|e| ClientError::Custom(e.to_string()))?; println!("{}", serde_json::to_string(&delegator).expect("result")); } - Commands::GetDelegation { space } => { + Commands::GetDelegation { subject } => { let delegation = cli .client - .get_delegation(space) + .get_delegation(subject) .await .map_err(|e| ClientError::Custom(e.to_string()))?; println!("{}", serde_json::to_string(&delegation).expect("result")); } - Commands::GetCommitment { space, root } => { + Commands::GetCommitment { subject, root } => { let c = cli .client - .get_commitment(space, root) + .get_commitment(subject, root) .await .map_err(|e| ClientError::Custom(e.to_string()))?; - println!("{}", serde_json::to_string(& c).expect("result")); + println!("{}", serde_json::to_string(&c).expect("result")); } } diff --git a/client/src/client.rs b/client/src/client.rs index 9299b36..388b9bb 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -15,7 +15,7 @@ use spaces_protocol::{ validate::{TxChangeSet, UpdateKind, Validator}, Bytes, Covenant, FullSpaceOut, RevokeReason, SpaceOut, }; -use spaces_ptr::{CommitmentKey, NumericKey, RegistryKey, RegistrySptrKey, PtrOutpointKey}; +use spaces_nums::{CommitmentKey, NumericKey, CommitmentTipKey, DelegatorKey, NumOutpointKey}; use spaces_wallet::bitcoin::{Network, Transaction}; use crate::{ @@ -78,7 +78,7 @@ pub struct BlockchainInfo { #[derive(Debug, Clone)] pub struct Client { validator: Validator, - ptr_validator: spaces_ptr::Validator, + ptr_validator: spaces_nums::Validator, tx_data: bool, } @@ -92,7 +92,7 @@ pub struct BlockMeta { /// A block structure containing validated transaction metadata for ptrs #[derive(Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] -pub struct PtrBlockMeta { +pub struct NumBlockMeta { pub height: u32, pub tx_meta: Vec, } @@ -100,7 +100,7 @@ pub struct PtrBlockMeta { #[derive(Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct PtrTxEntry { #[serde(flatten)] - pub changeset: spaces_ptr::TxChangeSet, + pub changeset: spaces_nums::TxChangeSet, #[serde(skip_serializing_if = "Option::is_none", flatten)] pub tx: Option, } @@ -142,7 +142,7 @@ impl Client { pub fn new(tx_data: bool) -> Self { Self { validator: Validator::new(), - ptr_validator: spaces_ptr::Validator::new(), + ptr_validator: spaces_nums::Validator::new(), tx_data, } } @@ -159,9 +159,9 @@ impl Client { .into()); } } - // Ptrs tip must connect to block - if chain.can_scan_ptrs(height) { - let tip = chain.ptrs_tip(); + // Nums tip must connect to block + if chain.can_scan_nums(height) { + let tip = chain.nums_tip(); if tip.hash != block.header.prev_blockhash || tip.height + 1 != height { return Err(SyncError { checkpoint: tip.clone(), @@ -182,7 +182,7 @@ impl Client { block: &Block, index_spaces: bool, index_ptrs: bool, - ) -> anyhow::Result<(Option, Option)> { + ) -> anyhow::Result<(Option, Option)> { Self::verify_block_connected(chain, height, block_hash, block)?; let mut spaces_meta = None; @@ -193,9 +193,9 @@ impl Client { }); } - let mut ptr_meta = None; + let mut num_meta = None; if index_ptrs { - ptr_meta = Some(PtrBlockMeta { + num_meta = Some(NumBlockMeta { height, tx_meta: vec![], }); @@ -253,8 +253,8 @@ impl Client { self.apply_space_tx(chain, &tx, validated_tx); } - let ptrs_ctx = if chain.can_scan_ptrs(height) { - spaces_ptr::TxContext::from_tx::( + let ptrs_ctx = if chain.can_scan_nums(height) { + spaces_nums::TxContext::from_tx::( chain, tx, spaceouts_input_ctx.is_some(), @@ -270,7 +270,7 @@ impl Client { let ptrs_validated = self.ptr_validator .process::(height, &tx, position as _, ptrs_ctx, spent_spaceouts, created_spaceouts); - if let Some(idx) = ptr_meta.as_mut() { + if let Some(idx) = num_meta.as_mut() { { idx.tx_meta.push(PtrTxEntry { changeset: ptrs_validated.clone(), @@ -292,52 +292,52 @@ impl Client { } chain.update_spaces_tip(height, block_hash); - if chain.can_scan_ptrs(height) { - chain.update_ptrs_tip(height, block_hash); + if chain.can_scan_nums(height) { + chain.update_nums_tip(height, block_hash); } - Ok((spaces_meta, ptr_meta)) + Ok((spaces_meta, num_meta)) } - fn apply_ptrs_tx(&self, state: &mut Chain, tx: &Transaction, changeset: spaces_ptr::TxChangeSet) { + fn apply_ptrs_tx(&self, state: &mut Chain, tx: &Transaction, changeset: spaces_nums::TxChangeSet) { // Remove spends for n in changeset.spends.into_iter() { let previous = tx.input[n].previous_output; - state.remove_ptr_utxo(previous); + state.remove_num_utxo(previous); } // Remove revoked delegations for revoked in changeset.revoked_delegations { - let sptr_key = RegistrySptrKey::from_sptr::(revoked.sptr); - state.remove_delegation(sptr_key); + let delegator_key = DelegatorKey::from_id::(revoked.id); + state.remove_delegator(delegator_key); } // Remove revoked commitments for revoked in changeset.revoked_commitments { let commitment_key = CommitmentKey::new::(&revoked.space, revoked.commitment.state_root); state.remove_commitment(commitment_key); - let registry_key = RegistryKey::from_slabel::(&revoked.space); + let registry_key = CommitmentTipKey::from_slabel::(&revoked.space); if let Some(prev) = revoked.commitment.prev_root { // Points space -> prev commitments tip - state.insert_registry(registry_key, prev); + state.insert_commitment_tip(registry_key, prev); } else { - state.remove_registry(registry_key); + state.remove_commitment_tip(registry_key); } } // Create new delegations for delegation in changeset.new_delegations { - let sptr_key = RegistrySptrKey::from_sptr::(delegation.sptr); - state.insert_delegation(sptr_key, delegation.space); + let delegator_key = DelegatorKey::from_id::(delegation.id); + state.insert_delegator(delegator_key, delegation.subject); } // Insert new commitments for commitment_info in changeset.commitments { let commitment_key = CommitmentKey::new::(&commitment_info.space, commitment_info.commitment.state_root); - let registry_key = RegistryKey::from_slabel::(&commitment_info.space); + let registry_key = CommitmentTipKey::from_slabel::(&commitment_info.space); // Points space -> commitments tip - state.insert_registry(registry_key, commitment_info.commitment.state_root); + state.insert_commitment_tip(registry_key, commitment_info.commitment.state_root); // commitment key = HASH(HASH(space) || state root) -> commitment state.insert_commitment(commitment_key, commitment_info.commitment); @@ -350,14 +350,14 @@ impl Client { vout: create.n as u32, }; - // Ptr => Outpoint + Numeric => Sptr - state.insert_ptr(create.sptr.id, outpoint.into()); - let numeric_key = NumericKey::from_numeric::(&create.sptr.numeric); - state.insert_numeric(numeric_key, create.sptr.id); + // Num => Outpoint + Numeric => NumId + state.insert_num_outpoint(create.num.id, outpoint.into()); + let numeric_key = NumericKey::from_numeric::(&create.num.name); + state.insert_num(numeric_key, create.num.id); // Outpoint => PtrOut - let outpoint_key = PtrOutpointKey::from_outpoint::(outpoint); - state.insert_ptrout(outpoint_key, create); + let outpoint_key = NumOutpointKey::from_outpoint::(outpoint); + state.insert_numout(outpoint_key, create); } } diff --git a/client/src/format.rs b/client/src/format.rs index 62ca936..8297e5d 100644 --- a/client/src/format.rs +++ b/client/src/format.rs @@ -19,7 +19,7 @@ use tabled::{Table, Tabled}; use crate::{ rpc::ServerInfo, - wallets::{ListPtrsResponse, ListSpacesResponse, TxInfo, TxResponse, WalletResponse}, + wallets::{ListNumsResponse, ListSpacesResponse, TxInfo, TxResponse, WalletResponse}, }; use crate::wallets::{WalletInfoWithProgress, WalletStatus}; @@ -386,11 +386,11 @@ pub fn print_list_spaces_response( Format::Json => println!("{}", serde_json::to_string_pretty(&response).unwrap()), } } -pub fn print_list_ptrs_response(response: ListPtrsResponse, format: Format) { +pub fn print_list_nums_response(response: ListNumsResponse, format: Format) { match format { Format::Text => { - if response.ptrs.is_empty() { - println!("No PTRs found"); + if response.nums.is_empty() { + println!("No numerics found"); return; } println!("{}", serde_json::to_string_pretty(&response).unwrap()); diff --git a/client/src/rpc.rs b/client/src/rpc.rs index a721009..2de693d 100644 --- a/client/src/rpc.rs +++ b/client/src/rpc.rs @@ -49,21 +49,23 @@ use tokio::{ sync::{broadcast, mpsc, oneshot, RwLock}, task::JoinSet, }; +use spaces_protocol::bitcoin::ScriptBuf; use spaces_protocol::hasher::Hash; -use spaces_ptr::{PtrSource, FullPtrOut, NumericKey, PtrOut, Commitment, RegistryKey, CommitmentKey, RegistrySptrKey, PtrOutpointKey, RootAnchor, ChainProofRequest, PtrKeyKind}; -use spaces_ptr::sptr::Sptr; +use spaces_nums::{NumSource, FullNumOut, NumericKey, NumOut, Commitment, CommitmentTipKey, CommitmentKey, DelegatorKey, NumOutpointKey, RootAnchor, ChainProofRequest, NumKeyKind}; +use spaces_nums::snumeric::SNumeric; +use spaces_nums::num_id::NumId; use spaces_wallet::bitcoin::hashes::sha256; use crate::auth::BasicAuthLayer; use crate::wallets::WalletInfoWithProgress; use crate::{ calc_progress, checker::TxChecker, - client::{BlockMeta, PtrBlockMeta, TxEntry, BlockchainInfo}, + client::{BlockMeta, NumBlockMeta, TxEntry, BlockchainInfo}, config::ExtendedNetwork, deserialize_base64, serialize_base64, source::BitcoinRpc, wallets::{ - AddressKind, ListPtrsResponse, ListSpacesResponse, RpcWallet, TxInfo, TxResponse, + AddressKind, ListNumsResponse, ListSpacesResponse, RpcWallet, TxInfo, TxResponse, WalletCommand, WalletResponse, }, }; @@ -104,10 +106,10 @@ pub struct BlockMetaWithHash { } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PtrBlockMetaWithHash { +pub struct NumBlockMetaWithHash { pub hash: BlockHash, #[serde(flatten)] - pub block_meta: PtrBlockMeta, + pub block_meta: NumBlockMeta, } pub enum ChainStateCommand { @@ -131,29 +133,29 @@ pub enum ChainStateCommand { resp: Responder>>, }, GetCommitment { - space: SLabel, + subject: Subject, root: Option, resp: Responder>>, }, GetDelegation { - space: SLabel, - resp: Responder>>, + subject: Subject, + resp: Responder>>, }, GetDelegator { subject: Subject, resp: Responder>>, }, - GetPtr { + GetNum { subject: Subject, - resp: Responder>>, + resp: Responder>>, }, - GetPtrOutpoint { + GetNumOutpoint { subject: Subject, resp: Responder>>, }, - GetPtrOut { + GetNumOut { outpoint: OutPoint, - resp: Responder>>, + resp: Responder>>, }, GetTxMeta { txid: Txid, @@ -163,9 +165,9 @@ pub enum ChainStateCommand { height_or_hash: HeightOrHash, resp: Responder>, }, - GetPtrBlockMeta { + GetNumBlockMeta { height_or_hash: HeightOrHash, - resp: Responder>, + resp: Responder>, }, EstimateBid { target: usize, @@ -226,26 +228,27 @@ pub trait Rpc { #[method(name = "getspaceout")] async fn get_spaceout(&self, outpoint: OutPoint) -> Result, ErrorObjectOwned>; - #[method(name = "getptr")] - async fn get_ptr( + #[method(name = "getnum")] + async fn get_num( &self, subject: Subject, - ) -> Result, ErrorObjectOwned>; + ) -> Result, ErrorObjectOwned>; - #[method(name = "getptrowner")] - async fn get_ptr_owner( + #[method(name = "getnumowner")] + async fn get_num_owner( &self, subject: Subject, ) -> Result, ErrorObjectOwned>; - #[method(name = "getptrout")] - async fn get_ptrout(&self, outpoint: OutPoint) -> Result, ErrorObjectOwned>; + #[method(name = "getnumout")] + async fn get_numout(&self, outpoint: OutPoint) -> Result, ErrorObjectOwned>; #[method(name = "getcommitment")] - async fn get_commitment(&self, space: SLabel, root: Option) -> Result, ErrorObjectOwned>; + async fn get_commitment(&self, subject: Subject, root: Option) -> Result, ErrorObjectOwned>; #[method(name = "getdelegation")] - async fn get_delegation(&self, space: SLabel) -> Result, ErrorObjectOwned>; + async fn get_delegation(&self, subject: Subject) -> Result, ErrorObjectOwned>; + #[method(name = "getdelegator")] async fn get_delegator(&self, subject: Subject) -> Result, ErrorObjectOwned>; @@ -268,11 +271,11 @@ pub trait Rpc { height_or_hash: HeightOrHash, ) -> Result; - #[method(name = "getptrblockmeta")] - async fn get_ptr_block_meta( + #[method(name = "getnumblockmeta")] + async fn get_num_block_meta( &self, height_or_hash: HeightOrHash, - ) -> Result; + ) -> Result; #[method(name = "gettxmeta")] async fn get_tx_meta(&self, txid: Txid) -> Result, ErrorObjectOwned>; @@ -291,7 +294,7 @@ pub trait Rpc { async fn wallet_can_operate( &self, wallet: &str, - space: SLabel, + subject: Subject, ) -> Result; #[method(name = "walletsignschnorr")] @@ -337,6 +340,13 @@ pub trait Rpc { kind: AddressKind, ) -> Result; + #[method(name = "walletincrementaddress")] + async fn wallet_increment_address( + &self, + wallet: &str, + kind: AddressKind, + ) -> Result; + #[method(name = "walletbumpfee")] async fn wallet_bump_fee( &self, @@ -398,11 +408,11 @@ pub trait Rpc { wallet: &str, ) -> Result; - #[method(name = "walletlistptrs")] - async fn wallet_list_ptrs( + #[method(name = "walletlistnums")] + async fn wallet_list_nums( &self, wallet: &str, - ) -> Result; + ) -> Result; #[method(name = "walletlistunspent")] async fn wallet_list_unspent( @@ -459,12 +469,14 @@ pub enum RpcWalletRequest { Register(RegisterParams), #[serde(rename = "transfer")] Transfer(TransferSpacesParams), - #[serde(rename = "createptr")] - CreatePtr(CreatePtrParams), + #[serde(rename = "createnum")] + CreateNum(CreateNumParams), #[serde(rename = "delegate")] Delegate(DelegateParams), #[serde(rename = "commit")] Commit(CommitParams), + #[serde(rename = "authorize")] + Authorize(AuthorizeParams), #[serde(rename = "setfallback")] SetFallback(SetFallbackParams), #[serde(rename = "send")] @@ -484,18 +496,25 @@ pub struct TransferSpacesParams { } #[derive(Clone, Serialize, Deserialize)] -pub struct CreatePtrParams { - pub spk: String, +pub struct CreateNumParams { + #[serde(skip_serializing_if = "Option::is_none")] + pub bind_spk: Option, } #[derive(Clone, Serialize, Deserialize)] pub struct DelegateParams { - pub space: SLabel, + pub subject: Subject, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct AuthorizeParams { + pub subject: Subject, + pub to: String, } #[derive(Clone, Serialize, Deserialize)] pub struct CommitParams { - pub space: SLabel, + pub subject: Subject, pub root: Option, } @@ -922,7 +941,7 @@ impl RpcServer for RpcServerImpl { Ok(spaceout) } - async fn get_ptr(&self, subject: Subject) -> Result, ErrorObjectOwned> { + async fn get_num(&self, subject: Subject) -> Result, ErrorObjectOwned> { let info = self .store .get_ptr(subject) @@ -931,7 +950,7 @@ impl RpcServer for RpcServerImpl { Ok(info) } - async fn get_ptr_owner(&self, subject: Subject) -> Result, ErrorObjectOwned> { + async fn get_num_owner(&self, subject: Subject) -> Result, ErrorObjectOwned> { let info = self .store .get_ptr_outpoint(subject) @@ -940,28 +959,28 @@ impl RpcServer for RpcServerImpl { Ok(info) } - async fn get_ptrout(&self, outpoint: OutPoint) -> Result, ErrorObjectOwned> { + async fn get_numout(&self, outpoint: OutPoint) -> Result, ErrorObjectOwned> { let spaceout = self .store - .get_ptrout(outpoint) + .get_numout(outpoint) .await .map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::))?; Ok(spaceout) } - async fn get_commitment(&self, space: SLabel, root: Option) -> Result, ErrorObjectOwned> { + async fn get_commitment(&self, subject: Subject, root: Option) -> Result, ErrorObjectOwned> { let c = self .store - .get_commitment(space, root.map(|r| *r.as_ref())) + .get_commitment(subject, root.map(|r| *r.as_ref())) .await .map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::))?; Ok(c) } - async fn get_delegation(&self, space: SLabel) -> Result, ErrorObjectOwned> { + async fn get_delegation(&self, subject: Subject) -> Result, ErrorObjectOwned> { let delegation = self .store - .get_delegation(space) + .get_delegation(subject) .await .map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::))?; Ok(delegation) @@ -1020,13 +1039,13 @@ impl RpcServer for RpcServerImpl { Ok(data) } - async fn get_ptr_block_meta( + async fn get_num_block_meta( &self, height_or_hash: HeightOrHash, - ) -> Result { + ) -> Result { let data = self .store - .get_ptr_block_meta(height_or_hash) + .get_num_block_meta(height_or_hash) .await .map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::))?; @@ -1084,11 +1103,11 @@ impl RpcServer for RpcServerImpl { async fn wallet_can_operate( &self, wallet: &str, - space: SLabel, + subject: Subject, ) -> Result { self.wallet(&wallet) .await? - .send_can_operate(space) + .send_can_operate(subject) .await .map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::)) } @@ -1169,6 +1188,18 @@ impl RpcServer for RpcServerImpl { .map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::)) } + async fn wallet_increment_address( + &self, + wallet: &str, + kind: AddressKind, + ) -> Result { + self.wallet(&wallet) + .await? + .send_increment_address(kind) + .await + .map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::)) + } + async fn wallet_bump_fee( &self, wallet: &str, @@ -1272,13 +1303,13 @@ impl RpcServer for RpcServerImpl { .map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::)) } - async fn wallet_list_ptrs( + async fn wallet_list_nums( &self, wallet: &str, - ) -> Result { + ) -> Result { self.wallet(&wallet) .await? - .send_list_ptrs() + .send_list_nums() .await .map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::)) } @@ -1312,7 +1343,7 @@ impl RpcServer for RpcServerImpl { async fn get_fallback(&self, subject: Subject) -> Result, ErrorObjectOwned> { let data = match &subject { - Subject::Space(label) => { + Subject::Label(label) if !label.is_numeric() => { let space_hash = SpaceKey::from(Sha256::hash(label.as_ref())); let fso = self.store.get_space(space_hash).await .map_err(|e| ErrorObjectOwned::owned(-1, e.to_string(), None::))?; @@ -1325,10 +1356,10 @@ impl RpcServer for RpcServerImpl { None }) } - Subject::Ptr(_) | Subject::Numeric(_) => { + _ => { let fpt = self.store.get_ptr(subject).await .map_err(|e| ErrorObjectOwned::owned(-1, e.to_string(), None::))?; - fpt.and_then(|fpt| fpt.ptrout.sptr.data.map(|b| b.to_vec())) + fpt.and_then(|fpt| fpt.numout.num.data.map(|b| b.to_vec())) } }; @@ -1454,7 +1485,7 @@ impl AsyncChainState { height_or_hash: HeightOrHash, client: &reqwest::Client, rpc: &BitcoinRpc, - ) -> Result { + ) -> Result { let hash = match height_or_hash { HeightOrHash::Hash(hash) => hash, HeightOrHash::Height(height) => rpc @@ -1463,7 +1494,7 @@ impl AsyncChainState { .map_err(|e| anyhow!("Could not retrieve block hash ({})", e))?, }; - if let Some(block_meta) = state.get_ptrs_block(hash)? { + if let Some(block_meta) = state.get_nums_block(hash)? { return Ok(block_meta); } @@ -1478,17 +1509,17 @@ impl AsyncChainState { .and_then(|h| u32::try_from(h).ok()) .ok_or_else(|| anyhow!("Could not retrieve block height"))?; - let ptrs_tip = state.ptrs_tip(); + let ptrs_tip = state.nums_tip(); if height > ptrs_tip.height { return Err(anyhow!( - "Ptrs is syncing at height {}, requested block height {}", + "Nums index is syncing at height {}, requested block height {}", ptrs_tip.height, height )); } - Ok(PtrBlockMetaWithHash { + Ok(NumBlockMetaWithHash { hash, - block_meta: PtrBlockMeta { + block_meta: NumBlockMeta { height, tx_meta: Vec::new(), }, @@ -1539,34 +1570,34 @@ impl AsyncChainState { .context("could not fetch spaceout"); let _ = resp.send(result); } - ChainStateCommand::GetPtr { subject, resp } => { - let result = resolve_sptr(state, &subject) - .and_then(|sptr| state.get_ptr_info(&sptr)); + ChainStateCommand::GetNum { subject, resp } => { + let result = resolve_num_id(state, &subject) + .and_then(|id| state.get_num_info(&id)); let _ = resp.send(result); } - ChainStateCommand::GetPtrOutpoint { subject, resp } => { - let result = resolve_sptr(state, &subject) - .and_then(|sptr| state.get_ptr_outpoint(&sptr).context("could not fetch ptrout")); + ChainStateCommand::GetNumOutpoint { subject, resp } => { + let result = resolve_num_id(state, &subject) + .and_then(|id| state.get_num_outpoint_by_id(&id).context("could not fetch numout")); let _ = resp.send(result); } - ChainStateCommand::GetCommitment { space, root, resp } => { - let result = get_commitment(state, space, root); + ChainStateCommand::GetCommitment { subject, root, resp } => { + let result = get_commitment(state, &subject, root); let _ = resp.send(result); } - ChainStateCommand::GetDelegation { space, resp } => { - let result = get_delegation(state, space); + ChainStateCommand::GetDelegation { subject, resp } => { + let result = get_delegation(state, &subject); let _ = resp.send(result); } ChainStateCommand::GetDelegator { subject, resp } => { - let result = resolve_sptr(state, &subject) - .and_then(|sptr| state.get_delegator(&RegistrySptrKey::from_sptr::(sptr)) + let result = resolve_num_id(state, &subject) + .and_then(|id| state.get_delegator(&DelegatorKey::from_id::(id)) .map_err(|e| anyhow!("could not get delegator: {}", e))); let _ = resp.send(result); } - ChainStateCommand::GetPtrOut { outpoint, resp } => { + ChainStateCommand::GetNumOut { outpoint, resp } => { let result = state - .get_ptrout(&outpoint) - .context("could not fetch ptrouts"); + .get_numout(&outpoint) + .context("could not fetch numouts"); let _ = resp.send(result); } ChainStateCommand::GetBlockMeta { @@ -1578,7 +1609,7 @@ impl AsyncChainState { .await; let _ = resp.send(res); } - ChainStateCommand::GetPtrBlockMeta { + ChainStateCommand::GetNumBlockMeta { height_or_hash, resp, } => { @@ -1679,8 +1710,8 @@ impl AsyncChainState { let meta: ChainAnchor = snapshot.metadata().try_into()?; // Try to compute PTR root if we're past PTR genesis - let ptrs_root = if state.can_scan_ptrs(meta.height) { - state.ptrs_mut().state.inner() + let ptrs_root = if state.can_scan_nums(meta.height) { + state.nums_mut().state.inner() .ok() .and_then(|s| s.compute_root().ok()) } else { @@ -1689,7 +1720,7 @@ impl AsyncChainState { Ok(vec![RootAnchor { spaces_root, - ptrs_root, + nums_root: ptrs_root, block: ChainAnchor { hash: meta.hash, height: meta.height, @@ -1711,11 +1742,11 @@ impl AsyncChainState { ) -> anyhow::Result { let mut most_recent_update = 0u32; let mut space_tree_keys: HashSet = HashSet::new(); - let mut ptr_tree_keys: HashSet = HashSet::new(); + let mut num_tree_keys: HashSet = HashSet::new(); for space in request.spaces { if space.is_numeric() { - request.ptrs_keys.push(PtrKeyKind::Numeric(space.try_into()?)); + request.nums.push(NumKeyKind::Num(space.try_into()?)); continue; } @@ -1736,43 +1767,43 @@ impl AsyncChainState { } } - let sptr = Sptr::from_spk::(fso.spaceout.script_pubkey); - request.ptrs_keys.push(PtrKeyKind::Sptr(sptr)); + let id = NumId::from_spk::(fso.spaceout.script_pubkey); + request.nums.push(NumKeyKind::Id(id)); } - for key in request.ptrs_keys { + for key in request.nums { match key { - PtrKeyKind::Numeric(numeric) => { + NumKeyKind::Num(numeric) => { let key = NumericKey::from_numeric::(&numeric); - let sptr = state.get_numeric(&key)?; - if let Some(sptr) = sptr { - let fpt = state.get_ptr_info(&sptr)? - .expect("sptr must exist if numeric exists"); - ptr_tree_keys.insert( - PtrOutpointKey::from_outpoint::(fpt.outpoint()).into() + let id = state.get_num_id(&key)?; + if let Some(id) = id { + let fpt = state.get_num_info(&id)? + .expect("num id must exist if numeric exists"); + num_tree_keys.insert( + NumOutpointKey::from_outpoint::(fpt.outpoint()).into() ); - most_recent_update = std::cmp::max(most_recent_update, fpt.ptrout.sptr.last_update); + most_recent_update = std::cmp::max(most_recent_update, fpt.numout.num.last_update); } else { // non-existence proof - ptr_tree_keys.insert(key.into()); + num_tree_keys.insert(key.into()); } } - PtrKeyKind::Sptr(sptr) => { - if let Some(fpt) = state.get_ptr_info(&sptr)? { - ptr_tree_keys.insert( - PtrOutpointKey::from_outpoint::(fpt.outpoint()).into() + NumKeyKind::Id(id) => { + if let Some(fpt) = state.get_num_info(&id)? { + num_tree_keys.insert( + NumOutpointKey::from_outpoint::(fpt.outpoint()).into() ); - most_recent_update = std::cmp::max(most_recent_update, fpt.ptrout.sptr.last_update); + most_recent_update = std::cmp::max(most_recent_update, fpt.numout.num.last_update); } else { // non-existence proof - ptr_tree_keys.insert(sptr.into()); + num_tree_keys.insert(id.into()); } } - PtrKeyKind::Commitment(k) => { - ptr_tree_keys.insert(k.into()); + NumKeyKind::Commitment(k) => { + num_tree_keys.insert(k.into()); }, - PtrKeyKind::Registry(k) => { - ptr_tree_keys.insert(k.into()); + NumKeyKind::CommitmentTip(k) => { + num_tree_keys.insert(k.into()); }, } } @@ -1788,7 +1819,7 @@ impl AsyncChainState { )); } - let ptr_tree_keys : Vec<_> = ptr_tree_keys.into_iter().collect(); + let num_tree_keys : Vec<_> = num_tree_keys.into_iter().collect(); let space_tree_keys : Vec<_> = space_tree_keys.into_iter().collect(); let cached_height = Self::cached_snapshot_height(tip.height); @@ -1803,14 +1834,14 @@ impl AsyncChainState { let spaces_proof = snapshot.spaces.prove(&space_tree_keys, ProofType::Standard)?; let spaces_root = spaces_proof.compute_root()?; - let ptrs_anchor: ChainAnchor = snapshot.ptrs.metadata().try_into()?; + let ptrs_anchor: ChainAnchor = snapshot.nums.metadata().try_into()?; if spaces_anchor != ptrs_anchor { return Err(anyhow!( "Spaces and PTRs snapshots at height {} have mismatched anchors", height )); } - let ptrs_proof = snapshot.ptrs.prove(&ptr_tree_keys, ProofType::Standard)?; + let ptrs_proof = snapshot.nums.prove(&num_tree_keys, ProofType::Standard)?; let ptrs_root = ptrs_proof.compute_root()?; (spaces_proof, spaces_root, spaces_anchor, ptrs_proof, ptrs_root) @@ -1820,7 +1851,7 @@ impl AsyncChainState { let spaces_anchor: ChainAnchor = spaces_snapshot.metadata().try_into()?; let spaces_proof = spaces_snapshot.prove(&space_tree_keys, ProofType::Standard)?; - let ptrs_snapshot = state.ptrs_mut().state.inner()?; + let ptrs_snapshot = state.nums_mut().state.inner()?; let ptrs_anchor: ChainAnchor = ptrs_snapshot.metadata().try_into()?; if spaces_anchor != ptrs_anchor { return Err(anyhow!( @@ -1829,7 +1860,7 @@ impl AsyncChainState { ptrs_anchor.height )); } - let ptrs_proof = ptrs_snapshot.prove(&ptr_tree_keys, ProofType::Standard)?; + let ptrs_proof = ptrs_snapshot.prove(&num_tree_keys, ProofType::Standard)?; let ptrs_root = ptrs_proof.compute_root()?; (spaces_proof, spaces_root, spaces_anchor, ptrs_proof, ptrs_root) @@ -1952,10 +1983,10 @@ impl AsyncChainState { resp_rx.await? } - pub async fn get_ptr(&self, subject: Subject) -> anyhow::Result> { + pub async fn get_ptr(&self, subject: Subject) -> anyhow::Result> { let (resp, resp_rx) = oneshot::channel(); self.sender - .send(ChainStateCommand::GetPtr { subject, resp }) + .send(ChainStateCommand::GetNum { subject, resp }) .await?; resp_rx.await? } @@ -1971,7 +2002,7 @@ impl AsyncChainState { pub async fn get_ptr_outpoint(&self, subject: Subject) -> anyhow::Result> { let (resp, resp_rx) = oneshot::channel(); self.sender - .send(ChainStateCommand::GetPtrOutpoint { subject, resp }) + .send(ChainStateCommand::GetNumOutpoint { subject, resp }) .await?; resp_rx.await? } @@ -2003,26 +2034,26 @@ impl AsyncChainState { resp_rx.await? } - pub async fn get_ptrout(&self, outpoint: OutPoint) -> anyhow::Result> { + pub async fn get_numout(&self, outpoint: OutPoint) -> anyhow::Result> { let (resp, resp_rx) = oneshot::channel(); self.sender - .send(ChainStateCommand::GetPtrOut { outpoint, resp }) + .send(ChainStateCommand::GetNumOut { outpoint, resp }) .await?; resp_rx.await? } - pub async fn get_commitment(&self, space: SLabel, root: Option) -> anyhow::Result> { + pub async fn get_commitment(&self, subject: Subject, root: Option) -> anyhow::Result> { let (resp, resp_rx) = oneshot::channel(); self.sender - .send(ChainStateCommand::GetCommitment { space, root, resp }) + .send(ChainStateCommand::GetCommitment { subject, root, resp }) .await?; resp_rx.await? } - pub async fn get_delegation(&self, space: SLabel) -> anyhow::Result> { + pub async fn get_delegation(&self, subject: Subject) -> anyhow::Result> { let (resp, resp_rx) = oneshot::channel(); self.sender - .send(ChainStateCommand::GetDelegation { space, resp }) + .send(ChainStateCommand::GetDelegation { subject, resp }) .await?; resp_rx.await? } @@ -2049,13 +2080,13 @@ impl AsyncChainState { resp_rx.await? } - pub async fn get_ptr_block_meta( + pub async fn get_num_block_meta( &self, height_or_hash: HeightOrHash, - ) -> anyhow::Result { + ) -> anyhow::Result { let (resp, resp_rx) = oneshot::channel(); self.sender - .send(ChainStateCommand::GetPtrBlockMeta { + .send(ChainStateCommand::GetNumBlockMeta { height_or_hash, resp, }) @@ -2072,15 +2103,16 @@ impl AsyncChainState { } } -fn resolve_sptr(state: &mut Chain, subject: &Subject) -> anyhow::Result { +fn resolve_num_id(state: &mut Chain, subject: &Subject) -> anyhow::Result { match subject { - Subject::Ptr(sptr) => Ok(*sptr), - Subject::Numeric(numeric) => { - let key = NumericKey::from_numeric::(numeric); - state.get_numeric(&key)? + Subject::NumId(id) => Ok(*id), + Subject::Label(label) if label.is_numeric() => { + let numeric: SNumeric = label.clone().try_into().unwrap(); + let key = NumericKey::from_numeric::(&numeric); + state.get_num_id(&key)? .ok_or_else(|| anyhow!("numeric '{}' not found", numeric)) } - Subject::Space(_) => Err(anyhow!("expected a ptr or numeric, not a space")), + Subject::Label(_) => Err(anyhow!("expected a num id or numeric, not a space")), } } @@ -2144,25 +2176,56 @@ async fn get_server_info( } -fn get_delegation(state: &mut Chain, space: SLabel) -> anyhow::Result> { - let info = match state.get_space_info(&SpaceKey::from(Sha256::hash(space.as_ref())))? { - None => return Ok(None), - Some(info) => info +fn resolve_label(state: &mut Chain, subject: &Subject) -> anyhow::Result { + match subject { + Subject::Label(label) => Ok(label.clone()), + Subject::NumId(id) => { + let info = state.get_num_info(id)? + .ok_or_else(|| anyhow!("num id '{}' not found", id))?; + Ok(info.numout.num.name.to_slabel()) + } + } +} + +fn get_delegation(state: &mut Chain, subject: &Subject) -> anyhow::Result> { + let (id, label) = match subject { + Subject::Label(num) if num.is_numeric() => { + let numeric: SNumeric = num.clone().try_into().expect("is_numeric"); + let key = NumericKey::from_numeric::(&numeric); + let Some(num_id) = state.get_num_id(&key)? else { + return Ok(None); + }; + let Some(num_info) = state.get_num_info(&num_id)? else { + return Ok(None); + }; + (NumId::from_spk::(num_info.numout.script_pubkey), num.clone()) + }, + Subject::Label(space) => { + let info = match state.get_space_info( + &SpaceKey::from(Sha256::hash(space.as_ref())) + )? { + None => return Ok(None), + Some(info) => info + }; + (NumId::from_spk::(info.spaceout.script_pubkey), space.clone()) + } + Subject::NumId(id) => return Ok(Some(id.clone())) }; - let sptr = Sptr::from_spk::(info.spaceout.script_pubkey); - let delegate = state.get_delegator(&RegistrySptrKey::from_sptr::(sptr))?; - // Only return the SPTR if the reverse mapping points back to this space + let delegate = state.get_delegator(&DelegatorKey::from_id::(id))?; + + // Only return the num id if the reverse mapping points back to this label match delegate { - Some(delegator) if delegator == space => Ok(Some(sptr)), + Some(delegator) if delegator == label => Ok(Some(id)), _ => Ok(None), } } -fn get_commitment(state: &mut Chain, space: SLabel, root: Option) -> anyhow::Result> { +fn get_commitment(state: &mut Chain, subject: &Subject, root: Option) -> anyhow::Result> { + let label = resolve_label(state, subject)?; let root = match root { None => { - let rk = RegistryKey::from_slabel::(&space); + let rk = CommitmentTipKey::from_slabel::(&label); let k = state.get_commitments_tip(&rk) .map_err(|e| anyhow!("could not fetch state root: {}", e))?; if let Some(k) = k { @@ -2174,7 +2237,7 @@ fn get_commitment(state: &mut Chain, space: SLabel, root: Option) -> anyho Some(r) => r, }; - let ck = CommitmentKey::new::(&space, root); + let ck = CommitmentKey::new::(&label, root); state.get_commitment(&ck) .map_err(|e| anyhow!("could not fetch commitment with root: {}: {}", hex::encode(root), e) diff --git a/client/src/spaces.rs b/client/src/spaces.rs index 8e81370..0edf93b 100644 --- a/client/src/spaces.rs +++ b/client/src/spaces.rs @@ -63,7 +63,7 @@ impl Spaced { block: Block, ) -> anyhow::Result<()> { let sp_idx = self.chain.has_spaces_index(); - let pt_idx = self.chain.has_ptrs_index(); + let pt_idx = self.chain.has_nums_index(); let (block_result,ptr_block_result) = node .scan_block(&mut self.chain, id.height, id.hash, &block, sp_idx, pt_idx)?; diff --git a/client/src/store/chain.rs b/client/src/store/chain.rs index decc8f6..22b619a 100644 --- a/client/src/store/chain.rs +++ b/client/src/store/chain.rs @@ -9,13 +9,13 @@ use spaces_protocol::hasher::{BaseHash, BidKey, OutpointKey, SpaceKey}; use spaces_protocol::prepare::SpacesSource; use spaces_protocol::{FullSpaceOut, SpaceOut}; use spaces_protocol::slabel::SLabel; -use spaces_ptr::{Commitment, CommitmentKey, FullPtrOut, NumericKey, PtrOut, PtrSource, RegistryKey, RegistrySptrKey, PtrOutpointKey, RootAnchor}; -use spaces_ptr::sptr::Sptr; +use spaces_nums::{Commitment, CommitmentKey, FullNumOut, NumericKey, NumOut, NumSource, CommitmentTipKey, DelegatorKey, NumOutpointKey, RootAnchor}; +use spaces_nums::num_id::NumId; use spaces_wallet::bitcoin::Network; -use crate::client::{BlockMeta, PtrBlockMeta}; -use crate::rpc::{BlockMetaWithHash, PtrBlockMetaWithHash}; +use crate::client::{BlockMeta, NumBlockMeta}; +use crate::rpc::{BlockMetaWithHash, NumBlockMetaWithHash}; use crate::store::{EncodableOutpoint, ReadTx, Sha256}; -use crate::store::ptrs::{PtrChainState, PtrLiveStore, PtrStore}; +use crate::store::ptrs::{NumChainState, NumLiveStore, NumStore}; use crate::store::spaces::{RolloutEntry, RolloutIterator, SpLiveStore, SpStore, SpStoreUtils, SpacesState}; pub const ROOT_ANCHORS_COUNT: u32 = 120; @@ -39,13 +39,13 @@ const_assert!( pub struct CachedSnapshot { pub height: u32, pub spaces: ReadTx, - pub ptrs: ReadTx, + pub nums: ReadTx, } pub struct Chain { db: LiveStore, idx: LiveIndex, - ptrs_genesis: ChainAnchor, + nums_genesis: ChainAnchor, cached_snapshot: Option, } @@ -54,7 +54,7 @@ impl Clone for Chain { Self { db: self.db.clone(), idx: self.idx.clone(), - ptrs_genesis: self.ptrs_genesis, + nums_genesis: self.nums_genesis, cached_snapshot: None, } } @@ -63,13 +63,13 @@ impl Clone for Chain { #[derive(Clone)] pub struct LiveStore { sp: SpLiveStore, - pt: PtrLiveStore, + num: NumLiveStore, } #[derive(Clone)] pub struct LiveIndex { sp: Option, - pt: Option, + num: Option, } impl SpacesSource for Chain { @@ -82,29 +82,29 @@ impl SpacesSource for Chain { } } -impl PtrSource for Chain { - fn get_ptr_outpoint(&mut self, space_hash: &Sptr) -> spaces_protocol::errors::Result> { - self.db.pt.state.get_ptr_outpoint(space_hash) +impl NumSource for Chain { + fn get_num_outpoint_by_id(&mut self, space_hash: &NumId) -> spaces_protocol::errors::Result> { + self.db.num.state.get_num_outpoint_by_id(space_hash) } fn get_commitment(&mut self, key: &CommitmentKey) -> spaces_protocol::errors::Result> { - self.db.pt.state.get_commitment(key) + self.db.num.state.get_commitment(key) } - fn get_delegator(&mut self, sptr: &RegistrySptrKey) -> spaces_protocol::errors::Result> { - self.db.pt.state.get_delegator(sptr) + fn get_delegator(&mut self, key: &DelegatorKey) -> spaces_protocol::errors::Result> { + self.db.num.state.get_delegator(key) } - fn get_commitments_tip(&mut self, key: &RegistryKey) -> spaces_protocol::errors::Result> { - self.db.pt.state.get_commitments_tip(key) + fn get_commitments_tip(&mut self, key: &CommitmentTipKey) -> spaces_protocol::errors::Result> { + self.db.num.state.get_commitments_tip(key) } - fn get_ptrout(&mut self, outpoint: &OutPoint) -> spaces_protocol::errors::Result> { - self.db.pt.state.get_ptrout(outpoint) + fn get_numout(&mut self, outpoint: &OutPoint) -> spaces_protocol::errors::Result> { + self.db.num.state.get_numout(outpoint) } - fn get_numeric(&mut self, key: &NumericKey) -> spaces_protocol::errors::Result> { - self.db.pt.state.get_numeric(key) + fn get_num_id(&mut self, key: &NumericKey) -> spaces_protocol::errors::Result> { + self.db.num.state.get_num_id(key) } } @@ -113,8 +113,8 @@ impl Chain { self.db.sp.state.get_space_info(space_hash) } - pub fn get_ptr_info(&mut self, key: &Sptr) -> anyhow::Result> { - self.db.pt.state.get_ptr_info(key) + pub fn get_num_info(&mut self, key: &NumId) -> anyhow::Result> { + self.db.num.state.get_num_info(key) } pub fn snapshot_at(&mut self, target_height: u32) -> anyhow::Result<&mut CachedSnapshot> { @@ -126,18 +126,18 @@ impl Chain { if !self.cached_snapshot.as_ref().is_some_and(|c| c.height == target_height) { let spaces = self.db.sp.state.read_at(target_height)?; - let ptrs = self.db.pt.state.read_at(target_height)?; - self.cached_snapshot = Some(CachedSnapshot { height: target_height, spaces, ptrs }); + let ptrs = self.db.num.state.read_at(target_height)?; + self.cached_snapshot = Some(CachedSnapshot { height: target_height, spaces, nums: ptrs }); } Ok(self.cached_snapshot.as_mut().unwrap()) } - pub fn load(_network: Network, genesis: ChainAnchor, ptrs_genesis: ChainAnchor, dir: &Path, index_spaces: bool, index_ptrs: bool) -> anyhow::Result { + pub fn load(_network: Network, genesis: ChainAnchor, nums_genesis: ChainAnchor, dir: &Path, index_spaces: bool, index_ptrs: bool) -> anyhow::Result { let proto_db_path = dir.join("root.sdb"); - let ptrs_db_path = dir.join("refs.sdb"); + let nums_db_path = dir.join("nums.sdb"); let initial_sp_sync = !proto_db_path.exists(); - let initial_pt_sync = !ptrs_db_path.exists(); + let initial_num_sync = !nums_db_path.exists(); let sp_store = SpStore::open(proto_db_path)?; let sp = SpLiveStore { @@ -145,10 +145,10 @@ impl Chain { store: sp_store, }; - let pt_store = PtrStore::open(ptrs_db_path)?; - let pt = PtrLiveStore { - state: pt_store.begin(&ptrs_genesis)?, - store: pt_store, + let num_store = NumStore::open(nums_db_path)?; + let num = NumLiveStore { + state: num_store.begin(&nums_genesis)?, + store: num_store, }; @@ -159,33 +159,33 @@ impl Chain { sp_idx = Some(load_sp_index(dir, genesis, *current_tip, initial_sp_sync)?) } - let mut pt_idx = None; + let mut num_idx = None; if index_ptrs { - let current_tip = pt.state.tip.read().expect("tip"); - pt_idx = Some(load_pt_index(dir, ptrs_genesis, *current_tip, initial_pt_sync)?) + let current_tip = num.state.tip.read().expect("tip"); + num_idx = Some(load_num_index(dir, nums_genesis, *current_tip, initial_num_sync)?) } let chain = Chain { - db: LiveStore { sp, pt }, - idx: LiveIndex { sp: sp_idx, pt: pt_idx }, - ptrs_genesis, + db: LiveStore { sp, num }, + idx: LiveIndex { sp: sp_idx, num: num_idx }, + nums_genesis, cached_snapshot: None, }; // If spaces synced past the ptrs point, reset the tip - if initial_pt_sync { + if initial_num_sync { let sp_tip = chain.db.sp.state.tip.read().expect("tip").clone(); - if sp_tip.height > ptrs_genesis.height { - info!("spaces tip = {} > ptrs genesis = {} - rescanning to index ptrs", - sp_tip.height, ptrs_genesis.height + if sp_tip.height > nums_genesis.height { + info!("spaces tip = {} > nums genesis = {} - rescanning to index nums", + sp_tip.height, nums_genesis.height ); assert_eq!( - ptrs_genesis.height % COMMIT_BLOCK_INTERVAL, 0, - "ptrs genesis must align with commit interval" + nums_genesis.height % COMMIT_BLOCK_INTERVAL, 0, + "nums genesis must align with commit interval" ); chain.restore_spaces(|_| { return Ok(BlockHash::from_slice(&[0u8; 32]).expect("hash")); - }, Some(ptrs_genesis.height))?; + }, Some(nums_genesis.height))?; } } @@ -210,9 +210,9 @@ impl Chain { pub fn apply_block_to_ptrs_index( &self, block_hash: BlockHash, - block: PtrBlockMeta, + block: NumBlockMeta, ) -> anyhow::Result<()> { - if let Some(idx) = &self.idx.pt { + if let Some(idx) = &self.idx.num { idx.state.insert(BaseHash::from_slice(block_hash.as_ref()), block); } Ok(()) @@ -224,10 +224,10 @@ impl Chain { } let spaces_batch = self.db.sp.store.write().expect("write handle"); - let ptrs_batch = self.db.pt.store.write().expect("write handle"); + let ptrs_batch = self.db.num.store.write().expect("write handle"); self.db.sp.state.commit(checkpoint.clone(), spaces_batch)?; - self.db.pt.state.commit(checkpoint.clone(), ptrs_batch)?; + self.db.num.state.commit(checkpoint.clone(), ptrs_batch)?; let sp_index_writer = self.idx.sp.clone(); if let Some(index) = sp_index_writer { @@ -235,7 +235,7 @@ impl Chain { index.state.commit(checkpoint, tx)?; } - let pt_index_writer = self.idx.pt.clone(); + let pt_index_writer = self.idx.num.clone(); if let Some(index) = pt_index_writer { let tx = index.store.write().expect("write handle"); index.state.commit(checkpoint, tx)?; @@ -247,16 +247,16 @@ impl Chain { &mut self.db.sp } - pub fn ptrs_mut(&mut self) -> &mut PtrLiveStore { - &mut self.db.pt + pub fn nums_mut(&mut self) -> &mut NumLiveStore { + &mut self.db.num } pub fn has_spaces_index(&self) -> bool { self.idx.sp.is_some() } - pub fn has_ptrs_index(&self) -> bool { - self.idx.pt.is_some() + pub fn has_nums_index(&self) -> bool { + self.idx.num.is_some() } pub fn rollout_iter(&self) -> anyhow::Result<(RolloutIterator, ReadTx)> { @@ -264,7 +264,7 @@ impl Chain { } pub fn is_dirty(&self) -> bool { - self.db.sp.state.is_dirty() || self.db.pt.state.is_dirty() + self.db.sp.state.is_dirty() || self.db.num.state.is_dirty() } pub fn spaces_tip_meatadata(&mut self) -> anyhow::Result<&[u8]> { @@ -294,16 +294,16 @@ impl Chain { self.db.sp.state.inner() } - pub fn ptrs_tip(&self) -> ChainAnchor { - *self.db.pt.state.tip.read().expect("ptrs tip") + pub fn nums_tip(&self) -> ChainAnchor { + *self.db.num.state.tip.read().expect("ptrs tip") } - pub fn can_scan_ptrs(&self, height: u32) -> bool { - height > self.ptrs_genesis.height + pub fn can_scan_nums(&self, height: u32) -> bool { + height > self.nums_genesis.height } - pub fn update_ptrs_tip(&self, height: u32, block_hash: BlockHash) { - let mut tip = self.db.pt.state.tip.write().expect("write tip"); + pub fn update_nums_tip(&self, height: u32, block_hash: BlockHash) { + let mut tip = self.db.num.state.tip.write().expect("write tip"); tip.height = height; tip.hash = block_hash; } @@ -315,47 +315,45 @@ impl Chain { tip.hash = block_hash; } - pub(crate) fn insert_ptrout(&self, key: PtrOutpointKey, ptrout: PtrOut) { - self.db.pt.state.insert(key, ptrout) + pub(crate) fn insert_numout(&self, key: NumOutpointKey, ptrout: NumOut) { + self.db.num.state.insert(key, ptrout) } - pub(crate) fn insert_ptr(&self, key: Sptr, outpoint: EncodableOutpoint) { - self.db.pt.state.insert(key, outpoint) + pub(crate) fn insert_num_outpoint(&self, key: NumId, outpoint: EncodableOutpoint) { + self.db.num.state.insert_num_outpoint(key, outpoint) } - pub(crate) fn insert_numeric(&self, key: NumericKey, sptr: Sptr) { - self.db.pt.state.insert_numeric(key, sptr) + pub(crate) fn insert_num(&self, key: NumericKey, id: NumId) { + self.db.num.state.insert_num(key, id) } - pub(crate) fn insert_delegation(&self, key: RegistrySptrKey, space: SLabel) { - self.db.pt.state.insert_registry_delegation(key, space) + pub(crate) fn insert_delegator(&self, key: DelegatorKey, space: SLabel) { + self.db.num.state.insert_delegator(key, space) } - pub fn remove_delegation(&mut self, delegation: RegistrySptrKey) { - self.db.pt.state.remove(delegation) + pub fn remove_delegator(&mut self, key: DelegatorKey) { + self.db.num.state.remove(key) } pub(crate) fn insert_commitment(&self, key: CommitmentKey, commitment: Commitment) { - self.db.pt.state.insert_commitment(key, commitment) + self.db.num.state.insert_commitment(key, commitment) } - pub(crate) fn insert_registry(&self, key: RegistryKey, state_root: Hash) { - self.db.pt.state.insert_registry(key, state_root) + pub(crate) fn insert_commitment_tip(&self, key: CommitmentTipKey, state_root: Hash) { + self.db.num.state.insert_commitment_tip(key, state_root) } - pub(crate) fn remove_registry(&self, key: RegistryKey) { - self.db.pt.state.remove_registry(key) + pub(crate) fn remove_commitment_tip(&self, key: CommitmentTipKey) { + self.db.num.state.remove_commitment_tip(key) } - pub fn remove_ptr_utxo(&mut self, outpoint: OutPoint) { + pub fn remove_num_utxo(&mut self, outpoint: OutPoint) { let key = OutpointKey::from_outpoint::(outpoint); - self.db.pt.state.remove(key) + self.db.num.state.remove(key) } - - pub fn remove_commitment(&mut self, commitment: CommitmentKey) { - self.db.pt.state.remove(commitment) + self.db.num.state.remove(commitment) } pub fn remove_space_utxo(&mut self, outpoint: OutPoint) { @@ -390,15 +388,15 @@ impl Chain { })) } - pub fn get_ptrs_block(&mut self, hash: BlockHash) -> anyhow::Result> { - let idx = match &mut self.idx.pt { + pub fn get_nums_block(&mut self, hash: BlockHash) -> anyhow::Result> { + let idx = match &mut self.idx.num { None => return Err(anyhow!("ptrs index must be enabled")), Some(idx) => idx }; let key = BaseHash::from_slice(hash.as_ref()); - let block = idx.state.get(key).context("could not retrieve ptr block meta")?; + let block = idx.state.get(key).context("could not retrieve num block meta")?; Ok(block.map(|b| { - PtrBlockMetaWithHash { + NumBlockMetaWithHash { hash, block_meta: b, } @@ -410,11 +408,11 @@ impl Chain { F: Fn(u32) -> anyhow::Result, { let point = self.restore_spaces(get_block_hash, None)?; - self.restore_ptrs(point) + self.restore_nums(point) } - pub fn restore_ptrs(&self, required_checkpoint: ChainAnchor) -> anyhow::Result<()> { - let iter = self.db.pt.store.iter(); + pub fn restore_nums(&self, required_checkpoint: ChainAnchor) -> anyhow::Result<()> { + let iter = self.db.num.store.iter(); let mut restore_point = None; for (idx, snapshot) in iter.enumerate() { @@ -428,37 +426,37 @@ impl Chain { let (snapshot_idx, snapshot, checkpoint) = match restore_point { - None => return Err(anyhow!("Could not restore ptrs to height = {}", required_checkpoint.height)), + None => return Err(anyhow!("Could not restore nums to height = {}", required_checkpoint.height)), Some(rp) => rp, }; - info!("Restoring ptrs block={} height={}", checkpoint.hash, checkpoint.height); + info!("Restoring nums block={} height={}", checkpoint.hash, checkpoint.height); - if let Some(ptr_idx) = self.idx.pt.as_ref() { - let idx = ptr_idx.store + if let Some(num_idx) = self.idx.num.as_ref() { + let idx = num_idx.store .iter().skip(snapshot_idx).next(); if idx.is_none() { return Err(anyhow!( - "Could not restore ptr block index due to missing snapshot" + "Could not restore num block index due to missing snapshot" )); } let idx = idx.unwrap()?; let idx_checkpoint: ChainAnchor = idx.metadata().try_into()?; if idx_checkpoint != checkpoint { return Err(anyhow!( - "ptr block index checkpoint does not match the ptr's checkpoint" + "num block index checkpoint does not match the num's checkpoint" )); } idx.rollback() - .context("could not rollback ptr block index snapshot")?; + .context("could not rollback num block index snapshot")?; } snapshot .rollback() - .context("could not rollback ptr snapshot")?; + .context("could not rollback num snapshot")?; - self.db.pt.state.restore(checkpoint.clone()); - if let Some(idx) = self.idx.pt.as_ref() { + self.db.num.state.restore(checkpoint.clone()); + if let Some(idx) = self.idx.num.as_ref() { idx.state.restore(checkpoint); } @@ -547,7 +545,7 @@ impl Chain { let mut anchors = Vec::new(); let sp_iter = self.db.sp.store.iter().take(num_anchors as _); - let mut pt_iter = self.db.pt.store.iter(); + let mut pt_iter = self.db.num.store.iter(); for sp_snap in sp_iter { let mut sp_snap = sp_snap?; @@ -569,7 +567,7 @@ impl Chain { // Preserve existing anchor but update ptrs_root if we have a new one let updated_anchor = RootAnchor { spaces_root: existing.spaces_root, - ptrs_root: ptrs_root.or(existing.ptrs_root), + nums_root: ptrs_root.or(existing.nums_root), block: existing.block.clone(), }; anchors.push(updated_anchor); @@ -577,7 +575,7 @@ impl Chain { let spaces_root = sp_snap.compute_root()?; anchors.push(RootAnchor { spaces_root, - ptrs_root, + nums_root: ptrs_root, block: anchor, }); } @@ -590,7 +588,7 @@ impl Chain { info!( "Latest root anchor spaces={} ptrs={} (height: {})", hex::encode(result.spaces_root), - result.ptrs_root.as_ref().map(hex::encode).unwrap_or_else(|| "none".to_string()), + result.nums_root.as_ref().map(hex::encode).unwrap_or_else(|| "none".to_string()), result.block.height ); } @@ -623,15 +621,15 @@ fn load_sp_index(dir: &Path, genesis: ChainAnchor, tip: ChainAnchor, initial_syn Ok(index) } -fn load_pt_index(dir: &Path, genesis: ChainAnchor, tip: ChainAnchor, initial_sync: bool) -> anyhow::Result { - let block_db_path = dir.join("ptrs_block_index.sdb"); +fn load_num_index(dir: &Path, genesis: ChainAnchor, tip: ChainAnchor, initial_sync: bool) -> anyhow::Result { + let block_db_path = dir.join("nums_block_index.sdb"); if !initial_sync && !block_db_path.exists() { return Err(anyhow::anyhow!( - "Ptr Block index must be enabled from the initial sync." + "Num Block index must be enabled from the initial sync." )); } - let block_store = PtrStore::open(block_db_path)?; - let index = PtrLiveStore { + let block_store = NumStore::open(block_db_path)?; + let index = NumLiveStore { state: block_store.begin(&genesis).expect("begin block index"), store: block_store, }; @@ -639,7 +637,7 @@ fn load_pt_index(dir: &Path, genesis: ChainAnchor, tip: ChainAnchor, initial_syn let idx_tip = index.state.tip.read().expect("index"); if idx_tip.height != tip.height || idx_tip.hash != tip.hash { return Err(anyhow::anyhow!( - "Ptrs tip and block index states don't match." + "Nums tip and block index states don't match." )); } } diff --git a/client/src/store/ptrs.rs b/client/src/store/ptrs.rs index 3d80fd9..0ffe768 100644 --- a/client/src/store/ptrs.rs +++ b/client/src/store/ptrs.rs @@ -22,8 +22,8 @@ use spaces_protocol::{ hasher::{KeyHash}, }; use spaces_protocol::slabel::SLabel; -use spaces_ptr::{Commitment, CommitmentKey, FullPtrOut, NumericKey, PtrOut, PtrSource, RegistryKey, RegistrySptrKey, PtrOutpointKey}; -use spaces_ptr::sptr::Sptr; +use spaces_nums::{Commitment, CommitmentKey, FullNumOut, NumericKey, NumOut, NumSource, CommitmentTipKey, DelegatorKey, NumOutpointKey}; +use spaces_nums::num_id::NumId; use crate::store::{EncodableOutpoint, Sha256}; type SpaceDb = Database; @@ -32,16 +32,16 @@ pub type WriteTx<'db> = WriteTransaction<'db, Sha256Hasher>; type WriteMemory = BTreeMap>>; #[derive(Clone)] -pub struct PtrStore(SpaceDb); +pub struct NumStore(SpaceDb); #[derive(Clone)] -pub struct PtrLiveStore { - pub store: PtrStore, - pub state: PtrLiveSnapshot, +pub struct NumLiveStore { + pub store: NumStore, + pub state: NumLiveSnapshot, } #[derive(Clone)] -pub struct PtrLiveSnapshot { +pub struct NumLiveSnapshot { db: SpaceDb, pub tip: Arc>, staged: Arc>, @@ -55,7 +55,7 @@ pub struct Staged { memory: WriteMemory, } -impl PtrStore { +impl NumStore { pub fn open(path: PathBuf) -> Result { let db = Self::open_db(path)?; Ok(Self(db)) @@ -85,7 +85,7 @@ impl PtrStore { Ok(self.0.begin_write()?) } - pub fn begin(&self, genesis_block: &ChainAnchor) -> Result { + pub fn begin(&self, genesis_block: &ChainAnchor) -> Result { let snapshot = self.0.begin_read()?; let anchor: ChainAnchor = if snapshot.metadata().len() == 0 { genesis_block.clone() @@ -94,7 +94,7 @@ impl PtrStore { }; let version = anchor.hash; - let live = PtrLiveSnapshot { + let live = NumLiveSnapshot { db: self.0.clone(), tip: Arc::new(RwLock::new(anchor)), staged: Arc::new(RwLock::new(Staged { @@ -108,24 +108,24 @@ impl PtrStore { } } -pub trait PtrChainState { - fn insert_ptrout(&self, key: PtrOutpointKey, ptrout: PtrOut); +pub trait NumChainState { + fn insert_numout(&self, key: NumOutpointKey, ptrout: NumOut); fn insert_commitment(&self, key: CommitmentKey, commitment: Commitment); - fn insert_registry(&self, key: RegistryKey, state_root: Hash); - fn remove_registry(&self, key: RegistryKey); - fn insert_registry_delegation(&self, key: RegistrySptrKey, space: SLabel); - fn insert_ptr(&self, key: Sptr, outpoint: EncodableOutpoint); - fn insert_numeric(&self, key: NumericKey, sptr: Sptr); + fn insert_commitment_tip(&self, key: CommitmentTipKey, state_root: Hash); + fn remove_commitment_tip(&self, key: CommitmentTipKey); + fn insert_delegator(&self, key: DelegatorKey, space: SLabel); + fn insert_num_outpoint(&self, key: NumId, outpoint: EncodableOutpoint); + fn insert_num(&self, key: NumericKey, id: NumId); #[allow(dead_code)] - fn get_ptr_info( + fn get_num_info( &mut self, - space_hash: &Sptr, - ) -> Result>; + id: &NumId, + ) -> Result>; } -impl PtrChainState for PtrLiveSnapshot { - fn insert_ptrout(&self, key: PtrOutpointKey, ptrout: PtrOut) { +impl NumChainState for NumLiveSnapshot { + fn insert_numout(&self, key: NumOutpointKey, ptrout: NumOut) { self.insert(key, ptrout) } @@ -133,42 +133,42 @@ impl PtrChainState for PtrLiveSnapshot { self.insert(key, commitment) } - fn insert_registry(&self, key: RegistryKey, state_root: Hash) { + fn insert_commitment_tip(&self, key: CommitmentTipKey, state_root: Hash) { self.insert(key, state_root) } - fn remove_registry(&self, key: RegistryKey) { + fn remove_commitment_tip(&self, key: CommitmentTipKey) { self.remove(key) } - fn insert_registry_delegation(&self, key: RegistrySptrKey, space: SLabel) { + fn insert_delegator(&self, key: DelegatorKey, space: SLabel) { self.insert(key, space) } - fn insert_ptr(&self, key: Sptr, outpoint: EncodableOutpoint) { + fn insert_num_outpoint(&self, key: NumId, outpoint: EncodableOutpoint) { self.insert(key, outpoint) } - fn insert_numeric(&self, key: NumericKey, sptr: Sptr) { - self.insert(key, sptr) + fn insert_num(&self, key: NumericKey, id: NumId) { + self.insert(key, id) } - fn get_ptr_info(&mut self, hash: &Sptr) -> Result> { - let outpoint = self.get_ptr_outpoint(hash)?; + fn get_num_info(&mut self, hash: &NumId) -> Result> { + let outpoint = self.get_num_outpoint_by_id(hash)?; if let Some(outpoint) = outpoint { - let spaceout = self.get_ptrout(&outpoint)?; + let spaceout = self.get_numout(&outpoint)?; - return Ok(Some(FullPtrOut { + return Ok(Some(FullNumOut { txid: outpoint.txid, - ptrout: spaceout.expect("should exist if outpoint exists"), + numout: spaceout.expect("should exist if outpoint exists"), })); } Ok(None) } } -impl PtrLiveSnapshot { +impl NumLiveSnapshot { #[inline] pub fn is_dirty(&self) -> bool { self.staged.read().expect("read").memory.len() > 0 @@ -311,13 +311,13 @@ impl PtrLiveSnapshot { } } -impl PtrSource for PtrLiveSnapshot { - fn get_ptr_outpoint( +impl NumSource for NumLiveSnapshot { + fn get_num_outpoint_by_id( &mut self, - sptr: &Sptr, + id: &NumId, ) -> spaces_protocol::errors::Result> { - let result: Option = self.get(*sptr).map_err(|err| { - spaces_protocol::errors::Error::IO(format!("getptroutpoint: {}", err.to_string())) + let result: Option = self.get(*id).map_err(|err| { + spaces_protocol::errors::Error::IO(format!("getnumoutpoint: {}", err.to_string())) })?; Ok(result.map(|out| out.into())) } @@ -329,32 +329,32 @@ impl PtrSource for PtrLiveSnapshot { Ok(result) } - fn get_delegator(&mut self, key: &RegistrySptrKey) -> spaces_protocol::errors::Result> { + fn get_delegator(&mut self, key: &DelegatorKey) -> spaces_protocol::errors::Result> { let result = self.get(*key).map_err(|err| { spaces_protocol::errors::Error::IO(format!("getdelegate: {}", err.to_string())) })?; Ok(result) } - fn get_commitments_tip(&mut self, key: &RegistryKey) -> spaces_protocol::errors::Result> { + fn get_commitments_tip(&mut self, key: &CommitmentTipKey) -> spaces_protocol::errors::Result> { let result = self.get(*key).map_err(|err| { spaces_protocol::errors::Error::IO(format!("getregistry: {}", err.to_string())) })?; Ok(result) } - fn get_ptrout( + fn get_numout( &mut self, outpoint: &OutPoint, - ) -> spaces_protocol::errors::Result> { - let h = PtrOutpointKey::from_outpoint::(*outpoint); + ) -> spaces_protocol::errors::Result> { + let h = NumOutpointKey::from_outpoint::(*outpoint); let result = self.get(h).map_err(|err| { spaces_protocol::errors::Error::IO(format!("getptrout: {}", err.to_string())) })?; Ok(result) } - fn get_numeric(&mut self, key: &NumericKey) -> spaces_protocol::errors::Result> { + fn get_num_id(&mut self, key: &NumericKey) -> spaces_protocol::errors::Result> { let result = self.get(*key).map_err(|err| { spaces_protocol::errors::Error::IO(format!("getnumeric: {}", err.to_string())) })?; diff --git a/client/src/store/spaces.rs b/client/src/store/spaces.rs index 81d75d8..fd27851 100644 --- a/client/src/store/spaces.rs +++ b/client/src/store/spaces.rs @@ -26,7 +26,7 @@ use spaces_protocol::{ prepare::SpacesSource, Covenant, FullSpaceOut, SpaceOut, }; -use spaces_ptr::RootAnchor; +use spaces_nums::RootAnchor; use crate::store::{EncodableOutpoint, ReadTx, Sha256, SpaceDb, WriteMemory, WriteTx}; #[derive(Clone)] @@ -111,7 +111,7 @@ impl SpStore { let spaces_root = snap.compute_root()?; anchors.push(RootAnchor { spaces_root, - ptrs_root: None, + nums_root: None, block: anchor, }); } diff --git a/client/src/wallets.rs b/client/src/wallets.rs index 6852a91..2485a62 100644 --- a/client/src/wallets.rs +++ b/client/src/wallets.rs @@ -29,7 +29,7 @@ use spaces_wallet::{ }; use crate::cbf::CompactFilterSync; -use crate::rpc::CommitParams; +use crate::rpc::{CommitParams}; use crate::spaces::Spaced; use crate::store::chain::Chain; use crate::store::Sha256; @@ -44,13 +44,13 @@ use crate::{ }, std_wait, }; +use spaces_nums::num_id::{NumId, NumIdParseError, NUM_HRP}; +use spaces_nums::snumeric::SNumeric; +use spaces_nums::{FullNumOut, NumericKey}; +use spaces_nums::{DelegatorKey, NumOut, NumSource}; use spaces_protocol::bitcoin::address::ParseError; use spaces_protocol::bitcoin::{Network, ScriptBuf}; -use spaces_ptr::snumeric::SNumeric; -use spaces_ptr::sptr::{Sptr, SptrParseError, SPTR_HRP}; -use spaces_ptr::NumericKey; -use spaces_ptr::{PtrOut, PtrSource, RegistrySptrKey}; -use spaces_wallet::builder::{CommitmentRequest, PtrRequest, PtrTransfer}; +use spaces_wallet::builder::{CommitmentRequest, NumDelegate, NumRequest, NumTransfer}; use tabled::Tabled; use tokio::{ select, @@ -66,7 +66,7 @@ pub enum ResolvableTarget { Space(SLabel), SpaceAddress(SpaceAddress), Address(Address), - Sptr(Sptr), + Snum(NumId), Numeric(SNumeric), } @@ -74,8 +74,8 @@ pub enum ResolvableTarget { pub enum ResolvableTargetParseError { SpaceLabelParseError(spaces_protocol::errors::Error), AddressParseError(ParseError), - SptrParseError(SptrParseError), - NumericParseError(spaces_ptr::snumeric::SNumericParseError), + NumParseError(NumIdParseError), + NumericParseError(spaces_nums::snumeric::SNumericParseError), } impl Display for ResolvableTargetParseError { @@ -83,7 +83,7 @@ impl Display for ResolvableTargetParseError { match self { ResolvableTargetParseError::SpaceLabelParseError(e) => write!(f, "{}", e), ResolvableTargetParseError::AddressParseError(e) => write!(f, "{}", e), - ResolvableTargetParseError::SptrParseError(e) => write!(f, "{}", e), + ResolvableTargetParseError::NumParseError(e) => write!(f, "{}", e), ResolvableTargetParseError::NumericParseError(e) => write!(f, "{}", e), } } @@ -101,10 +101,10 @@ impl FromStr for ResolvableTarget { .map(ResolvableTarget::Space) .map_err(ResolvableTargetParseError::SpaceLabelParseError); } - if s.starts_with(SPTR_HRP) { - return Sptr::from_str(s) - .map(ResolvableTarget::Sptr) - .map_err(ResolvableTargetParseError::SptrParseError); + if s.starts_with(NUM_HRP) { + return NumId::from_str(s) + .map(ResolvableTarget::Snum) + .map_err(ResolvableTargetParseError::NumParseError); } if s.starts_with('#') { return SNumeric::from_str(s) @@ -125,7 +125,7 @@ impl fmt::Display for ResolvableTarget { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ResolvableTarget::Space(label) => write!(f, "{}", label), - ResolvableTarget::Sptr(sptr) => write!(f, "{}", sptr), + ResolvableTarget::Snum(snum) => write!(f, "{}", snum), ResolvableTarget::Numeric(num) => write!(f, "{}", num), ResolvableTarget::SpaceAddress(addr) => write!(f, "{}", addr), ResolvableTarget::Address(addr) => write!(f, "{}", addr), @@ -196,16 +196,16 @@ pub struct ListSpacesResponse { } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PtrEntry { +pub struct NumEntry { pub txid: Txid, #[serde(flatten)] - pub ptrout: PtrOut, + pub numout: NumOut, pub delegating_for: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ListPtrsResponse { - pub ptrs: Vec, +pub struct ListNumsResponse { + pub nums: Vec, } #[derive(Tabled, Debug, Clone, Serialize, Deserialize)] @@ -270,6 +270,10 @@ pub enum WalletCommand { kind: AddressKind, resp: crate::rpc::Responder>, }, + IncrementAddress { + kind: AddressKind, + resp: crate::rpc::Responder>, + }, BumpFee { txid: Txid, fee_rate: FeeRate, @@ -285,7 +289,7 @@ pub enum WalletCommand { resp: crate::rpc::Responder>, }, ListPtrs { - resp: crate::rpc::Responder>, + resp: crate::rpc::Responder>, }, Buy { listing: Listing, @@ -319,7 +323,7 @@ pub enum WalletCommand { resp: crate::rpc::Responder>, }, CanOperate { - space: SLabel, + subject: Subject, resp: crate::rpc::Responder>, }, } @@ -343,36 +347,67 @@ impl spaces_wallet::Mempool for MempoolChecker<'_> { } } +fn resolve_subject_to_num_id( + chain: &mut Chain, + subject: &Subject, +) -> anyhow::Result { + match subject { + Subject::NumId(id) => Ok(*id), + Subject::Label(label) if label.is_numeric() => { + let numeric: SNumeric = label.clone().try_into().unwrap(); + let key = NumericKey::from_numeric::(&numeric); + chain + .get_num_id(&key)? + .ok_or_else(|| anyhow!("numeric '{}' not found", numeric)) + } + Subject::Label(label) => Err(anyhow!( + "expected a num id or numeric, not a space: '{}'", + label + )), + } +} + fn commit_params_to_req( chain: &mut Chain, wallet: &SpacesWallet, p: CommitParams, ) -> anyhow::Result { - let space_key = SpaceKey::from(Sha256::hash(p.space.as_ref())); - let info = match chain.get_space_info(&space_key)? { - None => return Err(anyhow!("commit: no such space {}", p.space)), - Some(info) => info, - }; - let sptr = Sptr::from_spk::(info.spaceout.script_pubkey.clone()); - let ptr_info = match chain.get_ptr_info(&sptr)? { - None => { - return Err(anyhow!( - "commit: sptr {} doesn't exist for space {} - have you created it?", - sptr, - p.space - )) + // Resolve to the spk-derived NumId (the operator num that holds the delegation) + let spk_id = match &p.subject { + Subject::Label(label) if label.is_numeric() => { + let numeric: SNumeric = label.clone().try_into().unwrap(); + let key = NumericKey::from_numeric::(&numeric); + let num_id = chain + .get_num_id(&key)? + .ok_or_else(|| anyhow!("commit: numeric '{}' not found", label))?; + let num_info = chain + .get_num_info(&num_id)? + .ok_or_else(|| anyhow!("commit: num '{}' not found", label))?; + NumId::from_spk::(num_info.numout.script_pubkey) + } + Subject::Label(label) => { + let info = chain + .get_space_info(&SpaceKey::from(Sha256::hash(label.as_ref())))? + .ok_or_else(|| anyhow!("commit: space '{}' not found", label))?; + + if info.spaceout.space.is_none() || !info.spaceout.space.as_ref().unwrap().is_owned() { + return Err(anyhow!("commit: space '{}' is not owned", label)); + } + NumId::from_spk::(info.spaceout.script_pubkey) } - Some(pt) => pt, + Subject::NumId(id) => *id, }; - if info.spaceout.space.is_none() - || !info.spaceout.space.as_ref().unwrap().is_owned() - || !wallet.is_mine(ptr_info.ptrout.script_pubkey.clone()) - { - return Err(anyhow!("commit: you don't control `{}`", sptr)); + + let num_info = chain + .get_num_info(&spk_id)? + .ok_or_else(|| anyhow!("commit: num '{}' not found - use delegate first", spk_id))?; + + if !wallet.is_mine(num_info.numout.script_pubkey.clone()) { + return Err(anyhow!("commit: you don't control '{}'", spk_id)); } Ok(CommitmentRequest { - ptrout: ptr_info, + numout: num_info, root: p.root.map(|p| *p.as_ref()), }) } @@ -591,6 +626,16 @@ impl RpcWallet { }; _ = resp.send(Ok(address)); } + WalletCommand::IncrementAddress { kind, resp } => { + let address = match kind { + AddressKind::Coin => wallet + .reveal_next_address(KeychainKind::External) + .address + .to_string(), + AddressKind::Space => wallet.reveal_next_space_address().to_string(), + }; + _ = resp.send(Ok(address)); + } WalletCommand::ListUnspent { resp } => { _ = resp.send(wallet.list_unspent_with_details(chain)); } @@ -603,7 +648,7 @@ impl RpcWallet { _ = resp.send(result); } WalletCommand::ListPtrs { resp } => { - let result = Self::list_ptrs(wallet, chain); + let result = Self::list_nums(wallet, chain); _ = resp.send(result); } WalletCommand::ListBidouts { resp } => { @@ -646,39 +691,65 @@ impl RpcWallet { } => { _ = resp.send(wallet.sign_schnorr::(chain, subject, &message)); } - WalletCommand::CanOperate { space, resp } => { - let result = Self::can_operate(wallet, chain, space); + WalletCommand::CanOperate { subject, resp } => { + let result = Self::can_operate(wallet, chain, &subject); _ = resp.send(result); } } Ok(()) } - /// Check if wallet can operate on a space by verifying it controls the delegated sptr + /// Check if wallet can operate on a subject by verifying it controls the delegated num fn can_operate( wallet: &SpacesWallet, chain: &mut Chain, - space: SLabel, + subject: &Subject, ) -> anyhow::Result { - // Use the same delegation lookup logic as get_delegation - let space_info = chain - .get_space_info(&SpaceKey::from(Sha256::hash(space.as_ref())))? - .ok_or_else(|| anyhow::anyhow!("Space not found: {}", space))?; + let label = match subject { + Subject::Label(label) => label.clone(), + Subject::NumId(id) => { + let info = chain + .get_num_info(id)? + .ok_or_else(|| anyhow::anyhow!("num id '{}' not found", id))?; + info.numout.num.name.to_slabel() + } + }; - let sptr = Sptr::from_spk::(space_info.spaceout.script_pubkey.clone()); + let spk_id = if label.is_numeric() { + // For numerics, resolve the num via the numeric key then + // derive the spk-based id (delegation is on the address, not the num) + let numeric: SNumeric = label + .clone() + .try_into() + .map_err(|e| anyhow::anyhow!("invalid numeric label: {}", e))?; + let key = NumericKey::from_numeric::(&numeric); + let num_id = chain + .get_num_id(&key)? + .ok_or_else(|| anyhow::anyhow!("Numeric not found: {}", label))?; + let num_info = chain + .get_num_info(&num_id)? + .ok_or_else(|| anyhow::anyhow!("Num not found: {}", label))?; + NumId::from_spk::(num_info.numout.script_pubkey) + } else { + // For spaces, derive the num id from the space's spk + let space_info = chain + .get_space_info(&SpaceKey::from(Sha256::hash(label.as_ref())))? + .ok_or_else(|| anyhow::anyhow!("Space not found: {}", label))?; + NumId::from_spk::(space_info.spaceout.script_pubkey.clone()) + }; // Check reverse mapping to verify delegation is valid - let delegator = chain.get_delegator(&RegistrySptrKey::from_sptr::(sptr))?; - if delegator.as_ref() != Some(&space) { + let delegator = chain.get_delegator(&DelegatorKey::from_id::(spk_id))?; + if delegator.as_ref() != Some(&label) { return Ok(false); } - // Get ptr info and check if wallet controls it - let ptr_info = chain - .get_ptr_info(&sptr)? - .ok_or_else(|| anyhow::anyhow!("PTR not found for sptr: {}", sptr))?; + // Get num info and check if wallet controls it + let num_info = chain + .get_num_info(&spk_id)? + .ok_or_else(|| anyhow::anyhow!("Num not found for id: {}", spk_id))?; - Ok(wallet.is_mine(ptr_info.ptrout.script_pubkey)) + Ok(wallet.is_mine(num_info.numout.script_pubkey)) } /// Returns true if Bitcoin, protocol, and wallet tips match. @@ -971,26 +1042,26 @@ impl RpcWallet { Ok(()) } - fn list_ptrs(wallet: &mut SpacesWallet, chain: &mut Chain) -> anyhow::Result { - let mut ptrs: Vec = Vec::new(); + fn list_nums(wallet: &mut SpacesWallet, chain: &mut Chain) -> anyhow::Result { + let mut nums: Vec = Vec::new(); for unspent in wallet.list_unspent() { - let sptr = Sptr::from_spk::(unspent.txout.script_pubkey); - let Some(fpo) = chain.get_ptr_info(&sptr)? else { + let snum = NumId::from_spk::(unspent.txout.script_pubkey); + let Some(fpo) = chain.get_num_info(&snum)? else { continue; }; if fpo.outpoint() != unspent.outpoint { continue; } - let rsk = RegistrySptrKey::from_sptr::(sptr); + let rsk = DelegatorKey::from_id::(snum); let delegating_for = chain.get_delegator(&rsk)?; - ptrs.push(PtrEntry { + nums.push(NumEntry { txid: fpo.txid, - ptrout: fpo.ptrout, + numout: fpo.numout, delegating_for, }) } - Ok(ListPtrsResponse { ptrs }) + Ok(ListNumsResponse { nums: nums }) } fn list_spaces( @@ -1004,7 +1075,11 @@ impl RpcWallet { .collect(); let mut recent_events: HashMap> = HashMap::new(); for (txid, event) in wallet.list_recent_events()? { - if !event.space.as_ref().is_some_and(|s| owned_spaces.contains(s)) { + if !event + .space + .as_ref() + .is_some_and(|s| owned_spaces.contains(s)) + { recent_events.entry(txid).or_default().push(event); } } @@ -1021,15 +1096,16 @@ impl RpcWallet { break; } } - recent_events_with_txs - .extend(recent_events.into_values().flatten().map(|e| (None, e))); + recent_events_with_txs.extend(recent_events.into_values().flatten().map(|e| (None, e))); let mut pending = vec![]; let mut outbid = vec![]; for (tx, event) in recent_events_with_txs { let name = SLabel::from_str(event.space.as_ref().unwrap()).expect("valid space name"); - if tx.as_ref() - .is_some_and(|tx| !tx.chain_position.is_confirmed()) { + if tx + .as_ref() + .is_some_and(|tx| !tx.chain_position.is_confirmed()) + { pending.push(name); continue; } @@ -1043,8 +1119,10 @@ impl RpcWallet { outbid.push(space); continue; } - if event.previous_spaceout - .is_some_and(|input| input == space.outpoint()) { + if event + .previous_spaceout + .is_some_and(|input| input == space.outpoint()) + { continue; } outbid.push(space); @@ -1152,22 +1230,22 @@ impl RpcWallet { Address::from_script(script_pubkey.as_script(), network.fallback_network())? } ResolvableTarget::SpaceAddress(address) => address.0, - ResolvableTarget::Sptr(sptr) => { - let script_pubkey = match chain.get_ptr_info(&sptr)? { + ResolvableTarget::Snum(snum) => { + let script_pubkey = match chain.get_num_info(&snum)? { None => return Ok(None), - Some(fullptrout) => fullptrout.ptrout.script_pubkey, + Some(fullnumout) => fullnumout.numout.script_pubkey, }; Address::from_script(script_pubkey.as_script(), network.fallback_network())? } ResolvableTarget::Numeric(numeric) => { let key = NumericKey::from_numeric::(&numeric); - let sptr = match chain.get_numeric(&key)? { + let snum = match chain.get_num_id(&key)? { None => return Ok(None), - Some(sptr) => sptr, + Some(snum) => snum, }; - let script_pubkey = match chain.get_ptr_info(&sptr)? { + let script_pubkey = match chain.get_num_info(&snum)? { None => return Ok(None), - Some(fullptrout) => fullptrout.ptrout.script_pubkey, + Some(fullnumout) => fullnumout.numout.script_pubkey, }; Address::from_script(script_pubkey.as_script(), network.fallback_network())? } @@ -1249,30 +1327,49 @@ impl RpcWallet { None }; - // Process each item - space, PTR, or numeric + // Process each item - space or num for item in ¶ms.spaces { match item { - Subject::Ptr(_) | Subject::Numeric(_) => { - let sptr = match item { - Subject::Ptr(s) => *s, - Subject::Numeric(numeric) => { - let key = NumericKey::from_numeric::(numeric); - chain.get_numeric(&key)?.ok_or_else(|| { - anyhow!("transfer: numeric '{}' not found", numeric) - })? + Subject::NumId(id) => { + let num = match chain.get_num_info(id)? { + None => return Err(anyhow!("transfer: num '{}' not found or not owned", id)), + Some(full) if !wallet.is_mine(full.numout.script_pubkey.clone()) => { + return Err(anyhow!("transfer: you don't own num '{}'", id)) + } + Some(full) if wallet.get_utxo(OutPoint::new(full.txid, full.numout.n as u32)).is_none() => { + return Err(anyhow!( + "transfer '{}': wallet already has a pending tx for this num", + id + )) } - _ => unreachable!(), + Some(full) => full, + }; + + let recipient_addr = match recipient.clone() { + None => wallet.reveal_next_space_address(), + Some(addr) => SpaceAddress::from(addr), }; - // Handle PTR transfer - let ptr = match chain.get_ptr_info(&sptr)? { - None => return Err(anyhow!("transfer: PTR '{}' not found or not owned", sptr)), - Some(full) if !wallet.is_mine(full.ptrout.script_pubkey.clone()) => { - return Err(anyhow!("transfer: you don't own PTR '{}'", sptr)) + + builder = builder.add_num_transfer(NumTransfer { + num, + recipient: recipient_addr, + }); + } + Subject::Label(label) if label.is_numeric() => { + let numeric: SNumeric = label.clone().try_into().unwrap(); + let key = NumericKey::from_numeric::(&numeric); + let id = chain.get_num_id(&key)?.ok_or_else(|| { + anyhow!("transfer: numeric '{}' not found", numeric) + })?; + let num = match chain.get_num_info(&id)? { + None => return Err(anyhow!("transfer: num '{}' not found or not owned", id)), + Some(full) if !wallet.is_mine(full.numout.script_pubkey.clone()) => { + return Err(anyhow!("transfer: you don't own num '{}'", id)) } - Some(full) if wallet.get_utxo(OutPoint::new(full.txid, full.ptrout.n as u32)).is_none() => { + Some(full) if wallet.get_utxo(OutPoint::new(full.txid, full.numout.n as u32)).is_none() => { return Err(anyhow!( - "transfer '{}': wallet already has a pending tx for this PTR", - sptr + "transfer '{}': wallet already has a pending tx for this num", + id )) } Some(full) => full, @@ -1283,12 +1380,12 @@ impl RpcWallet { Some(addr) => SpaceAddress::from(addr), }; - builder = builder.add_ptr_transfer(PtrTransfer { - ptr, + builder = builder.add_num_transfer(NumTransfer { + num, recipient: recipient_addr, }); } - Subject::Space(space) => { + Subject::Label(space) => { // Handle space transfer let spacehash = SpaceKey::from(Sha256::hash(space.as_ref())); match chain.get_space_info(&spacehash)? { @@ -1344,7 +1441,7 @@ impl RpcWallet { builder = builder.add_transfer(SpaceTransfer { space: full, recipient: recipient_addr.clone(), - create_ptr: false, + create_num: false, }); } }; @@ -1452,137 +1549,189 @@ impl RpcWallet { builder = builder.add_register(utxo, Some(address)); } - RpcWalletRequest::CreatePtr(params) => { - let spk_raw = hex::decode(params.spk) - .map_err(|e| anyhow!("transferptr: invalid spk: {:?}", e))?; - - let spk = ScriptBuf::from(spk_raw); - let sptr = Sptr::from_spk::(spk.clone()); + RpcWalletRequest::CreateNum(params) => { + let spk = match params.bind_spk { + Some(spk) => spk, + None => advance_address_to_unique_num_spk(chain, wallet)?, + }; + let snum = NumId::from_spk::(spk.clone()); - let sptr = chain.get_ptr_info(&sptr)?; - if sptr.is_some() && !tx.force { - return Err(anyhow!("sptr already exists")); + let snum = chain.get_num_info(&snum)?; + if snum.is_some() && !tx.force { + return Err(anyhow!("snum already exists")); } - builder = builder.add_ptr(PtrRequest { spk }) + builder = builder.add_num(NumRequest { bind_spk: spk }) } RpcWalletRequest::Commit(params) => { let reqs = commit_params_to_req(chain, wallet, params)?; builder = builder.add_commitment(reqs) } RpcWalletRequest::Delegate(params) => { - let space = params.space; - let spacehash = SpaceKey::from(Sha256::hash(space.as_ref())); - - // Validate space exists and is owned - let full = match chain.get_space_info(&spacehash)? { - None => return Err(anyhow!("delegate: space '{}' not found", space)), - Some(full) - if full.spaceout.space.is_none() - || !full.spaceout.space.as_ref().unwrap().is_owned() - || !wallet.is_mine(full.spaceout.script_pubkey.clone()) => - { - return Err(anyhow!("delegate: you don't own '{}'", space)) + let unique_num_spk = advance_address_to_unique_num_spk(chain, wallet)?; + + match ¶ms.subject { + Subject::Label(label) if label.is_numeric() => { + let numeric: SNumeric = label.clone().try_into().unwrap(); + let key = NumericKey::from_numeric::(&numeric); + let id = chain.get_num_id(&key)?.ok_or_else(|| { + anyhow!("delegate: numeric '{}' not found", label) + })?; + let num = match chain.get_num_info(&id)? { + None => return Err(anyhow!("delegate: num '{}' not found", id)), + Some(full) + if !wallet.is_mine(full.numout.script_pubkey.clone()) => + { + return Err(anyhow!("delegate: you don't own '{}'", label)) + } + Some(full) + if wallet + .get_utxo(OutPoint::new(full.txid, full.numout.n as u32)) + .is_none() => + { + return Err(anyhow!( + "delegate '{}': wallet already has a pending tx", + label + )) + } + Some(full) => full, + }; + builder = builder.add_num_delegate(NumDelegate { + num, + unique_num_spk: unique_num_spk.clone(), + }); } - Some(full) if wallet.get_utxo(full.outpoint()).is_none() => { - return Err(anyhow!( - "delegate '{}': wallet already has a pending tx for this space", - space - )) + Subject::NumId(id) => { + let num = match chain.get_num_info(id)? { + None => return Err(anyhow!("delegate: num '{}' not found", id)), + Some(full) + if !wallet.is_mine(full.numout.script_pubkey.clone()) => + { + return Err(anyhow!("delegate: you don't own '{}'", id)) + } + Some(full) + if wallet + .get_utxo(OutPoint::new(full.txid, full.numout.n as u32)) + .is_none() => + { + return Err(anyhow!( + "delegate '{}': wallet already has a pending tx", + id + )) + } + Some(full) => full, + }; + builder = builder.add_num_delegate(NumDelegate { + num, + unique_num_spk: unique_num_spk.clone(), + }); } - Some(full) => full, - }; + Subject::Label(label) => { + let spacehash = SpaceKey::from(Sha256::hash(label.as_ref())); - // Generate fresh unique address and verify SPTR is available - let recipient = loop { - let addr = wallet.reveal_next_space_address(); - let spk = addr.script_pubkey(); - let sptr = Sptr::from_spk::(spk); - let rsk = RegistrySptrKey::from_sptr::(sptr); - - // Check if SPTR is already taken - match chain.get_delegator(&rsk)? { - None => break addr, // SPTR is free, use this address - Some(_) => { - // Collision! This SPTR is already delegated, try next address - continue; - } - } - }; + let full = match chain.get_space_info(&spacehash)? { + None => { + return Err(anyhow!("delegate: space '{}' not found", label)) + } + Some(full) + if full.spaceout.space.is_none() + || !full.spaceout.space.as_ref().unwrap().is_owned() + || !wallet.is_mine(full.spaceout.script_pubkey.clone()) => + { + return Err(anyhow!("delegate: you don't own '{}'", label)) + } + Some(full) if wallet.get_utxo(full.outpoint()).is_none() => { + return Err(anyhow!( + "delegate '{}': wallet already has a pending tx", + label + )) + } + Some(full) => full, + }; - // Add transfer to builder (establishes/updates delegation and creates PTR) - builder = builder.add_transfer(SpaceTransfer { - space: full, - recipient, - create_ptr: true, - }); - } - RpcWalletRequest::SetFallback(params) => { - match params.subject { - Subject::Space(ref space) => { - let spacehash = SpaceKey::from(Sha256::hash(space.as_ref())); - let full = chain.get_space_info(&spacehash)? - .ok_or_else(|| anyhow!("setfallback: space '{}' not found", space))?; - if !wallet.is_mine(full.spaceout.script_pubkey.clone()) { - return Err(anyhow!("setfallback: you don't own '{}'", space)); - } let recipient = SpaceAddress( - Address::from_script( - full.spaceout.script_pubkey.as_script(), - wallet.config.network, - ).expect("valid script"), + Address::from_script(&unique_num_spk, wallet.config.network) + .expect("valid address"), ); - builder = builder - .add_transfer(SpaceTransfer { - space: full, - recipient, - create_ptr: false, - }) - .add_data(params.data); + builder = builder.add_transfer(SpaceTransfer { + space: full, + recipient, + create_num: true, + }); } - Subject::Ptr(_) | Subject::Numeric(_) => { - let sptr = match ¶ms.subject { - Subject::Ptr(s) => *s, - Subject::Numeric(numeric) => { - let key = NumericKey::from_numeric::(numeric); - chain.get_numeric(&key)?.ok_or_else(|| { - anyhow!("setfallback: numeric '{}' not found", numeric) - })? - } - _ => unreachable!(), - }; - let ptr_info = match chain.get_ptr_info(&sptr)? { - None => return Err(anyhow!("setfallback: PTR '{}' not found", sptr)), - Some(ptr) if !wallet.is_mine(ptr.ptrout.script_pubkey.clone()) => { - return Err(anyhow!("setfallback: you don't own '{}'", sptr)) + } + } + RpcWalletRequest::Authorize(params) => { + let delegate_utxo = find_delegate_utxo(chain, ¶ms.subject)?; + if !wallet.is_mine(delegate_utxo.numout.script_pubkey.clone()) { + return Err(anyhow!("authorize: you don't own '{}'", params.subject)); + } + let Some(r) = Self::resolve(network, chain, ¶ms.to, true)? else { + return Err(anyhow!("authorize: recipient '{}' not found", params.to)); + }; + builder = builder.add_num_transfer(NumTransfer { + num: delegate_utxo, + recipient: SpaceAddress::from(r), + }) + } + RpcWalletRequest::SetFallback(params) => match params.subject { + Subject::Label(ref label) if !label.is_numeric() => { + let spacehash = SpaceKey::from(Sha256::hash(label.as_ref())); + let full = chain + .get_space_info(&spacehash)? + .ok_or_else(|| anyhow!("setfallback: space '{}' not found", label))?; + if !wallet.is_mine(full.spaceout.script_pubkey.clone()) { + return Err(anyhow!("setfallback: you don't own '{}'", label)); + } + let recipient = SpaceAddress( + Address::from_script( + full.spaceout.script_pubkey.as_script(), + wallet.config.network, + ) + .expect("valid script"), + ); + builder = builder + .add_transfer(SpaceTransfer { + space: full, + recipient, + create_num: false, + }) + .add_data(params.data); + } + _ => { + let id = resolve_subject_to_num_id::(chain, ¶ms.subject)?; + let num_info = match chain.get_num_info(&id)? { + None => return Err(anyhow!("setfallback: num '{}' not found", id)), + Some(num) if !wallet.is_mine(num.numout.script_pubkey.clone()) => { + return Err(anyhow!("setfallback: you don't own '{}'", id)) } - Some(ptr) + Some(num) if wallet - .get_utxo(OutPoint::new(ptr.txid, ptr.ptrout.n as u32)) + .get_utxo(OutPoint::new(num.txid, num.numout.n as u32)) .is_none() => { return Err(anyhow!( - "setfallback '{}': wallet already has a pending tx for this PTR", - sptr + "setfallback '{}': wallet already has a pending tx for this num", + id )) } - Some(ptr) => ptr, + Some(num) => num, }; - let recipient = SpaceAddress( - Address::from_script( - ptr_info.ptrout.script_pubkey.as_script(), - wallet.config.network, - ).expect("valid script"), - ); - builder = builder - .add_ptr_transfer(PtrTransfer { - ptr: ptr_info, - recipient, - }) - .add_data(params.data); - } + let recipient = SpaceAddress( + Address::from_script( + num_info.numout.script_pubkey.as_script(), + wallet.config.network, + ) + .expect("valid script"), + ); + builder = builder + .add_num_transfer(NumTransfer { + num: num_info, + recipient, + }) + .add_data(params.data); } - } + }, } } @@ -1786,6 +1935,14 @@ impl RpcWallet { resp_rx.await? } + pub async fn send_increment_address(&self, kind: AddressKind) -> anyhow::Result { + let (resp, resp_rx) = oneshot::channel(); + self.sender + .send(WalletCommand::IncrementAddress { kind, resp }) + .await?; + resp_rx.await? + } + pub async fn send_fee_bump( &self, txid: Txid, @@ -1864,7 +2021,7 @@ impl RpcWallet { resp_rx.await? } - pub async fn send_list_ptrs(&self) -> anyhow::Result { + pub async fn send_list_nums(&self) -> anyhow::Result { let (resp, resp_rx) = oneshot::channel(); self.sender.send(WalletCommand::ListPtrs { resp }).await?; resp_rx.await? @@ -1912,10 +2069,10 @@ impl RpcWallet { resp_rx.await? } - pub async fn send_can_operate(&self, space: SLabel) -> anyhow::Result { + pub async fn send_can_operate(&self, subject: Subject) -> anyhow::Result { let (resp, resp_rx) = oneshot::channel(); self.sender - .send(WalletCommand::CanOperate { space, resp }) + .send(WalletCommand::CanOperate { subject, resp }) .await?; resp_rx.await? } @@ -1948,3 +2105,101 @@ async fn named_future( ) -> (String, Result) { (name, rx.await) } + +fn advance_address_to_unique_num_spk( + chain: &mut Chain, + w: &mut SpacesWallet, +) -> anyhow::Result { + loop { + let addr = w.reveal_next_space_address(); + let spk = addr.script_pubkey(); + let id = NumId::from_spk::(spk); + if chain.get_num_outpoint_by_id(&id)?.is_some() { + continue; + } + // the num utxo may not be present, but its id can still be delegated + let dk = DelegatorKey::from_id::(id); + match chain.get_delegator(&dk)? { + None => return Ok(addr.script_pubkey()), + Some(_) => continue, + } + } +} + + +fn find_delegate_utxo(chain: &mut Chain, subject: &Subject) -> anyhow::Result { + let num_id = match &subject { + Subject::NumId(id) => Some(id.clone()), + Subject::Label(label) if label.is_numeric() => { + let numeric: SNumeric = label + .clone() + .try_into() + .expect("valid numeric"); + let key = NumericKey::from_numeric::(&numeric); + let id = chain.get_num_id(&key)?.ok_or_else(|| { + anyhow!("authorize: numeric '{}' not found", label) + })?; + Some(id) + } + Subject::Label(_) => None, + }; + + let target = if let Some(num_id) = num_id { + let Some(num_utxo) = chain.get_num_info(&num_id)? else { + return Err(anyhow!("authorize: num {} not found", subject)); + }; + + let target = NumId::from_spk::(num_utxo.numout.script_pubkey); + if target == num_id { + return Err(anyhow!("authorize: num has no separate delegation - call delegate first")) + } + + let dk = DelegatorKey::from_id::(target); + let Some(delegator) = chain.get_delegator(&dk)? else { + return Err(anyhow!("authorize: num {} is not delegated - call delegate first", subject)); + }; + if !delegator.is_numeric() { + return Err(anyhow!("authorize: num {} is delegated to {} - call delegate to switch", + subject, delegator)); + } + let numeric : SNumeric = delegator.clone().try_into().expect("valid numeric"); + + if numeric != num_utxo.numout.num.name { + return Err(anyhow!("authorize: num {} is delegated to {} - call delegate to switch", + subject, delegator)); + } + + target + } else { + let Subject::Label(label) = subject else { + return Err(anyhow!("authorize: expected a space, got {}", subject)) + }; + + let space_utxo = chain + .get_space_info(&SpaceKey::from(Sha256::hash(label.as_ref())))? + .ok_or_else(|| anyhow!("authorize: space '{}' not found", label))?; + let Some(space) = space_utxo.spaceout.space else { + return Err(anyhow!("authorize: space {} not found", subject)); + }; + + let target = NumId::from_spk::(space_utxo.spaceout.script_pubkey); + let dk = DelegatorKey::from_id::(target); + let Some(delegator) = chain.get_delegator(&dk)? else { + return Err(anyhow!("authorize: space {} is not delegated - call delegate first", subject)); + }; + + if delegator != space.name { + return Err(anyhow!("authorize: num {} is delegated to {} - call delegate to switch", + target, delegator) + ); + } + + target + }; + + let Some(num_utxo) = chain.get_num_info(&target)? else { + return Err(anyhow!("authorize: target '{}' not found - call delegate first", target)); + }; + + Ok(num_utxo) +} diff --git a/client/tests/integration_tests.rs b/client/tests/integration_tests.rs index 3ed54cc..48f8fae 100644 --- a/client/tests/integration_tests.rs +++ b/client/tests/integration_tests.rs @@ -14,7 +14,7 @@ use spaces_protocol::{ slabel::SLabel, Bytes, Covenant, }; -use spaces_ptr::ChainProofRequest; +use spaces_nums::ChainProofRequest; use spaces_testutil::TestRig; use spaces_wallet::{export::WalletExport, tx_event::TxEventKind}; @@ -377,7 +377,7 @@ async fn it_should_allow_batch_transfers_refreshing_expire_height( .iter() .map(|out| { let name = out.spaceout.space.as_ref().expect("space").name.to_string(); - Subject::Space(SLabel::from_str(&name).expect("valid space")) + Subject::Label(SLabel::from_str(&name).expect("valid space")) }) .collect(); @@ -448,7 +448,7 @@ async fn it_should_allow_applying_script_in_batch(rig: &TestRig) -> anyhow::Resu .iter() .map(|out| { let name = out.spaceout.space.as_ref().expect("space").name.to_string(); - Subject::Space(SLabel::from_str(&name).expect("valid space")) + Subject::Label(SLabel::from_str(&name).expect("valid space")) }) .collect(); @@ -869,7 +869,7 @@ async fn it_should_not_allow_register_or_transfer_to_same_space_multiple_times( rig, ALICE, vec![RpcWalletRequest::Transfer(TransferSpacesParams { - spaces: vec![Subject::Space(SLabel::from_str(&transfer).expect("valid"))], + spaces: vec![Subject::Label(SLabel::from_str(&transfer).expect("valid"))], to: Some(bob_address.clone()), data: None, })], @@ -887,7 +887,7 @@ async fn it_should_not_allow_register_or_transfer_to_same_space_multiple_times( rig, ALICE, vec![RpcWalletRequest::Transfer(TransferSpacesParams { - spaces: vec![Subject::Space(SLabel::from_str(&transfer).expect("valid"))], + spaces: vec![Subject::Label(SLabel::from_str(&transfer).expect("valid"))], to: Some(bob_address), data: None, })], @@ -901,7 +901,7 @@ async fn it_should_not_allow_register_or_transfer_to_same_space_multiple_times( rig, ALICE, vec![RpcWalletRequest::Transfer(TransferSpacesParams { - spaces: vec![Subject::Space(SLabel::from_str(&setdata).expect("valid"))], + spaces: vec![Subject::Label(SLabel::from_str(&setdata).expect("valid"))], to: None, data: Some(vec![0xAA, 0xAA]), })], @@ -918,7 +918,7 @@ async fn it_should_not_allow_register_or_transfer_to_same_space_multiple_times( rig, ALICE, vec![RpcWalletRequest::Transfer(TransferSpacesParams { - spaces: vec![Subject::Space(SLabel::from_str(&setdata).expect("valid"))], + spaces: vec![Subject::Label(SLabel::from_str(&setdata).expect("valid"))], to: None, data: Some(vec![0xDE, 0xAD]), })], @@ -969,7 +969,7 @@ async fn it_can_batch_txs(rig: &TestRig) -> anyhow::Result<()> { ALICE, vec![ RpcWalletRequest::Transfer(TransferSpacesParams { - spaces: vec![Subject::Space(SLabel::from_str("@test9996").expect("valid"))], + spaces: vec![Subject::Label(SLabel::from_str("@test9996").expect("valid"))], to: Some(bob_address), data: None, }), @@ -988,9 +988,9 @@ async fn it_can_batch_txs(rig: &TestRig) -> anyhow::Result<()> { // Transfer spaces to self with data (replaces Execute) RpcWalletRequest::Transfer(TransferSpacesParams { spaces: vec![ - Subject::Space(SLabel::from_str("@test10000").expect("valid")), - Subject::Space(SLabel::from_str("@test9999").expect("valid")), - Subject::Space(SLabel::from_str("@test9998").expect("valid")), + Subject::Label(SLabel::from_str("@test10000").expect("valid")), + Subject::Label(SLabel::from_str("@test9999").expect("valid")), + Subject::Label(SLabel::from_str("@test9998").expect("valid")), ], to: None, data: Some(vec![0xEE, 0xEE, 0x22, 0x22]), @@ -1251,7 +1251,7 @@ async fn it_should_handle_expired_spaces(rig: &TestRig) -> anyhow::Result<()> { rig, ALICE, vec![RpcWalletRequest::Transfer(TransferSpacesParams { - spaces: vec![Subject::Space( + spaces: vec![Subject::Label( SLabel::from_str(&space_name).expect("valid") )], to: None, // renew to self @@ -1328,7 +1328,7 @@ async fn it_should_handle_expired_spaces(rig: &TestRig) -> anyhow::Result<()> { rig, ALICE, vec![RpcWalletRequest::Transfer(TransferSpacesParams { - spaces: vec![Subject::Space( + spaces: vec![Subject::Label( SLabel::from_str(&space_name_2).expect("valid") )], to: None, @@ -1390,7 +1390,7 @@ async fn it_should_sign_and_verify_schnorr(rig: &TestRig) -> anyhow::Result<()> let spaces = rig.spaced.client.wallet_list_spaces(BOB).await?; let space = spaces.owned.first().expect("bob should have at least 1 space"); let space_name = space.spaceout.space.as_ref().unwrap().name.to_string(); - let subject = Subject::Space(SLabel::from_str(&space_name).unwrap()); + let subject = Subject::Label(SLabel::from_str(&space_name).unwrap()); let message = Bytes::new(b"hello world".to_vec()); let signature = rig @@ -1451,7 +1451,7 @@ async fn it_should_build_chain_proof_with_snapshot_caching(rig: &TestRig) -> any .build_chain_proof( ChainProofRequest { spaces: vec![label.clone()], - ptrs_keys: vec![], + nums: vec![], }, Some(false), ) @@ -1483,7 +1483,7 @@ async fn it_should_build_chain_proof_with_snapshot_caching(rig: &TestRig) -> any .build_chain_proof( ChainProofRequest { spaces: vec![label.clone()], - ptrs_keys: vec![], + nums: vec![], }, Some(false), ) diff --git a/client/tests/ptr_tests.rs b/client/tests/ptr_tests.rs index 8424faf..24ff992 100644 --- a/client/tests/ptr_tests.rs +++ b/client/tests/ptr_tests.rs @@ -7,11 +7,11 @@ use spaces_client::{ }, wallets::{AddressKind, WalletResponse}, }; -use spaces_client::rpc::{CommitParams, CreatePtrParams, DelegateParams, SetFallbackParams, Subject, TransferSpacesParams}; +use spaces_client::rpc::{CommitParams, CreateNumParams, DelegateParams, SetFallbackParams, Subject, TransferSpacesParams}; use spaces_client::store::Sha256; use spaces_protocol::{bitcoin, bitcoin::{FeeRate}}; use spaces_protocol::bitcoin::hashes::{sha256, Hash}; -use spaces_ptr::sptr::Sptr; +use spaces_nums::num_id::NumId; use spaces_testutil::TestRig; use spaces_wallet::{export::WalletExport}; use spaces_wallet::address::SpaceAddress; @@ -80,9 +80,9 @@ async fn mine_and_sync(rig: &TestRig, blocks: usize) -> anyhow::Result<()> { sync_all(rig).await } -// ============== Test: Basic SPTR Creation ============== +// ============== Test: Basic Num Id Creation ============== -async fn it_should_create_sptrs(rig: &TestRig) -> anyhow::Result<()> { +async fn it_should_create_nums(rig: &TestRig) -> anyhow::Result<()> { rig.wait_until_wallet_synced(ALICE).await?; // Create ptr bound to addr0 @@ -90,12 +90,10 @@ async fn it_should_create_sptrs(rig: &TestRig) -> anyhow::Result<()> { let addr0_spk = bitcoin::address::Address::from_str(&addr0) .expect("valid").assume_checked() .script_pubkey(); - let addr0_spk_string = hex::encode(addr0_spk.as_bytes()); - let create0 = wallet_do( rig, ALICE, - vec![RpcWalletRequest::CreatePtr(CreatePtrParams { spk: addr0_spk_string.clone() })], + vec![RpcWalletRequest::CreateNum(CreateNumParams { bind_spk: Some(addr0_spk.clone()) })], false, ).await.expect("CreatePtr addr0"); assert!(wallet_res_err(&create0).is_ok(), "CreatePtr(addr0) must not error"); @@ -106,11 +104,11 @@ async fn it_should_create_sptrs(rig: &TestRig) -> anyhow::Result<()> { .expect("valid addr0") .assume_checked() .script_pubkey(); - let sptr0 = Sptr::from_spk::(spk0.clone()); + let id0 = NumId::from_spk::(spk0.clone()); - let ptr0 = rig.spaced.client.get_ptr(Subject::Ptr(sptr0)).await? + let ptr0 = rig.spaced.client.get_num(Subject::NumId(id0)).await? .expect("ptr must exist after first CreatePtr"); - let bound_spk_before = ptr0.ptrout.script_pubkey.clone(); + let bound_spk_before = ptr0.numout.script_pubkey.clone(); // Transfer ptr to addr1 (binding should change) let addr1 = rig.spaced.client.wallet_get_new_address(BOB, AddressKind::Space).await?; @@ -118,7 +116,7 @@ async fn it_should_create_sptrs(rig: &TestRig) -> anyhow::Result<()> { rig, ALICE, vec![RpcWalletRequest::Transfer(TransferSpacesParams { - spaces: vec![Subject::Ptr(sptr0)], + spaces: vec![Subject::NumId(id0)], to: Some(addr1.clone()), data: None, })], @@ -132,9 +130,9 @@ async fn it_should_create_sptrs(rig: &TestRig) -> anyhow::Result<()> { .expect("valid addr1") .script_pubkey(); - let ptr_after_xfer = rig.spaced.client.get_ptr(Subject::Ptr(sptr0)).await? + let ptr_after_xfer = rig.spaced.client.get_num(Subject::NumId(id0)).await? .expect("ptr must still resolve after transfer"); - let bound_spk_after = ptr_after_xfer.ptrout.script_pubkey.clone(); + let bound_spk_after = ptr_after_xfer.numout.script_pubkey.clone(); assert_ne!(bound_spk_before, bound_spk_after, "binding must change after transfer"); assert_eq!(bound_spk_after, spk1, "binding must equal new destination spk"); @@ -143,16 +141,16 @@ async fn it_should_create_sptrs(rig: &TestRig) -> anyhow::Result<()> { let dup = wallet_do( rig, ALICE, - vec![RpcWalletRequest::CreatePtr(CreatePtrParams { spk: addr0_spk_string })], + vec![RpcWalletRequest::CreateNum(CreateNumParams { bind_spk: Some(addr0_spk.clone()) })], true, ).await.expect("duplicate CreatePtr(addr0)"); assert!(wallet_res_err(&dup).is_ok(), "duplicate CreatePtr should not error"); mine_and_sync(rig, 1).await?; - let ptr_after_dup = rig.spaced.client.get_ptr(Subject::Ptr(sptr0)).await? + let ptr_after_dup = rig.spaced.client.get_num(Subject::NumId(id0)).await? .expect("ptr must still resolve after duplicate"); - let bound_spk_final = ptr_after_dup.ptrout.script_pubkey.clone(); + let bound_spk_final = ptr_after_dup.numout.script_pubkey.clone(); assert_eq!(bound_spk_final, spk1, "duplicate CreatePtr must be ignored"); assert_ne!(bound_spk_final, spk0, "binding must not revert to original"); @@ -172,12 +170,12 @@ async fn it_should_commit_and_rollback(rig: &TestRig) -> anyhow::Result<()> { let space_name = owned.spaceout.space.as_ref() .expect("space must exist").name.clone(); - // Setup: Delegate the space to establish SPTR + // Setup: Delegate the space to establish Num Id let delegate = wallet_do( rig, ALICE, vec![RpcWalletRequest::Delegate(DelegateParams { - space: space_name.clone(), + subject: space_name.clone().into(), })], false, ).await?; @@ -185,7 +183,7 @@ async fn it_should_commit_and_rollback(rig: &TestRig) -> anyhow::Result<()> { mine_and_sync(rig, 1).await?; // Verify delegation is set up - rig.spaced.client.get_delegation(space_name.clone()).await? + rig.spaced.client.get_delegation(space_name.clone().into()).await? .expect("delegation should be established"); // Test 1: Make initial commitment [1u8;32] @@ -194,7 +192,7 @@ async fn it_should_commit_and_rollback(rig: &TestRig) -> anyhow::Result<()> { rig, ALICE, vec![RpcWalletRequest::Commit(CommitParams { - space: space_name.clone(), + subject: space_name.clone().into(), root: Some(sha256::Hash::from_slice(&[1u8;32]).expect("valid")), })], false, @@ -202,7 +200,7 @@ async fn it_should_commit_and_rollback(rig: &TestRig) -> anyhow::Result<()> { assert!(wallet_res_err(&commit1).is_ok()); mine_and_sync(rig, 1).await?; - let tip = rig.spaced.client.get_commitment(space_name.clone(), None).await? + let tip = rig.spaced.client.get_commitment(space_name.clone().into(), None).await? .expect("commitment should exist"); assert_eq!(tip.state_root, [1u8;32]); assert_eq!(tip.prev_root, None); @@ -213,7 +211,7 @@ async fn it_should_commit_and_rollback(rig: &TestRig) -> anyhow::Result<()> { rig, ALICE, vec![RpcWalletRequest::Commit(CommitParams { - space: space_name.clone(), + subject: space_name.clone().into(), root: None, // None = rollback })], false, @@ -221,7 +219,7 @@ async fn it_should_commit_and_rollback(rig: &TestRig) -> anyhow::Result<()> { assert!(wallet_res_err(&rollback).is_ok()); mine_and_sync(rig, 1).await?; - let tip_after_rollback = rig.spaced.client.get_commitment(space_name.clone(), None).await?; + let tip_after_rollback = rig.spaced.client.get_commitment(space_name.clone().into(), None).await?; assert_eq!(tip_after_rollback, None, "commitment should be rolled back"); // Test 3: Create new commitment and finalize it @@ -230,7 +228,7 @@ async fn it_should_commit_and_rollback(rig: &TestRig) -> anyhow::Result<()> { rig, ALICE, vec![RpcWalletRequest::Commit(CommitParams { - space: space_name.clone(), + subject: space_name.clone().into(), root: Some(sha256::Hash::from_slice(&[2u8;32]).expect("valid")), })], false, @@ -248,7 +246,7 @@ async fn it_should_commit_and_rollback(rig: &TestRig) -> anyhow::Result<()> { rig, ALICE, vec![RpcWalletRequest::Commit(CommitParams { - space: space_name.clone(), + subject: space_name.clone().into(), root: None, // Rollback attempt })], false, @@ -256,7 +254,7 @@ async fn it_should_commit_and_rollback(rig: &TestRig) -> anyhow::Result<()> { assert!(wallet_res_err(&rollback_finalized).is_ok()); mine_and_sync(rig, 1).await?; - let tip_after_failed_rollback = rig.spaced.client.get_commitment(space_name.clone(), None).await? + let tip_after_failed_rollback = rig.spaced.client.get_commitment(space_name.clone().into(), None).await? .expect("finalized commitment should still exist"); assert_eq!(tip_after_failed_rollback.state_root, [2u8;32], "finalized commitment should not be rolled back"); @@ -267,7 +265,7 @@ async fn it_should_commit_and_rollback(rig: &TestRig) -> anyhow::Result<()> { rig, ALICE, vec![RpcWalletRequest::Commit(CommitParams { - space: space_name.clone(), + subject: space_name.clone().into(), root: Some(sha256::Hash::from_slice(&[3u8;32]).expect("valid")), })], false, @@ -275,14 +273,14 @@ async fn it_should_commit_and_rollback(rig: &TestRig) -> anyhow::Result<()> { assert!(wallet_res_err(&commit3).is_ok()); mine_and_sync(rig, 1).await?; - let tip_final = rig.spaced.client.get_commitment(space_name.clone(), None).await? + let tip_final = rig.spaced.client.get_commitment(space_name.clone().into(), None).await? .expect("new commitment should exist"); assert_eq!(tip_final.state_root, [3u8;32]); assert_eq!(tip_final.prev_root, Some([2u8;32])); // Verify finalized [2u8;32] still exists let finalized = rig.spaced.client.get_commitment( - space_name.clone(), + space_name.clone().into(), Some(sha256::Hash::from_slice(&[2u8;32]).expect("valid")) ).await?.expect("finalized commitment should be preserved"); assert_eq!(finalized.state_root, [2u8;32]); @@ -293,7 +291,7 @@ async fn it_should_commit_and_rollback(rig: &TestRig) -> anyhow::Result<()> { rig, ALICE, vec![RpcWalletRequest::Commit(CommitParams { - space: space_name.clone(), + subject: space_name.clone().into(), root: None, })], false, @@ -301,7 +299,7 @@ async fn it_should_commit_and_rollback(rig: &TestRig) -> anyhow::Result<()> { assert!(wallet_res_err(&rollback3).is_ok()); mine_and_sync(rig, 1).await?; - let tip_after_rollback = rig.spaced.client.get_commitment(space_name.clone(), None).await? + let tip_after_rollback = rig.spaced.client.get_commitment(space_name.clone().into(), None).await? .expect("should still have finalized commitment after rollback"); assert_eq!(tip_after_rollback.state_root, [2u8;32], "registry should point back to finalized [2u8;32] after rolling back pending"); @@ -324,13 +322,13 @@ async fn it_should_handle_multiple_commitments(rig: &TestRig) -> anyhow::Result< let space2_name = alice_spaces.owned[1].spaceout.space.as_ref() .expect("space must exist").name.clone(); - // Setup: Delegate both spaces to establish SPTRs + // Setup: Delegate both spaces to establish Num Ids for space_name in [&space1_name, &space2_name] { let delegate = wallet_do( rig, ALICE, vec![RpcWalletRequest::Delegate(DelegateParams { - space: space_name.clone(), + subject: space_name.clone().into(), })], false, ).await?; @@ -339,19 +337,19 @@ async fn it_should_handle_multiple_commitments(rig: &TestRig) -> anyhow::Result< mine_and_sync(rig, 1).await?; // Verify both delegations exist - let sptr1 = rig.spaced.client.get_delegation(space1_name.clone()).await? + let id1 = rig.spaced.client.get_delegation(space1_name.clone().into()).await? .expect("space1 should have delegation"); - let sptr2 = rig.spaced.client.get_delegation(space2_name.clone()).await? + let id2 = rig.spaced.client.get_delegation(space2_name.clone().into()).await? .expect("space2 should have delegation"); - println!("Space 1: {} -> SPTR: {}", space1_name, sptr1); - println!("Space 2: {} -> SPTR: {}", space2_name, sptr2); + println!("Space 1: {} -> Num Id: {}", space1_name, id1); + println!("Space 2: {} -> Num Id: {}", space2_name, id2); // Verify delegations match delegator - let delegator1 = rig.spaced.client.get_delegator(Subject::Ptr(sptr1)).await? - .expect("sptr1 delegator should exist"); - let delegator2 = rig.spaced.client.get_delegator(Subject::Ptr(sptr2)).await? - .expect("sptr2 delegator should exist"); + let delegator1 = rig.spaced.client.get_delegator(Subject::NumId(id1)).await? + .expect("id1 delegator should exist"); + let delegator2 = rig.spaced.client.get_delegator(Subject::NumId(id2)).await? + .expect("id2 delegator should exist"); assert_eq!(delegator1.to_string(), space1_name.to_string(), "space 1 delegators dont match"); assert_eq!(delegator2.to_string(), space2_name.to_string(), "space 2 delegators dont match"); @@ -364,11 +362,11 @@ async fn it_should_handle_multiple_commitments(rig: &TestRig) -> anyhow::Result< ALICE, vec![ RpcWalletRequest::Commit(CommitParams { - space: space1_name.clone(), + subject: space1_name.clone().into(), root: Some(sha256::Hash::from_slice(&[10u8;32]).expect("valid")), }), RpcWalletRequest::Commit(CommitParams { - space: space2_name.clone(), + subject: space2_name.clone().into(), root: Some(sha256::Hash::from_slice(&[20u8;32]).expect("valid")), }), ], @@ -378,9 +376,9 @@ async fn it_should_handle_multiple_commitments(rig: &TestRig) -> anyhow::Result< mine_and_sync(rig, 1).await?; // Verify both commitments were created - let commit2 = rig.spaced.client.get_commitment(space2_name.clone(), None).await? + let commit2 = rig.spaced.client.get_commitment(space2_name.clone().into(), None).await? .expect("space2 should have commitment"); - let commit1 = rig.spaced.client.get_commitment(space1_name.clone(), None).await? + let commit1 = rig.spaced.client.get_commitment(space1_name.clone().into(), None).await? .expect("space1 should have commitment"); @@ -394,11 +392,11 @@ async fn it_should_handle_multiple_commitments(rig: &TestRig) -> anyhow::Result< ALICE, vec![ RpcWalletRequest::Commit(CommitParams { - space: space1_name.clone(), + subject: space1_name.clone().into(), root: None, // rollback }), RpcWalletRequest::Commit(CommitParams { - space: space2_name.clone(), + subject: space2_name.clone().into(), root: None, // rollback }), ], @@ -408,8 +406,8 @@ async fn it_should_handle_multiple_commitments(rig: &TestRig) -> anyhow::Result< mine_and_sync(rig, 1).await?; // Verify both were rolled back - let commit1_after = rig.spaced.client.get_commitment(space1_name.clone(), None).await?; - let commit2_after = rig.spaced.client.get_commitment(space2_name.clone(), None).await?; + let commit1_after = rig.spaced.client.get_commitment(space1_name.clone().into(), None).await?; + let commit2_after = rig.spaced.client.get_commitment(space2_name.clone().into(), None).await?; assert_eq!(commit1_after, None, "space1 should be rolled back"); assert_eq!(commit2_after, None, "space2 should be rolled back"); @@ -421,7 +419,7 @@ async fn it_should_handle_multiple_commitments(rig: &TestRig) -> anyhow::Result< ALICE, vec![ RpcWalletRequest::Commit(CommitParams { - space: space1_name.clone(), + subject: space1_name.clone().into(), root: Some(sha256::Hash::from_slice(&[30u8;32]).expect("valid")), }), ], @@ -430,9 +428,9 @@ async fn it_should_handle_multiple_commitments(rig: &TestRig) -> anyhow::Result< assert!(wallet_res_err(&mixed).is_ok()); mine_and_sync(rig, 1).await?; - let commit1_final = rig.spaced.client.get_commitment(space1_name.clone(), None).await? + let commit1_final = rig.spaced.client.get_commitment(space1_name.clone().into(), None).await? .expect("space1 should have new commitment"); - let commit2_final = rig.spaced.client.get_commitment(space2_name.clone(), None).await?; + let commit2_final = rig.spaced.client.get_commitment(space2_name.clone().into(), None).await?; assert_eq!(commit1_final.state_root, [30u8;32], "space1 updated"); assert_eq!(commit2_final, None, "space2 unchanged"); @@ -449,12 +447,12 @@ async fn it_should_override_pending_commitments(rig: &TestRig) -> anyhow::Result let space_name = alice_spaces.owned[0].spaceout.space.as_ref() .expect("space must exist").name.clone(); - // Setup: Delegate the space to establish SPTR + // Setup: Delegate the space to establish Num Id let delegate = wallet_do( rig, ALICE, vec![RpcWalletRequest::Delegate(DelegateParams { - space: space_name.clone(), + subject: space_name.clone().into(), })], false, ).await?; @@ -467,7 +465,7 @@ async fn it_should_override_pending_commitments(rig: &TestRig) -> anyhow::Result rig, ALICE, vec![RpcWalletRequest::Commit(CommitParams { - space: space_name.clone(), + subject: space_name.clone().into(), root: Some(sha256::Hash::from_slice(&[1u8;32]).expect("valid")), })], false, @@ -481,7 +479,7 @@ async fn it_should_override_pending_commitments(rig: &TestRig) -> anyhow::Result rig, ALICE, vec![RpcWalletRequest::Commit(CommitParams { - space: space_name.clone(), + subject: space_name.clone().into(), root: Some(sha256::Hash::from_slice(&[2u8;32]).expect("valid")), })], false, @@ -491,12 +489,12 @@ async fn it_should_override_pending_commitments(rig: &TestRig) -> anyhow::Result // Verify [1u8;32] is gone, [2u8;32] is tip let old_commit = rig.spaced.client.get_commitment( - space_name.clone(), + space_name.clone().into(), Some(sha256::Hash::from_slice(&[1u8;32]).expect("valid")) ).await?; assert_eq!(old_commit, None, "[1u8;32] should be overridden"); - let tip = rig.spaced.client.get_commitment(space_name.clone(), None).await? + let tip = rig.spaced.client.get_commitment(space_name.clone().into(), None).await? .expect("tip should exist"); assert_eq!(tip.state_root, [2u8;32]); assert_eq!(tip.prev_root, None, "no previous since [1u8;32] was overridden"); @@ -511,7 +509,7 @@ async fn it_should_override_pending_commitments(rig: &TestRig) -> anyhow::Result rig, ALICE, vec![RpcWalletRequest::Commit(CommitParams { - space: space_name.clone(), + subject: space_name.clone().into(), root: Some(sha256::Hash::from_slice(&[3u8;32]).expect("valid")), })], false, @@ -521,12 +519,12 @@ async fn it_should_override_pending_commitments(rig: &TestRig) -> anyhow::Result // Verify [2u8;32] still exists and [3u8;32] chains from it let finalized = rig.spaced.client.get_commitment( - space_name.clone(), + space_name.clone().into(), Some(sha256::Hash::from_slice(&[2u8;32]).expect("valid")) ).await?.expect("[2u8;32] should still exist"); assert_eq!(finalized.state_root, [2u8;32]); - let new_tip = rig.spaced.client.get_commitment(space_name.clone(), None).await? + let new_tip = rig.spaced.client.get_commitment(space_name.clone().into(), None).await? .expect("new tip should exist"); assert_eq!(new_tip.state_root, [3u8;32]); assert_eq!(new_tip.prev_root, Some([2u8;32])); @@ -534,7 +532,7 @@ async fn it_should_override_pending_commitments(rig: &TestRig) -> anyhow::Result Ok(()) } -async fn it_should_reject_duplicate_sptr_delegations(rig: &TestRig) -> anyhow::Result<()> { +async fn it_should_reject_duplicate_num_id_delegations(rig: &TestRig) -> anyhow::Result<()> { sync_all(rig).await?; // Get two spaces that Alice owns @@ -546,17 +544,17 @@ async fn it_should_reject_duplicate_sptr_delegations(rig: &TestRig) -> anyhow::R let space2_name = alice_spaces.owned[1].spaceout.space.as_ref() .expect("space must exist").name.clone(); - println!("Testing SPTR uniqueness with {} and {}", space1_name, space2_name); + println!("Testing Num Id uniqueness with {} and {}", space1_name, space2_name); - // Get a common address to create the same SPTR + // Get a common address to create the same Num Id let common_addr = rig.spaced.client.wallet_get_new_address(ALICE, AddressKind::Space).await?; let common_spk = SpaceAddress::from_str(&common_addr) .expect("valid space address") .script_pubkey(); - let common_sptr = Sptr::from_spk::(common_spk.clone()); + let common_id = NumId::from_spk::(common_spk.clone()); println!("Common address: {}", common_addr); - println!("Expected SPTR: {}", common_sptr); + println!("Expected Num Id: {}", common_id); // Transfer space1 to the common address println!("Transferring {} to common address...", space1_name); @@ -564,7 +562,7 @@ async fn it_should_reject_duplicate_sptr_delegations(rig: &TestRig) -> anyhow::R rig, ALICE, vec![RpcWalletRequest::Transfer(TransferSpacesParams { - spaces: vec![Subject::Space(space1_name.clone())], + spaces: vec![Subject::Label(space1_name.clone())], to: Some(common_addr.clone()), data: None, })], @@ -573,21 +571,21 @@ async fn it_should_reject_duplicate_sptr_delegations(rig: &TestRig) -> anyhow::R assert!(wallet_res_err(&transfer1).is_ok()); mine_and_sync(rig, 1).await?; - // Verify the reverse mapping: SPTR -> space1 - let delegator1 = rig.spaced.client.get_delegator(Subject::Ptr(common_sptr)).await? - .expect("common SPTR should have delegator"); - assert_eq!(delegator1, space1_name, "common SPTR should point to space1"); + // Verify the reverse mapping: Num Id -> space1 + let delegator1 = rig.spaced.client.get_delegator(Subject::NumId(common_id)).await? + .expect("common Num Id should have delegator"); + assert_eq!(delegator1, space1_name, "common Num Id should point to space1"); - println!("✓ Space1 successfully claimed SPTR {} (reverse mapping: {} -> {})", - common_sptr, common_sptr, space1_name); + println!("✓ Space1 successfully claimed Num Id {} (reverse mapping: {} -> {})", + common_id, common_id, space1_name); - // Transfer space2 to the SAME address (same SPTR) - println!("Transferring {} to the same address (attempting to claim same SPTR)...", space2_name); + // Transfer space2 to the SAME address (same Num Id) + println!("Transferring {} to the same address (attempting to claim same Num Id)...", space2_name); let transfer2 = wallet_do( rig, ALICE, vec![RpcWalletRequest::Transfer(TransferSpacesParams { - spaces: vec![Subject::Space(space2_name.clone())], + spaces: vec![Subject::Label(space2_name.clone())], to: Some(common_addr.clone()), data: None, })], @@ -597,26 +595,26 @@ async fn it_should_reject_duplicate_sptr_delegations(rig: &TestRig) -> anyhow::R mine_and_sync(rig, 1).await?; // Key test: Verify the reverse mapping was NOT overwritten - // The SPTR should still point to space1, not space2 - let delegator_after = rig.spaced.client.get_delegator(Subject::Ptr(common_sptr)).await? - .expect("common SPTR should still have delegator"); + // The Num Id should still point to space1, not space2 + let delegator_after = rig.spaced.client.get_delegator(Subject::NumId(common_id)).await? + .expect("common Num Id should still have delegator"); assert_eq!(delegator_after, space1_name, - "CRITICAL: common SPTR should still point to space1 (not overwritten by space2)"); + "CRITICAL: common Num Id should still point to space1 (not overwritten by space2)"); println!("✓ Space2 correctly rejected - reverse mapping preserved ({} -> {})", - common_sptr, space1_name); + common_id, space1_name); - // Note: get_delegation for both spaces will return Some(common_sptr) because + // Note: get_delegation for both spaces will return Some(common_id) because // both are at the same address, but only space1 actually owns the delegation - // Transfer space1 away to free up the SPTR - println!("Moving {} away to free up SPTR...", space1_name); + // Transfer space1 away to free up the Num Id + println!("Moving {} away to free up Num Id...", space1_name); let new_addr = rig.spaced.client.wallet_get_new_address(ALICE, AddressKind::Space).await?; let transfer_away = wallet_do( rig, ALICE, vec![RpcWalletRequest::Transfer(TransferSpacesParams { - spaces: vec![Subject::Space(space1_name.clone())], + spaces: vec![Subject::Label(space1_name.clone())], to: Some(new_addr), data: None, })], @@ -625,19 +623,19 @@ async fn it_should_reject_duplicate_sptr_delegations(rig: &TestRig) -> anyhow::R assert!(wallet_res_err(&transfer_away).is_ok()); mine_and_sync(rig, 1).await?; - // Verify common_sptr is now free (reverse mapping removed) - let delegator_freed = rig.spaced.client.get_delegator(Subject::Ptr(common_sptr)).await?; - assert_eq!(delegator_freed, None, "common SPTR should be free (no reverse mapping) after space1 moved"); + // Verify common_id is now free (reverse mapping removed) + let delegator_freed = rig.spaced.client.get_delegator(Subject::NumId(common_id)).await?; + assert_eq!(delegator_freed, None, "common Num Id should be free (no reverse mapping) after space1 moved"); - println!("✓ SPTR freed - reverse mapping removed"); + println!("✓ Num Id freed - reverse mapping removed"); // Now space2 should be able to claim it if we transfer it back - println!("Re-transferring {} to now-free SPTR...", space2_name); + println!("Re-transferring {} to now-free Num Id...", space2_name); let transfer2_retry = wallet_do( rig, ALICE, vec![RpcWalletRequest::Transfer(TransferSpacesParams { - spaces: vec![Subject::Space(space2_name.clone())], + spaces: vec![Subject::Label(space2_name.clone())], to: Some(common_addr), data: None, })], @@ -647,18 +645,18 @@ async fn it_should_reject_duplicate_sptr_delegations(rig: &TestRig) -> anyhow::R mine_and_sync(rig, 1).await?; // Verify space2 now owns the reverse mapping - let delegator2_retry = rig.spaced.client.get_delegator(Subject::Ptr(common_sptr)).await? - .expect("common SPTR should have delegator"); + let delegator2_retry = rig.spaced.client.get_delegator(Subject::NumId(common_id)).await? + .expect("common Num Id should have delegator"); assert_eq!(delegator2_retry, space2_name, - "common SPTR should now point to space2 (reverse mapping updated)"); + "common Num Id should now point to space2 (reverse mapping updated)"); - println!("✓ Space2 successfully claimed SPTR after it was freed ({} -> {})", - common_sptr, space2_name); + println!("✓ Space2 successfully claimed Num Id after it was freed ({} -> {})", + common_id, space2_name); Ok(()) } -// ============== Test: Transfer Back to Original SPTR ============== +// ============== Test: Transfer Back to Original Num Id ============== // Regression test for https://github.com/spacesprotocol/spaces/issues/134 async fn it_should_restore_delegation_when_transferring_back(rig: &TestRig) -> anyhow::Result<()> { @@ -669,7 +667,7 @@ async fn it_should_restore_delegation_when_transferring_back(rig: &TestRig) -> a let original_spk = SpaceAddress::from_str(&original_addr) .expect("valid space address") .script_pubkey(); - let original_sptr = Sptr::from_spk::(original_spk.clone()); + let original_id = NumId::from_spk::(original_spk.clone()); // Get a space that Alice owns let alice_spaces = rig.spaced.client.wallet_list_spaces(ALICE).await?; @@ -688,7 +686,7 @@ async fn it_should_restore_delegation_when_transferring_back(rig: &TestRig) -> a rig, ALICE, vec![RpcWalletRequest::Transfer(TransferSpacesParams { - spaces: vec![Subject::Space(space_name.clone())], + spaces: vec![Subject::Label(space_name.clone())], to: Some(original_addr.clone()), data: None, })], @@ -698,26 +696,26 @@ async fn it_should_restore_delegation_when_transferring_back(rig: &TestRig) -> a mine_and_sync(rig, 1).await?; // Verify initial delegation is established - let initial_delegator = rig.spaced.client.get_delegator(Subject::Ptr(original_sptr)).await? - .expect("Original SPTR should have delegation after setup"); + let initial_delegator = rig.spaced.client.get_delegator(Subject::NumId(original_id)).await? + .expect("Original Num Id should have delegation after setup"); assert_eq!(initial_delegator, space_name); - println!("✓ Initial delegation established: {} -> {}", original_sptr, space_name); + println!("✓ Initial delegation established: {} -> {}", original_id, space_name); - // Step 2: Transfer space to a NEW address (different SPTR) + // Step 2: Transfer space to a NEW address (different Num Id) let new_addr = rig.spaced.client.wallet_get_new_address(ALICE, AddressKind::Space).await?; let new_spk = SpaceAddress::from_str(&new_addr) .expect("valid space address") .script_pubkey(); - let new_sptr = Sptr::from_spk::(new_spk.clone()); + let new_id = NumId::from_spk::(new_spk.clone()); println!("\nStep 2: Transferring {} to new address...", space_name); - println!("New SPTR will be: {}", new_sptr); + println!("New Num Id will be: {}", new_id); let transfer1 = wallet_do( rig, ALICE, vec![RpcWalletRequest::Transfer(TransferSpacesParams { - spaces: vec![Subject::Space(space_name.clone())], + spaces: vec![Subject::Label(space_name.clone())], to: Some(new_addr.clone()), data: None, })], @@ -726,18 +724,18 @@ async fn it_should_restore_delegation_when_transferring_back(rig: &TestRig) -> a assert!(wallet_res_err(&transfer1).is_ok()); mine_and_sync(rig, 1).await?; - // Verify: original SPTR should have NO delegation now - let delegator_after_transfer1 = rig.spaced.client.get_delegator(Subject::Ptr(original_sptr)).await?; + // Verify: original Num Id should have NO delegation now + let delegator_after_transfer1 = rig.spaced.client.get_delegator(Subject::NumId(original_id)).await?; assert_eq!(delegator_after_transfer1, None, - "Original SPTR should have no delegation after space was transferred away"); - println!("✓ Original SPTR delegation revoked: {:?}", delegator_after_transfer1); + "Original Num Id should have no delegation after space was transferred away"); + println!("✓ Original Num Id delegation revoked: {:?}", delegator_after_transfer1); - // Verify: new SPTR should have the delegation - let delegator_new = rig.spaced.client.get_delegator(Subject::Ptr(new_sptr)).await? - .expect("New SPTR should have delegation"); + // Verify: new Num Id should have the delegation + let delegator_new = rig.spaced.client.get_delegator(Subject::NumId(new_id)).await? + .expect("New Num Id should have delegation"); assert_eq!(delegator_new, space_name, - "New SPTR should point to the space"); - println!("✓ New SPTR has delegation: {} -> {}", new_sptr, delegator_new); + "New Num Id should point to the space"); + println!("✓ New Num Id has delegation: {} -> {}", new_id, delegator_new); // Step 3: Transfer space BACK to original address println!("\nStep 3: Transferring {} BACK to original address...", space_name); @@ -747,7 +745,7 @@ async fn it_should_restore_delegation_when_transferring_back(rig: &TestRig) -> a rig, ALICE, vec![RpcWalletRequest::Transfer(TransferSpacesParams { - spaces: vec![Subject::Space(space_name.clone())], + spaces: vec![Subject::Label(space_name.clone())], to: Some(original_addr.clone()), data: None, })], @@ -756,22 +754,22 @@ async fn it_should_restore_delegation_when_transferring_back(rig: &TestRig) -> a assert!(wallet_res_err(&transfer2).is_ok()); mine_and_sync(rig, 1).await?; - // KEY TEST: Original SPTR should have delegation RESTORED - let delegator_restored = rig.spaced.client.get_delegator(Subject::Ptr(original_sptr)).await?; + // KEY TEST: Original Num Id should have delegation RESTORED + let delegator_restored = rig.spaced.client.get_delegator(Subject::NumId(original_id)).await?; println!("Delegation after transfer back: {:?}", delegator_restored); assert!(delegator_restored.is_some(), - "Original SPTR should have delegation restored after transferring back!"); + "Original Num Id should have delegation restored after transferring back!"); assert_eq!(delegator_restored.unwrap(), space_name, - "Original SPTR should point to the space again"); + "Original Num Id should point to the space again"); - println!("✓ Original SPTR delegation RESTORED: {} -> {}", original_sptr, space_name); + println!("✓ Original Num Id delegation RESTORED: {} -> {}", original_id, space_name); - // Verify: new SPTR should have NO delegation now - let delegator_new_after = rig.spaced.client.get_delegator(Subject::Ptr(new_sptr)).await?; + // Verify: new Num Id should have NO delegation now + let delegator_new_after = rig.spaced.client.get_delegator(Subject::NumId(new_id)).await?; assert_eq!(delegator_new_after, None, - "New SPTR should have no delegation after space was transferred back"); - println!("✓ New SPTR delegation revoked"); + "New Num Id should have no delegation after space was transferred back"); + println!("✓ New Num Id delegation revoked"); println!("\n✓ Transfer-back delegation restoration working correctly!"); Ok(()) @@ -788,25 +786,23 @@ async fn it_should_set_and_persist_ptr_data(rig: &TestRig) -> anyhow::Result<()> let addr0_spk = bitcoin::address::Address::from_str(&addr0)? .assume_checked() .script_pubkey(); - let addr0_spk_string = hex::encode(addr0_spk.as_bytes()); - wallet_do( rig, ALICE, - vec![RpcWalletRequest::CreatePtr(CreatePtrParams { - spk: addr0_spk_string, + vec![RpcWalletRequest::CreateNum(CreateNumParams { + bind_spk: Some(addr0_spk.clone()), })], false, ).await?; mine_and_sync(rig, 1).await?; - let sptr = Sptr::from_spk::(addr0_spk.clone()); - println!("SPTR created: {}", sptr); + let id = NumId::from_spk::(addr0_spk.clone()); + println!("Num id created: {}", id); // Verify PTR exists with no data - let ptr_initial = rig.spaced.client.get_ptr(Subject::Ptr(sptr)).await? + let ptr_initial = rig.spaced.client.get_num(Subject::NumId(id)).await? .expect("ptr should exist"); - assert_eq!(ptr_initial.ptrout.sptr.data, None, "PTR should have no data initially"); + assert_eq!(ptr_initial.numout.num.data, None, "PTR should have no data initially"); // Test 2: Set data on the PTR println!("\nTest 2: Set data on PTR"); @@ -815,7 +811,7 @@ async fn it_should_set_and_persist_ptr_data(rig: &TestRig) -> anyhow::Result<()> rig, ALICE, vec![RpcWalletRequest::SetFallback(SetFallbackParams { - subject: Subject::Ptr(sptr), + subject: Subject::NumId(id), data: test_data.clone(), })], false, @@ -825,9 +821,9 @@ async fn it_should_set_and_persist_ptr_data(rig: &TestRig) -> anyhow::Result<()> use spaces_protocol::Bytes; // Verify data was set - let ptr_with_data = rig.spaced.client.get_ptr(Subject::Ptr(sptr)).await? + let ptr_with_data = rig.spaced.client.get_num(Subject::NumId(id)).await? .expect("ptr should exist"); - assert_eq!(ptr_with_data.ptrout.sptr.data, Some(Bytes::new(test_data.clone())), "PTR data should be set"); + assert_eq!(ptr_with_data.numout.num.data, Some(Bytes::new(test_data.clone())), "PTR data should be set"); println!("✓ PTR data set successfully: {:?}", String::from_utf8_lossy(&test_data)); // Test 3: Transfer PTR without data - data should persist @@ -837,7 +833,7 @@ async fn it_should_set_and_persist_ptr_data(rig: &TestRig) -> anyhow::Result<()> rig, ALICE, vec![RpcWalletRequest::Transfer(TransferSpacesParams { - spaces: vec![Subject::Ptr(sptr)], + spaces: vec![Subject::NumId(id)], to: Some(bob_addr.clone()), data: None, })], @@ -846,10 +842,10 @@ async fn it_should_set_and_persist_ptr_data(rig: &TestRig) -> anyhow::Result<()> assert!(wallet_res_err(&transfer).is_ok(), "Transfer PTR should succeed"); mine_and_sync(rig, 1).await?; - let ptr_after_transfer = rig.spaced.client.get_ptr(Subject::Ptr(sptr)).await? + let ptr_after_transfer = rig.spaced.client.get_num(Subject::NumId(id)).await? .expect("ptr should exist after transfer"); - assert_eq!(ptr_after_transfer.ptrout.sptr.data, Some(Bytes::new(test_data.clone())), - "PTR data should persist after transfer without new data"); + assert_eq!(ptr_after_transfer.numout.num.data, Some(Bytes::new(test_data.clone())), + "PTR data should persist after transfer without new data"); println!("✓ PTR data persisted after transfer"); // Test 4: Update data with new value @@ -859,7 +855,7 @@ async fn it_should_set_and_persist_ptr_data(rig: &TestRig) -> anyhow::Result<()> rig, BOB, vec![RpcWalletRequest::SetFallback(SetFallbackParams { - subject: Subject::Ptr(sptr), + subject: Subject::NumId(id), data: new_data.clone(), })], false, @@ -867,9 +863,9 @@ async fn it_should_set_and_persist_ptr_data(rig: &TestRig) -> anyhow::Result<()> assert!(wallet_res_err(&update_data).is_ok(), "SetFallback should succeed"); mine_and_sync(rig, 1).await?; - let ptr_updated = rig.spaced.client.get_ptr(Subject::Ptr(sptr)).await? + let ptr_updated = rig.spaced.client.get_num(Subject::NumId(id)).await? .expect("ptr should exist"); - assert_eq!(ptr_updated.ptrout.sptr.data, Some(Bytes::new(new_data.clone())), "PTR data should be updated"); + assert_eq!(ptr_updated.numout.num.data, Some(Bytes::new(new_data.clone())), "PTR data should be updated"); println!("✓ PTR data updated successfully: {:?}", String::from_utf8_lossy(&new_data)); // Test 5: Set empty data @@ -879,7 +875,7 @@ async fn it_should_set_and_persist_ptr_data(rig: &TestRig) -> anyhow::Result<()> rig, BOB, vec![RpcWalletRequest::SetFallback(SetFallbackParams { - subject: Subject::Ptr(sptr), + subject: Subject::NumId(id), data: empty_data.clone(), })], false, @@ -887,9 +883,9 @@ async fn it_should_set_and_persist_ptr_data(rig: &TestRig) -> anyhow::Result<()> assert!(wallet_res_err(&set_empty).is_ok(), "SetFallback with empty data should succeed"); mine_and_sync(rig, 1).await?; - let ptr_empty = rig.spaced.client.get_ptr(Subject::Ptr(sptr)).await? + let ptr_empty = rig.spaced.client.get_num(Subject::NumId(id)).await? .expect("ptr should exist"); - assert_eq!(ptr_empty.ptrout.sptr.data, Some(Bytes::new(empty_data)), "PTR data should be set to empty"); + assert_eq!(ptr_empty.numout.num.data, Some(Bytes::new(empty_data)), "PTR data should be set to empty"); println!("✓ PTR data set to empty successfully"); Ok(()) @@ -913,7 +909,7 @@ async fn it_should_set_and_get_space_fallback(rig: &TestRig) -> anyhow::Result<( let spk_before = space_before.spaceout.script_pubkey.clone(); // Verify no fallback data initially via getfallback - let subject = Subject::Space(space_name.clone()); + let subject = Subject::Label(space_name.clone()); let fallback_before = rig.spaced.client.get_fallback(subject.clone()).await?; assert!(fallback_before.is_none(), "space should have no fallback data initially"); println!("✓ No fallback data initially"); @@ -972,7 +968,7 @@ async fn it_should_set_and_get_space_fallback(rig: &TestRig) -> anyhow::Result<( rig, ALICE, vec![RpcWalletRequest::Transfer(TransferSpacesParams { - spaces: vec![Subject::Space(space_name.clone())], + spaces: vec![Subject::Label(space_name.clone())], to: Some(bob_addr), data: None, })], @@ -1030,11 +1026,11 @@ async fn run_ptr_tests() -> anyhow::Result<()> { load_wallet(&rig, wallets_path.clone(), BOB).await?; load_wallet(&rig, wallets_path, EVE).await?; - println!("\n=== Running SPTR Creation Tests ==="); - it_should_create_sptrs(&rig).await?; + println!("\n=== Running Num Id Creation Tests ==="); + it_should_create_nums(&rig).await?; - println!("\n=== Running SPTR Uniqueness Tests ==="); - it_should_reject_duplicate_sptr_delegations(&rig).await?; + println!("\n=== Running Num Id Uniqueness Tests ==="); + it_should_reject_duplicate_num_id_delegations(&rig).await?; println!("\n=== Running Transfer-Back Delegation Restoration Tests ==="); it_should_restore_delegation_when_transferring_back(&rig).await?; @@ -1057,6 +1053,15 @@ async fn run_ptr_tests() -> anyhow::Result<()> { println!("\n=== Running Space Fallback Data Tests ==="); it_should_set_and_get_space_fallback(&rig).await?; + println!("\n=== Running Numeric Delegation Tests ==="); + it_should_delegate_and_commit_numeric(&rig).await?; + + println!("\n=== Running Numeric Authorize Tests ==="); + it_should_authorize_numeric_to_another_wallet(&rig).await?; + + println!("\n=== Running Multiple Nums Same Tx Tests ==="); + it_should_create_multiple_nums_same_tx(&rig).await?; + println!("\n=== All tests passed! ==="); Ok(()) } @@ -1072,33 +1077,31 @@ async fn it_should_transfer_ptr_with_n_to_n_rule(rig: &TestRig) -> anyhow::Resul // Create a PTR let addr0 = rig.spaced.client.wallet_get_new_address(ALICE, AddressKind::Coin).await?; let addr0_spk = bitcoin::address::Address::from_str(&addr0)?.assume_checked().script_pubkey(); - let addr0_spk_string = hex::encode(addr0_spk.as_bytes()); - wallet_do(rig, ALICE, vec![ - RpcWalletRequest::CreatePtr(CreatePtrParams { spk: addr0_spk_string }) + RpcWalletRequest::CreateNum(CreateNumParams { bind_spk: Some(addr0_spk.clone()) }) ], false).await?; mine_and_sync(rig, 1).await?; - let sptr = Sptr::from_spk::(addr0_spk.clone()); - let ptr_before = rig.spaced.client.get_ptr(Subject::Ptr(sptr)).await?.expect("ptr must exist"); - let value_before = ptr_before.ptrout.value; + let id = NumId::from_spk::(addr0_spk.clone()); + let ptr_before = rig.spaced.client.get_num(Subject::NumId(id)).await?.expect("ptr must exist"); + let value_before = ptr_before.numout.value; // Transfer to addr1 with SAME value (should use n→n rule) let addr1 = rig.spaced.client.wallet_get_new_address(BOB, AddressKind::Space).await?; wallet_do(rig, ALICE, vec![ RpcWalletRequest::Transfer(TransferSpacesParams { - spaces: vec![Subject::Ptr(sptr)], + spaces: vec![Subject::NumId(id)], to: Some(addr1.clone()), data: None, }) ], false).await?; mine_and_sync(rig, 1).await?; - let ptr_after = rig.spaced.client.get_ptr(Subject::Ptr(sptr)).await?.expect("ptr must still exist"); + let ptr_after = rig.spaced.client.get_num(Subject::NumId(id)).await?.expect("ptr must still exist"); let spk1 = SpaceAddress::from_str(&addr1)?.script_pubkey(); - assert_eq!(ptr_after.ptrout.script_pubkey, spk1, "PTR should transfer to new address"); - assert_eq!(ptr_after.ptrout.value, value_before, "PTR value should remain same (n→n)"); + assert_eq!(ptr_after.numout.script_pubkey, spk1, "PTR should transfer to new address"); + assert_eq!(ptr_after.numout.value, value_before, "PTR value should remain same (n→n)"); println!("✓ n→n transfer successful (same value preserved)"); } @@ -1112,13 +1115,13 @@ async fn it_should_transfer_ptr_with_n_to_n_rule(rig: &TestRig) -> anyhow::Resul let spk_b = bitcoin::address::Address::from_str(&addr_b)?.assume_checked().script_pubkey(); wallet_do(rig, ALICE, vec![ - RpcWalletRequest::CreatePtr(CreatePtrParams { spk: hex::encode(spk_a.as_bytes()) }), - RpcWalletRequest::CreatePtr(CreatePtrParams { spk: hex::encode(spk_b.as_bytes()) }), + RpcWalletRequest::CreateNum(CreateNumParams { bind_spk: Some(spk_a.clone()) }), + RpcWalletRequest::CreateNum(CreateNumParams { bind_spk: Some(spk_b.clone()) }), ], false).await?; mine_and_sync(rig, 1).await?; - let sptr_a = Sptr::from_spk::(spk_a.clone()); - let sptr_b = Sptr::from_spk::(spk_b.clone()); + let id_a = NumId::from_spk::(spk_a.clone()); + let id_b = NumId::from_spk::(spk_b.clone()); // Transfer both to different addresses let dest_a = rig.spaced.client.wallet_get_new_address(BOB, AddressKind::Space).await?; @@ -1126,25 +1129,25 @@ async fn it_should_transfer_ptr_with_n_to_n_rule(rig: &TestRig) -> anyhow::Resul wallet_do(rig, ALICE, vec![ RpcWalletRequest::Transfer(TransferSpacesParams { - spaces: vec![Subject::Ptr(sptr_a)], + spaces: vec![Subject::NumId(id_a)], to: Some(dest_a.clone()), data: None, }), RpcWalletRequest::Transfer(TransferSpacesParams { - spaces: vec![Subject::Ptr(sptr_b)], + spaces: vec![Subject::NumId(id_b)], to: Some(dest_b.clone()), data: None, }), ], false).await?; mine_and_sync(rig, 1).await?; - let ptr_a_after = rig.spaced.client.get_ptr(Subject::Ptr(sptr_a)).await?.expect("ptr_a must exist"); - let ptr_b_after = rig.spaced.client.get_ptr(Subject::Ptr(sptr_b)).await?.expect("ptr_b must exist"); + let ptr_a_after = rig.spaced.client.get_num(Subject::NumId(id_a)).await?.expect("ptr_a must exist"); + let ptr_b_after = rig.spaced.client.get_num(Subject::NumId(id_b)).await?.expect("ptr_b must exist"); let spk_dest_a = SpaceAddress::from_str(&dest_a)?.script_pubkey(); let spk_dest_b = SpaceAddress::from_str(&dest_b)?.script_pubkey(); - assert_eq!(ptr_a_after.ptrout.script_pubkey, spk_dest_a, "PTR A should transfer correctly"); - assert_eq!(ptr_b_after.ptrout.script_pubkey, spk_dest_b, "PTR B should transfer correctly"); + assert_eq!(ptr_a_after.numout.script_pubkey, spk_dest_a, "PTR A should transfer correctly"); + assert_eq!(ptr_b_after.numout.script_pubkey, spk_dest_b, "PTR B should transfer correctly"); println!("✓ Multiple PTR transfers handled correctly"); } @@ -1158,36 +1161,358 @@ async fn it_should_transfer_ptr_with_n_to_n_rule(rig: &TestRig) -> anyhow::Resul // Delegate to create PTR wallet_do(rig, ALICE, vec![ - RpcWalletRequest::Delegate(DelegateParams { space: space_name.clone() }) + RpcWalletRequest::Delegate(DelegateParams { subject: space_name.clone().into() }) ], false).await?; mine_and_sync(rig, 1).await?; - let sptr = rig.spaced.client.get_delegation(space_name.clone()).await? + let id = rig.spaced.client.get_delegation(space_name.clone().into()).await? .expect("delegation should exist"); - let ptr_before_commit = rig.spaced.client.get_ptr(Subject::Ptr(sptr)).await?.expect("ptr must exist"); - let value_before = ptr_before_commit.ptrout.value; - let spk_before = ptr_before_commit.ptrout.script_pubkey.clone(); + let ptr_before_commit = rig.spaced.client.get_num(Subject::NumId(id)).await?.expect("ptr must exist"); + let value_before = ptr_before_commit.numout.value; + let spk_before = ptr_before_commit.numout.script_pubkey.clone(); // Make a commitment (should preserve value via n→n) wallet_do(rig, ALICE, vec![ RpcWalletRequest::Commit(CommitParams { - space: space_name.clone(), + subject: space_name.clone().into(), root: Some(sha256::Hash::from_slice(&[1u8; 32])?), }) ], false).await?; mine_and_sync(rig, 1).await?; - let ptr_after_commit = rig.spaced.client.get_ptr(Subject::Ptr(sptr)).await?.expect("ptr must exist after commit"); + let ptr_after_commit = rig.spaced.client.get_num(Subject::NumId(id)).await?.expect("ptr must exist after commit"); - assert_eq!(ptr_after_commit.ptrout.value, value_before, "Commitment should preserve PTR value (n→n)"); - assert_eq!(ptr_after_commit.ptrout.script_pubkey, spk_before, "Commitment should keep same address"); + assert_eq!(ptr_after_commit.numout.value, value_before, "Commitment should preserve PTR value (n→n)"); + assert_eq!(ptr_after_commit.numout.script_pubkey, spk_before, "Commitment should keep same address"); // Verify commitment was created - let commitment = rig.spaced.client.get_commitment(space_name.clone(), None).await? + let commitment = rig.spaced.client.get_commitment(space_name.clone().into(), None).await? .expect("commitment should exist"); assert_eq!(commitment.state_root, [1u8; 32], "Commitment root should match"); println!("✓ Commitment preserves PTR value and address (n→n rule)"); } + Ok(()) +} + +// ============== Test: Numeric Delegation and Commitment ============== + +async fn it_should_delegate_and_commit_numeric(rig: &TestRig) -> anyhow::Result<()> { + sync_all(rig).await?; + + // Create a num + println!("Test 1: Create and delegate a numeric"); + let addr = rig.spaced.client.wallet_get_new_address(ALICE, AddressKind::Coin).await?; + let spk = bitcoin::address::Address::from_str(&addr)?.assume_checked().script_pubkey(); + wallet_do(rig, ALICE, vec![ + RpcWalletRequest::CreateNum(CreateNumParams { bind_spk: Some(spk.clone()) }) + ], false).await?; + mine_and_sync(rig, 1).await?; + + let id = NumId::from_spk::(spk); + let num_info = rig.spaced.client.get_num(Subject::NumId(id)).await? + .expect("num must exist"); + let numeric = num_info.numout.num.name; + let numeric_label = numeric.to_slabel(); + println!(" Created numeric: {}", numeric); + + // Verify no delegation exists yet + let delegation_before = rig.spaced.client + .get_delegation(Subject::Label(numeric_label.clone())).await?; + assert!(delegation_before.is_none(), "no delegation before delegate"); + + // Delegate the numeric + let delegate_res = wallet_do(rig, ALICE, vec![ + RpcWalletRequest::Delegate(DelegateParams { + subject: Subject::Label(numeric_label.clone()), + }) + ], false).await?; + wallet_res_err(&delegate_res)?; + mine_and_sync(rig, 1).await?; + + // Verify delegation exists + let delegation = rig.spaced.client + .get_delegation(Subject::Label(numeric_label.clone())).await? + .expect("delegation should exist after delegate"); + println!(" Delegation established, delegator: {}", delegation); + + // Verify the num now has delegate dust value + let num_after = rig.spaced.client.get_num(Subject::NumId(id)).await? + .expect("num must still exist after delegate"); + assert_eq!( + num_after.numout.value.to_sat() % 10, 8, + "num should have delegate dust signaling (value % 10 == 8)" + ); + + // Verify can_operate works for numeric subjects + let can_op = rig.spaced.client + .wallet_can_operate(ALICE, Subject::Label(numeric_label.clone())).await?; + assert!(can_op, "owner should be able to operate delegated numeric"); + let can_op_bob = rig.spaced.client + .wallet_can_operate(BOB, Subject::Label(numeric_label.clone())).await?; + assert!(!can_op_bob, "non-owner should not be able to operate delegated numeric"); + println!("✓ Numeric delegation and can_operate successful"); + + // Test 2: Commit to the numeric + println!("\nTest 2: Commit to delegated numeric"); + let commit_res = wallet_do(rig, ALICE, vec![ + RpcWalletRequest::Commit(CommitParams { + subject: Subject::Label(numeric_label.clone()), + root: Some(sha256::Hash::from_slice(&[42u8; 32])?), + }) + ], false).await?; + wallet_res_err(&commit_res)?; + mine_and_sync(rig, 1).await?; + + let commitment = rig.spaced.client + .get_commitment(Subject::Label(numeric_label.clone()), None).await? + .expect("commitment should exist"); + assert_eq!(commitment.state_root, [42u8; 32], "commitment root should match"); + println!("✓ Numeric commitment successful"); + + // Test 3: Rollback the commitment + println!("\nTest 3: Rollback numeric commitment"); + let rollback_res = wallet_do(rig, ALICE, vec![ + RpcWalletRequest::Commit(CommitParams { + subject: Subject::Label(numeric_label.clone()), + root: None, + }) + ], false).await?; + wallet_res_err(&rollback_res)?; + mine_and_sync(rig, 1).await?; + + let after_rollback = rig.spaced.client + .get_commitment(Subject::Label(numeric_label.clone()), None).await?; + assert!(after_rollback.is_none(), "commitment tip should be gone after rollback"); + println!("✓ Numeric rollback successful"); + + Ok(()) +} + +// ============== Test: Authorize Numeric to Another Wallet ============== + +async fn it_should_authorize_numeric_to_another_wallet(rig: &TestRig) -> anyhow::Result<()> { + sync_all(rig).await?; + + // Create a num for Alice + println!("Test 1: Alice creates and delegates a numeric"); + wallet_do(rig, ALICE, vec![ + RpcWalletRequest::CreateNum(CreateNumParams { bind_spk: None }) + ], false).await?; + mine_and_sync(rig, 1).await?; + + let alice_nums = rig.spaced.client.wallet_list_nums(ALICE).await?; + let num_entry = alice_nums.nums.last().expect("Alice should have a num"); + let numeric_label = num_entry.numout.num.name.to_slabel(); + let num_id = num_entry.numout.num.id; + println!(" Created numeric: {} (id={})", num_entry.numout.num.name, num_id); + + // Delegate it + let delegate_res = wallet_do(rig, ALICE, vec![ + RpcWalletRequest::Delegate(DelegateParams { + subject: Subject::Label(numeric_label.clone()), + }) + ], false).await?; + wallet_res_err(&delegate_res)?; + mine_and_sync(rig, 1).await?; + + // Alice can operate, Bob cannot + let can_op_alice = rig.spaced.client + .wallet_can_operate(ALICE, Subject::Label(numeric_label.clone())).await?; + assert!(can_op_alice, "Alice should be able to operate before authorize"); + let can_op_bob = rig.spaced.client + .wallet_can_operate(BOB, Subject::Label(numeric_label.clone())).await?; + assert!(!can_op_bob, "Bob should not be able to operate before authorize"); + println!("✓ Pre-authorize: Alice can operate, Bob cannot"); + + // Get the delegating NumId + let delegation_id = rig.spaced.client + .get_delegation(Subject::Label(numeric_label.clone())).await? + .expect("delegation must exist"); + println!(" Delegating num id: {}", delegation_id); + + // Alice transfers the delegating num to Bob (authorize) + println!("\nTest 2: Alice authorizes Bob by transferring the delegating num"); + let bob_addr = rig.spaced.client + .wallet_get_new_address(BOB, AddressKind::Space).await?; + let authorize_res = wallet_do(rig, ALICE, vec![ + RpcWalletRequest::Transfer(TransferSpacesParams { + spaces: vec![Subject::NumId(delegation_id)], + to: Some(bob_addr), + data: None, + }) + ], false).await?; + wallet_res_err(&authorize_res)?; + mine_and_sync(rig, 1).await?; + + // Now Bob can operate, Alice cannot + let can_op_bob_after = rig.spaced.client + .wallet_can_operate(BOB, Subject::Label(numeric_label.clone())).await?; + assert!(can_op_bob_after, "Bob should be able to operate after authorize"); + let can_op_alice_after = rig.spaced.client + .wallet_can_operate(ALICE, Subject::Label(numeric_label.clone())).await?; + assert!(!can_op_alice_after, "Alice should not be able to operate after authorize"); + println!("✓ Post-authorize: Bob can operate, Alice cannot"); + + // Test 3: Bob can commit to the numeric + println!("\nTest 3: Bob commits to the numeric"); + let commit_res = wallet_do(rig, BOB, vec![ + RpcWalletRequest::Commit(CommitParams { + subject: Subject::Label(numeric_label.clone()), + root: Some(sha256::Hash::from_slice(&[99u8; 32])?), + }) + ], false).await?; + wallet_res_err(&commit_res)?; + mine_and_sync(rig, 1).await?; + + let commitment = rig.spaced.client + .get_commitment(Subject::Label(numeric_label.clone()), None).await? + .expect("commitment should exist after Bob commits"); + assert_eq!(commitment.state_root, [99u8; 32], "commitment root should match Bob's"); + println!("✓ Bob successfully committed to Alice's numeric"); + + // Test 4: Bob rolls back the commitment + println!("\nTest 4: Bob rolls back the commitment"); + let rollback_res = wallet_do(rig, BOB, vec![ + RpcWalletRequest::Commit(CommitParams { + subject: Subject::Label(numeric_label.clone()), + root: None, + }) + ], false).await?; + wallet_res_err(&rollback_res)?; + mine_and_sync(rig, 1).await?; + + let after_rollback = rig.spaced.client + .get_commitment(Subject::Label(numeric_label.clone()), None).await?; + assert!(after_rollback.is_none(), "commitment tip should be gone after rollback"); + + // Delegation should still be intact after rollback + let can_op_bob_after_rollback = rig.spaced.client + .wallet_can_operate(BOB, Subject::Label(numeric_label.clone())).await?; + assert!(can_op_bob_after_rollback, "Bob should still be able to operate after rollback"); + println!("✓ Bob rolled back successfully, delegation intact"); + + // Test 5: Alice re-delegates to revoke Bob's authorization + println!("\nTest 5: Alice re-delegates to revoke Bob's authorization"); + let redelegate_res = wallet_do(rig, ALICE, vec![ + RpcWalletRequest::Delegate(DelegateParams { + subject: Subject::Label(numeric_label.clone()), + }) + ], false).await?; + wallet_res_err(&redelegate_res)?; + mine_and_sync(rig, 1).await?; + + let can_op_alice_revoked = rig.spaced.client + .wallet_can_operate(ALICE, Subject::Label(numeric_label.clone())).await?; + assert!(can_op_alice_revoked, "Alice should be able to operate after re-delegate"); + let can_op_bob_revoked = rig.spaced.client + .wallet_can_operate(BOB, Subject::Label(numeric_label.clone())).await?; + assert!(!can_op_bob_revoked, "Bob should no longer be able to operate after revoke"); + println!("✓ Re-delegate revoked Bob's authorization, Alice has control again"); + + // Test 6: Alice commits successfully after re-delegation + println!("\nTest 6: Alice commits after revoking Bob"); + let alice_commit_res = wallet_do(rig, ALICE, vec![ + RpcWalletRequest::Commit(CommitParams { + subject: Subject::Label(numeric_label.clone()), + root: Some(sha256::Hash::from_slice(&[77u8; 32])?), + }) + ], false).await?; + wallet_res_err(&alice_commit_res)?; + mine_and_sync(rig, 1).await?; + + let alice_commitment = rig.spaced.client + .get_commitment(Subject::Label(numeric_label.clone()), None).await? + .expect("commitment should exist after Alice commits"); + assert_eq!(alice_commitment.state_root, [77u8; 32], "commitment root should match Alice's"); + println!("✓ Alice committed successfully"); + + // Test 7: Bob can no longer commit + println!("\nTest 7: Bob cannot commit after revocation"); + let bob_fail_res = wallet_do(rig, BOB, vec![ + RpcWalletRequest::Commit(CommitParams { + subject: Subject::Label(numeric_label.clone()), + root: Some(sha256::Hash::from_slice(&[88u8; 32])?), + }) + ], false).await; + assert!(bob_fail_res.is_err(), "Bob's commit should fail after revocation"); + println!("✓ Bob cannot commit after Alice revoked authorization"); + + Ok(()) +} + +// ============== Test: Multiple Nums Created in Same Tx ============== + +async fn it_should_create_multiple_nums_same_tx(rig: &TestRig) -> anyhow::Result<()> { + sync_all(rig).await?; + + println!("Test 1: Create two nums in a single transaction (auto-generated addresses)"); + let before = rig.spaced.client.wallet_list_nums(ALICE).await?; + let before_count = before.nums.len(); + + wallet_do(rig, ALICE, vec![ + RpcWalletRequest::CreateNum(CreateNumParams { bind_spk: None }), + RpcWalletRequest::CreateNum(CreateNumParams { bind_spk: None }), + ], false).await?; + mine_and_sync(rig, 1).await?; + + let after = rig.spaced.client.wallet_list_nums(ALICE).await?; + assert_eq!(after.nums.len(), before_count + 2, "two new nums created"); + + let new_nums: Vec<_> = after.nums.iter() + .filter(|e| !before.nums.iter().any(|b| b.numout.num.id == e.numout.num.id)) + .collect(); + assert_eq!(new_nums.len(), 2, "exactly two new nums"); + + let name_a = new_nums[0].numout.num.name; + let name_b = new_nums[1].numout.num.name; + let id_a = new_nums[0].numout.num.id; + let id_b = new_nums[1].numout.num.id; + + println!(" Num A: {} (id={})", name_a, id_a); + println!(" Num B: {} (id={})", name_b, id_b); + + // Both should share same block and tx_pos but have different vouts + assert_eq!(name_a.block(), name_b.block(), "same block"); + assert_eq!(name_a.tx_pos(), name_b.tx_pos(), "same tx position"); + assert_ne!(name_a.vout(), name_b.vout(), "different vouts"); + println!("✓ Multiple nums in same tx have unique SNumeric (different vout)"); + + // Test 2: Both can be looked up by their numeric label + println!("\nTest 2: Lookup both by numeric label"); + let lookup_a = rig.spaced.client + .get_num(Subject::Label(name_a.to_slabel())).await?; + let lookup_b = rig.spaced.client + .get_num(Subject::Label(name_b.to_slabel())).await?; + assert!(lookup_a.is_some(), "num A must be findable by numeric label"); + assert!(lookup_b.is_some(), "num B must be findable by numeric label"); + assert_eq!(lookup_a.unwrap().numout.num.id, id_a, "correct num A resolved"); + assert_eq!(lookup_b.unwrap().numout.num.id, id_b, "correct num B resolved"); + println!("✓ Both nums resolvable by their unique numeric labels"); + + // Test 3: Delegate both and verify independent delegations + println!("\nTest 3: Delegate both nums independently"); + wallet_do(rig, ALICE, vec![ + RpcWalletRequest::Delegate(DelegateParams { + subject: Subject::Label(name_a.to_slabel()), + }) + ], false).await?; + mine_and_sync(rig, 1).await?; + + wallet_do(rig, ALICE, vec![ + RpcWalletRequest::Delegate(DelegateParams { + subject: Subject::Label(name_b.to_slabel()), + }) + ], false).await?; + mine_and_sync(rig, 1).await?; + + let del_a = rig.spaced.client + .get_delegation(Subject::Label(name_a.to_slabel())).await?; + let del_b = rig.spaced.client + .get_delegation(Subject::Label(name_b.to_slabel())).await?; + assert!(del_a.is_some(), "delegation A should exist"); + assert!(del_b.is_some(), "delegation B should exist"); + println!("✓ Both nums delegated independently"); + Ok(()) } \ No newline at end of file diff --git a/ptr/Cargo.toml b/nums/Cargo.toml similarity index 95% rename from ptr/Cargo.toml rename to nums/Cargo.toml index 79322e5..d568149 100644 --- a/ptr/Cargo.toml +++ b/nums/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "spaces_ptr" +name = "spaces_nums" version = "0.1.0" edition = "2024" @@ -13,6 +13,7 @@ borsh_utils = { path = "../borsh_utils", optional = true } serde = { version = "1.0", features = ["derive"], default-features = false, optional = true } bech32 = { workspace = true, optional = true } hex = { workspace = true, optional = true } +log = "0.4.29" [features] default = ["std"] diff --git a/ptr/src/constants.rs b/nums/src/constants.rs similarity index 100% rename from ptr/src/constants.rs rename to nums/src/constants.rs diff --git a/ptr/src/lib.rs b/nums/src/lib.rs similarity index 60% rename from ptr/src/lib.rs rename to nums/src/lib.rs index f2163b6..46c9561 100644 --- a/ptr/src/lib.rs +++ b/nums/src/lib.rs @@ -1,6 +1,6 @@ -#[cfg(feature = "std")] -pub mod sptr; pub mod constants; +#[cfg(feature = "std")] +pub mod num_id; pub mod snumeric; #[cfg(feature = "borsh")] @@ -9,31 +9,46 @@ use borsh::{BorshDeserialize, BorshSerialize}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use spaces_protocol::constants::ChainAnchor; -use spaces_protocol::script::find_op_set_data; -use bitcoin::{Amount, OutPoint, ScriptBuf, Transaction, TxOut, Txid}; +use crate::constants::COMMITMENT_FINALITY_INTERVAL; +use crate::num_id::NumId; +use crate::snumeric::SNumeric; use bitcoin::absolute::LockTime; use bitcoin::opcodes::all::{OP_PUSHNUM_2, OP_RETURN}; use bitcoin::script::{Instruction, PushBytesBuf}; -use spaces_protocol::hasher::{KeyHasher, KeyHash, Hash}; +use bitcoin::{Amount, OutPoint, ScriptBuf, Transaction, TxOut, Txid}; +use spaces_protocol::constants::ChainAnchor; +use spaces_protocol::hasher::{Hash, KeyHash, KeyHasher}; +use spaces_protocol::script::find_op_set_data; use spaces_protocol::slabel::SLabel; use spaces_protocol::{Bytes, SpaceOut}; -use crate::constants::COMMITMENT_FINALITY_INTERVAL; -use crate::snumeric::SNumeric; -use crate::sptr::Sptr; -pub trait PtrSource { - fn get_ptr_outpoint(&mut self, sptr: &Sptr) -> spaces_protocol::errors::Result>; +pub trait NumSource { + fn get_num_outpoint_by_id( + &mut self, + id: &NumId, + ) -> spaces_protocol::errors::Result>; - fn get_commitment(&mut self, key: &CommitmentKey) -> spaces_protocol::errors::Result>; + fn get_commitment( + &mut self, + key: &CommitmentKey, + ) -> spaces_protocol::errors::Result>; - fn get_commitments_tip(&mut self, key: &RegistryKey) -> spaces_protocol::errors::Result>; + fn get_commitments_tip( + &mut self, + key: &CommitmentTipKey, + ) -> spaces_protocol::errors::Result>; - fn get_delegator(&mut self, sptr: &RegistrySptrKey) -> spaces_protocol::errors::Result>; + fn get_delegator( + &mut self, + key: &DelegatorKey, + ) -> spaces_protocol::errors::Result>; - fn get_ptrout(&mut self, outpoint: &OutPoint) -> spaces_protocol::errors::Result>; + fn get_numout( + &mut self, + outpoint: &OutPoint, + ) -> spaces_protocol::errors::Result>; - fn get_numeric(&mut self, key: &NumericKey) -> spaces_protocol::errors::Result>; + fn get_num_id(&mut self, key: &NumericKey) -> spaces_protocol::errors::Result>; } #[derive(Debug, Clone)] @@ -52,10 +67,10 @@ pub struct TxChangeSet { ) )] pub txid: Txid, - /// List of transaction input indexes spending a ptrout. + /// List of transaction input indexes spending nums. pub spends: Vec, - /// List of transaction outputs creating a ptrout. - pub creates: Vec, + /// List of transaction outputs creating numouts. + pub creates: Vec, /// New commitments made pub commitments: Vec, pub revoked_commitments: Vec, @@ -76,14 +91,14 @@ pub struct CommitmentInfo { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] pub struct DelegationInfo { - pub space: SLabel, - pub sptr: Sptr, + pub id: NumId, + pub subject: SLabel, } #[derive(Clone, PartialEq, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -pub struct FullPtrOut { +pub struct FullNumOut { #[cfg_attr( feature = "borsh", borsh( @@ -94,7 +109,7 @@ pub struct FullPtrOut { pub txid: Txid, #[cfg_attr(feature = "serde", serde(flatten))] - pub ptrout: PtrOut, + pub numout: NumOut, } /// PTR TxOut @@ -102,10 +117,10 @@ pub struct FullPtrOut { #[derive(Clone, PartialEq, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -pub struct PtrOut { +pub struct NumOut { pub n: usize, #[cfg_attr(feature = "serde", serde(flatten))] - pub sptr: Ptr, + pub num: Num, /// The value of the output, in satoshis. #[cfg_attr( feature = "borsh", @@ -129,9 +144,9 @@ pub struct PtrOut { #[derive(Clone, PartialEq, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -pub struct Ptr { - pub id: Sptr, - pub numeric: SNumeric, +pub struct Num { + pub id: NumId, + pub name: SNumeric, pub data: Option, pub last_update: u32, } @@ -141,24 +156,33 @@ pub struct Ptr { #[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] pub struct Commitment { /// Merkle/Trie commitment to the current state. - #[cfg_attr(feature = "serde", serde( - serialize_with = "serialize_hash_serde", - deserialize_with = "deserialize_hash_serde" - ))] + #[cfg_attr( + feature = "serde", + serde( + serialize_with = "serialize_hash_serde", + deserialize_with = "deserialize_hash_serde" + ) + )] pub state_root: [u8; 32], /// Previous state root (None for genesis). - #[cfg_attr(feature = "serde", serde( - serialize_with = "serialize_optional_hash_serde", - deserialize_with = "deserialize_optional_hash_serde" - ))] + #[cfg_attr( + feature = "serde", + serde( + serialize_with = "serialize_optional_hash_serde", + deserialize_with = "deserialize_optional_hash_serde" + ) + )] pub prev_root: Option<[u8; 32]>, /// Rolling hash for all previous commitments - #[cfg_attr(feature = "serde", serde( - serialize_with = "serialize_hash_serde", - deserialize_with = "deserialize_hash_serde" - ))] + #[cfg_attr( + feature = "serde", + serde( + serialize_with = "serialize_hash_serde", + deserialize_with = "deserialize_hash_serde" + ) + )] pub rolling_hash: [u8; 32], /// Block height at which the commitment was made @@ -169,10 +193,10 @@ pub struct Commitment { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum KeyKind { Commitment = 0x01, - Sptr = 0x02, + NumId = 0x02, Registry = 0x03, - RegistrySptr = 0x04, - PtrOutpoint = 0x05, + Delegator = 0x04, + NumOutpoint = 0x05, SNumeric = 0x06, } @@ -190,15 +214,14 @@ pub fn ns_hash(kind: KeyKind, data: [u8; 32]) -> [u8; 32] { H::hash(&buf) } - #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] #[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -pub struct RegistryKey([u8; 32]); +pub struct CommitmentTipKey([u8; 32]); #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -pub struct RegistrySptrKey([u8; 32]); +pub struct DelegatorKey([u8; 32]); #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] #[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] @@ -207,7 +230,7 @@ pub struct CommitmentKey([u8; 32]); #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -pub struct PtrOutpointKey([u8; 32]); +pub struct NumOutpointKey([u8; 32]); #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] #[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] @@ -216,18 +239,24 @@ pub struct NumericKey([u8; 32]); #[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct RootAnchor { - #[cfg_attr(feature = "serde", serde( - serialize_with = "serialize_hash_serde", - deserialize_with = "deserialize_hash_serde" - ))] + #[cfg_attr( + feature = "serde", + serde( + serialize_with = "serialize_hash_serde", + deserialize_with = "deserialize_hash_serde" + ) + )] pub spaces_root: Hash, - #[cfg_attr(feature = "serde", serde( - default, - skip_serializing_if = "Option::is_none", - serialize_with = "serialize_optional_hash_serde", - deserialize_with = "deserialize_optional_hash_serde" - ))] - pub ptrs_root: Option, + #[cfg_attr( + feature = "serde", + serde( + default, + skip_serializing_if = "Option::is_none", + serialize_with = "serialize_optional_hash_serde", + deserialize_with = "deserialize_optional_hash_serde" + ) + )] + pub nums_root: Option, pub block: ChainAnchor, } @@ -241,27 +270,30 @@ pub struct ChainProofRequest { /// Spaces to prove (server resolves to outpoint keys). pub spaces: Vec, /// Typed keys to prove in the ptrs tree. - pub ptrs_keys: Vec, + pub nums: Vec, } -/// A typed key for the ptrs tree. +/// A typed key for the nums tree. /// /// Server resolves these to the appropriate merkle proof paths. -/// For Sptr, the server must look o the outpoint to prove existence. +/// For a num id, the server must look up the outpoint to prove existence. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(tag = "key", content = "value", rename_all = "lowercase"))] +#[cfg_attr( + feature = "serde", + serde(tag = "key", content = "value", rename_all = "lowercase") +)] #[derive(Clone, Copy)] -pub enum PtrKeyKind { - Sptr(Sptr), - Numeric(SNumeric), +pub enum NumKeyKind { + Id(NumId), + Num(SNumeric), Commitment(CommitmentKey), - Registry(RegistryKey), + CommitmentTip(CommitmentTipKey), } -impl KeyHash for RegistryKey {} -impl KeyHash for RegistrySptrKey {} +impl KeyHash for CommitmentTipKey {} +impl KeyHash for DelegatorKey {} impl KeyHash for CommitmentKey {} -impl KeyHash for PtrOutpointKey {} +impl KeyHash for NumOutpointKey {} impl KeyHash for NumericKey {} impl Commitment { @@ -271,23 +303,23 @@ impl Commitment { } } -impl FullPtrOut { +impl FullNumOut { pub fn outpoint(&self) -> OutPoint { OutPoint { txid: self.txid, - vout: self.ptrout.n as _ + vout: self.numout.n as _, } } } -impl From for Hash { - fn from(value: RegistryKey) -> Self { +impl From for Hash { + fn from(value: CommitmentTipKey) -> Self { value.0 } } -impl From for Hash { - fn from(value: RegistrySptrKey) -> Self { +impl From for Hash { + fn from(value: DelegatorKey) -> Self { value.0 } } @@ -298,8 +330,8 @@ impl From for Hash { } } -impl From for Hash { - fn from(value: PtrOutpointKey) -> Self { +impl From for Hash { + fn from(value: NumOutpointKey) -> Self { value.0 } } @@ -310,12 +342,12 @@ impl From for Hash { } } -impl PtrOutpointKey { +impl NumOutpointKey { pub fn from_outpoint(outpoint: OutPoint) -> Self { let mut buffer = [0u8; 36]; buffer[0..32].copy_from_slice(outpoint.txid.as_ref()); buffer[32..36].copy_from_slice(&outpoint.vout.to_le_bytes()); - Self(ns_hash::(KeyKind::PtrOutpoint, H::hash(&buffer))) + Self(ns_hash::(KeyKind::NumOutpoint, H::hash(&buffer))) } } @@ -328,24 +360,24 @@ impl CommitmentKey { } } - -impl RegistryKey { - pub fn from_slabel(space: &SLabel) -> Self { - Self(ns_hash::(KeyKind::Registry, H::hash(space.as_ref()))) +impl CommitmentTipKey { + pub fn from_slabel(subject: &SLabel) -> Self { + Self(ns_hash::(KeyKind::Registry, H::hash(subject.as_ref()))) } } -impl RegistrySptrKey { - pub fn from_sptr(sptr: Sptr) -> Self { - RegistrySptrKey(ns_hash::(KeyKind::RegistrySptr, sptr.to_bytes())) +impl DelegatorKey { + pub fn from_id(id: NumId) -> Self { + DelegatorKey(ns_hash::(KeyKind::Delegator, id.to_bytes())) } } impl NumericKey { pub fn from_numeric(numeric: &SNumeric) -> Self { - let mut buf = [0u8; 6]; + let mut buf = [0u8; 8]; buf[..4].copy_from_slice(&numeric.block().to_le_bytes()); - buf[4..].copy_from_slice(&numeric.tx_pos().to_le_bytes()); + buf[4..6].copy_from_slice(&numeric.tx_pos().to_le_bytes()); + buf[6..8].copy_from_slice(&numeric.vout().to_le_bytes()); Self(ns_hash::(KeyKind::SNumeric, H::hash(&buf))) } } @@ -353,28 +385,31 @@ impl NumericKey { #[derive(Clone)] pub struct Stxo { pub n: usize, - pub ptrout: PtrOut, + pub numout: NumOut, pub delegate: Option, } #[derive(Clone)] pub struct DelegateContext { - space: SLabel, + subject: SLabel, pending_tip: Option, finalized_tip: Option, } pub struct TxContext { pub inputs: Vec, - pub relevant_sptr_spks: Vec, - // sptrs with existing delegations cannot be used multiple times - pub sptrs_with_delegations: Vec, + pub existing_num_spks: Vec, + // nums with existing delegations cannot be used multiple times + pub nums_with_delegations: Vec, } impl TxContext { - pub fn spending_ptrs(src: &mut T, tx: &Transaction) -> spaces_protocol::errors::Result { + pub fn spending_nums( + src: &mut T, + tx: &Transaction, + ) -> spaces_protocol::errors::Result { for input in tx.input.iter() { - if src.get_ptrout(&input.previous_output)?.is_some() { + if src.get_numout(&input.previous_output)?.is_some() { return Ok(true); } } @@ -386,19 +421,18 @@ impl TxContext { /// /// Returns `Some(TxContext)` if the transaction is ptrs tx. /// Returns `None` if the transaction is not relevant. - pub fn from_tx( + pub fn from_tx( src: &mut T, tx: &Transaction, spends_spaces: bool, space_outputs: Vec, height: u32, ) -> spaces_protocol::errors::Result> { - let has_ptr_outputs = is_ptr_minting_locktime(&tx.lock_time) && - tx.output.iter().any(|out| out.is_ptr_output()); + let has_num_outputs = is_num_minting_locktime(&tx.lock_time) + && tx.output.iter().any(|out| out.is_ptr_output()); let has_spaces = spends_spaces || space_outputs.len() > 0; - let relevant = has_spaces || has_ptr_outputs || Self::spending_ptrs(src, tx)?; - + let relevant = has_spaces || has_num_outputs || Self::spending_nums(src, tx)?; if !relevant { return Ok(None); } @@ -406,76 +440,84 @@ impl TxContext { let mut inputs = Vec::with_capacity(tx.input.len()); for (n, input) in tx.input.iter().enumerate() { - let ptrout = src.get_ptrout(&input.previous_output)?; - - if let Some(ptrout) = ptrout { - let delegate = { - let rsk = RegistrySptrKey::from_sptr::(ptrout.sptr.id); - - match src.get_delegator(&rsk)? { - Some(slabel) => { - let registry_key = RegistryKey::from_slabel::(&slabel); - let tip_root = src.get_commitments_tip(®istry_key)?; - let tip = match tip_root { - Some(root) => { - let ck = CommitmentKey::new::(&slabel, root); - src.get_commitment(&ck)? - } - None => None, - }; - - // Determine pending and finalized tips - let (pending_tip, finalized_tip) = match tip { - Some(t) if t.is_finalized(height) => { - (None, Some(t)) - } - Some(t) => { - // Tip is pending, check for previous finalized commitment - let finalized = match t.prev_root { - Some(prev_root) => { - let ck = CommitmentKey::new::(&slabel, prev_root); - src.get_commitment(&ck)? - } - None => None, - }; - (Some(t), finalized) - } - None => (None, None), - }; + let Some(numout) = src.get_numout(&input.previous_output)? else { + continue; + }; - Some(DelegateContext { - space: slabel, - pending_tip, - finalized_tip, - }) - } - None => None, + let delegate = { + let dk = DelegatorKey::from_id::(numout.num.id); + match src.get_delegator(&dk)? { + Some(slabel) => { + let ctip = CommitmentTipKey::from_slabel::(&slabel); + let tip_root = src.get_commitments_tip(&ctip)?; + let tip = match tip_root { + Some(root) => { + let ck = CommitmentKey::new::(&slabel, root); + src.get_commitment(&ck)? + } + None => None, + }; + + // Determine pending and finalized tips + let (pending_tip, finalized_tip) = match tip { + Some(t) if t.is_finalized(height) => (None, Some(t)), + Some(t) => { + // Tip is pending, check for previous finalized commitment + let finalized = match t.prev_root { + Some(prev_root) => { + let ck = CommitmentKey::new::(&slabel, prev_root); + src.get_commitment(&ck)? + } + None => None, + }; + (Some(t), finalized) + } + None => (None, None), + }; + + Some(DelegateContext { + subject: slabel, + pending_tip, + finalized_tip, + }) } - }; + None => None, + } + }; - inputs.push(Stxo { - n, - ptrout, - delegate, - }); - } + inputs.push(Stxo { + n, + numout, + delegate, + }); } - let mut sptrs_with_delegations = Vec::with_capacity(space_outputs.len()); + let mut nums_with_delegations = Vec::with_capacity(space_outputs.len()); for spaceout in space_outputs { - let rsk = RegistrySptrKey::from_sptr::(Sptr::from_spk::(spaceout.script_pubkey)); + let rsk = DelegatorKey::from_id::(NumId::from_spk::(spaceout.script_pubkey)); if src.get_delegator(&rsk)?.is_some() { - sptrs_with_delegations.push(rsk); + nums_with_delegations.push(rsk); + } + } + for input in &inputs { + let dk = DelegatorKey::from_id::( + NumId::from_spk::(input.numout.script_pubkey.clone()), + ); + if !nums_with_delegations.contains(&dk) { + if src.get_delegator(&dk)?.is_some() { + nums_with_delegations.push(dk); + } } } - // Build relevant SPTR script pubkeys for existence checks - let relevant_sptr_spks = tx.output + // Output script pubkeys that already have a num (skip minting duplicates) + let existing_num_spks = tx + .output .iter() .filter(|out| out.is_ptr_output()) .filter_map(|out| { - let sptr = Sptr::from_spk::(out.script_pubkey.clone()); - src.get_ptr_outpoint(&sptr) + let id = NumId::from_spk::(out.script_pubkey.clone()); + src.get_num_outpoint_by_id(&id) .ok()? .map(|_| out.script_pubkey.clone()) }) @@ -483,8 +525,8 @@ impl TxContext { Ok(Some(TxContext { inputs, - relevant_sptr_spks, - sptrs_with_delegations + existing_num_spks, + nums_with_delegations, })) } } @@ -502,7 +544,8 @@ impl Validator { } pub fn process( - &self, height: u32, + &self, + height: u32, tx: &Transaction, tx_pos: u16, mut ctx: TxContext, @@ -521,42 +564,69 @@ impl Validator { let commitment_op = find_op_commit(&tx.output); let data_op = find_op_set_data(&tx.output); + let has_spaces = !spent_space_utxos.is_empty() || !new_space_utxos.is_empty(); - // Remove sptr -> space mappings if a space is spent + // Revoke num id -> space delegations for spent space UTXOs changeset.revoked_delegations = spent_space_utxos .into_iter() .filter_map(|spent| { spent.space.as_ref().map(|space| { - let sptr = Sptr::from_spk::(spent.script_pubkey); + let id = NumId::from_spk::(spent.script_pubkey); DelegationInfo { - space: space.name.clone(), - sptr, + subject: space.name.clone(), + id, } }) }) .collect(); - // Allow sptrs to be redelegated - let revoked_keys: Vec = changeset.revoked_delegations + // Revoke num to num delegations only when the source num is spent + // and a delegation actually exists at that address. + changeset.revoked_delegations.extend( + ctx.inputs + .iter() + .filter_map(|input| { + let operator_id = NumId::from_spk::(input.numout.script_pubkey.clone()); + if operator_id == input.numout.num.id { + return None; + } + let dk = DelegatorKey::from_id::(operator_id); + if !ctx.nums_with_delegations.contains(&dk) { + return None; + } + Some(DelegationInfo { + subject: input.numout.num.name.to_slabel(), + id: operator_id, + }) + }), + ); + + // Clear revoked num ids so they can be redelegated in the same tx + let revoked_keys: Vec = changeset + .revoked_delegations .iter() - .map(|rd| RegistrySptrKey::from_sptr::(rd.sptr)) + .map(|rd| DelegatorKey::from_id::(rd.id)) .collect(); - ctx.sptrs_with_delegations.retain(|rsk| !revoked_keys.contains(rsk)); + ctx.nums_with_delegations + .retain(|rsk| !revoked_keys.contains(rsk)); - // Process new delegations from created space UTXOs + // Create delegations for owned spaces (Transfer covenant only). + // Spaces still in auction (Bid covenant) are not delegatable. changeset.new_delegations = new_space_utxos .iter() .filter_map(|created| { - let sptr = Sptr::from_spk::(created.script_pubkey.clone()); - let rsk = RegistrySptrKey::from_sptr::(sptr); - if ctx.sptrs_with_delegations.contains(&rsk) { + if !created.space.as_ref().is_some_and(|s| s.is_owned()) { return None; } - created.space.as_ref().map(|space| { - DelegationInfo { - space: space.name.clone(), - sptr, - } + + let id = NumId::from_spk::(created.script_pubkey.clone()); + let dk = DelegatorKey::from_id::(id); + if ctx.nums_with_delegations.contains(&dk) { + return None; + } + created.space.as_ref().map(|space| DelegationInfo { + subject: space.name.clone(), + id, }) }) .collect(); @@ -574,12 +644,10 @@ impl Validator { // Rollback applies to ALL delegates with pending commitments if let Some(pending) = delegate.pending_tip { if !pending.is_finalized(height) { - changeset.revoked_commitments.push( - CommitmentInfo { - space: delegate.space.clone(), - commitment: pending, - } - ); + changeset.revoked_commitments.push(CommitmentInfo { + space: delegate.subject.clone(), + commitment: pending, + }); } } } @@ -604,15 +672,13 @@ impl Validator { }; // Revoke pending commitment if let Some(pending) = delegate.pending_tip { - changeset.revoked_commitments.push( - CommitmentInfo { - space: delegate.space.clone(), - commitment: pending, - } - ); + changeset.revoked_commitments.push(CommitmentInfo { + space: delegate.subject.clone(), + commitment: pending, + }); } changeset.commitments.push(CommitmentInfo { - space: delegate.space, + space: delegate.subject, commitment, }); } @@ -622,30 +688,41 @@ impl Validator { } // Process spend changeset.spends.push(input_ctx.n); - self.process_spend(tx, input_ctx.n, input_ctx.ptrout, &new_space_utxos, &mut changeset, height, &data_op); + self.process_spend( + tx, + input_ctx.n, + input_ctx.numout, + &new_space_utxos, + &mut changeset, + height, + &data_op, + ); } - // Process new PTR outputs + // Process new nums for (n, output) in tx.output.iter().enumerate() { // Skip if not a PTR output or already processed if !output.is_ptr_output() || changeset.creates.iter().any(|x| x.n == n) - || new_space_utxos.iter().any(|x| x.n == n) { + || new_space_utxos.iter().any(|x| x.n == n) + { continue; } - // Skip if SPTR already exists - if ctx.relevant_sptr_spks + // Skip if num id already exists + if ctx + .existing_num_spks .iter() - .any(|spk| output.script_pubkey.as_bytes() == spk.as_bytes()) { + .any(|spk| output.script_pubkey.as_bytes() == spk.as_bytes()) + { continue; } - changeset.creates.push(PtrOut { + changeset.creates.push(NumOut { n, - sptr: Ptr { - id: Sptr::from_spk::(output.script_pubkey.clone()), - numeric: SNumeric::new(height, tx_pos), + num: Num { + id: NumId::from_spk::(output.script_pubkey.clone()), + name: SNumeric::new(height, tx_pos, n as u16), data: data_op.clone(), last_update: height, }, @@ -654,6 +731,30 @@ impl Validator { }); } + // Create delegations for nums that opt in via output + // value ending in 8. Most numerics don't need commitments, so this + // avoids creating a delegation entry for every PTR. + // Skipped for txs involving spaces to prevent a nums delegation + // from overwriting a space delegation sharing the same num id. + if !has_spaces { + for created in &changeset.creates { + // Here we are only concerned with the main num that wants to delegate.... + if created.value.to_sat() % 10 != 8 { + continue; + } + + // The current spk of the num points to the operator + let operator_id = NumId::from_spk::(created.script_pubkey.clone()); + let operator_key = DelegatorKey::from_id::(operator_id); + if !ctx.nums_with_delegations.contains(&operator_key) { + changeset.new_delegations.push(DelegationInfo { + subject: created.num.name.to_slabel(), + id: operator_id, + }); + } + } + } + changeset } @@ -661,15 +762,15 @@ impl Validator { &self, tx: &Transaction, input_index: usize, - mut ptrout: PtrOut, + mut numout: NumOut, new_space_utxos: &Vec, changeset: &mut TxChangeSet, height: u32, data: &Option, ) { - let mut ptr = ptrout.sptr; + let mut ptr = numout.num; // if a corresponding output at the same index has the same value, - // that output becomes the PTR + // that output becomes the num let mut output_index = input_index; let mut output = match tx.output.get(input_index) { None => return, // cannot be rebound, if N doesn't exist, then we can skip n+1 rule check @@ -677,11 +778,11 @@ impl Validator { }; // if the values don't match, then we assume it's a trading tx - ptr should be at n+1 - if output.value != ptrout.value { + if output.value != numout.value { output_index = input_index + 1; output = match tx.output.get(output_index) { None => return, // no rebounds - Some(output) => output + Some(output) => output, }; } @@ -695,19 +796,18 @@ impl Validator { // 1. A data OP_RETURN is present // 2. PTR is P2TR and input uses SIGHASH_ALL (prevents malicious data injection) if let Some(new_data) = data { - if ptrout.script_pubkey.is_p2tr() && is_p2tr_sighash_all(tx, input_index) { + if numout.script_pubkey.is_p2tr() && is_p2tr_sighash_all(tx, input_index) { ptr.data = Some(new_data.clone()); } } - ptrout.n = output_index; - ptrout.value = output.value; - ptrout.script_pubkey = output.script_pubkey.clone(); - ptrout.sptr = ptr; - changeset.creates.push(ptrout); + numout.n = output_index; + numout.value = output.value; + numout.script_pubkey = output.script_pubkey.clone(); + numout.num = ptr; + changeset.creates.push(numout); } } - pub enum CommitmentOp { /// Add one or more new commitments Commit(Vec<[u8; 32]>), @@ -715,7 +815,7 @@ pub enum CommitmentOp { Rollback, } -pub enum PtrOp { +pub enum NumOp { Commitment(CommitmentOp), Data(Vec), } @@ -727,29 +827,26 @@ pub fn find_op_commit(tx_outputs: &[TxOut]) -> Option { tx_outputs.iter().find_map(|s| { let mut instructions = s.script_pubkey.instructions().skip(1); match (instructions.next()?.ok()?, instructions.next()?.ok()?) { - (Instruction::Op(OP_PUSHNUM_2), Instruction::PushBytes(payload)) => - { - if payload.is_empty() { - Some(CommitmentOp::Rollback) - } else if payload.len() % 32 != 0 { - None - } else { - let mut commitments = Vec::with_capacity(payload.len() / 32); - for chunk in payload.as_bytes().chunks_exact(32) { - let mut commitment = [0u8; 32]; - commitment.copy_from_slice(chunk); - commitments.push(commitment); - } - Some(CommitmentOp::Commit(commitments)) + (Instruction::Op(OP_PUSHNUM_2), Instruction::PushBytes(payload)) => { + if payload.is_empty() { + Some(CommitmentOp::Rollback) + } else if payload.len() % 32 != 0 { + None + } else { + let mut commitments = Vec::with_capacity(payload.len() / 32); + for chunk in payload.as_bytes().chunks_exact(32) { + let mut commitment = [0u8; 32]; + commitment.copy_from_slice(chunk); + commitments.push(commitment); } - - }, + Some(CommitmentOp::Commit(commitments)) + } + } _ => None, } }) } - // Create commitment scripts // Format: OP_RETURN OP_PUSHNUM_2 pub fn create_commitment_script(op: &CommitmentOp) -> ScriptBuf { @@ -775,7 +872,6 @@ pub fn create_commitment_script(op: &CommitmentOp) -> ScriptBuf { builder.into_script() } - /// Check if an input uses SIGHASH_ALL for a P2TR key path spend /// Per BIP 341: /// - 64 bytes = SIGHASH_DEFAULT (0x00), equivalent to SIGHASH_ALL @@ -796,13 +892,13 @@ fn is_p2tr_sighash_all(tx: &Transaction, input_index: usize) -> bool { let sig = &witness[0]; match sig.len() { - 64 => true, // SIGHASH_DEFAULT (0x00) - equivalent to SIGHASH_ALL + 64 => true, // SIGHASH_DEFAULT (0x00) - equivalent to SIGHASH_ALL 65 => sig[64] == 0x01, // SIGHASH_ALL (0x01) _ => false, } } -pub fn is_ptr_minting_locktime(lock_time: &LockTime) -> bool { +pub fn is_num_minting_locktime(lock_time: &LockTime) -> bool { if let LockTime::Seconds(s) = lock_time { return s.to_consensus_u32() % 1000 == 777; } @@ -821,7 +917,7 @@ impl PtrTrackableOutput for TxOut { #[cfg(feature = "serde")] mod serde_helpers { - use serde::{Deserializer, Serializer, Deserialize}; + use serde::{Deserialize, Deserializer, Serializer}; pub fn serialize_hash_serde(bytes: &[u8; 32], serializer: S) -> Result where @@ -882,8 +978,8 @@ use serde_helpers::*; #[cfg(feature = "serde")] mod hash_key_serde { - use serde::{Deserializer, Serializer}; use super::serde_helpers::*; + use serde::{Deserializer, Serializer}; macro_rules! impl_hash_key_serde { ($ty:ident) => { @@ -901,8 +997,6 @@ mod hash_key_serde { }; } - impl_hash_key_serde!(RegistryKey); + impl_hash_key_serde!(CommitmentTipKey); impl_hash_key_serde!(CommitmentKey); } - - diff --git a/ptr/src/sptr.rs b/nums/src/num_id.rs similarity index 58% rename from ptr/src/sptr.rs rename to nums/src/num_id.rs index 02f4e0a..70d28b4 100644 --- a/ptr/src/sptr.rs +++ b/nums/src/num_id.rs @@ -4,52 +4,52 @@ use bitcoin::{ScriptBuf}; use spaces_protocol::hasher::{Hash, KeyHash, KeyHasher}; use crate::{ns_hash, KeyKind}; -pub const SPTR_HRP: &str = "sptr"; +pub const NUM_HRP: &str = "num"; -impl KeyHash for Sptr {} +impl KeyHash for NumId {} #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct Sptr(pub(crate) [u8; 32]); +pub struct NumId(pub(crate) [u8; 32]); -impl Sptr { +impl NumId { #[inline] pub fn as_slice(&self) -> &[u8] { &self.0 } #[inline] pub fn to_bytes(self) -> [u8; 32] { self.0 } pub fn from_spk(spk: ScriptBuf) -> Self { - Self(ns_hash::(KeyKind::Sptr, H::hash(&spk.as_bytes()))) + Self(ns_hash::(KeyKind::NumId, H::hash(&spk.as_bytes()))) } } -impl From for Hash { - fn from(value: Sptr) -> Self { +impl From for Hash { + fn from(value: NumId) -> Self { value.0 } } #[derive(Debug)] -pub enum SptrParseError { +pub enum NumIdParseError { Bech32(bech32::DecodeError), InvalidHrp, InvalidLen, } -impl fmt::Display for SptrParseError { +impl fmt::Display for NumIdParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - SptrParseError::Bech32(e) => write!(f, "bech32 decode error: {e}"), - SptrParseError::InvalidHrp => f.write_str("invalid HRP for sptr"), - SptrParseError::InvalidLen => f.write_str("invalid data length; expected 32 bytes"), + NumIdParseError::Bech32(e) => write!(f, "bech32 decode error: {e}"), + NumIdParseError::InvalidHrp => f.write_str("invalid HRP for num id"), + NumIdParseError::InvalidLen => f.write_str("invalid data length; expected 32 bytes"), } } } -impl std::error::Error for SptrParseError {} +impl std::error::Error for NumIdParseError {} #[cfg(feature = "serde")] -impl serde::Serialize for Sptr { +impl serde::Serialize for NumId { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, @@ -59,14 +59,14 @@ impl serde::Serialize for Sptr { } #[cfg(feature = "serde")] -impl<'de> serde::Deserialize<'de> for Sptr { +impl<'de> serde::Deserialize<'de> for NumId { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { use core::str::FromStr; let s = String::deserialize(deserializer)?; - Sptr::from_str(&s).map_err(serde::de::Error::custom) + NumId::from_str(&s).map_err(serde::de::Error::custom) } } @@ -75,43 +75,43 @@ mod borsh_impl { use borsh::{io, BorshDeserialize, BorshSerialize}; use super::*; - impl BorshSerialize for Sptr { + impl BorshSerialize for NumId { fn serialize(&self, writer: &mut W) -> io::Result<()> { writer.write_all(&self.0) } } - impl BorshDeserialize for Sptr { + impl BorshDeserialize for NumId { fn deserialize_reader(reader: &mut R) -> io::Result { let mut bytes = [0u8; 32]; reader.read_exact(&mut bytes)?; - Ok(Sptr(bytes)) + Ok(NumId(bytes)) } } } -impl From for SptrParseError { - fn from(e: bech32::DecodeError) -> Self { SptrParseError::Bech32(e) } +impl From for NumIdParseError { + fn from(e: bech32::DecodeError) -> Self { NumIdParseError::Bech32(e) } } -impl fmt::Display for Sptr { +impl fmt::Display for NumId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let hrp = Hrp::parse(SPTR_HRP).map_err(|_| fmt::Error)?; + let hrp = Hrp::parse(NUM_HRP).map_err(|_| fmt::Error)?; let s = bech32::encode::(hrp, &self.0).map_err(|_| fmt::Error)?; f.write_str(&s) } } -impl FromStr for Sptr { - type Err = SptrParseError; +impl FromStr for NumId { + type Err = NumIdParseError; fn from_str(s: &str) -> Result { let (hrp, data) = bech32::decode(s)?; - if hrp.as_str() != SPTR_HRP { return Err(SptrParseError::InvalidHrp); } - if data.len() != 32 { return Err(SptrParseError::InvalidLen); } + if hrp.as_str() != NUM_HRP { return Err(NumIdParseError::InvalidHrp); } + if data.len() != 32 { return Err(NumIdParseError::InvalidLen); } let mut arr = [0u8; 32]; arr.copy_from_slice(&data); - Ok(Sptr(arr)) + Ok(NumId(arr)) } } @@ -121,10 +121,10 @@ mod tests { use bech32::{Bech32m}; #[test] - fn sptr_roundtrip() { - let x = Sptr([7u8; 32]); + fn num_id_roundtrip() { + let x = NumId([7u8; 32]); let s = x.to_string(); - let y: Sptr = s.parse().unwrap(); + let y: NumId = s.parse().unwrap(); assert_eq!(x, y); } @@ -132,15 +132,15 @@ mod tests { fn rejects_wrong_hrp() { let hrp = Hrp::parse("nope").unwrap(); let s = bech32::encode::(hrp, &[0u8; 32]).unwrap(); - let err = s.parse::().unwrap_err(); - matches!(err, SptrParseError::InvalidHrp); + let err = s.parse::().unwrap_err(); + matches!(err, NumIdParseError::InvalidHrp); } #[test] fn rejects_wrong_len() { - let hrp = Hrp::parse(SPTR_HRP).unwrap(); + let hrp = Hrp::parse(NUM_HRP).unwrap(); let s = bech32::encode::(hrp, &[0u8; 31]).unwrap(); - let err = s.parse::().unwrap_err(); - matches!(err, SptrParseError::InvalidLen); + let err = s.parse::().unwrap_err(); + matches!(err, NumIdParseError::InvalidLen); } } diff --git a/ptr/src/snumeric.rs b/nums/src/snumeric.rs similarity index 65% rename from ptr/src/snumeric.rs rename to nums/src/snumeric.rs index 3d9bdc7..d9b2429 100644 --- a/ptr/src/snumeric.rs +++ b/nums/src/snumeric.rs @@ -5,11 +5,12 @@ use spaces_protocol::slabel::SLabel; pub struct SNumeric { block: u32, tx_pos: u16, + vout: u16, } impl SNumeric { - pub fn new(block: u32, tx_pos: u16) -> Self { - Self { block, tx_pos } + pub fn new(block: u32, tx_pos: u16, vout: u16) -> Self { + Self { block, tx_pos, vout } } #[inline] @@ -18,6 +19,9 @@ impl SNumeric { #[inline] pub fn tx_pos(&self) -> u16 { self.tx_pos } + #[inline] + pub fn vout(&self) -> u16 { self.vout } + pub fn to_slabel(&self) -> SLabel { SLabel::from_str(&self.to_string()).expect("valid numeric label") } @@ -35,18 +39,20 @@ impl TryFrom for SNumeric { #[derive(Debug)] pub enum SNumericParseError { MissingPrefix, - MissingSeparator, + InvalidFormat, InvalidBlock(core::num::ParseIntError), InvalidTxPos(core::num::ParseIntError), + InvalidVout(core::num::ParseIntError), } impl fmt::Display for SNumericParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { SNumericParseError::MissingPrefix => f.write_str("expected '#' prefix"), - SNumericParseError::MissingSeparator => f.write_str("expected '#-' format"), + SNumericParseError::InvalidFormat => f.write_str("expected '#--' format"), SNumericParseError::InvalidBlock(e) => write!(f, "invalid block number: {e}"), SNumericParseError::InvalidTxPos(e) => write!(f, "invalid tx position: {e}"), + SNumericParseError::InvalidVout(e) => write!(f, "invalid vout: {e}"), } } } @@ -55,7 +61,7 @@ impl std::error::Error for SNumericParseError {} impl fmt::Display for SNumeric { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "#{}-{}", self.block, self.tx_pos) + write!(f, "#{}-{}-{}", self.block, self.tx_pos, self.vout) } } @@ -64,10 +70,14 @@ impl FromStr for SNumeric { fn from_str(s: &str) -> Result { let s = s.strip_prefix('#').ok_or(SNumericParseError::MissingPrefix)?; - let (block_str, pos_str) = s.split_once('-').ok_or(SNumericParseError::MissingSeparator)?; + let mut parts = s.splitn(3, '-'); + let block_str = parts.next().ok_or(SNumericParseError::InvalidFormat)?; + let pos_str = parts.next().ok_or(SNumericParseError::InvalidFormat)?; + let vout_str = parts.next().ok_or(SNumericParseError::InvalidFormat)?; let block = block_str.parse::().map_err(SNumericParseError::InvalidBlock)?; let tx_pos = pos_str.parse::().map_err(SNumericParseError::InvalidTxPos)?; - Ok(SNumeric { block, tx_pos }) + let vout = vout_str.parse::().map_err(SNumericParseError::InvalidVout)?; + Ok(SNumeric { block, tx_pos, vout }) } } @@ -80,9 +90,10 @@ impl serde::Serialize for SNumeric { if serializer.is_human_readable() { serializer.serialize_str(&self.to_string()) } else { - let mut buf = [0u8; 6]; + let mut buf = [0u8; 8]; buf[..4].copy_from_slice(&self.block.to_le_bytes()); - buf[4..].copy_from_slice(&self.tx_pos.to_le_bytes()); + buf[4..6].copy_from_slice(&self.tx_pos.to_le_bytes()); + buf[6..8].copy_from_slice(&self.vout.to_le_bytes()); serializer.serialize_bytes(&buf) } } @@ -98,10 +109,11 @@ impl<'de> serde::Deserialize<'de> for SNumeric { let s = String::deserialize(deserializer)?; SNumeric::from_str(&s).map_err(serde::de::Error::custom) } else { - let buf = <[u8; 6]>::deserialize(deserializer)?; + let buf = <[u8; 8]>::deserialize(deserializer)?; Ok(SNumeric { block: u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]), tx_pos: u16::from_le_bytes([buf[4], buf[5]]), + vout: u16::from_le_bytes([buf[6], buf[7]]), }) } } @@ -115,7 +127,8 @@ mod borsh_impl { impl BorshSerialize for SNumeric { fn serialize(&self, writer: &mut W) -> io::Result<()> { self.block.serialize(writer)?; - self.tx_pos.serialize(writer) + self.tx_pos.serialize(writer)?; + self.vout.serialize(writer) } } @@ -123,7 +136,8 @@ mod borsh_impl { fn deserialize_reader(reader: &mut R) -> io::Result { let block = u32::deserialize_reader(reader)?; let tx_pos = u16::deserialize_reader(reader)?; - Ok(SNumeric { block, tx_pos }) + let vout = u16::deserialize_reader(reader)?; + Ok(SNumeric { block, tx_pos, vout }) } } } @@ -134,49 +148,55 @@ mod tests { #[test] fn roundtrip() { - let x = SNumeric::new(800_000, 3); + let x = SNumeric::new(800_000, 3, 1); let s = x.to_string(); - assert_eq!(s, "#800000-3"); + assert_eq!(s, "#800000-3-1"); let y: SNumeric = s.parse().unwrap(); assert_eq!(x, y); } #[test] fn zero() { - let x = SNumeric::new(0, 0); - assert_eq!(x.to_string(), "#0-0"); - assert_eq!("#0-0".parse::().unwrap(), x); + let x = SNumeric::new(0, 0, 0); + assert_eq!(x.to_string(), "#0-0-0"); + assert_eq!("#0-0-0".parse::().unwrap(), x); } #[test] fn rejects_missing_prefix() { - let err = "800000-3".parse::().unwrap_err(); + let err = "800000-3-1".parse::().unwrap_err(); matches!(err, SNumericParseError::MissingPrefix); } #[test] - fn rejects_missing_separator() { - let err = "#800000".parse::().unwrap_err(); - matches!(err, SNumericParseError::MissingSeparator); + fn rejects_missing_vout() { + let err = "#800000-3".parse::().unwrap_err(); + matches!(err, SNumericParseError::InvalidFormat); } #[test] fn rejects_invalid_block() { - let err = "#abc-3".parse::().unwrap_err(); + let err = "#abc-3-1".parse::().unwrap_err(); matches!(err, SNumericParseError::InvalidBlock(_)); } #[test] fn rejects_invalid_tx_pos() { - let err = "#800000-abc".parse::().unwrap_err(); + let err = "#800000-abc-1".parse::().unwrap_err(); matches!(err, SNumericParseError::InvalidTxPos(_)); } + #[test] + fn rejects_invalid_vout() { + let err = "#800000-3-abc".parse::().unwrap_err(); + matches!(err, SNumericParseError::InvalidVout(_)); + } + #[test] fn slabel_roundtrip() { - let x = SNumeric::new(800_000, 3); + let x = SNumeric::new(800_000, 3, 1); let label = x.to_slabel(); - assert_eq!(label.to_string(), "#800000-3"); + assert_eq!(label.to_string(), "#800000-3-1"); assert!(label.is_numeric()); let y = SNumeric::try_from(label).unwrap(); assert_eq!(x, y); diff --git a/protocol/src/slabel.rs b/protocol/src/slabel.rs index fdeec6f..dab8875 100644 --- a/protocol/src/slabel.rs +++ b/protocol/src/slabel.rs @@ -161,19 +161,27 @@ impl<'a> TryFrom<&'a [u8]> for SLabelRef<'a> { } let label = &value[..=len]; - // Numeric label: #- + // Numeric label: #-- if label[1] == b'#' { let content = &label[2..]; - let dash_pos = content.iter().position(|&c| c == b'-') + // Find first dash (block-txpos boundary) + let d1 = content.iter().position(|&c| c == b'-') + .filter(|&p| p > 0) + .ok_or(Error::Name(NameErrorKind::InvalidCharacter))?; + let rest = &content[d1 + 1..]; + // Find second dash (txpos-vout boundary) + let d2 = rest.iter().position(|&c| c == b'-') .filter(|&p| p > 0) .ok_or(Error::Name(NameErrorKind::InvalidCharacter))?; - let (before, after) = content.split_at(dash_pos); - let after = &after[1..]; // skip the dash + let block = &content[..d1]; + let tx_pos = &rest[..d2]; + let vout = &rest[d2 + 1..]; - if after.is_empty() - || !before.iter().all(|c| c.is_ascii_digit()) - || !after.iter().all(|c| c.is_ascii_digit()) + if block.is_empty() || tx_pos.is_empty() || vout.is_empty() + || !block.iter().all(|c| c.is_ascii_digit()) + || !tx_pos.iter().all(|c| c.is_ascii_digit()) + || !vout.iter().all(|c| c.is_ascii_digit()) { return Err(Error::Name(NameErrorKind::InvalidCharacter)); } @@ -504,20 +512,20 @@ mod tests { #[test] fn test_numeric_valid() { - let label = SLabel::try_from("#800000-3").unwrap(); - assert_eq!(label.to_string(), "#800000-3"); + let label = SLabel::try_from("#800000-3-1").unwrap(); + assert_eq!(label.to_string(), "#800000-3-1"); assert!(label.is_numeric()); - let label = SLabel::try_from("#0-0").unwrap(); - assert_eq!(label.to_string(), "#0-0"); + let label = SLabel::try_from("#0-0-0").unwrap(); + assert_eq!(label.to_string(), "#0-0-0"); assert!(label.is_numeric()); - let label = SLabel::try_from("#1-1").unwrap(); - assert_eq!(label.to_string(), "#1-1"); + let label = SLabel::try_from("#1-1-0").unwrap(); + assert_eq!(label.to_string(), "#1-1-0"); // Large values - let label = SLabel::try_from("#4294967295-65535").unwrap(); - assert_eq!(label.to_string(), "#4294967295-65535"); + let label = SLabel::try_from("#4294967295-65535-65535").unwrap(); + assert_eq!(label.to_string(), "#4294967295-65535-65535"); } #[test] @@ -525,24 +533,31 @@ mod tests { // Just "#" with nothing after assert!(SLabel::try_from("#").is_err(), "bare # should be invalid"); - // Missing separator - assert!(SLabel::try_from("#123").is_err(), "missing dash"); + // Missing separators + assert!(SLabel::try_from("#123").is_err(), "missing dashes"); + + // Only two parts (missing vout) + assert!(SLabel::try_from("#123-4").is_err(), "missing vout"); + + // No digits before first dash + assert!(SLabel::try_from("#-3-1").is_err(), "no digits before first dash"); - // No digits before dash - assert!(SLabel::try_from("#-3").is_err(), "no digits before dash"); + // No digits between dashes + assert!(SLabel::try_from("#3--1").is_err(), "no digits between dashes"); - // No digits after dash - assert!(SLabel::try_from("#3-").is_err(), "no digits after dash"); + // No digits after second dash + assert!(SLabel::try_from("#3-4-").is_err(), "no digits after second dash"); // Non-digit characters - assert!(SLabel::try_from("#abc-3").is_err(), "letters before dash"); - assert!(SLabel::try_from("#3-abc").is_err(), "letters after dash"); + assert!(SLabel::try_from("#abc-3-1").is_err(), "letters in block"); + assert!(SLabel::try_from("#3-abc-1").is_err(), "letters in txpos"); + assert!(SLabel::try_from("#3-4-abc").is_err(), "letters in vout"); - // Multiple dashes - assert!(SLabel::try_from("#3-4-5").is_err(), "multiple dashes"); + // Too many dashes + assert!(SLabel::try_from("#3-4-5-6").is_err(), "four parts"); // Spaces - assert!(SLabel::try_from("# 3-4").is_err(), "space in numeric"); + assert!(SLabel::try_from("# 3-4-1").is_err(), "space in numeric"); // Dash only content assert!(SLabel::try_from("#-").is_err(), "just dash after #"); @@ -553,41 +568,41 @@ mod tests { let named = SLabel::try_from("@example").unwrap(); assert!(!named.is_numeric()); - let numeric = SLabel::try_from("#100-5").unwrap(); + let numeric = SLabel::try_from("#100-5-0").unwrap(); assert!(numeric.is_numeric()); } #[test] fn test_numeric_display_no_at_prefix() { - let label = SLabel::try_from("#100-5").unwrap(); - // Should display as "#100-5" NOT "@#100-5" - assert_eq!(format!("{}", label), "#100-5"); + let label = SLabel::try_from("#100-5-0").unwrap(); + // Should display as "#100-5-0" NOT "@#100-5-0" + assert_eq!(format!("{}", label), "#100-5-0"); } #[test] fn test_numeric_fromstr_roundtrip() { - let original = "#999-42"; + let original = "#999-42-7"; let label = SLabel::from_str(original).unwrap(); assert_eq!(label.to_string(), original); } #[test] fn test_numeric_raw_bytes() { - let label = SLabel::try_from("#1-2").unwrap(); - // Content stored is "#1-2" (4 bytes), length byte = 4 + let label = SLabel::try_from("#1-2-3").unwrap(); + // Content stored is "#1-2-3" (6 bytes), length byte = 6 let raw = label.as_ref(); - assert_eq!(raw[0], 4); // length - assert_eq!(&raw[1..], b"#1-2"); + assert_eq!(raw[0], 6); // length + assert_eq!(&raw[1..], b"#1-2-3"); } #[test] fn test_numeric_slabelref() { - // Build raw bytes: length + "#1-2" - let bytes = b"\x04#1-2"; + // Build raw bytes: length + "#1-2-3" + let bytes = b"\x06#1-2-3"; let label_ref = SLabelRef::try_from(bytes.as_slice()).unwrap(); assert!(label_ref.is_numeric()); let owned = label_ref.to_owned(); assert!(owned.is_numeric()); - assert_eq!(owned.to_string(), "#1-2"); + assert_eq!(owned.to_string(), "#1-2-3"); } } diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index 63ad923..7e7faf0 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] spaces_protocol = { path = "../protocol", features = ["std"] } -spaces_ptr = { path = "../ptr", features = ["std"] } +spaces_nums = { path = "../nums", features = ["std"] } bitcoin = { version = "0.32.8", features = ["base64", "serde"] } bdk_wallet = { workspace = true } secp256k1 = { workspace = true } diff --git a/wallet/src/builder.rs b/wallet/src/builder.rs index 70e5575..8d278d4 100644 --- a/wallet/src/builder.rs +++ b/wallet/src/builder.rs @@ -1,11 +1,9 @@ -use std::{ - cmp::min, - collections::BTreeMap, - default::Default, - ops::{Add, Mul}, - str::FromStr, +use crate::{ + address::SpaceAddress, tx_event::TxRecord, DoubleUtxo, FullTxOut, SpaceScriptSigningInfo, + SpacesWallet, }; use anyhow::{anyhow, Context}; + use bdk_wallet::{ coin_selection::{ CoinSelectionAlgorithm, CoinSelectionResult, DefaultCoinSelectionAlgorithm, @@ -20,17 +18,21 @@ use bitcoin::{ Amount, FeeRate, Network, OutPoint, Psbt, Script, ScriptBuf, Sequence, Transaction, TxOut, Txid, Weight, Witness, }; + +use spaces_nums::{create_commitment_script, CommitmentOp, FullNumOut}; +use spaces_protocol::hasher::Hash; +use spaces_protocol::script::{create_data_script, create_open_data, nop_script}; use spaces_protocol::{ bitcoin::absolute::Height, constants::{BID_PSBT_INPUT_SEQUENCE, BID_PSBT_TX_VERSION}, Covenant, FullSpaceOut, Space, }; -use spaces_protocol::hasher::Hash; -use spaces_protocol::script::{create_data_script, create_open_data, nop_script}; -use spaces_ptr::{create_commitment_script, CommitmentOp, FullPtrOut}; -use crate::{ - address::SpaceAddress, tx_event::TxRecord, DoubleUtxo, FullTxOut, SpaceScriptSigningInfo, - SpacesWallet, +use std::{ + cmp::min, + collections::BTreeMap, + default::Default, + ops::{Add, Mul}, + str::FromStr, }; #[derive(Debug, Clone)] @@ -86,8 +88,9 @@ pub enum StackRequest { Transfer(SpaceTransfer), Send(CoinTransfer), Execute(ExecuteRequest), - Ptr(PtrRequest), - PtrTransfer(PtrTransfer), + Num(NumRequest), + NumTransfer(NumTransfer), + NumDelegate(NumDelegate), Commitment(CommitmentRequest), } @@ -95,7 +98,9 @@ pub enum StackOp { Prepare(CreateParams), Open(OpenRevealParams), Bid(BidRequest), - Ptr(PtrParams), + Num(NumParams), + NumDelegate(NumDelegate), + Commitment(Vec), } #[derive(Clone)] @@ -118,13 +123,13 @@ pub struct RegisterRequest { } #[derive(Debug, Clone)] -pub struct PtrRequest { - pub spk: ScriptBuf, +pub struct NumRequest { + pub bind_spk: ScriptBuf, } #[derive(Debug, Clone)] pub struct CommitmentRequest { - pub ptrout: FullPtrOut, + pub numout: FullNumOut, pub root: Option, } @@ -132,15 +137,23 @@ pub struct CommitmentRequest { pub struct SpaceTransfer { pub space: FullSpaceOut, pub recipient: SpaceAddress, - pub create_ptr: bool, + pub create_num: bool, } #[derive(Debug, Clone)] -pub struct PtrTransfer { - pub ptr: FullPtrOut, +pub struct NumTransfer { + pub num: FullNumOut, pub recipient: SpaceAddress, } +#[derive(Debug, Clone)] +pub struct NumDelegate { + pub num: FullNumOut, + // A new spk not used or delegated to by any num + // owned by the wallet + pub unique_num_spk: ScriptBuf, +} + #[derive(Debug, Clone)] pub struct CoinTransfer { pub amount: Amount, @@ -159,13 +172,12 @@ pub struct CreateParams { transfers: Vec, sends: Vec, bidouts: Option, - data: Option> + data: Option>, } -pub struct PtrParams { - transfers: Vec, - binds: Vec, - commitments: Vec, +pub struct NumParams { + transfers: Vec, + binds: Vec, data: Option>, } @@ -268,7 +280,7 @@ impl<'a, Cs: CoinSelectionAlgorithm> TxBuilderSpacesUtils<'a, Cs> for TxBuilder< placeholder.auction.outpoint.vout as u8, &offer, )?) - .expect("compressed psbt script bytes"); + .expect("compressed psbt script bytes"); let carrier = ScriptBuf::new_op_return(&compressed_psbt); @@ -357,7 +369,7 @@ impl Builder { fee_rate: FeeRate, dust: Option, confirmed_only: bool, - data: Option> + data: Option>, ) -> anyhow::Result<(Transaction, Vec)> { let mut vout: u32 = 0; let mut tap_outputs = Vec::new(); @@ -394,7 +406,7 @@ impl Builder { builder.nlocktime(signal_space_utxo_tracking_lock_time(median_time)); // handle transfers and collect PTR addresses - let mut ptr_addresses = Vec::new(); + let mut num_addresses = Vec::new(); if !space_transfers.is_empty() { // Must be an odd number of outputs so that // transfers align correctly @@ -408,8 +420,8 @@ impl Builder { vout += 1; } for transfer in space_transfers { - if transfer.create_ptr { - ptr_addresses.push(transfer.recipient.script_pubkey()); + if transfer.create_num { + num_addresses.push(transfer.recipient.script_pubkey()); } builder.add_transfer(transfer)?; vout += 1; @@ -443,7 +455,7 @@ impl Builder { } // Add PTR outputs for delegations - if !ptr_addresses.is_empty() { + if !num_addresses.is_empty() { if let Some(data) = data { let data_script = create_data_script(&data); builder.add_recipient(data_script, Amount::from_sat(0)); @@ -455,11 +467,8 @@ impl Builder { } vout += 1; - for spk in ptr_addresses { - builder.add_recipient( - spk, - ptr_utxo_dust(Amount::from_sat(1000)), - ); + for spk in num_addresses { + builder.add_recipient(spk, num_utxo_dust(Amount::from_sat(1000))); vout += 1; } } else if let Some(data) = data { @@ -641,21 +650,46 @@ impl Iterator for BuilderIterator<'_> { detailed })) } - StackOp::Ptr(params) => { - let tx = Builder::ptr_tx( + StackOp::Num(params) => { + let tx = create_num_tx( self.wallet, self.median_time, self.fee_rate, self.unspendables.clone(), - self.confirmed_only, self.force, + self.confirmed_only, + self.force, params, ); Some(tx.map(|tx| { let detailed = TxRecord::new(tx); - // TODO: add ptr metadata + // TODO: add num metadata detailed })) } + StackOp::NumDelegate(d) => { + let tx = create_num_delegate_tx( + self.wallet, + self.median_time, + self.fee_rate, + self.unspendables.clone(), + self.confirmed_only, + self.force, + d, + ); + Some(tx.map(|tx| TxRecord::new(tx))) + } + StackOp::Commitment(commitments) => { + let tx = create_commitment_tx( + self.wallet, + self.median_time, + self.fee_rate, + self.unspendables.clone(), + self.confirmed_only, + self.force, + commitments, + ); + Some(tx.map(|tx| TxRecord::new(tx))) + } } } } @@ -693,8 +727,7 @@ impl Builder { } pub fn add_commitment(mut self, req: CommitmentRequest) -> Self { - self.requests - .push(StackRequest::Commitment(req)); + self.requests.push(StackRequest::Commitment(req)); self } @@ -717,8 +750,13 @@ impl Builder { self } - pub fn add_ptr_transfer(mut self, request: PtrTransfer) -> Self { - self.requests.push(StackRequest::PtrTransfer(request)); + pub fn add_num_transfer(mut self, request: NumTransfer) -> Self { + self.requests.push(StackRequest::NumTransfer(request)); + self + } + + pub fn add_num_delegate(mut self, request: NumDelegate) -> Self { + self.requests.push(StackRequest::NumDelegate(request)); self } @@ -727,8 +765,8 @@ impl Builder { self } - pub fn add_ptr(mut self, request: PtrRequest) -> Self { - self.requests.push(StackRequest::Ptr(request)); + pub fn add_num(mut self, request: NumRequest) -> Self { + self.requests.push(StackRequest::Num(request)); self } @@ -817,8 +855,9 @@ impl Builder { let mut transfers = Vec::new(); let mut sends = Vec::new(); let mut executes = Vec::new(); - let mut ptrs = Vec::new(); - let mut ptr_transfers = Vec::new(); + let mut nums = Vec::new(); + let mut num_transfers = Vec::new(); + let mut num_delegates = Vec::new(); let mut commitments = Vec::new(); for req in self.requests { match req { @@ -832,14 +871,15 @@ impl Builder { transfers.push(SpaceTransfer { space: params.space, recipient: to, - create_ptr: false, + create_num: false, }) } StackRequest::Send(send) => sends.push(send), StackRequest::Transfer(params) => transfers.push(params), StackRequest::Execute(params) => executes.push(params), - StackRequest::Ptr(params) => ptrs.push(params), - StackRequest::PtrTransfer(params) => ptr_transfers.push(params), + StackRequest::Num(params) => nums.push(params), + StackRequest::NumTransfer(params) => num_transfers.push(params), + StackRequest::NumDelegate(params) => num_delegates.push(params), StackRequest::Commitment(req) => commitments.push(req), } } @@ -865,14 +905,21 @@ impl Builder { })); } - if !ptrs.is_empty() || !ptr_transfers.is_empty() || !commitments.is_empty() { - let params = PtrParams { - transfers: ptr_transfers, - binds: ptrs, - commitments, + if !nums.is_empty() || !num_transfers.is_empty() { + let params = NumParams { + transfers: num_transfers, + binds: nums, data: self.data.clone(), }; - stack.push(StackOp::Ptr(params)) + stack.push(StackOp::Num(params)) + } + + for d in num_delegates { + stack.push(StackOp::NumDelegate(d)); + } + + if !commitments.is_empty() { + stack.push(StackOp::Commitment(commitments)); } Ok(BuilderIterator { @@ -887,95 +934,6 @@ impl Builder { }) } - fn ptr_tx( - w: &mut SpacesWallet, - median_time: u64, - fee_rate: FeeRate, - unspendables: Vec, - confirmed_only: bool, - _force: bool, - params: PtrParams, - ) -> anyhow::Result { - let mut builder = w.build_tx(unspendables, confirmed_only)?; - builder - .nlocktime(signal_ptr_tracking_lock_time(median_time)) - .fee_rate(fee_rate); - - // Handle commitments: - if !params.commitments.is_empty() { - if !params.transfers.is_empty() || !params.binds.is_empty() { - return Err(anyhow!("combining commitments with binds and transfers is not yet supported")); - } - - let script = if params.commitments.iter().all(|c| c.root.is_none()) { - // rollback - create_commitment_script(&CommitmentOp::Rollback) - } else if params.commitments.iter().all(|c| c.root.is_some()) { - let roots = params.commitments.iter() - .map(|c| c.root.unwrap()).collect::>(); - create_commitment_script(&CommitmentOp::Commit(roots)) - } else { - return Err(anyhow!("cannot combine rollbacks and new commitments in the same tx")); - }; - - // Transfer ptrs we're committing - for c in params.commitments { - let outpoint = OutPoint { - txid: c.ptrout.txid, - vout: c.ptrout.ptrout.n as _, - }; - // spend ptr - builder.add_utxo(outpoint) - .map_err(|e| anyhow!("could not spend sptr at {}:{}", outpoint, e))?; - // add replacement at the same index - builder.add_recipient(c.ptrout.ptrout.script_pubkey.clone(), c.ptrout.ptrout.value); - } - - // Add OP_RETURN commitment last - builder.add_recipient(script, Amount::from_sat(0)); - - let psbt = builder.finish()?; - let signed = w.sign(psbt, None)?; - return Ok(signed); - } - - let has_transfers = !params.transfers.is_empty(); - - // Handle transfers: - for transfer in params.transfers { - let outpoint = OutPoint { - txid: transfer.ptr.txid, - vout: transfer.ptr.ptrout.n as _, - }; - - // spend ptr - builder.add_utxo(outpoint) - .map_err(|e| anyhow!("could not transfer ptr at {}:{}", outpoint, e))?; - // add replacement output at the same index - builder.add_recipient(transfer.recipient.script_pubkey(), transfer.ptr.ptrout.value); - } - - // Handle binds: add any binds last to not mess with input/output order for transfers - for ptr in params.binds { - builder.add_recipient( - ptr.spk, - ptr_utxo_dust(Amount::from_sat(1000)), - ); - } - - // Add data OP_RETURN if present (only makes sense with transfers) - if let Some(data) = params.data { - if has_transfers { - let script = create_data_script(&data); - builder.add_recipient(script, Amount::from_sat(0)); - } - } - - let psbt = builder.finish()?; - let signed = w.sign(psbt, None)?; - Ok(signed) - } - fn bid_tx( w: &mut SpacesWallet, prev: FullSpaceOut, @@ -1116,9 +1074,9 @@ impl CoinSelectionAlgorithm for SpacesAwareCoinSelection { weighted_utxo.utxo.txout().value > SpacesAwareCoinSelection::DUST_THRESHOLD && !self - .exclude_outputs - .iter() - .any(|o| o == &weighted_utxo.utxo.outpoint()) + .exclude_outputs + .iter() + .any(|o| o == &weighted_utxo.utxo.outpoint()) }); let mut result = self.default_algorithm.coin_select( @@ -1149,17 +1107,25 @@ impl CoinSelectionAlgorithm for SpacesAwareCoinSelection { } } -pub fn signal_ptr_tracking_lock_time(median_time: u64) -> LockTime { +pub fn signal_num_tracking_lock_time(median_time: u64) -> LockTime { let median_time = min(median_time, u32::MAX as u64) as u32; let magic_time = median_time - (median_time % 1000) - (1000 - 777); LockTime::from_time(magic_time).expect("valid time") } -pub fn ptr_utxo_dust(amount: Amount) -> Amount { +pub fn num_utxo_dust(amount: Amount) -> Amount { let amount = amount.to_sat(); Amount::from_sat(amount - (amount % 10) + 7) } +pub fn num_utxo_delegate_dust(amount: Amount) -> Amount { + let amount = amount.to_sat(); + Amount::from_sat(amount - (amount % 10) + 8) +} + +pub fn is_num_utxo_delegate_dust(amount: Amount) -> bool { + amount.to_sat() % 10 == 8 +} pub fn signal_space_utxo_tracking_lock_time(median_time: u64) -> LockTime { let median_time = min(median_time, u32::MAX as u64) as u32; @@ -1191,3 +1157,151 @@ pub fn space_dust(amount: Amount) -> Amount { pub fn is_space_dust(amount: Amount) -> bool { amount.to_sat() % 10 == 6 } + +fn create_num_delegate_tx( + w: &mut SpacesWallet, + median_time: u64, + fee_rate: FeeRate, + unspendables: Vec, + confirmed_only: bool, + _force: bool, + d: NumDelegate, +) -> anyhow::Result { + let mut builder = w.build_tx(unspendables, confirmed_only)?; + builder + .nlocktime(signal_num_tracking_lock_time(median_time)) + .fee_rate(fee_rate); + + // Spend the num we want to delegate at input N + builder + .add_utxo(d.num.outpoint()) + .map_err(|e| anyhow!("could not transfer num at {}:{}", d.num.outpoint(), e))?; + + // if num utxo is already delegate dust signaling, + // we can do a normal transfer: input n -> output n (keep same value) + let (a, b) = if is_num_utxo_delegate_dust(d.num.numout.value) { + (d.num.numout.value, num_utxo_dust(Amount::from_sat(1000))) + } else { + // Swap the value order so we use N+1 rule: + // output N gets a new mint with a DIFFERENT value to trigger the fall, + // output N+1 gets the original num rebound with delegate dust. + let new_mint = if d.num.numout.value == num_utxo_dust(Amount::from_sat(1000)) { + num_utxo_dust(Amount::from_sat(800)) + } else { + num_utxo_dust(Amount::from_sat(1000)) + }; + ( + new_mint, + num_utxo_delegate_dust(d.num.numout.value), + ) + }; + builder.add_recipient(d.unique_num_spk.clone(), a); + builder.add_recipient(d.unique_num_spk, b); + + let psbt = builder.finish()?; + let signed = w.sign(psbt, None)?; + Ok(signed) +} + +fn create_commitment_tx( + w: &mut SpacesWallet, + median_time: u64, + fee_rate: FeeRate, + unspendables: Vec, + confirmed_only: bool, + _force: bool, + commitments: Vec, +) -> anyhow::Result { + let mut builder = w.build_tx(unspendables, confirmed_only)?; + builder + .nlocktime(signal_num_tracking_lock_time(median_time)) + .fee_rate(fee_rate); + + let script = if commitments.iter().all(|c| c.root.is_none()) { + // rollback + create_commitment_script(&CommitmentOp::Rollback) + } else if commitments.iter().all(|c| c.root.is_some()) { + let roots = commitments + .iter() + .map(|c| c.root.unwrap()) + .collect::>(); + create_commitment_script(&CommitmentOp::Commit(roots)) + } else { + return Err(anyhow!( + "cannot combine rollbacks and new commitments in the same tx" + )); + }; + + // Transfer nums we're committing + for c in commitments { + let outpoint = OutPoint { + txid: c.numout.txid, + vout: c.numout.numout.n as _, + }; + // spend num + builder + .add_utxo(outpoint) + .map_err(|e| anyhow!("could not spend snum at {}:{}", outpoint, e))?; + // add replacement at the same index + builder.add_recipient(c.numout.numout.script_pubkey.clone(), c.numout.numout.value); + } + + // Add OP_RETURN commitment last + builder.add_recipient(script, Amount::from_sat(0)); + + let psbt = builder.finish()?; + let signed = w.sign(psbt, None)?; + Ok(signed) +} + +fn create_num_tx( + w: &mut SpacesWallet, + median_time: u64, + fee_rate: FeeRate, + unspendables: Vec, + confirmed_only: bool, + _force: bool, + params: NumParams, +) -> anyhow::Result { + let mut builder = w.build_tx(unspendables, confirmed_only)?; + builder + .nlocktime(signal_num_tracking_lock_time(median_time)) + .fee_rate(fee_rate); + + let has_transfers = !params.transfers.is_empty(); + + // Handle transfers: + for transfer in params.transfers { + let outpoint = OutPoint { + txid: transfer.num.txid, + vout: transfer.num.numout.n as _, + }; + + // spend num + builder + .add_utxo(outpoint) + .map_err(|e| anyhow!("could not transfer num at {}:{}", outpoint, e))?; + // add replacement output at the same index + builder.add_recipient( + transfer.recipient.script_pubkey(), + transfer.num.numout.value, + ); + } + + // Handle binds: add any binds last to not mess with input/output order for transfers + for num in params.binds { + builder.add_recipient(num.bind_spk, num_utxo_dust(Amount::from_sat(1000))); + } + + // Add data OP_RETURN if present (only makes sense with transfers) + if let Some(data) = params.data { + if has_transfers { + let script = create_data_script(&data); + builder.add_recipient(script, Amount::from_sat(0)); + } + } + + let psbt = builder.finish()?; + let signed = w.sign(psbt, None)?; + Ok(signed) +} diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 82772c9..5926695 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -1,4 +1,4 @@ -use std::{collections::BTreeMap, fmt::Debug, fs, ops::Mul, path::PathBuf, str::FromStr}; +use std::{collections::BTreeMap, fmt, fmt::Debug, fs, ops::Mul, path::PathBuf, str::FromStr}; use anyhow::{anyhow, Context}; use bdk_wallet::{ chain, @@ -46,8 +46,8 @@ use spaces_protocol::{ slabel::SLabel, Covenant, FullSpaceOut, Space, }; -use spaces_ptr::{NumericKey, PtrSource, sptr::Sptr}; -use spaces_ptr::snumeric::SNumeric; +use spaces_nums::{NumericKey, NumSource, num_id::{NumId, NUM_HRP}}; +use spaces_nums::snumeric::SNumeric; use crate::{ address::SpaceAddress, @@ -84,20 +84,60 @@ pub struct Balance { pub details: BalanceDetails, } -/// A space name, sptr, or numeric identifier +/// A space name (@bitcoin), numeric (#800000-3), or num id (num1...) #[derive(Debug, Clone)] pub enum Subject { - Space(SLabel), - Ptr(Sptr), - Numeric(SNumeric), + Label(SLabel), + NumId(NumId), +} + +impl From for Subject { + fn from(label: SLabel) -> Self { + Subject::Label(label) + } +} + +impl From for Subject { + fn from(id: NumId) -> Self { + Subject::NumId(id) + } +} + +impl fmt::Display for Subject { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Subject::Label(label) => write!(f, "{}", label), + Subject::NumId(id) => write!(f, "{}", id), + } + } +} + +impl FromStr for Subject { + type Err = String; + + fn from_str(s: &str) -> Result { + if s.starts_with(&format!("{}1", NUM_HRP)) { + NumId::from_str(s) + .map(Subject::NumId) + .map_err(|e| format!("invalid num id: {}", e)) + } else { + let normalized = if s.starts_with('#') || s.starts_with('@') { + s.to_ascii_lowercase() + } else { + format!("@{}", s.to_ascii_lowercase()) + }; + SLabel::from_str(&normalized) + .map(Subject::Label) + .map_err(|e| format!("invalid space or numeric: {}", e)) + } + } } impl Serialize for Subject { fn serialize(&self, serializer: S) -> Result { match self { - Subject::Space(label) => serializer.serialize_str(&label.to_string()), - Subject::Ptr(sptr) => serializer.serialize_str(&sptr.to_string()), - Subject::Numeric(num) => serializer.serialize_str(&num.to_string()), + Subject::Label(label) => serializer.serialize_str(&label.to_string()), + Subject::NumId(id) => serializer.serialize_str(&id.to_string()), } } } @@ -108,19 +148,7 @@ impl<'de> Deserialize<'de> for Subject { D: Deserializer<'de>, { let s = ::deserialize(deserializer)?; - if s.starts_with("sptr1") { - Sptr::from_str(&s) - .map(Subject::Ptr) - .map_err(serde::de::Error::custom) - } else if s.starts_with('#') { - SNumeric::from_str(&s) - .map(Subject::Numeric) - .map_err(serde::de::Error::custom) - } else { - SLabel::from_str(&s) - .map(Subject::Space) - .map_err(serde::de::Error::custom) - } + Subject::from_str(&s).map_err(serde::de::Error::custom) } } @@ -438,14 +466,22 @@ impl SpacesWallet { TxEvent::get_latest_events(&db_tx).context("could not read latest events") } - pub fn sign_event( + pub fn sign_event( &mut self, src: &mut S, subject: Subject, mut event: NostrEvent, ) -> anyhow::Result { let outpoint = match &subject { - Subject::Space(label) => { + Subject::Label(label) if label.is_numeric() => { + let numeric: SNumeric = label.clone().try_into().unwrap(); + let key = NumericKey::from_numeric::(&numeric); + let id = src.get_num_id(&key)? + .ok_or_else(|| anyhow::anyhow!("Numeric '{}' not found", numeric))?; + src.get_num_outpoint_by_id(&id)? + .ok_or_else(|| anyhow::anyhow!("Num id not found"))? + } + Subject::Label(label) => { if event.space().is_some_and(|s| s != label.to_string()) { return Err(anyhow::anyhow!("Space tag does not match specified space")); } @@ -453,23 +489,14 @@ impl SpacesWallet { src.get_space_outpoint(&space_key)? .ok_or_else(|| anyhow::anyhow!("Space not found"))? } - Subject::Ptr(sptr) => { - src.get_ptr_outpoint(sptr)? - .ok_or_else(|| anyhow::anyhow!("Sptr not found"))? - } - Subject::Numeric(numeric) => { - let key = NumericKey::from_numeric::(numeric); - let sptr = src.get_numeric(&key)? - .ok_or_else(|| anyhow::anyhow!("Numeric '{}' not found", numeric))?; - src.get_ptr_outpoint(&sptr)? - .ok_or_else(|| anyhow::anyhow!("Sptr not found"))? + Subject::NumId(id) => { + src.get_num_outpoint_by_id(id)? + .ok_or_else(|| anyhow::anyhow!("Num id not found"))? } }; // We use list_output instead of get_utxo because the output might // be spent in a pending tx, so signatures are still valid until confirmed. - // Mainly useful if subs is trying to sign a temporary certificate, but it already - // broadcasted a commitment spending the ptr. let utxo = self.internal.list_output().find(|o| o.outpoint == outpoint) .clone().ok_or_else(|| anyhow::anyhow!("Not owned by wallet"))?; @@ -481,13 +508,24 @@ impl SpacesWallet { Ok(event) } - pub fn verify_event( + pub fn verify_event( src: &mut S, subject: Subject, mut event: NostrEvent, ) -> anyhow::Result { let script_pubkey = match &subject { - Subject::Space(label) => { + Subject::Label(label) if label.is_numeric() => { + let numeric: SNumeric = label.clone().try_into().unwrap(); + let key = NumericKey::from_numeric::(&numeric); + let id = src.get_num_id(&key)? + .ok_or_else(|| anyhow::anyhow!("Numeric '{}' not found", numeric))?; + let outpoint = src.get_num_outpoint_by_id(&id)? + .ok_or_else(|| anyhow::anyhow!("Num id not found"))?; + let numout = src.get_numout(&outpoint)? + .ok_or_else(|| anyhow::anyhow!("Num output not found"))?; + numout.script_pubkey + } + Subject::Label(label) => { if event.space().is_some_and(|s| s != label.to_string()) { return Err(anyhow::anyhow!("Space tag does not match specified space")); } @@ -498,22 +536,12 @@ impl SpacesWallet { .ok_or_else(|| anyhow::anyhow!("Space not found"))?; spaceout.script_pubkey } - Subject::Ptr(sptr) => { - let outpoint = src.get_ptr_outpoint(sptr)? - .ok_or_else(|| anyhow::anyhow!("Sptr not found"))?; - let ptrout = src.get_ptrout(&outpoint)? - .ok_or_else(|| anyhow::anyhow!("Ptrout not found"))?; - ptrout.script_pubkey - } - Subject::Numeric(numeric) => { - let key = NumericKey::from_numeric::(numeric); - let sptr = src.get_numeric(&key)? - .ok_or_else(|| anyhow::anyhow!("Numeric '{}' not found", numeric))?; - let outpoint = src.get_ptr_outpoint(&sptr)? - .ok_or_else(|| anyhow::anyhow!("Sptr not found"))?; - let ptrout = src.get_ptrout(&outpoint)? - .ok_or_else(|| anyhow::anyhow!("Ptrout not found"))?; - ptrout.script_pubkey + Subject::NumId(id) => { + let outpoint = src.get_num_outpoint_by_id(id)? + .ok_or_else(|| anyhow::anyhow!("Num id not found"))?; + let numout = src.get_numout(&outpoint)? + .ok_or_else(|| anyhow::anyhow!("Num output not found"))?; + numout.script_pubkey } }; @@ -544,8 +572,8 @@ impl SpacesWallet { Ok(event) } - /// Sign a message with the key controlling a space or sptr - pub fn sign_schnorr( + /// Sign a message with the key controlling a space or num id + pub fn sign_schnorr( &mut self, src: &mut S, subject: Subject, @@ -554,28 +582,27 @@ impl SpacesWallet { use bitcoin::hashes::{sha256, Hash, HashEngine}; let outpoint = match &subject { - Subject::Space(label) => { + Subject::Label(label) if label.is_numeric() => { + let numeric: SNumeric = label.clone().try_into().unwrap(); + let key = NumericKey::from_numeric::(&numeric); + let id = src.get_num_id(&key)? + .ok_or_else(|| anyhow::anyhow!("Numeric '{}' not found", numeric))?; + src.get_num_outpoint_by_id(&id)? + .ok_or_else(|| anyhow::anyhow!("Num id not found"))? + } + Subject::Label(label) => { let space_key = SpaceKey::from(H::hash(label.as_ref())); src.get_space_outpoint(&space_key)? .ok_or_else(|| anyhow::anyhow!("Space not found"))? } - Subject::Ptr(sptr) => { - src.get_ptr_outpoint(sptr)? - .ok_or_else(|| anyhow::anyhow!("Sptr not found"))? - } - Subject::Numeric(numeric) => { - let key = NumericKey::from_numeric::(numeric); - let sptr = src.get_numeric(&key)? - .ok_or_else(|| anyhow::anyhow!("Numeric '{}' not found", numeric))?; - src.get_ptr_outpoint(&sptr)? - .ok_or_else(|| anyhow::anyhow!("Sptr not found"))? + Subject::NumId(id) => { + src.get_num_outpoint_by_id(id)? + .ok_or_else(|| anyhow::anyhow!("Num id not found"))? } }; // We use list_output instead of get_utxo because the output might // be spent in a pending tx, so signatures are still valid until confirmed. - // Mainly useful if subs is trying to sign a temporary certificate, but it already - // broadcasted a commitment spending the ptr. let utxo = self.internal.list_output().find(|o| o.outpoint == outpoint) .clone().ok_or_else(|| anyhow::anyhow!("Not owned by wallet"))?; @@ -594,8 +621,8 @@ impl SpacesWallet { Ok(sig) } - /// Verify a schnorr signature against a space or sptr's public key - pub fn verify_schnorr( + /// Verify a schnorr signature against a space or num id's public key + pub fn verify_schnorr( src: &mut S, subject: Subject, message: &[u8], @@ -604,7 +631,18 @@ impl SpacesWallet { use bitcoin::hashes::{sha256, Hash, HashEngine}; let script_pubkey = match &subject { - Subject::Space(label) => { + Subject::Label(label) if label.is_numeric() => { + let numeric: SNumeric = label.clone().try_into().unwrap(); + let key = NumericKey::from_numeric::(&numeric); + let id = src.get_num_id(&key)? + .ok_or_else(|| anyhow::anyhow!("Numeric '{}' not found", numeric))?; + let outpoint = src.get_num_outpoint_by_id(&id)? + .ok_or_else(|| anyhow::anyhow!("Num id not found"))?; + let numout = src.get_numout(&outpoint)? + .ok_or_else(|| anyhow::anyhow!("Num output not found"))?; + numout.script_pubkey + } + Subject::Label(label) => { let space_key = SpaceKey::from(H::hash(label.as_ref())); let outpoint = src.get_space_outpoint(&space_key)? .ok_or_else(|| anyhow::anyhow!("Space not found"))?; @@ -612,22 +650,12 @@ impl SpacesWallet { .ok_or_else(|| anyhow::anyhow!("Space not found"))?; spaceout.script_pubkey } - Subject::Ptr(sptr) => { - let outpoint = src.get_ptr_outpoint(sptr)? - .ok_or_else(|| anyhow::anyhow!("Sptr not found"))?; - let ptrout = src.get_ptrout(&outpoint)? - .ok_or_else(|| anyhow::anyhow!("Ptrout not found"))?; - ptrout.script_pubkey - } - Subject::Numeric(numeric) => { - let key = NumericKey::from_numeric::(numeric); - let sptr = src.get_numeric(&key)? - .ok_or_else(|| anyhow::anyhow!("Numeric '{}' not found", numeric))?; - let outpoint = src.get_ptr_outpoint(&sptr)? - .ok_or_else(|| anyhow::anyhow!("Sptr not found"))?; - let ptrout = src.get_ptrout(&outpoint)? - .ok_or_else(|| anyhow::anyhow!("Ptrout not found"))?; - ptrout.script_pubkey + Subject::NumId(id) => { + let outpoint = src.get_num_outpoint_by_id(id)? + .ok_or_else(|| anyhow::anyhow!("Num id not found"))?; + let numout = src.get_numout(&outpoint)? + .ok_or_else(|| anyhow::anyhow!("Num output not found"))?; + numout.script_pubkey } };