@emblemvault/primitives-stake 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js CHANGED
@@ -18,7 +18,7 @@
18
18
  import { PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY, Transaction, TransactionInstruction, TransactionMessage, VersionedTransaction, } from '@solana/web3.js';
19
19
  import { TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, createAssociatedTokenAccountIdempotentInstruction, getAssociatedTokenAddressSync, } from '@solana/spl-token';
20
20
  import { EmblemError, decodeSignedPayload, } from '@emblemvault/primitives-shared';
21
- import { decodeRewardDistributionAccount, decodeStakePoolAccount, decodeStakeVaultAccount, MULTIPLIER_SCALE, PENALTY_TAG_BURN, PENALTY_TAG_REDISTRIBUTE, PENALTY_TAG_TREASURY_PDA, REWARD_CADENCE_DAILY, REWARD_CADENCE_HOURLY, REWARD_CADENCE_WEEKLY, REWARD_FORMULA_LINEAR_V1, REWARD_FORMULA_TIME_WEIGHTED_V1, REWARD_SOURCE_TAG_DIST_VAULT, REWARD_SOURCE_TAG_SPLIT_RECIPIENT, STAKE_POOL_DISCRIMINATOR, STAKE_VAULT_DISCRIMINATOR, } from './codec.js';
21
+ import { decodePlatformAccrualAccount, decodeRewardDistributionAccount, decodeStakePlatformConfigAccount, decodeStakePoolAccount, decodeStakeVaultAccount, MULTIPLIER_SCALE, PENALTY_TAG_BURN, PENALTY_TAG_REDISTRIBUTE, PENALTY_TAG_TREASURY_PDA, REWARD_CADENCE_DAILY, REWARD_CADENCE_HOURLY, REWARD_CADENCE_WEEKLY, REWARD_DIST_DISCRIMINATOR, REWARD_FORMULA_LINEAR_V1, REWARD_FORMULA_TIME_WEIGHTED_V1, REWARD_SOURCE_TAG_DIST_VAULT, REWARD_SOURCE_TAG_SPLIT_RECIPIENT, STAKE_POOL_DISCRIMINATOR, STAKE_VAULT_DISCRIMINATOR, } from './codec.js';
22
22
  import { deriveRewardDistPda, deriveStakePoolPda, deriveStakeVaultPda, STAKE_PROGRAM_ID, } from './pda.js';
23
23
  // =============================================================================
24
24
  // Anchor instruction discriminators
@@ -26,15 +26,19 @@ import { deriveRewardDistPda, deriveStakePoolPda, deriveStakeVaultPda, STAKE_PRO
26
26
  const IX_CREATE_POOL = Buffer.from([
27
27
  0xe9, 0x92, 0xd1, 0x8e, 0xcf, 0x68, 0x40, 0xbc,
28
28
  ]);
29
- const IX_REGISTER_REWARD_VAULT = Buffer.from([
30
- 0xcb, 0x37, 0x29, 0x9c, 0xfc, 0x7f, 0xb9, 0xef,
31
- ]);
29
+ // Bundle 9+232 (2026-05-06): IX_REGISTER_REWARD_VAULT deleted with the
30
+ // Tier 2 tombstone. Per-stream reward vaults are init'd via
31
+ // IX_REGISTER_POOL_REWARD_MINT below.
32
32
  const IX_REGISTER_PENDING_ACTION_VAULT = Buffer.from([
33
33
  0x5b, 0x60, 0x22, 0xa1, 0x04, 0x7c, 0xa3, 0x62,
34
34
  ]);
35
35
  const IX_REGISTER_POOL_TOKEN_ACCOUNTS = Buffer.from([
36
36
  0x3c, 0xd3, 0x00, 0x47, 0x80, 0x7b, 0xc8, 0x52,
37
37
  ]);
38
+ /** Bundle 9+232 (2026-05-06) — sha256("global:register_pool_reward_mint"). */
39
+ const IX_REGISTER_POOL_REWARD_MINT = Buffer.from([
40
+ 0xd3, 0xa8, 0x16, 0xd6, 0xda, 0xfb, 0xea, 0x78,
41
+ ]);
38
42
  const IX_STAKE = Buffer.from([
39
43
  0xce, 0xb0, 0xca, 0x12, 0xc8, 0xd1, 0xb3, 0x6c,
40
44
  ]);
@@ -47,6 +51,14 @@ const IX_CLAIM = Buffer.from([
47
51
  const IX_DISTRIBUTE_REWARDS = Buffer.from([
48
52
  0x61, 0x06, 0xe3, 0xff, 0x7c, 0xa5, 0x03, 0x94,
49
53
  ]);
54
+ /** Bundle 9+232 (2026-05-06) — sha256("global:set_platform_fee_bps"). */
55
+ const IX_SET_PLATFORM_FEE_BPS = Buffer.from([
56
+ 0x99, 0x37, 0xb5, 0x3e, 0x41, 0xfc, 0xf7, 0xdb,
57
+ ]);
58
+ /** Bundle 9+232 (2026-05-06) — sha256("global:emblem_claim_platform_fee"). */
59
+ const IX_EMBLEM_CLAIM_PLATFORM_FEE = Buffer.from([
60
+ 0x11, 0x6d, 0x7e, 0xd1, 0xd5, 0x67, 0xa7, 0x97,
61
+ ]);
50
62
  // ST-2 follow-on (2026-05-04): platform-config + emergency-halt ixs.
51
63
  // All discriminators verified via Node crypto sha256("global:<name>").subarray(0, 8).
52
64
  const IX_INIT_PLATFORM_CONFIG = Buffer.from([
@@ -166,13 +178,15 @@ function encodeRewardFormula(f) {
166
178
  function deriveVaultAuthority(programId) {
167
179
  return PublicKey.findProgramAddressSync([Buffer.from('vault-authority')], programId)[0];
168
180
  }
169
- function deriveRewardVault(vaultPubkey, programId) {
170
- // Audit X-1 Pass 2: seed is now Pubkey (32 bytes), not String.
171
- return PublicKey.findProgramAddressSync([Buffer.from('reward-vault'), vaultPubkey.toBuffer()], programId)[0];
181
+ /** Bundle 9+232 (2026-05-06) — per-mint reward vault. Pre-bundle
182
+ * single-arg helpers (`deriveRewardVault(vaultPubkey, ...)` and
183
+ * `deriveRewardVaultForPool(pool, ...)`) are gone — the seed shape is
184
+ * now `[b"reward-vault", pool, reward_mint]`. */
185
+ function deriveRewardVaultForMint(poolPda, rewardMint, programId) {
186
+ return PublicKey.findProgramAddressSync([Buffer.from('reward-vault'), poolPda.toBytes(), rewardMint.toBytes()], programId)[0];
172
187
  }
173
- function deriveRewardVaultForPool(poolPda, programId) {
174
- // staker-pool reward vault uses the pool PDA bytes as the vault id
175
- return PublicKey.findProgramAddressSync([Buffer.from('reward-vault'), poolPda.toBytes()], programId)[0];
188
+ function derivePlatformAccrualForMint(poolPda, rewardMint, programId) {
189
+ return PublicKey.findProgramAddressSync([Buffer.from('platform-accrual'), poolPda.toBytes(), rewardMint.toBytes()], programId)[0];
176
190
  }
177
191
  function derivePendingActionVault(configId, mint, actionType, programId) {
178
192
  return PublicKey.findProgramAddressSync([
@@ -223,14 +237,35 @@ export class EmblemStakeClient {
223
237
  });
224
238
  return { vaultPda: pda.toBase58(), bump };
225
239
  }
240
+ /** Bundle 9+232 (2026-05-06) — RewardDistribution PDA now per
241
+ * `(pool, rewardMint, periodId)`. */
226
242
  deriveDistributionPda(args) {
227
243
  const { pda, bump } = deriveRewardDistPda({
228
244
  poolPda: args.poolPda,
245
+ rewardMint: args.rewardMint,
229
246
  periodId: args.periodId,
230
247
  programId: this.programId.toBase58(),
231
248
  });
232
249
  return { distributionPda: pda.toBase58(), bump };
233
250
  }
251
+ /** Bundle 9+232 (2026-05-06) — per-mint reward vault address. */
252
+ deriveRewardVaultPda(args) {
253
+ const [pda, bump] = PublicKey.findProgramAddressSync([
254
+ Buffer.from('reward-vault'),
255
+ new PublicKey(args.poolPda).toBytes(),
256
+ new PublicKey(args.rewardMint).toBytes(),
257
+ ], this.programId);
258
+ return { rewardVaultPda: pda.toBase58(), bump };
259
+ }
260
+ /** Bundle 9+232 (2026-05-06) — per-stream platform accrual PDA. */
261
+ derivePlatformAccrualPda(args) {
262
+ const [pda, bump] = PublicKey.findProgramAddressSync([
263
+ Buffer.from('platform-accrual'),
264
+ new PublicKey(args.poolPda).toBytes(),
265
+ new PublicKey(args.rewardMint).toBytes(),
266
+ ], this.programId);
267
+ return { accrualPda: pda.toBase58(), bump };
268
+ }
234
269
  /** ST-2 follow-on (2026-05-04). The stake program's PlatformConfig
235
270
  * PDA is a singleton — same shape as launch + sell. */
236
271
  derivePlatformConfigPda() {
@@ -609,6 +644,10 @@ export class EmblemStakeClient {
609
644
  // Convert decimal multiplier (1.5) to fixed-point ×1000 (1500)
610
645
  multiplier: Math.round(t.multiplier * MULTIPLIER_SCALE),
611
646
  }));
647
+ // Bundle 9+232 (2026-05-06) — initial reward mints registered at
648
+ // create_pool time. Empty Vec is valid; pool authority can append
649
+ // later via register_pool_reward_mint.
650
+ const rewardMints = (args.initialRewardMints ?? []).map((m) => new PublicKey(m));
612
651
  const data = Buffer.concat([
613
652
  IX_CREATE_POOL,
614
653
  u32(poolId),
@@ -626,6 +665,9 @@ export class EmblemStakeClient {
626
665
  ...cfg.rewardSources.map(encodeRewardSource),
627
666
  Buffer.from([encodeRewardCadence(cfg.rewardCadence)]),
628
667
  Buffer.from([encodeRewardFormula(cfg.rewardFormula)]),
668
+ // Bundle 9+232 — reward_mints: Vec<Pubkey>
669
+ u32(rewardMints.length),
670
+ ...rewardMints.map((m) => Buffer.from(m.toBytes())),
629
671
  ]);
630
672
  return new TransactionInstruction({
631
673
  programId: this.programId,
@@ -638,31 +680,105 @@ export class EmblemStakeClient {
638
680
  });
639
681
  }
640
682
  /**
641
- * Build the register_reward_vault instruction (Tier 2 settlement bucket).
642
- * Audit X-1 Pass 2 (2026-05-03): `vaultId: string` `vaultPubkey: Pubkey`.
643
- * The caller picks the pubkey; the on-chain vault address is derived from
644
- * `[b"reward-vault", vaultPubkey]`. Use `deriveDistributionVaultAddress`
645
- * to compute that address from the same pubkey.
683
+ * Bundle 9+232 (2026-05-06) register a reward token stream on an
684
+ * existing pool. Inits the per-mint reward vault PDA + the stream's
685
+ * PlatformAccrual PDA, and appends `reward_mint` to
686
+ * `pool.reward_mints`. Authority-only on chain (`has_one = authority`).
687
+ *
688
+ * Used by the `createPool()` self-submitting wrapper to chain one of
689
+ * these per `initialRewardMints` entry — but exposed here so callers
690
+ * can register additional streams later.
646
691
  */
647
- buildRegisterRewardVaultIx(args) {
648
- const mint = new PublicKey(args.mint);
649
- const vaultPubkeyKey = new PublicKey(args.vaultPubkey);
692
+ buildRegisterPoolRewardMintIx(args) {
693
+ const poolKey = new PublicKey(args.poolPda);
694
+ const mintKey = new PublicKey(args.rewardMint);
650
695
  const vaultAuthority = deriveVaultAuthority(this.programId);
651
- const vault = deriveRewardVault(vaultPubkeyKey, this.programId);
696
+ const rewardVault = deriveRewardVaultForMint(poolKey, mintKey, this.programId);
697
+ const platformAccrual = derivePlatformAccrualForMint(poolKey, mintKey, this.programId);
652
698
  return new TransactionInstruction({
653
699
  programId: this.programId,
654
700
  keys: [
655
- { pubkey: new PublicKey(args.authority), isSigner: true, isWritable: true },
656
- { pubkey: mint, isSigner: false, isWritable: false },
701
+ { pubkey: new PublicKey(args.payer), isSigner: true, isWritable: true },
702
+ { pubkey: new PublicKey(args.authority), isSigner: true, isWritable: false },
703
+ { pubkey: poolKey, isSigner: false, isWritable: true },
704
+ { pubkey: mintKey, isSigner: false, isWritable: false },
657
705
  { pubkey: vaultAuthority, isSigner: false, isWritable: false },
658
- { pubkey: vault, isSigner: false, isWritable: true },
706
+ { pubkey: rewardVault, isSigner: false, isWritable: true },
707
+ { pubkey: platformAccrual, isSigner: false, isWritable: true },
659
708
  { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
660
709
  { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
661
710
  { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
662
711
  ],
663
- data: Buffer.concat([IX_REGISTER_REWARD_VAULT, vaultPubkeyKey.toBuffer()]),
712
+ data: Buffer.concat([IX_REGISTER_POOL_REWARD_MINT, mintKey.toBuffer()]),
664
713
  });
665
714
  }
715
+ /**
716
+ * Bundle 9+232 (2026-05-06) — admin sets the protocol fee on reward
717
+ * distributions, in basis points. Capped at
718
+ * `STAKE_PLATFORM_FEE_BPS_MAX = 3_000` (30%) by the on-chain ix.
719
+ * Mainnet ships at 0.
720
+ */
721
+ buildSetPlatformFeeBpsIx(args) {
722
+ const { platformConfigPda } = this.derivePlatformConfigPda();
723
+ const data = Buffer.concat([IX_SET_PLATFORM_FEE_BPS, u16(args.feeBps)]);
724
+ return new TransactionInstruction({
725
+ programId: this.programId,
726
+ keys: [
727
+ { pubkey: new PublicKey(args.admin), isSigner: true, isWritable: false },
728
+ {
729
+ pubkey: new PublicKey(platformConfigPda),
730
+ isSigner: false,
731
+ isWritable: true,
732
+ },
733
+ ],
734
+ data,
735
+ });
736
+ }
737
+ /**
738
+ * Bundle 9+232 (2026-05-06) — drain accrued platform fees from one
739
+ * reward stream's `PlatformAccrual.pending` into the admin's treasury
740
+ * ATA. Admin-signed (`has_one = admin` on `PlatformConfig`); body
741
+ * also asserts `treasury.owner == admin` so the admin can't route
742
+ * Emblem fees to a third-party wallet. Idempotent-error at
743
+ * `pending == 0` with `StakePlatformAccrualNothingPending (519)`.
744
+ */
745
+ buildEmblemClaimPlatformFeeIx(args) {
746
+ const poolKey = new PublicKey(args.poolPda);
747
+ const mintKey = new PublicKey(args.rewardMint);
748
+ const { platformConfigPda } = this.derivePlatformConfigPda();
749
+ const platformAccrual = derivePlatformAccrualForMint(poolKey, mintKey, this.programId);
750
+ const rewardVault = deriveRewardVaultForMint(poolKey, mintKey, this.programId);
751
+ const vaultAuthority = deriveVaultAuthority(this.programId);
752
+ const data = Buffer.concat([
753
+ IX_EMBLEM_CLAIM_PLATFORM_FEE,
754
+ mintKey.toBuffer(),
755
+ ]);
756
+ return new TransactionInstruction({
757
+ programId: this.programId,
758
+ keys: [
759
+ { pubkey: new PublicKey(args.admin), isSigner: true, isWritable: false },
760
+ {
761
+ pubkey: new PublicKey(platformConfigPda),
762
+ isSigner: false,
763
+ isWritable: false,
764
+ },
765
+ { pubkey: poolKey, isSigner: false, isWritable: false },
766
+ { pubkey: platformAccrual, isSigner: false, isWritable: true },
767
+ { pubkey: rewardVault, isSigner: false, isWritable: true },
768
+ {
769
+ pubkey: new PublicKey(args.treasuryTokenAccount),
770
+ isSigner: false,
771
+ isWritable: true,
772
+ },
773
+ { pubkey: vaultAuthority, isSigner: false, isWritable: false },
774
+ { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
775
+ ],
776
+ data,
777
+ });
778
+ }
779
+ // Bundle 9+232 (2026-05-06): buildRegisterRewardVaultIx deleted with
780
+ // the Tier 2 tombstone. Use buildRegisterPoolRewardMintIx for per-stream
781
+ // reward vaults instead.
666
782
  /** Build the register_pending_action_vault instruction (Tier 3 settlement bucket). */
667
783
  buildRegisterPendingActionVaultIx(args) {
668
784
  const mint = new PublicKey(args.mint);
@@ -687,17 +803,15 @@ export class EmblemStakeClient {
687
803
  ]),
688
804
  });
689
805
  }
690
- /** Address of the staker-pool reward vault (Tier 2 destination for StakerPool recipients). */
691
- deriveStakerPoolRewardVault(poolPda) {
692
- return deriveRewardVaultForPool(new PublicKey(poolPda), this.programId).toBase58();
693
- }
694
806
  /**
695
- * Address of a distribution-vault (Tier 2 destination for DistributionVault
696
- * recipients). Audit X-1 Pass 2: arg is now Pubkey (was String). Pass the
697
- * same pubkey you used for `buildRegisterRewardVaultIx({ vaultPubkey })`.
807
+ * Bundle 9+232 (2026-05-06) — per-mint reward vault address. Pre-bundle
808
+ * `deriveStakerPoolRewardVault(pool)` (single-stream) and
809
+ * `deriveDistributionVaultAddress(vaultPubkey)` (Tier 2 tombstone)
810
+ * helpers are gone — see `deriveRewardVaultPda` above for the canonical
811
+ * `(pool, rewardMint)` derivation.
698
812
  */
699
- deriveDistributionVaultAddress(vaultPubkey) {
700
- return deriveRewardVault(new PublicKey(vaultPubkey), this.programId).toBase58();
813
+ deriveStakerPoolRewardVault(args) {
814
+ return deriveRewardVaultForMint(new PublicKey(args.poolPda), new PublicKey(args.rewardMint), this.programId).toBase58();
701
815
  }
702
816
  /** Address of a Tier 3 pending-action vault. */
703
817
  derivePendingActionVaultAddress(args) {
@@ -720,20 +834,27 @@ export class EmblemStakeClient {
720
834
  new PublicKey(args.owner).toBytes(),
721
835
  ], this.programId)[0].toBase58();
722
836
  }
723
- /** RewardDistribution PDA for a (pool, period_id) pair. */
837
+ /** Bundle 9+232 (2026-05-06) — RewardDistribution PDA for a
838
+ * `(pool, rewardMint, periodId)` triple. */
724
839
  deriveRewardDistributionAddress(args) {
725
840
  const periodBytes = Buffer.alloc(4);
726
841
  periodBytes.writeUInt32LE(args.periodId, 0);
727
- return PublicKey.findProgramAddressSync([Buffer.from('reward-dist'), new PublicKey(args.poolPda).toBytes(), periodBytes], this.programId)[0].toBase58();
842
+ return PublicKey.findProgramAddressSync([
843
+ Buffer.from('reward-dist'),
844
+ new PublicKey(args.poolPda).toBytes(),
845
+ new PublicKey(args.rewardMint).toBytes(),
846
+ periodBytes,
847
+ ], this.programId)[0].toBase58();
728
848
  }
729
849
  // ==========================================================================
730
850
  // Stake-flow ix builders
731
851
  // ==========================================================================
732
- /** Build the register_pool_token_accounts ix. Permissionless; one-time per pool. */
852
+ /** Build the register_pool_token_accounts ix. Permissionless;
853
+ * one-time per pool. Bundle 9+232 (2026-05-06) — principal-vault only;
854
+ * per-mint reward vaults moved to `register_pool_reward_mint`. */
733
855
  buildRegisterPoolTokenAccountsIx(args) {
734
856
  const poolKey = new PublicKey(args.poolPda);
735
857
  const principalVault = new PublicKey(this.derivePrincipalVaultAddress(args.poolPda));
736
- const rewardVault = new PublicKey(this.deriveStakerPoolRewardVault(args.poolPda));
737
858
  return new TransactionInstruction({
738
859
  programId: this.programId,
739
860
  keys: [
@@ -746,7 +867,6 @@ export class EmblemStakeClient {
746
867
  isWritable: false,
747
868
  },
748
869
  { pubkey: principalVault, isSigner: false, isWritable: true },
749
- { pubkey: rewardVault, isSigner: false, isWritable: true },
750
870
  { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
751
871
  { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
752
872
  { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
@@ -846,16 +966,24 @@ export class EmblemStakeClient {
846
966
  });
847
967
  }
848
968
  /**
849
- * Build the claim ix. The caller must pass RewardDistribution PDAs
850
- * for every period from `last_claim_period` to `current_period - 1`
851
- * inclusive in `remaining_accounts`.
969
+ * Build the claim ix. Bundle 9+232 (2026-05-06) caller specifies
970
+ * `rewardMint` (the stream to claim against). The caller must pass
971
+ * RewardDistribution PDAs for every period from
972
+ * `vault.last_claim_periods[mint_idx]` to `pool.current_periods[mint_idx] - 1`
973
+ * inclusive in `remaining_accounts`. Per-stream PDAs are seeded with
974
+ * `(pool, rewardMint, periodId)`.
852
975
  */
853
976
  buildClaimIx(args) {
854
977
  const poolKey = new PublicKey(args.poolPda);
978
+ const mintKey = new PublicKey(args.rewardMint);
855
979
  const stakeVault = new PublicKey(this.deriveStakeVaultAddress({ poolPda: args.poolPda, owner: args.user }));
856
- const rewardVault = new PublicKey(this.deriveStakerPoolRewardVault(args.poolPda));
980
+ const rewardVault = deriveRewardVaultForMint(poolKey, mintKey, this.programId);
857
981
  const distAccounts = args.periodIds.map((pid) => ({
858
- pubkey: new PublicKey(this.deriveRewardDistributionAddress({ poolPda: args.poolPda, periodId: pid })),
982
+ pubkey: new PublicKey(this.deriveRewardDistributionAddress({
983
+ poolPda: args.poolPda,
984
+ rewardMint: args.rewardMint,
985
+ periodId: pid,
986
+ })),
859
987
  isSigner: false,
860
988
  isWritable: false,
861
989
  }));
@@ -879,20 +1007,28 @@ export class EmblemStakeClient {
879
1007
  { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
880
1008
  ...distAccounts,
881
1009
  ],
882
- data: IX_CLAIM,
1010
+ // Bundle 9+232 — claim ix takes reward_mint: Pubkey arg.
1011
+ data: Buffer.concat([IX_CLAIM, mintKey.toBuffer()]),
883
1012
  });
884
1013
  }
885
1014
  /**
886
1015
  * Build the distribute_rewards ix. Permissionless trigger — anyone
887
- * pays tx fees to advance the period.
1016
+ * pays tx fees to advance the period for one stream. Bundle 9+232
1017
+ * (2026-05-06) — caller specifies `rewardMint`; ix data carries it
1018
+ * + reads `PlatformConfig.fee_bps` to bookkeep the staker-pool /
1019
+ * platform-accrual split.
888
1020
  */
889
1021
  buildDistributeRewardsIx(args) {
890
1022
  const poolKey = new PublicKey(args.poolPda);
891
- const rewardVault = new PublicKey(this.deriveStakerPoolRewardVault(args.poolPda));
1023
+ const mintKey = new PublicKey(args.rewardMint);
1024
+ const rewardVault = deriveRewardVaultForMint(poolKey, mintKey, this.programId);
892
1025
  const distribution = new PublicKey(this.deriveRewardDistributionAddress({
893
1026
  poolPda: args.poolPda,
1027
+ rewardMint: args.rewardMint,
894
1028
  periodId: args.periodId,
895
1029
  }));
1030
+ const { platformConfigPda } = this.derivePlatformConfigPda();
1031
+ const platformAccrual = derivePlatformAccrualForMint(poolKey, mintKey, this.programId);
896
1032
  return new TransactionInstruction({
897
1033
  programId: this.programId,
898
1034
  keys: [
@@ -900,31 +1036,136 @@ export class EmblemStakeClient {
900
1036
  { pubkey: poolKey, isSigner: false, isWritable: true },
901
1037
  { pubkey: rewardVault, isSigner: false, isWritable: false },
902
1038
  { pubkey: distribution, isSigner: false, isWritable: true },
1039
+ {
1040
+ pubkey: new PublicKey(platformConfigPda),
1041
+ isSigner: false,
1042
+ isWritable: false,
1043
+ },
1044
+ { pubkey: platformAccrual, isSigner: false, isWritable: true },
903
1045
  { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
904
1046
  ],
905
- data: IX_DISTRIBUTE_REWARDS,
1047
+ data: Buffer.concat([IX_DISTRIBUTE_REWARDS, mintKey.toBuffer()]),
906
1048
  });
907
1049
  }
908
1050
  // ==========================================================================
909
1051
  // Self-submitting wrappers
910
1052
  // ==========================================================================
1053
+ /**
1054
+ * Create a stake pool AND chain `register_pool_token_accounts` so the
1055
+ * principal vault + staker-pool reward vault are initialized in the
1056
+ * same call. Without the chain, the very first `stake()` reverts with
1057
+ * `AccountNotInitialized` on `principal_vault`. Caller gets back a
1058
+ * pool that is immediately ready to receive stakes.
1059
+ *
1060
+ * register_pool_token_accounts is permissionless and one-time per
1061
+ * pool, so we always include it. If it ever becomes a no-op for an
1062
+ * existing pool, this is still safe.
1063
+ */
1064
+ /**
1065
+ * Create a stake pool AND chain `register_pool_token_accounts` so the
1066
+ * principal vault is initialized in the same call. Bundle 9+232
1067
+ * (2026-05-06) — also chains one `register_pool_reward_mint` per
1068
+ * entry in `args.initialRewardMints`, initializing each stream's
1069
+ * reward vault + PlatformAccrual atomically with pool creation. If
1070
+ * `initialRewardMints` is empty, no reward streams are registered;
1071
+ * the pool authority can register them later via
1072
+ * `registerPoolRewardMint()`.
1073
+ */
911
1074
  async createPool(args) {
912
1075
  const signer = this.requireSigner('createPool');
913
- const ix = this.buildCreatePoolIx({ ...args, authority: signer.publicKey });
914
- const tx = new Transaction().add(ix);
915
- const txSignature = await this.signAndSubmit(tx, signer);
916
1076
  const { poolPda } = this.derivePoolPda({
917
1077
  asset: args.config.asset,
918
1078
  poolId: args.poolId ?? 0,
919
1079
  });
920
- return { poolPda, txSignature };
1080
+ const createIx = this.buildCreatePoolIx({
1081
+ ...args,
1082
+ authority: signer.publicKey,
1083
+ });
1084
+ const registerIx = this.buildRegisterPoolTokenAccountsIx({
1085
+ payer: signer.publicKey,
1086
+ poolPda,
1087
+ mint: args.config.asset,
1088
+ });
1089
+ const tx = new Transaction().add(createIx).add(registerIx);
1090
+ const initialRewardMints = args.initialRewardMints ?? [];
1091
+ for (const rewardMint of initialRewardMints) {
1092
+ tx.add(this.buildRegisterPoolRewardMintIx({
1093
+ payer: signer.publicKey,
1094
+ authority: signer.publicKey,
1095
+ poolPda,
1096
+ rewardMint,
1097
+ }));
1098
+ }
1099
+ const txSignature = await this.signAndSubmit(tx, signer);
1100
+ return {
1101
+ poolPda,
1102
+ principalVaultPda: this.derivePrincipalVaultAddress(poolPda),
1103
+ rewardVaultPdas: initialRewardMints.map((rewardMint) => ({
1104
+ rewardMint,
1105
+ rewardVaultPda: this.deriveStakerPoolRewardVault({ poolPda, rewardMint }),
1106
+ })),
1107
+ txSignature,
1108
+ };
1109
+ }
1110
+ /**
1111
+ * Bundle 9+232 (2026-05-06) — register a new reward token stream on
1112
+ * an existing pool. Authority-only on chain. Inits the per-mint
1113
+ * reward vault PDA + PlatformAccrual; appends to `pool.reward_mints`.
1114
+ */
1115
+ async registerPoolRewardMint(args) {
1116
+ const signer = this.requireSigner('registerPoolRewardMint');
1117
+ const ix = this.buildRegisterPoolRewardMintIx({
1118
+ payer: signer.publicKey,
1119
+ authority: signer.publicKey,
1120
+ poolPda: args.poolPda,
1121
+ rewardMint: args.rewardMint,
1122
+ });
1123
+ const tx = new Transaction().add(ix);
1124
+ const txSignature = await this.signAndSubmit(tx, signer);
1125
+ return {
1126
+ rewardVaultPda: this.deriveStakerPoolRewardVault(args),
1127
+ accrualPda: this.derivePlatformAccrualPda(args).accrualPda,
1128
+ txSignature,
1129
+ };
921
1130
  }
922
- async registerRewardVault(args) {
923
- const signer = this.requireSigner('registerRewardVault');
924
- const ix = this.buildRegisterRewardVaultIx({ ...args, authority: signer.publicKey });
1131
+ /**
1132
+ * Bundle 9+232 (2026-05-06) — admin updates Emblem's protocol fee
1133
+ * on reward distributions. Capped at 30% by the on-chain ix.
1134
+ * Mainnet ships at 0.
1135
+ */
1136
+ async setPlatformFeeBps(feeBps) {
1137
+ const signer = this.requireSigner('setPlatformFeeBps');
1138
+ const ix = this.buildSetPlatformFeeBpsIx({ admin: signer.publicKey, feeBps });
925
1139
  const tx = new Transaction().add(ix);
926
1140
  const txSignature = await this.signAndSubmit(tx, signer);
927
- return { vaultPda: this.deriveDistributionVaultAddress(args.vaultPubkey), txSignature };
1141
+ return { txSignature };
1142
+ }
1143
+ /**
1144
+ * Bundle 9+232 (2026-05-06) — admin drains accrued platform fees from
1145
+ * one reward stream into a treasury ATA. Auto-derives the canonical
1146
+ * admin-owned ATA for `rewardMint` if `treasuryTokenAccount` is
1147
+ * omitted, and prepends an idempotent ATA-create ix.
1148
+ */
1149
+ async emblemClaimPlatformFee(args) {
1150
+ const signer = this.requireSigner('emblemClaimPlatformFee');
1151
+ const tokenProgramId = await this.detectTokenProgramId(args.rewardMint);
1152
+ const mintKey = new PublicKey(args.rewardMint);
1153
+ const adminKey = new PublicKey(signer.publicKey);
1154
+ const treasuryAta = args.treasuryTokenAccount ??
1155
+ getAssociatedTokenAddressSync(mintKey, adminKey, true, tokenProgramId).toBase58();
1156
+ const ataIx = createAssociatedTokenAccountIdempotentInstruction(adminKey, new PublicKey(treasuryAta), adminKey, mintKey, tokenProgramId);
1157
+ const claimIx = this.buildEmblemClaimPlatformFeeIx({
1158
+ admin: signer.publicKey,
1159
+ poolPda: args.poolPda,
1160
+ rewardMint: args.rewardMint,
1161
+ treasuryTokenAccount: treasuryAta,
1162
+ });
1163
+ const tx = new Transaction().add(ataIx).add(claimIx);
1164
+ const txSignature = await this.signAndSubmit(tx, signer);
1165
+ return {
1166
+ accrualPda: this.derivePlatformAccrualPda(args).accrualPda,
1167
+ txSignature,
1168
+ };
928
1169
  }
929
1170
  async registerPendingActionVault(args) {
930
1171
  const signer = this.requireSigner('registerPendingActionVault');
@@ -1024,9 +1265,11 @@ export class EmblemStakeClient {
1024
1265
  return { txSignature };
1025
1266
  }
1026
1267
  /**
1027
- * Claim accrued rewards for one or more periods. Auto-derives the
1028
- * user's ATA from the pool's asset mint and prepends an idempotent
1029
- * ATA-create ix (covers first-time claimers).
1268
+ * Claim accrued rewards for one stream over one or more periods.
1269
+ * Bundle 9+232 (2026-05-06) claim is per-reward-stream; caller
1270
+ * specifies which `rewardMint` to pull. Auto-derives the user's ATA
1271
+ * for `rewardMint` (NOT pool.asset — these may differ) and prepends
1272
+ * an idempotent ATA-create ix.
1030
1273
  */
1031
1274
  async claimRewards(args) {
1032
1275
  const signer = this.requireSigner('claimRewards');
@@ -1034,8 +1277,11 @@ export class EmblemStakeClient {
1034
1277
  if (!pool) {
1035
1278
  throw new EmblemStakeError(EmblemError.MissingAccount, `Pool ${args.poolPda} not found`);
1036
1279
  }
1037
- const tokenProgramId = await this.detectTokenProgramId(pool.asset);
1038
- const mintKey = new PublicKey(pool.asset);
1280
+ if (!pool.rewardMints.includes(args.rewardMint)) {
1281
+ throw new EmblemStakeError(EmblemError.StakeRewardSourceInvalid, `Pool ${args.poolPda} has not registered reward mint ${args.rewardMint}; registered: [${pool.rewardMints.join(', ')}]`);
1282
+ }
1283
+ const tokenProgramId = await this.detectTokenProgramId(args.rewardMint);
1284
+ const mintKey = new PublicKey(args.rewardMint);
1039
1285
  const ownerKey = new PublicKey(signer.publicKey);
1040
1286
  const userAta = args.userTokenAccount ??
1041
1287
  getAssociatedTokenAddressSync(mintKey, ownerKey, true, tokenProgramId).toBase58();
@@ -1043,6 +1289,7 @@ export class EmblemStakeClient {
1043
1289
  const claimIx = this.buildClaimIx({
1044
1290
  user: signer.publicKey,
1045
1291
  poolPda: args.poolPda,
1292
+ rewardMint: args.rewardMint,
1046
1293
  userTokenAccount: userAta,
1047
1294
  periodIds: args.periodIds,
1048
1295
  });
@@ -1050,11 +1297,17 @@ export class EmblemStakeClient {
1050
1297
  const txSignature = await this.signAndSubmit(tx, signer);
1051
1298
  return { txSignature };
1052
1299
  }
1300
+ /**
1301
+ * Bundle 9+232 (2026-05-06) — advance one reward stream's period
1302
+ * counter. Caller specifies `rewardMint`; the cadence floor + the
1303
+ * platform-fee bookkeeping are per-stream.
1304
+ */
1053
1305
  async distributeRewards(args) {
1054
1306
  const signer = this.requireSigner('distributeRewards');
1055
1307
  const ix = this.buildDistributeRewardsIx({
1056
1308
  payer: signer.publicKey,
1057
1309
  poolPda: args.poolPda,
1310
+ rewardMint: args.rewardMint,
1058
1311
  periodId: args.periodId,
1059
1312
  });
1060
1313
  const tx = new Transaction().add(ix);
@@ -1062,11 +1315,69 @@ export class EmblemStakeClient {
1062
1315
  return {
1063
1316
  distributionPda: this.deriveRewardDistributionAddress({
1064
1317
  poolPda: args.poolPda,
1318
+ rewardMint: args.rewardMint,
1065
1319
  periodId: args.periodId,
1066
1320
  }),
1067
1321
  txSignature,
1068
1322
  };
1069
1323
  }
1324
+ /**
1325
+ * Bundle 9+232 (2026-05-06) — discovery helper. Lists every
1326
+ * RewardDistribution PDA for one pool, optionally filtered to a
1327
+ * single reward mint. Mirrors launch SDK's `listLaunchesByCreator`
1328
+ * pattern. Sorted by `(rewardMint, periodId ASC)` for deterministic
1329
+ * pagination on the agent side.
1330
+ */
1331
+ async listDistributionsByPool(args) {
1332
+ const poolKey = new PublicKey(args.poolPda);
1333
+ // pool_pda field offset = disc(8) + bump(1) = 9.
1334
+ const POOL_FIELD_OFFSET = 9;
1335
+ const filters = [
1336
+ { memcmp: { offset: 0, bytes: bs58Encode(REWARD_DIST_DISCRIMINATOR) } },
1337
+ { memcmp: { offset: POOL_FIELD_OFFSET, bytes: poolKey.toBase58() } },
1338
+ ];
1339
+ if (args.rewardMint) {
1340
+ // reward_mint at offset disc(8) + bump(1) + pool_pda(32) + period_id(4) +
1341
+ // total_distributed(8) + weighted_stake_at_snapshot(8) + distributed_at(8) = 69.
1342
+ const REWARD_MINT_OFFSET = 69;
1343
+ filters.push({
1344
+ memcmp: { offset: REWARD_MINT_OFFSET, bytes: args.rewardMint },
1345
+ });
1346
+ }
1347
+ const accounts = await this.connection.getProgramAccounts(this.programId, {
1348
+ commitment: this.commitment,
1349
+ filters,
1350
+ });
1351
+ const views = accounts.map((a) => {
1352
+ const decoded = decodeRewardDistributionAccount(a.account.data);
1353
+ return { ...decoded, distributionPda: a.pubkey.toBase58() };
1354
+ });
1355
+ views.sort((a, b) => {
1356
+ if (a.rewardMint !== b.rewardMint) {
1357
+ return a.rewardMint < b.rewardMint ? -1 : 1;
1358
+ }
1359
+ return a.periodId - b.periodId;
1360
+ });
1361
+ return views;
1362
+ }
1363
+ /** Bundle 9+232 (2026-05-06) — read PlatformAccrual for a stream. */
1364
+ async getPlatformAccrual(args) {
1365
+ const accrualPda = this.derivePlatformAccrualPda(args).accrualPda;
1366
+ const info = await this.connection.getAccountInfo(new PublicKey(accrualPda), this.commitment);
1367
+ if (!info)
1368
+ return null;
1369
+ const decoded = decodePlatformAccrualAccount(info.data);
1370
+ return { accrualPda, pending: decoded.pending };
1371
+ }
1372
+ /** Bundle 9+232 (2026-05-06) — read PlatformConfig (singleton). */
1373
+ async getPlatformConfig() {
1374
+ const { platformConfigPda } = this.derivePlatformConfigPda();
1375
+ const info = await this.connection.getAccountInfo(new PublicKey(platformConfigPda), this.commitment);
1376
+ if (!info)
1377
+ return null;
1378
+ const decoded = decodeStakePlatformConfigAccount(info.data);
1379
+ return { platformConfigPda, ...decoded };
1380
+ }
1070
1381
  async signAndSubmit(tx, signer) {
1071
1382
  const { blockhash } = await this.connection.getLatestBlockhash(this.commitment);
1072
1383
  const payerKey = new PublicKey(signer.publicKey);