Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions borsh_utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,34 @@ pub fn deserialize_outpoint<R: io::Read>(reader: &mut R) -> io::Result<OutPoint>
})
}

/// Serialize an `Option<OutPoint>`
pub fn serialize_optional_outpoint<W: io::Write>(
outpoint: &Option<OutPoint>,
writer: &mut W,
) -> io::Result<()> {
match outpoint {
None => writer.write_all(&[0u8]),
Some(outpoint) => {
writer.write_all(&[1u8])?;
serialize_outpoint(outpoint, writer)
}
}
}

/// Deserialize an `Option<OutPoint>`
pub fn deserialize_optional_outpoint<R: io::Read>(reader: &mut R) -> io::Result<Option<OutPoint>> {
let mut tag = [0u8; 1];
reader.read_exact(&mut tag)?;
match tag[0] {
0 => Ok(None),
1 => Ok(Some(deserialize_outpoint(reader)?)),
_ => Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid Option tag for OutPoint",
)),
}
}

/// Serialize a BlockHash
pub fn serialize_block_hash<W: io::Write>(hash: &BlockHash, writer: &mut W) -> io::Result<()> {
writer.write_all(&hash.to_byte_array())
Expand Down
61 changes: 59 additions & 2 deletions client/src/bin/space-cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use jsonrpsee::{
http_client::HttpClient,
};
use spaces_client::rpc::{
CommitParams, CreateNumParams, DelegateParams, OperateParams, SetFallbackParams,
CommitParams, CreateNumParams, DelegateParams, OperateParams, SetFallbackParams, UnbindParams,
};
use spaces_client::store::Sha256;
use spaces_client::{
Expand Down Expand Up @@ -162,6 +162,26 @@ enum Commands {
/// Space name, numeric, or num id
subject: Subject,
},
/// Unbind nums: spend them with no successor so they go dormant.
/// A dormant num can be revived at its death spk with `createnum`.
#[command(name = "unbind")]
Unbind {
/// Nums to unbind (e.g., num1... or #800000-3-1)
subjects: Vec<Subject>,
/// Read hex-encoded secret key from stdin for unbinding nums not owned by wallet
#[arg(long)]
secret_stdin: bool,
/// Fee rate to use in sat/vB
#[arg(long, short)]
fee_rate: Option<u64>,
},
/// Get the rebind parked at a script pubkey (a num that died there,
/// revivable with `createnum --bind-spk <hex>`), if any
#[command(name = "getrebind")]
GetRebind {
/// Script public key as hex string
script_pubkey: String,
},
/// Transfer ownership of spaces and/or nums to the given name or address
#[command(
name = "transfer",
Expand Down Expand Up @@ -607,7 +627,7 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client
let response = cli.client.wallet_create(&cli.wallet).await?;
println!("⚠️ Write down your recovery phrase NOW!");
println!("This is the ONLY time it will be shown:");
println!("{}", &response);
println!("{}", response);
}
Commands::RecoverWallet => {
print!("Enter mnemonic phrase: ");
Expand Down Expand Up @@ -1019,6 +1039,43 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client
.map_err(|e| ClientError::Custom(e.to_string()))?;
println!("{}", serde_json::to_string(&num).expect("result"));
}
Commands::Unbind {
subjects,
secret_stdin,
fee_rate,
} => {
let secret = if secret_stdin {
let mut input = String::new();
io::stdin().read_line(&mut input).map_err(|e| {
ClientError::Custom(format!("failed to read secret from stdin: {}", e))
})?;
Some(input.trim().to_string())
} else {
None
};
cli.send_request(
Some(RpcWalletRequest::Unbind(UnbindParams { subjects, secret })),
None,
fee_rate,
false,
)
.await?;
println!(
"Num(s) go dormant once the tx confirms; revive with `createnum --bind-spk <death spk>`"
);
}
Commands::GetRebind { script_pubkey } => {
let spk = ScriptBuf::from(
hex::decode(script_pubkey)
.map_err(|_| ClientError::Custom("Invalid spk hex".to_string()))?,
);
let rebind = cli
.client
.get_rebind(spk)
.await
.map_err(|e| ClientError::Custom(e.to_string()))?;
println!("{}", serde_json::to_string(&rebind).expect("result"));
}

Commands::GetNumOut { outpoint } => {
let numout = cli
Expand Down
39 changes: 33 additions & 6 deletions client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ use anyhow::{Result, anyhow};
use borsh::{BorshDeserialize, BorshSerialize};
use serde::de::Error as SerdeError;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use spaces_nums::{CommitmentKey, CommitmentTipKey, DelegatorKey, NumOutpointKey};
use spaces_nums::{
CommitmentKey, CommitmentTipKey, DelegatorKey, NumOutpointKey, RebindData, RebindKey,
};
use spaces_protocol::{
Bytes, Covenant, FullSpaceOut, RevokeReason, SpaceOut,
bitcoin::{Amount, Block, BlockHash, OutPoint, Txid},
Expand Down Expand Up @@ -303,7 +305,7 @@ impl Client {
});
}
}
self.apply_ptrs_tx(chain, tx, ptrs_validated);
self.apply_nums_tx(chain, tx, ptrs_validated);
}
}

Expand All @@ -315,7 +317,7 @@ impl Client {
Ok((spaces_meta, num_meta))
}

fn apply_ptrs_tx(
fn apply_nums_tx(
&self,
state: &mut Chain,
tx: &Transaction,
Expand Down Expand Up @@ -367,21 +369,46 @@ impl Client {
state.insert_commitment(commitment_key, commitment_info.commitment);
}

// Create ptrs
// Rebinds (revivals): consume the parked rebind and delete the
// tombstone. The revived num itself is in `creates`, whose identity
// write repoints the genesis slot.
for rebind in changeset.rebinds.into_iter() {
state.remove_num_utxo(rebind.prev_outpoint);
state.remove_rebind(rebind.key);
}

// Create nums
for create in changeset.creates.into_iter() {
let outpoint = OutPoint {
txid: changeset.txid,
vout: create.n as u32,
};

// Num => Outpoint + Numeric => NumId
state.insert_num_outpoint(create.num.id, outpoint.into());
// Num => Outpoint
state.insert_num_outpoint(create.num.id, outpoint);
// Numeric => NumId
state.insert_num(&create.num.name, create.num.id);

// Outpoint => PtrOut
let outpoint_key = NumOutpointKey::from_outpoint::<Sha256>(outpoint);
state.insert_numout(outpoint_key, create);
}

// Unbind nums: overwrite the numout with the spent tombstone and park
// a rebind (derived from it) at the death spk's rebind slot. The
// identity slot is untouched.
for fno in changeset.unbinds.into_iter() {
let rebind_key = RebindKey::from_spk::<Sha256>(fno.numout.script_pubkey.clone());
state.insert_rebind(
rebind_key,
RebindData {
prev_outpoint: fno.outpoint(),
prev: fno.numout.num.clone(),
},
);
let outpoint_key = NumOutpointKey::from_outpoint::<Sha256>(fno.outpoint());
state.insert_numout(outpoint_key, fno.numout);
}
}

fn apply_space_tx(&self, state: &mut Chain, tx: &Transaction, changeset: TxChangeSet) {
Expand Down
Loading
Loading