@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.
Files changed (118) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +254 -0
  3. package/dist/assets/amount.d.ts +80 -0
  4. package/dist/assets/amount.d.ts.map +1 -0
  5. package/dist/assets/amount.js +186 -0
  6. package/dist/assets/asset.d.ts +43 -0
  7. package/dist/assets/asset.d.ts.map +1 -0
  8. package/dist/assets/asset.js +1 -0
  9. package/dist/assets/formats.d.ts +54 -0
  10. package/dist/assets/formats.d.ts.map +1 -0
  11. package/dist/assets/formats.js +164 -0
  12. package/dist/assets/index.d.ts +27 -0
  13. package/dist/assets/index.d.ts.map +1 -0
  14. package/dist/assets/index.js +45 -0
  15. package/dist/assets/registry.d.ts +37 -0
  16. package/dist/assets/registry.d.ts.map +1 -0
  17. package/dist/assets/registry.js +487 -0
  18. package/dist/assets/router.d.ts +44 -0
  19. package/dist/assets/router.d.ts.map +1 -0
  20. package/dist/assets/router.js +142 -0
  21. package/dist/client.d.ts +70 -0
  22. package/dist/client.d.ts.map +1 -0
  23. package/dist/client.js +250 -0
  24. package/dist/config/constants.d.ts +25 -0
  25. package/dist/config/constants.d.ts.map +1 -0
  26. package/dist/config/constants.js +36 -0
  27. package/dist/config.d.ts +41 -0
  28. package/dist/config.d.ts.map +1 -0
  29. package/dist/config.js +72 -0
  30. package/dist/discovery/discovery.d.ts +39 -0
  31. package/dist/discovery/discovery.d.ts.map +1 -0
  32. package/dist/discovery/discovery.js +250 -0
  33. package/dist/discovery/graphql-client.d.ts +46 -0
  34. package/dist/discovery/graphql-client.d.ts.map +1 -0
  35. package/dist/discovery/graphql-client.js +137 -0
  36. package/dist/discovery/index.d.ts +9 -0
  37. package/dist/discovery/index.d.ts.map +1 -0
  38. package/dist/discovery/index.js +7 -0
  39. package/dist/discovery/types.d.ts +62 -0
  40. package/dist/discovery/types.d.ts.map +1 -0
  41. package/dist/discovery/types.js +5 -0
  42. package/dist/easy-routes.d.ts +216 -0
  43. package/dist/easy-routes.d.ts.map +1 -0
  44. package/dist/easy-routes.js +241 -0
  45. package/dist/errors.d.ts +65 -0
  46. package/dist/errors.d.ts.map +1 -0
  47. package/dist/errors.js +184 -0
  48. package/dist/index.d.ts +46 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +46 -0
  51. package/dist/modules/assets.d.ts +68 -0
  52. package/dist/modules/assets.d.ts.map +1 -0
  53. package/dist/modules/assets.js +127 -0
  54. package/dist/modules/deposit.d.ts +152 -0
  55. package/dist/modules/deposit.d.ts.map +1 -0
  56. package/dist/modules/deposit.js +233 -0
  57. package/dist/modules/index.d.ts +12 -0
  58. package/dist/modules/index.d.ts.map +1 -0
  59. package/dist/modules/index.js +9 -0
  60. package/dist/modules/orderbook.d.ts +80 -0
  61. package/dist/modules/orderbook.d.ts.map +1 -0
  62. package/dist/modules/orderbook.js +320 -0
  63. package/dist/modules/swap.d.ts +48 -0
  64. package/dist/modules/swap.d.ts.map +1 -0
  65. package/dist/modules/swap.js +318 -0
  66. package/dist/modules/withdraw.d.ts +46 -0
  67. package/dist/modules/withdraw.d.ts.map +1 -0
  68. package/dist/modules/withdraw.js +218 -0
  69. package/dist/services/fee-estimator.d.ts +14 -0
  70. package/dist/services/fee-estimator.d.ts.map +1 -0
  71. package/dist/services/fee-estimator.js +89 -0
  72. package/dist/services/price-impact.d.ts +11 -0
  73. package/dist/services/price-impact.d.ts.map +1 -0
  74. package/dist/services/price-impact.js +58 -0
  75. package/dist/signer/index.d.ts +3 -0
  76. package/dist/signer/index.d.ts.map +1 -0
  77. package/dist/signer/index.js +1 -0
  78. package/dist/signer/keysign-builder.d.ts +21 -0
  79. package/dist/signer/keysign-builder.d.ts.map +1 -0
  80. package/dist/signer/keysign-builder.js +106 -0
  81. package/dist/signer/types.d.ts +81 -0
  82. package/dist/signer/types.d.ts.map +1 -0
  83. package/dist/signer/types.js +8 -0
  84. package/dist/signer/vultisig-provider.d.ts +33 -0
  85. package/dist/signer/vultisig-provider.d.ts.map +1 -0
  86. package/dist/signer/vultisig-provider.js +242 -0
  87. package/dist/types.d.ts +375 -0
  88. package/dist/types.d.ts.map +1 -0
  89. package/dist/types.js +18 -0
  90. package/dist/utils/cache.d.ts +87 -0
  91. package/dist/utils/cache.d.ts.map +1 -0
  92. package/dist/utils/cache.js +124 -0
  93. package/dist/utils/denom-conversion.d.ts +47 -0
  94. package/dist/utils/denom-conversion.d.ts.map +1 -0
  95. package/dist/utils/denom-conversion.js +105 -0
  96. package/dist/utils/encoding.d.ts +17 -0
  97. package/dist/utils/encoding.d.ts.map +1 -0
  98. package/dist/utils/encoding.js +55 -0
  99. package/dist/utils/format.d.ts +108 -0
  100. package/dist/utils/format.d.ts.map +1 -0
  101. package/dist/utils/format.js +213 -0
  102. package/dist/utils/index.d.ts +10 -0
  103. package/dist/utils/index.d.ts.map +1 -0
  104. package/dist/utils/index.js +9 -0
  105. package/dist/utils/memo.d.ts +107 -0
  106. package/dist/utils/memo.d.ts.map +1 -0
  107. package/dist/utils/memo.js +190 -0
  108. package/dist/utils/rate-limiter.d.ts +38 -0
  109. package/dist/utils/rate-limiter.d.ts.map +1 -0
  110. package/dist/utils/rate-limiter.js +67 -0
  111. package/dist/utils/type-guards.d.ts +22 -0
  112. package/dist/utils/type-guards.d.ts.map +1 -0
  113. package/dist/utils/type-guards.js +27 -0
  114. package/dist/validation/address-validator.d.ts +15 -0
  115. package/dist/validation/address-validator.d.ts.map +1 -0
  116. package/dist/validation/address-validator.js +75 -0
  117. package/package.json +98 -0
  118. 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
+ }