yellowstone_shield_cli/command/
policy.rs

1use borsh::BorshDeserialize;
2use log::info;
3use solana_sdk::{
4    commitment_config::CommitmentConfig,
5    pubkey::Pubkey,
6    signature::{Keypair, Signer},
7};
8use spl_associated_token_account::get_associated_token_address_with_program_id;
9use spl_pod::optional_keys::OptionalNonZeroPubkey;
10use spl_token_2022::{
11    extension::{BaseStateWithExtensions, ExtensionType, PodStateWithExtensions},
12    pod::PodMint,
13    state::Mint,
14};
15use spl_token_metadata_interface::{
16    borsh::BorshDeserialize as MetadataInterfaceBorshDeserialize, state::TokenMetadata,
17};
18use yellowstone_shield_client::{
19    accounts::{Policy, PolicyV2},
20    instructions::{ClosePolicyBuilder, CreatePolicyBuilder},
21    types::{Kind, PermissionStrategy},
22    CreateAccountBuilder, CreateAsscoiatedTokenAccountBuilder, InitializeMetadataBuilder,
23    InitializeMint2Builder, MetadataPointerInitializeBuilder, PolicyTrait,
24    TokenExtensionsMintToBuilder, TransactionBuilder,
25};
26
27use super::{RunCommand, RunResult};
28use crate::command::{CommandComplete, CommandContext, SolanaAccount};
29
30pub enum PolicyVersion {
31    V1(Policy),
32    V2(PolicyV2),
33}
34
35impl PolicyVersion {
36    pub fn strategy(&self) -> u8 {
37        match self {
38            PolicyVersion::V1(pv1) => pv1.strategy,
39            PolicyVersion::V2(pv2) => pv2.strategy,
40        }
41    }
42}
43
44/// Builder for creating a new policy
45pub struct CreateCommandBuilder {
46    strategy: Option<PermissionStrategy>,
47    name: Option<String>,
48    symbol: Option<String>,
49    uri: Option<String>,
50}
51
52impl Default for CreateCommandBuilder {
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58impl CreateCommandBuilder {
59    /// Create a new PolicyBuilder
60    pub fn new() -> Self {
61        Self {
62            strategy: None,
63            name: None,
64            symbol: None,
65            uri: None,
66        }
67    }
68
69    /// Set the strategy for the policy
70    pub fn strategy(mut self, strategy: PermissionStrategy) -> Self {
71        self.strategy = Some(strategy);
72        self
73    }
74
75    /// Set the name for the token metadata
76    pub fn name(mut self, name: String) -> Self {
77        self.name = Some(name);
78        self
79    }
80
81    /// Set the symbol for the token metadata
82    pub fn symbol(mut self, symbol: String) -> Self {
83        self.symbol = Some(symbol);
84        self
85    }
86
87    /// Set the URI for the token metadata
88    pub fn uri(mut self, uri: String) -> Self {
89        self.uri = Some(uri);
90        self
91    }
92}
93
94#[async_trait::async_trait]
95impl RunCommand for CreateCommandBuilder {
96    /// Execute the creation of the policy
97    async fn run(&mut self, context: CommandContext) -> RunResult {
98        let CommandContext { keypair, client } = context;
99
100        // Given a PDA derived from the payer's public key.
101        let mint = Keypair::new();
102        // Create a token account for the payer.
103        let payer_token_account = get_associated_token_address_with_program_id(
104            &keypair.pubkey(),
105            &mint.pubkey(),
106            &spl_token_2022::ID,
107        );
108
109        // Calculate the space required for the mint account with extensions.
110        let mint_size =
111            ExtensionType::try_calculate_account_len::<Mint>(&[ExtensionType::MetadataPointer])
112                .unwrap();
113
114        let token_metadata = TokenMetadata {
115            update_authority: OptionalNonZeroPubkey::try_from(Some(keypair.pubkey())).unwrap(),
116            mint: mint.pubkey(),
117            name: self.name.clone().expect("name must be set"),
118            symbol: self.symbol.clone().expect("symbol must be set"),
119            uri: self.uri.clone().expect("uri must be set"),
120            additional_metadata: Vec::<(String, String)>::new(),
121        };
122
123        let rent = mint_size + token_metadata.tlv_size_of().unwrap();
124
125        let create_mint_ix = CreateAccountBuilder::build()
126            .payer(&keypair.pubkey())
127            .account(&mint.pubkey())
128            .space(mint_size)
129            .rent(rent)
130            .owner(&spl_token_2022::id())
131            .instruction();
132
133        // Initialize metadata pointer extension.
134        let init_metadata_pointer_ix = MetadataPointerInitializeBuilder::build()
135            .mint(&mint.pubkey())
136            .metadata(mint.pubkey())
137            .authority(keypair.pubkey())
138            .instruction();
139
140        let init_mint_ix = InitializeMint2Builder::build()
141            .mint(&mint.pubkey())
142            .mint_authority(&keypair.pubkey())
143            .instruction();
144
145        let init_metadata_ix = InitializeMetadataBuilder::new()
146            .mint(&mint.pubkey())
147            .owner(&keypair.pubkey())
148            .update_authority(&keypair.pubkey())
149            .mint_authority(&keypair.pubkey())
150            .name(token_metadata.name)
151            .symbol(token_metadata.symbol)
152            .uri(token_metadata.uri)
153            .instruction();
154
155        // Create the policy account.
156        // PDA seeds are same for both Policy and PolicyV2
157        let (address, _) = Policy::find_pda(&mint.pubkey());
158        let create_policy_ix = CreatePolicyBuilder::new()
159            .policy(address)
160            .mint(mint.pubkey())
161            .payer(keypair.pubkey())
162            .owner(keypair.pubkey())
163            .token_account(payer_token_account)
164            .strategy(self.strategy.expect("strategy must be set"))
165            .instruction();
166
167        // Initialize the payer's token account.
168        let init_payer_token_account_ix = CreateAsscoiatedTokenAccountBuilder::build()
169            .owner(&keypair.pubkey())
170            .mint(&mint.pubkey())
171            .payer(&keypair.pubkey())
172            .instruction();
173
174        // Mint 1 token to the payer's token account.
175        let mint_to_payer_ix = TokenExtensionsMintToBuilder::build()
176            .mint(&mint.pubkey())
177            .account(&payer_token_account)
178            .owner(&keypair.pubkey())
179            .amount(1)
180            .instruction();
181
182        let last_blockhash = client.get_latest_blockhash().await?;
183
184        let tx = TransactionBuilder::build()
185            .instruction(create_mint_ix)
186            .instruction(init_metadata_pointer_ix)
187            .instruction(init_mint_ix)
188            .instruction(init_metadata_ix)
189            .instruction(init_payer_token_account_ix)
190            .instruction(mint_to_payer_ix)
191            .instruction(create_policy_ix)
192            .signer(&keypair)
193            .signer(&mint)
194            .payer(&keypair.pubkey())
195            .recent_blockhash(last_blockhash)
196            .transaction();
197
198        client
199            .send_and_confirm_transaction_with_spinner_and_commitment(
200                &tx,
201                CommitmentConfig::confirmed(),
202            )
203            .await?;
204
205        let account_data = client.get_account(&address).await?;
206        let account_data: &[u8] = &account_data.data;
207
208        let policy_version = Kind::try_from_slice(&[account_data[0]])?;
209
210        let policy = match policy_version {
211            Kind::Policy => PolicyVersion::V1(Policy::from_bytes(&account_data[..Policy::LEN])?),
212            Kind::PolicyV2 => {
213                PolicyVersion::V2(PolicyV2::from_bytes(&account_data[..PolicyV2::LEN])?)
214            }
215        };
216
217        let mint_data = client.get_account(&mint.pubkey()).await?;
218        let account_data: &[u8] = &mint_data.data;
219
220        let mint_pod = PodStateWithExtensions::<PodMint>::unpack(account_data).unwrap();
221        let mint_bytes = mint_pod.get_extension_bytes::<TokenMetadata>().unwrap();
222        let token_metadata = TokenMetadata::try_from_slice(mint_bytes).unwrap();
223
224        Ok(CommandComplete(
225            SolanaAccount(mint.pubkey(), Some(token_metadata)),
226            SolanaAccount(address, Some(policy)),
227        ))
228    }
229}
230
231/// Builder for deleting a policy
232pub struct DeleteCommandBuilder<'a> {
233    mint: Option<&'a Pubkey>,
234}
235
236impl Default for DeleteCommandBuilder<'_> {
237    fn default() -> Self {
238        Self::new()
239    }
240}
241
242impl<'a> DeleteCommandBuilder<'a> {
243    /// Create a new DeleteCommandBuilder
244    pub fn new() -> Self {
245        Self { mint: None }
246    }
247
248    /// Set the mint address
249    pub fn mint(mut self, mint: &'a Pubkey) -> Self {
250        self.mint = Some(mint);
251        self
252    }
253}
254
255#[async_trait::async_trait]
256impl RunCommand for DeleteCommandBuilder<'_> {
257    /// Execute the deletion of a policy
258    async fn run(&mut self, context: CommandContext) -> RunResult {
259        let CommandContext { keypair, client } = context;
260
261        let mint = self.mint.expect("mint must be set");
262        // PDA seeds are same for both Policy and PolicyV2
263        let (address, _) = Policy::find_pda(mint);
264        let payer_token_account = get_associated_token_address_with_program_id(
265            &keypair.pubkey(),
266            mint,
267            &spl_token_2022::ID,
268        );
269
270        let close_policy = ClosePolicyBuilder::new()
271            .policy(address)
272            .mint(*mint)
273            .payer(keypair.pubkey())
274            .owner(keypair.pubkey())
275            .token_account(payer_token_account)
276            .instruction();
277
278        let last_blockhash = client.get_latest_blockhash().await?;
279
280        let tx = TransactionBuilder::build()
281            .instruction(close_policy)
282            .signer(&keypair)
283            .payer(&keypair.pubkey())
284            .recent_blockhash(last_blockhash)
285            .transaction();
286
287        let signature = client
288            .send_and_confirm_transaction_with_spinner_and_commitment(
289                &tx,
290                CommitmentConfig::confirmed(),
291            )
292            .await?;
293
294        info!("Transaction signature: {}", signature);
295
296        Ok(CommandComplete(
297            SolanaAccount(*mint, None),
298            SolanaAccount(address, None),
299        ))
300    }
301}
302
303pub struct ShowCommandBuilder<'a> {
304    mint: Option<&'a Pubkey>,
305}
306
307impl Default for ShowCommandBuilder<'_> {
308    fn default() -> Self {
309        Self::new()
310    }
311}
312
313impl<'a> ShowCommandBuilder<'a> {
314    pub fn new() -> Self {
315        Self { mint: None }
316    }
317
318    pub fn mint(mut self, mint: &'a Pubkey) -> Self {
319        self.mint = Some(mint);
320        self
321    }
322}
323
324#[async_trait::async_trait]
325impl RunCommand for ShowCommandBuilder<'_> {
326    async fn run(&mut self, context: CommandContext) -> RunResult {
327        let CommandContext { keypair: _, client } = context;
328
329        let mint = self.mint.expect("mint must be set");
330        // PDA seeds are same for both Policy and PolicyV2
331        let (address, _) = Policy::find_pda(mint);
332
333        let account_data = client.get_account(&address).await?;
334        let account_data: &[u8] = &account_data.data;
335
336        let policy_version = Kind::try_from_slice(&[account_data[0]])?;
337
338        let policy = match policy_version {
339            Kind::Policy => PolicyVersion::V1(Policy::from_bytes(account_data)?),
340            Kind::PolicyV2 => PolicyVersion::V2(PolicyV2::from_bytes(account_data)?),
341        };
342
343        let identities = match policy_version {
344            Kind::Policy => Policy::try_deserialize_identities(account_data)?,
345            Kind::PolicyV2 => PolicyV2::try_deserialize_identities(account_data)?,
346        };
347
348        let mint_data = client.get_account(mint).await?;
349        let account_data: &[u8] = &mint_data.data;
350
351        let mint_pod = PodStateWithExtensions::<PodMint>::unpack(account_data).unwrap();
352        let mint_bytes = mint_pod.get_extension_bytes::<TokenMetadata>().unwrap();
353        let token_metadata = TokenMetadata::try_from_slice(mint_bytes).unwrap();
354
355        info!("Identities in policy:");
356        for (i, identity) in identities.iter().enumerate() {
357            info!("  {}. {}", i, identity);
358        }
359
360        Ok(CommandComplete(
361            SolanaAccount(*mint, Some(token_metadata)),
362            SolanaAccount(address, Some(policy)),
363        ))
364    }
365}