Skip to main content

rgb/
pay.rs

1// RGB API library for smart contracts on Bitcoin & Lightning network
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Written in 2019-2023 by
6//     Dr Maxim Orlovsky <orlovsky@lnp-bp.org>
7//
8// Copyright (C) 2019-2023 LNP/BP Standards Association. All rights reserved.
9//
10// Licensed under the Apache License, Version 2.0 (the "License");
11// you may not use this file except in compliance with the License.
12// You may obtain a copy of the License at
13//
14//     http://www.apache.org/licenses/LICENSE-2.0
15//
16// Unless required by applicable law or agreed to in writing, software
17// distributed under the License is distributed on an "AS IS" BASIS,
18// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19// See the License for the specific language governing permissions and
20// limitations under the License.
21
22use 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    // Add payments to beneficiary and change
310    match assignment_state {
311        InvoiceState::Amount(amt) => {
312            // Pay beneficiary
313            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            // Pay change
326            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    // Enumerate state for other contracts
387    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        // Skip current contract
394        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    // Construct transitions for extra state
407    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        // ... here we pass PSBT around signers, if necessary
493        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        // 1. Validate contract and invoice
525        let context = validate_contract_and_invoice(stock, invoice)?;
526
527        // 2. Select state for the invoice
528        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        // 3. Build main transition
546        let main = build_main_transition(stock, invoice, &context, &prev_outputs, &meta)?;
547
548        // 4. Build extra transitions for other contracts
549        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            // save tweak only if tapret commitment is on the bitcoin change
573            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}