@send-fun/sdk 1.0.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/LICENSE +21 -0
- package/README.md +32 -0
- package/dist/index.cjs +12808 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +4406 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +4405 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +12749 -0
- package/dist/index.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +72 -0
- package/src/constants.ts +38 -0
- package/src/dex/generated/accounts/globalConfig.ts +189 -0
- package/src/dex/generated/accounts/index.ts +12 -0
- package/src/dex/generated/accounts/pool.ts +244 -0
- package/src/dex/generated/accounts/rewardAccrual.ts +191 -0
- package/src/dex/generated/accounts/sendDex.accounts.ts +47 -0
- package/src/dex/generated/errors/index.ts +9 -0
- package/src/dex/generated/errors/sendDex.ts +161 -0
- package/src/dex/generated/events/anchorEventCpiDiscriminator.framing.ts +28 -0
- package/src/dex/generated/events/creatorClaimEvent.ts +119 -0
- package/src/dex/generated/events/depositQuoteEvent.ts +115 -0
- package/src/dex/generated/events/index.ts +16 -0
- package/src/dex/generated/events/poolCreateEvent.ts +128 -0
- package/src/dex/generated/events/protocolClaimEvent.ts +111 -0
- package/src/dex/generated/events/sendDex.events.ts +184 -0
- package/src/dex/generated/events/tokenCreateEvent.ts +143 -0
- package/src/dex/generated/events/tradeEvent.ts +143 -0
- package/src/dex/generated/index.ts +16 -0
- package/src/dex/generated/instructions/buyExactIn.ts +815 -0
- package/src/dex/generated/instructions/buyExactOut.ts +815 -0
- package/src/dex/generated/instructions/claimCreatorFees.ts +648 -0
- package/src/dex/generated/instructions/claimProtocolFees.ts +640 -0
- package/src/dex/generated/instructions/index.ts +15 -0
- package/src/dex/generated/instructions/sellExactIn.ts +815 -0
- package/src/dex/generated/instructions/sellExactOut.ts +815 -0
- package/src/dex/generated/instructions/sendDex.instructions.ts +173 -0
- package/src/dex/generated/pdas/eventAuthority.ts +20 -0
- package/src/dex/generated/pdas/globalConfig.ts +20 -0
- package/src/dex/generated/pdas/index.ts +14 -0
- package/src/dex/generated/pdas/lpMint.ts +36 -0
- package/src/dex/generated/pdas/pool.ts +34 -0
- package/src/dex/generated/pdas/rewardAccrual.ts +37 -0
- package/src/dex/generated/pdas/wsolRewardAccrual.ts +20 -0
- package/src/dex/generated/plugins/index.ts +9 -0
- package/src/dex/generated/plugins/sendDex.ts +282 -0
- package/src/dex/generated/programs/index.ts +9 -0
- package/src/dex/generated/programs/sendDex.ts +12 -0
- package/src/dex/generated/types/index.ts +10 -0
- package/src/dex/generated/types/poolStatus.ts +38 -0
- package/src/dex/generated/types/tradeDirection.ts +38 -0
- package/src/dex/index.ts +8 -0
- package/src/dex/trade.ts +215 -0
- package/src/index.ts +8 -0
- package/src/launchpad/create.ts +116 -0
- package/src/launchpad/generated/accounts/bondingCurve.ts +273 -0
- package/src/launchpad/generated/accounts/globalConfig.ts +211 -0
- package/src/launchpad/generated/accounts/index.ts +12 -0
- package/src/launchpad/generated/accounts/rewardAccrual.ts +191 -0
- package/src/launchpad/generated/accounts/sendLaunchpad.accounts.ts +50 -0
- package/src/launchpad/generated/errors/index.ts +9 -0
- package/src/launchpad/generated/errors/sendLaunchpad.ts +175 -0
- package/src/launchpad/generated/events/anchorEventCpiDiscriminator.framing.ts +28 -0
- package/src/launchpad/generated/events/completeEvent.ts +120 -0
- package/src/launchpad/generated/events/creatorClaimEvent.ts +122 -0
- package/src/launchpad/generated/events/index.ts +17 -0
- package/src/launchpad/generated/events/migrateEvent.ts +121 -0
- package/src/launchpad/generated/events/protocolClaimEvent.ts +114 -0
- package/src/launchpad/generated/events/recycleEvent.ts +113 -0
- package/src/launchpad/generated/events/sendLaunchpad.events.ts +207 -0
- package/src/launchpad/generated/events/tokenCreateEvent.ts +156 -0
- package/src/launchpad/generated/events/tradeEvent.ts +148 -0
- package/src/launchpad/generated/index.ts +16 -0
- package/src/launchpad/generated/instructions/buyExactIn.ts +818 -0
- package/src/launchpad/generated/instructions/buyExactOut.ts +818 -0
- package/src/launchpad/generated/instructions/claimCreatorFees.ts +651 -0
- package/src/launchpad/generated/instructions/claimProtocolFees.ts +640 -0
- package/src/launchpad/generated/instructions/createToken.ts +1150 -0
- package/src/launchpad/generated/instructions/index.ts +17 -0
- package/src/launchpad/generated/instructions/migrate.ts +840 -0
- package/src/launchpad/generated/instructions/sellExactIn.ts +818 -0
- package/src/launchpad/generated/instructions/sellExactOut.ts +818 -0
- package/src/launchpad/generated/instructions/sendLaunchpad.instructions.ts +210 -0
- package/src/launchpad/generated/pdas/bondingCurve.ts +39 -0
- package/src/launchpad/generated/pdas/eventAuthority.ts +20 -0
- package/src/launchpad/generated/pdas/globalConfig.ts +20 -0
- package/src/launchpad/generated/pdas/index.ts +14 -0
- package/src/launchpad/generated/pdas/migrationAuthority.ts +20 -0
- package/src/launchpad/generated/pdas/rewardAccrual.ts +37 -0
- package/src/launchpad/generated/pdas/wsolRewardAccrual.ts +20 -0
- package/src/launchpad/generated/plugins/index.ts +9 -0
- package/src/launchpad/generated/plugins/sendLaunchpad.ts +315 -0
- package/src/launchpad/generated/programs/index.ts +9 -0
- package/src/launchpad/generated/programs/sendLaunchpad.ts +12 -0
- package/src/launchpad/generated/types/bondingCurveStatus.ts +43 -0
- package/src/launchpad/generated/types/index.ts +10 -0
- package/src/launchpad/generated/types/tradeDirection.ts +38 -0
- package/src/launchpad/index.ts +10 -0
- package/src/launchpad/migrate.ts +54 -0
- package/src/launchpad/trade.ts +247 -0
- package/src/math/amm.ts +511 -0
- package/src/math/fee-decay.ts +49 -0
- package/src/math/fees.ts +75 -0
- package/src/math/index.ts +3 -0
- package/src/math/internal.ts +16 -0
- package/src/nexus/fee-helpers.ts +49 -0
- package/src/nexus/generated/accounts/altRegistry.ts +177 -0
- package/src/nexus/generated/accounts/creatorFeeConfig.ts +200 -0
- package/src/nexus/generated/accounts/feePreset.ts +196 -0
- package/src/nexus/generated/accounts/globalConfig.ts +188 -0
- package/src/nexus/generated/accounts/index.ts +19 -0
- package/src/nexus/generated/accounts/partnerConfig.ts +209 -0
- package/src/nexus/generated/accounts/partnerMetadata.ts +216 -0
- package/src/nexus/generated/accounts/rewardState.ts +209 -0
- package/src/nexus/generated/accounts/sendNexus.accounts.ts +85 -0
- package/src/nexus/generated/accounts/stakingConfig.ts +197 -0
- package/src/nexus/generated/accounts/userRewardDebt.ts +221 -0
- package/src/nexus/generated/accounts/userStakePosition.ts +218 -0
- package/src/nexus/generated/constants/index.ts +9 -0
- package/src/nexus/generated/constants/sendNexus.ts +13 -0
- package/src/nexus/generated/errors/index.ts +9 -0
- package/src/nexus/generated/errors/sendNexus.ts +229 -0
- package/src/nexus/generated/events/anchorEventCpiDiscriminator.framing.ts +28 -0
- package/src/nexus/generated/events/claimEvent.ts +106 -0
- package/src/nexus/generated/events/index.ts +13 -0
- package/src/nexus/generated/events/sendNexus.events.ts +121 -0
- package/src/nexus/generated/events/stakeEvent.ts +106 -0
- package/src/nexus/generated/events/unstakeEvent.ts +106 -0
- package/src/nexus/generated/index.ts +17 -0
- package/src/nexus/generated/instructions/claim.ts +740 -0
- package/src/nexus/generated/instructions/createUserRewardDebt.ts +498 -0
- package/src/nexus/generated/instructions/index.ts +14 -0
- package/src/nexus/generated/instructions/sendNexus.instructions.ts +146 -0
- package/src/nexus/generated/instructions/settle.ts +517 -0
- package/src/nexus/generated/instructions/stake.ts +615 -0
- package/src/nexus/generated/instructions/unstake.ts +593 -0
- package/src/nexus/generated/pdas/altRegistry.ts +20 -0
- package/src/nexus/generated/pdas/creatorFeeConfig.ts +39 -0
- package/src/nexus/generated/pdas/defaultFeePreset.ts +37 -0
- package/src/nexus/generated/pdas/defaultPartner.ts +40 -0
- package/src/nexus/generated/pdas/defaultPartnerMetadata.ts +42 -0
- package/src/nexus/generated/pdas/eventAuthority.ts +20 -0
- package/src/nexus/generated/pdas/feePreset.ts +39 -0
- package/src/nexus/generated/pdas/globalConfig.ts +20 -0
- package/src/nexus/generated/pdas/index.ts +22 -0
- package/src/nexus/generated/pdas/partnerConfig.ts +36 -0
- package/src/nexus/generated/pdas/partnerMetadata.ts +38 -0
- package/src/nexus/generated/pdas/rewardState.ts +36 -0
- package/src/nexus/generated/pdas/stakingConfig.ts +20 -0
- package/src/nexus/generated/pdas/userRewardDebt.ts +41 -0
- package/src/nexus/generated/pdas/userStakePosition.ts +39 -0
- package/src/nexus/generated/plugins/index.ts +9 -0
- package/src/nexus/generated/plugins/sendNexus.ts +494 -0
- package/src/nexus/generated/programs/index.ts +9 -0
- package/src/nexus/generated/programs/sendNexus.ts +12 -0
- package/src/nexus/generated/types/authPlatformEntry.ts +52 -0
- package/src/nexus/generated/types/callerType.ts +38 -0
- package/src/nexus/generated/types/dexFees.ts +64 -0
- package/src/nexus/generated/types/feeConfig.ts +73 -0
- package/src/nexus/generated/types/index.ts +13 -0
- package/src/nexus/generated/types/launchpadFees.ts +63 -0
- package/src/nexus/index.ts +9 -0
- package/src/nexus/staking.ts +581 -0
- package/src/platform.ts +19 -0
- package/src/transfer-fee.ts +216 -0
- package/src/utils/chunk.ts +21 -0
- package/src/utils/creator-hash.ts +57 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/mint-info.ts +102 -0
- package/src/utils/partner.ts +5 -0
- package/src/utils/pda.ts +24 -0
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
import {
|
|
2
|
+
containsBytes,
|
|
3
|
+
fetchEncodedAccounts,
|
|
4
|
+
getAddressDecoder,
|
|
5
|
+
getBase64Encoder,
|
|
6
|
+
type Address,
|
|
7
|
+
type Instruction,
|
|
8
|
+
type Rpc,
|
|
9
|
+
type GetAccountInfoApi,
|
|
10
|
+
type GetMultipleAccountsApi,
|
|
11
|
+
type GetProgramAccountsApi,
|
|
12
|
+
type TransactionSigner,
|
|
13
|
+
} from '@solana/kit';
|
|
14
|
+
import {
|
|
15
|
+
SEND_NEXUS_PROGRAM_ADDRESS,
|
|
16
|
+
SYSTEM_PROGRAM_ADDRESS,
|
|
17
|
+
} from '../constants.js';
|
|
18
|
+
import { fetchInChunks } from '../utils/chunk.js';
|
|
19
|
+
import { findAssociatedTokenPda } from '../utils/pda.js';
|
|
20
|
+
import { fetchStakingConfig } from './generated/accounts/stakingConfig.js';
|
|
21
|
+
import {
|
|
22
|
+
fetchMaybeUserStakePosition,
|
|
23
|
+
type UserStakePosition,
|
|
24
|
+
} from './generated/accounts/userStakePosition.js';
|
|
25
|
+
import { getUserRewardDebtDecoder } from './generated/accounts/userRewardDebt.js';
|
|
26
|
+
import {
|
|
27
|
+
getRewardStateDecoder,
|
|
28
|
+
getRewardStateSize,
|
|
29
|
+
REWARD_STATE_DISCRIMINATOR,
|
|
30
|
+
} from './generated/accounts/rewardState.js';
|
|
31
|
+
import type { StakingConfig } from './generated/accounts/stakingConfig.js';
|
|
32
|
+
import { getStakeInstructionAsync } from './generated/instructions/stake.js';
|
|
33
|
+
import { getUnstakeInstructionAsync } from './generated/instructions/unstake.js';
|
|
34
|
+
import { getClaimInstructionAsync } from './generated/instructions/claim.js';
|
|
35
|
+
import { getSettleInstructionAsync } from './generated/instructions/settle.js';
|
|
36
|
+
import { getCreateUserRewardDebtInstructionAsync } from './generated/instructions/createUserRewardDebt.js';
|
|
37
|
+
import { findRewardStatePda } from './generated/pdas/rewardState.js';
|
|
38
|
+
import { findStakingConfigPda } from './generated/pdas/stakingConfig.js';
|
|
39
|
+
import { findUserRewardDebtPda } from './generated/pdas/userRewardDebt.js';
|
|
40
|
+
import { findUserStakePositionPda } from './generated/pdas/userStakePosition.js';
|
|
41
|
+
import { findRewardAccrualPda as findLaunchpadRewardAccrualPda } from '../launchpad/generated/pdas/rewardAccrual.js';
|
|
42
|
+
import { getRewardAccrualDecoder } from '../launchpad/generated/accounts/rewardAccrual.js';
|
|
43
|
+
import { findRewardAccrualPda as findDexRewardAccrualPda } from '../dex/generated/pdas/rewardAccrual.js';
|
|
44
|
+
|
|
45
|
+
// 1e12 scale of `RewardAccrual.accPerToken` on both programs; must match send_shared `PRECISION`.
|
|
46
|
+
const PRECISION = 1_000_000_000_000n;
|
|
47
|
+
|
|
48
|
+
/** `getProgramAccounts` is optional: many providers restrict it and LiteSVM lacks it, so pass `rewardMints` there. */
|
|
49
|
+
export type RewardMintRpc = Rpc<GetAccountInfoApi> &
|
|
50
|
+
Partial<Rpc<GetProgramAccountsApi>>;
|
|
51
|
+
|
|
52
|
+
function hasProgramAccounts(
|
|
53
|
+
rpc: RewardMintRpc,
|
|
54
|
+
): rpc is Rpc<GetAccountInfoApi> & Rpc<GetProgramAccountsApi> {
|
|
55
|
+
return typeof rpc.getProgramAccounts === 'function';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// On-chain body offsets + 8 for the discriminator; pinned by tests/staking.test.ts.
|
|
59
|
+
const REWARD_STATE_STAKING_CONFIG_OFFSET = 10n;
|
|
60
|
+
const REWARD_STATE_REWARD_MINT_OFFSET = 42;
|
|
61
|
+
|
|
62
|
+
function compareAddresses(a: Address, b: Address): number {
|
|
63
|
+
if (a < b) return -1;
|
|
64
|
+
if (a > b) return 1;
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Registered reward mints, sorted by address. Includes disabled mints: their accrued balance is
|
|
69
|
+
* still owed and the settle gate still counts them. Throws when the sweep disagrees with
|
|
70
|
+
* `StakingConfig.rewardCount` rather than settle a short list. */
|
|
71
|
+
export async function getRewardMints(
|
|
72
|
+
rpc: Rpc<GetAccountInfoApi> & Rpc<GetProgramAccountsApi>,
|
|
73
|
+
): Promise<readonly Address[]> {
|
|
74
|
+
const [stakingConfig] = findStakingConfigPda();
|
|
75
|
+
|
|
76
|
+
const [accounts, config] = await Promise.all([
|
|
77
|
+
rpc
|
|
78
|
+
.getProgramAccounts(SEND_NEXUS_PROGRAM_ADDRESS, {
|
|
79
|
+
encoding: 'base64',
|
|
80
|
+
filters: [
|
|
81
|
+
{ dataSize: BigInt(getRewardStateSize()) },
|
|
82
|
+
{
|
|
83
|
+
memcmp: {
|
|
84
|
+
bytes: stakingConfig,
|
|
85
|
+
encoding: 'base58',
|
|
86
|
+
offset: REWARD_STATE_STAKING_CONFIG_OFFSET,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
})
|
|
91
|
+
.send(),
|
|
92
|
+
fetchStakingConfigData(rpc),
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
const base64 = getBase64Encoder();
|
|
96
|
+
const addressDecoder = getAddressDecoder();
|
|
97
|
+
|
|
98
|
+
const mints: Address[] = [];
|
|
99
|
+
for (const { account } of accounts) {
|
|
100
|
+
const data = base64.encode(account.data[0]);
|
|
101
|
+
// Another nexus account of the same size could pass both filters.
|
|
102
|
+
if (!containsBytes(data, REWARD_STATE_DISCRIMINATOR, 0)) continue;
|
|
103
|
+
mints.push(
|
|
104
|
+
addressDecoder.decode(data, REWARD_STATE_REWARD_MINT_OFFSET),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (mints.length !== config.rewardCount) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`getProgramAccounts returned ${mints.length} RewardState accounts but StakingConfig.rewardCount is ${config.rewardCount}. Settling a short list forfeits rewards, so this refuses to guess -- retry, or pass rewardMints explicitly if the RPC restricts getProgramAccounts.`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return mints.toSorted(compareAddresses);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export type RewardMintSource = readonly Address[] | RewardMintRpc;
|
|
118
|
+
|
|
119
|
+
function isRewardMintList(
|
|
120
|
+
source: RewardMintSource,
|
|
121
|
+
): source is readonly Address[] {
|
|
122
|
+
return Array.isArray(source);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function resolveRewardMints(
|
|
126
|
+
rpc: RewardMintRpc,
|
|
127
|
+
rewardMints: readonly Address[] | undefined,
|
|
128
|
+
): Promise<readonly Address[]> {
|
|
129
|
+
if (rewardMints !== undefined) return rewardMints;
|
|
130
|
+
if (!hasProgramAccounts(rpc)) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
'This RPC has no getProgramAccounts, so the reward registry cannot be read from chain. Pass rewardMints explicitly.',
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return await getRewardMints(rpc);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface SettleParams {
|
|
139
|
+
/** `settle` is permissionless: the user does not sign. */
|
|
140
|
+
user: Address;
|
|
141
|
+
payer: TransactionSigner;
|
|
142
|
+
stakingMint: Address;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** One idempotent `settle` per mint; a partial mint list leaves `stake` and `unstake` failing `RewardsNotSettled`. */
|
|
146
|
+
export async function buildSettleInstructions(
|
|
147
|
+
source: RewardMintSource,
|
|
148
|
+
params: SettleParams,
|
|
149
|
+
): Promise<Instruction[]> {
|
|
150
|
+
const rewardMints = isRewardMintList(source)
|
|
151
|
+
? source
|
|
152
|
+
: await resolveRewardMints(source, undefined);
|
|
153
|
+
return Promise.all(
|
|
154
|
+
rewardMints.map((rewardMint) =>
|
|
155
|
+
getSettleInstructionAsync({
|
|
156
|
+
user: params.user,
|
|
157
|
+
payer: params.payer,
|
|
158
|
+
stakingMint: params.stakingMint,
|
|
159
|
+
rewardMint,
|
|
160
|
+
}),
|
|
161
|
+
),
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function fetchMissingUserRewardDebts(
|
|
166
|
+
rpc: RewardMintRpc & Rpc<GetMultipleAccountsApi>,
|
|
167
|
+
user: Address,
|
|
168
|
+
stakingMint: Address,
|
|
169
|
+
knownRewardMints?: readonly Address[],
|
|
170
|
+
): Promise<Address[]> {
|
|
171
|
+
const rewardMints = await resolveRewardMints(rpc, knownRewardMints);
|
|
172
|
+
|
|
173
|
+
const debtPdas = await Promise.all(
|
|
174
|
+
rewardMints.map(async (mint) => {
|
|
175
|
+
const [addr] = await findUserRewardDebtPda({
|
|
176
|
+
user,
|
|
177
|
+
stakingMint,
|
|
178
|
+
rewardMint: mint,
|
|
179
|
+
});
|
|
180
|
+
return addr;
|
|
181
|
+
}),
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const encodedAccounts = await fetchInChunks(debtPdas, (chunk) =>
|
|
185
|
+
fetchEncodedAccounts(rpc, chunk),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const missingMints: Address[] = [];
|
|
189
|
+
for (const [index, rewardMint] of rewardMints.entries()) {
|
|
190
|
+
if (!encodedAccounts[index].exists) {
|
|
191
|
+
missingMints.push(rewardMint);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return missingMints;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface PrepareStakingParams {
|
|
199
|
+
user: Address;
|
|
200
|
+
payer: TransactionSigner;
|
|
201
|
+
stakingMint: Address;
|
|
202
|
+
/** Must be the full registry when supplied; a subset leaves `stake` and `unstake` blocked. */
|
|
203
|
+
rewardMints?: readonly Address[];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Opens missing `UserRewardDebt`s, then settles every mint; the user never signs, so the stake or unstake after is one wallet prompt. */
|
|
207
|
+
export async function buildStakingPreflightInstructions(
|
|
208
|
+
rpc: RewardMintRpc & Rpc<GetMultipleAccountsApi>,
|
|
209
|
+
params: PrepareStakingParams,
|
|
210
|
+
): Promise<Instruction[]> {
|
|
211
|
+
// One sweep for both halves: two sweeps can disagree.
|
|
212
|
+
const rewardMints = await resolveRewardMints(rpc, params.rewardMints);
|
|
213
|
+
|
|
214
|
+
const missingMints = await fetchMissingUserRewardDebts(
|
|
215
|
+
rpc,
|
|
216
|
+
params.user,
|
|
217
|
+
params.stakingMint,
|
|
218
|
+
rewardMints,
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const createIxs = await Promise.all(
|
|
222
|
+
missingMints.map((rewardMint) =>
|
|
223
|
+
getCreateUserRewardDebtInstructionAsync({
|
|
224
|
+
user: params.user,
|
|
225
|
+
payer: params.payer,
|
|
226
|
+
stakingMint: params.stakingMint,
|
|
227
|
+
rewardMint,
|
|
228
|
+
}),
|
|
229
|
+
),
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const settleIxs = await buildSettleInstructions(rewardMints, params);
|
|
233
|
+
|
|
234
|
+
return [...createIxs, ...settleIxs];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** True once `stake` and `unstake` will pass the `settledCount == rewardCount` gate. */
|
|
238
|
+
export async function isFullySettled(
|
|
239
|
+
rpc: Rpc<GetAccountInfoApi>,
|
|
240
|
+
user: Address,
|
|
241
|
+
stakingMint: Address,
|
|
242
|
+
): Promise<boolean> {
|
|
243
|
+
const [config, position] = await Promise.all([
|
|
244
|
+
fetchStakingConfigData(rpc),
|
|
245
|
+
fetchUserStakePositionData(rpc, user, stakingMint),
|
|
246
|
+
]);
|
|
247
|
+
if (position === null) return config.rewardCount === 0;
|
|
248
|
+
return position.settledCount === config.rewardCount;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface StakeParams {
|
|
252
|
+
user: TransactionSigner;
|
|
253
|
+
payer?: TransactionSigner;
|
|
254
|
+
stakingMint: Address;
|
|
255
|
+
amount: bigint;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Fails `RewardsNotSettled` until every mint is settled at the current `stakeVersion`; run `buildStakingPreflightInstructions` first. */
|
|
259
|
+
export async function buildStakeInstruction(
|
|
260
|
+
params: StakeParams,
|
|
261
|
+
): Promise<Instruction> {
|
|
262
|
+
return getStakeInstructionAsync({
|
|
263
|
+
user: params.user,
|
|
264
|
+
payer: params.payer ?? params.user,
|
|
265
|
+
stakingMint: params.stakingMint,
|
|
266
|
+
amount: params.amount,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export interface UnstakeParams {
|
|
271
|
+
user: TransactionSigner;
|
|
272
|
+
payer?: TransactionSigner;
|
|
273
|
+
stakingMint: Address;
|
|
274
|
+
amount: bigint;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Settle immediately before via `buildStakingPreflightInstructions`: the gate passes on a stale settle,
|
|
278
|
+
* and the window since it is then paid at the post-unstake amount, forfeiting accrual. */
|
|
279
|
+
export async function buildUnstakeInstruction(
|
|
280
|
+
params: UnstakeParams,
|
|
281
|
+
): Promise<Instruction> {
|
|
282
|
+
return getUnstakeInstructionAsync({
|
|
283
|
+
user: params.user,
|
|
284
|
+
payer: params.payer ?? params.user,
|
|
285
|
+
stakingMint: params.stakingMint,
|
|
286
|
+
amount: params.amount,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export interface ClaimRewardsParams {
|
|
291
|
+
user: TransactionSigner;
|
|
292
|
+
payer?: TransactionSigner;
|
|
293
|
+
stakingMint: Address;
|
|
294
|
+
/** Defaults to every registered mint; each claim settles its own mint, so a subset needs no preflight. */
|
|
295
|
+
rewardMints?: readonly Address[];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** One `claim` per mint, preceded by `create_user_reward_debt` where the debt is missing:
|
|
299
|
+
* `Claim` requires the account to exist, so one missing debt fails the whole transaction. */
|
|
300
|
+
export async function buildClaimRewardsInstructions(
|
|
301
|
+
rpc: RewardMintRpc & Rpc<GetMultipleAccountsApi>,
|
|
302
|
+
params: ClaimRewardsParams,
|
|
303
|
+
): Promise<Instruction[]> {
|
|
304
|
+
const [stakingConfigAddr] = findStakingConfigPda();
|
|
305
|
+
const rewardMints = await resolveRewardMints(rpc, params.rewardMints);
|
|
306
|
+
if (rewardMints.length === 0) return [];
|
|
307
|
+
|
|
308
|
+
const [rewardStatePdas, debtPdas] = await Promise.all([
|
|
309
|
+
Promise.all(
|
|
310
|
+
rewardMints.map(async (rewardMint) => {
|
|
311
|
+
const [pda] = await findRewardStatePda({
|
|
312
|
+
stakingConfig: stakingConfigAddr,
|
|
313
|
+
rewardMint,
|
|
314
|
+
});
|
|
315
|
+
return pda;
|
|
316
|
+
}),
|
|
317
|
+
),
|
|
318
|
+
Promise.all(
|
|
319
|
+
rewardMints.map(async (rewardMint) => {
|
|
320
|
+
const [pda] = await findUserRewardDebtPda({
|
|
321
|
+
user: params.user.address,
|
|
322
|
+
stakingMint: params.stakingMint,
|
|
323
|
+
rewardMint,
|
|
324
|
+
});
|
|
325
|
+
return pda;
|
|
326
|
+
}),
|
|
327
|
+
),
|
|
328
|
+
]);
|
|
329
|
+
|
|
330
|
+
const fetched = await fetchInChunks(
|
|
331
|
+
[...rewardStatePdas, ...rewardMints, ...debtPdas],
|
|
332
|
+
(chunk) => fetchEncodedAccounts(rpc, chunk),
|
|
333
|
+
);
|
|
334
|
+
const rewardStateDecoder = getRewardStateDecoder();
|
|
335
|
+
const payer = params.payer ?? params.user;
|
|
336
|
+
|
|
337
|
+
const perMint = await Promise.all(
|
|
338
|
+
rewardMints.map(async (rewardMint, index) => {
|
|
339
|
+
const encodedRewardState = fetched[index];
|
|
340
|
+
if (!encodedRewardState.exists) {
|
|
341
|
+
throw new Error(`RewardState not found for mint ${rewardMint}`);
|
|
342
|
+
}
|
|
343
|
+
const { vault } = rewardStateDecoder.decode(
|
|
344
|
+
encodedRewardState.data,
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const encodedMint = fetched[rewardMints.length + index];
|
|
348
|
+
const tokenProgram = encodedMint.exists
|
|
349
|
+
? encodedMint.programAddress
|
|
350
|
+
: undefined;
|
|
351
|
+
const [destination] = await findAssociatedTokenPda(
|
|
352
|
+
params.user.address,
|
|
353
|
+
rewardMint,
|
|
354
|
+
tokenProgram,
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
const instructions: Instruction[] = [];
|
|
358
|
+
|
|
359
|
+
// Directly before its own `claim`, so slicing the list by mint keeps each pair together.
|
|
360
|
+
if (!fetched[rewardMints.length * 2 + index].exists) {
|
|
361
|
+
instructions.push(
|
|
362
|
+
await getCreateUserRewardDebtInstructionAsync({
|
|
363
|
+
user: params.user.address,
|
|
364
|
+
payer,
|
|
365
|
+
stakingMint: params.stakingMint,
|
|
366
|
+
rewardMint,
|
|
367
|
+
}),
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
instructions.push(
|
|
372
|
+
await getClaimInstructionAsync({
|
|
373
|
+
user: params.user,
|
|
374
|
+
payer,
|
|
375
|
+
stakingMint: params.stakingMint,
|
|
376
|
+
rewardMint,
|
|
377
|
+
vault,
|
|
378
|
+
destination,
|
|
379
|
+
tokenProgram,
|
|
380
|
+
}),
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
return instructions;
|
|
384
|
+
}),
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
return perMint.flat();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export interface WithdrawFeesAccounts {
|
|
391
|
+
rewardState: Address;
|
|
392
|
+
vault: Address;
|
|
393
|
+
rewardMint: Address;
|
|
394
|
+
destinationTokenAccount: Address;
|
|
395
|
+
tokenProgram: Address;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** Accounts for one mint's `withdraw_fees`; call once per mint. */
|
|
399
|
+
export async function buildWithdrawFeesAccounts(
|
|
400
|
+
rpc: Rpc<GetAccountInfoApi>,
|
|
401
|
+
params: {
|
|
402
|
+
rewardMint: Address;
|
|
403
|
+
/** Wallet, not token account: its ATA is derived. */
|
|
404
|
+
destination: Address;
|
|
405
|
+
tokenProgram?: Address;
|
|
406
|
+
},
|
|
407
|
+
): Promise<WithdrawFeesAccounts> {
|
|
408
|
+
const [stakingConfigAddr] = findStakingConfigPda();
|
|
409
|
+
|
|
410
|
+
const tokenProgram =
|
|
411
|
+
params.tokenProgram ??
|
|
412
|
+
(
|
|
413
|
+
await rpc
|
|
414
|
+
.getAccountInfo(params.rewardMint, { encoding: 'base64' })
|
|
415
|
+
.send()
|
|
416
|
+
).value?.owner;
|
|
417
|
+
|
|
418
|
+
const [[rewardState], [vault], [destinationTokenAccount]] =
|
|
419
|
+
await Promise.all([
|
|
420
|
+
findRewardStatePda({
|
|
421
|
+
stakingConfig: stakingConfigAddr,
|
|
422
|
+
rewardMint: params.rewardMint,
|
|
423
|
+
}),
|
|
424
|
+
findAssociatedTokenPda(
|
|
425
|
+
stakingConfigAddr,
|
|
426
|
+
params.rewardMint,
|
|
427
|
+
tokenProgram,
|
|
428
|
+
),
|
|
429
|
+
findAssociatedTokenPda(
|
|
430
|
+
params.destination,
|
|
431
|
+
params.rewardMint,
|
|
432
|
+
tokenProgram,
|
|
433
|
+
),
|
|
434
|
+
]);
|
|
435
|
+
|
|
436
|
+
if (tokenProgram === undefined) {
|
|
437
|
+
throw new Error(`Mint ${params.rewardMint} not found`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
rewardState,
|
|
442
|
+
vault,
|
|
443
|
+
rewardMint: params.rewardMint,
|
|
444
|
+
destinationTokenAccount,
|
|
445
|
+
tokenProgram,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export async function fetchStakingConfigData(
|
|
450
|
+
rpc: Rpc<GetAccountInfoApi>,
|
|
451
|
+
): Promise<StakingConfig> {
|
|
452
|
+
const [stakingConfigAddr] = findStakingConfigPda();
|
|
453
|
+
const config = await fetchStakingConfig(rpc, stakingConfigAddr);
|
|
454
|
+
return config.data;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export async function fetchUserStakePositionData(
|
|
458
|
+
rpc: Rpc<GetAccountInfoApi>,
|
|
459
|
+
user: Address,
|
|
460
|
+
stakingMint: Address,
|
|
461
|
+
): Promise<UserStakePosition | null> {
|
|
462
|
+
const [positionAddr] = await findUserStakePositionPda({
|
|
463
|
+
user,
|
|
464
|
+
stakingMint,
|
|
465
|
+
});
|
|
466
|
+
const maybePosition = await fetchMaybeUserStakePosition(rpc, positionAddr);
|
|
467
|
+
return maybePosition.exists ? maybePosition.data : null;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** Unbound reads as `Pubkey::default()` (the system program address), which `bind_staking_mint`
|
|
471
|
+
* can never store: it requires a token mint. */
|
|
472
|
+
export function isStakingMintBound(stakingMint: Address): boolean {
|
|
473
|
+
return stakingMint !== SYSTEM_PROGRAM_ADDRESS;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** The staking mint is immutable once bound, so this result is safe to cache. */
|
|
477
|
+
export async function fetchSendMint(
|
|
478
|
+
rpc: Rpc<GetAccountInfoApi>,
|
|
479
|
+
): Promise<Address> {
|
|
480
|
+
const { stakingMint } = await fetchStakingConfigData(rpc);
|
|
481
|
+
if (!isStakingMintBound(stakingMint)) {
|
|
482
|
+
throw new Error(
|
|
483
|
+
'Staking mint not yet bound -- nexus not fully initialized',
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
return stakingMint;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export interface PendingReward {
|
|
490
|
+
rewardMint: Address;
|
|
491
|
+
/** Base units of `rewardMint`. */
|
|
492
|
+
pending: bigint;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** One entry per reward mint, in `knownRewardMints` order, else sorted by address. Each amount is in
|
|
496
|
+
* its own mint's base units, so never sum them. */
|
|
497
|
+
export async function fetchPendingRewards(
|
|
498
|
+
rpc: RewardMintRpc & Rpc<GetMultipleAccountsApi>,
|
|
499
|
+
user: Address,
|
|
500
|
+
stakingMint: Address,
|
|
501
|
+
knownRewardMints?: readonly Address[],
|
|
502
|
+
): Promise<PendingReward[]> {
|
|
503
|
+
const rewardMints = await resolveRewardMints(rpc, knownRewardMints);
|
|
504
|
+
if (rewardMints.length === 0) return [];
|
|
505
|
+
|
|
506
|
+
const [userPositionPda] = await findUserStakePositionPda({
|
|
507
|
+
user,
|
|
508
|
+
stakingMint,
|
|
509
|
+
});
|
|
510
|
+
const maybePosition = await fetchMaybeUserStakePosition(
|
|
511
|
+
rpc,
|
|
512
|
+
userPositionPda,
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
if (!maybePosition.exists) {
|
|
516
|
+
return rewardMints.map((rewardMint) => ({ rewardMint, pending: 0n }));
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const stakeAmount = maybePosition.data.amount;
|
|
520
|
+
|
|
521
|
+
const pdas = await Promise.all(
|
|
522
|
+
rewardMints.map(async (rewardMint) => {
|
|
523
|
+
const [[debtPda], [launchpadAccrualPda], [dexAccrualPda]] =
|
|
524
|
+
await Promise.all([
|
|
525
|
+
findUserRewardDebtPda({ user, stakingMint, rewardMint }),
|
|
526
|
+
findLaunchpadRewardAccrualPda({ quoteMint: rewardMint }),
|
|
527
|
+
findDexRewardAccrualPda({ quoteMint: rewardMint }),
|
|
528
|
+
]);
|
|
529
|
+
return { rewardMint, debtPda, launchpadAccrualPda, dexAccrualPda };
|
|
530
|
+
}),
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
const encodedAccounts = await fetchInChunks(
|
|
534
|
+
pdas.flatMap((p) => [
|
|
535
|
+
p.debtPda,
|
|
536
|
+
p.launchpadAccrualPda,
|
|
537
|
+
p.dexAccrualPda,
|
|
538
|
+
]),
|
|
539
|
+
(chunk) => fetchEncodedAccounts(rpc, chunk),
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
const userRewardDebtDecoder = getUserRewardDebtDecoder();
|
|
543
|
+
// One decoder for both: the two `RewardAccrual` types share shape and discriminator.
|
|
544
|
+
const rewardAccrualDecoder = getRewardAccrualDecoder();
|
|
545
|
+
|
|
546
|
+
return pdas.map(({ rewardMint }, index) => {
|
|
547
|
+
const base = index * 3;
|
|
548
|
+
|
|
549
|
+
const encodedDebt = encodedAccounts[base];
|
|
550
|
+
// A missing debt is not zero owed: `create_user_reward_debt` opens it at
|
|
551
|
+
// `accSnapshot = 0`, `amountSnapshot = position.amount`, and the claim pays that.
|
|
552
|
+
const debt = encodedDebt.exists
|
|
553
|
+
? userRewardDebtDecoder.decode(encodedDebt.data)
|
|
554
|
+
: { owed: 0n, accSnapshot: 0n, amountSnapshot: stakeAmount };
|
|
555
|
+
|
|
556
|
+
const encodedLaunchpadAccrual = encodedAccounts[base + 1];
|
|
557
|
+
const launchpadAcc = encodedLaunchpadAccrual.exists
|
|
558
|
+
? rewardAccrualDecoder.decode(encodedLaunchpadAccrual.data)
|
|
559
|
+
.accPerToken
|
|
560
|
+
: 0n;
|
|
561
|
+
|
|
562
|
+
const encodedDexAccrual = encodedAccounts[base + 2];
|
|
563
|
+
const dexAcc = encodedDexAccrual.exists
|
|
564
|
+
? rewardAccrualDecoder.decode(encodedDexAccrual.data).accPerToken
|
|
565
|
+
: 0n;
|
|
566
|
+
|
|
567
|
+
// Mirrors `settle_debt`: basis is the smaller amount (`amountSnapshot` alone over-quotes
|
|
568
|
+
// after an unstake), floored once over the summed accumulators. A negative delta quotes 0
|
|
569
|
+
// where the program would fail.
|
|
570
|
+
const basis =
|
|
571
|
+
debt.amountSnapshot < stakeAmount
|
|
572
|
+
? debt.amountSnapshot
|
|
573
|
+
: stakeAmount;
|
|
574
|
+
const combined = launchpadAcc + dexAcc;
|
|
575
|
+
const delta =
|
|
576
|
+
combined > debt.accSnapshot ? combined - debt.accSnapshot : 0n;
|
|
577
|
+
const accrued = (basis * delta) / PRECISION;
|
|
578
|
+
|
|
579
|
+
return { rewardMint, pending: debt.owed + accrued };
|
|
580
|
+
});
|
|
581
|
+
}
|
package/src/platform.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { getProgramDerivedAddress } from '@solana/kit';
|
|
2
|
+
import type { ProgramDerivedAddress } from '@solana/kit';
|
|
3
|
+
import { SEND_NEXUS_PROGRAM_ADDRESS } from './nexus/generated/programs/index.js';
|
|
4
|
+
|
|
5
|
+
/** A platform is only a key, with no on-chain account; it is a PDA so a platform account can later `init` at it. */
|
|
6
|
+
export const PLATFORM_SEED = 'platform';
|
|
7
|
+
|
|
8
|
+
/** The name is the platform's identity: renaming one orphans every market and partner config under the old key. */
|
|
9
|
+
export async function findPlatformAddress(
|
|
10
|
+
name: string,
|
|
11
|
+
): Promise<ProgramDerivedAddress> {
|
|
12
|
+
return await getProgramDerivedAddress({
|
|
13
|
+
programAddress: SEND_NEXUS_PROGRAM_ADDRESS,
|
|
14
|
+
seeds: [
|
|
15
|
+
new TextEncoder().encode(PLATFORM_SEED),
|
|
16
|
+
new TextEncoder().encode(name),
|
|
17
|
+
],
|
|
18
|
+
});
|
|
19
|
+
}
|