Skip to content

Commit f59f98f

Browse files
authored
Merge pull request #58 from buffrr/buy-sell
Marketplace Listings API: buy, sell and verify listings
2 parents 6973522 + 7819599 commit f59f98f

File tree

7 files changed

+513
-15
lines changed

7 files changed

+513
-15
lines changed

node/src/bin/space-cli.rs

+92-6
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ use spaced::{
2222
store::Sha256,
2323
wallets::AddressKind,
2424
};
25+
use wallet::bitcoin::secp256k1::schnorr::Signature;
2526
use wallet::export::WalletExport;
27+
use wallet::Listing;
2628

2729
#[derive(Parser, Debug)]
2830
#[command(version, about, long_about = None)]
@@ -174,6 +176,46 @@ enum Commands {
174176
#[arg(long, short)]
175177
fee_rate: u64,
176178
},
179+
/// Buy a space from the specified listing
180+
#[command(name = "buy")]
181+
Buy {
182+
/// The space to buy
183+
space: String,
184+
/// The listing price
185+
price: u64,
186+
/// The seller's signature
187+
#[arg(long)]
188+
signature: String,
189+
/// The seller's address
190+
#[arg(long)]
191+
seller: String,
192+
/// Fee rate to use in sat/vB
193+
#[arg(long, short)]
194+
fee_rate: Option<u64>,
195+
},
196+
/// List a space you own for sale
197+
#[command(name = "sell")]
198+
Sell {
199+
/// The space to sell
200+
space: String,
201+
/// Amount in satoshis
202+
price: u64,
203+
},
204+
/// Verify a listing
205+
#[command(name = "verifylisting")]
206+
VerifyListing {
207+
/// The space to buy
208+
space: String,
209+
/// The listing price
210+
price: u64,
211+
/// The seller's signature
212+
#[arg(long)]
213+
signature: String,
214+
/// The seller's address
215+
#[arg(long)]
216+
seller: String,
217+
},
218+
177219
/// Get a spaceout - a Bitcoin output relevant to the Spaces protocol.
178220
#[command(name = "getspaceout")]
179221
GetSpaceOut {
@@ -452,7 +494,7 @@ async fn handle_commands(
452494
fee_rate,
453495
false,
454496
)
455-
.await?
497+
.await?
456498
}
457499
Commands::Bid {
458500
space,
@@ -469,7 +511,7 @@ async fn handle_commands(
469511
fee_rate,
470512
confirmed_only,
471513
)
472-
.await?
514+
.await?
473515
}
474516
Commands::CreateBidOuts { pairs, fee_rate } => {
475517
cli.send_request(None, Some(pairs), fee_rate, false).await?
@@ -488,7 +530,7 @@ async fn handle_commands(
488530
fee_rate,
489531
false,
490532
)
491-
.await?
533+
.await?
492534
}
493535
Commands::Transfer {
494536
spaces,
@@ -505,7 +547,7 @@ async fn handle_commands(
505547
fee_rate,
506548
false,
507549
)
508-
.await?
550+
.await?
509551
}
510552
Commands::SendCoins {
511553
amount,
@@ -521,7 +563,7 @@ async fn handle_commands(
521563
fee_rate,
522564
false,
523565
)
524-
.await?
566+
.await?
525567
}
526568
Commands::SetRawFallback {
527569
mut space,
@@ -550,7 +592,7 @@ async fn handle_commands(
550592
fee_rate,
551593
false,
552594
)
553-
.await?;
595+
.await?;
554596
}
555597
Commands::ListUnspent => {
556598
let spaces = cli.client.wallet_list_unspent(&cli.wallet).await?;
@@ -614,6 +656,50 @@ async fn handle_commands(
614656
hash_space(&space).map_err(|e| ClientError::Custom(e.to_string()))?
615657
);
616658
}
659+
Commands::Buy { space, price, signature, seller, fee_rate } => {
660+
let listing = Listing {
661+
space: normalize_space(&space),
662+
price,
663+
seller,
664+
signature: Signature::from_slice(hex::decode(signature)
665+
.map_err(|_| ClientError::Custom("Signature must be in hex format".to_string()))?.as_slice())
666+
.map_err(|_| ClientError::Custom("Invalid signature".to_string()))?,
667+
};
668+
let result = cli
669+
.client
670+
.wallet_buy(
671+
&cli.wallet,
672+
listing,
673+
fee_rate.map(|rate| FeeRate::from_sat_per_vb(rate).expect("valid fee rate")),
674+
cli.skip_tx_check,
675+
).await?;
676+
println!("{}", serde_json::to_string_pretty(&result).expect("result"));
677+
}
678+
Commands::Sell { space, price, } => {
679+
let result = cli
680+
.client
681+
.wallet_sell(
682+
&cli.wallet,
683+
space,
684+
price,
685+
).await?;
686+
println!("{}", serde_json::to_string_pretty(&result).expect("result"));
687+
}
688+
Commands::VerifyListing { space, price, signature, seller } => {
689+
let listing = Listing {
690+
space: normalize_space(&space),
691+
price,
692+
seller,
693+
signature: Signature::from_slice(hex::decode(signature)
694+
.map_err(|_| ClientError::Custom("Signature must be in hex format".to_string()))?.as_slice())
695+
.map_err(|_| ClientError::Custom("Invalid signature".to_string()))?,
696+
};
697+
698+
let result = cli
699+
.client
700+
.verify_listing(listing).await?;
701+
println!("{}", serde_json::to_string_pretty(&result).expect("result"));
702+
}
617703
}
618704

619705
Ok(())

node/src/rpc.rs

+63-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ use tokio::{
3535
sync::{broadcast, mpsc, oneshot, RwLock},
3636
task::JoinSet,
3737
};
38-
use wallet::{bdk_wallet as bdk, bdk_wallet::template::Bip86, bitcoin::hashes::Hash, export::WalletExport, Balance, DoubleUtxo, WalletConfig, WalletDescriptors, WalletInfo, WalletOutput};
38+
use wallet::{bdk_wallet as bdk, bdk_wallet::template::Bip86, bitcoin::hashes::Hash, export::WalletExport, Balance, DoubleUtxo, Listing, SpacesWallet, WalletConfig, WalletDescriptors, WalletInfo, WalletOutput};
3939

4040
use crate::{
4141
checker::TxChecker,
@@ -95,6 +95,10 @@ pub enum ChainStateCommand {
9595
target: usize,
9696
resp: Responder<anyhow::Result<Vec<RolloutEntry>>>,
9797
},
98+
VerifyListing {
99+
listing: Listing,
100+
resp: Responder<anyhow::Result<()>>,
101+
},
98102
}
99103

100104
#[derive(Clone)]
@@ -181,6 +185,29 @@ pub trait Rpc {
181185
skip_tx_check: bool,
182186
) -> Result<Vec<TxResponse>, ErrorObjectOwned>;
183187

188+
#[method(name = "walletbuy")]
189+
async fn wallet_buy(
190+
&self,
191+
wallet: &str,
192+
listing: Listing,
193+
fee_rate: Option<FeeRate>,
194+
skip_tx_check: bool,
195+
) -> Result<TxResponse, ErrorObjectOwned>;
196+
197+
#[method(name = "walletsell")]
198+
async fn wallet_sell(
199+
&self,
200+
wallet: &str,
201+
space: String,
202+
amount: u64,
203+
) -> Result<Listing, ErrorObjectOwned>;
204+
205+
#[method(name = "verifylisting")]
206+
async fn verify_listing(
207+
&self,
208+
listing: Listing,
209+
) -> Result<(), ErrorObjectOwned>;
210+
184211
#[method(name = "walletlisttransactions")]
185212
async fn wallet_list_transactions(
186213
&self,
@@ -199,7 +226,7 @@ pub trait Rpc {
199226

200227
#[method(name = "walletlistspaces")]
201228
async fn wallet_list_spaces(&self, wallet: &str)
202-
-> Result<ListSpacesResponse, ErrorObjectOwned>;
229+
-> Result<ListSpacesResponse, ErrorObjectOwned>;
203230

204231
#[method(name = "walletlistunspent")]
205232
async fn wallet_list_unspent(
@@ -754,6 +781,29 @@ impl RpcServer for RpcServerImpl {
754781
.map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::<String>))
755782
}
756783

784+
async fn wallet_buy(&self, wallet: &str, listing: Listing, fee_rate: Option<FeeRate>, skip_tx_check: bool) -> Result<TxResponse, ErrorObjectOwned> {
785+
self.wallet(&wallet)
786+
.await?
787+
.send_buy(listing, fee_rate, skip_tx_check)
788+
.await
789+
.map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::<String>))
790+
}
791+
792+
async fn wallet_sell(&self, wallet: &str, space: String, amount: u64) -> Result<Listing, ErrorObjectOwned> {
793+
self.wallet(&wallet)
794+
.await?
795+
.send_sell(space, amount)
796+
.await
797+
.map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::<String>))
798+
}
799+
800+
async fn verify_listing(&self, listing: Listing) -> Result<(), ErrorObjectOwned> {
801+
self.store
802+
.verify_listing(listing)
803+
.await
804+
.map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::<String>))
805+
}
806+
757807
async fn wallet_list_transactions(
758808
&self,
759809
wallet: &str,
@@ -953,6 +1003,9 @@ impl AsyncChainState {
9531003
let rollouts = chain_state.get_rollout(target);
9541004
_ = resp.send(rollouts);
9551005
}
1006+
ChainStateCommand::VerifyListing { listing, resp } => {
1007+
_ = resp.send(SpacesWallet::verify_listing::<Sha256>(chain_state, &listing).map(|_| ()));
1008+
}
9561009
}
9571010
}
9581011

@@ -986,6 +1039,14 @@ impl AsyncChainState {
9861039
resp_rx.await?
9871040
}
9881041

1042+
pub async fn verify_listing(&self, listing: Listing) -> anyhow::Result<()> {
1043+
let (resp, resp_rx) = oneshot::channel();
1044+
self.sender
1045+
.send(ChainStateCommand::VerifyListing { listing, resp })
1046+
.await?;
1047+
resp_rx.await?
1048+
}
1049+
9891050
pub async fn get_rollout(&self, target: usize) -> anyhow::Result<Vec<RolloutEntry>> {
9901051
let (resp, resp_rx) = oneshot::channel();
9911052
self.sender

0 commit comments

Comments
 (0)