@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,184 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
import type { Address, PublicClient, WalletClient, Hash } from 'viem';
|
|
3
|
+
import type { BondingCurveState, GlobalState } from '../../types.js';
|
|
4
|
+
import {
|
|
5
|
+
CURVE_SCENARIOS,
|
|
6
|
+
DEFAULT_GLOBAL_STATE,
|
|
7
|
+
TEST_ADDRESSES,
|
|
8
|
+
} from './fixtures.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create a mock bonding curve state with optional overrides
|
|
12
|
+
*/
|
|
13
|
+
export function createMockBondingCurveState(
|
|
14
|
+
overrides: Partial<BondingCurveState> = {}
|
|
15
|
+
): BondingCurveState {
|
|
16
|
+
return {
|
|
17
|
+
...CURVE_SCENARIOS.FRESH,
|
|
18
|
+
...overrides,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a mock global state with optional overrides
|
|
24
|
+
*/
|
|
25
|
+
export function createMockGlobalState(
|
|
26
|
+
overrides: Partial<GlobalState> = {}
|
|
27
|
+
): GlobalState {
|
|
28
|
+
return {
|
|
29
|
+
...DEFAULT_GLOBAL_STATE,
|
|
30
|
+
...overrides,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Mock transaction hash
|
|
36
|
+
*/
|
|
37
|
+
export const MOCK_TX_HASH =
|
|
38
|
+
'0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' as Hash;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create a mock public client with configurable responses
|
|
42
|
+
*/
|
|
43
|
+
export function createMockPublicClient(config: {
|
|
44
|
+
bondingCurve?: BondingCurveState;
|
|
45
|
+
globalState?: GlobalState;
|
|
46
|
+
buyQuote?: bigint;
|
|
47
|
+
sellQuote?: bigint;
|
|
48
|
+
fee?: bigint;
|
|
49
|
+
userVolume?: bigint;
|
|
50
|
+
} = {}): PublicClient {
|
|
51
|
+
const mockClient = {
|
|
52
|
+
readContract: vi.fn().mockImplementation(({ functionName, args }) => {
|
|
53
|
+
switch (functionName) {
|
|
54
|
+
case 'global':
|
|
55
|
+
return Promise.resolve([
|
|
56
|
+
config.globalState?.initialized ?? DEFAULT_GLOBAL_STATE.initialized,
|
|
57
|
+
config.globalState?.authority ?? DEFAULT_GLOBAL_STATE.authority,
|
|
58
|
+
config.globalState?.feeRecipient ?? DEFAULT_GLOBAL_STATE.feeRecipient,
|
|
59
|
+
config.globalState?.initialVirtualTokenReserves ??
|
|
60
|
+
DEFAULT_GLOBAL_STATE.initialVirtualTokenReserves,
|
|
61
|
+
config.globalState?.initialVirtualEthReserves ??
|
|
62
|
+
DEFAULT_GLOBAL_STATE.initialVirtualEthReserves,
|
|
63
|
+
config.globalState?.initialRealTokenReserves ??
|
|
64
|
+
DEFAULT_GLOBAL_STATE.initialRealTokenReserves,
|
|
65
|
+
config.globalState?.tokenTotalSupply ??
|
|
66
|
+
DEFAULT_GLOBAL_STATE.tokenTotalSupply,
|
|
67
|
+
config.globalState?.feeBasisPoints ??
|
|
68
|
+
DEFAULT_GLOBAL_STATE.feeBasisPoints,
|
|
69
|
+
config.globalState?.withdrawAuthority ??
|
|
70
|
+
DEFAULT_GLOBAL_STATE.withdrawAuthority,
|
|
71
|
+
config.globalState?.enableMigrate ??
|
|
72
|
+
DEFAULT_GLOBAL_STATE.enableMigrate,
|
|
73
|
+
config.globalState?.poolMigrationFee ??
|
|
74
|
+
DEFAULT_GLOBAL_STATE.poolMigrationFee,
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
case 'getBondingCurve': {
|
|
78
|
+
const curve = config.bondingCurve ?? CURVE_SCENARIOS.FRESH;
|
|
79
|
+
return Promise.resolve({
|
|
80
|
+
virtualTokenReserves: curve.virtualTokenReserves,
|
|
81
|
+
virtualEthReserves: curve.virtualEthReserves,
|
|
82
|
+
realTokenReserves: curve.realTokenReserves,
|
|
83
|
+
realEthReserves: curve.realEthReserves,
|
|
84
|
+
tokenTotalSupply: curve.tokenTotalSupply,
|
|
85
|
+
complete: curve.complete,
|
|
86
|
+
trackVolume: curve.trackVolume,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
case 'getBuyQuote':
|
|
91
|
+
return Promise.resolve(config.buyQuote ?? 1000000000000000000000n);
|
|
92
|
+
|
|
93
|
+
case 'getSellQuote':
|
|
94
|
+
return Promise.resolve(config.sellQuote ?? 10000000000000000n);
|
|
95
|
+
|
|
96
|
+
case 'getFee':
|
|
97
|
+
return Promise.resolve(config.fee ?? (args[0] as bigint) / 100n);
|
|
98
|
+
|
|
99
|
+
case 'userVolume':
|
|
100
|
+
return Promise.resolve(config.userVolume ?? 0n);
|
|
101
|
+
|
|
102
|
+
default:
|
|
103
|
+
return Promise.resolve(undefined);
|
|
104
|
+
}
|
|
105
|
+
}),
|
|
106
|
+
|
|
107
|
+
watchContractEvent: vi.fn().mockReturnValue(() => {}),
|
|
108
|
+
|
|
109
|
+
waitForTransactionReceipt: vi.fn().mockResolvedValue({
|
|
110
|
+
status: 'success',
|
|
111
|
+
logs: [],
|
|
112
|
+
}),
|
|
113
|
+
|
|
114
|
+
getBalance: vi.fn().mockResolvedValue(10000000000000000000n), // 10 ETH
|
|
115
|
+
} as unknown as PublicClient;
|
|
116
|
+
|
|
117
|
+
return mockClient;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Create a mock wallet client
|
|
122
|
+
*/
|
|
123
|
+
export function createMockWalletClient(config: {
|
|
124
|
+
address?: Address;
|
|
125
|
+
txHash?: Hash;
|
|
126
|
+
} = {}): WalletClient {
|
|
127
|
+
const address = config.address ?? TEST_ADDRESSES.USER1;
|
|
128
|
+
const txHash = config.txHash ?? MOCK_TX_HASH;
|
|
129
|
+
|
|
130
|
+
const mockClient = {
|
|
131
|
+
account: {
|
|
132
|
+
address,
|
|
133
|
+
},
|
|
134
|
+
writeContract: vi.fn().mockResolvedValue(txHash),
|
|
135
|
+
} as unknown as WalletClient;
|
|
136
|
+
|
|
137
|
+
return mockClient;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Create mock event logs for CreateEvent
|
|
142
|
+
*/
|
|
143
|
+
export function createMockCreateEventLogs(tokenAddress: Address) {
|
|
144
|
+
return [
|
|
145
|
+
{
|
|
146
|
+
args: {
|
|
147
|
+
name: 'Test Token',
|
|
148
|
+
symbol: 'TEST',
|
|
149
|
+
uri: 'https://example.com/metadata.json',
|
|
150
|
+
mint: tokenAddress,
|
|
151
|
+
user: TEST_ADDRESSES.USER1,
|
|
152
|
+
creator: TEST_ADDRESSES.USER1,
|
|
153
|
+
timestamp: BigInt(Date.now()),
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Create mock event logs for TradeEvent
|
|
161
|
+
*/
|
|
162
|
+
export function createMockTradeEventLogs(config: {
|
|
163
|
+
token: Address;
|
|
164
|
+
ethAmount: bigint;
|
|
165
|
+
tokenAmount: bigint;
|
|
166
|
+
isBuy: boolean;
|
|
167
|
+
}) {
|
|
168
|
+
return [
|
|
169
|
+
{
|
|
170
|
+
args: {
|
|
171
|
+
mint: config.token,
|
|
172
|
+
ethAmount: config.ethAmount,
|
|
173
|
+
tokenAmount: config.tokenAmount,
|
|
174
|
+
isBuy: config.isBuy,
|
|
175
|
+
user: TEST_ADDRESSES.USER1,
|
|
176
|
+
timestamp: BigInt(Date.now()),
|
|
177
|
+
virtualEthReserves: 1000000000000000000n,
|
|
178
|
+
virtualTokenReserves: 1000000000000000000000000000n,
|
|
179
|
+
realEthReserves: 100000000000000000n,
|
|
180
|
+
realTokenReserves: 700000000000000000000000000n,
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
2
|
+
import { parseEther, type Address } from 'viem';
|
|
3
|
+
import { ManiaSDK } from '../../mania.js';
|
|
4
|
+
import { createTestSDK, withTestRetry } from '../helpers/integrationHelpers.js';
|
|
5
|
+
import { RUN_INTEGRATION_TESTS } from '../setup.js';
|
|
6
|
+
|
|
7
|
+
describe.skipIf(!RUN_INTEGRATION_TESTS)('SDK Read Methods - Integration', () => {
|
|
8
|
+
let sdk: ManiaSDK;
|
|
9
|
+
let walletAddress: Address;
|
|
10
|
+
let testTokenAddress: Address | undefined;
|
|
11
|
+
|
|
12
|
+
beforeAll(async () => {
|
|
13
|
+
const testSetup = createTestSDK();
|
|
14
|
+
sdk = testSetup.sdk;
|
|
15
|
+
walletAddress = testSetup.walletAddress;
|
|
16
|
+
|
|
17
|
+
// Use existing test token if provided in environment
|
|
18
|
+
testTokenAddress = process.env.TEST_TOKEN_ADDRESS as Address | undefined;
|
|
19
|
+
|
|
20
|
+
console.log(`Running integration tests with wallet: ${walletAddress}`);
|
|
21
|
+
if (testTokenAddress) {
|
|
22
|
+
console.log(`Using test token: ${testTokenAddress}`);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('getGlobalState', () => {
|
|
27
|
+
it('should return valid global state', async () => {
|
|
28
|
+
const state = await withTestRetry(() => sdk.getGlobalState());
|
|
29
|
+
|
|
30
|
+
expect(state.initialized).toBe(true);
|
|
31
|
+
expect(state.authority).toMatch(/^0x[a-fA-F0-9]{40}$/);
|
|
32
|
+
expect(state.feeRecipient).toMatch(/^0x[a-fA-F0-9]{40}$/);
|
|
33
|
+
expect(state.feeBasisPoints).toBeGreaterThan(0n);
|
|
34
|
+
expect(state.feeBasisPoints).toBeLessThanOrEqual(1000n); // Max 10%
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should have valid reserve configurations', async () => {
|
|
38
|
+
const state = await withTestRetry(() => sdk.getGlobalState());
|
|
39
|
+
|
|
40
|
+
expect(state.initialVirtualTokenReserves).toBeGreaterThan(0n);
|
|
41
|
+
expect(state.initialVirtualEthReserves).toBeGreaterThan(0n);
|
|
42
|
+
expect(state.initialRealTokenReserves).toBeGreaterThan(0n);
|
|
43
|
+
expect(state.tokenTotalSupply).toBeGreaterThan(0n);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should have migration enabled', async () => {
|
|
47
|
+
const state = await withTestRetry(() => sdk.getGlobalState());
|
|
48
|
+
|
|
49
|
+
expect(state.enableMigrate).toBe(true);
|
|
50
|
+
expect(state.poolMigrationFee).toBeGreaterThanOrEqual(0n);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('getFee', () => {
|
|
55
|
+
it('should calculate fee correctly', async () => {
|
|
56
|
+
const amount = parseEther('1');
|
|
57
|
+
const fee = await withTestRetry(() => sdk.getFee(amount));
|
|
58
|
+
|
|
59
|
+
expect(fee).toBeGreaterThan(0n);
|
|
60
|
+
expect(fee).toBeLessThan(amount);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should scale proportionally with amount', async () => {
|
|
64
|
+
const smallAmount = parseEther('0.1');
|
|
65
|
+
const largeAmount = parseEther('1');
|
|
66
|
+
|
|
67
|
+
const [smallFee, largeFee] = await Promise.all([
|
|
68
|
+
withTestRetry(() => sdk.getFee(smallAmount)),
|
|
69
|
+
withTestRetry(() => sdk.getFee(largeAmount)),
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
// Large fee should be approximately 10x small fee
|
|
73
|
+
const ratio = Number(largeFee) / Number(smallFee);
|
|
74
|
+
expect(ratio).toBeCloseTo(10, 1);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('getUserVolume', () => {
|
|
79
|
+
it('should return bigint >= 0', async () => {
|
|
80
|
+
const volume = await withTestRetry(() => sdk.getUserVolume(walletAddress));
|
|
81
|
+
expect(volume).toBeGreaterThanOrEqual(0n);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe.skipIf(!testTokenAddress)('Token-specific read methods', () => {
|
|
86
|
+
it('should get bonding curve state', async () => {
|
|
87
|
+
const curve = await withTestRetry(() => sdk.getBondingCurve(testTokenAddress!));
|
|
88
|
+
|
|
89
|
+
expect(curve.virtualTokenReserves).toBeGreaterThanOrEqual(0n);
|
|
90
|
+
expect(curve.virtualEthReserves).toBeGreaterThanOrEqual(0n);
|
|
91
|
+
expect(curve.realTokenReserves).toBeGreaterThanOrEqual(0n);
|
|
92
|
+
expect(curve.realEthReserves).toBeGreaterThanOrEqual(0n);
|
|
93
|
+
expect(curve.tokenTotalSupply).toBeGreaterThan(0n);
|
|
94
|
+
expect(typeof curve.complete).toBe('boolean');
|
|
95
|
+
expect(typeof curve.trackVolume).toBe('boolean');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should get bonding curve instance', async () => {
|
|
99
|
+
const curve = await withTestRetry(() => sdk.getBondingCurveInstance(testTokenAddress!));
|
|
100
|
+
|
|
101
|
+
expect(curve).toBeDefined();
|
|
102
|
+
expect(curve.getCurrentPrice()).toBeGreaterThanOrEqual(0n);
|
|
103
|
+
expect(curve.getMigrationProgress()).toBeGreaterThanOrEqual(0);
|
|
104
|
+
expect(curve.getMigrationProgress()).toBeLessThanOrEqual(100);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should get comprehensive token info', async () => {
|
|
108
|
+
const info = await withTestRetry(() => sdk.getTokenInfo(testTokenAddress!));
|
|
109
|
+
|
|
110
|
+
expect(info.address).toBe(testTokenAddress);
|
|
111
|
+
expect(info.bondingCurve).toBeDefined();
|
|
112
|
+
expect(info.currentPrice).toBeGreaterThanOrEqual(0n);
|
|
113
|
+
expect(info.marketCapEth).toBeGreaterThanOrEqual(0n);
|
|
114
|
+
expect(info.migrationProgress).toBeGreaterThanOrEqual(0);
|
|
115
|
+
expect(info.migrationProgress).toBeLessThanOrEqual(100);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should get buy quote', async () => {
|
|
119
|
+
const ethAmount = parseEther('0.01');
|
|
120
|
+
const quote = await withTestRetry(() => sdk.getBuyQuote(testTokenAddress!, ethAmount));
|
|
121
|
+
|
|
122
|
+
// Quote should be > 0 unless curve is migrated
|
|
123
|
+
const isMigrated = await sdk.isMigrated(testTokenAddress!);
|
|
124
|
+
if (!isMigrated) {
|
|
125
|
+
expect(quote).toBeGreaterThan(0n);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should get sell quote', async () => {
|
|
130
|
+
const tokenAmount = parseEther('1000');
|
|
131
|
+
const quote = await withTestRetry(() => sdk.getSellQuote(testTokenAddress!, tokenAmount));
|
|
132
|
+
|
|
133
|
+
// Quote should be >= 0
|
|
134
|
+
expect(quote).toBeGreaterThanOrEqual(0n);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should return valid complete status', async () => {
|
|
138
|
+
const complete = await withTestRetry(() => sdk.isComplete(testTokenAddress!));
|
|
139
|
+
expect(typeof complete).toBe('boolean');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should return valid migrated status', async () => {
|
|
143
|
+
const migrated = await withTestRetry(() => sdk.isMigrated(testTokenAddress!));
|
|
144
|
+
expect(typeof migrated).toBe('boolean');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('migrated should imply complete', async () => {
|
|
148
|
+
const [complete, migrated] = await Promise.all([
|
|
149
|
+
withTestRetry(() => sdk.isComplete(testTokenAddress!)),
|
|
150
|
+
withTestRetry(() => sdk.isMigrated(testTokenAddress!)),
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
if (migrated) {
|
|
154
|
+
expect(complete).toBe(true);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('Quote consistency', () => {
|
|
160
|
+
it.skipIf(!testTokenAddress)('should return more tokens for larger ETH amounts', async () => {
|
|
161
|
+
const isMigrated = await sdk.isMigrated(testTokenAddress!);
|
|
162
|
+
if (isMigrated) return;
|
|
163
|
+
|
|
164
|
+
const [smallQuote, largeQuote] = await Promise.all([
|
|
165
|
+
withTestRetry(() => sdk.getBuyQuote(testTokenAddress!, parseEther('0.01'))),
|
|
166
|
+
withTestRetry(() => sdk.getBuyQuote(testTokenAddress!, parseEther('0.1'))),
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
expect(largeQuote).toBeGreaterThan(smallQuote);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('Factory address', () => {
|
|
174
|
+
it('should return correct factory address', () => {
|
|
175
|
+
const factoryAddress = sdk.getFactoryAddress();
|
|
176
|
+
expect(factoryAddress).toMatch(/^0x[a-fA-F0-9]{40}$/);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('Client accessors', () => {
|
|
181
|
+
it('should return public client', () => {
|
|
182
|
+
const publicClient = sdk.getPublicClient();
|
|
183
|
+
expect(publicClient).toBeDefined();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should return wallet client when connected', () => {
|
|
187
|
+
const walletClient = sdk.getWalletClient();
|
|
188
|
+
expect(walletClient).toBeDefined();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should return wallet address', () => {
|
|
192
|
+
const address = sdk.getWalletAddress();
|
|
193
|
+
expect(address).toBe(walletAddress);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|