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,257 @@
1
+ import { QUEUES } from '@/common/redis/constants'
2
+ import { EcoConfigService } from '@/eco-configs/eco-config.service'
3
+ import { WatchCreateIntentService } from '@/watch/intent/watch-create-intent.service'
4
+ import { createMock, DeepMocked } from '@golevelup/ts-jest'
5
+ import { BullModule, getQueueToken } from '@nestjs/bullmq'
6
+ import { Test, TestingModule } from '@nestjs/testing'
7
+ import { Job, Queue } from 'bullmq'
8
+ import { EcoError } from '@/common/errors/eco-error'
9
+ import { MultichainPublicClientService } from '@/transaction/multichain-public-client.service'
10
+ import { serialize } from '@/liquidity-manager/utils/serialize'
11
+ import { IntentCreatedLog } from '@/contracts'
12
+
13
+ describe('WatchIntentService', () => {
14
+ let watchIntentService: WatchCreateIntentService
15
+ let publicClientService: DeepMocked<MultichainPublicClientService>
16
+ let ecoConfigService: DeepMocked<EcoConfigService>
17
+ let queue: DeepMocked<Queue>
18
+ const mockLogDebug = jest.fn()
19
+ const mockLogLog = jest.fn()
20
+ const mockLogError = jest.fn()
21
+
22
+ const sources = [
23
+ { chainID: 1n, sourceAddress: '0x1234', provers: ['0x88'], network: 'testnet1' },
24
+ { chainID: 2n, sourceAddress: '0x5678', provers: ['0x88', '0x99'], network: 'testnet2' },
25
+ ] as any
26
+ const supportedChains = sources.map((s) => BigInt(s.chainID))
27
+
28
+ beforeEach(async () => {
29
+ const chainMod: TestingModule = await Test.createTestingModule({
30
+ providers: [
31
+ WatchCreateIntentService,
32
+ {
33
+ provide: MultichainPublicClientService,
34
+ useValue: createMock<MultichainPublicClientService>(),
35
+ },
36
+ { provide: EcoConfigService, useValue: createMock<EcoConfigService>() },
37
+ ],
38
+ imports: [
39
+ BullModule.registerQueue({
40
+ name: QUEUES.SOURCE_INTENT.queue,
41
+ }),
42
+ ],
43
+ })
44
+ .overrideProvider(getQueueToken(QUEUES.SOURCE_INTENT.queue))
45
+ .useValue(createMock<Queue>())
46
+ .compile()
47
+
48
+ watchIntentService = chainMod.get(WatchCreateIntentService)
49
+ publicClientService = chainMod.get(MultichainPublicClientService)
50
+ ecoConfigService = chainMod.get(EcoConfigService)
51
+ queue = chainMod.get(getQueueToken(QUEUES.SOURCE_INTENT.queue))
52
+
53
+ watchIntentService['logger'].debug = mockLogDebug
54
+ watchIntentService['logger'].log = mockLogLog
55
+ watchIntentService['logger'].error = mockLogError
56
+ })
57
+
58
+ afterEach(async () => {
59
+ // restore the spy created with spyOn
60
+ jest.restoreAllMocks()
61
+ mockLogDebug.mockClear()
62
+ mockLogLog.mockClear()
63
+ })
64
+
65
+ describe('on lifecycle', () => {
66
+ describe('on startup', () => {
67
+ it('should subscribe to nothing if no source intents', async () => {
68
+ const mock = jest.spyOn(watchIntentService, 'subscribe')
69
+ await watchIntentService.onApplicationBootstrap()
70
+ expect(mock).toHaveBeenCalledTimes(1)
71
+ })
72
+
73
+ it('should subscribe to all source intents', async () => {
74
+ const mockWatch = jest.fn()
75
+ publicClientService.getClient.mockResolvedValue({
76
+ watchContractEvent: mockWatch,
77
+ } as any)
78
+ ecoConfigService.getIntentSources.mockReturnValue(sources)
79
+ ecoConfigService.getSolvers.mockReturnValue(sources)
80
+ await watchIntentService.onApplicationBootstrap()
81
+ expect(mockWatch).toHaveBeenCalledTimes(2)
82
+
83
+ for (const [index, s] of sources.entries()) {
84
+ const { address, eventName, args } = mockWatch.mock.calls[index][0]
85
+ const partial = { address, eventName, args }
86
+ expect(partial).toEqual({
87
+ address: s.sourceAddress,
88
+ eventName: 'IntentCreated',
89
+ args: { prover: s.provers },
90
+ })
91
+ }
92
+ })
93
+ })
94
+
95
+ describe('on destroy', () => {
96
+ it('should unsubscribe to nothing if no source intents', async () => {
97
+ const mock = jest.spyOn(watchIntentService, 'unsubscribe')
98
+ await watchIntentService.onModuleDestroy()
99
+ expect(mock).toHaveBeenCalledTimes(1)
100
+ })
101
+
102
+ it('should unsubscribe to all source intents', async () => {
103
+ const mockUnwatch = jest.fn()
104
+ publicClientService.getClient.mockResolvedValue({
105
+ watchContractEvent: () => mockUnwatch,
106
+ } as any)
107
+ ecoConfigService.getIntentSources.mockReturnValue(sources)
108
+ ecoConfigService.getSolvers.mockReturnValue(sources)
109
+ await watchIntentService.onApplicationBootstrap()
110
+ await watchIntentService.onModuleDestroy()
111
+ expect(mockUnwatch).toHaveBeenCalledTimes(2)
112
+ })
113
+ })
114
+ })
115
+
116
+ describe('on intent', () => {
117
+ const s = sources[0]
118
+ const log: any = { logIndex: 2, args: { hash: '0x1' } as Partial<IntentCreatedLog['args']> }
119
+ let mockQueueAdd: jest.SpyInstance<Promise<Job<any, any, string>>>
120
+
121
+ beforeEach(async () => {
122
+ mockQueueAdd = jest.spyOn(queue, 'add')
123
+ await watchIntentService.addJob(s)([log])
124
+ expect(mockLogDebug).toHaveBeenCalledTimes(1)
125
+ })
126
+ it('should convert all bigints to strings', async () => {
127
+ expect(mockLogDebug.mock.calls[0][0].createIntent).toEqual(
128
+ expect.objectContaining(serialize(log)),
129
+ )
130
+ })
131
+
132
+ it('should should attach source chainID and network', async () => {
133
+ expect(mockLogDebug.mock.calls[0][0].createIntent).toEqual(
134
+ expect.objectContaining(
135
+ serialize({
136
+ sourceChainID: s.chainID,
137
+ sourceNetwork: s.network,
138
+ }),
139
+ ),
140
+ )
141
+ })
142
+
143
+ it('should should enque a job for every intent', async () => {
144
+ expect(mockQueueAdd).toHaveBeenCalledTimes(1)
145
+ expect(mockQueueAdd).toHaveBeenCalledWith(
146
+ QUEUES.SOURCE_INTENT.jobs.create_intent,
147
+ expect.any(Object),
148
+ { jobId: 'watch-create-intent-0x1-2' },
149
+ )
150
+ })
151
+ })
152
+
153
+ describe('on unsubscribe', () => {
154
+ let mockUnwatch1: jest.Mock = jest.fn()
155
+ let mockUnwatch2: jest.Mock = jest.fn()
156
+ beforeEach(async () => {
157
+ mockUnwatch1 = jest.fn()
158
+ mockUnwatch2 = jest.fn()
159
+ watchIntentService['unwatch'] = {
160
+ 1: mockUnwatch1,
161
+ 2: mockUnwatch2,
162
+ }
163
+ })
164
+
165
+ afterEach(async () => {
166
+ jest.clearAllMocks()
167
+ })
168
+
169
+ it('should unsubscribe to every unwatch and catch any throws', async () => {
170
+ const e = new Error('test')
171
+ mockUnwatch1.mockImplementation(() => {
172
+ throw e
173
+ })
174
+ await watchIntentService.unsubscribe()
175
+ expect(mockUnwatch1).toHaveBeenCalledTimes(1)
176
+ expect(mockUnwatch2).toHaveBeenCalledTimes(1)
177
+ expect(mockLogError).toHaveBeenCalledTimes(1)
178
+ expect(mockLogError).toHaveBeenCalledWith({
179
+ msg: 'watch-event: unsubscribe',
180
+ error: EcoError.WatchEventUnsubscribeError.toString(),
181
+ errorPassed: e,
182
+ })
183
+ })
184
+
185
+ it('should unsubscribe to every unwatch', async () => {
186
+ await watchIntentService.unsubscribe()
187
+ expect(mockUnwatch1).toHaveBeenCalledTimes(1)
188
+ expect(mockUnwatch2).toHaveBeenCalledTimes(1)
189
+ expect(mockLogError).toHaveBeenCalledTimes(0)
190
+ expect(mockLogDebug).toHaveBeenCalledTimes(1)
191
+ expect(mockLogDebug).toHaveBeenCalledWith({
192
+ msg: 'watch-event: unsubscribe',
193
+ })
194
+ })
195
+ })
196
+
197
+ describe('on unsubscribeFrom', () => {
198
+ let mockUnwatch1: jest.Mock = jest.fn()
199
+ const chainID = 1
200
+ beforeEach(async () => {
201
+ mockUnwatch1 = jest.fn()
202
+ watchIntentService['unwatch'] = {
203
+ [chainID]: mockUnwatch1,
204
+ }
205
+ })
206
+
207
+ afterEach(async () => {
208
+ jest.clearAllMocks()
209
+ })
210
+
211
+ describe('on unwatch exists', () => {
212
+ it('should unsubscribe to unwatch', async () => {
213
+ await watchIntentService.unsubscribeFrom(chainID)
214
+ expect(mockUnwatch1).toHaveBeenCalledTimes(1)
215
+ expect(mockLogDebug).toHaveBeenCalledTimes(1)
216
+ expect(mockLogError).toHaveBeenCalledTimes(0)
217
+ expect(mockLogDebug).toHaveBeenCalledWith({
218
+ msg: 'watch-event: unsubscribeFrom',
219
+ chainID,
220
+ })
221
+ })
222
+
223
+ it('should unsubscribe to unwatch and catch throw', async () => {
224
+ const e = new Error('test')
225
+ mockUnwatch1.mockImplementation(() => {
226
+ throw e
227
+ })
228
+ await watchIntentService.unsubscribeFrom(chainID)
229
+ expect(mockUnwatch1).toHaveBeenCalledTimes(1)
230
+ expect(mockLogError).toHaveBeenCalledTimes(1)
231
+ expect(mockLogError).toHaveBeenCalledWith({
232
+ msg: 'watch-event: unsubscribeFrom',
233
+ error: EcoError.WatchEventUnsubscribeFromError(chainID).toString(),
234
+ errorPassed: e,
235
+ chainID,
236
+ })
237
+ })
238
+ })
239
+
240
+ describe('on unwatch doesnt exist', () => {
241
+ beforeEach(async () => {
242
+ watchIntentService['unwatch'] = {}
243
+ })
244
+
245
+ it('should log error', async () => {
246
+ await watchIntentService.unsubscribeFrom(chainID)
247
+ expect(mockUnwatch1).toHaveBeenCalledTimes(0)
248
+ expect(mockLogError).toHaveBeenCalledTimes(1)
249
+ expect(mockLogError).toHaveBeenCalledWith({
250
+ msg: 'watch event: unsubscribeFrom',
251
+ error: EcoError.WatchEventNoUnsubscribeError(chainID).toString(),
252
+ chainID,
253
+ })
254
+ })
255
+ })
256
+ })
257
+ })
@@ -0,0 +1,141 @@
1
+ import { QUEUES } from '@/common/redis/constants'
2
+ import { EcoConfigService } from '@/eco-configs/eco-config.service'
3
+ import { MultichainPublicClientService } from '@/transaction/multichain-public-client.service'
4
+ import { WatchFulfillmentService } from '@/watch/intent/watch-fulfillment.service'
5
+ import { createMock, DeepMocked } from '@golevelup/ts-jest'
6
+ import { BullModule, getQueueToken } from '@nestjs/bullmq'
7
+ import { Test, TestingModule } from '@nestjs/testing'
8
+ import { Job, Queue } from 'bullmq'
9
+
10
+ describe('WatchFulfillmentService', () => {
11
+ let watchFulfillmentService: WatchFulfillmentService
12
+ let publicClientService: DeepMocked<MultichainPublicClientService>
13
+ let ecoConfigService: DeepMocked<EcoConfigService>
14
+ let queue: DeepMocked<Queue>
15
+ const mockLogDebug = jest.fn()
16
+ const mockLogLog = jest.fn()
17
+
18
+ const inboxes = [
19
+ { chainID: 1, inboxAddress: '0x1234' },
20
+ { chainID: 2, inboxAddress: '0x5678' },
21
+ ] as any
22
+ const inboxRecord = inboxes.reduce((acc, solver) => {
23
+ acc[solver.chainID] = solver
24
+ return acc
25
+ }, {})
26
+ const supportedChains = inboxes.map((s) => BigInt(s.chainID))
27
+
28
+ beforeEach(async () => {
29
+ const chainMod: TestingModule = await Test.createTestingModule({
30
+ providers: [
31
+ WatchFulfillmentService,
32
+ {
33
+ provide: MultichainPublicClientService,
34
+ useValue: createMock<MultichainPublicClientService>(),
35
+ },
36
+ { provide: EcoConfigService, useValue: createMock<EcoConfigService>() },
37
+ ],
38
+ imports: [
39
+ BullModule.registerQueue({
40
+ name: QUEUES.INBOX.queue,
41
+ }),
42
+ ],
43
+ })
44
+ .overrideProvider(getQueueToken(QUEUES.INBOX.queue))
45
+ .useValue(createMock<Queue>())
46
+ .compile()
47
+
48
+ watchFulfillmentService = chainMod.get(WatchFulfillmentService)
49
+ publicClientService = chainMod.get(MultichainPublicClientService)
50
+ ecoConfigService = chainMod.get(EcoConfigService)
51
+ queue = chainMod.get(getQueueToken(QUEUES.INBOX.queue))
52
+
53
+ watchFulfillmentService['logger'].debug = mockLogDebug
54
+ watchFulfillmentService['logger'].log = mockLogLog
55
+ })
56
+
57
+ afterEach(async () => {
58
+ // restore the spy created with spyOn
59
+ jest.restoreAllMocks()
60
+ mockLogDebug.mockClear()
61
+ mockLogLog.mockClear()
62
+ })
63
+
64
+ describe('on lifecycle', () => {
65
+ describe('on startup', () => {
66
+ it('should subscribe to nothing if no solvers', async () => {
67
+ const mock = jest.spyOn(watchFulfillmentService, 'subscribe')
68
+ await watchFulfillmentService.onApplicationBootstrap()
69
+ expect(mock).toHaveBeenCalledTimes(1)
70
+ })
71
+
72
+ it('should subscribe to all solvers', async () => {
73
+ const mockWatch = jest.fn()
74
+ publicClientService.getClient.mockResolvedValue({
75
+ watchContractEvent: mockWatch,
76
+ } as any)
77
+ ecoConfigService.getSolvers.mockReturnValue(inboxRecord)
78
+ watchFulfillmentService.getSupportedChains = jest.fn().mockReturnValue(supportedChains)
79
+ await watchFulfillmentService.onApplicationBootstrap()
80
+ expect(mockWatch).toHaveBeenCalledTimes(2)
81
+
82
+ for (const [index, s] of inboxes.entries()) {
83
+ const { address, eventName, args } = mockWatch.mock.calls[index][0]
84
+ const partial = { address, eventName, args }
85
+ expect(partial).toEqual({
86
+ address: s.inboxAddress,
87
+ eventName: 'Fulfillment',
88
+ args: { _sourceChainID: supportedChains },
89
+ })
90
+ }
91
+ })
92
+ })
93
+
94
+ describe('on destroy', () => {
95
+ it('should unsubscribe to nothing if no solvers', async () => {
96
+ const mock = jest.spyOn(watchFulfillmentService, 'unsubscribe')
97
+ await watchFulfillmentService.onModuleDestroy()
98
+ expect(mock).toHaveBeenCalledTimes(1)
99
+ })
100
+
101
+ it('should unsubscribe to all solvers', async () => {
102
+ const mockUnwatch = jest.fn()
103
+ publicClientService.getClient.mockResolvedValue({
104
+ watchContractEvent: () => mockUnwatch,
105
+ } as any)
106
+ ecoConfigService.getSolvers.mockReturnValue(inboxRecord)
107
+ await watchFulfillmentService.onApplicationBootstrap()
108
+ await watchFulfillmentService.onModuleDestroy()
109
+ expect(mockUnwatch).toHaveBeenCalledTimes(2)
110
+ })
111
+ })
112
+
113
+ describe('on fulfillment', () => {
114
+ const log = { args: { _hash: BigInt(1), logIndex: BigInt(2) } } as any
115
+ let mockQueueAdd: jest.SpyInstance<Promise<Job<any, any, string>>>
116
+
117
+ beforeEach(async () => {
118
+ mockQueueAdd = jest.spyOn(queue, 'add')
119
+ await watchFulfillmentService.addJob()([log])
120
+ expect(mockLogDebug).toHaveBeenCalledTimes(1)
121
+ })
122
+
123
+ it('should convert all bigints to strings', async () => {
124
+ expect(mockLogDebug.mock.calls[0][0].fulfillment).toEqual(
125
+ expect.objectContaining({
126
+ args: { _hash: log.args._hash.toString(), logIndex: log.args.logIndex.toString() },
127
+ }),
128
+ )
129
+ })
130
+
131
+ it('should should enque a job for every intent', async () => {
132
+ expect(mockQueueAdd).toHaveBeenCalledTimes(1)
133
+ expect(mockQueueAdd).toHaveBeenCalledWith(
134
+ QUEUES.INBOX.jobs.fulfillment,
135
+ expect.any(Object),
136
+ { jobId: 'watch-fulfillement-1-0' },
137
+ )
138
+ })
139
+ })
140
+ })
141
+ })
@@ -0,0 +1,106 @@
1
+ import { Injectable, Logger } from '@nestjs/common'
2
+ import { EcoConfigService } from '@/eco-configs/eco-config.service'
3
+ import { Queue } from 'bullmq'
4
+ import { QUEUES } from '@/common/redis/constants'
5
+ import { InjectQueue } from '@nestjs/bullmq'
6
+ import { getIntentJobId } from '@/common/utils/strings'
7
+ import { IntentSource } from '@/eco-configs/eco-config.types'
8
+ import { EcoLogMessage } from '@/common/logging/eco-log-message'
9
+ import { MultichainPublicClientService } from '@/transaction/multichain-public-client.service'
10
+ import { IntentCreatedLog } from '@/contracts'
11
+ import { PublicClient } from 'viem'
12
+ import { IntentSourceAbi } from '@eco-foundation/routes-ts'
13
+ import { WatchEventService } from '@/watch/intent/watch-event.service'
14
+ import * as BigIntSerializer from '@/liquidity-manager/utils/serialize'
15
+
16
+ /**
17
+ * This service subscribes to IntentSource contracts for IntentCreated events. It subscribes on all
18
+ * supported chains and prover addresses. When an event is emitted, it mutates the event log, and then
19
+ * adds it intent queue for processing.
20
+ */
21
+ @Injectable()
22
+ export class WatchCreateIntentService extends WatchEventService<IntentSource> {
23
+ protected logger = new Logger(WatchCreateIntentService.name)
24
+
25
+ constructor(
26
+ @InjectQueue(QUEUES.SOURCE_INTENT.queue) protected readonly intentQueue: Queue,
27
+ protected readonly publicClientService: MultichainPublicClientService,
28
+ protected readonly ecoConfigService: EcoConfigService,
29
+ ) {
30
+ super(intentQueue, publicClientService, ecoConfigService)
31
+ }
32
+
33
+ /**
34
+ * Subscribes to all IntentSource contracts for IntentCreated events. It subscribes on all supported chains
35
+ * filtering on the prover addresses and destination chain ids. It loads a mapping of the unsubscribe events to
36
+ * call {@link onModuleDestroy} to close the clients.
37
+ */
38
+ async subscribe(): Promise<void> {
39
+ const subscribeTasks = this.ecoConfigService.getIntentSources().map(async (source) => {
40
+ const client = await this.publicClientService.getClient(source.chainID)
41
+ await this.subscribeTo(client, source)
42
+ })
43
+
44
+ await Promise.all(subscribeTasks)
45
+ }
46
+
47
+ /**
48
+ * Unsubscribes from all IntentSource contracts. It closes all clients in {@link onModuleDestroy}
49
+ */
50
+ async unsubscribe() {
51
+ super.unsubscribe()
52
+ }
53
+
54
+ async subscribeTo(client: PublicClient, source: IntentSource) {
55
+ this.logger.debug(
56
+ EcoLogMessage.fromDefault({
57
+ message: `watch create intent: subscribeToSource`,
58
+ properties: {
59
+ source,
60
+ },
61
+ }),
62
+ )
63
+ this.unwatch[source.chainID] = client.watchContractEvent({
64
+ onError: async (error) => {
65
+ await this.onError(error, client, source)
66
+ },
67
+ address: source.sourceAddress,
68
+ abi: IntentSourceAbi,
69
+ eventName: 'IntentCreated',
70
+ args: {
71
+ // // restrict by acceptable chains, chain ids must be bigints
72
+ // _destinationChain: solverSupportedChains,
73
+ prover: source.provers,
74
+ },
75
+ onLogs: this.addJob(source),
76
+ })
77
+ }
78
+
79
+ addJob(source: IntentSource) {
80
+ return async (logs: IntentCreatedLog[]) => {
81
+ for (const log of logs) {
82
+ log.sourceChainID = BigInt(source.chainID)
83
+ log.sourceNetwork = source.network
84
+
85
+ // bigint as it can't serialize to JSON
86
+ const createIntent = BigIntSerializer.serialize(log)
87
+ const jobId = getIntentJobId(
88
+ 'watch-create-intent',
89
+ createIntent.args.hash,
90
+ createIntent.logIndex,
91
+ )
92
+ this.logger.debug(
93
+ EcoLogMessage.fromDefault({
94
+ message: `watch intent`,
95
+ properties: { createIntent, jobId },
96
+ }),
97
+ )
98
+ // add to processing queue
99
+ await this.intentQueue.add(QUEUES.SOURCE_INTENT.jobs.create_intent, createIntent, {
100
+ jobId,
101
+ ...this.intentJobConfig,
102
+ })
103
+ }
104
+ }
105
+ }
106
+ }
@@ -0,0 +1,133 @@
1
+ import { Injectable, Logger, OnApplicationBootstrap, OnModuleDestroy } from '@nestjs/common'
2
+ import { EcoConfigService } from '../../eco-configs/eco-config.service'
3
+ import { JobsOptions, Queue } from 'bullmq'
4
+ import { MultichainPublicClientService } from '../../transaction/multichain-public-client.service'
5
+ import { PublicClient, WatchContractEventReturnType } from 'viem'
6
+ import { EcoLogMessage } from '@/common/logging/eco-log-message'
7
+ import { EcoError } from '@/common/errors/eco-error'
8
+
9
+ /**
10
+ * This service subscribes has hooks for subscribing and unsubscribing to a contract event.
11
+ */
12
+ @Injectable()
13
+ export abstract class WatchEventService<T extends { chainID: number }>
14
+ implements OnApplicationBootstrap, OnModuleDestroy
15
+ {
16
+ protected logger: Logger
17
+ protected intentJobConfig: JobsOptions
18
+ protected unwatch: Record<string, WatchContractEventReturnType> = {}
19
+
20
+ constructor(
21
+ protected readonly queue: Queue,
22
+ protected readonly publicClientService: MultichainPublicClientService,
23
+ protected readonly ecoConfigService: EcoConfigService,
24
+ ) {}
25
+
26
+ async onModuleInit() {
27
+ this.intentJobConfig = this.ecoConfigService.getRedis().jobs.intentJobConfig
28
+ }
29
+
30
+ async onApplicationBootstrap() {
31
+ await this.subscribe()
32
+ }
33
+
34
+ async onModuleDestroy() {
35
+ // close all clients
36
+ this.unsubscribe()
37
+ }
38
+
39
+ /**
40
+ * Subscribes to the events. It loads a mapping of the unsubscribe events to
41
+ * call {@link onModuleDestroy} to close the clients.
42
+ */
43
+ abstract subscribe(): Promise<void>
44
+
45
+ /**
46
+ * Subscribes to a contract on a specific chain
47
+ * @param client the client to subscribe to
48
+ * @param contract the contract to subscribe to
49
+ */
50
+ abstract subscribeTo(client: PublicClient, contract: T): Promise<void>
51
+
52
+ /**
53
+ * Unsubscribes from all events. It closes all clients in {@link onModuleDestroy}
54
+ */
55
+ async unsubscribe(): Promise<void> {
56
+ this.logger.debug(
57
+ EcoLogMessage.fromDefault({
58
+ message: `watch-event: unsubscribe`,
59
+ }),
60
+ )
61
+ Object.values(this.unwatch).forEach((unwatch) => {
62
+ try {
63
+ unwatch()
64
+ } catch (e) {
65
+ this.logger.error(
66
+ EcoLogMessage.withError({
67
+ message: `watch-event: unsubscribe`,
68
+ error: EcoError.WatchEventUnsubscribeError,
69
+ properties: {
70
+ errorPassed: e,
71
+ },
72
+ }),
73
+ )
74
+ }
75
+ })
76
+ }
77
+
78
+ async onError(error: any, client: PublicClient, contract: T) {
79
+ this.logger.error(
80
+ EcoLogMessage.fromDefault({
81
+ message: `rpc client error`,
82
+ properties: {
83
+ error,
84
+ },
85
+ }),
86
+ )
87
+ //reset the filters as they might have expired or we might have been moved to a new node
88
+ //https://support.quicknode.com/hc/en-us/articles/10838914856977-Error-code-32000-message-filter-not-found
89
+ await this.unsubscribeFrom(contract.chainID)
90
+ await this.subscribeTo(client, contract)
91
+ }
92
+
93
+ /**
94
+ * Unsubscribes from a specific chain
95
+ * @param chainID the chain id to unsubscribe from
96
+ */
97
+ async unsubscribeFrom(chainID: number) {
98
+ if (this.unwatch[chainID]) {
99
+ this.logger.debug(
100
+ EcoLogMessage.fromDefault({
101
+ message: `watch-event: unsubscribeFrom`,
102
+ properties: {
103
+ chainID,
104
+ },
105
+ }),
106
+ )
107
+ try {
108
+ this.unwatch[chainID]()
109
+ } catch (e) {
110
+ this.logger.error(
111
+ EcoLogMessage.withError({
112
+ message: `watch-event: unsubscribeFrom`,
113
+ error: EcoError.WatchEventUnsubscribeFromError(chainID),
114
+ properties: {
115
+ chainID,
116
+ errorPassed: e,
117
+ },
118
+ }),
119
+ )
120
+ }
121
+ } else {
122
+ this.logger.error(
123
+ EcoLogMessage.withError({
124
+ message: `watch event: unsubscribeFrom`,
125
+ error: EcoError.WatchEventNoUnsubscribeError(chainID),
126
+ properties: {
127
+ chainID,
128
+ },
129
+ }),
130
+ )
131
+ }
132
+ }
133
+ }