@emblemvault/primitives-stake 0.5.1 → 0.7.1

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,16 @@ 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) — `create_pool`'s reward_mints arg is
648
+ // always encoded EMPTY here. The on-chain `create_pool` body only
649
+ // appends to `pool.reward_mints`; it does NOT init the per-mint
650
+ // reward-vault TokenAccount (that's `register_pool_reward_mint`'s
651
+ // job). If we wrote the mints here AND the caller chained
652
+ // register_pool_reward_mint per mint, the second ix would revert
653
+ // with StakePoolShapeInvalid (duplicate). So mints flow through the
654
+ // register-per-mint path exclusively, and `args.initialRewardMints`
655
+ // is consumed by the higher-level `createPool()` wrapper to build
656
+ // those follow-up ixs.
612
657
  const data = Buffer.concat([
613
658
  IX_CREATE_POOL,
614
659
  u32(poolId),
@@ -626,6 +671,8 @@ export class EmblemStakeClient {
626
671
  ...cfg.rewardSources.map(encodeRewardSource),
627
672
  Buffer.from([encodeRewardCadence(cfg.rewardCadence)]),
628
673
  Buffer.from([encodeRewardFormula(cfg.rewardFormula)]),
674
+ // Bundle 9+232 — reward_mints: Vec<Pubkey> always empty here.
675
+ u32(0),
629
676
  ]);
630
677
  return new TransactionInstruction({
631
678
  programId: this.programId,
@@ -638,31 +685,105 @@ export class EmblemStakeClient {
638
685
  });
639
686
  }
640
687
  /**
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.
688
+ * Bundle 9+232 (2026-05-06) register a reward token stream on an
689
+ * existing pool. Inits the per-mint reward vault PDA + the stream's
690
+ * PlatformAccrual PDA, and appends `reward_mint` to
691
+ * `pool.reward_mints`. Authority-only on chain (`has_one = authority`).
692
+ *
693
+ * Used by the `createPool()` self-submitting wrapper to chain one of
694
+ * these per `initialRewardMints` entry — but exposed here so callers
695
+ * can register additional streams later.
646
696
  */
647
- buildRegisterRewardVaultIx(args) {
648
- const mint = new PublicKey(args.mint);
649
- const vaultPubkeyKey = new PublicKey(args.vaultPubkey);
697
+ buildRegisterPoolRewardMintIx(args) {
698
+ const poolKey = new PublicKey(args.poolPda);
699
+ const mintKey = new PublicKey(args.rewardMint);
650
700
  const vaultAuthority = deriveVaultAuthority(this.programId);
651
- const vault = deriveRewardVault(vaultPubkeyKey, this.programId);
701
+ const rewardVault = deriveRewardVaultForMint(poolKey, mintKey, this.programId);
702
+ const platformAccrual = derivePlatformAccrualForMint(poolKey, mintKey, this.programId);
652
703
  return new TransactionInstruction({
653
704
  programId: this.programId,
654
705
  keys: [
655
- { pubkey: new PublicKey(args.authority), isSigner: true, isWritable: true },
656
- { pubkey: mint, isSigner: false, isWritable: false },
706
+ { pubkey: new PublicKey(args.payer), isSigner: true, isWritable: true },
707
+ { pubkey: new PublicKey(args.authority), isSigner: true, isWritable: false },
708
+ { pubkey: poolKey, isSigner: false, isWritable: true },
709
+ { pubkey: mintKey, isSigner: false, isWritable: false },
657
710
  { pubkey: vaultAuthority, isSigner: false, isWritable: false },
658
- { pubkey: vault, isSigner: false, isWritable: true },
711
+ { pubkey: rewardVault, isSigner: false, isWritable: true },
712
+ { pubkey: platformAccrual, isSigner: false, isWritable: true },
659
713
  { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
660
714
  { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
661
715
  { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
662
716
  ],
663
- data: Buffer.concat([IX_REGISTER_REWARD_VAULT, vaultPubkeyKey.toBuffer()]),
717
+ data: Buffer.concat([IX_REGISTER_POOL_REWARD_MINT, mintKey.toBuffer()]),
718
+ });
719
+ }
720
+ /**
721
+ * Bundle 9+232 (2026-05-06) — admin sets the protocol fee on reward
722
+ * distributions, in basis points. Capped at
723
+ * `STAKE_PLATFORM_FEE_BPS_MAX = 3_000` (30%) by the on-chain ix.
724
+ * Mainnet ships at 0.
725
+ */
726
+ buildSetPlatformFeeBpsIx(args) {
727
+ const { platformConfigPda } = this.derivePlatformConfigPda();
728
+ const data = Buffer.concat([IX_SET_PLATFORM_FEE_BPS, u16(args.feeBps)]);
729
+ return new TransactionInstruction({
730
+ programId: this.programId,
731
+ keys: [
732
+ { pubkey: new PublicKey(args.admin), isSigner: true, isWritable: false },
733
+ {
734
+ pubkey: new PublicKey(platformConfigPda),
735
+ isSigner: false,
736
+ isWritable: true,
737
+ },
738
+ ],
739
+ data,
664
740
  });
665
741
  }
742
+ /**
743
+ * Bundle 9+232 (2026-05-06) — drain accrued platform fees from one
744
+ * reward stream's `PlatformAccrual.pending` into the admin's treasury
745
+ * ATA. Admin-signed (`has_one = admin` on `PlatformConfig`); body
746
+ * also asserts `treasury.owner == admin` so the admin can't route
747
+ * Emblem fees to a third-party wallet. Idempotent-error at
748
+ * `pending == 0` with `StakePlatformAccrualNothingPending (519)`.
749
+ */
750
+ buildEmblemClaimPlatformFeeIx(args) {
751
+ const poolKey = new PublicKey(args.poolPda);
752
+ const mintKey = new PublicKey(args.rewardMint);
753
+ const { platformConfigPda } = this.derivePlatformConfigPda();
754
+ const platformAccrual = derivePlatformAccrualForMint(poolKey, mintKey, this.programId);
755
+ const rewardVault = deriveRewardVaultForMint(poolKey, mintKey, this.programId);
756
+ const vaultAuthority = deriveVaultAuthority(this.programId);
757
+ const data = Buffer.concat([
758
+ IX_EMBLEM_CLAIM_PLATFORM_FEE,
759
+ mintKey.toBuffer(),
760
+ ]);
761
+ return new TransactionInstruction({
762
+ programId: this.programId,
763
+ keys: [
764
+ { pubkey: new PublicKey(args.admin), isSigner: true, isWritable: false },
765
+ {
766
+ pubkey: new PublicKey(platformConfigPda),
767
+ isSigner: false,
768
+ isWritable: false,
769
+ },
770
+ { pubkey: poolKey, isSigner: false, isWritable: false },
771
+ { pubkey: platformAccrual, isSigner: false, isWritable: true },
772
+ { pubkey: rewardVault, isSigner: false, isWritable: true },
773
+ {
774
+ pubkey: new PublicKey(args.treasuryTokenAccount),
775
+ isSigner: false,
776
+ isWritable: true,
777
+ },
778
+ { pubkey: vaultAuthority, isSigner: false, isWritable: false },
779
+ { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
780
+ ],
781
+ data,
782
+ });
783
+ }
784
+ // Bundle 9+232 (2026-05-06): buildRegisterRewardVaultIx deleted with
785
+ // the Tier 2 tombstone. Use buildRegisterPoolRewardMintIx for per-stream
786
+ // reward vaults instead.
666
787
  /** Build the register_pending_action_vault instruction (Tier 3 settlement bucket). */
667
788
  buildRegisterPendingActionVaultIx(args) {
668
789
  const mint = new PublicKey(args.mint);
@@ -687,17 +808,15 @@ export class EmblemStakeClient {
687
808
  ]),
688
809
  });
689
810
  }
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
811
  /**
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 })`.
812
+ * Bundle 9+232 (2026-05-06) — per-mint reward vault address. Pre-bundle
813
+ * `deriveStakerPoolRewardVault(pool)` (single-stream) and
814
+ * `deriveDistributionVaultAddress(vaultPubkey)` (Tier 2 tombstone)
815
+ * helpers are gone — see `deriveRewardVaultPda` above for the canonical
816
+ * `(pool, rewardMint)` derivation.
698
817
  */
699
- deriveDistributionVaultAddress(vaultPubkey) {
700
- return deriveRewardVault(new PublicKey(vaultPubkey), this.programId).toBase58();
818
+ deriveStakerPoolRewardVault(args) {
819
+ return deriveRewardVaultForMint(new PublicKey(args.poolPda), new PublicKey(args.rewardMint), this.programId).toBase58();
701
820
  }
702
821
  /** Address of a Tier 3 pending-action vault. */
703
822
  derivePendingActionVaultAddress(args) {
@@ -720,20 +839,27 @@ export class EmblemStakeClient {
720
839
  new PublicKey(args.owner).toBytes(),
721
840
  ], this.programId)[0].toBase58();
722
841
  }
723
- /** RewardDistribution PDA for a (pool, period_id) pair. */
842
+ /** Bundle 9+232 (2026-05-06) — RewardDistribution PDA for a
843
+ * `(pool, rewardMint, periodId)` triple. */
724
844
  deriveRewardDistributionAddress(args) {
725
845
  const periodBytes = Buffer.alloc(4);
726
846
  periodBytes.writeUInt32LE(args.periodId, 0);
727
- return PublicKey.findProgramAddressSync([Buffer.from('reward-dist'), new PublicKey(args.poolPda).toBytes(), periodBytes], this.programId)[0].toBase58();
847
+ return PublicKey.findProgramAddressSync([
848
+ Buffer.from('reward-dist'),
849
+ new PublicKey(args.poolPda).toBytes(),
850
+ new PublicKey(args.rewardMint).toBytes(),
851
+ periodBytes,
852
+ ], this.programId)[0].toBase58();
728
853
  }
729
854
  // ==========================================================================
730
855
  // Stake-flow ix builders
731
856
  // ==========================================================================
732
- /** Build the register_pool_token_accounts ix. Permissionless; one-time per pool. */
857
+ /** Build the register_pool_token_accounts ix. Permissionless;
858
+ * one-time per pool. Bundle 9+232 (2026-05-06) — principal-vault only;
859
+ * per-mint reward vaults moved to `register_pool_reward_mint`. */
733
860
  buildRegisterPoolTokenAccountsIx(args) {
734
861
  const poolKey = new PublicKey(args.poolPda);
735
862
  const principalVault = new PublicKey(this.derivePrincipalVaultAddress(args.poolPda));
736
- const rewardVault = new PublicKey(this.deriveStakerPoolRewardVault(args.poolPda));
737
863
  return new TransactionInstruction({
738
864
  programId: this.programId,
739
865
  keys: [
@@ -746,7 +872,6 @@ export class EmblemStakeClient {
746
872
  isWritable: false,
747
873
  },
748
874
  { pubkey: principalVault, isSigner: false, isWritable: true },
749
- { pubkey: rewardVault, isSigner: false, isWritable: true },
750
875
  { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
751
876
  { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
752
877
  { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
@@ -846,16 +971,24 @@ export class EmblemStakeClient {
846
971
  });
847
972
  }
848
973
  /**
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`.
974
+ * Build the claim ix. Bundle 9+232 (2026-05-06) caller specifies
975
+ * `rewardMint` (the stream to claim against). The caller must pass
976
+ * RewardDistribution PDAs for every period from
977
+ * `vault.last_claim_periods[mint_idx]` to `pool.current_periods[mint_idx] - 1`
978
+ * inclusive in `remaining_accounts`. Per-stream PDAs are seeded with
979
+ * `(pool, rewardMint, periodId)`.
852
980
  */
853
981
  buildClaimIx(args) {
854
982
  const poolKey = new PublicKey(args.poolPda);
983
+ const mintKey = new PublicKey(args.rewardMint);
855
984
  const stakeVault = new PublicKey(this.deriveStakeVaultAddress({ poolPda: args.poolPda, owner: args.user }));
856
- const rewardVault = new PublicKey(this.deriveStakerPoolRewardVault(args.poolPda));
985
+ const rewardVault = deriveRewardVaultForMint(poolKey, mintKey, this.programId);
857
986
  const distAccounts = args.periodIds.map((pid) => ({
858
- pubkey: new PublicKey(this.deriveRewardDistributionAddress({ poolPda: args.poolPda, periodId: pid })),
987
+ pubkey: new PublicKey(this.deriveRewardDistributionAddress({
988
+ poolPda: args.poolPda,
989
+ rewardMint: args.rewardMint,
990
+ periodId: pid,
991
+ })),
859
992
  isSigner: false,
860
993
  isWritable: false,
861
994
  }));
@@ -879,20 +1012,28 @@ export class EmblemStakeClient {
879
1012
  { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
880
1013
  ...distAccounts,
881
1014
  ],
882
- data: IX_CLAIM,
1015
+ // Bundle 9+232 — claim ix takes reward_mint: Pubkey arg.
1016
+ data: Buffer.concat([IX_CLAIM, mintKey.toBuffer()]),
883
1017
  });
884
1018
  }
885
1019
  /**
886
1020
  * Build the distribute_rewards ix. Permissionless trigger — anyone
887
- * pays tx fees to advance the period.
1021
+ * pays tx fees to advance the period for one stream. Bundle 9+232
1022
+ * (2026-05-06) — caller specifies `rewardMint`; ix data carries it
1023
+ * + reads `PlatformConfig.fee_bps` to bookkeep the staker-pool /
1024
+ * platform-accrual split.
888
1025
  */
889
1026
  buildDistributeRewardsIx(args) {
890
1027
  const poolKey = new PublicKey(args.poolPda);
891
- const rewardVault = new PublicKey(this.deriveStakerPoolRewardVault(args.poolPda));
1028
+ const mintKey = new PublicKey(args.rewardMint);
1029
+ const rewardVault = deriveRewardVaultForMint(poolKey, mintKey, this.programId);
892
1030
  const distribution = new PublicKey(this.deriveRewardDistributionAddress({
893
1031
  poolPda: args.poolPda,
1032
+ rewardMint: args.rewardMint,
894
1033
  periodId: args.periodId,
895
1034
  }));
1035
+ const { platformConfigPda } = this.derivePlatformConfigPda();
1036
+ const platformAccrual = derivePlatformAccrualForMint(poolKey, mintKey, this.programId);
896
1037
  return new TransactionInstruction({
897
1038
  programId: this.programId,
898
1039
  keys: [
@@ -900,9 +1041,15 @@ export class EmblemStakeClient {
900
1041
  { pubkey: poolKey, isSigner: false, isWritable: true },
901
1042
  { pubkey: rewardVault, isSigner: false, isWritable: false },
902
1043
  { pubkey: distribution, isSigner: false, isWritable: true },
1044
+ {
1045
+ pubkey: new PublicKey(platformConfigPda),
1046
+ isSigner: false,
1047
+ isWritable: false,
1048
+ },
1049
+ { pubkey: platformAccrual, isSigner: false, isWritable: true },
903
1050
  { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
904
1051
  ],
905
- data: IX_DISTRIBUTE_REWARDS,
1052
+ data: Buffer.concat([IX_DISTRIBUTE_REWARDS, mintKey.toBuffer()]),
906
1053
  });
907
1054
  }
908
1055
  // ==========================================================================
@@ -919,6 +1066,16 @@ export class EmblemStakeClient {
919
1066
  * pool, so we always include it. If it ever becomes a no-op for an
920
1067
  * existing pool, this is still safe.
921
1068
  */
1069
+ /**
1070
+ * Create a stake pool AND chain `register_pool_token_accounts` so the
1071
+ * principal vault is initialized in the same call. Bundle 9+232
1072
+ * (2026-05-06) — also chains one `register_pool_reward_mint` per
1073
+ * entry in `args.initialRewardMints`, initializing each stream's
1074
+ * reward vault + PlatformAccrual atomically with pool creation. If
1075
+ * `initialRewardMints` is empty, no reward streams are registered;
1076
+ * the pool authority can register them later via
1077
+ * `registerPoolRewardMint()`.
1078
+ */
922
1079
  async createPool(args) {
923
1080
  const signer = this.requireSigner('createPool');
924
1081
  const { poolPda } = this.derivePoolPda({
@@ -935,20 +1092,85 @@ export class EmblemStakeClient {
935
1092
  mint: args.config.asset,
936
1093
  });
937
1094
  const tx = new Transaction().add(createIx).add(registerIx);
1095
+ const initialRewardMints = args.initialRewardMints ?? [];
1096
+ for (const rewardMint of initialRewardMints) {
1097
+ tx.add(this.buildRegisterPoolRewardMintIx({
1098
+ payer: signer.publicKey,
1099
+ authority: signer.publicKey,
1100
+ poolPda,
1101
+ rewardMint,
1102
+ }));
1103
+ }
938
1104
  const txSignature = await this.signAndSubmit(tx, signer);
939
1105
  return {
940
1106
  poolPda,
941
1107
  principalVaultPda: this.derivePrincipalVaultAddress(poolPda),
942
- rewardVaultPda: this.deriveStakerPoolRewardVault(poolPda),
1108
+ rewardVaultPdas: initialRewardMints.map((rewardMint) => ({
1109
+ rewardMint,
1110
+ rewardVaultPda: this.deriveStakerPoolRewardVault({ poolPda, rewardMint }),
1111
+ })),
943
1112
  txSignature,
944
1113
  };
945
1114
  }
946
- async registerRewardVault(args) {
947
- const signer = this.requireSigner('registerRewardVault');
948
- const ix = this.buildRegisterRewardVaultIx({ ...args, authority: signer.publicKey });
1115
+ /**
1116
+ * Bundle 9+232 (2026-05-06) — register a new reward token stream on
1117
+ * an existing pool. Authority-only on chain. Inits the per-mint
1118
+ * reward vault PDA + PlatformAccrual; appends to `pool.reward_mints`.
1119
+ */
1120
+ async registerPoolRewardMint(args) {
1121
+ const signer = this.requireSigner('registerPoolRewardMint');
1122
+ const ix = this.buildRegisterPoolRewardMintIx({
1123
+ payer: signer.publicKey,
1124
+ authority: signer.publicKey,
1125
+ poolPda: args.poolPda,
1126
+ rewardMint: args.rewardMint,
1127
+ });
1128
+ const tx = new Transaction().add(ix);
1129
+ const txSignature = await this.signAndSubmit(tx, signer);
1130
+ return {
1131
+ rewardVaultPda: this.deriveStakerPoolRewardVault(args),
1132
+ accrualPda: this.derivePlatformAccrualPda(args).accrualPda,
1133
+ txSignature,
1134
+ };
1135
+ }
1136
+ /**
1137
+ * Bundle 9+232 (2026-05-06) — admin updates Emblem's protocol fee
1138
+ * on reward distributions. Capped at 30% by the on-chain ix.
1139
+ * Mainnet ships at 0.
1140
+ */
1141
+ async setPlatformFeeBps(feeBps) {
1142
+ const signer = this.requireSigner('setPlatformFeeBps');
1143
+ const ix = this.buildSetPlatformFeeBpsIx({ admin: signer.publicKey, feeBps });
949
1144
  const tx = new Transaction().add(ix);
950
1145
  const txSignature = await this.signAndSubmit(tx, signer);
951
- return { vaultPda: this.deriveDistributionVaultAddress(args.vaultPubkey), txSignature };
1146
+ return { txSignature };
1147
+ }
1148
+ /**
1149
+ * Bundle 9+232 (2026-05-06) — admin drains accrued platform fees from
1150
+ * one reward stream into a treasury ATA. Auto-derives the canonical
1151
+ * admin-owned ATA for `rewardMint` if `treasuryTokenAccount` is
1152
+ * omitted, and prepends an idempotent ATA-create ix.
1153
+ */
1154
+ async emblemClaimPlatformFee(args) {
1155
+ const signer = this.requireSigner('emblemClaimPlatformFee');
1156
+ const tokenProgramId = await this.detectTokenProgramId(args.rewardMint);
1157
+ const mintKey = new PublicKey(args.rewardMint);
1158
+ const adminKey = new PublicKey(signer.publicKey);
1159
+ const treasuryAta = args.treasuryTokenAccount ??
1160
+ getAssociatedTokenAddressSync(mintKey, adminKey, true, tokenProgramId).toBase58();
1161
+ const ataIx = createAssociatedTokenAccountIdempotentInstruction(adminKey, new PublicKey(treasuryAta), adminKey, mintKey, tokenProgramId);
1162
+ const claimIx = this.buildEmblemClaimPlatformFeeIx({
1163
+ admin: signer.publicKey,
1164
+ poolPda: args.poolPda,
1165
+ rewardMint: args.rewardMint,
1166
+ treasuryTokenAccount: treasuryAta,
1167
+ });
1168
+ const tx = new Transaction().add(ataIx).add(claimIx);
1169
+ const txSignature = await this.signAndSubmit(tx, signer);
1170
+ return {
1171
+ accrualPda: this.derivePlatformAccrualPda(args).accrualPda,
1172
+ txSignature,
1173
+ };
952
1174
  }
953
1175
  async registerPendingActionVault(args) {
954
1176
  const signer = this.requireSigner('registerPendingActionVault');
@@ -1048,9 +1270,11 @@ export class EmblemStakeClient {
1048
1270
  return { txSignature };
1049
1271
  }
1050
1272
  /**
1051
- * Claim accrued rewards for one or more periods. Auto-derives the
1052
- * user's ATA from the pool's asset mint and prepends an idempotent
1053
- * ATA-create ix (covers first-time claimers).
1273
+ * Claim accrued rewards for one stream over one or more periods.
1274
+ * Bundle 9+232 (2026-05-06) claim is per-reward-stream; caller
1275
+ * specifies which `rewardMint` to pull. Auto-derives the user's ATA
1276
+ * for `rewardMint` (NOT pool.asset — these may differ) and prepends
1277
+ * an idempotent ATA-create ix.
1054
1278
  */
1055
1279
  async claimRewards(args) {
1056
1280
  const signer = this.requireSigner('claimRewards');
@@ -1058,8 +1282,11 @@ export class EmblemStakeClient {
1058
1282
  if (!pool) {
1059
1283
  throw new EmblemStakeError(EmblemError.MissingAccount, `Pool ${args.poolPda} not found`);
1060
1284
  }
1061
- const tokenProgramId = await this.detectTokenProgramId(pool.asset);
1062
- const mintKey = new PublicKey(pool.asset);
1285
+ if (!pool.rewardMints.includes(args.rewardMint)) {
1286
+ throw new EmblemStakeError(EmblemError.StakeRewardSourceInvalid, `Pool ${args.poolPda} has not registered reward mint ${args.rewardMint}; registered: [${pool.rewardMints.join(', ')}]`);
1287
+ }
1288
+ const tokenProgramId = await this.detectTokenProgramId(args.rewardMint);
1289
+ const mintKey = new PublicKey(args.rewardMint);
1063
1290
  const ownerKey = new PublicKey(signer.publicKey);
1064
1291
  const userAta = args.userTokenAccount ??
1065
1292
  getAssociatedTokenAddressSync(mintKey, ownerKey, true, tokenProgramId).toBase58();
@@ -1067,6 +1294,7 @@ export class EmblemStakeClient {
1067
1294
  const claimIx = this.buildClaimIx({
1068
1295
  user: signer.publicKey,
1069
1296
  poolPda: args.poolPda,
1297
+ rewardMint: args.rewardMint,
1070
1298
  userTokenAccount: userAta,
1071
1299
  periodIds: args.periodIds,
1072
1300
  });
@@ -1074,11 +1302,17 @@ export class EmblemStakeClient {
1074
1302
  const txSignature = await this.signAndSubmit(tx, signer);
1075
1303
  return { txSignature };
1076
1304
  }
1305
+ /**
1306
+ * Bundle 9+232 (2026-05-06) — advance one reward stream's period
1307
+ * counter. Caller specifies `rewardMint`; the cadence floor + the
1308
+ * platform-fee bookkeeping are per-stream.
1309
+ */
1077
1310
  async distributeRewards(args) {
1078
1311
  const signer = this.requireSigner('distributeRewards');
1079
1312
  const ix = this.buildDistributeRewardsIx({
1080
1313
  payer: signer.publicKey,
1081
1314
  poolPda: args.poolPda,
1315
+ rewardMint: args.rewardMint,
1082
1316
  periodId: args.periodId,
1083
1317
  });
1084
1318
  const tx = new Transaction().add(ix);
@@ -1086,11 +1320,69 @@ export class EmblemStakeClient {
1086
1320
  return {
1087
1321
  distributionPda: this.deriveRewardDistributionAddress({
1088
1322
  poolPda: args.poolPda,
1323
+ rewardMint: args.rewardMint,
1089
1324
  periodId: args.periodId,
1090
1325
  }),
1091
1326
  txSignature,
1092
1327
  };
1093
1328
  }
1329
+ /**
1330
+ * Bundle 9+232 (2026-05-06) — discovery helper. Lists every
1331
+ * RewardDistribution PDA for one pool, optionally filtered to a
1332
+ * single reward mint. Mirrors launch SDK's `listLaunchesByCreator`
1333
+ * pattern. Sorted by `(rewardMint, periodId ASC)` for deterministic
1334
+ * pagination on the agent side.
1335
+ */
1336
+ async listDistributionsByPool(args) {
1337
+ const poolKey = new PublicKey(args.poolPda);
1338
+ // pool_pda field offset = disc(8) + bump(1) = 9.
1339
+ const POOL_FIELD_OFFSET = 9;
1340
+ const filters = [
1341
+ { memcmp: { offset: 0, bytes: bs58Encode(REWARD_DIST_DISCRIMINATOR) } },
1342
+ { memcmp: { offset: POOL_FIELD_OFFSET, bytes: poolKey.toBase58() } },
1343
+ ];
1344
+ if (args.rewardMint) {
1345
+ // reward_mint at offset disc(8) + bump(1) + pool_pda(32) + period_id(4) +
1346
+ // total_distributed(8) + weighted_stake_at_snapshot(8) + distributed_at(8) = 69.
1347
+ const REWARD_MINT_OFFSET = 69;
1348
+ filters.push({
1349
+ memcmp: { offset: REWARD_MINT_OFFSET, bytes: args.rewardMint },
1350
+ });
1351
+ }
1352
+ const accounts = await this.connection.getProgramAccounts(this.programId, {
1353
+ commitment: this.commitment,
1354
+ filters,
1355
+ });
1356
+ const views = accounts.map((a) => {
1357
+ const decoded = decodeRewardDistributionAccount(a.account.data);
1358
+ return { ...decoded, distributionPda: a.pubkey.toBase58() };
1359
+ });
1360
+ views.sort((a, b) => {
1361
+ if (a.rewardMint !== b.rewardMint) {
1362
+ return a.rewardMint < b.rewardMint ? -1 : 1;
1363
+ }
1364
+ return a.periodId - b.periodId;
1365
+ });
1366
+ return views;
1367
+ }
1368
+ /** Bundle 9+232 (2026-05-06) — read PlatformAccrual for a stream. */
1369
+ async getPlatformAccrual(args) {
1370
+ const accrualPda = this.derivePlatformAccrualPda(args).accrualPda;
1371
+ const info = await this.connection.getAccountInfo(new PublicKey(accrualPda), this.commitment);
1372
+ if (!info)
1373
+ return null;
1374
+ const decoded = decodePlatformAccrualAccount(info.data);
1375
+ return { accrualPda, pending: decoded.pending };
1376
+ }
1377
+ /** Bundle 9+232 (2026-05-06) — read PlatformConfig (singleton). */
1378
+ async getPlatformConfig() {
1379
+ const { platformConfigPda } = this.derivePlatformConfigPda();
1380
+ const info = await this.connection.getAccountInfo(new PublicKey(platformConfigPda), this.commitment);
1381
+ if (!info)
1382
+ return null;
1383
+ const decoded = decodeStakePlatformConfigAccount(info.data);
1384
+ return { platformConfigPda, ...decoded };
1385
+ }
1094
1386
  async signAndSubmit(tx, signer) {
1095
1387
  const { blockhash } = await this.connection.getLatestBlockhash(this.commitment);
1096
1388
  const payerKey = new PublicKey(signer.publicKey);