@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
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
import type { LaunchpadSpec } from '@forge/shared';
|
|
16
16
|
export type LaunchCoinErrorCode = 'INVALID_INPUT' | 'CONFIG_NOT_FOUND' | 'CONFIG_MISMATCH' | 'POOL_EXISTS' | 'MINT_EXISTS'
|
|
17
17
|
/** A referral was given for the first buy: it would lose the first-swap minimum fee. */
|
|
18
|
-
| 'REFERRAL_UNSUPPORTED' | 'RENT_PAYER_REFUSED' | 'FIRST_BUY_EXCEEDS_CURVE' | 'QUOTE_FAILED' | 'UNEXPECTED_SIGNERS' | 'TX_TOO_LARGE'
|
|
18
|
+
| 'REFERRAL_UNSUPPORTED' | 'RENT_PAYER_REFUSED' | 'FIRST_BUY_EXCEEDS_CURVE' | 'QUOTE_FAILED' | 'UNEXPECTED_SIGNERS' | 'TX_TOO_LARGE'
|
|
19
|
+
/** FORGE_PLATFORM_FEE_WALLET does not exist or is not rent-exempt (user coin launch). */
|
|
20
|
+
| 'PLATFORM_FEE_WALLET_NOT_READY';
|
|
19
21
|
/** The launch transaction cannot be built. Nothing has been sent. */
|
|
20
22
|
export declare class LaunchCoinError extends Error {
|
|
21
23
|
readonly code: LaunchCoinErrorCode;
|
|
@@ -36,6 +38,8 @@ export declare const METADATA_LIMITS: {
|
|
|
36
38
|
* buy): every byte of the three strings is one byte of the transaction, and a 32-byte name, a
|
|
37
39
|
* 10-byte symbol and a 106-byte URI land at exactly 1,232 bytes (pool.test.ts, "size"). A smaller
|
|
38
40
|
* launch (no first buy, no nonce) has more room; this is the bound for the signer's shape.
|
|
41
|
+
* A priority fee never shrinks it: up to 136 bytes the launch carries the unit limit and the
|
|
42
|
+
* price, up to 144 the price alone, beyond that no price (`priorityFeePlacement`, pool.ts).
|
|
39
43
|
*/
|
|
40
44
|
export declare const LAUNCH_METADATA_BUDGET_BYTES = 148;
|
|
41
45
|
/** Largest first buy (the spec bound, `launchpadCoin.firstBuySol` <= 10 SOL). */
|
package/dist/launch-metadata.js
CHANGED
|
@@ -25,6 +25,8 @@ export const METADATA_LIMITS = { name: 32, symbol: 10, uri: 200 };
|
|
|
25
25
|
* buy): every byte of the three strings is one byte of the transaction, and a 32-byte name, a
|
|
26
26
|
* 10-byte symbol and a 106-byte URI land at exactly 1,232 bytes (pool.test.ts, "size"). A smaller
|
|
27
27
|
* launch (no first buy, no nonce) has more room; this is the bound for the signer's shape.
|
|
28
|
+
* A priority fee never shrinks it: up to 136 bytes the launch carries the unit limit and the
|
|
29
|
+
* price, up to 144 the price alone, beyond that no price (`priorityFeePlacement`, pool.ts).
|
|
28
30
|
*/
|
|
29
31
|
export const LAUNCH_METADATA_BUDGET_BYTES = 148;
|
|
30
32
|
/** Largest first buy (the spec bound, `launchpadCoin.firstBuySol` <= 10 SOL). */
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { PublicKey, Transaction, TransactionInstruction, type AccountInfo, type Commitment, type Connection } from '@solana/web3.js';
|
|
2
|
+
import { type PriorityFeeQuote } from './priority-fee.js';
|
|
3
|
+
import { type RetryOptions } from './reads.js';
|
|
4
|
+
/** Default seed of the referral account address (`createWithSeed(payer, seed, SPL Token)`). */
|
|
5
|
+
export declare const METEORA_REFERRAL_ACCOUNT_SEED = "forge-meteora-referral";
|
|
6
|
+
/** Longest seed `createWithSeed` accepts, in bytes (System program limit). */
|
|
7
|
+
export declare const MAX_METEORA_REFERRAL_SEED_BYTES = 32;
|
|
8
|
+
/**
|
|
9
|
+
* Compute unit limit of the setup transaction with a priority fee: one `createAccountWithSeed`
|
|
10
|
+
* and one `InitializeAccount3` (a few thousand units each), with a wide margin.
|
|
11
|
+
*/
|
|
12
|
+
export declare const METEORA_REFERRAL_SETUP_COMPUTE_UNITS = 30000;
|
|
13
|
+
/** SPL Token `InitializeAccount3` instruction tag (u8), followed by the 32-byte owner. */
|
|
14
|
+
export declare const TOKEN_INITIALIZE_ACCOUNT3_TAG = 18;
|
|
15
|
+
export type MeteoraReferralErrorCode = 'invalid_params';
|
|
16
|
+
export declare class MeteoraReferralError extends Error {
|
|
17
|
+
readonly code: MeteoraReferralErrorCode;
|
|
18
|
+
constructor(code: MeteoraReferralErrorCode, message: string, options?: {
|
|
19
|
+
cause?: unknown;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/** Checks a `createWithSeed` seed: 1 to 32 UTF-8 bytes, no control character. */
|
|
23
|
+
export declare function assertMeteoraReferralSeed(seed: string): void;
|
|
24
|
+
/**
|
|
25
|
+
* Address of the referral account created by `buildCreateMeteoraReferralAccountTx` for `payer`
|
|
26
|
+
* and `seed`: `createWithSeed(payer, seed, SPL Token program)`, i.e.
|
|
27
|
+
* `sha256(payer || seed || program)`, the same derivation as `PublicKey.createWithSeed`
|
|
28
|
+
* (synchronous here, so the builders stay pure).
|
|
29
|
+
*/
|
|
30
|
+
export declare function deriveMeteoraReferralAccount(payer: PublicKey, seed?: string): PublicKey;
|
|
31
|
+
export interface CreateMeteoraReferralAccountParams {
|
|
32
|
+
/** Fee payer, rent payer and base of the address derivation: the only signer. */
|
|
33
|
+
payer: PublicKey;
|
|
34
|
+
/** Owner of the token account: the FORGE revenue wallet (D34). */
|
|
35
|
+
owner: PublicKey;
|
|
36
|
+
/** Seed of the address (default `METEORA_REFERRAL_ACCOUNT_SEED`), 1 to 32 bytes. */
|
|
37
|
+
seed?: string;
|
|
38
|
+
/** Rent-exempt minimum of a token account (`ACCOUNT_SIZE` = 165 bytes), read by the caller. */
|
|
39
|
+
rentLamports: bigint;
|
|
40
|
+
}
|
|
41
|
+
export interface MeteoraReferralAccountInstructions {
|
|
42
|
+
/** The new token account: `deriveMeteoraReferralAccount(payer, seed)`. */
|
|
43
|
+
account: PublicKey;
|
|
44
|
+
seed: string;
|
|
45
|
+
/** `createAccountWithSeed` (payer -> account, rent, 165 bytes, SPL Token) then `InitializeAccount3`. */
|
|
46
|
+
instructions: TransactionInstruction[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The two instructions creating the standalone wrapped-SOL token account of `owner`:
|
|
50
|
+
* System `createAccountWithSeed` (from = base = payer; exactly the rent of 165 bytes; owner
|
|
51
|
+
* program = SPL Token) and SPL Token `InitializeAccount3` (account, wrapped SOL mint; owner in
|
|
52
|
+
* the data, no owner signature). Refuses an owner equal to the payer, a zero rent, and a seed
|
|
53
|
+
* whose address is the owner's associated wrapped-SOL account (impossible in practice: the
|
|
54
|
+
* associated account is a PDA, this address is a hash; checked all the same).
|
|
55
|
+
*/
|
|
56
|
+
export declare function buildCreateMeteoraReferralAccountIxs(p: CreateMeteoraReferralAccountParams): MeteoraReferralAccountInstructions;
|
|
57
|
+
export interface BuildCreateMeteoraReferralAccountTxParams extends CreateMeteoraReferralAccountParams {
|
|
58
|
+
blockhash: string;
|
|
59
|
+
lastValidBlockHeight: number;
|
|
60
|
+
/**
|
|
61
|
+
* Priority fee quote of the caller (`METEORA_REFERRAL_SETUP_COMPUTE_UNITS`): a unit limit then
|
|
62
|
+
* a unit price are put first when its price is not 0; nothing otherwise.
|
|
63
|
+
*/
|
|
64
|
+
priorityFee?: Pick<PriorityFeeQuote, 'computeUnitLimit' | 'microLamports'>;
|
|
65
|
+
}
|
|
66
|
+
export interface CreateMeteoraReferralAccountTx extends MeteoraReferralAccountInstructions {
|
|
67
|
+
/** Unsigned legacy transaction (fee payer = payer, blockhash set). */
|
|
68
|
+
transaction: Transaction;
|
|
69
|
+
lastValidBlockHeight: number;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Unsigned transaction creating the Meteora referral account (`buildCreateMeteoraReferralAccountIxs`
|
|
73
|
+
* after the optional compute budget). It does not check that the address is free: inspect it
|
|
74
|
+
* first (`readMeteoraReferralAccount`); creating it twice fails on-chain, nothing is lost but
|
|
75
|
+
* the fee.
|
|
76
|
+
*/
|
|
77
|
+
export declare function buildCreateMeteoraReferralAccountTx(p: BuildCreateMeteoraReferralAccountTxParams): CreateMeteoraReferralAccountTx;
|
|
78
|
+
export type MeteoraReferralMismatch =
|
|
79
|
+
/** The address holds something that is not an SPL Token account (or none that decodes). */
|
|
80
|
+
'not_a_token_account' | 'wrong_mint' | 'wrong_owner'
|
|
81
|
+
/** The account is the owner's associated wrapped-SOL account: Jupiter claims would land in it. */
|
|
82
|
+
| 'associated_account' | 'frozen'
|
|
83
|
+
/** A delegate or a close authority could move or close it: not acceptable for a fee account. */
|
|
84
|
+
| 'delegated' | 'close_authority';
|
|
85
|
+
export type MeteoraReferralAccountStatus =
|
|
86
|
+
/** No account at the address: the setup creates it. */
|
|
87
|
+
{
|
|
88
|
+
status: 'absent';
|
|
89
|
+
}
|
|
90
|
+
/** A wrapped-SOL token account held by the expected owner, standalone: nothing to do. */
|
|
91
|
+
| {
|
|
92
|
+
status: 'ready';
|
|
93
|
+
lamports: bigint;
|
|
94
|
+
amount: bigint;
|
|
95
|
+
}
|
|
96
|
+
/** Something else lives there: never create, never reuse (the caller refuses). */
|
|
97
|
+
| {
|
|
98
|
+
status: 'mismatch';
|
|
99
|
+
reason: MeteoraReferralMismatch;
|
|
100
|
+
detail: string;
|
|
101
|
+
};
|
|
102
|
+
export interface MeteoraReferralExpectation {
|
|
103
|
+
/** The FORGE revenue wallet. */
|
|
104
|
+
owner: PublicKey;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* What `address` holds, against the expected shape of the Meteora referral account: absent,
|
|
108
|
+
* ready (a standalone wrapped-SOL token account of `owner`, neither frozen, delegated nor
|
|
109
|
+
* closable by someone else), or a mismatch with its reason.
|
|
110
|
+
*/
|
|
111
|
+
export declare function inspectMeteoraReferralAccount(address: PublicKey, info: AccountInfo<Buffer> | null, expected: MeteoraReferralExpectation): MeteoraReferralAccountStatus;
|
|
112
|
+
export interface ReadMeteoraReferralAccountParams extends MeteoraReferralExpectation {
|
|
113
|
+
connection: Pick<Connection, 'getAccountInfo'>;
|
|
114
|
+
address: PublicKey;
|
|
115
|
+
commitment?: Commitment;
|
|
116
|
+
retry?: RetryOptions;
|
|
117
|
+
}
|
|
118
|
+
/** Reads `address` and inspects it (`inspectMeteoraReferralAccount`). Any cluster. */
|
|
119
|
+
export declare function readMeteoraReferralAccount(p: ReadMeteoraReferralAccountParams): Promise<MeteoraReferralAccountStatus>;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meteora referral account setup (docs/METEORA.md "The Meteora referral", docs/DECISIONS.md D34).
|
|
3
|
+
*
|
|
4
|
+
* The Meteora referral of every curve swap core builds is a wrapped-SOL token account held by
|
|
5
|
+
* the FORGE revenue wallet (`FORGE_METEORA_REFERRAL_ACCOUNT`). It must be a STANDALONE token
|
|
6
|
+
* account, never the wallet's associated token account: the Jupiter referral claims land in the
|
|
7
|
+
* associated account, and the revenue job books the two separately (`meteora_referral` /
|
|
8
|
+
* `jupiter_referral`).
|
|
9
|
+
*
|
|
10
|
+
* The account lives at an address derived from the fee payer and a fixed seed
|
|
11
|
+
* (`createAccountWithSeed`: no extra keypair to generate or keep, and the address can be
|
|
12
|
+
* re-derived at any time), 165 bytes owned by the SPL Token program, then `InitializeAccount3`
|
|
13
|
+
* sets its mint (wrapped SOL) and its owner (the revenue wallet) from the instruction data. The
|
|
14
|
+
* only signer is the payer: neither the owner nor the new account signs, so the revenue key never
|
|
15
|
+
* has to sign its own setup.
|
|
16
|
+
*
|
|
17
|
+
* Pure builders (byte-for-byte tested) and pure inspection of what the chain holds; the only
|
|
18
|
+
* network call is the optional `readMeteoraReferralAccount`. Any cluster: a WSOL token account
|
|
19
|
+
* exists on devnet too (the devnet proof of the revenue job uses one).
|
|
20
|
+
*/
|
|
21
|
+
import { createHash } from 'node:crypto';
|
|
22
|
+
import { ACCOUNT_SIZE, NATIVE_MINT, TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync, unpackAccount, } from '@solana/spl-token';
|
|
23
|
+
import { PublicKey, SystemProgram, Transaction, TransactionInstruction, } from '@solana/web3.js';
|
|
24
|
+
import { priorityFeeInstructions } from './priority-fee.js';
|
|
25
|
+
import { retryRpc } from './reads.js';
|
|
26
|
+
// ---------------------------------------------------------------------------------------------
|
|
27
|
+
// Constants
|
|
28
|
+
// ---------------------------------------------------------------------------------------------
|
|
29
|
+
/** Default seed of the referral account address (`createWithSeed(payer, seed, SPL Token)`). */
|
|
30
|
+
export const METEORA_REFERRAL_ACCOUNT_SEED = 'forge-meteora-referral';
|
|
31
|
+
/** Longest seed `createWithSeed` accepts, in bytes (System program limit). */
|
|
32
|
+
export const MAX_METEORA_REFERRAL_SEED_BYTES = 32;
|
|
33
|
+
/**
|
|
34
|
+
* Compute unit limit of the setup transaction with a priority fee: one `createAccountWithSeed`
|
|
35
|
+
* and one `InitializeAccount3` (a few thousand units each), with a wide margin.
|
|
36
|
+
*/
|
|
37
|
+
export const METEORA_REFERRAL_SETUP_COMPUTE_UNITS = 30_000;
|
|
38
|
+
/** SPL Token `InitializeAccount3` instruction tag (u8), followed by the 32-byte owner. */
|
|
39
|
+
export const TOKEN_INITIALIZE_ACCOUNT3_TAG = 18;
|
|
40
|
+
export class MeteoraReferralError extends Error {
|
|
41
|
+
code;
|
|
42
|
+
constructor(code, message, options) {
|
|
43
|
+
super(message, options);
|
|
44
|
+
this.name = 'MeteoraReferralError';
|
|
45
|
+
this.code = code;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function invalid(message) {
|
|
49
|
+
throw new MeteoraReferralError('invalid_params', message);
|
|
50
|
+
}
|
|
51
|
+
// ---------------------------------------------------------------------------------------------
|
|
52
|
+
// Address
|
|
53
|
+
// ---------------------------------------------------------------------------------------------
|
|
54
|
+
/** Checks a `createWithSeed` seed: 1 to 32 UTF-8 bytes, no control character. */
|
|
55
|
+
export function assertMeteoraReferralSeed(seed) {
|
|
56
|
+
if (typeof seed !== 'string' || seed.length === 0)
|
|
57
|
+
invalid('the seed must be a non-empty string');
|
|
58
|
+
if (/\p{Cc}/u.test(seed))
|
|
59
|
+
invalid('the seed must not contain control characters');
|
|
60
|
+
const bytes = Buffer.byteLength(seed, 'utf8');
|
|
61
|
+
if (bytes > MAX_METEORA_REFERRAL_SEED_BYTES) {
|
|
62
|
+
invalid(`the seed is ${bytes} bytes, at most ${MAX_METEORA_REFERRAL_SEED_BYTES} are allowed`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Address of the referral account created by `buildCreateMeteoraReferralAccountTx` for `payer`
|
|
67
|
+
* and `seed`: `createWithSeed(payer, seed, SPL Token program)`, i.e.
|
|
68
|
+
* `sha256(payer || seed || program)`, the same derivation as `PublicKey.createWithSeed`
|
|
69
|
+
* (synchronous here, so the builders stay pure).
|
|
70
|
+
*/
|
|
71
|
+
export function deriveMeteoraReferralAccount(payer, seed = METEORA_REFERRAL_ACCOUNT_SEED) {
|
|
72
|
+
assertMeteoraReferralSeed(seed);
|
|
73
|
+
const digest = createHash('sha256')
|
|
74
|
+
.update(payer.toBuffer())
|
|
75
|
+
.update(Buffer.from(seed, 'utf8'))
|
|
76
|
+
.update(TOKEN_PROGRAM_ID.toBuffer())
|
|
77
|
+
.digest();
|
|
78
|
+
return new PublicKey(digest);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The two instructions creating the standalone wrapped-SOL token account of `owner`:
|
|
82
|
+
* System `createAccountWithSeed` (from = base = payer; exactly the rent of 165 bytes; owner
|
|
83
|
+
* program = SPL Token) and SPL Token `InitializeAccount3` (account, wrapped SOL mint; owner in
|
|
84
|
+
* the data, no owner signature). Refuses an owner equal to the payer, a zero rent, and a seed
|
|
85
|
+
* whose address is the owner's associated wrapped-SOL account (impossible in practice: the
|
|
86
|
+
* associated account is a PDA, this address is a hash; checked all the same).
|
|
87
|
+
*/
|
|
88
|
+
export function buildCreateMeteoraReferralAccountIxs(p) {
|
|
89
|
+
const seed = p.seed ?? METEORA_REFERRAL_ACCOUNT_SEED;
|
|
90
|
+
if (p.payer.equals(p.owner))
|
|
91
|
+
invalid('the payer must not be the owner of the referral account');
|
|
92
|
+
if (p.owner.equals(PublicKey.default))
|
|
93
|
+
invalid('the owner must not be the default key');
|
|
94
|
+
if (typeof p.rentLamports !== 'bigint' || p.rentLamports <= 0n) {
|
|
95
|
+
invalid('rentLamports must be a positive bigint');
|
|
96
|
+
}
|
|
97
|
+
if (p.rentLamports > BigInt(Number.MAX_SAFE_INTEGER))
|
|
98
|
+
invalid('rentLamports is too large');
|
|
99
|
+
const account = deriveMeteoraReferralAccount(p.payer, seed);
|
|
100
|
+
if (account.equals(getAssociatedTokenAddressSync(NATIVE_MINT, p.owner, true))) {
|
|
101
|
+
invalid('the derived address is the associated wrapped-SOL account of the owner');
|
|
102
|
+
}
|
|
103
|
+
const create = SystemProgram.createAccountWithSeed({
|
|
104
|
+
fromPubkey: p.payer,
|
|
105
|
+
basePubkey: p.payer,
|
|
106
|
+
seed,
|
|
107
|
+
newAccountPubkey: account,
|
|
108
|
+
lamports: Number(p.rentLamports),
|
|
109
|
+
space: ACCOUNT_SIZE,
|
|
110
|
+
programId: TOKEN_PROGRAM_ID,
|
|
111
|
+
});
|
|
112
|
+
const initialize = new TransactionInstruction({
|
|
113
|
+
programId: TOKEN_PROGRAM_ID,
|
|
114
|
+
keys: [
|
|
115
|
+
{ pubkey: account, isSigner: false, isWritable: true },
|
|
116
|
+
{ pubkey: NATIVE_MINT, isSigner: false, isWritable: false },
|
|
117
|
+
],
|
|
118
|
+
data: Buffer.concat([Buffer.from([TOKEN_INITIALIZE_ACCOUNT3_TAG]), p.owner.toBuffer()]),
|
|
119
|
+
});
|
|
120
|
+
return { account, seed, instructions: [create, initialize] };
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Unsigned transaction creating the Meteora referral account (`buildCreateMeteoraReferralAccountIxs`
|
|
124
|
+
* after the optional compute budget). It does not check that the address is free: inspect it
|
|
125
|
+
* first (`readMeteoraReferralAccount`); creating it twice fails on-chain, nothing is lost but
|
|
126
|
+
* the fee.
|
|
127
|
+
*/
|
|
128
|
+
export function buildCreateMeteoraReferralAccountTx(p) {
|
|
129
|
+
if (typeof p.blockhash !== 'string' || p.blockhash.length === 0)
|
|
130
|
+
invalid('blockhash required');
|
|
131
|
+
if (!Number.isInteger(p.lastValidBlockHeight) || p.lastValidBlockHeight < 0) {
|
|
132
|
+
invalid('lastValidBlockHeight must be a non-negative integer');
|
|
133
|
+
}
|
|
134
|
+
const built = buildCreateMeteoraReferralAccountIxs(p);
|
|
135
|
+
const transaction = new Transaction({
|
|
136
|
+
feePayer: p.payer,
|
|
137
|
+
blockhash: p.blockhash,
|
|
138
|
+
lastValidBlockHeight: p.lastValidBlockHeight,
|
|
139
|
+
});
|
|
140
|
+
if (p.priorityFee && p.priorityFee.microLamports > 0) {
|
|
141
|
+
transaction.add(...priorityFeeInstructions(p.priorityFee));
|
|
142
|
+
}
|
|
143
|
+
transaction.add(...built.instructions);
|
|
144
|
+
return { ...built, transaction, lastValidBlockHeight: p.lastValidBlockHeight };
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* What `address` holds, against the expected shape of the Meteora referral account: absent,
|
|
148
|
+
* ready (a standalone wrapped-SOL token account of `owner`, neither frozen, delegated nor
|
|
149
|
+
* closable by someone else), or a mismatch with its reason.
|
|
150
|
+
*/
|
|
151
|
+
export function inspectMeteoraReferralAccount(address, info, expected) {
|
|
152
|
+
if (!info)
|
|
153
|
+
return { status: 'absent' };
|
|
154
|
+
const mismatch = (reason, detail) => ({ status: 'mismatch', reason, detail });
|
|
155
|
+
if (!info.owner.equals(TOKEN_PROGRAM_ID)) {
|
|
156
|
+
return mismatch('not_a_token_account', `the address is in use by an account owned by ${info.owner.toBase58()}`);
|
|
157
|
+
}
|
|
158
|
+
let account;
|
|
159
|
+
try {
|
|
160
|
+
account = unpackAccount(address, info, TOKEN_PROGRAM_ID);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return mismatch('not_a_token_account', 'the account data is not an SPL Token account');
|
|
164
|
+
}
|
|
165
|
+
if (!account.mint.equals(NATIVE_MINT)) {
|
|
166
|
+
return mismatch('wrong_mint', `the token account mint is ${account.mint.toBase58()}`);
|
|
167
|
+
}
|
|
168
|
+
if (!account.owner.equals(expected.owner)) {
|
|
169
|
+
return mismatch('wrong_owner', `the token account owner is ${account.owner.toBase58()}`);
|
|
170
|
+
}
|
|
171
|
+
if (address.equals(getAssociatedTokenAddressSync(NATIVE_MINT, expected.owner, true))) {
|
|
172
|
+
return mismatch('associated_account', 'the account is the associated wrapped-SOL account of the owner, not a standalone one');
|
|
173
|
+
}
|
|
174
|
+
if (account.isFrozen)
|
|
175
|
+
return mismatch('frozen', 'the token account is frozen');
|
|
176
|
+
if (account.delegate !== null) {
|
|
177
|
+
return mismatch('delegated', `the token account has a delegate ${account.delegate.toBase58()}`);
|
|
178
|
+
}
|
|
179
|
+
if (account.closeAuthority !== null && !account.closeAuthority.equals(expected.owner)) {
|
|
180
|
+
return mismatch('close_authority', `the token account can be closed by ${account.closeAuthority.toBase58()}`);
|
|
181
|
+
}
|
|
182
|
+
return { status: 'ready', lamports: BigInt(info.lamports), amount: account.amount };
|
|
183
|
+
}
|
|
184
|
+
/** Reads `address` and inspects it (`inspectMeteoraReferralAccount`). Any cluster. */
|
|
185
|
+
export async function readMeteoraReferralAccount(p) {
|
|
186
|
+
const info = await retryRpc(() => p.connection.getAccountInfo(p.address, p.commitment ?? 'confirmed'), p.retry ?? {});
|
|
187
|
+
return inspectMeteoraReferralAccount(p.address, info, { owner: p.owner });
|
|
188
|
+
}
|
package/dist/payments.d.ts
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* 1. a System Program transfer of `lamports` from the payer to `FORGE_CASHBOX_WALLET`;
|
|
6
6
|
* 2. an SPL Memo (v2) instruction, signed by the payer, whose UTF-8 data is
|
|
7
7
|
* `forge:job:<jobId>` (`jobId` = the job UUID, lowercase). See `paymentMemo`.
|
|
8
|
+
* With a `priority` option, two compute budget instructions come first (`setComputeUnitLimit`
|
|
9
|
+
* of `PAYMENT_COMPUTE_UNITS`, then `setComputeUnitPrice`, priced from the recent fees of the
|
|
10
|
+
* payer and the cashbox and bounded: priority-fee.ts), so a payment still lands under congestion.
|
|
8
11
|
*
|
|
9
12
|
* `verifyPayment` checks a transaction on-chain and returns a typed result; it never throws on a
|
|
10
13
|
* transaction that is "not there yet" (it returns `pending`). It throws only on configuration
|
|
@@ -18,6 +21,7 @@
|
|
|
18
21
|
*/
|
|
19
22
|
import { PublicKey, Transaction, TransactionInstruction, type Commitment, type Connection, type Finality, type VersionedTransactionResponse } from '@solana/web3.js';
|
|
20
23
|
import { type AddressEnv } from './addresses.js';
|
|
24
|
+
import { type PriorityFeeQuote, type PriorityFeeSetting } from './priority-fee.js';
|
|
21
25
|
import { type RetryOptions } from './reads.js';
|
|
22
26
|
/** SPL Memo program v2 (the one wallets and explorers decode). */
|
|
23
27
|
export declare const MEMO_PROGRAM_ID: PublicKey;
|
|
@@ -30,6 +34,11 @@ export declare const PAYMENT_MEMO_PREFIX = "forge:job:";
|
|
|
30
34
|
* against a unit mistake upstream.
|
|
31
35
|
*/
|
|
32
36
|
export declare const MAX_PAYMENT_LAMPORTS: bigint;
|
|
37
|
+
/**
|
|
38
|
+
* Compute unit limit of a payment with a priority fee: a System transfer (150 CU) and a memo of
|
|
39
|
+
* about 46 bytes signed by the payer (a few thousand CU), with a wide margin.
|
|
40
|
+
*/
|
|
41
|
+
export declare const PAYMENT_COMPUTE_UNITS = 50000;
|
|
33
42
|
/** Genesis hashes of the Solana clusters FORGE uses (public network constants). */
|
|
34
43
|
export declare const CLUSTER_GENESIS_HASH: {
|
|
35
44
|
readonly devnet: "EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG";
|
|
@@ -61,6 +70,12 @@ export interface PaymentTxOptions {
|
|
|
61
70
|
env?: AddressEnv;
|
|
62
71
|
commitment?: Commitment;
|
|
63
72
|
retry?: RetryOptions;
|
|
73
|
+
/**
|
|
74
|
+
* Priority fee read from the cluster for the payer and the cashbox, bounded by this setting
|
|
75
|
+
* (`{}` = `DEFAULT_PRIORITY_FEE_POLICY`, at most 0.0005 SOL). Without it the transaction holds
|
|
76
|
+
* the transfer and the memo only.
|
|
77
|
+
*/
|
|
78
|
+
priority?: PriorityFeeSetting;
|
|
64
79
|
}
|
|
65
80
|
export interface PaymentTxResult {
|
|
66
81
|
/** Unsigned transaction: fee payer = payer, recent blockhash set. The payer signs it. */
|
|
@@ -69,6 +84,8 @@ export interface PaymentTxResult {
|
|
|
69
84
|
recipient: PublicKey;
|
|
70
85
|
lamports: bigint;
|
|
71
86
|
memo: string;
|
|
87
|
+
/** The priority fee (price, fee, cap), or null without a `priority` option. */
|
|
88
|
+
priorityFee: PriorityFeeQuote | null;
|
|
72
89
|
}
|
|
73
90
|
/**
|
|
74
91
|
* Builds the payment transaction of `jobId`. Checks that the cashbox will be rent-exempt after
|
package/dist/payments.js
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* 1. a System Program transfer of `lamports` from the payer to `FORGE_CASHBOX_WALLET`;
|
|
6
6
|
* 2. an SPL Memo (v2) instruction, signed by the payer, whose UTF-8 data is
|
|
7
7
|
* `forge:job:<jobId>` (`jobId` = the job UUID, lowercase). See `paymentMemo`.
|
|
8
|
+
* With a `priority` option, two compute budget instructions come first (`setComputeUnitLimit`
|
|
9
|
+
* of `PAYMENT_COMPUTE_UNITS`, then `setComputeUnitPrice`, priced from the recent fees of the
|
|
10
|
+
* payer and the cashbox and bounded: priority-fee.ts), so a payment still lands under congestion.
|
|
8
11
|
*
|
|
9
12
|
* `verifyPayment` checks a transaction on-chain and returns a typed result; it never throws on a
|
|
10
13
|
* transaction that is "not there yet" (it returns `pending`). It throws only on configuration
|
|
@@ -20,6 +23,7 @@ import { PublicKey, SystemProgram, Transaction, TransactionInstruction, } from '
|
|
|
20
23
|
import { Base58Signature, Uuid } from '@forge/shared';
|
|
21
24
|
import { requireAddresses } from './addresses.js';
|
|
22
25
|
import { LAMPORTS_PER_SOL_BIGINT } from './constants.js';
|
|
26
|
+
import { estimatePriorityFee, priorityFeeInstructions, } from './priority-fee.js';
|
|
23
27
|
import { retryRpc } from './reads.js';
|
|
24
28
|
import { encodeBase58 } from './tx.js';
|
|
25
29
|
/** SPL Memo program v2 (the one wallets and explorers decode). */
|
|
@@ -33,6 +37,11 @@ export const PAYMENT_MEMO_PREFIX = 'forge:job:';
|
|
|
33
37
|
* against a unit mistake upstream.
|
|
34
38
|
*/
|
|
35
39
|
export const MAX_PAYMENT_LAMPORTS = 100n * LAMPORTS_PER_SOL_BIGINT;
|
|
40
|
+
/**
|
|
41
|
+
* Compute unit limit of a payment with a priority fee: a System transfer (150 CU) and a memo of
|
|
42
|
+
* about 46 bytes signed by the payer (a few thousand CU), with a wide margin.
|
|
43
|
+
*/
|
|
44
|
+
export const PAYMENT_COMPUTE_UNITS = 50_000;
|
|
36
45
|
/** Genesis hashes of the Solana clusters FORGE uses (public network constants). */
|
|
37
46
|
export const CLUSTER_GENESIS_HASH = {
|
|
38
47
|
devnet: 'EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG',
|
|
@@ -96,9 +105,17 @@ export async function buildPaymentTx(connection, params, options = {}) {
|
|
|
96
105
|
const cashbox = resolveCashbox(options.env);
|
|
97
106
|
const instructions = buildPaymentInstructions({ ...params, cashbox });
|
|
98
107
|
const commitment = options.commitment ?? 'confirmed';
|
|
99
|
-
const [rentExempt, cashboxBalance] = await Promise.all([
|
|
108
|
+
const [rentExempt, cashboxBalance, priorityFee] = await Promise.all([
|
|
100
109
|
retryRpc(() => connection.getMinimumBalanceForRentExemption(0, commitment), options.retry),
|
|
101
110
|
retryRpc(() => connection.getBalance(cashbox, commitment), options.retry),
|
|
111
|
+
options.priority === undefined
|
|
112
|
+
? Promise.resolve(null)
|
|
113
|
+
: estimatePriorityFee({
|
|
114
|
+
connection,
|
|
115
|
+
writableAccounts: [params.payer, cashbox],
|
|
116
|
+
computeUnitLimit: PAYMENT_COMPUTE_UNITS,
|
|
117
|
+
setting: options.priority,
|
|
118
|
+
}),
|
|
102
119
|
]);
|
|
103
120
|
if (BigInt(cashboxBalance) + params.lamports < BigInt(rentExempt)) {
|
|
104
121
|
throw new PaymentError('cashbox_not_rent_exempt', 'The cashbox would not be rent-exempt after this payment: fund it first');
|
|
@@ -108,13 +125,14 @@ export async function buildPaymentTx(connection, params, options = {}) {
|
|
|
108
125
|
feePayer: params.payer,
|
|
109
126
|
blockhash,
|
|
110
127
|
lastValidBlockHeight,
|
|
111
|
-
}).add(...instructions);
|
|
128
|
+
}).add(...(priorityFee ? priorityFeeInstructions(priorityFee) : []), ...instructions);
|
|
112
129
|
return {
|
|
113
130
|
transaction,
|
|
114
131
|
lastValidBlockHeight,
|
|
115
132
|
recipient: cashbox,
|
|
116
133
|
lamports: params.lamports,
|
|
117
134
|
memo: paymentMemo(params.jobId),
|
|
135
|
+
priorityFee,
|
|
118
136
|
};
|
|
119
137
|
}
|
|
120
138
|
const genesisCache = new WeakMap();
|