Skip to content

Commit 5a95803

Browse files
committed
✨ C# client for components and systems bundles
1 parent 22092e5 commit 5a95803

File tree

7 files changed

+186
-49
lines changed

7 files changed

+186
-49
lines changed

clients/csharp/Solana.Unity.Bolt/ECS/Component.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,23 @@ public class Component : Identifier {
1313
/// <param name="program">The program that the component belongs to.</param>
1414
/// <param name="name">The name of the component.</param>
1515
public Component(PublicKey program, string name) : base(program, name) {}
16+
17+
/// <summary>
18+
/// Static constructor from either a raw program id or an existing Component, like TS Component.from.
19+
/// </summary>
20+
public static Component From(object componentId)
21+
{
22+
if (componentId is Component c) return c;
23+
if (componentId is PublicKey pk) return new Component(pk, null);
24+
throw new global::System.ArgumentException("componentId must be PublicKey or Component");
25+
}
26+
27+
/// <summary>
28+
/// Build seeds: (seed ?? "") + (Name ?? ""), mirroring TS seeds().
29+
/// </summary>
30+
public string Seeds(string seed = null)
31+
{
32+
return (seed ?? "") + (Name ?? "");
33+
}
1634
}
1735
}

clients/csharp/Solana.Unity.Bolt/ECS/Identifier.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,15 @@ public Identifier(PublicKey program, string name) {
2525
this.Program = program;
2626
this.Name = name;
2727
}
28+
29+
/// <summary>
30+
/// Build the 8-byte Anchor discriminator for a given method, mirroring TS Identifier.getMethodDiscriminator.
31+
/// Format: "global:" + (name? name + "_" : "") + method
32+
/// </summary>
33+
public byte[] GetMethodDiscriminator(string method)
34+
{
35+
var prefix = string.IsNullOrEmpty(Name) ? "" : Name + "_";
36+
return World.GetDiscriminator("global:" + prefix + method);
37+
}
2838
}
2939
}

clients/csharp/Solana.Unity.Bolt/ECS/System.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,15 @@ public class System : Identifier {
1313
/// <param name="program">The program that the system belongs to.</param>
1414
/// <param name="name">The name of the system.</param>
1515
public System(PublicKey program, string name) : base(program, name) {}
16+
17+
/// <summary>
18+
/// Static constructor from either a raw program id or an existing System, like TS System.from.
19+
/// </summary>
20+
public static System From(object systemId)
21+
{
22+
if (systemId is System s) return s;
23+
if (systemId is PublicKey pk) return new System(pk, null);
24+
throw new global::System.ArgumentException("systemId must be PublicKey or System");
25+
}
1626
}
1727
}

clients/csharp/Solana.Unity.Bolt/WorldProgram/Bolt/ApplySystem.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,25 +78,25 @@ public static Solana.Unity.Rpc.Models.TransactionInstruction ApplySystem(
7878
for (int i = 0; i < components.Length; i++)
7979
{
8080
var comp = components[i];
81-
var seed = comp.Name; // bundled component uses name as seed
82-
var pda = WorldProgram.FindComponentPda(comp.Program, entity, seed);
81+
var providedSeed = (seeds != null && i < seeds.Length) ? seeds[i] : "";
82+
var pda = WorldProgram.FindComponentPda(comp.Program, entity, comp.Seeds(providedSeed));
8383
remainingAccounts.Add(Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(comp.Program, false));
8484
remainingAccounts.Add(Solana.Unity.Rpc.Models.AccountMeta.Writable(pda, false));
8585

86-
var discrName = "global:" + (comp.Name != null ? comp.Name + "_" : "") + (sessionToken != null ? "update_with_session" : "update");
87-
discriminators.Add(GetDiscriminator(discrName));
86+
var compDiscriminator = comp.GetMethodDiscriminator(sessionToken != null ? "update_with_session" : "update");
87+
discriminators.Add(compDiscriminator);
8888
}
8989
}
9090

9191
// Optional delimiter and extra accounts
92-
if ((extraAccounts != null && extraAccounts.Length > 0) || remainingAccounts.Count > 0)
92+
if (extraAccounts != null && extraAccounts.Length > 0)
9393
{
9494
remainingAccounts.Add(Solana.Unity.Rpc.Models.AccountMeta.ReadOnly(new PublicKey(WorldProgram.ID), false));
9595
if (extraAccounts != null)
9696
remainingAccounts.AddRange(extraAccounts);
9797
}
9898

99-
var systemDiscriminator = GetDiscriminator("global:" + (systemId.Name != null ? $"bolt_execute_{systemId.Name}" : "bolt_execute"));
99+
var systemDiscriminator = systemId.GetMethodDiscriminator("bolt_execute");
100100

101101
Solana.Unity.Rpc.Models.TransactionInstruction instruction;
102102
if (sessionToken != null)

clients/csharp/Solana.Unity.Bolt/WorldProgram/Bolt/DelegateComponent.cs

Lines changed: 115 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,43 +19,120 @@ public class DelegateComponentInstruction {
1919
public TransactionInstruction Instruction { get; set; }
2020
}
2121

22-
public static async Task<DelegateComponentInstruction> DelegateComponent(PublicKey payer, PublicKey entity, PublicKey componentId, string seed = "") {
23-
var account = WorldProgram.FindComponentPda(componentId, entity, seed);
24-
var bufferPda = WorldProgram.FindBufferPda(account, componentId);
25-
var delegationRecord = WorldProgram.FindDelegationProgramPda("delegation", account);
26-
var delegationMetadata = WorldProgram.FindDelegationProgramPda("delegation-metadata", account);
27-
28-
byte[] discriminator = new byte[] { 90, 147, 75, 178, 85, 88, 4, 137 };
29-
uint commitFrequencyMs = 0;
30-
byte[] commitFrequencyBytes = BitConverter.GetBytes(commitFrequencyMs);
31-
if (!BitConverter.IsLittleEndian) Array.Reverse(commitFrequencyBytes);
32-
var validator = new byte[1];
33-
validator[0] = 0;
34-
35-
var data = new byte[discriminator.Length + commitFrequencyBytes.Length + validator.Length];
36-
Array.Copy(discriminator, data, discriminator.Length);
37-
Array.Copy(commitFrequencyBytes, 0, data, discriminator.Length, commitFrequencyBytes.Length);
38-
Array.Copy(validator, 0, data, discriminator.Length + commitFrequencyBytes.Length, validator.Length);
39-
40-
TransactionInstruction instruction = new TransactionInstruction() {
41-
ProgramId = componentId,
42-
Keys = new List<AccountMeta>() {
43-
AccountMeta.ReadOnly(payer, true),
44-
AccountMeta.ReadOnly(entity, false),
45-
AccountMeta.Writable(account, false),
46-
AccountMeta.ReadOnly(componentId, false),
47-
AccountMeta.Writable(bufferPda, false),
48-
AccountMeta.Writable(delegationRecord, false),
49-
AccountMeta.Writable(delegationMetadata, false),
50-
AccountMeta.ReadOnly(WorldProgram.DelegationProgram, false),
51-
AccountMeta.ReadOnly(SystemProgram.ProgramIdKey, false),
52-
},
53-
Data = data,
54-
};
55-
return new DelegateComponentInstruction() {
56-
Pda = WorldProgram.FindDelegationProgramPda(seed, entity),
57-
Instruction = instruction,
58-
};
59-
}
22+
public static async Task<DelegateComponentInstruction> DelegateComponent(PublicKey payer, PublicKey entity, PublicKey componentId, string seed = "") {
23+
// Compute the delegated account PDA and related PDAs
24+
var account = WorldProgram.FindComponentPda(componentId, entity, seed);
25+
var bufferPda = WorldProgram.FindBufferPda(account, componentId);
26+
var delegationRecord = WorldProgram.FindDelegationProgramPda("delegation", account);
27+
var delegationMetadata = WorldProgram.FindDelegationProgramPda("delegation-metadata", account);
28+
29+
// Build instruction data per TS beet struct:
30+
// discriminator[8] + commitFrequencyMs[u32 le] + validator[COption<Pubkey>] + pdaSeeds[Vec<Bytes>]
31+
byte[] discriminator = new byte[] { 90, 147, 75, 178, 85, 88, 4, 137 };
32+
uint commitFrequencyMs = 0;
33+
byte[] commitFrequencyBytes = BitConverter.GetBytes(commitFrequencyMs); // little-endian on most platforms
34+
byte[] validatorNoneTag = new byte[] { 0 }; // COption None
35+
36+
// pdaSeeds = [seedBytes, entityPubkeyBytes]
37+
var seedBytes = Encoding.UTF8.GetBytes(seed ?? "");
38+
var entityBytes = entity.KeyBytes;
39+
byte[] pdaSeeds = BuildVecOfBytes(new byte[][] { seedBytes, entityBytes });
40+
41+
var data = Concat(discriminator, commitFrequencyBytes, validatorNoneTag, pdaSeeds);
42+
43+
TransactionInstruction instruction = new TransactionInstruction() {
44+
ProgramId = componentId,
45+
Keys = new List<AccountMeta>() {
46+
AccountMeta.ReadOnly(payer, true),
47+
AccountMeta.ReadOnly(entity, false),
48+
AccountMeta.Writable(account, false),
49+
AccountMeta.ReadOnly(componentId, false),
50+
AccountMeta.Writable(bufferPda, false),
51+
AccountMeta.Writable(delegationRecord, false),
52+
AccountMeta.Writable(delegationMetadata, false),
53+
AccountMeta.ReadOnly(WorldProgram.DelegationProgram, false),
54+
AccountMeta.ReadOnly(SystemProgram.ProgramIdKey, false),
55+
},
56+
Data = data,
57+
};
58+
return new DelegateComponentInstruction() {
59+
Pda = account,
60+
Instruction = instruction,
61+
};
62+
}
63+
64+
/// <summary>
65+
/// Overload for bundled components: seed is augmented with component name.
66+
/// Mirrors TS behavior using component.seeds(seed) for PDA seeds.
67+
/// </summary>
68+
public static async Task<DelegateComponentInstruction> DelegateComponent(PublicKey payer, PublicKey entity, Component component, string seed = "") {
69+
var account = WorldProgram.FindComponentPda(component.Program, entity, component.Seeds(seed));
70+
var bufferPda = WorldProgram.FindBufferPda(account, component.Program);
71+
var delegationRecord = WorldProgram.FindDelegationProgramPda("delegation", account);
72+
var delegationMetadata = WorldProgram.FindDelegationProgramPda("delegation-metadata", account);
73+
74+
byte[] discriminator = new byte[] { 90, 147, 75, 178, 85, 88, 4, 137 };
75+
uint commitFrequencyMs = 0;
76+
byte[] commitFrequencyBytes = BitConverter.GetBytes(commitFrequencyMs);
77+
byte[] validatorNoneTag = new byte[] { 0 };
78+
79+
var seedBytes = Encoding.UTF8.GetBytes(component.Seeds(seed));
80+
var entityBytes = entity.KeyBytes;
81+
byte[] pdaSeeds = BuildVecOfBytes(new byte[][] { seedBytes, entityBytes });
82+
83+
var data = Concat(discriminator, commitFrequencyBytes, validatorNoneTag, pdaSeeds);
84+
85+
TransactionInstruction instruction = new TransactionInstruction() {
86+
ProgramId = component.Program,
87+
Keys = new List<AccountMeta>() {
88+
AccountMeta.ReadOnly(payer, true),
89+
AccountMeta.ReadOnly(entity, false),
90+
AccountMeta.Writable(account, false),
91+
AccountMeta.ReadOnly(component.Program, false),
92+
AccountMeta.Writable(bufferPda, false),
93+
AccountMeta.Writable(delegationRecord, false),
94+
AccountMeta.Writable(delegationMetadata, false),
95+
AccountMeta.ReadOnly(WorldProgram.DelegationProgram, false),
96+
AccountMeta.ReadOnly(SystemProgram.ProgramIdKey, false),
97+
},
98+
Data = data,
99+
};
100+
return new DelegateComponentInstruction() {
101+
Pda = account,
102+
Instruction = instruction,
103+
};
104+
}
105+
106+
private static byte[] BuildVecOfBytes(byte[][] items)
107+
{
108+
// beet array encoding: u32 count, then each element as beet.bytes => u32 length + bytes
109+
var countLe = BitConverter.GetBytes((uint)items.Length);
110+
if (!BitConverter.IsLittleEndian) Array.Reverse(countLe);
111+
List<byte> result = new List<byte>(4);
112+
result.AddRange(countLe);
113+
foreach (var item in items)
114+
{
115+
var lenLe = BitConverter.GetBytes((uint)(item?.Length ?? 0));
116+
if (!BitConverter.IsLittleEndian) Array.Reverse(lenLe);
117+
result.AddRange(lenLe);
118+
if (item != null && item.Length > 0)
119+
result.AddRange(item);
120+
}
121+
return result.ToArray();
122+
}
123+
124+
private static byte[] Concat(params byte[][] arrays)
125+
{
126+
int total = 0;
127+
foreach (var a in arrays) total += a.Length;
128+
var buf = new byte[total];
129+
int offset = 0;
130+
foreach (var a in arrays)
131+
{
132+
Buffer.BlockCopy(a, 0, buf, offset, a.Length);
133+
offset += a.Length;
134+
}
135+
return buf;
136+
}
60137
}
61138
}

clients/csharp/Solana.Unity.Bolt/WorldProgram/Bolt/DestroyComponent.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,27 @@ public static async Task<DestroyComponentInstruction> DestroyComponent(PublicKey
4343
Instruction = instruction
4444
};
4545
}
46+
47+
/// <summary>
48+
/// Overload accepting bundled component identifier; seed defaults to component name.
49+
/// Mirrors TS: discriminator derived from component name if provided.
50+
/// </summary>
51+
public static async Task<DestroyComponentInstruction> DestroyComponent(PublicKey authority, PublicKey receiver, PublicKey entity, Component component, string seed = "") {
52+
var pda = WorldProgram.FindComponentPda(component.Program, entity, component.Seeds(seed));
53+
var componentProgramData = WorldProgram.FindComponentProgramDataPda(component.Program);
54+
var destroyComponent = new DestroyComponentAccounts() {
55+
Authority = authority,
56+
Receiver = receiver,
57+
Entity = entity,
58+
Component = pda,
59+
ComponentProgram = component.Program,
60+
ComponentProgramData = componentProgramData,
61+
CpiAuth = WorldProgram.CpiAuthAddress
62+
};
63+
var instruction = WorldProgram.DestroyComponent(destroyComponent, component.GetMethodDiscriminator("destroy"));
64+
return new DestroyComponentInstruction() {
65+
Instruction = instruction
66+
};
67+
}
4668
}
4769
}

clients/csharp/Solana.Unity.Bolt/WorldProgram/Bolt/InitializeComponent.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public static async Task<InitializeComponentInstruction> InitializeComponent(Pub
3434
Entity = entity,
3535
Data = componentPda,
3636
ComponentProgram = componentId,
37-
Authority = new PublicKey(WorldProgram.ID),
37+
Authority = authority ?? new PublicKey(WorldProgram.ID),
3838
CpiAuth = WorldProgram.CpiAuthAddress
3939
};
4040
var instruction = WorldProgram.InitializeComponent(initializeComponent, GetDiscriminator("global:initialize"));
@@ -51,9 +51,10 @@ public static async Task<InitializeComponentInstruction> InitializeComponent(Pub
5151
/// <param name="payer">Payer public key.</param>
5252
/// <param name="entity">Entity PDA.</param>
5353
/// <param name="component">Bundled component identifier (program + name).</param>
54+
/// <param name="seed">Optional additional seed; defaults to empty. Final seed is seed + component name.</param>
5455
/// <param name="authority">Optional authority, defaults to world program id.</param>
55-
public static async Task<InitializeComponentInstruction> InitializeComponent(PublicKey payer, PublicKey entity, Component component, PublicKey authority = null) {
56-
var componentPda = WorldProgram.FindComponentPda(component.Program, entity, component.Name);
56+
public static async Task<InitializeComponentInstruction> InitializeComponent(PublicKey payer, PublicKey entity, Component component, string seed = "", PublicKey authority = null) {
57+
var componentPda = WorldProgram.FindComponentPda(component.Program, entity, component.Seeds(seed));
5758
var initializeComponent = new InitializeComponentAccounts() {
5859
Payer = payer,
5960
Entity = entity,
@@ -62,8 +63,7 @@ public static async Task<InitializeComponentInstruction> InitializeComponent(Pub
6263
Authority = authority ?? new PublicKey(WorldProgram.ID),
6364
CpiAuth = WorldProgram.CpiAuthAddress
6465
};
65-
var discriminatorName = $"global:{component.Name}_initialize";
66-
var instruction = WorldProgram.InitializeComponent(initializeComponent, GetDiscriminator(discriminatorName));
66+
var instruction = WorldProgram.InitializeComponent(initializeComponent, component.GetMethodDiscriminator("initialize"));
6767
return new InitializeComponentInstruction() {
6868
Pda = componentPda,
6969
Instruction = instruction

0 commit comments

Comments
 (0)