gpl_compression/instructions/
post.rs

1use crate::events::{CompressedPostDeleted, CompressedPostNew, CompressedPostUpdated};
2use crate::state::TreeConfig;
3use crate::utils::{append_leaf, replace_leaf, try_find_asset_id, LeafSchema};
4use anchor_lang::Discriminator;
5
6use gpl_core::errors::PostError;
7use spl_account_compression::wrap_application_data_v1;
8use spl_account_compression::Node;
9
10use gpl_core::state::{Post, Profile, User, MAX_LEN_URI};
11
12use anchor_lang::prelude::*;
13use std::convert::AsRef;
14
15use gpl_core::constants::*;
16
17use anchor_lang::solana_program::keccak::hashv;
18use gpl_core::program::GplCore;
19use spl_account_compression::program::SplAccountCompression;
20use spl_account_compression::Noop;
21
22// Create Post
23#[derive(Accounts)]
24#[instruction(metadata_uri: String, random_hash: [u8;32])]
25pub struct CreateCompressedPost<'info> {
26    #[account(
27        seeds = [
28            PROFILE_PREFIX_SEED.as_bytes(),
29            profile.namespace.as_ref().as_bytes(),
30            user.to_account_info().key.as_ref(),
31        ],
32        seeds::program = gpl_core_program.key(),
33        bump,
34        has_one = user,
35    )]
36    pub profile: Account<'info, Profile>,
37    #[account(
38        seeds = [
39            USER_PREFIX_SEED.as_bytes(),
40            user.random_hash.as_ref(),
41        ],
42        seeds::program = gpl_core_program.key(),
43        bump,
44        has_one = authority,
45    )]
46    pub user: Account<'info, User>,
47
48    #[account(seeds = [merkle_tree.key.as_ref()], bump)]
49    pub tree_config: Account<'info, TreeConfig>,
50
51    #[account(mut)]
52    /// CHECK The account must have the same authority as that of the config
53    pub merkle_tree: UncheckedAccount<'info>,
54
55    #[account(mut)]
56    pub authority: Signer<'info>,
57
58    pub compression_program: Program<'info, SplAccountCompression>,
59    pub log_wrapper_program: Program<'info, Noop>,
60    pub gpl_core_program: Program<'info, GplCore>,
61    pub system_program: Program<'info, System>,
62}
63
64// Handler to create a new Post
65pub fn create_compressed_post_handler(
66    ctx: Context<CreateCompressedPost>,
67    metadata_uri: String,
68    random_hash: [u8; 32],
69) -> Result<()> {
70    require!(metadata_uri.len() <= MAX_LEN_URI, PostError::URITooLong);
71
72    let post_seeds = [POST_PREFIX_SEED.as_bytes(), random_hash.as_ref()];
73
74    let (post_id, post_bump) =
75        Pubkey::try_find_program_address(&post_seeds, &GplCore::id()).unwrap();
76
77    let seed_hash = hashv(&post_seeds).to_bytes();
78
79    let asset_id = try_find_asset_id(ctx.accounts.merkle_tree.key, seed_hash)?;
80
81    let post = Post {
82        metadata_uri,
83        random_hash,
84        profile: *ctx.accounts.profile.to_account_info().key,
85        reply_to: None,
86    };
87
88    let leaf = LeafSchema {
89        asset_id,
90        seed_hash,
91        data_hash: hashv(&[&Post::DISCRIMINATOR, &post.try_to_vec()?]).to_bytes(),
92    };
93
94    let leaf_node = leaf.to_node()?;
95
96    wrap_application_data_v1(leaf_node.to_vec(), &ctx.accounts.log_wrapper_program)?;
97
98    append_leaf(
99        ctx.accounts.merkle_tree.key,
100        ctx.bumps["tree_config"],
101        &ctx.accounts.authority.to_account_info(),
102        leaf_node,
103        &ctx.accounts.merkle_tree,
104        &ctx.accounts.compression_program,
105        &ctx.accounts.log_wrapper_program,
106    )?;
107
108    emit!(CompressedPostNew {
109        asset_id,
110        post_id,
111        post_bump,
112        profile: *ctx.accounts.profile.to_account_info().key,
113        user: *ctx.accounts.user.to_account_info().key,
114        random_hash: random_hash,
115        metadata_uri: post.metadata_uri.clone(),
116        timestamp: Clock::get()?.unix_timestamp,
117        index: 0 // TODO: Get the index from the tree
118    });
119
120    Ok(())
121}
122
123// Update a post
124#[derive(Accounts)]
125#[instruction(metadata_uri: String, new_metadata_uri: String, random_hash: [u8;32], root: [u8;32], index: u32)]
126pub struct UpdateCompressedPost<'info> {
127    #[account(
128        seeds = [
129            PROFILE_PREFIX_SEED.as_bytes(),
130            profile.namespace.as_ref().as_bytes(),
131            user.to_account_info().key.as_ref(),
132        ],
133        seeds::program = gpl_core_program.key(),
134        bump,
135        has_one = user,
136    )]
137    pub profile: Account<'info, Profile>,
138    #[account(
139        seeds = [
140            USER_PREFIX_SEED.as_bytes(),
141            user.random_hash.as_ref(),
142        ],
143        seeds::program = gpl_core_program.key(),
144        bump,
145        has_one = authority,
146    )]
147    pub user: Account<'info, User>,
148    #[account(seeds = [merkle_tree.key.as_ref()], bump)]
149    pub tree_config: Account<'info, TreeConfig>,
150
151    #[account(mut)]
152    /// CHECK The account must have the same authority as that of the config
153    pub merkle_tree: UncheckedAccount<'info>,
154
155    #[account(mut)]
156    pub authority: Signer<'info>,
157
158    pub compression_program: Program<'info, SplAccountCompression>,
159    pub log_wrapper_program: Program<'info, Noop>,
160    pub gpl_core_program: Program<'info, GplCore>,
161    pub system_program: Program<'info, System>,
162}
163
164// Handler to update a Post
165pub fn update_compressed_post_handler<'info>(
166    ctx: Context<'_, '_, '_, 'info, UpdateCompressedPost<'info>>,
167    metadata_uri: String,
168    new_metadata_uri: String,
169    random_hash: [u8; 32],
170    root: [u8; 32],
171    index: u32,
172) -> Result<()> {
173    // CHECK metadata_uri length
174    require!(metadata_uri.len() <= MAX_LEN_URI, PostError::URITooLong);
175
176    let post_seeds = [POST_PREFIX_SEED.as_bytes(), random_hash.as_ref()];
177
178    let (post_id, post_bump) =
179        Pubkey::try_find_program_address(&post_seeds, &GplCore::id()).unwrap();
180
181    let seed_hash = hashv(&post_seeds).to_bytes();
182
183    let asset_id = try_find_asset_id(ctx.accounts.merkle_tree.key, seed_hash)?;
184
185    let old_post = Post {
186        metadata_uri,
187        random_hash,
188        profile: *ctx.accounts.profile.to_account_info().key,
189        reply_to: None,
190    };
191
192    let old_leaf = LeafSchema {
193        asset_id,
194        seed_hash,
195        // May be better as a trait?
196        data_hash: hashv(&[&Post::DISCRIMINATOR, &old_post.try_to_vec()?]).to_bytes(),
197    };
198
199    let old_leaf_node = old_leaf.to_node()?;
200
201    let new_post = Post {
202        metadata_uri: new_metadata_uri,
203        random_hash,
204        profile: *ctx.accounts.profile.to_account_info().key,
205        reply_to: None,
206    };
207
208    let new_leaf = LeafSchema {
209        asset_id,
210        seed_hash,
211        // May be better as a trait?
212        data_hash: hashv(&[&Post::DISCRIMINATOR, &new_post.try_to_vec()?]).to_bytes(),
213    };
214
215    let new_leaf_node = new_leaf.to_node()?;
216
217    wrap_application_data_v1(new_leaf_node.to_vec(), &ctx.accounts.log_wrapper_program)?;
218
219    replace_leaf(
220        ctx.accounts.merkle_tree.key,
221        ctx.bumps["tree_config"],
222        &ctx.accounts.authority.to_account_info(),
223        &ctx.accounts.merkle_tree,
224        root,
225        old_leaf_node,
226        new_leaf_node,
227        index,
228        ctx.remaining_accounts,
229        &ctx.accounts.compression_program,
230        &ctx.accounts.log_wrapper_program,
231    )?;
232
233    // emit update post event
234    emit!(CompressedPostUpdated {
235        asset_id,
236        post_id,
237        post_bump,
238        profile: *ctx.accounts.profile.to_account_info().key,
239        user: *ctx.accounts.user.to_account_info().key,
240        random_hash: random_hash,
241        metadata_uri: new_post.metadata_uri.clone(),
242        timestamp: Clock::get()?.unix_timestamp,
243        index: index
244    });
245
246    Ok(())
247}
248
249// Delete a post
250#[derive(Accounts)]
251// Ideally this should be compacted down to asset_id, root, index
252#[instruction(metadata_uri: String, random_hash: [u8;32], root: [u8;32], index: u32)]
253pub struct DeleteCompressedPost<'info> {
254    #[account(
255        seeds = [
256            PROFILE_PREFIX_SEED.as_bytes(),
257            profile.namespace.as_ref().as_bytes(),
258            user.to_account_info().key.as_ref(),
259        ],
260        seeds::program = gpl_core_program.key(),
261        bump,
262        has_one = user,
263    )]
264    pub profile: Account<'info, Profile>,
265    #[account(
266        seeds = [
267            USER_PREFIX_SEED.as_bytes(),
268            user.random_hash.as_ref(),
269        ],
270        seeds::program = gpl_core_program.key(),
271        bump,
272        has_one = authority,
273    )]
274    pub user: Account<'info, User>,
275
276    #[account(seeds = [merkle_tree.key.as_ref()], bump)]
277    pub tree_config: Account<'info, TreeConfig>,
278
279    #[account(mut)]
280    /// CHECK The account must have the same authority as that of the config
281    pub merkle_tree: UncheckedAccount<'info>,
282
283    #[account(mut)]
284    pub authority: Signer<'info>,
285    pub compression_program: Program<'info, SplAccountCompression>,
286    pub log_wrapper_program: Program<'info, Noop>,
287    pub gpl_core_program: Program<'info, GplCore>,
288    pub system_program: Program<'info, System>,
289}
290
291// Handler to delete a compressed post
292pub fn delete_compressed_post_handler<'info>(
293    ctx: Context<'_, '_, '_, 'info, DeleteCompressedPost<'info>>,
294    metadata_uri: String,
295    random_hash: [u8; 32],
296    root: [u8; 32],
297    index: u32,
298) -> Result<()> {
299    // CHECK metadata_uri length
300    require!(metadata_uri.len() <= MAX_LEN_URI, PostError::URITooLong);
301
302    let post_seeds = [POST_PREFIX_SEED.as_bytes(), random_hash.as_ref()];
303
304    let (post_id, post_bump) =
305        Pubkey::try_find_program_address(&post_seeds, &GplCore::id()).unwrap();
306
307    let seed_hash = hashv(&post_seeds).to_bytes();
308
309    let asset_id = try_find_asset_id(ctx.accounts.merkle_tree.key, seed_hash)?;
310
311    let old_post = Post {
312        metadata_uri,
313        random_hash,
314        profile: *ctx.accounts.profile.to_account_info().key,
315        reply_to: None,
316    };
317
318    let old_leaf = LeafSchema {
319        asset_id,
320        seed_hash,
321        // May be better as a trait?
322        data_hash: hashv(&[&Post::DISCRIMINATOR, &old_post.try_to_vec()?]).to_bytes(),
323    };
324
325    let old_leaf_node = old_leaf.to_node()?;
326
327    let new_leaf_node = Node::default();
328
329    wrap_application_data_v1(new_leaf_node.to_vec(), &ctx.accounts.log_wrapper_program)?;
330
331    replace_leaf(
332        ctx.accounts.merkle_tree.key,
333        ctx.bumps["tree_config"],
334        &ctx.accounts.authority.to_account_info(),
335        &ctx.accounts.merkle_tree,
336        root,
337        old_leaf_node,
338        new_leaf_node,
339        index,
340        ctx.remaining_accounts,
341        &ctx.accounts.compression_program,
342        &ctx.accounts.log_wrapper_program,
343    )?;
344
345    // emit delete post event
346    emit!(CompressedPostDeleted {
347        asset_id,
348        post_id,
349        post_bump,
350        profile: *ctx.accounts.profile.to_account_info().key,
351        user: *ctx.accounts.user.to_account_info().key,
352        random_hash: random_hash,
353        metadata_uri: old_post.metadata_uri.clone(),
354        timestamp: Clock::get()?.unix_timestamp,
355        index: index
356    });
357
358    Ok(())
359}