@make-software/cspr-trade-mcp-sdk 0.1.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/README.md +257 -0
- package/dist/assets/proxy_caller.wasm +0 -0
- package/dist/index.d.ts +362 -0
- package/dist/index.js +1003 -0
- package/package.json +32 -0
- package/src/api/currencies.ts +11 -0
- package/src/api/http.ts +57 -0
- package/src/api/index.ts +9 -0
- package/src/api/liquidity.ts +22 -0
- package/src/api/pairs.ts +77 -0
- package/src/api/quotes.ts +23 -0
- package/src/api/rates.ts +33 -0
- package/src/api/submission.ts +42 -0
- package/src/api/swaps.ts +24 -0
- package/src/api/tokens.ts +57 -0
- package/src/assets/index.ts +21 -0
- package/src/assets/proxy_caller.wasm +0 -0
- package/src/client.ts +587 -0
- package/src/config.ts +60 -0
- package/src/index.ts +4 -0
- package/src/resolver/currency-resolver.ts +19 -0
- package/src/resolver/index.ts +2 -0
- package/src/resolver/token-resolver.ts +43 -0
- package/src/transactions/approve.ts +14 -0
- package/src/transactions/index.ts +5 -0
- package/src/transactions/liquidity.ts +92 -0
- package/src/transactions/proxy-wasm.ts +33 -0
- package/src/transactions/swap.ts +76 -0
- package/src/transactions/transaction-builder.ts +44 -0
- package/src/types/api.ts +32 -0
- package/src/types/index.ts +6 -0
- package/src/types/liquidity.ts +72 -0
- package/src/types/pair.ts +29 -0
- package/src/types/quote.ts +41 -0
- package/src/types/token.ts +48 -0
- package/src/types/transaction.ts +72 -0
- package/src/utils/amounts.ts +30 -0
- package/tests/integration/api.integration.test.ts +64 -0
- package/tests/unit/api/http.test.ts +68 -0
- package/tests/unit/api/liquidity.test.ts +40 -0
- package/tests/unit/api/pairs.test.ts +53 -0
- package/tests/unit/api/quotes.test.ts +59 -0
- package/tests/unit/api/rates.test.ts +27 -0
- package/tests/unit/api/tokens.test.ts +100 -0
- package/tests/unit/assets/proxy-caller.test.ts +21 -0
- package/tests/unit/client.test.ts +73 -0
- package/tests/unit/config.test.ts +23 -0
- package/tests/unit/resolver/currency-resolver.test.ts +32 -0
- package/tests/unit/resolver/token-resolver.test.ts +51 -0
- package/tests/unit/transactions/approve.test.ts +13 -0
- package/tests/unit/transactions/liquidity.test.ts +59 -0
- package/tests/unit/transactions/proxy-wasm.test.ts +50 -0
- package/tests/unit/transactions/swap.test.ts +77 -0
- package/tests/unit/utils/amounts.test.ts +44 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { CsprTradeClient } from '../../src/client.js';
|
|
3
|
+
|
|
4
|
+
const SHOULD_RUN = !!process.env.CSPR_TRADE_INTEGRATION;
|
|
5
|
+
|
|
6
|
+
describe.skipIf(!SHOULD_RUN)('SDK Integration Tests (testnet)', () => {
|
|
7
|
+
const client = new CsprTradeClient({ network: 'testnet' });
|
|
8
|
+
|
|
9
|
+
it('should fetch tokens from testnet', async () => {
|
|
10
|
+
const tokens = await client.getTokens();
|
|
11
|
+
expect(tokens.length).toBeGreaterThan(0);
|
|
12
|
+
const cspr = tokens.find(t => t.symbol === 'CSPR');
|
|
13
|
+
expect(cspr).toBeDefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should fetch tokens with USD pricing', async () => {
|
|
17
|
+
const tokens = await client.getTokens('USD');
|
|
18
|
+
expect(tokens.length).toBeGreaterThan(0);
|
|
19
|
+
const cspr = tokens.find(t => t.symbol === 'CSPR');
|
|
20
|
+
expect(cspr).toBeDefined();
|
|
21
|
+
expect(cspr!.fiatPrice).toBeTypeOf('number');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should fetch pairs from testnet', async () => {
|
|
25
|
+
const result = await client.getPairs({ page: 1, pageSize: 5 });
|
|
26
|
+
expect(result.data.length).toBeGreaterThan(0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should fetch currencies', async () => {
|
|
30
|
+
const currencies = await client.getCurrencies();
|
|
31
|
+
expect(currencies.length).toBeGreaterThan(0);
|
|
32
|
+
const usd = currencies.find(c => c.code === 'USD');
|
|
33
|
+
expect(usd).toBeDefined();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should resolve tokens by symbol', async () => {
|
|
37
|
+
const token = await client.resolveToken('CSPR');
|
|
38
|
+
expect(token.symbol).toBe('CSPR');
|
|
39
|
+
expect(token.decimals).toBe(9);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should get a swap quote', async () => {
|
|
43
|
+
const tokens = await client.getTokens();
|
|
44
|
+
// Find any non-CSPR token to pair with
|
|
45
|
+
const otherToken = tokens.find(t => t.symbol !== 'CSPR' && t.symbol !== 'WCSPR');
|
|
46
|
+
if (!otherToken) {
|
|
47
|
+
console.warn('No non-CSPR token found on testnet, skipping quote test');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const quote = await client.getQuote({
|
|
52
|
+
tokenIn: 'CSPR',
|
|
53
|
+
tokenOut: otherToken.symbol,
|
|
54
|
+
amount: '100',
|
|
55
|
+
type: 'exact_in',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(quote.amountIn).toBeDefined();
|
|
59
|
+
expect(quote.amountOut).toBeDefined();
|
|
60
|
+
expect(quote.tokenInSymbol).toBe('CSPR');
|
|
61
|
+
expect(quote.tokenOutSymbol).toBe(otherToken.symbol);
|
|
62
|
+
expect(quote.pathSymbols.length).toBeGreaterThanOrEqual(2);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { HttpClient } from '../../../src/api/http.js';
|
|
3
|
+
|
|
4
|
+
describe('HttpClient', () => {
|
|
5
|
+
let client: HttpClient;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
client = new HttpClient('https://api.example.com');
|
|
9
|
+
global.fetch = vi.fn();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
vi.restoreAllMocks();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should make GET requests with correct URL', async () => {
|
|
17
|
+
vi.mocked(fetch).mockResolvedValueOnce(new Response(JSON.stringify({ data: 'test' })));
|
|
18
|
+
|
|
19
|
+
await client.get('/tokens');
|
|
20
|
+
|
|
21
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
22
|
+
'https://api.example.com/tokens',
|
|
23
|
+
expect.objectContaining({ method: 'GET' })
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should append query parameters', async () => {
|
|
28
|
+
vi.mocked(fetch).mockResolvedValueOnce(new Response(JSON.stringify({ data: 'test' })));
|
|
29
|
+
|
|
30
|
+
await client.get('/tokens', { includes: 'csprtrade_data(1)' });
|
|
31
|
+
|
|
32
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
33
|
+
'https://api.example.com/tokens?includes=csprtrade_data%281%29',
|
|
34
|
+
expect.anything()
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should parse JSON responses', async () => {
|
|
39
|
+
vi.mocked(fetch).mockResolvedValueOnce(
|
|
40
|
+
new Response(JSON.stringify({ data: [{ id: 1 }] }))
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const result = await client.get<{ data: { id: number }[] }>('/tokens');
|
|
44
|
+
expect(result.data).toEqual([{ id: 1 }]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should throw on HTTP errors', async () => {
|
|
48
|
+
vi.mocked(fetch).mockResolvedValueOnce(
|
|
49
|
+
new Response(JSON.stringify({ error: { message: 'Not found' } }), { status: 404 })
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
await expect(client.get('/missing')).rejects.toThrow();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should make POST requests with body', async () => {
|
|
56
|
+
vi.mocked(fetch).mockResolvedValueOnce(new Response(JSON.stringify({ data: 'ok' })));
|
|
57
|
+
|
|
58
|
+
await client.post('/submit', { key: 'value' });
|
|
59
|
+
|
|
60
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
61
|
+
'https://api.example.com/submit',
|
|
62
|
+
expect.objectContaining({
|
|
63
|
+
method: 'POST',
|
|
64
|
+
body: JSON.stringify({ key: 'value' }),
|
|
65
|
+
})
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { LiquidityApi } from '../../../src/api/liquidity.js';
|
|
3
|
+
import { HttpClient } from '../../../src/api/http.js';
|
|
4
|
+
|
|
5
|
+
vi.mock('../../../src/api/http.js');
|
|
6
|
+
|
|
7
|
+
describe('LiquidityApi', () => {
|
|
8
|
+
let api: LiquidityApi;
|
|
9
|
+
let mockHttp: HttpClient;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
mockHttp = new HttpClient('https://api.example.com');
|
|
13
|
+
api = new LiquidityApi(mockHttp);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should fetch liquidity positions', async () => {
|
|
17
|
+
vi.mocked(mockHttp.get).mockResolvedValueOnce({
|
|
18
|
+
data: [{
|
|
19
|
+
account_hash: 'account-hash-abc',
|
|
20
|
+
pair_contract_package_hash: 'hash-pair1',
|
|
21
|
+
lp_token_balance: '1000000',
|
|
22
|
+
pair: { token0_contract_package: { metadata: { symbol: 'CSPR' } }, token1_contract_package: { metadata: { symbol: 'USDT' } } },
|
|
23
|
+
pair_lp_tokens_total_supply: '10000000',
|
|
24
|
+
}],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const positions = await api.getPositions('01abc123');
|
|
28
|
+
expect(positions).toHaveLength(1);
|
|
29
|
+
expect(positions[0].pair_contract_package_hash).toBe('hash-pair1');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should fetch impermanent loss', async () => {
|
|
33
|
+
vi.mocked(mockHttp.get).mockResolvedValueOnce({
|
|
34
|
+
data: { value: '-2.5', pair_contract_package_hash: 'hash-pair1', account_hash: 'account-hash-abc', timestamp: '2025-01-01T00:00:00Z' },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const il = await api.getImpermanentLoss('01abc123', 'hash-pair1');
|
|
38
|
+
expect(il.value).toBe('-2.5');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { PairsApi } from '../../../src/api/pairs.js';
|
|
3
|
+
import { HttpClient } from '../../../src/api/http.js';
|
|
4
|
+
|
|
5
|
+
vi.mock('../../../src/api/http.js');
|
|
6
|
+
|
|
7
|
+
const MOCK_PAIR = {
|
|
8
|
+
contract_package_hash: 'hash-pair1',
|
|
9
|
+
token0_contract_package_hash: 'hash-token0',
|
|
10
|
+
token1_contract_package_hash: 'hash-token1',
|
|
11
|
+
decimals0: 9,
|
|
12
|
+
decimals1: 6,
|
|
13
|
+
reserve0: '1000000000000',
|
|
14
|
+
reserve1: '500000000',
|
|
15
|
+
timestamp: '2025-01-01T00:00:00Z',
|
|
16
|
+
latest_event_id: '123',
|
|
17
|
+
contract_package: { name: 'Pair' },
|
|
18
|
+
token0_contract_package: { metadata: { symbol: 'CSPR', name: 'Casper', decimals: 9 }, icon_url: null, csprtrade_data: { price: 0.02 } },
|
|
19
|
+
token1_contract_package: { metadata: { symbol: 'USDT', name: 'Tether', decimals: 6 }, icon_url: null, csprtrade_data: { price: 1.0 } },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
describe('PairsApi', () => {
|
|
23
|
+
let api: PairsApi;
|
|
24
|
+
let mockHttp: HttpClient;
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
mockHttp = new HttpClient('https://api.example.com');
|
|
28
|
+
api = new PairsApi(mockHttp);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should fetch paginated pairs', async () => {
|
|
32
|
+
vi.mocked(mockHttp.get).mockResolvedValueOnce({
|
|
33
|
+
data: [MOCK_PAIR],
|
|
34
|
+
item_count: 1,
|
|
35
|
+
page_count: 1,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const result = await api.getPairs({ page: 1, pageSize: 10 });
|
|
39
|
+
|
|
40
|
+
expect(result.data).toHaveLength(1);
|
|
41
|
+
expect(result.data[0].token0.symbol).toBe('CSPR');
|
|
42
|
+
expect(result.itemCount).toBe(1);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should fetch pair by hash', async () => {
|
|
46
|
+
vi.mocked(mockHttp.get).mockResolvedValueOnce({ data: MOCK_PAIR });
|
|
47
|
+
|
|
48
|
+
const pair = await api.getPairDetails('hash-pair1');
|
|
49
|
+
|
|
50
|
+
expect(pair.contractPackageHash).toBe('hash-pair1');
|
|
51
|
+
expect(pair.token0.symbol).toBe('CSPR');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { QuotesApi } from '../../../src/api/quotes.js';
|
|
3
|
+
import { HttpClient } from '../../../src/api/http.js';
|
|
4
|
+
|
|
5
|
+
vi.mock('../../../src/api/http.js');
|
|
6
|
+
|
|
7
|
+
describe('QuotesApi', () => {
|
|
8
|
+
let api: QuotesApi;
|
|
9
|
+
let mockHttp: HttpClient;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
mockHttp = new HttpClient('https://api.example.com');
|
|
13
|
+
api = new QuotesApi(mockHttp);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should fetch exact-in quote', async () => {
|
|
17
|
+
vi.mocked(mockHttp.get).mockResolvedValueOnce({
|
|
18
|
+
data: {
|
|
19
|
+
amount_in: '100000000000',
|
|
20
|
+
amount_out: '50000000',
|
|
21
|
+
execution_price: '50000000',
|
|
22
|
+
mid_price: '50100000',
|
|
23
|
+
path: ['hash-wcspr', 'hash-usdt'],
|
|
24
|
+
price_impact: '0.20',
|
|
25
|
+
recommended_slippage_bps: '20',
|
|
26
|
+
type_id: 1,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const quote = await api.getQuote({
|
|
31
|
+
tokenIn: 'hash-0000',
|
|
32
|
+
tokenOut: 'hash-usdt',
|
|
33
|
+
amount: '100000000000',
|
|
34
|
+
typeId: 1,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(quote.amount_in).toBe('100000000000');
|
|
38
|
+
expect(quote.amount_out).toBe('50000000');
|
|
39
|
+
expect(quote.price_impact).toBe('0.20');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should pass correct query params', async () => {
|
|
43
|
+
vi.mocked(mockHttp.get).mockResolvedValueOnce({ data: {} });
|
|
44
|
+
|
|
45
|
+
await api.getQuote({
|
|
46
|
+
tokenIn: 'hash-aaa',
|
|
47
|
+
tokenOut: 'hash-bbb',
|
|
48
|
+
amount: '1000',
|
|
49
|
+
typeId: 2,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(mockHttp.get).toHaveBeenCalledWith('/quote', {
|
|
53
|
+
token_in: 'hash-aaa',
|
|
54
|
+
token_out: 'hash-bbb',
|
|
55
|
+
amount: '1000',
|
|
56
|
+
type_id: '2',
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { RatesApi } from '../../../src/api/rates.js';
|
|
3
|
+
import { HttpClient } from '../../../src/api/http.js';
|
|
4
|
+
|
|
5
|
+
vi.mock('../../../src/api/http.js');
|
|
6
|
+
|
|
7
|
+
describe('RatesApi', () => {
|
|
8
|
+
let api: RatesApi;
|
|
9
|
+
let mockHttp: HttpClient;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
mockHttp = new HttpClient('https://api.example.com');
|
|
13
|
+
api = new RatesApi(mockHttp);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should fetch CSPR fiat rate', async () => {
|
|
17
|
+
vi.mocked(mockHttp.get).mockResolvedValueOnce({ data: { rate: 0.025 } });
|
|
18
|
+
const rate = await api.getCsprRate(1);
|
|
19
|
+
expect(mockHttp.get).toHaveBeenCalledWith('/rates/1/latest');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should fetch token fiat rate', async () => {
|
|
23
|
+
vi.mocked(mockHttp.get).mockResolvedValueOnce({ data: { amount: '1.00' } });
|
|
24
|
+
const rate = await api.getTokenRate('hash-abc', 1);
|
|
25
|
+
expect(mockHttp.get).toHaveBeenCalledWith('/ft/hash-abc/rates/latest', { currency_id: '1' });
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { TokensApi } from '../../../src/api/tokens.js';
|
|
3
|
+
import { HttpClient } from '../../../src/api/http.js';
|
|
4
|
+
|
|
5
|
+
vi.mock('../../../src/api/http.js');
|
|
6
|
+
|
|
7
|
+
const WCSPR_HASH = 'hash-3d80df21ba4ee4d66a2a1f60c32570dd5685e4b279f6538162a5fd1314847c1e';
|
|
8
|
+
|
|
9
|
+
describe('TokensApi', () => {
|
|
10
|
+
let api: TokensApi;
|
|
11
|
+
let mockHttp: HttpClient;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
mockHttp = new HttpClient('https://api.example.com');
|
|
15
|
+
vi.mocked(mockHttp.get).mockResolvedValue({
|
|
16
|
+
data: [
|
|
17
|
+
{
|
|
18
|
+
contract_package_hash: WCSPR_HASH,
|
|
19
|
+
contract_package: {
|
|
20
|
+
contract_package_hash: WCSPR_HASH,
|
|
21
|
+
name: 'Wrapped CSPR',
|
|
22
|
+
metadata: { symbol: 'WCSPR', name: 'Wrapped CSPR', decimals: 9 },
|
|
23
|
+
icon_url: 'https://example.com/cspr.png',
|
|
24
|
+
csprtrade_data: { price: 0.02 },
|
|
25
|
+
},
|
|
26
|
+
listed_at: '2025-01-01T00:00:00Z',
|
|
27
|
+
sorting_order: 0,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
contract_package_hash: 'hash-abc123',
|
|
31
|
+
contract_package: {
|
|
32
|
+
contract_package_hash: 'hash-abc123',
|
|
33
|
+
name: 'USD Tether',
|
|
34
|
+
metadata: { symbol: 'USDT', name: 'USD Tether', decimals: 6 },
|
|
35
|
+
icon_url: 'https://example.com/usdt.png',
|
|
36
|
+
csprtrade_data: { price: 1.0 },
|
|
37
|
+
},
|
|
38
|
+
listed_at: '2025-01-01T00:00:00Z',
|
|
39
|
+
sorting_order: 1,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
});
|
|
43
|
+
api = new TokensApi(mockHttp, WCSPR_HASH);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should transform WCSPR into virtual CSPR token', async () => {
|
|
47
|
+
const tokens = await api.getTokens(1);
|
|
48
|
+
|
|
49
|
+
expect(mockHttp.get).toHaveBeenCalledWith('/tokens', {
|
|
50
|
+
includes: 'csprtrade_data(1)',
|
|
51
|
+
});
|
|
52
|
+
expect(tokens).toHaveLength(2);
|
|
53
|
+
// WCSPR is transformed into CSPR
|
|
54
|
+
expect(tokens[0].id).toBe('cspr');
|
|
55
|
+
expect(tokens[0].symbol).toBe('CSPR');
|
|
56
|
+
expect(tokens[0].name).toBe('Casper');
|
|
57
|
+
expect(tokens[0].packageHash).toBe(WCSPR_HASH); // keeps real WCSPR hash
|
|
58
|
+
expect(tokens[0].decimals).toBe(9);
|
|
59
|
+
expect(tokens[0].fiatPrice).toBe(0.02);
|
|
60
|
+
// USDT unchanged
|
|
61
|
+
expect(tokens[1].symbol).toBe('USDT');
|
|
62
|
+
expect(tokens[1].decimals).toBe(6);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should fetch tokens without currency', async () => {
|
|
66
|
+
await api.getTokens();
|
|
67
|
+
expect(mockHttp.get).toHaveBeenCalledWith('/tokens', {
|
|
68
|
+
includes: undefined,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should not include CSPR if WCSPR is not in the token list', async () => {
|
|
73
|
+
vi.mocked(mockHttp.get).mockResolvedValueOnce({
|
|
74
|
+
data: [
|
|
75
|
+
{
|
|
76
|
+
contract_package_hash: 'hash-abc123',
|
|
77
|
+
contract_package: {
|
|
78
|
+
contract_package_hash: 'hash-abc123',
|
|
79
|
+
name: 'USD Tether',
|
|
80
|
+
metadata: { symbol: 'USDT', name: 'USD Tether', decimals: 6 },
|
|
81
|
+
icon_url: null,
|
|
82
|
+
csprtrade_data: null,
|
|
83
|
+
},
|
|
84
|
+
listed_at: '2025-01-01T00:00:00Z',
|
|
85
|
+
sorting_order: 1,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const tokens = await api.getTokens();
|
|
91
|
+
const csprTokens = tokens.filter(t => t.id === 'cspr');
|
|
92
|
+
expect(csprTokens).toHaveLength(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should return raw API response via getTokensRaw', async () => {
|
|
96
|
+
const raw = await api.getTokensRaw(1);
|
|
97
|
+
expect(raw).toHaveLength(2);
|
|
98
|
+
expect(raw[0].contract_package_hash).toBe(WCSPR_HASH);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { getProxyCallerWasm } from '../../../src/assets/index.js';
|
|
3
|
+
|
|
4
|
+
describe('getProxyCallerWasm', () => {
|
|
5
|
+
it('should load the proxy_caller.wasm binary', async () => {
|
|
6
|
+
const wasm = await getProxyCallerWasm();
|
|
7
|
+
expect(wasm).toBeInstanceOf(Uint8Array);
|
|
8
|
+
expect(wasm.length).toBeGreaterThan(0);
|
|
9
|
+
// WASM magic bytes: \0asm
|
|
10
|
+
expect(wasm[0]).toBe(0x00);
|
|
11
|
+
expect(wasm[1]).toBe(0x61); // 'a'
|
|
12
|
+
expect(wasm[2]).toBe(0x73); // 's'
|
|
13
|
+
expect(wasm[3]).toBe(0x6d); // 'm'
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should cache the binary on subsequent calls', async () => {
|
|
17
|
+
const wasm1 = await getProxyCallerWasm();
|
|
18
|
+
const wasm2 = await getProxyCallerWasm();
|
|
19
|
+
expect(wasm1).toBe(wasm2); // same reference
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { CsprTradeClient } from '../../src/client.js';
|
|
3
|
+
|
|
4
|
+
// Mock fetch globally
|
|
5
|
+
global.fetch = vi.fn();
|
|
6
|
+
|
|
7
|
+
describe('CsprTradeClient', () => {
|
|
8
|
+
let client: CsprTradeClient;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.restoreAllMocks();
|
|
12
|
+
client = new CsprTradeClient({ network: 'testnet' });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should create with testnet config', () => {
|
|
16
|
+
expect(client).toBeDefined();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should create with custom API URL', () => {
|
|
20
|
+
const custom = new CsprTradeClient({
|
|
21
|
+
network: 'testnet',
|
|
22
|
+
apiUrl: 'https://custom-api.example.com',
|
|
23
|
+
});
|
|
24
|
+
expect(custom).toBeDefined();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should expose getTokens method', async () => {
|
|
28
|
+
vi.mocked(fetch).mockResolvedValueOnce(
|
|
29
|
+
new Response(JSON.stringify({ data: [] }))
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const tokens = await client.getTokens();
|
|
33
|
+
expect(Array.isArray(tokens)).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should expose getQuote method', async () => {
|
|
37
|
+
// First call: getTokens for resolution (WCSPR gets transformed to CSPR)
|
|
38
|
+
vi.mocked(fetch).mockResolvedValueOnce(
|
|
39
|
+
new Response(JSON.stringify({
|
|
40
|
+
data: [
|
|
41
|
+
{ contract_package_hash: 'hash-3d80df21ba4ee4d66a2a1f60c32570dd5685e4b279f6538162a5fd1314847c1e', contract_package: { metadata: { symbol: 'WCSPR', name: 'Wrapped CSPR', decimals: 9 } } },
|
|
42
|
+
{ contract_package_hash: 'hash-aaa', contract_package: { metadata: { symbol: 'USDT', name: 'Tether', decimals: 6 } } },
|
|
43
|
+
],
|
|
44
|
+
}))
|
|
45
|
+
);
|
|
46
|
+
// Second call: the actual quote
|
|
47
|
+
vi.mocked(fetch).mockResolvedValueOnce(
|
|
48
|
+
new Response(JSON.stringify({
|
|
49
|
+
data: {
|
|
50
|
+
amount_in: '100000000000',
|
|
51
|
+
amount_out: '50000000',
|
|
52
|
+
execution_price: '0.5',
|
|
53
|
+
mid_price: '0.5',
|
|
54
|
+
path: ['hash-wcspr', 'hash-aaa'],
|
|
55
|
+
price_impact: '0.1',
|
|
56
|
+
recommended_slippage_bps: '10',
|
|
57
|
+
type_id: 1,
|
|
58
|
+
},
|
|
59
|
+
}))
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const quote = await client.getQuote({
|
|
63
|
+
tokenIn: 'CSPR',
|
|
64
|
+
tokenOut: 'USDT',
|
|
65
|
+
amount: '100',
|
|
66
|
+
type: 'exact_in',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(quote.amountIn).toBe('100000000000');
|
|
70
|
+
expect(quote.amountOut).toBe('50000000');
|
|
71
|
+
expect(quote.tokenInSymbol).toBe('CSPR');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { getNetworkConfig, TESTNET_CONFIG, DEFAULT_SLIPPAGE_BPS, DEFAULT_DEADLINE_MINUTES } from '../../src/config.js';
|
|
3
|
+
|
|
4
|
+
describe('Network Config', () => {
|
|
5
|
+
it('should return testnet config', () => {
|
|
6
|
+
const config = getNetworkConfig('testnet');
|
|
7
|
+
expect(config.chainName).toBe('casper-test');
|
|
8
|
+
expect(config.apiUrl).toContain('dev.make.services');
|
|
9
|
+
expect(config.routerPackageHash).toMatch(/^hash-/);
|
|
10
|
+
expect(config.wcsprPackageHash).toMatch(/^hash-/);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should return mainnet config', () => {
|
|
14
|
+
const config = getNetworkConfig('mainnet');
|
|
15
|
+
expect(config.chainName).toBe('casper');
|
|
16
|
+
expect(config.routerPackageHash).toMatch(/^hash-/);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should have correct defaults', () => {
|
|
20
|
+
expect(DEFAULT_SLIPPAGE_BPS).toBe(300);
|
|
21
|
+
expect(DEFAULT_DEADLINE_MINUTES).toBe(20);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { CurrencyResolver } from '../../../src/resolver/currency-resolver.js';
|
|
3
|
+
import type { Currency } from '../../../src/types/index.js';
|
|
4
|
+
|
|
5
|
+
const MOCK_CURRENCIES: Currency[] = [
|
|
6
|
+
{ id: 1, code: 'USD', name: 'US Dollar', symbol: '$' },
|
|
7
|
+
{ id: 2, code: 'EUR', name: 'Euro', symbol: '€' },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
describe('CurrencyResolver', () => {
|
|
11
|
+
let resolver: CurrencyResolver;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
const fetchCurrencies = vi.fn().mockResolvedValue(MOCK_CURRENCIES);
|
|
15
|
+
resolver = new CurrencyResolver(fetchCurrencies);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should resolve currency code to ID', async () => {
|
|
19
|
+
const id = await resolver.resolveToId('USD');
|
|
20
|
+
expect(id).toBe(1);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should be case-insensitive', async () => {
|
|
24
|
+
const id = await resolver.resolveToId('eur');
|
|
25
|
+
expect(id).toBe(2);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should return undefined for unknown currency', async () => {
|
|
29
|
+
const id = await resolver.resolveToId('JPY');
|
|
30
|
+
expect(id).toBeUndefined();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { TokenResolver } from '../../../src/resolver/token-resolver.js';
|
|
3
|
+
import type { Token } from '../../../src/types/index.js';
|
|
4
|
+
|
|
5
|
+
const MOCK_TOKENS: Token[] = [
|
|
6
|
+
{ id: 'cspr', name: 'Casper', symbol: 'CSPR', decimals: 9, packageHash: 'hash-0000000000000000000000000000000000000000000000000000000000000000', iconUrl: null, fiatPrice: null },
|
|
7
|
+
{ id: 'hash-aaa111', name: 'USD Tether', symbol: 'USDT', decimals: 6, packageHash: 'hash-aaa111', iconUrl: null, fiatPrice: 1.0 },
|
|
8
|
+
{ id: 'hash-bbb222', name: 'Wrapped Casper', symbol: 'WCSPR', decimals: 9, packageHash: 'hash-bbb222', iconUrl: null, fiatPrice: null },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
describe('TokenResolver', () => {
|
|
12
|
+
let resolver: TokenResolver;
|
|
13
|
+
let fetchTokens: ReturnType<typeof vi.fn>;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
fetchTokens = vi.fn().mockResolvedValue(MOCK_TOKENS);
|
|
17
|
+
resolver = new TokenResolver(fetchTokens);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should resolve by symbol (case-insensitive)', async () => {
|
|
21
|
+
const token = await resolver.resolve('usdt');
|
|
22
|
+
expect(token.symbol).toBe('USDT');
|
|
23
|
+
expect(token.packageHash).toBe('hash-aaa111');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should resolve CSPR as native token', async () => {
|
|
27
|
+
const token = await resolver.resolve('CSPR');
|
|
28
|
+
expect(token.id).toBe('cspr');
|
|
29
|
+
expect(token.decimals).toBe(9);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should resolve by contract package hash', async () => {
|
|
33
|
+
const token = await resolver.resolve('hash-aaa111');
|
|
34
|
+
expect(token.symbol).toBe('USDT');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should resolve by name', async () => {
|
|
38
|
+
const token = await resolver.resolve('USD Tether');
|
|
39
|
+
expect(token.symbol).toBe('USDT');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should throw for unknown token', async () => {
|
|
43
|
+
await expect(resolver.resolve('UNKNOWN')).rejects.toThrow('Token not found');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should cache token list', async () => {
|
|
47
|
+
await resolver.resolve('CSPR');
|
|
48
|
+
await resolver.resolve('USDT');
|
|
49
|
+
expect(fetchTokens).toHaveBeenCalledTimes(1);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { buildApproveArgs } from '../../../src/transactions/approve.js';
|
|
3
|
+
|
|
4
|
+
describe('Approve transaction builder', () => {
|
|
5
|
+
it('should build approve args with spender and amount', () => {
|
|
6
|
+
const args = buildApproveArgs({
|
|
7
|
+
spenderPackageHash: 'hash-04a11a367e708c52557930c4e9c1301f4465100d1b1b6d0a62b48d3e32402867',
|
|
8
|
+
amount: '100000000000',
|
|
9
|
+
});
|
|
10
|
+
const bytes = args.toBytes();
|
|
11
|
+
expect(bytes.length).toBeGreaterThan(0);
|
|
12
|
+
});
|
|
13
|
+
});
|