From c57846880a4bfc625d74c8a8e4dbab87bac94062 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Sat, 18 Jan 2025 18:48:57 +0100 Subject: [PATCH 01/15] add operation `include` --- src/mound.rs | 4 ++-- src/popls/bp.rs | 8 +++++--- src/stockpile.rs | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/mound.rs b/src/mound.rs index e67412cd..6c42759b 100644 --- a/src/mound.rs +++ b/src/mound.rs @@ -165,14 +165,14 @@ impl> Mound { .filter_map(|(id, stockpile)| stockpile.seal(seal).map(|addr| (*id, addr))) } - pub fn attest( + pub fn include( &mut self, pub_witness: &::PubWitness, anchors: impl IntoIterator::CliWitness)>, ) { for (contract_id, opid, anchor) in anchors { self.contract_mut(contract_id) - .attest(opid, anchor, pub_witness); + .include(opid, anchor, pub_witness); } } diff --git a/src/popls/bp.rs b/src/popls/bp.rs index c8957e83..01a5c505 100644 --- a/src/popls/bp.rs +++ b/src/popls/bp.rs @@ -395,7 +395,7 @@ impl, X: Excavate> B Prefab { closes, defines, operation } } - /// Completes creation of a prefabricated operation pack, adding blank operations if necessary. + /// Complete creation of a prefabricated operation pack, adding blank operations if necessary. /// /// # Arguments /// @@ -493,7 +493,8 @@ impl, X: Excavate> B PrefabBundle(SmallOrdSet::try_from(prefabs).expect("too many operations")) } - pub fn attest( + /// Include prefab bundle into the mound, creating necessary anchors. + pub fn include( &mut self, bundle: &PrefabBundle, witness: &Tx, @@ -521,9 +522,10 @@ impl, X: Excavate> B }; (prefab.operation.contract_id, opid, anchor) }); - self.mound.attest(witness, iter); + self.mound.include(witness, iter); } + /// Consume consignment. #[allow(clippy::result_large_err)] pub fn consume( &mut self, diff --git a/src/stockpile.rs b/src/stockpile.rs index d6460689..a046169a 100644 --- a/src/stockpile.rs +++ b/src/stockpile.rs @@ -265,7 +265,7 @@ impl Stockpile { } } - pub fn attest( + pub fn include( &mut self, opid: Opid, anchor: ::CliWitness, From 15cd8c188cff58283ec9736618c1b8ef1d487574 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Thu, 23 Jan 2025 11:56:05 +0100 Subject: [PATCH 02/15] add errors to include operation --- Cargo.lock | 10 +++++----- src/mound.rs | 10 +++++----- src/pile.rs | 5 ++++- src/popls/bp.rs | 42 +++++++++++++++++++++++++++--------------- src/stockpile.rs | 4 ++-- 5 files changed, 43 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4079d690..e2be15af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -324,9 +324,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.2.9" +version = "1.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b" +checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" dependencies = [ "shlex", ] @@ -526,7 +526,7 @@ dependencies = [ [[package]] name = "hypersonic" version = "0.12.0-beta.4" -source = "git+https://github.com/AluVM/sonic?branch=master#5eb21d80bb2e00ff386adcd12b191be1e937a6f2" +source = "git+https://github.com/AluVM/sonic?branch=master#02fea6d4472bfada05555f2a01348787498add23" dependencies = [ "aluvm", "amplify", @@ -953,7 +953,7 @@ dependencies = [ [[package]] name = "sonic-api" version = "0.12.0-beta.4" -source = "git+https://github.com/AluVM/sonic?branch=master#5eb21d80bb2e00ff386adcd12b191be1e937a6f2" +source = "git+https://github.com/AluVM/sonic?branch=master#02fea6d4472bfada05555f2a01348787498add23" dependencies = [ "aluvm", "amplify", @@ -972,7 +972,7 @@ dependencies = [ [[package]] name = "sonic-callreq" version = "0.12.0-beta.4" -source = "git+https://github.com/AluVM/sonic?branch=master#5eb21d80bb2e00ff386adcd12b191be1e937a6f2" +source = "git+https://github.com/AluVM/sonic?branch=master#02fea6d4472bfada05555f2a01348787498add23" dependencies = [ "amplify", "baid64", diff --git a/src/mound.rs b/src/mound.rs index 6c42759b..e0cfa96c 100644 --- a/src/mound.rs +++ b/src/mound.rs @@ -167,13 +167,13 @@ impl> Mound { pub fn include( &mut self, + contract_id: ContractId, + opid: Opid, pub_witness: &::PubWitness, - anchors: impl IntoIterator::CliWitness)>, + anchor: ::CliWitness, ) { - for (contract_id, opid, anchor) in anchors { - self.contract_mut(contract_id) - .include(opid, anchor, pub_witness); - } + self.contract_mut(contract_id) + .include(opid, anchor, pub_witness) } pub fn consign( diff --git a/src/pile.rs b/src/pile.rs index de35b72c..0504ac49 100644 --- a/src/pile.rs +++ b/src/pile.rs @@ -73,7 +73,10 @@ pub trait Pile { if self.hoard_mut().has(pubid) { let mut prev_anchor = self.hoard_mut().read(pubid); if prev_anchor != anchor { - prev_anchor.merge(anchor).expect("Invalid anchor"); + prev_anchor.merge(anchor).expect( + "existing anchor is not compatible with new one; this indicates either bug in \ + RGB standard library or a compromised storage", + ); self.hoard_mut().append(pubid, &prev_anchor); } } else { diff --git a/src/popls/bp.rs b/src/popls/bp.rs index 01a5c505..dd878d6a 100644 --- a/src/popls/bp.rs +++ b/src/popls/bp.rs @@ -501,28 +501,29 @@ impl, X: Excavate> B mpc: mpc::MerkleBlock, dbc: Option, prevouts: &[Outpoint], - ) { - let iter = bundle.iter().map(|prefab| { + ) -> Result<(), IncludeError> { + for prefab in bundle { let protocol_id = ProtocolId::from(prefab.operation.contract_id.to_byte_array()); let opid = prefab.operation.opid(); + let mut map = bmap! {}; + for prevout in &prefab.closes { + let pos = prevouts + .iter() + .position(|p| p == prevout) + .ok_or(IncludeError::MissingPrevout(*prevout))?; + map.insert(pos as u32, mmb::Message::from_byte_array(opid.to_byte_array())); + } let anchor = Anchor { - mmb_proof: mmb::BundleProof { - map: SmallOrdMap::from_iter_checked(prefab.closes.iter().map(|prevout| { - let pos = prevouts - .iter() - .position(|p| p == prevout) - .expect("PSBT misses one of operation inputs"); - (pos as u32, mmb::Message::from_byte_array(opid.to_byte_array())) - })), - }, + mmb_proof: mmb::BundleProof { map: SmallOrdMap::from_checked(map) }, mpc_protocol: protocol_id, - mpc_proof: mpc.to_merkle_proof(protocol_id).expect("Invalid MPC proof"), + mpc_proof: mpc.to_merkle_proof(protocol_id)?, dbc_proof: dbc.clone(), fallback_proof: default!(), }; - (prefab.operation.contract_id, opid, anchor) - }); - self.mound.include(witness, iter); + self.mound + .include(prefab.operation.contract_id, opid, witness, anchor); + } + Ok(()) } /// Consume consignment. @@ -539,6 +540,17 @@ impl, X: Excavate> B } } +#[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)] +#[display(doc_comments)] +pub enum IncludeError { + /// prefab bundle references unknown previous output {0}. + MissingPrevout(Outpoint), + + /// multi-protocol commitment proof is invalid; {0} + #[from] + Mpc(mpc::LeafNotKnown), +} + #[cfg(feature = "fs")] pub mod file { use std::fs::File; diff --git a/src/stockpile.rs b/src/stockpile.rs index a046169a..a3f62a94 100644 --- a/src/stockpile.rs +++ b/src/stockpile.rs @@ -271,7 +271,7 @@ impl Stockpile { anchor: ::CliWitness, published: &::PubWitness, ) { - self.pile.append(opid, anchor, published); + self.pile.append(opid, anchor, published) } pub fn consign( @@ -412,7 +412,7 @@ impl ContractApi for Stockpile { } fn apply_witness(&mut self, opid: Opid, witness: SealWitness) { - self.pile.append(opid, witness.client, &witness.published); + self.pile.append(opid, witness.client, &witness.published) } } From 8d69661ef0de7cde71e343752631ec39a5a89069 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Thu, 23 Jan 2025 11:56:12 +0100 Subject: [PATCH 03/15] add operation `fulfill` --- src/popls/bp.rs | 127 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 123 insertions(+), 4 deletions(-) diff --git a/src/popls/bp.rs b/src/popls/bp.rs index dd878d6a..acd389df 100644 --- a/src/popls/bp.rs +++ b/src/popls/bp.rs @@ -26,7 +26,7 @@ //! proof of publication layer 1. use alloc::collections::{btree_set, BTreeMap, BTreeSet}; -use std::vec; +use alloc::vec; use amplify::confinement::{SmallOrdMap, SmallOrdSet, SmallVec, TinyVec}; use amplify::{confinement, ByteArray, Bytes32, Wrapper}; @@ -38,9 +38,10 @@ use commit_verify::{mpc, Digest, DigestExt, Sha256}; use hypersonic::aora::Aora; use hypersonic::{ AuthToken, CallParams, CellAddr, ContractId, CoreParams, DataCell, MethodName, NamedState, - Operation, StateAtom, StateCalc, StateName, Supply, + Operation, StateAtom, StateCalc, StateName, Supply, UncountableState, }; use invoice::bp::{Address, WitnessOut}; +use invoice::{RgbBeneficiary, RgbInvoice}; use rgb::SealAuthToken; use strict_encoding::{ReadRaw, StrictDecode, StrictDeserialize, StrictReader, StrictSerialize}; use strict_types::StrictVal; @@ -61,14 +62,32 @@ pub trait WalletProvider { fn next_address(&mut self) -> Address; } +pub trait Coinselect { + fn coinselect( + &mut self, + invoiced_state: &StrictVal, + calc: &mut (impl StateCalc + ?Sized), + owned_state: &BTreeMap>, + ) -> Option>; +} + pub const BP_BLANK_METHOD: &str = "_"; +#[derive(Clone, Eq, PartialEq, Hash, Debug, Display)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(untagged))] +pub enum WoutAmount { + #[display(inner)] + Fixed(Sats), + #[display("~")] + Change, +} + #[derive(Clone, Eq, PartialEq, Hash, Debug, Display)] #[display("{wout}/{amount}")] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))] pub struct WoutAssignment { pub wout: WitnessOut, - pub amount: Sats, + pub amount: WoutAmount, } impl Into for WoutAssignment { @@ -310,7 +329,7 @@ impl, X: Excavate> B WitnessOut::new(address.payload, nonce) } - pub fn state( + pub fn state_own( &mut self, contract_id: Option, ) -> impl Iterator)> + use<'_, W, S, P, X> { @@ -346,6 +365,79 @@ impl, X: Excavate> B noise_engine } + pub fn fulfill( + &mut self, + invoice: RgbInvoice, + mut coinselect: impl Coinselect, + // TODO: Consider adding requested amount of sats to the `RgbInvoice` + giveaway: Option, + ) -> Result, FulfillError> { + let contract_id = invoice.scope; + + // Determine method + let stockpile = self.mound.contract(contract_id); + let api = &stockpile.stock().articles().schema.default_api; + let call = invoice + .call + .or_else(|| api.default_call().cloned()) + .ok_or(FulfillError::CallStateUnknown)?; + let state_name = call.destructible.ok_or(FulfillError::StateNameUnknown)?; + let mut calc = api.calculate(state_name.clone()); + + // Do coinselection + let (_, state) = self + .state_own(Some(contract_id)) + .next() + .ok_or(FulfillError::ContractUnavailable(contract_id))?; + let state = state + .owned + .get(&state_name) + .ok_or(FulfillError::StateUnavailable)?; + let reading = coinselect + .coinselect(&invoice.data, calc.as_mut(), state) + .ok_or(FulfillError::StateInsufficient)?; + + // Add beneficiaries + let seal = match invoice.auth { + RgbBeneficiary::Token(auth) => EitherSeal::Token(auth), + RgbBeneficiary::WitnessOut(wout) => { + let wout = WoutAssignment { + wout, + amount: WoutAmount::Fixed(giveaway.ok_or(FulfillError::WoutRequiresGiveaway)?), + }; + EitherSeal::Alt(wout) + } + }; + calc.lessen(invoice.data.clone())?; + let assignment = Assignment { seal, data: invoice.data }; + let state = NamedState { name: state_name.clone(), state: assignment }; + let mut owned = vec![state]; + + // Add change + let diff = calc.diff()?; + let change_address = self.wallet.next_address(); + let wout_assign = WoutAssignment { + wout: WitnessOut::new(change_address.payload, 0), + amount: WoutAmount::Change, + }; + let seal = EitherSeal::Alt(wout_assign); + for data in diff { + let assignment = Assignment { seal: seal.clone(), data }; + let state = NamedState { name: state_name.clone(), state: assignment }; + owned.push(state); + } + + // Construct PrefabParams + Ok(PrefabParams { + contract_id, + method: call.method, + using: none!(), + global: none!(), + reading, + owned, + }) + } + /// Creates a single operation basing on the provided construction parameters. pub fn prefab(&mut self, params: PrefabParams) -> Prefab { // convert ConstructParams into CallParams @@ -540,6 +632,33 @@ impl, X: Excavate> B } } +#[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)] +#[display(doc_comments)] +pub enum FulfillError { + /// the wallet doesn't own any state for {0} to fulfill the invoice. + ContractUnavailable(ContractId), + + /// neither invoice nor contract API contains information about the transfer method. + CallStateUnknown, + + /// neither invoice nor contract API contains information about the state name. + StateNameUnknown, + + /// the wallet doesn't own any state in order to fulfill the invoice. + StateUnavailable, + + /// the state owned by the wallet is insufficient to fulfill the invoice. + StateInsufficient, + + #[from] + #[display(inner)] + StateUncountable(UncountableState), + + /// the invoice asks to create an UTXO for the receiver, but method call doesn't provide + /// information on how much sats can be put there (`giveaway` parameter). + WoutRequiresGiveaway, +} + #[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)] #[display(doc_comments)] pub enum IncludeError { From 58850c856752e7a516c79d9e7dba67a1ab230b07 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Thu, 23 Jan 2025 12:31:15 +0100 Subject: [PATCH 04/15] improve namings and wordings --- src/popls/bp.rs | 48 ++++++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/src/popls/bp.rs b/src/popls/bp.rs index acd389df..75d611fd 100644 --- a/src/popls/bp.rs +++ b/src/popls/bp.rs @@ -156,6 +156,7 @@ pub struct UsedState { pub val: StrictVal, } +/// A set of multiple operation requests (see [`OpRequests`]) under a single or multiple contracts. #[derive(Wrapper, WrapperMut, Clone, PartialEq, Eq, Debug, From)] #[wrapper(Deref)] #[wrapper_mut(DerefMut)] @@ -167,20 +168,20 @@ pub struct UsedState { bound = "T: serde::Serialize + for<'d> serde::Deserialize<'d>" ) )] -pub struct PrefabParamsSet(TinyVec>); +pub struct OpRequestSet(TinyVec>); -impl IntoIterator for PrefabParamsSet { - type Item = PrefabParams; - type IntoIter = vec::IntoIter>; +impl IntoIterator for OpRequestSet { + type Item = OpRequest; + type IntoIter = vec::IntoIter>; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } -impl> PrefabParamsSet { +impl> OpRequestSet { pub fn resolve_seals( self, resolver: impl Fn(&ScriptPubkey) -> Option, - ) -> Result, UnresolvedSeal> { + ) -> Result, UnresolvedSeal> { let mut items = Vec::with_capacity(self.0.len()); for params in self.0 { let mut owned = Vec::with_capacity(params.owned.len()); @@ -198,7 +199,7 @@ impl> PrefabParamsSet { state: Assignment { seal, data: assignment.state.data }, }); } - items.push(PrefabParams:: { + items.push(OpRequest:: { contract_id: params.contract_id, method: params.method, reading: params.reading, @@ -207,7 +208,7 @@ impl> PrefabParamsSet { owned, }); } - Ok(PrefabParamsSet(TinyVec::from_iter_checked(items))) + Ok(OpRequestSet(TinyVec::from_iter_checked(items))) } } @@ -217,7 +218,10 @@ pub struct UnresolvedSeal(ScriptPubkey); /// Parameters used by BP-based wallet for constructing operations. /// -/// Differs from [`CallParams`] in the fact that it uses [`BuilderSeal`]s instead of +/// NB: [`OpRequest`] must contain pre-computed information about the change; otherwise the +/// excessive state will be lost. +/// +/// Differs from [`CallParams`] in the fact that it uses [`EitherSeal`]s instead of /// [`hypersonic::AuthTokens`] for output definitions. #[derive(Clone, PartialEq, Eq, Debug)] #[cfg_attr( @@ -228,7 +232,7 @@ pub struct UnresolvedSeal(ScriptPubkey); bound = "T: serde::Serialize + for<'d> serde::Deserialize<'d>" ) )] -pub struct PrefabParams { +pub struct OpRequest { pub contract_id: ContractId, pub method: MethodName, pub reading: Vec, @@ -371,7 +375,7 @@ impl, X: Excavate> B mut coinselect: impl Coinselect, // TODO: Consider adding requested amount of sats to the `RgbInvoice` giveaway: Option, - ) -> Result, FulfillError> { + ) -> Result, FulfillError> { let contract_id = invoice.scope; // Determine method @@ -428,7 +432,7 @@ impl, X: Excavate> B } // Construct PrefabParams - Ok(PrefabParams { + Ok(OpRequest { contract_id, method: call.method, using: none!(), @@ -439,7 +443,7 @@ impl, X: Excavate> B } /// Creates a single operation basing on the provided construction parameters. - pub fn prefab(&mut self, params: PrefabParams) -> Prefab { + pub fn prefab(&mut self, params: OpRequest) -> Prefab { // convert ConstructParams into CallParams let (closes, using) = params .using @@ -487,19 +491,23 @@ impl, X: Excavate> B Prefab { closes, defines, operation } } - /// Complete creation of a prefabricated operation pack, adding blank operations if necessary. + /// Complete creation of a prefabricated operation bundle from operation requests, adding blank + /// operations if necessary. Operation requests can be multiple. + /// + /// A set of operations is either a collection of them, or a [`OpRequestSet`] - any structure + /// which implements `IntoIterator` trait. /// /// # Arguments /// - /// - `items`: a set of instructions to create non-blank operations (potentially under multiple - /// contracts); + /// - `requests`: a set of instructions to create non-blank operations (potentially under + /// multiple contracts); /// - `seal`: a single-use seal definition where all blank outputs will be assigned to. pub fn bundle( &mut self, - items: impl IntoIterator>, + requests: impl IntoIterator>, change: Vout, ) -> PrefabBundle { - let ops = items.into_iter().map(|params| self.prefab(params)); + let ops = requests.into_iter().map(|params| self.prefab(params)); let mut outpoints = BTreeSet::::new(); let mut contracts = BTreeSet::new(); @@ -569,7 +577,7 @@ impl, X: Excavate> B } } - let params = PrefabParams { + let params = OpRequest { contract_id, method: MethodName::from(BP_BLANK_METHOD), global: none!(), @@ -585,7 +593,7 @@ impl, X: Excavate> B PrefabBundle(SmallOrdSet::try_from(prefabs).expect("too many operations")) } - /// Include prefab bundle into the mound, creating necessary anchors. + /// Include prefab bundle into the mound, creating necessary anchors on the fly. pub fn include( &mut self, bundle: &PrefabBundle, From fd3cbcd4438934a4a64021fdac614edb9173d1d5 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Fri, 24 Jan 2025 15:40:54 +0100 Subject: [PATCH 05/15] bp: refactor how change seals are handled --- src/popls/bp.rs | 42 ++++++++++++++++++------------------------ 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/src/popls/bp.rs b/src/popls/bp.rs index 75d611fd..101b9c06 100644 --- a/src/popls/bp.rs +++ b/src/popls/bp.rs @@ -73,21 +73,12 @@ pub trait Coinselect { pub const BP_BLANK_METHOD: &str = "_"; -#[derive(Clone, Eq, PartialEq, Hash, Debug, Display)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(untagged))] -pub enum WoutAmount { - #[display(inner)] - Fixed(Sats), - #[display("~")] - Change, -} - #[derive(Clone, Eq, PartialEq, Hash, Debug, Display)] #[display("{wout}/{amount}")] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))] pub struct WoutAssignment { pub wout: WitnessOut, - pub amount: WoutAmount, + pub amount: Sats, } impl Into for WoutAssignment { @@ -177,21 +168,23 @@ impl IntoIterator for OpRequestSet { fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } -impl> OpRequestSet { +impl> OpRequestSet> { pub fn resolve_seals( self, resolver: impl Fn(&ScriptPubkey) -> Option, + change: Option, ) -> Result, UnresolvedSeal> { let mut items = Vec::with_capacity(self.0.len()); for params in self.0 { let mut owned = Vec::with_capacity(params.owned.len()); for assignment in params.owned { let seal = match assignment.state.seal { - EitherSeal::Alt(seal) => { + EitherSeal::Alt(Some(seal)) => { let spk = seal.into(); - let vout = resolver(&spk).ok_or(UnresolvedSeal(spk))?; + let vout = resolver(&spk).ok_or(UnresolvedSeal::Spk(spk))?; EitherSeal::Alt(vout) } + EitherSeal::Alt(None) => EitherSeal::Alt(change.ok_or(UnresolvedSeal::Change)?), EitherSeal::Token(auth) => EitherSeal::Token(auth), }; owned.push(NamedState { @@ -213,8 +206,14 @@ impl> OpRequestSet { } #[derive(Clone, PartialEq, Eq, Debug, Display, Error)] -#[display("unable to resolve seal witness output seal definition for script pubkey {0:x}")] -pub struct UnresolvedSeal(ScriptPubkey); +#[display(doc_comments)] +pub enum UnresolvedSeal { + /// unable to resolve seal witness output seal definition for script pubkey {0:x}. + Spk(ScriptPubkey), + + /// seal requires assignment to a change output, but the transaction lacks change. + Change, +} /// Parameters used by BP-based wallet for constructing operations. /// @@ -375,7 +374,7 @@ impl, X: Excavate> B mut coinselect: impl Coinselect, // TODO: Consider adding requested amount of sats to the `RgbInvoice` giveaway: Option, - ) -> Result, FulfillError> { + ) -> Result>, FulfillError> { let contract_id = invoice.scope; // Determine method @@ -407,9 +406,9 @@ impl, X: Excavate> B RgbBeneficiary::WitnessOut(wout) => { let wout = WoutAssignment { wout, - amount: WoutAmount::Fixed(giveaway.ok_or(FulfillError::WoutRequiresGiveaway)?), + amount: giveaway.ok_or(FulfillError::WoutRequiresGiveaway)?, }; - EitherSeal::Alt(wout) + EitherSeal::Alt(Some(wout)) } }; calc.lessen(invoice.data.clone())?; @@ -419,12 +418,7 @@ impl, X: Excavate> B // Add change let diff = calc.diff()?; - let change_address = self.wallet.next_address(); - let wout_assign = WoutAssignment { - wout: WitnessOut::new(change_address.payload, 0), - amount: WoutAmount::Change, - }; - let seal = EitherSeal::Alt(wout_assign); + let seal = EitherSeal::Alt(None); for data in diff { let assignment = Assignment { seal: seal.clone(), data }; let state = NamedState { name: state_name.clone(), state: assignment }; From 688aa844b36aeb8fde9c18c9aec27d318f3941f2 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Fri, 24 Jan 2025 16:00:50 +0100 Subject: [PATCH 06/15] bp: improve bundling with proper error messages --- src/popls/bp.rs | 101 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 69 insertions(+), 32 deletions(-) diff --git a/src/popls/bp.rs b/src/popls/bp.rs index 101b9c06..4a2888f6 100644 --- a/src/popls/bp.rs +++ b/src/popls/bp.rs @@ -28,7 +28,7 @@ use alloc::collections::{btree_set, BTreeMap, BTreeSet}; use alloc::vec; -use amplify::confinement::{SmallOrdMap, SmallOrdSet, SmallVec, TinyVec}; +use amplify::confinement::{Collection, SmallOrdMap, SmallOrdSet, SmallVec, TinyVec}; use amplify::{confinement, ByteArray, Bytes32, Wrapper}; use bp::dbc::tapret::TapretProof; use bp::seals::{mmb, Anchor, TxoSeal}; @@ -218,7 +218,8 @@ pub enum UnresolvedSeal { /// Parameters used by BP-based wallet for constructing operations. /// /// NB: [`OpRequest`] must contain pre-computed information about the change; otherwise the -/// excessive state will be lost. +/// excessive state will be lost. Change information allows wallet to construct complex transactions +/// with multiple changes etc. /// /// Differs from [`CallParams`] in the fact that it uses [`EitherSeal`]s instead of /// [`hypersonic::AuthTokens`] for output definitions. @@ -437,38 +438,37 @@ impl, X: Excavate> B } /// Creates a single operation basing on the provided construction parameters. - pub fn prefab(&mut self, params: OpRequest) -> Prefab { + pub fn prefab(&mut self, params: OpRequest) -> Result { // convert ConstructParams into CallParams let (closes, using) = params .using .into_iter() .map(|used| (used.outpoint, (used.addr, used.val))) .unzip(); - let closes = SmallOrdSet::try_from(closes).expect("too many inputs"); + let closes = SmallOrdSet::try_from(closes).map_err(|_| PrefabError::TooManyInputs)?; let mut defines = SmallOrdSet::new(); let mut seals = SmallVec::new(); let mut noise_engine = self.noise_engine(); noise_engine.input_raw(params.contract_id.as_slice()); - let owned = params - .owned - .into_iter() - .enumerate() - .map(|(nonce, assignment)| { - let auth = match assignment.state.seal { - EitherSeal::Alt(vout) => { - defines.push(vout).expect("too many seals"); - let seal = - TxoSeal::vout_no_fallback(vout, noise_engine.clone(), nonce as u64); - seals.push(seal).expect("too many seals"); - seal.auth_token() - } - EitherSeal::Token(auth) => auth, - }; - let state = DataCell { data: assignment.state.data, auth, lock: None }; - NamedState { name: assignment.name, state } - }) - .collect(); + + let mut owned = Vec::with_capacity(params.owned.len()); + for (nonce, assignment) in params.owned.into_iter().enumerate() { + let auth = match assignment.state.seal { + EitherSeal::Alt(vout) => { + defines + .push(vout) + .map_err(|_| PrefabError::TooManyOutputs)?; + let seal = TxoSeal::vout_no_fallback(vout, noise_engine.clone(), nonce as u64); + seals.push(seal).expect("checked above"); + seal.auth_token() + } + EitherSeal::Token(auth) => auth, + }; + let state = DataCell { data: assignment.state.data, auth, lock: None }; + let named_state = NamedState { name: assignment.name, state }; + owned.push(named_state); + } let call = CallParams { core: CoreParams { method: params.method, global: params.global, owned }, @@ -482,7 +482,7 @@ impl, X: Excavate> B stockpile.pile_mut().keep_mut().append(opid, &seals); debug_assert_eq!(operation.contract_id, params.contract_id); - Prefab { closes, defines, operation } + Ok(Prefab { closes, defines, operation }) } /// Complete creation of a prefabricated operation bundle from operation requests, adding blank @@ -499,20 +499,22 @@ impl, X: Excavate> B pub fn bundle( &mut self, requests: impl IntoIterator>, - change: Vout, - ) -> PrefabBundle { + change: Option, + ) -> Result { let ops = requests.into_iter().map(|params| self.prefab(params)); let mut outpoints = BTreeSet::::new(); let mut contracts = BTreeSet::new(); let mut prefabs = BTreeSet::new(); for prefab in ops { + let prefab = prefab?; contracts.insert(prefab.operation.contract_id); outpoints.extend(&prefab.closes); prefabs.insert(prefab); } - let mut prefab_params = Vec::new(); + // Constructing blank operation requests + let mut blank_requests = Vec::new(); let root_noise_engine = self.noise_engine(); for (contract_id, stockpile) in self .mound @@ -557,12 +559,13 @@ impl, X: Excavate> B let calc = calcs .entry(name.clone()) .or_insert_with(|| api.calculate(name)); - calc.accumulate(val.clone()).expect("non-computable state"); + calc.accumulate(val.clone())?; } let mut owned = Vec::new(); for (name, calc) in calcs { - for data in calc.diff().expect("non-computable state") { + for data in calc.diff()? { + let change = change.ok_or(BundleError::ChangeRequired)?; let state = NamedState { name: name.clone(), state: Assignment { seal: EitherSeal::Alt(change), data }, @@ -579,12 +582,14 @@ impl, X: Excavate> B using, owned, }; - prefab_params.push(params); + blank_requests.push(params); } - prefabs.extend(prefab_params.into_iter().map(|params| self.prefab(params))); + for request in blank_requests { + prefabs.push(self.prefab(request).map_err(BundleError::Blank)?); + } - PrefabBundle(SmallOrdSet::try_from(prefabs).expect("too many operations")) + Ok(PrefabBundle(SmallOrdSet::try_from(prefabs).map_err(|_| BundleError::TooManyBlanks)?)) } /// Include prefab bundle into the mound, creating necessary anchors on the fly. @@ -634,6 +639,38 @@ impl, X: Excavate> B } } +#[derive(Copy, Clone, Eq, PartialEq, Debug, Display, Error)] +#[display(doc_comments)] +pub enum PrefabError { + /// operation request contains too many inputs (maximum number of inputs is 64k). + TooManyInputs, + /// operation request contains too many outputs (maximum number of outputs is 64k). + TooManyOutputs, +} + +#[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)] +#[display(doc_comments)] +pub enum BundleError { + #[from] + #[display(inner)] + Prefab(PrefabError), + + /// blank {0} + Blank(PrefabError), + + /// the requested set of operations requires creation of blank operations for other contracts, + /// which in turn require transaction to contain a change output. + ChangeRequired, + + #[from] + #[display(inner)] + UncountableState(UncountableState), + + /// one or multiple outputs used in operation requests contain too many contracts; it is + /// impossible to create a bundle with more than 64k of operations. + TooManyBlanks, +} + #[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)] #[display(doc_comments)] pub enum FulfillError { From 5bee79ff83d7e05445571803f9cd9ca29a68a741 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Fri, 24 Jan 2025 16:41:26 +0100 Subject: [PATCH 07/15] bp: use non-empty vec for request set --- src/popls/bp.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/popls/bp.rs b/src/popls/bp.rs index 4a2888f6..01618044 100644 --- a/src/popls/bp.rs +++ b/src/popls/bp.rs @@ -28,7 +28,9 @@ use alloc::collections::{btree_set, BTreeMap, BTreeSet}; use alloc::vec; -use amplify::confinement::{Collection, SmallOrdMap, SmallOrdSet, SmallVec, TinyVec}; +use amplify::confinement::{ + Collection, NonEmptyVec, SmallOrdMap, SmallOrdSet, SmallVec, U8 as U8MAX, +}; use amplify::{confinement, ByteArray, Bytes32, Wrapper}; use bp::dbc::tapret::TapretProof; use bp::seals::{mmb, Anchor, TxoSeal}; @@ -159,7 +161,7 @@ pub struct UsedState { bound = "T: serde::Serialize + for<'d> serde::Deserialize<'d>" ) )] -pub struct OpRequestSet(TinyVec>); +pub struct OpRequestSet(NonEmptyVec, U8MAX>); impl IntoIterator for OpRequestSet { type Item = OpRequest; @@ -168,6 +170,10 @@ impl IntoIterator for OpRequestSet { fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } +impl OpRequestSet { + pub fn with(request: OpRequest) -> Self { Self(NonEmptyVec::with(request)) } +} + impl> OpRequestSet> { pub fn resolve_seals( self, @@ -201,7 +207,7 @@ impl> OpRequestSet> { owned, }); } - Ok(OpRequestSet(TinyVec::from_iter_checked(items))) + Ok(OpRequestSet(NonEmptyVec::from_iter_checked(items))) } } From bef775e70493502c2494602f70c053a2049b99c3 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Fri, 24 Jan 2025 17:51:02 +0100 Subject: [PATCH 08/15] bp: make Barrow::fulfill to take invoice by ref --- src/popls/bp.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/popls/bp.rs b/src/popls/bp.rs index 01618044..c44364d6 100644 --- a/src/popls/bp.rs +++ b/src/popls/bp.rs @@ -377,7 +377,7 @@ impl, X: Excavate> B pub fn fulfill( &mut self, - invoice: RgbInvoice, + invoice: &RgbInvoice, mut coinselect: impl Coinselect, // TODO: Consider adding requested amount of sats to the `RgbInvoice` giveaway: Option, @@ -389,9 +389,14 @@ impl, X: Excavate> B let api = &stockpile.stock().articles().schema.default_api; let call = invoice .call - .or_else(|| api.default_call().cloned()) + .as_ref() + .or_else(|| api.default_call()) .ok_or(FulfillError::CallStateUnknown)?; - let state_name = call.destructible.ok_or(FulfillError::StateNameUnknown)?; + let method = call.method.clone(); + let state_name = call + .destructible + .clone() + .ok_or(FulfillError::StateNameUnknown)?; let mut calc = api.calculate(state_name.clone()); // Do coinselection @@ -419,7 +424,7 @@ impl, X: Excavate> B } }; calc.lessen(invoice.data.clone())?; - let assignment = Assignment { seal, data: invoice.data }; + let assignment = Assignment { seal, data: invoice.data.clone() }; let state = NamedState { name: state_name.clone(), state: assignment }; let mut owned = vec![state]; @@ -435,7 +440,7 @@ impl, X: Excavate> B // Construct PrefabParams Ok(OpRequest { contract_id, - method: call.method, + method, using: none!(), global: none!(), reading, From df0e6fde3b74373a1657b2ca6f6f9cf9b037df9a Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Fri, 24 Jan 2025 18:23:43 +0100 Subject: [PATCH 09/15] bp: improve coinselect API --- Cargo.lock | 26 +++++++++++++------------- src/popls/bp.rs | 10 ++++++++-- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e2be15af..70a1d38a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -354,9 +354,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.26" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783" +checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" dependencies = [ "clap_builder", "clap_derive", @@ -364,9 +364,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.26" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" dependencies = [ "anstream", "anstyle", @@ -526,7 +526,7 @@ dependencies = [ [[package]] name = "hypersonic" version = "0.12.0-beta.4" -source = "git+https://github.com/AluVM/sonic?branch=master#02fea6d4472bfada05555f2a01348787498add23" +source = "git+https://github.com/AluVM/sonic?branch=master#e9e315120f301f77a646d4ec02b5acc44bb862d5" dependencies = [ "aluvm", "amplify", @@ -568,9 +568,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", "hashbrown", @@ -881,9 +881,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.135" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" +checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" dependencies = [ "itoa", "memchr", @@ -953,7 +953,7 @@ dependencies = [ [[package]] name = "sonic-api" version = "0.12.0-beta.4" -source = "git+https://github.com/AluVM/sonic?branch=master#02fea6d4472bfada05555f2a01348787498add23" +source = "git+https://github.com/AluVM/sonic?branch=master#e9e315120f301f77a646d4ec02b5acc44bb862d5" dependencies = [ "aluvm", "amplify", @@ -972,7 +972,7 @@ dependencies = [ [[package]] name = "sonic-callreq" version = "0.12.0-beta.4" -source = "git+https://github.com/AluVM/sonic?branch=master#02fea6d4472bfada05555f2a01348787498add23" +source = "git+https://github.com/AluVM/sonic?branch=master#e9e315120f301f77a646d4ec02b5acc44bb862d5" dependencies = [ "amplify", "baid64", @@ -1146,9 +1146,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "11cd88e12b17c6494200a9c1b683a04fcac9573ed74cd1b62aeb2727c5592243" [[package]] name = "unsafe-libyaml" diff --git a/src/popls/bp.rs b/src/popls/bp.rs index c44364d6..baa7a77a 100644 --- a/src/popls/bp.rs +++ b/src/popls/bp.rs @@ -69,7 +69,7 @@ pub trait Coinselect { &mut self, invoiced_state: &StrictVal, calc: &mut (impl StateCalc + ?Sized), - owned_state: &BTreeMap>, + owned_state: BTreeMap, ) -> Option>; } @@ -407,7 +407,13 @@ impl, X: Excavate> B let state = state .owned .get(&state_name) - .ok_or(FulfillError::StateUnavailable)?; + .ok_or(FulfillError::StateUnavailable)? + .iter() + .filter_map(|(addr, assignment)| { + calc.measure(&assignment.data, &invoice.data) + .map(|coef| (coef, (*addr, &assignment.data))) + }) + .collect::>(); let reading = coinselect .coinselect(&invoice.data, calc.as_mut(), state) .ok_or(FulfillError::StateInsufficient)?; From d19bfefb813240e2e6edbcb99dfb1abd0358166c Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Fri, 24 Jan 2025 22:53:17 +0100 Subject: [PATCH 10/15] bp: use new StateCalc api --- Cargo.lock | 6 +++--- src/popls/bp.rs | 14 ++++++-------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 70a1d38a..885b7cfc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -526,7 +526,7 @@ dependencies = [ [[package]] name = "hypersonic" version = "0.12.0-beta.4" -source = "git+https://github.com/AluVM/sonic?branch=master#e9e315120f301f77a646d4ec02b5acc44bb862d5" +source = "git+https://github.com/AluVM/sonic?branch=master#9c26410b570b9d51f54bb92f2909333631b16a01" dependencies = [ "aluvm", "amplify", @@ -953,7 +953,7 @@ dependencies = [ [[package]] name = "sonic-api" version = "0.12.0-beta.4" -source = "git+https://github.com/AluVM/sonic?branch=master#e9e315120f301f77a646d4ec02b5acc44bb862d5" +source = "git+https://github.com/AluVM/sonic?branch=master#9c26410b570b9d51f54bb92f2909333631b16a01" dependencies = [ "aluvm", "amplify", @@ -972,7 +972,7 @@ dependencies = [ [[package]] name = "sonic-callreq" version = "0.12.0-beta.4" -source = "git+https://github.com/AluVM/sonic?branch=master#e9e315120f301f77a646d4ec02b5acc44bb862d5" +source = "git+https://github.com/AluVM/sonic?branch=master#9c26410b570b9d51f54bb92f2909333631b16a01" dependencies = [ "amplify", "baid64", diff --git a/src/popls/bp.rs b/src/popls/bp.rs index baa7a77a..ee34e1e7 100644 --- a/src/popls/bp.rs +++ b/src/popls/bp.rs @@ -69,7 +69,8 @@ pub trait Coinselect { &mut self, invoiced_state: &StrictVal, calc: &mut (impl StateCalc + ?Sized), - owned_state: BTreeMap, + // Sorted vector by values + owned_state: Vec<(CellAddr, &StrictVal)>, ) -> Option>; } @@ -409,11 +410,8 @@ impl, X: Excavate> B .get(&state_name) .ok_or(FulfillError::StateUnavailable)? .iter() - .filter_map(|(addr, assignment)| { - calc.measure(&assignment.data, &invoice.data) - .map(|coef| (coef, (*addr, &assignment.data))) - }) - .collect::>(); + .map(|(addr, assignment)| (*addr, &assignment.data)) + .collect::>(); let reading = coinselect .coinselect(&invoice.data, calc.as_mut(), state) .ok_or(FulfillError::StateInsufficient)?; @@ -429,7 +427,7 @@ impl, X: Excavate> B EitherSeal::Alt(Some(wout)) } }; - calc.lessen(invoice.data.clone())?; + calc.lessen(&invoice.data)?; let assignment = Assignment { seal, data: invoice.data.clone() }; let state = NamedState { name: state_name.clone(), state: assignment }; let mut owned = vec![state]; @@ -576,7 +574,7 @@ impl, X: Excavate> B let calc = calcs .entry(name.clone()) .or_insert_with(|| api.calculate(name)); - calc.accumulate(val.clone())?; + calc.accumulate(val)?; } let mut owned = Vec::new(); From cf9003b90304f2d648055858b2de113e8929a53b Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Sat, 25 Jan 2025 12:05:19 +0100 Subject: [PATCH 11/15] chore: fix clippy lints --- invoice/src/bp.rs | 6 +++--- src/popls/bp.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/invoice/src/bp.rs b/invoice/src/bp.rs index daf6f873..680bde79 100644 --- a/invoice/src/bp.rs +++ b/invoice/src/bp.rs @@ -51,8 +51,8 @@ pub struct WitnessOut { impl StrictSerialize for WitnessOut {} impl StrictDeserialize for WitnessOut {} -impl Into for WitnessOut { - fn into(self) -> ScriptPubkey { self.address.script_pubkey() } +impl From for ScriptPubkey { + fn from(val: WitnessOut) -> Self { val.address.script_pubkey() } } impl WitnessOut { @@ -74,7 +74,7 @@ impl WitnessOut { pub fn checksum(&self) -> [u8; 4] { let key = Sha256::digest(WITNESS_OUT_HRI.as_bytes()); let mut sha = Sha256::new_with_prefix(key); - sha.update(&[0]); + sha.update([0]); sha.update(self.salt.to_le_bytes()); sha.update(self.script_pubkey().as_slice()); let sha = sha.finalize(); diff --git a/src/popls/bp.rs b/src/popls/bp.rs index ee34e1e7..0149ce20 100644 --- a/src/popls/bp.rs +++ b/src/popls/bp.rs @@ -84,8 +84,8 @@ pub struct WoutAssignment { pub amount: Sats, } -impl Into for WoutAssignment { - fn into(self) -> ScriptPubkey { self.wout.into() } +impl From for ScriptPubkey { + fn from(val: WoutAssignment) -> Self { val.wout.into() } } impl EitherSeal { From cd5eaf2831fa77b94c2caf78ce660779601a47f2 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Sat, 25 Jan 2025 12:07:01 +0100 Subject: [PATCH 12/15] ci: update list of tested features --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 083105ff..1b4980f1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,7 +33,7 @@ jobs: strategy: fail-fast: false matrix: - feature: [ bp, fs, serde ] + feature: [ bitcoin, liquid, prime, uri, fs, serde, stl ] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable From 46ea8516fcd3781f27015aa0f1cc269c005617a4 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Sat, 25 Jan 2025 12:27:46 +0100 Subject: [PATCH 13/15] bp: add PaymentScript type alias --- src/popls/bp.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/popls/bp.rs b/src/popls/bp.rs index 0149ce20..70e44469 100644 --- a/src/popls/bp.rs +++ b/src/popls/bp.rs @@ -150,6 +150,8 @@ pub struct UsedState { pub val: StrictVal, } +pub type PaymentScript = OpRequestSet>; + /// A set of multiple operation requests (see [`OpRequests`]) under a single or multiple contracts. #[derive(Wrapper, WrapperMut, Clone, PartialEq, Eq, Debug, From)] #[wrapper(Deref)] @@ -441,7 +443,7 @@ impl, X: Excavate> B owned.push(state); } - // Construct PrefabParams + // Construct operation request Ok(OpRequest { contract_id, method, From 468c7d677ea5eaf7031104ade870ca8a1913c715 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Sat, 25 Jan 2025 12:27:56 +0100 Subject: [PATCH 14/15] invoice: test and fix WitnessOut display/from_str --- invoice/src/bp.rs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/invoice/src/bp.rs b/invoice/src/bp.rs index 680bde79..30c6f9cc 100644 --- a/invoice/src/bp.rs +++ b/invoice/src/bp.rs @@ -115,7 +115,8 @@ impl FromStr for WitnessOut { fn from_str(s: &str) -> Result { let s = s .strip_prefix(WITNESS_OUT_HRI) - .ok_or(ParseWitnessOutError::NoPrefix)?; + .ok_or(ParseWitnessOutError::NoPrefix)? + .replace('-', ""); let alphabet = Alphabet::new(BAID64_ALPHABET).expect("invalid Baid64 alphabet"); let engine = GeneralPurpose::new( @@ -151,7 +152,7 @@ pub enum ParseWitnessOutError { /// checksum of the provided witness output seal definition is invalid. InvalidChecksum, - /// invalid Base64 encoding in itness output seal definition - {0}. + /// invalid Base64 encoding in witness output seal definition - {0}. #[from] Base64(DecodeError), @@ -163,3 +164,20 @@ pub enum ParseWitnessOutError { #[from] Encoding(DeserializeError), } + +#[cfg(test)] +mod tests { + use bp::OutputPk; + use strict_encoding::StrictDumb; + + use super::*; + + #[test] + fn display_from_str() { + let wout = WitnessOut::new(AddressPayload::Tr(OutputPk::strict_dumb()), 0xdeadbeaf1badcafe); + let s = wout.to_string(); + assert_eq!(s, "wout:AP7KrRuv-vq3eIAEB-AQEBAQEB-AQEBAQEB-AQEBAQEB-AQEBAQEB-AQEBAQEB-zbu7~w"); + let wout2 = WitnessOut::from_str(&s).unwrap(); + assert_eq!(wout, wout2); + } +} From 79e68c101d8e297a9e3a99100f1da727deda00e5 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Sat, 25 Jan 2025 12:28:22 +0100 Subject: [PATCH 15/15] ci: temporarily disable codecov targets --- codecov.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codecov.yml b/codecov.yml index 240d7310..4a291d86 100644 --- a/codecov.yml +++ b/codecov.yml @@ -8,12 +8,12 @@ coverage: status: project: default: - target: 75% + # target: 75% threshold: 1% branches: - master patch: default: - target: 60% + # target: 60% threshold: 1% only_pulls: true