@wzrd_sol/sdk 0.1.1 → 0.1.2
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/README.md +2 -0
- package/dist/accounts.d.ts +2 -0
- package/dist/accounts.js +2 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -2
- package/dist/instructions.d.ts +482 -5
- package/dist/instructions.js +1268 -24
- package/dist/instructions.test.d.ts +7 -0
- package/dist/instructions.test.js +318 -0
- package/dist/nav.d.ts +40 -0
- package/dist/nav.js +39 -0
- package/dist/nav.test.d.ts +4 -0
- package/dist/nav.test.js +98 -0
- package/package.json +1 -1
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for SDK instruction builders and helpers.
|
|
3
|
+
*
|
|
4
|
+
* Tests the sync / pure parts of instructions.ts, pda.ts, accounts.ts, and constants.ts
|
|
5
|
+
* without requiring an RPC Connection.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import { PublicKey, SystemProgram } from '@solana/web3.js';
|
|
9
|
+
import { anchorDisc, createAtaIdempotentIx, createAddLiquidityIx, createRemoveLiquidityIx, DLMM_PROGRAM_ID, } from './instructions.js';
|
|
10
|
+
import { PROGRAM_ID, MAINNET_PROGRAM_ID, DEVNET_PROGRAM_ID, TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID, PROTOCOL_STATE_SEED, MARKET_VAULT_SEED, MARKET_POSITION_SEED, GLOBAL_ROOT_SEED, CLAIM_STATE_GLOBAL_SEED, CHANNEL_CONFIG_V2_SEED, } from './constants.js';
|
|
11
|
+
import { getProtocolStatePDA, getMarketVaultPDA, getUserPositionPDA, getGlobalRootConfigPDA, getClaimStatePDA, getChannelConfigV2PDA, getAta, } from './pda.js';
|
|
12
|
+
import { parseMarketVault, parseProtocolState, parseUserMarketPosition, } from './accounts.js';
|
|
13
|
+
// ── Test fixtures ────────────────────────────────────────
|
|
14
|
+
const DUMMY_KEY_A = new PublicKey('11111111111111111111111111111111');
|
|
15
|
+
const DUMMY_KEY_B = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
|
|
16
|
+
const CCM_MINT = new PublicKey('Dxk8mAb3C7AM8JN6tAJfVuSja5yidhZM5sEKW3SRX2BM');
|
|
17
|
+
const USDC_MINT = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v');
|
|
18
|
+
// ── anchorDisc ──────────────────────────────────────────
|
|
19
|
+
describe('anchorDisc', () => {
|
|
20
|
+
it('produces an 8-byte Buffer', async () => {
|
|
21
|
+
const disc = await anchorDisc('deposit_market');
|
|
22
|
+
expect(disc).toBeInstanceOf(Buffer);
|
|
23
|
+
expect(disc.length).toBe(8);
|
|
24
|
+
});
|
|
25
|
+
it('is deterministic — same name gives same bytes', async () => {
|
|
26
|
+
const a = await anchorDisc('deposit_market');
|
|
27
|
+
const b = await anchorDisc('deposit_market');
|
|
28
|
+
expect(a.equals(b)).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
it('different names produce different discriminators', async () => {
|
|
31
|
+
const deposit = await anchorDisc('deposit_market');
|
|
32
|
+
const settle = await anchorDisc('settle_market');
|
|
33
|
+
expect(deposit.equals(settle)).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
it('matches SHA-256("global:<name>")[0..8]', async () => {
|
|
36
|
+
// We manually verify deposit_market against the Node.js crypto module
|
|
37
|
+
const { createHash } = await import('crypto');
|
|
38
|
+
const hash = createHash('sha256').update('global:deposit_market').digest();
|
|
39
|
+
const expected = hash.subarray(0, 8);
|
|
40
|
+
const disc = await anchorDisc('deposit_market');
|
|
41
|
+
expect(disc.equals(expected)).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
it('produces correct disc for claim_global', async () => {
|
|
44
|
+
const { createHash } = await import('crypto');
|
|
45
|
+
const hash = createHash('sha256').update('global:claim_global').digest();
|
|
46
|
+
const expected = hash.subarray(0, 8);
|
|
47
|
+
const disc = await anchorDisc('claim_global');
|
|
48
|
+
expect(disc.equals(expected)).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
it('handles empty name gracefully', async () => {
|
|
51
|
+
const disc = await anchorDisc('');
|
|
52
|
+
expect(disc.length).toBe(8);
|
|
53
|
+
});
|
|
54
|
+
it('handles snake_case names', async () => {
|
|
55
|
+
const disc = await anchorDisc('initialize_market_vault');
|
|
56
|
+
expect(disc.length).toBe(8);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
// ── createAtaIdempotentIx ───────────────────────────────
|
|
60
|
+
describe('createAtaIdempotentIx', () => {
|
|
61
|
+
it('uses ASSOCIATED_TOKEN_PROGRAM_ID as programId', () => {
|
|
62
|
+
const ix = createAtaIdempotentIx(DUMMY_KEY_A, DUMMY_KEY_B, DUMMY_KEY_A, USDC_MINT);
|
|
63
|
+
expect(ix.programId.equals(ASSOCIATED_TOKEN_PROGRAM_ID)).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
it('has exactly 6 account keys', () => {
|
|
66
|
+
const ix = createAtaIdempotentIx(DUMMY_KEY_A, DUMMY_KEY_B, DUMMY_KEY_A, USDC_MINT);
|
|
67
|
+
expect(ix.keys.length).toBe(6);
|
|
68
|
+
});
|
|
69
|
+
it('sets payer as signer + writable', () => {
|
|
70
|
+
const payer = CCM_MINT;
|
|
71
|
+
const ix = createAtaIdempotentIx(payer, DUMMY_KEY_B, DUMMY_KEY_A, USDC_MINT);
|
|
72
|
+
expect(ix.keys[0].pubkey.equals(payer)).toBe(true);
|
|
73
|
+
expect(ix.keys[0].isSigner).toBe(true);
|
|
74
|
+
expect(ix.keys[0].isWritable).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
it('sets ata as writable, not signer', () => {
|
|
77
|
+
const ata = DUMMY_KEY_B;
|
|
78
|
+
const ix = createAtaIdempotentIx(DUMMY_KEY_A, ata, DUMMY_KEY_A, USDC_MINT);
|
|
79
|
+
expect(ix.keys[1].pubkey.equals(ata)).toBe(true);
|
|
80
|
+
expect(ix.keys[1].isSigner).toBe(false);
|
|
81
|
+
expect(ix.keys[1].isWritable).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
it('data is single byte [1] (CreateIdempotent)', () => {
|
|
84
|
+
const ix = createAtaIdempotentIx(DUMMY_KEY_A, DUMMY_KEY_B, DUMMY_KEY_A, USDC_MINT);
|
|
85
|
+
expect(ix.data.length).toBe(1);
|
|
86
|
+
expect(ix.data[0]).toBe(1);
|
|
87
|
+
});
|
|
88
|
+
it('defaults to TOKEN_PROGRAM_ID when no tokenProgramId provided', () => {
|
|
89
|
+
const ix = createAtaIdempotentIx(DUMMY_KEY_A, DUMMY_KEY_B, DUMMY_KEY_A, USDC_MINT);
|
|
90
|
+
// 5th account is tokenProgramId
|
|
91
|
+
expect(ix.keys[5].pubkey.equals(TOKEN_PROGRAM_ID)).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
it('uses Token-2022 when specified', () => {
|
|
94
|
+
const ix = createAtaIdempotentIx(DUMMY_KEY_A, DUMMY_KEY_B, DUMMY_KEY_A, CCM_MINT, TOKEN_2022_PROGRAM_ID);
|
|
95
|
+
expect(ix.keys[5].pubkey.equals(TOKEN_2022_PROGRAM_ID)).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
it('includes SystemProgram in account keys', () => {
|
|
98
|
+
const ix = createAtaIdempotentIx(DUMMY_KEY_A, DUMMY_KEY_B, DUMMY_KEY_A, USDC_MINT);
|
|
99
|
+
expect(ix.keys[4].pubkey.equals(SystemProgram.programId)).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
// ── createAddLiquidityIx ────────────────────────────────
|
|
103
|
+
describe('createAddLiquidityIx', () => {
|
|
104
|
+
const pool = PublicKey.unique();
|
|
105
|
+
const position = PublicKey.unique();
|
|
106
|
+
const owner = PublicKey.unique();
|
|
107
|
+
const tokenXMint = PublicKey.unique();
|
|
108
|
+
const tokenYMint = PublicKey.unique();
|
|
109
|
+
const userTokenX = PublicKey.unique();
|
|
110
|
+
const userTokenY = PublicKey.unique();
|
|
111
|
+
const reserveX = PublicKey.unique();
|
|
112
|
+
const reserveY = PublicKey.unique();
|
|
113
|
+
const binArrayLower = PublicKey.unique();
|
|
114
|
+
const binArrayUpper = PublicKey.unique();
|
|
115
|
+
const ix = createAddLiquidityIx(pool, position, owner, tokenXMint, tokenYMint, userTokenX, userTokenY, reserveX, reserveY, binArrayLower, binArrayUpper, 1000n, 2000n, 100, 5);
|
|
116
|
+
it('uses DLMM_PROGRAM_ID', () => {
|
|
117
|
+
expect(ix.programId.equals(DLMM_PROGRAM_ID)).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
it('has 14 account keys', () => {
|
|
120
|
+
expect(ix.keys.length).toBe(14);
|
|
121
|
+
});
|
|
122
|
+
it('data is 33 bytes (8 disc + 8 amountX + 8 amountY + 4 activeBinId + 4 binCount + 1 strategyType)', () => {
|
|
123
|
+
expect(ix.data.length).toBe(33);
|
|
124
|
+
});
|
|
125
|
+
it('encodes amountX correctly as LE u64', () => {
|
|
126
|
+
const amountX = ix.data.readBigUInt64LE(8);
|
|
127
|
+
expect(amountX).toBe(1000n);
|
|
128
|
+
});
|
|
129
|
+
it('encodes amountY correctly as LE u64', () => {
|
|
130
|
+
const amountY = ix.data.readBigUInt64LE(16);
|
|
131
|
+
expect(amountY).toBe(2000n);
|
|
132
|
+
});
|
|
133
|
+
it('encodes activeBinId as LE i32', () => {
|
|
134
|
+
const activeBinId = ix.data.readInt32LE(24);
|
|
135
|
+
expect(activeBinId).toBe(100);
|
|
136
|
+
});
|
|
137
|
+
it('encodes binCount as LE i32', () => {
|
|
138
|
+
const binCount = ix.data.readInt32LE(28);
|
|
139
|
+
expect(binCount).toBe(5);
|
|
140
|
+
});
|
|
141
|
+
it('sets strategy type to 0 (Spot)', () => {
|
|
142
|
+
expect(ix.data.readUInt8(32)).toBe(0);
|
|
143
|
+
});
|
|
144
|
+
it('marks owner as signer', () => {
|
|
145
|
+
const ownerKey = ix.keys.find(k => k.pubkey.equals(owner));
|
|
146
|
+
expect(ownerKey?.isSigner).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
// ── createRemoveLiquidityIx ─────────────────────────────
|
|
150
|
+
describe('createRemoveLiquidityIx', () => {
|
|
151
|
+
const pool = PublicKey.unique();
|
|
152
|
+
const position = PublicKey.unique();
|
|
153
|
+
const owner = PublicKey.unique();
|
|
154
|
+
const reserveX = PublicKey.unique();
|
|
155
|
+
const reserveY = PublicKey.unique();
|
|
156
|
+
const userTokenX = PublicKey.unique();
|
|
157
|
+
const userTokenY = PublicKey.unique();
|
|
158
|
+
const tokenXMint = PublicKey.unique();
|
|
159
|
+
const tokenYMint = PublicKey.unique();
|
|
160
|
+
const binArrayLower = PublicKey.unique();
|
|
161
|
+
const binArrayUpper = PublicKey.unique();
|
|
162
|
+
const ix = createRemoveLiquidityIx(pool, position, owner, reserveX, reserveY, userTokenX, userTokenY, tokenXMint, tokenYMint, binArrayLower, binArrayUpper, 10000);
|
|
163
|
+
it('uses DLMM_PROGRAM_ID', () => {
|
|
164
|
+
expect(ix.programId.equals(DLMM_PROGRAM_ID)).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
it('has 13 account keys', () => {
|
|
167
|
+
expect(ix.keys.length).toBe(13);
|
|
168
|
+
});
|
|
169
|
+
it('data is 10 bytes (8 disc + 2 bps)', () => {
|
|
170
|
+
expect(ix.data.length).toBe(10);
|
|
171
|
+
});
|
|
172
|
+
it('encodes bpsBasisPointsToRemove as LE u16', () => {
|
|
173
|
+
const bps = ix.data.readUInt16LE(8);
|
|
174
|
+
expect(bps).toBe(10000);
|
|
175
|
+
});
|
|
176
|
+
it('marks owner as signer', () => {
|
|
177
|
+
const ownerKey = ix.keys.find(k => k.pubkey.equals(owner));
|
|
178
|
+
expect(ownerKey?.isSigner).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
it('encodes partial removal correctly', () => {
|
|
181
|
+
const partialIx = createRemoveLiquidityIx(pool, position, owner, reserveX, reserveY, userTokenX, userTokenY, tokenXMint, tokenYMint, binArrayLower, binArrayUpper, 5000);
|
|
182
|
+
expect(partialIx.data.readUInt16LE(8)).toBe(5000);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
// ── PDA derivation ──────────────────────────────────────
|
|
186
|
+
describe('PDA derivation', () => {
|
|
187
|
+
it('getProtocolStatePDA is deterministic', () => {
|
|
188
|
+
const a = getProtocolStatePDA(PROGRAM_ID);
|
|
189
|
+
const b = getProtocolStatePDA(PROGRAM_ID);
|
|
190
|
+
expect(a.equals(b)).toBe(true);
|
|
191
|
+
});
|
|
192
|
+
it('getProtocolStatePDA differs by program', () => {
|
|
193
|
+
const mainnet = getProtocolStatePDA(MAINNET_PROGRAM_ID);
|
|
194
|
+
const devnet = getProtocolStatePDA(DEVNET_PROGRAM_ID);
|
|
195
|
+
expect(mainnet.equals(devnet)).toBe(false);
|
|
196
|
+
});
|
|
197
|
+
it('getMarketVaultPDA differs by market ID', () => {
|
|
198
|
+
const ps = getProtocolStatePDA(PROGRAM_ID);
|
|
199
|
+
const vault1 = getMarketVaultPDA(ps, 1, PROGRAM_ID);
|
|
200
|
+
const vault2 = getMarketVaultPDA(ps, 2, PROGRAM_ID);
|
|
201
|
+
expect(vault1.equals(vault2)).toBe(false);
|
|
202
|
+
});
|
|
203
|
+
it('getUserPositionPDA differs by user', () => {
|
|
204
|
+
const ps = getProtocolStatePDA(PROGRAM_ID);
|
|
205
|
+
const vault = getMarketVaultPDA(ps, 1, PROGRAM_ID);
|
|
206
|
+
const pos1 = getUserPositionPDA(vault, CCM_MINT, PROGRAM_ID);
|
|
207
|
+
const pos2 = getUserPositionPDA(vault, USDC_MINT, PROGRAM_ID);
|
|
208
|
+
expect(pos1.equals(pos2)).toBe(false);
|
|
209
|
+
});
|
|
210
|
+
it('getGlobalRootConfigPDA derives from CCM mint', () => {
|
|
211
|
+
const root = getGlobalRootConfigPDA(CCM_MINT, PROGRAM_ID);
|
|
212
|
+
expect(PublicKey.isOnCurve(root.toBuffer())).toBe(false); // PDAs are off-curve
|
|
213
|
+
});
|
|
214
|
+
it('getClaimStatePDA is unique per claimer', () => {
|
|
215
|
+
const claim1 = getClaimStatePDA(CCM_MINT, USDC_MINT, PROGRAM_ID);
|
|
216
|
+
const claim2 = getClaimStatePDA(CCM_MINT, CCM_MINT, PROGRAM_ID);
|
|
217
|
+
expect(claim1.equals(claim2)).toBe(false);
|
|
218
|
+
});
|
|
219
|
+
it('getChannelConfigV2PDA is unique per subject', () => {
|
|
220
|
+
const ch1 = getChannelConfigV2PDA(CCM_MINT, USDC_MINT, PROGRAM_ID);
|
|
221
|
+
const ch2 = getChannelConfigV2PDA(CCM_MINT, CCM_MINT, PROGRAM_ID);
|
|
222
|
+
expect(ch1.equals(ch2)).toBe(false);
|
|
223
|
+
});
|
|
224
|
+
it('getAta derives a valid off-curve address', () => {
|
|
225
|
+
const ata = getAta(CCM_MINT, USDC_MINT, TOKEN_PROGRAM_ID);
|
|
226
|
+
expect(PublicKey.isOnCurve(ata.toBuffer())).toBe(false);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
// ── Constants ───────────────────────────────────────────
|
|
230
|
+
describe('constants', () => {
|
|
231
|
+
it('PROGRAM_ID equals MAINNET_PROGRAM_ID', () => {
|
|
232
|
+
expect(PROGRAM_ID.equals(MAINNET_PROGRAM_ID)).toBe(true);
|
|
233
|
+
});
|
|
234
|
+
it('DEVNET_PROGRAM_ID is different from mainnet', () => {
|
|
235
|
+
expect(DEVNET_PROGRAM_ID.equals(MAINNET_PROGRAM_ID)).toBe(false);
|
|
236
|
+
});
|
|
237
|
+
it('TOKEN_PROGRAM_ID is the legacy SPL token program', () => {
|
|
238
|
+
expect(TOKEN_PROGRAM_ID.toBase58()).toBe('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
|
|
239
|
+
});
|
|
240
|
+
it('TOKEN_2022_PROGRAM_ID is the Token-2022 program', () => {
|
|
241
|
+
expect(TOKEN_2022_PROGRAM_ID.toBase58()).toBe('TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb');
|
|
242
|
+
});
|
|
243
|
+
it('DLMM_PROGRAM_ID matches Meteora mainnet', () => {
|
|
244
|
+
expect(DLMM_PROGRAM_ID.toBase58()).toBe('LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo');
|
|
245
|
+
});
|
|
246
|
+
it('seed strings match expected values', () => {
|
|
247
|
+
expect(PROTOCOL_STATE_SEED).toBe('protocol_state');
|
|
248
|
+
expect(MARKET_VAULT_SEED).toBe('market_vault');
|
|
249
|
+
expect(MARKET_POSITION_SEED).toBe('market_position');
|
|
250
|
+
expect(GLOBAL_ROOT_SEED).toBe('global_root');
|
|
251
|
+
expect(CLAIM_STATE_GLOBAL_SEED).toBe('claim_global');
|
|
252
|
+
expect(CHANNEL_CONFIG_V2_SEED).toBe('channel_cfg_v2');
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
// ── Account parsers ─────────────────────────────────────
|
|
256
|
+
describe('parseMarketVault', () => {
|
|
257
|
+
it('extracts depositMint at correct offset', () => {
|
|
258
|
+
// Build a fake MarketVault buffer: 8 disc + 1 bump + 8 marketId + 32 depositMint + ...
|
|
259
|
+
const buf = Buffer.alloc(8 + 1 + 8 + 32 + 32 + 32 + 16);
|
|
260
|
+
USDC_MINT.toBuffer().copy(buf, 8 + 1 + 8); // depositMint at offset 17
|
|
261
|
+
CCM_MINT.toBuffer().copy(buf, 8 + 1 + 8 + 32); // vlofiMint at offset 49
|
|
262
|
+
const vault = parseMarketVault(buf);
|
|
263
|
+
expect(vault.depositMint.equals(USDC_MINT)).toBe(true);
|
|
264
|
+
});
|
|
265
|
+
it('extracts vlofiMint at correct offset', () => {
|
|
266
|
+
const buf = Buffer.alloc(8 + 1 + 8 + 32 + 32 + 32 + 16);
|
|
267
|
+
USDC_MINT.toBuffer().copy(buf, 8 + 1 + 8);
|
|
268
|
+
CCM_MINT.toBuffer().copy(buf, 8 + 1 + 8 + 32);
|
|
269
|
+
const vault = parseMarketVault(buf);
|
|
270
|
+
expect(vault.vlofiMint.equals(CCM_MINT)).toBe(true);
|
|
271
|
+
});
|
|
272
|
+
it('reads bump from first byte after discriminator', () => {
|
|
273
|
+
const buf = Buffer.alloc(8 + 1 + 8 + 32 + 32 + 32 + 16);
|
|
274
|
+
buf[8] = 254; // bump
|
|
275
|
+
const vault = parseMarketVault(buf);
|
|
276
|
+
expect(vault.bump).toBe(254);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
describe('parseProtocolState', () => {
|
|
280
|
+
it('reads isInitialized correctly', () => {
|
|
281
|
+
const buf = Buffer.alloc(8 + 165);
|
|
282
|
+
buf[8] = 1; // isInitialized
|
|
283
|
+
const state = parseProtocolState(buf);
|
|
284
|
+
expect(state.isInitialized).toBe(true);
|
|
285
|
+
});
|
|
286
|
+
it('reads paused flag correctly', () => {
|
|
287
|
+
const buf = Buffer.alloc(8 + 165);
|
|
288
|
+
buf[8 + 162] = 1; // paused
|
|
289
|
+
const state = parseProtocolState(buf);
|
|
290
|
+
expect(state.paused).toBe(true);
|
|
291
|
+
});
|
|
292
|
+
it('reads version byte', () => {
|
|
293
|
+
const buf = Buffer.alloc(8 + 165);
|
|
294
|
+
buf[8 + 1] = 3; // version
|
|
295
|
+
const state = parseProtocolState(buf);
|
|
296
|
+
expect(state.version).toBe(3);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
describe('parseUserMarketPosition', () => {
|
|
300
|
+
it('returns null for data too short', () => {
|
|
301
|
+
const buf = Buffer.alloc(50);
|
|
302
|
+
expect(parseUserMarketPosition(buf)).toBeNull();
|
|
303
|
+
});
|
|
304
|
+
it('reads settled flag correctly', () => {
|
|
305
|
+
const buf = Buffer.alloc(8 + 100);
|
|
306
|
+
buf[8 + 89] = 1; // settled
|
|
307
|
+
const pos = parseUserMarketPosition(buf);
|
|
308
|
+
expect(pos).not.toBeNull();
|
|
309
|
+
expect(pos.settled).toBe(true);
|
|
310
|
+
});
|
|
311
|
+
it('reads depositedAmount as u64 LE', () => {
|
|
312
|
+
const buf = Buffer.alloc(8 + 100);
|
|
313
|
+
buf.writeBigUInt64LE(1000000n, 8 + 1 + 32 + 32); // depositedAmount at offset 73 from disc
|
|
314
|
+
const pos = parseUserMarketPosition(buf);
|
|
315
|
+
expect(pos).not.toBeNull();
|
|
316
|
+
expect(pos.depositedAmount).toBe(1000000n);
|
|
317
|
+
});
|
|
318
|
+
});
|
package/dist/nav.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NAV (Net Asset Value) helpers for vLOFI share computation.
|
|
3
|
+
*
|
|
4
|
+
* These pure functions mirror the on-chain deposit/settle math so that
|
|
5
|
+
* clients can preview shares received or principal returned without
|
|
6
|
+
* submitting a transaction.
|
|
7
|
+
*
|
|
8
|
+
* NAV BPS domain:
|
|
9
|
+
* - 10_000 = 1.00x (1 USDC per share, initial NAV)
|
|
10
|
+
* - 50_000 = 5.00x (max NAV, protocol ceiling)
|
|
11
|
+
* - 0 = uninitialized (pre-realloc vaults, treated as 10_000)
|
|
12
|
+
*/
|
|
13
|
+
/** Structured NAV data extracted from a MarketVault. */
|
|
14
|
+
export interface NavInfo {
|
|
15
|
+
/** NAV per share in basis points (10_000 = 1.0x). Zero means uninitialized. */
|
|
16
|
+
navPerShareBps: bigint;
|
|
17
|
+
/** Slot at which NAV was last updated on-chain. */
|
|
18
|
+
lastNavUpdateSlot: bigint;
|
|
19
|
+
/** Human-readable share price in USDC terms (navPerShareBps / 10_000). */
|
|
20
|
+
sharePrice: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Compute the number of vLOFI shares a deposit of `amount` base units will mint.
|
|
24
|
+
*
|
|
25
|
+
* Formula (matches vault.rs deposit_market):
|
|
26
|
+
* shares = amount * 10_000 / nav_per_share_bps
|
|
27
|
+
*
|
|
28
|
+
* If navPerShareBps is 0 (uninitialized / pre-realloc vault), falls back to
|
|
29
|
+
* the default 10_000 BPS (1:1 ratio).
|
|
30
|
+
*/
|
|
31
|
+
export declare function computeSharesForDeposit(amount: bigint, navPerShareBps: bigint): bigint;
|
|
32
|
+
/**
|
|
33
|
+
* Compute the USDC principal returned when settling `shares` of vLOFI.
|
|
34
|
+
*
|
|
35
|
+
* Formula (matches vault.rs settle_market):
|
|
36
|
+
* principal = shares * nav_per_share_bps / 10_000
|
|
37
|
+
*
|
|
38
|
+
* If navPerShareBps is 0 (uninitialized), falls back to 10_000 BPS.
|
|
39
|
+
*/
|
|
40
|
+
export declare function computePrincipalForSettle(shares: bigint, navPerShareBps: bigint): bigint;
|
package/dist/nav.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NAV (Net Asset Value) helpers for vLOFI share computation.
|
|
3
|
+
*
|
|
4
|
+
* These pure functions mirror the on-chain deposit/settle math so that
|
|
5
|
+
* clients can preview shares received or principal returned without
|
|
6
|
+
* submitting a transaction.
|
|
7
|
+
*
|
|
8
|
+
* NAV BPS domain:
|
|
9
|
+
* - 10_000 = 1.00x (1 USDC per share, initial NAV)
|
|
10
|
+
* - 50_000 = 5.00x (max NAV, protocol ceiling)
|
|
11
|
+
* - 0 = uninitialized (pre-realloc vaults, treated as 10_000)
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_NAV_BPS = 10000n;
|
|
14
|
+
const BPS_SCALE = 10000n;
|
|
15
|
+
/**
|
|
16
|
+
* Compute the number of vLOFI shares a deposit of `amount` base units will mint.
|
|
17
|
+
*
|
|
18
|
+
* Formula (matches vault.rs deposit_market):
|
|
19
|
+
* shares = amount * 10_000 / nav_per_share_bps
|
|
20
|
+
*
|
|
21
|
+
* If navPerShareBps is 0 (uninitialized / pre-realloc vault), falls back to
|
|
22
|
+
* the default 10_000 BPS (1:1 ratio).
|
|
23
|
+
*/
|
|
24
|
+
export function computeSharesForDeposit(amount, navPerShareBps) {
|
|
25
|
+
const effectiveNav = navPerShareBps === 0n ? DEFAULT_NAV_BPS : navPerShareBps;
|
|
26
|
+
return (amount * BPS_SCALE) / effectiveNav;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Compute the USDC principal returned when settling `shares` of vLOFI.
|
|
30
|
+
*
|
|
31
|
+
* Formula (matches vault.rs settle_market):
|
|
32
|
+
* principal = shares * nav_per_share_bps / 10_000
|
|
33
|
+
*
|
|
34
|
+
* If navPerShareBps is 0 (uninitialized), falls back to 10_000 BPS.
|
|
35
|
+
*/
|
|
36
|
+
export function computePrincipalForSettle(shares, navPerShareBps) {
|
|
37
|
+
const effectiveNav = navPerShareBps === 0n ? DEFAULT_NAV_BPS : navPerShareBps;
|
|
38
|
+
return (shares * effectiveNav) / BPS_SCALE;
|
|
39
|
+
}
|
package/dist/nav.test.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for NAV computation helpers (computeSharesForDeposit, computePrincipalForSettle).
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import { computeSharesForDeposit, computePrincipalForSettle } from './nav.js';
|
|
6
|
+
// ── computeSharesForDeposit ──────────────────────────────
|
|
7
|
+
describe('computeSharesForDeposit', () => {
|
|
8
|
+
it('returns 1:1 shares at default NAV (10_000 BPS)', () => {
|
|
9
|
+
const shares = computeSharesForDeposit(1000000n, 10000n);
|
|
10
|
+
expect(shares).toBe(1000000n);
|
|
11
|
+
});
|
|
12
|
+
it('returns half shares at 2x NAV (20_000 BPS)', () => {
|
|
13
|
+
const shares = computeSharesForDeposit(1000000n, 20000n);
|
|
14
|
+
expect(shares).toBe(500000n);
|
|
15
|
+
});
|
|
16
|
+
it('handles zero NAV fallback — treats as 10_000 BPS', () => {
|
|
17
|
+
// zero nav fallback: uninitialized vault should behave like 1:1
|
|
18
|
+
const shares = computeSharesForDeposit(1000000n, 0n);
|
|
19
|
+
expect(shares).toBe(1000000n);
|
|
20
|
+
});
|
|
21
|
+
it('handles max NAV of 50_000 BPS (5.0x)', () => {
|
|
22
|
+
// max nav: 50_000 BPS ceiling
|
|
23
|
+
const shares = computeSharesForDeposit(5000000n, 50000n);
|
|
24
|
+
expect(shares).toBe(1000000n);
|
|
25
|
+
});
|
|
26
|
+
it('returns 0 shares for 0 deposit amount', () => {
|
|
27
|
+
const shares = computeSharesForDeposit(0n, 10000n);
|
|
28
|
+
expect(shares).toBe(0n);
|
|
29
|
+
});
|
|
30
|
+
it('handles large deposit without overflow', () => {
|
|
31
|
+
// overflow test: large amount should not throw
|
|
32
|
+
const amount = 1000000000000n; // 1M USDC in base units
|
|
33
|
+
const shares = computeSharesForDeposit(amount, 10000n);
|
|
34
|
+
expect(shares).toBe(amount);
|
|
35
|
+
});
|
|
36
|
+
it('truncates fractional shares (integer division)', () => {
|
|
37
|
+
// 3 base units at 2x NAV = 1.5 → truncates to 1
|
|
38
|
+
const shares = computeSharesForDeposit(3n, 20000n);
|
|
39
|
+
expect(shares).toBe(1n);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
// ── computePrincipalForSettle ─────────────────────────────
|
|
43
|
+
describe('computePrincipalForSettle', () => {
|
|
44
|
+
it('returns 1:1 principal at default NAV (10_000 BPS)', () => {
|
|
45
|
+
const principal = computePrincipalForSettle(1000000n, 10000n);
|
|
46
|
+
expect(principal).toBe(1000000n);
|
|
47
|
+
});
|
|
48
|
+
it('returns double principal at 2x NAV (20_000 BPS)', () => {
|
|
49
|
+
const principal = computePrincipalForSettle(1000000n, 20000n);
|
|
50
|
+
expect(principal).toBe(2000000n);
|
|
51
|
+
});
|
|
52
|
+
it('handles zero NAV fallback — treats as 10_000 BPS', () => {
|
|
53
|
+
// zero nav fallback
|
|
54
|
+
const principal = computePrincipalForSettle(1000000n, 0n);
|
|
55
|
+
expect(principal).toBe(1000000n);
|
|
56
|
+
});
|
|
57
|
+
it('handles max NAV of 50_000 BPS (5.0x)', () => {
|
|
58
|
+
// max nav: 50_000 BPS ceiling
|
|
59
|
+
const principal = computePrincipalForSettle(1000000n, 50000n);
|
|
60
|
+
expect(principal).toBe(5000000n);
|
|
61
|
+
});
|
|
62
|
+
it('returns 0 principal for 0 shares', () => {
|
|
63
|
+
const principal = computePrincipalForSettle(0n, 10000n);
|
|
64
|
+
expect(principal).toBe(0n);
|
|
65
|
+
});
|
|
66
|
+
it('handles large shares without overflow', () => {
|
|
67
|
+
// overflow test: large shares
|
|
68
|
+
const shares = 1000000000000n;
|
|
69
|
+
const principal = computePrincipalForSettle(shares, 10000n);
|
|
70
|
+
expect(principal).toBe(shares);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
// ── NavInfo type ─────────────────────────────────────────
|
|
74
|
+
describe('NavInfo type', () => {
|
|
75
|
+
it('can be constructed with valid fields', () => {
|
|
76
|
+
const info = {
|
|
77
|
+
navPerShareBps: 15000n,
|
|
78
|
+
lastNavUpdateSlot: 405000000n,
|
|
79
|
+
sharePrice: 1.5,
|
|
80
|
+
};
|
|
81
|
+
expect(info.navPerShareBps).toBe(15000n);
|
|
82
|
+
expect(info.lastNavUpdateSlot).toBe(405000000n);
|
|
83
|
+
expect(info.sharePrice).toBe(1.5);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
// ── Round-trip deposit→settle ────────────────────────────
|
|
87
|
+
describe('deposit-settle round trip', () => {
|
|
88
|
+
it('deposit then settle at same NAV returns original amount', () => {
|
|
89
|
+
const amount = 1000000n;
|
|
90
|
+
const nav = 12500n;
|
|
91
|
+
const shares = computeSharesForDeposit(amount, nav);
|
|
92
|
+
const principal = computePrincipalForSettle(shares, nav);
|
|
93
|
+
// Due to integer division, principal <= amount
|
|
94
|
+
expect(principal).toBeLessThanOrEqual(amount);
|
|
95
|
+
// But should be close (within 1 base unit rounding)
|
|
96
|
+
expect(amount - principal).toBeLessThan(nav);
|
|
97
|
+
});
|
|
98
|
+
});
|