@getomnichain/omnichain 0.2.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 (296) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +66 -0
  3. package/dist/abstract_gas_pricing.d.ts +31 -0
  4. package/dist/abstract_gas_pricing.d.ts.map +1 -0
  5. package/dist/abstract_gas_pricing.js +22 -0
  6. package/dist/abstract_gas_pricing.js.map +1 -0
  7. package/dist/address.d.ts +10 -0
  8. package/dist/address.d.ts.map +1 -0
  9. package/dist/address.factory.d.ts +26 -0
  10. package/dist/address.factory.d.ts.map +1 -0
  11. package/dist/address.factory.js +75 -0
  12. package/dist/address.factory.js.map +1 -0
  13. package/dist/address.js +13 -0
  14. package/dist/address.js.map +1 -0
  15. package/dist/address_field.decorator.d.ts +17 -0
  16. package/dist/address_field.decorator.d.ts.map +1 -0
  17. package/dist/address_field.decorator.js +34 -0
  18. package/dist/address_field.decorator.js.map +1 -0
  19. package/dist/chain.base.d.ts +79 -0
  20. package/dist/chain.base.d.ts.map +1 -0
  21. package/dist/chain.base.js +113 -0
  22. package/dist/chain.base.js.map +1 -0
  23. package/dist/chain_ids.d.ts +122 -0
  24. package/dist/chain_ids.d.ts.map +1 -0
  25. package/dist/chain_ids.js +193 -0
  26. package/dist/chain_ids.js.map +1 -0
  27. package/dist/errors.d.ts +42 -0
  28. package/dist/errors.d.ts.map +1 -0
  29. package/dist/errors.js +62 -0
  30. package/dist/errors.js.map +1 -0
  31. package/dist/evm/evm_address.d.ts +11 -0
  32. package/dist/evm/evm_address.d.ts.map +1 -0
  33. package/dist/evm/evm_address.js +56 -0
  34. package/dist/evm/evm_address.js.map +1 -0
  35. package/dist/evm/evm_chain.d.ts +123 -0
  36. package/dist/evm/evm_chain.d.ts.map +1 -0
  37. package/dist/evm/evm_chain.js +685 -0
  38. package/dist/evm/evm_chain.js.map +1 -0
  39. package/dist/evm/evm_chains.d.ts +83 -0
  40. package/dist/evm/evm_chains.d.ts.map +1 -0
  41. package/dist/evm/evm_chains.js +493 -0
  42. package/dist/evm/evm_chains.js.map +1 -0
  43. package/dist/evm/evm_gas_estimate.d.ts +45 -0
  44. package/dist/evm/evm_gas_estimate.d.ts.map +1 -0
  45. package/dist/evm/evm_gas_estimate.js +70 -0
  46. package/dist/evm/evm_gas_estimate.js.map +1 -0
  47. package/dist/evm/evm_gas_pricing.d.ts +40 -0
  48. package/dist/evm/evm_gas_pricing.d.ts.map +1 -0
  49. package/dist/evm/evm_gas_pricing.js +56 -0
  50. package/dist/evm/evm_gas_pricing.js.map +1 -0
  51. package/dist/evm/evm_token.d.ts +7 -0
  52. package/dist/evm/evm_token.d.ts.map +1 -0
  53. package/dist/evm/evm_token.js +22 -0
  54. package/dist/evm/evm_token.js.map +1 -0
  55. package/dist/evm/evm_tokens.d.ts +81 -0
  56. package/dist/evm/evm_tokens.d.ts.map +1 -0
  57. package/dist/evm/evm_tokens.js +104 -0
  58. package/dist/evm/evm_tokens.js.map +1 -0
  59. package/dist/evm/evm_transaction_status.d.ts +108 -0
  60. package/dist/evm/evm_transaction_status.d.ts.map +1 -0
  61. package/dist/evm/evm_transaction_status.js +177 -0
  62. package/dist/evm/evm_transaction_status.js.map +1 -0
  63. package/dist/evm/index.d.ts +10 -0
  64. package/dist/evm/index.d.ts.map +1 -0
  65. package/dist/evm/index.js +10 -0
  66. package/dist/evm/index.js.map +1 -0
  67. package/dist/evm/unsigned_evm_transaction.d.ts +19 -0
  68. package/dist/evm/unsigned_evm_transaction.d.ts.map +1 -0
  69. package/dist/evm/unsigned_evm_transaction.js +18 -0
  70. package/dist/evm/unsigned_evm_transaction.js.map +1 -0
  71. package/dist/index.d.ts +18 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +18 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/is_address.decorator.d.ts +3 -0
  76. package/dist/is_address.decorator.d.ts.map +1 -0
  77. package/dist/is_address.decorator.js +51 -0
  78. package/dist/is_address.decorator.js.map +1 -0
  79. package/dist/network_type.d.ts +73 -0
  80. package/dist/network_type.d.ts.map +1 -0
  81. package/dist/network_type.js +144 -0
  82. package/dist/network_type.js.map +1 -0
  83. package/dist/priority.d.ts +23 -0
  84. package/dist/priority.d.ts.map +1 -0
  85. package/dist/priority.js +23 -0
  86. package/dist/priority.js.map +1 -0
  87. package/dist/solana/index.d.ts +9 -0
  88. package/dist/solana/index.d.ts.map +1 -0
  89. package/dist/solana/index.js +9 -0
  90. package/dist/solana/index.js.map +1 -0
  91. package/dist/solana/solana_address.d.ts +20 -0
  92. package/dist/solana/solana_address.d.ts.map +1 -0
  93. package/dist/solana/solana_address.js +45 -0
  94. package/dist/solana/solana_address.js.map +1 -0
  95. package/dist/solana/solana_chain.d.ts +213 -0
  96. package/dist/solana/solana_chain.d.ts.map +1 -0
  97. package/dist/solana/solana_chain.js +704 -0
  98. package/dist/solana/solana_chain.js.map +1 -0
  99. package/dist/solana/solana_chains.d.ts +24 -0
  100. package/dist/solana/solana_chains.d.ts.map +1 -0
  101. package/dist/solana/solana_chains.js +84 -0
  102. package/dist/solana/solana_chains.js.map +1 -0
  103. package/dist/solana/solana_gas_pricing.d.ts +30 -0
  104. package/dist/solana/solana_gas_pricing.d.ts.map +1 -0
  105. package/dist/solana/solana_gas_pricing.js +26 -0
  106. package/dist/solana/solana_gas_pricing.js.map +1 -0
  107. package/dist/solana/solana_token.d.ts +19 -0
  108. package/dist/solana/solana_token.d.ts.map +1 -0
  109. package/dist/solana/solana_token.js +34 -0
  110. package/dist/solana/solana_token.js.map +1 -0
  111. package/dist/solana/solana_tokens.d.ts +22 -0
  112. package/dist/solana/solana_tokens.d.ts.map +1 -0
  113. package/dist/solana/solana_tokens.js +26 -0
  114. package/dist/solana/solana_tokens.js.map +1 -0
  115. package/dist/solana/solana_transaction_status.d.ts +59 -0
  116. package/dist/solana/solana_transaction_status.d.ts.map +1 -0
  117. package/dist/solana/solana_transaction_status.js +144 -0
  118. package/dist/solana/solana_transaction_status.js.map +1 -0
  119. package/dist/solana/unsigned_solana_transaction.d.ts +31 -0
  120. package/dist/solana/unsigned_solana_transaction.d.ts.map +1 -0
  121. package/dist/solana/unsigned_solana_transaction.js +26 -0
  122. package/dist/solana/unsigned_solana_transaction.js.map +1 -0
  123. package/dist/token.d.ts +35 -0
  124. package/dist/token.d.ts.map +1 -0
  125. package/dist/token.js +62 -0
  126. package/dist/token.js.map +1 -0
  127. package/dist/ton/index.d.ts +2 -0
  128. package/dist/ton/index.d.ts.map +1 -0
  129. package/dist/ton/index.js +2 -0
  130. package/dist/ton/index.js.map +1 -0
  131. package/dist/ton/ton_address.d.ts +10 -0
  132. package/dist/ton/ton_address.d.ts.map +1 -0
  133. package/dist/ton/ton_address.js +53 -0
  134. package/dist/ton/ton_address.js.map +1 -0
  135. package/dist/transaction_status.d.ts +140 -0
  136. package/dist/transaction_status.d.ts.map +1 -0
  137. package/dist/transaction_status.js +271 -0
  138. package/dist/transaction_status.js.map +1 -0
  139. package/dist/unsigned_transaction.d.ts +7 -0
  140. package/dist/unsigned_transaction.d.ts.map +1 -0
  141. package/dist/unsigned_transaction.js +9 -0
  142. package/dist/unsigned_transaction.js.map +1 -0
  143. package/dist/utxo/btc/btc_address.d.ts +15 -0
  144. package/dist/utxo/btc/btc_address.d.ts.map +1 -0
  145. package/dist/utxo/btc/btc_address.js +39 -0
  146. package/dist/utxo/btc/btc_address.js.map +1 -0
  147. package/dist/utxo/btc/btc_chain.d.ts +34 -0
  148. package/dist/utxo/btc/btc_chain.d.ts.map +1 -0
  149. package/dist/utxo/btc/btc_chain.js +62 -0
  150. package/dist/utxo/btc/btc_chain.js.map +1 -0
  151. package/dist/utxo/btc/btc_chains.d.ts +25 -0
  152. package/dist/utxo/btc/btc_chains.d.ts.map +1 -0
  153. package/dist/utxo/btc/btc_chains.js +43 -0
  154. package/dist/utxo/btc/btc_chains.js.map +1 -0
  155. package/dist/utxo/btc/btc_token.d.ts +6 -0
  156. package/dist/utxo/btc/btc_token.d.ts.map +1 -0
  157. package/dist/utxo/btc/btc_token.js +11 -0
  158. package/dist/utxo/btc/btc_token.js.map +1 -0
  159. package/dist/utxo/btc/index.d.ts +14 -0
  160. package/dist/utxo/btc/index.d.ts.map +1 -0
  161. package/dist/utxo/btc/index.js +14 -0
  162. package/dist/utxo/btc/index.js.map +1 -0
  163. package/dist/utxo/btc/network_params.d.ts +50 -0
  164. package/dist/utxo/btc/network_params.d.ts.map +1 -0
  165. package/dist/utxo/btc/network_params.js +188 -0
  166. package/dist/utxo/btc/network_params.js.map +1 -0
  167. package/dist/utxo/btc/tools/asset_outpoint.d.ts +7 -0
  168. package/dist/utxo/btc/tools/asset_outpoint.d.ts.map +1 -0
  169. package/dist/utxo/btc/tools/asset_outpoint.js +12 -0
  170. package/dist/utxo/btc/tools/asset_outpoint.js.map +1 -0
  171. package/dist/utxo/btc/tools/bitcoin_core.tool.d.ts +41 -0
  172. package/dist/utxo/btc/tools/bitcoin_core.tool.d.ts.map +1 -0
  173. package/dist/utxo/btc/tools/bitcoin_core.tool.js +172 -0
  174. package/dist/utxo/btc/tools/bitcoin_core.tool.js.map +1 -0
  175. package/dist/utxo/btc/tools/esplora.tool.d.ts +27 -0
  176. package/dist/utxo/btc/tools/esplora.tool.d.ts.map +1 -0
  177. package/dist/utxo/btc/tools/esplora.tool.js +113 -0
  178. package/dist/utxo/btc/tools/esplora.tool.js.map +1 -0
  179. package/dist/utxo/btc/tools/inscription_index.d.ts +6 -0
  180. package/dist/utxo/btc/tools/inscription_index.d.ts.map +1 -0
  181. package/dist/utxo/btc/tools/inscription_index.js +2 -0
  182. package/dist/utxo/btc/tools/inscription_index.js.map +1 -0
  183. package/dist/utxo/btc/tools/ordiscan.tool.d.ts +19 -0
  184. package/dist/utxo/btc/tools/ordiscan.tool.d.ts.map +1 -0
  185. package/dist/utxo/btc/tools/ordiscan.tool.js +53 -0
  186. package/dist/utxo/btc/tools/ordiscan.tool.js.map +1 -0
  187. package/dist/utxo/btc/tools/rare_sat_index.d.ts +6 -0
  188. package/dist/utxo/btc/tools/rare_sat_index.d.ts.map +1 -0
  189. package/dist/utxo/btc/tools/rare_sat_index.js +2 -0
  190. package/dist/utxo/btc/tools/rare_sat_index.js.map +1 -0
  191. package/dist/utxo/btc/tools/rune_index.d.ts +6 -0
  192. package/dist/utxo/btc/tools/rune_index.d.ts.map +1 -0
  193. package/dist/utxo/btc/tools/rune_index.js +2 -0
  194. package/dist/utxo/btc/tools/rune_index.js.map +1 -0
  195. package/dist/utxo/btc/tools/unisat.tool.d.ts +18 -0
  196. package/dist/utxo/btc/tools/unisat.tool.d.ts.map +1 -0
  197. package/dist/utxo/btc/tools/unisat.tool.js +49 -0
  198. package/dist/utxo/btc/tools/unisat.tool.js.map +1 -0
  199. package/dist/utxo/coin_selection.d.ts +29 -0
  200. package/dist/utxo/coin_selection.d.ts.map +1 -0
  201. package/dist/utxo/coin_selection.js +156 -0
  202. package/dist/utxo/coin_selection.js.map +1 -0
  203. package/dist/utxo/doge/doge_chains.d.ts +17 -0
  204. package/dist/utxo/doge/doge_chains.d.ts.map +1 -0
  205. package/dist/utxo/doge/doge_chains.js +28 -0
  206. package/dist/utxo/doge/doge_chains.js.map +1 -0
  207. package/dist/utxo/doge/index.d.ts +3 -0
  208. package/dist/utxo/doge/index.d.ts.map +1 -0
  209. package/dist/utxo/doge/index.js +3 -0
  210. package/dist/utxo/doge/index.js.map +1 -0
  211. package/dist/utxo/doge/network_params.d.ts +4 -0
  212. package/dist/utxo/doge/network_params.d.ts.map +1 -0
  213. package/dist/utxo/doge/network_params.js +24 -0
  214. package/dist/utxo/doge/network_params.js.map +1 -0
  215. package/dist/utxo/ecc.d.ts +2 -0
  216. package/dist/utxo/ecc.d.ts.map +1 -0
  217. package/dist/utxo/ecc.js +11 -0
  218. package/dist/utxo/ecc.js.map +1 -0
  219. package/dist/utxo/fee.d.ts +12 -0
  220. package/dist/utxo/fee.d.ts.map +1 -0
  221. package/dist/utxo/fee.js +51 -0
  222. package/dist/utxo/fee.js.map +1 -0
  223. package/dist/utxo/index.d.ts +20 -0
  224. package/dist/utxo/index.d.ts.map +1 -0
  225. package/dist/utxo/index.js +20 -0
  226. package/dist/utxo/index.js.map +1 -0
  227. package/dist/utxo/ltc/index.d.ts +3 -0
  228. package/dist/utxo/ltc/index.d.ts.map +1 -0
  229. package/dist/utxo/ltc/index.js +3 -0
  230. package/dist/utxo/ltc/index.js.map +1 -0
  231. package/dist/utxo/ltc/ltc_chains.d.ts +17 -0
  232. package/dist/utxo/ltc/ltc_chains.d.ts.map +1 -0
  233. package/dist/utxo/ltc/ltc_chains.js +28 -0
  234. package/dist/utxo/ltc/ltc_chains.js.map +1 -0
  235. package/dist/utxo/ltc/network_params.d.ts +4 -0
  236. package/dist/utxo/ltc/network_params.d.ts.map +1 -0
  237. package/dist/utxo/ltc/network_params.js +27 -0
  238. package/dist/utxo/ltc/network_params.js.map +1 -0
  239. package/dist/utxo/script.d.ts +16 -0
  240. package/dist/utxo/script.d.ts.map +1 -0
  241. package/dist/utxo/script.js +52 -0
  242. package/dist/utxo/script.js.map +1 -0
  243. package/dist/utxo/tools/broadcaster.d.ts +6 -0
  244. package/dist/utxo/tools/broadcaster.d.ts.map +1 -0
  245. package/dist/utxo/tools/broadcaster.js +2 -0
  246. package/dist/utxo/tools/broadcaster.js.map +1 -0
  247. package/dist/utxo/tools/chain_tip_provider.d.ts +5 -0
  248. package/dist/utxo/tools/chain_tip_provider.d.ts.map +1 -0
  249. package/dist/utxo/tools/chain_tip_provider.js +2 -0
  250. package/dist/utxo/tools/chain_tip_provider.js.map +1 -0
  251. package/dist/utxo/tools/fee_estimator.d.ts +6 -0
  252. package/dist/utxo/tools/fee_estimator.d.ts.map +1 -0
  253. package/dist/utxo/tools/fee_estimator.js +2 -0
  254. package/dist/utxo/tools/fee_estimator.js.map +1 -0
  255. package/dist/utxo/tools/raw_transaction_provider.d.ts +8 -0
  256. package/dist/utxo/tools/raw_transaction_provider.d.ts.map +1 -0
  257. package/dist/utxo/tools/raw_transaction_provider.js +2 -0
  258. package/dist/utxo/tools/raw_transaction_provider.js.map +1 -0
  259. package/dist/utxo/tools/utxo_provider.d.ts +7 -0
  260. package/dist/utxo/tools/utxo_provider.d.ts.map +1 -0
  261. package/dist/utxo/tools/utxo_provider.js +2 -0
  262. package/dist/utxo/tools/utxo_provider.js.map +1 -0
  263. package/dist/utxo/unsigned_utxo_transaction.d.ts +32 -0
  264. package/dist/utxo/unsigned_utxo_transaction.d.ts.map +1 -0
  265. package/dist/utxo/unsigned_utxo_transaction.js +32 -0
  266. package/dist/utxo/unsigned_utxo_transaction.js.map +1 -0
  267. package/dist/utxo/utxo.d.ts +43 -0
  268. package/dist/utxo/utxo.d.ts.map +1 -0
  269. package/dist/utxo/utxo.js +2 -0
  270. package/dist/utxo/utxo.js.map +1 -0
  271. package/dist/utxo/utxo_chain.d.ts +102 -0
  272. package/dist/utxo/utxo_chain.d.ts.map +1 -0
  273. package/dist/utxo/utxo_chain.js +524 -0
  274. package/dist/utxo/utxo_chain.js.map +1 -0
  275. package/dist/utxo/utxo_chains.d.ts +29 -0
  276. package/dist/utxo/utxo_chains.d.ts.map +1 -0
  277. package/dist/utxo/utxo_chains.js +63 -0
  278. package/dist/utxo/utxo_chains.js.map +1 -0
  279. package/dist/utxo/utxo_gas_pricing.d.ts +22 -0
  280. package/dist/utxo/utxo_gas_pricing.d.ts.map +1 -0
  281. package/dist/utxo/utxo_gas_pricing.js +20 -0
  282. package/dist/utxo/utxo_gas_pricing.js.map +1 -0
  283. package/dist/utxo/utxo_network_params.d.ts +25 -0
  284. package/dist/utxo/utxo_network_params.d.ts.map +1 -0
  285. package/dist/utxo/utxo_network_params.js +15 -0
  286. package/dist/utxo/utxo_network_params.js.map +1 -0
  287. package/dist/utxo/utxo_transaction_status.d.ts +73 -0
  288. package/dist/utxo/utxo_transaction_status.d.ts.map +1 -0
  289. package/dist/utxo/utxo_transaction_status.js +89 -0
  290. package/dist/utxo/utxo_transaction_status.js.map +1 -0
  291. package/docs/PUBLISHING.md +119 -0
  292. package/docs/README.md +273 -0
  293. package/docs/evm.md +205 -0
  294. package/docs/solana.md +220 -0
  295. package/docs/utxo.md +364 -0
  296. package/package.json +94 -0
@@ -0,0 +1,685 @@
1
+ import { AbiCoder, Contract, Interface, JsonRpcProvider, } from 'ethers';
2
+ import { NetworkType, tryNetworkTypeOf } from "../network_type.js";
3
+ import { Chain, resolveTransferAmount } from "../chain.base.js";
4
+ import { ChainError, ChainErrorKinds, sanitizeCause, sanitizeMessage } from "../errors.js";
5
+ import { Priority } from "../priority.js";
6
+ import { EvmGasEstimate } from "./evm_gas_estimate.js";
7
+ import { AssetBalanceChange, } from "../transaction_status.js";
8
+ import { ERC20_TRANSFER_TOPIC as SHARED_ERC20_TRANSFER_TOPIC, EvmParsedTransactionLog, EvmTransactionGasFees, EvmTransactionStatus, } from "./evm_transaction_status.js";
9
+ import { EvmAddress } from "./evm_address.js";
10
+ import { EvmToken } from "./evm_token.js";
11
+ import { UnsignedEvmTransaction } from "./unsigned_evm_transaction.js";
12
+ // Priority profile mirrors omnichain-py/impl/evm/base.py:440-444
13
+ // (`_FEE_PRIORITY_PROFILE`): the reward-percentile is used for the 1559
14
+ // path (get_1559_fees's reward_percentile arg); the legacy multiplier
15
+ // applies ONLY when supportsEip1559 is false (Python does not fall back
16
+ // to the multiplier on the 1559 path).
17
+ const PRIORITY_REWARD_PERCENTILE = {
18
+ [Priority.SLOW]: 25,
19
+ [Priority.NORMAL]: 50,
20
+ [Priority.FAST]: 75,
21
+ };
22
+ // Legacy gas-price multiplier per priority tier — for the legacy branch only.
23
+ // Expressed as basis points × 100 so integer bigint arithmetic works.
24
+ const GAS_MULTIPLIER_PCT = {
25
+ [Priority.SLOW]: 100n,
26
+ [Priority.NORMAL]: 120n,
27
+ [Priority.FAST]: 150n,
28
+ };
29
+ // Empty-reward fallback tip: 2 gwei — matches Python `Web3.to_wei(2, 'gwei')`
30
+ // at impl/evm/base.py:1124.
31
+ const EMPTY_REWARD_FALLBACK_TIP = 2000000000n;
32
+ // Number of latest blocks sampled by eth_feeHistory. Python default 10
33
+ // (impl/evm/base.py:1099).
34
+ const FEE_HISTORY_BLOCK_COUNT = 10;
35
+ // Final-tip percentile picked across the sampled blocks after sorting.
36
+ // Python default 90.0 (impl/evm/base.py:1102).
37
+ const FINAL_TIP_PERCENTILE = 90;
38
+ // TS-only safety floor applied to any suggested gas price / tip. Python does
39
+ // NOT enforce a minimum: `sorted_tips[idx]` can be 0 on quiet L2 blocks.
40
+ // Silently unmineable transactions are unacceptable for a signing SDK, so
41
+ // TS clamps to 0.05 gwei.
42
+ const MIN_GAS_PRICE_FLOOR = 50000000n;
43
+ function atLeast(n, min) {
44
+ return n < min ? min : n;
45
+ }
46
+ const ERC20_TRANSFER_TOPIC = SHARED_ERC20_TRANSFER_TOPIC;
47
+ const ERC20_ABI = [
48
+ 'function balanceOf(address owner) view returns (uint256)',
49
+ 'function decimals() view returns (uint8)',
50
+ 'function symbol() view returns (string)',
51
+ 'function transfer(address to, uint256 value)',
52
+ ];
53
+ const ERC20_INTERFACE = new Interface(ERC20_ABI);
54
+ export class EvmChain extends Chain {
55
+ rpcUrl;
56
+ supportsEip1559;
57
+ hasL1Fee;
58
+ /**
59
+ * Default gas limit for native transfers. Matches Python's public attribute
60
+ * at `impl/evm/base.py:495` — v0 does not consume it inside the TS SDK
61
+ * (see `createTransferUnsignedTransaction`) but exposes it so consumers
62
+ * building their own transfer paths get identical numbers to Python. Wired
63
+ * into the SDK-owned transfer builder in a follow-up branch.
64
+ */
65
+ nativeTransferGasLimit;
66
+ /**
67
+ * Legacy `eth_gasPrice` multiplier for native transfers on non-1559 chains.
68
+ * Matches Python's public attribute at `impl/evm/base.py:496`. Same
69
+ * "declarative for consumers, unused internally in v0" note as
70
+ * `nativeTransferGasLimit`.
71
+ */
72
+ nativeTransferGasMultiplier;
73
+ _nativeToken;
74
+ _provider = null;
75
+ _resolvedRpcUrl = null;
76
+ constructor(init) {
77
+ // Conflict guard — refuse to construct an EvmChain over a chainId that
78
+ // the static family seeds have already registered as non-EVM. Prevents
79
+ // the case where `new EvmChain({ chainId: 728126428, … })` would silently
80
+ // succeed and then contradict `addressFor(728126428, …)` /
81
+ // `networkTypeOf(728126428)` (both return TRON). Mirrors
82
+ // `registerNonEvmChain`'s throw-on-conflict guard.
83
+ const existing = tryNetworkTypeOf(init.chainId);
84
+ if (existing !== undefined && existing !== NetworkType.EVM) {
85
+ throw new ChainError(ChainErrorKinds.InvalidArgument, `chainId ${init.chainId} is registered as ${existing}; refusing to construct EvmChain for it`, { chainId: init.chainId });
86
+ }
87
+ super(init.chainId, init.name, NetworkType.EVM, init.blockTimeSeconds, init.nativeSymbol, init.explorerBaseUrl);
88
+ this.rpcUrl = init.rpcUrl;
89
+ this.supportsEip1559 = init.supportsEip1559 ?? true;
90
+ this.hasL1Fee = init.hasL1Fee ?? false;
91
+ this.nativeTransferGasLimit = init.nativeTransferGasLimit ?? 21000;
92
+ this.nativeTransferGasMultiplier = init.nativeTransferGasMultiplier ?? 1.4;
93
+ this._nativeToken = EvmToken.native(init.chainId, init.nativeSymbol, init.nativeDecimals ?? 18);
94
+ // NOTE: no `registerNonEvmChain(id, EVM)` call — a module-scope write on
95
+ // 48 pre-baked chains would let a consumer's earlier
96
+ // `registerNonEvmChain(id, COSMOS)` turn `import '@getomnichain/omnichain'`
97
+ // into a permanently-poisoned module. Conflict detection lives in a
98
+ // single place: `registerNonEvmChain` reads `tryNetworkTypeOf` (which
99
+ // synthesizes EVM for positive-unregistered ids), so claiming a positive
100
+ // id for a non-EVM family requires `unregisterChain(id)` first.
101
+ }
102
+ get nativeToken() {
103
+ return this._nativeToken;
104
+ }
105
+ getErc20Token(symbol, contractAddress, decimals) {
106
+ // `EvmToken.erc20` → `new EvmToken` normalizes the identifier internally;
107
+ // no need to checksum here as well.
108
+ return EvmToken.erc20(this.chainId, symbol, contractAddress, decimals);
109
+ }
110
+ getProvider() {
111
+ if (this._provider)
112
+ return this._provider;
113
+ const rpcUrl = this.readRpcUrl();
114
+ this._resolvedRpcUrl = rpcUrl;
115
+ this._provider = new JsonRpcProvider(rpcUrl);
116
+ return this._provider;
117
+ }
118
+ /**
119
+ * Suggest gas fees for a priority tier. Mirrors Python's `_resolve_gas_pricing`
120
+ * dispatch (impl/evm/base.py:807-833):
121
+ *
122
+ * - legacy chains (`supportsEip1559 === false`): scale `eth_gasPrice` by
123
+ * the priority tier's legacy multiplier.
124
+ * - 1559 chains: sample eth_feeHistory across the last 10 blocks at the
125
+ * tier's reward percentile, sort, pick the p90 across blocks. On empty
126
+ * rewards, use 2 gwei. On RPC failure, bubble as `ChainError(RpcError)`
127
+ * (Python bubbles too — no defensive fallback).
128
+ */
129
+ async suggestGas(priority) {
130
+ const provider = this.getProvider();
131
+ if (!this.supportsEip1559) {
132
+ const mult = GAS_MULTIPLIER_PCT[priority];
133
+ // Direct `eth_gasPrice` — one round-trip, matches Python
134
+ // impl/evm/base.py:831-833. Avoids getFeeData's implicit
135
+ // `getBlock("latest")` + potential `eth_maxPriorityFeePerGas` extra
136
+ // calls, and dodges the trap where getFeeData's maxFeePerGas is
137
+ // `2×baseFee + tip` (not a legacy price) — using that as the legacy
138
+ // gasPrice would 3-4× overpay.
139
+ let gasPriceHex;
140
+ try {
141
+ gasPriceHex = (await provider.send('eth_gasPrice', []));
142
+ }
143
+ catch (err) {
144
+ throw this.rpcError('legacy eth_gasPrice transport failed', err);
145
+ }
146
+ let providerHint;
147
+ try {
148
+ providerHint = BigInt(gasPriceHex);
149
+ }
150
+ catch {
151
+ // Genuinely unparseable response — bubble as RpcError so on-call
152
+ // triage sees the RPC returned nonsense, not "0 gas price".
153
+ throw this.rpcError(`legacy eth_gasPrice returned unparseable response`, new Error(`could not parse ${String(gasPriceHex)} as bigint`));
154
+ }
155
+ // Zero-or-negative gasPrice: clamp up to MIN_GAS_PRICE_FLOOR, mirroring
156
+ // the 1559 branch's floor-on-sub-floor-tips behavior (evm_chain.ts:307).
157
+ // Python (`impl/evm/base.py:831-833`) multiplies whatever `eth_gasPrice`
158
+ // returns and never throws; the TS floor is the safety measure that
159
+ // prevents an unmineable suggestion. Consistent policy across branches.
160
+ const base = providerHint > 0n ? providerHint : MIN_GAS_PRICE_FLOOR;
161
+ const scaled = atLeast((base * mult) / 100n, MIN_GAS_PRICE_FLOOR);
162
+ // Legacy chains: only `gasPrice` is authoritative — matches Python
163
+ // (`impl/evm/base.py:830-833` returns `EvmGasPricing(gas_price=...)`).
164
+ // Populating maxFeePerGas/maxPriorityFeePerGas as duplicates would let
165
+ // a consumer build a type-2 tx paying the full gasPrice as a tip on
166
+ // top of base fee.
167
+ return new EvmGasEstimate({ kind: 'legacy', gasPrice: scaled });
168
+ }
169
+ // 1559 path — mirrors omnichain-py get_1559_fees (impl/evm/base.py:1098-1132).
170
+ // NB: Python reads `base_fee_per_gas` from `eth_getBlock("latest")` (line
171
+ // 1109-1110); TS reuses `feeHistory.baseFeePerGas[-1]` — the JSON-RPC
172
+ // "next block projection", up to ±12.5% off. Deliberate: saves an RPC
173
+ // round-trip. (See the local SINAN questions doc for the upstream
174
+ // discussion — TS uses feeHistory's projection to skip the extra RPC.)
175
+ const rewardPercentile = PRIORITY_REWARD_PERCENTILE[priority];
176
+ let raw;
177
+ // Narrow try — only the RPC call. Response-shape and parse errors are
178
+ // distinct from transport failures and get a distinct ChainError kind.
179
+ try {
180
+ raw = (await provider.send('eth_feeHistory', [
181
+ `0x${FEE_HISTORY_BLOCK_COUNT.toString(16)}`,
182
+ 'latest',
183
+ [rewardPercentile],
184
+ ]));
185
+ }
186
+ catch (err) {
187
+ throw this.rpcError('eth_feeHistory transport failed', err);
188
+ }
189
+ // Narrow parse — only the shape checks + BigInt() coercions can throw
190
+ // for structural reasons here. `EvmGasEstimate` construction and the
191
+ // pure-arithmetic tip selection happen outside so that a defect there
192
+ // isn't mislabelled as an RPC-response malformed error.
193
+ let latestBaseFee;
194
+ let tips;
195
+ try {
196
+ const baseFees = raw.baseFeePerGas;
197
+ if (!Array.isArray(baseFees) || baseFees.length === 0) {
198
+ throw new Error('missing baseFeePerGas');
199
+ }
200
+ latestBaseFee = BigInt(baseFees[baseFees.length - 1] ?? '0x0');
201
+ const rewardRows = Array.isArray(raw.reward) ? raw.reward : [];
202
+ tips = rewardRows
203
+ .map((row) => (Array.isArray(row) ? row[0] : undefined))
204
+ .filter((v) => typeof v === 'string')
205
+ .map((v) => BigInt(v));
206
+ }
207
+ catch (err) {
208
+ // Distinct from the RpcError raised by the provider.send catch above:
209
+ // parse/shape failures indict the response, not the transport. Routed
210
+ // through rpcError() so the URL sanitizer scrubs the response body.
211
+ throw this.rpcError(`eth_feeHistory response malformed`, err);
212
+ }
213
+ let selectedTip;
214
+ if (tips.length === 0) {
215
+ selectedTip = EMPTY_REWARD_FALLBACK_TIP;
216
+ }
217
+ else {
218
+ const sortedTips = [...tips].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
219
+ const idx = Math.min(Math.floor((sortedTips.length * FINAL_TIP_PERCENTILE) / 100), sortedTips.length - 1);
220
+ selectedTip = sortedTips[idx] ?? 0n;
221
+ }
222
+ // TS safety floor — Python doesn't clamp (see MIN_GAS_PRICE_FLOOR).
223
+ const finalTip = atLeast(selectedTip, MIN_GAS_PRICE_FLOOR);
224
+ // Safety buffer: base fee can climb 12.5%/block; caller may wait multiple blocks.
225
+ const maxFeePerGas = latestBaseFee * 2n + finalTip;
226
+ return new EvmGasEstimate({
227
+ kind: 'eip1559',
228
+ maxPriorityFeePerGas: finalTip,
229
+ maxFeePerGas,
230
+ });
231
+ }
232
+ readRpcUrl() {
233
+ if (this.rpcUrl && this.rpcUrl.trim().length > 0)
234
+ return this.rpcUrl.trim();
235
+ const env = globalThis.process?.env;
236
+ const candidates = envCandidatesFor(this.name, this.chainId);
237
+ for (const key of candidates) {
238
+ const value = env?.[key];
239
+ if (value && value.trim().length > 0)
240
+ return value.trim();
241
+ }
242
+ throw new ChainError(ChainErrorKinds.RpcNotConfigured, `${this.name} RPC URL is not configured (pass rpcUrl at construction, or set one of: ${candidates.join(', ')})`, { chainId: this.chainId, envCandidates: candidates });
243
+ }
244
+ rpcHost() {
245
+ const url = this._resolvedRpcUrl ?? this.rpcUrl;
246
+ if (!url)
247
+ return undefined;
248
+ try {
249
+ const u = new URL(url);
250
+ return `${u.protocol}//${u.host}`;
251
+ }
252
+ catch {
253
+ return undefined;
254
+ }
255
+ }
256
+ getWalletExplorerUrl(address) {
257
+ return `${this.explorerBaseUrl}/address/${address}`;
258
+ }
259
+ getTokenExplorerUrl(tokenIdentifier) {
260
+ if (tokenIdentifier === undefined) {
261
+ return this.explorerBaseUrl;
262
+ }
263
+ return `${this.explorerBaseUrl}/token/${tokenIdentifier}`;
264
+ }
265
+ getTransactionExplorerUrl(txHash) {
266
+ const prefixed = txHash.startsWith('0x') ? txHash : `0x${txHash}`;
267
+ return `${this.explorerBaseUrl}/tx/${prefixed}`;
268
+ }
269
+ validateAddress(raw) {
270
+ try {
271
+ new EvmAddress(raw);
272
+ return true;
273
+ }
274
+ catch {
275
+ return false;
276
+ }
277
+ }
278
+ validateTokenIdentifier(raw) {
279
+ if (raw === undefined)
280
+ return true;
281
+ return this.validateAddress(raw);
282
+ }
283
+ async getBalance(owner, tokenIdentifier) {
284
+ if (!this.validateAddress(owner)) {
285
+ throw new ChainError(ChainErrorKinds.InvalidAddress, `Invalid EVM address: ${owner}`, {
286
+ chainId: this.chainId,
287
+ address: owner,
288
+ });
289
+ }
290
+ if (!this.validateTokenIdentifier(tokenIdentifier)) {
291
+ throw new ChainError(ChainErrorKinds.InvalidTokenIdentifier, `Invalid EVM token identifier: ${tokenIdentifier}`, { chainId: this.chainId, identifier: tokenIdentifier });
292
+ }
293
+ const provider = this.getProvider();
294
+ try {
295
+ if (tokenIdentifier === undefined) {
296
+ return await provider.getBalance(owner);
297
+ }
298
+ const contract = new Contract(tokenIdentifier, ERC20_ABI, provider);
299
+ return (await contract.balanceOf(owner));
300
+ }
301
+ catch (err) {
302
+ throw this.rpcError(`Failed to read balance for ${owner}`, err);
303
+ }
304
+ }
305
+ async createTransferUnsignedTransaction(req) {
306
+ if (!this.validateAddress(req.to)) {
307
+ throw new ChainError(ChainErrorKinds.InvalidAddress, `Invalid recipient address: ${req.to}`, {
308
+ chainId: this.chainId,
309
+ address: req.to,
310
+ });
311
+ }
312
+ if (req.from !== undefined && !this.validateAddress(req.from)) {
313
+ throw new ChainError(ChainErrorKinds.InvalidAddress, `Invalid sender address: ${req.from}`, {
314
+ chainId: this.chainId,
315
+ address: req.from,
316
+ });
317
+ }
318
+ if (!this.validateTokenIdentifier(req.tokenIdentifier)) {
319
+ throw new ChainError(ChainErrorKinds.InvalidTokenIdentifier, `Invalid token identifier: ${req.tokenIdentifier}`, { chainId: this.chainId, identifier: req.tokenIdentifier });
320
+ }
321
+ // gasPricing is on the CreateTransferRequest interface (Python
322
+ // parity) but per-chain wiring is deferred to a follow-up card.
323
+ // Rejecting it here rather than silently ignoring — a money knob
324
+ // that fails open is worse than not shipping it.
325
+ if (req.gasPricing !== undefined) {
326
+ throw new ChainError(ChainErrorKinds.InvalidArgument, 'CreateTransferRequest.gasPricing is not yet consumed by EvmChain (Phase 2 follow-up). Use ethers tx builder options directly for now.', { chainId: this.chainId });
327
+ }
328
+ // isFullBalance is on the CreateTransferRequest interface (Python
329
+ // parity) but needs a fee-reserve computation before it can be
330
+ // safely wired — sending `value = balance` on native leaves 0 for
331
+ // gas → guaranteed insufficient-funds; on SPL it leaves 0 for rent.
332
+ // Deferred to a follow-up card that lands the estimated-fee subtract.
333
+ if (req.isFullBalance === true) {
334
+ throw new ChainError(ChainErrorKinds.InvalidArgument, 'CreateTransferRequest.isFullBalance is not yet supported on EvmChain — pass `amount` or `amountHr` explicitly and reserve fees yourself.', { chainId: this.chainId });
335
+ }
336
+ // Only fetch decimals when amountHr is the input form — avoids two
337
+ // extra eth_calls on every bigint-amount transfer. Native path uses
338
+ // the SDK-declared decimals; ERC-20 path uses a STRICT resolver
339
+ // that throws on RPC failure (the defensive fallback to decimals=0
340
+ // would silently produce a wildly wrong minor-units amount).
341
+ let tokenDecimals = 0;
342
+ if (req.amountHr !== undefined) {
343
+ tokenDecimals =
344
+ req.tokenIdentifier === undefined
345
+ ? this._nativeToken.decimals
346
+ : await this.resolveErc20DecimalsStrict(req.tokenIdentifier);
347
+ }
348
+ const resolved = resolveTransferAmount(req, tokenDecimals);
349
+ const amountMr = resolved.kind === 'exact' ? resolved.amountMr : 0n;
350
+ if (req.tokenIdentifier === undefined) {
351
+ return new UnsignedEvmTransaction({
352
+ chainId: this.chainId,
353
+ from: req.from,
354
+ to: req.to,
355
+ value: amountMr,
356
+ data: '0x',
357
+ });
358
+ }
359
+ const data = ERC20_INTERFACE.encodeFunctionData('transfer', [req.to, amountMr]);
360
+ return new UnsignedEvmTransaction({
361
+ chainId: this.chainId,
362
+ from: req.from,
363
+ to: req.tokenIdentifier,
364
+ value: 0n,
365
+ data,
366
+ });
367
+ }
368
+ async getChainTipHeight() {
369
+ try {
370
+ return await this.getProvider().getBlockNumber();
371
+ }
372
+ catch (err) {
373
+ throw this.rpcError('Failed to read chain tip', err);
374
+ }
375
+ }
376
+ async getTransactionStatus(txHash) {
377
+ const provider = this.getProvider();
378
+ let tx = null;
379
+ let receipt = null;
380
+ try {
381
+ [tx, receipt] = await Promise.all([
382
+ provider.getTransaction(txHash),
383
+ provider.getTransactionReceipt(txHash),
384
+ ]);
385
+ }
386
+ catch (err) {
387
+ throw this.rpcError(`Failed to read tx ${txHash}`, err, { txHash });
388
+ }
389
+ if (tx === null && receipt === null)
390
+ return EvmTransactionStatus.notFound(this.chainId, null);
391
+ if (receipt === null)
392
+ return EvmTransactionStatus.pending(this.chainId);
393
+ // Receipt exists but the tx body doesn't — real race on load-balanced/
394
+ // pruned RPC endpoints where getTransactionReceipt succeeds while
395
+ // getTransactionByHash returns null. `tx.value` and `tx.to` are load-
396
+ // bearing for the native transfer legs of decodeBalanceChanges; falling
397
+ // back to `?? 0n` / `?? null` would emit a Success with a silently
398
+ // truncated balanceChanges map (recipient row missing entirely,
399
+ // sender debit off by the transfer value). Python parity: omnichain-py's
400
+ // impl/evm/base.py accesses `tx.value` directly and blows up on a
401
+ // missing body. TS returns Pending — receipt-only is transient node
402
+ // state; the consumer's next poll will land on a node with the full tx.
403
+ if (tx === null)
404
+ return EvmTransactionStatus.pending(this.chainId);
405
+ const succeeded = receipt.status === 1;
406
+ // Parallelize the two independent post-receipt lookups: getBlock (for
407
+ // inclusionAt) and the OP-stack raw-receipt fetch (for l1Fee). Both
408
+ // depend only on receipt.blockNumber / txHash, not on each other.
409
+ const [inclusionAt, l1Fee] = await Promise.all([
410
+ (async () => {
411
+ try {
412
+ const block = await provider.getBlock(receipt.blockNumber);
413
+ if (block?.timestamp !== undefined) {
414
+ return new Date(Number(block.timestamp) * 1000);
415
+ }
416
+ return null;
417
+ }
418
+ catch {
419
+ // leave null — a 1970-01-01 sentinel would silently pass
420
+ // through consumer SLA/age math as a real inclusion time.
421
+ return null;
422
+ }
423
+ })(),
424
+ (async () => {
425
+ // OP-stack rollups charge an L1 data fee (`l1Fee` on the raw
426
+ // receipt). ethers v6 strips it in its parsed TransactionReceipt
427
+ // shape, so a second raw fetch is required. Gated on `hasL1Fee`.
428
+ // Return undefined for "provider did not surface it" (either
429
+ // hasL1Fee=false or the raw send failed/omitted the field) —
430
+ // tracked separately from "l1Fee is genuinely 0n" so
431
+ // fees.l1FeeWei reflects presence, not value.
432
+ if (!this.hasL1Fee)
433
+ return undefined;
434
+ try {
435
+ const raw = (await provider.send('eth_getTransactionReceipt', [txHash]));
436
+ const rawL1 = raw?.l1Fee;
437
+ if (rawL1 === undefined || rawL1 === null)
438
+ return undefined;
439
+ if (typeof rawL1 === 'bigint')
440
+ return rawL1;
441
+ if (typeof rawL1 === 'number')
442
+ return BigInt(rawL1);
443
+ if (typeof rawL1 === 'string' && rawL1.length > 0)
444
+ return BigInt(rawL1);
445
+ return undefined;
446
+ }
447
+ catch {
448
+ return undefined;
449
+ }
450
+ })(),
451
+ ]);
452
+ // gasLimit: the sender-set limit from tx.gasLimit. `null` when the tx
453
+ // body isn't available (pruned/receipt-only) — falling back to
454
+ // receipt.gasUsed here would fabricate a "100% utilization" that
455
+ // consumers can't distinguish from a real observation.
456
+ const fees = new EvmTransactionGasFees({
457
+ gasLimit: tx?.gasLimit ?? null,
458
+ gasLimitUsed: receipt.gasUsed,
459
+ effectiveGasPrice: receipt.gasPrice,
460
+ gasPrice: tx?.gasPrice ?? undefined,
461
+ maxFeePerGas: tx?.maxFeePerGas ?? undefined,
462
+ maxPriorityFeePerGas: tx?.maxPriorityFeePerGas ?? undefined,
463
+ l1FeeWei: l1Fee,
464
+ });
465
+ if (!succeeded) {
466
+ const errorInfo = await extractRevertInfo(provider, tx, receipt);
467
+ return EvmTransactionStatus.failed({
468
+ chainId: this.chainId,
469
+ inclusionAt,
470
+ error: errorInfo,
471
+ fees,
472
+ });
473
+ }
474
+ // Only allocated on the Success path — failed txs carry logs: null.
475
+ const logs = (receipt.logs ?? []).map((l) => new EvmParsedTransactionLog(l.address, [...l.topics], l.data));
476
+ const nativeValue = tx?.value ?? 0n;
477
+ let balanceChanges;
478
+ try {
479
+ balanceChanges = await this.decodeBalanceChanges({
480
+ from: tx ? tx.from : (receipt.from ?? ''),
481
+ to: tx?.to ?? receipt.to ?? null,
482
+ value: nativeValue,
483
+ gasCost: fees.totalNativeDebitWei,
484
+ receipt,
485
+ });
486
+ }
487
+ catch (err) {
488
+ if (err instanceof ChainError)
489
+ throw err;
490
+ throw new ChainError(ChainErrorKinds.TransactionDecodeFailed, `Failed to decode receipt logs for tx ${txHash}`, { chainId: this.chainId, txHash, rpcHost: this.rpcHost() }, err);
491
+ }
492
+ return EvmTransactionStatus.successful({
493
+ chainId: this.chainId,
494
+ inclusionAt,
495
+ balanceChanges,
496
+ logs,
497
+ fees,
498
+ });
499
+ }
500
+ async decodeBalanceChanges(args) {
501
+ const { from, to, value, gasCost, receipt } = args;
502
+ if (!receipt || !Array.isArray(receipt.logs)) {
503
+ throw new ChainError(ChainErrorKinds.TransactionDecodeFailed, 'Receipt is missing required fields', { chainId: this.chainId, txHash: receipt?.hash, rpcHost: this.rpcHost() });
504
+ }
505
+ const rawChanges = new Map();
506
+ const fromAddr = (from ?? '').toLowerCase();
507
+ const toAddr = to ? to.toLowerCase() : null;
508
+ // Sender's native debit = value + gasCost (matches Python
509
+ // impl/evm/base.py:_get_balance_changes fee-inclusive semantics).
510
+ // Credit toAddr whenever value>0 — for self-transfers (from===to) the
511
+ // per-(wallet,token) netting in upsert cancels the +value against the
512
+ // -value component of the debit, leaving the -gasCost we actually want.
513
+ if (fromAddr) {
514
+ addRaw(rawChanges, fromAddr, '', -(value + gasCost));
515
+ }
516
+ if (value > 0n && toAddr) {
517
+ addRaw(rawChanges, toAddr, '', value);
518
+ }
519
+ const tokenContracts = new Set();
520
+ const transferLogs = [];
521
+ for (const log of receipt.logs) {
522
+ if (log.topics.length !== 3)
523
+ continue;
524
+ if (log.topics[0].toLowerCase() !== ERC20_TRANSFER_TOPIC)
525
+ continue;
526
+ const contractAddr = log.address.toLowerCase();
527
+ const logFrom = `0x${log.topics[1].slice(26)}`.toLowerCase();
528
+ const logTo = `0x${log.topics[2].slice(26)}`.toLowerCase();
529
+ let amount;
530
+ try {
531
+ amount = BigInt(log.data);
532
+ }
533
+ catch {
534
+ continue;
535
+ }
536
+ tokenContracts.add(contractAddr);
537
+ transferLogs.push({ contract: contractAddr, from: logFrom, to: logTo, amount });
538
+ }
539
+ const tokensByContract = new Map();
540
+ const resolved = await Promise.all([...tokenContracts].map((addr) => this.resolveErc20TokenDefensive(addr).then((t) => ({ addr, token: t }))));
541
+ for (const { addr, token } of resolved)
542
+ tokensByContract.set(addr, token);
543
+ for (const t of transferLogs) {
544
+ // Symmetric with the native path: always add both legs. Self-transfer
545
+ // nets to zero at the per-(wallet, contract) map level and is dropped
546
+ // by the `delta === 0n` filter below — the explicit gate here would
547
+ // over-suppress the credit if a future decoder gained non-cancelling
548
+ // side effects.
549
+ addRaw(rawChanges, t.from, t.contract, -t.amount);
550
+ addRaw(rawChanges, t.to, t.contract, t.amount);
551
+ }
552
+ const result = new Map();
553
+ for (const [wallet, perTokenAddr] of rawChanges) {
554
+ for (const [tokenAddr, delta] of perTokenAddr) {
555
+ if (delta === 0n)
556
+ continue;
557
+ const token = tokenAddr === '' ? this._nativeToken : tokensByContract.get(tokenAddr);
558
+ if (!token)
559
+ continue;
560
+ AssetBalanceChange.upsert(result, wallet, token, AssetBalanceChange.fromMr(delta, token.decimals));
561
+ }
562
+ }
563
+ return result;
564
+ }
565
+ async resolveErc20TokenDefensive(contractAddress) {
566
+ const checksum = (() => {
567
+ try {
568
+ return new EvmAddress(contractAddress).toChecksum();
569
+ }
570
+ catch {
571
+ return contractAddress;
572
+ }
573
+ })();
574
+ const placeholder = () => new EvmToken(this.chainId, `UNKNOWN_${checksum.slice(2, 8)}`, checksum, 0);
575
+ let provider;
576
+ try {
577
+ provider = this.getProvider();
578
+ }
579
+ catch {
580
+ return placeholder();
581
+ }
582
+ const contract = new Contract(contractAddress, ERC20_ABI, provider);
583
+ try {
584
+ const [symbol, decimals] = await Promise.all([
585
+ contract.symbol(),
586
+ contract.decimals(),
587
+ ]);
588
+ return new EvmToken(this.chainId, symbol, checksum, Number(decimals));
589
+ }
590
+ catch {
591
+ return placeholder();
592
+ }
593
+ }
594
+ /**
595
+ * STRICT decimals resolver for the transfer builder — throws
596
+ * `ChainError(RpcError)` on any failure to reach the contract.
597
+ * Used exclusively when converting `amountHr: Decimal` →
598
+ * `amountMr: bigint`, where a defensive fallback to `decimals=0`
599
+ * would silently sign a transaction for a wildly wrong amount
600
+ * (e.g. `1.5 USDC` → `1n` minor units instead of `1_500_000n`).
601
+ * The receipt-decoder still uses the defensive variant, since a
602
+ * placeholder token there is a display concern, not a money one.
603
+ */
604
+ async resolveErc20DecimalsStrict(contractAddress) {
605
+ const provider = this.getProvider();
606
+ const contract = new Contract(contractAddress, ERC20_ABI, provider);
607
+ try {
608
+ const decimals = (await contract.decimals());
609
+ return Number(decimals);
610
+ }
611
+ catch (err) {
612
+ throw this.rpcError(`Failed to read ERC-20 decimals for ${contractAddress}`, err);
613
+ }
614
+ }
615
+ rpcError(message, cause, extra = {}) {
616
+ const sanitizedMessage = sanitizeMessage(`${message}: ${stringifyErr(cause)}`, this._resolvedRpcUrl);
617
+ return new ChainError(ChainErrorKinds.RpcError, sanitizedMessage, { chainId: this.chainId, rpcHost: this.rpcHost(), ...extra }, sanitizeCause(cause, this._resolvedRpcUrl));
618
+ }
619
+ }
620
+ function addRaw(container, wallet, tokenAddr, delta) {
621
+ let perToken = container.get(wallet);
622
+ if (!perToken) {
623
+ perToken = new Map();
624
+ container.set(wallet, perToken);
625
+ }
626
+ perToken.set(tokenAddr, (perToken.get(tokenAddr) ?? 0n) + delta);
627
+ }
628
+ const ERROR_STRING_SELECTOR = '0x08c379a0';
629
+ const PANIC_UINT256_SELECTOR = '0x4e487b71';
630
+ const ABI_CODER = AbiCoder.defaultAbiCoder();
631
+ export function decodeRevertData(data) {
632
+ if (typeof data !== 'string' || data.length < 10)
633
+ return undefined;
634
+ const selector = data.slice(0, 10).toLowerCase();
635
+ const payload = '0x' + data.slice(10);
636
+ if (selector === ERROR_STRING_SELECTOR) {
637
+ try {
638
+ const reason = ABI_CODER.decode(['string'], payload)[0];
639
+ return typeof reason === 'string' ? reason : undefined;
640
+ }
641
+ catch {
642
+ return undefined;
643
+ }
644
+ }
645
+ if (selector === PANIC_UINT256_SELECTOR) {
646
+ try {
647
+ const code = ABI_CODER.decode(['uint256'], payload)[0];
648
+ return typeof code === 'bigint' ? `Panic(0x${code.toString(16)})` : undefined;
649
+ }
650
+ catch {
651
+ return undefined;
652
+ }
653
+ }
654
+ return undefined;
655
+ }
656
+ async function extractRevertInfo(provider, tx, receipt) {
657
+ if (!tx)
658
+ return { code: 'REVERTED' };
659
+ try {
660
+ await provider.call({
661
+ from: tx.from,
662
+ to: tx.to ?? undefined,
663
+ data: tx.data,
664
+ value: tx.value,
665
+ blockTag: receipt.blockNumber,
666
+ });
667
+ return { code: 'REVERTED' };
668
+ }
669
+ catch (err) {
670
+ const data = err?.data ??
671
+ err?.info?.error?.data;
672
+ const reason = typeof data === 'string' ? decodeRevertData(data) : undefined;
673
+ return reason !== undefined ? { code: 'REVERTED', reason } : { code: 'REVERTED' };
674
+ }
675
+ }
676
+ function stringifyErr(err) {
677
+ if (err instanceof Error)
678
+ return err.message;
679
+ return String(err);
680
+ }
681
+ function envCandidatesFor(name, chainId) {
682
+ const normalized = name.replace(/ /g, '_').toUpperCase() + '_RPC_URL';
683
+ return [normalized, `EVM_${chainId}_RPC_URL`];
684
+ }
685
+ //# sourceMappingURL=evm_chain.js.map