Skip to content

Commit 424e7bf

Browse files
authored
Merge pull request #31 from DIG-Network/mirror-collateral-support
Dig Collateral Coin Utility and Mirror Coin support
2 parents 6a2d7a2 + d0f7a99 commit 424e7bf

File tree

5 files changed

+441
-273
lines changed

5 files changed

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

0 commit comments

Comments
 (0)