eco-solver 0.0.1-security → 1.5.1

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.

Potentially problematic release.


This version of eco-solver might be problematic. Click here for more details.

Files changed (244) hide show
  1. package/.eslintignore +8 -0
  2. package/.eslintrc.js +24 -0
  3. package/.github/workflows/ci.yaml +38 -0
  4. package/.nvmrc +1 -0
  5. package/.prettierignore +3 -0
  6. package/.prettierrc +8 -0
  7. package/Dockerfile +11 -0
  8. package/LICENSE +21 -0
  9. package/README.md +29 -5
  10. package/config/default.ts +135 -0
  11. package/config/development.ts +95 -0
  12. package/config/preproduction.ts +17 -0
  13. package/config/production.ts +17 -0
  14. package/config/staging.ts +17 -0
  15. package/config/test.ts +7 -0
  16. package/index.js +43 -0
  17. package/jest.config.ts +14 -0
  18. package/nest-cli.json +8 -0
  19. package/package.json +115 -6
  20. package/src/api/api.module.ts +27 -0
  21. package/src/api/balance.controller.ts +41 -0
  22. package/src/api/quote.controller.ts +54 -0
  23. package/src/api/tests/balance.controller.spec.ts +113 -0
  24. package/src/api/tests/quote.controller.spec.ts +83 -0
  25. package/src/app.module.ts +74 -0
  26. package/src/balance/balance.module.ts +14 -0
  27. package/src/balance/balance.service.ts +230 -0
  28. package/src/balance/balance.ws.service.ts +104 -0
  29. package/src/balance/types.ts +16 -0
  30. package/src/bullmq/bullmq.helper.ts +41 -0
  31. package/src/bullmq/processors/eth-ws.processor.ts +47 -0
  32. package/src/bullmq/processors/inbox.processor.ts +55 -0
  33. package/src/bullmq/processors/interval.processor.ts +54 -0
  34. package/src/bullmq/processors/processor.module.ts +14 -0
  35. package/src/bullmq/processors/signer.processor.ts +41 -0
  36. package/src/bullmq/processors/solve-intent.processor.ts +73 -0
  37. package/src/bullmq/processors/tests/solve-intent.processor.spec.ts +3 -0
  38. package/src/bullmq/utils/queue.ts +22 -0
  39. package/src/chain-monitor/chain-monitor.module.ts +12 -0
  40. package/src/chain-monitor/chain-sync.service.ts +134 -0
  41. package/src/chain-monitor/tests/chain-sync.service.spec.ts +190 -0
  42. package/src/commander/.eslintrc.js +6 -0
  43. package/src/commander/balance/balance-command.module.ts +12 -0
  44. package/src/commander/balance/balance.command.ts +73 -0
  45. package/src/commander/command-main.ts +15 -0
  46. package/src/commander/commander-app.module.ts +31 -0
  47. package/src/commander/eco-config.command.ts +20 -0
  48. package/src/commander/safe/safe-command.module.ts +11 -0
  49. package/src/commander/safe/safe.command.ts +70 -0
  50. package/src/commander/transfer/client.command.ts +24 -0
  51. package/src/commander/transfer/transfer-command.module.ts +26 -0
  52. package/src/commander/transfer/transfer.command.ts +138 -0
  53. package/src/commander/utils.ts +8 -0
  54. package/src/common/chains/definitions/arbitrum.ts +12 -0
  55. package/src/common/chains/definitions/base.ts +21 -0
  56. package/src/common/chains/definitions/eco.ts +54 -0
  57. package/src/common/chains/definitions/ethereum.ts +22 -0
  58. package/src/common/chains/definitions/helix.ts +53 -0
  59. package/src/common/chains/definitions/mantle.ts +12 -0
  60. package/src/common/chains/definitions/optimism.ts +22 -0
  61. package/src/common/chains/definitions/polygon.ts +12 -0
  62. package/src/common/chains/supported.ts +26 -0
  63. package/src/common/chains/transport.ts +19 -0
  64. package/src/common/errors/eco-error.ts +155 -0
  65. package/src/common/events/constants.ts +3 -0
  66. package/src/common/events/viem.ts +22 -0
  67. package/src/common/logging/eco-log-message.ts +74 -0
  68. package/src/common/redis/constants.ts +55 -0
  69. package/src/common/redis/redis-connection-utils.ts +106 -0
  70. package/src/common/routes/constants.ts +3 -0
  71. package/src/common/utils/objects.ts +34 -0
  72. package/src/common/utils/strings.ts +49 -0
  73. package/src/common/utils/tests/objects.spec.ts +23 -0
  74. package/src/common/utils/tests/strings.spec.ts +22 -0
  75. package/src/common/viem/contracts.ts +25 -0
  76. package/src/common/viem/tests/utils.spec.ts +115 -0
  77. package/src/common/viem/utils.ts +78 -0
  78. package/src/contracts/ERC20.contract.ts +389 -0
  79. package/src/contracts/EntryPoint.V6.contract.ts +1309 -0
  80. package/src/contracts/KernelAccount.abi.ts +87 -0
  81. package/src/contracts/OwnableExecutor.abi.ts +128 -0
  82. package/src/contracts/SimpleAccount.contract.ts +524 -0
  83. package/src/contracts/inbox.ts +8 -0
  84. package/src/contracts/index.ts +9 -0
  85. package/src/contracts/intent-source.ts +55 -0
  86. package/src/contracts/interfaces/index.ts +1 -0
  87. package/src/contracts/interfaces/prover.interface.ts +22 -0
  88. package/src/contracts/prover.ts +9 -0
  89. package/src/contracts/tests/erc20.contract.spec.ts +59 -0
  90. package/src/contracts/utils.ts +31 -0
  91. package/src/decoder/decoder.interface.ts +3 -0
  92. package/src/decoder/tests/utils.spec.ts +36 -0
  93. package/src/decoder/utils.ts +24 -0
  94. package/src/decorators/cacheable.decorator.ts +48 -0
  95. package/src/eco-configs/aws-config.service.ts +75 -0
  96. package/src/eco-configs/eco-config.module.ts +44 -0
  97. package/src/eco-configs/eco-config.service.ts +220 -0
  98. package/src/eco-configs/eco-config.types.ts +278 -0
  99. package/src/eco-configs/interfaces/config-source.interface.ts +3 -0
  100. package/src/eco-configs/tests/aws-config.service.spec.ts +52 -0
  101. package/src/eco-configs/tests/eco-config.service.spec.ts +137 -0
  102. package/src/eco-configs/tests/utils.spec.ts +84 -0
  103. package/src/eco-configs/utils.ts +49 -0
  104. package/src/fee/fee.module.ts +10 -0
  105. package/src/fee/fee.service.ts +467 -0
  106. package/src/fee/tests/fee.service.spec.ts +909 -0
  107. package/src/fee/tests/utils.spec.ts +49 -0
  108. package/src/fee/types.ts +44 -0
  109. package/src/fee/utils.ts +23 -0
  110. package/src/flags/flags.module.ts +10 -0
  111. package/src/flags/flags.service.ts +112 -0
  112. package/src/flags/tests/flags.service.spec.ts +68 -0
  113. package/src/flags/utils.ts +22 -0
  114. package/src/health/constants.ts +1 -0
  115. package/src/health/health.controller.ts +23 -0
  116. package/src/health/health.module.ts +25 -0
  117. package/src/health/health.service.ts +40 -0
  118. package/src/health/indicators/balance.indicator.ts +196 -0
  119. package/src/health/indicators/eco-redis.indicator.ts +23 -0
  120. package/src/health/indicators/git-commit.indicator.ts +67 -0
  121. package/src/health/indicators/mongodb.indicator.ts +11 -0
  122. package/src/health/indicators/permission.indicator.ts +64 -0
  123. package/src/intent/create-intent.service.ts +129 -0
  124. package/src/intent/feasable-intent.service.ts +80 -0
  125. package/src/intent/fulfill-intent.service.ts +318 -0
  126. package/src/intent/intent.controller.ts +199 -0
  127. package/src/intent/intent.module.ts +49 -0
  128. package/src/intent/schemas/intent-call-data.schema.ts +16 -0
  129. package/src/intent/schemas/intent-data.schema.ts +114 -0
  130. package/src/intent/schemas/intent-source.schema.ts +33 -0
  131. package/src/intent/schemas/intent-token-amount.schema.ts +14 -0
  132. package/src/intent/schemas/reward-data.schema.ts +48 -0
  133. package/src/intent/schemas/route-data.schema.ts +52 -0
  134. package/src/intent/schemas/watch-event.schema.ts +32 -0
  135. package/src/intent/tests/create-intent.service.spec.ts +215 -0
  136. package/src/intent/tests/feasable-intent.service.spec.ts +155 -0
  137. package/src/intent/tests/fulfill-intent.service.spec.ts +564 -0
  138. package/src/intent/tests/utils-intent.service.spec.ts +308 -0
  139. package/src/intent/tests/utils.spec.ts +62 -0
  140. package/src/intent/tests/validate-intent.service.spec.ts +297 -0
  141. package/src/intent/tests/validation.service.spec.ts +337 -0
  142. package/src/intent/utils-intent.service.ts +168 -0
  143. package/src/intent/utils.ts +37 -0
  144. package/src/intent/validate-intent.service.ts +176 -0
  145. package/src/intent/validation.sevice.ts +223 -0
  146. package/src/interceptors/big-int.interceptor.ts +30 -0
  147. package/src/intervals/interval.module.ts +18 -0
  148. package/src/intervals/retry-infeasable-intents.service.ts +89 -0
  149. package/src/intervals/tests/retry-infeasable-intents.service.spec.ts +167 -0
  150. package/src/kms/errors.ts +0 -0
  151. package/src/kms/kms.module.ts +12 -0
  152. package/src/kms/kms.service.ts +65 -0
  153. package/src/kms/tests/kms.service.spec.ts +60 -0
  154. package/src/liquidity-manager/jobs/check-balances-cron.job.ts +229 -0
  155. package/src/liquidity-manager/jobs/liquidity-manager.job.ts +52 -0
  156. package/src/liquidity-manager/jobs/rebalance.job.ts +61 -0
  157. package/src/liquidity-manager/liquidity-manager.module.ts +29 -0
  158. package/src/liquidity-manager/processors/base.processor.ts +117 -0
  159. package/src/liquidity-manager/processors/eco-protocol-intents.processor.ts +34 -0
  160. package/src/liquidity-manager/processors/grouped-jobs.processor.ts +103 -0
  161. package/src/liquidity-manager/queues/liquidity-manager.queue.ts +48 -0
  162. package/src/liquidity-manager/schemas/rebalance-token.schema.ts +32 -0
  163. package/src/liquidity-manager/schemas/rebalance.schema.ts +32 -0
  164. package/src/liquidity-manager/services/liquidity-manager.service.ts +188 -0
  165. package/src/liquidity-manager/services/liquidity-provider.service.ts +25 -0
  166. package/src/liquidity-manager/services/liquidity-providers/LiFi/lifi-provider.service.spec.ts +125 -0
  167. package/src/liquidity-manager/services/liquidity-providers/LiFi/lifi-provider.service.ts +117 -0
  168. package/src/liquidity-manager/services/liquidity-providers/LiFi/utils/get-transaction-hashes.ts +16 -0
  169. package/src/liquidity-manager/tests/liquidity-manager.service.spec.ts +142 -0
  170. package/src/liquidity-manager/types/token-state.enum.ts +5 -0
  171. package/src/liquidity-manager/types/types.d.ts +52 -0
  172. package/src/liquidity-manager/utils/address.ts +5 -0
  173. package/src/liquidity-manager/utils/math.ts +9 -0
  174. package/src/liquidity-manager/utils/serialize.spec.ts +24 -0
  175. package/src/liquidity-manager/utils/serialize.ts +47 -0
  176. package/src/liquidity-manager/utils/token.ts +91 -0
  177. package/src/main.ts +63 -0
  178. package/src/nest-redlock/nest-redlock.config.ts +14 -0
  179. package/src/nest-redlock/nest-redlock.interface.ts +5 -0
  180. package/src/nest-redlock/nest-redlock.module.ts +64 -0
  181. package/src/nest-redlock/nest-redlock.service.ts +59 -0
  182. package/src/prover/proof.service.ts +184 -0
  183. package/src/prover/prover.module.ts +10 -0
  184. package/src/prover/tests/proof.service.spec.ts +154 -0
  185. package/src/quote/dto/quote.intent.data.dto.ts +35 -0
  186. package/src/quote/dto/quote.reward.data.dto.ts +67 -0
  187. package/src/quote/dto/quote.route.data.dto.ts +71 -0
  188. package/src/quote/dto/types.ts +18 -0
  189. package/src/quote/errors.ts +215 -0
  190. package/src/quote/quote.module.ts +17 -0
  191. package/src/quote/quote.service.ts +299 -0
  192. package/src/quote/schemas/quote-call.schema.ts +16 -0
  193. package/src/quote/schemas/quote-intent.schema.ts +27 -0
  194. package/src/quote/schemas/quote-reward.schema.ts +24 -0
  195. package/src/quote/schemas/quote-route.schema.ts +30 -0
  196. package/src/quote/schemas/quote-token.schema.ts +14 -0
  197. package/src/quote/tests/quote.service.spec.ts +444 -0
  198. package/src/sign/atomic-signer.service.ts +24 -0
  199. package/src/sign/atomic.nonce.service.ts +114 -0
  200. package/src/sign/kms-account/kmsToAccount.ts +73 -0
  201. package/src/sign/kms-account/signKms.ts +30 -0
  202. package/src/sign/kms-account/signKmsTransaction.ts +37 -0
  203. package/src/sign/kms-account/signKmsTypedData.ts +21 -0
  204. package/src/sign/nonce.service.ts +89 -0
  205. package/src/sign/schemas/nonce.schema.ts +36 -0
  206. package/src/sign/sign.controller.ts +52 -0
  207. package/src/sign/sign.helper.ts +23 -0
  208. package/src/sign/sign.module.ts +27 -0
  209. package/src/sign/signer-kms.service.ts +27 -0
  210. package/src/sign/signer.service.ts +26 -0
  211. package/src/solver/filters/tests/valid-smart-wallet.service.spec.ts +87 -0
  212. package/src/solver/filters/valid-smart-wallet.service.ts +58 -0
  213. package/src/solver/solver.module.ts +10 -0
  214. package/src/transaction/multichain-public-client.service.ts +15 -0
  215. package/src/transaction/smart-wallets/kernel/actions/encodeData.kernel.ts +57 -0
  216. package/src/transaction/smart-wallets/kernel/create-kernel-client-v2.account.ts +183 -0
  217. package/src/transaction/smart-wallets/kernel/create.kernel.account.ts +270 -0
  218. package/src/transaction/smart-wallets/kernel/index.ts +2 -0
  219. package/src/transaction/smart-wallets/kernel/kernel-account-client-v2.service.ts +90 -0
  220. package/src/transaction/smart-wallets/kernel/kernel-account-client.service.ts +107 -0
  221. package/src/transaction/smart-wallets/kernel/kernel-account.client.ts +105 -0
  222. package/src/transaction/smart-wallets/kernel/kernel-account.config.ts +34 -0
  223. package/src/transaction/smart-wallets/simple-account/create.simple.account.ts +19 -0
  224. package/src/transaction/smart-wallets/simple-account/index.ts +2 -0
  225. package/src/transaction/smart-wallets/simple-account/simple-account-client.service.ts +42 -0
  226. package/src/transaction/smart-wallets/simple-account/simple-account.client.ts +83 -0
  227. package/src/transaction/smart-wallets/simple-account/simple-account.config.ts +5 -0
  228. package/src/transaction/smart-wallets/smart-wallet.types.ts +38 -0
  229. package/src/transaction/smart-wallets/utils.ts +14 -0
  230. package/src/transaction/transaction.module.ts +25 -0
  231. package/src/transaction/viem_multichain_client.service.ts +100 -0
  232. package/src/transforms/viem-address.decorator.ts +14 -0
  233. package/src/utils/bigint.ts +44 -0
  234. package/src/utils/types.ts +18 -0
  235. package/src/watch/intent/tests/watch-create-intent.service.spec.ts +257 -0
  236. package/src/watch/intent/tests/watch-fulfillment.service.spec.ts +141 -0
  237. package/src/watch/intent/watch-create-intent.service.ts +106 -0
  238. package/src/watch/intent/watch-event.service.ts +133 -0
  239. package/src/watch/intent/watch-fulfillment.service.ts +115 -0
  240. package/src/watch/watch.module.ts +13 -0
  241. package/test/app.e2e-spec.ts +21 -0
  242. package/test/jest-e2e.json +9 -0
  243. package/tsconfig.build.json +4 -0
  244. package/tsconfig.json +29 -0
@@ -0,0 +1,909 @@
1
+ const mockGetTransactionTargetData = jest.fn()
2
+ const mockIsERC20Target = jest.fn()
3
+ import { BalanceService, TokenFetchAnalysis } from '@/balance/balance.service'
4
+ import { getERC20Selector } from '@/contracts'
5
+ import { EcoConfigService } from '@/eco-configs/eco-config.service'
6
+ import { FeeConfigType } from '@/eco-configs/eco-config.types'
7
+ import { BASE_DECIMALS, FeeService } from '@/fee/fee.service'
8
+ import { NormalizedToken } from '@/fee/types'
9
+ import { QuoteError } from '@/quote/errors'
10
+ import { createMock, DeepMocked } from '@golevelup/ts-jest'
11
+ import { Test, TestingModule } from '@nestjs/testing'
12
+ import { Hex } from 'viem'
13
+ import * as _ from 'lodash'
14
+
15
+ jest.mock('@/intent/utils', () => {
16
+ return {
17
+ ...jest.requireActual('@/intent/utils'),
18
+ getTransactionTargetData: mockGetTransactionTargetData,
19
+ }
20
+ })
21
+
22
+ jest.mock('@/contracts', () => {
23
+ return {
24
+ ...jest.requireActual('@/contracts'),
25
+ isERC20Target: mockIsERC20Target,
26
+ }
27
+ })
28
+
29
+ describe('FeeService', () => {
30
+ let feeService: FeeService
31
+ let balanceService: DeepMocked<BalanceService>
32
+ let ecoConfigService: DeepMocked<EcoConfigService>
33
+ const mockLogDebug = jest.fn()
34
+ const mockLogLog = jest.fn()
35
+ const mockLogError = jest.fn()
36
+ beforeEach(async () => {
37
+ const chainMod: TestingModule = await Test.createTestingModule({
38
+ providers: [
39
+ FeeService,
40
+ { provide: BalanceService, useValue: createMock<BalanceService>() },
41
+ { provide: EcoConfigService, useValue: createMock<EcoConfigService>() },
42
+ ],
43
+ }).compile()
44
+
45
+ feeService = chainMod.get(FeeService)
46
+
47
+ balanceService = chainMod.get(BalanceService)
48
+ ecoConfigService = chainMod.get(EcoConfigService)
49
+
50
+ feeService['logger'].debug = mockLogDebug
51
+ feeService['logger'].log = mockLogLog
52
+ feeService['logger'].error = mockLogError
53
+ })
54
+
55
+ afterEach(async () => {
56
+ // restore the spy created with spyOn
57
+ jest.restoreAllMocks()
58
+ mockLogDebug.mockClear()
59
+ mockLogLog.mockClear()
60
+ mockLogError.mockClear()
61
+ })
62
+
63
+ const defaultFee: FeeConfigType = {
64
+ limitFillBase6: 1000n * 10n ** 6n,
65
+ algorithm: 'linear',
66
+ constants: {
67
+ baseFee: 20_000n,
68
+ tranche: {
69
+ unitFee: 15_000n,
70
+ unitSize: 100_000_000n,
71
+ },
72
+ },
73
+ }
74
+
75
+ const linearSolver = {
76
+ fee: defaultFee,
77
+ } as any
78
+
79
+ describe('on onModuleInit', () => {
80
+ it('should set the config defaults', async () => {
81
+ const whitelist = { '0x1': { '10': { limitFillBase6: 123n } } }
82
+ expect(feeService['defaultFee']).toBeUndefined()
83
+ expect(feeService['whitelist']).toBeUndefined()
84
+ const mockGetIntentConfig = jest.spyOn(ecoConfigService, 'getIntentConfigs').mockReturnValue({
85
+ defaultFee,
86
+ } as any)
87
+ const mockGetWhitelist = jest
88
+ .spyOn(ecoConfigService, 'getWhitelist')
89
+ .mockReturnValue(whitelist as any)
90
+ await feeService.onModuleInit()
91
+ expect(feeService['defaultFee']).toEqual(defaultFee)
92
+ expect(feeService['whitelist']).toEqual(whitelist)
93
+ expect(mockGetIntentConfig).toHaveBeenCalledTimes(1)
94
+ expect(mockGetWhitelist).toHaveBeenCalledTimes(1)
95
+ })
96
+ })
97
+
98
+ describe('on getFeeConfig', () => {
99
+ const creator = '0x1'
100
+ const source = 10
101
+ const intent = {
102
+ reward: {
103
+ creator,
104
+ },
105
+ route: {
106
+ source,
107
+ },
108
+ } as any
109
+
110
+ beforeEach(() => {
111
+ feeService['defaultFee'] = defaultFee
112
+ feeService['getAskRouteDestinationSolver'] = jest.fn().mockReturnValue({ fee: defaultFee })
113
+ })
114
+
115
+ it('should return the default fee if no intent in arguments', async () => {
116
+ expect(feeService.getFeeConfig()).toEqual(defaultFee)
117
+ })
118
+
119
+ it('should set the default fee if its passed in as argument', async () => {
120
+ const argFee = { limitFillBase6: 123n } as any
121
+ expect(feeService.getFeeConfig({ defaultFeeArg: argFee })).toEqual(argFee)
122
+ })
123
+
124
+ it('should return the default fee for the solver if intent is set', async () => {
125
+ const solverFee = { asd: 123n } as any
126
+ feeService['whitelist'] = {}
127
+ feeService['defaultFee'] = { asd: 333n } as any
128
+ feeService['getAskRouteDestinationSolver'] = jest.fn().mockReturnValue({ fee: solverFee })
129
+ expect(feeService.getFeeConfig({ intent })).toEqual(solverFee)
130
+ })
131
+
132
+ it('should return the default fee if no special fee for creator', async () => {
133
+ feeService['whitelist'] = {}
134
+ expect(feeService.getFeeConfig({ intent })).toEqual(defaultFee)
135
+ })
136
+
137
+ it('should return the default fee if creator special fee is empty', async () => {
138
+ feeService['whitelist'] = { [creator]: {} }
139
+ expect(feeService.getFeeConfig({ intent })).toEqual(defaultFee)
140
+ })
141
+
142
+ it('should return the source chain creator default fee, merged, if no chain specific one and its not complete', async () => {
143
+ const creatorDefault = { limitFillBase6: 123n } as any
144
+ feeService['whitelist'] = { [creator]: { default: creatorDefault } }
145
+ expect(feeService.getFeeConfig({ intent })).toEqual(_.merge({}, defaultFee, creatorDefault))
146
+ })
147
+
148
+ it('should return the source chain creator default fee without merge if its complete', async () => {
149
+ const creatorDefault = {
150
+ limitFillBase6: 1n,
151
+ algorithm: 'linear',
152
+ constants: {
153
+ baseFee: 2n,
154
+ tranche: {
155
+ unitFee: 3n,
156
+ unitSize: 4n,
157
+ },
158
+ },
159
+ } as any
160
+ feeService['whitelist'] = { [creator]: { default: creatorDefault } }
161
+ expect(feeService.getFeeConfig({ intent })).toEqual(creatorDefault)
162
+ })
163
+
164
+ it('should return the source chain specific fee for a creator', async () => {
165
+ const chainConfig = { constants: { tranche: { unitFee: 9911n } } } as any
166
+ const creatorDefault = { limitFillBase6: 123n } as any
167
+ feeService['whitelist'] = { [creator]: { [source]: chainConfig, default: creatorDefault } }
168
+ expect(feeService.getFeeConfig({ intent })).toEqual(
169
+ _.merge({}, defaultFee, creatorDefault, chainConfig),
170
+ )
171
+ })
172
+ })
173
+
174
+ describe('on getAsk', () => {
175
+ const route = {
176
+ destination: 8452n,
177
+ source: 10n,
178
+ } as any
179
+
180
+ const reward = {
181
+ reward: {},
182
+ } as any
183
+
184
+ const intent = {
185
+ route,
186
+ reward,
187
+ } as any
188
+
189
+ describe('on invalid solver', () => {
190
+ it('should throw if no solver found', async () => {
191
+ const getSolver = jest.spyOn(ecoConfigService, 'getSolver').mockReturnValue(undefined)
192
+ expect(() => feeService.getAsk(1_000_000n, intent)).toThrow(
193
+ QuoteError.NoSolverForDestination(route.destination),
194
+ )
195
+ expect(getSolver).toHaveBeenCalledTimes(1)
196
+ })
197
+
198
+ it('should throw when solver doesnt have a supported algorithm', async () => {
199
+ const solver = { fee: { algorithm: 'unsupported' } } as any
200
+ const getSolver = jest.spyOn(ecoConfigService, 'getSolver').mockReturnValue(solver)
201
+ jest.spyOn(feeService, 'getFeeConfig').mockReturnValue(solver.fee)
202
+ expect(() => feeService.getAsk(1_000_000n, intent)).toThrow(
203
+ QuoteError.InvalidSolverAlgorithm(route.destination, solver.fee.algorithm),
204
+ )
205
+ expect(getSolver).toHaveBeenCalledTimes(1)
206
+ })
207
+ })
208
+
209
+ describe('on linear fee algorithm', () => {
210
+ let solverSpy: jest.SpyInstance
211
+ let feeSpy: jest.SpyInstance
212
+ beforeEach(() => {
213
+ solverSpy = jest.spyOn(ecoConfigService, 'getSolver').mockReturnValue(linearSolver)
214
+ feeSpy = jest.spyOn(feeService, 'getFeeConfig').mockReturnValue(linearSolver.fee)
215
+ })
216
+
217
+ it('should return the correct ask for less than $100', async () => {
218
+ const ask = feeService.getAsk(1_000_000n, intent)
219
+ expect(ask).toBe(1_020_000n)
220
+ })
221
+
222
+ it('should return the correct ask for multiples of $100', async () => {
223
+ expect(feeService.getAsk(99_000_000n, intent)).toBe(99_020_000n)
224
+ expect(feeService.getAsk(100_000_000n, intent)).toBe(100_035_000n)
225
+ expect(feeService.getAsk(999_000_000n, intent)).toBe(999_155_000n)
226
+ expect(feeService.getAsk(1_000_000_000n, intent)).toBe(1000_170_000n)
227
+ })
228
+ })
229
+ })
230
+
231
+ describe('on isRouteFeasible', () => {
232
+ let quote: any
233
+ const ask = 11n
234
+ const totalRewardsNormalized = 10n
235
+ const totalFillNormalized = 10n
236
+ const error = { error: 'error' } as any
237
+ beforeEach(() => {
238
+ quote = {
239
+ route: {
240
+ calls: [{}],
241
+ },
242
+ }
243
+ })
244
+ it('should return an error if route has more than 1 call', async () => {
245
+ quote.route.calls.push({})
246
+ expect(await feeService.isRouteFeasible(quote)).toEqual({
247
+ error: QuoteError.MultiFulfillRoute(),
248
+ })
249
+ })
250
+
251
+ it('should return an error if getTotalFill fails', async () => {
252
+ const getTotallFill = jest.spyOn(feeService, 'getTotalFill').mockResolvedValue(error)
253
+ expect(await feeService.isRouteFeasible(quote)).toEqual(error)
254
+ expect(getTotallFill).toHaveBeenCalledTimes(1)
255
+ })
256
+
257
+ it('should return an error if getTotalRewards fails', async () => {
258
+ jest
259
+ .spyOn(feeService, 'getTotalFill')
260
+ .mockResolvedValue({ totalFillNormalized: 10n, error: undefined })
261
+ const getTotalRewards = jest.spyOn(feeService, 'getTotalRewards').mockResolvedValue(error)
262
+ expect(await feeService.isRouteFeasible(quote)).toEqual(error)
263
+ expect(getTotalRewards).toHaveBeenCalledTimes(1)
264
+ })
265
+
266
+ it('should return an error if the ask is less than the total reward', async () => {
267
+ jest
268
+ .spyOn(feeService, 'getTotalFill')
269
+ .mockResolvedValue({ totalFillNormalized, error: undefined })
270
+ jest
271
+ .spyOn(feeService, 'getTotalRewards')
272
+ .mockResolvedValue({ totalRewardsNormalized, error: undefined })
273
+ const getAsk = jest.spyOn(feeService, 'getAsk').mockReturnValue(11n)
274
+ expect(await feeService.isRouteFeasible(quote)).toEqual({
275
+ error: QuoteError.RouteIsInfeasable(ask, totalRewardsNormalized),
276
+ })
277
+ expect(getAsk).toHaveBeenCalledTimes(1)
278
+ })
279
+
280
+ it('should return an undefined error if the route is feasible', async () => {
281
+ jest
282
+ .spyOn(feeService, 'getTotalFill')
283
+ .mockResolvedValue({ totalFillNormalized, error: undefined })
284
+ jest.spyOn(feeService, 'getTotalRewards').mockResolvedValue({
285
+ totalRewardsNormalized: totalRewardsNormalized + 2n,
286
+ error: undefined,
287
+ })
288
+ jest.spyOn(feeService, 'getAsk').mockReturnValue(ask)
289
+ expect(await feeService.isRouteFeasible(quote)).toEqual({ error: undefined })
290
+ })
291
+ })
292
+
293
+ describe('on getTotalFill', () => {
294
+ it('should return an error upstream from getCallsNormalized', async () => {
295
+ const error = { error: 'error' }
296
+ const getCallsNormalized = jest
297
+ .spyOn(feeService, 'getCallsNormalized')
298
+ .mockResolvedValue(error as any)
299
+ expect(await feeService.getTotalFill([] as any)).toEqual({
300
+ totalFillNormalized: 0n,
301
+ ...error,
302
+ })
303
+ expect(getCallsNormalized).toHaveBeenCalledTimes(1)
304
+ })
305
+
306
+ it('should reduce and return the total rewards', async () => {
307
+ const getCallsNormalized = jest.spyOn(feeService, 'getCallsNormalized').mockResolvedValue({
308
+ calls: [{ balance: 10n }, { balance: 20n }] as any,
309
+ error: undefined,
310
+ }) as any
311
+ expect(await feeService.getTotalFill([] as any)).toEqual({ totalFillNormalized: 30n })
312
+ expect(getCallsNormalized).toHaveBeenCalledTimes(1)
313
+ })
314
+ })
315
+
316
+ describe('on getTotalRewards', () => {
317
+ it('should return an error upstream from getRewardsNormalized', async () => {
318
+ const error = { error: 'error' }
319
+ const getRewardsNormalized = jest
320
+ .spyOn(feeService, 'getRewardsNormalized')
321
+ .mockResolvedValue(error as any)
322
+ expect(await feeService.getTotalRewards([] as any)).toEqual({
323
+ totalRewardsNormalized: 0n,
324
+ ...error,
325
+ })
326
+ expect(getRewardsNormalized).toHaveBeenCalledTimes(1)
327
+ })
328
+
329
+ it('should reduce and return the total rewards', async () => {
330
+ const getRewardsNormalized = jest
331
+ .spyOn(feeService, 'getRewardsNormalized')
332
+ .mockResolvedValue({
333
+ rewards: [{ balance: 10n }, { balance: 20n }] as any,
334
+ })
335
+ expect(await feeService.getTotalRewards([] as any)).toEqual({ totalRewardsNormalized: 30n })
336
+ expect(getRewardsNormalized).toHaveBeenCalledTimes(1)
337
+ })
338
+ })
339
+
340
+ describe('on calculateTokens', () => {
341
+ const quote = {
342
+ route: {
343
+ source: 10n,
344
+ destination: 11n,
345
+ rewards: [
346
+ {
347
+ address: '0x4Fd9098af9ddcB41DA48A1d78F91F1398965addc' as Hex,
348
+ decimals: 8,
349
+ balance: 100_000_000n,
350
+ },
351
+ {
352
+ address: '0x9D6AC51b972544251Fcc0F2902e633E3f9BD3f29' as Hex,
353
+ decimals: 4,
354
+ balance: 1_000n,
355
+ },
356
+ ],
357
+ },
358
+ reward: {
359
+ tokens: [
360
+ { token: '0x4Fd9098af9ddcB41DA48A1d78F91F1398965addc', amount: 10_000_000_000n },
361
+ { token: '0x9D6AC51b972544251Fcc0F2902e633E3f9BD3f29', amount: 1_000n },
362
+ ],
363
+ },
364
+ } as any
365
+ const source = {
366
+ chainID: 10n,
367
+ tokens: [
368
+ '0x4Fd9098af9ddcB41DA48A1d78F91F1398965addc',
369
+ '0x9D6AC51b972544251Fcc0F2902e633E3f9BD3f29',
370
+ '0x1',
371
+ '0x2',
372
+ '0x3',
373
+ ],
374
+ } as any
375
+
376
+ const tokenAnalysis = [
377
+ {
378
+ token: {
379
+ address: '0x1',
380
+ },
381
+ },
382
+ {
383
+ token: {
384
+ address: '0x2',
385
+ },
386
+ },
387
+ {
388
+ token: {
389
+ address: '0x3',
390
+ },
391
+ },
392
+ ] as any
393
+ it('should return error if source is not found', async () => {
394
+ jest.spyOn(ecoConfigService, 'getIntentSources').mockReturnValue([] as any)
395
+ const r = await feeService.calculateTokens(quote as any)
396
+ expect(r).toEqual({ error: QuoteError.NoIntentSourceForSource(quote.route.source) })
397
+ expect(mockLogError).toHaveBeenCalled()
398
+ expect(mockLogError).toHaveBeenCalledWith(
399
+ expect.objectContaining({
400
+ msg: QuoteError.NoIntentSourceForSource(quote.route.source).message,
401
+ error: QuoteError.NoIntentSourceForSource(quote.route.source),
402
+ source: undefined,
403
+ }),
404
+ )
405
+ })
406
+
407
+ it('should return error if solver is not found', async () => {
408
+ jest.spyOn(ecoConfigService, 'getSolver').mockReturnValue(undefined)
409
+ const r = await feeService.calculateTokens(quote as any)
410
+ expect(r).toEqual({ error: QuoteError.NoSolverForDestination(quote.route.destination) })
411
+ expect(mockLogError).toHaveBeenCalled()
412
+ expect(mockLogError).toHaveBeenCalledWith(
413
+ expect.objectContaining({
414
+ msg: QuoteError.NoSolverForDestination(quote.route.destination).message,
415
+ error: QuoteError.NoSolverForDestination(quote.route.destination),
416
+ solver: undefined,
417
+ }),
418
+ )
419
+ })
420
+
421
+ it('should return error if fetching token data fails', async () => {
422
+ jest.spyOn(ecoConfigService, 'getIntentSources').mockReturnValue([source])
423
+ jest.spyOn(ecoConfigService, 'getSolver').mockReturnValue(linearSolver)
424
+ jest.spyOn(balanceService, 'fetchTokenData').mockResolvedValue(undefined as any)
425
+ await expect(feeService.calculateTokens(quote as any)).rejects.toThrow(
426
+ QuoteError.FetchingCallTokensFailed(quote.route.source),
427
+ )
428
+ })
429
+
430
+ it('should return error if getRewardsNormalized fails', async () => {
431
+ const error = { error: 'error' }
432
+ jest.spyOn(ecoConfigService, 'getIntentSources').mockReturnValue([source])
433
+ jest.spyOn(ecoConfigService, 'getSolver').mockReturnValue(linearSolver)
434
+ jest.spyOn(balanceService, 'fetchTokenData').mockResolvedValue(tokenAnalysis)
435
+ jest.spyOn(feeService, 'calculateDelta').mockReturnValue(10n as any)
436
+ const rew = jest.spyOn(feeService, 'getRewardsNormalized').mockReturnValue({ error } as any)
437
+ const call = jest
438
+ .spyOn(feeService, 'getCallsNormalized')
439
+ .mockReturnValue({ calls: {} } as any)
440
+ expect(await feeService.calculateTokens(quote as any)).toEqual({ error })
441
+ expect(rew).toHaveBeenCalledTimes(1)
442
+ expect(call).toHaveBeenCalledTimes(1)
443
+ })
444
+
445
+ it('should return error if getCallsNormalized fails', async () => {
446
+ const error = { error: 'error' }
447
+ jest.spyOn(ecoConfigService, 'getIntentSources').mockReturnValue([source])
448
+ jest.spyOn(ecoConfigService, 'getSolver').mockReturnValue(linearSolver)
449
+ jest.spyOn(balanceService, 'fetchTokenData').mockResolvedValue(tokenAnalysis)
450
+ jest.spyOn(feeService, 'calculateDelta').mockReturnValue(10n as any)
451
+ const rew = jest
452
+ .spyOn(feeService, 'getRewardsNormalized')
453
+ .mockReturnValue({ rewards: {} } as any)
454
+ const call = jest.spyOn(feeService, 'getCallsNormalized').mockReturnValue({ error } as any)
455
+ expect(await feeService.calculateTokens(quote as any)).toEqual({ error })
456
+ expect(rew).toHaveBeenCalledTimes(1)
457
+ expect(call).toHaveBeenCalledTimes(1)
458
+ })
459
+
460
+ it('should calculate the delta for all tokens', async () => {
461
+ jest.spyOn(ecoConfigService, 'getIntentSources').mockReturnValue([source])
462
+ jest.spyOn(ecoConfigService, 'getSolver').mockReturnValue(linearSolver)
463
+ jest.spyOn(balanceService, 'fetchTokenData').mockResolvedValue(tokenAnalysis)
464
+ const cal = jest.spyOn(feeService, 'calculateDelta').mockImplementation((token) => {
465
+ return BigInt(token.token.address) as any
466
+ })
467
+ const rewards = { stuff: 'asdf' } as any
468
+ const rew = jest.spyOn(feeService, 'getRewardsNormalized').mockReturnValue({ rewards } as any)
469
+ const calls = { stuff: '123' } as any
470
+ const call = jest.spyOn(feeService, 'getCallsNormalized').mockReturnValue({ calls } as any)
471
+ const deficitDescending = tokenAnalysis.map((ta) => {
472
+ return { ...ta, delta: BigInt(ta.token.address) }
473
+ })
474
+ expect(await feeService.calculateTokens(quote as any)).toEqual({
475
+ calculated: {
476
+ solver: linearSolver,
477
+ rewards,
478
+ calls,
479
+ deficitDescending,
480
+ },
481
+ })
482
+ expect(cal).toHaveBeenCalledTimes(tokenAnalysis.length)
483
+ expect(rew).toHaveBeenCalledTimes(1)
484
+ expect(call).toHaveBeenCalledTimes(1)
485
+ })
486
+ })
487
+
488
+ describe('on getRewardsNormalized', () => {
489
+ const quote = {
490
+ route: {
491
+ source: 10n,
492
+ destination: 11n,
493
+ rewards: [
494
+ {
495
+ address: '0x4Fd9098af9ddcB41DA48A1d78F91F1398965addc' as Hex,
496
+ decimals: 8,
497
+ balance: 100_000_000n,
498
+ },
499
+ {
500
+ address: '0x9D6AC51b972544251Fcc0F2902e633E3f9BD3f29' as Hex,
501
+ decimals: 4,
502
+ balance: 1_000n,
503
+ },
504
+ ],
505
+ },
506
+ reward: {
507
+ tokens: [
508
+ { token: '0x4Fd9098af9ddcB41DA48A1d78F91F1398965addc', amount: 10_000_000_000n },
509
+ { token: '0x9D6AC51b972544251Fcc0F2902e633E3f9BD3f29', amount: 1_000n },
510
+ ],
511
+ },
512
+ } as any
513
+
514
+ const erc20Rewards = {
515
+ '0x4Fd9098af9ddcB41DA48A1d78F91F1398965addc': {
516
+ address: '0x4Fd9098af9ddcB41DA48A1d78F91F1398965addc',
517
+ decimals: 8,
518
+ balance: 10_000_000_000n,
519
+ },
520
+ '0x9D6AC51b972544251Fcc0F2902e633E3f9BD3f29': {
521
+ address: '0x9D6AC51b972544251Fcc0F2902e633E3f9BD3f29',
522
+ decimals: 4,
523
+ balance: 1_000n,
524
+ },
525
+ } as any
526
+
527
+ it('should return error if not intent source', async () => {
528
+ jest.spyOn(ecoConfigService, 'getIntentSources').mockReturnValue([])
529
+ expect(await feeService.getRewardsNormalized(quote as any)).toEqual({
530
+ rewards: [],
531
+ error: QuoteError.NoIntentSourceForSource(quote.route.source),
532
+ })
533
+ })
534
+
535
+ it('should return an error if the balances call fails', async () => {
536
+ const mockBalance = jest.spyOn(balanceService, 'fetchTokenBalances').mockResolvedValue({})
537
+ expect(await feeService.getRewardsNormalized(quote as any)).toEqual({
538
+ rewards: [],
539
+ error: QuoteError.FetchingRewardTokensFailed(BigInt(quote.route.source)),
540
+ })
541
+ expect(mockBalance).toHaveBeenCalledTimes(1)
542
+ expect(mockBalance).toHaveBeenCalledWith(
543
+ Number(quote.route.source),
544
+ quote.reward.tokens.map((reward) => reward.token),
545
+ )
546
+ })
547
+
548
+ it('should map rewards and convertNormalize the output', async () => {
549
+ jest.spyOn(balanceService, 'fetchTokenBalances').mockResolvedValue(erc20Rewards)
550
+ const convert = jest.spyOn(feeService, 'convertNormalize')
551
+ expect(await feeService.getRewardsNormalized(quote as any)).toEqual({
552
+ rewards: [
553
+ {
554
+ chainID: quote.route.source,
555
+ address: '0x4Fd9098af9ddcB41DA48A1d78F91F1398965addc',
556
+ decimals: 6,
557
+ balance: 100_000_000n,
558
+ },
559
+ {
560
+ chainID: quote.route.source,
561
+ address: '0x9D6AC51b972544251Fcc0F2902e633E3f9BD3f29',
562
+ decimals: 6,
563
+ balance: 100_000n,
564
+ },
565
+ ],
566
+ })
567
+ expect(convert).toHaveBeenCalledTimes(2)
568
+ })
569
+ })
570
+
571
+ describe('on getCallsNormalized', () => {
572
+ const quote = {
573
+ route: {
574
+ destination: 1n,
575
+ calls: [
576
+ { target: '0x1' as Hex, selector: '0x2' as Hex, data: '0x3' as Hex },
577
+ { target: '0x4' as Hex, selector: '0x5' as Hex, data: '0x6' as Hex },
578
+ ],
579
+ },
580
+ }
581
+
582
+ const solver = {
583
+ chainID: 1n,
584
+ } as any
585
+
586
+ it('should return an error if a solver cant be found', async () => {
587
+ const mockSolver = jest.spyOn(ecoConfigService, 'getSolver').mockReturnValue(undefined)
588
+ expect(await feeService.getCallsNormalized(quote as any)).toEqual({
589
+ calls: [],
590
+ error: QuoteError.NoSolverForDestination(quote.route.destination),
591
+ })
592
+ expect(mockSolver).toHaveBeenCalledTimes(1)
593
+ })
594
+
595
+ it('should return an error if the balances call fails', async () => {
596
+ jest.spyOn(ecoConfigService, 'getSolver').mockReturnValue(solver)
597
+ const mockBalance = jest.spyOn(balanceService, 'fetchTokenBalances').mockResolvedValue({})
598
+ expect(await feeService.getCallsNormalized(quote as any)).toEqual({
599
+ calls: [],
600
+ error: QuoteError.FetchingCallTokensFailed(BigInt(solver.chainID)),
601
+ })
602
+ expect(mockBalance).toHaveBeenCalledTimes(1)
603
+ expect(mockBalance).toHaveBeenCalledWith(solver.chainID, expect.any(Array))
604
+ })
605
+
606
+ describe('on route calls mapping', () => {
607
+ let callBalances: any
608
+ const transferAmount = 1000_000_000n
609
+ const txTargetData = {
610
+ targetConfig: {
611
+ contractType: 'erc20',
612
+ },
613
+ decodedFunctionData: {
614
+ args: [0, transferAmount],
615
+ },
616
+ } as any
617
+ let solverWithTargets: any = {
618
+ chainID: 1n,
619
+ targets: {
620
+ '0x1': {
621
+ type: 'erc20',
622
+ minBalance: 200,
623
+ targetBalance: 222,
624
+ },
625
+ '0x4': {
626
+ type: 'erc20',
627
+ minBalance: 300,
628
+ targetBalance: 111,
629
+ },
630
+ },
631
+ }
632
+ let tokenAnalysis: any
633
+ beforeEach(() => {
634
+ callBalances = {
635
+ '0x1': {
636
+ address: '0x1',
637
+ decimals: 6,
638
+ balance: transferAmount,
639
+ },
640
+ '0x4': {
641
+ address: '0x4',
642
+ decimals: 4,
643
+ balance: transferAmount,
644
+ },
645
+ } as any
646
+ tokenAnalysis = {
647
+ '0x1': {
648
+ chainId: 1n,
649
+ token: callBalances['0x1'],
650
+ config: {
651
+ address: '0x1',
652
+ chainId: 1n,
653
+ ...solverWithTargets.targets['0x1'],
654
+ },
655
+ },
656
+ '0x4': {
657
+ chainId: 1n,
658
+ token: callBalances['0x4'],
659
+ config: {
660
+ address: '0x4',
661
+ chainId: 1n,
662
+ ...solverWithTargets.targets['0x4'],
663
+ },
664
+ },
665
+ }
666
+ })
667
+
668
+ it('should return an error if tx target data is not for an erc20 transfer', async () => {
669
+ jest.spyOn(ecoConfigService, 'getSolver').mockReturnValue(solverWithTargets)
670
+ jest.spyOn(balanceService, 'fetchTokenBalances').mockResolvedValue(callBalances)
671
+ mockGetTransactionTargetData.mockReturnValue(null)
672
+ mockIsERC20Target.mockReturnValue(false)
673
+ expect(await feeService.getCallsNormalized(quote as any)).toEqual({
674
+ calls: [],
675
+ error: QuoteError.NonERC20TargetInCalls(),
676
+ })
677
+ expect(mockGetTransactionTargetData).toHaveBeenCalledTimes(1)
678
+ expect(mockLogError).toHaveBeenCalledTimes(1)
679
+ expect(mockLogError).toHaveBeenCalledWith({
680
+ msg: QuoteError.NonERC20TargetInCalls().message,
681
+ call: quote.route.calls[0],
682
+ error: QuoteError.NonERC20TargetInCalls(),
683
+ ttd: null,
684
+ })
685
+ expect(mockIsERC20Target).toHaveBeenCalledTimes(1)
686
+ expect(mockIsERC20Target).toHaveBeenCalledWith(null, getERC20Selector('transfer'))
687
+ })
688
+
689
+ it('should return an error if the call target is not in the fetched balances', async () => {
690
+ jest.spyOn(ecoConfigService, 'getSolver').mockReturnValue(solverWithTargets)
691
+ jest
692
+ .spyOn(balanceService, 'fetchTokenBalances')
693
+ .mockResolvedValue({ '0x4': callBalances['0x4'] })
694
+ mockGetTransactionTargetData.mockReturnValue(txTargetData)
695
+ mockIsERC20Target.mockReturnValue(true)
696
+ expect(await feeService.getCallsNormalized(quote as any)).toEqual({
697
+ calls: [],
698
+ error: QuoteError.FailedToFetchTarget(
699
+ solverWithTargets.chainID,
700
+ quote.route.calls[0].target,
701
+ ),
702
+ })
703
+ })
704
+
705
+ it('should return an error if solver lacks liquidity in a call token', async () => {
706
+ const normMinBalance = feeService.getNormalizedMinBalance(tokenAnalysis['0x1'])
707
+ callBalances['0x1'].balance = transferAmount + normMinBalance - 1n
708
+ jest.spyOn(ecoConfigService, 'getSolver').mockReturnValue(solverWithTargets)
709
+ jest.spyOn(balanceService, 'fetchTokenBalances').mockResolvedValue(callBalances)
710
+ mockGetTransactionTargetData.mockReturnValue(txTargetData)
711
+ mockIsERC20Target.mockReturnValue(true)
712
+ const convert = jest.spyOn(feeService, 'convertNormalize')
713
+ const error = QuoteError.SolverLacksLiquidity(
714
+ solver.chainID,
715
+ quote.route.calls[0].target,
716
+ transferAmount,
717
+ callBalances['0x1'].balance,
718
+ normMinBalance,
719
+ )
720
+ expect(await feeService.getCallsNormalized(quote as any)).toEqual({
721
+ calls: [],
722
+ error,
723
+ })
724
+ expect(convert).toHaveBeenCalledTimes(0)
725
+ expect(mockLogError).toHaveBeenCalledTimes(1)
726
+ expect(mockLogError).toHaveBeenCalledWith({
727
+ msg: QuoteError.SolverLacksLiquidity.name,
728
+ error,
729
+ quote,
730
+ callTarget: tokenAnalysis['0x1'],
731
+ })
732
+ })
733
+
734
+ it('should convert an normalize the erc20 calls', async () => {
735
+ const normMinBalance1 = feeService.getNormalizedMinBalance(tokenAnalysis['0x1'])
736
+ const normMinBalance4 = feeService.getNormalizedMinBalance(tokenAnalysis['0x4'])
737
+ jest.spyOn(ecoConfigService, 'getSolver').mockReturnValue(solverWithTargets)
738
+ callBalances['0x1'].balance = transferAmount + normMinBalance1 + 1n
739
+ callBalances['0x4'].balance = transferAmount + normMinBalance4 + 1n
740
+ jest.spyOn(balanceService, 'fetchTokenBalances').mockResolvedValue(callBalances)
741
+ mockGetTransactionTargetData.mockReturnValue(txTargetData)
742
+ mockIsERC20Target.mockReturnValue(true)
743
+ const convert = jest.spyOn(feeService, 'convertNormalize')
744
+ expect(await feeService.getCallsNormalized(quote as any)).toEqual({
745
+ calls: [
746
+ {
747
+ balance: transferAmount,
748
+ chainID: solver.chainID,
749
+ address: '0x1',
750
+ decimals: BASE_DECIMALS,
751
+ },
752
+ {
753
+ balance: transferAmount * 10n ** 2n,
754
+ chainID: solver.chainID,
755
+ address: '0x4',
756
+ decimals: BASE_DECIMALS,
757
+ },
758
+ ],
759
+ })
760
+ expect(convert).toHaveBeenCalledTimes(2)
761
+ })
762
+ })
763
+ })
764
+
765
+ describe('on calculateDelta', () => {
766
+ let token: TokenFetchAnalysis
767
+ beforeEach(() => {
768
+ token = {
769
+ config: {
770
+ address: '0x1',
771
+ chainId: 10,
772
+ minBalance: 200,
773
+ targetBalance: 500,
774
+ type: 'erc20',
775
+ },
776
+ token: {
777
+ address: '0x1',
778
+ decimals: BASE_DECIMALS,
779
+ balance: 300_000_000n,
780
+ },
781
+ chainId: 10,
782
+ }
783
+ })
784
+ it('should calculate the delta for surplus', async () => {
785
+ const convertNormalizeSpy = jest.spyOn(feeService, 'convertNormalize')
786
+ const normToken = feeService.calculateDelta(token)
787
+ const expectedNorm: NormalizedToken = {
788
+ balance: 100_000_000n,
789
+ chainID: BigInt(token.chainId),
790
+ address: token.config.address,
791
+ decimals: token.token.decimals,
792
+ }
793
+ expect(normToken).toEqual(expectedNorm)
794
+
795
+ expect(convertNormalizeSpy).toHaveBeenCalledTimes(1)
796
+ expect(convertNormalizeSpy).toHaveBeenCalledWith(100_000_000n, expect.any(Object))
797
+ })
798
+
799
+ it('should calculate the delta for deficit', async () => {
800
+ const convertNormalizeSpy = jest.spyOn(feeService, 'convertNormalize')
801
+ token.token.balance = 100_000_000n
802
+ const normToken = feeService.calculateDelta(token)
803
+ const expectedNorm: NormalizedToken = {
804
+ balance: -100_000_000n,
805
+ chainID: BigInt(token.chainId),
806
+ address: token.config.address,
807
+ decimals: token.token.decimals,
808
+ }
809
+ expect(normToken).toEqual(expectedNorm)
810
+
811
+ expect(convertNormalizeSpy).toHaveBeenCalledTimes(1)
812
+ expect(convertNormalizeSpy).toHaveBeenCalledWith(-100_000_000n, expect.any(Object))
813
+ })
814
+
815
+ it('should call correct normalization', async () => {
816
+ token.token.decimals = 4
817
+ token.token.balance = token.token.balance / 10n ** 2n
818
+ const convertNormalizeSpy = jest.spyOn(feeService, 'convertNormalize')
819
+ const normToken = feeService.calculateDelta(token)
820
+ const expectedNorm: NormalizedToken = {
821
+ balance: 100_000_000n, //300 - 200 = 100 base 6 decimals
822
+ chainID: BigInt(token.chainId),
823
+ address: token.config.address,
824
+ decimals: BASE_DECIMALS,
825
+ }
826
+ expect(normToken).toEqual(expectedNorm)
827
+
828
+ expect(convertNormalizeSpy).toHaveBeenCalledTimes(1)
829
+ expect(convertNormalizeSpy).toHaveBeenCalledWith(1_000_000n, expect.any(Object)) // 100 base 4
830
+ })
831
+ })
832
+
833
+ describe('on convertNormalize', () => {
834
+ it('should normalize the output', async () => {
835
+ const orig = { chainID: 1n, address: '0x' as Hex, decimals: 6 }
836
+ expect(feeService.convertNormalize(100n, orig)).toEqual({ balance: 100n, ...orig })
837
+ const second = { chainID: 1n, address: '0x' as Hex, decimals: 4 }
838
+ expect(feeService.convertNormalize(100n, second)).toEqual({
839
+ balance: 10000n,
840
+ ...second,
841
+ decimals: 6,
842
+ })
843
+ })
844
+
845
+ it('should change the decimals to the normalized value', async () => {
846
+ const second = { chainID: 1n, address: '0x' as Hex, decimals: 4 }
847
+ expect(feeService.convertNormalize(100n, second)).toEqual({
848
+ balance: 10000n,
849
+ ...second,
850
+ decimals: 6,
851
+ })
852
+ })
853
+ })
854
+
855
+ describe('on deconvertNormalize', () => {
856
+ it('should denormalize the output', async () => {
857
+ const orig = { chainID: 1n, address: '0x' as Hex, decimals: 6 }
858
+ expect(feeService.deconvertNormalize(100n, orig)).toEqual({ balance: 100n, ...orig })
859
+ const second = { chainID: 1n, address: '0x' as Hex, decimals: 4 }
860
+ expect(feeService.deconvertNormalize(100n, second)).toEqual({ balance: 1n, ...second })
861
+ })
862
+ })
863
+
864
+ describe('on getAskRouteDestinationSolver', () => {
865
+ let solverSpy: jest.SpyInstance
866
+ beforeEach(() => {
867
+ solverSpy = jest.spyOn(ecoConfigService, 'getSolver').mockReturnValue(linearSolver)
868
+ })
869
+
870
+ it('should return the destination route solver', async () => {
871
+ let route = {
872
+ destination: 10n,
873
+ source: 20n,
874
+ } as any
875
+ feeService.getAskRouteDestinationSolver(route)
876
+ expect(solverSpy).toHaveBeenCalledWith(route.destination)
877
+ })
878
+
879
+ it('should return the eth solver if its one of the networks in the route', async () => {
880
+ let route = {
881
+ destination: 1n,
882
+ source: 2n,
883
+ } as any
884
+ feeService.getAskRouteDestinationSolver(route)
885
+ expect(solverSpy).toHaveBeenCalledWith(1n)
886
+
887
+ route = {
888
+ destination: 2n,
889
+ source: 1n,
890
+ } as any
891
+ feeService.getAskRouteDestinationSolver(route)
892
+ expect(solverSpy).toHaveBeenCalledWith(1n)
893
+
894
+ route = {
895
+ destination: 11155111n,
896
+ source: 2n,
897
+ } as any
898
+ feeService.getAskRouteDestinationSolver(route)
899
+ expect(solverSpy).toHaveBeenCalledWith(11155111n)
900
+
901
+ route = {
902
+ destination: 2n,
903
+ source: 11155111n,
904
+ } as any
905
+ feeService.getAskRouteDestinationSolver(route)
906
+ expect(solverSpy).toHaveBeenCalledWith(11155111n)
907
+ })
908
+ })
909
+ })