@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,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatting utilities for Rujira SDK
|
|
3
|
+
* @module utils/format
|
|
4
|
+
*/
|
|
5
|
+
import { findAssetByFormat } from '../assets/index.js';
|
|
6
|
+
/**
|
|
7
|
+
* Convert human-readable amount to base units
|
|
8
|
+
*
|
|
9
|
+
* @param amount - Human readable amount (e.g., "1.5")
|
|
10
|
+
* @param decimals - Number of decimals
|
|
11
|
+
* @returns Base units as string
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* toBaseUnits("1.5", 8); // "150000000"
|
|
16
|
+
* toBaseUnits("0.001", 18); // "1000000000000000"
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function toBaseUnits(amount, decimals) {
|
|
20
|
+
const amountStr = amount.toString();
|
|
21
|
+
const [whole, fraction = ''] = amountStr.split('.');
|
|
22
|
+
// Pad or truncate fraction to match decimals
|
|
23
|
+
const paddedFraction = fraction.padEnd(decimals, '0').slice(0, decimals);
|
|
24
|
+
// Combine and remove leading zeros
|
|
25
|
+
const result = `${whole}${paddedFraction}`.replace(/^0+/, '') || '0';
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Convert base units to human-readable amount
|
|
30
|
+
*
|
|
31
|
+
* @param baseUnits - Amount in base units
|
|
32
|
+
* @param decimals - Number of decimals
|
|
33
|
+
* @returns Human readable amount
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* fromBaseUnits("150000000", 8); // "1.5"
|
|
38
|
+
* fromBaseUnits("1000000000000000", 18); // "0.001"
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function fromBaseUnits(baseUnits, decimals) {
|
|
42
|
+
if (decimals === 0)
|
|
43
|
+
return baseUnits.toString();
|
|
44
|
+
const str = baseUnits.toString().padStart(decimals + 1, '0');
|
|
45
|
+
const whole = str.slice(0, -decimals) || '0';
|
|
46
|
+
const fraction = str.slice(-decimals).replace(/0+$/, '');
|
|
47
|
+
return fraction ? `${whole}.${fraction}` : whole;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Format amount for display (truncates fractional digits).
|
|
51
|
+
* For fee displays where underestimation is harmful, use {@link formatFee} instead.
|
|
52
|
+
*
|
|
53
|
+
* @param baseUnits - Amount in base units
|
|
54
|
+
* @param asset - Asset identifier (any format recognized by the asset registry)
|
|
55
|
+
* @param maxDecimals - Maximum decimal places to show
|
|
56
|
+
*/
|
|
57
|
+
export function formatAmount(baseUnits, asset, maxDecimals = 6) {
|
|
58
|
+
const found = findAssetByFormat(asset);
|
|
59
|
+
if (!found) {
|
|
60
|
+
return baseUnits.toString();
|
|
61
|
+
}
|
|
62
|
+
const human = fromBaseUnits(baseUnits, found.decimals.fin);
|
|
63
|
+
const parts = human.split('.');
|
|
64
|
+
const whole = parts[0] || '0';
|
|
65
|
+
const fraction = parts[1] || '';
|
|
66
|
+
// Truncate to maxDecimals
|
|
67
|
+
const truncatedFraction = fraction.slice(0, maxDecimals);
|
|
68
|
+
// Add thousand separators to whole part
|
|
69
|
+
const formattedWhole = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
70
|
+
return truncatedFraction ? `${formattedWhole}.${truncatedFraction}` : formattedWhole;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Format fee amount for display (rounds UP to avoid underestimating costs).
|
|
74
|
+
* Use this for fee/gas displays where showing less than actual is misleading.
|
|
75
|
+
*
|
|
76
|
+
* @param baseUnits - Amount in base units
|
|
77
|
+
* @param asset - Asset identifier (any format recognized by the asset registry)
|
|
78
|
+
* @param maxDecimals - Maximum decimal places to show
|
|
79
|
+
*/
|
|
80
|
+
export function formatFee(baseUnits, asset, maxDecimals = 6) {
|
|
81
|
+
const found = findAssetByFormat(asset);
|
|
82
|
+
if (!found) {
|
|
83
|
+
return baseUnits.toString();
|
|
84
|
+
}
|
|
85
|
+
const human = fromBaseUnits(baseUnits, found.decimals.fin);
|
|
86
|
+
const parts = human.split('.');
|
|
87
|
+
const whole = parts[0] || '0';
|
|
88
|
+
const fraction = parts[1] || '';
|
|
89
|
+
if (fraction.length <= maxDecimals) {
|
|
90
|
+
const formattedWhole = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
91
|
+
const trimmed = fraction.replace(/0+$/, '');
|
|
92
|
+
return trimmed ? `${formattedWhole}.${trimmed}` : formattedWhole;
|
|
93
|
+
}
|
|
94
|
+
// Round up: if any digit beyond maxDecimals is non-zero, increment last visible digit
|
|
95
|
+
const visible = fraction.slice(0, maxDecimals);
|
|
96
|
+
const remainder = fraction.slice(maxDecimals);
|
|
97
|
+
const hasRemainder = /[1-9]/.test(remainder);
|
|
98
|
+
if (!hasRemainder) {
|
|
99
|
+
const trimmed = visible.replace(/0+$/, '');
|
|
100
|
+
const formattedWhole = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
101
|
+
return trimmed ? `${formattedWhole}.${trimmed}` : formattedWhole;
|
|
102
|
+
}
|
|
103
|
+
// Increment the visible fraction by 1 at the last position
|
|
104
|
+
const visibleNum = BigInt(visible) + 1n;
|
|
105
|
+
const roundedFraction = visibleNum.toString().padStart(maxDecimals, '0');
|
|
106
|
+
// Handle carry (e.g., 999 + 1 = 1000)
|
|
107
|
+
if (roundedFraction.length > maxDecimals) {
|
|
108
|
+
const carriedWhole = (BigInt(whole) + 1n).toString();
|
|
109
|
+
const formattedWhole = carriedWhole.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
110
|
+
return formattedWhole;
|
|
111
|
+
}
|
|
112
|
+
const trimmed = roundedFraction.replace(/0+$/, '');
|
|
113
|
+
const formattedWhole = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
114
|
+
return trimmed ? `${formattedWhole}.${trimmed}` : formattedWhole;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Calculate minimum return after slippage
|
|
118
|
+
*
|
|
119
|
+
* @param expectedOutput - Expected output in base units
|
|
120
|
+
* @param slippageBps - Slippage tolerance in basis points
|
|
121
|
+
* @returns Minimum acceptable output
|
|
122
|
+
*/
|
|
123
|
+
export function calculateMinReturn(expectedOutput, slippageBps) {
|
|
124
|
+
const expected = BigInt(expectedOutput);
|
|
125
|
+
const slippageAmount = (expected * BigInt(slippageBps)) / 10000n;
|
|
126
|
+
return (expected - slippageAmount).toString();
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Calculate slippage percentage from expected vs actual
|
|
130
|
+
*
|
|
131
|
+
* @param expected - Expected amount
|
|
132
|
+
* @param actual - Actual amount received
|
|
133
|
+
* @returns Slippage percentage (negative if worse than expected)
|
|
134
|
+
*/
|
|
135
|
+
export function calculateSlippage(expected, actual) {
|
|
136
|
+
const exp = BigInt(expected);
|
|
137
|
+
const act = BigInt(actual);
|
|
138
|
+
if (exp === 0n)
|
|
139
|
+
return '0';
|
|
140
|
+
const diff = act - exp;
|
|
141
|
+
const percentage = (diff * 10000n) / exp;
|
|
142
|
+
return (Number(percentage) / 100).toFixed(2);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Generate a unique quote ID using cryptographic randomness.
|
|
146
|
+
*/
|
|
147
|
+
export function generateQuoteId() {
|
|
148
|
+
const timestamp = Date.now().toString(36);
|
|
149
|
+
let random;
|
|
150
|
+
if (typeof globalThis.crypto?.randomUUID === 'function') {
|
|
151
|
+
random = globalThis.crypto.randomUUID().replace(/-/g, '').slice(0, 12);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
// Fallback for environments without crypto.randomUUID
|
|
155
|
+
random = Math.random().toString(36).slice(2, 10);
|
|
156
|
+
}
|
|
157
|
+
return `quote-${timestamp}-${random}`;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Build a swap message from parameters
|
|
161
|
+
*
|
|
162
|
+
* @param minReturn - Minimum return amount
|
|
163
|
+
* @param to - Destination address (optional)
|
|
164
|
+
*/
|
|
165
|
+
export function buildSwapMsg(minReturn, to) {
|
|
166
|
+
return {
|
|
167
|
+
swap: {
|
|
168
|
+
min: {
|
|
169
|
+
min_return: minReturn,
|
|
170
|
+
to,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Truncate string in the middle (for addresses)
|
|
177
|
+
*
|
|
178
|
+
* @param str - String to truncate
|
|
179
|
+
* @param startChars - Characters to show at start
|
|
180
|
+
* @param endChars - Characters to show at end
|
|
181
|
+
*/
|
|
182
|
+
export function truncateMiddle(str, startChars = 8, endChars = 6) {
|
|
183
|
+
if (str.length <= startChars + endChars) {
|
|
184
|
+
return str;
|
|
185
|
+
}
|
|
186
|
+
return `${str.slice(0, startChars)}...${str.slice(-endChars)}`;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Format percentage for display
|
|
190
|
+
*
|
|
191
|
+
* @param value - Decimal value (e.g., 0.015 for 1.5%)
|
|
192
|
+
* @param decimals - Decimal places to show
|
|
193
|
+
*/
|
|
194
|
+
export function formatPercentage(value, decimals = 2) {
|
|
195
|
+
const num = typeof value === 'string' ? parseFloat(value) : value;
|
|
196
|
+
return `${(num * 100).toFixed(decimals)}%`;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Format basis points as percentage
|
|
200
|
+
*
|
|
201
|
+
* @param bps - Basis points
|
|
202
|
+
*/
|
|
203
|
+
export function bpsToPercent(bps) {
|
|
204
|
+
return `${(bps / 100).toFixed(2)}%`;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Convert percentage to basis points
|
|
208
|
+
*
|
|
209
|
+
* @param percent - Percentage (e.g., 1.5 for 1.5%)
|
|
210
|
+
*/
|
|
211
|
+
export function percentToBps(percent) {
|
|
212
|
+
return Math.round(percent * 100);
|
|
213
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,YAAY,CAAA;AAC1B,cAAc,uBAAuB,CAAA;AACrC,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA;AACzB,cAAc,mBAAmB,CAAA"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memo utilities for THORChain transactions
|
|
3
|
+
* @module utils/memo
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Validate a memo component does not contain delimiter characters.
|
|
7
|
+
* THORChain memos use ':' as field separator - any ':' in user input
|
|
8
|
+
* would corrupt memo parsing and could enable injection attacks.
|
|
9
|
+
*
|
|
10
|
+
* @param value - The memo component to validate
|
|
11
|
+
* @param fieldName - Name of the field (for error messages)
|
|
12
|
+
* @throws Error if the value contains ':'
|
|
13
|
+
*/
|
|
14
|
+
export declare function validateMemoComponent(value: string, fieldName: string): void;
|
|
15
|
+
/**
|
|
16
|
+
* Build a CosmWasm execution memo for Layer 1 deposits
|
|
17
|
+
*
|
|
18
|
+
* Format: x:{contract}:{base64_payload}
|
|
19
|
+
*
|
|
20
|
+
* @param contractAddress - CosmWasm contract address
|
|
21
|
+
* @param msg - Execute message to encode
|
|
22
|
+
* @returns Formatted memo string
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* const memo = buildExecuteMemo('thor1...fin...', {
|
|
27
|
+
* swap: { min: { min_return: '1000000' } }
|
|
28
|
+
* });
|
|
29
|
+
* // Returns: "x:thor1...fin...:eyJzd2FwIjp7Im1pbiI6eyJtaW5fcmV0dXJuIjoiMTAwMDAwMCJ9fX0="
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildExecuteMemo(contractAddress: string, msg: object): string;
|
|
33
|
+
/**
|
|
34
|
+
* Parse a CosmWasm execution memo
|
|
35
|
+
*
|
|
36
|
+
* @param memo - Memo string to parse
|
|
37
|
+
* @returns Parsed memo or null if invalid
|
|
38
|
+
*/
|
|
39
|
+
export declare function parseExecuteMemo(memo: string): {
|
|
40
|
+
contract: string;
|
|
41
|
+
msg: object;
|
|
42
|
+
} | null;
|
|
43
|
+
/**
|
|
44
|
+
* Build a swap memo for Layer 1 deposits
|
|
45
|
+
*
|
|
46
|
+
* @param contractAddress - FIN contract address
|
|
47
|
+
* @param minReturn - Minimum return amount
|
|
48
|
+
* @param destination - Destination address (optional)
|
|
49
|
+
* @returns Formatted memo
|
|
50
|
+
*/
|
|
51
|
+
export declare function buildSwapMemo(contractAddress: string, minReturn: string, destination?: string): string;
|
|
52
|
+
/**
|
|
53
|
+
* Build a standard THORChain swap memo
|
|
54
|
+
*
|
|
55
|
+
* Format: =:ASSET:DESTINATION:LIMIT:AFFILIATE:FEE
|
|
56
|
+
*
|
|
57
|
+
* @param asset - Destination asset (e.g., "BTC.BTC")
|
|
58
|
+
* @param destination - Destination address
|
|
59
|
+
* @param limit - Minimum output (optional)
|
|
60
|
+
* @param affiliate - Affiliate address (optional)
|
|
61
|
+
* @param affiliateFee - Affiliate fee in basis points (optional)
|
|
62
|
+
*/
|
|
63
|
+
export declare function buildThorSwapMemo(asset: string, destination: string, limit?: string, affiliate?: string, affiliateFee?: number): string;
|
|
64
|
+
/**
|
|
65
|
+
* Build a secured asset mint memo
|
|
66
|
+
*
|
|
67
|
+
* Format: secure+:DESTINATION
|
|
68
|
+
*
|
|
69
|
+
* @param destination - THORChain address to receive secured asset
|
|
70
|
+
*/
|
|
71
|
+
export declare function buildSecureMintMemo(destination: string): string;
|
|
72
|
+
/**
|
|
73
|
+
* Build a secured asset redeem memo
|
|
74
|
+
*
|
|
75
|
+
* Format: secure-:DESTINATION
|
|
76
|
+
*
|
|
77
|
+
* @param destination - L1 address to receive native asset
|
|
78
|
+
*/
|
|
79
|
+
export declare function buildSecureRedeemMemo(destination: string): string;
|
|
80
|
+
/**
|
|
81
|
+
* Parse a THORChain memo to determine its type
|
|
82
|
+
*/
|
|
83
|
+
export declare function parseMemoType(memo: string): {
|
|
84
|
+
type: 'swap';
|
|
85
|
+
asset: string;
|
|
86
|
+
destination: string;
|
|
87
|
+
} | {
|
|
88
|
+
type: 'execute';
|
|
89
|
+
contract: string;
|
|
90
|
+
} | {
|
|
91
|
+
type: 'secure-mint';
|
|
92
|
+
destination: string;
|
|
93
|
+
} | {
|
|
94
|
+
type: 'secure-redeem';
|
|
95
|
+
destination: string;
|
|
96
|
+
} | {
|
|
97
|
+
type: 'unknown';
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Validate memo length (THORChain has a limit)
|
|
101
|
+
*/
|
|
102
|
+
export declare function validateMemoLength(memo: string, maxLength?: number): boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Estimate memo length for a swap
|
|
105
|
+
*/
|
|
106
|
+
export declare function estimateSwapMemoLength(contractAddress: string, minReturn: string, destination?: string): number;
|
|
107
|
+
//# sourceMappingURL=memo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memo.d.ts","sourceRoot":"","sources":["../../src/utils/memo.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAI5E;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAAC,eAAe,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAS7E;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAC9C,QAAQ,EAAE,MAAM,CAAA;IAChB,GAAG,EAAE,MAAM,CAAA;CACZ,GAAG,IAAI,CAuBP;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,eAAe,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAUtG;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,KAAK,CAAC,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,MAAM,EAClB,YAAY,CAAC,EAAE,MAAM,GACpB,MAAM,CAsBR;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAG/D;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAGjE;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,GAEV;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GACpD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAC9C;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAiCtB;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,SAAM,GAAG,OAAO,CAEzE;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,eAAe,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAG/G"}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memo utilities for THORChain transactions
|
|
3
|
+
* @module utils/memo
|
|
4
|
+
*/
|
|
5
|
+
import { base64Decode, base64Encode } from './encoding.js';
|
|
6
|
+
/**
|
|
7
|
+
* Validate a memo component does not contain delimiter characters.
|
|
8
|
+
* THORChain memos use ':' as field separator - any ':' in user input
|
|
9
|
+
* would corrupt memo parsing and could enable injection attacks.
|
|
10
|
+
*
|
|
11
|
+
* @param value - The memo component to validate
|
|
12
|
+
* @param fieldName - Name of the field (for error messages)
|
|
13
|
+
* @throws Error if the value contains ':'
|
|
14
|
+
*/
|
|
15
|
+
export function validateMemoComponent(value, fieldName) {
|
|
16
|
+
if (value.includes(':')) {
|
|
17
|
+
throw new Error(`Invalid ${fieldName}: contains ':' which would corrupt memo parsing. Got: ${value}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Build a CosmWasm execution memo for Layer 1 deposits
|
|
22
|
+
*
|
|
23
|
+
* Format: x:{contract}:{base64_payload}
|
|
24
|
+
*
|
|
25
|
+
* @param contractAddress - CosmWasm contract address
|
|
26
|
+
* @param msg - Execute message to encode
|
|
27
|
+
* @returns Formatted memo string
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* const memo = buildExecuteMemo('thor1...fin...', {
|
|
32
|
+
* swap: { min: { min_return: '1000000' } }
|
|
33
|
+
* });
|
|
34
|
+
* // Returns: "x:thor1...fin...:eyJzd2FwIjp7Im1pbiI6eyJtaW5fcmV0dXJuIjoiMTAwMDAwMCJ9fX0="
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function buildExecuteMemo(contractAddress, msg) {
|
|
38
|
+
const msgBase64 = base64Encode(JSON.stringify(msg));
|
|
39
|
+
const memo = `x:${contractAddress}:${msgBase64}`;
|
|
40
|
+
if (!validateMemoLength(memo)) {
|
|
41
|
+
throw new Error(`Memo exceeds THORChain 250-char limit (${memo.length} chars). Simplify the message or use a shorter contract address.`);
|
|
42
|
+
}
|
|
43
|
+
return memo;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Parse a CosmWasm execution memo
|
|
47
|
+
*
|
|
48
|
+
* @param memo - Memo string to parse
|
|
49
|
+
* @returns Parsed memo or null if invalid
|
|
50
|
+
*/
|
|
51
|
+
export function parseExecuteMemo(memo) {
|
|
52
|
+
if (!memo.startsWith('x:')) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const parts = memo.split(':');
|
|
56
|
+
if (parts.length !== 3) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const contract = parts[1];
|
|
60
|
+
const msgBase64 = parts[2];
|
|
61
|
+
if (!contract || !msgBase64) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const msg = JSON.parse(base64Decode(msgBase64));
|
|
66
|
+
return { contract, msg };
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Build a swap memo for Layer 1 deposits
|
|
74
|
+
*
|
|
75
|
+
* @param contractAddress - FIN contract address
|
|
76
|
+
* @param minReturn - Minimum return amount
|
|
77
|
+
* @param destination - Destination address (optional)
|
|
78
|
+
* @returns Formatted memo
|
|
79
|
+
*/
|
|
80
|
+
export function buildSwapMemo(contractAddress, minReturn, destination) {
|
|
81
|
+
const msg = {
|
|
82
|
+
swap: {
|
|
83
|
+
min: {
|
|
84
|
+
min_return: minReturn,
|
|
85
|
+
to: destination,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
return buildExecuteMemo(contractAddress, msg);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Build a standard THORChain swap memo
|
|
93
|
+
*
|
|
94
|
+
* Format: =:ASSET:DESTINATION:LIMIT:AFFILIATE:FEE
|
|
95
|
+
*
|
|
96
|
+
* @param asset - Destination asset (e.g., "BTC.BTC")
|
|
97
|
+
* @param destination - Destination address
|
|
98
|
+
* @param limit - Minimum output (optional)
|
|
99
|
+
* @param affiliate - Affiliate address (optional)
|
|
100
|
+
* @param affiliateFee - Affiliate fee in basis points (optional)
|
|
101
|
+
*/
|
|
102
|
+
export function buildThorSwapMemo(asset, destination, limit, affiliate, affiliateFee) {
|
|
103
|
+
validateMemoComponent(asset, 'asset');
|
|
104
|
+
validateMemoComponent(destination, 'destination');
|
|
105
|
+
if (limit)
|
|
106
|
+
validateMemoComponent(limit, 'limit');
|
|
107
|
+
if (affiliate)
|
|
108
|
+
validateMemoComponent(affiliate, 'affiliate');
|
|
109
|
+
const parts = ['=', asset, destination];
|
|
110
|
+
if (limit) {
|
|
111
|
+
parts.push(limit);
|
|
112
|
+
if (affiliate && affiliateFee) {
|
|
113
|
+
parts.push(affiliate);
|
|
114
|
+
parts.push(affiliateFee.toString());
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const memo = parts.join(':');
|
|
118
|
+
if (!validateMemoLength(memo)) {
|
|
119
|
+
throw new Error(`Memo exceeds THORChain 250-char limit (${memo.length} chars).`);
|
|
120
|
+
}
|
|
121
|
+
return memo;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Build a secured asset mint memo
|
|
125
|
+
*
|
|
126
|
+
* Format: secure+:DESTINATION
|
|
127
|
+
*
|
|
128
|
+
* @param destination - THORChain address to receive secured asset
|
|
129
|
+
*/
|
|
130
|
+
export function buildSecureMintMemo(destination) {
|
|
131
|
+
validateMemoComponent(destination, 'destination');
|
|
132
|
+
return `secure+:${destination}`;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Build a secured asset redeem memo
|
|
136
|
+
*
|
|
137
|
+
* Format: secure-:DESTINATION
|
|
138
|
+
*
|
|
139
|
+
* @param destination - L1 address to receive native asset
|
|
140
|
+
*/
|
|
141
|
+
export function buildSecureRedeemMemo(destination) {
|
|
142
|
+
validateMemoComponent(destination, 'destination');
|
|
143
|
+
return `secure-:${destination}`;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Parse a THORChain memo to determine its type
|
|
147
|
+
*/
|
|
148
|
+
export function parseMemoType(memo) {
|
|
149
|
+
if (memo.startsWith('=:')) {
|
|
150
|
+
const parts = memo.split(':');
|
|
151
|
+
return {
|
|
152
|
+
type: 'swap',
|
|
153
|
+
asset: parts[1] || '',
|
|
154
|
+
destination: parts[2] || '',
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (memo.startsWith('x:')) {
|
|
158
|
+
const parts = memo.split(':');
|
|
159
|
+
return {
|
|
160
|
+
type: 'execute',
|
|
161
|
+
contract: parts[1] || '',
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (memo.startsWith('secure+:')) {
|
|
165
|
+
return {
|
|
166
|
+
type: 'secure-mint',
|
|
167
|
+
destination: memo.slice('secure+:'.length),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (memo.startsWith('secure-:')) {
|
|
171
|
+
return {
|
|
172
|
+
type: 'secure-redeem',
|
|
173
|
+
destination: memo.slice('secure-:'.length),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return { type: 'unknown' };
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Validate memo length (THORChain has a limit)
|
|
180
|
+
*/
|
|
181
|
+
export function validateMemoLength(memo, maxLength = 250) {
|
|
182
|
+
return memo.length <= maxLength;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Estimate memo length for a swap
|
|
186
|
+
*/
|
|
187
|
+
export function estimateSwapMemoLength(contractAddress, minReturn, destination) {
|
|
188
|
+
const memo = buildSwapMemo(contractAddress, minReturn, destination);
|
|
189
|
+
return memo.length;
|
|
190
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token bucket rate limiter for THORNode API calls.
|
|
3
|
+
*
|
|
4
|
+
* THORChain quote endpoints are rate-limited to 1 request/second per IP.
|
|
5
|
+
* This limiter ensures all SDK HTTP calls to THORNode respect that limit.
|
|
6
|
+
*
|
|
7
|
+
* @module utils/rate-limiter
|
|
8
|
+
*/
|
|
9
|
+
export type RateLimiterOptions = {
|
|
10
|
+
/** Maximum requests per interval (default: 1) */
|
|
11
|
+
maxTokens?: number;
|
|
12
|
+
/** Interval in milliseconds to refill one token (default: 1000 = 1 req/sec) */
|
|
13
|
+
refillIntervalMs?: number;
|
|
14
|
+
};
|
|
15
|
+
export declare class RateLimiter {
|
|
16
|
+
private tokens;
|
|
17
|
+
private readonly maxTokens;
|
|
18
|
+
private readonly refillIntervalMs;
|
|
19
|
+
private lastRefill;
|
|
20
|
+
private queue;
|
|
21
|
+
constructor(options?: RateLimiterOptions);
|
|
22
|
+
private refill;
|
|
23
|
+
private processQueue;
|
|
24
|
+
/**
|
|
25
|
+
* Acquire a token. Resolves when a request slot is available.
|
|
26
|
+
* Use before each HTTP call to THORNode.
|
|
27
|
+
*/
|
|
28
|
+
acquire(): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Wrap a fetch call with rate limiting.
|
|
31
|
+
*/
|
|
32
|
+
fetch(url: string, init?: RequestInit): Promise<Response>;
|
|
33
|
+
/** Number of pending requests in queue */
|
|
34
|
+
get pending(): number;
|
|
35
|
+
}
|
|
36
|
+
/** Shared rate limiter for all THORNode API calls (1 req/sec) */
|
|
37
|
+
export declare const thornodeRateLimiter: RateLimiter;
|
|
38
|
+
//# sourceMappingURL=rate-limiter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../../src/utils/rate-limiter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,+EAA+E;IAC/E,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B,CAAA;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAQ;IACzC,OAAO,CAAC,UAAU,CAAQ;IAC1B,OAAO,CAAC,KAAK,CAAwB;gBAEzB,OAAO,GAAE,kBAAuB;IAO5C,OAAO,CAAC,MAAM;IAUd,OAAO,CAAC,YAAY;IAapB;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAe9B;;OAEG;IACG,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC;IAK/D,0CAA0C;IAC1C,IAAI,OAAO,IAAI,MAAM,CAEpB;CACF;AAED,iEAAiE;AACjE,eAAO,MAAM,mBAAmB,aAA4D,CAAA"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token bucket rate limiter for THORNode API calls.
|
|
3
|
+
*
|
|
4
|
+
* THORChain quote endpoints are rate-limited to 1 request/second per IP.
|
|
5
|
+
* This limiter ensures all SDK HTTP calls to THORNode respect that limit.
|
|
6
|
+
*
|
|
7
|
+
* @module utils/rate-limiter
|
|
8
|
+
*/
|
|
9
|
+
export class RateLimiter {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.queue = [];
|
|
12
|
+
this.maxTokens = options.maxTokens ?? 1;
|
|
13
|
+
this.refillIntervalMs = options.refillIntervalMs ?? 1000;
|
|
14
|
+
this.tokens = this.maxTokens;
|
|
15
|
+
this.lastRefill = Date.now();
|
|
16
|
+
}
|
|
17
|
+
refill() {
|
|
18
|
+
const now = Date.now();
|
|
19
|
+
const elapsed = now - this.lastRefill;
|
|
20
|
+
const newTokens = Math.floor(elapsed / this.refillIntervalMs);
|
|
21
|
+
if (newTokens > 0) {
|
|
22
|
+
this.tokens = Math.min(this.maxTokens, this.tokens + newTokens);
|
|
23
|
+
this.lastRefill = now;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
processQueue() {
|
|
27
|
+
this.refill();
|
|
28
|
+
while (this.queue.length > 0 && this.tokens > 0) {
|
|
29
|
+
this.tokens--;
|
|
30
|
+
const resolve = this.queue.shift();
|
|
31
|
+
resolve();
|
|
32
|
+
}
|
|
33
|
+
if (this.queue.length > 0) {
|
|
34
|
+
setTimeout(() => this.processQueue(), this.refillIntervalMs);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Acquire a token. Resolves when a request slot is available.
|
|
39
|
+
* Use before each HTTP call to THORNode.
|
|
40
|
+
*/
|
|
41
|
+
async acquire() {
|
|
42
|
+
this.refill();
|
|
43
|
+
if (this.tokens > 0) {
|
|
44
|
+
this.tokens--;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
return new Promise(resolve => {
|
|
48
|
+
this.queue.push(resolve);
|
|
49
|
+
if (this.queue.length === 1) {
|
|
50
|
+
setTimeout(() => this.processQueue(), this.refillIntervalMs);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Wrap a fetch call with rate limiting.
|
|
56
|
+
*/
|
|
57
|
+
async fetch(url, init) {
|
|
58
|
+
await this.acquire();
|
|
59
|
+
return fetch(url, init);
|
|
60
|
+
}
|
|
61
|
+
/** Number of pending requests in queue */
|
|
62
|
+
get pending() {
|
|
63
|
+
return this.queue.length;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Shared rate limiter for all THORNode API calls (1 req/sec) */
|
|
67
|
+
export const thornodeRateLimiter = new RateLimiter({ maxTokens: 1, refillIntervalMs: 1000 });
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Asset } from '../assets/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Type guard to check if an object is a valid Asset with a secured FIN denom.
|
|
4
|
+
* Secured assets follow the `chain-symbol` pattern (e.g., `btc-btc`, `eth-usdc-0xa0b8...`).
|
|
5
|
+
* Native tokens (`rune`, `tcy`) and module tokens (`x/ruji`) are excluded.
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export declare function isFinAsset(obj: unknown): obj is Asset & {
|
|
9
|
+
formats: {
|
|
10
|
+
fin: string;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Parse a THORChain asset string into chain and symbol components.
|
|
15
|
+
* @param asset - Asset string (e.g., "BTC.BTC", "ETH.USDC-0xA0b8...")
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export declare function parseAsset(asset: string): {
|
|
19
|
+
chain: string;
|
|
20
|
+
symbol: string;
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=type-guards.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"type-guards.d.ts","sourceRoot":"","sources":["../../src/utils/type-guards.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAE/C;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,KAAK,GAAG;IAAE,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CASpF;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAM3E"}
|