chia_sdk_driver/actions/
update_nft.rs

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}