hpl_hive_control/instructions/
user_instructions.rs

1use {
2    crate::{constants::vault_seeds, errors::HplHiveControlError, state::*},
3    anchor_lang::prelude::*,
4    hpl_toolkit::prelude::*,
5    spl_account_compression::{program::SplAccountCompression, Noop},
6    std::collections::HashSet,
7};
8
9fn create_user<'info>(
10    info: UserInfo,
11    social_info: Option<SocialInfo>,
12    wallets: Wallets,
13    clock_slot: u64,
14    vault_bump: u8,
15    global: &mut Account<'info, Global>,
16    merkle_tree: &AccountInfo<'info>,
17    vault: &AccountInfo<'info>,
18    compression_program: &Program<'info, SplAccountCompression>,
19    log_wrapper: &Program<'info, Noop>,
20) -> Result<u64> {
21    let (leaf_idx, _) = global
22        .user_trees
23        .assert_append(merkle_tree.to_account_info())?;
24
25    global.total_users += 1;
26
27    let user = User {
28        id: global.total_users,
29        info,
30        wallets,
31        social_info: social_info.unwrap_or_default(),
32        placeholder_1: PlaceHolder::None,
33        placeholder_2: PlaceHolder::None,
34        placeholder_3: PlaceHolder::None,
35        placeholder_4: PlaceHolder::None,
36    };
37
38    append_leaf(
39        user.to_compressed().to_node(),
40        &vault,
41        merkle_tree,
42        compression_program,
43        log_wrapper,
44        Some(&[&vault_seeds(&[vault_bump])[..]]),
45    )?;
46
47    user.emit(clock_slot, leaf_idx, merkle_tree, log_wrapper, 0)?;
48
49    Ok(user.id)
50}
51
52#[derive(Accounts)]
53pub struct NewUser<'info> {
54    #[account(mut)]
55    pub global: Account<'info, Global>,
56
57    /// CHECK: unsafe
58    #[account(mut)]
59    pub merkle_tree: AccountInfo<'info>,
60
61    /// Wallet that has delegate authority over user actions
62    /// This wallet is used for shadow signing transactions on behalf of this user
63    /// CHECK: This is not dangerous
64    pub shadow_signer: AccountInfo<'info>,
65
66    /// User default wallet
67    /// CHECK: This is not dangerous
68    pub wallet: AccountInfo<'info>,
69
70    /// The honeycomb driver
71    pub authority: Signer<'info>,
72
73    /// CHECK: This is not dangerous
74    #[account(mut, seeds = [b"vault".as_ref(), crate::id().as_ref()], bump)]
75    pub vault: AccountInfo<'info>,
76
77    /// System Program
78    pub system_program: Program<'info, System>,
79
80    /// SPL account compression program.
81    pub compression_program: Program<'info, SplAccountCompression>,
82
83    /// SPL Noop program.
84    pub log_wrapper: Program<'info, Noop>,
85
86    /// NATIVE SYSVAR CLOCK
87    pub clock: Sysvar<'info, Clock>,
88
89    /// SYSVAR RENT ACCOUNT
90    pub rent_sysvar: Sysvar<'info, Rent>,
91
92    /// NATIVE INSTRUCTIONS SYSVAR
93    /// CHECK: This is not dangerous because we don't read or write from this account
94    #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)]
95    pub instructions_sysvar: AccountInfo<'info>,
96}
97
98/// Structure representing the arguments for initializing a user.
99///
100/// # Fields
101///
102/// - `username`: The username of the user to be initialized.
103/// - `name`: The name of the user to be initialized.
104/// - `bio`: The biography of the user to be initialized.
105/// - `pfp`: The profile picture URL or identifier of the user to be initialized.
106#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
107pub struct NewUserArgs {
108    pub username: ShortString,
109    pub name: ShortString,
110    pub bio: ShortString,
111    pub pfp: ShortString,
112    pub social_info: Option<SocialInfoInput>,
113}
114
115pub fn new_user(ctx: Context<NewUser>, args: NewUserArgs) -> Result<u64> {
116    let wallets = ShortVec::new_from_vec_unchecked(vec![ctx.accounts.wallet.key()]);
117
118    let social_info = args
119        .social_info
120        .map(|input| input.into_data(ctx.remaining_accounts, &wallets));
121
122    create_user(
123        UserInfo {
124            username: args.username,
125            name: args.name,
126            bio: args.bio,
127            pfp: args.pfp,
128        },
129        social_info,
130        Wallets {
131            shadow: ctx.accounts.shadow_signer.key(),
132            wallets,
133        },
134        ctx.accounts.clock.slot,
135        ctx.bumps.vault,
136        &mut ctx.accounts.global,
137        &ctx.accounts.merkle_tree,
138        &ctx.accounts.vault,
139        &ctx.accounts.compression_program,
140        &ctx.accounts.log_wrapper,
141    )
142}
143
144pub fn populate_user(ctx: Context<NewUser>, user: User) -> Result<u64> {
145    create_user(
146        user.info,
147        Some(user.social_info),
148        user.wallets,
149        ctx.accounts.clock.slot,
150        ctx.bumps.vault,
151        &mut ctx.accounts.global,
152        &ctx.accounts.merkle_tree,
153        &ctx.accounts.vault,
154        &ctx.accounts.compression_program,
155        &ctx.accounts.log_wrapper,
156    )
157}
158
159#[derive(Accounts)]
160pub struct NewUserWithProfile<'info> {
161    #[account(mut)]
162    pub global: Account<'info, Global>,
163
164    #[account(mut)]
165    pub project: Account<'info, Project>,
166
167    /// CHECK: unsafe
168    #[account(mut)]
169    pub users_merkle_tree: AccountInfo<'info>,
170
171    /// CHECK: unsafe
172    #[account(mut)]
173    pub profiles_merkle_tree: AccountInfo<'info>,
174
175    /// Wallet that has delegate authority over user actions
176    /// This wallet is used for shadow signing transactions on behalf of this user
177    /// CHECK: This is not dangerous
178    pub shadow_signer: AccountInfo<'info>,
179
180    /// User default wallet
181    /// CHECK: This is not dangerous
182    pub wallet: AccountInfo<'info>,
183
184    /// The honeycomb driver
185    pub authority: Signer<'info>,
186
187    /// The honeycomb driver
188    #[account(mut)]
189    pub payer: Signer<'info>,
190
191    /// CHECK: This is not dangerous
192    #[account(mut, seeds = [b"vault".as_ref(), crate::id().as_ref()], bump)]
193    pub vault: AccountInfo<'info>,
194
195    /// System Program
196    pub system_program: Program<'info, System>,
197
198    /// SPL account compression program.
199    pub compression_program: Program<'info, SplAccountCompression>,
200
201    /// SPL Noop program.
202    pub log_wrapper: Program<'info, Noop>,
203
204    /// NATIVE SYSVAR CLOCK
205    pub clock: Sysvar<'info, Clock>,
206
207    /// SYSVAR RENT ACCOUNT
208    pub rent_sysvar: Sysvar<'info, Rent>,
209
210    /// NATIVE INSTRUCTIONS SYSVAR
211    /// CHECK: This is not dangerous because we don't read or write from this account
212    #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)]
213    pub instructions_sysvar: AccountInfo<'info>,
214}
215
216/// Structure representing the arguments for initializing a user.
217///
218/// # Fields
219///
220/// - `username`: The username of the user to be initialized.
221/// - `name`: The name of the user to be initialized.
222/// - `bio`: The biography of the user to be initialized.
223/// - `pfp`: The profile picture URL or identifier of the user to be initialized.
224#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
225pub struct NewUserWithProfileArgs {
226    pub user_info: UserInfo,
227    pub user_social_info: Option<SocialInfoInput>,
228    pub profile_identity: Option<ShortString>,
229}
230
231pub fn new_user_with_profile(
232    ctx: &mut Context<NewUserWithProfile>,
233    args: NewUserWithProfileArgs,
234) -> Result<()> {
235    let wallets = ShortVec::new_from_vec_unchecked(vec![ctx.accounts.wallet.key()]);
236    let social_info = args
237        .user_social_info
238        .map(|input| input.into_data(ctx.remaining_accounts, &wallets));
239
240    let user_id = create_user(
241        UserInfo {
242            username: args.user_info.username,
243            name: args.user_info.name.clone(),
244            bio: args.user_info.bio.clone(),
245            pfp: args.user_info.pfp.clone(),
246        },
247        social_info,
248        Wallets {
249            shadow: ctx.accounts.shadow_signer.key(),
250            wallets,
251        },
252        ctx.accounts.clock.slot,
253        ctx.bumps.vault,
254        &mut ctx.accounts.global,
255        &ctx.accounts.users_merkle_tree,
256        &ctx.accounts.vault,
257        &ctx.accounts.compression_program,
258        &ctx.accounts.log_wrapper,
259    )?;
260
261    super::create_profile(
262        user_id,
263        args.profile_identity,
264        Some(ProfileInfo {
265            name: Some(args.user_info.name),
266            bio: Some(args.user_info.bio),
267            pfp: Some(args.user_info.pfp),
268        }),
269        &mut ctx.accounts.project,
270        ctx.accounts.clock.slot,
271        ctx.bumps.vault,
272        &ctx.accounts.profiles_merkle_tree,
273        &ctx.accounts.vault,
274        &ctx.accounts.compression_program,
275        &ctx.accounts.log_wrapper,
276    )
277}
278
279/// Structure representing the arguments for initializing a user.
280///
281/// # Fields
282///
283/// - `username`: The username of the user to be initialized.
284/// - `name`: The name of the user to be initialized.
285/// - `bio`: The biography of the user to be initialized.
286/// - `pfp`: The profile picture URL or identifier of the user to be initialized.
287#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
288pub struct PopulateUserWithProfileArgs {
289    pub user: User,
290    pub profile: Profile,
291}
292
293pub fn populate_user_with_profile(
294    ctx: Context<NewUserWithProfile>,
295    args: PopulateUserWithProfileArgs,
296) -> Result<()> {
297    let mut profile = args.profile;
298    profile.user_id = create_user(
299        args.user.info,
300        Some(args.user.social_info),
301        args.user.wallets,
302        ctx.accounts.clock.slot,
303        ctx.bumps.vault,
304        &mut ctx.accounts.global,
305        &ctx.accounts.users_merkle_tree,
306        &ctx.accounts.vault,
307        &ctx.accounts.compression_program,
308        &ctx.accounts.log_wrapper,
309    )?;
310    let (leaf_idx, _) = ctx
311        .accounts
312        .project
313        .profile_trees
314        .assert_append(ctx.accounts.profiles_merkle_tree.to_account_info())?;
315
316    append_leaf(
317        profile.to_compressed().to_node(),
318        &ctx.accounts.vault,
319        &ctx.accounts.profiles_merkle_tree,
320        &ctx.accounts.compression_program,
321        &ctx.accounts.log_wrapper,
322        Some(&[&vault_seeds(&[ctx.bumps.vault])[..]]),
323    )?;
324
325    profile.emit(
326        ctx.accounts.clock.slot,
327        leaf_idx,
328        &ctx.accounts.profiles_merkle_tree,
329        &ctx.accounts.log_wrapper,
330        0,
331    )
332}
333
334#[derive(Accounts)]
335pub struct UpdateUser<'info> {
336    pub global: Account<'info, Global>,
337
338    /// CHECK: unsafe
339    #[account(mut)]
340    pub merkle_tree: AccountInfo<'info>,
341
342    /// Honeycomb auth driver
343    pub authority: Signer<'info>,
344
345    /// CHECK: This is not dangerous
346    #[account(mut, seeds = [b"vault".as_ref(), crate::id().as_ref()], bump)]
347    pub vault: AccountInfo<'info>,
348
349    /// System Program
350    pub system_program: Program<'info, System>,
351
352    /// SPL account compression program.
353    pub compression_program: Program<'info, SplAccountCompression>,
354
355    /// SPL Noop program.
356    pub log_wrapper: Program<'info, Noop>,
357
358    /// NATIVE SYSVAR CLOCK
359    pub clock: Sysvar<'info, Clock>,
360
361    /// SYSVAR RENT ACCOUNT
362    pub rent_sysvar: Sysvar<'info, Rent>,
363
364    /// NATIVE INSTRUCTIONS SYSVAR
365    /// CHECK: This is not dangerous because we don't read or write from this account
366    #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)]
367    pub instructions_sysvar: AccountInfo<'info>,
368}
369
370#[derive(AnchorSerialize, AnchorDeserialize)]
371pub struct UpdateUserArgs {
372    pub root: [u8; 32],
373    pub leaf_idx: u32,
374    pub user_id: u64,
375    pub info: DataOrHash<UserInfoUpdates>,
376    pub wallets: DataOrHash<UserWalletsUpdates>,
377    pub social_info: DataOrHash<SocialInfoUpdates>,
378}
379
380pub fn update_user<'info>(
381    ctx: Context<'_, '_, '_, 'info, UpdateUser<'info>>,
382    args: UpdateUserArgs,
383) -> Result<()> {
384    let mut current_user_data = UserCompressed {
385        id: args.user_id,
386        info: [0; 32],
387        wallets: [0; 32],
388        social_info: [0; 32],
389        placeholder_1: PlaceHolder::None.to_node(),
390        placeholder_2: PlaceHolder::None.to_node(),
391        placeholder_3: PlaceHolder::None.to_node(),
392        placeholder_4: PlaceHolder::None.to_node(),
393    };
394
395    let mut new_user_data = UserCompressed {
396        id: args.user_id,
397        info: [0; 32],
398        wallets: [0; 32],
399        social_info: [0; 32],
400        placeholder_1: PlaceHolder::None.to_node(),
401        placeholder_2: PlaceHolder::None.to_node(),
402        placeholder_3: PlaceHolder::None.to_node(),
403        placeholder_4: PlaceHolder::None.to_node(),
404    };
405
406    let mut info_to_emit = None::<UserInfo>;
407    match args.info {
408        DataOrHash::Data(updates) => {
409            let mut info = UserInfo {
410                username: updates.current_username,
411                name: updates.current_name,
412                bio: updates.current_bio,
413                pfp: updates.current_pfp,
414            };
415            current_user_data.info = info.to_node();
416
417            if let Some(username) = &updates.new_username {
418                info.username = username.to_owned();
419            }
420
421            if let Some(name) = &updates.new_name {
422                info.name = name.to_owned();
423            }
424
425            if let Some(bio) = &updates.new_bio {
426                info.bio = bio.to_owned();
427            }
428
429            if let Some(pfp) = &updates.new_pfp {
430                info.pfp = pfp.to_owned();
431            }
432
433            new_user_data.info = info.to_node();
434            info_to_emit = Some(info);
435        }
436        DataOrHash::Hash(hash) => {
437            current_user_data.info = hash;
438            new_user_data.info = hash;
439        }
440    }
441
442    let mut wallet_changes = None::<UserWalletOutput>;
443    let mut wallets_to_emit = None::<Wallets>;
444    match args.wallets {
445        DataOrHash::Data(updates) => {
446            let mut wallets = updates.wallets.clone();
447            current_user_data.wallets = wallets.to_node();
448
449            wallet_changes = Some(wallets.apply_changes_and_return_effects(updates.changes));
450
451            new_user_data.wallets = wallets.to_node();
452            wallets_to_emit = Some(wallets);
453        }
454        DataOrHash::Hash(hash) => {
455            current_user_data.wallets = hash;
456            new_user_data.wallets = hash;
457        }
458    }
459
460    let mut social_info_to_emit = None::<SocialInfo>;
461    let mut remaining_account_offset: usize = 0;
462    match args.social_info {
463        DataOrHash::Data(updates) => {
464            let mut social_info = SocialInfo {
465                twitter: updates.current_twitter,
466                discord: updates.current_discord,
467                steam: updates.current_steam,
468                civic: updates.current_civic,
469            };
470            current_user_data.social_info = social_info.to_node();
471
472            if let Some(user_wallet_output) = wallet_changes {
473                social_info.civic = user_wallet_output.commit_to_civic_info(social_info.civic)
474            }
475            // social_info.civic = social_info.civic.into_iter().filter(|ci| ci.wallet_index)
476            if let Some(twitter) = &updates.new_twitter {
477                social_info.twitter = Some(twitter.clone());
478            }
479
480            if let Some(discord) = &updates.new_discord {
481                social_info.discord = Some(discord.clone());
482            }
483
484            if let Some(steam) = &updates.new_steam {
485                social_info.steam = Some(steam.clone());
486            }
487            let civic_found_set: HashSet<(CivicGateway, u8)> = if updates.new_civic.len() > 0 {
488                let mut set = HashSet::new();
489                for civic in &social_info.civic.as_vec() {
490                    set.insert((civic.gatekeeper_network.to_owned(), civic.wallet_index));
491                }
492                set
493            } else {
494                HashSet::new()
495            };
496            for civic_input in updates.new_civic {
497                let gateway_token = ctx.remaining_accounts.get(remaining_account_offset);
498                remaining_account_offset += 1;
499                if gateway_token.is_none() {
500                    return Err(HplHiveControlError::GatewayTokenNotProvided.into());
501                }
502                if !civic_found_set.contains(&(
503                    civic_input.gatekeeper_network.to_owned(),
504                    civic_input.wallet_index.to_owned(),
505                )) {
506                    let gateway_token = gateway_token.unwrap();
507                    social_info.civic.push(civic_input.to_civic_info_validated(
508                        gateway_token,
509                        &wallets_to_emit.as_ref().unwrap().wallets,
510                    ));
511                }
512            }
513
514            new_user_data.social_info = social_info.to_node();
515            social_info_to_emit = Some(social_info);
516        }
517
518        DataOrHash::Hash(hash) => {
519            if UserWalletOutput::require_to_update_civic(&wallet_changes) {
520                return Err(HplHiveControlError::SocialInfoRequiredWithWalletRemoval.into());
521            }
522            current_user_data.social_info = hash;
523            new_user_data.social_info = hash;
524        }
525    }
526
527    replace_leaf(
528        args.root,
529        current_user_data.to_node(),
530        new_user_data.to_node(),
531        args.leaf_idx,
532        &ctx.accounts.vault,
533        &ctx.accounts.merkle_tree,
534        &ctx.accounts.compression_program,
535        &ctx.accounts.log_wrapper,
536        ctx.remaining_accounts[remaining_account_offset..].to_vec(),
537        Some(&[&vault_seeds(&[ctx.bumps.vault])[..]]),
538    )?;
539
540    let mut seq_offset = 0;
541    if let Some(info) = info_to_emit {
542        info.emit(
543            ctx.accounts.clock.slot,
544            args.leaf_idx,
545            &ctx.accounts.merkle_tree,
546            &ctx.accounts.log_wrapper,
547            seq_offset,
548        )?;
549        seq_offset += 1;
550    }
551
552    if let Some(wallets) = wallets_to_emit {
553        wallets.emit(
554            ctx.accounts.clock.slot,
555            args.leaf_idx,
556            &ctx.accounts.merkle_tree,
557            &ctx.accounts.log_wrapper,
558            seq_offset,
559        )?;
560        seq_offset += 1;
561    }
562
563    if let Some(social_info) = social_info_to_emit {
564        social_info.emit(
565            ctx.accounts.clock.slot,
566            args.leaf_idx,
567            &ctx.accounts.merkle_tree,
568            &ctx.accounts.log_wrapper,
569            seq_offset,
570        )?;
571    }
572
573    Ok(())
574}
575#[derive(AnchorSerialize, AnchorDeserialize)]
576pub struct UpdateUserBrutallyArgs {
577    pub root: [u8; 32],
578    pub leaf_idx: u32,
579    pub previous_leaf: [u8; 32],
580    pub id: u64,
581    pub info: UserInfo,
582    pub wallets: Wallets,
583    pub social_info: SocialInfo,
584}
585
586pub fn update_user_brutally<'info>(
587    ctx: Context<'_, '_, '_, 'info, UpdateUser<'info>>,
588    args: UpdateUserBrutallyArgs,
589) -> Result<()> {
590    replace_leaf(
591        args.root,
592        args.previous_leaf.to_node(),
593        UserCompressed {
594            id: args.id,
595            info: [0; 32],
596            wallets: [0; 32],
597            social_info: [0; 32],
598            placeholder_1: PlaceHolder::None.to_node(),
599            placeholder_2: PlaceHolder::None.to_node(),
600            placeholder_3: PlaceHolder::None.to_node(),
601            placeholder_4: PlaceHolder::None.to_node(),
602        }
603        .to_node(),
604        args.leaf_idx,
605        &ctx.accounts.vault,
606        &ctx.accounts.merkle_tree,
607        &ctx.accounts.compression_program,
608        &ctx.accounts.log_wrapper,
609        ctx.remaining_accounts.to_vec(),
610        Some(&[&vault_seeds(&[ctx.bumps.vault])[..]]),
611    )?;
612
613    args.info.emit(
614        ctx.accounts.clock.slot,
615        args.leaf_idx,
616        &ctx.accounts.merkle_tree,
617        &ctx.accounts.log_wrapper,
618        0,
619    )?;
620
621    args.wallets.emit(
622        ctx.accounts.clock.slot,
623        args.leaf_idx,
624        &ctx.accounts.merkle_tree,
625        &ctx.accounts.log_wrapper,
626        1,
627    )?;
628
629    args.social_info.emit(
630        ctx.accounts.clock.slot,
631        args.leaf_idx,
632        &ctx.accounts.merkle_tree,
633        &ctx.accounts.log_wrapper,
634        2,
635    )?;
636
637    Ok(())
638}