yellowstone_shield_cli/command/
policy.rs

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