Skip to main content

aeko_rust_sdk/
builders.rs

1use borsh::{to_vec, BorshDeserialize, BorshSerialize};
2
3pub type PubkeyString = String;
4
5#[derive(Clone, Debug, PartialEq, Eq)]
6pub struct AccountMetaPlan {
7    pub pubkey: PubkeyString,
8    pub is_signer: bool,
9    pub is_writable: bool,
10}
11
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct InstructionPlan {
14    pub program_id: PubkeyString,
15    pub accounts: Vec<AccountMetaPlan>,
16    pub data: Vec<u8>,
17}
18
19impl InstructionPlan {
20    pub fn data_base64(&self) -> String {
21        use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
22        BASE64.encode(&self.data)
23    }
24}
25
26#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct TransactionPlan {
28    pub payer: PubkeyString,
29    pub recent_blockhash: String,
30    pub instructions: Vec<InstructionPlan>,
31}
32
33#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
34pub struct MetadataAttribute {
35    pub trait_type: String,
36    pub value: String,
37}
38
39#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
40pub struct NftMetadata {
41    pub name: String,
42    pub description: Option<String>,
43    pub uri: String,
44    pub image_uri: Option<String>,
45    pub attributes: Vec<MetadataAttribute>,
46}
47
48#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
49pub struct Aeko721Collection {
50    pub authority: PubkeyString,
51    pub name: String,
52    pub symbol: String,
53    pub base_uri: Option<String>,
54    pub total_minted: u64,
55    pub is_initialized: bool,
56}
57
58#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
59pub struct Aeko721Token {
60    pub collection: PubkeyString,
61    pub token_id: u64,
62    pub owner: PubkeyString,
63    pub creator: PubkeyString,
64    pub royalty_bps: u16,
65    pub metadata: NftMetadata,
66    pub frozen: bool,
67    pub is_initialized: bool,
68}
69
70#[derive(Clone, Copy, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
71pub enum PermissionRole {
72    Owner,
73    Spender,
74    Viewer,
75}
76
77#[derive(Clone, Copy, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
78pub enum PermissionStatus {
79    Active,
80    Revoked,
81    Expired,
82    Frozen,
83}
84
85#[derive(Clone, Copy, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
86pub enum ProgramPolicyMode {
87    DenyByDefault,
88    AllowByDefault,
89}
90
91#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
92pub struct TokenSpendCap {
93    pub mint: PubkeyString,
94    pub max_single_tx: Option<u64>,
95    pub max_daily: Option<u64>,
96}
97
98#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
99pub struct SpendLimitPolicy {
100    pub max_single_tx_aeko: Option<u64>,
101    pub max_daily_aeko: Option<u64>,
102    pub token_caps: Vec<TokenSpendCap>,
103}
104
105#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
106pub struct DelegatePermission {
107    pub delegate: PubkeyString,
108    pub role: PermissionRole,
109    pub label: Option<String>,
110    pub status: PermissionStatus,
111    pub valid_from_epoch: u64,
112    pub valid_until_epoch: Option<u64>,
113    pub spend_limit: SpendLimitPolicy,
114    pub program_allowlist: Vec<PubkeyString>,
115    pub token_allowlist: Vec<PubkeyString>,
116    pub app_scope_hashes: Vec<[u8; 32]>,
117    pub requires_reauth: bool,
118    pub last_used_epoch: Option<u64>,
119    pub last_used_slot: Option<u64>,
120}
121
122#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
123pub struct TokenSpendCounter {
124    pub mint: PubkeyString,
125    pub amount: u64,
126}
127
128#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
129pub struct DelegateUsageWindow {
130    pub delegate: PubkeyString,
131    pub day_index: u64,
132    pub aeko_spent_today: u64,
133    pub token_spent_today: Vec<TokenSpendCounter>,
134}
135
136#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
137pub struct AuditEventSummary {
138    pub role: Option<PermissionRole>,
139    pub status: Option<PermissionStatus>,
140    pub affected_programs: Vec<PubkeyString>,
141    pub affected_mints: Vec<PubkeyString>,
142    pub valid_until_epoch: Option<u64>,
143    pub amount_hint: Option<u64>,
144}
145
146#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
147pub struct WalletPermissionAuditLogEntry {
148    pub wallet: PubkeyString,
149    pub sequence: u64,
150    pub actor: PubkeyString,
151    pub target_delegate: Option<PubkeyString>,
152    pub event_type: u8,
153    pub event_summary: AuditEventSummary,
154    pub created_at_epoch: u64,
155    pub created_at_slot: u64,
156}
157
158#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
159pub struct WalletPermissionAuditLogAccount {
160    pub wallet: PubkeyString,
161    pub next_sequence: u64,
162    pub entries: Vec<WalletPermissionAuditLogEntry>,
163    pub is_initialized: bool,
164}
165
166#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
167pub struct WalletPermissionAccount {
168    pub wallet: PubkeyString,
169    pub did: String,
170    pub version: u8,
171    pub policy_nonce: u64,
172    pub is_frozen: bool,
173    pub freeze_reason_code: Option<u16>,
174    pub reauth_required_until_epoch: Option<u64>,
175    pub owner: PubkeyString,
176    pub delegates: Vec<DelegatePermission>,
177    pub usage_windows: Vec<DelegateUsageWindow>,
178    pub default_program_policy: ProgramPolicyMode,
179    pub created_at_epoch: u64,
180    pub updated_at_epoch: u64,
181    pub is_initialized: bool,
182}
183
184#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
185enum Token721Instruction {
186    InitializeCollection {
187        name: String,
188        symbol: String,
189        base_uri: Option<String>,
190    },
191    MintNft {
192        token_id: u64,
193        owner: PubkeyString,
194        creator: PubkeyString,
195        royalty_bps: u16,
196        metadata: NftMetadata,
197    },
198    FreezeNft,
199    ThawNft,
200    TransferNft {
201        new_owner: PubkeyString,
202    },
203    UpdateMetadata {
204        metadata: NftMetadata,
205    },
206}
207
208#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
209enum WalletPermissionsInstruction {
210    InitializePermissionAccount {
211        wallet: PubkeyString,
212        did: String,
213        current_epoch: u64,
214        default_program_policy: ProgramPolicyMode,
215    },
216    GrantDelegate {
217        delegate_permission: DelegatePermission,
218        current_epoch: u64,
219        current_slot: u64,
220    },
221    UpdateDelegate {
222        delegate: PubkeyString,
223        role: Option<PermissionRole>,
224        label: Option<Option<String>>,
225        valid_until_epoch: Option<Option<u64>>,
226        spend_limit: Option<SpendLimitPolicy>,
227        program_allowlist: Option<Vec<PubkeyString>>,
228        token_allowlist: Option<Vec<PubkeyString>>,
229        app_scope_hashes: Option<Vec<[u8; 32]>>,
230        requires_reauth: Option<bool>,
231        current_epoch: u64,
232        current_slot: u64,
233    },
234    RevokeDelegate {
235        delegate: PubkeyString,
236        current_epoch: u64,
237        current_slot: u64,
238    },
239    FreezeWallet {
240        reason_code: Option<u16>,
241        reauth_required_until_epoch: Option<u64>,
242        current_epoch: u64,
243        current_slot: u64,
244    },
245    UnfreezeWallet {
246        current_epoch: u64,
247        current_slot: u64,
248    },
249    RecordDelegateUsage {
250        delegate: PubkeyString,
251        target_program: Option<PubkeyString>,
252        mint: Option<PubkeyString>,
253        amount: u64,
254        day_index: u64,
255        current_epoch: u64,
256        current_slot: u64,
257    },
258    ReadEffectivePermissions {
259        delegate: PubkeyString,
260        current_epoch: u64,
261    },
262}
263
264#[derive(Clone, Debug)]
265pub struct InitializeCollectionInput {
266    pub program_id: PubkeyString,
267    pub collection: PubkeyString,
268    pub authority: PubkeyString,
269    pub name: String,
270    pub symbol: String,
271    pub base_uri: Option<String>,
272}
273
274#[derive(Clone, Debug)]
275pub struct MintNftInput {
276    pub program_id: PubkeyString,
277    pub collection: PubkeyString,
278    pub token: PubkeyString,
279    pub authority: PubkeyString,
280    pub token_id: u64,
281    pub owner: PubkeyString,
282    pub creator: PubkeyString,
283    pub royalty_bps: u16,
284    pub metadata: NftMetadata,
285}
286
287#[derive(Clone, Debug)]
288pub struct TransferNftInput {
289    pub program_id: PubkeyString,
290    pub token: PubkeyString,
291    pub owner: PubkeyString,
292    pub new_owner: PubkeyString,
293}
294
295#[derive(Clone, Debug)]
296pub struct UpdateMetadataInput {
297    pub program_id: PubkeyString,
298    pub token: PubkeyString,
299    pub authority: PubkeyString,
300    pub metadata: NftMetadata,
301}
302
303#[derive(Clone, Debug)]
304pub struct ToggleNftFreezeInput {
305    pub program_id: PubkeyString,
306    pub token: PubkeyString,
307    pub authority: PubkeyString,
308}
309
310#[derive(Clone, Debug)]
311pub struct InitializeWalletPermissionsInput {
312    pub program_id: PubkeyString,
313    pub permission_state: PubkeyString,
314    pub audit_log: PubkeyString,
315    pub owner: PubkeyString,
316    pub wallet: PubkeyString,
317    pub did: String,
318    pub current_epoch: u64,
319    pub default_program_policy: ProgramPolicyMode,
320}
321
322#[derive(Clone, Debug)]
323pub struct GrantDelegateInput {
324    pub program_id: PubkeyString,
325    pub permission_state: PubkeyString,
326    pub audit_log: PubkeyString,
327    pub owner: PubkeyString,
328    pub delegate_permission: DelegatePermission,
329    pub current_epoch: u64,
330    pub current_slot: u64,
331}
332
333#[derive(Clone, Debug)]
334pub struct UpdateDelegateInput {
335    pub program_id: PubkeyString,
336    pub permission_state: PubkeyString,
337    pub audit_log: PubkeyString,
338    pub owner: PubkeyString,
339    pub delegate: PubkeyString,
340    pub role: Option<PermissionRole>,
341    pub label: Option<Option<String>>,
342    pub valid_until_epoch: Option<Option<u64>>,
343    pub spend_limit: Option<SpendLimitPolicy>,
344    pub program_allowlist: Option<Vec<PubkeyString>>,
345    pub token_allowlist: Option<Vec<PubkeyString>>,
346    pub app_scope_hashes: Option<Vec<[u8; 32]>>,
347    pub requires_reauth: Option<bool>,
348    pub current_epoch: u64,
349    pub current_slot: u64,
350}
351
352#[derive(Clone, Debug)]
353pub struct RevokeDelegateInput {
354    pub program_id: PubkeyString,
355    pub permission_state: PubkeyString,
356    pub audit_log: PubkeyString,
357    pub owner: PubkeyString,
358    pub delegate: PubkeyString,
359    pub current_epoch: u64,
360    pub current_slot: u64,
361}
362
363#[derive(Clone, Debug)]
364pub struct FreezeWalletInput {
365    pub program_id: PubkeyString,
366    pub permission_state: PubkeyString,
367    pub audit_log: PubkeyString,
368    pub owner: PubkeyString,
369    pub reason_code: Option<u16>,
370    pub reauth_required_until_epoch: Option<u64>,
371    pub current_epoch: u64,
372    pub current_slot: u64,
373}
374
375#[derive(Clone, Debug)]
376pub struct UnfreezeWalletInput {
377    pub program_id: PubkeyString,
378    pub permission_state: PubkeyString,
379    pub audit_log: PubkeyString,
380    pub owner: PubkeyString,
381    pub current_epoch: u64,
382    pub current_slot: u64,
383}
384
385#[derive(Clone, Debug)]
386pub struct RecordDelegateUsageInput {
387    pub program_id: PubkeyString,
388    pub permission_state: PubkeyString,
389    pub audit_log: PubkeyString,
390    pub owner: PubkeyString,
391    pub delegate: PubkeyString,
392    pub target_program: Option<PubkeyString>,
393    pub mint: Option<PubkeyString>,
394    pub amount: u64,
395    pub day_index: u64,
396    pub current_epoch: u64,
397    pub current_slot: u64,
398}
399
400#[derive(Clone, Debug)]
401pub struct ReadEffectivePermissionsInput {
402    pub program_id: PubkeyString,
403    pub permission_state: PubkeyString,
404    pub delegate: PubkeyString,
405    pub current_epoch: u64,
406}
407
408pub fn default_token_721_program_id() -> PubkeyString {
409    bs58::encode([10u8; 32]).into_string()
410}
411
412pub fn default_wallet_permissions_program_id() -> PubkeyString {
413    bs58::encode([10u8; 32]).into_string()
414}
415
416pub fn build_initialize_collection_instruction(input: &InitializeCollectionInput) -> InstructionPlan {
417    instruction_plan(
418        input.program_id.clone(),
419        vec![
420            writable(&input.collection),
421            readonly_signer(&input.authority),
422        ],
423        Token721Instruction::InitializeCollection {
424            name: input.name.clone(),
425            symbol: input.symbol.clone(),
426            base_uri: input.base_uri.clone(),
427        },
428    )
429}
430
431pub fn build_mint_nft_instruction(input: &MintNftInput) -> InstructionPlan {
432    instruction_plan(
433        input.program_id.clone(),
434        vec![
435            writable(&input.collection),
436            writable(&input.token),
437            readonly_signer(&input.authority),
438        ],
439        Token721Instruction::MintNft {
440            token_id: input.token_id,
441            owner: input.owner.clone(),
442            creator: input.creator.clone(),
443            royalty_bps: input.royalty_bps,
444            metadata: input.metadata.clone(),
445        },
446    )
447}
448
449pub fn build_transfer_nft_instruction(input: &TransferNftInput) -> InstructionPlan {
450    instruction_plan(
451        input.program_id.clone(),
452        vec![writable(&input.token), readonly_signer(&input.owner)],
453        Token721Instruction::TransferNft {
454            new_owner: input.new_owner.clone(),
455        },
456    )
457}
458
459pub fn build_update_metadata_instruction(input: &UpdateMetadataInput) -> InstructionPlan {
460    instruction_plan(
461        input.program_id.clone(),
462        vec![writable(&input.token), readonly_signer(&input.authority)],
463        Token721Instruction::UpdateMetadata {
464            metadata: input.metadata.clone(),
465        },
466    )
467}
468
469pub fn build_freeze_nft_instruction(input: &ToggleNftFreezeInput) -> InstructionPlan {
470    instruction_plan(
471        input.program_id.clone(),
472        vec![writable(&input.token), readonly_signer(&input.authority)],
473        Token721Instruction::FreezeNft,
474    )
475}
476
477pub fn build_thaw_nft_instruction(input: &ToggleNftFreezeInput) -> InstructionPlan {
478    instruction_plan(
479        input.program_id.clone(),
480        vec![writable(&input.token), readonly_signer(&input.authority)],
481        Token721Instruction::ThawNft,
482    )
483}
484
485pub fn build_initialize_wallet_permissions_instruction(
486    input: &InitializeWalletPermissionsInput,
487) -> InstructionPlan {
488    instruction_plan(
489        input.program_id.clone(),
490        vec![
491            writable(&input.permission_state),
492            writable(&input.audit_log),
493            readonly_signer(&input.owner),
494        ],
495        WalletPermissionsInstruction::InitializePermissionAccount {
496            wallet: input.wallet.clone(),
497            did: input.did.clone(),
498            current_epoch: input.current_epoch,
499            default_program_policy: input.default_program_policy,
500        },
501    )
502}
503
504pub fn build_grant_delegate_instruction(input: &GrantDelegateInput) -> InstructionPlan {
505    instruction_plan(
506        input.program_id.clone(),
507        vec![
508            writable(&input.permission_state),
509            writable(&input.audit_log),
510            readonly_signer(&input.owner),
511        ],
512        WalletPermissionsInstruction::GrantDelegate {
513            delegate_permission: input.delegate_permission.clone(),
514            current_epoch: input.current_epoch,
515            current_slot: input.current_slot,
516        },
517    )
518}
519
520pub fn build_update_delegate_instruction(input: &UpdateDelegateInput) -> InstructionPlan {
521    instruction_plan(
522        input.program_id.clone(),
523        vec![
524            writable(&input.permission_state),
525            writable(&input.audit_log),
526            readonly_signer(&input.owner),
527        ],
528        WalletPermissionsInstruction::UpdateDelegate {
529            delegate: input.delegate.clone(),
530            role: input.role,
531            label: input.label.clone(),
532            valid_until_epoch: input.valid_until_epoch,
533            spend_limit: input.spend_limit.clone(),
534            program_allowlist: input.program_allowlist.clone(),
535            token_allowlist: input.token_allowlist.clone(),
536            app_scope_hashes: input.app_scope_hashes.clone(),
537            requires_reauth: input.requires_reauth,
538            current_epoch: input.current_epoch,
539            current_slot: input.current_slot,
540        },
541    )
542}
543
544pub fn build_revoke_delegate_instruction(input: &RevokeDelegateInput) -> InstructionPlan {
545    instruction_plan(
546        input.program_id.clone(),
547        vec![
548            writable(&input.permission_state),
549            writable(&input.audit_log),
550            readonly_signer(&input.owner),
551        ],
552        WalletPermissionsInstruction::RevokeDelegate {
553            delegate: input.delegate.clone(),
554            current_epoch: input.current_epoch,
555            current_slot: input.current_slot,
556        },
557    )
558}
559
560pub fn build_freeze_wallet_instruction(input: &FreezeWalletInput) -> InstructionPlan {
561    instruction_plan(
562        input.program_id.clone(),
563        vec![
564            writable(&input.permission_state),
565            writable(&input.audit_log),
566            readonly_signer(&input.owner),
567        ],
568        WalletPermissionsInstruction::FreezeWallet {
569            reason_code: input.reason_code,
570            reauth_required_until_epoch: input.reauth_required_until_epoch,
571            current_epoch: input.current_epoch,
572            current_slot: input.current_slot,
573        },
574    )
575}
576
577pub fn build_unfreeze_wallet_instruction(input: &UnfreezeWalletInput) -> InstructionPlan {
578    instruction_plan(
579        input.program_id.clone(),
580        vec![
581            writable(&input.permission_state),
582            writable(&input.audit_log),
583            readonly_signer(&input.owner),
584        ],
585        WalletPermissionsInstruction::UnfreezeWallet {
586            current_epoch: input.current_epoch,
587            current_slot: input.current_slot,
588        },
589    )
590}
591
592pub fn build_record_delegate_usage_instruction(input: &RecordDelegateUsageInput) -> InstructionPlan {
593    instruction_plan(
594        input.program_id.clone(),
595        vec![
596            writable(&input.permission_state),
597            writable(&input.audit_log),
598            readonly_signer(&input.owner),
599        ],
600        WalletPermissionsInstruction::RecordDelegateUsage {
601            delegate: input.delegate.clone(),
602            target_program: input.target_program.clone(),
603            mint: input.mint.clone(),
604            amount: input.amount,
605            day_index: input.day_index,
606            current_epoch: input.current_epoch,
607            current_slot: input.current_slot,
608        },
609    )
610}
611
612pub fn build_read_effective_permissions_instruction(
613    input: &ReadEffectivePermissionsInput,
614) -> InstructionPlan {
615    instruction_plan(
616        input.program_id.clone(),
617        vec![readonly(&input.permission_state)],
618        WalletPermissionsInstruction::ReadEffectivePermissions {
619            delegate: input.delegate.clone(),
620            current_epoch: input.current_epoch,
621        },
622    )
623}
624
625pub fn build_transaction_plan(
626    instructions: Vec<InstructionPlan>,
627    payer: impl Into<String>,
628    recent_blockhash: impl Into<String>,
629) -> TransactionPlan {
630    TransactionPlan {
631        payer: payer.into(),
632        recent_blockhash: recent_blockhash.into(),
633        instructions,
634    }
635}
636
637fn instruction_plan<T: BorshSerialize>(
638    program_id: String,
639    accounts: Vec<AccountMetaPlan>,
640    payload: T,
641) -> InstructionPlan {
642    InstructionPlan {
643        program_id,
644        accounts,
645        data: to_vec(&payload).expect("borsh serialization should succeed"),
646    }
647}
648
649fn readonly(pubkey: &str) -> AccountMetaPlan {
650    AccountMetaPlan {
651        pubkey: pubkey.to_string(),
652        is_signer: false,
653        is_writable: false,
654    }
655}
656
657fn writable(pubkey: &str) -> AccountMetaPlan {
658    AccountMetaPlan {
659        pubkey: pubkey.to_string(),
660        is_signer: false,
661        is_writable: true,
662    }
663}
664
665fn readonly_signer(pubkey: &str) -> AccountMetaPlan {
666    AccountMetaPlan {
667        pubkey: pubkey.to_string(),
668        is_signer: true,
669        is_writable: false,
670    }
671}
672
673#[cfg(test)]
674mod tests {
675    use super::*;
676
677    fn fake_pubkey(seed: u8) -> String {
678        bs58::encode([seed; 32]).into_string()
679    }
680
681    #[test]
682    fn builds_initialize_collection_instruction() {
683        let instruction = build_initialize_collection_instruction(&InitializeCollectionInput {
684            program_id: default_token_721_program_id(),
685            collection: fake_pubkey(1),
686            authority: fake_pubkey(2),
687            name: "AEKO Demo".to_string(),
688            symbol: "ADMO".to_string(),
689            base_uri: Some("https://example.aeko".to_string()),
690        });
691
692        assert_eq!(instruction.accounts.len(), 2);
693        assert_eq!(instruction.accounts[0].is_writable, true);
694        assert!(!instruction.data.is_empty());
695    }
696
697    #[test]
698    fn builds_wallet_permission_transaction_plan() {
699        let instruction = build_unfreeze_wallet_instruction(&UnfreezeWalletInput {
700            program_id: default_wallet_permissions_program_id(),
701            permission_state: fake_pubkey(3),
702            audit_log: fake_pubkey(4),
703            owner: fake_pubkey(5),
704            current_epoch: 11,
705            current_slot: 99,
706        });
707
708        let plan = build_transaction_plan(vec![instruction], fake_pubkey(5), "blockhash");
709        assert_eq!(plan.instructions.len(), 1);
710        assert_eq!(plan.payer, fake_pubkey(5));
711    }
712}