diff --git a/Cargo.lock b/Cargo.lock index e575d43..5349e2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -240,12 +240,6 @@ dependencies = [ "sha2", ] -[[package]] -name = "base58" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6107fe1be6682a68940da878d9e9f5e90ca5745b3dec9fd1bb393c8777d4f581" - [[package]] name = "base64" version = "0.22.1" @@ -1118,7 +1112,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -1378,11 +1372,12 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rgb-core" version = "0.11.0-beta.9" -source = "git+https://github.com/RGB-WG/rgb-core?branch=feat/fungible-nonconf#8267ce26b1edf905553a49e5dcc42d5f2f0df338" +source = "git+https://github.com/RGB-WG/rgb-core?branch=feat/fungible-nonconf#e92e5b176886b3f04d8cfbb41714ab8f7d0b8046" dependencies = [ "aluvm", "amplify", "baid64", + "base64", "bp-core", "chrono", "commit_verify", @@ -1397,11 +1392,10 @@ dependencies = [ [[package]] name = "rgb-invoice" version = "0.11.0-beta.9" -source = "git+https://github.com/RGB-WG/rgb-std?branch=feat/fungible-nonconf#8f7020c5a04d019e2fb376d95031be1fecfdedf6" +source = "git+https://github.com/RGB-WG/rgb-std?branch=feat/fungible-nonconf#bc455d70d168deb56551ad67885c498f6f27d2b4" dependencies = [ "amplify", "baid64", - "base58", "bp-core", "bp-invoice", "fluent-uri", @@ -1462,7 +1456,7 @@ dependencies = [ [[package]] name = "rgb-std" version = "0.11.0-beta.9" -source = "git+https://github.com/RGB-WG/rgb-std?branch=feat/fungible-nonconf#8f7020c5a04d019e2fb376d95031be1fecfdedf6" +source = "git+https://github.com/RGB-WG/rgb-std?branch=feat/fungible-nonconf#bc455d70d168deb56551ad67885c498f6f27d2b4" dependencies = [ "aluvm", "amplify", @@ -1800,9 +1794,8 @@ dependencies = [ [[package]] name = "strict_types" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f16e8855a575633815f01482ac927ebaca3d2485aec8e17226c6826de29154e" +version = "2.7.1" +source = "git+https://github.com/strict-types/strict-types?branch=develop#729a4f86d25dfcea15ed15bbeb1e027473401c58" dependencies = [ "amplify", "ascii-armor", diff --git a/Cargo.toml b/Cargo.toml index 818ac6d..56aea97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ amplify = "4.7.0" nonasync = { version = "0.1.2", features = ["log"] } baid64 = "0.2.2" strict_encoding = "2.7.0" -strict_types = "2.7.0" +strict_types = "2.7.1" commit_verify = "0.11.0-beta.9" bp-core = "0.11.0-beta.9" bp-seals = "0.11.0-beta.9" @@ -106,6 +106,7 @@ features = ["all"] [patch.crates-io] commit_verify = { git = "https://github.com/LNP-BP/client_side_validation", branch = "develop" } single_use_seals = { git = "https://github.com/LNP-BP/client_side_validation", branch = "develop" } +strict_types = { git = "https://github.com/strict-types/strict-types", branch = "develop" } aluvm = { git = "https://github.com/AluVM/rust-aluvm", branch = "develop" } bp-consensus = { git = "https://github.com/BP-WG/bp-core", branch = "develop" } bp-dbc = { git = "https://github.com/BP-WG/bp-core", branch = "develop" } diff --git a/cli/src/command.rs b/cli/src/command.rs index 93aec14..66833c9 100644 --- a/cli/src/command.rs +++ b/cli/src/command.rs @@ -25,11 +25,11 @@ use std::ops::ControlFlow; use std::path::PathBuf; use std::str::FromStr; -use amplify::confinement::{SmallOrdMap, TinyOrdMap, TinyOrdSet, U16 as MAX16}; +use amplify::confinement::{SmallOrdMap, TinyOrdMap, TinyOrdSet}; use baid64::DisplayBaid64; use bpstd::{Sats, XpubDerivable}; -use bpwallet::cli::{BpCommand, Config, Exec}; use bpwallet::Wallet; +use bpwallet::cli::{BpCommand, Config, Exec}; use psbt::{Psbt, PsbtVer}; use rgb::containers::{ BuilderSeal, ConsignmentExt, ContainerVer, ContentId, ContentSigs, Contract, FileContent, @@ -37,24 +37,20 @@ use rgb::containers::{ }; use rgb::interface::{AssignmentsFilter, ContractOp, IfaceId}; use rgb::invoice::{Beneficiary, Pay2Vout, RgbInvoice, RgbInvoiceBuilder, XChainNet}; -use rgb::persistence::{MemContract, StashReadProvider, Stock}; +use rgb::persistence::{MemContract, StashReadProvider, Stock, StockError}; use rgb::resolvers::ContractIssueResolver; use rgb::schema::SchemaId; use rgb::validation::Validity; use rgb::vm::{RgbIsa, WitnessOrd}; use rgb::{ - Allocation, BundleId, ContractId, DescriptorRgb, GenesisSeal, GraphSeal, Identity, OpId, - OutputSeal, OwnedFraction, RgbDescr, RgbKeychain, RgbWallet, StateType, TokenIndex, - TransferParams, WalletError, WalletProvider, XChain, XOutpoint, XWitnessId, + AttachId, BundleId, ContractId, DescriptorRgb, GenesisSeal, GraphSeal, Identity, OpId, + OutputSeal, RgbDescr, RgbKeychain, RgbWallet, STATE_DATA_MAX_LEN, TransferParams, WalletError, + WalletProvider, XChain, XOutpoint, XWitnessId, }; -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}; -use strict_types::encoding::{FieldName, TypeName}; use strict_types::StrictVal; +use strict_types::encoding::{FieldName, TypeName}; use crate::RgbArgs; @@ -184,27 +180,19 @@ pub enum Command { /// If no state name is provided, the interface default state name for the operation is /// used. #[arg(short, long, requires = "operation")] - state: Option, + assignment: Option, /// Contract identifier contract_id: ContractId, - /// Amount of tokens (in the smallest unit) to transfer - #[arg(short, long)] - amount: Option, - - /// Token index for NFT transfer - #[arg(long)] - token_index: Option, - - /// Fraction of an NFT token to transfer - #[arg(long, requires = "token_index")] - token_fraction: Option, + /// State for the invoice + state: Option, }, /// Prepare PSBT file for transferring RGB assets /// - /// In the most of cases you need to use `transfer` command instead of `prepare` and `consign`. + /// In the most of the cases you need to use `transfer` command instead of `prepare` and + /// `consign`. #[display("prepare")] Prepare { /// Encode PSBT as V2 @@ -398,16 +386,14 @@ impl Exec for RgbArgs { ty, opids, state, + attach_id, to, witness, } in history { print!("{:9}\t", direction.to_string()); - if let AllocatedState::Amount(amount) = state { - print!("{: >9}", amount.value()); - } else { - print!("{state:>9}"); - } + + print!("{state}"); if *details { print!("\t{ty}"); } @@ -420,13 +406,16 @@ impl Exec for RgbArgs { ); if *details { println!( - "\topid={}", + "\topid {}", opids .iter() .map(OpId::to_string) .collect::>() - .join("\n\topid=") - ) + .join("\n\topid ") + ); + if let Some(attach) = attach_id { + println!("attach {attach}"); + } } } } @@ -609,60 +598,22 @@ impl Exec for RgbArgs { } println!("\nOwned:"); - fn witness( - allocation: &OutputAssignment, - contract: &ContractIface>, - ) -> String { - allocation - .witness - .and_then(|w| contract.witness_info(w)) - .map(|info| format!("{} ({})", info.id, info.ord)) - .unwrap_or_else(|| s!("~")) - } for owned in &contract.iface.assignments { println!(" State \t{:78}\tWitness", "Seal"); println!(" {}:", owned.name); - if let Ok(allocations) = contract.fungible(owned.name.clone(), &filter) { - for allocation in allocations { + if let Ok(outputs) = contract.outputs_by_type(owned.name.clone(), &filter) { + for output in outputs { + let witness = output + .witness + .and_then(|w| contract.witness_info(w)) + .map(|info| format!("{} ({})", info.id, info.ord)) + .unwrap_or_else(|| s!("~")); println!( " {: >9}\t{}\t{} {}", - allocation.state.value(), - allocation.seal, - witness(&allocation, &contract), - filter.comment(allocation.seal.to_outpoint()) - ); - } - } - if let Ok(allocations) = contract.data(owned.name.clone(), &filter) { - for allocation in allocations { - println!( - " {: >9}\t{}\t{} {}", - allocation.state, - allocation.seal, - witness(&allocation, &contract), - filter.comment(allocation.seal.to_outpoint()) - ); - } - } - if let Ok(allocations) = contract.attachments(owned.name.clone(), &filter) { - for allocation in allocations { - println!( - " {: >9}\t{}\t{} {}", - allocation.state, - allocation.seal, - witness(&allocation, &contract), - filter.comment(allocation.seal.to_outpoint()) - ); - } - } - if let Ok(allocations) = contract.rights(owned.name.clone(), &filter) { - for allocation in allocations { - println!( - " {: >9}\t{}\t{} {}", - "right", - allocation.seal, - witness(&allocation, &contract), - filter.comment(allocation.seal.to_outpoint()) + output.state, + output.seal, + witness, + filter.comment(output.seal.to_outpoint()) ); } } @@ -674,21 +625,15 @@ impl Exec for RgbArgs { contract, } => { let mut stock = self.rgb_stock()?; - - let file = fs::File::open(contract)?; - - let code = serde_yaml::from_reader::<_, serde_yaml::Value>(file)?; - - let code = code - .as_mapping() - .expect("invalid YAML root-level structure"); + let file = File::open(contract)?; + let src = serde_yaml::from_reader::<_, serde_yaml::Value>(file)?; + let code = src.as_mapping().expect("invalid YAML root-level structure"); let iface_name = code .get("interface") .expect("contract must specify interface under which it is constructed") .as_str() .expect("interface name must be a string"); - let schema_ifaces = stock.schema(*schema_id)?; let iface_name = tn!(iface_name.to_owned()); let iface = stock .iface(iface_name.clone()) @@ -698,14 +643,8 @@ impl Exec for RgbArgs { })? .clone(); let iface_id = iface.iface_id(); - let iface_impl = schema_ifaces.get(iface_id).ok_or_else(|| { - WalletError::Custom(format!( - "no known interface implementation for {iface_name}" - )) - })?; let mut builder = stock.contract_builder(issuer.clone(), *schema_id, iface_id)?; - let types = builder.type_system().clone(); if let Some(globals) = code.get("globals") { for (name, val) in globals @@ -715,31 +654,12 @@ impl Exec for RgbArgs { let name = name .as_str() .expect("invalid YAML: global name must be a string"); - let state_type = iface_impl - .global_state - .iter() - .find(|info| info.name.as_str() == name) - .unwrap_or_else(|| panic!("unknown type name '{name}'")) - .id; - let sem_id = schema_ifaces - .schema - .global_types - .get(&state_type) - .expect("invalid schema implementation") - .sem_id; - let val = StrictVal::from(val.clone()); - let typed_val = types - .typify(val, sem_id) - .expect("global type doesn't match type definition"); - - let serialized = types - .strict_serialize_type::(&typed_val) - .expect("internal error"); // Workaround for borrow checker: let field_name = FieldName::try_from(name.to_owned()).expect("invalid type name"); + let value = StrictVal::from(val.clone()); builder = builder - .add_global_state(field_name, serialized) + .add_global_state(field_name, value) .expect("invalid global state data"); } } @@ -752,17 +672,9 @@ impl Exec for RgbArgs { let name = name .as_str() .expect("invalid YAML: assignments name must be a string"); - let state_type = iface_impl - .assignments - .iter() - .find(|info| info.name.as_str() == name) - .expect("unknown type name") - .id; - let state_schema = schema_ifaces - .schema - .owned_types - .get(&state_type) - .expect("invalid schema implementation"); + // Workaround for borrow checker: + let field_name = + FieldName::try_from(name.to_owned()).expect("invalid type name"); let assign = val.as_mapping().expect("an assignment must be a mapping"); let seal = assign @@ -772,26 +684,17 @@ impl Exec for RgbArgs { .expect("seal must be a string"); let seal = OutputSeal::from_str(seal).expect("invalid seal definition"); let seal = GenesisSeal::new_random(seal.method, seal.txid, seal.vout); + let seal = BuilderSeal::Revealed(XChain::Bitcoin(seal)); - // Workaround for borrow checker: - let field_name = - FieldName::try_from(name.to_owned()).expect("invalid type name"); - match state_schema.state_type() { - StateType::Void => todo!(), - StateType::Fungible => { - let amount = assign - .get("amount") - .expect("owned state must be a fungible amount") - .as_u64() - .expect("fungible state must be an integer"); - let seal = BuilderSeal::Revealed(XChain::Bitcoin(seal)); - builder = builder - .add_fungible_state(field_name, seal, amount) - .expect("invalid global state data"); - } - StateType::Structured => todo!(), - StateType::Attachment => todo!(), - } + let data = + StrictVal::from(assign.get("state").expect("absent state").clone()); + let attach = assign.get("attachment").map(|id| { + AttachId::from_str(id.as_str().expect("invalid attachment data")) + .expect("invalid attachment id string") + }); + builder = builder + .add_owned_state(field_name, seal, data, attach) + .expect("invalid owned state data"); } } @@ -806,12 +709,10 @@ impl Exec for RgbArgs { Command::Invoice { address_based, operation, - state, + assignment, contract_id, iface, - amount, - token_index, - token_fraction, + state, } => { let mut wallet = self.rgb_wallet(&config)?; @@ -873,7 +774,7 @@ impl Exec for RgbArgs { "interface {iface_name} doesn't have operation {op_name}" ))); }; - let state_name = state + let state_name = assignment .clone() .map(FieldName::try_from) .transpose() @@ -898,77 +799,27 @@ impl Exec for RgbArgs { if operation.is_some() { builder = builder.set_operation(op_name); - if let Some(state) = state { + if let Some(state) = assignment { builder = builder.set_operation(fname!(state.clone())); } } - match (assign_iface.owned_state, amount, token_index.map(|i| (i, token_fraction))) { - ( - OwnedIface::Rights - | OwnedIface::Amount - | OwnedIface::AnyData - | OwnedIface::Data(_), - None, - None, - ) => { - // There is no state which has to be added to the invoice - } - (OwnedIface::Rights, Some(_), None | Some(_)) - | (OwnedIface::Rights, None, Some(_)) => { - return Err(WalletError::Invoicing(format!( - "state {state_name} in interface {iface_name} defines a right and it \ - can't has a value or a token information" - ))); - } - (OwnedIface::Amount, _, Some(_)) => { - return Err(WalletError::Invoicing(format!( - "state {state_name} in interface {iface_name} defines a fungible \ - state, while a non-fungible token index is provided for the invoice. \ - Please use only --amount argument" - ))); - } - (OwnedIface::Amount, Some(amount), None) => { - builder = builder.set_amount_raw(*amount); - } - (OwnedIface::Data(_) | OwnedIface::AnyData, Some(_), _) => { - return Err(WalletError::Invoicing(format!( - "state {state_name} in interface {iface_name} defines a non-fungible \ - state, while a fungible amount is provided for the invoice. Please \ - use only --token-index and --token-fraction arguments" - ))); - } - (OwnedIface::Data(sem_id), None, 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" - ))); - } - (OwnedIface::AnyData | OwnedIface::Data(_), None, Some((index, fraction))) => { - builder = builder.set_allocation_raw(Allocation::with( - index, - fraction.unwrap_or(OwnedFraction::from(0)), - )) - } - - (OwnedIface::Any, _, _) => { + if let Some(state) = state { + let state: StrictVal = serde_yaml::from_str(&state)?; + let Some(sem_id) = assign_iface.state_ty else { return Err(WalletError::Invoicing(format!( - "state {state_name} in interface {iface_name} can be of any type; \ - adding it to the invoice is impossible" + "interface {iface_name} doesn't define a state type for the invoiced \ + assignment" ))); - } - (OwnedIface::AnyAttach, _, _) => { - return Err(WalletError::Invoicing(s!( - "invoicing with attachments is not yet supported" - ))); - } + }; + let types = wallet.stock().type_system(iface)?; + let value = types + .typify(state, sem_id) + .map_err(|e| WalletError::Invoicing(format!("invalid state data - {e}")))?; + let data = types + .strict_serialize_value::(&value) + .map_err(|_| WalletError::Invoicing(s!("state data too large")))?; + builder = builder.set_state(data.into()); } let invoice = builder.finish(); @@ -986,7 +837,7 @@ impl Exec for RgbArgs { let params = TransferParams::with(*fee, *sats); let (psbt, _) = wallet - .construct_psbt(invoice, params) + .construct_psbt(invoice.clone(), params) .map_err(|err| err.to_string())?; let ver = if *v2 { PsbtVer::V2 } else { PsbtVer::V0 }; diff --git a/examples/rgb20-demo.yaml b/examples/rgb20-demo.yaml index 575c6d7..d4a5684 100644 --- a/examples/rgb20-demo.yaml +++ b/examples/rgb20-demo.yaml @@ -27,4 +27,4 @@ globals: assignments: assetOwner: seal: tapret1st:b449f7eaa3f98c145b27ad0eeb7b5679ceb567faef7a52479bc995792b65f804:1 - amount: 100000000 # this is 1 million (we have two digits for cents) + state: 100000000 # this is 1 million (we have two digits for cents) diff --git a/src/pay.rs b/src/pay.rs index 2eaf1dd..6163ffc 100644 --- a/src/pay.rs +++ b/src/pay.rs @@ -33,10 +33,10 @@ use psrgbt::{ }; use rgbstd::containers::Transfer; use rgbstd::interface::AssignmentsFilter; -use rgbstd::invoice::{Beneficiary, InvoiceState, RgbInvoice}; +use rgbstd::invoice::{Beneficiary, RgbInvoice}; use rgbstd::persistence::{IndexProvider, StashProvider, StateProvider, Stock}; use rgbstd::validation::ResolveWitness; -use rgbstd::{ContractId, XChain, XOutpoint}; +use rgbstd::{ContractId, State, XChain, XOutpoint}; use crate::validation::WitnessResolverError; use crate::vm::{WitnessOrd, XWitnessTx}; @@ -123,7 +123,7 @@ where Self::Descr: DescriptorRgb invoice: &RgbInvoice, params: TransferParams, ) -> Result<(Psbt, PsbtMeta, Transfer), PayError> { - let (mut psbt, meta) = self.construct_psbt_rgb(stock, invoice, params)?; + let (mut psbt, meta) = self.construct_psbt_rgb(stock, invoice.clone(), params)?; // ... here we pass PSBT around signers, if necessary let transfer = match self.transfer(stock, invoice, &mut psbt) { Ok(transfer) => transfer, @@ -136,7 +136,7 @@ where Self::Descr: DescriptorRgb fn construct_psbt_rgb( &mut self, stock: &Stock, - invoice: &RgbInvoice, + invoice: RgbInvoice, mut params: TransferParams, ) -> Result<(Psbt, PsbtMeta), CompositionError> { let contract_id = invoice.contract.ok_or(CompositionError::NoContract)?; @@ -172,13 +172,6 @@ where Self::Descr: DescriptorRgb let contract = stock .contract_iface(contract_id, iface_name) .map_err(|e| e.to_string())?; - let prev_outputs = match &invoice.owned_state { - InvoiceState::Specific(state) => contract - .assignments_fulfilling(assignment_name, &filter, state, |s| s.state.value.clone())? - .map(|a| a.seal) - .collect::>(), - _ => return Err(CompositionError::Unsupported), - }; let beneficiaries = match invoice.beneficiary.into_inner() { Beneficiary::BlindedSeal(_) => vec![], Beneficiary::WitnessVout(pay2vout) => { @@ -188,6 +181,12 @@ where Self::Descr: DescriptorRgb )] } }; + let state_data = invoice.state.clone().ok_or(CompositionError::Unsupported)?; + let state = State::from(state_data); // TODO: Support attachments when they will be added to invoices + let prev_outputs = contract + .output_selection(assignment_name, &filter, |s| s.state.data.clone(), &state)? + .map(|a| a.seal) + .collect::>(); let prev_outpoints = prev_outputs .iter() // TODO: Support liquid diff --git a/src/wallet.rs b/src/wallet.rs index c24ac80..3a9fe89 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -25,9 +25,9 @@ use std::path::PathBuf; use bpstd::XpubDerivable; #[cfg(feature = "fs")] -use bpwallet::fs::FsTextStore; -#[cfg(feature = "fs")] use bpwallet::Wallet; +#[cfg(feature = "fs")] +use bpwallet::fs::FsTextStore; use bpwallet::{Layer2, NoLayer2}; #[cfg(not(target_arch = "wasm32"))] use nonasync::persistence::PersistenceProvider; @@ -140,7 +140,7 @@ where W::Descr: DescriptorRgb #[allow(clippy::result_large_err)] pub fn construct_psbt( &mut self, - invoice: &RgbInvoice, + invoice: RgbInvoice, params: TransferParams, ) -> Result<(Psbt, PsbtMeta), CompositionError> { self.wallet.construct_psbt_rgb(&self.stock, invoice, params)