Jelajahi Sumber

:sparkles: Adding DestroyComponent function (#143)

Danilo Guanabara 7 bulan lalu
induk
melakukan
e7cffca975

+ 2 - 0
Cargo.lock

@@ -1092,6 +1092,7 @@ version = "0.2.1"
 dependencies = [
  "ahash 0.8.11",
  "anchor-lang",
+ "bincode",
  "bolt-attribute-bolt-arguments 0.2.1",
  "bolt-attribute-bolt-component 0.2.1",
  "bolt-attribute-bolt-component-deserialize 0.2.1",
@@ -1106,6 +1107,7 @@ dependencies = [
  "serde",
  "serde_json",
  "session-keys",
+ "solana-program",
  "world 0.2.1",
 ]
 

+ 1 - 0
Cargo.toml

@@ -56,6 +56,7 @@ heck = "0.5.0"
 clap = { version = "4.2.4", features = ["derive"] }
 ahash = "=0.8.11"
 ephemeral-rollups-sdk = "=0.2.1"
+bincode = "=1.3.3"
 
 [profile.release]
 overflow-checks = true

+ 46 - 0
clients/bolt-sdk/src/generated/idl/world.json

@@ -190,6 +190,52 @@
       ],
       "args": []
     },
+    {
+      "name": "destroy_component",
+      "discriminator": [
+        40,
+        197,
+        69,
+        196,
+        67,
+        95,
+        219,
+        73
+      ],
+      "accounts": [
+        {
+          "name": "authority",
+          "writable": true,
+          "signer": true
+        },
+        {
+          "name": "receiver",
+          "writable": true
+        },
+        {
+          "name": "component_program"
+        },
+        {
+          "name": "component_program_data"
+        },
+        {
+          "name": "entity"
+        },
+        {
+          "name": "component",
+          "writable": true
+        },
+        {
+          "name": "instruction_sysvar_account",
+          "address": "Sysvar1nstructions1111111111111111111111111"
+        },
+        {
+          "name": "system_program",
+          "address": "11111111111111111111111111111111"
+        }
+      ],
+      "args": []
+    },
     {
       "name": "initialize_component",
       "discriminator": [

+ 37 - 0
clients/bolt-sdk/src/generated/types/world.ts

@@ -151,6 +151,43 @@ export type World = {
       ];
       args: [];
     },
+    {
+      name: "destroyComponent";
+      discriminator: [40, 197, 69, 196, 67, 95, 219, 73];
+      accounts: [
+        {
+          name: "authority";
+          writable: true;
+          signer: true;
+        },
+        {
+          name: "receiver";
+          writable: true;
+        },
+        {
+          name: "componentProgram";
+        },
+        {
+          name: "componentProgramData";
+        },
+        {
+          name: "entity";
+        },
+        {
+          name: "component";
+          writable: true;
+        },
+        {
+          name: "instructionSysvarAccount";
+          address: "Sysvar1nstructions1111111111111111111111111";
+        },
+        {
+          name: "systemProgram";
+          address: "11111111111111111111111111111111";
+        },
+      ];
+      args: [];
+    },
     {
       name: "initializeComponent";
       discriminator: [36, 143, 233, 113, 12, 234, 61, 30];

+ 11 - 0
clients/bolt-sdk/src/index.ts

@@ -88,6 +88,17 @@ export function FindSessionTokenPda({
   )[0];
 }
 
+export function FindComponentProgramDataPda({
+  programId,
+}: {
+  programId: PublicKey;
+}) {
+  return PublicKey.findProgramAddressSync(
+    [programId.toBuffer()],
+    new PublicKey("BPFLoaderUpgradeab1e11111111111111111111111"),
+  )[0];
+}
+
 // TODO: seed must be Uint8Array like the other FindPda functions
 export function FindComponentPda({
   componentId,

+ 51 - 0
clients/bolt-sdk/src/world/transactions.ts

@@ -16,6 +16,7 @@ import {
   FindSessionTokenPda,
   WORLD_PROGRAM_ID,
   BN,
+  FindComponentProgramDataPda,
 } from "../index";
 import type web3 from "@solana/web3.js";
 import {
@@ -329,6 +330,56 @@ export async function AddEntity({
   };
 }
 
+/**
+ * Create the transaction to Destroy a component
+ * @param authority
+ * @param component
+ * @param world
+ * @param connection
+ * @constructor
+ */
+export async function DestroyComponent({
+  authority,
+  entity,
+  componentId,
+  receiver,
+  seed,
+}: {
+  authority: PublicKey;
+  entity: PublicKey;
+  componentId: PublicKey;
+  receiver: PublicKey;
+  seed?: string;
+}): Promise<{
+  instruction: TransactionInstruction;
+  transaction: Transaction;
+}> {
+  const program = new Program(
+    worldIdl as Idl,
+  ) as unknown as Program<WorldProgram>;
+  const componentProgramData = FindComponentProgramDataPda({
+    programId: componentId,
+  });
+  const componentProgram = componentId;
+  const component = FindComponentPda({ componentId, entity, seed });
+  const instruction = await program.methods
+    .destroyComponent()
+    .accounts({
+      authority,
+      component,
+      entity,
+      componentProgram,
+      componentProgramData,
+      receiver,
+    })
+    .instruction();
+  const transaction = new Transaction().add(instruction);
+  return {
+    instruction,
+    transaction,
+  };
+}
+
 /**
  * Create the transaction to Initialize a new component
  * @param payer

+ 5 - 3
crates/bolt-lang/Cargo.toml

@@ -37,6 +37,8 @@ session-keys = { workspace = true }
 ephemeral-rollups-sdk = { workspace = true, features = ["anchor"]}
 
 # Other dependencies
-serde = { workspace = true }
-serde_json = {workspace = true }
-ahash = { workspace = true }
+serde.workspace = true
+serde_json.workspace = true
+ahash.workspace = true
+solana-program.workspace = true
+bincode.workspace = true

+ 63 - 0
crates/bolt-lang/attribute/bolt-program/src/lib.rs

@@ -43,6 +43,7 @@ pub fn bolt_program(args: TokenStream, input: TokenStream) -> TokenStream {
 /// Modifies the component module and adds the necessary functions and structs.
 fn modify_component_module(mut module: ItemMod, component_type: &Type) -> ItemMod {
     let (initialize_fn, initialize_struct) = generate_initialize(component_type);
+    let (destroy_fn, destroy_struct) = generate_destroy(component_type);
     //let (apply_fn, apply_struct, apply_impl, update_fn, update_struct) = generate_instructions(component_type);
     let (update_fn, update_with_session_fn, update_struct, update_with_session_struct) =
         generate_update(component_type);
@@ -56,6 +57,8 @@ fn modify_component_module(mut module: ItemMod, component_type: &Type) -> ItemMo
                 update_struct,
                 update_with_session_fn,
                 update_with_session_struct,
+                destroy_fn,
+                destroy_struct,
             ]
             .into_iter()
             .map(|item| syn::parse2(item).unwrap())
@@ -115,6 +118,66 @@ fn create_check_attribute() -> Attribute {
     }
 }
 
+/// Generates the destroy function and struct.
+fn generate_destroy(component_type: &Type) -> (TokenStream2, TokenStream2) {
+    (
+        quote! {
+            #[automatically_derived]
+            pub fn destroy(ctx: Context<Destroy>) -> Result<()> {
+                let program_data_address =
+                    Pubkey::find_program_address(&[crate::id().as_ref()], &bolt_lang::prelude::solana_program::bpf_loader_upgradeable::id()).0;
+
+                if !program_data_address.eq(ctx.accounts.component_program_data.key) {
+                    return Err(BoltError::InvalidAuthority.into());
+                }
+
+                let program_account_data = ctx.accounts.component_program_data.try_borrow_data()?;
+                let upgrade_authority = if let bolt_lang::prelude::solana_program::bpf_loader_upgradeable::UpgradeableLoaderState::ProgramData {
+                    upgrade_authority_address,
+                    ..
+                } =
+                    bolt_lang::prelude::bincode::deserialize(&program_account_data).map_err(|_| BoltError::InvalidAuthority)?
+                {
+                    Ok(upgrade_authority_address)
+                } else {
+                    Err(anchor_lang::error::Error::from(BoltError::InvalidAuthority))
+                }?.ok_or_else(|| BoltError::InvalidAuthority)?;
+
+                if ctx.accounts.authority.key != &ctx.accounts.component.bolt_metadata.authority && ctx.accounts.authority.key != &upgrade_authority {
+                    return Err(BoltError::InvalidAuthority.into());
+                }
+
+                let instruction = anchor_lang::solana_program::sysvar::instructions::get_instruction_relative(
+                    0, &ctx.accounts.instruction_sysvar_account.to_account_info()
+                ).map_err(|_| BoltError::InvalidCaller)?;
+                if instruction.program_id != World::id() {
+                    return Err(BoltError::InvalidCaller.into());
+                }
+                Ok(())
+            }
+        },
+        quote! {
+            #[automatically_derived]
+            #[derive(Accounts)]
+            pub struct Destroy<'info> {
+                #[account()]
+                pub authority: Signer<'info>,
+                #[account(mut)]
+                pub receiver: AccountInfo<'info>,
+                #[account()]
+                pub entity: Account<'info, Entity>,
+                #[account(mut, close = receiver, seeds = [<#component_type>::seed(), entity.key().as_ref()], bump)]
+                pub component: Account<'info, #component_type>,
+                #[account()]
+                pub component_program_data: AccountInfo<'info>,
+                #[account(address = anchor_lang::solana_program::sysvar::instructions::id())]
+                pub instruction_sysvar_account: AccountInfo<'info>,
+                pub system_program: Program<'info, System>,
+            }
+        },
+    )
+}
+
 /// Generates the initialize function and struct.
 fn generate_initialize(component_type: &Type) -> (TokenStream2, TokenStream2) {
     (

+ 2 - 0
crates/bolt-lang/src/prelude.rs

@@ -1,2 +1,4 @@
 pub use anchor_lang;
 pub use anchor_lang::prelude::*;
+pub use bincode;
+pub use solana_program;

+ 26 - 0
crates/programs/bolt-component/src/lib.rs

@@ -10,6 +10,10 @@ pub mod bolt_component {
         Ok(())
     }
 
+    pub fn destroy(_ctx: Context<Destroy>) -> Result<()> {
+        Ok(())
+    }
+
     pub fn update(_ctx: Context<Update>, _data: Vec<u8>) -> Result<()> {
         Ok(())
     }
@@ -67,6 +71,28 @@ pub struct Initialize<'info> {
     pub system_program: Program<'info, System>,
 }
 
+#[derive(Accounts)]
+pub struct Destroy<'info> {
+    #[account()]
+    pub authority: Signer<'info>,
+    #[account(mut)]
+    /// CHECK: The receiver of the component
+    pub receiver: AccountInfo<'info>,
+    #[account()]
+    /// CHECK: The entity to destroy the component on
+    pub entity: AccountInfo<'info>,
+    #[account(mut)]
+    /// CHECK: The component to destroy
+    pub component: UncheckedAccount<'info>,
+    #[account()]
+    /// CHECK: The component program data
+    pub component_program_data: AccountInfo<'info>,
+    #[account(address = anchor_lang::solana_program::sysvar::instructions::id())]
+    /// CHECK: The instruction sysvar
+    pub instruction_sysvar_account: AccountInfo<'info>,
+    pub system_program: Program<'info, System>,
+}
+
 #[derive(InitSpace, AnchorSerialize, AnchorDeserialize, Default, Copy, Clone)]
 pub struct BoltMetadata {
     pub authority: Pubkey,

+ 0 - 1
crates/programs/world/Cargo.toml

@@ -29,4 +29,3 @@ bolt-component.workspace = true
 bolt-system.workspace = true
 solana-security-txt.workspace = true
 tuple-conv.workspace = true
-

+ 46 - 0
crates/programs/world/src/lib.rs

@@ -265,6 +265,11 @@ pub mod world {
         Ok(())
     }
 
+    pub fn destroy_component(ctx: Context<DestroyComponent>) -> Result<()> {
+        bolt_component::cpi::destroy(ctx.accounts.build())?;
+        Ok(())
+    }
+
     pub fn apply<'info>(
         ctx: Context<'_, '_, '_, 'info, Apply<'info>>,
         args: Vec<u8>,
@@ -552,6 +557,47 @@ impl<'info> InitializeComponent<'info> {
     }
 }
 
+#[derive(Accounts)]
+pub struct DestroyComponent<'info> {
+    #[account(mut)]
+    pub authority: Signer<'info>,
+    #[account(mut)]
+    /// CHECK: receiver check
+    pub receiver: AccountInfo<'info>,
+    /// CHECK: component program check
+    pub component_program: AccountInfo<'info>,
+    /// CHECK: component program data check
+    pub component_program_data: AccountInfo<'info>,
+    #[account()]
+    pub entity: Account<'info, Entity>,
+    #[account(mut)]
+    /// CHECK: component data check
+    pub component: UncheckedAccount<'info>,
+    #[account(address = anchor_lang::solana_program::sysvar::instructions::id())]
+    /// CHECK: instruction sysvar check
+    pub instruction_sysvar_account: UncheckedAccount<'info>,
+    pub system_program: Program<'info, System>,
+}
+
+impl<'info> DestroyComponent<'info> {
+    pub fn build(
+        &self,
+    ) -> CpiContext<'_, '_, '_, 'info, bolt_component::cpi::accounts::Destroy<'info>> {
+        let cpi_program = self.component_program.to_account_info();
+
+        let cpi_accounts = bolt_component::cpi::accounts::Destroy {
+            authority: self.authority.to_account_info(),
+            receiver: self.receiver.to_account_info(),
+            entity: self.entity.to_account_info(),
+            component: self.component.to_account_info(),
+            component_program_data: self.component_program_data.to_account_info(),
+            instruction_sysvar_account: self.instruction_sysvar_account.to_account_info(),
+            system_program: self.system_program.to_account_info(),
+        };
+        CpiContext::new(cpi_program, cpi_accounts)
+    }
+}
+
 #[account]
 #[derive(InitSpace, Default, Copy)]
 pub struct Registry {

+ 24 - 0
tests/intermediate-level/ecs.ts

@@ -3,6 +3,7 @@ import {
   AddEntity,
   ApplySystem,
   InitializeComponent,
+  DestroyComponent,
 } from "../../clients/bolt-sdk/lib";
 import { Direction, Framework } from "../framework";
 import { expect } from "chai";
@@ -55,6 +56,7 @@ export function ecs(framework: Framework) {
         entity: framework.entity1Pda,
         componentId: framework.exampleComponentVelocity.programId,
         seed: "component-velocity",
+        authority: framework.provider.wallet.publicKey,
       });
       await framework.provider.sendAndConfirm(initializeComponent.transaction);
       framework.componentVelocityEntity1Pda = initializeComponent.componentPda; // Saved for later
@@ -282,5 +284,27 @@ export function ecs(framework: Framework) {
       expect(position.y.toNumber()).to.equal(0);
       expect(position.z.toNumber()).to.equal(1);
     });
+
+    it("Destroy Velocity Component on Entity 1", async () => {
+      const keypair = web3.Keypair.generate();
+
+      let componentBalance = await framework.provider.connection.getBalance(
+        framework.componentVelocityEntity1Pda,
+      );
+
+      const destroyComponent = await DestroyComponent({
+        authority: framework.provider.wallet.publicKey,
+        entity: framework.entity1Pda,
+        componentId: framework.exampleComponentVelocity.programId,
+        receiver: keypair.publicKey,
+        seed: "component-velocity",
+      });
+      await framework.provider.sendAndConfirm(destroyComponent.transaction);
+
+      const balance = await framework.provider.connection.getBalance(
+        keypair.publicKey,
+      );
+      expect(balance).to.equal(componentBalance);
+    });
   });
 }

+ 33 - 7
tests/low-level/ecs.ts

@@ -3,6 +3,7 @@ import {
   anchor,
   web3,
   FindComponentPda,
+  FindComponentProgramDataPda,
   FindEntityPda,
   SerializeArgs,
 } from "../../clients/bolt-sdk/lib";
@@ -102,7 +103,7 @@ export function ecs(framework) {
           entity: framework.entity1Pda,
           data: framework.componentVelocityEntity1Pda,
           componentProgram: componentId,
-          authority: framework.worldProgram.programId,
+          authority: framework.provider.wallet.publicKey,
         })
         .instruction();
       const transaction = new anchor.web3.Transaction().add(instruction);
@@ -186,7 +187,6 @@ export function ecs(framework) {
           authority: framework.provider.wallet.publicKey,
           boltSystem: framework.systemSimpleMovement.programId,
           world: framework.worldPda,
-          sessionToken: null,
         })
         .remainingAccounts([
           {
@@ -221,7 +221,6 @@ export function ecs(framework) {
           authority: framework.provider.wallet.publicKey,
           boltSystem: framework.systemSimpleMovement.programId,
           world: framework.worldPda,
-          sessionToken: null,
         })
         .remainingAccounts([
           {
@@ -255,7 +254,6 @@ export function ecs(framework) {
           authority: framework.provider.wallet.publicKey,
           boltSystem: framework.systemFly.programId,
           world: framework.worldPda,
-          sessionToken: null,
         })
         .remainingAccounts([
           {
@@ -289,7 +287,6 @@ export function ecs(framework) {
           authority: framework.provider.wallet.publicKey,
           boltSystem: framework.systemApplyVelocity.programId,
           world: framework.worldPda,
-          sessionToken: null,
         })
         .remainingAccounts([
           {
@@ -342,7 +339,6 @@ export function ecs(framework) {
           authority: framework.provider.wallet.publicKey,
           boltSystem: framework.systemApplyVelocity.programId,
           world: framework.worldPda,
-          sessionToken: null,
         })
         .remainingAccounts([
           {
@@ -398,7 +394,6 @@ export function ecs(framework) {
           authority: framework.provider.wallet.publicKey,
           boltSystem: framework.systemFly.programId,
           world: framework.worldPda,
-          sessionToken: null,
         })
         .remainingAccounts([
           {
@@ -424,5 +419,36 @@ export function ecs(framework) {
       expect(position.y.toNumber()).to.equal(0);
       expect(position.z.toNumber()).to.equal(1);
     });
+
+    it("Destroy Velocity Component on Entity 1", async () => {
+      const keypair = web3.Keypair.generate();
+
+      let componentBalance = await framework.provider.connection.getBalance(
+        framework.componentVelocityEntity1Pda,
+      );
+
+      const componentProgramData = FindComponentProgramDataPda({
+        programId: framework.exampleComponentVelocity.programId,
+      });
+
+      const instruction = await framework.worldProgram.methods
+        .destroyComponent()
+        .accounts({
+          authority: framework.provider.wallet.publicKey,
+          componentProgram: framework.exampleComponentVelocity.programId,
+          entity: framework.entity1Pda,
+          component: framework.componentVelocityEntity1Pda,
+          componentProgramData: componentProgramData,
+          receiver: keypair.publicKey,
+        })
+        .instruction();
+      const transaction = new anchor.web3.Transaction().add(instruction);
+      await framework.provider.sendAndConfirm(transaction);
+
+      const balance = await framework.provider.connection.getBalance(
+        keypair.publicKey,
+      );
+      expect(balance).to.equal(componentBalance);
+    });
   });
 }

+ 0 - 2
tests/low-level/permissioning/component.ts

@@ -66,7 +66,6 @@ export function component(framework) {
           authority: keypair.publicKey,
           boltSystem: framework.systemFly.programId,
           world: framework.worldPda,
-          sessionToken: null,
         })
         .remainingAccounts([
           {
@@ -116,7 +115,6 @@ export function component(framework) {
           authority: framework.provider.wallet.publicKey,
           boltSystem: framework.systemFly.programId,
           world: framework.worldPda,
-          sessionToken: null,
         })
         .remainingAccounts([
           {

+ 0 - 3
tests/low-level/permissioning/world.ts

@@ -121,7 +121,6 @@ export function world(framework) {
           authority: framework.provider.wallet.publicKey,
           boltSystem: framework.systemFly.programId,
           world: framework.worldPda,
-          sessionToken: null,
         })
         .remainingAccounts([
           {
@@ -169,7 +168,6 @@ export function world(framework) {
           authority: framework.provider.wallet.publicKey,
           boltSystem: framework.systemFly.programId,
           world: framework.worldPda,
-          sessionToken: null,
         })
         .remainingAccounts([
           {
@@ -224,7 +222,6 @@ export function world(framework) {
           .accounts({
             boltComponent: framework.componentPositionEntity4Pda,
             authority: framework.provider.wallet.publicKey,
-            sessionToken: null,
           })
           .rpc();
       } catch (error) {