@forgedevv/core 0.1.0 → 0.3.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/CHANGELOG.md +227 -4
- package/README.md +4 -3
- package/dist/addresses.d.ts +73 -7
- package/dist/addresses.js +96 -5
- package/dist/claims.d.ts +43 -6
- package/dist/claims.js +75 -16
- package/dist/config.d.ts +48 -37
- package/dist/config.js +282 -52
- package/dist/constants.d.ts +16 -0
- package/dist/constants.js +20 -0
- package/dist/index.d.ts +9 -5
- package/dist/index.js +15 -4
- package/dist/jupiter-referral.d.ts +334 -0
- package/dist/jupiter-referral.js +666 -0
- package/dist/launch-metadata.d.ts +5 -1
- package/dist/launch-metadata.js +2 -0
- package/dist/meteora-referral.d.ts +119 -0
- package/dist/meteora-referral.js +188 -0
- package/dist/payments.d.ts +17 -0
- package/dist/payments.js +20 -2
- package/dist/pool.d.ts +108 -17
- package/dist/pool.js +237 -60
- package/dist/position-claims.d.ts +215 -0
- package/dist/position-claims.js +457 -0
- package/dist/priority-fee.d.ts +202 -0
- package/dist/priority-fee.js +322 -0
- package/dist/reads.d.ts +20 -0
- package/dist/reads.js +39 -2
- package/dist/settings.d.ts +73 -0
- package/dist/settings.js +119 -0
- package/dist/swap.d.ts +95 -23
- package/dist/swap.js +144 -30
- package/dist/tx.d.ts +2 -0
- package/dist/tx.js +4 -2
- package/dist/validate.d.ts +14 -1
- package/dist/validate.js +86 -6
- package/package.json +3 -2
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { PublicKey, Transaction, type Connection } from '@solana/web3.js';
|
|
2
|
+
import { type PoolState as DammV2PoolState, type PositionState as DammV2PositionState } from '@meteora-ag/cp-amm-sdk';
|
|
3
|
+
import { type AddressEnv } from './addresses.js';
|
|
4
|
+
import { type ClaimOptions } from './claims.js';
|
|
5
|
+
import { type PriorityFeeQuote } from './priority-fee.js';
|
|
6
|
+
import { type DbcPoolState, type ReadOptions, type TokenAmounts } from './reads.js';
|
|
7
|
+
export type { DammV2PoolState, DammV2PositionState };
|
|
8
|
+
/** DAMM v2 (cp-amm) program id, from the pinned SDK. */
|
|
9
|
+
export declare const DAMM_V2_PROGRAM_ID: PublicKey;
|
|
10
|
+
/** Anchor discriminator of `claim_position_fee` (sha256("global:claim_position_fee")[0..8]). */
|
|
11
|
+
export declare const CLAIM_POSITION_FEE_DISCRIMINATOR: Buffer<ArrayBuffer>;
|
|
12
|
+
/** Account indexes of `claim_position_fee` (cp-amm SDK 1.5.1 IDL). */
|
|
13
|
+
export declare const CLAIM_POSITION_FEE_ACCOUNTS: {
|
|
14
|
+
readonly poolAuthority: 0;
|
|
15
|
+
readonly pool: 1;
|
|
16
|
+
readonly position: 2;
|
|
17
|
+
readonly tokenAAccount: 3;
|
|
18
|
+
readonly tokenBAccount: 4;
|
|
19
|
+
readonly tokenAVault: 5;
|
|
20
|
+
readonly tokenBVault: 6;
|
|
21
|
+
readonly tokenAMint: 7;
|
|
22
|
+
readonly tokenBMint: 8;
|
|
23
|
+
readonly positionNftAccount: 9;
|
|
24
|
+
readonly owner: 10;
|
|
25
|
+
readonly tokenAProgram: 11;
|
|
26
|
+
readonly tokenBProgram: 12;
|
|
27
|
+
readonly eventAuthority: 13;
|
|
28
|
+
readonly program: 14;
|
|
29
|
+
};
|
|
30
|
+
/** `tokenAFlag` / `tokenBFlag` of a DAMM v2 pool: 0 = SPL Token, 1 = Token-2022. */
|
|
31
|
+
export declare function dammV2TokenProgram(flag: number): PublicKey;
|
|
32
|
+
export interface MigratedDammV2Pool {
|
|
33
|
+
/** The DAMM v2 pool the DBC pool migrated to. */
|
|
34
|
+
dammPool: PublicKey;
|
|
35
|
+
/** The DAMM v2 config of the migration fee option (`DAMM_V2_MIGRATION_FEE_ADDRESS`). */
|
|
36
|
+
dammConfig: PublicKey;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Pure: the DAMM v2 pool of a migrated DBC pool, derived like the SDK's `migrateToDammV2`
|
|
40
|
+
* (`deriveDammV2PoolAddress(dammConfig, baseMint, quoteMint)` with the DAMM v2 config the SDK
|
|
41
|
+
* pins for the config's `migrationFeeOption`, `DAMM_V2_MIGRATION_FEE_ADDRESS`). Null when the
|
|
42
|
+
* pool has not migrated yet, migrates to DAMM v1, or has a migration fee option the pinned SDK
|
|
43
|
+
* does not know (FORGE configs use `FixedBps25`, D27).
|
|
44
|
+
*/
|
|
45
|
+
export declare function migratedDammV2Pool(state: Pick<DbcPoolState, 'virtualPool' | 'config'>): MigratedDammV2Pool | null;
|
|
46
|
+
/** A DAMM v2 position: the account, the NFT that owns it and the NFT's token account. */
|
|
47
|
+
export interface DammV2PositionRef {
|
|
48
|
+
position: PublicKey;
|
|
49
|
+
nftMint: PublicKey;
|
|
50
|
+
positionNftAccount: PublicKey;
|
|
51
|
+
}
|
|
52
|
+
export interface DammV2PositionSummary extends DammV2PositionRef {
|
|
53
|
+
/** DAMM v2 pool of the position. */
|
|
54
|
+
dammPool: PublicKey;
|
|
55
|
+
liquidity: {
|
|
56
|
+
unlocked: bigint;
|
|
57
|
+
vested: bigint;
|
|
58
|
+
permanentLocked: bigint;
|
|
59
|
+
};
|
|
60
|
+
/** Fees claimable now: `base` = token A (the coin), `quote` = token B (SOL), raw units. */
|
|
61
|
+
pendingFees: TokenAmounts;
|
|
62
|
+
}
|
|
63
|
+
/** Pure: unclaimed fees of a position (SDK `getUnClaimLpFee`), as bigint. */
|
|
64
|
+
export declare function positionPendingFees(poolState: DammV2PoolState, positionState: DammV2PositionState): TokenAmounts;
|
|
65
|
+
/** Pure: summary of one position of `poolState`. */
|
|
66
|
+
export declare function summarizePosition(poolState: DammV2PoolState, ref: DammV2PositionRef, positionState: DammV2PositionState): DammV2PositionSummary;
|
|
67
|
+
/** Pure: sum of the pending fees of `positions`. */
|
|
68
|
+
export declare function sumPendingFees(positions: readonly DammV2PositionSummary[]): TokenAmounts;
|
|
69
|
+
/** Fetches a DAMM v2 pool. @throws ClaimError `damm_pool_not_found` when the account is missing. */
|
|
70
|
+
export declare function getDammV2PoolState(connection: Connection, dammPool: PublicKey, options?: ReadOptions): Promise<DammV2PoolState>;
|
|
71
|
+
/**
|
|
72
|
+
* The migrated DAMM v2 pool of a DBC pool and the creator's positions on it, with their fees.
|
|
73
|
+
* Null when the pool has not migrated (see `migratedDammV2Pool`).
|
|
74
|
+
*/
|
|
75
|
+
export interface CreatorDammV2Positions {
|
|
76
|
+
/** The DBC pool. */
|
|
77
|
+
pool: PublicKey;
|
|
78
|
+
dammPool: PublicKey;
|
|
79
|
+
dammConfig: PublicKey;
|
|
80
|
+
creator: PublicKey;
|
|
81
|
+
/** Positions whose NFT the creator holds, sorted by address. */
|
|
82
|
+
positions: DammV2PositionSummary[];
|
|
83
|
+
/** Sum of the positions' pending fees. */
|
|
84
|
+
claimable: TokenAmounts;
|
|
85
|
+
}
|
|
86
|
+
export interface FindCreatorPositionsParams {
|
|
87
|
+
/** The DBC pool, or its already-read state. */
|
|
88
|
+
pool: PublicKey | DbcPoolState;
|
|
89
|
+
/** The pool creator (must match the pool state). */
|
|
90
|
+
creator: PublicKey;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Finds the creator's positions on the migrated DAMM v2 pool of a DBC pool (SDK
|
|
94
|
+
* `getUserPositionByPool`: the Token-2022 accounts of the creator holding one NFT, then the
|
|
95
|
+
* position of each), checked again (pool, NFT held by the creator) and sorted by address.
|
|
96
|
+
* Returns null when the pool has not migrated to DAMM v2.
|
|
97
|
+
* @throws ClaimError `not_pool_creator`, `damm_pool_not_found`, `damm_pool_mismatch`.
|
|
98
|
+
* @throws DbcReadError when the DBC pool or its config does not exist.
|
|
99
|
+
*/
|
|
100
|
+
export declare function findCreatorDammV2Positions(connection: Connection, params: FindCreatorPositionsParams, options?: ReadOptions): Promise<CreatorDammV2Positions | null>;
|
|
101
|
+
/**
|
|
102
|
+
* Compute unit limit of a position fee claim with a priority fee: two idempotent token account
|
|
103
|
+
* creations and a close (about 60,000 CU), plus about 40,000 CU per `claim_position_fee`, doubled.
|
|
104
|
+
*/
|
|
105
|
+
export declare function positionClaimComputeUnits(positions: number): number;
|
|
106
|
+
/** Lamport costs of a position fee claim, as bigint. */
|
|
107
|
+
export interface PositionClaimCosts {
|
|
108
|
+
/** Signature fees (signers x 5000 lamports) plus the priority fee, if any; paid by the payer. */
|
|
109
|
+
networkFeeLamports: bigint;
|
|
110
|
+
/**
|
|
111
|
+
* Rent of the receiver's coin account, created on the first claim of a pool (paid by the
|
|
112
|
+
* payer, kept in that account, which the receiver owns). 0 when it already exists.
|
|
113
|
+
*/
|
|
114
|
+
receiverTokenAccountRentLamports: bigint;
|
|
115
|
+
/**
|
|
116
|
+
* Rent of the temporary WSOL account the payer funds: closed to the receiver with the fees, it
|
|
117
|
+
* reaches the receiver as lamports (not refunded to the payer). 0 when the account exists.
|
|
118
|
+
*/
|
|
119
|
+
unwrapAccountRentLamports: bigint;
|
|
120
|
+
/** Everything the payer's balance decreases by: the three amounts above. */
|
|
121
|
+
payerTotalLamports: bigint;
|
|
122
|
+
/** Balance the payer needs before signing (same as the total: nothing is refunded). */
|
|
123
|
+
payerRequiredBalanceLamports: bigint;
|
|
124
|
+
}
|
|
125
|
+
export interface PositionClaimSummary {
|
|
126
|
+
kind: 'position';
|
|
127
|
+
/** The DBC pool. */
|
|
128
|
+
pool: PublicKey;
|
|
129
|
+
dammPool: PublicKey;
|
|
130
|
+
dammConfig: PublicKey;
|
|
131
|
+
baseMint: PublicKey;
|
|
132
|
+
/** The creator: owner of the positions, signs the claim. */
|
|
133
|
+
claimer: PublicKey;
|
|
134
|
+
receiver: PublicKey;
|
|
135
|
+
/** Fee payer of the transaction. */
|
|
136
|
+
payer: PublicKey;
|
|
137
|
+
/** The positions claimed (sorted by address), with their fees when the claim was built. */
|
|
138
|
+
positions: DammV2PositionSummary[];
|
|
139
|
+
/** Sum of the positions' pending fees when the claim was built (a lower bound at execution). */
|
|
140
|
+
claimable: TokenAmounts;
|
|
141
|
+
/**
|
|
142
|
+
* SOL the receiver gets from the transaction, lamports: the claimable SOL fees plus the
|
|
143
|
+
* lamports of the temporary WSOL account closed to it (its rent, and the WSOL it already held).
|
|
144
|
+
* A lower bound: the claim is uncapped and fees accrue until it executes. Never exact
|
|
145
|
+
* (`quoteIsExact` is always false).
|
|
146
|
+
*/
|
|
147
|
+
receiverQuoteLamports: bigint;
|
|
148
|
+
quoteIsExact: false;
|
|
149
|
+
/**
|
|
150
|
+
* WSOL the creator's temporary account already held when the transaction was built (anyone
|
|
151
|
+
* can send some to it): it reaches the receiver with the close, counted in
|
|
152
|
+
* `receiverQuoteLamports`. Zero when the account is empty or new.
|
|
153
|
+
*/
|
|
154
|
+
existingWsolLamports: bigint;
|
|
155
|
+
/** Zero costs when there is no transaction. */
|
|
156
|
+
costs: PositionClaimCosts;
|
|
157
|
+
/** `receiverQuoteLamports - costs.payerTotalLamports`. Coin fees are not valued. */
|
|
158
|
+
netLamports: bigint;
|
|
159
|
+
/** The priority fee of the transaction (price, fee, cap), or null without one. */
|
|
160
|
+
priorityFee: PriorityFeeQuote | null;
|
|
161
|
+
}
|
|
162
|
+
export type PositionClaimSkipReason = 'no_position' | 'nothing_to_claim' | 'not_worthwhile';
|
|
163
|
+
export interface PositionClaimTxResult {
|
|
164
|
+
/** Unsigned claim transaction, or `null` when skipped (see `skipReason`). */
|
|
165
|
+
transaction: Transaction | null;
|
|
166
|
+
lastValidBlockHeight: number | null;
|
|
167
|
+
skipReason: PositionClaimSkipReason | null;
|
|
168
|
+
summary: PositionClaimSummary;
|
|
169
|
+
}
|
|
170
|
+
export interface CreatorPositionClaimParams {
|
|
171
|
+
/** The DBC pool (the DAMM v2 pool is derived from it). */
|
|
172
|
+
pool: PublicKey;
|
|
173
|
+
/** Pool creator and owner of the positions; signs the claim. */
|
|
174
|
+
creator: PublicKey;
|
|
175
|
+
/** Where the fees go. */
|
|
176
|
+
receiver: PublicKey;
|
|
177
|
+
/** When set, `receiver` must equal it (`receiver_mismatch` otherwise). */
|
|
178
|
+
expectedReceiver?: PublicKey;
|
|
179
|
+
/** Fee payer; defaults to the creator. */
|
|
180
|
+
payer?: PublicKey;
|
|
181
|
+
/**
|
|
182
|
+
* Positions to claim (addresses). Each must belong to the migrated pool and be held by the
|
|
183
|
+
* creator. Default: every position of the creator on that pool.
|
|
184
|
+
*/
|
|
185
|
+
positions?: readonly PublicKey[];
|
|
186
|
+
}
|
|
187
|
+
/** True when the claim collects more SOL than it costs, by more than `minNetLamports`. */
|
|
188
|
+
export declare function isPositionClaimWorthwhile(summary: PositionClaimSummary, minNetLamports?: bigint): boolean;
|
|
189
|
+
/**
|
|
190
|
+
* Builds the claim of the creator's DAMM v2 position fees of a migrated DBC pool to `receiver`
|
|
191
|
+
* (see the module comment for the transaction).
|
|
192
|
+
* @throws ClaimError when `creator` is not the pool creator, the pool is not SOL-quoted or not
|
|
193
|
+
* migrated to DAMM v2 (`not_migrated`), the receiver is invalid or differs from
|
|
194
|
+
* `expectedReceiver`, a position is missing, on another pool or not held by the creator, or
|
|
195
|
+
* the creator's WSOL account holds WSOL while the receiver is someone else.
|
|
196
|
+
* @throws DbcReadError when the DBC pool or its config does not exist.
|
|
197
|
+
*/
|
|
198
|
+
export declare function buildCreatorPositionFeeClaimTx(connection: Connection, params: CreatorPositionClaimParams, options?: ClaimOptions): Promise<PositionClaimTxResult>;
|
|
199
|
+
export interface ForgeCreatorPositionClaimParams {
|
|
200
|
+
pool: PublicKey;
|
|
201
|
+
/** FORGE creator wallet of the launchpad (the pool creator); signs the claim. */
|
|
202
|
+
creator: PublicKey;
|
|
203
|
+
/** Fee payer; defaults to the creator. */
|
|
204
|
+
payer?: PublicKey;
|
|
205
|
+
positions?: readonly PublicKey[];
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* The position fee claim of a FORGE creator wallet: the receiver is always the FORGE multisig
|
|
209
|
+
* vault (`FORGE_MULTISIG_VAULT`, SECURITY.md §5), read from `env` (defaults to `process.env`).
|
|
210
|
+
* @throws ForgeAddressError when `FORGE_MULTISIG_VAULT` is missing or invalid; as
|
|
211
|
+
* `buildCreatorPositionFeeClaimTx` otherwise.
|
|
212
|
+
*/
|
|
213
|
+
export declare function buildForgeCreatorPositionFeeClaimTx(connection: Connection, params: ForgeCreatorPositionClaimParams, options?: ClaimOptions & {
|
|
214
|
+
env?: AddressEnv;
|
|
215
|
+
}): Promise<PositionClaimTxResult>;
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DAMM v2 position fee claims of a migrated DBC pool (docs/METEORA.md "Migration", DECISIONS.md
|
|
3
|
+
* D6 and D27, devnet test 2).
|
|
4
|
+
*
|
|
5
|
+
* When a launchpad coin graduates, `migrateToDammV2` creates the DAMM v2 pool and two locked
|
|
6
|
+
* positions: the partner's and the creator's. The creator's position (a FORGE creator wallet for
|
|
7
|
+
* a launchpad coin, permanently locked) keeps earning the migrated pool's trading fees; nothing
|
|
8
|
+
* but a `claim_position_fee` signed by the position owner moves them. This module:
|
|
9
|
+
* - finds the migrated DAMM v2 pool of a DBC pool (pure, from the decoded DBC state) and the
|
|
10
|
+
* creator's position(s) on it (the position NFT is held by the creator);
|
|
11
|
+
* - builds the UNSIGNED claim of their fees to a receiver (`@meteora-ag/cp-amm-sdk`, pinned
|
|
12
|
+
* 1.5.1), the creator signing, with an optional fee payer; shaped like `buildCreatorClaimTx`:
|
|
13
|
+
* transaction, summary, costs, skip reasons.
|
|
14
|
+
*
|
|
15
|
+
* Transaction (SOL-quoted pools only):
|
|
16
|
+
* 1. create-idempotent the receiver's coin account (the payer funds its rent, once per pool);
|
|
17
|
+
* 2. create-idempotent the creator's WSOL associated account (temporary, rent from the payer);
|
|
18
|
+
* 3. one `claim_position_fee` per position: coin fees to the receiver's coin account, SOL fees
|
|
19
|
+
* to the creator's temporary WSOL account;
|
|
20
|
+
* 4. close the temporary WSOL account to the receiver: the fees AND the account's rent reach
|
|
21
|
+
* the receiver as lamports. `claim_position_fee` has no amount cap and fees keep accruing
|
|
22
|
+
* until the transaction executes, so the exact forward of the DBC claim is not possible
|
|
23
|
+
* here: the rent (~0.002 SOL) moves from the payer to the receiver with every claim.
|
|
24
|
+
* The transaction never removes liquidity, never closes, locks, splits or transfers the position
|
|
25
|
+
* or its NFT, and contains no other program. With `ClaimOptions.priority`, two compute budget
|
|
26
|
+
* instructions come first (`setComputeUnitLimit` of `positionClaimComputeUnits`, then
|
|
27
|
+
* `setComputeUnitPrice`, bounded: priority-fee.ts), counted in the network fee.
|
|
28
|
+
*/
|
|
29
|
+
import { ACCOUNT_SIZE, NATIVE_MINT, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, createAssociatedTokenAccountIdempotentInstruction, createCloseAccountInstruction, getAssociatedTokenAddressSync, unpackAccount, } from '@solana/spl-token';
|
|
30
|
+
import { PublicKey, Transaction, } from '@solana/web3.js';
|
|
31
|
+
import { DAMM_V2_MIGRATION_FEE_ADDRESS, MigrationOption, deriveDammV2PoolAddress, } from '@meteora-ag/dynamic-bonding-curve-sdk';
|
|
32
|
+
import { CP_AMM_PROGRAM_ID, CpAmm, derivePositionNftAccount, getUnClaimLpFee, } from '@meteora-ag/cp-amm-sdk';
|
|
33
|
+
import { getMultisigVault } from './addresses.js';
|
|
34
|
+
import { CLAIM_LAMPORTS_PER_SIGNATURE, ClaimError, assertClaimPriority, assertClaimReceiver, assertSolQuotedPool, claimPriorityFee, } from './claims.js';
|
|
35
|
+
import { MAX_TRANSACTION_COMPUTE_UNITS } from './priority-fee.js';
|
|
36
|
+
import { READ_COMMITMENT, getDbcPoolState, graduationStatus, retryRpc, toBigint, } from './reads.js';
|
|
37
|
+
/** DAMM v2 (cp-amm) program id, from the pinned SDK. */
|
|
38
|
+
export const DAMM_V2_PROGRAM_ID = CP_AMM_PROGRAM_ID;
|
|
39
|
+
/** Anchor discriminator of `claim_position_fee` (sha256("global:claim_position_fee")[0..8]). */
|
|
40
|
+
export const CLAIM_POSITION_FEE_DISCRIMINATOR = Buffer.from([180, 38, 154, 17, 133, 33, 162, 211]);
|
|
41
|
+
/** Account indexes of `claim_position_fee` (cp-amm SDK 1.5.1 IDL). */
|
|
42
|
+
export const CLAIM_POSITION_FEE_ACCOUNTS = {
|
|
43
|
+
poolAuthority: 0,
|
|
44
|
+
pool: 1,
|
|
45
|
+
position: 2,
|
|
46
|
+
tokenAAccount: 3,
|
|
47
|
+
tokenBAccount: 4,
|
|
48
|
+
tokenAVault: 5,
|
|
49
|
+
tokenBVault: 6,
|
|
50
|
+
tokenAMint: 7,
|
|
51
|
+
tokenBMint: 8,
|
|
52
|
+
positionNftAccount: 9,
|
|
53
|
+
owner: 10,
|
|
54
|
+
tokenAProgram: 11,
|
|
55
|
+
tokenBProgram: 12,
|
|
56
|
+
eventAuthority: 13,
|
|
57
|
+
program: 14,
|
|
58
|
+
};
|
|
59
|
+
/** `tokenAFlag` / `tokenBFlag` of a DAMM v2 pool: 0 = SPL Token, 1 = Token-2022. */
|
|
60
|
+
export function dammV2TokenProgram(flag) {
|
|
61
|
+
return flag === 1 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Pure: the DAMM v2 pool of a migrated DBC pool, derived like the SDK's `migrateToDammV2`
|
|
65
|
+
* (`deriveDammV2PoolAddress(dammConfig, baseMint, quoteMint)` with the DAMM v2 config the SDK
|
|
66
|
+
* pins for the config's `migrationFeeOption`, `DAMM_V2_MIGRATION_FEE_ADDRESS`). Null when the
|
|
67
|
+
* pool has not migrated yet, migrates to DAMM v1, or has a migration fee option the pinned SDK
|
|
68
|
+
* does not know (FORGE configs use `FixedBps25`, D27).
|
|
69
|
+
*/
|
|
70
|
+
export function migratedDammV2Pool(state) {
|
|
71
|
+
if (graduationStatus(state) !== 'migrated')
|
|
72
|
+
return null;
|
|
73
|
+
if (state.config.migrationOption !== MigrationOption.MET_DAMM_V2)
|
|
74
|
+
return null;
|
|
75
|
+
const dammConfig = DAMM_V2_MIGRATION_FEE_ADDRESS[state.config.migrationFeeOption];
|
|
76
|
+
if (!dammConfig)
|
|
77
|
+
return null;
|
|
78
|
+
const p = state.virtualPool.poolState;
|
|
79
|
+
return {
|
|
80
|
+
dammPool: deriveDammV2PoolAddress(dammConfig, p.baseMint, state.config.quoteMint),
|
|
81
|
+
dammConfig,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** Pure: unclaimed fees of a position (SDK `getUnClaimLpFee`), as bigint. */
|
|
85
|
+
export function positionPendingFees(poolState, positionState) {
|
|
86
|
+
const fees = getUnClaimLpFee(poolState, positionState);
|
|
87
|
+
return { base: toBigint(fees.feeTokenA), quote: toBigint(fees.feeTokenB) };
|
|
88
|
+
}
|
|
89
|
+
/** Pure: summary of one position of `poolState`. */
|
|
90
|
+
export function summarizePosition(poolState, ref, positionState) {
|
|
91
|
+
return {
|
|
92
|
+
...ref,
|
|
93
|
+
dammPool: positionState.pool,
|
|
94
|
+
liquidity: {
|
|
95
|
+
unlocked: toBigint(positionState.unlockedLiquidity),
|
|
96
|
+
vested: toBigint(positionState.vestedLiquidity),
|
|
97
|
+
permanentLocked: toBigint(positionState.permanentLockedLiquidity),
|
|
98
|
+
},
|
|
99
|
+
pendingFees: positionPendingFees(poolState, positionState),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** Pure: sum of the pending fees of `positions`. */
|
|
103
|
+
export function sumPendingFees(positions) {
|
|
104
|
+
let base = 0n;
|
|
105
|
+
let quote = 0n;
|
|
106
|
+
for (const p of positions) {
|
|
107
|
+
base += p.pendingFees.base;
|
|
108
|
+
quote += p.pendingFees.quote;
|
|
109
|
+
}
|
|
110
|
+
return { base, quote };
|
|
111
|
+
}
|
|
112
|
+
// ---------------------------------------------------------------------------------------------
|
|
113
|
+
// Reads
|
|
114
|
+
// ---------------------------------------------------------------------------------------------
|
|
115
|
+
function cpAmm(connection) {
|
|
116
|
+
return new CpAmm(connection);
|
|
117
|
+
}
|
|
118
|
+
/** Fetches a DAMM v2 pool. @throws ClaimError `damm_pool_not_found` when the account is missing. */
|
|
119
|
+
export async function getDammV2PoolState(connection, dammPool, options = {}) {
|
|
120
|
+
try {
|
|
121
|
+
return await retryRpc(() => cpAmm(connection).fetchPoolState(dammPool), options.retry);
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
if (err instanceof Error && /not found/i.test(err.message)) {
|
|
125
|
+
throw new ClaimError('damm_pool_not_found', `DAMM v2 pool not found: ${dammPool.toBase58()}`);
|
|
126
|
+
}
|
|
127
|
+
throw err;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Resolves and checks positions: each must belong to `dammPool`, and its NFT token account
|
|
132
|
+
* (Token-2022) must be owned by `owner` and hold the NFT. Sorted by position address.
|
|
133
|
+
* @throws ClaimError `position_not_found`, `position_mismatch` or `position_not_owned`.
|
|
134
|
+
*/
|
|
135
|
+
async function resolvePositions(connection, amm, dammPool, poolState, owner, candidates, options) {
|
|
136
|
+
const commitment = options.commitment ?? READ_COMMITMENT;
|
|
137
|
+
const unique = new Map();
|
|
138
|
+
for (const c of candidates)
|
|
139
|
+
unique.set(c.position.toBase58(), c);
|
|
140
|
+
const ordered = [...unique.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
141
|
+
if (ordered.length === 0)
|
|
142
|
+
return [];
|
|
143
|
+
const resolved = [];
|
|
144
|
+
for (const [id, candidate] of ordered) {
|
|
145
|
+
let state = candidate.positionState;
|
|
146
|
+
if (!state) {
|
|
147
|
+
try {
|
|
148
|
+
state = await retryRpc(() => amm.fetchPositionState(candidate.position), options.retry);
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
if (err instanceof Error && /not found/i.test(err.message)) {
|
|
152
|
+
throw new ClaimError('position_not_found', `DAMM v2 position not found: ${id}`);
|
|
153
|
+
}
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (!state.pool.equals(dammPool)) {
|
|
158
|
+
throw new ClaimError('position_mismatch', `Position ${id} belongs to pool ${state.pool.toBase58()}, not to ${dammPool.toBase58()}`);
|
|
159
|
+
}
|
|
160
|
+
resolved.push({
|
|
161
|
+
ref: {
|
|
162
|
+
position: candidate.position,
|
|
163
|
+
nftMint: state.nftMint,
|
|
164
|
+
positionNftAccount: candidate.positionNftAccount ?? derivePositionNftAccount(state.nftMint),
|
|
165
|
+
},
|
|
166
|
+
state,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
const nftInfos = await retryRpc(() => connection.getMultipleAccountsInfo(resolved.map((r) => r.ref.positionNftAccount), commitment), options.retry);
|
|
170
|
+
resolved.forEach(({ ref }, i) => {
|
|
171
|
+
const info = nftInfos[i];
|
|
172
|
+
const problem = nftOwnershipProblem(ref, info ?? null, owner);
|
|
173
|
+
if (problem) {
|
|
174
|
+
throw new ClaimError('position_not_owned', `Position ${ref.position.toBase58()} is not held by ${owner.toBase58()}: ${problem}`);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
return resolved.map(({ ref, state }) => summarizePosition(poolState, ref, state));
|
|
178
|
+
}
|
|
179
|
+
/** Why `info` is not a Token-2022 account of `owner` holding the position NFT; null when it is. */
|
|
180
|
+
function nftOwnershipProblem(ref, info, owner) {
|
|
181
|
+
if (!info)
|
|
182
|
+
return 'position NFT token account missing';
|
|
183
|
+
let account;
|
|
184
|
+
try {
|
|
185
|
+
account = unpackAccount(ref.positionNftAccount, info, TOKEN_2022_PROGRAM_ID);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return 'position NFT token account is not a Token-2022 account';
|
|
189
|
+
}
|
|
190
|
+
if (!account.mint.equals(ref.nftMint))
|
|
191
|
+
return 'token account mint is not the position NFT';
|
|
192
|
+
if (account.amount !== 1n)
|
|
193
|
+
return 'token account does not hold the position NFT';
|
|
194
|
+
if (!account.owner.equals(owner))
|
|
195
|
+
return `held by ${account.owner.toBase58()}`;
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Finds the creator's positions on the migrated DAMM v2 pool of a DBC pool (SDK
|
|
200
|
+
* `getUserPositionByPool`: the Token-2022 accounts of the creator holding one NFT, then the
|
|
201
|
+
* position of each), checked again (pool, NFT held by the creator) and sorted by address.
|
|
202
|
+
* Returns null when the pool has not migrated to DAMM v2.
|
|
203
|
+
* @throws ClaimError `not_pool_creator`, `damm_pool_not_found`, `damm_pool_mismatch`.
|
|
204
|
+
* @throws DbcReadError when the DBC pool or its config does not exist.
|
|
205
|
+
*/
|
|
206
|
+
export async function findCreatorDammV2Positions(connection, params, options = {}) {
|
|
207
|
+
const state = params.pool instanceof PublicKey
|
|
208
|
+
? await getDbcPoolState(connection, params.pool, options)
|
|
209
|
+
: params.pool;
|
|
210
|
+
const poolCreator = state.virtualPool.poolState.creator;
|
|
211
|
+
if (!poolCreator.equals(params.creator)) {
|
|
212
|
+
throw new ClaimError('not_pool_creator', `${params.creator.toBase58()} is not the creator of pool ${state.address.toBase58()}`);
|
|
213
|
+
}
|
|
214
|
+
const migrated = migratedDammV2Pool(state);
|
|
215
|
+
if (!migrated)
|
|
216
|
+
return null;
|
|
217
|
+
const amm = cpAmm(connection);
|
|
218
|
+
const poolState = await getDammV2PoolState(connection, migrated.dammPool, options);
|
|
219
|
+
assertDammPoolMatches(state, migrated.dammPool, poolState);
|
|
220
|
+
const found = await retryRpc(() => amm.getUserPositionByPool(migrated.dammPool, params.creator), options.retry);
|
|
221
|
+
const positions = await resolvePositions(connection, amm, migrated.dammPool, poolState, params.creator, found.map((f) => ({
|
|
222
|
+
position: f.position,
|
|
223
|
+
positionNftAccount: f.positionNftAccount,
|
|
224
|
+
positionState: f.positionState,
|
|
225
|
+
})), options);
|
|
226
|
+
return {
|
|
227
|
+
pool: state.address,
|
|
228
|
+
dammPool: migrated.dammPool,
|
|
229
|
+
dammConfig: migrated.dammConfig,
|
|
230
|
+
creator: params.creator,
|
|
231
|
+
positions,
|
|
232
|
+
claimable: sumPendingFees(positions),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function assertDammPoolMatches(state, dammPool, poolState) {
|
|
236
|
+
const baseMint = state.virtualPool.poolState.baseMint;
|
|
237
|
+
if (!poolState.tokenAMint.equals(baseMint) || !poolState.tokenBMint.equals(state.config.quoteMint)) {
|
|
238
|
+
throw new ClaimError('damm_pool_mismatch', `DAMM v2 pool ${dammPool.toBase58()} is not the ${baseMint.toBase58()} / SOL pool of DBC pool ${state.address.toBase58()}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// ---------------------------------------------------------------------------------------------
|
|
242
|
+
// Claim builder
|
|
243
|
+
// ---------------------------------------------------------------------------------------------
|
|
244
|
+
/**
|
|
245
|
+
* Compute unit limit of a position fee claim with a priority fee: two idempotent token account
|
|
246
|
+
* creations and a close (about 60,000 CU), plus about 40,000 CU per `claim_position_fee`, doubled.
|
|
247
|
+
*/
|
|
248
|
+
export function positionClaimComputeUnits(positions) {
|
|
249
|
+
return Math.min(MAX_TRANSACTION_COMPUTE_UNITS, 120_000 + 80_000 * Math.max(1, positions));
|
|
250
|
+
}
|
|
251
|
+
const ZERO_COSTS = {
|
|
252
|
+
networkFeeLamports: 0n,
|
|
253
|
+
receiverTokenAccountRentLamports: 0n,
|
|
254
|
+
unwrapAccountRentLamports: 0n,
|
|
255
|
+
payerTotalLamports: 0n,
|
|
256
|
+
payerRequiredBalanceLamports: 0n,
|
|
257
|
+
};
|
|
258
|
+
/** True when the claim collects more SOL than it costs, by more than `minNetLamports`. */
|
|
259
|
+
export function isPositionClaimWorthwhile(summary, minNetLamports = 0n) {
|
|
260
|
+
return summary.claimable.quote > 0n && summary.netLamports > minNetLamports;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Builds the claim of the creator's DAMM v2 position fees of a migrated DBC pool to `receiver`
|
|
264
|
+
* (see the module comment for the transaction).
|
|
265
|
+
* @throws ClaimError when `creator` is not the pool creator, the pool is not SOL-quoted or not
|
|
266
|
+
* migrated to DAMM v2 (`not_migrated`), the receiver is invalid or differs from
|
|
267
|
+
* `expectedReceiver`, a position is missing, on another pool or not held by the creator, or
|
|
268
|
+
* the creator's WSOL account holds WSOL while the receiver is someone else.
|
|
269
|
+
* @throws DbcReadError when the DBC pool or its config does not exist.
|
|
270
|
+
*/
|
|
271
|
+
export async function buildCreatorPositionFeeClaimTx(connection, params, options = {}) {
|
|
272
|
+
const commitment = options.commitment ?? READ_COMMITMENT;
|
|
273
|
+
const readOptions = { commitment, retry: options.retry };
|
|
274
|
+
assertClaimReceiver(params.receiver);
|
|
275
|
+
assertClaimPriority(options);
|
|
276
|
+
if (params.expectedReceiver && !params.expectedReceiver.equals(params.receiver)) {
|
|
277
|
+
throw new ClaimError('receiver_mismatch', `Position fees of pool ${params.pool.toBase58()} must go to ${params.expectedReceiver.toBase58()}, not ${params.receiver.toBase58()}`);
|
|
278
|
+
}
|
|
279
|
+
const state = await getDbcPoolState(connection, params.pool, readOptions);
|
|
280
|
+
assertSolQuotedPool(state);
|
|
281
|
+
const poolCreator = state.virtualPool.poolState.creator;
|
|
282
|
+
if (!poolCreator.equals(params.creator)) {
|
|
283
|
+
throw new ClaimError('not_pool_creator', `${params.creator.toBase58()} is not the creator of pool ${params.pool.toBase58()}`);
|
|
284
|
+
}
|
|
285
|
+
const migrated = migratedDammV2Pool(state);
|
|
286
|
+
if (!migrated) {
|
|
287
|
+
throw new ClaimError('not_migrated', `Pool ${params.pool.toBase58()} has not migrated to DAMM v2: no position to claim`);
|
|
288
|
+
}
|
|
289
|
+
const { dammPool, dammConfig } = migrated;
|
|
290
|
+
const amm = cpAmm(connection);
|
|
291
|
+
const poolState = await getDammV2PoolState(connection, dammPool, readOptions);
|
|
292
|
+
assertDammPoolMatches(state, dammPool, poolState);
|
|
293
|
+
let candidates;
|
|
294
|
+
if (params.positions) {
|
|
295
|
+
candidates = params.positions.map((position) => ({
|
|
296
|
+
position,
|
|
297
|
+
positionNftAccount: null,
|
|
298
|
+
positionState: null,
|
|
299
|
+
}));
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
const found = await retryRpc(() => amm.getUserPositionByPool(dammPool, params.creator), options.retry);
|
|
303
|
+
candidates = found.map((f) => ({
|
|
304
|
+
position: f.position,
|
|
305
|
+
positionNftAccount: f.positionNftAccount,
|
|
306
|
+
positionState: f.positionState,
|
|
307
|
+
}));
|
|
308
|
+
}
|
|
309
|
+
const positions = await resolvePositions(connection, amm, dammPool, poolState, params.creator, candidates, readOptions);
|
|
310
|
+
const payer = params.payer ?? params.creator;
|
|
311
|
+
const receiver = params.receiver;
|
|
312
|
+
const baseMint = state.virtualPool.poolState.baseMint;
|
|
313
|
+
const claimable = sumPendingFees(positions);
|
|
314
|
+
const baseSummary = {
|
|
315
|
+
kind: 'position',
|
|
316
|
+
pool: state.address,
|
|
317
|
+
dammPool,
|
|
318
|
+
dammConfig,
|
|
319
|
+
baseMint,
|
|
320
|
+
claimer: params.creator,
|
|
321
|
+
receiver,
|
|
322
|
+
payer,
|
|
323
|
+
positions,
|
|
324
|
+
claimable,
|
|
325
|
+
quoteIsExact: false,
|
|
326
|
+
};
|
|
327
|
+
const skip = (skipReason) => ({
|
|
328
|
+
transaction: null,
|
|
329
|
+
lastValidBlockHeight: null,
|
|
330
|
+
skipReason,
|
|
331
|
+
summary: {
|
|
332
|
+
...baseSummary,
|
|
333
|
+
receiverQuoteLamports: 0n,
|
|
334
|
+
existingWsolLamports: 0n,
|
|
335
|
+
costs: ZERO_COSTS,
|
|
336
|
+
netLamports: 0n,
|
|
337
|
+
priorityFee: null,
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
if (positions.length === 0)
|
|
341
|
+
return skip('no_position');
|
|
342
|
+
if (claimable.base === 0n && claimable.quote === 0n)
|
|
343
|
+
return skip('nothing_to_claim');
|
|
344
|
+
// 1. The cp-amm claim instructions, built by the SDK (accounts: receiver's coin account,
|
|
345
|
+
// creator's temporary WSOL account). Its pre/post instructions are replaced below.
|
|
346
|
+
const tokenAProgram = dammV2TokenProgram(poolState.tokenAFlag);
|
|
347
|
+
const tokenBProgram = dammV2TokenProgram(poolState.tokenBFlag);
|
|
348
|
+
if (!tokenBProgram.equals(TOKEN_PROGRAM_ID)) {
|
|
349
|
+
throw new ClaimError('unsupported_quote_mint', 'The SOL side of the pool is not SPL Token WSOL');
|
|
350
|
+
}
|
|
351
|
+
const receiverBase = getAssociatedTokenAddressSync(baseMint, receiver, true, tokenAProgram);
|
|
352
|
+
const creatorWsol = getAssociatedTokenAddressSync(NATIVE_MINT, params.creator, true, TOKEN_PROGRAM_ID);
|
|
353
|
+
const claimIxs = [];
|
|
354
|
+
for (const p of positions) {
|
|
355
|
+
const sdkTx = await retryRpc(() => amm.claimPositionFee({
|
|
356
|
+
owner: params.creator,
|
|
357
|
+
position: p.position,
|
|
358
|
+
pool: dammPool,
|
|
359
|
+
positionNftAccount: p.positionNftAccount,
|
|
360
|
+
tokenAMint: poolState.tokenAMint,
|
|
361
|
+
tokenBMint: poolState.tokenBMint,
|
|
362
|
+
tokenAVault: poolState.tokenAVault,
|
|
363
|
+
tokenBVault: poolState.tokenBVault,
|
|
364
|
+
tokenAProgram,
|
|
365
|
+
tokenBProgram,
|
|
366
|
+
receiver,
|
|
367
|
+
feePayer: payer,
|
|
368
|
+
tempWSolAccount: params.creator,
|
|
369
|
+
}), options.retry);
|
|
370
|
+
const ixs = sdkTx.instructions.filter((ix) => ix.programId.equals(DAMM_V2_PROGRAM_ID));
|
|
371
|
+
const ix = ixs[0];
|
|
372
|
+
const at = (i) => ix?.keys[i]?.pubkey;
|
|
373
|
+
if (ixs.length !== 1 ||
|
|
374
|
+
!ix ||
|
|
375
|
+
!CLAIM_POSITION_FEE_DISCRIMINATOR.equals(Buffer.from(ix.data).subarray(0, 8)) ||
|
|
376
|
+
!at(CLAIM_POSITION_FEE_ACCOUNTS.pool)?.equals(dammPool) ||
|
|
377
|
+
!at(CLAIM_POSITION_FEE_ACCOUNTS.position)?.equals(p.position) ||
|
|
378
|
+
!at(CLAIM_POSITION_FEE_ACCOUNTS.tokenAAccount)?.equals(receiverBase) ||
|
|
379
|
+
!at(CLAIM_POSITION_FEE_ACCOUNTS.tokenBAccount)?.equals(creatorWsol) ||
|
|
380
|
+
!at(CLAIM_POSITION_FEE_ACCOUNTS.positionNftAccount)?.equals(p.positionNftAccount) ||
|
|
381
|
+
!at(CLAIM_POSITION_FEE_ACCOUNTS.owner)?.equals(params.creator)) {
|
|
382
|
+
throw new ClaimError('unexpected_sdk_instructions', 'The SDK position fee claim does not have the expected claim instruction');
|
|
383
|
+
}
|
|
384
|
+
claimIxs.push(ix);
|
|
385
|
+
}
|
|
386
|
+
// 2. On-chain state: the receiver's coin account, the creator's WSOL account.
|
|
387
|
+
const [infos, tokenAccountRent] = await Promise.all([
|
|
388
|
+
retryRpc(() => connection.getMultipleAccountsInfo([receiverBase, creatorWsol], commitment), options.retry),
|
|
389
|
+
retryRpc(() => connection.getMinimumBalanceForRentExemption(ACCOUNT_SIZE, commitment), options.retry),
|
|
390
|
+
]);
|
|
391
|
+
const [receiverBaseInfo, creatorWsolInfo] = infos;
|
|
392
|
+
// The temporary account is closed to the receiver: WSOL it already holds (anyone can send
|
|
393
|
+
// some to it, 1 lamport is enough) simply goes with it, counted as received. Refusing it
|
|
394
|
+
// would let anyone block a pool's claims for the price of a lamport.
|
|
395
|
+
let existingWsolLamports = 0n;
|
|
396
|
+
if (creatorWsolInfo && creatorWsolInfo.owner.equals(TOKEN_PROGRAM_ID)) {
|
|
397
|
+
try {
|
|
398
|
+
existingWsolLamports = unpackAccount(creatorWsol, creatorWsolInfo, TOKEN_PROGRAM_ID).amount;
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
// Not a token account (e.g. lamports sent to the address): the idempotent creation takes it.
|
|
402
|
+
existingWsolLamports = 0n;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
// 3. Instructions.
|
|
406
|
+
const instructions = [
|
|
407
|
+
createAssociatedTokenAccountIdempotentInstruction(payer, receiverBase, receiver, baseMint, tokenAProgram),
|
|
408
|
+
createAssociatedTokenAccountIdempotentInstruction(payer, creatorWsol, params.creator, NATIVE_MINT, TOKEN_PROGRAM_ID),
|
|
409
|
+
...claimIxs,
|
|
410
|
+
createCloseAccountInstruction(creatorWsol, receiver, params.creator),
|
|
411
|
+
];
|
|
412
|
+
const priority = await claimPriorityFee(connection, instructions, positionClaimComputeUnits(claimIxs.length), options.priority);
|
|
413
|
+
const { blockhash, lastValidBlockHeight } = await retryRpc(() => connection.getLatestBlockhash(commitment), options.retry);
|
|
414
|
+
const transaction = new Transaction({ feePayer: payer, blockhash, lastValidBlockHeight });
|
|
415
|
+
transaction.add(...priority.instructions, ...instructions);
|
|
416
|
+
// 4. Costs and net.
|
|
417
|
+
const signatures = BigInt(transaction.compileMessage().header.numRequiredSignatures);
|
|
418
|
+
const networkFeeLamports = signatures * CLAIM_LAMPORTS_PER_SIGNATURE + (priority.quote?.priorityFeeLamports ?? 0n);
|
|
419
|
+
const receiverTokenAccountRentLamports = receiverBaseInfo ? 0n : BigInt(tokenAccountRent);
|
|
420
|
+
const unwrapAccountRentLamports = creatorWsolInfo ? 0n : BigInt(tokenAccountRent);
|
|
421
|
+
const payerTotalLamports = networkFeeLamports + receiverTokenAccountRentLamports + unwrapAccountRentLamports;
|
|
422
|
+
const costs = {
|
|
423
|
+
networkFeeLamports,
|
|
424
|
+
receiverTokenAccountRentLamports,
|
|
425
|
+
unwrapAccountRentLamports,
|
|
426
|
+
payerTotalLamports,
|
|
427
|
+
payerRequiredBalanceLamports: payerTotalLamports,
|
|
428
|
+
};
|
|
429
|
+
// The closed account hands the receiver its whole balance: its rent (new or existing).
|
|
430
|
+
const closedLamports = creatorWsolInfo
|
|
431
|
+
? BigInt(creatorWsolInfo.lamports)
|
|
432
|
+
: unwrapAccountRentLamports;
|
|
433
|
+
const receiverQuoteLamports = claimable.quote + closedLamports;
|
|
434
|
+
const summary = {
|
|
435
|
+
...baseSummary,
|
|
436
|
+
receiverQuoteLamports,
|
|
437
|
+
existingWsolLamports,
|
|
438
|
+
costs,
|
|
439
|
+
netLamports: receiverQuoteLamports - payerTotalLamports,
|
|
440
|
+
priorityFee: priority.quote,
|
|
441
|
+
};
|
|
442
|
+
if (options.minNetLamports !== undefined &&
|
|
443
|
+
!isPositionClaimWorthwhile(summary, options.minNetLamports)) {
|
|
444
|
+
return { transaction: null, lastValidBlockHeight: null, skipReason: 'not_worthwhile', summary };
|
|
445
|
+
}
|
|
446
|
+
return { transaction, lastValidBlockHeight, skipReason: null, summary };
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* The position fee claim of a FORGE creator wallet: the receiver is always the FORGE multisig
|
|
450
|
+
* vault (`FORGE_MULTISIG_VAULT`, SECURITY.md §5), read from `env` (defaults to `process.env`).
|
|
451
|
+
* @throws ForgeAddressError when `FORGE_MULTISIG_VAULT` is missing or invalid; as
|
|
452
|
+
* `buildCreatorPositionFeeClaimTx` otherwise.
|
|
453
|
+
*/
|
|
454
|
+
export async function buildForgeCreatorPositionFeeClaimTx(connection, params, options = {}) {
|
|
455
|
+
const vault = getMultisigVault(options.env);
|
|
456
|
+
return buildCreatorPositionFeeClaimTx(connection, { ...params, receiver: vault, expectedReceiver: vault }, options);
|
|
457
|
+
}
|