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
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ anyhow = "1.0"
hex = "0.4"
serde_json = "1.0"
tokio = { version = "1", features = ["full"] }
spacedb = { version = "0.1.2", features = ["hash-idx"] }
spacedb = { version = "0.1.4", features = ["hash-idx"] }
base64 = "0.22"
rand = "0.8"
log = "0.4"
Expand Down
72 changes: 58 additions & 14 deletions client/src/bin/space-cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,16 +320,16 @@ enum Commands {
/// List a space you own for sale
#[command(name = "sell")]
Sell {
/// The space to sell
space: String,
/// The space (@bitcoin), numeric (#800000-3-1), or num id (num1...) to sell
subject: String,
/// Amount in satoshis
price: u64,
},
/// Buy a space from the specified listing
/// Buy a space or num from the specified listing
#[command(name = "buy")]
Buy {
/// The space to buy
space: String,
/// The space (@bitcoin), numeric (#800000-3-1), or num id (num1...) to buy
subject: String,
/// The listing price
price: u64,
/// The seller's signature
Expand All @@ -338,15 +338,32 @@ enum Commands {
/// The seller's address
#[arg(long)]
seller: String,
/// Deliver the space/num to this address instead of the wallet's own
/// (e.g. an external keystore)
#[arg(long)]
to: Option<String>,
/// Fee rate to use in sat/vB
#[arg(long, short)]
fee_rate: Option<u64>,
},
/// Fund and broadcast one or more externally-signed transfer PSBTs.
/// Each PSBT must be a single input/output pair signed with
/// SIGHASH_SINGLE|ANYONECANPAY whose input value equals its output value;
/// the wallet adds fee inputs and change and broadcasts one transaction.
#[command(name = "fundtransfer")]
FundTransfer {
/// Base64-encoded PSBT(s), one per transfer to batch into the tx
#[arg(required = true)]
psbts: Vec<String>,
/// Fee rate to use in sat/vB
#[arg(long, short)]
fee_rate: Option<u64>,
},
/// Verify a listing
#[command(name = "verifylisting")]
VerifyListing {
/// The space to buy
space: String,
/// The space (@bitcoin), numeric (#800000-3-1), or num id (num1...)
subject: String,
/// The listing price
price: u64,
/// The seller's signature
Expand Down Expand Up @@ -546,6 +563,14 @@ fn normalize_space(space: &str) -> String {
}
}

/// Normalize a listing subject: @space, #numeric, or num1... — validated and
/// canonicalized via [`Subject`].
fn normalize_subject(subject: &str) -> Result<String, ClientError> {
spaces_wallet::Subject::from_str(subject)
.map(|s| s.to_string())
.map_err(ClientError::Custom)
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let (cli, args) = SpaceCli::configure().await?;
Expand Down Expand Up @@ -917,14 +942,15 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client
);
}
Commands::Buy {
space,
subject,
price,
signature,
seller,
to,
fee_rate,
} => {
let listing = Listing {
space: normalize_space(&space),
subject: normalize_subject(&subject)?,
price,
seller,
signature: Signature::from_slice(
Expand All @@ -941,6 +967,7 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client
.wallet_buy(
&cli.wallet,
listing,
to,
fee_rate.map(|rate| FeeRate::from_sat_per_vb(rate).expect("valid fee rate")),
cli.skip_tx_check,
)
Expand All @@ -953,19 +980,36 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client
cli.format,
);
}
Commands::Sell { mut space, price } => {
space = normalize_space(&space);
let result = cli.client.wallet_sell(&cli.wallet, space, price).await?;
Commands::FundTransfer { psbts, fee_rate } => {
let result = cli
.client
.wallet_fund_transfer(
&cli.wallet,
psbts,
fee_rate.map(|rate| FeeRate::from_sat_per_vb(rate).expect("valid fee rate")),
)
.await?;
print_wallet_response(
cli.network.fallback_network(),
WalletResponse {
result: vec![result],
},
cli.format,
);
}
Commands::Sell { subject, price } => {
let subject = normalize_subject(&subject)?;
let result = cli.client.wallet_sell(&cli.wallet, subject, price).await?;
println!("{}", serde_json::to_string_pretty(&result).expect("result"));
}
Commands::VerifyListing {
space,
subject,
price,
signature,
seller,
} => {
let listing = Listing {
space: normalize_space(&space),
subject: normalize_subject(&subject)?,
price,
seller,
signature: Signature::from_slice(
Expand Down
67 changes: 66 additions & 1 deletion client/src/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,11 +375,15 @@ pub trait Rpc {
skip_tx_check: bool,
) -> Result<Vec<TxResponse>, ErrorObjectOwned>;

/// Buy a listed space or num. `recipient` optionally delivers the subject
/// to an external address (e.g. another keystore); when omitted it goes to
/// a fresh address of the funding wallet.
#[method(name = "walletbuy")]
async fn wallet_buy(
&self,
wallet: &str,
listing: Listing,
recipient: Option<String>,
fee_rate: Option<FeeRate>,
skip_tx_check: bool,
) -> Result<TxResponse, ErrorObjectOwned>;
Expand All @@ -392,6 +396,17 @@ pub trait Rpc {
amount: u64,
) -> Result<Listing, ErrorObjectOwned>;

/// Fund and broadcast one or more externally-signed transfer PSBTs
/// (single input/output, SIGHASH_SINGLE|ANYONECANPAY, value in == value
/// out) as a single transaction. The wallet supplies fee + change.
#[method(name = "walletfundtransfer")]
async fn wallet_fund_transfer(
&self,
wallet: &str,
psbts: Vec<String>,
fee_rate: Option<FeeRate>,
) -> Result<TxResponse, ErrorObjectOwned>;

#[method(name = "verifylisting")]
async fn verify_listing(&self, listing: Listing) -> Result<(), ErrorObjectOwned>;

Expand Down Expand Up @@ -473,6 +488,17 @@ pub trait Rpc {
locktime: Option<u32>,
fee_rate: FeeRate,
) -> Result<TxResponse, ErrorObjectOwned>;

/// Debug: produce a value-preserving num transfer PSBT (single input/output,
/// SIGHASH_SINGLE|ANYONECANPAY) for one of the wallet's nums, to be funded
/// via `walletfundtransfer`. Returns the base64 PSBT. Regtest only.
#[method(name = "debugsigntransfer")]
async fn debug_sign_transfer(
&self,
wallet: &str,
subject: String,
recipient: ScriptBuf,
) -> Result<String, ErrorObjectOwned>;
}

#[derive(Clone, Debug, Serialize, Deserialize)]
Expand Down Expand Up @@ -1352,12 +1378,13 @@ impl RpcServer for RpcServerImpl {
&self,
wallet: &str,
listing: Listing,
recipient: Option<String>,
fee_rate: Option<FeeRate>,
skip_tx_check: bool,
) -> Result<TxResponse, ErrorObjectOwned> {
self.wallet(wallet)
.await?
.send_buy(listing, fee_rate, skip_tx_check)
.send_buy(listing, recipient, fee_rate, skip_tx_check)
.await
.map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::<String>))
}
Expand All @@ -1375,6 +1402,19 @@ impl RpcServer for RpcServerImpl {
.map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::<String>))
}

async fn wallet_fund_transfer(
&self,
wallet: &str,
psbts: Vec<String>,
fee_rate: Option<FeeRate>,
) -> Result<TxResponse, ErrorObjectOwned> {
self.wallet(wallet)
.await?
.send_fund_transfer(psbts, fee_rate)
.await
.map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::<String>))
}

async fn verify_listing(&self, listing: Listing) -> Result<(), ErrorObjectOwned> {
self.store
.verify_listing(listing)
Expand Down Expand Up @@ -1583,6 +1623,31 @@ impl RpcServer for RpcServerImpl {
.await
.map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::<String>))
}

async fn debug_sign_transfer(
&self,
wallet: &str,
subject: String,
recipient: ScriptBuf,
) -> Result<String, ErrorObjectOwned> {
let info = self
.store
.get_server_info()
.await
.map_err(|e| ErrorObjectOwned::owned(-1, e.to_string(), None::<String>))?;
if info.network != ExtendedNetwork::Regtest {
return Err(ErrorObjectOwned::owned(
-1,
"debug_sign_transfer is only available on regtest",
None::<String>,
));
}
self.wallet(wallet)
.await?
.send_debug_sign_transfer(subject, recipient)
.await
.map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::<String>))
}
}

impl AsyncChainState {
Expand Down
Loading
Loading