Skip to content

Commit 50725fa

Browse files
committed
feat: dedicated collateral coin utility
feat: dedicated dig coin utility feat: error handling improvements
1 parent 6a2d7a2 commit 50725fa

File tree

5 files changed

+386
-272
lines changed

5 files changed

+386
-272
lines changed

src/dig_coin.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
use crate::error::WalletError;
2+
use crate::wallet::DIG_ASSET_ID;
3+
use crate::{Bytes32, Coin, Peer};
4+
use chia::protocol::CoinState;
5+
use chia::puzzles::cat::CatArgs;
6+
use chia_wallet_sdk::driver::{Asset, Cat, Puzzle, SpendContext};
7+
use chia_wallet_sdk::prelude::{TreeHash, MAINNET_CONSTANTS};
8+
9+
pub struct DigCoin {
10+
cat: Cat,
11+
}
12+
13+
impl DigCoin {
14+
#[inline]
15+
pub fn cat(&self) -> Cat {
16+
self.cat
17+
}
18+
19+
pub fn puzzle_hash(wallet_puzzle_hash: Bytes32) -> Bytes32 {
20+
let ph_bytes =
21+
CatArgs::curry_tree_hash(DIG_ASSET_ID, TreeHash::from(wallet_puzzle_hash)).to_bytes();
22+
Bytes32::from(ph_bytes)
23+
}
24+
25+
pub async fn from_coin_state(peer: &Peer, coin_state: &CoinState) -> Result<Self, WalletError> {
26+
let coin_created_height = coin_state.created_height.ok_or(WalletError::Parse(
27+
"Cannot determine coin creation height".to_string(),
28+
))?;
29+
Self::from_coin(peer, &coin_state.coin, coin_created_height).await
30+
}
31+
32+
/// Function to validate that a coin is a $DIG CAT coin. Returns an instantiated DIG token
33+
/// CAT for the coin if it's a valid $DIG CAT
34+
pub async fn from_coin(
35+
peer: &Peer,
36+
coin: &Coin,
37+
coin_created_height: u32,
38+
) -> Result<Self, WalletError> {
39+
let mut ctx = SpendContext::new();
40+
41+
// 1) Request parent coin state
42+
let parent_state_response = peer
43+
.request_coin_state(
44+
vec![coin.parent_coin_info],
45+
None,
46+
MAINNET_CONSTANTS.genesis_challenge,
47+
false,
48+
)
49+
.await?;
50+
51+
let parent_state = parent_state_response.map_err(|_| WalletError::RejectCoinState)?;
52+
53+
// 2) Request parent puzzle and solution
54+
let parent_puzzle_and_solution_response = peer
55+
.request_puzzle_and_solution(parent_state.coin_ids[0], coin_created_height)
56+
.await?;
57+
58+
let parent_puzzle_and_solution =
59+
parent_puzzle_and_solution_response.map_err(|_| WalletError::RejectPuzzleSolution)?;
60+
61+
// 3) Convert puzzle to CLVM
62+
let parent_puzzle_ptr = ctx.alloc(&parent_puzzle_and_solution.puzzle)?;
63+
let parent_puzzle = Puzzle::parse(&ctx, parent_puzzle_ptr);
64+
65+
// 4) Convert solution to CLVM
66+
let parent_solution = ctx.alloc(&parent_puzzle_and_solution.solution)?;
67+
68+
// 5) Parse CAT
69+
let parsed_children = Cat::parse_children(
70+
&mut ctx,
71+
parent_state.coin_states[0].coin,
72+
parent_puzzle,
73+
parent_solution,
74+
)?
75+
.ok_or(WalletError::UnknownCoin)?;
76+
77+
let proved_cat = parsed_children
78+
.into_iter()
79+
.find(|parsed_child| {
80+
parsed_child.coin_id() == coin.coin_id()
81+
&& parsed_child.lineage_proof.is_some()
82+
&& parsed_child.info.asset_id == DIG_ASSET_ID
83+
})
84+
.ok_or_else(|| WalletError::UnknownCoin)?;
85+
86+
Ok(Self { cat: proved_cat })
87+
}
88+
}

src/dig_collateral_coin.rs

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
use crate::dig_coin::DigCoin;
2+
use crate::error::WalletError;
3+
use crate::wallet::DIG_ASSET_ID;
4+
use crate::{Bytes, Bytes32, Coin, CoinSpend, CoinState, P2ParentCoin, Peer, PublicKey};
5+
use chia::puzzles::Memos;
6+
use chia::traits::Streamable;
7+
use chia_wallet_sdk::driver::{
8+
Action, Id, Puzzle, Relation, SpendContext, SpendWithConditions, Spends, StandardLayer,
9+
};
10+
use chia_wallet_sdk::prelude::{AssertConcurrentSpend, Conditions, ToTreeHash, MAINNET_CONSTANTS};
11+
use clvm_traits::{FromClvm, ToClvm};
12+
use clvmr::Allocator;
13+
use indexmap::indexmap;
14+
use num_bigint::BigInt;
15+
16+
pub struct DigCollateralCoin {
17+
inner: P2ParentCoin,
18+
morphed_store_id: Option<Bytes32>,
19+
mirror_urls: Option<Vec<String>>,
20+
}
21+
22+
impl DigCollateralCoin {
23+
/// Morphs a DIG store launcher ID into the DIG store collateral coin namespace.
24+
pub fn morph_store_launcher_if_for_collateral(store_launcher_id: Bytes32) -> Bytes32 {
25+
(store_launcher_id, "DIG_STORE_COLLATERAL")
26+
.tree_hash()
27+
.into()
28+
}
29+
30+
/// Morphs a DIG store launcher ID into the DIG mirror collateral coin namespace.
31+
pub fn morph_store_launcher_id_for_mirror(
32+
store_launcher_id: Bytes32,
33+
offset: &BigInt,
34+
) -> Bytes32 {
35+
let launcher_id_int = BigInt::from_signed_bytes_be(&store_launcher_id);
36+
let offset_launcher_id = launcher_id_int + offset;
37+
38+
(offset_launcher_id, "DIG_STORE_MIRROR_COLLATERAL")
39+
.tree_hash()
40+
.into()
41+
}
42+
43+
/// Instantiates a $DIG collateral coin
44+
/// Verifies that coin is unspent and locked by the $DIG P2Parent puzzle
45+
pub async fn from_coin_state(peer: &Peer, coin_state: CoinState) -> Result<Self, WalletError> {
46+
let coin = coin_state.coin;
47+
48+
// verify coin is unspent
49+
if matches!(coin_state.spent_height, Some(x) if x != 0) {
50+
return Err(WalletError::CoinIsAlreadySpent);
51+
}
52+
53+
// verify that the coin is $DIG p2 parent
54+
let p2_parent_hash = P2ParentCoin::puzzle_hash(Some(DIG_ASSET_ID));
55+
if coin.puzzle_hash != p2_parent_hash.into() {
56+
return Err(WalletError::PuzzleHashMismatch(format!(
57+
"Coin {} is not locked by the $DIG collateral puzzle",
58+
coin.coin_id()
59+
)));
60+
}
61+
62+
let Some(created_height) = coin_state.created_height else {
63+
return Err(WalletError::UnknownCoin);
64+
};
65+
66+
let parent_state = peer
67+
.request_coin_state(
68+
vec![coin.parent_coin_info],
69+
None,
70+
MAINNET_CONSTANTS.genesis_challenge,
71+
false,
72+
)
73+
.await?
74+
.map_err(|_| WalletError::RejectCoinState)?
75+
.coin_states
76+
.first()
77+
.copied()
78+
.ok_or(WalletError::UnknownCoin)?;
79+
80+
let parent_puzzle_and_solution_response = peer
81+
.request_puzzle_and_solution(coin.parent_coin_info, created_height)
82+
.await?
83+
.map_err(|_| WalletError::RejectPuzzleSolution)?;
84+
85+
let mut allocator = Allocator::new();
86+
let parent_puzzle_ptr = parent_puzzle_and_solution_response
87+
.puzzle
88+
.to_clvm(&mut allocator)?;
89+
let parent_solution_ptr = parent_puzzle_and_solution_response
90+
.solution
91+
.to_clvm(&mut allocator)?;
92+
93+
let parent_puzzle = Puzzle::parse(&allocator, parent_puzzle_ptr);
94+
95+
let (p2_parent, memos) = P2ParentCoin::parse_child(
96+
&mut allocator,
97+
parent_state.coin,
98+
parent_puzzle,
99+
parent_solution_ptr,
100+
)?
101+
.ok_or(WalletError::Parse(
102+
"Failed to instantiate from parent state".to_string(),
103+
))?;
104+
105+
let memos_vec = match memos {
106+
Memos::Some(node) => Vec::<Bytes>::from_clvm(&mut allocator, node)
107+
.ok()
108+
.unwrap_or_default(),
109+
Memos::None => Vec::new(),
110+
};
111+
112+
let morphed_store_id: Option<Bytes32> = if memos_vec.is_empty() {
113+
None
114+
} else {
115+
Bytes32::from_bytes(&memos_vec[0]).ok()
116+
};
117+
118+
let mut mirror_urls_vec = Vec::new();
119+
for i in 1..memos_vec.len() {
120+
if let Ok(url_string) = String::from_utf8(memos_vec[i].to_vec()) {
121+
mirror_urls_vec.push(url_string);
122+
}
123+
}
124+
125+
let mirror_urls = if mirror_urls_vec.is_empty() {
126+
None
127+
} else {
128+
Some(mirror_urls_vec)
129+
};
130+
131+
Ok(Self {
132+
inner: p2_parent,
133+
morphed_store_id,
134+
mirror_urls,
135+
})
136+
}
137+
138+
/// Uses the specified $DIG to create a collateral coin for the provided DIG store ID (launcher ID)
139+
pub fn create(
140+
dig_coins: Vec<DigCoin>,
141+
collateral_amount: u64,
142+
store_id: Bytes32,
143+
mirror_urls: Option<Vec<String>>,
144+
synthetic_key: PublicKey,
145+
fee_coins: Vec<Coin>,
146+
fee: u64,
147+
) -> Result<Vec<CoinSpend>, WalletError> {
148+
let p2_parent_inner_hash = P2ParentCoin::inner_puzzle_hash(Some(DIG_ASSET_ID));
149+
150+
let mut ctx = SpendContext::new();
151+
152+
let morphed_store_id = Self::morph_store_launcher_if_for_collateral(store_id);
153+
154+
let memos = match mirror_urls {
155+
Some(urls) => {
156+
let mut memos_vec = Vec::with_capacity(urls.len() + 1);
157+
memos_vec.push(morphed_store_id.to_vec());
158+
159+
for url in &urls {
160+
memos_vec.push(url.as_bytes().to_vec());
161+
}
162+
163+
let memos_node_ptr = ctx.alloc(&memos_vec)?;
164+
Memos::Some(memos_node_ptr)
165+
}
166+
None => ctx.hint(morphed_store_id)?,
167+
};
168+
169+
let actions = [
170+
Action::fee(fee),
171+
Action::send(
172+
Id::Existing(DIG_ASSET_ID),
173+
p2_parent_inner_hash.into(),
174+
collateral_amount,
175+
memos,
176+
),
177+
];
178+
179+
let p2_layer = StandardLayer::new(synthetic_key);
180+
let p2_puzzle_hash: Bytes32 = p2_layer.tree_hash().into();
181+
let mut spends = Spends::new(p2_puzzle_hash);
182+
183+
// add collateral coins to spends
184+
for dig_coin in dig_coins {
185+
spends.add(dig_coin.cat());
186+
}
187+
188+
// add fee coins to spends
189+
for fee_xch_coin in fee_coins {
190+
spends.add(fee_xch_coin);
191+
}
192+
193+
let deltas = spends.apply(&mut ctx, &actions)?;
194+
let index_map = indexmap! {p2_puzzle_hash => synthetic_key};
195+
196+
let _outputs =
197+
spends.finish_with_keys(&mut ctx, &deltas, Relation::AssertConcurrent, &index_map)?;
198+
199+
Ok(ctx.take())
200+
}
201+
202+
/// Builds the spend bundle for spending the $DIG collateral coin to de-collateralize
203+
/// the store and return spendable $DIG to the wallet that created the collateral coin.
204+
pub fn spend(
205+
&self,
206+
synthetic_key: PublicKey,
207+
fee_coins: Vec<Coin>,
208+
fee: u64,
209+
) -> Result<Vec<CoinSpend>, WalletError> {
210+
let p2_layer = StandardLayer::new(synthetic_key);
211+
let p2_puzzle_hash: Bytes32 = p2_layer.tree_hash().into();
212+
213+
if p2_puzzle_hash != self.inner.proof.parent_inner_puzzle_hash {
214+
return Err(WalletError::PuzzleHashMismatch(
215+
"This coin is not owned by this wallet".to_string(),
216+
));
217+
}
218+
219+
let collateral_spend_conditions =
220+
Conditions::new().create_coin(p2_puzzle_hash, self.inner.coin.amount, Memos::None);
221+
222+
let mut ctx = SpendContext::new();
223+
224+
// add the collateral p2 parent spend to the spend context
225+
let p2_delegated_spend =
226+
p2_layer.spend_with_conditions(&mut ctx, collateral_spend_conditions)?;
227+
228+
self.inner.spend(&mut ctx, p2_delegated_spend, ())?;
229+
230+
// use actions and spends to attach fee to transaction and generate change
231+
let actions = [Action::fee(fee)];
232+
let mut fee_spends = Spends::new(p2_puzzle_hash);
233+
fee_spends
234+
.conditions
235+
.required
236+
.push(AssertConcurrentSpend::new(self.inner.coin.coin_id()));
237+
238+
// add fee coins to spends
239+
for fee_xch_coin in fee_coins {
240+
fee_spends.add(fee_xch_coin);
241+
}
242+
243+
let deltas = fee_spends.apply(&mut ctx, &actions)?;
244+
let index_map = indexmap! {p2_puzzle_hash => synthetic_key};
245+
246+
let _outputs = fee_spends.finish_with_keys(
247+
&mut ctx,
248+
&deltas,
249+
Relation::AssertConcurrent,
250+
&index_map,
251+
)?;
252+
253+
Ok(ctx.take())
254+
}
255+
}

src/error.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,15 @@ pub enum WalletError {
2323
#[error("{0:?}")]
2424
Driver(#[from] DriverError),
2525

26-
#[error("ParseError")]
27-
Parse,
26+
#[error("ParseError: {0}")]
27+
Parse(String),
2828

2929
#[error("UnknownCoin")]
3030
UnknownCoin,
3131

3232
#[error("Clvm error")]
3333
Clvm,
34+
3435
#[error("ToClvm error: {0}")]
3536
ToClvm(#[from] chia::clvm_traits::ToClvmError),
3637

0 commit comments

Comments
 (0)