@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
package/src/client.ts
ADDED
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
import casperSdk from 'casper-js-sdk';
|
|
2
|
+
const { PublicKey, HttpHandler, RpcClient, Transaction } = casperSdk;
|
|
3
|
+
|
|
4
|
+
import { HttpClient } from './api/http.js';
|
|
5
|
+
import { TokensApi } from './api/tokens.js';
|
|
6
|
+
import { PairsApi, type PairQuery, type PaginatedResult } from './api/pairs.js';
|
|
7
|
+
import { QuotesApi } from './api/quotes.js';
|
|
8
|
+
import { LiquidityApi } from './api/liquidity.js';
|
|
9
|
+
import { RatesApi } from './api/rates.js';
|
|
10
|
+
import { CurrenciesApi } from './api/currencies.js';
|
|
11
|
+
import { SwapsApi } from './api/swaps.js';
|
|
12
|
+
import { TokenResolver } from './resolver/token-resolver.js';
|
|
13
|
+
import { CurrencyResolver } from './resolver/currency-resolver.js';
|
|
14
|
+
import {
|
|
15
|
+
getNetworkConfig,
|
|
16
|
+
GAS_COSTS,
|
|
17
|
+
DEFAULT_SLIPPAGE_BPS,
|
|
18
|
+
DEFAULT_DEADLINE_MINUTES,
|
|
19
|
+
CSPR_TOKEN_ID,
|
|
20
|
+
ZERO_HASH,
|
|
21
|
+
PRICE_IMPACT_WARNING_THRESHOLD,
|
|
22
|
+
PRICE_IMPACT_HIGH_THRESHOLD,
|
|
23
|
+
SLIPPAGE_WARNING_THRESHOLD,
|
|
24
|
+
type NetworkConfig,
|
|
25
|
+
} from './config.js';
|
|
26
|
+
import { toRawAmount, toFormattedAmount, calculateMinWithSlippage, calculateMaxWithSlippage } from './utils/amounts.js';
|
|
27
|
+
import { buildProxyWasmArgs } from './transactions/proxy-wasm.js';
|
|
28
|
+
import { getSwapEntryPoint, buildSwapInnerArgs, getSwapAttachedValue } from './transactions/swap.js';
|
|
29
|
+
import { buildAddLiquidityInnerArgs, buildRemoveLiquidityInnerArgs } from './transactions/liquidity.js';
|
|
30
|
+
import { buildApproveArgs } from './transactions/approve.js';
|
|
31
|
+
import { buildWasmTransaction, buildContractCallTransaction } from './transactions/transaction-builder.js';
|
|
32
|
+
import { getProxyCallerWasm } from './assets/index.js';
|
|
33
|
+
import type {
|
|
34
|
+
Token, Pair, Quote, QuoteParams, QuoteType, Currency,
|
|
35
|
+
LiquidityPosition, LiquidityPositionApiResponse, ImpermanentLoss,
|
|
36
|
+
SwapParams, ApprovalParams, AddLiquidityParams, RemoveLiquidityParams,
|
|
37
|
+
TransactionBundle, SubmitResult, Signer,
|
|
38
|
+
SwapHistoryQuery,
|
|
39
|
+
} from './types/index.js';
|
|
40
|
+
|
|
41
|
+
export interface CsprTradeClientConfig {
|
|
42
|
+
network: 'mainnet' | 'testnet';
|
|
43
|
+
apiUrl?: string;
|
|
44
|
+
routerPackageHash?: string;
|
|
45
|
+
wcsprPackageHash?: string;
|
|
46
|
+
signer?: Signer;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class CsprTradeClient {
|
|
50
|
+
private readonly http: HttpClient;
|
|
51
|
+
private readonly tokensApi: TokensApi;
|
|
52
|
+
private readonly pairsApi: PairsApi;
|
|
53
|
+
private readonly quotesApi: QuotesApi;
|
|
54
|
+
private readonly liquidityApi: LiquidityApi;
|
|
55
|
+
private readonly ratesApi: RatesApi;
|
|
56
|
+
private readonly currenciesApi: CurrenciesApi;
|
|
57
|
+
private readonly swapsApi: SwapsApi;
|
|
58
|
+
private readonly tokenResolver: TokenResolver;
|
|
59
|
+
private readonly currencyResolver: CurrencyResolver;
|
|
60
|
+
private readonly networkConfig: NetworkConfig;
|
|
61
|
+
private readonly signer?: Signer;
|
|
62
|
+
|
|
63
|
+
constructor(config: CsprTradeClientConfig) {
|
|
64
|
+
const baseConfig = getNetworkConfig(config.network);
|
|
65
|
+
this.networkConfig = {
|
|
66
|
+
...baseConfig,
|
|
67
|
+
apiUrl: config.apiUrl ?? baseConfig.apiUrl,
|
|
68
|
+
routerPackageHash: config.routerPackageHash ?? baseConfig.routerPackageHash,
|
|
69
|
+
wcsprPackageHash: config.wcsprPackageHash ?? baseConfig.wcsprPackageHash,
|
|
70
|
+
};
|
|
71
|
+
this.signer = config.signer;
|
|
72
|
+
|
|
73
|
+
this.http = new HttpClient(this.networkConfig.apiUrl);
|
|
74
|
+
this.tokensApi = new TokensApi(this.http, this.networkConfig.wcsprPackageHash);
|
|
75
|
+
this.pairsApi = new PairsApi(this.http);
|
|
76
|
+
this.quotesApi = new QuotesApi(this.http);
|
|
77
|
+
this.liquidityApi = new LiquidityApi(this.http);
|
|
78
|
+
this.ratesApi = new RatesApi(this.http);
|
|
79
|
+
this.currenciesApi = new CurrenciesApi(this.http);
|
|
80
|
+
this.swapsApi = new SwapsApi(this.http);
|
|
81
|
+
|
|
82
|
+
this.tokenResolver = new TokenResolver(() => this.tokensApi.getTokens());
|
|
83
|
+
this.currencyResolver = new CurrencyResolver(() => this.currenciesApi.getCurrencies());
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// --- Market Data ---
|
|
87
|
+
|
|
88
|
+
async getTokens(currency?: string): Promise<Token[]> {
|
|
89
|
+
const currencyId = currency ? await this.currencyResolver.resolveToId(currency) : undefined;
|
|
90
|
+
return this.tokensApi.getTokens(currencyId);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async getPairs(opts?: PairQuery & { currency?: string }): Promise<PaginatedResult<Pair>> {
|
|
94
|
+
const currencyId = opts?.currency ? await this.currencyResolver.resolveToId(opts.currency) : undefined;
|
|
95
|
+
return this.pairsApi.getPairs({ ...opts, currencyId });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async getPairDetails(pairIdentifier: string, currency?: string): Promise<Pair> {
|
|
99
|
+
const currencyId = currency ? await this.currencyResolver.resolveToId(currency) : undefined;
|
|
100
|
+
return this.pairsApi.getPairDetails(pairIdentifier, currencyId);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async getQuote(params: QuoteParams): Promise<Quote> {
|
|
104
|
+
const tokenIn = await this.tokenResolver.resolve(params.tokenIn);
|
|
105
|
+
const tokenOut = await this.tokenResolver.resolve(params.tokenOut);
|
|
106
|
+
|
|
107
|
+
const rawAmount = toRawAmount(params.amount, params.type === 'exact_in' ? tokenIn.decimals : tokenOut.decimals);
|
|
108
|
+
|
|
109
|
+
// Quote API expects ZERO_HASH for native CSPR (matching frontend behavior)
|
|
110
|
+
const quoteTokenIn = tokenIn.id === CSPR_TOKEN_ID ? ZERO_HASH : tokenIn.packageHash;
|
|
111
|
+
const quoteTokenOut = tokenOut.id === CSPR_TOKEN_ID ? ZERO_HASH : tokenOut.packageHash;
|
|
112
|
+
|
|
113
|
+
const apiQuote = await this.quotesApi.getQuote({
|
|
114
|
+
tokenIn: quoteTokenIn,
|
|
115
|
+
tokenOut: quoteTokenOut,
|
|
116
|
+
amount: rawAmount,
|
|
117
|
+
typeId: params.type === 'exact_in' ? 1 : 2,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Resolve path symbols
|
|
121
|
+
const tokens = await this.tokenResolver.getTokens();
|
|
122
|
+
const pathSymbols = apiQuote.path.map(hash => {
|
|
123
|
+
const wcsprHash = this.networkConfig.wcsprPackageHash.replace('hash-', '');
|
|
124
|
+
if (hash.replace('hash-', '') === wcsprHash) return 'CSPR';
|
|
125
|
+
const t = tokens.find(tk => tk.packageHash.replace('hash-', '') === hash.replace('hash-', ''));
|
|
126
|
+
return t?.symbol ?? hash.slice(0, 12) + '...';
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
amountIn: apiQuote.amount_in,
|
|
131
|
+
amountOut: apiQuote.amount_out,
|
|
132
|
+
amountInFormatted: toFormattedAmount(apiQuote.amount_in, tokenIn.decimals),
|
|
133
|
+
amountOutFormatted: toFormattedAmount(apiQuote.amount_out, tokenOut.decimals),
|
|
134
|
+
executionPrice: apiQuote.execution_price,
|
|
135
|
+
midPrice: apiQuote.mid_price,
|
|
136
|
+
path: apiQuote.path,
|
|
137
|
+
pathSymbols,
|
|
138
|
+
priceImpact: apiQuote.price_impact,
|
|
139
|
+
recommendedSlippageBps: apiQuote.recommended_slippage_bps,
|
|
140
|
+
type: params.type,
|
|
141
|
+
tokenInSymbol: tokenIn.symbol,
|
|
142
|
+
tokenOutSymbol: tokenOut.symbol,
|
|
143
|
+
tokenInDecimals: tokenIn.decimals,
|
|
144
|
+
tokenOutDecimals: tokenOut.decimals,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async getCurrencies(): Promise<Currency[]> {
|
|
149
|
+
return this.currenciesApi.getCurrencies();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// --- Account Data ---
|
|
153
|
+
|
|
154
|
+
async getLiquidityPositions(publicKey: string, currency?: string): Promise<LiquidityPosition[]> {
|
|
155
|
+
const currencyId = currency ? await this.currencyResolver.resolveToId(currency) : undefined;
|
|
156
|
+
const raw = await this.liquidityApi.getPositions(publicKey, currencyId);
|
|
157
|
+
return raw.map(mapLiquidityPosition);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async getImpermanentLoss(publicKey: string, pairHash: string): Promise<ImpermanentLoss> {
|
|
161
|
+
const raw = await this.liquidityApi.getImpermanentLoss(publicKey, pairHash);
|
|
162
|
+
return {
|
|
163
|
+
pairContractPackageHash: raw.pair_contract_package_hash,
|
|
164
|
+
value: raw.value,
|
|
165
|
+
timestamp: raw.timestamp,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async getSwapHistory(opts?: SwapHistoryQuery) {
|
|
170
|
+
const accountHash = opts?.publicKey
|
|
171
|
+
? PublicKey.fromHex(opts.publicKey).accountHash().toHex()
|
|
172
|
+
: undefined;
|
|
173
|
+
return this.swapsApi.getSwaps({
|
|
174
|
+
senderAccountHash: accountHash,
|
|
175
|
+
pairContractPackageHash: opts?.pairContractPackageHash,
|
|
176
|
+
page: opts?.page,
|
|
177
|
+
pageSize: opts?.pageSize,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// --- Transaction Building ---
|
|
182
|
+
|
|
183
|
+
async buildSwap(params: SwapParams): Promise<TransactionBundle> {
|
|
184
|
+
const tokenIn = await this.tokenResolver.resolve(params.tokenIn);
|
|
185
|
+
const tokenOut = await this.tokenResolver.resolve(params.tokenOut);
|
|
186
|
+
|
|
187
|
+
// Fetch fresh quote
|
|
188
|
+
const quote = await this.getQuote({
|
|
189
|
+
tokenIn: params.tokenIn,
|
|
190
|
+
tokenOut: params.tokenOut,
|
|
191
|
+
amount: params.amount,
|
|
192
|
+
type: params.type,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const slippageBps = params.slippageBps ?? DEFAULT_SLIPPAGE_BPS;
|
|
196
|
+
const deadlineMinutes = params.deadlineMinutes ?? DEFAULT_DEADLINE_MINUTES;
|
|
197
|
+
const deadline = Date.now() + deadlineMinutes * 60 * 1000;
|
|
198
|
+
|
|
199
|
+
const isFirstTokenNative = tokenIn.id === CSPR_TOKEN_ID;
|
|
200
|
+
const isSecondTokenNative = tokenOut.id === CSPR_TOKEN_ID;
|
|
201
|
+
|
|
202
|
+
const amountOutMin = calculateMinWithSlippage(quote.amountOut, slippageBps);
|
|
203
|
+
const amountInMax = calculateMaxWithSlippage(quote.amountIn, slippageBps);
|
|
204
|
+
|
|
205
|
+
const accountHash = PublicKey.fromHex(params.senderPublicKey).accountHash().toPrefixedString();
|
|
206
|
+
|
|
207
|
+
const path = quote.path.map(h => h.startsWith('hash-') ? h : `hash-${h}`);
|
|
208
|
+
|
|
209
|
+
const entryPoint = getSwapEntryPoint(isFirstTokenNative, isSecondTokenNative, params.type);
|
|
210
|
+
|
|
211
|
+
const innerArgs = buildSwapInnerArgs({
|
|
212
|
+
isFirstTokenNative,
|
|
213
|
+
isSecondTokenNative,
|
|
214
|
+
quoteType: params.type,
|
|
215
|
+
path,
|
|
216
|
+
accountHash,
|
|
217
|
+
deadline,
|
|
218
|
+
amountIn: quote.amountIn,
|
|
219
|
+
amountOut: quote.amountOut,
|
|
220
|
+
amountInMax,
|
|
221
|
+
amountOutMin,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const attachedValue = getSwapAttachedValue({
|
|
225
|
+
isFirstTokenNative,
|
|
226
|
+
quoteType: params.type,
|
|
227
|
+
amountIn: quote.amountIn,
|
|
228
|
+
amountInMax,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const isBothNotNative = !isFirstTokenNative && !isSecondTokenNative;
|
|
232
|
+
const gasCost = isBothNotNative ? GAS_COSTS.swapTokenForToken : GAS_COSTS.swapCsprForToken;
|
|
233
|
+
|
|
234
|
+
const proxyArgs = buildProxyWasmArgs({
|
|
235
|
+
routerPackageHash: this.networkConfig.routerPackageHash.replace('hash-', ''),
|
|
236
|
+
entryPoint,
|
|
237
|
+
innerArgs,
|
|
238
|
+
attachedValue,
|
|
239
|
+
});
|
|
240
|
+
const wasmBinary = await getProxyCallerWasm();
|
|
241
|
+
const transaction = buildWasmTransaction({
|
|
242
|
+
publicKey: params.senderPublicKey,
|
|
243
|
+
paymentAmount: gasCost.toString(),
|
|
244
|
+
wasmBinary,
|
|
245
|
+
runtimeArgs: proxyArgs,
|
|
246
|
+
networkConfig: this.networkConfig,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const warnings = buildWarnings(quote.priceImpact, slippageBps);
|
|
250
|
+
|
|
251
|
+
// Build approval transaction for non-CSPR input tokens.
|
|
252
|
+
// When CSPR is the input, the router handles wrapping internally — no approval needed.
|
|
253
|
+
const approvals: TransactionBundle[] = [];
|
|
254
|
+
if (!isFirstTokenNative) {
|
|
255
|
+
const approvalAmount = params.tokenInBalance ?? quote.amountIn;
|
|
256
|
+
const approval = await this.buildApproval({
|
|
257
|
+
tokenContractPackageHash: tokenIn.packageHash,
|
|
258
|
+
spenderPackageHash: this.networkConfig.routerPackageHash,
|
|
259
|
+
amount: approvalAmount,
|
|
260
|
+
senderPublicKey: params.senderPublicKey,
|
|
261
|
+
});
|
|
262
|
+
approval.summary = params.tokenInBalance
|
|
263
|
+
? `Approve ${tokenIn.symbol} for router (full balance — one-time)`
|
|
264
|
+
: `Approve ${tokenIn.symbol} for router (swap amount only)`;
|
|
265
|
+
approvals.push(approval);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const slippagePct = (slippageBps / 100).toFixed(2);
|
|
269
|
+
const summary = [
|
|
270
|
+
`Swap ${quote.amountInFormatted} ${tokenIn.symbol} for ~${quote.amountOutFormatted} ${tokenOut.symbol}`,
|
|
271
|
+
`Route: ${quote.pathSymbols.join(' → ')}`,
|
|
272
|
+
`Price impact: ${quote.priceImpact}%`,
|
|
273
|
+
`Max slippage: ${slippagePct}%`,
|
|
274
|
+
`Deadline: ${deadlineMinutes} minutes`,
|
|
275
|
+
`Estimated gas: ${Number(gasCost) / 1_000_000_000} CSPR`,
|
|
276
|
+
].join('\n');
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
transactionJson: JSON.stringify(transaction.toJSON()),
|
|
280
|
+
summary,
|
|
281
|
+
estimatedGasCost: `${Number(gasCost) / 1_000_000_000} CSPR`,
|
|
282
|
+
approvalsRequired: approvals.length > 0 ? approvals : undefined,
|
|
283
|
+
warnings,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async buildApproval(params: ApprovalParams): Promise<TransactionBundle> {
|
|
288
|
+
const spender = params.spenderPackageHash || this.networkConfig.routerPackageHash;
|
|
289
|
+
const args = buildApproveArgs({
|
|
290
|
+
spenderPackageHash: spender,
|
|
291
|
+
amount: params.amount,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const transaction = buildContractCallTransaction({
|
|
295
|
+
publicKey: params.senderPublicKey,
|
|
296
|
+
paymentAmount: GAS_COSTS.approve.toString(),
|
|
297
|
+
contractPackageHash: params.tokenContractPackageHash,
|
|
298
|
+
entryPoint: 'approve',
|
|
299
|
+
runtimeArgs: args,
|
|
300
|
+
networkConfig: this.networkConfig,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
transactionJson: JSON.stringify(transaction.toJSON()),
|
|
305
|
+
summary: `Approve token spending for ${params.tokenContractPackageHash.slice(0, 16)}...`,
|
|
306
|
+
estimatedGasCost: `${Number(GAS_COSTS.approve) / 1_000_000_000} CSPR`,
|
|
307
|
+
warnings: [],
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async buildAddLiquidity(params: AddLiquidityParams): Promise<TransactionBundle> {
|
|
312
|
+
const tokenA = await this.tokenResolver.resolve(params.tokenA);
|
|
313
|
+
const tokenB = await this.tokenResolver.resolve(params.tokenB);
|
|
314
|
+
|
|
315
|
+
const slippageBps = params.slippageBps ?? DEFAULT_SLIPPAGE_BPS;
|
|
316
|
+
const deadlineMinutes = params.deadlineMinutes ?? DEFAULT_DEADLINE_MINUTES;
|
|
317
|
+
const deadline = Date.now() + deadlineMinutes * 60 * 1000;
|
|
318
|
+
|
|
319
|
+
const isCSPRPair = tokenA.id === CSPR_TOKEN_ID || tokenB.id === CSPR_TOKEN_ID;
|
|
320
|
+
const rawAmountA = toRawAmount(params.amountA, tokenA.decimals);
|
|
321
|
+
const rawAmountB = toRawAmount(params.amountB, tokenB.decimals);
|
|
322
|
+
const accountHash = PublicKey.fromHex(params.senderPublicKey).accountHash().toPrefixedString();
|
|
323
|
+
|
|
324
|
+
let innerArgs;
|
|
325
|
+
let entryPoint: string;
|
|
326
|
+
let attachedValue = '0';
|
|
327
|
+
|
|
328
|
+
if (isCSPRPair) {
|
|
329
|
+
const csprToken = tokenA.id === CSPR_TOKEN_ID ? tokenA : tokenB;
|
|
330
|
+
const otherToken = tokenA.id === CSPR_TOKEN_ID ? tokenB : tokenA;
|
|
331
|
+
const motesAmount = tokenA.id === CSPR_TOKEN_ID ? rawAmountA : rawAmountB;
|
|
332
|
+
const tokenAmount = tokenA.id === CSPR_TOKEN_ID ? rawAmountB : rawAmountA;
|
|
333
|
+
|
|
334
|
+
innerArgs = buildAddLiquidityInnerArgs({
|
|
335
|
+
isCSPRPair: true,
|
|
336
|
+
tokenHash: otherToken.packageHash,
|
|
337
|
+
amountTokenDesired: tokenAmount,
|
|
338
|
+
amountTokenMin: calculateMinWithSlippage(tokenAmount, slippageBps),
|
|
339
|
+
amountCSPRMin: calculateMinWithSlippage(motesAmount, slippageBps),
|
|
340
|
+
accountHash,
|
|
341
|
+
deadline,
|
|
342
|
+
});
|
|
343
|
+
entryPoint = 'add_liquidity_cspr';
|
|
344
|
+
attachedValue = motesAmount;
|
|
345
|
+
} else {
|
|
346
|
+
// Sort tokens by package hash for consistency
|
|
347
|
+
const [sortedA, sortedB, sortedAmountA, sortedAmountB] =
|
|
348
|
+
tokenA.packageHash < tokenB.packageHash
|
|
349
|
+
? [tokenA, tokenB, rawAmountA, rawAmountB]
|
|
350
|
+
: [tokenB, tokenA, rawAmountB, rawAmountA];
|
|
351
|
+
|
|
352
|
+
innerArgs = buildAddLiquidityInnerArgs({
|
|
353
|
+
isCSPRPair: false,
|
|
354
|
+
tokenAHash: sortedA.packageHash,
|
|
355
|
+
tokenBHash: sortedB.packageHash,
|
|
356
|
+
amountADesired: sortedAmountA,
|
|
357
|
+
amountBDesired: sortedAmountB,
|
|
358
|
+
amountAMin: calculateMinWithSlippage(sortedAmountA, slippageBps),
|
|
359
|
+
amountBMin: calculateMinWithSlippage(sortedAmountB, slippageBps),
|
|
360
|
+
accountHash,
|
|
361
|
+
deadline,
|
|
362
|
+
});
|
|
363
|
+
entryPoint = 'add_liquidity';
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Use higher gas for potentially new pools
|
|
367
|
+
const gasCost = GAS_COSTS.addLiquidity; // caller can override if new pool
|
|
368
|
+
|
|
369
|
+
const proxyArgs = buildProxyWasmArgs({
|
|
370
|
+
routerPackageHash: this.networkConfig.routerPackageHash.replace('hash-', ''),
|
|
371
|
+
entryPoint,
|
|
372
|
+
innerArgs,
|
|
373
|
+
attachedValue,
|
|
374
|
+
});
|
|
375
|
+
const wasmBinary = await getProxyCallerWasm();
|
|
376
|
+
const transaction = buildWasmTransaction({
|
|
377
|
+
publicKey: params.senderPublicKey,
|
|
378
|
+
paymentAmount: gasCost.toString(),
|
|
379
|
+
wasmBinary,
|
|
380
|
+
runtimeArgs: proxyArgs,
|
|
381
|
+
networkConfig: this.networkConfig,
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Build approval transactions for non-CSPR tokens.
|
|
385
|
+
// When CSPR is one side, the router handles wrapping internally — no approval needed for that side.
|
|
386
|
+
const tokenBalances = [params.tokenABalance, params.tokenBBalance];
|
|
387
|
+
const rawAmounts = [rawAmountA, rawAmountB];
|
|
388
|
+
const approvals: TransactionBundle[] = [];
|
|
389
|
+
for (let i = 0; i < 2; i++) {
|
|
390
|
+
const token = [tokenA, tokenB][i];
|
|
391
|
+
if (token.id === CSPR_TOKEN_ID) continue; // Router handles CSPR wrapping
|
|
392
|
+
const approvalAmount = tokenBalances[i] ?? rawAmounts[i];
|
|
393
|
+
const approval = await this.buildApproval({
|
|
394
|
+
tokenContractPackageHash: token.packageHash,
|
|
395
|
+
spenderPackageHash: this.networkConfig.routerPackageHash,
|
|
396
|
+
amount: approvalAmount,
|
|
397
|
+
senderPublicKey: params.senderPublicKey,
|
|
398
|
+
});
|
|
399
|
+
approval.summary = tokenBalances[i]
|
|
400
|
+
? `Approve ${token.symbol} for router (full balance — one-time)`
|
|
401
|
+
: `Approve ${token.symbol} for router (liquidity amount only)`;
|
|
402
|
+
approvals.push(approval);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const summary = [
|
|
406
|
+
`Add liquidity: ${params.amountA} ${tokenA.symbol} + ${params.amountB} ${tokenB.symbol}`,
|
|
407
|
+
`Slippage tolerance: ${(slippageBps / 100).toFixed(2)}%`,
|
|
408
|
+
`Deadline: ${deadlineMinutes} minutes`,
|
|
409
|
+
`Estimated gas: ${Number(gasCost) / 1_000_000_000} CSPR`,
|
|
410
|
+
].join('\n');
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
transactionJson: JSON.stringify(transaction.toJSON()),
|
|
414
|
+
summary,
|
|
415
|
+
estimatedGasCost: `${Number(gasCost) / 1_000_000_000} CSPR`,
|
|
416
|
+
approvalsRequired: approvals.length > 0 ? approvals : undefined,
|
|
417
|
+
warnings: [],
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async buildRemoveLiquidity(params: RemoveLiquidityParams): Promise<TransactionBundle> {
|
|
422
|
+
// Fetch user's position to get LP balance and pair info
|
|
423
|
+
const positions = await this.liquidityApi.getPositions(params.senderPublicKey);
|
|
424
|
+
const position = positions.find(
|
|
425
|
+
p => p.pair_contract_package_hash === params.pairContractPackageHash,
|
|
426
|
+
);
|
|
427
|
+
if (!position) {
|
|
428
|
+
throw new Error(`No liquidity position found for pair ${params.pairContractPackageHash}`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const slippageBps = params.slippageBps ?? DEFAULT_SLIPPAGE_BPS;
|
|
432
|
+
const deadlineMinutes = params.deadlineMinutes ?? DEFAULT_DEADLINE_MINUTES;
|
|
433
|
+
const deadline = Date.now() + deadlineMinutes * 60 * 1000;
|
|
434
|
+
const accountHash = PublicKey.fromHex(params.senderPublicKey).accountHash().toPrefixedString();
|
|
435
|
+
|
|
436
|
+
// Calculate LP amount to burn
|
|
437
|
+
const lpBalance = BigInt(position.lp_token_balance);
|
|
438
|
+
const lpToBurn = (lpBalance * BigInt(params.percentage)) / 100n;
|
|
439
|
+
const lpTotalSupply = BigInt(position.pair_lp_tokens_total_supply);
|
|
440
|
+
|
|
441
|
+
// Estimate token amounts
|
|
442
|
+
const reserve0 = BigInt(position.pair.reserve0);
|
|
443
|
+
const reserve1 = BigInt(position.pair.reserve1);
|
|
444
|
+
const estAmount0 = (lpToBurn * reserve0 / lpTotalSupply).toString();
|
|
445
|
+
const estAmount1 = (lpToBurn * reserve1 / lpTotalSupply).toString();
|
|
446
|
+
|
|
447
|
+
const wcsprHash = this.networkConfig.wcsprPackageHash.replace('hash-', '');
|
|
448
|
+
const isCSPRPair =
|
|
449
|
+
position.pair.token0_contract_package_hash.replace('hash-', '') === wcsprHash ||
|
|
450
|
+
position.pair.token1_contract_package_hash.replace('hash-', '') === wcsprHash;
|
|
451
|
+
|
|
452
|
+
let innerArgs;
|
|
453
|
+
let entryPoint: string;
|
|
454
|
+
|
|
455
|
+
if (isCSPRPair) {
|
|
456
|
+
const isToken0CSPR = position.pair.token0_contract_package_hash.replace('hash-', '') === wcsprHash;
|
|
457
|
+
innerArgs = buildRemoveLiquidityInnerArgs({
|
|
458
|
+
isCSPRPair: true,
|
|
459
|
+
tokenHash: isToken0CSPR ? position.pair.token1_contract_package_hash : position.pair.token0_contract_package_hash,
|
|
460
|
+
liquidity: lpToBurn.toString(),
|
|
461
|
+
amountTokenMin: calculateMinWithSlippage(isToken0CSPR ? estAmount1 : estAmount0, slippageBps),
|
|
462
|
+
amountCSPRMin: calculateMinWithSlippage(isToken0CSPR ? estAmount0 : estAmount1, slippageBps),
|
|
463
|
+
accountHash,
|
|
464
|
+
deadline,
|
|
465
|
+
});
|
|
466
|
+
entryPoint = 'remove_liquidity_cspr';
|
|
467
|
+
} else {
|
|
468
|
+
innerArgs = buildRemoveLiquidityInnerArgs({
|
|
469
|
+
isCSPRPair: false,
|
|
470
|
+
tokenAHash: position.pair.token0_contract_package_hash,
|
|
471
|
+
tokenBHash: position.pair.token1_contract_package_hash,
|
|
472
|
+
liquidity: lpToBurn.toString(),
|
|
473
|
+
amountAMin: calculateMinWithSlippage(estAmount0, slippageBps),
|
|
474
|
+
amountBMin: calculateMinWithSlippage(estAmount1, slippageBps),
|
|
475
|
+
accountHash,
|
|
476
|
+
deadline,
|
|
477
|
+
});
|
|
478
|
+
entryPoint = 'remove_liquidity';
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const proxyArgs = buildProxyWasmArgs({
|
|
482
|
+
routerPackageHash: this.networkConfig.routerPackageHash.replace('hash-', ''),
|
|
483
|
+
entryPoint,
|
|
484
|
+
innerArgs,
|
|
485
|
+
attachedValue: '0',
|
|
486
|
+
});
|
|
487
|
+
const wasmBinary = await getProxyCallerWasm();
|
|
488
|
+
const transaction = buildWasmTransaction({
|
|
489
|
+
publicKey: params.senderPublicKey,
|
|
490
|
+
paymentAmount: GAS_COSTS.removeLiquidity.toString(),
|
|
491
|
+
wasmBinary,
|
|
492
|
+
runtimeArgs: proxyArgs,
|
|
493
|
+
networkConfig: this.networkConfig,
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
const summary = [
|
|
497
|
+
`Remove ${params.percentage}% liquidity from pair ${params.pairContractPackageHash.slice(0, 16)}...`,
|
|
498
|
+
`LP tokens to burn: ${lpToBurn.toString()}`,
|
|
499
|
+
`Estimated gas: ${Number(GAS_COSTS.removeLiquidity) / 1_000_000_000} CSPR`,
|
|
500
|
+
].join('\n');
|
|
501
|
+
|
|
502
|
+
return {
|
|
503
|
+
transactionJson: JSON.stringify(transaction.toJSON()),
|
|
504
|
+
summary,
|
|
505
|
+
estimatedGasCost: `${Number(GAS_COSTS.removeLiquidity) / 1_000_000_000} CSPR`,
|
|
506
|
+
warnings: [],
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// --- Transaction Submission ---
|
|
511
|
+
|
|
512
|
+
/** Submit a signed transaction to the Casper node RPC */
|
|
513
|
+
async submitTransaction(signedTransactionJson: string): Promise<SubmitResult> {
|
|
514
|
+
const handler = new HttpHandler(this.networkConfig.nodeRpcUrl);
|
|
515
|
+
const rpcClient = new RpcClient(handler);
|
|
516
|
+
const transaction = Transaction.fromJSON(JSON.parse(signedTransactionJson));
|
|
517
|
+
const result = await rpcClient.putTransaction(transaction);
|
|
518
|
+
return {
|
|
519
|
+
transactionHash: result.transactionHash?.toHex() ?? '',
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** @deprecated Use submitTransaction instead — all transactions now submit via node RPC */
|
|
524
|
+
async submitTransactionDirect(signedTransactionJson: string): Promise<SubmitResult> {
|
|
525
|
+
return this.submitTransaction(signedTransactionJson);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// --- Token Resolution ---
|
|
529
|
+
|
|
530
|
+
async resolveToken(identifier: string): Promise<Token> {
|
|
531
|
+
return this.tokenResolver.resolve(identifier);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function mapLiquidityPosition(raw: LiquidityPositionApiResponse): LiquidityPosition {
|
|
536
|
+
const lpBalance = BigInt(raw.lp_token_balance);
|
|
537
|
+
const totalSupply = BigInt(raw.pair_lp_tokens_total_supply);
|
|
538
|
+
const reserve0 = BigInt(raw.pair.reserve0);
|
|
539
|
+
const reserve1 = BigInt(raw.pair.reserve1);
|
|
540
|
+
|
|
541
|
+
const poolShare = totalSupply > 0n
|
|
542
|
+
? ((lpBalance * 10000n) / totalSupply).toString()
|
|
543
|
+
: '0';
|
|
544
|
+
|
|
545
|
+
const estToken0 = totalSupply > 0n ? (lpBalance * reserve0 / totalSupply).toString() : '0';
|
|
546
|
+
const estToken1 = totalSupply > 0n ? (lpBalance * reserve1 / totalSupply).toString() : '0';
|
|
547
|
+
|
|
548
|
+
const meta0 = raw.pair.token0_contract_package?.metadata;
|
|
549
|
+
const meta1 = raw.pair.token1_contract_package?.metadata;
|
|
550
|
+
|
|
551
|
+
return {
|
|
552
|
+
accountHash: raw.account_hash,
|
|
553
|
+
pairContractPackageHash: raw.pair_contract_package_hash,
|
|
554
|
+
lpTokenBalance: raw.lp_token_balance,
|
|
555
|
+
lpTokenTotalSupply: raw.pair_lp_tokens_total_supply,
|
|
556
|
+
pair: {
|
|
557
|
+
token0Symbol: meta0?.symbol ?? '',
|
|
558
|
+
token1Symbol: meta1?.symbol ?? '',
|
|
559
|
+
token0PackageHash: raw.pair.token0_contract_package_hash,
|
|
560
|
+
token1PackageHash: raw.pair.token1_contract_package_hash,
|
|
561
|
+
reserve0: raw.pair.reserve0,
|
|
562
|
+
reserve1: raw.pair.reserve1,
|
|
563
|
+
decimals0: raw.pair.decimals0,
|
|
564
|
+
decimals1: raw.pair.decimals1,
|
|
565
|
+
},
|
|
566
|
+
poolShare: (Number(poolShare) / 100).toFixed(2),
|
|
567
|
+
estimatedToken0Amount: estToken0,
|
|
568
|
+
estimatedToken1Amount: estToken1,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function buildWarnings(priceImpact: string, slippageBps: number): string[] {
|
|
573
|
+
const warnings: string[] = [];
|
|
574
|
+
const impact = parseFloat(priceImpact);
|
|
575
|
+
|
|
576
|
+
if (impact > PRICE_IMPACT_HIGH_THRESHOLD) {
|
|
577
|
+
warnings.push(`HIGH PRICE IMPACT: ${priceImpact}% — you will lose a significant portion of your trade to price impact.`);
|
|
578
|
+
} else if (impact > PRICE_IMPACT_WARNING_THRESHOLD) {
|
|
579
|
+
warnings.push(`Price impact is ${priceImpact}% — consider trading a smaller amount.`);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (slippageBps / 100 > SLIPPAGE_WARNING_THRESHOLD) {
|
|
583
|
+
warnings.push(`High slippage tolerance: ${(slippageBps / 100).toFixed(2)}% — you may receive significantly less than quoted.`);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return warnings;
|
|
587
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export interface NetworkConfig {
|
|
2
|
+
chainName: string;
|
|
3
|
+
apiUrl: string;
|
|
4
|
+
nodeRpcUrl: string;
|
|
5
|
+
routerPackageHash: string;
|
|
6
|
+
wcsprPackageHash: string;
|
|
7
|
+
gasPrice: number;
|
|
8
|
+
ttl: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const TESTNET_CONFIG: NetworkConfig = {
|
|
12
|
+
chainName: 'casper-test',
|
|
13
|
+
apiUrl: 'https://cspr-trade-api.dev.make.services',
|
|
14
|
+
nodeRpcUrl: 'https://node.testnet.casper.network/rpc',
|
|
15
|
+
routerPackageHash: 'hash-04a11a367e708c52557930c4e9c1301f4465100d1b1b6d0a62b48d3e32402867',
|
|
16
|
+
wcsprPackageHash: 'hash-3d80df21ba4ee4d66a2a1f60c32570dd5685e4b279f6538162a5fd1314847c1e',
|
|
17
|
+
gasPrice: 1,
|
|
18
|
+
ttl: 1_800_000, // 30 minutes
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const MAINNET_CONFIG: NetworkConfig = {
|
|
22
|
+
chainName: 'casper',
|
|
23
|
+
apiUrl: 'https://api.cspr.trade',
|
|
24
|
+
nodeRpcUrl: 'https://node.mainnet.casper.network/rpc',
|
|
25
|
+
// TODO: confirm mainnet addresses
|
|
26
|
+
routerPackageHash: 'hash-0000000000000000000000000000000000000000000000000000000000000000',
|
|
27
|
+
wcsprPackageHash: 'hash-0000000000000000000000000000000000000000000000000000000000000000',
|
|
28
|
+
gasPrice: 1,
|
|
29
|
+
ttl: 1_800_000,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function getNetworkConfig(network: 'mainnet' | 'testnet'): NetworkConfig {
|
|
33
|
+
return network === 'testnet' ? TESTNET_CONFIG : MAINNET_CONFIG;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Default slippage in basis points (3% = 300 bps) */
|
|
37
|
+
export const DEFAULT_SLIPPAGE_BPS = 300;
|
|
38
|
+
|
|
39
|
+
/** Default deadline in minutes */
|
|
40
|
+
export const DEFAULT_DEADLINE_MINUTES = 20;
|
|
41
|
+
|
|
42
|
+
/** Gas costs in motes (1 CSPR = 1_000_000_000 motes) */
|
|
43
|
+
export const GAS_COSTS = {
|
|
44
|
+
approve: 5_000_000_000n, // 5 CSPR
|
|
45
|
+
swapCsprForToken: 30_000_000_000n, // 30 CSPR
|
|
46
|
+
swapTokenForToken: 30_000_000_000n, // 30 CSPR
|
|
47
|
+
addLiquidity: 50_000_000_000n, // 50 CSPR
|
|
48
|
+
addNewLiquidity: 500_000_000_000n, // 500 CSPR
|
|
49
|
+
removeLiquidity: 30_000_000_000n, // 30 CSPR
|
|
50
|
+
} as const;
|
|
51
|
+
|
|
52
|
+
/** CSPR token constants */
|
|
53
|
+
export const CSPR_TOKEN_ID = 'cspr';
|
|
54
|
+
export const CSPR_DECIMALS = 9;
|
|
55
|
+
export const ZERO_HASH = '0000000000000000000000000000000000000000000000000000000000000000';
|
|
56
|
+
|
|
57
|
+
/** Safety thresholds */
|
|
58
|
+
export const PRICE_IMPACT_WARNING_THRESHOLD = 5;
|
|
59
|
+
export const PRICE_IMPACT_HIGH_THRESHOLD = 15;
|
|
60
|
+
export const SLIPPAGE_WARNING_THRESHOLD = 10;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { CsprTradeClient, type CsprTradeClientConfig } from './client.js';
|
|
2
|
+
export * from './types/index.js';
|
|
3
|
+
export { getNetworkConfig, TESTNET_CONFIG, MAINNET_CONFIG, type NetworkConfig } from './config.js';
|
|
4
|
+
export { toRawAmount, toFormattedAmount, calculateMinWithSlippage, calculateMaxWithSlippage } from './utils/amounts.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Currency } from '../types/index.js';
|
|
2
|
+
|
|
3
|
+
export class CurrencyResolver {
|
|
4
|
+
private cache: Currency[] | null = null;
|
|
5
|
+
|
|
6
|
+
constructor(private readonly fetchCurrencies: () => Promise<Currency[]>) {}
|
|
7
|
+
|
|
8
|
+
async resolveToId(code: string): Promise<number | undefined> {
|
|
9
|
+
const currencies = await this.getCurrencies();
|
|
10
|
+
const match = currencies.find(c => c.code.toLowerCase() === code.toLowerCase());
|
|
11
|
+
return match?.id;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async getCurrencies(): Promise<Currency[]> {
|
|
15
|
+
if (this.cache) return this.cache;
|
|
16
|
+
this.cache = await this.fetchCurrencies();
|
|
17
|
+
return this.cache;
|
|
18
|
+
}
|
|
19
|
+
}
|