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}