@mania-labs/mania-sdk 1.0.0 → 1.0.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 +5 -5
- package/dist/index.js +32 -17
- package/dist/index.mjs +32 -17
- package/package.json +7 -1
- package/src/.claude/settings.local.json +2 -1
- package/src/__tests__/helpers/fixtures.ts +136 -0
- package/src/__tests__/helpers/integrationHelpers.ts +258 -0
- package/src/__tests__/helpers/mocks.ts +184 -0
- package/src/__tests__/integration/sdk-read.test.ts +196 -0
- package/src/__tests__/integration/sdk-write.test.ts +383 -0
- package/src/__tests__/setup.ts +34 -0
- package/src/__tests__/unit/bondingCurve.test.ts +357 -0
- package/src/__tests__/unit/constants.test.ts +136 -0
- package/src/__tests__/unit/mania.test.ts +495 -0
- package/src/__tests__/unit/utils.test.ts +328 -0
- package/src/constants.ts +13 -2
- package/src/mania.ts +15 -10
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
|
2
|
+
import { parseEther, formatEther, type Address } from 'viem';
|
|
3
|
+
import { ManiaSDK } from '../../mania.js';
|
|
4
|
+
import {
|
|
5
|
+
createTestSDK,
|
|
6
|
+
getBalanceSnapshot,
|
|
7
|
+
compareBalances,
|
|
8
|
+
getTokenBalance,
|
|
9
|
+
hasEnoughETH,
|
|
10
|
+
generateUniqueTokenParams,
|
|
11
|
+
withTestRetry,
|
|
12
|
+
type BalanceSnapshot,
|
|
13
|
+
} from '../helpers/integrationHelpers.js';
|
|
14
|
+
import { RUN_INTEGRATION_TESTS } from '../setup.js';
|
|
15
|
+
|
|
16
|
+
describe.skipIf(!RUN_INTEGRATION_TESTS)('SDK Write Methods - Integration', () => {
|
|
17
|
+
let sdk: ManiaSDK;
|
|
18
|
+
let walletAddress: Address;
|
|
19
|
+
let publicClient: ReturnType<typeof createTestSDK>['publicClient'];
|
|
20
|
+
|
|
21
|
+
// Token created during tests
|
|
22
|
+
let createdTokenAddress: Address | undefined;
|
|
23
|
+
|
|
24
|
+
// Minimum ETH required for tests
|
|
25
|
+
const MIN_ETH_REQUIRED = parseEther('0.5');
|
|
26
|
+
|
|
27
|
+
beforeAll(async () => {
|
|
28
|
+
const testSetup = createTestSDK();
|
|
29
|
+
sdk = testSetup.sdk;
|
|
30
|
+
walletAddress = testSetup.walletAddress;
|
|
31
|
+
publicClient = testSetup.publicClient;
|
|
32
|
+
|
|
33
|
+
console.log(`Running write integration tests with wallet: ${walletAddress}`);
|
|
34
|
+
|
|
35
|
+
// Check wallet has enough ETH
|
|
36
|
+
const hasEnough = await hasEnoughETH(publicClient, walletAddress, MIN_ETH_REQUIRED);
|
|
37
|
+
if (!hasEnough) {
|
|
38
|
+
const balance = await publicClient.getBalance({ address: walletAddress });
|
|
39
|
+
console.warn(
|
|
40
|
+
`Warning: Wallet balance (${formatEther(balance)} ETH) may be insufficient for all tests. ` +
|
|
41
|
+
`Recommended: ${formatEther(MIN_ETH_REQUIRED)} ETH`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('create', () => {
|
|
47
|
+
it('should create a new token', async () => {
|
|
48
|
+
const params = generateUniqueTokenParams();
|
|
49
|
+
const balanceBefore = await getBalanceSnapshot(publicClient, walletAddress);
|
|
50
|
+
|
|
51
|
+
const result = await withTestRetry(() =>
|
|
52
|
+
sdk.create({
|
|
53
|
+
...params,
|
|
54
|
+
creator: walletAddress,
|
|
55
|
+
})
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(result.hash).toMatch(/^0x[a-fA-F0-9]{64}$/);
|
|
59
|
+
expect(result.success).toBe(true);
|
|
60
|
+
expect(result.tokenAddress).toMatch(/^0x[a-fA-F0-9]{40}$/);
|
|
61
|
+
|
|
62
|
+
createdTokenAddress = result.tokenAddress;
|
|
63
|
+
|
|
64
|
+
// Verify bonding curve exists
|
|
65
|
+
const curve = await sdk.getBondingCurve(createdTokenAddress!);
|
|
66
|
+
expect(curve.tokenTotalSupply).toBeGreaterThan(0n);
|
|
67
|
+
expect(curve.complete).toBe(false);
|
|
68
|
+
|
|
69
|
+
// Verify ETH was spent (gas only)
|
|
70
|
+
const balanceAfter = await getBalanceSnapshot(publicClient, walletAddress);
|
|
71
|
+
const diff = compareBalances(balanceBefore, balanceAfter);
|
|
72
|
+
expect(diff.ethDiff).toBeLessThan(0n); // Spent gas
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('createAndBuy', () => {
|
|
77
|
+
it('should create token and buy in one transaction', async () => {
|
|
78
|
+
const params = generateUniqueTokenParams();
|
|
79
|
+
const buyAmount = parseEther('0.01');
|
|
80
|
+
const balanceBefore = await getBalanceSnapshot(publicClient, walletAddress);
|
|
81
|
+
|
|
82
|
+
const result = await withTestRetry(() =>
|
|
83
|
+
sdk.createAndBuy({
|
|
84
|
+
...params,
|
|
85
|
+
creator: walletAddress,
|
|
86
|
+
buyAmountEth: buyAmount,
|
|
87
|
+
minTokensOut: 0n, // Accept any amount for test
|
|
88
|
+
})
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
expect(result.hash).toMatch(/^0x[a-fA-F0-9]{64}$/);
|
|
92
|
+
expect(result.success).toBe(true);
|
|
93
|
+
expect(result.tokenAddress).toMatch(/^0x[a-fA-F0-9]{40}$/);
|
|
94
|
+
|
|
95
|
+
const tokenAddress = result.tokenAddress!;
|
|
96
|
+
|
|
97
|
+
// Verify token balance > 0
|
|
98
|
+
const tokenBalance = await getTokenBalance(publicClient, tokenAddress, walletAddress);
|
|
99
|
+
expect(tokenBalance).toBeGreaterThan(0n);
|
|
100
|
+
console.log(`Bought ${formatEther(tokenBalance)} tokens`);
|
|
101
|
+
|
|
102
|
+
// Verify bonding curve has real ETH reserves
|
|
103
|
+
const curve = await sdk.getBondingCurve(tokenAddress);
|
|
104
|
+
expect(curve.realEthReserves).toBeGreaterThan(0n);
|
|
105
|
+
|
|
106
|
+
// Verify ETH was spent (buyAmount + gas)
|
|
107
|
+
const balanceAfter = await getBalanceSnapshot(publicClient, walletAddress);
|
|
108
|
+
const diff = compareBalances(balanceBefore, balanceAfter);
|
|
109
|
+
expect(diff.ethDiff).toBeLessThan(-buyAmount / 2n); // At least half of buy amount spent
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('buy', () => {
|
|
114
|
+
it('should buy tokens and increase balance', async () => {
|
|
115
|
+
// Create a fresh token for this test
|
|
116
|
+
const params = generateUniqueTokenParams();
|
|
117
|
+
const createResult = await withTestRetry(() =>
|
|
118
|
+
sdk.create({
|
|
119
|
+
...params,
|
|
120
|
+
creator: walletAddress,
|
|
121
|
+
})
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
if (!createResult.tokenAddress) {
|
|
125
|
+
throw new Error('Token creation failed');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const tokenAddress = createResult.tokenAddress;
|
|
129
|
+
const tokenBalanceBefore = await getTokenBalance(publicClient, tokenAddress, walletAddress);
|
|
130
|
+
const ethBalanceBefore = await publicClient.getBalance({ address: walletAddress });
|
|
131
|
+
|
|
132
|
+
const buyAmount = parseEther('0.01');
|
|
133
|
+
|
|
134
|
+
const result = await withTestRetry(() =>
|
|
135
|
+
sdk.buy({
|
|
136
|
+
token: tokenAddress,
|
|
137
|
+
amountEth: buyAmount,
|
|
138
|
+
minTokensOut: 0n,
|
|
139
|
+
})
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
expect(result.hash).toMatch(/^0x[a-fA-F0-9]{64}$/);
|
|
143
|
+
expect(result.success).toBe(true);
|
|
144
|
+
|
|
145
|
+
// Verify token balance increased
|
|
146
|
+
const tokenBalanceAfter = await getTokenBalance(publicClient, tokenAddress, walletAddress);
|
|
147
|
+
expect(tokenBalanceAfter).toBeGreaterThan(tokenBalanceBefore);
|
|
148
|
+
console.log(
|
|
149
|
+
`Token balance: ${formatEther(tokenBalanceBefore)} -> ${formatEther(tokenBalanceAfter)}`
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Verify ETH balance decreased
|
|
153
|
+
const ethBalanceAfter = await publicClient.getBalance({ address: walletAddress });
|
|
154
|
+
expect(ethBalanceAfter).toBeLessThan(ethBalanceBefore);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should fail with unrealistic minTokensOut', async () => {
|
|
158
|
+
// Create a fresh token for this test
|
|
159
|
+
const params = generateUniqueTokenParams();
|
|
160
|
+
const createResult = await withTestRetry(() =>
|
|
161
|
+
sdk.create({
|
|
162
|
+
...params,
|
|
163
|
+
creator: walletAddress,
|
|
164
|
+
})
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
if (!createResult.tokenAddress) {
|
|
168
|
+
throw new Error('Token creation failed');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const tokenAddress = createResult.tokenAddress;
|
|
172
|
+
const buyAmount = parseEther('0.001');
|
|
173
|
+
const unrealisticMinTokens = parseEther('999999999999'); // Way too high
|
|
174
|
+
|
|
175
|
+
await expect(
|
|
176
|
+
sdk.buy({
|
|
177
|
+
token: tokenAddress,
|
|
178
|
+
amountEth: buyAmount,
|
|
179
|
+
minTokensOut: unrealisticMinTokens,
|
|
180
|
+
})
|
|
181
|
+
).rejects.toThrow();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('buyWithSlippage', () => {
|
|
186
|
+
it('should buy with automatic slippage calculation', async () => {
|
|
187
|
+
// Create a fresh token for this test
|
|
188
|
+
const params = generateUniqueTokenParams();
|
|
189
|
+
const createResult = await withTestRetry(() =>
|
|
190
|
+
sdk.create({
|
|
191
|
+
...params,
|
|
192
|
+
creator: walletAddress,
|
|
193
|
+
})
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
if (!createResult.tokenAddress) {
|
|
197
|
+
throw new Error('Token creation failed');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const tokenAddress = createResult.tokenAddress;
|
|
201
|
+
const buyAmount = parseEther('0.01');
|
|
202
|
+
|
|
203
|
+
const result = await withTestRetry(() =>
|
|
204
|
+
sdk.buyWithSlippage(tokenAddress, buyAmount, 500) // 5% slippage
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
expect(result.hash).toMatch(/^0x[a-fA-F0-9]{64}$/);
|
|
208
|
+
expect(result.success).toBe(true);
|
|
209
|
+
|
|
210
|
+
// Verify tokens received
|
|
211
|
+
const tokenBalance = await getTokenBalance(publicClient, tokenAddress, walletAddress);
|
|
212
|
+
expect(tokenBalance).toBeGreaterThan(0n);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('sell', () => {
|
|
217
|
+
it('should sell tokens and receive ETH', async () => {
|
|
218
|
+
// Create and buy tokens first
|
|
219
|
+
const params = generateUniqueTokenParams();
|
|
220
|
+
const createResult = await withTestRetry(() =>
|
|
221
|
+
sdk.createAndBuy({
|
|
222
|
+
...params,
|
|
223
|
+
creator: walletAddress,
|
|
224
|
+
buyAmountEth: parseEther('0.02'),
|
|
225
|
+
minTokensOut: 0n,
|
|
226
|
+
})
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
if (!createResult.tokenAddress) {
|
|
230
|
+
throw new Error('Token creation failed');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const tokenAddress = createResult.tokenAddress;
|
|
234
|
+
|
|
235
|
+
// Get balances before sell
|
|
236
|
+
const tokenBalanceBefore = await getTokenBalance(publicClient, tokenAddress, walletAddress);
|
|
237
|
+
const ethBalanceBefore = await publicClient.getBalance({ address: walletAddress });
|
|
238
|
+
|
|
239
|
+
// Sell half of tokens
|
|
240
|
+
const sellAmount = tokenBalanceBefore / 2n;
|
|
241
|
+
|
|
242
|
+
const result = await withTestRetry(() =>
|
|
243
|
+
sdk.sell({
|
|
244
|
+
token: tokenAddress,
|
|
245
|
+
amountTokens: sellAmount,
|
|
246
|
+
minEthOut: 0n, // Accept any amount for test
|
|
247
|
+
})
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
expect(result.hash).toMatch(/^0x[a-fA-F0-9]{64}$/);
|
|
251
|
+
expect(result.success).toBe(true);
|
|
252
|
+
|
|
253
|
+
// Verify token balance decreased
|
|
254
|
+
const tokenBalanceAfter = await getTokenBalance(publicClient, tokenAddress, walletAddress);
|
|
255
|
+
expect(tokenBalanceAfter).toBeLessThan(tokenBalanceBefore);
|
|
256
|
+
expect(tokenBalanceAfter).toBe(tokenBalanceBefore - sellAmount);
|
|
257
|
+
console.log(
|
|
258
|
+
`Token balance: ${formatEther(tokenBalanceBefore)} -> ${formatEther(tokenBalanceAfter)}`
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
// Verify ETH balance increased (accounting for gas)
|
|
262
|
+
const ethBalanceAfter = await publicClient.getBalance({ address: walletAddress });
|
|
263
|
+
// Note: ETH might slightly decrease due to gas, but should be close
|
|
264
|
+
const ethChange = ethBalanceAfter - ethBalanceBefore;
|
|
265
|
+
console.log(`ETH change: ${formatEther(ethChange)}`);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe('sellWithSlippage', () => {
|
|
270
|
+
it('should sell with automatic slippage calculation', async () => {
|
|
271
|
+
// Create and buy tokens first
|
|
272
|
+
const params = generateUniqueTokenParams();
|
|
273
|
+
const createResult = await withTestRetry(() =>
|
|
274
|
+
sdk.createAndBuy({
|
|
275
|
+
...params,
|
|
276
|
+
creator: walletAddress,
|
|
277
|
+
buyAmountEth: parseEther('0.02'),
|
|
278
|
+
minTokensOut: 0n,
|
|
279
|
+
})
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
if (!createResult.tokenAddress) {
|
|
283
|
+
throw new Error('Token creation failed');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const tokenAddress = createResult.tokenAddress;
|
|
287
|
+
const tokenBalance = await getTokenBalance(publicClient, tokenAddress, walletAddress);
|
|
288
|
+
|
|
289
|
+
// Sell 25% of tokens
|
|
290
|
+
const sellAmount = tokenBalance / 4n;
|
|
291
|
+
|
|
292
|
+
const result = await withTestRetry(() =>
|
|
293
|
+
sdk.sellWithSlippage(tokenAddress, sellAmount, 500) // 5% slippage
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
expect(result.hash).toMatch(/^0x[a-fA-F0-9]{64}$/);
|
|
297
|
+
expect(result.success).toBe(true);
|
|
298
|
+
|
|
299
|
+
// Verify tokens were sold
|
|
300
|
+
const tokenBalanceAfter = await getTokenBalance(publicClient, tokenAddress, walletAddress);
|
|
301
|
+
expect(tokenBalanceAfter).toBeLessThan(tokenBalance);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe('Balance verification summary', () => {
|
|
306
|
+
it('should track full buy/sell cycle balances', async () => {
|
|
307
|
+
// Create fresh token
|
|
308
|
+
const params = generateUniqueTokenParams();
|
|
309
|
+
|
|
310
|
+
// Initial balances
|
|
311
|
+
const initialEth = await publicClient.getBalance({ address: walletAddress });
|
|
312
|
+
console.log(`\n=== Balance Tracking Test ===`);
|
|
313
|
+
console.log(`Initial ETH: ${formatEther(initialEth)}`);
|
|
314
|
+
|
|
315
|
+
// Create and buy
|
|
316
|
+
const buyAmount = parseEther('0.02');
|
|
317
|
+
const createResult = await withTestRetry(() =>
|
|
318
|
+
sdk.createAndBuy({
|
|
319
|
+
...params,
|
|
320
|
+
creator: walletAddress,
|
|
321
|
+
buyAmountEth: buyAmount,
|
|
322
|
+
minTokensOut: 0n,
|
|
323
|
+
})
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
const tokenAddress = createResult.tokenAddress!;
|
|
327
|
+
const tokensReceived = await getTokenBalance(publicClient, tokenAddress, walletAddress);
|
|
328
|
+
const ethAfterBuy = await publicClient.getBalance({ address: walletAddress });
|
|
329
|
+
|
|
330
|
+
console.log(`After createAndBuy:`);
|
|
331
|
+
console.log(` Tokens received: ${formatEther(tokensReceived)}`);
|
|
332
|
+
console.log(` ETH spent: ${formatEther(initialEth - ethAfterBuy)}`);
|
|
333
|
+
|
|
334
|
+
// Sell all tokens
|
|
335
|
+
const sellResult = await withTestRetry(() =>
|
|
336
|
+
sdk.sell({
|
|
337
|
+
token: tokenAddress,
|
|
338
|
+
amountTokens: tokensReceived,
|
|
339
|
+
minEthOut: 0n,
|
|
340
|
+
})
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
const tokensAfterSell = await getTokenBalance(publicClient, tokenAddress, walletAddress);
|
|
344
|
+
const ethAfterSell = await publicClient.getBalance({ address: walletAddress });
|
|
345
|
+
|
|
346
|
+
console.log(`After sell:`);
|
|
347
|
+
console.log(` Tokens remaining: ${formatEther(tokensAfterSell)}`);
|
|
348
|
+
console.log(` ETH recovered: ${formatEther(ethAfterSell - ethAfterBuy)}`);
|
|
349
|
+
console.log(` Total ETH lost (fees + gas): ${formatEther(initialEth - ethAfterSell)}`);
|
|
350
|
+
|
|
351
|
+
// Verify all tokens were sold
|
|
352
|
+
expect(tokensAfterSell).toBe(0n);
|
|
353
|
+
|
|
354
|
+
// Verify we lost some ETH to fees (but not all)
|
|
355
|
+
expect(ethAfterSell).toBeLessThan(initialEth);
|
|
356
|
+
expect(ethAfterSell).toBeGreaterThan(initialEth - buyAmount);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// Migration test - skip by default as it requires a complete curve
|
|
361
|
+
describe.skip('migrate', () => {
|
|
362
|
+
it('should migrate complete curve to Uniswap', async () => {
|
|
363
|
+
const completedToken = process.env.TEST_COMPLETED_TOKEN_ADDRESS as Address | undefined;
|
|
364
|
+
if (!completedToken) {
|
|
365
|
+
console.log('Skipping migration test - no completed token provided');
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Verify curve is complete
|
|
370
|
+
const isComplete = await sdk.isComplete(completedToken);
|
|
371
|
+
expect(isComplete).toBe(true);
|
|
372
|
+
|
|
373
|
+
const result = await sdk.migrate({ token: completedToken });
|
|
374
|
+
|
|
375
|
+
expect(result.hash).toMatch(/^0x[a-fA-F0-9]{64}$/);
|
|
376
|
+
expect(result.success).toBe(true);
|
|
377
|
+
|
|
378
|
+
// Verify token is now migrated
|
|
379
|
+
const isMigrated = await sdk.isMigrated(completedToken);
|
|
380
|
+
expect(isMigrated).toBe(true);
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { config } from 'dotenv';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
|
|
4
|
+
// Load test environment variables
|
|
5
|
+
config({ path: resolve(process.cwd(), '.env.test') });
|
|
6
|
+
|
|
7
|
+
// Validate required environment variables for integration tests
|
|
8
|
+
export function validateIntegrationEnv(): boolean {
|
|
9
|
+
const required = ['TEST_PRIVATE_KEY', 'MEGA_ETH_RPC_URL'];
|
|
10
|
+
const missing = required.filter((key) => !process.env[key]);
|
|
11
|
+
|
|
12
|
+
if (missing.length > 0) {
|
|
13
|
+
console.warn(
|
|
14
|
+
`Missing environment variables for integration tests: ${missing.join(', ')}`
|
|
15
|
+
);
|
|
16
|
+
console.warn('Integration tests will be skipped.');
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Check if integration tests should run
|
|
24
|
+
export const RUN_INTEGRATION_TESTS =
|
|
25
|
+
process.env.RUN_INTEGRATION_TESTS === 'true' && validateIntegrationEnv();
|
|
26
|
+
|
|
27
|
+
// Global test setup
|
|
28
|
+
beforeAll(() => {
|
|
29
|
+
// Any global setup can go here
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterAll(() => {
|
|
33
|
+
// Any global cleanup can go here
|
|
34
|
+
});
|