1use chia_sdk_types::{
2 Conditions,
3 conditions::{TradePrice, TransferNft},
4};
5
6use crate::{
7 Deltas, DriverError, Id, SingletonInfo, Spend, SpendAction, SpendContext, SpendKind, Spends,
8 assignment_puzzle_announcement_id,
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 deltas.update(self.id).input += 1;
50 deltas.update(self.id).output += 1;
51 deltas.set_needed(self.id);
52
53 if let Some(transfer) = &self.transfer
54 && let Some(did_id) = transfer.did_id
55 {
56 deltas.update(did_id).input += 1;
57 deltas.update(did_id).output += 1;
58 deltas.set_needed(did_id);
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, UriKind};
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 = MetadataUpdate {
148 kind: UriKind::Data,
149 uri: "https://example.com/2".to_string(),
150 }
151 .spend(&mut ctx)?;
152 metadata
153 .data_uris
154 .insert(0, "https://example.com/2".to_string());
155 let updated_metadata = ctx.alloc_hashed(&metadata)?;
156
157 let mut spends = Spends::new(alice.puzzle_hash);
158 spends.add(alice.coin);
159
160 let deltas = spends.apply(
161 &mut ctx,
162 &[
163 Action::mint_nft(
164 original_metadata,
165 NFT_METADATA_UPDATER_DEFAULT_HASH.into(),
166 Bytes32::default(),
167 0,
168 1,
169 ),
170 Action::update_nft(Id::New(0), vec![metadata_update_spend], None),
171 ],
172 )?;
173
174 let outputs = spends.finish_with_keys(
175 &mut ctx,
176 &deltas,
177 Relation::None,
178 &indexmap! { alice.puzzle_hash => alice.pk },
179 )?;
180
181 sim.spend_coins(ctx.take(), &[alice.sk])?;
182
183 let nft = outputs.nfts[&Id::New(0)];
184 assert_ne!(sim.coin_state(nft.coin.coin_id()), None);
185 assert_eq!(nft.info.p2_puzzle_hash, alice.puzzle_hash);
186 assert_eq!(nft.info.metadata, updated_metadata);
187
188 Ok(())
189 }
190
191 #[test]
192 fn test_action_update_nft_uri_twice() -> Result<()> {
193 let mut sim = Simulator::new();
194 let mut ctx = SpendContext::new();
195
196 let alice = sim.bls(1);
197
198 let mut metadata = NftMetadata {
199 data_hash: Some(Bytes32::default()),
200 data_uris: vec!["https://example.com/1".to_string()],
201 ..Default::default()
202 };
203 let original_metadata = ctx.alloc_hashed(&metadata)?;
204
205 let metadata_update_spends = vec![
206 MetadataUpdate {
207 kind: UriKind::Data,
208 uri: "https://example.com/2".to_string(),
209 }
210 .spend(&mut ctx)?,
211 MetadataUpdate {
212 kind: UriKind::Data,
213 uri: "https://example.com/3".to_string(),
214 }
215 .spend(&mut ctx)?,
216 ];
217 metadata
218 .data_uris
219 .insert(0, "https://example.com/3".to_string());
220 metadata
221 .data_uris
222 .insert(0, "https://example.com/2".to_string());
223 let updated_metadata = ctx.alloc_hashed(&metadata)?;
224
225 let mut spends = Spends::new(alice.puzzle_hash);
226 spends.add(alice.coin);
227
228 let deltas = spends.apply(
229 &mut ctx,
230 &[
231 Action::mint_nft(
232 original_metadata,
233 NFT_METADATA_UPDATER_DEFAULT_HASH.into(),
234 Bytes32::default(),
235 0,
236 1,
237 ),
238 Action::update_nft(Id::New(0), metadata_update_spends, None),
239 ],
240 )?;
241
242 let outputs = spends.finish_with_keys(
243 &mut ctx,
244 &deltas,
245 Relation::None,
246 &indexmap! { alice.puzzle_hash => alice.pk },
247 )?;
248
249 sim.spend_coins(ctx.take(), &[alice.sk])?;
250
251 let nft = outputs.nfts[&Id::New(0)];
252 assert_ne!(sim.coin_state(nft.coin.coin_id()), None);
253 assert_eq!(nft.info.p2_puzzle_hash, alice.puzzle_hash);
254 assert_eq!(nft.info.metadata, updated_metadata);
255
256 Ok(())
257 }
258
259 #[test]
260 fn test_action_update_nft_owner() -> Result<()> {
261 let mut sim = Simulator::new();
262 let mut ctx = SpendContext::new();
263
264 let alice = sim.bls(2);
265
266 let mut spends = Spends::new(alice.puzzle_hash);
267 spends.add(alice.coin);
268
269 let deltas = spends.apply(
270 &mut ctx,
271 &[
272 Action::create_empty_did(),
273 Action::mint_nft(HashedPtr::NIL, Bytes32::default(), Bytes32::default(), 0, 1),
274 Action::update_nft(
275 Id::New(1),
276 Vec::new(),
277 Some(TransferNftById::new(Some(Id::New(0)), vec![])),
278 ),
279 ],
280 )?;
281
282 let outputs = spends.finish_with_keys(
283 &mut ctx,
284 &deltas,
285 Relation::None,
286 &indexmap! { alice.puzzle_hash => alice.pk },
287 )?;
288
289 sim.spend_coins(ctx.take(), &[alice.sk])?;
290
291 let did = outputs.dids[&Id::New(0)];
292 assert_ne!(sim.coin_state(did.coin.coin_id()), None);
293 assert_eq!(did.info.p2_puzzle_hash, alice.puzzle_hash);
294
295 let nft = outputs.nfts[&Id::New(1)];
296 assert_ne!(sim.coin_state(nft.coin.coin_id()), None);
297 assert_eq!(nft.info.p2_puzzle_hash, alice.puzzle_hash);
298 assert_eq!(nft.info.current_owner, Some(did.info.launcher_id));
299
300 Ok(())
301 }
302}