@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,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,10 @@
1
+ /**
2
+ * Utility exports
3
+ * @module utils
4
+ */
5
+ export * from './cache.js';
6
+ export * from './denom-conversion.js';
7
+ export * from './format.js';
8
+ export * from './memo.js';
9
+ export * from './rate-limiter.js';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -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,9 @@
1
+ /**
2
+ * Utility exports
3
+ * @module utils
4
+ */
5
+ export * from './cache.js';
6
+ export * from './denom-conversion.js';
7
+ export * from './format.js';
8
+ export * from './memo.js';
9
+ export * from './rate-limiter.js';
@@ -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"}