Skip to content

Commit 82679a9

Browse files
committed
support rune offer creation and acceptance across multiple utxos, updated documentation
1 parent d9570d7 commit 82679a9

File tree

5 files changed

+859
-101
lines changed

5 files changed

+859
-101
lines changed

docs/src/guides/wallet.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -465,13 +465,13 @@ ord wallet offer create --rune <DECIMAL:RUNE> --fee-rate <FEE_RATE> --amount <AM
465465
Accepting an Inscription or Runes Buy Offer
466466
-------------------------------------------
467467

468-
Accept the offer to buy the inscription `INSCRIPTION_ID` for `AMOUNT` via `PSBT` using:
468+
Accept the offer to buy the inscription `INSCRIPTION_ID` for `AMOUNT` in `PSBT` using:
469469

470470
```
471471
ord wallet offer accept --inscription <INSCRIPTION_ID> --amount <AMOUNT> --psbt <PSBT>
472472
```
473473

474-
Accept the offer to buy the rune balance `DECIMAL:RUNE` sold in `PSBT` using:
474+
Accept the offer to buy the rune balance `DECIMAL:RUNE` in `PSBT` using:
475475

476476
```
477477
ord wallet offer accept --rune <DECIMAL:RUNE> --amount <AMOUNT> --psbt <PSBT>

src/subcommand/wallet/offer/accept.rs

Lines changed: 55 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -39,21 +39,12 @@ impl Accept {
3939
}
4040
}
4141

42-
ensure! {
43-
outgoing.len() <= 1,
44-
"PSBT contains {} inputs owned by wallet", outgoing.len(),
45-
}
46-
47-
let Some((index, outgoing)) = outgoing.into_iter().next() else {
48-
bail!("PSBT contains no inputs owned by wallet");
49-
};
50-
5142
match (self.inscription, self.rune.clone()) {
5243
(Some(inscription), None) => {
53-
self.check_inscription_buy_offer(&wallet, outgoing, inscription)?
44+
self.check_inscription_buy_offer(&wallet, outgoing.clone(), inscription)?
5445
}
5546
(None, Some(rune)) => {
56-
self.check_rune_buy_offer(&wallet, psbt.unsigned_tx.clone(), outgoing, rune)?
47+
self.check_rune_buy_offer(&wallet, psbt.unsigned_tx.clone(), outgoing.clone(), rune)?
5748
}
5849
(None, None) => bail!("must include either --inscription or --rune"),
5950
(Some(_), Some(_)) => bail!("cannot include both --inscription and --rune"),
@@ -71,7 +62,7 @@ impl Accept {
7162
for (i, signature) in signatures.iter().enumerate() {
7263
let outpoint = psbt.unsigned_tx.input[i].previous_output;
7364

74-
if i == index {
65+
if outgoing.contains_key(&i) {
7566
ensure! {
7667
signature.is_none(),
7768
"seller input `{outpoint}` is signed: seller input must not be signed",
@@ -115,7 +106,7 @@ impl Accept {
115106
{
116107
let outpoint = signed_tx.input[i].previous_output;
117108

118-
if i == index {
109+
if outgoing.contains_key(&i) {
119110
ensure! {
120111
new.is_some(),
121112
"seller input `{outpoint}` was not signed by wallet",
@@ -138,9 +129,18 @@ impl Accept {
138129
fn check_inscription_buy_offer(
139130
&self,
140131
wallet: &Wallet,
141-
outgoing: OutPoint,
132+
outgoing: BTreeMap<usize, OutPoint>,
142133
inscription_id: InscriptionId,
143134
) -> Result {
135+
ensure! {
136+
outgoing.len() <= 1,
137+
"PSBT contains {} inputs owned by wallet", outgoing.len(),
138+
}
139+
140+
let Some((_, outgoing)) = outgoing.into_iter().next() else {
141+
bail!("PSBT contains no inputs owned by wallet");
142+
};
143+
144144
if let Some(runes) = wallet.get_runes_balances_in_output(&outgoing)? {
145145
ensure! {
146146
runes.is_empty(),
@@ -175,9 +175,14 @@ impl Accept {
175175
&self,
176176
wallet: &Wallet,
177177
unsigned_tx: Transaction,
178-
outgoing: OutPoint,
178+
outgoing: BTreeMap<usize, OutPoint>,
179179
rune: Outgoing,
180180
) -> Result {
181+
ensure! {
182+
!outgoing.is_empty(),
183+
"PSBT contains no inputs owned by wallet"
184+
}
185+
181186
let (decimal, spaced_rune) = match rune {
182187
Outgoing::Rune { decimal, rune } => (decimal, rune),
183188
_ => bail!("invalid format for --rune (must be `DECIMAL:RUNE`)"),
@@ -192,36 +197,46 @@ impl Accept {
192197
.get_rune(spaced_rune.rune)?
193198
.with_context(|| format!("rune `{}` has not been etched", spaced_rune.rune))?;
194199

195-
let Some(runes) = wallet.get_runes_balances_in_output(&outgoing)? else {
196-
bail!("outgoing input contains no runes");
197-
};
200+
let mut contains_multiple_runes = false;
201+
let mut amount = 0;
198202

199-
if let Some(inscriptions) = wallet.get_inscriptions_in_output(&outgoing)? {
200-
ensure! {
201-
inscriptions.is_empty(),
202-
"outgoing input {} contains {} inscription(s)",
203-
outgoing,
204-
inscriptions.len()
205-
}
206-
};
203+
for utxo in outgoing.values() {
204+
let Some(runes) = wallet.get_runes_balances_in_output(utxo)? else {
205+
bail! {
206+
"outgoing input {} contains no runes",
207+
utxo
208+
}
209+
};
207210

208-
let Some(pile) = runes.get(&spaced_rune) else {
209-
bail!(format!(
210-
"outgoing input {} does not contain rune {}",
211-
outgoing, spaced_rune
212-
));
213-
};
211+
if let Some(inscriptions) = wallet.get_inscriptions_in_output(utxo)? {
212+
ensure! {
213+
inscriptions.is_empty(),
214+
"outgoing input {} contains {} inscription(s)",
215+
utxo,
216+
inscriptions.len()
217+
}
218+
};
219+
220+
let Some(pile) = runes.get(&spaced_rune) else {
221+
bail!(format!(
222+
"outgoing input {} does not contain rune {}",
223+
utxo, spaced_rune
224+
));
225+
};
226+
227+
contains_multiple_runes = contains_multiple_runes || runes.len() > 1;
228+
amount += pile.amount;
229+
}
214230

215231
ensure! {
216-
pile.amount == decimal.value,
217-
"unexpected rune {} balance at outgoing input {} ({} vs. {})",
232+
amount == decimal.value,
233+
"unexpected rune {} balance at outgoing input(s) ({} vs. {})",
218234
spaced_rune,
219-
outgoing,
220-
pile.amount,
235+
amount,
221236
decimal.value
222237
}
223238

224-
if runes.len() > 1 {
239+
if contains_multiple_runes {
225240
let Some(runestone) = Runestone::decipher(&unsigned_tx) else {
226241
bail!("missing runestone in PSBT");
227242
};
@@ -242,7 +257,9 @@ impl Accept {
242257

243258
ensure! {
244259
!unsigned_tx.output.is_empty() &&
245-
unsigned_tx.output[0].script_pubkey == wallet.utxos().get(&outgoing).unwrap().script_pubkey,
260+
outgoing.values().any(|outgoing| {
261+
unsigned_tx.output[0].script_pubkey == wallet.utxos().get(outgoing).unwrap().script_pubkey
262+
}),
246263
"unexpected seller address in PSBT"
247264
}
248265
} else {

src/subcommand/wallet/offer/create.rs

Lines changed: 69 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pub(crate) struct Create {
1919
#[arg(long, help = "<FEE_RATE> for finalized transaction.")]
2020
fee_rate: FeeRate,
2121
#[arg(long, help = "UTXO to make an offer for. (format: <TXID:VOUT>)")]
22-
utxo: Option<OutPoint>,
22+
utxo: Vec<OutPoint>,
2323
#[arg(
2424
long,
2525
help = "Include at least <AMOUNT> postage with receive output. [default: 10000sat]"
@@ -70,9 +70,9 @@ impl Create {
7070
);
7171
};
7272

73-
if let Some(utxo) = self.utxo {
73+
for utxo in &self.utxo {
7474
ensure! {
75-
inscription.satpoint.outpoint == utxo,
75+
inscription.satpoint.outpoint == *utxo,
7676
"inscription utxo {} does not match provided utxo {}",
7777
inscription.satpoint.outpoint,
7878
utxo
@@ -137,65 +137,83 @@ impl Create {
137137
.get_rune(spaced_rune.rune)?
138138
.with_context(|| format!("rune `{}` has not been etched", spaced_rune.rune))?;
139139

140-
let Some(utxo) = self.utxo else {
141-
bail!("--utxo must be set");
142-
};
143-
144-
ensure!(
145-
!wallet.utxos().contains_key(&utxo),
146-
"utxo {} already in wallet",
147-
utxo
148-
);
149-
150140
ensure! {
151-
wallet.output_exists(utxo)?,
152-
"utxo {} does not exist",
153-
utxo
141+
!self.utxo.is_empty(),
142+
"--utxo must be set"
154143
}
155144

156-
let Some(output_info) = wallet.get_output_info(utxo)? else {
157-
bail!("utxo {} does not exist", utxo);
158-
};
145+
let mut contains_multiple_runes = false;
146+
let mut amount = 0;
147+
let mut seller_address = None;
148+
let mut seller_postage = Amount::ZERO;
159149

160-
let Some(seller_address) = output_info.address else {
161-
bail!("utxo {} script pubkey not valid address", utxo);
162-
};
150+
for utxo in &self.utxo {
151+
ensure!(
152+
!wallet.utxos().contains_key(utxo),
153+
"utxo {} already in wallet",
154+
*utxo
155+
);
163156

164-
let Some(runes) = output_info.runes else {
165-
bail!("utxo {} does not hold any runes", utxo);
166-
};
157+
ensure! {
158+
wallet.output_exists(*utxo)?,
159+
"utxo {} does not exist",
160+
*utxo
161+
}
167162

168-
let Some(pile) = runes.get(&spaced_rune) else {
169-
bail!("utxo {} does not hold any {} runes", utxo, spaced_rune);
170-
};
163+
let Some(output_info) = wallet.get_output_info(*utxo)? else {
164+
bail!("utxo {} does not exist", *utxo);
165+
};
166+
167+
let Some(utxo_seller_address) = output_info.address else {
168+
bail!("utxo {} script pubkey not valid address", *utxo);
169+
};
170+
171+
let Some(runes) = output_info.runes else {
172+
bail!("utxo {} does not hold any runes", *utxo);
173+
};
171174

172-
if pile.amount < decimal.value {
175+
let Some(pile) = runes.get(&spaced_rune) else {
176+
bail!("utxo {} does not hold any {} runes", *utxo, spaced_rune);
177+
};
178+
179+
if runes.len() > 1 {
180+
contains_multiple_runes = true;
181+
}
182+
183+
amount += pile.amount;
184+
185+
if seller_address.is_none() {
186+
seller_address = Some(utxo_seller_address);
187+
}
188+
189+
seller_postage += Amount::from_sat(output_info.value);
190+
}
191+
192+
if amount < decimal.value {
173193
bail!(
174-
"utxo {} holds less {} than required ({} < {})",
175-
utxo,
194+
"utxo(s) hold less {} than required ({} < {})",
176195
spaced_rune,
177-
pile.amount,
196+
amount,
178197
decimal.value,
179198
);
180199
}
181200

182-
if pile.amount > decimal.value {
201+
if amount > decimal.value {
183202
bail!(
184-
"utxo {} holds more {} than expected ({} > {})",
185-
utxo,
203+
"utxo(s) hold more {} than expected ({} > {})",
186204
spaced_rune,
187-
pile.amount,
205+
amount,
188206
decimal.value,
189207
);
190208
}
191209

192210
let buyer_postage = self.postage.unwrap_or(TARGET_POSTAGE);
193211

194-
let seller_postage = Amount::from_sat(output_info.value);
195-
196-
let seller_address = seller_address.require_network(wallet.chain().network())?;
212+
let seller_address = seller_address
213+
.unwrap()
214+
.require_network(wallet.chain().network())?;
197215

198-
let output = if runes.len() > 1 {
216+
let output = if contains_multiple_runes {
199217
let runestone = Runestone {
200218
edicts: vec![Edict {
201219
amount: 0,
@@ -239,12 +257,17 @@ impl Create {
239257
let tx = Transaction {
240258
version: Version(2),
241259
lock_time: LockTime::ZERO,
242-
input: vec![TxIn {
243-
previous_output: utxo,
244-
script_sig: ScriptBuf::new(),
245-
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
246-
witness: Witness::new(),
247-
}],
260+
input: self
261+
.utxo
262+
.clone()
263+
.into_iter()
264+
.map(|utxo| TxIn {
265+
previous_output: utxo,
266+
script_sig: ScriptBuf::new(),
267+
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
268+
witness: Witness::new(),
269+
})
270+
.collect(),
248271
output,
249272
};
250273

0 commit comments

Comments
 (0)