chia_sdk_driver/actions/
update_nft.rs

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}