eco-solver 0.0.1-security → 1.5.2
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 +117 -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,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
|
+
}
|