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,30 @@
1
+ import {
2
+ TokenAmountDataModel,
3
+ TokenAmountDataSchema,
4
+ } from '@/intent/schemas/intent-token-amount.schema'
5
+ import { QuoteRouteDataInterface } from '@/quote/dto/quote.route.data.dto'
6
+ import {
7
+ QuoteRouteCallDataModel,
8
+ QuoteRouteCallDataSchema,
9
+ } from '@/quote/schemas/quote-call.schema'
10
+ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'
11
+ import { Hex } from 'viem'
12
+
13
+ @Schema({ timestamps: true })
14
+ export class QuoteRouteDataModel implements QuoteRouteDataInterface {
15
+ @Prop({ required: true, type: BigInt })
16
+ source: bigint
17
+ @Prop({ required: true, type: BigInt })
18
+ destination: bigint
19
+ @Prop({ required: true, type: String })
20
+ inbox: Hex
21
+ @Prop({ required: true, type: [TokenAmountDataSchema] })
22
+ tokens: TokenAmountDataModel[]
23
+ @Prop({ required: true, type: [QuoteRouteCallDataSchema] })
24
+ calls: QuoteRouteCallDataModel[]
25
+ }
26
+
27
+ export const QuoteRouteDataSchema = SchemaFactory.createForClass(QuoteRouteDataModel)
28
+ QuoteRouteDataSchema.index({ source: 1 }, { unique: false })
29
+ QuoteRouteDataSchema.index({ destination: 1 }, { unique: false })
30
+ QuoteRouteDataSchema.index({ inbox: 1 }, { unique: false })
@@ -0,0 +1,14 @@
1
+ import { RewardTokensInterface } from '@/contracts'
2
+ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'
3
+ import { Hex } from 'viem'
4
+
5
+ @Schema({ timestamps: true })
6
+ export class QuoteRewardTokenDataModel implements RewardTokensInterface {
7
+ @Prop({ required: true, type: String })
8
+ token: Hex
9
+ @Prop({ required: true, type: BigInt })
10
+ amount: bigint
11
+ }
12
+
13
+ export const QuoteRewardTokenDataSchema = SchemaFactory.createForClass(QuoteRewardTokenDataModel)
14
+ QuoteRewardTokenDataSchema.index({ token: 1 }, { unique: false })
@@ -0,0 +1,444 @@
1
+ const mockGetTransactionTargetData = jest.fn()
2
+ import { EcoConfigService } from '@/eco-configs/eco-config.service'
3
+ import { FeeService } from '@/fee/fee.service'
4
+ import { ValidationChecks, ValidationService } from '@/intent/validation.sevice'
5
+ import {
6
+ InfeasibleQuote,
7
+ InsufficientBalance,
8
+ InternalQuoteError,
9
+ InternalSaveError,
10
+ InvalidQuoteIntent,
11
+ QuoteError,
12
+ SolverUnsupported,
13
+ } from '@/quote/errors'
14
+ import { QuoteService } from '@/quote/quote.service'
15
+ import { QuoteIntentModel } from '@/quote/schemas/quote-intent.schema'
16
+ import { createMock, DeepMocked } from '@golevelup/ts-jest'
17
+ import { getModelToken } from '@nestjs/mongoose'
18
+ import { Test, TestingModule } from '@nestjs/testing'
19
+ import { Model } from 'mongoose'
20
+
21
+ jest.mock('@/intent/utils', () => {
22
+ return {
23
+ ...jest.requireActual('@/intent/utils'),
24
+ getTransactionTargetData: mockGetTransactionTargetData,
25
+ }
26
+ })
27
+
28
+ describe('QuotesService', () => {
29
+ let quoteService: QuoteService
30
+ let feeService: DeepMocked<FeeService>
31
+ let validationService: DeepMocked<ValidationService>
32
+ let ecoConfigService: DeepMocked<EcoConfigService>
33
+ let quoteModel: DeepMocked<Model<QuoteIntentModel>>
34
+ const mockLogDebug = jest.fn()
35
+ const mockLogLog = jest.fn()
36
+ const mockLogError = jest.fn()
37
+
38
+ beforeEach(async () => {
39
+ const chainMod: TestingModule = await Test.createTestingModule({
40
+ providers: [
41
+ QuoteService,
42
+ { provide: FeeService, useValue: createMock<FeeService>() },
43
+ { provide: ValidationService, useValue: createMock<ValidationService>() },
44
+ { provide: FeeService, useValue: createMock<FeeService>() },
45
+ { provide: EcoConfigService, useValue: createMock<EcoConfigService>() },
46
+ {
47
+ provide: getModelToken(QuoteIntentModel.name),
48
+ useValue: createMock<Model<QuoteIntentModel>>(),
49
+ },
50
+ ],
51
+ }).compile()
52
+
53
+ quoteService = chainMod.get(QuoteService)
54
+ feeService = chainMod.get(FeeService)
55
+ validationService = chainMod.get(ValidationService)
56
+
57
+ ecoConfigService = chainMod.get(EcoConfigService)
58
+ quoteModel = chainMod.get(getModelToken(QuoteIntentModel.name))
59
+
60
+ quoteService['logger'].debug = mockLogDebug
61
+ quoteService['logger'].log = mockLogLog
62
+ quoteService['logger'].error = mockLogError
63
+ })
64
+
65
+ afterEach(async () => {
66
+ // restore the spy created with spyOn
67
+ jest.restoreAllMocks()
68
+ mockLogDebug.mockClear()
69
+ mockLogLog.mockClear()
70
+ mockLogError.mockClear()
71
+ })
72
+
73
+ describe('on getQuote', () => {
74
+ const quoteIntent = { reward: { tokens: [] }, route: {} } as any
75
+ it('should throw an error if it cant store the quote in the db ', async () => {
76
+ const failedStore = new Error('error')
77
+ quoteService.storeQuoteIntentData = jest.fn().mockResolvedValue(failedStore)
78
+ expect(await quoteService.getQuote({} as any)).toEqual(InternalSaveError(failedStore))
79
+ })
80
+
81
+ it('should return a 400 if it fails to validate the quote data', async () => {
82
+ quoteService.storeQuoteIntentData = jest.fn().mockResolvedValue({})
83
+ quoteService.validateQuoteIntentData = jest.fn().mockResolvedValue(SolverUnsupported)
84
+ expect(await quoteService.getQuote({} as any)).toEqual(SolverUnsupported)
85
+ })
86
+
87
+ it('should save any error in getting the quote to the db', async () => {
88
+ const failedStore = new Error('error')
89
+ quoteService.storeQuoteIntentData = jest.fn().mockResolvedValue(quoteIntent)
90
+ quoteService.validateQuoteIntentData = jest.fn().mockResolvedValue(undefined)
91
+ quoteService.generateQuote = jest.fn().mockImplementation(() => {
92
+ throw failedStore
93
+ })
94
+ const mockDb = jest.spyOn(quoteService, 'updateQuoteDb')
95
+ expect(await quoteService.getQuote({} as any)).toEqual(InternalQuoteError(failedStore))
96
+ expect(mockDb).toHaveBeenCalled()
97
+ expect(mockDb).toHaveBeenCalledWith(quoteIntent, InternalQuoteError(failedStore))
98
+ })
99
+
100
+ it('should return the quote', async () => {
101
+ const quoteReciept = { fee: 1n }
102
+ quoteService.storeQuoteIntentData = jest.fn().mockResolvedValue(quoteIntent)
103
+ quoteService.validateQuoteIntentData = jest.fn().mockResolvedValue(undefined)
104
+ quoteService.generateQuote = jest.fn().mockResolvedValue(quoteReciept)
105
+ const mockDb = jest.spyOn(quoteService, 'updateQuoteDb')
106
+ expect(await quoteService.getQuote({} as any)).toEqual(quoteReciept)
107
+ expect(mockDb).toHaveBeenCalled()
108
+ expect(mockDb).toHaveBeenCalledWith(quoteIntent, quoteReciept)
109
+ })
110
+ })
111
+
112
+ describe('on storeQuoteIntentData', () => {
113
+ it('should log error if storing fails', async () => {
114
+ const failedStore = new Error('error')
115
+ jest.spyOn(quoteModel, 'create').mockRejectedValue(failedStore)
116
+ const r = await quoteService.storeQuoteIntentData({} as any)
117
+ expect(r).toEqual(failedStore)
118
+ expect(mockLogError).toHaveBeenCalled()
119
+ })
120
+
121
+ it('should save the DTO and return a record', async () => {
122
+ const data = { fee: 1n }
123
+ jest.spyOn(quoteModel, 'create').mockResolvedValue(data as any)
124
+ const r = await quoteService.storeQuoteIntentData({} as any)
125
+ expect(r).toEqual(data)
126
+ expect(mockLogError).not.toHaveBeenCalled()
127
+ expect(mockLogLog).toHaveBeenCalled()
128
+ })
129
+ })
130
+
131
+ describe('on validateQuoteIntentData', () => {
132
+ const quoteIntentModel = {
133
+ _id: 'id9',
134
+ route: {
135
+ destination: 1n,
136
+ },
137
+ }
138
+ const failValidations: ValidationChecks = {
139
+ supportedProver: true,
140
+ supportedTargets: true,
141
+ supportedSelectors: true,
142
+ validTransferLimit: true,
143
+ validExpirationTime: true,
144
+ validDestination: false,
145
+ fulfillOnDifferentChain: true,
146
+ }
147
+ const validValidations: ValidationChecks = {
148
+ supportedProver: true,
149
+ supportedTargets: true,
150
+ supportedSelectors: true,
151
+ validTransferLimit: true,
152
+ validExpirationTime: true,
153
+ validDestination: true,
154
+ fulfillOnDifferentChain: true,
155
+ }
156
+ let updateQuoteDb: jest.SpyInstance
157
+ beforeEach(() => {
158
+ updateQuoteDb = jest.spyOn(quoteService, 'updateQuoteDb')
159
+ })
160
+
161
+ afterEach(() => {
162
+ updateQuoteDb.mockClear()
163
+ })
164
+
165
+ it('should return solver unsupported if no solver for destination', async () => {
166
+ ecoConfigService.getSolver = jest.fn().mockReturnValue(undefined)
167
+ expect(await quoteService.validateQuoteIntentData(quoteIntentModel as any)).toEqual(
168
+ SolverUnsupported,
169
+ )
170
+ expect(mockLogLog).toHaveBeenCalled()
171
+ expect(mockLogLog).toHaveBeenCalledWith({
172
+ msg: `validateQuoteIntentData: No solver found for destination : ${quoteIntentModel.route.destination}`,
173
+ quoteIntentModel,
174
+ })
175
+ expect(updateQuoteDb).toHaveBeenCalledWith(quoteIntentModel, { error: SolverUnsupported })
176
+ })
177
+
178
+ it('should return invalid quote if the quote fails validations', async () => {
179
+ ecoConfigService.getSolver = jest.fn().mockReturnValue({})
180
+ validationService.assertValidations = jest.fn().mockReturnValue(failValidations)
181
+
182
+ expect(await quoteService.validateQuoteIntentData(quoteIntentModel as any)).toEqual(
183
+ InvalidQuoteIntent(failValidations),
184
+ )
185
+ expect(mockLogLog).toHaveBeenCalled()
186
+ expect(mockLogLog).toHaveBeenCalledWith({
187
+ msg: `validateQuoteIntentData: Some validations failed`,
188
+ quoteIntentModel,
189
+ validations: failValidations,
190
+ })
191
+ expect(updateQuoteDb).toHaveBeenCalledWith(quoteIntentModel, {
192
+ error: InvalidQuoteIntent(failValidations),
193
+ })
194
+ })
195
+
196
+ it('should return infeasable if the quote is infeasable', async () => {
197
+ const error = QuoteError.SolverLacksLiquidity(1, '0x2', 4n, 3n, 2n)
198
+ ecoConfigService.getSolver = jest.fn().mockReturnValue({})
199
+ validationService.assertValidations = jest.fn().mockReturnValue(validValidations)
200
+ feeService.isRouteFeasible = jest.fn().mockResolvedValue({ error })
201
+ expect(await quoteService.validateQuoteIntentData(quoteIntentModel as any)).toEqual(
202
+ InfeasibleQuote(error),
203
+ )
204
+ expect(mockLogLog).toHaveBeenCalled()
205
+ expect(mockLogLog).toHaveBeenCalledWith({
206
+ msg: `validateQuoteIntentData: quote intent is not feasable ${quoteIntentModel._id}`,
207
+ quoteIntentModel,
208
+ feasable: false,
209
+ error: InfeasibleQuote(error),
210
+ })
211
+ expect(updateQuoteDb).toHaveBeenCalledWith(quoteIntentModel, {
212
+ error: InfeasibleQuote(error),
213
+ })
214
+ })
215
+
216
+ it('should return nothing if all the validations pass', async () => {
217
+ ecoConfigService.getSolver = jest.fn().mockReturnValue({})
218
+ validationService.assertValidations = jest.fn().mockReturnValue(validValidations)
219
+ feeService.isRouteFeasible = jest.fn().mockResolvedValue({})
220
+ expect(await quoteService.validateQuoteIntentData(quoteIntentModel as any)).toEqual(undefined)
221
+ expect(updateQuoteDb).not.toHaveBeenCalled()
222
+ })
223
+ })
224
+
225
+ describe('on generateQuote', () => {
226
+ it('should return error on calculate tokens failed', async () => {
227
+ const error = new Error('error') as any
228
+ feeService.calculateTokens = jest.fn().mockResolvedValue({ error } as any)
229
+ expect(await quoteService.generateQuote({} as any)).toEqual(InternalQuoteError(error))
230
+ })
231
+
232
+ it('should return error on calculate tokens doesnt return the calculated tokens', async () => {
233
+ feeService.calculateTokens = jest.fn().mockResolvedValue({ calculated: undefined } as any)
234
+ expect(await quoteService.generateQuote({} as any)).toEqual(InternalQuoteError(undefined))
235
+ })
236
+
237
+ it('should return an insufficient balance if the reward doesnt meet the ask', async () => {
238
+ const calculated = {
239
+ solver: {},
240
+ rewards: [{ balance: 10n }, { balance: 102n }],
241
+ calls: [{ balance: 280n }, { balance: 102n }],
242
+ deficitDescending: [],
243
+ } as any
244
+ jest.spyOn(feeService, 'calculateTokens').mockResolvedValue({ calculated })
245
+ const ask = calculated.calls.reduce((a, b) => a + b.balance, 0n)
246
+ const askMock = jest.spyOn(feeService, 'getAsk').mockReturnValue(ask)
247
+ expect(await quoteService.generateQuote({ route: {} } as any)).toEqual(
248
+ InsufficientBalance(ask, 112n),
249
+ )
250
+ expect(askMock).toHaveBeenCalled()
251
+ })
252
+
253
+ describe('on building quote', () => {
254
+ beforeEach(() => {})
255
+
256
+ async function generateHelper(
257
+ calculated: any,
258
+ expectedTokens: { token: string; amount: bigint }[],
259
+ ) {
260
+ const ask = calculated.calls.reduce((a, b) => a + b.balance, 0n)
261
+ jest.spyOn(feeService, 'getAsk').mockReturnValue(ask)
262
+ jest.spyOn(feeService, 'calculateTokens').mockResolvedValue({ calculated })
263
+ feeService.deconvertNormalize = jest.fn().mockImplementation((amount) => {
264
+ return { balance: amount }
265
+ })
266
+ expect(await quoteService.generateQuote({ route: {} } as any)).toEqual({
267
+ tokens: expectedTokens,
268
+ expiryTime: expect.any(String),
269
+ })
270
+ }
271
+
272
+ it('should fill up the most deficit balance', async () => {
273
+ const calculated = {
274
+ solver: {},
275
+ rewards: [
276
+ { address: '0x1', balance: 100n },
277
+ { address: '0x2', balance: 200n },
278
+ ],
279
+ calls: [{ balance: 50n }],
280
+ deficitDescending: [
281
+ { delta: { balance: -100n, address: '0x1' } },
282
+ { delta: { balance: -50n }, address: '0x2' },
283
+ ],
284
+ } as any
285
+ await generateHelper(calculated, [{ token: '0x1', amount: 50n }])
286
+ })
287
+
288
+ it('should fill deficit that has rewards to fill it', async () => {
289
+ const calculated = {
290
+ solver: {},
291
+ rewards: [{ address: '0x2', balance: 200n }],
292
+ calls: [{ balance: 150n }],
293
+ deficitDescending: [
294
+ { delta: { balance: -100n, address: '0x1' } },
295
+ { delta: { balance: -50n, address: '0x2' } },
296
+ ],
297
+ } as any
298
+ await generateHelper(calculated, [{ token: '0x2', amount: 150n }])
299
+ })
300
+
301
+ it('should fill surplus if no deficit', async () => {
302
+ const calculated = {
303
+ solver: {},
304
+ rewards: [{ address: '0x2', balance: 200n }],
305
+ calls: [{ balance: 40n }],
306
+ deficitDescending: [
307
+ { delta: { balance: 100n, address: '0x1' } },
308
+ { delta: { balance: 200n, address: '0x2' } },
309
+ ],
310
+ } as any
311
+ await generateHelper(calculated, [{ token: '0x2', amount: 40n }])
312
+ })
313
+
314
+ it('should fill partial deficits', async () => {
315
+ const calculated = {
316
+ solver: {},
317
+ rewards: [
318
+ { address: '0x1', balance: 200n },
319
+ { address: '0x2', balance: 200n },
320
+ ],
321
+ calls: [{ balance: 150n }],
322
+ deficitDescending: [
323
+ { delta: { balance: -100n, address: '0x1' } },
324
+ { delta: { balance: -50n, address: '0x2' } },
325
+ ],
326
+ } as any
327
+ await generateHelper(calculated, [
328
+ { token: '0x1', amount: 100n },
329
+ { token: '0x2', amount: 50n },
330
+ ])
331
+ })
332
+
333
+ it('should fill surplus if deficit is not rewarded', async () => {
334
+ const calculated = {
335
+ solver: {},
336
+ rewards: [{ address: '0x2', balance: 200n }],
337
+ calls: [{ balance: 150n }],
338
+ deficitDescending: [
339
+ { delta: { balance: -100n, address: '0x1' } },
340
+ { delta: { balance: 100n, address: '0x2' } },
341
+ ],
342
+ } as any
343
+ await generateHelper(calculated, [{ token: '0x2', amount: 150n }])
344
+ })
345
+
346
+ it('should fill deficit as much as it can and then surplus', async () => {
347
+ const calculated = {
348
+ solver: {},
349
+ rewards: [
350
+ { address: '0x1', balance: 50n },
351
+ { address: '0x2', balance: 20n },
352
+ { address: '0x3', balance: 200n },
353
+ ],
354
+ calls: [{ balance: 150n }],
355
+ deficitDescending: [
356
+ { delta: { balance: -100n, address: '0x1' } },
357
+ { delta: { balance: -50n, address: '0x2' } },
358
+ { delta: { balance: 100n, address: '0x3' } },
359
+ ],
360
+ } as any
361
+ await generateHelper(calculated, [
362
+ { token: '0x1', amount: 50n },
363
+ { token: '0x2', amount: 20n },
364
+ { token: '0x3', amount: 80n },
365
+ ])
366
+ })
367
+
368
+ it('should fill deficit in remaining funds loop that can be filled when rewards dont allow order', async () => {
369
+ const calculated = {
370
+ solver: {},
371
+ rewards: [
372
+ { address: '0x1', balance: 50n },
373
+ { address: '0x2', balance: 200n },
374
+ ],
375
+ calls: [{ balance: 250n }],
376
+ deficitDescending: [
377
+ { delta: { balance: -100n, address: '0x1' } },
378
+ { delta: { balance: -50n, address: '0x2' } },
379
+ ],
380
+ } as any
381
+ await generateHelper(calculated, [
382
+ { token: '0x1', amount: 50n },
383
+ { token: '0x2', amount: 200n },
384
+ ])
385
+ })
386
+
387
+ it('should fill surpluses in ascending order', async () => {
388
+ const calculated = {
389
+ solver: {},
390
+ rewards: [
391
+ { address: '0x1', balance: 150n },
392
+ { address: '0x2', balance: 150n },
393
+ ],
394
+ calls: [{ balance: 250n }],
395
+ deficitDescending: [
396
+ { delta: { balance: 10n, address: '0x1' } },
397
+ { delta: { balance: 20n, address: '0x2' } },
398
+ ],
399
+ } as any
400
+ await generateHelper(calculated, [
401
+ { token: '0x1', amount: 150n },
402
+ { token: '0x2', amount: 100n },
403
+ ])
404
+ })
405
+ })
406
+ })
407
+
408
+ describe('on getQuoteExpiryTime', () => {
409
+ it('should return the correct expiry time', async () => {
410
+ const expiryTime = quoteService.getQuoteExpiryTime()
411
+ expect(Number(expiryTime)).toBeGreaterThan(0)
412
+ })
413
+ })
414
+
415
+ describe('on updateQuoteDb', () => {
416
+ const _id = 'id9'
417
+ it('should return error if db save fails', async () => {
418
+ const failedStore = new Error('error')
419
+ jest.spyOn(quoteModel, 'updateOne').mockRejectedValue(failedStore)
420
+ const r = await quoteService.updateQuoteDb({ _id } as any)
421
+ expect(r).toEqual(failedStore)
422
+ expect(mockLogError).toHaveBeenCalled()
423
+ })
424
+
425
+ it('should save the DTO', async () => {
426
+ const data = { fee: 1n }
427
+ jest.spyOn(quoteModel, 'updateOne').mockResolvedValue(data as any)
428
+ const r = await quoteService.updateQuoteDb({ _id } as any)
429
+ expect(r).toBeUndefined()
430
+ expect(mockLogError).not.toHaveBeenCalled()
431
+ expect(jest.spyOn(quoteModel, 'updateOne')).toHaveBeenCalledWith({ _id }, { _id })
432
+ })
433
+
434
+ it('should save the DTO with a reciept', async () => {
435
+ const data = { fee: 1n }
436
+ const receipt = 'receipt'
437
+ jest.spyOn(quoteModel, 'updateOne').mockResolvedValue(data as any)
438
+ const r = await quoteService.updateQuoteDb({ _id, receipt } as any)
439
+ expect(r).toBeUndefined()
440
+ expect(mockLogError).not.toHaveBeenCalled()
441
+ expect(jest.spyOn(quoteModel, 'updateOne')).toHaveBeenCalledWith({ _id }, { _id, receipt })
442
+ })
443
+ })
444
+ })
@@ -0,0 +1,24 @@
1
+ import { Injectable } from '@nestjs/common'
2
+ import { EcoConfigService } from '../eco-configs/eco-config.service'
3
+ import { NonceService } from './nonce.service'
4
+ import { privateKeyAndNonceToAccountSigner } from './sign.helper'
5
+ import { SignerService } from './signer.service'
6
+ import { Hex, PrivateKeyAccount } from 'viem'
7
+
8
+ @Injectable()
9
+ export class AtomicSignerService extends SignerService {
10
+ constructor(
11
+ readonly nonceService: NonceService,
12
+ readonly ecoConfigService: EcoConfigService,
13
+ ) {
14
+ super(ecoConfigService)
15
+ }
16
+
17
+ protected buildAccount(): PrivateKeyAccount {
18
+ return privateKeyAndNonceToAccountSigner(this.nonceService, this.getPrivateKey())
19
+ }
20
+
21
+ protected override getPrivateKey(): Hex {
22
+ return this.ecoConfigService.getEth().simpleAccount.signerPrivateKey
23
+ }
24
+ }
@@ -0,0 +1,114 @@
1
+ import { Hex, NonceManagerSource, Prettify, PublicClient } from 'viem'
2
+ import type { Address } from 'abitype'
3
+ import { Model, QueryOptions } from 'mongoose'
4
+ import type { Client } from 'viem/_types/clients/createClient'
5
+ import { Injectable, Logger } from '@nestjs/common'
6
+ import { EcoLogMessage } from '../common/logging/eco-log-message'
7
+ import { getAtomicNonceKey } from './sign.helper'
8
+
9
+ export type AtomicKeyParams = {
10
+ address: Hex
11
+ chainId: number
12
+ }
13
+
14
+ export type AtomicKeyClientParams = Prettify<
15
+ Pick<AtomicKeyParams, 'address'> & {
16
+ client: PublicClient
17
+ }
18
+ >
19
+
20
+ export type AtomicGetParameters = Prettify<AtomicKeyParams & { client: Client }>
21
+
22
+ /** An atomic JSON-RPC source for a nonce manager. It initializes the nonce
23
+ * to the current RPC returned transaction count, then it stores and increments
24
+ * the nonce through an atomic call locally. Ie. a database that can enforce atomicity.
25
+ *
26
+ * This way the account for the nonce can be shared amongs multiple processes simultaneously without
27
+ * the treat of nonce collisions. Such as in a kubernetes cluster.
28
+ */
29
+ @Injectable()
30
+ export abstract class AtomicNonceService<T extends { nonce: number }>
31
+ implements NonceManagerSource
32
+ {
33
+ protected logger = new Logger(AtomicNonceService.name)
34
+
35
+ constructor(protected model: Model<T>) {}
36
+
37
+ async syncNonces(): Promise<void> {
38
+ const params: AtomicKeyClientParams[] = await this.getSyncParams()
39
+ if (params.length === 0) {
40
+ return
41
+ }
42
+
43
+ const nonceSyncs = params.map(async (param: AtomicKeyClientParams) => {
44
+ const { address, client } = param
45
+ const nonceNum = await client.getTransactionCount({ address, blockTag: 'pending' })
46
+
47
+ return {
48
+ nonceNum,
49
+ chainID: client.chain?.id,
50
+ address: address,
51
+ }
52
+ })
53
+
54
+ try {
55
+ const updatedNonces = await Promise.all(nonceSyncs)
56
+ const updates = updatedNonces.map(async (nonce) => {
57
+ const { address, chainID } = nonce
58
+ const key = getAtomicNonceKey({ address, chainId: chainID ?? 0 })
59
+ const query = { key }
60
+ const updates = { $set: { nonce: nonce.nonceNum, chainID, address } }
61
+ const options = { upsert: true, new: true }
62
+ this.logger.debug(
63
+ EcoLogMessage.fromDefault({
64
+ message: `AtomicNonceService: updating nonce in sync`,
65
+ properties: {
66
+ query,
67
+ updates,
68
+ },
69
+ }),
70
+ )
71
+ return this.model.findOneAndUpdate(query, updates, options).exec()
72
+ })
73
+
74
+ await Promise.all(updates)
75
+ } catch (e) {
76
+ EcoLogMessage.fromDefault({
77
+ message: `Error syncing nonces`,
78
+ properties: {
79
+ error: e,
80
+ },
81
+ })
82
+ }
83
+ }
84
+
85
+ async get(parameters: AtomicGetParameters): Promise<number> {
86
+ return await this.getIncNonce(parameters)
87
+ }
88
+
89
+ async set(params: AtomicGetParameters, nonce: number): Promise<void> {} // eslint-disable-line @typescript-eslint/no-unused-vars
90
+
91
+ async getIncNonce(parameters: AtomicGetParameters): Promise<number> {
92
+ const query = { key: getAtomicNonceKey(parameters) }
93
+ const updates = { $inc: { nonce: 1 } }
94
+ const options: QueryOptions = {
95
+ upsert: true, //creates a new document if one doesn't exist
96
+ new: true, //returns the updated document instead of the document before update
97
+ }
98
+ //get and increment from db
99
+ const updateResponse = await this.model.findOneAndUpdate(query, updates, options).exec()
100
+ return updateResponse?.nonce ?? 0
101
+ }
102
+
103
+ protected async getSyncParams(): Promise<AtomicKeyClientParams[]> {
104
+ return []
105
+ }
106
+
107
+ async getNonces(): Promise<T[]> {
108
+ return this.model.find().exec()
109
+ }
110
+
111
+ static getNonceQueueKey(address: Address, chainId: number): string {
112
+ return `${address}.${chainId}`
113
+ }
114
+ }