diff --git a/.github/workflows/publish-bolt-crates.yml b/.github/workflows/publish-bolt-crates.yml index 15d83a6e..eca7841d 100644 --- a/.github/workflows/publish-bolt-crates.yml +++ b/.github/workflows/publish-bolt-crates.yml @@ -139,7 +139,7 @@ jobs: echo ${{ secrets.SYSTEM_APPLY_VELOCITY }} > target/deploy/system_apply_velocity-keypair.json echo ${{ secrets.SYSTEM_FLY }} > target/deploy/system_fly-keypair.json echo ${{ secrets.SYSTEM_SIMPLE_MOVEMENT }} > target/deploy/system_simple_movement-keypair.json - + echo ${{ secrets.EXAMPLE_BUNDLE }} > target/deploy/example_bundle-keypair.json - name: Check versions are aligned run: | # Fails if versions are not aligned @@ -180,15 +180,12 @@ jobs: -p bolt-cli \ -p bolt-lang \ -p bolt-utils \ - -p bolt-system \ - -p bolt-component \ -p bolt-attribute-bolt-arguments \ + -p bolt-attribute-bolt-bundle \ -p bolt-attribute-bolt-component \ -p bolt-attribute-bolt-component-deserialize \ -p bolt-attribute-bolt-component-id \ - -p bolt-attribute-bolt-delegate \ -p bolt-attribute-bolt-extra-accounts \ - -p bolt-attribute-bolt-program \ -p bolt-attribute-bolt-system \ -p bolt-attribute-bolt-system-input env: diff --git a/.github/workflows/publish-bolt-sdk.yml b/.github/workflows/publish-bolt-sdk.yml index 4d58aadb..0a6f4933 100644 --- a/.github/workflows/publish-bolt-sdk.yml +++ b/.github/workflows/publish-bolt-sdk.yml @@ -144,7 +144,7 @@ jobs: echo ${{ secrets.SYSTEM_APPLY_VELOCITY }} > target/deploy/system_apply_velocity-keypair.json echo ${{ secrets.SYSTEM_FLY }} > target/deploy/system_fly-keypair.json echo ${{ secrets.SYSTEM_SIMPLE_MOVEMENT }} > target/deploy/system_simple_movement-keypair.json - + echo ${{ secrets.EXAMPLE_BUNDLE }} > target/deploy/example_bundle-keypair.json - name: Check versions are aligned run: | # Fails if versions are not aligned diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 7f5e07f4..9f03171c 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -89,6 +89,7 @@ jobs: echo ${{ secrets.SYSTEM_APPLY_VELOCITY }} > target/deploy/system_apply_velocity-keypair.json echo ${{ secrets.SYSTEM_FLY }} > target/deploy/system_fly-keypair.json echo ${{ secrets.SYSTEM_SIMPLE_MOVEMENT }} > target/deploy/system_simple_movement-keypair.json + echo ${{ secrets.EXAMPLE_BUNDLE }} > target/deploy/example_bundle-keypair.json - name: Run Build and Tests run: | diff --git a/Anchor.toml b/Anchor.toml index 39056e1b..3902748c 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -8,10 +8,9 @@ skip-lint = false world = "WorLD15A7CrDwLcLy4fRqtaTb9fbd8o8iqiEMUDse2n" [programs.localnet] -bolt-component = "CmP2djJgABZ4cRokm4ndxuq6LerqpNHLBsaUv2XKEJua" -bolt-system = "7X4EFsDJ5aYTcEjKzJ94rD8FRKgQeXC89fkpeTS4KaqP" component-small = "9yBADAhoTWCkNRB6hbfpwUgPpxyJiF9uEiWVPR6k7A4y" escrow-funding = "4Um2d8SvyfWyLLtfu2iJMFhM77DdjjyQusEy7K3VhPkd" +example-bundle = "CgfPBUeDUL3GT6b5AUDFE56KKgU4ycWA9ERjEWsfMZCj" position = "Fn1JzzEdyb55fsyduWS94mYHizGhJZuhvjX6DVvrmGbQ" system-apply-velocity = "6LHhFVwif6N9Po3jHtSmMVtPjF6zRfL3xMosSzcrQAS8" system-fly = "HT2YawJjkNmqWcLNfPAMvNsLdWwPvvvbKA5bpMw4eUpq" @@ -36,7 +35,7 @@ cluster = "localnet" wallet = "./tests/fixtures/provider.json" [workspace] -members = ["crates/programs/bolt-component", "crates/programs/bolt-system", "crates/programs/world", "examples/component-position", "examples/component-velocity", "examples/system-apply-velocity", "examples/system-fly", "examples/system-simple-movement", "examples/component-small", "examples/system-with-1-component", "examples/system-with-2-components", "examples/system-with-3-components", "examples/system-with-4-components", "examples/system-with-5-components", "examples/system-with-6-components", "examples/system-with-7-components", "examples/system-with-8-components", "examples/system-with-9-components", "examples/system-with-10-components", "examples/escrow-funding"] +members = ["crates/programs/world", "examples/component-position", "examples/component-velocity", "examples/system-apply-velocity", "examples/system-fly", "examples/system-simple-movement", "examples/component-small", "examples/system-with-1-component", "examples/system-with-2-components", "examples/system-with-3-components", "examples/system-with-4-components", "examples/system-with-5-components", "examples/system-with-6-components", "examples/system-with-7-components", "examples/system-with-8-components", "examples/system-with-9-components", "examples/system-with-10-components", "examples/escrow-funding", "examples/bundle"] [scripts] test = "tests/script.sh" diff --git a/Cargo.lock b/Cargo.lock index b0151bef..3b965c58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -773,55 +773,52 @@ dependencies = [ ] [[package]] -name = "bolt-attribute-bolt-arguments" +name = "bolt-attribute" version = "0.2.6" dependencies = [ + "bolt-utils", + "heck 0.5.0", "proc-macro2", "quote", "syn 1.0.109", ] [[package]] -name = "bolt-attribute-bolt-component" +name = "bolt-attribute-bolt-arguments" version = "0.2.6" dependencies = [ - "bolt-utils", - "heck 0.5.0", "proc-macro2", "quote", "syn 1.0.109", ] [[package]] -name = "bolt-attribute-bolt-component-deserialize" +name = "bolt-attribute-bolt-bundle" version = "0.2.6" dependencies = [ - "bolt-utils", - "proc-macro2", - "quote", - "syn 1.0.109", + "bolt-attribute", ] [[package]] -name = "bolt-attribute-bolt-component-id" +name = "bolt-attribute-bolt-component" version = "0.2.6" dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", + "bolt-attribute", ] [[package]] -name = "bolt-attribute-bolt-delegate" +name = "bolt-attribute-bolt-component-deserialize" version = "0.2.6" dependencies = [ + "bolt-utils", "proc-macro2", "quote", + "sha2 0.10.8", "syn 1.0.109", ] [[package]] -name = "bolt-attribute-bolt-extra-accounts" +name = "bolt-attribute-bolt-component-id" version = "0.2.6" dependencies = [ "proc-macro2", @@ -830,9 +827,10 @@ dependencies = [ ] [[package]] -name = "bolt-attribute-bolt-program" +name = "bolt-attribute-bolt-extra-accounts" version = "0.2.6" dependencies = [ + "heck 0.5.0", "proc-macro2", "quote", "syn 1.0.109", @@ -842,6 +840,7 @@ dependencies = [ name = "bolt-attribute-bolt-system" version = "0.2.6" dependencies = [ + "bolt-attribute", "proc-macro2", "quote", "syn 1.0.109", @@ -876,14 +875,6 @@ dependencies = [ "world", ] -[[package]] -name = "bolt-component" -version = "0.2.6" -dependencies = [ - "anchor-lang", - "bolt-system", -] - [[package]] name = "bolt-lang" version = "0.2.6" @@ -892,15 +883,13 @@ dependencies = [ "anchor-lang", "bincode", "bolt-attribute-bolt-arguments", + "bolt-attribute-bolt-bundle", "bolt-attribute-bolt-component", "bolt-attribute-bolt-component-deserialize", "bolt-attribute-bolt-component-id", - "bolt-attribute-bolt-delegate", "bolt-attribute-bolt-extra-accounts", - "bolt-attribute-bolt-program", "bolt-attribute-bolt-system", "bolt-attribute-bolt-system-input", - "bolt-system", "ephemeral-rollups-sdk", "getrandom 0.1.16", "serde", @@ -911,13 +900,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "bolt-system" -version = "0.2.6" -dependencies = [ - "anchor-lang", -] - [[package]] name = "bolt-types" version = "0.2.6" @@ -1300,6 +1282,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "const-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c06f1eb05f06cf2e380fdded278fbf056a38974299d77960555a311dcf91a52" +dependencies = [ + "keccak-const", + "sha2-const-stable", +] + [[package]] name = "constant_time_eq" version = "0.3.1" @@ -1871,6 +1863,14 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "example-bundle" +version = "0.2.5" +dependencies = [ + "bolt-lang", + "serde", +] + [[package]] name = "fastbloom" version = "0.9.0" @@ -2650,6 +2650,12 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "keccak-const" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d8d8ce877200136358e0bbff3a77965875db3af755a11e1fa6b1b3e2df13ea" + [[package]] name = "lalrpop" version = "0.20.2" @@ -4156,6 +4162,12 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha2-const-stable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9" + [[package]] name = "sha3" version = "0.10.8" @@ -7169,8 +7181,7 @@ name = "world" version = "0.2.6" dependencies = [ "anchor-lang", - "bolt-component", - "bolt-system", + "const-crypto", "solana-security-txt", "tuple-conv", ] diff --git a/Cargo.toml b/Cargo.toml index fc13c77f..6f61907f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,8 +3,6 @@ resolver = "2" members = [ "crates/bolt-cli", "crates/bolt-lang", - "crates/programs/bolt-component", - "crates/programs/bolt-system", "crates/programs/world", "crates/types", "examples/*", @@ -21,8 +19,9 @@ edition = "2021" [workspace.dependencies] bolt-types = { path = "crates/types", version = "=0.2.6" } bolt-lang = { path = "crates/bolt-lang", version = "=0.2.6" } +bolt-attribute = { path = "crates/bolt-lang/attribute", version = "=0.2.6" } +bolt-attribute-bolt-bundle = { path = "crates/bolt-lang/attribute/bundle", version = "=0.2.6" } bolt-attribute-bolt-program = { path = "crates/bolt-lang/attribute/bolt-program", version = "=0.2.6" } -bolt-attribute-bolt-delegate = { path = "crates/bolt-lang/attribute/delegate", version = "=0.2.6" } bolt-attribute-bolt-component = { path = "crates/bolt-lang/attribute/component", version = "=0.2.6" } bolt-attribute-bolt-system = { path = "crates/bolt-lang/attribute/system", version = "=0.2.6"} bolt-attribute-bolt-system-input = { path = "crates/bolt-lang/attribute/system-input", version = "=0.2.6" } @@ -32,9 +31,8 @@ bolt-attribute-bolt-component-deserialize = { path = "crates/bolt-lang/attribute bolt-attribute-bolt-component-id = { path = "crates/bolt-lang/attribute/component-id", version = "=0.2.6" } bolt-utils = { path = "crates/bolt-lang/utils", version = "=0.2.6" } world = { path = "crates/programs/world", features = ["cpi"], version = "=0.2.6"} -bolt-system = { path = "crates/programs/bolt-system", features = ["cpi"], version = "=0.2.6"} -bolt-component = { path = "crates/programs/bolt-component", features = ["cpi"], version = "=0.2.6"} small = { path = "examples/component-small", features = ["cpi"], version = "=0.2.6"} +component-large = { path = "examples/component-large", features = ["cpi"], version = "=0.2.6"} ## External crates session-keys = { version = "^2", features = ["no-entrypoint"] } @@ -63,6 +61,8 @@ which = "^7" tokio = { version = "^1", features = ["full"] } sysinfo = "=0.36.1" bytemuck_derive = "^1" +const-crypto = "0.3.0" +sha2 = "^0.10" [profile.release] overflow-checks = true diff --git a/clients/csharp/Solana.Unity.Bolt.Test/AccelerationTest.cs b/clients/csharp/Solana.Unity.Bolt.Test/AccelerationTest.cs index ccf3cd55..ae5d7d51 100644 --- a/clients/csharp/Solana.Unity.Bolt.Test/AccelerationTest.cs +++ b/clients/csharp/Solana.Unity.Bolt.Test/AccelerationTest.cs @@ -9,6 +9,8 @@ using World.Program; using System.Diagnostics; using Solana.Unity.Rpc.Types; +using Bolt; + namespace AccelerationTest { public class Test { public static async Task Run(Framework framework) { @@ -45,14 +47,16 @@ public static async Task DelegateComponent(Framework framework) { public static async Task ApplySimpleMovementSystemOnAccelerator(Framework framework) { for (int i = 0; i < 10; i++) { - var apply = new ApplyAccounts() { - Authority = framework.Wallet.Account.PublicKey, - BoltSystem = framework.SystemSimpleMovement, - World = framework.WorldPda, - }; - var instruction = WorldProgram.Apply(apply, Bolt.World.SerializeArgs(new { direction = "Up" })); - instruction.Keys.Add(AccountMeta.ReadOnly(framework.ExampleComponentPosition, false)); - instruction.Keys.Add(AccountMeta.Writable(framework.AccelerationComponentPositionPda, false)); + var instruction = Bolt.World.ApplySystem( + framework.WorldPda, + framework.SystemSimpleMovement, + new Bolt.World.EntityType[] { + new Bolt.World.EntityType(framework.AccelerationEntityPda, + new PublicKey[] { framework.ExampleComponentPosition }) + }, + Bolt.World.SerializeArgs(new { direction = "Up" }), + framework.Wallet.Account.PublicKey + ); await framework.SendAndConfirmInstruction(framework.AcceleratorClient, instruction); await Task.Delay(50); } diff --git a/clients/csharp/Solana.Unity.Bolt.Test/ECSTest.cs b/clients/csharp/Solana.Unity.Bolt.Test/ECSTest.cs index 6682eb6f..e82cc0e9 100644 --- a/clients/csharp/Solana.Unity.Bolt.Test/ECSTest.cs +++ b/clients/csharp/Solana.Unity.Bolt.Test/ECSTest.cs @@ -27,6 +27,12 @@ await Profiler.Run("AddEntity4WithSeed", async () => { await Profiler.Run("InitializeVelocityComponentOnEntity1WithSeed", async () => { await InitializeVelocityComponentOnEntity1WithSeed(framework); }); + await Profiler.Run("InitializeBundledPositionOnEntity1", async () => { + await InitializeBundledPositionOnEntity1(framework); + }); + await Profiler.Run("InitializeBundledVelocityOnEntity1", async () => { + await InitializeBundledVelocityOnEntity1(framework); + }); await Profiler.Run("InitializePositionComponentOnEntity1", async () => { await InitializePositionComponentOnEntity1(framework); }); @@ -45,6 +51,12 @@ await Profiler.Run("ApplySimpleMovementSystemUpOnEntity1", async () => { await Profiler.Run("ApplySimpleMovementSystemRightOnEntity1", async () => { await ApplySimpleMovementSystemRightOnEntity1(framework); }); + await Profiler.Run("ApplyBundledMovementOnEntity1", async () => { + await ApplyBundledMovementOnEntity1(framework); + }); + await Profiler.Run("ApplyBundledStopOnEntity1", async () => { + await ApplyBundledStopOnEntity1(framework); + }); await Profiler.Run("DestroyVelocityComponentOnEntity1", async () => { await DestroyVelocityComponentOnEntity1(framework); }); @@ -79,6 +91,43 @@ public static async Task InitializeVelocityComponentOnEntity1WithSeed(Framework await framework.SendAndConfirmInstruction(initializeComponent.Instruction); } + public static async Task InitializeBundledPositionOnEntity1(Framework framework) { + var initializeComponent = await Bolt.World.InitializeComponent( + framework.Wallet.Account.PublicKey, + framework.Entity1Pda, + new Bolt.Component(framework.ExampleBundleProgramId, "position") + ); + framework.BundlePositionEntity1Pda = initializeComponent.Pda; + await framework.SendAndConfirmInstruction(initializeComponent.Instruction); + + var accountInfo = await framework.GetAccountInfo(framework.BundlePositionEntity1Pda); + var data = Convert.FromBase64String(accountInfo.Data[0]); + var position = Position.Accounts.Position.Deserialize(data); + Debug.Assert(0 == position.X, "X is not equal to 0"); + Debug.Assert(0 == position.Y, "Y is not equal to 0"); + Debug.Assert(0 == position.Z, "Z is not equal to 0"); + } + + public static async Task InitializeBundledVelocityOnEntity1(Framework framework) { + var initializeComponent = await Bolt.World.InitializeComponent( + framework.Wallet.Account.PublicKey, + framework.Entity1Pda, + new Bolt.Component(framework.ExampleBundleProgramId, "velocity") + ); + framework.BundleVelocityEntity1Pda = initializeComponent.Pda; + await framework.SendAndConfirmInstruction(initializeComponent.Instruction); + + var accountInfo = await framework.GetAccountInfo(framework.BundleVelocityEntity1Pda); + var data = Convert.FromBase64String(accountInfo.Data[0]); + int offset = 8; // skip discriminator + long x = BitConverter.ToInt64(data, offset); offset += 8; + long y = BitConverter.ToInt64(data, offset); offset += 8; + long z = BitConverter.ToInt64(data, offset); offset += 8; + Debug.Assert(1 == x, "X is not equal to 1"); + Debug.Assert(2 == y, "Y is not equal to 2"); + Debug.Assert(3 == z, "Z is not equal to 3"); + } + public static async Task InitializePositionComponentOnEntity1(Framework framework) { var initializeComponent = await Bolt.World.InitializeComponent(framework.Wallet.Account.PublicKey, framework.Entity1Pda, framework.ExampleComponentPosition); framework.ComponentPositionEntity1Pda = initializeComponent.Pda; @@ -107,14 +156,16 @@ public static async Task CheckPositionOnEntity1IsDefault(Framework framework) { } public static async Task ApplySimpleMovementSystemUpOnEntity1(Framework framework) { - var apply = new ApplyAccounts() { - Authority = framework.Wallet.Account.PublicKey, - BoltSystem = framework.SystemSimpleMovement, - World = framework.WorldPda, - }; - var instruction = WorldProgram.Apply(apply, Bolt.World.SerializeArgs(new { direction = "Up" })); - instruction.Keys.Add(AccountMeta.ReadOnly(framework.ExampleComponentPosition, false)); - instruction.Keys.Add(AccountMeta.Writable(framework.ComponentPositionEntity1Pda, false)); + var instruction = Bolt.World.ApplySystem( + framework.WorldPda, + framework.SystemSimpleMovement, + new Bolt.World.EntityType[] { + new Bolt.World.EntityType(framework.Entity1Pda, + new PublicKey[] { framework.ExampleComponentPosition }) + }, + Bolt.World.SerializeArgs(new { direction = "Up" }), + framework.Wallet.Account.PublicKey + ); await framework.SendAndConfirmInstruction(instruction); var accountInfo = await framework.GetAccountInfo(framework.ComponentPositionEntity1Pda); @@ -145,6 +196,54 @@ public static async Task ApplySimpleMovementSystemRightOnEntity1(Framework frame Debug.Assert(0 == position.Z, "Z is not equal to 0"); } + public static async Task ApplyBundledMovementOnEntity1(Framework framework) { + var instruction = Bolt.World.ApplySystem( + framework.WorldPda, + new Bolt.System(framework.ExampleBundleProgramId, "movement"), + new (PublicKey entity, Bolt.Component[] components, string[] seeds)?[] { + (framework.Entity1Pda, new Bolt.Component[] { + new Bolt.Component(framework.ExampleBundleProgramId, "position"), + new Bolt.Component(framework.ExampleBundleProgramId, "velocity") + }, Array.Empty()) + }, + new { }, + framework.Wallet.Account.PublicKey + ); + await framework.SendAndConfirmInstruction(instruction); + + var accountInfo = await framework.GetAccountInfo(framework.BundlePositionEntity1Pda); + var data = Convert.FromBase64String(accountInfo.Data[0]); + var position = Position.Accounts.Position.Deserialize(data); + Debug.Assert(1 == position.X, "X is not equal to 1"); + Debug.Assert(2 == position.Y, "Y is not equal to 2"); + Debug.Assert(3 == position.Z, "Z is not equal to 3"); + } + + public static async Task ApplyBundledStopOnEntity1(Framework framework) { + var instruction = Bolt.World.ApplySystem( + framework.WorldPda, + new Bolt.System(framework.ExampleBundleProgramId, "stop"), + new (PublicKey entity, Bolt.Component[] components, string[] seeds)?[] { + (framework.Entity1Pda, new Bolt.Component[] { + new Bolt.Component(framework.ExampleBundleProgramId, "velocity") + }, Array.Empty()) + }, + new { }, + framework.Wallet.Account.PublicKey + ); + await framework.SendAndConfirmInstruction(instruction); + + var accountInfo = await framework.GetAccountInfo(framework.BundleVelocityEntity1Pda); + var data = Convert.FromBase64String(accountInfo.Data[0]); + int offset = 8; // skip discriminator + long x = BitConverter.ToInt64(data, offset); offset += 8; + long y = BitConverter.ToInt64(data, offset); offset += 8; + long z = BitConverter.ToInt64(data, offset); offset += 8; + Debug.Assert(0 == x, "X is not equal to 0"); + Debug.Assert(0 == y, "Y is not equal to 0"); + Debug.Assert(0 == z, "Z is not equal to 0"); + } + public static async Task DestroyVelocityComponentOnEntity1(Framework framework) { var receiver = new Wallet(new Mnemonic(WordList.English, WordCount.Twelve)); diff --git a/clients/csharp/Solana.Unity.Bolt.Test/Framework.cs b/clients/csharp/Solana.Unity.Bolt.Test/Framework.cs index ed6f4ac9..2f7a2b17 100644 --- a/clients/csharp/Solana.Unity.Bolt.Test/Framework.cs +++ b/clients/csharp/Solana.Unity.Bolt.Test/Framework.cs @@ -56,6 +56,11 @@ public class Framework public PublicKey SessionToken { get; set; } + // Example bundle + public PublicKey ExampleBundleProgramId { get; set; } + public PublicKey BundlePositionEntity1Pda { get; set; } + public PublicKey BundleVelocityEntity1Pda { get; set; } + public Framework() { SecondAuthority = new Wallet.Wallet(new Mnemonic(WordList.English, WordCount.Twelve)); @@ -67,6 +72,7 @@ public Framework() ExampleComponentPosition = new PublicKey(Position.Program.PositionProgram.ID); ExampleComponentVelocity = new PublicKey(Velocity.Program.VelocityProgram.ID); SystemSimpleMovement = new PublicKey("FSa6qoJXFBR3a7ThQkTAMrC15p6NkchPEjBdd4n6dXxA"); + ExampleBundleProgramId = new PublicKey("CgfPBUeDUL3GT6b5AUDFE56KKgU4ycWA9ERjEWsfMZCj"); } public async Task Initialize() diff --git a/clients/csharp/Solana.Unity.Bolt/ECS/Component.cs b/clients/csharp/Solana.Unity.Bolt/ECS/Component.cs new file mode 100644 index 00000000..6aae585d --- /dev/null +++ b/clients/csharp/Solana.Unity.Bolt/ECS/Component.cs @@ -0,0 +1,35 @@ + + +using Solana.Unity.Wallet; + +namespace Bolt { + /// + /// A class that represents an ECS component. + /// + public class Component : Identifier { + /// + /// Initializes a new instance of the class. + /// + /// The program that the component belongs to. + /// The name of the component. + public Component(PublicKey program, string name) : base(program, name) {} + + /// + /// Static constructor from either a raw program id or an existing Component, like TS Component.from. + /// + public static Component From(object componentId) + { + if (componentId is Component c) return c; + if (componentId is PublicKey pk) return new Component(pk, null); + throw new global::System.ArgumentException("componentId must be PublicKey or Component"); + } + + /// + /// Build seeds: (seed ?? "") + (Name ?? ""), mirroring TS seeds(). + /// + public string Seeds(string seed = null) + { + return (seed ?? "") + (Name ?? ""); + } + } +} \ No newline at end of file diff --git a/clients/csharp/Solana.Unity.Bolt/ECS/Identifier.cs b/clients/csharp/Solana.Unity.Bolt/ECS/Identifier.cs new file mode 100644 index 00000000..24716abb --- /dev/null +++ b/clients/csharp/Solana.Unity.Bolt/ECS/Identifier.cs @@ -0,0 +1,39 @@ + +using Solana.Unity.Wallet; + +namespace Bolt { + /// + /// A class that represents an identifier for an ECS component or system. + /// + public class Identifier { + /// + /// The program that the identifier belongs to. + /// + public PublicKey Program { get; set; } + + /// + /// The name of the identifier. + /// + public string Name { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The program that the identifier belongs to. + /// The name of the identifier. + public Identifier(PublicKey program, string name) { + this.Program = program; + this.Name = name; + } + + /// + /// Build the 8-byte Anchor discriminator for a given method, mirroring TS Identifier.getMethodDiscriminator. + /// Format: "global:" + (name? name + "_" : "") + method + /// + public byte[] GetMethodDiscriminator(string method) + { + var prefix = string.IsNullOrEmpty(Name) ? "" : Name + "_"; + return World.GetDiscriminator("global:" + prefix + method); + } + } +} \ No newline at end of file diff --git a/clients/csharp/Solana.Unity.Bolt/ECS/System.cs b/clients/csharp/Solana.Unity.Bolt/ECS/System.cs new file mode 100644 index 00000000..e4d05f1e --- /dev/null +++ b/clients/csharp/Solana.Unity.Bolt/ECS/System.cs @@ -0,0 +1,27 @@ + + +using Solana.Unity.Wallet; + +namespace Bolt { + /// + /// A class that represents an ECS system. + /// + public class System : Identifier { + /// + /// Initializes a new instance of the class. + /// + /// The program that the system belongs to. + /// The name of the system. + public System(PublicKey program, string name) : base(program, name) {} + + /// + /// Static constructor from either a raw program id or an existing System, like TS System.from. + /// + public static System From(object systemId) + { + if (systemId is System s) return s; + if (systemId is PublicKey pk) return new System(pk, null); + throw new global::System.ArgumentException("systemId must be PublicKey or System"); + } + } +} \ No newline at end of file diff --git a/clients/csharp/Solana.Unity.Bolt/WorldProgram/Bolt.cs b/clients/csharp/Solana.Unity.Bolt/WorldProgram/Bolt.cs index a5403f46..2b5cea8e 100644 --- a/clients/csharp/Solana.Unity.Bolt/WorldProgram/Bolt.cs +++ b/clients/csharp/Solana.Unity.Bolt/WorldProgram/Bolt.cs @@ -4,8 +4,10 @@ using Solana.Unity.Rpc.Types; using Solana.Unity.Wallet; using System; +using System.Text; using System.Threading.Tasks; using WorldNamespace = World; +using System.Security.Cryptography; namespace Bolt { public partial class World { @@ -22,8 +24,19 @@ public partial class World { public static byte[] SerializeArgs(object args) { - return System.Text.Encoding.UTF8.GetBytes(Newtonsoft.Json.JsonConvert.SerializeObject(args)); + return Encoding.UTF8.GetBytes(Newtonsoft.Json.JsonConvert.SerializeObject(args)); } + public static byte[] GetDiscriminator(string name) { + // Anchor uses the first 8 bytes of the SHA256 hash of the instruction name. + // See: https://github.com/coral-xyz/anchor/blob/master/lang/syn/src/codegen/accounts/discriminator.rs + var nameBytes = Encoding.UTF8.GetBytes(name); + using (var sha256 = SHA256.Create()) { + var hash = sha256.ComputeHash(nameBytes); + var discriminator = new byte[8]; + Array.Copy(hash, discriminator, 8); + return discriminator; + } + } } } diff --git a/clients/csharp/Solana.Unity.Bolt/WorldProgram/Bolt/ApplySystem.cs b/clients/csharp/Solana.Unity.Bolt/WorldProgram/Bolt/ApplySystem.cs index 86143eeb..23e95230 100644 --- a/clients/csharp/Solana.Unity.Bolt/WorldProgram/Bolt/ApplySystem.cs +++ b/clients/csharp/Solana.Unity.Bolt/WorldProgram/Bolt/ApplySystem.cs @@ -8,6 +8,9 @@ namespace Bolt { public partial class World { + /// + /// Apply a system providing raw public keys (existing overloads) + /// public static Solana.Unity.Rpc.Models.TransactionInstruction ApplySystem( PublicKey world, PublicKey system, @@ -46,6 +49,96 @@ public static Solana.Unity.Rpc.Models.TransactionInstruction ApplySystem( return WorldProgram.ApplySystem(world, system, entityTypes, new byte[] {}, authority, sessionToken, programId); } + /// + /// Apply a bundled system and components, mirroring TS client behavior. + /// Chooses among apply/applyWithSession/applyWithDiscriminator/applyWithSessionAndDiscriminator + /// based on whether the System has a name (discriminator) and whether a session token is provided. + /// Component discriminators are no longer sent; only component id + PDA pairs are included. + /// + public static Solana.Unity.Rpc.Models.TransactionInstruction ApplySystem( + PublicKey world, + System systemId, + (PublicKey entity, Component[] components, string[] seeds)?[] entities, + object args, + PublicKey authority, + PublicKey sessionToken = null, + PublicKey programId = null, + Solana.Unity.Rpc.Models.AccountMeta[] extraAccounts = null) + { + programId ??= new(WorldProgram.ID); + + var remainingAccounts = new global::System.Collections.Generic.List(); + + foreach (var entry in entities) + { + if (entry == null) continue; + var (entity, components, seeds) = entry.Value; + for (int i = 0; i < components.Length; i++) + { + var comp = components[i]; + var providedSeed = (seeds != null && i < seeds.Length) ? seeds[i] : ""; + var pda = WorldProgram.FindComponentPda(comp.Program, entity, comp.Seeds(providedSeed)); + remainingAccounts.Add(Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(comp.Program, false)); + remainingAccounts.Add(Solana.Unity.Rpc.Models.AccountMeta.Writable(pda, false)); + } + } + + // Optional delimiter and extra accounts + if (extraAccounts != null && extraAccounts.Length > 0) + { + remainingAccounts.Add(Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(new PublicKey(WorldProgram.ID), false)); + remainingAccounts.AddRange(extraAccounts); + } + + bool hasSystemName = !string.IsNullOrEmpty(systemId.Name); + var serializedArgs = SerializeArgs(args); + + Solana.Unity.Rpc.Models.TransactionInstruction instruction; + if (sessionToken != null) + { + var accounts = new ApplyWithSessionAccounts() + { + BoltSystem = systemId.Program, + Authority = authority, + World = world, + SessionToken = sessionToken, + }; + if (hasSystemName) + { + var sysDisc = systemId.GetMethodDiscriminator("bolt_execute"); + instruction = WorldProgram.ApplyWithSessionAndDiscriminator(accounts, sysDisc, serializedArgs, programId); + } + else + { + instruction = WorldProgram.ApplyWithSession(accounts, serializedArgs, programId); + } + } + else + { + var accounts = new ApplyAccounts() + { + BoltSystem = systemId.Program, + Authority = authority, + World = world, + }; + if (hasSystemName) + { + var sysDisc = systemId.GetMethodDiscriminator("bolt_execute"); + instruction = WorldProgram.ApplyWithDiscriminator(accounts, sysDisc, serializedArgs, programId); + } + else + { + instruction = WorldProgram.Apply(accounts, serializedArgs, programId); + } + } + + // Append remaining accounts (component id+pda pairs and extras) + foreach (var meta in remainingAccounts) + instruction.Keys.Add(meta); + + return instruction; + } + public class EntityType { public PublicKey[] Components { get; set; } diff --git a/clients/csharp/Solana.Unity.Bolt/WorldProgram/Bolt/DelegateComponent.cs b/clients/csharp/Solana.Unity.Bolt/WorldProgram/Bolt/DelegateComponent.cs index ad323339..25cd5d00 100644 --- a/clients/csharp/Solana.Unity.Bolt/WorldProgram/Bolt/DelegateComponent.cs +++ b/clients/csharp/Solana.Unity.Bolt/WorldProgram/Bolt/DelegateComponent.cs @@ -19,43 +19,93 @@ public class DelegateComponentInstruction { public TransactionInstruction Instruction { get; set; } } - public static async Task DelegateComponent(PublicKey payer, PublicKey entity, PublicKey componentId, string seed = "") { - var account = WorldProgram.FindComponentPda(componentId, entity, seed); - var bufferPda = WorldProgram.FindBufferPda(account, componentId); - var delegationRecord = WorldProgram.FindDelegationProgramPda("delegation", account); - var delegationMetadata = WorldProgram.FindDelegationProgramPda("delegation-metadata", account); - - byte[] discriminator = new byte[] { 90, 147, 75, 178, 85, 88, 4, 137 }; - uint commitFrequencyMs = 0; - byte[] commitFrequencyBytes = BitConverter.GetBytes(commitFrequencyMs); - if (!BitConverter.IsLittleEndian) Array.Reverse(commitFrequencyBytes); - var validator = new byte[1]; - validator[0] = 0; - - var data = new byte[discriminator.Length + commitFrequencyBytes.Length + validator.Length]; - Array.Copy(discriminator, data, discriminator.Length); - Array.Copy(commitFrequencyBytes, 0, data, discriminator.Length, commitFrequencyBytes.Length); - Array.Copy(validator, 0, data, discriminator.Length + commitFrequencyBytes.Length, validator.Length); - - TransactionInstruction instruction = new TransactionInstruction() { - ProgramId = componentId, - Keys = new List() { - AccountMeta.ReadOnly(payer, true), - AccountMeta.ReadOnly(entity, false), - AccountMeta.Writable(account, false), - AccountMeta.ReadOnly(componentId, false), - AccountMeta.Writable(bufferPda, false), - AccountMeta.Writable(delegationRecord, false), - AccountMeta.Writable(delegationMetadata, false), - AccountMeta.ReadOnly(WorldProgram.DelegationProgram, false), - AccountMeta.ReadOnly(SystemProgram.ProgramIdKey, false), - }, - Data = data, - }; - return new DelegateComponentInstruction() { - Pda = WorldProgram.FindDelegationProgramPda(seed, entity), - Instruction = instruction, - }; - } + public static async Task DelegateComponent(PublicKey payer, PublicKey entity, PublicKey componentId, string seed = "") { + // Compute the delegated account PDA and related PDAs + var account = WorldProgram.FindComponentPda(componentId, entity, seed); + var bufferPda = WorldProgram.FindBufferPda(account, componentId); + var delegationRecord = WorldProgram.FindDelegationProgramPda("delegation", account); + var delegationMetadata = WorldProgram.FindDelegationProgramPda("delegation-metadata", account); + + // Build instruction data per TS beet struct: + // discriminator[8] + commitFrequencyMs[u32 le] + validator[COption] + pdaSeeds[Vec] + byte[] discriminator = new byte[] { 90, 147, 75, 178, 85, 88, 4, 137 }; + uint commitFrequencyMs = 0; + byte[] commitFrequencyBytes = BitConverter.GetBytes(commitFrequencyMs); // little-endian on most platforms + byte[] validatorNoneTag = new byte[] { 0 }; // COption None + + var data = Concat(discriminator, commitFrequencyBytes, validatorNoneTag); + + TransactionInstruction instruction = new TransactionInstruction() { + ProgramId = componentId, + Keys = new List() { + AccountMeta.ReadOnly(payer, true), + AccountMeta.ReadOnly(entity, false), + AccountMeta.Writable(account, false), + AccountMeta.ReadOnly(componentId, false), + AccountMeta.Writable(bufferPda, false), + AccountMeta.Writable(delegationRecord, false), + AccountMeta.Writable(delegationMetadata, false), + AccountMeta.ReadOnly(WorldProgram.DelegationProgram, false), + AccountMeta.ReadOnly(SystemProgram.ProgramIdKey, false), + }, + Data = data, + }; + return new DelegateComponentInstruction() { + Pda = account, + Instruction = instruction, + }; + } + + /// + /// Overload for bundled components: seed is augmented with component name. + /// Mirrors TS behavior using component.seeds(seed) for PDA seeds. + /// + public static async Task DelegateComponent(PublicKey payer, PublicKey entity, Component component, string seed = "") { + var account = WorldProgram.FindComponentPda(component.Program, entity, component.Seeds(seed)); + var bufferPda = WorldProgram.FindBufferPda(account, component.Program); + var delegationRecord = WorldProgram.FindDelegationProgramPda("delegation", account); + var delegationMetadata = WorldProgram.FindDelegationProgramPda("delegation-metadata", account); + + byte[] discriminator = new byte[] { 90, 147, 75, 178, 85, 88, 4, 137 }; + uint commitFrequencyMs = 0; + byte[] commitFrequencyBytes = BitConverter.GetBytes(commitFrequencyMs); + byte[] validatorNoneTag = new byte[] { 0 }; + + var data = Concat(discriminator, commitFrequencyBytes, validatorNoneTag); + + TransactionInstruction instruction = new TransactionInstruction() { + ProgramId = component.Program, + Keys = new List() { + AccountMeta.ReadOnly(payer, true), + AccountMeta.ReadOnly(entity, false), + AccountMeta.Writable(account, false), + AccountMeta.ReadOnly(component.Program, false), + AccountMeta.Writable(bufferPda, false), + AccountMeta.Writable(delegationRecord, false), + AccountMeta.Writable(delegationMetadata, false), + AccountMeta.ReadOnly(WorldProgram.DelegationProgram, false), + AccountMeta.ReadOnly(SystemProgram.ProgramIdKey, false), + }, + Data = data, + }; + return new DelegateComponentInstruction() { + Pda = account, + Instruction = instruction, + }; + } + + private static byte[] Concat(params byte[][] arrays) + { + int total = 0; + foreach (var a in arrays) total += a.Length; + var buf = new byte[total]; + int offset = 0; + foreach (var a in arrays) + { + Buffer.BlockCopy(a, 0, buf, offset, a.Length); + offset += a.Length; + } + return buf; + } } } diff --git a/clients/csharp/Solana.Unity.Bolt/WorldProgram/Bolt/DestroyComponent.cs b/clients/csharp/Solana.Unity.Bolt/WorldProgram/Bolt/DestroyComponent.cs index 8437c6cc..91f3dc71 100644 --- a/clients/csharp/Solana.Unity.Bolt/WorldProgram/Bolt/DestroyComponent.cs +++ b/clients/csharp/Solana.Unity.Bolt/WorldProgram/Bolt/DestroyComponent.cs @@ -35,7 +35,28 @@ public static async Task DestroyComponent(PublicKey Entity = entity, Component = componentPda, ComponentProgram = componentProgram, - ComponentProgramData = componentProgramData + ComponentProgramData = componentProgramData, + }; + var instruction = WorldProgram.DestroyComponent(destroyComponent); + return new DestroyComponentInstruction() { + Instruction = instruction + }; + } + + /// + /// Overload accepting bundled component identifier; seed defaults to component name. + /// Mirrors TS: discriminator derived from component name if provided. + /// + public static async Task DestroyComponent(PublicKey authority, PublicKey receiver, PublicKey entity, Component component, string seed = "") { + var pda = WorldProgram.FindComponentPda(component.Program, entity, component.Seeds(seed)); + var componentProgramData = WorldProgram.FindComponentProgramDataPda(component.Program); + var destroyComponent = new DestroyComponentAccounts() { + Authority = authority, + Receiver = receiver, + Entity = entity, + Component = pda, + ComponentProgram = component.Program, + ComponentProgramData = componentProgramData, }; var instruction = WorldProgram.DestroyComponent(destroyComponent); return new DestroyComponentInstruction() { diff --git a/clients/csharp/Solana.Unity.Bolt/WorldProgram/Bolt/InitializeComponent.cs b/clients/csharp/Solana.Unity.Bolt/WorldProgram/Bolt/InitializeComponent.cs index 6d297455..7069e12f 100644 --- a/clients/csharp/Solana.Unity.Bolt/WorldProgram/Bolt/InitializeComponent.cs +++ b/clients/csharp/Solana.Unity.Bolt/WorldProgram/Bolt/InitializeComponent.cs @@ -3,6 +3,7 @@ using Solana.Unity.Rpc.Models; using Solana.Unity.Wallet; +using System; using System.Threading.Tasks; using World.Program; @@ -34,7 +35,7 @@ public static async Task InitializeComponent(Pub Entity = entity, Data = componentPda, ComponentProgram = componentId, - Authority = authority ?? new PublicKey(WorldProgram.ID) + Authority = authority ?? new PublicKey(WorldProgram.ID), }; var instruction = WorldProgram.InitializeComponent(initializeComponent); return new InitializeComponentInstruction() { @@ -42,5 +43,33 @@ public static async Task InitializeComponent(Pub Instruction = instruction }; } + + /// + /// Initialize a bundled component using its program and name, mirroring TS client behavior. + /// Uses component name as seed and component-specific initialize discriminator. + /// + /// Payer public key. + /// Entity PDA. + /// Bundled component identifier (program + name). + /// Optional additional seed; defaults to empty. Final seed is seed + component name. + /// Optional authority, defaults to world program id. + public static async Task InitializeComponent(PublicKey payer, PublicKey entity, Component component, string seed = "", PublicKey authority = null) { + if (component is null) throw new ArgumentNullException(nameof(component)); + var discriminator = component.GetMethodDiscriminator("initialize"); + if (discriminator is null || discriminator.Length != 8) throw new ArgumentException("Invalid discriminator", nameof(component)); + var componentPda = WorldProgram.FindComponentPda(component.Program, entity, component.Seeds(seed)); + var initializeComponent = new InitializeComponentAccounts() { + Payer = payer, + Entity = entity, + Data = componentPda, + ComponentProgram = component.Program, + Authority = authority ?? new PublicKey(WorldProgram.ID), + }; + var instruction = WorldProgram.InitializeComponentWithDiscriminator(initializeComponent, discriminator); + return new InitializeComponentInstruction() { + Pda = componentPda, + Instruction = instruction + }; + } } } \ No newline at end of file diff --git a/clients/csharp/Solana.Unity.Bolt/WorldProgram/Generated.cs b/clients/csharp/Solana.Unity.Bolt/WorldProgram/Generated.cs index 82ba99a4..b1f023a1 100644 --- a/clients/csharp/Solana.Unity.Bolt/WorldProgram/Generated.cs +++ b/clients/csharp/Solana.Unity.Bolt/WorldProgram/Generated.cs @@ -1,568 +1,587 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using System.Threading.Tasks; -using Solana.Unity; -using Solana.Unity.Programs.Abstract; -using Solana.Unity.Programs.Utilities; -using Solana.Unity.Rpc; -using Solana.Unity.Rpc.Builders; -using Solana.Unity.Rpc.Core.Http; -using Solana.Unity.Rpc.Core.Sockets; -using Solana.Unity.Rpc.Types; -using Solana.Unity.Wallet; -using World; -using World.Program; -using World.Errors; -using World.Accounts; -using World.Types; - -namespace World -{ - namespace Accounts - { - public partial class Entity - { - public static ulong ACCOUNT_DISCRIMINATOR => 1751670451238706478UL; - public static ReadOnlySpan ACCOUNT_DISCRIMINATOR_BYTES => new byte[]{46, 157, 161, 161, 254, 46, 79, 24}; - public static string ACCOUNT_DISCRIMINATOR_B58 => "8oEQa6zH67R"; - public ulong Id { get; set; } - - public static Entity Deserialize(ReadOnlySpan _data) - { - int offset = 0; - ulong accountHashValue = _data.GetU64(offset); - offset += 8; - if (accountHashValue != ACCOUNT_DISCRIMINATOR) - { - return null; - } - - Entity result = new Entity(); - result.Id = _data.GetU64(offset); - offset += 8; - return result; - } - } - - public partial class Registry - { - public static ulong ACCOUNT_DISCRIMINATOR => 15779688099924061743UL; - public static ReadOnlySpan ACCOUNT_DISCRIMINATOR_BYTES => new byte[]{47, 174, 110, 246, 184, 182, 252, 218}; - public static string ACCOUNT_DISCRIMINATOR_B58 => "8ya1XGY4XBP"; - public ulong Worlds { get; set; } - - public static Registry Deserialize(ReadOnlySpan _data) - { - int offset = 0; - ulong accountHashValue = _data.GetU64(offset); - offset += 8; - if (accountHashValue != ACCOUNT_DISCRIMINATOR) - { - return null; - } - - Registry result = new Registry(); - result.Worlds = _data.GetU64(offset); - offset += 8; - return result; - } - } - - public partial class World - { - public static ulong ACCOUNT_DISCRIMINATOR => 8978805993381703057UL; - public static ReadOnlySpan ACCOUNT_DISCRIMINATOR_BYTES => new byte[]{145, 45, 170, 174, 122, 32, 155, 124}; - public static string ACCOUNT_DISCRIMINATOR_B58 => "RHQudtaQtu1"; - public ulong Id { get; set; } - - public ulong Entities { get; set; } - - public PublicKey[] Authorities { get; set; } - - public bool Permissionless { get; set; } - - public byte[] Systems { get; set; } - - public static World Deserialize(ReadOnlySpan _data) - { - int offset = 0; - ulong accountHashValue = _data.GetU64(offset); - offset += 8; - if (accountHashValue != ACCOUNT_DISCRIMINATOR) - { - return null; - } - - World result = new World(); - result.Id = _data.GetU64(offset); - offset += 8; - result.Entities = _data.GetU64(offset); - offset += 8; - int resultAuthoritiesLength = (int)_data.GetU32(offset); - offset += 4; - result.Authorities = new PublicKey[resultAuthoritiesLength]; - for (uint resultAuthoritiesIdx = 0; resultAuthoritiesIdx < resultAuthoritiesLength; resultAuthoritiesIdx++) - { - result.Authorities[resultAuthoritiesIdx] = _data.GetPubKey(offset); - offset += 32; - } - - result.Permissionless = _data.GetBool(offset); - offset += 1; - int resultSystemsLength = (int)_data.GetU32(offset); - offset += 4; - result.Systems = _data.GetBytes(offset, resultSystemsLength); - offset += resultSystemsLength; - return result; - } - } - } - - namespace Errors - { - public enum WorldErrorKind : uint - { - InvalidAuthority = 6000U, - InvalidSystemOutput = 6001U, - WorldAccountMismatch = 6002U, - TooManyAuthorities = 6003U, - AuthorityNotFound = 6004U, - SystemNotApproved = 6005U - } - } - - namespace Types - { - } - - public partial class WorldClient : TransactionalBaseClient - { - public WorldClient(IRpcClient rpcClient, IStreamingRpcClient streamingRpcClient, PublicKey programId = null) : base(rpcClient, streamingRpcClient, programId ?? new PublicKey(WorldProgram.ID)) - { - } - - public async Task>> GetEntitysAsync(string programAddress = WorldProgram.ID, Commitment commitment = Commitment.Confirmed) - { - var list = new List{new Solana.Unity.Rpc.Models.MemCmp{Bytes = Entity.ACCOUNT_DISCRIMINATOR_B58, Offset = 0}}; - var res = await RpcClient.GetProgramAccountsAsync(programAddress, commitment, memCmpList: list); - if (!res.WasSuccessful || !(res.Result?.Count > 0)) - return new Solana.Unity.Programs.Models.ProgramAccountsResultWrapper>(res); - List resultingAccounts = new List(res.Result.Count); - resultingAccounts.AddRange(res.Result.Select(result => Entity.Deserialize(Convert.FromBase64String(result.Account.Data[0])))); - return new Solana.Unity.Programs.Models.ProgramAccountsResultWrapper>(res, resultingAccounts); - } - - public async Task>> GetRegistrysAsync(string programAddress = WorldProgram.ID, Commitment commitment = Commitment.Confirmed) - { - var list = new List{new Solana.Unity.Rpc.Models.MemCmp{Bytes = Registry.ACCOUNT_DISCRIMINATOR_B58, Offset = 0}}; - var res = await RpcClient.GetProgramAccountsAsync(programAddress, commitment, memCmpList: list); - if (!res.WasSuccessful || !(res.Result?.Count > 0)) - return new Solana.Unity.Programs.Models.ProgramAccountsResultWrapper>(res); - List resultingAccounts = new List(res.Result.Count); - resultingAccounts.AddRange(res.Result.Select(result => Registry.Deserialize(Convert.FromBase64String(result.Account.Data[0])))); - return new Solana.Unity.Programs.Models.ProgramAccountsResultWrapper>(res, resultingAccounts); - } - - public async Task>> GetWorldsAsync(string programAddress = WorldProgram.ID, Commitment commitment = Commitment.Confirmed) - { - var list = new List{new Solana.Unity.Rpc.Models.MemCmp{Bytes = World.Accounts.World.ACCOUNT_DISCRIMINATOR_B58, Offset = 0}}; - var res = await RpcClient.GetProgramAccountsAsync(programAddress, commitment, memCmpList: list); - if (!res.WasSuccessful || !(res.Result?.Count > 0)) - return new Solana.Unity.Programs.Models.ProgramAccountsResultWrapper>(res); - List resultingAccounts = new List(res.Result.Count); - resultingAccounts.AddRange(res.Result.Select(result => World.Accounts.World.Deserialize(Convert.FromBase64String(result.Account.Data[0])))); - return new Solana.Unity.Programs.Models.ProgramAccountsResultWrapper>(res, resultingAccounts); - } - - public async Task> GetEntityAsync(string accountAddress, Commitment commitment = Commitment.Finalized) - { - var res = await RpcClient.GetAccountInfoAsync(accountAddress, commitment); - if (!res.WasSuccessful) - return new Solana.Unity.Programs.Models.AccountResultWrapper(res); - var resultingAccount = Entity.Deserialize(Convert.FromBase64String(res.Result.Value.Data[0])); - return new Solana.Unity.Programs.Models.AccountResultWrapper(res, resultingAccount); - } - - public async Task> GetRegistryAsync(string accountAddress, Commitment commitment = Commitment.Finalized) - { - var res = await RpcClient.GetAccountInfoAsync(accountAddress, commitment); - if (!res.WasSuccessful) - return new Solana.Unity.Programs.Models.AccountResultWrapper(res); - var resultingAccount = Registry.Deserialize(Convert.FromBase64String(res.Result.Value.Data[0])); - return new Solana.Unity.Programs.Models.AccountResultWrapper(res, resultingAccount); - } - - public async Task> GetWorldAsync(string accountAddress, Commitment commitment = Commitment.Finalized) - { - var res = await RpcClient.GetAccountInfoAsync(accountAddress, commitment); - if (!res.WasSuccessful) - return new Solana.Unity.Programs.Models.AccountResultWrapper(res); - var resultingAccount = World.Accounts.World.Deserialize(Convert.FromBase64String(res.Result.Value.Data[0])); - return new Solana.Unity.Programs.Models.AccountResultWrapper(res, resultingAccount); - } - - public async Task SubscribeEntityAsync(string accountAddress, Action, Entity> callback, Commitment commitment = Commitment.Finalized) - { - SubscriptionState res = await StreamingRpcClient.SubscribeAccountInfoAsync(accountAddress, (s, e) => - { - Entity parsingResult = null; - if (e.Value?.Data?.Count > 0) - parsingResult = Entity.Deserialize(Convert.FromBase64String(e.Value.Data[0])); - callback(s, e, parsingResult); - }, commitment); - return res; - } - - public async Task SubscribeRegistryAsync(string accountAddress, Action, Registry> callback, Commitment commitment = Commitment.Finalized) - { - SubscriptionState res = await StreamingRpcClient.SubscribeAccountInfoAsync(accountAddress, (s, e) => - { - Registry parsingResult = null; - if (e.Value?.Data?.Count > 0) - parsingResult = Registry.Deserialize(Convert.FromBase64String(e.Value.Data[0])); - callback(s, e, parsingResult); - }, commitment); - return res; - } - - public async Task SubscribeWorldAsync(string accountAddress, Action, World.Accounts.World> callback, Commitment commitment = Commitment.Finalized) - { - SubscriptionState res = await StreamingRpcClient.SubscribeAccountInfoAsync(accountAddress, (s, e) => - { - World.Accounts.World parsingResult = null; - if (e.Value?.Data?.Count > 0) - parsingResult = World.Accounts.World.Deserialize(Convert.FromBase64String(e.Value.Data[0])); - callback(s, e, parsingResult); - }, commitment); - return res; - } - - protected override Dictionary> BuildErrorsDictionary() - { - return new Dictionary>{{6000U, new ProgramError(WorldErrorKind.InvalidAuthority, "Invalid authority for instruction")}, {6001U, new ProgramError(WorldErrorKind.InvalidSystemOutput, "Invalid system output")}, {6002U, new ProgramError(WorldErrorKind.WorldAccountMismatch, "The provided world account does not match the expected PDA.")}, {6003U, new ProgramError(WorldErrorKind.TooManyAuthorities, "Exceed the maximum number of authorities.")}, {6004U, new ProgramError(WorldErrorKind.AuthorityNotFound, "The provided authority not found")}, {6005U, new ProgramError(WorldErrorKind.SystemNotApproved, "The system is not approved in this world instance")}, }; - } - } - - namespace Program - { - public class AddAuthorityAccounts - { - public PublicKey Authority { get; set; } - - public PublicKey NewAuthority { get; set; } - - public PublicKey World { get; set; } - - public PublicKey SystemProgram { get; set; } = new PublicKey("11111111111111111111111111111111"); - } - - public class AddEntityAccounts - { - public PublicKey Payer { get; set; } - - public PublicKey Entity { get; set; } - - public PublicKey World { get; set; } - - public PublicKey SystemProgram { get; set; } = new PublicKey("11111111111111111111111111111111"); - } - - public class ApplyAccounts - { - public PublicKey BoltSystem { get; set; } - - public PublicKey Authority { get; set; } - - public PublicKey InstructionSysvarAccount { get; set; } = new PublicKey("Sysvar1nstructions1111111111111111111111111"); - public PublicKey World { get; set; } - } - - public class ApplyWithSessionAccounts - { - public PublicKey BoltSystem { get; set; } - - public PublicKey Authority { get; set; } - - public PublicKey InstructionSysvarAccount { get; set; } = new PublicKey("Sysvar1nstructions1111111111111111111111111"); - public PublicKey World { get; set; } - - public PublicKey SessionToken { get; set; } - } - - public class ApproveSystemAccounts - { - public PublicKey Authority { get; set; } - - public PublicKey World { get; set; } - - public PublicKey System { get; set; } - - public PublicKey SystemProgram { get; set; } = new PublicKey("11111111111111111111111111111111"); - } - - public class DestroyComponentAccounts - { - public PublicKey Authority { get; set; } - - public PublicKey Receiver { get; set; } - - public PublicKey ComponentProgram { get; set; } - - public PublicKey ComponentProgramData { get; set; } - - public PublicKey Entity { get; set; } - - public PublicKey Component { get; set; } - - public PublicKey InstructionSysvarAccount { get; set; } = new PublicKey("Sysvar1nstructions1111111111111111111111111"); - public PublicKey SystemProgram { get; set; } = new PublicKey("11111111111111111111111111111111"); - } - - public class InitializeComponentAccounts - { - public PublicKey Payer { get; set; } - - public PublicKey Data { get; set; } - - public PublicKey Entity { get; set; } - - public PublicKey ComponentProgram { get; set; } - - public PublicKey Authority { get; set; } - - public PublicKey InstructionSysvarAccount { get; set; } = new PublicKey("Sysvar1nstructions1111111111111111111111111"); - public PublicKey SystemProgram { get; set; } = new PublicKey("11111111111111111111111111111111"); - } - - public class InitializeNewWorldAccounts - { - public PublicKey Payer { get; set; } - - public PublicKey World { get; set; } - - public PublicKey Registry { get; set; } - - public PublicKey SystemProgram { get; set; } = new PublicKey("11111111111111111111111111111111"); - } - - public class InitializeRegistryAccounts - { - public PublicKey Registry { get; set; } - - public PublicKey Payer { get; set; } - - public PublicKey SystemProgram { get; set; } = new PublicKey("11111111111111111111111111111111"); - } - - public class RemoveAuthorityAccounts - { - public PublicKey Authority { get; set; } - - public PublicKey AuthorityToDelete { get; set; } - - public PublicKey World { get; set; } - - public PublicKey SystemProgram { get; set; } = new PublicKey("11111111111111111111111111111111"); - } - - public class RemoveSystemAccounts - { - public PublicKey Authority { get; set; } - - public PublicKey World { get; set; } - - public PublicKey System { get; set; } - - public PublicKey SystemProgram { get; set; } = new PublicKey("11111111111111111111111111111111"); - } - - public partial class WorldProgram - { - public const string ID = "WorLD15A7CrDwLcLy4fRqtaTb9fbd8o8iqiEMUDse2n"; - public static Solana.Unity.Rpc.Models.TransactionInstruction AddAuthority(AddAuthorityAccounts accounts, ulong world_id, PublicKey programId = null) - { - programId ??= new(ID); - List keys = new() - {Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Authority, true), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.NewAuthority, false), Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.World, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.SystemProgram, false)}; - byte[] _data = new byte[1200]; - int offset = 0; - _data.WriteU64(13217455069452700133UL, offset); - offset += 8; - _data.WriteU64(world_id, offset); - offset += 8; - byte[] resultData = new byte[offset]; - Array.Copy(_data, resultData, offset); - return new Solana.Unity.Rpc.Models.TransactionInstruction{Keys = keys, ProgramId = programId.KeyBytes, Data = resultData}; - } - - public static Solana.Unity.Rpc.Models.TransactionInstruction AddEntity(AddEntityAccounts accounts, byte[] extra_seed, PublicKey programId = null) - { - programId ??= new(ID); - List keys = new() - {Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Payer, true), Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Entity, false), Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.World, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.SystemProgram, false)}; - byte[] _data = new byte[1200]; - int offset = 0; - _data.WriteU64(4121062988444201379UL, offset); - offset += 8; - if (extra_seed != null) - { - _data.WriteU8(1, offset); - offset += 1; - _data.WriteS32(extra_seed.Length, offset); - offset += 4; - _data.WriteSpan(extra_seed, offset); - offset += extra_seed.Length; - } - else - { - _data.WriteU8(0, offset); - offset += 1; - } - - byte[] resultData = new byte[offset]; - Array.Copy(_data, resultData, offset); - return new Solana.Unity.Rpc.Models.TransactionInstruction{Keys = keys, ProgramId = programId.KeyBytes, Data = resultData}; - } - - public static Solana.Unity.Rpc.Models.TransactionInstruction Apply(ApplyAccounts accounts, byte[] args, PublicKey programId = null) - { - programId ??= new(ID); - List keys = new() - {Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.BoltSystem, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.Authority, true), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.InstructionSysvarAccount, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.World, false)}; - byte[] _data = new byte[1200]; - int offset = 0; - _data.WriteU64(16258613031726085112UL, offset); - offset += 8; - _data.WriteS32(args.Length, offset); - offset += 4; - _data.WriteSpan(args, offset); - offset += args.Length; - byte[] resultData = new byte[offset]; - Array.Copy(_data, resultData, offset); - return new Solana.Unity.Rpc.Models.TransactionInstruction{Keys = keys, ProgramId = programId.KeyBytes, Data = resultData}; - } - - public static Solana.Unity.Rpc.Models.TransactionInstruction ApplyWithSession(ApplyWithSessionAccounts accounts, byte[] args, PublicKey programId = null) - { - programId ??= new(ID); - List keys = new() - {Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.BoltSystem, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.Authority, true), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.InstructionSysvarAccount, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.World, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.SessionToken, false)}; - byte[] _data = new byte[1200]; - int offset = 0; - _data.WriteU64(7459768094276011477UL, offset); - offset += 8; - _data.WriteS32(args.Length, offset); - offset += 4; - _data.WriteSpan(args, offset); - offset += args.Length; - byte[] resultData = new byte[offset]; - Array.Copy(_data, resultData, offset); - return new Solana.Unity.Rpc.Models.TransactionInstruction{Keys = keys, ProgramId = programId.KeyBytes, Data = resultData}; - } - - public static Solana.Unity.Rpc.Models.TransactionInstruction ApproveSystem(ApproveSystemAccounts accounts, PublicKey programId = null) - { - programId ??= new(ID); - List keys = new() - {Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Authority, true), Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.World, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.System, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.SystemProgram, false)}; - byte[] _data = new byte[1200]; - int offset = 0; - _data.WriteU64(8777308090533520754UL, offset); - offset += 8; - byte[] resultData = new byte[offset]; - Array.Copy(_data, resultData, offset); - return new Solana.Unity.Rpc.Models.TransactionInstruction{Keys = keys, ProgramId = programId.KeyBytes, Data = resultData}; - } - - public static Solana.Unity.Rpc.Models.TransactionInstruction DestroyComponent(DestroyComponentAccounts accounts, PublicKey programId = null) - { - programId ??= new(ID); - List keys = new() - {Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Authority, true), Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Receiver, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.ComponentProgram, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.ComponentProgramData, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.Entity, false), Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Component, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.InstructionSysvarAccount, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.SystemProgram, false)}; - byte[] _data = new byte[1200]; - int offset = 0; - _data.WriteU64(5321952129328727336UL, offset); - offset += 8; - byte[] resultData = new byte[offset]; - Array.Copy(_data, resultData, offset); - return new Solana.Unity.Rpc.Models.TransactionInstruction{Keys = keys, ProgramId = programId.KeyBytes, Data = resultData}; - } - - public static Solana.Unity.Rpc.Models.TransactionInstruction InitializeComponent(InitializeComponentAccounts accounts, PublicKey programId = null) - { - programId ??= new(ID); - List keys = new() - {Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Payer, true), Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Data, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.Entity, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.ComponentProgram, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.Authority, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.InstructionSysvarAccount, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.SystemProgram, false)}; - byte[] _data = new byte[1200]; - int offset = 0; - _data.WriteU64(2179155133888827172UL, offset); - offset += 8; - byte[] resultData = new byte[offset]; - Array.Copy(_data, resultData, offset); - return new Solana.Unity.Rpc.Models.TransactionInstruction{Keys = keys, ProgramId = programId.KeyBytes, Data = resultData}; - } - - public static Solana.Unity.Rpc.Models.TransactionInstruction InitializeNewWorld(InitializeNewWorldAccounts accounts, PublicKey programId = null) - { - programId ??= new(ID); - List keys = new() - {Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Payer, true), Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.World, false), Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Registry, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.SystemProgram, false)}; - byte[] _data = new byte[1200]; - int offset = 0; - _data.WriteU64(7118163274173538327UL, offset); - offset += 8; - byte[] resultData = new byte[offset]; - Array.Copy(_data, resultData, offset); - return new Solana.Unity.Rpc.Models.TransactionInstruction{Keys = keys, ProgramId = programId.KeyBytes, Data = resultData}; - } - - public static Solana.Unity.Rpc.Models.TransactionInstruction InitializeRegistry(InitializeRegistryAccounts accounts, PublicKey programId = null) - { - programId ??= new(ID); - List keys = new() - {Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Registry, false), Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Payer, true), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.SystemProgram, false)}; - byte[] _data = new byte[1200]; - int offset = 0; - _data.WriteU64(4321548737212364221UL, offset); - offset += 8; - byte[] resultData = new byte[offset]; - Array.Copy(_data, resultData, offset); - return new Solana.Unity.Rpc.Models.TransactionInstruction{Keys = keys, ProgramId = programId.KeyBytes, Data = resultData}; - } - - public static Solana.Unity.Rpc.Models.TransactionInstruction RemoveAuthority(RemoveAuthorityAccounts accounts, ulong world_id, PublicKey programId = null) - { - programId ??= new(ID); - List keys = new() - {Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Authority, true), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.AuthorityToDelete, false), Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.World, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.SystemProgram, false)}; - byte[] _data = new byte[1200]; - int offset = 0; - _data.WriteU64(15585545156648003826UL, offset); - offset += 8; - _data.WriteU64(world_id, offset); - offset += 8; - byte[] resultData = new byte[offset]; - Array.Copy(_data, resultData, offset); - return new Solana.Unity.Rpc.Models.TransactionInstruction{Keys = keys, ProgramId = programId.KeyBytes, Data = resultData}; - } - - public static Solana.Unity.Rpc.Models.TransactionInstruction RemoveSystem(RemoveSystemAccounts accounts, PublicKey programId = null) - { - programId ??= new(ID); - List keys = new() - {Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Authority, true), Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.World, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.System, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.SystemProgram, false)}; - byte[] _data = new byte[1200]; - int offset = 0; - _data.WriteU64(8688994685429436634UL, offset); - offset += 8; - byte[] resultData = new byte[offset]; - Array.Copy(_data, resultData, offset); - return new Solana.Unity.Rpc.Models.TransactionInstruction{Keys = keys, ProgramId = programId.KeyBytes, Data = resultData}; - } - } - } +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Threading.Tasks; +using Solana.Unity; +using Solana.Unity.Programs.Abstract; +using Solana.Unity.Programs.Utilities; +using Solana.Unity.Rpc; +using Solana.Unity.Rpc.Builders; +using Solana.Unity.Rpc.Core.Http; +using Solana.Unity.Rpc.Core.Sockets; +using Solana.Unity.Rpc.Types; +using Solana.Unity.Wallet; +using World; +using World.Program; +using World.Errors; +using World.Accounts; +using World.Types; + +namespace World +{ + namespace Accounts + { + public partial class Entity + { + public static ulong ACCOUNT_DISCRIMINATOR => 1751670451238706478UL; + public static ReadOnlySpan ACCOUNT_DISCRIMINATOR_BYTES => new byte[]{46, 157, 161, 161, 254, 46, 79, 24}; + public static string ACCOUNT_DISCRIMINATOR_B58 => "8oEQa6zH67R"; + public ulong Id { get; set; } + + public static Entity Deserialize(ReadOnlySpan _data) + { + int offset = 0; + ulong accountHashValue = _data.GetU64(offset); + offset += 8; + if (accountHashValue != ACCOUNT_DISCRIMINATOR) + { + return null; + } + + Entity result = new Entity(); + result.Id = _data.GetU64(offset); + offset += 8; + return result; + } + } + + public partial class Registry + { + public static ulong ACCOUNT_DISCRIMINATOR => 15779688099924061743UL; + public static ReadOnlySpan ACCOUNT_DISCRIMINATOR_BYTES => new byte[]{47, 174, 110, 246, 184, 182, 252, 218}; + public static string ACCOUNT_DISCRIMINATOR_B58 => "8ya1XGY4XBP"; + public ulong Worlds { get; set; } + + public static Registry Deserialize(ReadOnlySpan _data) + { + int offset = 0; + ulong accountHashValue = _data.GetU64(offset); + offset += 8; + if (accountHashValue != ACCOUNT_DISCRIMINATOR) + { + return null; + } + + Registry result = new Registry(); + result.Worlds = _data.GetU64(offset); + offset += 8; + return result; + } + } + + public partial class World + { + public static ulong ACCOUNT_DISCRIMINATOR => 8978805993381703057UL; + public static ReadOnlySpan ACCOUNT_DISCRIMINATOR_BYTES => new byte[]{145, 45, 170, 174, 122, 32, 155, 124}; + public static string ACCOUNT_DISCRIMINATOR_B58 => "RHQudtaQtu1"; + public ulong Id { get; set; } + + public ulong Entities { get; set; } + + public PublicKey[] Authorities { get; set; } + + public bool Permissionless { get; set; } + + public byte[] Systems { get; set; } + + public static World Deserialize(ReadOnlySpan _data) + { + int offset = 0; + ulong accountHashValue = _data.GetU64(offset); + offset += 8; + if (accountHashValue != ACCOUNT_DISCRIMINATOR) + { + return null; + } + + World result = new World(); + result.Id = _data.GetU64(offset); + offset += 8; + result.Entities = _data.GetU64(offset); + offset += 8; + int resultAuthoritiesLength = (int)_data.GetU32(offset); + offset += 4; + result.Authorities = new PublicKey[resultAuthoritiesLength]; + for (uint resultAuthoritiesIdx = 0; resultAuthoritiesIdx < resultAuthoritiesLength; resultAuthoritiesIdx++) + { + result.Authorities[resultAuthoritiesIdx] = _data.GetPubKey(offset); + offset += 32; + } + + result.Permissionless = _data.GetBool(offset); + offset += 1; + int resultSystemsLength = (int)_data.GetU32(offset); + offset += 4; + result.Systems = _data.GetBytes(offset, resultSystemsLength); + offset += resultSystemsLength; + return result; + } + } + } + + namespace Errors + { + public enum WorldErrorKind : uint + { + InvalidAuthority = 6000U, + InvalidSystemOutput = 6001U, + WorldAccountMismatch = 6002U, + TooManyAuthorities = 6003U, + AuthorityNotFound = 6004U, + SystemNotApproved = 6005U + } + } + + namespace Types + { + } + + public partial class WorldClient : TransactionalBaseClient + { + public WorldClient(IRpcClient rpcClient, IStreamingRpcClient streamingRpcClient, PublicKey programId = null) : base(rpcClient, streamingRpcClient, programId ?? new PublicKey(WorldProgram.ID)) + { + } + + public async Task>> GetEntitysAsync(string programAddress = WorldProgram.ID, Commitment commitment = Commitment.Confirmed) + { + var list = new List{new Solana.Unity.Rpc.Models.MemCmp{Bytes = Entity.ACCOUNT_DISCRIMINATOR_B58, Offset = 0}}; + var res = await RpcClient.GetProgramAccountsAsync(programAddress, commitment, memCmpList: list); + if (!res.WasSuccessful || !(res.Result?.Count > 0)) + return new Solana.Unity.Programs.Models.ProgramAccountsResultWrapper>(res); + List resultingAccounts = new List(res.Result.Count); + resultingAccounts.AddRange(res.Result.Select(result => Entity.Deserialize(Convert.FromBase64String(result.Account.Data[0])))); + return new Solana.Unity.Programs.Models.ProgramAccountsResultWrapper>(res, resultingAccounts); + } + + public async Task>> GetRegistrysAsync(string programAddress = WorldProgram.ID, Commitment commitment = Commitment.Confirmed) + { + var list = new List{new Solana.Unity.Rpc.Models.MemCmp{Bytes = Registry.ACCOUNT_DISCRIMINATOR_B58, Offset = 0}}; + var res = await RpcClient.GetProgramAccountsAsync(programAddress, commitment, memCmpList: list); + if (!res.WasSuccessful || !(res.Result?.Count > 0)) + return new Solana.Unity.Programs.Models.ProgramAccountsResultWrapper>(res); + List resultingAccounts = new List(res.Result.Count); + resultingAccounts.AddRange(res.Result.Select(result => Registry.Deserialize(Convert.FromBase64String(result.Account.Data[0])))); + return new Solana.Unity.Programs.Models.ProgramAccountsResultWrapper>(res, resultingAccounts); + } + + public async Task>> GetWorldsAsync(string programAddress = WorldProgram.ID, Commitment commitment = Commitment.Confirmed) + { + var list = new List{new Solana.Unity.Rpc.Models.MemCmp{Bytes = World.Accounts.World.ACCOUNT_DISCRIMINATOR_B58, Offset = 0}}; + var res = await RpcClient.GetProgramAccountsAsync(programAddress, commitment, memCmpList: list); + if (!res.WasSuccessful || !(res.Result?.Count > 0)) + return new Solana.Unity.Programs.Models.ProgramAccountsResultWrapper>(res); + List resultingAccounts = new List(res.Result.Count); + resultingAccounts.AddRange(res.Result.Select(result => World.Accounts.World.Deserialize(Convert.FromBase64String(result.Account.Data[0])))); + return new Solana.Unity.Programs.Models.ProgramAccountsResultWrapper>(res, resultingAccounts); + } + + public async Task> GetEntityAsync(string accountAddress, Commitment commitment = Commitment.Finalized) + { + var res = await RpcClient.GetAccountInfoAsync(accountAddress, commitment); + if (!res.WasSuccessful) + return new Solana.Unity.Programs.Models.AccountResultWrapper(res); + var resultingAccount = Entity.Deserialize(Convert.FromBase64String(res.Result.Value.Data[0])); + return new Solana.Unity.Programs.Models.AccountResultWrapper(res, resultingAccount); + } + + public async Task> GetRegistryAsync(string accountAddress, Commitment commitment = Commitment.Finalized) + { + var res = await RpcClient.GetAccountInfoAsync(accountAddress, commitment); + if (!res.WasSuccessful) + return new Solana.Unity.Programs.Models.AccountResultWrapper(res); + var resultingAccount = Registry.Deserialize(Convert.FromBase64String(res.Result.Value.Data[0])); + return new Solana.Unity.Programs.Models.AccountResultWrapper(res, resultingAccount); + } + + public async Task> GetWorldAsync(string accountAddress, Commitment commitment = Commitment.Finalized) + { + var res = await RpcClient.GetAccountInfoAsync(accountAddress, commitment); + if (!res.WasSuccessful) + return new Solana.Unity.Programs.Models.AccountResultWrapper(res); + var resultingAccount = World.Accounts.World.Deserialize(Convert.FromBase64String(res.Result.Value.Data[0])); + return new Solana.Unity.Programs.Models.AccountResultWrapper(res, resultingAccount); + } + + public async Task SubscribeEntityAsync(string accountAddress, Action, Entity> callback, Commitment commitment = Commitment.Finalized) + { + SubscriptionState res = await StreamingRpcClient.SubscribeAccountInfoAsync(accountAddress, (s, e) => + { + Entity parsingResult = null; + if (e.Value?.Data?.Count > 0) + parsingResult = Entity.Deserialize(Convert.FromBase64String(e.Value.Data[0])); + callback(s, e, parsingResult); + }, commitment); + return res; + } + + public async Task SubscribeRegistryAsync(string accountAddress, Action, Registry> callback, Commitment commitment = Commitment.Finalized) + { + SubscriptionState res = await StreamingRpcClient.SubscribeAccountInfoAsync(accountAddress, (s, e) => + { + Registry parsingResult = null; + if (e.Value?.Data?.Count > 0) + parsingResult = Registry.Deserialize(Convert.FromBase64String(e.Value.Data[0])); + callback(s, e, parsingResult); + }, commitment); + return res; + } + + public async Task SubscribeWorldAsync(string accountAddress, Action, World.Accounts.World> callback, Commitment commitment = Commitment.Finalized) + { + SubscriptionState res = await StreamingRpcClient.SubscribeAccountInfoAsync(accountAddress, (s, e) => + { + World.Accounts.World parsingResult = null; + if (e.Value?.Data?.Count > 0) + parsingResult = World.Accounts.World.Deserialize(Convert.FromBase64String(e.Value.Data[0])); + callback(s, e, parsingResult); + }, commitment); + return res; + } + + protected override Dictionary> BuildErrorsDictionary() + { + return new Dictionary>{{6000U, new ProgramError(WorldErrorKind.InvalidAuthority, "Invalid authority for instruction")}, {6001U, new ProgramError(WorldErrorKind.InvalidSystemOutput, "Invalid system output")}, {6002U, new ProgramError(WorldErrorKind.WorldAccountMismatch, "The provided world account does not match the expected PDA.")}, {6003U, new ProgramError(WorldErrorKind.TooManyAuthorities, "Exceed the maximum number of authorities.")}, {6004U, new ProgramError(WorldErrorKind.AuthorityNotFound, "The provided authority not found")}, {6005U, new ProgramError(WorldErrorKind.SystemNotApproved, "The system is not approved in this world instance")}, }; + } + } + + namespace Program + { + public class AddAuthorityAccounts + { + public PublicKey Authority { get; set; } + + public PublicKey NewAuthority { get; set; } + + public PublicKey World { get; set; } + + public PublicKey SystemProgram { get; set; } = new PublicKey("11111111111111111111111111111111"); + } + + public class AddEntityAccounts + { + public PublicKey Payer { get; set; } + + public PublicKey Entity { get; set; } + + public PublicKey World { get; set; } + + public PublicKey SystemProgram { get; set; } = new PublicKey("11111111111111111111111111111111"); + } + + public class ApplyAccounts + { + public PublicKey BoltSystem { get; set; } + + public PublicKey Authority { get; set; } + + public PublicKey InstructionSysvarAccount { get; set; } = new PublicKey("Sysvar1nstructions1111111111111111111111111"); + public PublicKey World { get; set; } + } + + public class ApplyWithSessionAccounts + { + public PublicKey BoltSystem { get; set; } + + public PublicKey Authority { get; set; } + + public PublicKey InstructionSysvarAccount { get; set; } = new PublicKey("Sysvar1nstructions1111111111111111111111111"); + public PublicKey World { get; set; } + + public PublicKey SessionToken { get; set; } + } + + public class ApproveSystemAccounts + { + public PublicKey Authority { get; set; } + + public PublicKey World { get; set; } + + public PublicKey System { get; set; } + + public PublicKey SystemProgram { get; set; } = new PublicKey("11111111111111111111111111111111"); + } + + public class DestroyComponentAccounts + { + public PublicKey Authority { get; set; } + + public PublicKey Receiver { get; set; } + + public PublicKey ComponentProgram { get; set; } + + public PublicKey ComponentProgramData { get; set; } + + public PublicKey Entity { get; set; } + + public PublicKey Component { get; set; } + + public PublicKey InstructionSysvarAccount { get; set; } = new PublicKey("Sysvar1nstructions1111111111111111111111111"); + public PublicKey SystemProgram { get; set; } = new PublicKey("11111111111111111111111111111111"); + } + + public class InitializeComponentAccounts + { + public PublicKey Payer { get; set; } + + public PublicKey Data { get; set; } + + public PublicKey Entity { get; set; } + + public PublicKey ComponentProgram { get; set; } + + public PublicKey Authority { get; set; } + + public PublicKey InstructionSysvarAccount { get; set; } = new PublicKey("Sysvar1nstructions1111111111111111111111111"); + public PublicKey SystemProgram { get; set; } = new PublicKey("11111111111111111111111111111111"); + } + + public class InitializeNewWorldAccounts + { + public PublicKey Payer { get; set; } + + public PublicKey World { get; set; } + + public PublicKey Registry { get; set; } + + public PublicKey SystemProgram { get; set; } = new PublicKey("11111111111111111111111111111111"); + } + + public class InitializeRegistryAccounts + { + public PublicKey Registry { get; set; } + + public PublicKey Payer { get; set; } + + public PublicKey SystemProgram { get; set; } = new PublicKey("11111111111111111111111111111111"); + } + + public class RemoveAuthorityAccounts + { + public PublicKey Authority { get; set; } + + public PublicKey AuthorityToDelete { get; set; } + + public PublicKey World { get; set; } + + public PublicKey SystemProgram { get; set; } = new PublicKey("11111111111111111111111111111111"); + } + + public class RemoveSystemAccounts + { + public PublicKey Authority { get; set; } + + public PublicKey World { get; set; } + + public PublicKey System { get; set; } + + public PublicKey SystemProgram { get; set; } = new PublicKey("11111111111111111111111111111111"); + } + + public partial class WorldProgram + { + public const string ID = "WorLD15A7CrDwLcLy4fRqtaTb9fbd8o8iqiEMUDse2n"; + public static Solana.Unity.Rpc.Models.TransactionInstruction AddAuthority(AddAuthorityAccounts accounts, ulong world_id, PublicKey programId = null) + { + programId ??= new(ID); + List keys = new() + {Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Authority, true), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.NewAuthority, false), Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.World, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.SystemProgram, false)}; + byte[] _data = new byte[1200]; + int offset = 0; + _data.WriteU64(13217455069452700133UL, offset); + offset += 8; + _data.WriteU64(world_id, offset); + offset += 8; + byte[] resultData = new byte[offset]; + Array.Copy(_data, resultData, offset); + return new Solana.Unity.Rpc.Models.TransactionInstruction{Keys = keys, ProgramId = programId.KeyBytes, Data = resultData}; + } + + public static Solana.Unity.Rpc.Models.TransactionInstruction AddEntity(AddEntityAccounts accounts, byte[] extra_seed, PublicKey programId = null) + { + programId ??= new(ID); + List keys = new() + {Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Payer, true), Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Entity, false), Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.World, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.SystemProgram, false)}; + byte[] _data = new byte[1200]; + int offset = 0; + _data.WriteU64(4121062988444201379UL, offset); + offset += 8; + if (extra_seed != null) + { + _data.WriteU8(1, offset); + offset += 1; + _data.WriteS32(extra_seed.Length, offset); + offset += 4; + _data.WriteSpan(extra_seed, offset); + offset += extra_seed.Length; + } + else + { + _data.WriteU8(0, offset); + offset += 1; + } + + byte[] resultData = new byte[offset]; + Array.Copy(_data, resultData, offset); + return new Solana.Unity.Rpc.Models.TransactionInstruction{Keys = keys, ProgramId = programId.KeyBytes, Data = resultData}; + } + + public static Solana.Unity.Rpc.Models.TransactionInstruction Apply(ApplyAccounts accounts, byte[] args, PublicKey programId = null) + { + programId ??= new(ID); + List keys = new() + {Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.BoltSystem, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.Authority, true), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.InstructionSysvarAccount, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.World, false)}; + byte[] _data = new byte[1200]; + int offset = 0; + _data.WriteU64(16258613031726085112UL, offset); + offset += 8; + _data.WriteS32(args.Length, offset); + offset += 4; + _data.WriteSpan(args, offset); + offset += args.Length; + byte[] resultData = new byte[offset]; + Array.Copy(_data, resultData, offset); + return new Solana.Unity.Rpc.Models.TransactionInstruction{Keys = keys, ProgramId = programId.KeyBytes, Data = resultData}; + } + + public static Solana.Unity.Rpc.Models.TransactionInstruction ApplyWithSession(ApplyWithSessionAccounts accounts, byte[] args, PublicKey programId = null) + { + programId ??= new(ID); + List keys = new() + {Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.BoltSystem, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.Authority, true), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.InstructionSysvarAccount, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.World, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.SessionToken, false)}; + byte[] _data = new byte[1200]; + int offset = 0; + _data.WriteU64(7459768094276011477UL, offset); + offset += 8; + _data.WriteS32(args.Length, offset); + offset += 4; + _data.WriteSpan(args, offset); + offset += args.Length; + byte[] resultData = new byte[offset]; + Array.Copy(_data, resultData, offset); + return new Solana.Unity.Rpc.Models.TransactionInstruction{Keys = keys, ProgramId = programId.KeyBytes, Data = resultData}; + } + + public static Solana.Unity.Rpc.Models.TransactionInstruction ApproveSystem(ApproveSystemAccounts accounts, PublicKey programId = null) + { + programId ??= new(ID); + List keys = new() + {Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Authority, true), Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.World, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.System, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.SystemProgram, false)}; + byte[] _data = new byte[1200]; + int offset = 0; + _data.WriteU64(8777308090533520754UL, offset); + offset += 8; + byte[] resultData = new byte[offset]; + Array.Copy(_data, resultData, offset); + return new Solana.Unity.Rpc.Models.TransactionInstruction{Keys = keys, ProgramId = programId.KeyBytes, Data = resultData}; + } + + public static Solana.Unity.Rpc.Models.TransactionInstruction DestroyComponent(DestroyComponentAccounts accounts, PublicKey programId = null) + { + programId ??= new(ID); + List keys = new() + {Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Authority, true), Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Receiver, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.ComponentProgram, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.ComponentProgramData, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.Entity, false), Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Component, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.InstructionSysvarAccount, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.SystemProgram, false)}; + byte[] _data = new byte[1200]; + int offset = 0; + _data.WriteU64(5321952129328727336UL, offset); + offset += 8; + byte[] resultData = new byte[offset]; + Array.Copy(_data, resultData, offset); + return new Solana.Unity.Rpc.Models.TransactionInstruction{Keys = keys, ProgramId = programId.KeyBytes, Data = resultData}; + } + + public static Solana.Unity.Rpc.Models.TransactionInstruction InitializeComponent(InitializeComponentAccounts accounts, PublicKey programId = null) + { + programId ??= new(ID); + List keys = new() + {Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Payer, true), Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Data, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.Entity, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.ComponentProgram, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.Authority, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.InstructionSysvarAccount, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.SystemProgram, false)}; + byte[] _data = new byte[1200]; + int offset = 0; + _data.WriteU64(2179155133888827172UL, offset); + offset += 8; + byte[] resultData = new byte[offset]; + Array.Copy(_data, resultData, offset); + return new Solana.Unity.Rpc.Models.TransactionInstruction{Keys = keys, ProgramId = programId.KeyBytes, Data = resultData}; + } + + public static Solana.Unity.Rpc.Models.TransactionInstruction InitializeComponentWithDiscriminator(InitializeComponentAccounts accounts, byte[] discriminator, PublicKey programId = null) + { + byte[] IX_INITIALIZE_COMPONENT_WITH_DISCRIMINATOR = new byte[] { 174, 196, 222, 15, 149, 54, 137, 23 }; + programId ??= new(ID); + List keys = new() + {Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Payer, true), Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Data, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.Entity, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.ComponentProgram, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.Authority, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.InstructionSysvarAccount, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.SystemProgram, false)}; + byte[] _data = new byte[1200]; + int offset = 0; + _data.WriteSpan(IX_INITIALIZE_COMPONENT_WITH_DISCRIMINATOR, offset); + offset += 8; + _data.WriteS32(discriminator.Length, offset); + offset += 4; + _data.WriteSpan(discriminator, offset); + offset += discriminator.Length; + byte[] resultData = new byte[offset]; + Array.Copy(_data, resultData, offset); + return new Solana.Unity.Rpc.Models.TransactionInstruction{Keys = keys, ProgramId = programId.KeyBytes, Data = resultData}; + } + + public static Solana.Unity.Rpc.Models.TransactionInstruction InitializeNewWorld(InitializeNewWorldAccounts accounts, PublicKey programId = null) + { + programId ??= new(ID); + List keys = new() + {Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Payer, true), Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.World, false), Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Registry, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.SystemProgram, false)}; + byte[] _data = new byte[1200]; + int offset = 0; + _data.WriteU64(7118163274173538327UL, offset); + offset += 8; + byte[] resultData = new byte[offset]; + Array.Copy(_data, resultData, offset); + return new Solana.Unity.Rpc.Models.TransactionInstruction{Keys = keys, ProgramId = programId.KeyBytes, Data = resultData}; + } + + public static Solana.Unity.Rpc.Models.TransactionInstruction InitializeRegistry(InitializeRegistryAccounts accounts, PublicKey programId = null) + { + programId ??= new(ID); + List keys = new() + {Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Registry, false), Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Payer, true), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.SystemProgram, false)}; + byte[] _data = new byte[1200]; + int offset = 0; + _data.WriteU64(4321548737212364221UL, offset); + offset += 8; + byte[] resultData = new byte[offset]; + Array.Copy(_data, resultData, offset); + return new Solana.Unity.Rpc.Models.TransactionInstruction{Keys = keys, ProgramId = programId.KeyBytes, Data = resultData}; + } + + public static Solana.Unity.Rpc.Models.TransactionInstruction RemoveAuthority(RemoveAuthorityAccounts accounts, ulong world_id, PublicKey programId = null) + { + programId ??= new(ID); + List keys = new() + {Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Authority, true), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.AuthorityToDelete, false), Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.World, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.SystemProgram, false)}; + byte[] _data = new byte[1200]; + int offset = 0; + _data.WriteU64(15585545156648003826UL, offset); + offset += 8; + _data.WriteU64(world_id, offset); + offset += 8; + byte[] resultData = new byte[offset]; + Array.Copy(_data, resultData, offset); + return new Solana.Unity.Rpc.Models.TransactionInstruction{Keys = keys, ProgramId = programId.KeyBytes, Data = resultData}; + } + + public static Solana.Unity.Rpc.Models.TransactionInstruction RemoveSystem(RemoveSystemAccounts accounts, PublicKey programId = null) + { + programId ??= new(ID); + List keys = new() + {Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.Authority, true), Solana.Unity.Rpc.Models.AccountMeta.Writable(accounts.World, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.System, false), Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.SystemProgram, false)}; + byte[] _data = new byte[1200]; + int offset = 0; + _data.WriteU64(8688994685429436634UL, offset); + offset += 8; + byte[] resultData = new byte[offset]; + Array.Copy(_data, resultData, offset); + return new Solana.Unity.Rpc.Models.TransactionInstruction{Keys = keys, ProgramId = programId.KeyBytes, Data = resultData}; + } + } + } } \ No newline at end of file diff --git a/clients/csharp/Solana.Unity.Bolt/WorldProgram/World.cs b/clients/csharp/Solana.Unity.Bolt/WorldProgram/World.cs index 8334073c..885b26c3 100644 --- a/clients/csharp/Solana.Unity.Bolt/WorldProgram/World.cs +++ b/clients/csharp/Solana.Unity.Bolt/WorldProgram/World.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text; using Solana.Unity.Programs; +using Solana.Unity.Programs.Utilities; using Solana.Unity.Wallet; using Solana.Unity.Rpc.Models; using GplSession.Program; @@ -15,6 +16,11 @@ namespace Program { public partial class WorldProgram { + public static readonly PublicKey CpiAuthAddress = new("B2f2y3QTBv346wE6nWKor72AUhUvFF6mPk7TWCF2QVhi"); + private static readonly byte[] IX_APPLY = new byte[] { 248, 243, 145, 24, 105, 50, 162, 225 }; + private static readonly byte[] IX_APPLY_WITH_DISCRIMINATOR = new byte[] { 126, 75, 184, 115, 193, 245, 69, 15 }; + private static readonly byte[] IX_APPLY_WITH_SESSION = new byte[] { 213, 69, 29, 230, 142, 107, 134, 103 }; + private static readonly byte[] IX_APPLY_WITH_SESSION_AND_DISCRIMINATOR = new byte[] { 156, 187, 1, 148, 179, 240, 139, 27 }; public static Solana.Unity.Rpc.Models.TransactionInstruction AddEntity(AddEntityAccounts accounts, PublicKey programId = null) { programId ??= new(ID); @@ -242,13 +248,63 @@ public static Solana.Unity.Rpc.Models.TransactionInstruction ApplySystem( instruction.Keys.Add(AccountMeta.Writable(componentPdas[i], false)); } - if (componentIds.Count > 0) { - // program id delimits the end of the component list - instruction.Keys.Add(AccountMeta.ReadOnly(new PublicKey(WorldProgram.ID), false)); - } - return instruction; } + + public static Solana.Unity.Rpc.Models.TransactionInstruction ApplyWithDiscriminator(ApplyAccounts accounts, byte[] system_discriminator, byte[] args, PublicKey programId = null) + { + programId ??= new(ID); + List keys = new() + { + Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.BoltSystem, false), + Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.Authority, true), + Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.InstructionSysvarAccount, false), + Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.World, false) + }; + byte[] _data = new byte[1200]; + int offset = 0; + _data.WriteSpan(IX_APPLY_WITH_DISCRIMINATOR, offset); + offset += 8; + _data.WriteS32(system_discriminator.Length, offset); + offset += 4; + _data.WriteSpan(system_discriminator, offset); + offset += system_discriminator.Length; + _data.WriteS32(args.Length, offset); + offset += 4; + _data.WriteSpan(args, offset); + offset += args.Length; + byte[] resultData = new byte[offset]; + Array.Copy(_data, resultData, offset); + return new Solana.Unity.Rpc.Models.TransactionInstruction{Keys = keys, ProgramId = programId.KeyBytes, Data = resultData}; + } + + public static Solana.Unity.Rpc.Models.TransactionInstruction ApplyWithSessionAndDiscriminator(ApplyWithSessionAccounts accounts, byte[] system_discriminator, byte[] args, PublicKey programId = null) + { + programId ??= new(ID); + List keys = new() + { + Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.BoltSystem, false), + Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.Authority, true), + Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.InstructionSysvarAccount, false), + Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.World, false), + Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(accounts.SessionToken, false) + }; + byte[] _data = new byte[1200]; + int offset = 0; + _data.WriteSpan(IX_APPLY_WITH_SESSION_AND_DISCRIMINATOR, offset); + offset += 8; + _data.WriteS32(system_discriminator.Length, offset); + offset += 4; + _data.WriteSpan(system_discriminator, offset); + offset += system_discriminator.Length; + _data.WriteS32(args.Length, offset); + offset += 4; + _data.WriteSpan(args, offset); + offset += args.Length; + byte[] resultData = new byte[offset]; + Array.Copy(_data, resultData, offset); + return new Solana.Unity.Rpc.Models.TransactionInstruction{Keys = keys, ProgramId = programId.KeyBytes, Data = resultData}; + } } } } \ No newline at end of file diff --git a/clients/typescript/src/delegation/delegate.ts b/clients/typescript/src/delegation/delegate.ts index 017e442c..a35cb1f5 100644 --- a/clients/typescript/src/delegation/delegate.ts +++ b/clients/typescript/src/delegation/delegate.ts @@ -7,7 +7,7 @@ import { delegationMetadataPdaFromDelegatedAccount, delegationRecordPdaFromDelegatedAccount, } from "@magicblock-labs/ephemeral-rollups-sdk"; -import { FindComponentPda } from "../index"; +import { Component } from "../index"; import { type PublicKey, Transaction, @@ -164,7 +164,7 @@ export async function DelegateComponent({ }: { payer: PublicKey; entity: PublicKey; - componentId: PublicKey; + componentId: PublicKey | Component; seed?: string; buffer?: web3.PublicKey; delegationRecord?: web3.PublicKey; @@ -176,12 +176,14 @@ export async function DelegateComponent({ transaction: Transaction; componentPda: PublicKey; }> { - const componentPda = FindComponentPda({ componentId, entity, seed }); + const component = Component.from(componentId); + let ownerProgram = component.program; + const componentPda = component.pda(entity, seed); const delegateComponentIx = createDelegateInstruction({ payer, entity, account: componentPda, - ownerProgram: componentId, + ownerProgram, buffer, delegationRecord, delegationMetadata, diff --git a/clients/typescript/src/ecs/component.ts b/clients/typescript/src/ecs/component.ts new file mode 100644 index 00000000..ae4fc3bf --- /dev/null +++ b/clients/typescript/src/ecs/component.ts @@ -0,0 +1,27 @@ +import { PublicKey } from "@solana/web3.js"; +import { Identifier } from "./identifier"; +import { FindComponentPda } from "../index"; + +export class Component extends Identifier { + constructor(program: PublicKey, name?: string) { + super(program, name); + } + + static from(componentId: PublicKey | Component): Component { + return componentId instanceof Component + ? componentId + : new Component(componentId); + } + + pda(entity: PublicKey, seed?: string): PublicKey { + return FindComponentPda({ + componentId: this.program, + entity, + seed: this.seeds(seed), + }); + } + + seeds(seed?: string): string { + return (this.name ?? "") + (seed ?? ""); + } +} diff --git a/clients/typescript/src/ecs/identifier.ts b/clients/typescript/src/ecs/identifier.ts new file mode 100644 index 00000000..d100255c --- /dev/null +++ b/clients/typescript/src/ecs/identifier.ts @@ -0,0 +1,18 @@ +import { PublicKey } from "@solana/web3.js"; +import { GetDiscriminator } from "../index"; + +export class Identifier { + public program: PublicKey; + public name?: string; + + constructor(program: PublicKey, name?: string) { + this.program = program; + this.name = name; + } + + getMethodDiscriminator(method: string): Buffer { + return GetDiscriminator( + "global:" + (this.name ? this.name + "_" : "") + method, + ); + } +} diff --git a/clients/typescript/src/ecs/index.ts b/clients/typescript/src/ecs/index.ts new file mode 100644 index 00000000..0648bb55 --- /dev/null +++ b/clients/typescript/src/ecs/index.ts @@ -0,0 +1,2 @@ +export { Component } from "./component"; +export { System } from "./system"; diff --git a/clients/typescript/src/ecs/system.ts b/clients/typescript/src/ecs/system.ts new file mode 100644 index 00000000..ab4fa968 --- /dev/null +++ b/clients/typescript/src/ecs/system.ts @@ -0,0 +1,12 @@ +import { PublicKey } from "@solana/web3.js"; +import { Identifier } from "./identifier"; + +export class System extends Identifier { + constructor(program: PublicKey, name?: string) { + super(program, name); + } + + static from(systemId: PublicKey | System): System { + return systemId instanceof System ? systemId : new System(systemId); + } +} diff --git a/clients/typescript/src/generated/idl/world.json b/clients/typescript/src/generated/idl/world.json index 6e928b46..a12c3555 100644 --- a/clients/typescript/src/generated/idl/world.json +++ b/clients/typescript/src/generated/idl/world.json @@ -138,6 +138,45 @@ } ] }, + { + "name": "apply_with_discriminator", + "discriminator": [ + 126, + 75, + 184, + 115, + 193, + 245, + 69, + 15 + ], + "accounts": [ + { + "name": "bolt_system" + }, + { + "name": "authority", + "signer": true + }, + { + "name": "instruction_sysvar_account", + "address": "Sysvar1nstructions1111111111111111111111111" + }, + { + "name": "world" + } + ], + "args": [ + { + "name": "system_discriminator", + "type": "bytes" + }, + { + "name": "args", + "type": "bytes" + } + ] + }, { "name": "apply_with_session", "discriminator": [ @@ -176,6 +215,48 @@ } ] }, + { + "name": "apply_with_session_and_discriminator", + "discriminator": [ + 156, + 187, + 1, + 148, + 179, + 240, + 139, + 27 + ], + "accounts": [ + { + "name": "bolt_system" + }, + { + "name": "authority", + "signer": true + }, + { + "name": "instruction_sysvar_account", + "address": "Sysvar1nstructions1111111111111111111111111" + }, + { + "name": "world" + }, + { + "name": "session_token" + } + ], + "args": [ + { + "name": "system_discriminator", + "type": "bytes" + }, + { + "name": "args", + "type": "bytes" + } + ] + }, { "name": "approve_system", "discriminator": [ @@ -254,6 +335,57 @@ ], "args": [] }, + { + "name": "destroy_component_with_discriminator", + "discriminator": [ + 71, + 25, + 153, + 201, + 108, + 92, + 114, + 125 + ], + "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": "discriminator", + "type": "bytes" + } + ] + }, { "name": "initialize_component", "discriminator": [ @@ -296,6 +428,53 @@ ], "args": [] }, + { + "name": "initialize_component_with_discriminator", + "discriminator": [ + 174, + 196, + 222, + 15, + 149, + 54, + 137, + 23 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "data", + "writable": true + }, + { + "name": "entity" + }, + { + "name": "component_program" + }, + { + "name": "authority" + }, + { + "name": "instruction_sysvar_account", + "address": "Sysvar1nstructions1111111111111111111111111" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "discriminator", + "type": "bytes" + } + ] + }, { "name": "initialize_new_world", "discriminator": [ diff --git a/clients/typescript/src/generated/types/world.ts b/clients/typescript/src/generated/types/world.ts index e3b6ea2a..b7e5e3e8 100644 --- a/clients/typescript/src/generated/types/world.ts +++ b/clients/typescript/src/generated/types/world.ts @@ -111,6 +111,36 @@ export type World = { }, ]; }, + { + name: "applyWithDiscriminator"; + discriminator: [126, 75, 184, 115, 193, 245, 69, 15]; + accounts: [ + { + name: "boltSystem"; + }, + { + name: "authority"; + signer: true; + }, + { + name: "instructionSysvarAccount"; + address: "Sysvar1nstructions1111111111111111111111111"; + }, + { + name: "world"; + }, + ]; + args: [ + { + name: "systemDiscriminator"; + type: "bytes"; + }, + { + name: "args"; + type: "bytes"; + }, + ]; + }, { name: "applyWithSession"; discriminator: [213, 69, 29, 230, 142, 107, 134, 103]; @@ -140,6 +170,39 @@ export type World = { }, ]; }, + { + name: "applyWithSessionAndDiscriminator"; + discriminator: [156, 187, 1, 148, 179, 240, 139, 27]; + accounts: [ + { + name: "boltSystem"; + }, + { + name: "authority"; + signer: true; + }, + { + name: "instructionSysvarAccount"; + address: "Sysvar1nstructions1111111111111111111111111"; + }, + { + name: "world"; + }, + { + name: "sessionToken"; + }, + ]; + args: [ + { + name: "systemDiscriminator"; + type: "bytes"; + }, + { + name: "args"; + type: "bytes"; + }, + ]; + }, { name: "approveSystem"; discriminator: [114, 165, 105, 68, 52, 67, 207, 121]; @@ -200,6 +263,48 @@ export type World = { ]; args: []; }, + { + name: "destroyComponentWithDiscriminator"; + discriminator: [71, 25, 153, 201, 108, 92, 114, 125]; + 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: "discriminator"; + type: "bytes"; + }, + ]; + }, { name: "initializeComponent"; discriminator: [36, 143, 233, 113, 12, 234, 61, 30]; @@ -233,6 +338,44 @@ export type World = { ]; args: []; }, + { + name: "initializeComponentWithDiscriminator"; + discriminator: [174, 196, 222, 15, 149, 54, 137, 23]; + accounts: [ + { + name: "payer"; + writable: true; + signer: true; + }, + { + name: "data"; + writable: true; + }, + { + name: "entity"; + }, + { + name: "componentProgram"; + }, + { + name: "authority"; + }, + { + name: "instructionSysvarAccount"; + address: "Sysvar1nstructions1111111111111111111111111"; + }, + { + name: "systemProgram"; + address: "11111111111111111111111111111111"; + }, + ]; + args: [ + { + name: "discriminator"; + type: "bytes"; + }, + ]; + }, { name: "initializeNewWorld"; discriminator: [23, 96, 88, 194, 200, 203, 200, 98]; diff --git a/clients/typescript/src/index.ts b/clients/typescript/src/index.ts index 7eb33778..86507469 100644 --- a/clients/typescript/src/index.ts +++ b/clients/typescript/src/index.ts @@ -2,6 +2,7 @@ import { PublicKey } from "@solana/web3.js"; import BN from "bn.js"; import { PROGRAM_ID as WORLD_PROGRAM_ID } from "./generated"; import { World as WORLD_PROGRAM_IDL } from "./generated/types"; +import crypto from "crypto"; export { BN }; export * from "./generated/accounts"; export * from "./generated/instructions"; @@ -16,11 +17,16 @@ import { SessionProgram, Session } from "./session"; export { anchor }; export { Provider, Program, Wallet, web3, workspace } from "@coral-xyz/anchor"; export { WORLD_PROGRAM_ID, WORLD_PROGRAM_IDL }; +export { Component, System } from "./ecs"; export const SYSVAR_INSTRUCTIONS_PUBKEY = new PublicKey( "Sysvar1nstructions1111111111111111111111111", ); +export function GetDiscriminator(name: string) { + return crypto.createHash("sha256").update(name).digest().subarray(0, 8); +} + export function FindRegistryPda({ programId }: { programId?: PublicKey }) { return PublicKey.findProgramAddressSync( [Buffer.from("registry")], diff --git a/clients/typescript/src/world/transactions.ts b/clients/typescript/src/world/transactions.ts index a99919de..930178c3 100644 --- a/clients/typescript/src/world/transactions.ts +++ b/clients/typescript/src/world/transactions.ts @@ -1,7 +1,5 @@ import { - createApplyInstruction, createAddEntityInstruction, - createInitializeComponentInstruction, createInitializeNewWorldInstruction, FindComponentPda, FindEntityPda, @@ -9,7 +7,6 @@ import { FindRegistryPda, Registry, SerializeArgs, - SYSVAR_INSTRUCTIONS_PUBKEY, World, SessionProgram, Session, @@ -17,22 +14,27 @@ import { WORLD_PROGRAM_ID, BN, FindComponentProgramDataPda, + GetDiscriminator, + Component, } from "../index"; import type web3 from "@solana/web3.js"; import { type Connection, Keypair, type PublicKey, + SYSVAR_INSTRUCTIONS_PUBKEY, Transaction, type TransactionInstruction, } from "@solana/web3.js"; import type WorldProgram from "../generated"; import { + createInitializeComponentInstruction, createInitializeRegistryInstruction, PROGRAM_ID, worldIdl, } from "../generated"; import { type Idl, Program } from "@coral-xyz/anchor"; +import { System } from "../ecs"; export async function InitializeRegistry({ payer, @@ -347,26 +349,28 @@ export async function DestroyComponent({ }: { authority: PublicKey; entity: PublicKey; - componentId: PublicKey; + componentId: PublicKey | Component; receiver: PublicKey; seed?: string; }): Promise<{ instruction: TransactionInstruction; transaction: Transaction; }> { + const component = Component.from(componentId); const program = new Program( worldIdl as Idl, ) as unknown as Program; + const discriminator = component.getMethodDiscriminator("destroy"); + const componentProgram = component.program; const componentProgramData = FindComponentProgramDataPda({ - programId: componentId, + programId: componentProgram, }); - const componentProgram = componentId; - const component = FindComponentPda({ componentId, entity, seed }); + const componentPda = component.pda(entity, seed); const instruction = await program.methods - .destroyComponent() + .destroyComponentWithDiscriminator(discriminator) .accounts({ authority, - component, + component: componentPda, entity, componentProgram, componentProgramData, @@ -400,7 +404,7 @@ export async function InitializeComponent({ }: { payer: PublicKey; entity: PublicKey; - componentId: PublicKey; + componentId: PublicKey | Component; seed?: string; authority?: web3.PublicKey; anchorRemainingAccounts?: web3.AccountMeta[]; @@ -409,16 +413,26 @@ export async function InitializeComponent({ transaction: Transaction; componentPda: PublicKey; }> { - const componentPda = FindComponentPda({ componentId, entity, seed }); - const instruction = createInitializeComponentInstruction({ - payer, - entity, - data: componentPda, - componentProgram: componentId, - authority: authority ?? PROGRAM_ID, - instructionSysvarAccount: SYSVAR_INSTRUCTIONS_PUBKEY, - anchorRemainingAccounts, - }); + const component = Component.from(componentId); + const discriminator = component.getMethodDiscriminator("initialize"); + const componentProgram = component.program; + const componentPda = component.pda(entity, seed); + const program = new Program( + worldIdl as Idl, + ) as unknown as Program; + + const instruction = await program.methods + .initializeComponentWithDiscriminator(discriminator) + .accounts({ + payer, + entity, + data: componentPda, + componentProgram: componentProgram, + authority: authority ?? PROGRAM_ID, + }) + .remainingAccounts(anchorRemainingAccounts ?? []) + .instruction(); + const transaction = new Transaction().add(instruction); return { instruction, @@ -429,7 +443,7 @@ export async function InitializeComponent({ interface ApplySystemInstruction { authority: PublicKey; - systemId: PublicKey; + systemId: PublicKey | System; entities: ApplySystemEntity[]; world: PublicKey; session?: Session; @@ -445,6 +459,7 @@ async function createApplySystemInstruction({ extraAccounts, args, }: ApplySystemInstruction): Promise { + let system = System.from(systemId); const program = new Program( worldIdl as Idl, ) as unknown as Program; @@ -459,15 +474,13 @@ async function createApplySystemInstruction({ let remainingAccounts: web3.AccountMeta[] = []; let components: { id: PublicKey; pda: PublicKey }[] = []; for (const entity of entities) { - for (const component of entity.components) { - const componentPda = FindComponentPda({ - componentId: component.componentId, - entity: entity.entity, - seed: component.seed, - }); + for (const applyComponent of entity.components) { + const component = Component.from(applyComponent.componentId); + const id = component.program; + const pda = component.pda(entity.entity, applyComponent.seed); components.push({ - id: component.componentId, - pda: componentPda, + id, + pda, }); } } @@ -495,27 +508,56 @@ async function createApplySystemInstruction({ } } - if (session) - return program.methods - .applyWithSession(SerializeArgs(args)) - .accounts({ - authority: authority ?? PROGRAM_ID, - boltSystem: systemId, - sessionToken: session.token, - world, - }) - .remainingAccounts(remainingAccounts) - .instruction(); - else - return program.methods - .apply(SerializeArgs(args)) - .accounts({ - authority: authority ?? PROGRAM_ID, - boltSystem: systemId, - world, - }) - .remainingAccounts(remainingAccounts) - .instruction(); + const systemDiscriminator = system.getMethodDiscriminator("bolt_execute"); + + if (system.name != null) { + if (session) + return program.methods + .applyWithSessionAndDiscriminator( + systemDiscriminator, + SerializeArgs(args), + ) + .accounts({ + authority: authority ?? PROGRAM_ID, + boltSystem: system.program, + sessionToken: session.token, + world, + }) + .remainingAccounts(remainingAccounts) + .instruction(); + else + return program.methods + .applyWithDiscriminator(systemDiscriminator, SerializeArgs(args)) + .accounts({ + authority: authority ?? PROGRAM_ID, + boltSystem: system.program, + world, + }) + .remainingAccounts(remainingAccounts) + .instruction(); + } else { + if (session) + return program.methods + .applyWithSession(SerializeArgs(args)) + .accounts({ + authority: authority ?? PROGRAM_ID, + boltSystem: system.program, + sessionToken: session.token, + world, + }) + .remainingAccounts(remainingAccounts) + .instruction(); + else + return program.methods + .apply(SerializeArgs(args)) + .accounts({ + authority: authority ?? PROGRAM_ID, + boltSystem: system.program, + world, + }) + .remainingAccounts(remainingAccounts) + .instruction(); + } } interface ApplySystemEntity { @@ -546,7 +588,7 @@ export async function ApplySystem({ session, }: { authority: PublicKey; - systemId: PublicKey; + systemId: PublicKey | System; entities: ApplySystemEntity[]; world: PublicKey; extraAccounts?: web3.AccountMeta[]; diff --git a/clients/typescript/test/framework.ts b/clients/typescript/test/framework.ts index 962a0c25..031830e8 100644 --- a/clients/typescript/test/framework.ts +++ b/clients/typescript/test/framework.ts @@ -26,6 +26,7 @@ import { With7Components } from "../../../target/types/with_7_components"; import { With8Components } from "../../../target/types/with_8_components"; import { With9Components } from "../../../target/types/with_9_components"; import { With10Components } from "../../../target/types/with_10_components"; +import { ExampleBundle } from "../../../target/types/example_bundle"; export class Framework { provider: anchor.AnchorProvider; @@ -33,6 +34,7 @@ export class Framework { worldProgram: anchor.Program; exampleComponentPosition: anchor.Program; exampleComponentVelocity: anchor.Program; + exampleBundle: anchor.Program; systemSimpleMovement: anchor.Program; systemFly: anchor.Program; systemApplyVelocity: anchor.Program; @@ -59,7 +61,9 @@ export class Framework { acceleratedComponentPositionPda: PublicKey; componentPositionEntity1Pda: PublicKey; + bundlePositionEntity1Pda: PublicKey; componentVelocityEntity1Pda: PublicKey; + bundleVelocityEntity1Pda: PublicKey; componentPositionEntity4Pda: PublicKey; constructor() { @@ -67,6 +71,7 @@ export class Framework { this.worldProgram = anchor.workspace.World; this.exampleComponentPosition = anchor.workspace.Position; this.exampleComponentVelocity = anchor.workspace.Velocity; + this.exampleBundle = anchor.workspace.ExampleBundle; this.systemSimpleMovement = anchor.workspace.SystemSimpleMovement; this.systemFly = anchor.workspace.SystemFly; this.systemApplyVelocity = anchor.workspace.SystemApplyVelocity; diff --git a/clients/typescript/test/intermediate-level/acceleration.ts b/clients/typescript/test/intermediate-level/acceleration.ts index 9c13ddf4..cdc0cd54 100644 --- a/clients/typescript/test/intermediate-level/acceleration.ts +++ b/clients/typescript/test/intermediate-level/acceleration.ts @@ -1,6 +1,7 @@ import { AddEntity, ApplySystem, + Component, DelegateComponent, DELEGATION_PROGRAM_ID, InitializeComponent, @@ -43,6 +44,32 @@ export function acceleration(framework: Framework) { ); }); + it("Create accelerated bundled component position", async () => { + const createAcceleratedBundledComponentPosition = + await InitializeComponent({ + payer: framework.provider.wallet.publicKey, + entity: framework.acceleratedEntityPda, + componentId: new Component( + framework.exampleBundle.programId, + "position", + ), + }); + + framework.componentPositionEntity1Pda = + createAcceleratedBundledComponentPosition.componentPda; + + await framework.provider.sendAndConfirm( + createAcceleratedBundledComponentPosition.transaction, + ); + + const position = await framework.exampleBundle.account.position.fetch( + framework.componentPositionEntity1Pda, + ); + expect(position.x.toNumber()).to.equal(0); + expect(position.y.toNumber()).to.equal(0); + expect(position.z.toNumber()).to.equal(0); + }); + it("Check component delegation to accelerator", async () => { const delegateComponent = await DelegateComponent({ payer: framework.provider.wallet.publicKey, @@ -54,7 +81,6 @@ export function acceleration(framework: Framework) { delegateComponent.transaction, [], { - skipPreflight: true, commitment: "confirmed", }, ); @@ -64,6 +90,29 @@ export function acceleration(framework: Framework) { expect(acc?.owner.toBase58()).to.equal(DELEGATION_PROGRAM_ID.toBase58()); }); + it("Check bundled component position delegation to accelerator", async () => { + const delegateComponent = await DelegateComponent({ + payer: framework.provider.wallet.publicKey, + entity: framework.acceleratedEntityPda, + componentId: new Component( + framework.exampleBundle.programId, + "position", + ), + }); + await framework.provider.sendAndConfirm( + delegateComponent.transaction, + [], + { + commitment: "confirmed", + }, + ); + + const acc = await framework.provider.connection.getAccountInfo( + delegateComponent.componentPda, + ); + expect(acc?.owner.toBase58()).to.equal(DELEGATION_PROGRAM_ID.toBase58()); + }); + it("Apply Simple Movement System (Up) on Entity 1 on Accelerator 10 times", async () => { for (let i = 0; i < 10; i++) { let applySystem = await ApplySystem({ @@ -87,7 +136,6 @@ export function acceleration(framework: Framework) { applySystem.transaction, [], { - skipPreflight: true, commitment: "processed", }, ); diff --git a/clients/typescript/test/intermediate-level/ecs.ts b/clients/typescript/test/intermediate-level/ecs.ts index 7a08daf7..031bc8ed 100644 --- a/clients/typescript/test/intermediate-level/ecs.ts +++ b/clients/typescript/test/intermediate-level/ecs.ts @@ -4,6 +4,8 @@ import { ApplySystem, InitializeComponent, DestroyComponent, + Component, + System, } from "../../lib"; import { Direction, Framework } from "../framework"; import { expect } from "chai"; @@ -62,6 +64,47 @@ export function ecs(framework: Framework) { framework.componentVelocityEntity1Pda = initializeComponent.componentPda; // Saved for later }); + it("Initialize Bundled Position Component on Entity 1", async () => { + const initializeComponent = await InitializeComponent({ + payer: framework.provider.wallet.publicKey, + entity: framework.entity1Pda, + componentId: new Component( + framework.exampleBundle.programId, + "position", + ), + }); + + await framework.provider.sendAndConfirm(initializeComponent.transaction); + framework.bundlePositionEntity1Pda = initializeComponent.componentPda; // Saved for later + + const position = await framework.exampleBundle.account.position.fetch( + framework.bundlePositionEntity1Pda, + ); + expect(position.x.toNumber()).to.equal(0); + expect(position.y.toNumber()).to.equal(0); + expect(position.z.toNumber()).to.equal(0); + }); + + it("Initialize Bundled Velocity Component on Entity 1", async () => { + const initializeComponent = await InitializeComponent({ + payer: framework.provider.wallet.publicKey, + entity: framework.entity1Pda, + componentId: new Component( + framework.exampleBundle.programId, + "velocity", + ), + }); + await framework.provider.sendAndConfirm(initializeComponent.transaction); + framework.bundleVelocityEntity1Pda = initializeComponent.componentPda; // Saved for later + + const velocity = await framework.exampleBundle.account.velocity.fetch( + framework.bundleVelocityEntity1Pda, + ); + expect(velocity.x.toNumber()).to.equal(1); + expect(velocity.y.toNumber()).to.equal(2); + expect(velocity.z.toNumber()).to.equal(3); + }); + it("Initialize Position Component on Entity 1", async () => { const initializeComponent = await InitializeComponent({ payer: framework.provider.wallet.publicKey, @@ -118,9 +161,7 @@ export function ecs(framework: Framework) { direction: Direction.Up, }, }); - await framework.provider.sendAndConfirm(applySystem.transaction, [], { - skipPreflight: true, - }); + await framework.provider.sendAndConfirm(applySystem.transaction); const position = await framework.exampleComponentPosition.account.position.fetch( @@ -131,6 +172,70 @@ export function ecs(framework: Framework) { expect(position.z.toNumber()).to.equal(0); }); + it("Apply bundled movement system on Entity 1", async () => { + const applySystem = await ApplySystem({ + authority: framework.provider.wallet.publicKey, + systemId: new System(framework.exampleBundle.programId, "movement"), + world: framework.worldPda, + entities: [ + { + entity: framework.entity1Pda, + components: [ + { + componentId: new Component( + framework.exampleBundle.programId, + "position", + ), + }, + { + componentId: new Component( + framework.exampleBundle.programId, + "velocity", + ), + }, + ], + }, + ], + }); + await framework.provider.sendAndConfirm(applySystem.transaction); + + const position = await framework.exampleBundle.account.position.fetch( + framework.bundlePositionEntity1Pda, + ); + expect(position.x.toNumber()).to.equal(1); + expect(position.y.toNumber()).to.equal(2); + expect(position.z.toNumber()).to.equal(3); + }); + + it("Apply bundled stop system on Entity 1", async () => { + const applySystem = await ApplySystem({ + authority: framework.provider.wallet.publicKey, + systemId: new System(framework.exampleBundle.programId, "stop"), + world: framework.worldPda, + entities: [ + { + entity: framework.entity1Pda, + components: [ + { + componentId: new Component( + framework.exampleBundle.programId, + "velocity", + ), + }, + ], + }, + ], + }); + await framework.provider.sendAndConfirm(applySystem.transaction); + + const velocity = await framework.exampleBundle.account.velocity.fetch( + framework.bundleVelocityEntity1Pda, + ); + expect(velocity.x.toNumber()).to.equal(0); + expect(velocity.y.toNumber()).to.equal(0); + expect(velocity.z.toNumber()).to.equal(0); + }); + it("Apply Simple Movement System (Right) on Entity 1", async () => { const applySystem = await ApplySystem({ authority: framework.provider.wallet.publicKey, diff --git a/crates/bolt-cli/src/bundle.rs b/crates/bolt-cli/src/bundle.rs new file mode 100644 index 00000000..5b601134 --- /dev/null +++ b/crates/bolt-cli/src/bundle.rs @@ -0,0 +1,42 @@ +use crate::{rust_template::create_bundle, workspace::with_workspace}; +use anchor_cli::config::{ConfigOverride, ProgramDeployment}; +use anyhow::{anyhow, Result}; +use std::fs; + +// Create a new component from the template +pub fn new_bundle(cfg_override: &ConfigOverride, name: String) -> Result<()> { + with_workspace(cfg_override, |cfg| { + match cfg.path().parent() { + None => { + println!("Unable to make new bundle"); + } + Some(parent) => { + std::env::set_current_dir(parent)?; + + let cluster = cfg.provider.cluster.clone(); + let programs = cfg.programs.entry(cluster).or_default(); + if programs.contains_key(&name) { + return Err(anyhow!("Program already exists")); + } + + programs.insert( + name.clone(), + ProgramDeployment { + address: { + create_bundle(&name)?; + anchor_cli::rust_template::get_or_create_program_id(&name) + }, + path: None, + idl: None, + }, + ); + + let toml = cfg.to_string(); + fs::write("Anchor.toml", toml)?; + + println!("Created new bundle: {}", name); + } + }; + Ok(()) + }) +} diff --git a/crates/bolt-cli/src/lib.rs b/crates/bolt-cli/src/lib.rs index d6f50422..2e48b381 100644 --- a/crates/bolt-cli/src/lib.rs +++ b/crates/bolt-cli/src/lib.rs @@ -1,6 +1,7 @@ mod commands; mod ephemeral_validator; +mod bundle; mod component; mod instructions; mod rust_template; @@ -10,6 +11,7 @@ mod workspace; pub use ephemeral_validator::EphemeralValidator; +use crate::bundle::new_bundle; use crate::component::new_component; use crate::instructions::{ approve_system, authorize, create_registry, create_world, deauthorize, remove_system, @@ -39,6 +41,8 @@ pub const ANCHOR_VERSION: &str = anchor_cli::VERSION; pub enum BoltCommand { #[clap(about = "Create a new component")] Component(ComponentCommand), + #[clap(about = "Create a new Component-System bundle")] + Bundle(BundleCommand), #[clap(about = "Create a new system")] System(SystemCommand), // Include all existing commands from anchor_cli::Command @@ -69,6 +73,11 @@ pub struct ComponentCommand { pub name: String, } +#[derive(Debug, Parser)] +pub struct BundleCommand { + pub name: String, +} + #[derive(Debug, Parser)] pub struct SystemCommand { pub name: String, @@ -186,6 +195,7 @@ pub async fn entry(opts: Opts) -> Result<()> { } }, BoltCommand::Component(command) => new_component(&opts.cfg_override, command.name), + BoltCommand::Bundle(command) => new_bundle(&opts.cfg_override, command.name), BoltCommand::System(command) => new_system(&opts.cfg_override, command.name), BoltCommand::Registry(_command) => create_registry(&opts.cfg_override).await, BoltCommand::World(_command) => create_world(&opts.cfg_override).await, diff --git a/crates/bolt-cli/src/rust_template.rs b/crates/bolt-cli/src/rust_template.rs index 22e73e8c..92229419 100644 --- a/crates/bolt-cli/src/rust_template.rs +++ b/crates/bolt-cli/src/rust_template.rs @@ -4,6 +4,7 @@ use anchor_lang_idl::types::{IdlArrayLen, IdlGenericArg, IdlType}; use anyhow::Result; use std::path::{Path, PathBuf}; +use crate::templates::bundle::create_bundle_template; use crate::templates::component::create_component_template_simple; use crate::templates::program::{create_program_template_multiple, create_program_template_single}; use crate::templates::system::create_system_template_simple; @@ -27,6 +28,22 @@ pub fn create_component(name: &str) -> Result<()> { anchor_cli::create_files(&[common_files, template_files].concat()) } +/// Create a bundle from a given name. +pub(crate) fn create_bundle(name: &str) -> Result<()> { + let program_path = Path::new("programs-ecs/bundles").join(name); + let common_files = vec![ + ( + PathBuf::from("Cargo.toml".to_string()), + workspace_manifest().to_string(), + ), + (program_path.join("Cargo.toml"), cargo_toml_with_serde(name)), + (program_path.join("Xargo.toml"), xargo_toml().to_string()), + ] as Files; + + let template_files = create_bundle_template(name, &program_path); + anchor_cli::create_files(&[common_files, template_files].concat()) +} + /// Create a system from the given name. pub(crate) fn create_system(name: &str) -> Result<()> { let program_path = Path::new("programs-ecs/systems").join(name); diff --git a/crates/bolt-cli/src/templates/bundle/lib.rs.template b/crates/bolt-cli/src/templates/bundle/lib.rs.template new file mode 100644 index 00000000..e5c5de74 --- /dev/null +++ b/crates/bolt-cli/src/templates/bundle/lib.rs.template @@ -0,0 +1,33 @@ +use bolt_lang::*; + +declare_id!("{program_id}"); + +#[bundle] +pub mod {program_name} {{ + + #[component] + #[derive(Default)] + pub struct Position {{ + pub x: i64, + pub y: i64, + pub z: i64, + #[max_len(20)] + pub description: String, + }} + + #[system] + pub mod system {{ + + pub fn execute(ctx: Context, _args_p: Vec) -> Result {{ + let position = &mut ctx.accounts.position; + position.x += 1; + position.y += 1; + Ok(ctx.accounts) + }} + + #[system_input] + pub struct Components {{ + pub position: Position, + }} + }} +}} diff --git a/crates/bolt-cli/src/templates/bundle/mod.rs b/crates/bolt-cli/src/templates/bundle/mod.rs new file mode 100644 index 00000000..f6bf41c6 --- /dev/null +++ b/crates/bolt-cli/src/templates/bundle/mod.rs @@ -0,0 +1,16 @@ +use anchor_cli::Files; +use heck::ToSnakeCase; +use std::path::Path; + +pub fn create_bundle_template(name: &str, program_path: &Path) -> Files { + let program_id = anchor_cli::rust_template::get_or_create_program_id(name); + let program_name = name.to_snake_case(); + vec![( + program_path.join("src").join("lib.rs"), + format!( + include_str!("lib.rs.template"), + program_id = program_id, + program_name = program_name + ), + )] +} diff --git a/crates/bolt-cli/src/templates/mod.rs b/crates/bolt-cli/src/templates/mod.rs index b647cf62..29506ad4 100644 --- a/crates/bolt-cli/src/templates/mod.rs +++ b/crates/bolt-cli/src/templates/mod.rs @@ -1,3 +1,4 @@ +pub mod bundle; pub mod component; pub mod program; pub mod system; diff --git a/crates/bolt-cli/src/templates/workspace/workspace.toml.template b/crates/bolt-cli/src/templates/workspace/workspace.toml.template index 66c266b2..bcdac367 100644 --- a/crates/bolt-cli/src/templates/workspace/workspace.toml.template +++ b/crates/bolt-cli/src/templates/workspace/workspace.toml.template @@ -1,8 +1,9 @@ [workspace] members = [ - "programs/*", - "programs-ecs/components/*", - "programs-ecs/systems/*" + "programs/*", + "programs-ecs/components/*", + "programs-ecs/systems/*", + "programs-ecs/bundles/*" ] resolver = "2" diff --git a/crates/bolt-lang/Cargo.toml b/crates/bolt-lang/Cargo.toml index a47effc5..cb3bc112 100644 --- a/crates/bolt-lang/Cargo.toml +++ b/crates/bolt-lang/Cargo.toml @@ -16,8 +16,7 @@ idl-build = ["anchor-lang/idl-build"] anchor-lang = { workspace = true } # Bolt Attributes -bolt-attribute-bolt-program = { workspace = true } -bolt-attribute-bolt-delegate = { workspace = true } +bolt-attribute-bolt-bundle = { workspace = true } bolt-attribute-bolt-component = { workspace = true } bolt-attribute-bolt-system = { workspace = true } bolt-attribute-bolt-system-input = { workspace = true } @@ -28,7 +27,6 @@ bolt-attribute-bolt-arguments = { workspace = true } # Bolt Programs world = { workspace = true } -bolt-system = { workspace = true } # Session Keys session-keys = { workspace = true } diff --git a/crates/bolt-lang/attribute/bolt-program/Cargo.toml b/crates/bolt-lang/attribute/Cargo.toml similarity index 72% rename from crates/bolt-lang/attribute/bolt-program/Cargo.toml rename to crates/bolt-lang/attribute/Cargo.toml index 86e2a89d..ce257ee4 100644 --- a/crates/bolt-lang/attribute/bolt-program/Cargo.toml +++ b/crates/bolt-lang/attribute/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "bolt-attribute-bolt-program" -description = "Bolt attribute-bolt-program" +name = "bolt-attribute" +description = "bolt-attribute" version = { workspace = true } authors = { workspace = true } repository = { workspace = true } @@ -8,10 +8,9 @@ homepage = { workspace = true } license = { workspace = true } edition = { workspace = true } -[lib] -proc-macro = true - [dependencies] syn = { workspace = true } quote = { workspace = true } proc-macro2 = { workspace = true } +bolt-utils = { workspace = true } +heck = { workspace = true } diff --git a/crates/bolt-lang/attribute/bolt-program/src/lib.rs b/crates/bolt-lang/attribute/bolt-program/src/lib.rs deleted file mode 100644 index 5dda3383..00000000 --- a/crates/bolt-lang/attribute/bolt-program/src/lib.rs +++ /dev/null @@ -1,296 +0,0 @@ -use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; -use quote::{quote, ToTokens}; -use syn::{ - parse_macro_input, parse_quote, Attribute, AttributeArgs, Field, Fields, ItemMod, ItemStruct, - NestedMeta, Type, -}; - -/// This macro attribute is used to define a BOLT component. -/// -/// Bolt components are themselves programs that can be called by other programs. -/// -/// # Example -/// ```ignore -/// #[bolt_program(Position)] -/// #[program] -/// pub mod component_position { -/// use super::*; -/// } -/// -/// -/// #[component] -/// pub struct Position { -/// pub x: i64, -/// pub y: i64, -/// pub z: i64, -/// } -/// ``` -#[proc_macro_attribute] -pub fn bolt_program(args: TokenStream, input: TokenStream) -> TokenStream { - let ast = parse_macro_input!(input as syn::ItemMod); - let args = parse_macro_input!(args as syn::AttributeArgs); - let component_type = - extract_type_name(&args).expect("Expected a component type in macro arguments"); - let modified = modify_component_module(ast, &component_type); - let additional_macro: Attribute = parse_quote! { #[program] }; - TokenStream::from(quote! { - #additional_macro - #modified - }) -} - -/// 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); - - module.content = module.content.map(|(brace, mut items)| { - items.extend( - vec![ - initialize_fn, - initialize_struct, - update_fn, - update_struct, - update_with_session_fn, - update_with_session_struct, - destroy_fn, - destroy_struct, - ] - .into_iter() - .map(|item| syn::parse2(item).unwrap()) - .collect::>(), - ); - - let modified_items = items - .into_iter() - .map(|item| match item { - syn::Item::Struct(mut struct_item) - if struct_item.ident == "Apply" || struct_item.ident == "ApplyWithSession" => - { - modify_apply_struct(&mut struct_item); - syn::Item::Struct(struct_item) - } - _ => item, - }) - .collect(); - (brace, modified_items) - }); - - module -} - -/// Extracts the type name from attribute arguments. -fn extract_type_name(args: &AttributeArgs) -> Option { - args.iter().find_map(|arg| { - if let NestedMeta::Meta(syn::Meta::Path(path)) = arg { - Some(Type::Path(syn::TypePath { - qself: None, - path: path.clone(), - })) - } else { - None - } - }) -} - -/// Modifies the Apply struct, change the bolt system to accept any compatible system. -fn modify_apply_struct(struct_item: &mut ItemStruct) { - if let Fields::Named(fields_named) = &mut struct_item.fields { - fields_named - .named - .iter_mut() - .filter(|field| is_expecting_program(field)) - .for_each(|field| { - field.ty = syn::parse_str("UncheckedAccount<'info>").expect("Failed to parse type"); - field.attrs.push(create_check_attribute()); - }); - } -} - -/// Creates the check attribute. -fn create_check_attribute() -> Attribute { - parse_quote! { - #[doc = "CHECK: This program can modify the data of the component"] - } -} - -/// Generates the destroy function and struct. -fn generate_destroy(component_type: &Type) -> (TokenStream2, TokenStream2) { - ( - quote! { - #[automatically_derived] - pub fn destroy(ctx: Context) -> 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) { - ( - quote! { - #[automatically_derived] - pub fn initialize(ctx: Context) -> Result<()> { - 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()); - } - ctx.accounts.data.set_inner(<#component_type>::default()); - ctx.accounts.data.bolt_metadata.authority = *ctx.accounts.authority.key; - Ok(()) - } - }, - quote! { - #[automatically_derived] - #[derive(Accounts)] - pub struct Initialize<'info> { - #[account(mut)] - pub payer: Signer<'info>, - #[account(init_if_needed, payer = payer, space = <#component_type>::size(), seeds = [<#component_type>::seed(), entity.key().as_ref()], bump)] - pub data: Account<'info, #component_type>, - #[account()] - pub entity: Account<'info, Entity>, - #[account()] - pub authority: AccountInfo<'info>, - #[account(address = anchor_lang::solana_program::sysvar::instructions::id())] - pub instruction_sysvar_account: UncheckedAccount<'info>, - pub system_program: Program<'info, System>, - } - }, - ) -} - -/// Generates the instructions and related structs to inject in the component. -fn generate_update( - component_type: &Type, -) -> (TokenStream2, TokenStream2, TokenStream2, TokenStream2) { - ( - quote! { - #[automatically_derived] - pub fn update(ctx: Context, data: Vec) -> Result<()> { - require!(ctx.accounts.bolt_component.bolt_metadata.authority == World::id() || (ctx.accounts.bolt_component.bolt_metadata.authority == *ctx.accounts.authority.key && ctx.accounts.authority.is_signer), BoltError::InvalidAuthority); - - // Check if the instruction is called from the world program - let instruction = anchor_lang::solana_program::sysvar::instructions::get_instruction_relative( - 0, &ctx.accounts.instruction_sysvar_account.to_account_info() - ).map_err(|_| BoltError::InvalidCaller)?; - require_eq!(instruction.program_id, World::id(), BoltError::InvalidCaller); - - ctx.accounts.bolt_component.set_inner(<#component_type>::try_from_slice(&data)?); - Ok(()) - } - }, - quote! { - #[automatically_derived] - pub fn update_with_session(ctx: Context, data: Vec) -> Result<()> { - if ctx.accounts.bolt_component.bolt_metadata.authority == World::id() { - require!(Clock::get()?.unix_timestamp < ctx.accounts.session_token.valid_until, bolt_lang::session_keys::SessionError::InvalidToken); - } else { - let validity_ctx = bolt_lang::session_keys::ValidityChecker { - session_token: ctx.accounts.session_token.clone(), - session_signer: ctx.accounts.authority.clone(), - authority: ctx.accounts.bolt_component.bolt_metadata.authority.clone(), - target_program: World::id(), - }; - require!(ctx.accounts.session_token.validate(validity_ctx)?, bolt_lang::session_keys::SessionError::InvalidToken); - require_eq!(ctx.accounts.bolt_component.bolt_metadata.authority, ctx.accounts.session_token.authority, bolt_lang::session_keys::SessionError::InvalidToken); - } - - // Check if the instruction is called from the world program - let instruction = anchor_lang::solana_program::sysvar::instructions::get_instruction_relative( - 0, &ctx.accounts.instruction_sysvar_account.to_account_info() - ).map_err(|_| BoltError::InvalidCaller)?; - require_eq!(instruction.program_id, World::id(), BoltError::InvalidCaller); - - ctx.accounts.bolt_component.set_inner(<#component_type>::try_from_slice(&data)?); - Ok(()) - } - }, - quote! { - #[automatically_derived] - #[derive(Accounts)] - pub struct Update<'info> { - #[account(mut)] - pub bolt_component: Account<'info, #component_type>, - #[account()] - pub authority: Signer<'info>, - #[account(address = anchor_lang::solana_program::sysvar::instructions::id())] - pub instruction_sysvar_account: UncheckedAccount<'info> - } - }, - quote! { - #[automatically_derived] - #[derive(Accounts)] - pub struct UpdateWithSession<'info> { - #[account(mut)] - pub bolt_component: Account<'info, #component_type>, - #[account()] - pub authority: Signer<'info>, - #[account(address = anchor_lang::solana_program::sysvar::instructions::id())] - pub instruction_sysvar_account: UncheckedAccount<'info>, - #[account(constraint = session_token.to_account_info().owner == &bolt_lang::session_keys::ID)] - pub session_token: Account<'info, bolt_lang::session_keys::SessionToken>, - } - }, - ) -} - -/// Checks if the field is expecting a program. -fn is_expecting_program(field: &Field) -> bool { - field.ty.to_token_stream().to_string().contains("Program") -} diff --git a/crates/bolt-lang/attribute/delegate/Cargo.toml b/crates/bolt-lang/attribute/bundle/Cargo.toml similarity index 58% rename from crates/bolt-lang/attribute/delegate/Cargo.toml rename to crates/bolt-lang/attribute/bundle/Cargo.toml index 146c9377..1595f755 100644 --- a/crates/bolt-lang/attribute/delegate/Cargo.toml +++ b/crates/bolt-lang/attribute/bundle/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "bolt-attribute-bolt-delegate" -description = "Bolt attribute-bolt-delegate" +name = "bolt-attribute-bolt-bundle" +description = "bolt-attribute-bolt-bundle" version = { workspace = true } authors = { workspace = true } repository = { workspace = true } @@ -12,6 +12,4 @@ edition = { workspace = true } proc-macro = true [dependencies] -syn = { workspace = true } -quote = { workspace = true } -proc-macro2 = { workspace = true } +bolt-attribute.workspace = true diff --git a/crates/bolt-lang/attribute/bundle/src/lib.rs b/crates/bolt-lang/attribute/bundle/src/lib.rs new file mode 100644 index 00000000..677fd15e --- /dev/null +++ b/crates/bolt-lang/attribute/bundle/src/lib.rs @@ -0,0 +1,11 @@ +use proc_macro::TokenStream; + +/// #[bundle] +/// +/// Combines one `#[component]` and one `#[system]` into a single Anchor `#[program]` module. +/// Reuses the existing macros to generate code, strips their internal `#[program]` wrappers, +/// and exposes wrapper instruction functions under a unified program. +#[proc_macro_attribute] +pub fn bundle(attr: TokenStream, item: TokenStream) -> TokenStream { + bolt_attribute::bundle::process(attr, item) +} diff --git a/crates/bolt-lang/attribute/component-deserialize/Cargo.toml b/crates/bolt-lang/attribute/component-deserialize/Cargo.toml index 89ff8eb3..e0e1f22e 100644 --- a/crates/bolt-lang/attribute/component-deserialize/Cargo.toml +++ b/crates/bolt-lang/attribute/component-deserialize/Cargo.toml @@ -16,3 +16,4 @@ syn = { workspace = true } bolt-utils = { workspace = true } quote = { workspace = true } proc-macro2 = { workspace = true } +sha2 = { workspace = true } \ No newline at end of file diff --git a/crates/bolt-lang/attribute/component-deserialize/src/lib.rs b/crates/bolt-lang/attribute/component-deserialize/src/lib.rs index 6ec8d234..48fe1262 100644 --- a/crates/bolt-lang/attribute/component-deserialize/src/lib.rs +++ b/crates/bolt-lang/attribute/component-deserialize/src/lib.rs @@ -1,6 +1,7 @@ use bolt_utils::add_bolt_metadata; use proc_macro::TokenStream; use quote::quote; +use sha2::{Digest, Sha256}; use syn::{parse_macro_input, Attribute, DeriveInput}; /// This macro is used to defined a struct as a BOLT component and automatically implements the @@ -38,6 +39,9 @@ pub fn component_deserialize(_attr: TokenStream, item: TokenStream) -> TokenStre } }; } + let mut sha256 = Sha256::new(); + sha256.update(format!("account:{}", name_str).as_bytes()); + let discriminator = sha256.finalize()[0..8].to_vec(); let expanded = quote! { #input @@ -70,7 +74,7 @@ pub fn component_deserialize(_attr: TokenStream, item: TokenStream) -> TokenStre #[automatically_derived] impl anchor_lang::Discriminator for #name { - const DISCRIMINATOR: &'static [u8] = &[1, 1, 1, 1, 1, 1, 1, 1]; + const DISCRIMINATOR: &'static [u8] = &[#(#discriminator),*]; } #owner_definition diff --git a/crates/bolt-lang/attribute/component/Cargo.toml b/crates/bolt-lang/attribute/component/Cargo.toml index a8c29001..cef21bc2 100644 --- a/crates/bolt-lang/attribute/component/Cargo.toml +++ b/crates/bolt-lang/attribute/component/Cargo.toml @@ -12,8 +12,4 @@ edition = { workspace = true } proc-macro = true [dependencies] -syn = { workspace = true } -bolt-utils = { workspace = true } -heck = { workspace = true } -quote = { workspace = true } -proc-macro2 = { workspace = true } \ No newline at end of file +bolt-attribute.workspace = true diff --git a/crates/bolt-lang/attribute/component/src/lib.rs b/crates/bolt-lang/attribute/component/src/lib.rs index 2f4fa649..383dfd93 100644 --- a/crates/bolt-lang/attribute/component/src/lib.rs +++ b/crates/bolt-lang/attribute/component/src/lib.rs @@ -1,14 +1,5 @@ use proc_macro::TokenStream; -use quote::quote; -use syn::{ - parse_macro_input, parse_quote, Attribute, DeriveInput, Lit, Meta, MetaList, MetaNameValue, - NestedMeta, -}; - -use bolt_utils::add_bolt_metadata; -use heck::ToSnakeCase; - /// This Component attribute is used to automatically generate the seed and size functions /// /// The component_id can be used to define the seed used to generate the PDA which stores the component data. @@ -24,151 +15,5 @@ use heck::ToSnakeCase; /// ``` #[proc_macro_attribute] pub fn component(attr: TokenStream, item: TokenStream) -> TokenStream { - let mut input = parse_macro_input!(item as DeriveInput); - let mut component_id_value = None; - let mut delegate_set = false; - - if !attr.is_empty() { - let attr_meta = parse_macro_input!(attr as Meta); - delegate_set = is_delegate_set(&attr_meta); - component_id_value = match attr_meta { - Meta::Path(_) => None, - Meta::NameValue(meta_name_value) => extract_component_id(&meta_name_value), - Meta::List(meta_list) => { - if !delegate_set { - delegate_set = is_delegate_set(&Meta::List(meta_list.clone())); - } - find_component_id_in_list(meta_list) - } - }; - } - - let component_id_value = component_id_value.unwrap_or_else(|| "".to_string()); - - let additional_macro: Attribute = parse_quote! { #[account] }; - let additional_derives: Attribute = parse_quote! { #[derive(InitSpace)] }; - input.attrs.push(additional_derives); - - let new_fn = define_new_fn(&input); - - add_bolt_metadata(&mut input); - - let name = &input.ident; - - let snake_case_name = name.to_string().to_snake_case(); - let component_name = syn::Ident::new(&snake_case_name, input.ident.span()); - - let bolt_program = if delegate_set { - quote! { - #[delegate(#name)] - #[bolt_program(#name)] - pub mod #component_name { - use super::*; - } - } - } else { - quote! { - #[bolt_program(#name)] - pub mod #component_name { - use super::*; - } - } - }; - - let expanded = quote! { - #bolt_program - - #additional_macro - #input - - #new_fn - - #[automatically_derived] - impl ComponentTraits for #name { - fn seed() -> &'static [u8] { - #component_id_value.as_bytes() - } - - fn size() -> usize { - 8 + <#name>::INIT_SPACE - } - } - - }; - expanded.into() -} - -/// Create a fn `new` to initialize the struct without bolt_metadata field -fn define_new_fn(input: &DeriveInput) -> proc_macro2::TokenStream { - let struct_name = &input.ident; - let init_struct_name = syn::Ident::new(&format!("{}Init", struct_name), struct_name.span()); - - if let syn::Data::Struct(ref data) = input.data { - if let syn::Fields::Named(ref fields) = data.fields { - // Generate fields for the init struct - let init_struct_fields = fields.named.iter().map(|f| { - let name = &f.ident; - let ty = &f.ty; - quote! { pub #name: #ty } - }); - - // Generate struct initialization code using the init struct - let struct_init_fields = fields.named.iter().map(|f| { - let name = &f.ident; - quote! { #name: init_struct.#name } - }); - - // Generate the new function and the init struct - let gen = quote! { - // Define a new struct to hold initialization parameters - pub struct #init_struct_name { - #(#init_struct_fields),* - } - - impl #struct_name { - pub fn new(init_struct: #init_struct_name) -> Self { - Self { - #(#struct_init_fields,)* - bolt_metadata: BoltMetadata::default(), - } - } - } - }; - return gen; - } - } - quote! {} -} - -fn is_delegate_set(meta: &Meta) -> bool { - match meta { - Meta::Path(path) => path.is_ident("delegate"), - Meta::List(meta_list) => meta_list.nested.iter().any(|nested_meta| { - if let NestedMeta::Meta(Meta::Path(path)) = nested_meta { - path.is_ident("delegate") - } else { - false - } - }), - _ => false, - } -} - -fn extract_component_id(meta_name_value: &MetaNameValue) -> Option { - if meta_name_value.path.is_ident("component_id") { - if let Lit::Str(lit) = &meta_name_value.lit { - return Some(lit.value()); - } - } - None -} - -fn find_component_id_in_list(meta_list: MetaList) -> Option { - meta_list.nested.into_iter().find_map(|nested_meta| { - if let NestedMeta::Meta(Meta::NameValue(meta_name_value)) = nested_meta { - extract_component_id(&meta_name_value) - } else { - None - } - }) + bolt_attribute::component::process(attr, item) } diff --git a/crates/bolt-lang/attribute/delegate/src/lib.rs b/crates/bolt-lang/attribute/delegate/src/lib.rs deleted file mode 100644 index e4cce3b7..00000000 --- a/crates/bolt-lang/attribute/delegate/src/lib.rs +++ /dev/null @@ -1,210 +0,0 @@ -use proc_macro::TokenStream; - -use proc_macro2::TokenStream as TokenStream2; -use quote::quote; -use syn::{parse_macro_input, AttributeArgs, ItemMod, NestedMeta, Type}; - -/// This macro attribute is used to inject instructions and struct needed to delegate BOLT component. -/// -/// Components can be delegate in order to be updated in an Ephemeral Rollup -/// -/// # Example -/// ```ignore -/// -/// #[component(delegate)] -/// pub struct Position { -/// pub x: i64, -/// pub y: i64, -/// pub z: i64, -/// } -/// ``` -#[proc_macro_attribute] -pub fn delegate(args: TokenStream, input: TokenStream) -> TokenStream { - let ast = parse_macro_input!(input as syn::ItemMod); - let args = parse_macro_input!(args as syn::AttributeArgs); - let component_type = - extract_type_name(&args).expect("Expected a component type in macro arguments"); - let modified = modify_component_module(ast, &component_type); - TokenStream::from(quote! { - #modified - }) -} - -/// Modifies the component module and adds the necessary functions and structs. -fn modify_component_module(mut module: ItemMod, component_type: &Type) -> ItemMod { - let (delegate_fn, delegate_struct) = generate_delegate(component_type); - let (reinit_undelegate_fn, reinit_undelegate_struct) = generate_reinit_after_undelegate(); - let (undelegate_fn, undelegate_struct) = generate_undelegate(); - module.content = module.content.map(|(brace, mut items)| { - items.extend( - vec![ - delegate_fn, - delegate_struct, - reinit_undelegate_fn, - reinit_undelegate_struct, - undelegate_fn, - undelegate_struct, - ] - .into_iter() - .map(|item| syn::parse2(item).unwrap()) - .collect::>(), - ); - (brace, items) - }); - module -} - -/// Generates the allow_undelegate function and struct. -fn generate_undelegate() -> (TokenStream2, TokenStream2) { - ( - quote! { - #[automatically_derived] - pub fn undelegate(ctx: Context) -> Result<()> { - ::bolt_lang::commit_and_undelegate_accounts( - &ctx.accounts.payer, - vec![&ctx.accounts.delegated_account.to_account_info()], - &ctx.accounts.magic_context, - &ctx.accounts.magic_program, - )?; - Ok(()) - } - }, - quote! { - #[automatically_derived] - #[derive(Accounts)] - pub struct Undelegate<'info> { - #[account(mut)] - pub payer: Signer<'info>, - #[account(mut)] - /// CHECK: The delegated component - pub delegated_account: AccountInfo<'info>, - #[account(mut, address = ::bolt_lang::MAGIC_CONTEXT_ID)] - /// CHECK:` - pub magic_context: AccountInfo<'info>, - #[account()] - /// CHECK:` - pub magic_program: Program<'info, MagicProgram> - } - }, - ) -} - -/// Generates the undelegate function and struct. -fn generate_reinit_after_undelegate() -> (TokenStream2, TokenStream2) { - ( - quote! { - #[automatically_derived] - pub fn process_undelegation(ctx: Context, account_seeds: Vec>) -> Result<()> { - let [delegated_account, buffer, payer, system_program] = [ - &ctx.accounts.delegated_account, - &ctx.accounts.buffer, - &ctx.accounts.payer, - &ctx.accounts.system_program, - ]; - ::bolt_lang::undelegate_account( - delegated_account, - &id(), - buffer, - payer, - system_program, - account_seeds, - )?; - Ok(()) - } - }, - quote! { - #[automatically_derived] - #[derive(Accounts)] - pub struct InitializeAfterUndelegation<'info> { - /// CHECK:` - #[account(mut)] - pub delegated_account: AccountInfo<'info>, - /// CHECK:` - #[account()] - pub buffer: AccountInfo<'info>, - /// CHECK: - #[account(mut)] - pub payer: AccountInfo<'info>, - /// CHECK: - pub system_program: AccountInfo<'info>, - } - }, - ) -} - -/// Generates the delegate instruction and related structs to inject in the component. -fn generate_delegate(component_type: &Type) -> (TokenStream2, TokenStream2) { - ( - quote! { - #[automatically_derived] - pub fn delegate(ctx: Context, commit_frequency_ms: u32, validator: Option) -> Result<()> { - let pda_seeds: &[&[u8]] = &[<#component_type>::seed(), &ctx.accounts.entity.key().to_bytes()]; - - let del_accounts = ::bolt_lang::DelegateAccounts { - payer: &ctx.accounts.payer, - pda: &ctx.accounts.account, - owner_program: &ctx.accounts.owner_program, - buffer: &ctx.accounts.buffer, - delegation_record: &ctx.accounts.delegation_record, - delegation_metadata: &ctx.accounts.delegation_metadata, - delegation_program: &ctx.accounts.delegation_program, - system_program: &ctx.accounts.system_program, - }; - - let config = ::bolt_lang::DelegateConfig { - commit_frequency_ms, - validator, - }; - - ::bolt_lang::delegate_account( - del_accounts, - pda_seeds, - config, - )?; - - Ok(()) - } - }, - quote! { - #[automatically_derived] - #[derive(Accounts)] - pub struct DelegateInput<'info> { - pub payer: Signer<'info>, - #[account()] - pub entity: Account<'info, Entity>, - /// CHECK: - #[account(mut)] - pub account: AccountInfo<'info>, - /// CHECK:` - pub owner_program: AccountInfo<'info>, - /// CHECK: - #[account(mut)] - pub buffer: AccountInfo<'info>, - /// CHECK:` - #[account(mut)] - pub delegation_record: AccountInfo<'info>, - /// CHECK:` - #[account(mut)] - pub delegation_metadata: AccountInfo<'info>, - /// CHECK:` - pub delegation_program: AccountInfo<'info>, - /// CHECK:` - pub system_program: AccountInfo<'info>, - } - }, - ) -} - -/// Extracts the type name from attribute arguments. -fn extract_type_name(args: &AttributeArgs) -> Option { - args.iter().find_map(|arg| { - if let NestedMeta::Meta(syn::Meta::Path(path)) = arg { - Some(Type::Path(syn::TypePath { - qself: None, - path: path.clone(), - })) - } else { - None - } - }) -} diff --git a/crates/bolt-lang/attribute/extra-accounts/Cargo.toml b/crates/bolt-lang/attribute/extra-accounts/Cargo.toml index 52b04bf3..d8a6cdbd 100644 --- a/crates/bolt-lang/attribute/extra-accounts/Cargo.toml +++ b/crates/bolt-lang/attribute/extra-accounts/Cargo.toml @@ -14,4 +14,5 @@ proc-macro = true [dependencies] syn = { workspace = true, features = ["visit-mut"] } quote = { workspace = true } -proc-macro2 = { workspace = true } \ No newline at end of file +proc-macro2 = { workspace = true } +heck = { workspace = true } \ No newline at end of file diff --git a/crates/bolt-lang/attribute/extra-accounts/src/lib.rs b/crates/bolt-lang/attribute/extra-accounts/src/lib.rs index 4d6cd15a..8b4288db 100644 --- a/crates/bolt-lang/attribute/extra-accounts/src/lib.rs +++ b/crates/bolt-lang/attribute/extra-accounts/src/lib.rs @@ -1,5 +1,7 @@ +use heck::ToPascalCase; use proc_macro::TokenStream; +use proc_macro2::Span; use quote::quote; use syn::{parse_macro_input, Fields, ItemStruct, LitStr}; @@ -22,9 +24,23 @@ use syn::{parse_macro_input, Fields, ItemStruct, LitStr}; /// /// ``` #[proc_macro_attribute] -pub fn extra_accounts(_attr: TokenStream, item: TokenStream) -> TokenStream { +pub fn extra_accounts(attr: TokenStream, item: TokenStream) -> TokenStream { + let attr = parse_macro_input!(attr as syn::MetaNameValue); + let literal = if let syn::Lit::Str(lit_str) = attr.lit { + lit_str.value() + } else { + panic!("Invalid literal"); + }; + let type_path = syn::parse_str::(&literal).expect("Invalid type path"); let input = parse_macro_input!(item as ItemStruct); let extra_accounts_struct_name = &input.ident; + let context_extension_name = syn::Ident::new( + &format!( + "ContextExtensions{}", + extra_accounts_struct_name.to_string().to_pascal_case() + ), + Span::call_site(), + ); // Ensure the struct has named fields let fields = match &input.fields { @@ -59,7 +75,7 @@ pub fn extra_accounts(_attr: TokenStream, item: TokenStream) -> TokenStream { }); let output_trait = quote! { - pub trait ContextExtensions<'a, 'b, 'c, 'info, T> + pub trait #context_extension_name<'a, 'b, 'c, 'info, T> { #(#helper_functions)* } @@ -71,13 +87,13 @@ pub fn extra_accounts(_attr: TokenStream, item: TokenStream) -> TokenStream { let index = syn::Index::from(index); // Create a compile-time index representation quote! { fn #field_name(&self) -> Result<&'c AccountInfo<'info>> { - self.remaining_accounts.get(Self::NUMBER_OF_COMPONENTS + #index).ok_or_else(|| ErrorCode::ConstraintAccountIsNone.into()) + self.remaining_accounts.get(<#type_path as bolt_lang::NumberOfComponents>::NUMBER_OF_COMPONENTS + #index).ok_or_else(|| ErrorCode::ConstraintAccountIsNone.into()) } } }); let output_trait_implementation = quote! { - impl<'a, 'b, 'c, 'info, T: bolt_lang::Bumps> ContextExtensions<'a, 'b, 'c, 'info, T> for Context<'a, 'b, 'c, 'info, T> { + impl<'a, 'b, 'c, 'info> #context_extension_name<'a, 'b, 'c, 'info, #type_path> for Context<'a, 'b, 'c, 'info, #type_path> { #(#helper_functions_impl)* } }; diff --git a/crates/bolt-lang/attribute/src/bundle/mod.rs b/crates/bolt-lang/attribute/src/bundle/mod.rs new file mode 100644 index 00000000..3a520900 --- /dev/null +++ b/crates/bolt-lang/attribute/src/bundle/mod.rs @@ -0,0 +1,80 @@ +use heck::ToSnakeCase; +use proc_macro::TokenStream; +use quote::ToTokens; +use syn::{parse_macro_input, parse_quote, ItemMod}; + +use crate::common::generate_program; +use crate::component; +use crate::system; + +pub fn process(_attr: TokenStream, item: TokenStream) -> TokenStream { + let bundle_mod = parse_macro_input!(item as ItemMod); + let mut program = generate_program(&bundle_mod.ident.to_string()); + component::generate_update(&mut program); + let mut delegate_components = Vec::new(); + if let Some((_, items)) = bundle_mod.content { + for item in items { + match item { + syn::Item::Struct(item) => { + let attributes = component::Attributes::from(item.attrs.clone()); + if attributes.is_component { + if attributes.delegate { + delegate_components + .push((item.ident.clone(), item.ident.to_string().to_snake_case())); + } + let data = syn::Data::Struct(syn::DataStruct { + struct_token: Default::default(), + fields: item.fields, + semi_token: Default::default(), + }); + let mut type_ = syn::DeriveInput { + attrs: item.attrs, + vis: item.vis, + ident: item.ident, + generics: item.generics, + data, + }; + component::generate_implementation(&mut program, &attributes, &type_); + component::generate_instructions( + &mut program, + &type_.ident, + Some(&type_.ident.to_string().to_snake_case()), + ); + component::remove_component_attributes(&mut type_.attrs); + component::enrich_type(&mut type_); + let (_, items) = program.content.as_mut().unwrap(); + items.push(parse_quote!(#type_)); + } else { + // Not a bolt component; include as-is + let (_, program_items) = program.content.as_mut().unwrap(); + let original: syn::Item = syn::Item::Struct(item); + program_items.push(parse_quote!(#original)); + } + } + syn::Item::Mod(mut mod_item) => { + if mod_item.attrs.iter().any(|a| a.path.is_ident("system")) { + let suffix = mod_item.ident.to_string().to_snake_case(); + let inlined_items = + system::transform_module_for_bundle(&mut mod_item, Some(&suffix)); + let (_, program_items) = program.content.as_mut().unwrap(); + program_items.extend(inlined_items.into_iter()); + } else { + // Regular module; include as-is + let (_, program_items) = program.content.as_mut().unwrap(); + let original: syn::Item = syn::Item::Mod(mod_item); + program_items.push(parse_quote!(#original)); + } + } + other => { + // Any other non-bolt item; include as-is + let (_, program_items) = program.content.as_mut().unwrap(); + program_items.push(parse_quote!(#other)); + } + } + } + } + + crate::delegate::inject_delegate_items(&mut program, delegate_components); + + program.to_token_stream().into() +} diff --git a/crates/bolt-lang/attribute/src/common/mod.rs b/crates/bolt-lang/attribute/src/common/mod.rs new file mode 100644 index 00000000..f94e6379 --- /dev/null +++ b/crates/bolt-lang/attribute/src/common/mod.rs @@ -0,0 +1,31 @@ +use heck::ToSnakeCase; +use proc_macro2::Span; +use syn::parse_quote; + +pub fn generate_program(identifier: &str) -> syn::ItemMod { + let snake_case_name = identifier.to_snake_case(); + let snake_case_name = syn::Ident::new(&snake_case_name, Span::call_site()); + + let mut module: syn::ItemMod = parse_quote! { + pub mod #snake_case_name {} + }; + inject_program(&mut module); + module +} + +pub fn inject_program(module: &mut syn::ItemMod) { + module.attrs.push(syn::parse_quote! { #[program] }); + module.content.as_mut().map(|(brace, items)| { + items.insert(0, syn::Item::Use(syn::parse_quote! { use super::*; })); + items.insert( + 1, + syn::Item::Struct(syn::parse_quote! { + #[derive(Accounts)] + pub struct VariadicBoltComponents<'info> { + pub authority: Signer<'info>, + } + }), + ); + (brace, items.clone()) + }); +} diff --git a/crates/bolt-lang/attribute/src/component/attributes.rs b/crates/bolt-lang/attribute/src/component/attributes.rs new file mode 100644 index 00000000..7b6503e5 --- /dev/null +++ b/crates/bolt-lang/attribute/src/component/attributes.rs @@ -0,0 +1,101 @@ +#[derive(Default, Debug)] +pub struct Attributes { + pub is_component: bool, + pub component_id: String, + pub delegate: bool, +} + +use std::ops::Not; + +use proc_macro::TokenStream; +use syn::{Lit, Meta, MetaList, MetaNameValue, NestedMeta}; + +impl From> for Attributes { + fn from(attrs: Vec) -> Self { + attrs + .iter() + .find(|attr| attr.path.is_ident("component")) + .map(|attr| Self::from(attr.parse_meta().unwrap())) + .unwrap_or_default() + } +} + +impl From for Attributes { + fn from(attr: TokenStream) -> Self { + attr.is_empty() + .not() + .then(|| { + let attr_meta: Meta = syn::parse(attr).expect("Invalid component attribute"); + Self::from(attr_meta) + }) + .unwrap_or_default() + } +} + +impl From for Attributes { + fn from(meta: syn::Meta) -> Self { + let mut delegate = is_delegate_set(&meta); + let is_component = is_component_set(&meta); + let component_id_value = match meta { + Meta::Path(_) => None, + Meta::NameValue(meta_name_value) => extract_component_id(&meta_name_value), + Meta::List(meta_list) => { + if !delegate { + delegate = is_delegate_set(&Meta::List(meta_list.clone())); + } + find_component_id_in_list(meta_list) + } + }; + + let component_id = component_id_value.unwrap_or_else(|| "".to_string()); + Self { + is_component, + component_id, + delegate, + } + } +} + +pub fn is_component_set(meta: &Meta) -> bool { + match meta { + // #[component] + Meta::Path(path) => path.is_ident("component"), + // #[component(...)] + Meta::List(meta_list) => meta_list.path.is_ident("component"), + // #[component = ...] (not expected, but handle defensively) + Meta::NameValue(name_value) => name_value.path.is_ident("component"), + } +} + +pub fn is_delegate_set(meta: &Meta) -> bool { + match meta { + Meta::Path(path) => path.is_ident("delegate"), + Meta::List(meta_list) => meta_list.nested.iter().any(|nested_meta| { + if let NestedMeta::Meta(Meta::Path(path)) = nested_meta { + path.is_ident("delegate") + } else { + false + } + }), + _ => false, + } +} + +pub fn extract_component_id(meta_name_value: &MetaNameValue) -> Option { + if meta_name_value.path.is_ident("component_id") { + if let Lit::Str(lit) = &meta_name_value.lit { + return Some(lit.value()); + } + } + None +} + +pub fn find_component_id_in_list(meta_list: MetaList) -> Option { + meta_list.nested.into_iter().find_map(|nested_meta| { + if let NestedMeta::Meta(Meta::NameValue(meta_name_value)) = nested_meta { + extract_component_id(&meta_name_value) + } else { + None + } + }) +} diff --git a/crates/bolt-lang/attribute/src/component/generate/mod.rs b/crates/bolt-lang/attribute/src/component/generate/mod.rs new file mode 100644 index 00000000..8b02600c --- /dev/null +++ b/crates/bolt-lang/attribute/src/component/generate/mod.rs @@ -0,0 +1,90 @@ +mod program; + +use quote::quote; +use syn::{parse_quote, Attribute}; +use syn::{DeriveInput, ItemMod}; + +pub use program::*; + +pub fn enrich_type(type_: &mut DeriveInput) { + let account_macro: Attribute = parse_quote! { #[account] }; + let init_space_derive: Attribute = parse_quote! { #[derive(InitSpace)] }; + type_.attrs.push(init_space_derive); + type_.attrs.push(account_macro); + bolt_utils::add_bolt_metadata(type_); +} + +pub fn generate_implementation( + program: &mut ItemMod, + attributes: &super::Attributes, + input: &DeriveInput, +) { + generate_new_fn(program, input); + generate_component_traits(program, attributes, input); +} + +fn generate_component_traits( + program: &mut ItemMod, + attributes: &super::Attributes, + input: &DeriveInput, +) { + let name = &input.ident; + let component_id_value = &attributes.component_id; + let implementation = quote! { + #[automatically_derived] + impl ComponentTraits for #name { + fn seed() -> &'static [u8] { + #component_id_value.as_bytes() + } + + fn size() -> usize { + 8 + <#name>::INIT_SPACE + } + } + }; + let (_, items) = program.content.as_mut().unwrap(); + items.push(parse_quote!(#implementation)); +} + +/// Create a fn `new` to initialize the struct without bolt_metadata field +fn generate_new_fn(program: &mut ItemMod, input: &DeriveInput) { + let struct_name = &input.ident; + let init_struct_name = syn::Ident::new(&format!("{}Init", struct_name), struct_name.span()); + + if let syn::Data::Struct(ref data) = input.data { + if let syn::Fields::Named(ref fields) = data.fields { + // Generate fields for the init struct + let init_struct_fields = fields.named.iter().map(|f| { + let name = &f.ident; + let ty = &f.ty; + quote! { pub #name: #ty } + }); + + // Generate struct initialization code using the init struct + let struct_init_fields = fields.named.iter().map(|f| { + let name = &f.ident; + quote! { #name: init_struct.#name } + }); + + let structure = quote! { + // Define a new struct to hold initialization parameters + pub struct #init_struct_name { + #(#init_struct_fields),* + } + }; + let implementation = quote! { + impl #struct_name { + pub fn new(init_struct: #init_struct_name) -> Self { + Self { + #(#struct_init_fields,)* + bolt_metadata: BoltMetadata::default(), + } + } + } + }; + let (_, items) = program.content.as_mut().unwrap(); + items.push(parse_quote!(#structure)); + items.push(parse_quote!(#implementation)); + } + } +} diff --git a/crates/bolt-lang/attribute/src/component/generate/program.rs b/crates/bolt-lang/attribute/src/component/generate/program.rs new file mode 100644 index 00000000..4c92b1d9 --- /dev/null +++ b/crates/bolt-lang/attribute/src/component/generate/program.rs @@ -0,0 +1,253 @@ +use heck::ToPascalCase; +use quote::{quote, ToTokens}; + +use proc_macro2::TokenStream as TokenStream2; +use syn::{ + parse_quote, spanned::Spanned, Attribute, Field, Fields, ItemMod, ItemStruct, LitByteStr, Type, +}; + +pub fn remove_component_attributes(attrs: &mut Vec) { + attrs.retain(|attr| !attr.path.is_ident("component")); +} + +pub fn generate_instructions( + program_mod: &mut ItemMod, + pascal_case_name: &syn::Ident, + component_name: Option<&String>, +) { + let component_type = Type::Path(syn::TypePath { + qself: None, + path: pascal_case_name.clone().into(), + }); + modify_component_module(program_mod, &component_type, component_name) +} + +/// Modifies the component module and adds the necessary functions and structs. +fn modify_component_module( + module: &mut ItemMod, + component_type: &Type, + component_name: Option<&String>, +) { + let (initialize_fn, initialize_struct) = generate_initialize(component_type, component_name); + let (destroy_fn, destroy_struct) = generate_destroy(component_type, component_name); + + module.content.as_mut().map(|(brace, items)| { + items.extend( + vec![initialize_fn, initialize_struct, destroy_fn, destroy_struct] + .into_iter() + .map(|item| { + syn::parse2(item).expect("Failed to parse generate initialize and destroy item") + }) + .collect::>(), + ); + + let modified_items: Vec = items + .iter_mut() + .map(|item| match item.clone() { + syn::Item::Struct(mut struct_item) + if struct_item.ident == "Apply" || struct_item.ident == "ApplyWithSession" => + { + modify_apply_struct(&mut struct_item); + syn::Item::Struct(struct_item) + } + _ => item.clone(), + }) + .collect(); + (brace, modified_items) + }); +} + +/// Modifies the Apply struct, change the bolt system to accept any compatible system. +fn modify_apply_struct(struct_item: &mut ItemStruct) { + if let Fields::Named(fields_named) = &mut struct_item.fields { + fields_named + .named + .iter_mut() + .filter(|field| is_expecting_program(field)) + .for_each(|field| { + field.ty = syn::parse_str("UncheckedAccount<'info>").expect("Failed to parse type"); + field.attrs.push(create_check_attribute()); + }); + } +} + +/// Creates the check attribute. +fn create_check_attribute() -> Attribute { + parse_quote! { + #[doc = "CHECK: This program can modify the data of the component"] + } +} + +/// Generates the destroy function and struct. +fn generate_destroy( + component_type: &Type, + component_name: Option<&String>, +) -> (TokenStream2, TokenStream2) { + let structure_name = if let Some(name) = component_name { + syn::Ident::new( + &format!("{}Destroy", name.to_pascal_case()), + component_type.span(), + ) + } else { + syn::Ident::new("Destroy", component_type.span()) + }; + let fn_destroy = if let Some(name) = component_name { + syn::Ident::new(&format!("{}_destroy", name), component_type.span()) + } else { + syn::Ident::new("destroy", component_type.span()) + }; + // Build PDA seeds, adding component name when bundled + let seeds_tokens = if let Some(name) = component_name { + let name_bytes = LitByteStr::new(name.as_bytes(), component_type.span()); + quote! { [#name_bytes, entity.key().as_ref()] } + } else { + quote! { [<#component_type>::seed(), entity.key().as_ref()] } + }; + + ( + quote! { + pub fn #fn_destroy(ctx: Context<#structure_name>) -> Result<()> { + bolt_lang::instructions::destroy(&crate::id(), &ctx.accounts.instruction_sysvar_account.to_account_info(), &ctx.accounts.authority.to_account_info(), &ctx.accounts.component_program_data, ctx.accounts.component.bolt_metadata.authority) + } + }, + quote! { + #[automatically_derived] + #[derive(Accounts)] + pub struct #structure_name<'info> { + #[account()] + pub authority: Signer<'info>, + #[account(mut)] + pub receiver: AccountInfo<'info>, + #[account()] + pub entity: Account<'info, Entity>, + #[account(mut, close = receiver, seeds = #seeds_tokens, 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, + component_name: Option<&String>, +) -> (TokenStream2, TokenStream2) { + let structure_name = if let Some(name) = component_name { + syn::Ident::new( + &format!("{}Initialize", name.to_pascal_case()), + component_type.span(), + ) + } else { + syn::Ident::new("Initialize", component_type.span()) + }; + let fn_initialize = if let Some(name) = component_name { + syn::Ident::new(&format!("{}_initialize", name), component_type.span()) + } else { + syn::Ident::new("initialize", component_type.span()) + }; + // Build PDA seeds, adding component name when bundled + let seeds_tokens = if let Some(name) = component_name { + let name_bytes = LitByteStr::new(name.as_bytes(), component_type.span()); + quote! { [#name_bytes, entity.key().as_ref()] } + } else { + quote! { [<#component_type>::seed(), entity.key().as_ref()] } + }; + + ( + quote! { + #[automatically_derived] + pub fn #fn_initialize(ctx: Context<#structure_name>) -> Result<()> { + bolt_lang::instructions::initialize(&ctx.accounts.instruction_sysvar_account.to_account_info(), &mut ctx.accounts.data)?; + ctx.accounts.data.bolt_metadata.authority = *ctx.accounts.authority.key; + Ok(()) + } + }, + quote! { + #[automatically_derived] + #[derive(Accounts)] + pub struct #structure_name<'info> { + #[account(mut)] + pub payer: Signer<'info>, + #[account(init_if_needed, payer = payer, space = <#component_type>::size(), seeds = #seeds_tokens, bump)] + pub data: Account<'info, #component_type>, + #[account()] + pub entity: Account<'info, Entity>, + #[account()] + pub authority: 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 instructions and related structs to inject in the component. +pub fn generate_update(module: &mut ItemMod) { + let update_fn = quote! { + #[automatically_derived] + pub fn update(ctx: Context, data: Vec) -> Result<()> { + let bolt_metadata = BoltMetadata::try_from_account_info(&ctx.accounts.bolt_component)?; + bolt_lang::instructions::update(&ctx.accounts.instruction_sysvar_account.to_account_info(), &ctx.accounts.authority.to_account_info(), bolt_metadata.authority, &mut ctx.accounts.bolt_component, &data)?; + Ok(()) + } + }; + let update_with_session_fn = quote! { + #[automatically_derived] + pub fn update_with_session(ctx: Context, data: Vec) -> Result<()> { + let bolt_metadata = BoltMetadata::try_from_account_info(&ctx.accounts.bolt_component)?; + bolt_lang::instructions::update_with_session(&ctx.accounts.instruction_sysvar_account.to_account_info(), &ctx.accounts.authority, bolt_metadata.authority, &mut ctx.accounts.bolt_component, &ctx.accounts.session_token, &data)?; + Ok(()) + } + }; + let update_struct = quote! { + #[automatically_derived] + #[derive(Accounts)] + pub struct Update<'info> { + #[account(mut)] + pub bolt_component: AccountInfo<'info>, + #[account()] + pub authority: Signer<'info>, + #[account(address = anchor_lang::solana_program::sysvar::instructions::id())] + pub instruction_sysvar_account: AccountInfo<'info>, + } + }; + let update_with_session_struct = quote! { + #[automatically_derived] + #[derive(Accounts)] + pub struct UpdateWithSession<'info> { + #[account(mut)] + pub bolt_component: AccountInfo<'info>, + #[account()] + pub authority: Signer<'info>, + #[account(address = anchor_lang::solana_program::sysvar::instructions::id())] + pub instruction_sysvar_account: AccountInfo<'info>, + #[account(constraint = session_token.to_account_info().owner == &bolt_lang::session_keys::ID)] + pub session_token: Account<'info, bolt_lang::session_keys::SessionToken>, + } + }; + module.content.as_mut().map(|(brace, items)| { + items.extend( + vec![ + update_fn, + update_struct, + update_with_session_fn, + update_with_session_struct, + ] + .into_iter() + .map(|item| syn::parse2(item).expect("Failed to parse generate update item")) + .collect::>(), + ); + (brace, items.clone()) + }); +} + +/// Checks if the field is expecting a program. +fn is_expecting_program(field: &Field) -> bool { + field.ty.to_token_stream().to_string().contains("Program") +} diff --git a/crates/bolt-lang/attribute/src/component/mod.rs b/crates/bolt-lang/attribute/src/component/mod.rs new file mode 100644 index 00000000..ba731c6e --- /dev/null +++ b/crates/bolt-lang/attribute/src/component/mod.rs @@ -0,0 +1,31 @@ +mod attributes; +mod generate; + +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, DeriveInput}; + +pub use attributes::*; +pub use generate::*; + +use crate::{common::generate_program, delegate::inject_delegate_items}; + +pub fn process(attr: TokenStream, item: TokenStream) -> TokenStream { + let mut type_ = parse_macro_input!(item as DeriveInput); + let mut program = generate_program(&type_.ident.to_string()); + + let attributes = Attributes::from(attr); + generate_implementation(&mut program, &attributes, &type_); + generate_instructions(&mut program, &type_.ident, None); + if attributes.delegate { + inject_delegate_items(&mut program, vec![(type_.ident.clone(), "".to_string())]); + } + generate_update(&mut program); + enrich_type(&mut type_); + + let expanded = quote! { + #program + #type_ + }; + expanded.into() +} diff --git a/crates/bolt-lang/attribute/src/delegate/mod.rs b/crates/bolt-lang/attribute/src/delegate/mod.rs new file mode 100644 index 00000000..967cc58a --- /dev/null +++ b/crates/bolt-lang/attribute/src/delegate/mod.rs @@ -0,0 +1,192 @@ +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::ItemMod; + +/// Injects delegate-related functions and structs directly into the program module. +pub fn inject_delegate_items(module: &mut ItemMod, components: Vec<(syn::Ident, String)>) { + if components.is_empty() { + return; + } + + let ( + delegate_fn, + delegate_struct, + reinit_undelegate_fn, + reinit_undelegate_struct, + undelegate_fn, + undelegate_struct, + ) = generate_delegate_set(components); + + module.content.as_mut().map(|(brace, items)| { + items.extend( + vec![ + delegate_fn, + delegate_struct, + reinit_undelegate_fn, + reinit_undelegate_struct, + undelegate_fn, + undelegate_struct, + ] + .into_iter() + .map(|item| syn::parse2(item).expect("Failed to parse delegate item")) + .collect::>(), + ); + (brace, items.clone()) + }); +} + +/// Generates the delegate/undelegate functions and related structs to inject in the component program. +fn generate_delegate_set( + components: Vec<(syn::Ident, String)>, +) -> ( + TokenStream2, + TokenStream2, + TokenStream2, + TokenStream2, + TokenStream2, + TokenStream2, +) { + let component_matches = components.iter().map(|(component, name)| quote! { + #component::DISCRIMINATOR => &[#component::seed(), #name.as_bytes(), &ctx.accounts.entity.key().to_bytes()] + }).collect::>(); + + let delegate_fn = quote! { + #[automatically_derived] + pub fn delegate(ctx: Context, commit_frequency_ms: u32, validator: Option) -> Result<()> { + let discriminator = ::bolt_lang::BoltMetadata::discriminator_from_account_info(&ctx.accounts.account)?; + + let pda_seeds: &[&[u8]] = match discriminator.as_slice() { + #(#component_matches),*, + _ => return Err(error!(::bolt_lang::BoltError::ComponentNotDelegateable)), + }; + + let del_accounts = ::bolt_lang::DelegateAccounts { + payer: &ctx.accounts.payer, + pda: &ctx.accounts.account, + owner_program: &ctx.accounts.owner_program, + buffer: &ctx.accounts.buffer, + delegation_record: &ctx.accounts.delegation_record, + delegation_metadata: &ctx.accounts.delegation_metadata, + delegation_program: &ctx.accounts.delegation_program, + system_program: &ctx.accounts.system_program, + }; + + let config = ::bolt_lang::DelegateConfig { commit_frequency_ms, validator }; + + ::bolt_lang::delegate_account( + del_accounts, + pda_seeds, + config, + )?; + + Ok(()) + } + }; + + let delegate_struct = quote! { + #[automatically_derived] + #[derive(Accounts)] + pub struct DelegateInput<'info> { + pub payer: Signer<'info>, + #[account()] + pub entity: Account<'info, Entity>, + /// CHECK: + #[account(mut)] + pub account: AccountInfo<'info>, + /// CHECK:` + pub owner_program: AccountInfo<'info>, + /// CHECK: + #[account(mut)] + pub buffer: AccountInfo<'info>, + /// CHECK:` + #[account(mut)] + pub delegation_record: AccountInfo<'info>, + /// CHECK:` + #[account(mut)] + pub delegation_metadata: AccountInfo<'info>, + /// CHECK:` + pub delegation_program: AccountInfo<'info>, + /// CHECK:` + pub system_program: AccountInfo<'info>, + } + }; + + let reinit_undelegate_fn = quote! { + #[automatically_derived] + pub fn process_undelegation(ctx: Context, account_seeds: Vec>) -> Result<()> { + let [delegated_account, buffer, payer, system_program] = [ + &ctx.accounts.delegated_account, + &ctx.accounts.buffer, + &ctx.accounts.payer, + &ctx.accounts.system_program, + ]; + ::bolt_lang::undelegate_account( + delegated_account, + &id(), + buffer, + payer, + system_program, + account_seeds, + )?; + Ok(()) + } + }; + + let reinit_undelegate_struct = quote! { + #[automatically_derived] + #[derive(Accounts)] + pub struct ProcessUndelegation<'info> { + /// CHECK:` + #[account(mut)] + pub delegated_account: AccountInfo<'info>, + /// CHECK:` + #[account()] + pub buffer: AccountInfo<'info>, + /// CHECK: + #[account(mut)] + pub payer: AccountInfo<'info>, + /// CHECK: + pub system_program: AccountInfo<'info>, + } + }; + + let undelegate_fn = quote! { + #[automatically_derived] + pub fn undelegate(ctx: Context) -> Result<()> { + ::bolt_lang::commit_and_undelegate_accounts( + &ctx.accounts.payer, + vec![&ctx.accounts.delegated_account.to_account_info()], + &ctx.accounts.magic_context, + &ctx.accounts.magic_program, + )?; + Ok(()) + } + }; + + let undelegate_struct = quote! { + #[automatically_derived] + #[derive(Accounts)] + pub struct Undelegate<'info> { + #[account(mut)] + pub payer: Signer<'info>, + #[account(mut)] + /// CHECK: The delegated component + pub delegated_account: AccountInfo<'info>, + #[account(mut, address = ::bolt_lang::MAGIC_CONTEXT_ID)] + /// CHECK:` + pub magic_context: AccountInfo<'info>, + #[account()] + /// CHECK:` + pub magic_program: Program<'info, MagicProgram> + } + }; + + ( + delegate_fn, + delegate_struct, + reinit_undelegate_fn, + reinit_undelegate_struct, + undelegate_fn, + undelegate_struct, + ) +} diff --git a/crates/bolt-lang/attribute/src/lib.rs b/crates/bolt-lang/attribute/src/lib.rs new file mode 100644 index 00000000..9ee8c617 --- /dev/null +++ b/crates/bolt-lang/attribute/src/lib.rs @@ -0,0 +1,7 @@ +extern crate proc_macro; + +pub mod bundle; +mod common; +pub mod component; +pub mod delegate; +pub mod system; diff --git a/crates/bolt-lang/attribute/src/system/mod.rs b/crates/bolt-lang/attribute/src/system/mod.rs new file mode 100644 index 00000000..5981c1fa --- /dev/null +++ b/crates/bolt-lang/attribute/src/system/mod.rs @@ -0,0 +1,561 @@ +use heck::{ToPascalCase, ToSnakeCase}; +use proc_macro::TokenStream; +use proc_macro2::{Ident, Span}; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::{ + parse_macro_input, parse_quote, visit_mut::VisitMut, Expr, FnArg, GenericArgument, Item, + ItemFn, ItemMod, ItemStruct, PathArguments, ReturnType, Stmt, Type, TypePath, +}; + +#[derive(Default)] +struct SystemTransform { + is_bundle: bool, +} + +#[derive(Default)] +struct Extractor { + context_struct_name: Option, + field_count: Option, +} + +fn generate_bolt_execute_wrapper( + fn_ident: Ident, + callee_ident: Ident, + components_ident: Ident, + bumps_ident: Ident, +) -> Item { + parse_quote! { + pub fn #fn_ident<'a, 'b, 'info>(ctx: Context<'a, 'b, 'info, 'info, VariadicBoltComponents<'info>>, args: Vec) -> Result>> { + let mut components = #components_ident::try_from(&ctx)?; + let bumps = #bumps_ident {}; + let context = Context::new(ctx.program_id, &mut components, ctx.remaining_accounts, bumps); + #callee_ident(context, args) + } + } +} + +pub fn process(_attr: TokenStream, item: TokenStream) -> TokenStream { + let mut ast = parse_macro_input!(item as ItemMod); + + let mut extractor = Extractor::default(); + extractor.visit_item_mod_mut(&mut ast); + + if extractor.field_count.is_some() { + let use_super = syn::parse_quote! { use super::*; }; + if let Some((_, ref mut items)) = ast.content { + items.insert(0, syn::Item::Use(use_super)); + // Ensure a single VariadicBoltComponents per program for standalone #[system] + let has_variadic = items.iter().any( + |it| matches!(it, syn::Item::Struct(s) if s.ident == "VariadicBoltComponents"), + ); + if !has_variadic { + let variadic_struct: Item = parse_quote! { + #[derive(Accounts)] + pub struct VariadicBoltComponents<'info> { + pub authority: Signer<'info>, + } + }; + items.insert(1, variadic_struct); + } + let wrapper = generate_bolt_execute_wrapper( + Ident::new("bolt_execute", Span::call_site()), + Ident::new("execute", Span::call_site()), + Ident::new("Components", Span::call_site()), + Ident::new("ComponentsBumps", Span::call_site()), + ); + items.push(wrapper); + } + + let mut transform = SystemTransform { is_bundle: false }; + transform.visit_item_mod_mut(&mut ast); + + let expanded = quote! { + #[program] + #ast + }; + + TokenStream::from(expanded) + } else { + panic!( + "Could not find the component bundle: {} in the module", + extractor.context_struct_name.unwrap() + ); + } +} + +pub fn transform_module_for_bundle(module: &mut ItemMod, name_suffix: Option<&str>) -> Vec { + module.attrs.retain(|a| !a.path.is_ident("system")); + + let mut extractor = Extractor::default(); + extractor.visit_item_mod_mut(module); + + if extractor.field_count.is_none() { + panic!( + "Could not find the component bundle: {} in the module", + extractor.context_struct_name.unwrap_or_default() + ); + } + + let mut transform = SystemTransform { is_bundle: true }; + transform.visit_item_mod_mut(module); + + let mut items: Vec = match module.content.take() { + Some((_, items)) => items, + None => vec![], + }; + + if let Some(suffix) = name_suffix { + let pascal = suffix.to_pascal_case(); + let new_components_ident = Ident::new(&format!("{}Components", pascal), Span::call_site()); + let new_bumps_ident = Ident::new(&format!("{}ComponentsBumps", pascal), Span::call_site()); + + struct SystemRename { + new_components: Ident, + new_bumps: Ident, + } + impl VisitMut for SystemRename { + fn visit_item_struct_mut(&mut self, i: &mut ItemStruct) { + if i.ident == "Components" { + i.ident = self.new_components.clone(); + } else if i.ident == "ComponentsBumps" { + i.ident = self.new_bumps.clone(); + } + syn::visit_mut::visit_item_struct_mut(self, i); + } + fn visit_type_path_mut(&mut self, i: &mut TypePath) { + for seg in i.path.segments.iter_mut() { + if seg.ident == "Components" { + seg.ident = self.new_components.clone(); + } else if seg.ident == "ComponentsBumps" { + seg.ident = self.new_bumps.clone(); + } + } + syn::visit_mut::visit_type_path_mut(self, i); + } + fn visit_expr_path_mut(&mut self, i: &mut syn::ExprPath) { + if let Some(seg) = i.path.segments.last_mut() { + if seg.ident == "Components" { + seg.ident = self.new_components.clone(); + } else if seg.ident == "ComponentsBumps" { + seg.ident = self.new_bumps.clone(); + } + } + syn::visit_mut::visit_expr_path_mut(self, i); + } + } + + // Rename inner execute to a unique name per system to avoid collisions + let new_execute_ident = Ident::new(&format!("execute_{}", suffix), Span::call_site()); + struct ExecRename { + new_ident: Ident, + } + impl VisitMut for ExecRename { + fn visit_item_fn_mut(&mut self, i: &mut ItemFn) { + if i.sig.ident == "execute" { + i.sig.ident = self.new_ident.clone(); + } + syn::visit_mut::visit_item_fn_mut(self, i); + } + } + + let mut renamer = SystemRename { + new_components: new_components_ident.clone(), + new_bumps: new_bumps_ident.clone(), + }; + for item in items.iter_mut() { + renamer.visit_item_mut(item); + } + + let mut exec_renamer = ExecRename { + new_ident: new_execute_ident.clone(), + }; + for item in items.iter_mut() { + exec_renamer.visit_item_mut(item); + } + + let fn_ident = Ident::new(&format!("{}_bolt_execute", suffix), Span::call_site()); + let wrapper_fn = generate_bolt_execute_wrapper( + fn_ident, + new_execute_ident, + new_components_ident, + new_bumps_ident, + ); + items.push(wrapper_fn); + } else { + let wrapper_fn = generate_bolt_execute_wrapper( + Ident::new("bolt_execute", Span::call_site()), + Ident::new("execute", Span::call_site()), + Ident::new("Components", Span::call_site()), + Ident::new("ComponentsBumps", Span::call_site()), + ); + items.push(wrapper_fn); + } + + items +} + +impl SystemTransform { + fn visit_stmts_mut(&mut self, stmts: &mut Vec) { + for stmt in stmts { + if let Stmt::Expr(ref mut expr) | Stmt::Semi(ref mut expr, _) = stmt { + self.visit_expr_mut(expr); + } + } + } +} + +impl VisitMut for SystemTransform { + fn visit_expr_mut(&mut self, expr: &mut Expr) { + match expr { + Expr::ForLoop(for_loop_expr) => { + self.visit_stmts_mut(&mut for_loop_expr.body.stmts); + } + Expr::Loop(loop_expr) => { + self.visit_stmts_mut(&mut loop_expr.body.stmts); + } + Expr::If(if_expr) => { + self.visit_stmts_mut(&mut if_expr.then_branch.stmts); + if let Some((_, else_expr)) = &mut if_expr.else_branch { + self.visit_expr_mut(else_expr); + } + } + Expr::Block(block_expr) => { + self.visit_stmts_mut(&mut block_expr.block.stmts); + } + _ => (), + } + if let Some(inner_variable) = Self::extract_inner_ok_expression(expr) { + let new_return_expr: Expr = match inner_variable { + Expr::Tuple(tuple_expr) => { + let tuple_elements = tuple_expr.elems.iter().map(|elem| { + quote! { (#elem).try_to_vec()? } + }); + parse_quote! { Ok((#(#tuple_elements),*)) } + } + _ => { + parse_quote! { + #inner_variable.try_to_vec() + } + } + }; + if let Expr::Return(return_expr) = expr { + return_expr.expr = Some(Box::new(new_return_expr)); + } else { + *expr = new_return_expr; + } + } + } + + fn visit_item_fn_mut(&mut self, item_fn: &mut ItemFn) { + if item_fn.sig.ident == "execute" { + Self::inject_lifetimes_and_context(item_fn); + if let ReturnType::Type(_, type_box) = &item_fn.sig.output { + if let Type::Path(type_path) = &**type_box { + if !Self::check_is_result_vec_u8(type_path) { + item_fn.sig.output = parse_quote! { -> Result>> }; + let block = &mut item_fn.block; + self.visit_stmts_mut(&mut block.stmts); + } + } + } + Self::modify_args(item_fn); + } + } + + fn visit_item_mod_mut(&mut self, item_mod: &mut ItemMod) { + let content = match item_mod.content.as_mut() { + Some(content) => &mut content.1, + None => return, + }; + + let mut extra_accounts_struct_name = None; + + let system_input = content + .iter() + .find_map(|item| { + if let syn::Item::Struct(item_struct) = item { + if item_struct + .attrs + .iter() + .any(|attr| attr.path.is_ident("system_input")) + { + Some(item_struct) + } else { + None + } + } else { + None + } + }) + .cloned(); + + for item in content.iter_mut() { + match item { + syn::Item::Fn(item_fn) => self.visit_item_fn_mut(item_fn), + syn::Item::Struct(item_struct) => { + if let Some(attr) = item_struct + .attrs + .iter_mut() + .find(|attr| attr.path.is_ident("system_input")) + { + attr.tokens.append_all(quote! { (session_key) }); + } + if let Some(attr) = item_struct + .attrs + .iter_mut() + .find(|attr| attr.path.is_ident("extra_accounts")) + { + if let Some(system_input) = &system_input { + let mod_ident = &item_mod.ident.to_string().to_pascal_case(); + let ident = if self.is_bundle { + syn::Ident::new( + &format!("{}{}", mod_ident, &system_input.ident), + Span::call_site(), + ) + } else { + system_input.ident.clone() + }; + let type_path: syn::TypePath = syn::parse_quote! { #ident<'info> }; + let literal = type_path.to_token_stream().to_string(); + attr.tokens.append_all(quote! { (input = #literal) }); + } + extra_accounts_struct_name = Some(&item_struct.ident); + } + } + _ => {} + } + } + + if let Some(struct_name) = extra_accounts_struct_name { + let init_extra_accounts_name = syn::Ident::new( + &format!( + "init_extra_accounts_{}", + struct_name.to_string().to_snake_case() + ), + Span::call_site(), + ); + let initialize_extra_accounts = quote! { + #[automatically_derived] + pub fn #init_extra_accounts_name(_ctx: Context<#struct_name>) -> Result<()> { + Ok(()) + } + }; + content.push(syn::parse2(initialize_extra_accounts).unwrap()); + } + } +} + +impl SystemTransform { + fn inject_lifetimes_and_context(item_fn: &mut ItemFn) { + let lifetime_idents = ["a", "b", "c", "info"]; + for name in lifetime_idents.iter() { + let exists = item_fn.sig.generics.params.iter().any(|p| match p { + syn::GenericParam::Lifetime(l) => l.lifetime.ident == *name, + _ => false, + }); + if !exists { + let lifetime: syn::Lifetime = + syn::parse_str(&format!("'{}", name)).expect("valid lifetime"); + let gp: syn::GenericParam = syn::parse_quote!(#lifetime); + item_fn.sig.generics.params.push(gp); + } + } + + if let Some(FnArg::Typed(pat_type)) = item_fn.sig.inputs.first_mut() { + if let Type::Path(type_path) = pat_type.ty.as_mut() { + if let Some(last_segment) = type_path.path.segments.last_mut() { + if last_segment.ident == "Context" { + let mut components_ty_opt: Option = None; + if let PathArguments::AngleBracketed(args) = &last_segment.arguments { + for ga in args.args.iter() { + if let GenericArgument::Type(t) = ga { + components_ty_opt = Some(t.clone()); + break; + } + } + } + + if let Some(components_ty) = components_ty_opt { + let components_with_info: Type = match components_ty { + Type::Path(mut tp) => { + let seg = tp.path.segments.last_mut().unwrap(); + match &mut seg.arguments { + PathArguments::AngleBracketed(ab) => { + if ab.args.is_empty() { + ab.args.push(GenericArgument::Lifetime( + syn::parse_quote!('info), + )); + } + } + _ => { + seg.arguments = PathArguments::AngleBracketed( + syn::AngleBracketedGenericArguments { + colon2_token: None, + lt_token: Default::default(), + args: std::iter::once( + GenericArgument::Lifetime( + syn::parse_quote!('info), + ), + ) + .collect(), + gt_token: Default::default(), + }, + ); + } + } + Type::Path(tp) + } + other => other, + }; + + let new_ty: Type = syn::parse_quote! { + Context<'a, 'b, 'c, 'info, #components_with_info> + }; + pat_type.ty = Box::new(new_ty); + } + } + } + } + } + } + + fn check_is_result_vec_u8(ty: &TypePath) -> bool { + if let Some(segment) = ty.path.segments.last() { + if segment.ident == "Result" { + if let PathArguments::AngleBracketed(args) = &segment.arguments { + if let Some(GenericArgument::Type(Type::Tuple(tuple))) = args.args.first() { + return tuple.elems.iter().all(|elem| { + if let Type::Path(type_path) = elem { + if let Some(segment) = type_path.path.segments.first() { + return segment.ident == "Vec" && Self::is_u8_vec(segment); + } + } + false + }); + } else if let Some(GenericArgument::Type(Type::Path(type_path))) = + args.args.first() + { + if let Some(segment) = type_path.path.segments.first() { + return segment.ident == "Vec" && Self::is_u8_vec(segment); + } + } + } + } + } + false + } + + fn is_u8_vec(segment: &syn::PathSegment) -> bool { + if let PathArguments::AngleBracketed(args) = &segment.arguments { + if let Some(GenericArgument::Type(Type::Path(path))) = args.args.first() { + if let Some(segment) = path.path.segments.first() { + return segment.ident == "u8"; + } + } + } + false + } + + fn extract_inner_ok_expression(expr: &Expr) -> Option<&Expr> { + match expr { + Expr::Call(expr_call) => { + if let Expr::Path(expr_path) = &*expr_call.func { + if let Some(last_segment) = expr_path.path.segments.last() { + if last_segment.ident == "Ok" && !expr_call.args.is_empty() { + return expr_call.args.first(); + } + } + } + } + Expr::Return(expr_return) => { + if let Some(expr_return_inner) = &expr_return.expr { + if let Expr::Call(expr_call) = expr_return_inner.as_ref() { + if let Expr::Path(expr_path) = &*expr_call.func { + if let Some(last_segment) = expr_path.path.segments.last() { + if last_segment.ident == "Ok" && !expr_call.args.is_empty() { + return expr_call.args.first(); + } + } + } + } + } + } + _ => {} + } + None + } + + fn modify_args(item_fn: &mut ItemFn) { + if item_fn.sig.inputs.len() >= 2 { + let second_arg = &mut item_fn.sig.inputs[1]; + let is_vec_u8 = if let FnArg::Typed(syn::PatType { ty, .. }) = second_arg { + match &**ty { + Type::Path(type_path) => { + if let Some(segment) = type_path.path.segments.first() { + segment.ident == "Vec" && Self::is_u8_vec(segment) + } else { + false + } + } + _ => false, + } + } else { + false + }; + if !is_vec_u8 { + if let FnArg::Typed(pat_type) = second_arg { + let original_type = pat_type.ty.to_token_stream(); + let arg_original_name = pat_type.pat.to_token_stream(); + if let syn::Pat::Ident(ref mut pat_ident) = *pat_type.pat { + let new_ident_name = format!("_{}", pat_ident.ident); + pat_ident.ident = + Ident::new(&new_ident_name, proc_macro2::Span::call_site()); + } + let arg_name = pat_type.pat.to_token_stream(); + pat_type.ty = Box::new(syn::parse_quote! { Vec }); + let parse_stmt: Stmt = parse_quote! { + let #arg_original_name = parse_args::<#original_type>(&#arg_name); + }; + item_fn.block.stmts.insert(0, parse_stmt); + } + } + } + } +} + +impl VisitMut for Extractor { + fn visit_item_fn_mut(&mut self, i: &mut ItemFn) { + for input in &i.sig.inputs { + if let FnArg::Typed(pat_type) = input { + if let Type::Path(type_path) = &*pat_type.ty { + let last_segment = type_path + .path + .segments + .last() + .expect("Context segment not found"); + if last_segment.ident == "Context" { + if let PathArguments::AngleBracketed(args) = &last_segment.arguments { + for ga in args.args.iter() { + if let syn::GenericArgument::Type(syn::Type::Path(type_path)) = ga { + if let Some(first_seg) = type_path.path.segments.first() { + self.context_struct_name = + Some(first_seg.ident.to_string()); + break; + } + } + } + } + } + } + } + } + } + + fn visit_item_struct_mut(&mut self, i: &mut ItemStruct) { + if let Some(name) = &self.context_struct_name { + if i.ident == name { + self.field_count = Some(i.fields.len()); + } + } + } +} diff --git a/crates/bolt-lang/attribute/system-input/src/lib.rs b/crates/bolt-lang/attribute/system-input/src/lib.rs index 2701aa8c..70232764 100644 --- a/crates/bolt-lang/attribute/system-input/src/lib.rs +++ b/crates/bolt-lang/attribute/system-input/src/lib.rs @@ -76,9 +76,7 @@ pub fn system_input(_attr: TokenStream, item: TokenStream) -> TokenStream { #[derive(Accounts)] pub struct #name<'info> { #(#transformed_fields)* - /// CHECK: Authority check - #[account()] - pub authority: AccountInfo<'info>, + pub authority: Signer<'info>, } }; @@ -99,14 +97,10 @@ pub fn system_input(_attr: TokenStream, item: TokenStream) -> TokenStream { let number_of_components = fields.len(); - let output_trait = quote! { - pub trait NumberOfComponents<'a, 'b, 'c, 'info, T> { - const NUMBER_OF_COMPONENTS: usize; - } - }; + // NumberOfComponents trait now lives in bolt_lang; no local trait emission needed let output_trait_implementation = quote! { - impl<'a, 'b, 'c, 'info, T: bolt_lang::Bumps> NumberOfComponents<'a, 'b, 'c, 'info, T> for Context<'a, 'b, 'c, 'info, T> { + impl<'info> bolt_lang::NumberOfComponents for #name<'info> { const NUMBER_OF_COMPONENTS: usize = #number_of_components; } }; @@ -131,16 +125,8 @@ pub fn system_input(_attr: TokenStream, item: TokenStream) -> TokenStream { let output = quote! { #output_struct #output_impl - #output_trait #output_trait_implementation #(#components_imports)* - - #[derive(Accounts)] - pub struct VariadicBoltComponents<'info> { - /// CHECK: Authority check - #[account()] - pub authority: AccountInfo<'info>, - } }; TokenStream::from(output) diff --git a/crates/bolt-lang/attribute/system/Cargo.toml b/crates/bolt-lang/attribute/system/Cargo.toml index 0475629b..28e79b50 100644 --- a/crates/bolt-lang/attribute/system/Cargo.toml +++ b/crates/bolt-lang/attribute/system/Cargo.toml @@ -14,4 +14,5 @@ proc-macro = true [dependencies] syn = { workspace = true, features = ["visit-mut"] } quote = { workspace = true } -proc-macro2 = { workspace = true } \ No newline at end of file +proc-macro2 = { workspace = true } +bolt-attribute.workspace = true \ No newline at end of file diff --git a/crates/bolt-lang/attribute/system/src/lib.rs b/crates/bolt-lang/attribute/system/src/lib.rs index ef49a41f..2a0c6e08 100644 --- a/crates/bolt-lang/attribute/system/src/lib.rs +++ b/crates/bolt-lang/attribute/system/src/lib.rs @@ -1,424 +1,6 @@ use proc_macro::TokenStream; -use proc_macro2::Ident; -use quote::{quote, ToTokens, TokenStreamExt}; -use syn::{ - parse_macro_input, parse_quote, visit_mut::VisitMut, Expr, FnArg, GenericArgument, ItemFn, - ItemMod, ItemStruct, PathArguments, ReturnType, Stmt, Type, TypePath, -}; -#[derive(Default)] -struct SystemTransform; - -#[derive(Default)] -struct Extractor { - context_struct_name: Option, - field_count: Option, -} - -/// This macro attribute is used to define a BOLT system. -/// -/// Bolt components are themselves programs. The macro adds parsing and serialization -/// -/// # Example -/// ```ignore -/// #[system] -/// pub mod system_fly { -/// pub fn execute(ctx: Context, _args: Vec) -> Result { -/// let pos = Position { -/// x: ctx.accounts.position.x, -/// y: ctx.accounts.position.y, -/// z: ctx.accounts.position.z + 1, -/// }; -/// Ok(pos) -/// } -/// } -/// ``` #[proc_macro_attribute] -pub fn system(_attr: TokenStream, item: TokenStream) -> TokenStream { - let mut ast = parse_macro_input!(item as ItemMod); - - // Extract the number of components from the module - let mut extractor = Extractor::default(); - extractor.visit_item_mod_mut(&mut ast); - - if extractor.field_count.is_some() { - let use_super = syn::parse_quote! { use super::*; }; - if let Some((_, ref mut items)) = ast.content { - items.insert(0, syn::Item::Use(use_super)); - SystemTransform::add_variadic_execute_function(items); - } - - let mut transform = SystemTransform; - transform.visit_item_mod_mut(&mut ast); - - // Add `#[program]` macro and try_to_vec implementation - let expanded = quote! { - #[program] - #ast - }; - - TokenStream::from(expanded) - } else { - panic!( - "Could not find the component bundle: {} in the module", - extractor.context_struct_name.unwrap() - ); - } -} - -impl SystemTransform { - fn visit_stmts_mut(&mut self, stmts: &mut Vec) { - for stmt in stmts { - if let Stmt::Expr(ref mut expr) | Stmt::Semi(ref mut expr, _) = stmt { - self.visit_expr_mut(expr); - } - } - } -} - -/// Visits the AST and modifies the system function -impl VisitMut for SystemTransform { - // Modify the return instruction to return Result> - fn visit_expr_mut(&mut self, expr: &mut Expr) { - match expr { - Expr::ForLoop(for_loop_expr) => { - self.visit_stmts_mut(&mut for_loop_expr.body.stmts); - } - Expr::Loop(loop_expr) => { - self.visit_stmts_mut(&mut loop_expr.body.stmts); - } - Expr::If(if_expr) => { - self.visit_stmts_mut(&mut if_expr.then_branch.stmts); - if let Some((_, else_expr)) = &mut if_expr.else_branch { - self.visit_expr_mut(else_expr); - } - } - Expr::Block(block_expr) => { - self.visit_stmts_mut(&mut block_expr.block.stmts); - } - _ => (), - } - if let Some(inner_variable) = Self::extract_inner_ok_expression(expr) { - let new_return_expr: Expr = match inner_variable { - Expr::Tuple(tuple_expr) => { - let tuple_elements = tuple_expr.elems.iter().map(|elem| { - quote! { (#elem).try_to_vec()? } - }); - parse_quote! { Ok((#(#tuple_elements),*)) } - } - _ => { - parse_quote! { - #inner_variable.try_to_vec() - } - } - }; - if let Expr::Return(return_expr) = expr { - return_expr.expr = Some(Box::new(new_return_expr)); - } else { - *expr = new_return_expr; - } - } - } - - // Modify the return type of the system function to Result,*> - fn visit_item_fn_mut(&mut self, item_fn: &mut ItemFn) { - if item_fn.sig.ident == "execute" { - // Ensure execute has lifetimes and a fully-qualified Context - Self::inject_lifetimes_and_context(item_fn); - // Modify the return type to Result> if necessary - if let ReturnType::Type(_, type_box) = &item_fn.sig.output { - if let Type::Path(type_path) = &**type_box { - if !Self::check_is_result_vec_u8(type_path) { - item_fn.sig.output = parse_quote! { -> Result>> }; - // Modify the return statement inside the function body - let block = &mut item_fn.block; - self.visit_stmts_mut(&mut block.stmts); - } - } - } - // If second argument is not Vec, modify it to be so and use parse_args - Self::modify_args(item_fn); - } - } - - // Visit all the functions inside the system module and inject the init_extra_accounts function - // if the module contains a struct with the `extra_accounts` attribute - fn visit_item_mod_mut(&mut self, item_mod: &mut ItemMod) { - let content = match item_mod.content.as_mut() { - Some(content) => &mut content.1, - None => return, - }; - - let mut extra_accounts_struct_name = None; - - for item in content.iter_mut() { - match item { - syn::Item::Fn(item_fn) => self.visit_item_fn_mut(item_fn), - syn::Item::Struct(item_struct) => { - if let Some(attr) = item_struct - .attrs - .iter_mut() - .find(|attr| attr.path.is_ident("system_input")) - { - attr.tokens.append_all(quote! { (session_key) }); - } - if item_struct - .attrs - .iter() - .any(|attr| attr.path.is_ident("extra_accounts")) - { - extra_accounts_struct_name = Some(&item_struct.ident); - break; - } - } - _ => {} - } - } - - if let Some(struct_name) = extra_accounts_struct_name { - let initialize_extra_accounts = quote! { - #[automatically_derived] - pub fn init_extra_accounts(_ctx: Context<#struct_name>) -> Result<()> { - Ok(()) - } - }; - content.push(syn::parse2(initialize_extra_accounts).unwrap()); - } - } -} - -impl SystemTransform { - fn inject_lifetimes_and_context(item_fn: &mut ItemFn) { - // Add lifetimes <'a, 'b, 'c, 'info> if missing - let lifetime_idents = ["a", "b", "c", "info"]; - for name in lifetime_idents.iter() { - let exists = item_fn.sig.generics.params.iter().any(|p| match p { - syn::GenericParam::Lifetime(l) => l.lifetime.ident == *name, - _ => false, - }); - if !exists { - let lifetime: syn::Lifetime = - syn::parse_str(&format!("'{}", name)).expect("valid lifetime"); - let gp: syn::GenericParam = syn::parse_quote!(#lifetime); - item_fn.sig.generics.params.push(gp); - } - } - - // Update the first argument type from Context to Context<'a, 'b, 'c, 'info, Components<'info>> - if let Some(FnArg::Typed(pat_type)) = item_fn.sig.inputs.first_mut() { - if let Type::Path(type_path) = pat_type.ty.as_mut() { - if let Some(last_segment) = type_path.path.segments.last_mut() { - if last_segment.ident == "Context" { - // Extract Components path from existing generic args (if any) - let mut components_ty_opt: Option = None; - if let PathArguments::AngleBracketed(args) = &last_segment.arguments { - for ga in args.args.iter() { - if let GenericArgument::Type(t) = ga { - components_ty_opt = Some(t.clone()); - break; - } - } - } - - // If not found, leave early - if let Some(components_ty) = components_ty_opt { - // Ensure Components<'info> - let components_with_info: Type = match components_ty { - Type::Path(mut tp) => { - let seg = tp.path.segments.last_mut().unwrap(); - match &mut seg.arguments { - PathArguments::AngleBracketed(ab) => { - if ab.args.is_empty() { - ab.args.push(GenericArgument::Lifetime( - syn::parse_quote!('info), - )); - } - } - _ => { - seg.arguments = PathArguments::AngleBracketed( - syn::AngleBracketedGenericArguments { - colon2_token: None, - lt_token: Default::default(), - args: std::iter::once( - GenericArgument::Lifetime( - syn::parse_quote!('info), - ), - ) - .collect(), - gt_token: Default::default(), - }, - ); - } - } - Type::Path(tp) - } - other => other, - }; - - // Build new Context<'a, 'b, 'c, 'info, Components<'info>> type - let new_ty: Type = syn::parse_quote! { - Context<'a, 'b, 'c, 'info, #components_with_info> - }; - pat_type.ty = Box::new(new_ty); - } - } - } - } - } - } - fn add_variadic_execute_function(content: &mut Vec) { - content.push(syn::parse2(quote! { - pub fn bolt_execute<'a, 'b, 'info>(ctx: Context<'a, 'b, 'info, 'info, VariadicBoltComponents<'info>>, args: Vec) -> Result>> { - let mut components = Components::try_from(&ctx)?; - let bumps = ComponentsBumps {}; - let context = Context::new(ctx.program_id, &mut components, ctx.remaining_accounts, bumps); - execute(context, args) - } - }).unwrap()); - } - - // Helper function to check if a type is `Vec` or `(Vec, Vec, ...)` - fn check_is_result_vec_u8(ty: &TypePath) -> bool { - if let Some(segment) = ty.path.segments.last() { - if segment.ident == "Result" { - if let PathArguments::AngleBracketed(args) = &segment.arguments { - if let Some(GenericArgument::Type(Type::Tuple(tuple))) = args.args.first() { - return tuple.elems.iter().all(|elem| { - if let Type::Path(type_path) = elem { - if let Some(segment) = type_path.path.segments.first() { - return segment.ident == "Vec" && Self::is_u8_vec(segment); - } - } - false - }); - } else if let Some(GenericArgument::Type(Type::Path(type_path))) = - args.args.first() - { - if let Some(segment) = type_path.path.segments.first() { - return segment.ident == "Vec" && Self::is_u8_vec(segment); - } - } - } - } - } - false - } - - // Helper function to check if a type is Vec - fn is_u8_vec(segment: &syn::PathSegment) -> bool { - if let PathArguments::AngleBracketed(args) = &segment.arguments { - if let Some(GenericArgument::Type(Type::Path(path))) = args.args.first() { - if let Some(segment) = path.path.segments.first() { - return segment.ident == "u8"; - } - } - } - false - } - - // Helper function to check if an expression is an `Ok(...)` or `return Ok(...);` variant - fn extract_inner_ok_expression(expr: &Expr) -> Option<&Expr> { - match expr { - Expr::Call(expr_call) => { - // Direct `Ok(...)` call - if let Expr::Path(expr_path) = &*expr_call.func { - if let Some(last_segment) = expr_path.path.segments.last() { - if last_segment.ident == "Ok" && !expr_call.args.is_empty() { - // Return the first argument of the Ok(...) call - return expr_call.args.first(); - } - } - } - } - Expr::Return(expr_return) => { - // `return Ok(...);` - if let Some(expr_return_inner) = &expr_return.expr { - if let Expr::Call(expr_call) = expr_return_inner.as_ref() { - if let Expr::Path(expr_path) = &*expr_call.func { - if let Some(last_segment) = expr_path.path.segments.last() { - if last_segment.ident == "Ok" && !expr_call.args.is_empty() { - // Return the first argument of the return Ok(...) call - return expr_call.args.first(); - } - } - } - } - } - } - _ => {} - } - None - } - - fn modify_args(item_fn: &mut ItemFn) { - if item_fn.sig.inputs.len() >= 2 { - let second_arg = &mut item_fn.sig.inputs[1]; - let is_vec_u8 = if let FnArg::Typed(syn::PatType { ty, .. }) = second_arg { - match &**ty { - Type::Path(type_path) => { - if let Some(segment) = type_path.path.segments.first() { - segment.ident == "Vec" && Self::is_u8_vec(segment) - } else { - false - } - } - _ => false, - } - } else { - false - }; - if !is_vec_u8 { - if let FnArg::Typed(pat_type) = second_arg { - let original_type = pat_type.ty.to_token_stream(); - let arg_original_name = pat_type.pat.to_token_stream(); - if let syn::Pat::Ident(ref mut pat_ident) = *pat_type.pat { - let new_ident_name = format!("_{}", pat_ident.ident); - pat_ident.ident = - Ident::new(&new_ident_name, proc_macro2::Span::call_site()); - } - let arg_name = pat_type.pat.to_token_stream(); - pat_type.ty = Box::new(syn::parse_quote! { Vec }); - let parse_stmt: Stmt = parse_quote! { - let #arg_original_name = parse_args::<#original_type>(&#arg_name); - }; - item_fn.block.stmts.insert(0, parse_stmt); - } - } - } - } -} - -/// Visits the AST to extract the number of input components -impl VisitMut for Extractor { - fn visit_item_fn_mut(&mut self, i: &mut ItemFn) { - for input in &i.sig.inputs { - if let FnArg::Typed(pat_type) = input { - if let Type::Path(type_path) = &*pat_type.ty { - let last_segment = type_path.path.segments.last().unwrap(); - if last_segment.ident == "Context" { - if let PathArguments::AngleBracketed(args) = &last_segment.arguments { - // Find the first generic argument that is a Type::Path (e.g., Components) - for ga in args.args.iter() { - if let syn::GenericArgument::Type(syn::Type::Path(type_path)) = ga { - if let Some(first_seg) = type_path.path.segments.first() { - self.context_struct_name = - Some(first_seg.ident.to_string()); - break; - } - } - } - } - } - } - } - } - } - - fn visit_item_struct_mut(&mut self, i: &mut ItemStruct) { - if let Some(name) = &self.context_struct_name { - if i.ident == name { - self.field_count = Some(i.fields.len()); - } - } - } +pub fn system(attr: TokenStream, item: TokenStream) -> TokenStream { + bolt_attribute::system::process(attr, item) } diff --git a/crates/bolt-lang/src/cpi/mod.rs b/crates/bolt-lang/src/cpi/mod.rs new file mode 100644 index 00000000..c85feab7 --- /dev/null +++ b/crates/bolt-lang/src/cpi/mod.rs @@ -0,0 +1,19 @@ +use world::program::World; + +use crate::prelude::*; +use crate::BoltError; + +#[inline(always)] +pub fn check(instruction_sysvar_account: &AccountInfo<'_>) -> Result<()> { + let instruction = anchor_lang::solana_program::sysvar::instructions::get_instruction_relative( + 0, + &instruction_sysvar_account.to_account_info(), + ) + .map_err(|_| BoltError::InvalidCaller)?; + require_eq!( + instruction.program_id, + World::id(), + BoltError::InvalidCaller + ); + Ok(()) +} diff --git a/crates/bolt-lang/src/errors.rs b/crates/bolt-lang/src/errors.rs index 6b4fb3f4..4feddc45 100644 --- a/crates/bolt-lang/src/errors.rs +++ b/crates/bolt-lang/src/errors.rs @@ -8,4 +8,10 @@ pub enum BoltError { /// Returned if the wrong authority attempts to sign for an instruction #[msg("Invalid caller: must be called from a CPI instruction")] InvalidCaller, + /// Returned if the account mismatch + #[msg("Account mismatch")] + AccountMismatch, + /// Component is not delegateable + #[msg("Component is not delegateable")] + ComponentNotDelegateable, } diff --git a/crates/bolt-lang/src/instructions/destroy.rs b/crates/bolt-lang/src/instructions/destroy.rs new file mode 100644 index 00000000..e3e9e7a7 --- /dev/null +++ b/crates/bolt-lang/src/instructions/destroy.rs @@ -0,0 +1,39 @@ +use anchor_lang::prelude::*; + +use crate::BoltError; + +pub fn destroy<'info>( + program_id: &Pubkey, + instruction_sysvar_account: &AccountInfo<'info>, + authority: &AccountInfo<'info>, + component_program_data: &AccountInfo<'info>, + component_authority: Pubkey, +) -> Result<()> { + let pda = Pubkey::find_program_address( + &[program_id.as_ref()], + &crate::prelude::solana_program::bpf_loader_upgradeable::id(), + ) + .0; + + if !pda.eq(component_program_data.key) { + return Err(BoltError::InvalidAuthority.into()); + } + + let program_account_data = component_program_data.try_borrow_data()?; + let upgrade_authority = if let crate::prelude::solana_program::bpf_loader_upgradeable::UpgradeableLoaderState::ProgramData { + upgrade_authority_address, + .. + } = + crate::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(BoltError::InvalidAuthority)?; + + if authority.key != &component_authority && authority.key != &upgrade_authority { + return Err(BoltError::InvalidAuthority.into()); + } + + crate::cpi::check(&instruction_sysvar_account.to_account_info()) +} diff --git a/crates/bolt-lang/src/instructions/initialize.rs b/crates/bolt-lang/src/instructions/initialize.rs new file mode 100644 index 00000000..53ea7c3c --- /dev/null +++ b/crates/bolt-lang/src/instructions/initialize.rs @@ -0,0 +1,14 @@ +use crate::borsh::BorshDeserialize; +use anchor_lang::prelude::*; + +pub fn initialize< + 'info, + T: Default + AccountSerialize + AccountDeserialize + BorshDeserialize + Clone, +>( + instruction_sysvar_account: &AccountInfo<'info>, + bolt_component: &mut Account<'info, T>, +) -> Result<()> { + crate::cpi::check(instruction_sysvar_account)?; + bolt_component.set_inner(::default()); + Ok(()) +} diff --git a/crates/bolt-lang/src/instructions/mod.rs b/crates/bolt-lang/src/instructions/mod.rs new file mode 100644 index 00000000..ec33eb69 --- /dev/null +++ b/crates/bolt-lang/src/instructions/mod.rs @@ -0,0 +1,7 @@ +mod destroy; +mod initialize; +mod update; + +pub use destroy::*; +pub use initialize::*; +pub use update::*; diff --git a/crates/bolt-lang/src/instructions/update.rs b/crates/bolt-lang/src/instructions/update.rs new file mode 100644 index 00000000..1720fe8e --- /dev/null +++ b/crates/bolt-lang/src/instructions/update.rs @@ -0,0 +1,78 @@ +use crate::world; +use crate::{cpi::check, errors::BoltError}; +use anchor_lang::prelude::*; +use session_keys::SessionToken; + +pub fn update<'info>( + instruction_sysvar_account: &AccountInfo<'info>, + authority: &AccountInfo<'info>, + component_authority: Pubkey, + bolt_component: &AccountInfo<'info>, + data: &[u8], +) -> Result<()> { + require!( + component_authority == world::id_const() + || (component_authority == *authority.key && authority.is_signer), + BoltError::InvalidAuthority + ); + check(&instruction_sysvar_account.to_account_info())?; + let mut account_data = bolt_component + .try_borrow_mut_data() + .map_err(|_| BoltError::AccountMismatch)?; + // Anchor account data starts with an 8-byte discriminator; skip it when writing + require!( + 8 + data.len() <= account_data.len(), + BoltError::AccountMismatch + ); + let start = 8; + let end = start + data.len(); + account_data[start..end].copy_from_slice(data); + Ok(()) +} + +pub fn update_with_session<'info>( + instruction_sysvar_account: &AccountInfo<'info>, + authority: &Signer<'info>, + component_authority: Pubkey, + bolt_component: &AccountInfo<'info>, + session_token: &Account<'info, SessionToken>, + data: &[u8], +) -> Result<()> { + if component_authority == world::id_const() { + require!( + Clock::get()?.unix_timestamp < session_token.valid_until, + crate::session_keys::SessionError::InvalidToken + ); + } else { + let validity_ctx = crate::session_keys::ValidityChecker { + session_token: session_token.clone(), + session_signer: authority.clone(), + authority: component_authority, + target_program: world::id_const(), + }; + require!( + session_token.validate(validity_ctx)?, + crate::session_keys::SessionError::InvalidToken + ); + require_eq!( + component_authority, + session_token.authority, + crate::session_keys::SessionError::InvalidToken + ); + } + + crate::cpi::check(&instruction_sysvar_account.to_account_info())?; + + let mut account_data = bolt_component + .try_borrow_mut_data() + .map_err(|_| BoltError::AccountMismatch)?; + // Anchor account data starts with an 8-byte discriminator; skip it when writing + require!( + 8 + data.len() <= account_data.len(), + BoltError::AccountMismatch + ); + let start = 8; + let end = start + data.len(); + account_data[start..end].copy_from_slice(data); + Ok(()) +} diff --git a/crates/bolt-lang/src/lib.rs b/crates/bolt-lang/src/lib.rs index 714d1c99..12d1d82f 100644 --- a/crates/bolt-lang/src/lib.rs +++ b/crates/bolt-lang/src/lib.rs @@ -7,20 +7,21 @@ pub use anchor_lang::{ AccountDeserialize, AccountSerialize, AnchorDeserialize, AnchorSerialize, Bumps, Result, }; +pub mod cpi; +pub mod instructions; + pub use session_keys; pub use bolt_attribute_bolt_arguments::arguments; +pub use bolt_attribute_bolt_bundle::bundle; pub use bolt_attribute_bolt_component::component; pub use bolt_attribute_bolt_component_deserialize::component_deserialize; pub use bolt_attribute_bolt_component_id::component_id; -pub use bolt_attribute_bolt_delegate::delegate; pub use bolt_attribute_bolt_extra_accounts::extra_accounts; pub use bolt_attribute_bolt_extra_accounts::pubkey; -pub use bolt_attribute_bolt_program::bolt_program; pub use bolt_attribute_bolt_system::system; pub use bolt_attribute_bolt_system_input::system_input; -pub use bolt_system; pub use world; pub use world::program::World; pub use world::Entity; @@ -68,6 +69,11 @@ pub trait ComponentDeserialize: Sized { fn from_account_info(account: &anchor_lang::prelude::AccountInfo) -> Result; } +/// Number of system input components expected by a system. +pub trait NumberOfComponents { + const NUMBER_OF_COMPONENTS: usize; +} + /// Metadata for the component. #[derive(InitSpace, AnchorSerialize, AnchorDeserialize, Default, Copy, Clone)] pub struct BoltMetadata { @@ -78,3 +84,22 @@ pub struct BoltMetadata { pub fn pubkey_from_str(s: &str) -> solana_program::pubkey::Pubkey { solana_program::pubkey::Pubkey::from_str(s).unwrap() } + +impl BoltMetadata { + pub fn try_from_account_info(account: &AccountInfo) -> Result { + let data = account.try_borrow_data()?; + require!( + data.len() >= 8 + BoltMetadata::INIT_SPACE, + ErrorCode::AccountDidNotDeserialize + ); + let slice = &data[8..8 + BoltMetadata::INIT_SPACE]; + Ok(BoltMetadata::try_from_slice(slice)?) + } + + pub fn discriminator_from_account_info(account: &AccountInfo) -> Result> { + let data = account.try_borrow_data()?; + require!(data.len() >= 8, ErrorCode::AccountDidNotDeserialize); + let discriminator = &data[0..8]; + Ok(discriminator.to_vec()) + } +} diff --git a/crates/bolt-lang/utils/src/lib.rs b/crates/bolt-lang/utils/src/lib.rs index 1d44e75d..a7f8ec65 100644 --- a/crates/bolt-lang/utils/src/lib.rs +++ b/crates/bolt-lang/utils/src/lib.rs @@ -2,7 +2,7 @@ use proc_macro2::Ident; use syn::{DeriveInput, Field, Type, Visibility}; pub fn add_bolt_metadata(input: &mut DeriveInput) { - let authority_field: Field = Field { + let bolt_metadata_field: Field = Field { attrs: vec![], vis: Visibility::Public(syn::VisPublic { pub_token: Default::default(), @@ -16,7 +16,7 @@ pub fn add_bolt_metadata(input: &mut DeriveInput) { }; if let syn::Data::Struct(ref mut data) = input.data { if let syn::Fields::Named(ref mut fields) = data.fields { - fields.named.push(authority_field); + fields.named.insert(0, bolt_metadata_field); } } } diff --git a/crates/programs/bolt-component/Cargo.toml b/crates/programs/bolt-component/Cargo.toml deleted file mode 100644 index 8509503b..00000000 --- a/crates/programs/bolt-component/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "bolt-component" -description = "Bolt component template" -version = { workspace = true } -authors = { workspace = true } -repository = { workspace = true } -homepage = { workspace = true } -license = { workspace = true } -edition = { workspace = true } - -[lib] -crate-type = ["cdylib", "lib"] -name = "bolt_component" - -[features] -no-entrypoint = [] -no-idl = [] -no-log-ix-name = [] -cpi = ["no-entrypoint"] -default = [] -idl-build = ["anchor-lang/idl-build"] -anchor-debug = ["anchor-lang/anchor-debug"] -custom-heap = [] -custom-panic = [] - -[dependencies] -anchor-lang.workspace = true -bolt-system.workspace = true \ No newline at end of file diff --git a/crates/programs/bolt-component/src/lib.rs b/crates/programs/bolt-component/src/lib.rs deleted file mode 100644 index 3d22538c..00000000 --- a/crates/programs/bolt-component/src/lib.rs +++ /dev/null @@ -1,129 +0,0 @@ -use anchor_lang::prelude::*; - -declare_id!("CmP2djJgABZ4cRokm4ndxuq6LerqpNHLBsaUv2XKEJua"); - -#[program] -pub mod bolt_component { - use super::*; - - pub fn initialize(_ctx: Context) -> Result<()> { - Ok(()) - } - - pub fn destroy(_ctx: Context) -> Result<()> { - Ok(()) - } - - pub fn update(_ctx: Context, _data: Vec) -> Result<()> { - Ok(()) - } - - pub fn update_with_session(_ctx: Context, _data: Vec) -> Result<()> { - Ok(()) - } - - #[derive(Accounts)] - pub struct Update<'info> { - #[account(mut)] - /// CHECK: The component to update - pub bolt_component: UncheckedAccount<'info>, - #[account()] - /// CHECK: The authority of the component - pub authority: Signer<'info>, - #[account(address = anchor_lang::solana_program::sysvar::instructions::id())] - /// CHECK: The instruction sysvar - pub instruction_sysvar_account: AccountInfo<'info>, - } - - #[derive(Accounts)] - pub struct UpdateWithSession<'info> { - #[account(mut)] - /// CHECK: The component to update - pub bolt_component: UncheckedAccount<'info>, - #[account()] - /// CHECK: The authority of the component - pub authority: Signer<'info>, - #[account(address = anchor_lang::solana_program::sysvar::instructions::id())] - /// CHECK: The instruction sysvar - pub instruction_sysvar_account: AccountInfo<'info>, - #[account()] - /// CHECK: The session token - pub session_token: UncheckedAccount<'info>, - } -} - -#[derive(Accounts)] -pub struct Initialize<'info> { - #[account(mut)] - pub payer: Signer<'info>, - #[account(mut)] - /// CHECK: The component to initialize - pub data: UncheckedAccount<'info>, - #[account()] - /// CHECK: A generic entity account - pub entity: AccountInfo<'info>, - #[account()] - /// CHECK: The authority of the component - pub authority: 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(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, -} - -#[cfg(feature = "cpi")] -pub trait CpiContextBuilder<'info>: ToAccountMetas + ToAccountInfos<'info> + Sized { - fn build_cpi_context( - self, - program: AccountInfo<'info>, - ) -> CpiContext<'info, 'info, 'info, 'info, Self>; -} - -#[cfg(feature = "cpi")] -impl<'info> CpiContextBuilder<'info> for cpi::accounts::Update<'info> { - fn build_cpi_context( - self, - program: AccountInfo<'info>, - ) -> CpiContext<'info, 'info, 'info, 'info, Self> { - let cpi_program = program.to_account_info(); - CpiContext::new(cpi_program, self) - } -} - -#[cfg(feature = "cpi")] -impl<'info> CpiContextBuilder<'info> for cpi::accounts::UpdateWithSession<'info> { - fn build_cpi_context( - self, - program: AccountInfo<'info>, - ) -> CpiContext<'info, 'info, 'info, 'info, Self> { - let cpi_program = program.to_account_info(); - CpiContext::new(cpi_program, self) - } -} diff --git a/crates/programs/bolt-system/Cargo.toml b/crates/programs/bolt-system/Cargo.toml deleted file mode 100644 index c4fcc0ac..00000000 --- a/crates/programs/bolt-system/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "bolt-system" -description = "Bolt system template" -version = { workspace = true } -authors = { workspace = true } -repository = { workspace = true } -homepage = { workspace = true } -license = { workspace = true } -edition = { workspace = true } - -[lib] -crate-type = ["cdylib", "lib"] -name = "bolt_system" - -[features] -no-entrypoint = [] -no-idl = [] -no-log-ix-name = [] -cpi = ["no-entrypoint"] -default = [] -idl-build = ["anchor-lang/idl-build"] -anchor-debug = ["anchor-lang/anchor-debug"] -custom-heap = [] -custom-panic = [] - -[dependencies] -anchor-lang.workspace = true diff --git a/crates/programs/bolt-system/Xargo.toml b/crates/programs/bolt-system/Xargo.toml deleted file mode 100644 index 475fb71e..00000000 --- a/crates/programs/bolt-system/Xargo.toml +++ /dev/null @@ -1,2 +0,0 @@ -[target.bpfel-unknown-unknown.dependencies.std] -features = [] diff --git a/crates/programs/bolt-system/src/lib.rs b/crates/programs/bolt-system/src/lib.rs deleted file mode 100644 index 1cc8c70d..00000000 --- a/crates/programs/bolt-system/src/lib.rs +++ /dev/null @@ -1,18 +0,0 @@ -use anchor_lang::prelude::*; - -declare_id!("7X4EFsDJ5aYTcEjKzJ94rD8FRKgQeXC89fkpeTS4KaqP"); - -#[program] -pub mod bolt_system { - use super::*; - pub fn bolt_execute(_ctx: Context, _args: Vec) -> Result>> { - Ok(Vec::new()) - } -} - -#[derive(Accounts, Clone)] -pub struct BoltExecute<'info> { - /// CHECK: authority check - #[account()] - pub authority: AccountInfo<'info>, -} diff --git a/crates/programs/world/Cargo.toml b/crates/programs/world/Cargo.toml index 4292d2a7..c6a795f1 100644 --- a/crates/programs/world/Cargo.toml +++ b/crates/programs/world/Cargo.toml @@ -25,7 +25,6 @@ custom-panic = [] [dependencies] anchor-lang.workspace = true -bolt-component.workspace = true -bolt-system.workspace = true solana-security-txt.workspace = true tuple-conv.workspace = true +const-crypto.workspace = true \ No newline at end of file diff --git a/crates/programs/world/src/lib.rs b/crates/programs/world/src/lib.rs index 4d4e0f14..4f8f9ec2 100644 --- a/crates/programs/world/src/lib.rs +++ b/crates/programs/world/src/lib.rs @@ -1,12 +1,24 @@ #![allow(clippy::manual_unwrap_or_default)] -use anchor_lang::prelude::*; -use bolt_component::CpiContextBuilder; +use anchor_lang::{ + prelude::*, + solana_program::instruction::{AccountMeta, Instruction}, + solana_program::program::invoke, +}; use error::WorldError; use std::collections::BTreeSet; +pub mod utils; +use utils::discriminator_for; + #[cfg(not(feature = "no-entrypoint"))] use solana_security_txt::security_txt; +pub const BOLT_EXECUTE: [u8; 8] = discriminator_for("global:bolt_execute"); +pub const INITIALIZE: [u8; 8] = discriminator_for("global:initialize"); +pub const DESTROY: [u8; 8] = discriminator_for("global:destroy"); +pub const UPDATE: [u8; 8] = discriminator_for("global:update"); +pub const UPDATE_WITH_SESSION: [u8; 8] = discriminator_for("global:update_with_session"); + declare_id!("WorLD15A7CrDwLcLy4fRqtaTb9fbd8o8iqiEMUDse2n"); #[cfg(not(feature = "no-entrypoint"))] @@ -258,15 +270,95 @@ pub mod world { } pub fn initialize_component(ctx: Context) -> Result<()> { + initialize_component_with_discriminator(ctx, INITIALIZE.to_vec()) + } + + pub fn initialize_component_with_discriminator( + ctx: Context, + discriminator: Vec, + ) -> Result<()> { if !ctx.accounts.authority.is_signer && ctx.accounts.authority.key != &ID { return Err(WorldError::InvalidAuthority.into()); } - bolt_component::cpi::initialize(ctx.accounts.build())?; + // Pure Solana SDK logic for CPI to bolt_component::initialize + use anchor_lang::solana_program::instruction::{AccountMeta, Instruction}; + + // Prepare the accounts for the CPI + let accounts = vec![ + AccountMeta::new(ctx.accounts.payer.key(), true), + AccountMeta::new(ctx.accounts.data.key(), false), + AccountMeta::new_readonly(ctx.accounts.entity.key(), false), + AccountMeta::new_readonly(ctx.accounts.authority.key(), false), + AccountMeta::new_readonly(ctx.accounts.instruction_sysvar_account.key(), false), + AccountMeta::new_readonly(ctx.accounts.system_program.key(), false), + ]; + + let data = discriminator; + + let ix = Instruction { + program_id: ctx.accounts.component_program.key(), + accounts, + data, + }; + + // CPI: invoke the instruction + invoke( + &ix, + &[ + ctx.accounts.payer.to_account_info(), + ctx.accounts.data.to_account_info(), + ctx.accounts.entity.to_account_info(), + ctx.accounts.authority.to_account_info(), + ctx.accounts.instruction_sysvar_account.to_account_info(), + ctx.accounts.system_program.to_account_info(), + ], + )?; Ok(()) } pub fn destroy_component(ctx: Context) -> Result<()> { - bolt_component::cpi::destroy(ctx.accounts.build())?; + destroy_component_with_discriminator(ctx, DESTROY.to_vec()) + } + + pub fn destroy_component_with_discriminator( + ctx: Context, + discriminator: Vec, + ) -> Result<()> { + // Pure Solana SDK logic for CPI to bolt_component::destroy + use anchor_lang::solana_program::instruction::{AccountMeta, Instruction}; + + // Prepare the accounts for the CPI (must match bolt_component::Destroy) + let accounts = vec![ + AccountMeta::new_readonly(ctx.accounts.authority.key(), true), + AccountMeta::new(ctx.accounts.receiver.key(), false), + AccountMeta::new_readonly(ctx.accounts.entity.key(), false), + AccountMeta::new(ctx.accounts.component.key(), false), + AccountMeta::new_readonly(ctx.accounts.component_program_data.key(), false), + AccountMeta::new_readonly(ctx.accounts.instruction_sysvar_account.key(), false), + AccountMeta::new_readonly(ctx.accounts.system_program.key(), false), + ]; + + let data = discriminator; + + let ix = Instruction { + program_id: ctx.accounts.component_program.key(), + accounts, + data, + }; + + // CPI: invoke the instruction + invoke( + &ix, + &[ + ctx.accounts.authority.to_account_info(), + ctx.accounts.receiver.to_account_info(), + ctx.accounts.entity.to_account_info(), + ctx.accounts.component.to_account_info(), + ctx.accounts.component_program_data.to_account_info(), + ctx.accounts.instruction_sysvar_account.to_account_info(), + ctx.accounts.system_program.to_account_info(), + ], + )?; Ok(()) } @@ -274,26 +366,15 @@ pub mod world { ctx: Context<'_, '_, '_, 'info, Apply<'info>>, args: Vec, ) -> Result<()> { - let (pairs, results) = apply_impl( - &ctx.accounts.authority, - &ctx.accounts.world, - &ctx.accounts.bolt_system, - ctx.accounts.build(), - args, - ctx.remaining_accounts.to_vec(), - )?; - for ((program, component), result) in pairs.into_iter().zip(results.into_iter()) { - bolt_component::cpi::update( - build_update_context( - program, - component, - ctx.accounts.authority.clone(), - ctx.accounts.instruction_sysvar_account.clone(), - ), - result, - )?; - } - Ok(()) + apply_impl(ctx, BOLT_EXECUTE.to_vec(), args) + } + + pub fn apply_with_discriminator<'info>( + ctx: Context<'_, '_, '_, 'info, Apply<'info>>, + system_discriminator: Vec, + args: Vec, + ) -> Result<()> { + apply_impl(ctx, system_discriminator, args) } #[derive(Accounts)] @@ -311,43 +392,19 @@ pub mod world { pub world: Account<'info, World>, } - impl<'info> Apply<'info> { - pub fn build( - &self, - ) -> CpiContext<'_, '_, '_, 'info, bolt_system::cpi::accounts::BoltExecute<'info>> { - let cpi_program = self.bolt_system.to_account_info(); - let cpi_accounts = bolt_system::cpi::accounts::BoltExecute { - authority: self.authority.to_account_info(), - }; - CpiContext::new(cpi_program, cpi_accounts) - } + pub fn apply_with_session<'info>( + ctx: Context<'_, '_, '_, 'info, ApplyWithSession<'info>>, + args: Vec, + ) -> Result<()> { + apply_with_session_impl(ctx, BOLT_EXECUTE.to_vec(), args) } - pub fn apply_with_session<'info>( + pub fn apply_with_session_and_discriminator<'info>( ctx: Context<'_, '_, '_, 'info, ApplyWithSession<'info>>, + system_discriminator: Vec, args: Vec, ) -> Result<()> { - let (pairs, results) = apply_impl( - &ctx.accounts.authority, - &ctx.accounts.world, - &ctx.accounts.bolt_system, - ctx.accounts.build(), - args, - ctx.remaining_accounts.to_vec(), - )?; - for ((program, component), result) in pairs.into_iter().zip(results.into_iter()) { - bolt_component::cpi::update_with_session( - build_update_context_with_session( - program, - component, - ctx.accounts.authority.clone(), - ctx.accounts.instruction_sysvar_account.clone(), - ctx.accounts.session_token.clone(), - ), - result, - )?; - } - Ok(()) + apply_with_session_impl(ctx, system_discriminator, args) } #[derive(Accounts)] @@ -367,26 +424,104 @@ pub mod world { /// CHECK: The session token pub session_token: UncheckedAccount<'info>, } +} - impl<'info> ApplyWithSession<'info> { - pub fn build( - &self, - ) -> CpiContext<'_, '_, '_, 'info, bolt_system::cpi::accounts::BoltExecute<'info>> { - let cpi_program = self.bolt_system.to_account_info(); - let cpi_accounts = bolt_system::cpi::accounts::BoltExecute { - authority: self.authority.to_account_info(), - }; - CpiContext::new(cpi_program, cpi_accounts) - } +pub fn apply_impl<'info>( + ctx: Context<'_, '_, '_, 'info, Apply<'info>>, + system_discriminator: Vec, + args: Vec, +) -> Result<()> { + let (pairs, results) = system_execute( + &ctx.accounts.authority, + &ctx.accounts.world, + &ctx.accounts.bolt_system, + system_discriminator, + args, + ctx.remaining_accounts.to_vec(), + )?; + + for ((program, component), result) in pairs.into_iter().zip(results.into_iter()) { + let accounts = vec![ + AccountMeta::new(component.key(), false), + AccountMeta::new_readonly(ctx.accounts.authority.key(), true), + AccountMeta::new_readonly(ctx.accounts.instruction_sysvar_account.key(), false), + ]; + + let mut data = UPDATE.to_vec(); + let len_le = (result.len() as u32).to_le_bytes(); + data.extend_from_slice(&len_le); + data.extend_from_slice(result.as_slice()); + + let ix = Instruction { + program_id: program.key(), + accounts, + data, + }; + + invoke( + &ix, + &[ + component.clone(), + ctx.accounts.authority.to_account_info(), + ctx.accounts.instruction_sysvar_account.to_account_info(), + ], + )?; } + Ok(()) +} + +pub fn apply_with_session_impl<'info>( + ctx: Context<'_, '_, '_, 'info, ApplyWithSession<'info>>, + system_discriminator: Vec, + args: Vec, +) -> Result<()> { + let (pairs, results) = system_execute( + &ctx.accounts.authority, + &ctx.accounts.world, + &ctx.accounts.bolt_system, + system_discriminator, + args, + ctx.remaining_accounts.to_vec(), + )?; + + for ((program, component), result) in pairs.into_iter().zip(results.into_iter()) { + let accounts = vec![ + AccountMeta::new(component.key(), false), + AccountMeta::new_readonly(ctx.accounts.authority.key(), true), + AccountMeta::new_readonly(ctx.accounts.instruction_sysvar_account.key(), false), + AccountMeta::new_readonly(ctx.accounts.session_token.key(), false), + ]; + + let mut data = UPDATE_WITH_SESSION.to_vec(); + let len_le = (result.len() as u32).to_le_bytes(); + data.extend_from_slice(&len_le); + data.extend_from_slice(result.as_slice()); + + let ix = Instruction { + program_id: program.key(), + accounts, + data, + }; + + invoke( + &ix, + &[ + component.clone(), + ctx.accounts.authority.to_account_info(), + ctx.accounts.instruction_sysvar_account.to_account_info(), + ctx.accounts.session_token.to_account_info(), + ], + )?; + } + Ok(()) } #[allow(clippy::type_complexity)] -fn apply_impl<'info>( +fn system_execute<'info>( authority: &Signer<'info>, world: &Account<'info, World>, bolt_system: &UncheckedAccount<'info>, - cpi_context: CpiContext<'_, '_, '_, 'info, bolt_system::cpi::accounts::BoltExecute<'info>>, + system_discriminator: Vec, args: Vec, mut remaining_accounts: Vec>, ) -> Result<(Vec<(AccountInfo<'info>, AccountInfo<'info>)>, Vec>)> { @@ -420,11 +555,46 @@ fn apply_impl<'info>( components_accounts.append(&mut remaining_accounts); let remaining_accounts = components_accounts; - let results = bolt_system::cpi::bolt_execute( - cpi_context.with_remaining_accounts(remaining_accounts), - args, - )? - .get(); + let mut data = system_discriminator; + let len_le = (args.len() as u32).to_le_bytes(); + data.extend_from_slice(&len_le); + data.extend_from_slice(args.as_slice()); + + use anchor_lang::solana_program::instruction::{AccountMeta, Instruction}; + use anchor_lang::solana_program::program::invoke; + + let mut accounts = vec![AccountMeta { + pubkey: authority.key(), + is_signer: authority.is_signer, + is_writable: authority.is_writable, + }]; + accounts.extend(remaining_accounts.iter().map(|account| AccountMeta { + pubkey: account.key(), + is_signer: account.is_signer, + is_writable: account.is_writable, + })); + + let mut account_infos = vec![authority.to_account_info()]; + account_infos.extend( + remaining_accounts + .iter() + .map(|account| account.to_account_info()), + ); + + let ix = Instruction { + program_id: bolt_system.key(), + accounts, + data, + }; + + invoke(&ix, &account_infos)?; + + // Extract return data using Solana SDK + use anchor_lang::solana_program::program::get_return_data; + let (pid, data) = get_return_data().ok_or(WorldError::InvalidSystemOutput)?; + require_keys_eq!(pid, bolt_system.key(), WorldError::InvalidSystemOutput); + let results: Vec> = borsh::BorshDeserialize::try_from_slice(&data) + .map_err(|_| WorldError::InvalidSystemOutput)?; if results.len() != pairs.len() { return Err(WorldError::InvalidSystemOutput.into()); @@ -539,24 +709,6 @@ pub struct InitializeComponent<'info> { pub system_program: Program<'info, System>, } -impl<'info> InitializeComponent<'info> { - pub fn build( - &self, - ) -> CpiContext<'_, '_, '_, 'info, bolt_component::cpi::accounts::Initialize<'info>> { - let cpi_program = self.component_program.to_account_info(); - - let cpi_accounts = bolt_component::cpi::accounts::Initialize { - payer: self.payer.to_account_info(), - data: self.data.to_account_info(), - entity: self.entity.to_account_info(), - authority: self.authority.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) - } -} - #[derive(Accounts)] pub struct DestroyComponent<'info> { #[account(mut)] @@ -579,25 +731,6 @@ pub struct DestroyComponent<'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 { @@ -702,43 +835,3 @@ impl SystemWhitelist { 8 + Registry::INIT_SPACE } } - -/// Builds the context for updating a component. -pub fn build_update_context<'info>( - component_program: AccountInfo<'info>, - bolt_component: AccountInfo<'info>, - authority: Signer<'info>, - instruction_sysvar_account: UncheckedAccount<'info>, -) -> CpiContext<'info, 'info, 'info, 'info, bolt_component::cpi::accounts::Update<'info>> { - let authority = authority.to_account_info(); - let instruction_sysvar_account = instruction_sysvar_account.to_account_info(); - let cpi_program = component_program; - bolt_component::cpi::accounts::Update { - bolt_component, - authority, - instruction_sysvar_account, - } - .build_cpi_context(cpi_program) -} - -/// Builds the context for updating a component. -pub fn build_update_context_with_session<'info>( - component_program: AccountInfo<'info>, - bolt_component: AccountInfo<'info>, - authority: Signer<'info>, - instruction_sysvar_account: UncheckedAccount<'info>, - session_token: UncheckedAccount<'info>, -) -> CpiContext<'info, 'info, 'info, 'info, bolt_component::cpi::accounts::UpdateWithSession<'info>> -{ - let authority = authority.to_account_info(); - let instruction_sysvar_account = instruction_sysvar_account.to_account_info(); - let cpi_program = component_program; - let session_token = session_token.to_account_info(); - bolt_component::cpi::accounts::UpdateWithSession { - bolt_component, - authority, - instruction_sysvar_account, - session_token, - } - .build_cpi_context(cpi_program) -} diff --git a/crates/programs/world/src/utils.rs b/crates/programs/world/src/utils.rs new file mode 100644 index 00000000..b9c2c598 --- /dev/null +++ b/crates/programs/world/src/utils.rs @@ -0,0 +1,36 @@ +/// Computes an 8-byte discriminator for the given name. +/// +/// The discriminator is derived by taking the first 8 bytes of the SHA-256 hash +/// of the input name. This is used for discriminator-based routing in bundled +/// components and systems. +/// +/// # Collision Risk +/// +/// Using 8 bytes (64 bits) of a hash introduces a small collision probability. +/// With the birthday paradox, collisions become likely after ~2^32 different names. +/// This is acceptable for component/system name spaces in practice. +/// +/// # Examples +/// +/// ``` +/// let disc = discriminator_for("Position"); +/// assert_eq!(disc.len(), 8); +/// ``` +pub const fn discriminator_for(name: &str) -> [u8; 8] { + let mut discriminator = [0u8; 8]; + + let hash = const_crypto::sha2::Sha256::new() + .update(name.as_bytes()) + .finalize(); + + let hash_bytes = hash.as_slice(); + + // Manual loop required for const fn compatibility + let mut i = 0; + while i < 8 { + discriminator[i] = hash_bytes[i]; + i += 1; + } + + discriminator +} diff --git a/docs/REPORT.md b/docs/REPORT.md index e87a1c36..eb739685 100644 --- a/docs/REPORT.md +++ b/docs/REPORT.md @@ -4,6 +4,6 @@ xychart title "Bolt Apply System Cost" x-axis ["1C-CPIs:2","2C-CPIs:3","3C-CPIs:4","4C-CPIs:5","5C-CPIs:6","6C-CPIs:7","7C-CPIs:8","8C-CPIs:9","9C-CPIs:10","10C-CPIs:11"] y-axis "CU" 5000 --> 200000 - bar [15254,24352,33653,43017,52358,61568,71006,80482,89958,99299] - bar [6162,11236,16305,21374,26443,31516,36608,41892,46984,52077] + bar [13110,20640,28678,37169,46306,55754,65697,76307,87375,98746] + bar [4581,8633,13163,18120,23526,29382,35706,42669,49889,57556] ``` diff --git a/examples/bundle/Cargo.toml b/examples/bundle/Cargo.toml new file mode 100644 index 00000000..89d3edb8 --- /dev/null +++ b/examples/bundle/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "example-bundle" +version = "0.2.5" +description = "Created with Bolt" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "example_bundle" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] +idl-build = ["bolt-lang/idl-build"] +anchor-debug = ["bolt-lang/anchor-debug"] +custom-heap = [] +custom-panic = [] + + +[dependencies] +bolt-lang.workspace = true +serde = { version = "1.0", features = ["derive"] } diff --git a/crates/programs/bolt-component/Xargo.toml b/examples/bundle/Xargo.toml similarity index 100% rename from crates/programs/bolt-component/Xargo.toml rename to examples/bundle/Xargo.toml diff --git a/examples/bundle/src/lib.rs b/examples/bundle/src/lib.rs new file mode 100644 index 00000000..a4f7c9a6 --- /dev/null +++ b/examples/bundle/src/lib.rs @@ -0,0 +1,67 @@ +use bolt_lang::*; + +declare_id!("CgfPBUeDUL3GT6b5AUDFE56KKgU4ycWA9ERjEWsfMZCj"); + +#[bundle] +pub mod example_bundle { + + #[component(delegate)] + #[derive(Default)] + pub struct Position { + pub x: i64, + pub y: i64, + pub z: i64, + } + + #[component] + pub struct Velocity { + pub x: i64, + pub y: i64, + pub z: i64, + } + + impl Default for Velocity { + fn default() -> Self { + Self { + x: 1, + y: 2, + z: 3, + bolt_metadata: Default::default(), + } + } + } + + #[system] + pub mod movement { + + pub fn execute(ctx: Context, _args_p: Vec) -> Result { + let velocity = &ctx.accounts.velocity; + let position = &mut ctx.accounts.position; + position.x += velocity.x; + position.y += velocity.y; + position.z += velocity.z; + Ok(ctx.accounts) + } + + #[system_input] + pub struct Components { + pub position: Position, + pub velocity: Velocity, + } + } + + #[system] + pub mod stop { + pub fn execute(ctx: Context, _args_p: Vec) -> Result { + ctx.accounts.velocity.x = 0; + ctx.accounts.velocity.y = 0; + ctx.accounts.velocity.z = 0; + Ok(ctx.accounts) + } + + #[system_input] + pub struct Components { + pub velocity: Velocity, + } + } +} diff --git a/scripts/test-publish.sh b/scripts/test-publish.sh index 48ba1584..06723806 100755 --- a/scripts/test-publish.sh +++ b/scripts/test-publish.sh @@ -13,14 +13,11 @@ cargo +nightly publish -Zpackage-workspace $DRY_RUN_FLAG $NO_VERIFY_FLAG \ -p bolt-cli \ -p bolt-lang \ -p bolt-utils \ - -p bolt-system \ - -p bolt-component \ -p bolt-attribute-bolt-arguments \ + -p bolt-attribute-bolt-bundle \ -p bolt-attribute-bolt-component \ -p bolt-attribute-bolt-component-deserialize \ -p bolt-attribute-bolt-component-id \ - -p bolt-attribute-bolt-delegate \ -p bolt-attribute-bolt-extra-accounts \ - -p bolt-attribute-bolt-program \ -p bolt-attribute-bolt-system \ -p bolt-attribute-bolt-system-input \ No newline at end of file