1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
23use std::convert::Infallible;
24
25use amplify::confinement::{Confined, U24};
26use chrono::Utc;
27use psrgbt::{RgbOutExt, RgbPropKeyExt, RgbPsbtExt, TapretKeyError, Terminal};
28use rgbstd::containers::{Batch, BuilderSeal, Transfer};
29use rgbstd::contract::{AllocatedState, AssignmentsFilter, BuilderError};
30use rgbstd::invoice::{Amount, Beneficiary, InvoiceState, RgbInvoice};
31use rgbstd::persistence::{IndexProvider, StashInconsistency, StashProvider, StateProvider, Stock};
32use rgbstd::rgbcore::dbc::tapret::{TapretCommitment, TapretProof};
33use rgbstd::rgbcore::dbc::Proof;
34use rgbstd::rgbcore::seals::txout::{CloseMethod, ExplicitSeal};
35use rgbstd::rgbcore::secp256k1::rand;
36use rgbstd::validation::WitnessOrdProvider;
37use rgbstd::{
38 AssignmentType, ContractId, GraphSeal, Opout, Outpoint, OutputSeal, RevealedData, Transition,
39 TransitionType, Txid,
40};
41
42use crate::filters::{Filter, WalletFilter};
43use crate::invoice::NonFungible;
44use crate::validation::WitnessResolverError;
45use crate::vm::WitnessOrd;
46use crate::{CompletionError, CompositionError, PayError, WalletError};
47
48#[derive(Copy, Clone, PartialEq, Debug)]
49pub struct TxParams {
50 pub fee_sats: u64,
51 pub lock_time: Option<u32>,
52 pub seq_no: u32,
53 pub change_shift: bool,
54 pub change_keychain: u8,
55}
56
57impl TxParams {
58 pub fn with(fee_sats: u64) -> Self {
59 TxParams {
60 fee_sats,
61 lock_time: None,
62 seq_no: 0,
63 change_shift: true,
64 change_keychain: 1,
65 }
66 }
67}
68
69#[derive(Clone, PartialEq, Debug)]
70pub struct TransferParams {
71 pub tx: TxParams,
72 pub min_amount: u64,
73}
74
75impl TransferParams {
76 pub fn with(fee_sats: u64, min_amount_sats: u64) -> Self {
77 TransferParams {
78 tx: TxParams::with(fee_sats),
79 min_amount: min_amount_sats,
80 }
81 }
82}
83
84#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
85pub struct PsbtMeta {
86 pub beneficiary_vout: Option<u32>,
87 pub change_vout: Option<u32>,
88}
89
90struct PaymentContext {
91 contract_id: ContractId,
92 assignment_type: AssignmentType,
93 transition_type: TransitionType,
94}
95
96struct ContractOutpointsFilter<
97 'stock,
98 'wallet,
99 W: WalletProvider + ?Sized,
100 S: StashProvider,
101 H: StateProvider,
102 I: IndexProvider,
103> {
104 contract_id: ContractId,
105 stock: &'stock Stock<S, H, I>,
106 wallet: &'wallet W,
107}
108
109impl<W: WalletProvider + ?Sized, S: StashProvider, H: StateProvider, I: IndexProvider>
110 AssignmentsFilter for ContractOutpointsFilter<'_, '_, W, S, H, I>
111{
112 fn should_include(&self, outpoint: impl Into<Outpoint>, witness_id: Option<Txid>) -> bool {
113 let outpoint = outpoint.into();
114 if !self
115 .wallet
116 .filter_unspent()
117 .should_include(outpoint, witness_id)
118 {
119 return false;
120 }
121 matches!(self.stock.contract_assignments_for(self.contract_id, [outpoint]), Ok(list) if !list.is_empty())
122 }
123}
124
125#[allow(clippy::result_large_err)]
126fn validate_contract_and_invoice<S: StashProvider, H: StateProvider, I: IndexProvider>(
127 stock: &Stock<S, H, I>,
128 invoice: &RgbInvoice,
129) -> Result<PaymentContext, CompositionError> {
130 let contract_id = invoice.contract.ok_or(CompositionError::NoContract)?;
131 let contract = stock
132 .contract_data(contract_id)
133 .map_err(|e| e.to_string())?;
134
135 if let Some(invoice_schema) = invoice.schema {
136 if invoice_schema != contract.schema.schema_id() {
137 return Err(CompositionError::InvalidSchema);
138 }
139 }
140
141 let contract_genesis = stock
142 .as_stash_provider()
143 .genesis(contract_id)
144 .map_err(|_| CompositionError::UnknownContract)?;
145 let contract_chain_net = contract_genesis.chain_net;
146 let invoice_chain_net = invoice.chain_network();
147 if contract_chain_net != invoice_chain_net {
148 return Err(CompositionError::InvoiceBeneficiaryWrongChainNet(
149 invoice_chain_net,
150 contract_chain_net,
151 ));
152 }
153
154 if let Some(expiry) = invoice.expiry {
155 if expiry < Utc::now().timestamp() {
156 return Err(CompositionError::InvoiceExpired);
157 }
158 }
159
160 let Some(ref assignment_state) = invoice.assignment_state else {
161 return Err(CompositionError::NoAssignmentState);
162 };
163
164 let invoice_assignment_type = invoice
165 .assignment_name
166 .as_ref()
167 .map(|n| contract.schema.assignment_type(n.clone()));
168 let assignment_type = invoice_assignment_type
169 .as_ref()
170 .or_else(|| {
171 let assignment_types = contract
172 .schema
173 .assignment_types_for_state(assignment_state.clone().into());
174 if assignment_types.len() == 1 {
175 Some(assignment_types[0])
176 } else {
177 contract
178 .schema
179 .default_assignment
180 .as_ref()
181 .filter(|&assignment| assignment_types.contains(&assignment))
182 }
183 })
184 .ok_or(CompositionError::NoAssignmentType)?;
185 let transition_type = contract
186 .schema
187 .default_transition_for_assignment(assignment_type);
188
189 Ok(PaymentContext {
190 contract_id,
191 assignment_type: *assignment_type,
192 transition_type,
193 })
194}
195
196#[allow(clippy::result_large_err)]
197fn select_state_for_invoice<S: StashProvider, H: StateProvider, I: IndexProvider>(
198 stock: &Stock<S, H, I>,
199 invoice: &RgbInvoice,
200 context: &PaymentContext,
201 filter: &impl AssignmentsFilter,
202) -> Result<BTreeSet<OutputSeal>, CompositionError> {
203 let contract = stock
204 .contract_data(context.contract_id)
205 .map_err(|e| e.to_string())?;
206
207 let Some(ref assignment_state) = invoice.assignment_state else {
208 return Err(CompositionError::NoAssignmentState);
209 };
210
211 let prev_outputs = match assignment_state {
212 InvoiceState::Amount(amount) => {
213 let mut state: BTreeMap<_, Vec<Amount>> = BTreeMap::new();
214 for a in contract.fungible_raw(context.assignment_type, filter)? {
215 state.entry(a.seal).or_default().push(a.state);
216 }
217 let mut state: Vec<_> = state
218 .into_iter()
219 .map(|(seal, vals)| (vals.iter().copied().sum::<Amount>(), seal, vals))
220 .collect();
221 state.sort_by_key(|(sum, _, _)| *sum);
222 let mut sum = Amount::ZERO;
223 let selection = state
224 .iter()
225 .rev()
226 .take_while(|(val, _, _)| {
227 if sum >= *amount {
228 false
229 } else {
230 sum += *val;
231 true
232 }
233 })
234 .map(|(_, seal, _)| *seal)
235 .collect::<BTreeSet<_>>();
236
237 if sum < *amount {
238 bset![]
239 } else {
240 selection
241 }
242 }
243 InvoiceState::Data(NonFungible::FractionedToken(allocation)) => {
244 let data_state = RevealedData::from(*allocation);
245 contract
246 .data_raw(context.assignment_type, filter)?
247 .filter(|x| x.state == data_state)
248 .map(|x| x.seal)
249 .collect::<BTreeSet<_>>()
250 }
251 InvoiceState::Void => contract
252 .rights_raw(context.assignment_type, filter)?
253 .map(|x| x.seal)
254 .collect::<BTreeSet<_>>(),
255 };
256
257 Ok(prev_outputs)
258}
259
260#[allow(clippy::result_large_err)]
261fn build_main_transition<S: StashProvider, H: StateProvider, I: IndexProvider>(
262 stock: &Stock<S, H, I>,
263 invoice: &RgbInvoice,
264 context: &PaymentContext,
265 prev_outputs: &BTreeSet<OutputSeal>,
266 meta: &PsbtMeta,
267) -> Result<Transition, CompositionError> {
268 let Some(ref assignment_state) = invoice.assignment_state else {
269 return Err(CompositionError::NoAssignmentState);
270 };
271
272 let builder_seal = match (invoice.beneficiary.into_inner(), meta.beneficiary_vout) {
273 (Beneficiary::BlindedSeal(seal), None) => BuilderSeal::Concealed(seal),
274 (Beneficiary::BlindedSeal(_), Some(_)) => {
275 return Err(CompositionError::BeneficiaryVout);
276 }
277 (Beneficiary::WitnessVout(_, _), Some(vout)) => {
278 let seal = GraphSeal::with_blinded_vout(vout, rand::random());
279 BuilderSeal::Revealed(seal)
280 }
281 (Beneficiary::WitnessVout(_, _), None) => {
282 return Err(CompositionError::NoBeneficiaryOutput);
283 }
284 };
285
286 let mut main_builder = stock
287 .transition_builder_raw(context.contract_id, context.transition_type)
288 .map_err(|e| e.to_string())?;
289
290 let mut sum_inputs = Amount::ZERO;
291 let mut data_inputs = vec![];
292 for (_output, list) in stock
293 .contract_assignments_for(context.contract_id, prev_outputs.iter().copied())
294 .map_err(|e| e.to_string())?
295 {
296 for (opout, state) in list {
297 main_builder = main_builder.add_input(opout, state.clone())?;
298 if opout.ty != context.assignment_type {
299 let seal = create_change_output_seal(opout.ty, meta)?;
300 main_builder = main_builder.add_owned_state_raw(opout.ty, seal, state)?;
301 } else if let AllocatedState::Amount(value) = state {
302 sum_inputs += value.into();
303 } else if let AllocatedState::Data(value) = state {
304 data_inputs.push(value);
305 }
306 }
307 }
308
309 match assignment_state {
311 InvoiceState::Amount(amt) => {
312 if sum_inputs < *amt {
314 return Err(CompositionError::InsufficientState);
315 }
316
317 if *amt > Amount::ZERO {
318 main_builder = main_builder.add_fungible_state_raw(
319 context.assignment_type,
320 builder_seal,
321 *amt,
322 )?;
323 }
324
325 if sum_inputs > *amt {
327 let change_seal = create_change_output_seal(context.assignment_type, meta)?;
328 main_builder = main_builder.add_fungible_state_raw(
329 context.assignment_type,
330 change_seal,
331 sum_inputs - *amt,
332 )?;
333 }
334 }
335 InvoiceState::Data(data) => match data {
336 NonFungible::FractionedToken(allocation) => {
337 let lookup_state = RevealedData::from(*allocation);
338 if !data_inputs.into_iter().any(|x| x == lookup_state) {
339 return Err(CompositionError::InsufficientState);
340 }
341
342 main_builder = main_builder.add_data_raw(
343 context.assignment_type,
344 builder_seal,
345 lookup_state,
346 )?;
347 }
348 },
349 InvoiceState::Void => {
350 main_builder = main_builder.add_rights_raw(context.assignment_type, builder_seal)?;
351 }
352 }
353
354 if !main_builder.has_inputs() {
355 return Err(CompositionError::InsufficientState);
356 }
357
358 let transition = main_builder.complete_transition()?;
359 Ok(transition)
360}
361
362#[allow(clippy::result_large_err)]
363fn create_change_output_seal(
364 assignment_type: AssignmentType,
365 meta: &PsbtMeta,
366) -> Result<BuilderSeal<GraphSeal>, CompositionError> {
367 let vout = meta
368 .change_vout
369 .ok_or(CompositionError::NoExtraOrChange(assignment_type))?;
370 let seal = GraphSeal::with_blinded_vout(vout, rand::random());
371 Ok(BuilderSeal::Revealed(seal))
372}
373
374#[allow(clippy::result_large_err)]
375fn build_extra_transitions<S: StashProvider, H: StateProvider, I: IndexProvider>(
376 stock: &Stock<S, H, I>,
377 contract_id: ContractId,
378 prev_outputs: &BTreeSet<OutputSeal>,
379 meta: &PsbtMeta,
380) -> Result<Confined<Vec<Transition>, 0, { U24 - 1 }>, CompositionError> {
381 let prev_outputs_set = prev_outputs
382 .iter()
383 .copied()
384 .collect::<HashSet<OutputSeal>>();
385
386 let mut extra_state =
388 HashMap::<ContractId, HashMap<OutputSeal, HashMap<Opout, AllocatedState>>>::new();
389 for id in stock
390 .contracts_assigning(prev_outputs_set.iter().copied())
391 .map_err(|e| e.to_string())?
392 {
393 if id == contract_id {
395 continue;
396 }
397 let state = stock
398 .contract_assignments_for(id, prev_outputs_set.iter().copied())
399 .map_err(|e| e.to_string())?;
400 let entry = extra_state.entry(id).or_default();
401 for (seal, assigns) in state {
402 entry.entry(seal).or_default().extend(assigns);
403 }
404 }
405
406 let mut extras = Confined::<Vec<_>, 0, { U24 - 1 }>::with_capacity(extra_state.len());
408 for (id, seal_map) in extra_state {
409 let schema = stock
410 .as_stash_provider()
411 .contract_schema(id)
412 .map_err(|_| BuilderError::Inconsistency(StashInconsistency::ContractAbsent(id)))?;
413
414 for (_output, assigns) in seal_map {
415 for (opout, state) in assigns {
416 let transition_type = schema.default_transition_for_assignment(&opout.ty);
417
418 let mut extra_builder = stock
419 .transition_builder_raw(id, transition_type)
420 .map_err(|e| e.to_string())?;
421
422 let seal = create_change_output_seal(opout.ty, meta)?;
423 extra_builder = extra_builder
424 .add_input(opout, state.clone())?
425 .add_owned_state_raw(opout.ty, seal, state)?;
426
427 if !extra_builder.has_inputs() {
428 continue;
429 }
430 let transition = extra_builder.complete_transition()?;
431 extras
432 .push(transition)
433 .map_err(|_| CompositionError::TooManyExtras)?;
434 }
435 }
436 }
437
438 Ok(extras)
439}
440
441pub trait WalletProvider {
442 type P: RgbPropKeyExt;
443 type O: RgbOutExt<Self::P>;
444 type Psbt: RgbPsbtExt<Self::P, Self::O>;
445
446 fn close_method(&self) -> CloseMethod;
447
448 fn filter_outpoints(&self) -> impl AssignmentsFilter + Clone {
449 WalletFilter::new(self, Filter::Outpoints)
450 }
451
452 fn filter_unspent(&self) -> impl AssignmentsFilter + Clone {
453 WalletFilter::new(self, Filter::Unspent)
454 }
455
456 fn filter_witnesses(&self) -> impl AssignmentsFilter + Clone {
457 WalletFilter::new(self, Filter::Witness)
458 }
459
460 fn is_unspent(&self, outpoint: Outpoint) -> bool;
461
462 fn has_outpoint(&self, outpoint: Outpoint) -> bool;
463
464 fn should_include_witness(&self, witness_id: Option<Txid>) -> bool;
465
466 fn add_tapret_tweak(
467 &mut self,
468 terminal: Terminal,
469 tweak: TapretCommitment,
470 ) -> Result<(), Infallible>;
471
472 fn try_add_tapret_tweak(
473 &mut self,
474 transfer: Transfer,
475 txid: &Txid,
476 ) -> Result<(), Box<WalletError>>;
477
478 #[allow(clippy::result_large_err)]
479 fn pay<
480 S: StashProvider,
481 H: StateProvider,
482 I: IndexProvider,
483 P: RgbPropKeyExt,
484 O: RgbOutExt<P>,
485 >(
486 &mut self,
487 stock: &mut Stock<S, H, I>,
488 invoice: &RgbInvoice,
489 params: TransferParams,
490 ) -> Result<(Self::Psbt, PsbtMeta, Transfer), PayError> {
491 let (mut psbt, meta) = self.construct_psbt_rgb::<S, H, I, P, O>(stock, invoice, params)?;
492 let transfer = match self.transfer(stock, invoice, &mut psbt, meta.beneficiary_vout) {
494 Ok(transfer) => transfer,
495 Err(e) => return Err(PayError::Completion(e)),
496 };
497 Ok((psbt, meta, transfer))
498 }
499
500 #[allow(clippy::result_large_err)]
501 fn create_psbt(
502 &mut self,
503 invoice: &RgbInvoice,
504 close_method: CloseMethod,
505 coins: impl IntoIterator<Item = Outpoint>,
506 params: TransferParams,
507 ) -> Result<(Self::Psbt, PsbtMeta), CompositionError>;
508
509 #[allow(clippy::result_large_err)]
510 fn construct_psbt_rgb<
511 S: StashProvider,
512 H: StateProvider,
513 I: IndexProvider,
514 P: RgbPropKeyExt,
515 O: RgbOutExt<P>,
516 >(
517 &mut self,
518 stock: &Stock<S, H, I>,
519 invoice: &RgbInvoice,
520 params: TransferParams,
521 ) -> Result<(Self::Psbt, PsbtMeta), CompositionError> {
522 let close_method = self.close_method();
523
524 let context = validate_contract_and_invoice(stock, invoice)?;
526
527 let filter = ContractOutpointsFilter {
529 contract_id: context.contract_id,
530 stock,
531 wallet: self,
532 };
533 let prev_outputs = select_state_for_invoice(stock, invoice, &context, &filter)?;
534
535 if prev_outputs.is_empty() {
536 return Err(CompositionError::InsufficientState);
537 }
538
539 let prev_outpoints = prev_outputs
540 .iter()
541 .map(|o| Outpoint::new(o.txid, o.vout.to_u32()));
542
543 let (mut psbt, meta) = self.create_psbt(invoice, close_method, prev_outpoints, params)?;
544
545 let main = build_main_transition(stock, invoice, &context, &prev_outputs, &meta)?;
547
548 let extras = build_extra_transitions(stock, context.contract_id, &prev_outputs, &meta)?;
550
551 let mut batch = Batch { main, extras };
552 batch.set_priority(u64::MAX);
553
554 psbt.set_rgb_close_method(close_method);
555 psbt.set_as_unmodifiable();
556 psbt.rgb_embed(batch)?;
557 Ok((psbt, meta))
558 }
559
560 #[allow(clippy::result_large_err)]
561 fn transfer<S: StashProvider, H: StateProvider, P: IndexProvider>(
562 &mut self,
563 stock: &mut Stock<S, H, P>,
564 invoice: &RgbInvoice,
565 psbt: &mut Self::Psbt,
566 beneficiary_vout: Option<u32>,
567 ) -> Result<Transfer, CompletionError> {
568 let contract_id = invoice.contract.ok_or(CompletionError::NoContract)?;
569
570 let fascia = psbt.rgb_commit()?;
571 if matches!(fascia.seal_witness().dbc_proof.method(), CloseMethod::TapretFirst) {
572 if psbt.rgb_tapret_host_on_change() {
574 let output = psbt
575 .dbc_output::<TapretProof>()
576 .ok_or(TapretKeyError::NotTaprootOutput)?;
577 let terminal = output
578 .terminal_derivation()
579 .ok_or_else(|| CompletionError::InconclusiveDerivation)?;
580 let tapret_commitment = output.tapret_commitment()?;
581 self.add_tapret_tweak(terminal, tapret_commitment)?;
582 }
583 }
584
585 let witness_id = psbt.get_txid();
586 let (beneficiary1, beneficiary2) = match invoice.beneficiary.into_inner() {
587 Beneficiary::WitnessVout(_, _) => {
588 let seal = ExplicitSeal::new(Outpoint::new(witness_id, beneficiary_vout.unwrap()));
589 (vec![], vec![seal])
590 }
591 Beneficiary::BlindedSeal(seal) => (vec![seal], vec![]),
592 };
593
594 struct FasciaResolver {
595 witness_id: Txid,
596 }
597 impl WitnessOrdProvider for FasciaResolver {
598 fn witness_ord(&self, witness_id: Txid) -> Result<WitnessOrd, WitnessResolverError> {
599 assert_eq!(witness_id, self.witness_id);
600 Ok(WitnessOrd::Tentative)
601 }
602 }
603
604 stock
605 .consume_fascia(fascia, FasciaResolver { witness_id })
606 .map_err(|e| e.to_string())?;
607 let transfer = stock
608 .transfer(contract_id, beneficiary2, beneficiary1, [], Some(witness_id))
609 .map_err(|e| e.to_string())?;
610
611 Ok(transfer)
612 }
613}