Skip to content

Commit

Permalink
cli: support NFT invoicing
Browse files Browse the repository at this point in the history
  • Loading branch information
dr-orlovsky committed Sep 22, 2024
1 parent da65a6d commit 955c52f
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 25 deletions.
163 changes: 138 additions & 25 deletions cli/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,13 @@ use rgb::schema::SchemaId;
use rgb::validation::Validity;
use rgb::vm::{RgbIsa, WitnessOrd};
use rgb::{
BundleId, ContractId, DescriptorRgb, GenesisSeal, GraphSeal, Identity, OpId, OutputSeal,
RgbDescr, RgbKeychain, RgbWallet, StateType, TransferParams, WalletError, WalletProvider,
XChain, XOutpoint, XWitnessId,
Allocation, BundleId, ContractId, DescriptorRgb, GenesisSeal, GraphSeal, Identity, OpId,
OutputSeal, RgbDescr, RgbKeychain, RgbWallet, StateType, TokenIndex, TransferParams,
WalletError, WalletProvider, XChain, XOutpoint, XWitnessId,
};
use rgbstd::interface::{AllocatedState, ContractIface};
use rgbstd::interface::{AllocatedState, ContractIface, OwnedIface};
use rgbstd::persistence::{MemContractState, StockError};
use rgbstd::stl::rgb_contract_stl;
use rgbstd::{KnownState, OutputAssignment};
use seals::SecretSeal;
use serde_crate::{Deserialize, Serialize};
Expand Down Expand Up @@ -120,7 +121,7 @@ pub enum Command {
#[display("state")]
State {
/// Show all state, including already spent and not owned by the wallet
#[clap(short, long)]
#[arg(short, long)]
all: bool,

/// Contract identifier
Expand All @@ -135,7 +136,7 @@ pub enum Command {
#[display("history")]
History {
/// Print detailed information
#[clap(long)]
#[arg(long)]
details: bool,

Check warning on line 140 in cli/src/command.rs

View check run for this annotation

Codecov / codecov/patch

cli/src/command.rs#L140

Added line #L140 was not covered by tests

/// Contract identifier
Expand Down Expand Up @@ -165,17 +166,31 @@ pub enum Command {
#[display("invoice")]
Invoice {
/// Force address-based invoice
#[clap(short, long)]
#[arg(short, long)]
address_based: bool,

/// Contract identifier
contract_id: ContractId,

/// Interface to interpret the state data
#[arg(short, long)]
iface: Option<String>,

/// Operation to use for the invoice
///
/// If no operation is provided, the interface default operation is used.
#[arg(short, long)]
operation: Option<String>,

/// State name to use for the invoice
///
/// If no state name is provided, the interface default state name for the operation is
/// used.
#[arg(short, long, requires = "operation")]
state: Option<String>,

/// Contract identifier
contract_id: ContractId,

/// Value (for fungible token) or token ID (for NFT) to transfer
value: u64,
value: Option<u64>,
},

/// Prepare PSBT file for transferring RGB assets
Expand All @@ -189,7 +204,7 @@ pub enum Command {

/// Amount of satoshis which should be paid to the address-based
/// beneficiary
#[clap(long, default_value = "2000")]
#[arg(long, default_value = "2000")]
sats: Sats,

/// Invoice data
Expand Down Expand Up @@ -222,19 +237,19 @@ pub enum Command {
#[display("transfer")]
Transfer {
/// Encode PSBT as V2
#[clap(short = '2')]
#[arg(short = '2')]
v2: bool,

/// Amount of satoshis which should be paid to the address-based
/// beneficiary
#[clap(long, default_value = "2000")]
#[arg(long, default_value = "2000")]
sats: Sats,

/// Invoice data
invoice: RgbInvoice,

/// Fee for bitcoin transaction, in satoshis
#[clap(short, long, default_value = "400")]
#[arg(short, long, default_value = "400")]
fee: Sats,

/// File for generated transfer consignment
Expand Down Expand Up @@ -781,18 +796,14 @@ impl Exec for RgbArgs {
}
Command::Invoice {
address_based,
operation,
state,

Check warning on line 800 in cli/src/command.rs

View check run for this annotation

Codecov / codecov/patch

cli/src/command.rs#L799-L800

Added lines #L799 - L800 were not covered by tests
contract_id,
iface,
value,
} => {
let mut wallet = self.rgb_wallet(&config)?;

let iface = match contract_default_iface_name(*contract_id, wallet.stock(), iface)?
{
ControlFlow::Continue(name) => name,
ControlFlow::Break(_) => return Ok(()),
};

let outpoint = wallet
.wallet()
.coinselect(Sats::ZERO, |utxo| {
Expand Down Expand Up @@ -828,11 +839,113 @@ impl Exec for RgbArgs {
Beneficiary::BlindedSeal(*seal.to_secret_seal().as_reduced_unsafe())
}
};
let invoice = RgbInvoiceBuilder::new(XChainNet::bitcoin(network, beneficiary))

let iface = match contract_default_iface_name(*contract_id, wallet.stock(), iface)?

Check warning on line 843 in cli/src/command.rs

View check run for this annotation

Codecov / codecov/patch

cli/src/command.rs#L843

Added line #L843 was not covered by tests
{
ControlFlow::Continue(name) => wallet.stock().iface(name)?,
ControlFlow::Break(_) => return Ok(()),

Check warning on line 846 in cli/src/command.rs

View check run for this annotation

Codecov / codecov/patch

cli/src/command.rs#L845-L846

Added lines #L845 - L846 were not covered by tests
};
let iface_name = &iface.name;
let Some(op_name) = operation
.clone()
.map(FieldName::try_from)
.transpose()
.map_err(|e| WalletError::Invoicing(format!("invalid operation name - {e}")))?
.or(iface.default_operation.clone())

Check warning on line 854 in cli/src/command.rs

View check run for this annotation

Codecov / codecov/patch

cli/src/command.rs#L848-L854

Added lines #L848 - L854 were not covered by tests
else {
return Err(WalletError::Invoicing(format!(
"interface {iface_name} doesn't have default operation"
)));

Check warning on line 858 in cli/src/command.rs

View check run for this annotation

Codecov / codecov/patch

cli/src/command.rs#L856-L858

Added lines #L856 - L858 were not covered by tests
};
let Some(iface_op) = iface.transitions.get(&op_name) else {
return Err(WalletError::Invoicing(format!(
"interface {iface_name} doesn't have operation {op_name}"
)));

Check warning on line 863 in cli/src/command.rs

View check run for this annotation

Codecov / codecov/patch

cli/src/command.rs#L860-L863

Added lines #L860 - L863 were not covered by tests
};
let state_name = state
.clone()
.map(FieldName::try_from)
.transpose()
.map_err(|e| WalletError::Invoicing(format!("invalid state name - {e}")))?
.or_else(|| iface_op.default_assignment.clone())
.ok_or_else(|| {
WalletError::Invoicing(format!(
"interface {iface_name} doesn't have a default state for the \
operation {op_name}"
))
})?;
let Some(assign_iface) = iface.assignments.get(&state_name) else {
return Err(WalletError::Invoicing(format!(
"interface {iface_name} doesn't have state {state_name} in operation \
{op_name}"
)));

Check warning on line 881 in cli/src/command.rs

View check run for this annotation

Codecov / codecov/patch

cli/src/command.rs#L865-L881

Added lines #L865 - L881 were not covered by tests
};

let mut builder = RgbInvoiceBuilder::new(XChainNet::bitcoin(network, beneficiary))

Check warning on line 884 in cli/src/command.rs

View check run for this annotation

Codecov / codecov/patch

cli/src/command.rs#L884

Added line #L884 was not covered by tests
.set_contract(*contract_id)
.set_interface(iface)
.set_amount_raw(*value)
.finish();
.set_interface(iface_name.clone());

if operation.is_some() {
builder = builder.set_operation(op_name);
if let Some(state) = state {
builder = builder.set_operation(fname!(state.clone()));
}
}

Check warning on line 893 in cli/src/command.rs

View check run for this annotation

Codecov / codecov/patch

cli/src/command.rs#L886-L893

Added lines #L886 - L893 were not covered by tests

match (assign_iface.owned_state, value) {

Check warning on line 895 in cli/src/command.rs

View check run for this annotation

Codecov / codecov/patch

cli/src/command.rs#L895

Added line #L895 was not covered by tests
(
OwnedIface::Rights
| OwnedIface::Amount
| OwnedIface::AnyData
| OwnedIface::Data(_),
None,
) => {
// There is no state which has to be added to the invoice
}

Check warning on line 904 in cli/src/command.rs

View check run for this annotation

Codecov / codecov/patch

cli/src/command.rs#L902-L904

Added lines #L902 - L904 were not covered by tests
(OwnedIface::Rights, Some(_)) => {
return Err(WalletError::Invoicing(format!(
"state {state_name} in interface {iface_name} defines a right and it \
can't has a value"
)));

Check warning on line 909 in cli/src/command.rs

View check run for this annotation

Codecov / codecov/patch

cli/src/command.rs#L906-L909

Added lines #L906 - L909 were not covered by tests
}
(OwnedIface::Amount, Some(amount)) => {
builder = builder.set_amount_raw(*amount);
}
(OwnedIface::Data(sem_id), Some(_))
if sem_id
!= rgb_contract_stl()
.types
.get(&tn!("Allocation"))
.expect("STL is broken")
.sem_id_named(&tn!("Allocation")) =>
{
return Err(WalletError::Invoicing(format!(
"state {state_name} in interface {iface_name} has a type which can't \
be used with a non-fungible state allocation"
)));

Check warning on line 925 in cli/src/command.rs

View check run for this annotation

Codecov / codecov/patch

cli/src/command.rs#L911-L925

Added lines #L911 - L925 were not covered by tests
}
(OwnedIface::AnyData | OwnedIface::Data(_), Some(value)) => {
builder = builder.set_allocation_raw(Allocation::with(
TokenIndex::from(*value as u32),
// TODO: Support fractional NFT invoicing
0,
))

Check warning on line 932 in cli/src/command.rs

View check run for this annotation

Codecov / codecov/patch

cli/src/command.rs#L927-L932

Added lines #L927 - L932 were not covered by tests
}

(OwnedIface::Any, _) => {
return Err(WalletError::Invoicing(format!(
"state {state_name} in interface {iface_name} can be of any type; \
adding it to the invoice is impossible"
)));

Check warning on line 939 in cli/src/command.rs

View check run for this annotation

Codecov / codecov/patch

cli/src/command.rs#L936-L939

Added lines #L936 - L939 were not covered by tests
}
(OwnedIface::AnyAttach, _) => {
return Err(WalletError::Invoicing(s!(
"invoicing with attachments is not yet supported"
)));

Check warning on line 944 in cli/src/command.rs

View check run for this annotation

Codecov / codecov/patch

cli/src/command.rs#L942-L944

Added lines #L942 - L944 were not covered by tests
}
}

let invoice = builder.finish();

Check warning on line 948 in cli/src/command.rs

View check run for this annotation

Codecov / codecov/patch

cli/src/command.rs#L948

Added line #L948 was not covered by tests
println!("{invoice}");
}
Command::Prepare {
Expand Down
2 changes: 2 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ pub enum WalletError {
#[from]
Contract(ContractError),

Invoicing(String),

#[from]
PsbtDecode(psrgbt::DecodeError),

Expand Down

0 comments on commit 955c52f

Please sign in to comment.