@vultisig/rujira 8.0.0 → 10.0.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/CHANGELOG.md +14 -0
- package/dist/client.d.ts +6 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +6 -0
- package/dist/config/constants.d.ts.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -5
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/modules/ghost.d.ts +123 -0
- package/dist/modules/ghost.d.ts.map +1 -0
- package/dist/modules/ghost.js +293 -0
- package/dist/modules/orderbook.d.ts +19 -1
- package/dist/modules/orderbook.d.ts.map +1 -1
- package/dist/modules/orderbook.js +36 -5
- package/dist/modules/perps.d.ts +127 -0
- package/dist/modules/perps.d.ts.map +1 -0
- package/dist/modules/perps.js +214 -0
- package/dist/modules/staking.d.ts +81 -0
- package/dist/modules/staking.d.ts.map +1 -0
- package/dist/modules/staking.js +139 -0
- package/dist/services/fee-estimator.d.ts.map +1 -1
- package/dist/services/fee-estimator.js +2 -6
- package/dist/services/price-impact.d.ts.map +1 -1
- package/dist/signer/types.d.ts +1 -1
- package/dist/signer/types.d.ts.map +1 -1
- package/dist/types.d.ts +6 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/format.d.ts +8 -0
- package/dist/utils/format.d.ts.map +1 -1
- package/dist/utils/format.js +18 -0
- package/package.json +15 -3
|
@@ -7,6 +7,7 @@ import { DEFAULT_MAKER_FEE, DEFAULT_TAKER_FEE } from '../config/constants.js';
|
|
|
7
7
|
import { RujiraError, RujiraErrorCode } from '../errors.js';
|
|
8
8
|
import { fromContractSide, toContractSide } from '../types.js';
|
|
9
9
|
import { denomToAsset as sharedDenomToAsset } from '../utils/denom-conversion.js';
|
|
10
|
+
import { isPositiveBigInt } from '../utils/format.js';
|
|
10
11
|
/**
|
|
11
12
|
* Orderbook module for managing limit orders.
|
|
12
13
|
*
|
|
@@ -127,7 +128,7 @@ export class RujiraOrderbook {
|
|
|
127
128
|
}
|
|
128
129
|
// Convert SDK side to contract's Side enum format
|
|
129
130
|
const contractSide = toContractSide(params.side);
|
|
130
|
-
const orderTarget = [contractSide, params.price, params.amount];
|
|
131
|
+
const orderTarget = [contractSide, { fixed: params.price }, params.amount];
|
|
131
132
|
const msg = {
|
|
132
133
|
order: [[orderTarget], null],
|
|
133
134
|
};
|
|
@@ -163,15 +164,45 @@ export class RujiraOrderbook {
|
|
|
163
164
|
* Cancel an open order.
|
|
164
165
|
*/
|
|
165
166
|
async cancelOrder(contractAddress, side, price) {
|
|
166
|
-
// Convert SDK side to contract's Side enum format
|
|
167
167
|
const contractSide = toContractSide(side);
|
|
168
|
-
const orderTarget = [contractSide, price, null];
|
|
168
|
+
const orderTarget = [contractSide, { fixed: price }, null];
|
|
169
169
|
const msg = {
|
|
170
170
|
order: [[orderTarget], null],
|
|
171
171
|
};
|
|
172
172
|
const result = await this.client.executeContract(contractAddress, msg, []);
|
|
173
173
|
return { txHash: result.transactionHash };
|
|
174
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Build a limit order transaction without executing.
|
|
177
|
+
* Returns contract address, message, and funds for external signing.
|
|
178
|
+
*/
|
|
179
|
+
async buildPlaceOrder(params) {
|
|
180
|
+
this.validateOrderParams(params);
|
|
181
|
+
const contractAddress = await this.resolveContract(typeof params.pair === 'string' ? params.pair : params.pair.contractAddress);
|
|
182
|
+
const assetInfo = await this.getOfferAsset(params);
|
|
183
|
+
if (!assetInfo) {
|
|
184
|
+
throw new RujiraError(RujiraErrorCode.INVALID_ASSET, 'Could not determine offer asset for order');
|
|
185
|
+
}
|
|
186
|
+
const contractSide = toContractSide(params.side);
|
|
187
|
+
const orderTarget = [contractSide, { fixed: params.price }, params.amount];
|
|
188
|
+
const msg = {
|
|
189
|
+
order: [[orderTarget], null],
|
|
190
|
+
};
|
|
191
|
+
const funds = [{ denom: assetInfo.denom, amount: this.calculateOfferAmount(params) }];
|
|
192
|
+
return { contractAddress, msg, funds };
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Build a cancel order transaction without executing.
|
|
196
|
+
*/
|
|
197
|
+
buildCancelOrder(contractAddress, side, price) {
|
|
198
|
+
const contractSide = toContractSide(side);
|
|
199
|
+
const orderTarget = [contractSide, { fixed: price }, null];
|
|
200
|
+
return {
|
|
201
|
+
contractAddress,
|
|
202
|
+
msg: { order: [[orderTarget], null] },
|
|
203
|
+
funds: [],
|
|
204
|
+
};
|
|
205
|
+
}
|
|
175
206
|
/**
|
|
176
207
|
* Get user's open orders.
|
|
177
208
|
*/
|
|
@@ -284,8 +315,8 @@ export class RujiraOrderbook {
|
|
|
284
315
|
if (!params.price || parseFloat(params.price) <= 0) {
|
|
285
316
|
throw new RujiraError(RujiraErrorCode.INVALID_PRICE, 'Order price must be positive');
|
|
286
317
|
}
|
|
287
|
-
if (!params.amount ||
|
|
288
|
-
throw new RujiraError(RujiraErrorCode.INVALID_AMOUNT, 'Order amount must be positive');
|
|
318
|
+
if (!params.amount || !isPositiveBigInt(params.amount)) {
|
|
319
|
+
throw new RujiraError(RujiraErrorCode.INVALID_AMOUNT, 'Order amount must be a positive integer string');
|
|
289
320
|
}
|
|
290
321
|
if (!['buy', 'sell'].includes(params.side)) {
|
|
291
322
|
throw new RujiraError(RujiraErrorCode.INVALID_AMOUNT, 'Order side must be "buy" or "sell"');
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Perpetual futures module for Levana perps on Rujira
|
|
3
|
+
* @module modules/perps
|
|
4
|
+
*/
|
|
5
|
+
import type { Coin } from '@cosmjs/proto-signing';
|
|
6
|
+
import type { RujiraClient } from '../client.js';
|
|
7
|
+
export type PerpsMarket = {
|
|
8
|
+
/** Market contract address */
|
|
9
|
+
address: string;
|
|
10
|
+
/** Market name (e.g. 'BTC_USDC') */
|
|
11
|
+
name: string;
|
|
12
|
+
/** Base asset (e.g. 'BTC.BTC') */
|
|
13
|
+
baseAsset: string;
|
|
14
|
+
/** Quote asset (e.g. 'ETH-USDC-...') */
|
|
15
|
+
quoteAsset: string;
|
|
16
|
+
};
|
|
17
|
+
export type PerpsTransactionParams = {
|
|
18
|
+
contractAddress: string;
|
|
19
|
+
executeMsg: object;
|
|
20
|
+
funds: Coin[];
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Levana perpetual futures module.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* const client = new RujiraClient();
|
|
28
|
+
* await client.connect();
|
|
29
|
+
*
|
|
30
|
+
* // List markets
|
|
31
|
+
* const markets = await client.perps.getMarkets();
|
|
32
|
+
*
|
|
33
|
+
* // Build open position
|
|
34
|
+
* const tx = client.perps.buildOpenPosition({
|
|
35
|
+
* market: 'thor1cyd6...',
|
|
36
|
+
* direction: 'long',
|
|
37
|
+
* leverage: '10',
|
|
38
|
+
* collateralDenom: 'eth-usdc-0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
|
39
|
+
* collateralAmount: '100000000',
|
|
40
|
+
* });
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare class RujiraPerps {
|
|
44
|
+
private readonly client;
|
|
45
|
+
constructor(client: RujiraClient);
|
|
46
|
+
/**
|
|
47
|
+
* Get available perps markets from GraphQL.
|
|
48
|
+
*/
|
|
49
|
+
getMarkets(): Promise<PerpsMarket[]>;
|
|
50
|
+
/**
|
|
51
|
+
* Query market status from the Levana contract.
|
|
52
|
+
*/
|
|
53
|
+
getMarketStatus(marketAddress: string): Promise<Record<string, unknown>>;
|
|
54
|
+
/**
|
|
55
|
+
* Query positions for an owner on a market.
|
|
56
|
+
*/
|
|
57
|
+
getPositions(marketAddress: string, owner: string): Promise<Record<string, unknown>>;
|
|
58
|
+
/**
|
|
59
|
+
* Query limit orders for an owner on a market.
|
|
60
|
+
*/
|
|
61
|
+
getLimitOrders(marketAddress: string, owner: string): Promise<Record<string, unknown>>;
|
|
62
|
+
/**
|
|
63
|
+
* Build open position transaction.
|
|
64
|
+
*/
|
|
65
|
+
buildOpenPosition(params: {
|
|
66
|
+
market: string;
|
|
67
|
+
direction: 'long' | 'short';
|
|
68
|
+
leverage: string;
|
|
69
|
+
collateralDenom: string;
|
|
70
|
+
collateralAmount: string;
|
|
71
|
+
takeProfit?: string;
|
|
72
|
+
stopLoss?: string;
|
|
73
|
+
}): PerpsTransactionParams;
|
|
74
|
+
/**
|
|
75
|
+
* Build close position transaction.
|
|
76
|
+
*/
|
|
77
|
+
buildClosePosition(params: {
|
|
78
|
+
market: string;
|
|
79
|
+
positionId: string;
|
|
80
|
+
}): PerpsTransactionParams;
|
|
81
|
+
/**
|
|
82
|
+
* Build update take profit transaction.
|
|
83
|
+
*/
|
|
84
|
+
buildUpdateTakeProfit(params: {
|
|
85
|
+
market: string;
|
|
86
|
+
positionId: string;
|
|
87
|
+
price: string;
|
|
88
|
+
}): PerpsTransactionParams;
|
|
89
|
+
/**
|
|
90
|
+
* Build update stop loss transaction.
|
|
91
|
+
*/
|
|
92
|
+
buildUpdateStopLoss(params: {
|
|
93
|
+
market: string;
|
|
94
|
+
positionId: string;
|
|
95
|
+
stopLoss: string | 'remove';
|
|
96
|
+
}): PerpsTransactionParams;
|
|
97
|
+
/**
|
|
98
|
+
* Build add collateral transaction.
|
|
99
|
+
*/
|
|
100
|
+
buildAddCollateral(params: {
|
|
101
|
+
market: string;
|
|
102
|
+
positionId: string;
|
|
103
|
+
denom: string;
|
|
104
|
+
amount: string;
|
|
105
|
+
}): PerpsTransactionParams;
|
|
106
|
+
/**
|
|
107
|
+
* Build place limit order transaction.
|
|
108
|
+
*/
|
|
109
|
+
buildPlaceLimitOrder(params: {
|
|
110
|
+
market: string;
|
|
111
|
+
direction: 'long' | 'short';
|
|
112
|
+
leverage: string;
|
|
113
|
+
triggerPrice: string;
|
|
114
|
+
collateralDenom: string;
|
|
115
|
+
collateralAmount: string;
|
|
116
|
+
takeProfit?: string;
|
|
117
|
+
stopLoss?: string;
|
|
118
|
+
}): PerpsTransactionParams;
|
|
119
|
+
/**
|
|
120
|
+
* Build cancel limit order transaction.
|
|
121
|
+
*/
|
|
122
|
+
buildCancelLimitOrder(params: {
|
|
123
|
+
market: string;
|
|
124
|
+
orderId: string;
|
|
125
|
+
}): PerpsTransactionParams;
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=perps.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"perps.d.ts","sourceRoot":"","sources":["../../src/modules/perps.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Perpetual futures module for Levana perps on Rujira
|
|
3
|
+
* @module modules/perps
|
|
4
|
+
*/
|
|
5
|
+
import { RujiraError, RujiraErrorCode, wrapError } from '../errors.js';
|
|
6
|
+
import { isPositiveBigInt, isPositiveNumber } from '../utils/format.js';
|
|
7
|
+
// GraphQL endpoint
|
|
8
|
+
const RUJIRA_GRAPHQL_URL = 'https://api.rujira.network/api/graphql';
|
|
9
|
+
const PERPS_MARKETS_QUERY = `
|
|
10
|
+
{
|
|
11
|
+
perps {
|
|
12
|
+
id
|
|
13
|
+
name
|
|
14
|
+
address
|
|
15
|
+
baseAsset { asset }
|
|
16
|
+
quoteAsset { asset }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
`;
|
|
20
|
+
/**
|
|
21
|
+
* Levana perpetual futures module.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* const client = new RujiraClient();
|
|
26
|
+
* await client.connect();
|
|
27
|
+
*
|
|
28
|
+
* // List markets
|
|
29
|
+
* const markets = await client.perps.getMarkets();
|
|
30
|
+
*
|
|
31
|
+
* // Build open position
|
|
32
|
+
* const tx = client.perps.buildOpenPosition({
|
|
33
|
+
* market: 'thor1cyd6...',
|
|
34
|
+
* direction: 'long',
|
|
35
|
+
* leverage: '10',
|
|
36
|
+
* collateralDenom: 'eth-usdc-0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
|
37
|
+
* collateralAmount: '100000000',
|
|
38
|
+
* });
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export class RujiraPerps {
|
|
42
|
+
constructor(client) {
|
|
43
|
+
this.client = client;
|
|
44
|
+
}
|
|
45
|
+
// --- Market Discovery ---
|
|
46
|
+
/**
|
|
47
|
+
* Get available perps markets from GraphQL.
|
|
48
|
+
*/
|
|
49
|
+
async getMarkets() {
|
|
50
|
+
try {
|
|
51
|
+
const response = await fetch(RUJIRA_GRAPHQL_URL, {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: { 'Content-Type': 'application/json' },
|
|
54
|
+
body: JSON.stringify({ query: PERPS_MARKETS_QUERY }),
|
|
55
|
+
});
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
throw new RujiraError(RujiraErrorCode.NETWORK_ERROR, `GraphQL request failed: ${response.status}`);
|
|
58
|
+
}
|
|
59
|
+
const json = (await response.json());
|
|
60
|
+
if (json.errors?.length) {
|
|
61
|
+
throw new RujiraError(RujiraErrorCode.NETWORK_ERROR, `GraphQL errors: ${json.errors[0].message}`);
|
|
62
|
+
}
|
|
63
|
+
return (json.data?.perps ?? []).map(m => ({
|
|
64
|
+
address: m.address,
|
|
65
|
+
name: m.name,
|
|
66
|
+
baseAsset: m.baseAsset.asset,
|
|
67
|
+
quoteAsset: m.quoteAsset.asset,
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
throw wrapError(error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// --- Market Queries ---
|
|
75
|
+
/**
|
|
76
|
+
* Query market status from the Levana contract.
|
|
77
|
+
*/
|
|
78
|
+
async getMarketStatus(marketAddress) {
|
|
79
|
+
try {
|
|
80
|
+
return await this.client.queryContract(marketAddress, { status: { price: null } });
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
throw wrapError(error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Query positions for an owner on a market.
|
|
88
|
+
*/
|
|
89
|
+
async getPositions(marketAddress, owner) {
|
|
90
|
+
try {
|
|
91
|
+
return await this.client.queryContract(marketAddress, {
|
|
92
|
+
positions: { owner, start_after: null, limit: 50 },
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
throw wrapError(error);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Query limit orders for an owner on a market.
|
|
101
|
+
*/
|
|
102
|
+
async getLimitOrders(marketAddress, owner) {
|
|
103
|
+
try {
|
|
104
|
+
return await this.client.queryContract(marketAddress, {
|
|
105
|
+
limit_orders: { owner, start_after: null, limit: 50 },
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
throw wrapError(error);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// --- Position Management ---
|
|
113
|
+
/**
|
|
114
|
+
* Build open position transaction.
|
|
115
|
+
*/
|
|
116
|
+
buildOpenPosition(params) {
|
|
117
|
+
if (!params.leverage || !isPositiveNumber(params.leverage)) {
|
|
118
|
+
throw new RujiraError(RujiraErrorCode.INVALID_AMOUNT, 'Leverage must be a positive number string (e.g. "2", "1.5")');
|
|
119
|
+
}
|
|
120
|
+
if (!params.collateralAmount || !isPositiveBigInt(params.collateralAmount)) {
|
|
121
|
+
throw new RujiraError(RujiraErrorCode.INVALID_AMOUNT, 'Collateral amount must be a positive integer string');
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
contractAddress: params.market,
|
|
125
|
+
executeMsg: {
|
|
126
|
+
open_position: {
|
|
127
|
+
slippage_assert: null,
|
|
128
|
+
leverage: params.leverage,
|
|
129
|
+
direction: params.direction,
|
|
130
|
+
stop_loss_override: params.stopLoss ?? null,
|
|
131
|
+
take_profit: params.takeProfit ?? '+Inf',
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
funds: [{ denom: params.collateralDenom, amount: params.collateralAmount }],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Build close position transaction.
|
|
139
|
+
*/
|
|
140
|
+
buildClosePosition(params) {
|
|
141
|
+
return {
|
|
142
|
+
contractAddress: params.market,
|
|
143
|
+
executeMsg: { close_position: { id: params.positionId, slippage_assert: null } },
|
|
144
|
+
funds: [],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Build update take profit transaction.
|
|
149
|
+
*/
|
|
150
|
+
buildUpdateTakeProfit(params) {
|
|
151
|
+
return {
|
|
152
|
+
contractAddress: params.market,
|
|
153
|
+
executeMsg: { update_position_take_profit_price: { id: params.positionId, price: params.price } },
|
|
154
|
+
funds: [],
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Build update stop loss transaction.
|
|
159
|
+
*/
|
|
160
|
+
buildUpdateStopLoss(params) {
|
|
161
|
+
return {
|
|
162
|
+
contractAddress: params.market,
|
|
163
|
+
executeMsg: { update_position_stop_loss_price: { id: params.positionId, stop_loss: params.stopLoss } },
|
|
164
|
+
funds: [],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Build add collateral transaction.
|
|
169
|
+
*/
|
|
170
|
+
buildAddCollateral(params) {
|
|
171
|
+
if (!params.amount || !isPositiveBigInt(params.amount)) {
|
|
172
|
+
throw new RujiraError(RujiraErrorCode.INVALID_AMOUNT, 'Collateral amount must be a positive integer string');
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
contractAddress: params.market,
|
|
176
|
+
executeMsg: { update_position_add_collateral_impact_leverage: { id: params.positionId } },
|
|
177
|
+
funds: [{ denom: params.denom, amount: params.amount }],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Build place limit order transaction.
|
|
182
|
+
*/
|
|
183
|
+
buildPlaceLimitOrder(params) {
|
|
184
|
+
if (!params.leverage || !isPositiveNumber(params.leverage)) {
|
|
185
|
+
throw new RujiraError(RujiraErrorCode.INVALID_AMOUNT, 'Leverage must be a positive number string (e.g. "2", "1.5")');
|
|
186
|
+
}
|
|
187
|
+
if (!params.collateralAmount || !isPositiveBigInt(params.collateralAmount)) {
|
|
188
|
+
throw new RujiraError(RujiraErrorCode.INVALID_AMOUNT, 'Collateral amount must be a positive integer string');
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
contractAddress: params.market,
|
|
192
|
+
executeMsg: {
|
|
193
|
+
place_limit_order: {
|
|
194
|
+
trigger_price: params.triggerPrice,
|
|
195
|
+
leverage: params.leverage,
|
|
196
|
+
direction: params.direction,
|
|
197
|
+
stop_loss_override: params.stopLoss ?? null,
|
|
198
|
+
take_profit: params.takeProfit ?? '+Inf',
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
funds: [{ denom: params.collateralDenom, amount: params.collateralAmount }],
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Build cancel limit order transaction.
|
|
206
|
+
*/
|
|
207
|
+
buildCancelLimitOrder(params) {
|
|
208
|
+
return {
|
|
209
|
+
contractAddress: params.market,
|
|
210
|
+
executeMsg: { cancel_limit_order: { order_id: params.orderId } },
|
|
211
|
+
funds: [],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Staking module for RUJI token staking on THORChain
|
|
3
|
+
* @module modules/staking
|
|
4
|
+
*/
|
|
5
|
+
import type { Coin } from '@cosmjs/proto-signing';
|
|
6
|
+
import type { RujiraClient } from '../client.js';
|
|
7
|
+
export type StakingPosition = {
|
|
8
|
+
/** Bonded RUJI amount in base units (8 decimals) */
|
|
9
|
+
bonded: string;
|
|
10
|
+
/** Human-readable bonded amount */
|
|
11
|
+
bondedFormatted: string;
|
|
12
|
+
/** Bond asset ticker */
|
|
13
|
+
bondTicker: string;
|
|
14
|
+
/** Pending USDC rewards in base units (6 decimals) */
|
|
15
|
+
rewards: string;
|
|
16
|
+
/** Human-readable rewards amount */
|
|
17
|
+
rewardsFormatted: string;
|
|
18
|
+
/** Rewards asset ticker */
|
|
19
|
+
rewardsTicker: string;
|
|
20
|
+
/** Annual percentage rate as decimal (0.15 = 15%) */
|
|
21
|
+
apr: number;
|
|
22
|
+
/** APR as percentage string (e.g. "15.00") */
|
|
23
|
+
aprPercent: string;
|
|
24
|
+
};
|
|
25
|
+
export type StakeParams = {
|
|
26
|
+
/** Amount of RUJI to stake in base units (8 decimals) */
|
|
27
|
+
amount: string;
|
|
28
|
+
};
|
|
29
|
+
export type UnstakeParams = {
|
|
30
|
+
/** Amount of RUJI to unstake in base units (8 decimals) */
|
|
31
|
+
amount: string;
|
|
32
|
+
};
|
|
33
|
+
export type StakeTransactionParams = {
|
|
34
|
+
contractAddress: string;
|
|
35
|
+
executeMsg: object;
|
|
36
|
+
funds: Coin[];
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* RUJI staking module.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```typescript
|
|
43
|
+
* const client = new RujiraClient();
|
|
44
|
+
* await client.connect();
|
|
45
|
+
*
|
|
46
|
+
* // Query staking position
|
|
47
|
+
* const position = await client.staking.getPosition('thor1...');
|
|
48
|
+
* console.log(`Staked: ${position.bondedFormatted} RUJI`);
|
|
49
|
+
* console.log(`Rewards: ${position.rewardsFormatted} USDC`);
|
|
50
|
+
*
|
|
51
|
+
* // Build stake transaction params (for external signing)
|
|
52
|
+
* const tx = client.staking.buildStake({ amount: '100000000' });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare class RujiraStaking {
|
|
56
|
+
private readonly client;
|
|
57
|
+
constructor(client: RujiraClient);
|
|
58
|
+
/**
|
|
59
|
+
* Get staking position for a THORChain address.
|
|
60
|
+
* Queries the Rujira GraphQL API for bonded amount, pending rewards, and APR.
|
|
61
|
+
*/
|
|
62
|
+
getPosition(address: string): Promise<StakingPosition | null>;
|
|
63
|
+
/**
|
|
64
|
+
* Build stake (bond) transaction parameters.
|
|
65
|
+
* Returns the contract address, execute message, and funds needed for signing.
|
|
66
|
+
*/
|
|
67
|
+
buildStake(params: StakeParams): StakeTransactionParams;
|
|
68
|
+
/**
|
|
69
|
+
* Build unstake (withdraw) transaction parameters.
|
|
70
|
+
*/
|
|
71
|
+
buildUnstake(params: UnstakeParams): StakeTransactionParams;
|
|
72
|
+
/**
|
|
73
|
+
* Build claim rewards transaction parameters.
|
|
74
|
+
*/
|
|
75
|
+
buildClaimRewards(): StakeTransactionParams;
|
|
76
|
+
/** Staking contract address */
|
|
77
|
+
get contractAddress(): string;
|
|
78
|
+
/** Bond denom (x/ruji) */
|
|
79
|
+
get bondDenom(): string;
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=staking.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"staking.d.ts","sourceRoot":"","sources":["../../src/modules/staking.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAA;AAEjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAgBhD,MAAM,MAAM,eAAe,GAAG;IAC5B,oDAAoD;IACpD,MAAM,EAAE,MAAM,CAAA;IACd,mCAAmC;IACnC,eAAe,EAAE,MAAM,CAAA;IACvB,wBAAwB;IACxB,UAAU,EAAE,MAAM,CAAA;IAClB,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAA;IACf,oCAAoC;IACpC,gBAAgB,EAAE,MAAM,CAAA;IACxB,2BAA2B;IAC3B,aAAa,EAAE,MAAM,CAAA;IACrB,qDAAqD;IACrD,GAAG,EAAE,MAAM,CAAA;IACX,8CAA8C;IAC9C,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,2DAA2D;IAC3D,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,eAAe,EAAE,MAAM,CAAA;IACvB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,IAAI,EAAE,CAAA;CACd,CAAA;AA4BD;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,MAAM,EAAE,YAAY;IAIhC;;;OAGG;IACG,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IA+CnE;;;OAGG;IACH,UAAU,CAAC,MAAM,EAAE,WAAW,GAAG,sBAAsB;IAYvD;;OAEG;IACH,YAAY,CAAC,MAAM,EAAE,aAAa,GAAG,sBAAsB;IAY3D;;OAEG;IACH,iBAAiB,IAAI,sBAAsB;IAQ3C,+BAA+B;IAC/B,IAAI,eAAe,IAAI,MAAM,CAE5B;IAED,0BAA0B;IAC1B,IAAI,SAAS,IAAI,MAAM,CAEtB;CACF"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Staking module for RUJI token staking on THORChain
|
|
3
|
+
* @module modules/staking
|
|
4
|
+
*/
|
|
5
|
+
import { RujiraError, RujiraErrorCode, wrapError } from '../errors.js';
|
|
6
|
+
import { base64Encode } from '../utils/encoding.js';
|
|
7
|
+
import { fromBaseUnits, isPositiveBigInt } from '../utils/format.js';
|
|
8
|
+
import { validateThorAddress } from '../validation/address-validator.js';
|
|
9
|
+
// Constants
|
|
10
|
+
const STAKING_CONTRACT = 'thor13g83nn5ef4qzqeafp0508dnvkvm0zqr3sj7eefcn5umu65gqluusrml5cr';
|
|
11
|
+
const STAKING_GRAPHQL_URL = 'https://api.vultisig.com/ruji/api/graphql';
|
|
12
|
+
const BOND_DENOM = 'x/ruji';
|
|
13
|
+
const BOND_DECIMALS = 8;
|
|
14
|
+
const REVENUE_DECIMALS = 6;
|
|
15
|
+
const STAKING_QUERY = `
|
|
16
|
+
query ($id: ID!) {
|
|
17
|
+
node(id: $id) {
|
|
18
|
+
... on Account {
|
|
19
|
+
stakingV2 {
|
|
20
|
+
bonded { amount asset { metadata { symbol } } }
|
|
21
|
+
pendingRevenue { amount asset { metadata { symbol } } }
|
|
22
|
+
pool { summary { apr { value } } }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
`;
|
|
28
|
+
/**
|
|
29
|
+
* RUJI staking module.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* const client = new RujiraClient();
|
|
34
|
+
* await client.connect();
|
|
35
|
+
*
|
|
36
|
+
* // Query staking position
|
|
37
|
+
* const position = await client.staking.getPosition('thor1...');
|
|
38
|
+
* console.log(`Staked: ${position.bondedFormatted} RUJI`);
|
|
39
|
+
* console.log(`Rewards: ${position.rewardsFormatted} USDC`);
|
|
40
|
+
*
|
|
41
|
+
* // Build stake transaction params (for external signing)
|
|
42
|
+
* const tx = client.staking.buildStake({ amount: '100000000' });
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export class RujiraStaking {
|
|
46
|
+
constructor(client) {
|
|
47
|
+
this.client = client;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Get staking position for a THORChain address.
|
|
51
|
+
* Queries the Rujira GraphQL API for bonded amount, pending rewards, and APR.
|
|
52
|
+
*/
|
|
53
|
+
async getPosition(address) {
|
|
54
|
+
validateThorAddress(address);
|
|
55
|
+
try {
|
|
56
|
+
const nodeId = base64Encode(`Account:${address}`);
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const timeout = setTimeout(() => controller.abort(), 15_000);
|
|
59
|
+
const response = await fetch(STAKING_GRAPHQL_URL, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: { 'Content-Type': 'application/json' },
|
|
62
|
+
body: JSON.stringify({ query: STAKING_QUERY, variables: { id: nodeId } }),
|
|
63
|
+
signal: controller.signal,
|
|
64
|
+
}).finally(() => clearTimeout(timeout));
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
throw new RujiraError(RujiraErrorCode.NETWORK_ERROR, `GraphQL request failed: ${response.status}`);
|
|
67
|
+
}
|
|
68
|
+
const json = (await response.json());
|
|
69
|
+
if (json.errors?.length) {
|
|
70
|
+
throw new RujiraError(RujiraErrorCode.NETWORK_ERROR, `GraphQL errors: ${json.errors[0].message}`);
|
|
71
|
+
}
|
|
72
|
+
const stakingData = json.data?.node?.stakingV2?.[0];
|
|
73
|
+
if (!stakingData) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const bonded = stakingData.bonded.amount || '0';
|
|
77
|
+
const rewards = stakingData.pendingRevenue?.amount || '0';
|
|
78
|
+
const aprValue = stakingData.pool?.summary?.apr?.value;
|
|
79
|
+
return {
|
|
80
|
+
bonded,
|
|
81
|
+
bondedFormatted: fromBaseUnits(bonded, BOND_DECIMALS),
|
|
82
|
+
bondTicker: stakingData.bonded.asset?.metadata?.symbol || 'RUJI',
|
|
83
|
+
rewards,
|
|
84
|
+
rewardsFormatted: fromBaseUnits(rewards, REVENUE_DECIMALS),
|
|
85
|
+
rewardsTicker: stakingData.pendingRevenue?.asset?.metadata?.symbol || 'USDC',
|
|
86
|
+
apr: aprValue && !Number.isNaN(parseFloat(aprValue)) ? parseFloat(aprValue) : 0,
|
|
87
|
+
aprPercent: aprValue && !Number.isNaN(parseFloat(aprValue)) ? (parseFloat(aprValue) * 100).toFixed(2) : '0.00',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
throw wrapError(error);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Build stake (bond) transaction parameters.
|
|
96
|
+
* Returns the contract address, execute message, and funds needed for signing.
|
|
97
|
+
*/
|
|
98
|
+
buildStake(params) {
|
|
99
|
+
if (!params.amount || !isPositiveBigInt(params.amount)) {
|
|
100
|
+
throw new RujiraError(RujiraErrorCode.INVALID_AMOUNT, 'Stake amount must be positive');
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
contractAddress: STAKING_CONTRACT,
|
|
104
|
+
executeMsg: { account: { bond: {} } },
|
|
105
|
+
funds: [{ denom: BOND_DENOM, amount: params.amount }],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Build unstake (withdraw) transaction parameters.
|
|
110
|
+
*/
|
|
111
|
+
buildUnstake(params) {
|
|
112
|
+
if (!params.amount || !isPositiveBigInt(params.amount)) {
|
|
113
|
+
throw new RujiraError(RujiraErrorCode.INVALID_AMOUNT, 'Unstake amount must be positive');
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
contractAddress: STAKING_CONTRACT,
|
|
117
|
+
executeMsg: { account: { withdraw: { amount: params.amount } } },
|
|
118
|
+
funds: [],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Build claim rewards transaction parameters.
|
|
123
|
+
*/
|
|
124
|
+
buildClaimRewards() {
|
|
125
|
+
return {
|
|
126
|
+
contractAddress: STAKING_CONTRACT,
|
|
127
|
+
executeMsg: { account: { claim: {} } },
|
|
128
|
+
funds: [],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/** Staking contract address */
|
|
132
|
+
get contractAddress() {
|
|
133
|
+
return STAKING_CONTRACT;
|
|
134
|
+
}
|
|
135
|
+
/** Bond denom (x/ruji) */
|
|
136
|
+
get bondDenom() {
|
|
137
|
+
return BOND_DENOM;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fee-estimator.d.ts","sourceRoot":"","sources":["../../src/services/fee-estimator.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAqBH;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,
|
|
1
|
+
{"version":3,"file":"fee-estimator.d.ts","sourceRoot":"","sources":["../../src/services/fee-estimator.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAqBH;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAqE9G"}
|
|
@@ -34,7 +34,7 @@ export async function estimateWithdrawFee(thornodeUrl, asset, _amount) {
|
|
|
34
34
|
const response = await thornodeRateLimiter.fetch(`${thornodeUrl}/thorchain/inbound_addresses`);
|
|
35
35
|
if (response.ok) {
|
|
36
36
|
const addresses = (await response.json());
|
|
37
|
-
const chainInfo = addresses.find(
|
|
37
|
+
const chainInfo = addresses.find(a => a.chain === chain);
|
|
38
38
|
if (chainInfo?.outbound_fee) {
|
|
39
39
|
gasAssetOutboundFee = chainInfo.outbound_fee;
|
|
40
40
|
}
|
|
@@ -72,11 +72,7 @@ export async function estimateWithdrawFee(thornodeUrl, asset, _amount) {
|
|
|
72
72
|
const gasBalRune = BigInt(gasPool.balance_rune);
|
|
73
73
|
const tgtBalAsset = BigInt(targetPool.balance_asset);
|
|
74
74
|
const tgtBalRune = BigInt(targetPool.balance_rune);
|
|
75
|
-
if (gasFee === 0n ||
|
|
76
|
-
gasBalAsset === 0n ||
|
|
77
|
-
gasBalRune === 0n ||
|
|
78
|
-
tgtBalAsset === 0n ||
|
|
79
|
-
tgtBalRune === 0n) {
|
|
75
|
+
if (gasFee === 0n || gasBalAsset === 0n || gasBalRune === 0n || tgtBalAsset === 0n || tgtBalRune === 0n) {
|
|
80
76
|
return gasAssetOutboundFee;
|
|
81
77
|
}
|
|
82
78
|
const runeFee = (gasFee * gasBalRune) / gasBalAsset;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"price-impact.d.ts","sourceRoot":"","sources":["../../src/services/price-impact.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,
|
|
1
|
+
{"version":3,"file":"price-impact.d.ts","sourceRoot":"","sources":["../../src/services/price-impact.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAE5C;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAAG;IACtC,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,SAAS,GAAG,IAAI,CAAA;IAC3B;;;OAGG;IACH,mBAAmB,EAAE,OAAO,CAAA;CAC7B,CAAA;AAaD;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,WAAW,EACX,YAAY,EACZ,SAAS,EACT,mBAAmB,GACpB,EAAE,yBAAyB,GAAG,MAAM,CAyCpC"}
|