1use chia_sdk_types::{
2 conditions::{TradePrice, TransferNft},
3 Conditions,
4};
5
6use crate::{
7 assignment_puzzle_announcement_id, Deltas, DriverError, Id, Spend, SpendAction, SpendContext,
8 SpendKind, Spends,
9};
10
11#[derive(Debug, Default, Clone)]
12pub struct TransferNftById {
13 pub did_id: Option<Id>,
14 pub trade_prices: Vec<TradePrice>,
15}
16
17impl TransferNftById {
18 pub fn new(did_id: Option<Id>, trade_prices: Vec<TradePrice>) -> Self {
19 Self {
20 did_id,
21 trade_prices,
22 }
23 }
24}
25
26#[derive(Debug, Clone)]
27pub struct UpdateNftAction {
28 pub id: Id,
29 pub metadata_update_spends: Vec<Spend>,
30 pub transfer: Option<TransferNftById>,
31}
32
33impl UpdateNftAction {
34 pub fn new(
35 id: Id,
36 metadata_update_spends: Vec<Spend>,
37 transfer: Option<TransferNftById>,
38 ) -> Self {
39 Self {
40 id,
41 metadata_update_spends,
42 transfer,
43 }
44 }
45}
46
47impl SpendAction for UpdateNftAction {
48 fn calculate_delta(&self, deltas: &mut Deltas, _index: usize) {
49 let nft = deltas.update(self.id);
50 nft.input += 1;
51 nft.output += 1;
52
53 if let Some(transfer) = &self.transfer {
54 if let Some(did_id) = transfer.did_id {
55 let did = deltas.update(did_id);
56 did.input += 1;
57 did.output += 1;
58 }
59 }
60 }
61
62 fn spend(
63 &self,
64 _ctx: &mut SpendContext,
65 spends: &mut Spends,
66 _index: usize,
67 ) -> Result<(), DriverError> {
68 let nft = spends
69 .nfts
70 .get_mut(&self.id)
71 .ok_or(DriverError::InvalidAssetId)?
72 .last_mut()?;
73
74 nft.child_info
75 .metadata_update_spends
76 .extend_from_slice(&self.metadata_update_spends);
77
78 if let Some(transfer) = self.transfer.clone() {
79 let transfer_condition = if let Some(did_id) = transfer.did_id {
80 let did = spends
81 .dids
82 .get_mut(&did_id)
83 .ok_or(DriverError::InvalidAssetId)?
84 .last_mut()?;
85
86 let transfer_condition = TransferNft::new(
87 Some(did.asset.info.launcher_id),
88 transfer.trade_prices,
89 Some(did.asset.info.inner_puzzle_hash().into()),
90 );
91
92 match &mut did.kind {
93 SpendKind::Conditions(spend) => {
94 spend.add_conditions(
95 Conditions::new()
96 .assert_puzzle_announcement(assignment_puzzle_announcement_id(
97 nft.asset.coin.puzzle_hash,
98 &transfer_condition,
99 ))
100 .create_puzzle_announcement(nft.asset.info.launcher_id.into()),
101 );
102 }
103 SpendKind::Settlement(_) => {
104 return Err(DriverError::CannotEmitConditions);
105 }
106 }
107
108 transfer_condition
109 } else {
110 TransferNft::new(None, transfer.trade_prices, None)
111 };
112
113 nft.child_info.transfer_condition = Some(transfer_condition);
114 }
115
116 Ok(())
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use anyhow::Result;
123 use chia_protocol::Bytes32;
124 use chia_puzzle_types::nft::NftMetadata;
125 use chia_puzzles::NFT_METADATA_UPDATER_DEFAULT_HASH;
126 use chia_sdk_test::Simulator;
127 use indexmap::indexmap;
128
129 use crate::{Action, HashedPtr, MetadataUpdate, Relation};
130
131 use super::*;
132
133 #[test]
134 fn test_action_update_nft_uri() -> Result<()> {
135 let mut sim = Simulator::new();
136 let mut ctx = SpendContext::new();
137
138 let alice = sim.bls(1);
139
140 let mut metadata = NftMetadata {
141 data_hash: Some(Bytes32::default()),
142 data_uris: vec!["https://example.com/1".to_string()],
143 ..Default::default()
144 };
145 let original_metadata = ctx.alloc_hashed(&metadata)?;
146
147 let metadata_update_spend =
148 MetadataUpdate::NewDataUri("https://example.com/2".to_string()).spend(&mut ctx)?;
149 metadata
150 .data_uris
151 .insert(0, "https://example.com/2".to_string());
152 let updated_metadata = ctx.alloc_hashed(&metadata)?;
153
154 let mut spends = Spends::new(alice.puzzle_hash);
155 spends.add(alice.coin);
156
157 let deltas = spends.apply(
158 &mut ctx,
159 &[
160 Action::mint_nft(
161 original_metadata,
162 NFT_METADATA_UPDATER_DEFAULT_HASH.into(),
163 Bytes32::default(),
164 0,
165 1,
166 ),
167 Action::update_nft(Id::New(0), vec![metadata_update_spend], None),
168 ],
169 )?;
170
171 let outputs = spends.finish_with_keys(
172 &mut ctx,
173 &deltas,
174 Relation::None,
175 &indexmap! { alice.puzzle_hash => alice.pk },
176 )?;
177
178 sim.spend_coins(ctx.take(), &[alice.sk])?;
179
180 let nft = outputs.nfts[&Id::New(0)];
181 assert_ne!(sim.coin_state(nft.coin.coin_id()), None);
182 assert_eq!(nft.info.p2_puzzle_hash, alice.puzzle_hash);
183 assert_eq!(nft.info.metadata, updated_metadata);
184
185 Ok(())
186 }
187
188 #[test]
189 fn test_action_update_nft_uri_twice() -> Result<()> {
190 let mut sim = Simulator::new();
191 let mut ctx = SpendContext::new();
192
193 let alice = sim.bls(1);
194
195 let mut metadata = NftMetadata {
196 data_hash: Some(Bytes32::default()),
197 data_uris: vec!["https://example.com/1".to_string()],
198 ..Default::default()
199 };
200 let original_metadata = ctx.alloc_hashed(&metadata)?;
201
202 let metadata_update_spends = vec![
203 MetadataUpdate::NewDataUri("https://example.com/2".to_string()).spend(&mut ctx)?,
204 MetadataUpdate::NewDataUri("https://example.com/3".to_string()).spend(&mut ctx)?,
205 ];
206 metadata
207 .data_uris
208 .insert(0, "https://example.com/3".to_string());
209 metadata
210 .data_uris
211 .insert(0, "https://example.com/2".to_string());
212 let updated_metadata = ctx.alloc_hashed(&metadata)?;
213
214 let mut spends = Spends::new(alice.puzzle_hash);
215 spends.add(alice.coin);
216
217 let deltas = spends.apply(
218 &mut ctx,
219 &[
220 Action::mint_nft(
221 original_metadata,
222 NFT_METADATA_UPDATER_DEFAULT_HASH.into(),
223 Bytes32::default(),
224 0,
225 1,
226 ),
227 Action::update_nft(Id::New(0), metadata_update_spends, None),
228 ],
229 )?;
230
231 let outputs = spends.finish_with_keys(
232 &mut ctx,
233 &deltas,
234 Relation::None,
235 &indexmap! { alice.puzzle_hash => alice.pk },
236 )?;
237
238 sim.spend_coins(ctx.take(), &[alice.sk])?;
239
240 let nft = outputs.nfts[&Id::New(0)];
241 assert_ne!(sim.coin_state(nft.coin.coin_id()), None);
242 assert_eq!(nft.info.p2_puzzle_hash, alice.puzzle_hash);
243 assert_eq!(nft.info.metadata, updated_metadata);
244
245 Ok(())
246 }
247
248 #[test]
249 fn test_action_update_nft_owner() -> Result<()> {
250 let mut sim = Simulator::new();
251 let mut ctx = SpendContext::new();
252
253 let alice = sim.bls(2);
254
255 let mut spends = Spends::new(alice.puzzle_hash);
256 spends.add(alice.coin);
257
258 let deltas = spends.apply(
259 &mut ctx,
260 &[
261 Action::create_empty_did(),
262 Action::mint_nft(HashedPtr::NIL, Bytes32::default(), Bytes32::default(), 0, 1),
263 Action::update_nft(
264 Id::New(1),
265 Vec::new(),
266 Some(TransferNftById::new(Some(Id::New(0)), vec![])),
267 ),
268 ],
269 )?;
270
271 let outputs = spends.finish_with_keys(
272 &mut ctx,
273 &deltas,
274 Relation::None,
275 &indexmap! { alice.puzzle_hash => alice.pk },
276 )?;
277
278 sim.spend_coins(ctx.take(), &[alice.sk])?;
279
280 let did = outputs.dids[&Id::New(0)];
281 assert_ne!(sim.coin_state(did.coin.coin_id()), None);
282 assert_eq!(did.info.p2_puzzle_hash, alice.puzzle_hash);
283
284 let nft = outputs.nfts[&Id::New(1)];
285 assert_ne!(sim.coin_state(nft.coin.coin_id()), None);
286 assert_eq!(nft.info.p2_puzzle_hash, alice.puzzle_hash);
287 assert_eq!(nft.info.current_owner, Some(did.info.launcher_id));
288
289 Ok(())
290 }
291}