@hyperlane-xyz/sdk 42.0.0 → 44.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 (221) hide show
  1. package/dist/consts/multisigIsm.d.ts.map +1 -1
  2. package/dist/consts/multisigIsm.js +12 -0
  3. package/dist/consts/multisigIsm.js.map +1 -1
  4. package/dist/core/adapters/StarknetCoreAdapter.d.ts.map +1 -1
  5. package/dist/core/adapters/StarknetCoreAdapter.js +3 -3
  6. package/dist/core/adapters/StarknetCoreAdapter.js.map +1 -1
  7. package/dist/core/types.d.ts +47 -1635
  8. package/dist/core/types.d.ts.map +1 -1
  9. package/dist/core/types.js +2 -2
  10. package/dist/core/types.js.map +1 -1
  11. package/dist/deploy/HyperlaneDeployer.js +1 -1
  12. package/dist/deploy/proxy.d.ts +8 -40
  13. package/dist/deploy/proxy.d.ts.map +1 -1
  14. package/dist/deploy/proxy.js +4 -4
  15. package/dist/deploy/proxy.js.map +1 -1
  16. package/dist/deploy/types.d.ts +1 -19
  17. package/dist/deploy/types.d.ts.map +1 -1
  18. package/dist/fee/EvmTokenFeeDeployer.d.ts.map +1 -1
  19. package/dist/fee/EvmTokenFeeDeployer.hardhat-test.js +68 -0
  20. package/dist/fee/EvmTokenFeeDeployer.hardhat-test.js.map +1 -1
  21. package/dist/fee/EvmTokenFeeDeployer.js +18 -10
  22. package/dist/fee/EvmTokenFeeDeployer.js.map +1 -1
  23. package/dist/fee/EvmTokenFeeModule.d.ts.map +1 -1
  24. package/dist/fee/EvmTokenFeeModule.hardhat-test.js +18 -8
  25. package/dist/fee/EvmTokenFeeModule.hardhat-test.js.map +1 -1
  26. package/dist/fee/EvmTokenFeeModule.js +8 -1
  27. package/dist/fee/EvmTokenFeeModule.js.map +1 -1
  28. package/dist/fee/EvmTokenFeeReader.d.ts +2 -2
  29. package/dist/fee/EvmTokenFeeReader.d.ts.map +1 -1
  30. package/dist/fee/EvmTokenFeeReader.js.map +1 -1
  31. package/dist/fee/feeConfigMapping.d.ts.map +1 -1
  32. package/dist/fee/feeConfigMapping.js +1 -0
  33. package/dist/fee/feeConfigMapping.js.map +1 -1
  34. package/dist/fee/types.d.ts +147 -646
  35. package/dist/fee/types.d.ts.map +1 -1
  36. package/dist/fee/types.js +26 -17
  37. package/dist/fee/types.js.map +1 -1
  38. package/dist/fork/types.d.ts +125 -1924
  39. package/dist/fork/types.d.ts.map +1 -1
  40. package/dist/fork/types.js +3 -3
  41. package/dist/fork/types.js.map +1 -1
  42. package/dist/gas/oracle/types.d.ts +5 -56
  43. package/dist/gas/oracle/types.d.ts.map +1 -1
  44. package/dist/hook/EvmHookModule.d.ts.map +1 -1
  45. package/dist/hook/EvmHookModule.hardhat-test.js +12 -1
  46. package/dist/hook/EvmHookModule.hardhat-test.js.map +1 -1
  47. package/dist/hook/EvmHookModule.js +3 -0
  48. package/dist/hook/EvmHookModule.js.map +1 -1
  49. package/dist/hook/HyperlaneHookDeployer.d.ts +12 -0
  50. package/dist/hook/HyperlaneHookDeployer.d.ts.map +1 -1
  51. package/dist/hook/HyperlaneHookDeployer.hardhat-test.d.ts +2 -0
  52. package/dist/hook/HyperlaneHookDeployer.hardhat-test.d.ts.map +1 -0
  53. package/dist/hook/HyperlaneHookDeployer.hardhat-test.js +322 -0
  54. package/dist/hook/HyperlaneHookDeployer.hardhat-test.js.map +1 -0
  55. package/dist/hook/HyperlaneHookDeployer.js +262 -5
  56. package/dist/hook/HyperlaneHookDeployer.js.map +1 -1
  57. package/dist/hook/types.d.ts +44 -319
  58. package/dist/hook/types.d.ts.map +1 -1
  59. package/dist/hook/types.js +18 -11
  60. package/dist/hook/types.js.map +1 -1
  61. package/dist/ica/types.d.ts +35 -900
  62. package/dist/ica/types.d.ts.map +1 -1
  63. package/dist/ica/types.js +1 -1
  64. package/dist/ica/types.js.map +1 -1
  65. package/dist/index.d.ts +1 -1
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +1 -1
  68. package/dist/index.js.map +1 -1
  69. package/dist/ism/EvmIsmModule.d.ts +1 -1
  70. package/dist/ism/EvmIsmModule.d.ts.map +1 -1
  71. package/dist/ism/EvmIsmModule.hardhat-test.js +35 -1
  72. package/dist/ism/EvmIsmModule.hardhat-test.js.map +1 -1
  73. package/dist/ism/EvmIsmModule.js +15 -11
  74. package/dist/ism/EvmIsmModule.js.map +1 -1
  75. package/dist/ism/HyperlaneIsmFactory.d.ts +3 -1
  76. package/dist/ism/HyperlaneIsmFactory.d.ts.map +1 -1
  77. package/dist/ism/HyperlaneIsmFactory.hardhat-test.js +61 -3
  78. package/dist/ism/HyperlaneIsmFactory.hardhat-test.js.map +1 -1
  79. package/dist/ism/HyperlaneIsmFactory.js +42 -6
  80. package/dist/ism/HyperlaneIsmFactory.js.map +1 -1
  81. package/dist/ism/types.d.ts +48 -330
  82. package/dist/ism/types.d.ts.map +1 -1
  83. package/dist/ism/types.js +8 -10
  84. package/dist/ism/types.js.map +1 -1
  85. package/dist/metadata/agentConfig.d.ts +537 -10384
  86. package/dist/metadata/agentConfig.d.ts.map +1 -1
  87. package/dist/metadata/agentConfig.js +18 -18
  88. package/dist/metadata/agentConfig.js.map +1 -1
  89. package/dist/metadata/chainMetadataConversion.d.ts.map +1 -1
  90. package/dist/metadata/chainMetadataConversion.js +1 -0
  91. package/dist/metadata/chainMetadataConversion.js.map +1 -1
  92. package/dist/metadata/chainMetadataTypes.d.ts +70 -4678
  93. package/dist/metadata/chainMetadataTypes.d.ts.map +1 -1
  94. package/dist/metadata/chainMetadataTypes.js +8 -7
  95. package/dist/metadata/chainMetadataTypes.js.map +1 -1
  96. package/dist/metadata/customZodTypes.d.ts +5 -5
  97. package/dist/metadata/customZodTypes.d.ts.map +1 -1
  98. package/dist/metadata/customZodTypes.js +4 -5
  99. package/dist/metadata/customZodTypes.js.map +1 -1
  100. package/dist/metadata/deploymentArtifacts.d.ts +1 -13
  101. package/dist/metadata/deploymentArtifacts.d.ts.map +1 -1
  102. package/dist/metadata/matchingList.d.ts +12 -40
  103. package/dist/metadata/matchingList.d.ts.map +1 -1
  104. package/dist/metadata/warpRouteConfig.d.ts +8 -114
  105. package/dist/metadata/warpRouteConfig.d.ts.map +1 -1
  106. package/dist/metadata/warpRouteConfig.js +3 -3
  107. package/dist/metadata/warpRouteConfig.js.map +1 -1
  108. package/dist/middleware/account/InterchainAccount.d.ts.map +1 -1
  109. package/dist/middleware/account/InterchainAccount.js +12 -8
  110. package/dist/middleware/account/InterchainAccount.js.map +1 -1
  111. package/dist/middleware/account/InterchainAccount.test.js +22 -3
  112. package/dist/middleware/account/InterchainAccount.test.js.map +1 -1
  113. package/dist/middleware/account/icaCalls.d.ts +13 -145
  114. package/dist/middleware/account/icaCalls.d.ts.map +1 -1
  115. package/dist/middleware/account/types.d.ts +11 -105
  116. package/dist/middleware/account/types.d.ts.map +1 -1
  117. package/dist/predicate/PredicateApiClient.d.ts +1 -11
  118. package/dist/predicate/PredicateApiClient.d.ts.map +1 -1
  119. package/dist/providers/MultiProvider.d.ts.map +1 -1
  120. package/dist/providers/MultiProvider.js +3 -2
  121. package/dist/providers/MultiProvider.js.map +1 -1
  122. package/dist/providers/ProviderType.d.ts +4 -4
  123. package/dist/providers/ProviderType.d.ts.map +1 -1
  124. package/dist/providers/transactions/submitter/builder/types.d.ts +13 -280
  125. package/dist/providers/transactions/submitter/builder/types.d.ts.map +1 -1
  126. package/dist/providers/transactions/submitter/builder/types.js +2 -4
  127. package/dist/providers/transactions/submitter/builder/types.js.map +1 -1
  128. package/dist/providers/transactions/submitter/ethersV5/EV5GnosisSafeTxSubmitter.d.ts +2 -0
  129. package/dist/providers/transactions/submitter/ethersV5/EV5GnosisSafeTxSubmitter.d.ts.map +1 -1
  130. package/dist/providers/transactions/submitter/ethersV5/EV5GnosisSafeTxSubmitter.js +15 -7
  131. package/dist/providers/transactions/submitter/ethersV5/EV5GnosisSafeTxSubmitter.js.map +1 -1
  132. package/dist/providers/transactions/submitter/ethersV5/EV5GnosisSafeTxSubmitter.test.d.ts +2 -0
  133. package/dist/providers/transactions/submitter/ethersV5/EV5GnosisSafeTxSubmitter.test.d.ts.map +1 -0
  134. package/dist/providers/transactions/submitter/ethersV5/EV5GnosisSafeTxSubmitter.test.js +145 -0
  135. package/dist/providers/transactions/submitter/ethersV5/EV5GnosisSafeTxSubmitter.test.js.map +1 -0
  136. package/dist/providers/transactions/submitter/ethersV5/types.d.ts +30 -102
  137. package/dist/providers/transactions/submitter/ethersV5/types.d.ts.map +1 -1
  138. package/dist/providers/transactions/submitter/ethersV5/types.js +3 -21
  139. package/dist/providers/transactions/submitter/ethersV5/types.js.map +1 -1
  140. package/dist/providers/transactions/submitter/types.d.ts +5 -51
  141. package/dist/providers/transactions/submitter/types.d.ts.map +1 -1
  142. package/dist/providers/transactions/types.d.ts +1 -9
  143. package/dist/providers/transactions/types.d.ts.map +1 -1
  144. package/dist/router/types.d.ts +33 -819
  145. package/dist/router/types.d.ts.map +1 -1
  146. package/dist/router/types.js +4 -4
  147. package/dist/router/types.js.map +1 -1
  148. package/dist/rpc/evm/EvmEventLogsReader.d.ts +19 -27
  149. package/dist/rpc/evm/EvmEventLogsReader.d.ts.map +1 -1
  150. package/dist/rpc/evm/EvmEventLogsReader.js +23 -0
  151. package/dist/rpc/evm/EvmEventLogsReader.js.map +1 -1
  152. package/dist/rpc/evm/EvmEventLogsReader.test.js +53 -1
  153. package/dist/rpc/evm/EvmEventLogsReader.test.js.map +1 -1
  154. package/dist/signers/starknet/starknetjs.d.ts.map +1 -1
  155. package/dist/signers/starknet/starknetjs.js +5 -1
  156. package/dist/signers/starknet/starknetjs.js.map +1 -1
  157. package/dist/signers/turnkeyClient.d.ts +1 -15
  158. package/dist/signers/turnkeyClient.d.ts.map +1 -1
  159. package/dist/timelock/types.d.ts +8 -32
  160. package/dist/timelock/types.d.ts.map +1 -1
  161. package/dist/token/EvmWarpRouteReader.d.ts.map +1 -1
  162. package/dist/token/EvmWarpRouteReader.js +36 -19
  163. package/dist/token/EvmWarpRouteReader.js.map +1 -1
  164. package/dist/token/EvmWarpRouteReader.test.js +219 -2
  165. package/dist/token/EvmWarpRouteReader.test.js.map +1 -1
  166. package/dist/token/EvmXERC20Reader.d.ts +9 -21
  167. package/dist/token/EvmXERC20Reader.d.ts.map +1 -1
  168. package/dist/token/EvmXERC20Reader.js +24 -48
  169. package/dist/token/EvmXERC20Reader.js.map +1 -1
  170. package/dist/token/EvmXERC20Reader.test.js +97 -0
  171. package/dist/token/EvmXERC20Reader.test.js.map +1 -1
  172. package/dist/token/ITokenMetadata.d.ts +12 -130
  173. package/dist/token/ITokenMetadata.d.ts.map +1 -1
  174. package/dist/token/ITokenMetadata.js +2 -2
  175. package/dist/token/ITokenMetadata.js.map +1 -1
  176. package/dist/token/TokenConnection.d.ts +3 -35
  177. package/dist/token/TokenConnection.d.ts.map +1 -1
  178. package/dist/token/adapters/StarknetTokenAdapter.js +10 -2
  179. package/dist/token/adapters/StarknetTokenAdapter.js.map +1 -1
  180. package/dist/token/configUtils.d.ts.map +1 -1
  181. package/dist/token/configUtils.js +16 -0
  182. package/dist/token/configUtils.js.map +1 -1
  183. package/dist/token/configUtils.test.js +57 -10
  184. package/dist/token/configUtils.test.js.map +1 -1
  185. package/dist/token/deploy.hardhat-test.js +165 -5
  186. package/dist/token/deploy.hardhat-test.js.map +1 -1
  187. package/dist/token/types.d.ts +4012 -22806
  188. package/dist/token/types.d.ts.map +1 -1
  189. package/dist/token/types.js +10 -12
  190. package/dist/token/types.js.map +1 -1
  191. package/dist/token/xerc20-abi.d.ts +31 -0
  192. package/dist/token/xerc20-abi.d.ts.map +1 -1
  193. package/dist/token/xerc20-abi.js +40 -0
  194. package/dist/token/xerc20-abi.js.map +1 -1
  195. package/dist/token/xerc20-limits.d.ts +63 -0
  196. package/dist/token/xerc20-limits.d.ts.map +1 -0
  197. package/dist/token/xerc20-limits.js +80 -0
  198. package/dist/token/xerc20-limits.js.map +1 -0
  199. package/dist/token/xerc20.d.ts +30 -1
  200. package/dist/token/xerc20.d.ts.map +1 -1
  201. package/dist/token/xerc20.js +137 -54
  202. package/dist/token/xerc20.js.map +1 -1
  203. package/dist/token/xerc20.test.js +461 -169
  204. package/dist/token/xerc20.test.js.map +1 -1
  205. package/dist/types.d.ts +6 -36
  206. package/dist/types.d.ts.map +1 -1
  207. package/dist/types.js +1 -1
  208. package/dist/types.js.map +1 -1
  209. package/dist/utils/cosmos.d.ts +113 -844
  210. package/dist/utils/cosmos.d.ts.map +1 -1
  211. package/dist/utils/cosmos.js +50 -91
  212. package/dist/utils/cosmos.js.map +1 -1
  213. package/dist/utils/schemas.d.ts +3 -3
  214. package/dist/utils/schemas.d.ts.map +1 -1
  215. package/dist/utils/schemas.js +2 -1
  216. package/dist/utils/schemas.js.map +1 -1
  217. package/dist/utils/starknet.js +5 -1
  218. package/dist/utils/starknet.js.map +1 -1
  219. package/dist/warp/types.d.ts +25 -367
  220. package/dist/warp/types.d.ts.map +1 -1
  221. package/package.json +14 -14
@@ -2,16 +2,18 @@ import chai, { expect } from 'chai';
2
2
  import chaiAsPromised from 'chai-as-promised';
3
3
  import { ethers } from 'ethers';
4
4
  import sinon from 'sinon';
5
+ import { assert } from '@hyperlane-xyz/utils';
5
6
  import { TestChainName } from '../consts/testChains.js';
7
+ import { EIP1967_IMPLEMENTATION_SLOT } from '../deploy/proxy.js';
6
8
  import { MultiProvider } from '../providers/MultiProvider.js';
7
9
  import { EvmEventLogsReader } from '../rpc/evm/EvmEventLogsReader.js';
10
+ import { viemLogFromGetEventLogsResponse } from '../rpc/evm/utils.js';
8
11
  import { XERC20Type } from './types.js';
9
- import { CONFIGURATION_CHANGED_EVENT_SELECTOR } from './xerc20-abi.js';
10
- import { deriveXERC20TokenType, getExtraLockBoxConfigs } from './xerc20.js';
12
+ import { BRIDGE_LIMITS_SET_EVENT_SELECTOR, CONFIGURATION_CHANGED_EVENT_SELECTOR, } from './xerc20-abi.js';
13
+ import { UnknownXERC20TypeError, deriveXERC20TokenType, getExtraLockBoxConfigs, latestConfigurationPerBridge, } from './xerc20.js';
11
14
  chai.use(chaiAsPromised);
12
15
  const PROXY_ADDRESS = '0x1111111111111111111111111111111111111111';
13
16
  const IMPLEMENTATION_ADDRESS = '0x2222222222222222222222222222222222222222';
14
- const PROXY_ADMIN_ADDRESS = '0x3333333333333333333333333333333333333333';
15
17
  const setBufferCapSelector = ethers.utils
16
18
  .id('setBufferCap(address,uint256)')
17
19
  .slice(2, 10)
@@ -20,10 +22,6 @@ const setLimitsSelector = ethers.utils
20
22
  .id('setLimits(address,uint256,uint256)')
21
23
  .slice(2, 10)
22
24
  .toLowerCase();
23
- // EIP-1967 admin slot read by proxyAdmin() / isProxy().
24
- const ADMIN_SLOT = '0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103';
25
- // EIP-1967 implementation slot read by proxyImplementation().
26
- const IMPLEMENTATION_SLOT = '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc';
27
25
  function storageAddress(address) {
28
26
  return ethers.utils.hexZeroPad(address, 32);
29
27
  }
@@ -45,15 +43,15 @@ const cases = [
45
43
  },
46
44
  {
47
45
  // Proxy bytecode is a delegatecall stub lacking the selectors; the
48
- // implementation bytecode carries the Velodrome selector.
46
+ // implementation bytecode carries the Velodrome selector. The admin slot is
47
+ // left out so the stub throws if the derivation reads one.
49
48
  name: 'inspects the implementation bytecode when the Velo token is behind a proxy',
50
49
  code: {
51
50
  [PROXY_ADDRESS]: '0xdead',
52
51
  [IMPLEMENTATION_ADDRESS]: `0x${setBufferCapSelector}`,
53
52
  },
54
53
  storage: {
55
- [ADMIN_SLOT]: storageAddress(PROXY_ADMIN_ADDRESS),
56
- [IMPLEMENTATION_SLOT]: storageAddress(IMPLEMENTATION_ADDRESS),
54
+ [EIP1967_IMPLEMENTATION_SLOT]: storageAddress(IMPLEMENTATION_ADDRESS),
57
55
  },
58
56
  expected: { type: XERC20Type.Velo },
59
57
  },
@@ -65,11 +63,48 @@ const cases = [
65
63
  [IMPLEMENTATION_ADDRESS]: `0x${setLimitsSelector}`,
66
64
  },
67
65
  storage: {
68
- [ADMIN_SLOT]: storageAddress(PROXY_ADMIN_ADDRESS),
69
- [IMPLEMENTATION_SLOT]: storageAddress(IMPLEMENTATION_ADDRESS),
66
+ [EIP1967_IMPLEMENTATION_SLOT]: storageAddress(IMPLEMENTATION_ADDRESS),
70
67
  },
71
68
  expected: { type: XERC20Type.Standard },
72
69
  },
70
+ {
71
+ // A UUPS proxy keeps its upgrade logic in the implementation, so its admin
72
+ // slot is empty. Reading the implementation off an admin would skip the
73
+ // only bytecode carrying the selectors; omitting the admin slot here makes
74
+ // the stub throw if it is consulted.
75
+ name: 'inspects the implementation bytecode of a Velo token behind a UUPS proxy',
76
+ code: {
77
+ [PROXY_ADDRESS]: '0xdead',
78
+ [IMPLEMENTATION_ADDRESS]: `0x${setBufferCapSelector}`,
79
+ },
80
+ storage: {
81
+ [EIP1967_IMPLEMENTATION_SLOT]: storageAddress(IMPLEMENTATION_ADDRESS),
82
+ },
83
+ expected: { type: XERC20Type.Velo },
84
+ },
85
+ {
86
+ name: 'inspects the implementation bytecode of a Standard token behind a UUPS proxy',
87
+ code: {
88
+ [PROXY_ADDRESS]: '0xdead',
89
+ [IMPLEMENTATION_ADDRESS]: `0x${setLimitsSelector}`,
90
+ },
91
+ storage: {
92
+ [EIP1967_IMPLEMENTATION_SLOT]: storageAddress(IMPLEMENTATION_ADDRESS),
93
+ },
94
+ expected: { type: XERC20Type.Standard },
95
+ },
96
+ {
97
+ // Nothing behind the address to inspect, so the zero implementation must
98
+ // not be fetched as though it were one.
99
+ name: 'throws when the implementation slot is empty',
100
+ code: { [PROXY_ADDRESS]: '0xdead' },
101
+ storage: {
102
+ [EIP1967_IMPLEMENTATION_SLOT]: storageAddress(ethers.constants.AddressZero),
103
+ },
104
+ expected: {
105
+ errorIncludes: 'does not implement Standard or Velodrome XERC20 interface',
106
+ },
107
+ },
73
108
  {
74
109
  // Proxy resolves to an implementation that still lacks both selectors.
75
110
  name: 'throws when neither the proxy nor its implementation implements a known interface',
@@ -78,8 +113,7 @@ const cases = [
78
113
  [IMPLEMENTATION_ADDRESS]: '0xbeef',
79
114
  },
80
115
  storage: {
81
- [ADMIN_SLOT]: storageAddress(PROXY_ADMIN_ADDRESS),
82
- [IMPLEMENTATION_SLOT]: storageAddress(IMPLEMENTATION_ADDRESS),
116
+ [EIP1967_IMPLEMENTATION_SLOT]: storageAddress(IMPLEMENTATION_ADDRESS),
83
117
  },
84
118
  expected: {
85
119
  errorIncludes: 'does not implement Standard or Velodrome XERC20 interface',
@@ -144,12 +178,64 @@ describe('deriveXERC20TokenType', () => {
144
178
  });
145
179
  }
146
180
  });
181
+ describe('UnknownXERC20TypeError', () => {
182
+ let sandbox;
183
+ let multiProvider;
184
+ beforeEach(() => {
185
+ sandbox = sinon.createSandbox();
186
+ multiProvider = MultiProvider.createTestMultiProvider();
187
+ });
188
+ afterEach(() => {
189
+ sandbox.restore();
190
+ });
191
+ // Callers tell an undetectable interface apart from a failure to read the
192
+ // bytecode by the error type, so the type is part of the contract.
193
+ it('is thrown when the contract implements neither known interface', async () => {
194
+ const provider = multiProvider.getProvider(TestChainName.test1);
195
+ sandbox.stub(provider, 'getCode').resolves('0xbeef');
196
+ sandbox
197
+ .stub(provider, 'getStorageAt')
198
+ .resolves(storageAddress(PROXY_ADDRESS));
199
+ let thrown;
200
+ try {
201
+ await deriveXERC20TokenType(multiProvider, TestChainName.test1, PROXY_ADDRESS);
202
+ }
203
+ catch (error) {
204
+ thrown = error;
205
+ }
206
+ expect(thrown).to.be.instanceOf(UnknownXERC20TypeError);
207
+ });
208
+ it('is not thrown when the bytecode cannot be read at all', async () => {
209
+ const provider = multiProvider.getProvider(TestChainName.test1);
210
+ const transientError = new Error('Invalid response from provider');
211
+ sandbox.stub(provider, 'getCode').rejects(transientError);
212
+ let thrown;
213
+ try {
214
+ await deriveXERC20TokenType(multiProvider, TestChainName.test1, PROXY_ADDRESS);
215
+ }
216
+ catch (error) {
217
+ thrown = error;
218
+ }
219
+ expect(thrown).to.equal(transientError);
220
+ });
221
+ });
147
222
  const XERC20_ADDRESS = '0x6666666666666666666666666666666666666666';
148
223
  const BRIDGE_A = '0x4444444444444444444444444444444444444444';
149
224
  const BRIDGE_B = '0x5555555555555555555555555555555555555555';
150
- // Value returned by a lockbox's XERC20() getter; never asserted on.
151
- const LOCKBOX_XERC20 = '0x7777777777777777777777777777777777777777';
152
- function configurationChangedLog({ bridge, bufferCap, rateLimitPerSecond, blockNumber, logIndex = 0, }) {
225
+ // A mainnet warp route router, which its xERC20 holds bridge limits for like
226
+ // any other bridge. Those limits are reported as warpRouteLimits, never as an
227
+ // extra bridge.
228
+ const WARP_ROUTER = '0x88AC0fC430130983c0DDEB4C22574056D8340Ca8';
229
+ const rateLimitsSelector = ethers.utils.id('rateLimits(address)').slice(0, 10);
230
+ const mintingMaxLimitOfSelector = ethers.utils
231
+ .id('mintingMaxLimitOf(address)')
232
+ .slice(0, 10);
233
+ const burningMaxLimitOfSelector = ethers.utils
234
+ .id('burningMaxLimitOf(address)')
235
+ .slice(0, 10);
236
+ // The scan reads the bridge out of topics[1] and nothing else, so the payload
237
+ // only matters to latestConfigurationPerBridge, which parses it.
238
+ function configurationChangedLog({ bridge, bufferCap = 100, rateLimitPerSecond = 1, blockNumber, logIndex = 0, }) {
153
239
  return {
154
240
  address: XERC20_ADDRESS,
155
241
  blockNumber,
@@ -163,152 +249,112 @@ function configurationChangedLog({ bridge, bufferCap, rateLimitPerSecond, blockN
163
249
  transactionIndex: 0,
164
250
  };
165
251
  }
166
- const lockboxCases = [
167
- {
168
- name: 'keeps only the most recent configuration of a bridge',
169
- logs: [
170
- configurationChangedLog({
171
- bridge: BRIDGE_A,
172
- bufferCap: 100,
173
- rateLimitPerSecond: 1,
174
- blockNumber: 10,
175
- }),
176
- configurationChangedLog({
177
- bridge: BRIDGE_A,
178
- bufferCap: 500,
179
- rateLimitPerSecond: 2,
180
- blockNumber: 20,
181
- }),
252
+ function bridgeLimitsSetLog({ bridge, blockNumber, logIndex = 0, }) {
253
+ return {
254
+ address: XERC20_ADDRESS,
255
+ blockNumber,
256
+ data: ethers.utils.defaultAbiCoder.encode(['uint256', 'uint256'], [100, 100]),
257
+ logIndex,
258
+ topics: [
259
+ BRIDGE_LIMITS_SET_EVENT_SELECTOR,
260
+ ethers.utils.hexZeroPad(bridge, 32),
182
261
  ],
183
- lockboxBridges: [BRIDGE_A],
262
+ transactionHash: ethers.utils.hexZeroPad(`0x${blockNumber}`, 32),
263
+ transactionIndex: 0,
264
+ };
265
+ }
266
+ const bridgeCases = [
267
+ {
268
+ // Every non-lockbox bridge used to be dropped, because the reader kept only
269
+ // the addresses answering the lockbox XERC20() getter.
270
+ name: 'reports a configured bridge that is not a lockbox',
271
+ type: XERC20Type.Velo,
272
+ logs: [configurationChangedLog({ bridge: BRIDGE_A, blockNumber: 10 })],
273
+ onChainLimits: {
274
+ [BRIDGE_A.toLowerCase()]: { bufferCap: '100', rateLimitPerSecond: '1' },
275
+ },
184
276
  expected: [
185
277
  {
186
- lockbox: BRIDGE_A,
278
+ lockbox: BRIDGE_A.toLowerCase(),
187
279
  limits: {
188
280
  type: XERC20Type.Velo,
189
- bufferCap: '500',
190
- rateLimitPerSecond: '2',
281
+ bufferCap: '100',
282
+ rateLimitPerSecond: '1',
191
283
  },
192
284
  },
193
285
  ],
194
286
  },
195
287
  {
196
- // A bridge can be reconfigured twice in the same block, so ordering on the
197
- // block number alone kept the first configuration instead of the last.
198
- name: 'keeps the highest logIndex configuration of a bridge within a block',
288
+ // The route's router holds bridge limits like any other bridge, and the
289
+ // lockbox probe used to be what kept it out of the extra bridges.
290
+ name: "omits the warp route's own router",
291
+ type: XERC20Type.Velo,
199
292
  logs: [
200
- configurationChangedLog({
201
- bridge: BRIDGE_A,
202
- bufferCap: 100,
203
- rateLimitPerSecond: 1,
204
- blockNumber: 10,
205
- logIndex: 3,
206
- }),
207
- configurationChangedLog({
208
- bridge: BRIDGE_A,
209
- bufferCap: 500,
210
- rateLimitPerSecond: 2,
211
- blockNumber: 10,
212
- logIndex: 7,
213
- }),
293
+ configurationChangedLog({ bridge: WARP_ROUTER, blockNumber: 10 }),
294
+ configurationChangedLog({ bridge: BRIDGE_A, blockNumber: 11 }),
214
295
  ],
215
- lockboxBridges: [BRIDGE_A],
296
+ warpRouteAddress: WARP_ROUTER,
297
+ onChainLimits: {
298
+ [WARP_ROUTER.toLowerCase()]: {
299
+ bufferCap: '300',
300
+ rateLimitPerSecond: '3',
301
+ },
302
+ [BRIDGE_A.toLowerCase()]: { bufferCap: '100', rateLimitPerSecond: '1' },
303
+ },
216
304
  expected: [
217
305
  {
218
- lockbox: BRIDGE_A,
306
+ lockbox: BRIDGE_A.toLowerCase(),
219
307
  limits: {
220
308
  type: XERC20Type.Velo,
221
- bufferCap: '500',
222
- rateLimitPerSecond: '2',
309
+ bufferCap: '100',
310
+ rateLimitPerSecond: '1',
223
311
  },
224
312
  },
225
313
  ],
226
314
  },
227
315
  {
228
- // The same-block tiebreaker must not depend on the order the logs arrive in
229
- name: 'keeps the highest logIndex configuration when logs arrive out of order',
230
- logs: [
231
- configurationChangedLog({
232
- bridge: BRIDGE_A,
233
- bufferCap: 500,
234
- rateLimitPerSecond: 2,
235
- blockNumber: 10,
236
- logIndex: 7,
237
- }),
238
- configurationChangedLog({
239
- bridge: BRIDGE_A,
240
- bufferCap: 100,
241
- rateLimitPerSecond: 1,
242
- blockNumber: 10,
243
- logIndex: 3,
244
- }),
245
- ],
246
- lockboxBridges: [BRIDGE_A],
316
+ // The event is a record of a past announcement, not of the current state:
317
+ // only what the token holds now decides whether a bridge is active.
318
+ name: 'reports a bridge on the limits it holds, not the ones it was announced with',
319
+ type: XERC20Type.Velo,
320
+ logs: [configurationChangedLog({ bridge: BRIDGE_A, blockNumber: 20 })],
321
+ onChainLimits: {
322
+ [BRIDGE_A.toLowerCase()]: { bufferCap: '900', rateLimitPerSecond: '9' },
323
+ },
247
324
  expected: [
248
325
  {
249
- lockbox: BRIDGE_A,
326
+ lockbox: BRIDGE_A.toLowerCase(),
250
327
  limits: {
251
328
  type: XERC20Type.Velo,
252
- bufferCap: '500',
253
- rateLimitPerSecond: '2',
329
+ bufferCap: '900',
330
+ rateLimitPerSecond: '9',
254
331
  },
255
332
  },
256
333
  ],
257
334
  },
258
335
  {
259
- // Deduplication must run before the zero-limit filter, otherwise the stale
260
- // non-zero configuration would keep a deactivated bridge alive.
261
- name: 'drops a bridge whose most recent configuration zeroes both limits',
262
- logs: [
263
- configurationChangedLog({
264
- bridge: BRIDGE_A,
265
- bufferCap: 100,
266
- rateLimitPerSecond: 1,
267
- blockNumber: 10,
268
- }),
269
- configurationChangedLog({
270
- bridge: BRIDGE_A,
271
- bufferCap: 0,
272
- rateLimitPerSecond: 0,
273
- blockNumber: 20,
274
- }),
275
- ],
276
- lockboxBridges: [BRIDGE_A],
277
- expected: [],
278
- },
279
- {
280
- name: 'drops a bridge configured with both limits set to zero',
281
- logs: [
282
- configurationChangedLog({
283
- bridge: BRIDGE_A,
284
- bufferCap: 0,
285
- rateLimitPerSecond: 0,
286
- blockNumber: 10,
287
- }),
288
- ],
289
- lockboxBridges: [BRIDGE_A],
336
+ name: 'drops a bridge the token holds no limits for',
337
+ type: XERC20Type.Velo,
338
+ logs: [configurationChangedLog({ bridge: BRIDGE_A, blockNumber: 10 })],
339
+ onChainLimits: {
340
+ [BRIDGE_A.toLowerCase()]: { bufferCap: '0', rateLimitPerSecond: '0' },
341
+ },
290
342
  expected: [],
291
343
  },
292
344
  {
293
- name: 'keeps bridges where either limit is non zero',
345
+ name: 'keeps a bridge where either limit is non zero',
346
+ type: XERC20Type.Velo,
294
347
  logs: [
295
- configurationChangedLog({
296
- bridge: BRIDGE_A,
297
- bufferCap: 0,
298
- rateLimitPerSecond: 7,
299
- blockNumber: 10,
300
- }),
301
- configurationChangedLog({
302
- bridge: BRIDGE_B,
303
- bufferCap: 9,
304
- rateLimitPerSecond: 0,
305
- blockNumber: 11,
306
- }),
348
+ configurationChangedLog({ bridge: BRIDGE_A, blockNumber: 10 }),
349
+ configurationChangedLog({ bridge: BRIDGE_B, blockNumber: 11 }),
307
350
  ],
308
- lockboxBridges: [BRIDGE_A, BRIDGE_B],
351
+ onChainLimits: {
352
+ [BRIDGE_A.toLowerCase()]: { bufferCap: '0', rateLimitPerSecond: '7' },
353
+ [BRIDGE_B.toLowerCase()]: { bufferCap: '9', rateLimitPerSecond: '0' },
354
+ },
309
355
  expected: [
310
356
  {
311
- lockbox: BRIDGE_A,
357
+ lockbox: BRIDGE_A.toLowerCase(),
312
358
  limits: {
313
359
  type: XERC20Type.Velo,
314
360
  bufferCap: '0',
@@ -316,7 +362,7 @@ const lockboxCases = [
316
362
  },
317
363
  },
318
364
  {
319
- lockbox: BRIDGE_B,
365
+ lockbox: BRIDGE_B.toLowerCase(),
320
366
  limits: {
321
367
  type: XERC20Type.Velo,
322
368
  bufferCap: '9',
@@ -326,25 +372,25 @@ const lockboxCases = [
326
372
  ],
327
373
  },
328
374
  {
329
- name: 'drops bridges that are not lockbox contracts',
375
+ // A bridge is announced again every time its limits change, so the same
376
+ // address arrives many times over a token's history.
377
+ name: 'reports a bridge announced more than once only once',
378
+ type: XERC20Type.Velo,
330
379
  logs: [
380
+ configurationChangedLog({ bridge: BRIDGE_A, blockNumber: 10 }),
381
+ configurationChangedLog({ bridge: BRIDGE_A, blockNumber: 20 }),
331
382
  configurationChangedLog({
332
383
  bridge: BRIDGE_A,
333
- bufferCap: 100,
334
- rateLimitPerSecond: 1,
335
- blockNumber: 10,
336
- }),
337
- configurationChangedLog({
338
- bridge: BRIDGE_B,
339
- bufferCap: 200,
340
- rateLimitPerSecond: 2,
341
- blockNumber: 11,
384
+ blockNumber: 20,
385
+ logIndex: 4,
342
386
  }),
343
387
  ],
344
- lockboxBridges: [BRIDGE_A],
388
+ onChainLimits: {
389
+ [BRIDGE_A.toLowerCase()]: { bufferCap: '100', rateLimitPerSecond: '1' },
390
+ },
345
391
  expected: [
346
392
  {
347
- lockbox: BRIDGE_A,
393
+ lockbox: BRIDGE_A.toLowerCase(),
348
394
  limits: {
349
395
  type: XERC20Type.Velo,
350
396
  bufferCap: '100',
@@ -354,78 +400,220 @@ const lockboxCases = [
354
400
  ],
355
401
  },
356
402
  {
357
- name: 'returns an empty list when no configuration events were emitted',
358
- logs: [],
359
- lockboxBridges: [],
360
- expected: [],
403
+ // A Standard token announces its bridges through a different event and
404
+ // exposes no rateLimits getter, so it used to be invisible on both counts.
405
+ name: 'discovers and reports the bridges of a Standard xERC20',
406
+ type: XERC20Type.Standard,
407
+ logs: [
408
+ bridgeLimitsSetLog({ bridge: BRIDGE_A, blockNumber: 10 }),
409
+ bridgeLimitsSetLog({ bridge: BRIDGE_B, blockNumber: 11 }),
410
+ ],
411
+ onChainLimits: {
412
+ [BRIDGE_A.toLowerCase()]: { mint: '400', burn: '500' },
413
+ [BRIDGE_B.toLowerCase()]: { mint: '0', burn: '0' },
414
+ },
415
+ expected: [
416
+ {
417
+ lockbox: BRIDGE_A.toLowerCase(),
418
+ limits: { type: XERC20Type.Standard, mint: '400', burn: '500' },
419
+ },
420
+ ],
361
421
  },
362
422
  ];
423
+ const DEPLOYMENT_BLOCK = 5755967;
363
424
  describe('getExtraLockBoxConfigs', () => {
364
425
  let sandbox;
365
426
  let multiProvider;
427
+ let deploymentBlock;
366
428
  beforeEach(() => {
367
429
  sandbox = sinon.createSandbox();
368
430
  multiProvider = MultiProvider.createTestMultiProvider();
431
+ deploymentBlock = sandbox
432
+ .stub(EvmEventLogsReader.prototype, 'getContractDeploymentBlock')
433
+ .resolves(DEPLOYMENT_BLOCK);
369
434
  });
370
435
  afterEach(() => {
371
436
  sandbox.restore();
372
437
  });
373
- function stubLockboxCalls(lockboxBridges) {
374
- const lockboxes = new Set(lockboxBridges.map((b) => b.toLowerCase()));
438
+ // Serves the token's bytecode, which is what the type derivation reads, and
439
+ // the limit getter of that type. An address the token holds no limits for
440
+ // reverts with empty return data, like a getter reading an unset entry on a
441
+ // contract that does not have it.
442
+ function stubXERC20(type, onChainLimits) {
375
443
  const provider = multiProvider.getProvider(TestChainName.test1);
444
+ sandbox
445
+ .stub(provider, 'getCode')
446
+ .resolves(`0x${type === XERC20Type.Velo ? setBufferCapSelector : setLimitsSelector}`);
376
447
  sandbox.stub(provider, 'call').callsFake(async (transaction) => {
377
- const to = await transaction.to;
378
- if (typeof to === 'string' && lockboxes.has(to.toLowerCase())) {
379
- return ethers.utils.defaultAbiCoder.encode(['address'], [LOCKBOX_XERC20]);
448
+ const data = await transaction.data;
449
+ assert(typeof data === 'string', 'Expected call data');
450
+ const selector = data.slice(0, 10);
451
+ const [bridge] = ethers.utils.defaultAbiCoder.decode(['address'], `0x${data.slice(10)}`);
452
+ const limits = onChainLimits[bridge.toLowerCase()];
453
+ if (!limits) {
454
+ throw Object.assign(new Error('call revert exception'), {
455
+ code: 'CALL_EXCEPTION',
456
+ data: '0x',
457
+ });
380
458
  }
381
- throw Object.assign(new Error('call revert exception'), {
382
- code: 'CALL_EXCEPTION',
383
- data: '0x',
384
- });
459
+ if (selector === rateLimitsSelector && 'bufferCap' in limits) {
460
+ return ethers.utils.defaultAbiCoder.encode(['tuple(uint128,uint112,uint32,uint112,uint112)'], [[limits.rateLimitPerSecond, limits.bufferCap, 0, 0, 0]]);
461
+ }
462
+ if (selector === mintingMaxLimitOfSelector && 'mint' in limits) {
463
+ return ethers.utils.defaultAbiCoder.encode(['uint256'], [limits.mint]);
464
+ }
465
+ if (selector === burningMaxLimitOfSelector && 'burn' in limits) {
466
+ return ethers.utils.defaultAbiCoder.encode(['uint256'], [limits.burn]);
467
+ }
468
+ throw new Error(`Unexpected call ${selector} for ${bridge}`);
385
469
  });
386
470
  }
387
- for (const c of lockboxCases) {
471
+ for (const c of bridgeCases) {
388
472
  it(c.name, async () => {
389
473
  sandbox
390
474
  .stub(EvmEventLogsReader.prototype, 'getLogsByTopic')
391
475
  .resolves(c.logs);
392
- stubLockboxCalls(c.lockboxBridges);
476
+ stubXERC20(c.type, c.onChainLimits);
393
477
  const result = await getExtraLockBoxConfigs({
394
478
  chain: TestChainName.test1,
395
479
  xERC20Address: XERC20_ADDRESS,
396
480
  multiProvider,
481
+ warpRouteAddress: c.warpRouteAddress,
397
482
  });
398
483
  expect(result).to.deep.equal(c.expected);
399
484
  });
400
485
  }
401
- it('reads the ConfigurationChanged logs with the xERC20 scan block range', async () => {
486
+ // Querying the wrong signature's topic returns nothing, which reads as a
487
+ // token with no bridges rather than as a scan that asked the wrong question.
488
+ const topicCases = [
489
+ {
490
+ name: 'scans the Velodrome event topic for a Velodrome xERC20',
491
+ type: XERC20Type.Velo,
492
+ expectedTopic: '0xb4ff6a860e04455b1ce16833b74cde19765c95e55c5e7e4f5a69e9707d8cc96d',
493
+ },
494
+ {
495
+ name: 'scans the Standard event topic for a Standard xERC20',
496
+ type: XERC20Type.Standard,
497
+ expectedTopic: '0x93f3bbfe8cfb354ec059175107653f49f6eb479a8622a7d83866ea015435c944',
498
+ },
499
+ ];
500
+ for (const c of topicCases) {
501
+ it(c.name, async () => {
502
+ const fromConfig = sandbox.spy(EvmEventLogsReader, 'fromConfig');
503
+ const getLogsByTopic = sandbox
504
+ .stub(EvmEventLogsReader.prototype, 'getLogsByTopic')
505
+ .resolves([
506
+ configurationChangedLog({ bridge: BRIDGE_A, blockNumber: 10 }),
507
+ ]);
508
+ // Non-empty so the read settles on the first answer; what it asked for is
509
+ // what this pins.
510
+ stubXERC20(c.type, {
511
+ [BRIDGE_A.toLowerCase()]: c.type === XERC20Type.Velo
512
+ ? { bufferCap: '1', rateLimitPerSecond: '1' }
513
+ : { mint: '1', burn: '1' },
514
+ });
515
+ await getExtraLockBoxConfigs({
516
+ chain: TestChainName.test1,
517
+ xERC20Address: XERC20_ADDRESS,
518
+ multiProvider,
519
+ });
520
+ expect(fromConfig.calledOnce).to.be.true;
521
+ expect(fromConfig.firstCall.args[0]).to.deep.equal({
522
+ chain: TestChainName.test1,
523
+ paginationBlockRange: 1_000_000,
524
+ });
525
+ expect(getLogsByTopic.calledOnceWithExactly({
526
+ contractAddress: XERC20_ADDRESS,
527
+ eventTopic: c.expectedTopic,
528
+ fromBlock: DEPLOYMENT_BLOCK,
529
+ })).to.be.true;
530
+ });
531
+ }
532
+ // A route's token holds limits for at least the route's own router, so it has
533
+ // announced at least one bridge: an explorer answering with none has failed to
534
+ // answer, which is indistinguishable from a token that has none until the RPC
535
+ // is asked. Two of the six oUSDT chains report exactly this way.
536
+ it('re-reads over the RPC when the explorer announces no bridges', async () => {
402
537
  const fromConfig = sandbox.spy(EvmEventLogsReader, 'fromConfig');
403
- const getLogsByTopic = sandbox
404
- .stub(EvmEventLogsReader.prototype, 'getLogsByTopic')
405
- .resolves([]);
406
- stubLockboxCalls([]);
407
- await getExtraLockBoxConfigs({
538
+ const getLogsByTopic = sandbox.stub(EvmEventLogsReader.prototype, 'getLogsByTopic');
539
+ getLogsByTopic.onFirstCall().resolves([]);
540
+ getLogsByTopic
541
+ .onSecondCall()
542
+ .resolves([
543
+ configurationChangedLog({ bridge: BRIDGE_A, blockNumber: 10 }),
544
+ ]);
545
+ stubXERC20(XERC20Type.Velo, {
546
+ [BRIDGE_A.toLowerCase()]: { bufferCap: '100', rateLimitPerSecond: '1' },
547
+ });
548
+ const result = await getExtraLockBoxConfigs({
408
549
  chain: TestChainName.test1,
409
550
  xERC20Address: XERC20_ADDRESS,
410
551
  multiProvider,
411
552
  });
412
- expect(fromConfig.calledOnce).to.be.true;
413
- expect(fromConfig.firstCall.args[0]).to.deep.equal({
553
+ // The deployment block is resolved once and handed to both reads. Letting
554
+ // the RPC reader derive it is what fails on a chain that serves no archive
555
+ // state, so a second resolution here is the defect this guards.
556
+ expect(deploymentBlock.calledOnce).to.be.true;
557
+ const query = {
558
+ contractAddress: XERC20_ADDRESS,
559
+ eventTopic: '0xb4ff6a860e04455b1ce16833b74cde19765c95e55c5e7e4f5a69e9707d8cc96d',
560
+ fromBlock: DEPLOYMENT_BLOCK,
561
+ };
562
+ expect(getLogsByTopic.calledTwice).to.be.true;
563
+ expect(getLogsByTopic.firstCall.args[0]).to.deep.equal(query);
564
+ expect(getLogsByTopic.secondCall.args[0]).to.deep.equal(query);
565
+ expect(fromConfig.calledTwice).to.be.true;
566
+ expect(fromConfig.secondCall.args[0]).to.deep.equal({
414
567
  chain: TestChainName.test1,
415
568
  paginationBlockRange: 1_000_000,
569
+ useRPC: true,
416
570
  });
417
- expect(getLogsByTopic.calledOnceWithExactly({
418
- contractAddress: XERC20_ADDRESS,
419
- eventTopic: CONFIGURATION_CHANGED_EVENT_SELECTOR,
420
- })).to.be.true;
571
+ expect(result).to.deep.equal([
572
+ {
573
+ lockbox: BRIDGE_A.toLowerCase(),
574
+ limits: {
575
+ type: XERC20Type.Velo,
576
+ bufferCap: '100',
577
+ rateLimitPerSecond: '1',
578
+ },
579
+ },
580
+ ]);
581
+ });
582
+ // The RPC is the reader's own fallback when a chain declares no explorer, so
583
+ // re-reading there would scan the token's whole history a second time to
584
+ // reach the answer it already gave.
585
+ it('does not re-read when the chain declares no block explorer', async () => {
586
+ sandbox.stub(multiProvider, 'tryGetEvmExplorerMetadataList').returns([]);
587
+ const fromConfig = sandbox.spy(EvmEventLogsReader, 'fromConfig');
588
+ sandbox.stub(EvmEventLogsReader.prototype, 'getLogsByTopic').resolves([]);
589
+ stubXERC20(XERC20Type.Velo, {});
590
+ const result = await getExtraLockBoxConfigs({
591
+ chain: TestChainName.test1,
592
+ xERC20Address: XERC20_ADDRESS,
593
+ multiProvider,
594
+ });
595
+ expect(fromConfig.calledOnce).to.be.true;
596
+ expect(result).to.deep.equal([]);
597
+ });
598
+ it('propagates an RPC re-read failure', async () => {
599
+ const getLogsByTopic = sandbox.stub(EvmEventLogsReader.prototype, 'getLogsByTopic');
600
+ getLogsByTopic.onFirstCall().resolves([]);
601
+ getLogsByTopic.onSecondCall().rejects(new Error('rpc down'));
602
+ stubXERC20(XERC20Type.Velo, {});
603
+ await expect(getExtraLockBoxConfigs({
604
+ chain: TestChainName.test1,
605
+ xERC20Address: XERC20_ADDRESS,
606
+ multiProvider,
607
+ })).to.be.rejectedWith('rpc down');
421
608
  });
422
609
  // A failed log read used to be swallowed into an empty list, which reported
423
- // a token with extra lockboxes as having none.
424
- it('propagates a log read failure instead of reporting no lockboxes', async () => {
610
+ // a token with extra bridges as having none. The events are the only place
611
+ // the bridge addresses exist, so a failure to read them is the answer.
612
+ it('propagates a log read failure', async () => {
425
613
  sandbox
426
614
  .stub(EvmEventLogsReader.prototype, 'getLogsByTopic')
427
615
  .rejects(new Error('rpc down'));
428
- stubLockboxCalls([]);
616
+ stubXERC20(XERC20Type.Velo, {});
429
617
  await expect(getExtraLockBoxConfigs({
430
618
  chain: TestChainName.test1,
431
619
  xERC20Address: XERC20_ADDRESS,
@@ -433,4 +621,108 @@ describe('getExtraLockBoxConfigs', () => {
433
621
  })).to.be.rejectedWith('rpc down');
434
622
  });
435
623
  });
624
+ describe('latestConfigurationPerBridge', () => {
625
+ const orderingCases = [
626
+ {
627
+ name: 'keeps only the most recent configuration of a bridge',
628
+ logs: [
629
+ configurationChangedLog({
630
+ bridge: BRIDGE_A,
631
+ bufferCap: 100,
632
+ rateLimitPerSecond: 1,
633
+ blockNumber: 10,
634
+ }),
635
+ configurationChangedLog({
636
+ bridge: BRIDGE_A,
637
+ bufferCap: 500,
638
+ rateLimitPerSecond: 2,
639
+ blockNumber: 20,
640
+ }),
641
+ ],
642
+ expected: {
643
+ [BRIDGE_A.toLowerCase()]: { bufferCap: 500n, rateLimitPerSecond: 2n },
644
+ },
645
+ },
646
+ {
647
+ // A bridge can be reconfigured twice in the same block, so ordering on the
648
+ // block number alone kept the first configuration instead of the last.
649
+ name: 'keeps the highest logIndex configuration of a bridge within a block',
650
+ logs: [
651
+ configurationChangedLog({
652
+ bridge: BRIDGE_A,
653
+ bufferCap: 100,
654
+ rateLimitPerSecond: 1,
655
+ blockNumber: 10,
656
+ logIndex: 3,
657
+ }),
658
+ configurationChangedLog({
659
+ bridge: BRIDGE_A,
660
+ bufferCap: 500,
661
+ rateLimitPerSecond: 2,
662
+ blockNumber: 10,
663
+ logIndex: 7,
664
+ }),
665
+ ],
666
+ expected: {
667
+ [BRIDGE_A.toLowerCase()]: { bufferCap: 500n, rateLimitPerSecond: 2n },
668
+ },
669
+ },
670
+ {
671
+ // The same-block tiebreaker must not depend on the order the logs arrive in
672
+ name: 'keeps the highest logIndex configuration when logs arrive out of order',
673
+ logs: [
674
+ configurationChangedLog({
675
+ bridge: BRIDGE_A,
676
+ bufferCap: 500,
677
+ rateLimitPerSecond: 2,
678
+ blockNumber: 10,
679
+ logIndex: 7,
680
+ }),
681
+ configurationChangedLog({
682
+ bridge: BRIDGE_A,
683
+ bufferCap: 100,
684
+ rateLimitPerSecond: 1,
685
+ blockNumber: 10,
686
+ logIndex: 3,
687
+ }),
688
+ ],
689
+ expected: {
690
+ [BRIDGE_A.toLowerCase()]: { bufferCap: 500n, rateLimitPerSecond: 2n },
691
+ },
692
+ },
693
+ {
694
+ name: 'keeps every bridge that was configured',
695
+ logs: [
696
+ configurationChangedLog({
697
+ bridge: BRIDGE_A,
698
+ bufferCap: 100,
699
+ rateLimitPerSecond: 1,
700
+ blockNumber: 10,
701
+ }),
702
+ configurationChangedLog({
703
+ bridge: BRIDGE_B,
704
+ bufferCap: 200,
705
+ rateLimitPerSecond: 2,
706
+ blockNumber: 11,
707
+ }),
708
+ ],
709
+ expected: {
710
+ [BRIDGE_A.toLowerCase()]: { bufferCap: 100n, rateLimitPerSecond: 1n },
711
+ [BRIDGE_B.toLowerCase()]: { bufferCap: 200n, rateLimitPerSecond: 2n },
712
+ },
713
+ },
714
+ ];
715
+ for (const c of orderingCases) {
716
+ it(c.name, () => {
717
+ const latest = latestConfigurationPerBridge(c.logs.map(viemLogFromGetEventLogsResponse));
718
+ expect([...latest.keys()]).to.have.members(Object.keys(c.expected));
719
+ for (const [bridge, limits] of Object.entries(c.expected)) {
720
+ const log = latest.get(bridge);
721
+ assert(log, `Missing configuration for ${bridge}`);
722
+ expect(log.args.bufferCap).to.equal(limits.bufferCap);
723
+ expect(log.args.rateLimitPerSecond).to.equal(limits.rateLimitPerSecond);
724
+ }
725
+ });
726
+ }
727
+ });
436
728
  //# sourceMappingURL=xerc20.test.js.map