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.
- package/.eslintignore +8 -0
- package/.eslintrc.js +24 -0
- package/.github/workflows/ci.yaml +38 -0
- package/.nvmrc +1 -0
- package/.prettierignore +3 -0
- package/.prettierrc +8 -0
- package/Dockerfile +11 -0
- package/LICENSE +21 -0
- package/README.md +29 -5
- package/config/default.ts +135 -0
- package/config/development.ts +95 -0
- package/config/preproduction.ts +17 -0
- package/config/production.ts +17 -0
- package/config/staging.ts +17 -0
- package/config/test.ts +7 -0
- package/index.js +43 -0
- package/jest.config.ts +14 -0
- package/nest-cli.json +8 -0
- package/package.json +115 -6
- package/src/api/api.module.ts +27 -0
- package/src/api/balance.controller.ts +41 -0
- package/src/api/quote.controller.ts +54 -0
- package/src/api/tests/balance.controller.spec.ts +113 -0
- package/src/api/tests/quote.controller.spec.ts +83 -0
- package/src/app.module.ts +74 -0
- package/src/balance/balance.module.ts +14 -0
- package/src/balance/balance.service.ts +230 -0
- package/src/balance/balance.ws.service.ts +104 -0
- package/src/balance/types.ts +16 -0
- package/src/bullmq/bullmq.helper.ts +41 -0
- package/src/bullmq/processors/eth-ws.processor.ts +47 -0
- package/src/bullmq/processors/inbox.processor.ts +55 -0
- package/src/bullmq/processors/interval.processor.ts +54 -0
- package/src/bullmq/processors/processor.module.ts +14 -0
- package/src/bullmq/processors/signer.processor.ts +41 -0
- package/src/bullmq/processors/solve-intent.processor.ts +73 -0
- package/src/bullmq/processors/tests/solve-intent.processor.spec.ts +3 -0
- package/src/bullmq/utils/queue.ts +22 -0
- package/src/chain-monitor/chain-monitor.module.ts +12 -0
- package/src/chain-monitor/chain-sync.service.ts +134 -0
- package/src/chain-monitor/tests/chain-sync.service.spec.ts +190 -0
- package/src/commander/.eslintrc.js +6 -0
- package/src/commander/balance/balance-command.module.ts +12 -0
- package/src/commander/balance/balance.command.ts +73 -0
- package/src/commander/command-main.ts +15 -0
- package/src/commander/commander-app.module.ts +31 -0
- package/src/commander/eco-config.command.ts +20 -0
- package/src/commander/safe/safe-command.module.ts +11 -0
- package/src/commander/safe/safe.command.ts +70 -0
- package/src/commander/transfer/client.command.ts +24 -0
- package/src/commander/transfer/transfer-command.module.ts +26 -0
- package/src/commander/transfer/transfer.command.ts +138 -0
- package/src/commander/utils.ts +8 -0
- package/src/common/chains/definitions/arbitrum.ts +12 -0
- package/src/common/chains/definitions/base.ts +21 -0
- package/src/common/chains/definitions/eco.ts +54 -0
- package/src/common/chains/definitions/ethereum.ts +22 -0
- package/src/common/chains/definitions/helix.ts +53 -0
- package/src/common/chains/definitions/mantle.ts +12 -0
- package/src/common/chains/definitions/optimism.ts +22 -0
- package/src/common/chains/definitions/polygon.ts +12 -0
- package/src/common/chains/supported.ts +26 -0
- package/src/common/chains/transport.ts +19 -0
- package/src/common/errors/eco-error.ts +155 -0
- package/src/common/events/constants.ts +3 -0
- package/src/common/events/viem.ts +22 -0
- package/src/common/logging/eco-log-message.ts +74 -0
- package/src/common/redis/constants.ts +55 -0
- package/src/common/redis/redis-connection-utils.ts +106 -0
- package/src/common/routes/constants.ts +3 -0
- package/src/common/utils/objects.ts +34 -0
- package/src/common/utils/strings.ts +49 -0
- package/src/common/utils/tests/objects.spec.ts +23 -0
- package/src/common/utils/tests/strings.spec.ts +22 -0
- package/src/common/viem/contracts.ts +25 -0
- package/src/common/viem/tests/utils.spec.ts +115 -0
- package/src/common/viem/utils.ts +78 -0
- package/src/contracts/ERC20.contract.ts +389 -0
- package/src/contracts/EntryPoint.V6.contract.ts +1309 -0
- package/src/contracts/KernelAccount.abi.ts +87 -0
- package/src/contracts/OwnableExecutor.abi.ts +128 -0
- package/src/contracts/SimpleAccount.contract.ts +524 -0
- package/src/contracts/inbox.ts +8 -0
- package/src/contracts/index.ts +9 -0
- package/src/contracts/intent-source.ts +55 -0
- package/src/contracts/interfaces/index.ts +1 -0
- package/src/contracts/interfaces/prover.interface.ts +22 -0
- package/src/contracts/prover.ts +9 -0
- package/src/contracts/tests/erc20.contract.spec.ts +59 -0
- package/src/contracts/utils.ts +31 -0
- package/src/decoder/decoder.interface.ts +3 -0
- package/src/decoder/tests/utils.spec.ts +36 -0
- package/src/decoder/utils.ts +24 -0
- package/src/decorators/cacheable.decorator.ts +48 -0
- package/src/eco-configs/aws-config.service.ts +75 -0
- package/src/eco-configs/eco-config.module.ts +44 -0
- package/src/eco-configs/eco-config.service.ts +220 -0
- package/src/eco-configs/eco-config.types.ts +278 -0
- package/src/eco-configs/interfaces/config-source.interface.ts +3 -0
- package/src/eco-configs/tests/aws-config.service.spec.ts +52 -0
- package/src/eco-configs/tests/eco-config.service.spec.ts +137 -0
- package/src/eco-configs/tests/utils.spec.ts +84 -0
- package/src/eco-configs/utils.ts +49 -0
- package/src/fee/fee.module.ts +10 -0
- package/src/fee/fee.service.ts +467 -0
- package/src/fee/tests/fee.service.spec.ts +909 -0
- package/src/fee/tests/utils.spec.ts +49 -0
- package/src/fee/types.ts +44 -0
- package/src/fee/utils.ts +23 -0
- package/src/flags/flags.module.ts +10 -0
- package/src/flags/flags.service.ts +112 -0
- package/src/flags/tests/flags.service.spec.ts +68 -0
- package/src/flags/utils.ts +22 -0
- package/src/health/constants.ts +1 -0
- package/src/health/health.controller.ts +23 -0
- package/src/health/health.module.ts +25 -0
- package/src/health/health.service.ts +40 -0
- package/src/health/indicators/balance.indicator.ts +196 -0
- package/src/health/indicators/eco-redis.indicator.ts +23 -0
- package/src/health/indicators/git-commit.indicator.ts +67 -0
- package/src/health/indicators/mongodb.indicator.ts +11 -0
- package/src/health/indicators/permission.indicator.ts +64 -0
- package/src/intent/create-intent.service.ts +129 -0
- package/src/intent/feasable-intent.service.ts +80 -0
- package/src/intent/fulfill-intent.service.ts +318 -0
- package/src/intent/intent.controller.ts +199 -0
- package/src/intent/intent.module.ts +49 -0
- package/src/intent/schemas/intent-call-data.schema.ts +16 -0
- package/src/intent/schemas/intent-data.schema.ts +114 -0
- package/src/intent/schemas/intent-source.schema.ts +33 -0
- package/src/intent/schemas/intent-token-amount.schema.ts +14 -0
- package/src/intent/schemas/reward-data.schema.ts +48 -0
- package/src/intent/schemas/route-data.schema.ts +52 -0
- package/src/intent/schemas/watch-event.schema.ts +32 -0
- package/src/intent/tests/create-intent.service.spec.ts +215 -0
- package/src/intent/tests/feasable-intent.service.spec.ts +155 -0
- package/src/intent/tests/fulfill-intent.service.spec.ts +564 -0
- package/src/intent/tests/utils-intent.service.spec.ts +308 -0
- package/src/intent/tests/utils.spec.ts +62 -0
- package/src/intent/tests/validate-intent.service.spec.ts +297 -0
- package/src/intent/tests/validation.service.spec.ts +337 -0
- package/src/intent/utils-intent.service.ts +168 -0
- package/src/intent/utils.ts +37 -0
- package/src/intent/validate-intent.service.ts +176 -0
- package/src/intent/validation.sevice.ts +223 -0
- package/src/interceptors/big-int.interceptor.ts +30 -0
- package/src/intervals/interval.module.ts +18 -0
- package/src/intervals/retry-infeasable-intents.service.ts +89 -0
- package/src/intervals/tests/retry-infeasable-intents.service.spec.ts +167 -0
- package/src/kms/errors.ts +0 -0
- package/src/kms/kms.module.ts +12 -0
- package/src/kms/kms.service.ts +65 -0
- package/src/kms/tests/kms.service.spec.ts +60 -0
- package/src/liquidity-manager/jobs/check-balances-cron.job.ts +229 -0
- package/src/liquidity-manager/jobs/liquidity-manager.job.ts +52 -0
- package/src/liquidity-manager/jobs/rebalance.job.ts +61 -0
- package/src/liquidity-manager/liquidity-manager.module.ts +29 -0
- package/src/liquidity-manager/processors/base.processor.ts +117 -0
- package/src/liquidity-manager/processors/eco-protocol-intents.processor.ts +34 -0
- package/src/liquidity-manager/processors/grouped-jobs.processor.ts +103 -0
- package/src/liquidity-manager/queues/liquidity-manager.queue.ts +48 -0
- package/src/liquidity-manager/schemas/rebalance-token.schema.ts +32 -0
- package/src/liquidity-manager/schemas/rebalance.schema.ts +32 -0
- package/src/liquidity-manager/services/liquidity-manager.service.ts +188 -0
- package/src/liquidity-manager/services/liquidity-provider.service.ts +25 -0
- package/src/liquidity-manager/services/liquidity-providers/LiFi/lifi-provider.service.spec.ts +125 -0
- package/src/liquidity-manager/services/liquidity-providers/LiFi/lifi-provider.service.ts +117 -0
- package/src/liquidity-manager/services/liquidity-providers/LiFi/utils/get-transaction-hashes.ts +16 -0
- package/src/liquidity-manager/tests/liquidity-manager.service.spec.ts +142 -0
- package/src/liquidity-manager/types/token-state.enum.ts +5 -0
- package/src/liquidity-manager/types/types.d.ts +52 -0
- package/src/liquidity-manager/utils/address.ts +5 -0
- package/src/liquidity-manager/utils/math.ts +9 -0
- package/src/liquidity-manager/utils/serialize.spec.ts +24 -0
- package/src/liquidity-manager/utils/serialize.ts +47 -0
- package/src/liquidity-manager/utils/token.ts +91 -0
- package/src/main.ts +63 -0
- package/src/nest-redlock/nest-redlock.config.ts +14 -0
- package/src/nest-redlock/nest-redlock.interface.ts +5 -0
- package/src/nest-redlock/nest-redlock.module.ts +64 -0
- package/src/nest-redlock/nest-redlock.service.ts +59 -0
- package/src/prover/proof.service.ts +184 -0
- package/src/prover/prover.module.ts +10 -0
- package/src/prover/tests/proof.service.spec.ts +154 -0
- package/src/quote/dto/quote.intent.data.dto.ts +35 -0
- package/src/quote/dto/quote.reward.data.dto.ts +67 -0
- package/src/quote/dto/quote.route.data.dto.ts +71 -0
- package/src/quote/dto/types.ts +18 -0
- package/src/quote/errors.ts +215 -0
- package/src/quote/quote.module.ts +17 -0
- package/src/quote/quote.service.ts +299 -0
- package/src/quote/schemas/quote-call.schema.ts +16 -0
- package/src/quote/schemas/quote-intent.schema.ts +27 -0
- package/src/quote/schemas/quote-reward.schema.ts +24 -0
- package/src/quote/schemas/quote-route.schema.ts +30 -0
- package/src/quote/schemas/quote-token.schema.ts +14 -0
- package/src/quote/tests/quote.service.spec.ts +444 -0
- package/src/sign/atomic-signer.service.ts +24 -0
- package/src/sign/atomic.nonce.service.ts +114 -0
- package/src/sign/kms-account/kmsToAccount.ts +73 -0
- package/src/sign/kms-account/signKms.ts +30 -0
- package/src/sign/kms-account/signKmsTransaction.ts +37 -0
- package/src/sign/kms-account/signKmsTypedData.ts +21 -0
- package/src/sign/nonce.service.ts +89 -0
- package/src/sign/schemas/nonce.schema.ts +36 -0
- package/src/sign/sign.controller.ts +52 -0
- package/src/sign/sign.helper.ts +23 -0
- package/src/sign/sign.module.ts +27 -0
- package/src/sign/signer-kms.service.ts +27 -0
- package/src/sign/signer.service.ts +26 -0
- package/src/solver/filters/tests/valid-smart-wallet.service.spec.ts +87 -0
- package/src/solver/filters/valid-smart-wallet.service.ts +58 -0
- package/src/solver/solver.module.ts +10 -0
- package/src/transaction/multichain-public-client.service.ts +15 -0
- package/src/transaction/smart-wallets/kernel/actions/encodeData.kernel.ts +57 -0
- package/src/transaction/smart-wallets/kernel/create-kernel-client-v2.account.ts +183 -0
- package/src/transaction/smart-wallets/kernel/create.kernel.account.ts +270 -0
- package/src/transaction/smart-wallets/kernel/index.ts +2 -0
- package/src/transaction/smart-wallets/kernel/kernel-account-client-v2.service.ts +90 -0
- package/src/transaction/smart-wallets/kernel/kernel-account-client.service.ts +107 -0
- package/src/transaction/smart-wallets/kernel/kernel-account.client.ts +105 -0
- package/src/transaction/smart-wallets/kernel/kernel-account.config.ts +34 -0
- package/src/transaction/smart-wallets/simple-account/create.simple.account.ts +19 -0
- package/src/transaction/smart-wallets/simple-account/index.ts +2 -0
- package/src/transaction/smart-wallets/simple-account/simple-account-client.service.ts +42 -0
- package/src/transaction/smart-wallets/simple-account/simple-account.client.ts +83 -0
- package/src/transaction/smart-wallets/simple-account/simple-account.config.ts +5 -0
- package/src/transaction/smart-wallets/smart-wallet.types.ts +38 -0
- package/src/transaction/smart-wallets/utils.ts +14 -0
- package/src/transaction/transaction.module.ts +25 -0
- package/src/transaction/viem_multichain_client.service.ts +100 -0
- package/src/transforms/viem-address.decorator.ts +14 -0
- package/src/utils/bigint.ts +44 -0
- package/src/utils/types.ts +18 -0
- package/src/watch/intent/tests/watch-create-intent.service.spec.ts +257 -0
- package/src/watch/intent/tests/watch-fulfillment.service.spec.ts +141 -0
- package/src/watch/intent/watch-create-intent.service.ts +106 -0
- package/src/watch/intent/watch-event.service.ts +133 -0
- package/src/watch/intent/watch-fulfillment.service.ts +115 -0
- package/src/watch/watch.module.ts +13 -0
- package/test/app.e2e-spec.ts +21 -0
- package/test/jest-e2e.json +9 -0
- package/tsconfig.build.json +4 -0
- 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
|
+
})
|