@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,250 @@
|
|
|
1
|
+
import { MAINNET_CONFIG } from '../config.js';
|
|
2
|
+
import { DEFAULT_MAKER_FEE, DEFAULT_TAKER_FEE } from '../config/constants.js';
|
|
3
|
+
import { thornodeRateLimiter } from '../utils/rate-limiter.js';
|
|
4
|
+
import { GraphQLClient } from './graphql-client.js';
|
|
5
|
+
export class RujiraDiscovery {
|
|
6
|
+
constructor(options = {}) {
|
|
7
|
+
this.cache = null;
|
|
8
|
+
this.pendingDiscovery = null;
|
|
9
|
+
const networkConfig = MAINNET_CONFIG;
|
|
10
|
+
this.rpcEndpoint = options.rpcEndpoint || networkConfig.rpcEndpoint;
|
|
11
|
+
this.cacheTtl = options.cacheTtl ?? 5 * 60 * 1000;
|
|
12
|
+
this.debug = options.debug || false;
|
|
13
|
+
this.finCodeId = networkConfig.contracts.finCodeId;
|
|
14
|
+
// Mainnet-only GraphQL endpoints
|
|
15
|
+
this.graphql = new GraphQLClient({
|
|
16
|
+
httpEndpoint: 'https://api.rujira.network/api/graphql',
|
|
17
|
+
wsEndpoint: 'wss://api.rujira.network/socket',
|
|
18
|
+
...options.graphql,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
async discoverContracts(forceRefresh = false) {
|
|
22
|
+
if (!forceRefresh && this.cache && this.isCacheValid()) {
|
|
23
|
+
this.log('Using cached contracts');
|
|
24
|
+
return this.cache;
|
|
25
|
+
}
|
|
26
|
+
if (this.pendingDiscovery) {
|
|
27
|
+
this.log('Discovery already in progress, waiting for existing request...');
|
|
28
|
+
return this.pendingDiscovery;
|
|
29
|
+
}
|
|
30
|
+
this.log('Discovering contracts...');
|
|
31
|
+
this.pendingDiscovery = this.performDiscovery();
|
|
32
|
+
try {
|
|
33
|
+
return await this.pendingDiscovery;
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
this.pendingDiscovery = null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async performDiscovery() {
|
|
40
|
+
try {
|
|
41
|
+
const contracts = await this.discoverViaGraphQL();
|
|
42
|
+
this.cache = contracts;
|
|
43
|
+
return contracts;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
this.log('GraphQL discovery failed, analyzing error...', error);
|
|
47
|
+
if (!this.shouldFallbackToChain(error)) {
|
|
48
|
+
this.log('Error is not recoverable, failing without fallback');
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
this.log('Error is recoverable, trying chain query fallback...');
|
|
52
|
+
try {
|
|
53
|
+
const contracts = await this.discoverViaChain();
|
|
54
|
+
this.cache = contracts;
|
|
55
|
+
return contracts;
|
|
56
|
+
}
|
|
57
|
+
catch (chainError) {
|
|
58
|
+
this.log('Chain discovery also failed', chainError);
|
|
59
|
+
if (error instanceof GraphQLClient.GraphQLError && error.type === 'auth') {
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
fin: {},
|
|
64
|
+
discoveredAt: Date.now(),
|
|
65
|
+
source: 'fallback-failed',
|
|
66
|
+
lastError: error instanceof Error ? error.message : String(error),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
shouldFallbackToChain(error) {
|
|
72
|
+
if (error instanceof GraphQLClient.GraphQLError) {
|
|
73
|
+
switch (error.type) {
|
|
74
|
+
case 'auth':
|
|
75
|
+
return false;
|
|
76
|
+
default:
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
async findMarket(baseAsset, quoteAsset) {
|
|
83
|
+
try {
|
|
84
|
+
const market = await this.graphql.getMarket(baseAsset, quoteAsset);
|
|
85
|
+
if (market) {
|
|
86
|
+
return this.transformMarket(market);
|
|
87
|
+
}
|
|
88
|
+
const reverseMarket = await this.graphql.getMarket(quoteAsset, baseAsset);
|
|
89
|
+
if (reverseMarket) {
|
|
90
|
+
return this.transformMarket(reverseMarket);
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
this.log('findMarket failed', error);
|
|
96
|
+
const contracts = await this.discoverContracts();
|
|
97
|
+
const pairKey = `${baseAsset}/${quoteAsset}`;
|
|
98
|
+
const reversePairKey = `${quoteAsset}/${baseAsset}`;
|
|
99
|
+
const address = contracts.fin[pairKey] || contracts.fin[reversePairKey];
|
|
100
|
+
if (address) {
|
|
101
|
+
return {
|
|
102
|
+
address,
|
|
103
|
+
baseAsset,
|
|
104
|
+
quoteAsset,
|
|
105
|
+
baseDenom: '',
|
|
106
|
+
quoteDenom: '',
|
|
107
|
+
tick: '0',
|
|
108
|
+
takerFee: DEFAULT_TAKER_FEE,
|
|
109
|
+
makerFee: DEFAULT_MAKER_FEE,
|
|
110
|
+
active: true,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async getContractAddress(baseAsset, quoteAsset) {
|
|
117
|
+
const market = await this.findMarket(baseAsset, quoteAsset);
|
|
118
|
+
return market?.address || null;
|
|
119
|
+
}
|
|
120
|
+
async listMarkets() {
|
|
121
|
+
try {
|
|
122
|
+
const response = await this.graphql.getMarkets();
|
|
123
|
+
return response.markets.map(m => this.transformMarket(m));
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
this.log('listMarkets failed', error);
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
clearCache() {
|
|
131
|
+
this.cache = null;
|
|
132
|
+
}
|
|
133
|
+
getCacheTtl() {
|
|
134
|
+
return this.cacheTtl;
|
|
135
|
+
}
|
|
136
|
+
getCacheStatus() {
|
|
137
|
+
const ttl = this.cacheTtl;
|
|
138
|
+
if (!this.cache) {
|
|
139
|
+
return { cached: false, valid: false, ttl };
|
|
140
|
+
}
|
|
141
|
+
const age = Date.now() - this.cache.discoveredAt;
|
|
142
|
+
const valid = this.isCacheValid();
|
|
143
|
+
return { cached: true, age, valid, ttl };
|
|
144
|
+
}
|
|
145
|
+
async discoverViaGraphQL() {
|
|
146
|
+
const response = await this.graphql.getMarkets();
|
|
147
|
+
const fin = {};
|
|
148
|
+
for (const market of response.markets) {
|
|
149
|
+
const pairKey = `${market.denoms.base}/${market.denoms.quote}`;
|
|
150
|
+
fin[pairKey] = market.address;
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
fin,
|
|
154
|
+
discoveredAt: Date.now(),
|
|
155
|
+
source: 'graphql',
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
async discoverViaChain() {
|
|
159
|
+
const fin = {};
|
|
160
|
+
const baseUrl = this.rpcEndpoint.replace(':26657', '').replace('rpc', 'thornode');
|
|
161
|
+
const restUrl = baseUrl.includes('thornode') ? baseUrl : 'https://thornode.ninerealms.com';
|
|
162
|
+
try {
|
|
163
|
+
// Paginate through all contracts for this code ID
|
|
164
|
+
const PAGE_SIZE = 50;
|
|
165
|
+
const allContracts = [];
|
|
166
|
+
let nextKey = null;
|
|
167
|
+
do {
|
|
168
|
+
let url = `${restUrl}/cosmwasm/wasm/v1/code/${this.finCodeId}/contracts?pagination.limit=${PAGE_SIZE}`;
|
|
169
|
+
if (nextKey) {
|
|
170
|
+
url += `&pagination.key=${encodeURIComponent(nextKey)}`;
|
|
171
|
+
}
|
|
172
|
+
const contractsResponse = await thornodeRateLimiter.fetch(url);
|
|
173
|
+
if (!contractsResponse.ok) {
|
|
174
|
+
throw new Error(`Failed to fetch contracts: ${contractsResponse.status}`);
|
|
175
|
+
}
|
|
176
|
+
const page = (await contractsResponse.json());
|
|
177
|
+
allContracts.push(...page.contracts);
|
|
178
|
+
nextKey = page.pagination?.next_key || null;
|
|
179
|
+
this.log(`Fetched ${page.contracts.length} contracts (total: ${allContracts.length})`);
|
|
180
|
+
} while (nextKey);
|
|
181
|
+
const addresses = allContracts;
|
|
182
|
+
this.log(`Found ${addresses.length} FIN contracts total`);
|
|
183
|
+
// Query contract configs in parallel with concurrency cap
|
|
184
|
+
const CONCURRENCY = 5;
|
|
185
|
+
for (let i = 0; i < addresses.length; i += CONCURRENCY) {
|
|
186
|
+
const batch = addresses.slice(i, i + CONCURRENCY);
|
|
187
|
+
const results = await Promise.allSettled(batch.map(async (address) => {
|
|
188
|
+
const configResponse = await thornodeRateLimiter.fetch(`${restUrl}/cosmwasm/wasm/v1/contract/${address}/smart/eyJjb25maWciOnt9fQ==`);
|
|
189
|
+
if (!configResponse.ok)
|
|
190
|
+
return null;
|
|
191
|
+
const configData = (await configResponse.json());
|
|
192
|
+
if (configData.data?.denoms?.length === 2) {
|
|
193
|
+
const base = this.normalizeDenom(configData.data.denoms[0]);
|
|
194
|
+
const quote = this.normalizeDenom(configData.data.denoms[1]);
|
|
195
|
+
return { pairKey: `${base}/${quote}`, address };
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}));
|
|
199
|
+
for (const result of results) {
|
|
200
|
+
if (result.status === 'fulfilled' && result.value) {
|
|
201
|
+
fin[result.value.pairKey] = result.value.address;
|
|
202
|
+
this.log(`Discovered: ${result.value.pairKey} -> ${result.value.address.slice(0, 20)}...`);
|
|
203
|
+
}
|
|
204
|
+
else if (result.status === 'rejected') {
|
|
205
|
+
this.log('Failed to query contract config:', result.reason);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
this.log(`Chain discovery complete: ${Object.keys(fin).length} markets`);
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
this.log('Chain discovery failed:', error);
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
fin,
|
|
216
|
+
discoveredAt: Date.now(),
|
|
217
|
+
source: 'chain',
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
normalizeDenom(denom) {
|
|
221
|
+
return denom.toLowerCase();
|
|
222
|
+
}
|
|
223
|
+
transformMarket(market) {
|
|
224
|
+
const baseDenom = market.denoms.base.toLowerCase();
|
|
225
|
+
const quoteDenom = market.denoms.quote.toLowerCase();
|
|
226
|
+
return {
|
|
227
|
+
address: market.address,
|
|
228
|
+
baseAsset: baseDenom,
|
|
229
|
+
quoteAsset: quoteDenom,
|
|
230
|
+
baseDenom,
|
|
231
|
+
quoteDenom,
|
|
232
|
+
tick: market.config?.tick || '0',
|
|
233
|
+
takerFee: market.config?.fee_taker || DEFAULT_TAKER_FEE,
|
|
234
|
+
makerFee: market.config?.fee_maker || DEFAULT_MAKER_FEE,
|
|
235
|
+
active: true,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
isCacheValid() {
|
|
239
|
+
if (!this.cache)
|
|
240
|
+
return false;
|
|
241
|
+
if (this.cacheTtl === 0)
|
|
242
|
+
return false;
|
|
243
|
+
return Date.now() - this.cache.discoveredAt < this.cacheTtl;
|
|
244
|
+
}
|
|
245
|
+
log(...args) {
|
|
246
|
+
if (this.debug) {
|
|
247
|
+
console.log('[RujiraDiscovery]', ...args);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { GraphQLMarketsResponse } from './types.js';
|
|
2
|
+
export type GraphQLClientOptions = {
|
|
3
|
+
wsEndpoint?: string;
|
|
4
|
+
httpEndpoint?: string;
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
timeout?: number;
|
|
7
|
+
/** Max retries on transient (5xx/network) errors. Default: 3 */
|
|
8
|
+
maxRetries?: number;
|
|
9
|
+
};
|
|
10
|
+
export declare class GraphQLClient {
|
|
11
|
+
private httpEndpoint;
|
|
12
|
+
private wsEndpoint;
|
|
13
|
+
private apiKey?;
|
|
14
|
+
private timeout;
|
|
15
|
+
private maxRetries;
|
|
16
|
+
/** Backoff schedule in ms for each retry attempt */
|
|
17
|
+
private static readonly RETRY_BACKOFF_MS;
|
|
18
|
+
constructor(options?: GraphQLClientOptions);
|
|
19
|
+
static GraphQLError: {
|
|
20
|
+
new (message: string, type: "network" | "server" | "graphql" | "timeout" | "auth" | "unknown", status?: number | undefined, graphqlErrors?: Array<{
|
|
21
|
+
message: string;
|
|
22
|
+
extensions?: Record<string, unknown>;
|
|
23
|
+
}> | undefined): {
|
|
24
|
+
readonly type: "network" | "server" | "graphql" | "timeout" | "auth" | "unknown";
|
|
25
|
+
readonly status?: number | undefined;
|
|
26
|
+
readonly graphqlErrors?: Array<{
|
|
27
|
+
message: string;
|
|
28
|
+
extensions?: Record<string, unknown>;
|
|
29
|
+
}> | undefined;
|
|
30
|
+
name: string;
|
|
31
|
+
message: string;
|
|
32
|
+
stack?: string;
|
|
33
|
+
};
|
|
34
|
+
captureStackTrace(targetObject: object, constructorOpt?: Function): void;
|
|
35
|
+
prepareStackTrace(err: Error, stackTraces: NodeJS.CallSite[]): any;
|
|
36
|
+
stackTraceLimit: number;
|
|
37
|
+
};
|
|
38
|
+
/** Returns true if the error type is eligible for retry (transient failures only). */
|
|
39
|
+
private static isRetryable;
|
|
40
|
+
query<T = unknown>(query: string, variables?: Record<string, unknown>): Promise<T>;
|
|
41
|
+
private executeQuery;
|
|
42
|
+
getMarkets(): Promise<GraphQLMarketsResponse>;
|
|
43
|
+
getMarket(baseAsset: string, quoteAsset: string): Promise<GraphQLMarketsResponse['markets'][0] | null>;
|
|
44
|
+
getWsEndpoint(): string;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=graphql-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graphql-client.d.ts","sourceRoot":"","sources":["../../src/discovery/graphql-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAA;AAExD,MAAM,MAAM,oBAAoB,GAAG;IACjC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,gEAAgE;IAChE,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,YAAY,CAAQ;IAC5B,OAAO,CAAC,UAAU,CAAQ;IAC1B,OAAO,CAAC,MAAM,CAAC,CAAQ;IACvB,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,UAAU,CAAQ;IAE1B,oDAAoD;IACpD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAoB;gBAEhD,OAAO,GAAE,oBAAyB;IAQ9C,MAAM,CAAC,YAAY;sBAEN,MAAM,QACO,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,WAC9D,MAAM,8BACC,KAAK,CAAC;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;SAAE,CAAC;2BAF1E,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS;8BAC9D,MAAM;qCACC,KAAK,CAAC;gBAAE,OAAO,EAAE,MAAM,CAAC;gBAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;aAAE,CAAC;;;;;;;;MAKnG;IAED,sFAAsF;IACtF,OAAO,CAAC,MAAM,CAAC,WAAW;IAIpB,KAAK,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;YA0B1E,YAAY;IA2FpB,UAAU,IAAI,OAAO,CAAC,sBAAsB,CAAC;IA6C7C,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAY5G,aAAa,IAAI,MAAM;CAGxB"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
export class GraphQLClient {
|
|
2
|
+
constructor(options = {}) {
|
|
3
|
+
this.httpEndpoint = options.httpEndpoint || 'https://api.rujira.network/api/graphql';
|
|
4
|
+
this.wsEndpoint = options.wsEndpoint || 'wss://api.rujira.network/socket';
|
|
5
|
+
this.apiKey = options.apiKey;
|
|
6
|
+
this.timeout = options.timeout || 30000;
|
|
7
|
+
this.maxRetries = options.maxRetries ?? 3;
|
|
8
|
+
}
|
|
9
|
+
/** Returns true if the error type is eligible for retry (transient failures only). */
|
|
10
|
+
static isRetryable(error) {
|
|
11
|
+
return error.type === 'server' || (error.type === 'network' && error.status !== 429);
|
|
12
|
+
}
|
|
13
|
+
async query(query, variables) {
|
|
14
|
+
let lastError;
|
|
15
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
16
|
+
if (attempt > 0 && lastError) {
|
|
17
|
+
const backoff = GraphQLClient.RETRY_BACKOFF_MS[attempt - 1] ?? 2000;
|
|
18
|
+
await new Promise(resolve => setTimeout(resolve, backoff));
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
return await this.executeQuery(query, variables);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (!(error instanceof GraphQLClient.GraphQLError))
|
|
25
|
+
throw error;
|
|
26
|
+
lastError = error;
|
|
27
|
+
// Only retry on transient (5xx / network) failures
|
|
28
|
+
if (!GraphQLClient.isRetryable(error) || attempt === this.maxRetries) {
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Unreachable, but satisfies TypeScript
|
|
34
|
+
throw lastError;
|
|
35
|
+
}
|
|
36
|
+
async executeQuery(query, variables) {
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
39
|
+
try {
|
|
40
|
+
const headers = {
|
|
41
|
+
'Content-Type': 'application/json',
|
|
42
|
+
};
|
|
43
|
+
if (this.apiKey) {
|
|
44
|
+
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
|
45
|
+
}
|
|
46
|
+
const response = await fetch(this.httpEndpoint, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers,
|
|
49
|
+
body: JSON.stringify({ query, variables }),
|
|
50
|
+
signal: controller.signal,
|
|
51
|
+
});
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
if (response.status === 401 || response.status === 403) {
|
|
54
|
+
throw new GraphQLClient.GraphQLError(`Authentication failed: ${response.status} ${response.statusText}`, 'auth', response.status);
|
|
55
|
+
}
|
|
56
|
+
if (response.status === 429) {
|
|
57
|
+
throw new GraphQLClient.GraphQLError(`Rate limited by Rujira API: ${response.status} ${response.statusText}. ` +
|
|
58
|
+
'Provide an API token (RujiraClientOptions.apiKey / GraphQLClientOptions.apiKey) to increase limits.', 'network', response.status);
|
|
59
|
+
}
|
|
60
|
+
if (response.status >= 500) {
|
|
61
|
+
throw new GraphQLClient.GraphQLError(`Server error: ${response.status} ${response.statusText}`, 'server', response.status);
|
|
62
|
+
}
|
|
63
|
+
throw new GraphQLClient.GraphQLError(`GraphQL request failed: ${response.status} ${response.statusText}`, 'network', response.status);
|
|
64
|
+
}
|
|
65
|
+
const result = (await response.json());
|
|
66
|
+
if (result.errors && result.errors.length > 0) {
|
|
67
|
+
throw new GraphQLClient.GraphQLError(`GraphQL errors: ${result.errors.map(e => e.message).join(', ')}`, 'graphql', undefined, result.errors);
|
|
68
|
+
}
|
|
69
|
+
return result.data;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error instanceof GraphQLClient.GraphQLError) {
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
76
|
+
throw new GraphQLClient.GraphQLError(`GraphQL request timed out after ${this.timeout}ms`, 'timeout');
|
|
77
|
+
}
|
|
78
|
+
throw new GraphQLClient.GraphQLError(`GraphQL request failed: ${error instanceof Error ? error.message : String(error)}`, 'unknown');
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
clearTimeout(timeoutId);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async getMarkets() {
|
|
85
|
+
const query = `
|
|
86
|
+
query FinMarkets {
|
|
87
|
+
fin {
|
|
88
|
+
address
|
|
89
|
+
assetBase {
|
|
90
|
+
asset
|
|
91
|
+
}
|
|
92
|
+
assetQuote {
|
|
93
|
+
asset
|
|
94
|
+
}
|
|
95
|
+
tick
|
|
96
|
+
feeTaker
|
|
97
|
+
feeMaker
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
`;
|
|
101
|
+
const result = await this.query(query);
|
|
102
|
+
return {
|
|
103
|
+
markets: result.fin.map(m => ({
|
|
104
|
+
address: m.address,
|
|
105
|
+
denoms: {
|
|
106
|
+
base: m.assetBase.asset.toLowerCase(),
|
|
107
|
+
quote: m.assetQuote.asset.toLowerCase(),
|
|
108
|
+
},
|
|
109
|
+
config: {
|
|
110
|
+
tick: m.tick,
|
|
111
|
+
fee_taker: m.feeTaker,
|
|
112
|
+
fee_maker: m.feeMaker,
|
|
113
|
+
},
|
|
114
|
+
})),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
async getMarket(baseAsset, quoteAsset) {
|
|
118
|
+
const allMarkets = await this.getMarkets();
|
|
119
|
+
const market = allMarkets.markets.find(m => (m.denoms.base === baseAsset && m.denoms.quote === quoteAsset) ||
|
|
120
|
+
(m.denoms.base === quoteAsset && m.denoms.quote === baseAsset));
|
|
121
|
+
return market || null;
|
|
122
|
+
}
|
|
123
|
+
getWsEndpoint() {
|
|
124
|
+
return this.wsEndpoint;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** Backoff schedule in ms for each retry attempt */
|
|
128
|
+
GraphQLClient.RETRY_BACKOFF_MS = [500, 1000, 2000];
|
|
129
|
+
GraphQLClient.GraphQLError = class extends Error {
|
|
130
|
+
constructor(message, type, status, graphqlErrors) {
|
|
131
|
+
super(message);
|
|
132
|
+
this.type = type;
|
|
133
|
+
this.status = status;
|
|
134
|
+
this.graphqlErrors = graphqlErrors;
|
|
135
|
+
this.name = 'GraphQLError';
|
|
136
|
+
}
|
|
137
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract discovery module for Rujira SDK
|
|
3
|
+
* Discovers FIN contract addresses via Rujira GraphQL API
|
|
4
|
+
* @module discovery
|
|
5
|
+
*/
|
|
6
|
+
export { type DiscoveryOptions, RujiraDiscovery } from './discovery.js';
|
|
7
|
+
export { GraphQLClient, type GraphQLClientOptions } from './graphql-client.js';
|
|
8
|
+
export type { DiscoveredContracts, Market } from './types.js';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/discovery/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,KAAK,gBAAgB,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AACvE,OAAO,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAC9E,YAAY,EAAE,mBAAmB,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discovery types
|
|
3
|
+
* @module discovery/types
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Market/trading pair from Rujira API
|
|
7
|
+
*/
|
|
8
|
+
export type Market = {
|
|
9
|
+
/** FIN contract address */
|
|
10
|
+
address: string;
|
|
11
|
+
/** Base asset (e.g., "BTC.BTC") */
|
|
12
|
+
baseAsset: string;
|
|
13
|
+
/** Quote asset (e.g., "THOR.RUNE") */
|
|
14
|
+
quoteAsset: string;
|
|
15
|
+
/** Base asset denom */
|
|
16
|
+
baseDenom: string;
|
|
17
|
+
/** Quote asset denom */
|
|
18
|
+
quoteDenom: string;
|
|
19
|
+
/** Tick size */
|
|
20
|
+
tick: string;
|
|
21
|
+
/** Taker fee */
|
|
22
|
+
takerFee: string;
|
|
23
|
+
/** Maker fee */
|
|
24
|
+
makerFee: string;
|
|
25
|
+
/** Whether market is active */
|
|
26
|
+
active: boolean;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Discovered contracts result
|
|
30
|
+
*/
|
|
31
|
+
export type DiscoveredContracts = {
|
|
32
|
+
/** FIN contracts by pair key (e.g., "BTC.BTC/THOR.RUNE") */
|
|
33
|
+
fin: Record<string, string>;
|
|
34
|
+
/** BOW (AMM) contracts if discovered */
|
|
35
|
+
bow?: Record<string, string>;
|
|
36
|
+
/** Other contracts */
|
|
37
|
+
other?: Record<string, string>;
|
|
38
|
+
/** Discovery timestamp */
|
|
39
|
+
discoveredAt: number;
|
|
40
|
+
/** Source of discovery */
|
|
41
|
+
source: 'graphql' | 'chain' | 'cache' | 'fallback-failed';
|
|
42
|
+
/** Last error message if discovery failed */
|
|
43
|
+
lastError?: string;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* GraphQL market response
|
|
47
|
+
*/
|
|
48
|
+
export type GraphQLMarketsResponse = {
|
|
49
|
+
markets: Array<{
|
|
50
|
+
address: string;
|
|
51
|
+
denoms: {
|
|
52
|
+
base: string;
|
|
53
|
+
quote: string;
|
|
54
|
+
};
|
|
55
|
+
config?: {
|
|
56
|
+
tick?: string;
|
|
57
|
+
fee_taker?: string;
|
|
58
|
+
fee_maker?: string;
|
|
59
|
+
};
|
|
60
|
+
}>;
|
|
61
|
+
};
|
|
62
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/discovery/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;GAEG;AACH,MAAM,MAAM,MAAM,GAAG;IACnB,2BAA2B;IAC3B,OAAO,EAAE,MAAM,CAAA;IACf,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAA;IACjB,sCAAsC;IACtC,UAAU,EAAE,MAAM,CAAA;IAClB,uBAAuB;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,wBAAwB;IACxB,UAAU,EAAE,MAAM,CAAA;IAClB,gBAAgB;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,gBAAgB;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,gBAAgB;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,+BAA+B;IAC/B,MAAM,EAAE,OAAO,CAAA;CAChB,CAAA;AAED;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,4DAA4D;IAC5D,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC3B,wCAAwC;IACxC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5B,sBAAsB;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,0BAA0B;IAC1B,YAAY,EAAE,MAAM,CAAA;IACpB,0BAA0B;IAC1B,MAAM,EAAE,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,iBAAiB,CAAA;IACzD,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,KAAK,CAAC;QACb,OAAO,EAAE,MAAM,CAAA;QACf,MAAM,EAAE;YACN,IAAI,EAAE,MAAM,CAAA;YACZ,KAAK,EAAE,MAAM,CAAA;SACd,CAAA;QACD,MAAM,CAAC,EAAE;YACP,IAAI,CAAC,EAAE,MAAM,CAAA;YACb,SAAS,CAAC,EAAE,MAAM,CAAA;YAClB,SAAS,CAAC,EAAE,MAAM,CAAA;SACnB,CAAA;KACF,CAAC,CAAA;CACH,CAAA"}
|