@emblemvault/primitives-stake 0.5.1 → 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()]),
713
+ });
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,
664
735
  });
665
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,9 +1036,15 @@ 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
  // ==========================================================================
@@ -919,6 +1061,16 @@ export class EmblemStakeClient {
919
1061
  * pool, so we always include it. If it ever becomes a no-op for an
920
1062
  * existing pool, this is still safe.
921
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
+ */
922
1074
  async createPool(args) {
923
1075
  const signer = this.requireSigner('createPool');
924
1076
  const { poolPda } = this.derivePoolPda({
@@ -935,20 +1087,85 @@ export class EmblemStakeClient {
935
1087
  mint: args.config.asset,
936
1088
  });
937
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
+ }
938
1099
  const txSignature = await this.signAndSubmit(tx, signer);
939
1100
  return {
940
1101
  poolPda,
941
1102
  principalVaultPda: this.derivePrincipalVaultAddress(poolPda),
942
- rewardVaultPda: this.deriveStakerPoolRewardVault(poolPda),
1103
+ rewardVaultPdas: initialRewardMints.map((rewardMint) => ({
1104
+ rewardMint,
1105
+ rewardVaultPda: this.deriveStakerPoolRewardVault({ poolPda, rewardMint }),
1106
+ })),
943
1107
  txSignature,
944
1108
  };
945
1109
  }
946
- async registerRewardVault(args) {
947
- const signer = this.requireSigner('registerRewardVault');
948
- const ix = this.buildRegisterRewardVaultIx({ ...args, authority: signer.publicKey });
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
+ };
1130
+ }
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 });
949
1139
  const tx = new Transaction().add(ix);
950
1140
  const txSignature = await this.signAndSubmit(tx, signer);
951
- 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
+ };
952
1169
  }
953
1170
  async registerPendingActionVault(args) {
954
1171
  const signer = this.requireSigner('registerPendingActionVault');
@@ -1048,9 +1265,11 @@ export class EmblemStakeClient {
1048
1265
  return { txSignature };
1049
1266
  }
1050
1267
  /**
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).
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.
1054
1273
  */
1055
1274
  async claimRewards(args) {
1056
1275
  const signer = this.requireSigner('claimRewards');
@@ -1058,8 +1277,11 @@ export class EmblemStakeClient {
1058
1277
  if (!pool) {
1059
1278
  throw new EmblemStakeError(EmblemError.MissingAccount, `Pool ${args.poolPda} not found`);
1060
1279
  }
1061
- const tokenProgramId = await this.detectTokenProgramId(pool.asset);
1062
- 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);
1063
1285
  const ownerKey = new PublicKey(signer.publicKey);
1064
1286
  const userAta = args.userTokenAccount ??
1065
1287
  getAssociatedTokenAddressSync(mintKey, ownerKey, true, tokenProgramId).toBase58();
@@ -1067,6 +1289,7 @@ export class EmblemStakeClient {
1067
1289
  const claimIx = this.buildClaimIx({
1068
1290
  user: signer.publicKey,
1069
1291
  poolPda: args.poolPda,
1292
+ rewardMint: args.rewardMint,
1070
1293
  userTokenAccount: userAta,
1071
1294
  periodIds: args.periodIds,
1072
1295
  });
@@ -1074,11 +1297,17 @@ export class EmblemStakeClient {
1074
1297
  const txSignature = await this.signAndSubmit(tx, signer);
1075
1298
  return { txSignature };
1076
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
+ */
1077
1305
  async distributeRewards(args) {
1078
1306
  const signer = this.requireSigner('distributeRewards');
1079
1307
  const ix = this.buildDistributeRewardsIx({
1080
1308
  payer: signer.publicKey,
1081
1309
  poolPda: args.poolPda,
1310
+ rewardMint: args.rewardMint,
1082
1311
  periodId: args.periodId,
1083
1312
  });
1084
1313
  const tx = new Transaction().add(ix);
@@ -1086,11 +1315,69 @@ export class EmblemStakeClient {
1086
1315
  return {
1087
1316
  distributionPda: this.deriveRewardDistributionAddress({
1088
1317
  poolPda: args.poolPda,
1318
+ rewardMint: args.rewardMint,
1089
1319
  periodId: args.periodId,
1090
1320
  }),
1091
1321
  txSignature,
1092
1322
  };
1093
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
+ }
1094
1381
  async signAndSubmit(tx, signer) {
1095
1382
  const { blockhash } = await this.connection.getLatestBlockhash(this.commitment);
1096
1383
  const payerKey = new PublicKey(signer.publicKey);