@vultisig/rujira 1.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 +23 -0
- package/README.md +254 -0
- package/dist/assets/amount.d.ts +80 -0
- package/dist/assets/amount.d.ts.map +1 -0
- package/dist/assets/amount.js +186 -0
- package/dist/assets/asset.d.ts +43 -0
- package/dist/assets/asset.d.ts.map +1 -0
- package/dist/assets/asset.js +1 -0
- package/dist/assets/formats.d.ts +54 -0
- package/dist/assets/formats.d.ts.map +1 -0
- package/dist/assets/formats.js +164 -0
- package/dist/assets/index.d.ts +27 -0
- package/dist/assets/index.d.ts.map +1 -0
- package/dist/assets/index.js +45 -0
- package/dist/assets/registry.d.ts +37 -0
- package/dist/assets/registry.d.ts.map +1 -0
- package/dist/assets/registry.js +487 -0
- package/dist/assets/router.d.ts +44 -0
- package/dist/assets/router.d.ts.map +1 -0
- package/dist/assets/router.js +142 -0
- package/dist/client.d.ts +70 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +250 -0
- package/dist/config/constants.d.ts +25 -0
- package/dist/config/constants.d.ts.map +1 -0
- package/dist/config/constants.js +36 -0
- package/dist/config.d.ts +41 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +72 -0
- package/dist/discovery/discovery.d.ts +39 -0
- package/dist/discovery/discovery.d.ts.map +1 -0
- package/dist/discovery/discovery.js +250 -0
- package/dist/discovery/graphql-client.d.ts +46 -0
- package/dist/discovery/graphql-client.d.ts.map +1 -0
- package/dist/discovery/graphql-client.js +137 -0
- package/dist/discovery/index.d.ts +9 -0
- package/dist/discovery/index.d.ts.map +1 -0
- package/dist/discovery/index.js +7 -0
- package/dist/discovery/types.d.ts +62 -0
- package/dist/discovery/types.d.ts.map +1 -0
- package/dist/discovery/types.js +5 -0
- package/dist/easy-routes.d.ts +216 -0
- package/dist/easy-routes.d.ts.map +1 -0
- package/dist/easy-routes.js +241 -0
- package/dist/errors.d.ts +65 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +184 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/modules/assets.d.ts +68 -0
- package/dist/modules/assets.d.ts.map +1 -0
- package/dist/modules/assets.js +127 -0
- package/dist/modules/deposit.d.ts +152 -0
- package/dist/modules/deposit.d.ts.map +1 -0
- package/dist/modules/deposit.js +233 -0
- package/dist/modules/index.d.ts +12 -0
- package/dist/modules/index.d.ts.map +1 -0
- package/dist/modules/index.js +9 -0
- package/dist/modules/orderbook.d.ts +80 -0
- package/dist/modules/orderbook.d.ts.map +1 -0
- package/dist/modules/orderbook.js +320 -0
- package/dist/modules/swap.d.ts +48 -0
- package/dist/modules/swap.d.ts.map +1 -0
- package/dist/modules/swap.js +318 -0
- package/dist/modules/withdraw.d.ts +46 -0
- package/dist/modules/withdraw.d.ts.map +1 -0
- package/dist/modules/withdraw.js +218 -0
- package/dist/services/fee-estimator.d.ts +14 -0
- package/dist/services/fee-estimator.d.ts.map +1 -0
- package/dist/services/fee-estimator.js +89 -0
- package/dist/services/price-impact.d.ts +11 -0
- package/dist/services/price-impact.d.ts.map +1 -0
- package/dist/services/price-impact.js +58 -0
- package/dist/signer/index.d.ts +3 -0
- package/dist/signer/index.d.ts.map +1 -0
- package/dist/signer/index.js +1 -0
- package/dist/signer/keysign-builder.d.ts +21 -0
- package/dist/signer/keysign-builder.d.ts.map +1 -0
- package/dist/signer/keysign-builder.js +106 -0
- package/dist/signer/types.d.ts +81 -0
- package/dist/signer/types.d.ts.map +1 -0
- package/dist/signer/types.js +8 -0
- package/dist/signer/vultisig-provider.d.ts +33 -0
- package/dist/signer/vultisig-provider.d.ts.map +1 -0
- package/dist/signer/vultisig-provider.js +242 -0
- package/dist/types.d.ts +375 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +18 -0
- package/dist/utils/cache.d.ts +87 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +124 -0
- package/dist/utils/denom-conversion.d.ts +47 -0
- package/dist/utils/denom-conversion.d.ts.map +1 -0
- package/dist/utils/denom-conversion.js +105 -0
- package/dist/utils/encoding.d.ts +17 -0
- package/dist/utils/encoding.d.ts.map +1 -0
- package/dist/utils/encoding.js +55 -0
- package/dist/utils/format.d.ts +108 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +213 -0
- package/dist/utils/index.d.ts +10 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +9 -0
- package/dist/utils/memo.d.ts +107 -0
- package/dist/utils/memo.d.ts.map +1 -0
- package/dist/utils/memo.js +190 -0
- package/dist/utils/rate-limiter.d.ts +38 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/rate-limiter.js +67 -0
- package/dist/utils/type-guards.d.ts +22 -0
- package/dist/utils/type-guards.d.ts.map +1 -0
- package/dist/utils/type-guards.js +27 -0
- package/dist/validation/address-validator.d.ts +15 -0
- package/dist/validation/address-validator.d.ts.map +1 -0
- package/dist/validation/address-validator.js +75 -0
- package/package.json +98 -0
- package/src/__tests__/live/README.md +47 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deposit module for securing L1 assets on THORChain
|
|
3
|
+
* @module modules/deposit
|
|
4
|
+
*/
|
|
5
|
+
import { findAssetByFormat } from '../assets/index.js';
|
|
6
|
+
import { CHAIN_PROCESSING_TIMES } from '../config.js';
|
|
7
|
+
import { RujiraError, RujiraErrorCode, wrapError } from '../errors.js';
|
|
8
|
+
import { denomToAsset as sharedDenomToAsset, extractSymbol as sharedExtractSymbol, parseAsset as sharedParseAsset, } from '../utils/denom-conversion.js';
|
|
9
|
+
import { fromBaseUnits } from '../utils/format.js';
|
|
10
|
+
import { buildSecureMintMemo, validateMemoComponent } from '../utils/memo.js';
|
|
11
|
+
import { thornodeRateLimiter } from '../utils/rate-limiter.js';
|
|
12
|
+
import { isFinAsset } from '../utils/type-guards.js';
|
|
13
|
+
import { validateThorAddress as validateThorAddressStrict } from '../validation/address-validator.js';
|
|
14
|
+
// MODULE
|
|
15
|
+
/**
|
|
16
|
+
* Deposit module for securing L1 assets on THORChain
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const client = new RujiraClient({ network: 'mainnet' });
|
|
21
|
+
*
|
|
22
|
+
* // Prepare a BTC deposit
|
|
23
|
+
* const deposit = await client.deposit.prepare({
|
|
24
|
+
* fromAsset: 'BTC.BTC',
|
|
25
|
+
* amount: '1000000', // 0.01 BTC in sats
|
|
26
|
+
* thorAddress: 'thor1...'
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* // Use the returned details to send an L1 transaction
|
|
30
|
+
* console.log(`Send ${deposit.amount} to ${deposit.inboundAddress}`);
|
|
31
|
+
* console.log(`With memo: ${deposit.memo}`);
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export class RujiraDeposit {
|
|
35
|
+
constructor(client) {
|
|
36
|
+
this.client = client;
|
|
37
|
+
this.inboundCache = null;
|
|
38
|
+
// Short TTL: inbound addresses change during vault churn
|
|
39
|
+
this.CACHE_TTL_MS = 15000; // 15 seconds
|
|
40
|
+
this.thornodeUrl = client.config.restEndpoint;
|
|
41
|
+
}
|
|
42
|
+
// PUBLIC API
|
|
43
|
+
/**
|
|
44
|
+
* Prepare a deposit transaction
|
|
45
|
+
* Returns all details needed to send an L1 transaction
|
|
46
|
+
*/
|
|
47
|
+
async prepare(params) {
|
|
48
|
+
// Validate inputs
|
|
49
|
+
this.validateDepositParams(params);
|
|
50
|
+
// Parse asset to get chain
|
|
51
|
+
const { chain } = sharedParseAsset(params.fromAsset);
|
|
52
|
+
// Get inbound address for the chain
|
|
53
|
+
const inbound = await this.getInboundAddress(chain);
|
|
54
|
+
if (!inbound) {
|
|
55
|
+
throw new RujiraError(RujiraErrorCode.INVALID_ASSET, `No inbound address available for chain: ${chain}`);
|
|
56
|
+
}
|
|
57
|
+
// Build the deposit memo
|
|
58
|
+
const memo = this.buildDepositMemo(params.thorAddress, params.affiliate, params.affiliateBps);
|
|
59
|
+
// Determine resulting secured denom on THORChain (FIN denom when known)
|
|
60
|
+
let resultingDenom = params.fromAsset.toLowerCase().replace('.', '-');
|
|
61
|
+
const assetData = findAssetByFormat(params.fromAsset);
|
|
62
|
+
if (isFinAsset(assetData)) {
|
|
63
|
+
resultingDenom = assetData.formats.fin;
|
|
64
|
+
}
|
|
65
|
+
// Build warning if applicable
|
|
66
|
+
let warning;
|
|
67
|
+
if (inbound.halted) {
|
|
68
|
+
warning = `Chain ${chain} is currently halted. Deposits will not be processed.`;
|
|
69
|
+
}
|
|
70
|
+
else if (inbound.chain_trading_paused) {
|
|
71
|
+
warning = `Trading is paused for ${chain}. Deposits may be delayed.`;
|
|
72
|
+
}
|
|
73
|
+
else if (inbound.global_trading_paused) {
|
|
74
|
+
warning = 'Global trading is paused. Deposits may be delayed.';
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
chain,
|
|
78
|
+
inboundAddress: inbound.address,
|
|
79
|
+
memo,
|
|
80
|
+
amount: params.amount,
|
|
81
|
+
asset: params.fromAsset,
|
|
82
|
+
resultingDenom,
|
|
83
|
+
estimatedTimeMinutes: this.estimateDepositTime(chain),
|
|
84
|
+
minimumAmount: inbound.dust_threshold,
|
|
85
|
+
gasRate: inbound.gas_rate,
|
|
86
|
+
gasRateUnits: inbound.gas_rate_units,
|
|
87
|
+
warning,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Get secured balances for a THORChain address
|
|
92
|
+
*/
|
|
93
|
+
async getBalances(thorAddress) {
|
|
94
|
+
this.validateThorAddress(thorAddress);
|
|
95
|
+
try {
|
|
96
|
+
const response = await thornodeRateLimiter.fetch(`${this.thornodeUrl}/cosmos/bank/v1beta1/balances/${thorAddress}`);
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
99
|
+
}
|
|
100
|
+
const data = (await response.json());
|
|
101
|
+
const balances = [];
|
|
102
|
+
for (const balance of data.balances || []) {
|
|
103
|
+
// Map denom back to asset (best effort)
|
|
104
|
+
const asset = sharedDenomToAsset(balance.denom);
|
|
105
|
+
// THORChain secured assets always use 8 decimals for storage
|
|
106
|
+
const decimals = 8;
|
|
107
|
+
const formatted = fromBaseUnits(balance.amount, decimals);
|
|
108
|
+
const symbol = sharedExtractSymbol(asset || balance.denom);
|
|
109
|
+
balances.push({
|
|
110
|
+
denom: balance.denom,
|
|
111
|
+
asset: asset || balance.denom,
|
|
112
|
+
amount: balance.amount,
|
|
113
|
+
formatted,
|
|
114
|
+
decimals,
|
|
115
|
+
symbol,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return balances;
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
throw wrapError(error, RujiraErrorCode.NETWORK_ERROR);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get balance for a specific asset
|
|
126
|
+
*/
|
|
127
|
+
async getBalance(thorAddress, asset) {
|
|
128
|
+
const balances = await this.getBalances(thorAddress);
|
|
129
|
+
// Try to find by FIN denom from registry
|
|
130
|
+
const assetData = findAssetByFormat(asset);
|
|
131
|
+
if (isFinAsset(assetData)) {
|
|
132
|
+
return balances.find(b => b.denom === assetData.formats.fin) || null;
|
|
133
|
+
}
|
|
134
|
+
// Fallback to computed denom
|
|
135
|
+
const denom = asset.toLowerCase().replace('.', '-');
|
|
136
|
+
return balances.find(b => b.denom === denom) || null;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Get inbound address for a specific chain
|
|
140
|
+
*/
|
|
141
|
+
async getInboundAddress(chain) {
|
|
142
|
+
const addresses = await this.getInboundAddresses();
|
|
143
|
+
return addresses.find(a => a.chain === chain.toUpperCase()) || null;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Get all inbound addresses
|
|
147
|
+
* @param forceRefresh Bypass cache and fetch fresh data (use before executing deposits)
|
|
148
|
+
*/
|
|
149
|
+
async getInboundAddresses(forceRefresh = false) {
|
|
150
|
+
// Check cache (skip if forceRefresh)
|
|
151
|
+
if (!forceRefresh && this.inboundCache && Date.now() - this.inboundCache.timestamp < this.CACHE_TTL_MS) {
|
|
152
|
+
return this.inboundCache.data;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
const response = await thornodeRateLimiter.fetch(`${this.thornodeUrl}/thorchain/inbound_addresses`);
|
|
156
|
+
if (!response.ok) {
|
|
157
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
158
|
+
}
|
|
159
|
+
const data = (await response.json());
|
|
160
|
+
// Cache the result
|
|
161
|
+
this.inboundCache = {
|
|
162
|
+
data,
|
|
163
|
+
timestamp: Date.now(),
|
|
164
|
+
};
|
|
165
|
+
return data;
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
throw wrapError(error, RujiraErrorCode.NETWORK_ERROR);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Build deposit memo for L1 transaction
|
|
173
|
+
*/
|
|
174
|
+
buildDepositMemo(thorAddress, affiliate, affiliateBps) {
|
|
175
|
+
// Format: secure+:THORADDR for L1 → Secured deposits
|
|
176
|
+
// This mints secured assets on THORChain without swapping
|
|
177
|
+
let memo = buildSecureMintMemo(thorAddress);
|
|
178
|
+
if (affiliate && affiliateBps !== undefined && affiliateBps > 0) {
|
|
179
|
+
validateMemoComponent(affiliate, 'affiliate');
|
|
180
|
+
memo += `:${affiliate}:${affiliateBps}`;
|
|
181
|
+
}
|
|
182
|
+
return memo;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Estimate deposit confirmation time in minutes
|
|
186
|
+
*/
|
|
187
|
+
estimateDepositTime(chain) {
|
|
188
|
+
return CHAIN_PROCESSING_TIMES[chain.toUpperCase()] || 15;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Get supported chains for deposits
|
|
192
|
+
*/
|
|
193
|
+
getSupportedChains() {
|
|
194
|
+
return Object.keys(CHAIN_PROCESSING_TIMES);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Check if a chain is supported
|
|
198
|
+
*/
|
|
199
|
+
isChainSupported(chain) {
|
|
200
|
+
return chain.toUpperCase() in CHAIN_PROCESSING_TIMES;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Check if an asset can be deposited
|
|
204
|
+
*/
|
|
205
|
+
canDeposit(asset) {
|
|
206
|
+
const { chain } = sharedParseAsset(asset);
|
|
207
|
+
return this.isChainSupported(chain);
|
|
208
|
+
}
|
|
209
|
+
// INTERNAL HELPERS
|
|
210
|
+
validateDepositParams(params) {
|
|
211
|
+
// Validate asset
|
|
212
|
+
if (!params.fromAsset || !params.fromAsset.includes('.')) {
|
|
213
|
+
throw new RujiraError(RujiraErrorCode.INVALID_ASSET, `Invalid asset format: ${params.fromAsset}. Expected format: CHAIN.SYMBOL`);
|
|
214
|
+
}
|
|
215
|
+
const { chain } = sharedParseAsset(params.fromAsset);
|
|
216
|
+
if (!this.isChainSupported(chain)) {
|
|
217
|
+
throw new RujiraError(RujiraErrorCode.INVALID_ASSET, `Unsupported chain: ${chain}. Supported: ${this.getSupportedChains().join(', ')}`);
|
|
218
|
+
}
|
|
219
|
+
// Validate amount
|
|
220
|
+
if (!params.amount || !/^\d+$/.test(params.amount)) {
|
|
221
|
+
throw new RujiraError(RujiraErrorCode.INVALID_AMOUNT, 'Amount must be a positive integer in base units');
|
|
222
|
+
}
|
|
223
|
+
const amountBigInt = BigInt(params.amount);
|
|
224
|
+
if (amountBigInt <= 0n) {
|
|
225
|
+
throw new RujiraError(RujiraErrorCode.INVALID_AMOUNT, 'Amount must be greater than zero');
|
|
226
|
+
}
|
|
227
|
+
// Validate thor address
|
|
228
|
+
this.validateThorAddress(params.thorAddress);
|
|
229
|
+
}
|
|
230
|
+
validateThorAddress(address) {
|
|
231
|
+
validateThorAddressStrict(address);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module exports
|
|
3
|
+
* @module modules
|
|
4
|
+
*/
|
|
5
|
+
export { RujiraAssets } from './assets.js';
|
|
6
|
+
export { RujiraDeposit } from './deposit.js';
|
|
7
|
+
export { RujiraOrderbook } from './orderbook.js';
|
|
8
|
+
export { RujiraSwap } from './swap.js';
|
|
9
|
+
export { RujiraWithdraw } from './withdraw.js';
|
|
10
|
+
export type { DepositParams, InboundAddress, PreparedDeposit, SecuredBalance } from './deposit.js';
|
|
11
|
+
export type { PreparedWithdraw, WithdrawParams, WithdrawResult } from './withdraw.js';
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/modules/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AACtC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAG9C,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAGlG,YAAY,EAAE,gBAAgB,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module exports
|
|
3
|
+
* @module modules
|
|
4
|
+
*/
|
|
5
|
+
export { RujiraAssets } from './assets.js';
|
|
6
|
+
export { RujiraDeposit } from './deposit.js';
|
|
7
|
+
export { RujiraOrderbook } from './orderbook.js';
|
|
8
|
+
export { RujiraSwap } from './swap.js';
|
|
9
|
+
export { RujiraWithdraw } from './withdraw.js';
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orderbook module for limit orders on Rujira DEX
|
|
3
|
+
*/
|
|
4
|
+
import type { RujiraClient } from '../client.js';
|
|
5
|
+
import type { LimitOrderParams, Order, OrderBook, OrderResult, OrderSide } from '../types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Orderbook module for managing limit orders.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* const client = new RujiraClient({ network: 'mainnet', signer });
|
|
12
|
+
* await client.connect();
|
|
13
|
+
*
|
|
14
|
+
* const book = await client.orderbook.getOrderBook('RUNE/BTC');
|
|
15
|
+
* console.log('Best bid:', book.bids[0]?.price);
|
|
16
|
+
* console.log('Best ask:', book.asks[0]?.price);
|
|
17
|
+
*
|
|
18
|
+
* const order = await client.orderbook.placeOrder({
|
|
19
|
+
* pair: 'RUNE/BTC',
|
|
20
|
+
* side: 'buy',
|
|
21
|
+
* price: '0.000025',
|
|
22
|
+
* amount: '100000000',
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare class RujiraOrderbook {
|
|
27
|
+
private readonly client;
|
|
28
|
+
constructor(client: RujiraClient);
|
|
29
|
+
/**
|
|
30
|
+
* Convenience alias used by older examples/tests.
|
|
31
|
+
*
|
|
32
|
+
* Accepts two asset identifiers (any format supported by the asset registry)
|
|
33
|
+
* and resolves the FIN contract key as "<baseDenom>/<quoteDenom>".
|
|
34
|
+
*/
|
|
35
|
+
getBook(baseAsset: string, quoteAsset: string, limit?: number): Promise<OrderBook>;
|
|
36
|
+
/**
|
|
37
|
+
* Get the order book for a trading pair.
|
|
38
|
+
*
|
|
39
|
+
* @param pairOrContract - Trading pair string or contract address
|
|
40
|
+
* @param limit - Maximum entries per side (default: 50)
|
|
41
|
+
*/
|
|
42
|
+
getOrderBook(pairOrContract: string, limit?: number): Promise<OrderBook>;
|
|
43
|
+
/**
|
|
44
|
+
* Get contract configuration including pair info.
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
47
|
+
private getContractConfig;
|
|
48
|
+
/**
|
|
49
|
+
* Convert denom to asset identifier.
|
|
50
|
+
* @internal
|
|
51
|
+
*/
|
|
52
|
+
private denomToAsset;
|
|
53
|
+
/**
|
|
54
|
+
* Place a limit order.
|
|
55
|
+
*/
|
|
56
|
+
placeOrder(params: LimitOrderParams): Promise<OrderResult>;
|
|
57
|
+
/**
|
|
58
|
+
* Cancel an open order.
|
|
59
|
+
*/
|
|
60
|
+
cancelOrder(contractAddress: string, side: OrderSide, price: string): Promise<{
|
|
61
|
+
txHash: string;
|
|
62
|
+
}>;
|
|
63
|
+
/**
|
|
64
|
+
* Get user's open orders.
|
|
65
|
+
*/
|
|
66
|
+
getOrders(contractAddress: string, owner?: string, side?: OrderSide, limit?: number, offset?: number): Promise<Order[]>;
|
|
67
|
+
/**
|
|
68
|
+
* Get a specific order.
|
|
69
|
+
*/
|
|
70
|
+
getOrder(contractAddress: string, owner: string, side: OrderSide, price: string): Promise<Order | null>;
|
|
71
|
+
private resolveContract;
|
|
72
|
+
/**
|
|
73
|
+
* Uses string-based decimal arithmetic to avoid floating-point precision loss.
|
|
74
|
+
*/
|
|
75
|
+
private transformBookEntries;
|
|
76
|
+
private validateOrderParams;
|
|
77
|
+
private getOfferAsset;
|
|
78
|
+
private calculateOfferAmount;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=orderbook.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orderbook.d.ts","sourceRoot":"","sources":["../../src/modules/orderbook.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAGhD,OAAO,KAAK,EAIV,gBAAgB,EAChB,KAAK,EACL,SAAS,EAET,WAAW,EACX,SAAS,EACV,MAAM,aAAa,CAAA;AAIpB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,YAAY;IAEjD;;;;;OAKG;IACG,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,OAAO,CAAC,SAAS,CAAC;IAcpF;;;;;OAKG;IACG,YAAY,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,OAAO,CAAC,SAAS,CAAC;IAuC1E;;;OAGG;YACW,iBAAiB;IAiC/B;;;OAGG;IACH,OAAO,CAAC,YAAY;IAIpB;;OAEG;IACG,UAAU,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,WAAW,CAAC;IAsDhE;;OAEG;IACG,WAAW,CAAC,eAAe,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAcvG;;OAEG;IACG,SAAS,CAAC,eAAe,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,KAAK,SAAK,EAAE,MAAM,SAAI,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;IAiEpH;;OAEG;IACG,QAAQ,CAAC,eAAe,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC;YAgD/F,eAAe;IAa7B;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAsB5B,OAAO,CAAC,mBAAmB;YAcb,aAAa;IAyB3B,OAAO,CAAC,oBAAoB;CAS7B"}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orderbook module for limit orders on Rujira DEX
|
|
3
|
+
*/
|
|
4
|
+
import Big from 'big.js';
|
|
5
|
+
import { findAssetByFormat } from '../assets/index.js';
|
|
6
|
+
import { DEFAULT_MAKER_FEE, DEFAULT_TAKER_FEE } from '../config/constants.js';
|
|
7
|
+
import { RujiraError, RujiraErrorCode } from '../errors.js';
|
|
8
|
+
import { fromContractSide, toContractSide } from '../types.js';
|
|
9
|
+
import { denomToAsset as sharedDenomToAsset } from '../utils/denom-conversion.js';
|
|
10
|
+
/**
|
|
11
|
+
* Orderbook module for managing limit orders.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* const client = new RujiraClient({ network: 'mainnet', signer });
|
|
16
|
+
* await client.connect();
|
|
17
|
+
*
|
|
18
|
+
* const book = await client.orderbook.getOrderBook('RUNE/BTC');
|
|
19
|
+
* console.log('Best bid:', book.bids[0]?.price);
|
|
20
|
+
* console.log('Best ask:', book.asks[0]?.price);
|
|
21
|
+
*
|
|
22
|
+
* const order = await client.orderbook.placeOrder({
|
|
23
|
+
* pair: 'RUNE/BTC',
|
|
24
|
+
* side: 'buy',
|
|
25
|
+
* price: '0.000025',
|
|
26
|
+
* amount: '100000000',
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export class RujiraOrderbook {
|
|
31
|
+
constructor(client) {
|
|
32
|
+
this.client = client;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Convenience alias used by older examples/tests.
|
|
36
|
+
*
|
|
37
|
+
* Accepts two asset identifiers (any format supported by the asset registry)
|
|
38
|
+
* and resolves the FIN contract key as "<baseDenom>/<quoteDenom>".
|
|
39
|
+
*/
|
|
40
|
+
async getBook(baseAsset, quoteAsset, limit = 10) {
|
|
41
|
+
const base = findAssetByFormat(baseAsset);
|
|
42
|
+
const quote = findAssetByFormat(quoteAsset);
|
|
43
|
+
const baseDenom = base?.formats?.fin;
|
|
44
|
+
const quoteDenom = quote?.formats?.fin;
|
|
45
|
+
if (!baseDenom || !quoteDenom) {
|
|
46
|
+
throw new RujiraError(RujiraErrorCode.INVALID_ASSET, `Unknown asset(s): ${baseAsset}, ${quoteAsset}`);
|
|
47
|
+
}
|
|
48
|
+
return this.getOrderBook(`${baseDenom}/${quoteDenom}`, limit);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Get the order book for a trading pair.
|
|
52
|
+
*
|
|
53
|
+
* @param pairOrContract - Trading pair string or contract address
|
|
54
|
+
* @param limit - Maximum entries per side (default: 50)
|
|
55
|
+
*/
|
|
56
|
+
async getOrderBook(pairOrContract, limit = 50) {
|
|
57
|
+
const contractAddress = await this.resolveContract(pairOrContract);
|
|
58
|
+
const [response, config] = await Promise.all([
|
|
59
|
+
this.client.getOrderBook(contractAddress, limit),
|
|
60
|
+
this.getContractConfig(contractAddress),
|
|
61
|
+
]);
|
|
62
|
+
const bids = this.transformBookEntries(response.base, 'desc');
|
|
63
|
+
const asks = this.transformBookEntries(response.quote, 'asc');
|
|
64
|
+
const bestBid = bids[0]?.price ? parseFloat(bids[0].price) : 0;
|
|
65
|
+
const bestAsk = asks[0]?.price ? parseFloat(asks[0].price) : 0;
|
|
66
|
+
let spread = '0';
|
|
67
|
+
if (bestBid > 0 && bestAsk > 0) {
|
|
68
|
+
const midPrice = (bestAsk + bestBid) / 2;
|
|
69
|
+
spread = (((bestAsk - bestBid) / midPrice) * 100).toFixed(4);
|
|
70
|
+
}
|
|
71
|
+
const lastPrice = config.lastPrice || bids[0]?.price || asks[0]?.price || '0';
|
|
72
|
+
return {
|
|
73
|
+
pair: {
|
|
74
|
+
base: config.base,
|
|
75
|
+
quote: config.quote,
|
|
76
|
+
contractAddress,
|
|
77
|
+
tick: config.tick || '0',
|
|
78
|
+
takerFee: config.takerFee || DEFAULT_TAKER_FEE,
|
|
79
|
+
makerFee: config.makerFee || DEFAULT_MAKER_FEE,
|
|
80
|
+
},
|
|
81
|
+
bids,
|
|
82
|
+
asks,
|
|
83
|
+
spread,
|
|
84
|
+
lastPrice,
|
|
85
|
+
timestamp: Date.now(),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get contract configuration including pair info.
|
|
90
|
+
* @internal
|
|
91
|
+
*/
|
|
92
|
+
async getContractConfig(contractAddress) {
|
|
93
|
+
try {
|
|
94
|
+
const query = { config: {} };
|
|
95
|
+
const response = await this.client.queryContract(contractAddress, query);
|
|
96
|
+
const base = this.denomToAsset(response.denoms.base);
|
|
97
|
+
const quote = this.denomToAsset(response.denoms.quote);
|
|
98
|
+
return {
|
|
99
|
+
base,
|
|
100
|
+
quote,
|
|
101
|
+
tick: response.tick,
|
|
102
|
+
takerFee: response.fee?.taker,
|
|
103
|
+
makerFee: response.fee?.maker,
|
|
104
|
+
lastPrice: response.last_price,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return { base: '', quote: '' };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Convert denom to asset identifier.
|
|
113
|
+
* @internal
|
|
114
|
+
*/
|
|
115
|
+
denomToAsset(denom) {
|
|
116
|
+
return sharedDenomToAsset(denom) ?? denom.toUpperCase();
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Place a limit order.
|
|
120
|
+
*/
|
|
121
|
+
async placeOrder(params) {
|
|
122
|
+
this.validateOrderParams(params);
|
|
123
|
+
const contractAddress = await this.resolveContract(typeof params.pair === 'string' ? params.pair : params.pair.contractAddress);
|
|
124
|
+
const assetInfo = await this.getOfferAsset(params);
|
|
125
|
+
if (!assetInfo) {
|
|
126
|
+
throw new RujiraError(RujiraErrorCode.INVALID_ASSET, 'Could not determine offer asset for order');
|
|
127
|
+
}
|
|
128
|
+
// Convert SDK side to contract's Side enum format
|
|
129
|
+
const contractSide = toContractSide(params.side);
|
|
130
|
+
const orderTarget = [contractSide, params.price, params.amount];
|
|
131
|
+
const msg = {
|
|
132
|
+
order: [[orderTarget], null],
|
|
133
|
+
};
|
|
134
|
+
const funds = [
|
|
135
|
+
{
|
|
136
|
+
denom: assetInfo.denom,
|
|
137
|
+
amount: this.calculateOfferAmount(params),
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
const result = await this.client.executeContract(contractAddress, msg, funds);
|
|
141
|
+
const orderId = `${result.transactionHash}-0`;
|
|
142
|
+
return {
|
|
143
|
+
orderId,
|
|
144
|
+
txHash: result.transactionHash,
|
|
145
|
+
order: {
|
|
146
|
+
orderId,
|
|
147
|
+
owner: await this.client.getAddress(),
|
|
148
|
+
pair: typeof params.pair === 'string'
|
|
149
|
+
? { base: '', quote: '', contractAddress, tick: '0', takerFee: '0', makerFee: '0' }
|
|
150
|
+
: params.pair,
|
|
151
|
+
side: params.side,
|
|
152
|
+
price: params.price,
|
|
153
|
+
amount: params.amount,
|
|
154
|
+
filled: '0',
|
|
155
|
+
remaining: params.amount,
|
|
156
|
+
status: 'open',
|
|
157
|
+
createdAt: Date.now(),
|
|
158
|
+
updatedAt: Date.now(),
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Cancel an open order.
|
|
164
|
+
*/
|
|
165
|
+
async cancelOrder(contractAddress, side, price) {
|
|
166
|
+
// Convert SDK side to contract's Side enum format
|
|
167
|
+
const contractSide = toContractSide(side);
|
|
168
|
+
const orderTarget = [contractSide, price, null];
|
|
169
|
+
const msg = {
|
|
170
|
+
order: [[orderTarget], null],
|
|
171
|
+
};
|
|
172
|
+
const result = await this.client.executeContract(contractAddress, msg, []);
|
|
173
|
+
return { txHash: result.transactionHash };
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Get user's open orders.
|
|
177
|
+
*/
|
|
178
|
+
async getOrders(contractAddress, owner, side, limit = 30, offset = 0) {
|
|
179
|
+
const address = owner || (await this.client.getAddress());
|
|
180
|
+
// Convert SDK side to contract side for query
|
|
181
|
+
const contractSide = side ? toContractSide(side) : undefined;
|
|
182
|
+
const query = {
|
|
183
|
+
orders: {
|
|
184
|
+
owner: address,
|
|
185
|
+
side: contractSide,
|
|
186
|
+
offset,
|
|
187
|
+
limit,
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
const response = await this.client.queryContract(contractAddress, query);
|
|
191
|
+
return response.orders.map((o) => {
|
|
192
|
+
// Convert contract side back to SDK side
|
|
193
|
+
const sdkSide = fromContractSide(o.side);
|
|
194
|
+
return {
|
|
195
|
+
orderId: `${address}-${o.side}-${o.price}`,
|
|
196
|
+
owner: o.owner,
|
|
197
|
+
pair: {
|
|
198
|
+
base: '',
|
|
199
|
+
quote: '',
|
|
200
|
+
contractAddress,
|
|
201
|
+
tick: '0',
|
|
202
|
+
takerFee: '0',
|
|
203
|
+
makerFee: '0',
|
|
204
|
+
},
|
|
205
|
+
side: sdkSide,
|
|
206
|
+
price: o.price,
|
|
207
|
+
amount: o.offer,
|
|
208
|
+
filled: o.filled,
|
|
209
|
+
remaining: o.remaining,
|
|
210
|
+
status: BigInt(o.remaining) === 0n ? 'filled' : BigInt(o.filled) > 0n ? 'partial' : 'open',
|
|
211
|
+
createdAt: parseInt(o.updated_at),
|
|
212
|
+
updatedAt: parseInt(o.updated_at),
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Get a specific order.
|
|
218
|
+
*/
|
|
219
|
+
async getOrder(contractAddress, owner, side, price) {
|
|
220
|
+
// Convert SDK side to contract side for query
|
|
221
|
+
const contractSide = toContractSide(side);
|
|
222
|
+
const query = {
|
|
223
|
+
order: [owner, contractSide, price],
|
|
224
|
+
};
|
|
225
|
+
try {
|
|
226
|
+
const response = await this.client.queryContract(contractAddress, query);
|
|
227
|
+
// Convert contract side back to SDK side
|
|
228
|
+
const sdkSide = fromContractSide(response.side);
|
|
229
|
+
return {
|
|
230
|
+
orderId: `${owner}-${response.side}-${price}`,
|
|
231
|
+
owner: response.owner,
|
|
232
|
+
pair: {
|
|
233
|
+
base: '',
|
|
234
|
+
quote: '',
|
|
235
|
+
contractAddress,
|
|
236
|
+
tick: '0',
|
|
237
|
+
takerFee: '0',
|
|
238
|
+
makerFee: '0',
|
|
239
|
+
},
|
|
240
|
+
side: sdkSide,
|
|
241
|
+
price: response.price,
|
|
242
|
+
amount: response.offer,
|
|
243
|
+
filled: response.filled,
|
|
244
|
+
remaining: response.remaining,
|
|
245
|
+
status: BigInt(response.remaining) === 0n ? 'filled' : BigInt(response.filled) > 0n ? 'partial' : 'open',
|
|
246
|
+
createdAt: parseInt(response.updated_at),
|
|
247
|
+
updatedAt: parseInt(response.updated_at),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async resolveContract(pairOrContract) {
|
|
255
|
+
if (pairOrContract.startsWith('thor1')) {
|
|
256
|
+
return pairOrContract;
|
|
257
|
+
}
|
|
258
|
+
const knownContracts = this.client.config.contracts.finContracts;
|
|
259
|
+
if (knownContracts[pairOrContract]) {
|
|
260
|
+
return knownContracts[pairOrContract];
|
|
261
|
+
}
|
|
262
|
+
throw new RujiraError(RujiraErrorCode.INVALID_PAIR, `Unknown trading pair: ${pairOrContract}`);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Uses string-based decimal arithmetic to avoid floating-point precision loss.
|
|
266
|
+
*/
|
|
267
|
+
transformBookEntries(entries, sortOrder) {
|
|
268
|
+
const transformed = entries.map(e => {
|
|
269
|
+
const price = Big(e.price);
|
|
270
|
+
const amount = Big(e.total);
|
|
271
|
+
return {
|
|
272
|
+
price: e.price,
|
|
273
|
+
amount: e.total,
|
|
274
|
+
total: price.mul(amount).toFixed(8),
|
|
275
|
+
};
|
|
276
|
+
});
|
|
277
|
+
return transformed.sort((a, b) => {
|
|
278
|
+
const pA = Big(a.price);
|
|
279
|
+
const pB = Big(b.price);
|
|
280
|
+
return sortOrder === 'asc' ? pA.cmp(pB) : pB.cmp(pA);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
validateOrderParams(params) {
|
|
284
|
+
if (!params.price || parseFloat(params.price) <= 0) {
|
|
285
|
+
throw new RujiraError(RujiraErrorCode.INVALID_PRICE, 'Order price must be positive');
|
|
286
|
+
}
|
|
287
|
+
if (!params.amount || BigInt(params.amount) <= 0n) {
|
|
288
|
+
throw new RujiraError(RujiraErrorCode.INVALID_AMOUNT, 'Order amount must be positive');
|
|
289
|
+
}
|
|
290
|
+
if (!['buy', 'sell'].includes(params.side)) {
|
|
291
|
+
throw new RujiraError(RujiraErrorCode.INVALID_AMOUNT, 'Order side must be "buy" or "sell"');
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async getOfferAsset(params) {
|
|
295
|
+
const getAssetInfo = (assetId) => {
|
|
296
|
+
const asset = findAssetByFormat(assetId);
|
|
297
|
+
if (!asset?.formats?.fin)
|
|
298
|
+
return undefined;
|
|
299
|
+
return { denom: asset.formats.fin, decimals: asset.decimals?.fin ?? 8 };
|
|
300
|
+
};
|
|
301
|
+
if (typeof params.pair !== 'string' && params.pair.base && params.pair.quote) {
|
|
302
|
+
const assetId = params.side === 'buy' ? params.pair.quote : params.pair.base;
|
|
303
|
+
return getAssetInfo(assetId);
|
|
304
|
+
}
|
|
305
|
+
const contractAddress = await this.resolveContract(typeof params.pair === 'string' ? params.pair : params.pair.contractAddress);
|
|
306
|
+
const config = await this.getContractConfig(contractAddress);
|
|
307
|
+
if (params.side === 'buy') {
|
|
308
|
+
return config.quote ? getAssetInfo(config.quote) : getAssetInfo('THOR.RUNE');
|
|
309
|
+
}
|
|
310
|
+
return config.base ? getAssetInfo(config.base) : undefined;
|
|
311
|
+
}
|
|
312
|
+
calculateOfferAmount(params) {
|
|
313
|
+
if (params.side === 'buy') {
|
|
314
|
+
const amount = Big(params.amount);
|
|
315
|
+
const price = Big(params.price);
|
|
316
|
+
return amount.mul(price).toFixed(0, 0);
|
|
317
|
+
}
|
|
318
|
+
return params.amount;
|
|
319
|
+
}
|
|
320
|
+
}
|