├── .eslintignore ├── .prettierignore ├── src ├── cli │ ├── bootstrap.ts │ └── cli.ts ├── chain-evm │ ├── fees │ │ ├── types.ts │ │ ├── fetcher-legacy.ts │ │ ├── fetcher-legacy.test.ts │ │ ├── defaults.ts │ │ ├── fetcher-eip1559.test.ts │ │ ├── manager.test.ts │ │ └── fetcher-eip1559.ts │ ├── preferences │ │ ├── Polygon.ts │ │ ├── BNB.ts │ │ ├── BNB.test.ts │ │ └── store.ts │ ├── signer.test.ts │ ├── networking │ │ └── broadcaster.test.utils.ts │ ├── order-estimator.ts │ ├── tx-generators │ │ ├── createBatchOrderUnlockTx.ts │ │ ├── createOrderFullfillTx.ts │ │ ├── createERC20ApproveTxs.ts │ │ └── ierc20.json │ ├── signer.ts │ ├── tx-builder.ts │ └── order-validator.ts ├── utils.ts ├── configurator │ ├── index.ts │ └── tokenPriceService.ts ├── init.ts ├── errors.ts ├── hooks │ ├── HookHandler.ts │ ├── types │ │ ├── OrderEstimation.ts │ │ └── HookParams.ts │ ├── HooksEngine.ts │ └── HookEnums.ts ├── chain-common │ ├── mixins.ts │ ├── order-evaluation-context.ts │ ├── tx-builder.ts │ ├── order-taker.ts │ ├── order.ts │ └── order-estimator.ts ├── env-utils.ts ├── filters │ ├── white.listed.orderid.ts │ ├── disable.fulfill.ts │ ├── order.filter.ts │ ├── index.ts │ ├── white.listed.marker.ts │ ├── black.listed.give.token.ts │ ├── black.listed.take.token.ts │ ├── white.listed.receiver.ts │ ├── white.listed.take.token.ts │ ├── white.listed.give.token.ts │ ├── give.amount.usd.equivalent.between.ts │ └── take.amount.usd.equivalent.between.ts ├── dln-ts-client.utils.ts ├── index.ts ├── chain-solana │ ├── tx-generators │ │ ├── createOrderFullfillTx.ts │ │ ├── createBatchOrderUnlockTx.ts │ │ └── tryInitTakerALT.ts │ ├── signer.ts │ └── tx-builder.ts ├── processors │ ├── StatsAPI.ts │ ├── mempool.service.ts │ ├── swap-connector-implementation.service.ts │ ├── DataStore.ts │ ├── TVLBudgetController.ts │ ├── throughput.ts │ └── BatchUnlocker.ts ├── interfaces.ts ├── orderFeeds │ └── u256-utils.ts ├── environments.ts └── config.ts ├── .husky ├── pre-commit └── post-commit ├── tsconfig.verify1.json ├── assets └── DlnPrincipalScheme.png ├── funding.json ├── tsconfig.dev.json ├── tsconfig.verify2.json ├── .editorconfig ├── .lintstagedrc.mjs ├── .gitignore ├── hardhat.config.ts ├── .github └── workflows │ ├── lint_and_tests.yml │ └── npm_publish.yml ├── tsconfig.build.json ├── tests ├── conversions.test.ts ├── throughput.test.ts └── helpers │ └── index.ts ├── Dockerfile ├── docs └── Solana-key-management.md ├── .prettierrc.js ├── docker-compose.yml ├── sample.env ├── .eslintrc.js ├── package.json ├── sample.config.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | src/pmm_common.ts -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | src/pmm_common.ts -------------------------------------------------------------------------------- /src/cli/bootstrap.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../init'); 4 | require('./cli'); 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /tsconfig.verify1.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["./sample.config.ts"] 4 | } -------------------------------------------------------------------------------- /.husky/post-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | git update-index --again 5 | -------------------------------------------------------------------------------- /src/chain-evm/fees/types.ts: -------------------------------------------------------------------------------- 1 | export enum GasCategory { 2 | PROJECTED, 3 | NORMAL, 4 | AGGRESSIVE, 5 | } 6 | -------------------------------------------------------------------------------- /assets/DlnPrincipalScheme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/debridge-finance/dln-taker/HEAD/assets/DlnPrincipalScheme.png -------------------------------------------------------------------------------- /funding.json: -------------------------------------------------------------------------------- 1 | { 2 | "opRetro": { 3 | "projectId": "0xf24315614063278ba3543e1717791132a9afa79b0e65baa01a85bcccbdfa215f" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noUnusedLocals": false, 5 | "noUnusedParameters": false 6 | } 7 | } -------------------------------------------------------------------------------- /tsconfig.verify2.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["./sample.config.ts"], 4 | "compilerOptions": { 5 | "noUnusedLocals": false, 6 | "noUnusedParameters": false 7 | } 8 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /src/chain-evm/preferences/Polygon.ts: -------------------------------------------------------------------------------- 1 | import { SuggestedOpts } from './store'; 2 | 3 | export const suggestedOpts: SuggestedOpts = { 4 | feeManagerOpts: { 5 | legacyEnforceAggressive: true, 6 | eip1559EnforceAggressive: true, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /.lintstagedrc.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | "**/*.ts": [ 3 | "eslint --cache --fix", 4 | () => 'npm run lint:tsc', 5 | "npm run pretty:ts" 6 | ], 7 | "package.json": [ 8 | "prettier-package-json --write" 9 | ] 10 | } -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export function safeIntToBigInt(v: number): bigint { 2 | return BigInt(Math.trunc(v)); 3 | } 4 | 5 | export function findMaxBigInt(...bigInts: Array) { 6 | return bigInts.reduce((max, curr) => (curr > max ? curr : max), 0n); 7 | } 8 | -------------------------------------------------------------------------------- /src/chain-evm/signer.test.ts: -------------------------------------------------------------------------------- 1 | describe(`Signer`, () => { 2 | xit('TODO Must get positive receipt'); 3 | xit('TODO Must get negative receipt'); 4 | xit('TODO Must get rejected txn (unable to estimate)'); 5 | xit('TODO Must get rejected txn (capping reached)'); 6 | }); 7 | -------------------------------------------------------------------------------- /src/configurator/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-default-export -- Allowed to simplify references in the configuration file */ 2 | 3 | import { tokenPriceService } from './tokenPriceService'; 4 | 5 | export { tokenPriceService }; 6 | 7 | export default { 8 | tokenPriceService, 9 | }; 10 | -------------------------------------------------------------------------------- /src/init.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | 3 | // this is needed to serialize objects with a bigint inside 4 | (BigInt.prototype as any).toJSON = function () { 5 | return this.toString(); 6 | }; 7 | 8 | // Almost never return exponential notation: 9 | BigNumber.config({ EXPONENTIAL_AT: 1e9 }); 10 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | export function die(message: string) { 2 | // eslint-disable-next-line no-console -- Enable access to console log explicitly for dying 3 | console.trace(message); 4 | process.exit(1); 5 | } 6 | 7 | export function assert(validation: boolean, message: string): asserts validation { 8 | if (!validation) die(message); 9 | } 10 | -------------------------------------------------------------------------------- /src/hooks/HookHandler.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from 'pino'; 2 | 3 | import { Hooks } from './HookEnums'; 4 | import { HookParams } from './types/HookParams'; 5 | 6 | export type HookContext = { 7 | logger: Logger; 8 | }; 9 | 10 | export type HookHandler = ( 11 | args: HookParams, 12 | context: HookContext, 13 | ) => Promise; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build files 2 | dist 3 | 4 | # development 5 | .idea 6 | node_modules 7 | .eslintcache 8 | 9 | # safety 10 | .env 11 | executor*.config.ts 12 | 13 | node_modules 14 | .env 15 | 16 | # Hardhat files 17 | /cache 18 | /artifacts 19 | 20 | # TypeChain files 21 | /typechain 22 | /typechain-types 23 | 24 | # solidity-coverage files 25 | /coverage 26 | /coverage.json 27 | -------------------------------------------------------------------------------- /src/hooks/types/OrderEstimation.ts: -------------------------------------------------------------------------------- 1 | export type OrderEstimation = { 2 | isProfitable: boolean; // CalculateResult.isProfitable 3 | reserveToken: Uint8Array; // CalculateResult.reserveDstToken 4 | requiredReserveAmount: string; // CalculateResult.requiredReserveDstAmount 5 | fulfillToken: Uint8Array; // order.take.tokenAddress 6 | projectedFulfillAmount: string; // CalculateResult.profitableTakeAmount 7 | }; 8 | -------------------------------------------------------------------------------- /src/chain-common/mixins.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, tokenAddressToString } from '@debridge-finance/dln-client'; 2 | 3 | declare global { 4 | interface Uint8Array { 5 | toAddress(chain: ChainId): string; 6 | } 7 | } 8 | 9 | // eslint-disable-next-line no-extend-native -- Intentional extend of the object 10 | Uint8Array.prototype.toAddress = function (chain: ChainId): string { 11 | return tokenAddressToString(chain, this); 12 | }; 13 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies,import/no-default-export -- only for testing purposes */ 2 | import { HardhatUserConfig } from 'hardhat/config'; 3 | import '@nomiclabs/hardhat-web3'; 4 | import '@nomicfoundation/hardhat-chai-matchers'; 5 | import '@nomicfoundation/hardhat-ethers'; 6 | 7 | const config: HardhatUserConfig = { 8 | solidity: '0.8.19', 9 | defaultNetwork: process.env.TEST_AGAINST_LOCAL_NODE === 'true' ? 'localhost' : undefined, 10 | }; 11 | export default config; 12 | -------------------------------------------------------------------------------- /.github/workflows/lint_and_tests.yml: -------------------------------------------------------------------------------- 1 | name: lint_and_tests 2 | on: [push] 3 | 4 | jobs: 5 | tests: 6 | runs-on: ubuntu-latest 7 | name: Run eslint 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: actions/setup-node@v3 11 | with: 12 | node-version: 18 13 | registry-url: 'https://registry.npmjs.org' 14 | cache: 'npm' 15 | - run: npm ci 16 | - run: npm rebuild && npm run prepare --if-present 17 | - run: npm run lint 18 | - run: npm run test 19 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"], 4 | "exclude": ["src/**/*.test.ts", "src/**/*.test.utils.ts", "tests"], 5 | 6 | "compilerOptions": { 7 | "outDir": "./dist", 8 | "noEmit": false 9 | }, 10 | 11 | "typedocOptions": { 12 | "entryPoints": ["./src/index.ts"], 13 | "entryPointStrategy": "expand", 14 | "plugin": "typedoc-plugin-markdown", 15 | "excludeExternals": true, 16 | "externalPattern": ["**/node_modules/**"], 17 | "excludePrivate": true, 18 | "out": "docs" 19 | } 20 | } -------------------------------------------------------------------------------- /src/env-utils.ts: -------------------------------------------------------------------------------- 1 | export function getFloat(name: string, defaultValue: number): number { 2 | return parseFloat(process.env[name] || '0') || defaultValue; 3 | } 4 | export function getBoolean(name: string, defaultValue: boolean): boolean { 5 | const value = process.env[name]; 6 | if (value === 'true' || value === '1') return true; 7 | if (value === 'false' || value === '0') return false; 8 | return defaultValue; 9 | } 10 | export function getInt(name: string, defaultValue: number): number { 11 | return Math.trunc(getFloat(name, defaultValue)); 12 | } 13 | export function getBigInt(name: string, defaultValue: bigint): bigint { 14 | const v = (process.env[name] || '0').match(/^\d+/)?.[0] || '0'; 15 | return BigInt(v) || defaultValue; 16 | } 17 | -------------------------------------------------------------------------------- /tests/conversions.test.ts: -------------------------------------------------------------------------------- 1 | import { helpers } from '@debridge-finance/solana-utils'; 2 | import assert from 'assert'; 3 | import 'mocha'; 4 | import { U256 } from '../src/orderFeeds/u256-utils'; 5 | 6 | function testConversion() { 7 | it('can convert hex to bigint', () => { 8 | const buf = helpers.hexToBuffer( 9 | '0x0000000000000000000000000000000000000000000000000000000000736f6c', 10 | ); 11 | assert.equal(BigInt(7565164), U256.fromBytesBE(buf).toBigInt()); 12 | const buf2 = helpers.hexToBuffer( 13 | '0x0000000000000000000000000000000000000000000000000000000000000064', 14 | ); 15 | assert.equal(BigInt(100), U256.fromBytesBE(buf2).toBigInt()); 16 | }); 17 | } 18 | 19 | describe('Can convert between various formats', testConversion); 20 | -------------------------------------------------------------------------------- /src/filters/white.listed.orderid.ts: -------------------------------------------------------------------------------- 1 | import { Order, OrderData } from '@debridge-finance/dln-client'; 2 | 3 | import { FilterContext, OrderFilter, OrderFilterInitializer } from './order.filter'; 4 | 5 | export function whitelistedOrderId(orderIds: string[]): OrderFilterInitializer { 6 | return async ( 7 | /* chainId: ChainId */ {}, 8 | /* context: OrderFilterInitContext */ {}, 9 | ): Promise => 10 | async (order: OrderData, context: FilterContext): Promise => { 11 | const logger = context.logger.child({ filter: 'whitelistedOrderId' }); 12 | const result = orderIds.some((orderId) => orderId === Order.calculateId(order)); 13 | 14 | logger.info(`approve status: ${result}, orderId is whitelisted`); 15 | return Promise.resolve(result); 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/filters/disable.fulfill.ts: -------------------------------------------------------------------------------- 1 | import { FilterContext, OrderFilterInitializer } from './order.filter'; 2 | 3 | /** 4 | * Prevents orders coming to the given chain from fulfillment. 5 | * This filter is useful to filter off orders that are targeted to the chain you don't want to fulfill in, which is still needed to be presented in the configuration file to enable orders coming from this chain. 6 | */ 7 | export function disableFulfill(): OrderFilterInitializer { 8 | return async (/* chainId: ChainId */ {}, /* context: OrderFilterInitContext */ {}) => 9 | async (/* order: OrderData */ {}, context: FilterContext) => { 10 | const result = false; 11 | const logger = context.logger.child({ filter: 'disableFulfill' }); 12 | logger.info(`approve status: ${result}`); 13 | return Promise.resolve(false); 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/filters/order.filter.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, OrderData } from '@debridge-finance/dln-client'; 2 | import { Logger } from 'pino'; 3 | 4 | import { ExecutorSupportedChain, IExecutor } from '../executor'; 5 | 6 | export interface FilterContext { 7 | logger: Logger; 8 | config: IExecutor; 9 | giveChain: ExecutorSupportedChain; 10 | takeChain: ExecutorSupportedChain; 11 | } 12 | 13 | export type OrderFilterInitContext = { 14 | logger: Logger; 15 | }; 16 | 17 | export type OrderFilterInitializer = ( 18 | chainId: ChainId, 19 | context: OrderFilterInitContext, 20 | ) => Promise; 21 | 22 | /** 23 | * Represents an order filter routine. Can be chained. 24 | * Returns true if order can be processed, false otherwise. 25 | * 26 | */ 27 | export type OrderFilter = (order: OrderData, context: FilterContext) => Promise; 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18.12.1 as builder 2 | 3 | 4 | WORKDIR /build 5 | 6 | COPY package.json /build 7 | COPY package-lock.json /build 8 | RUN npm install 9 | 10 | 11 | 12 | COPY tsconfig.json /build 13 | COPY tsconfig.base.json /build 14 | COPY tsconfig.cjs.json /build 15 | COPY tsconfig.esm.json /build 16 | COPY src /build/src 17 | 18 | FROM node:18.12.1-alpine 19 | WORKDIR /app 20 | COPY --from=builder /build/node_modules /app/node_modules 21 | COPY --from=builder /build/package.json /app 22 | COPY --from=builder /build/package-lock.json /app 23 | COPY --from=builder /build/tsconfig.json /app 24 | COPY --from=builder /build/tsconfig.base.json /app 25 | COPY --from=builder /build/tsconfig.cjs.json /app 26 | COPY --from=builder /build/tsconfig.esm.json /app 27 | COPY --from=builder /build/src /app/src 28 | COPY debridge.config.ts /app 29 | 30 | CMD npm run executor debridge.config.ts 31 | -------------------------------------------------------------------------------- /docs/Solana-key-management.md: -------------------------------------------------------------------------------- 1 | 2 | ### Generating keypair for solana 3 | 4 | You can generate keypair for solana using `@solana/web3.js` library or using Solana CLI 5 | 6 | #### Solana CLI keypair generation 7 | 1. Install [Solana CLI](https://docs.solana.com/ru/cli/install-solana-cli-tools) 8 | 2. Generate keypair using [Paper Wallet](https://docs.solana.com/ru/wallet-guide/paper-wallet#creating-a-paper-wallet) which supports both creating kp from scratch and seed phrase derivation 9 | 10 | #### JS Keypair generation 11 | 1. Install @solana/web3.js - `npm i @solana/web3.js` 12 | 2. Execute following code 13 | ```ts 14 | import { Keypair } from "@solana/web3.js"; 15 | 16 | const kp = Keypair.generate(); 17 | const pubkey = kp.publicKey.toBase58(); 18 | const priv = Buffer.from(kp.secretKey).toString("hex"); 19 | 20 | console.log(`Private key (hex-encoded) : 0x${priv}, public (base58): ${pubkey}!`) 21 | ``` -------------------------------------------------------------------------------- /src/chain-evm/preferences/BNB.ts: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3'; 2 | import { getBigInt } from '../../env-utils'; 3 | import { GasCategory } from '../fees/types'; 4 | import { SuggestedOpts } from './store'; 5 | 6 | const MIN_GAS_PRICE = getBigInt('EVM_BNB_MIN_GAS_PRICE', 3_000_000_000n); 7 | const MAX_GAS_PRICE = getBigInt('EVM_BNB_MAX_GAS_PRICE', 5_000_000_000n); 8 | 9 | export const legacyFeeFetcher = async ( 10 | gasCategory: GasCategory, 11 | connection: Web3, 12 | ): Promise => { 13 | const minGasPrice = gasCategory === GasCategory.AGGRESSIVE ? MAX_GAS_PRICE : MIN_GAS_PRICE; 14 | let gasPrice = BigInt(await connection.eth.getGasPrice()); 15 | if (gasPrice < minGasPrice) gasPrice = minGasPrice; 16 | if (gasPrice > MAX_GAS_PRICE) gasPrice = MAX_GAS_PRICE; 17 | return gasPrice; 18 | }; 19 | 20 | export const suggestedOpts: SuggestedOpts = { 21 | feeHandlers: () => ({ 22 | legacyFeeFetcher, 23 | }), 24 | }; 25 | -------------------------------------------------------------------------------- /src/chain-evm/preferences/BNB.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Web3 from 'web3'; 3 | import { GasCategory } from '../fees/types'; 4 | import { legacyFeeFetcher } from './BNB'; 5 | 6 | describe('BNB', () => { 7 | it('legacyFeeFetcher must return corrected values for projected', async () => { 8 | const testSuite = [ 9 | [2_000_000_000n, 3_000_000_000n], 10 | [3_000_000_000n, 3_000_000_000n], 11 | [4_000_000_000n, 4_000_000_000n], 12 | [5_000_000_000n, 5_000_000_000n], 13 | [6_000_000_000n, 5_000_000_000n], 14 | ]; 15 | 16 | for (const [mocked, expected] of testSuite) { 17 | const conn = { 18 | eth: { 19 | getGasPrice: async () => mocked.toString(), 20 | }, 21 | }; 22 | assert.equal( 23 | await legacyFeeFetcher(GasCategory.PROJECTED, conn), 24 | expected, 25 | `${mocked} mocked`, 26 | ); 27 | } 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | // Some settings automatically inherited from .editorconfig 2 | 3 | module.exports = { 4 | printWidth: 100, // https://github.com/airbnb/javascript#19.13 5 | tabWidth: 2, // https://github.com/airbnb/javascript#19.1 6 | useTabs: false, // https://github.com/airbnb/javascript#19.1 7 | semi: true, // https://github.com/airbnb/javascript#21.1 8 | singleQuote: true, // https://github.com/airbnb/javascript#6.1 9 | quoteProps: 'as-needed', // https://github.com/airbnb/javascript#3.6 10 | jsxSingleQuote: false, // https://github.com/airbnb/javascript/tree/master/react#quotes 11 | trailingComma: 'all', // https://github.com/airbnb/javascript#20.2 12 | bracketSpacing: true, // https://github.com/airbnb/javascript#19.12 13 | arrowParens: 'always', // https://github.com/airbnb/javascript#8.4 14 | overrides: [ 15 | { 16 | files: '.editorconfig', 17 | options: { parser: 'yaml' }, 18 | }, 19 | ] 20 | }; -------------------------------------------------------------------------------- /src/filters/index.ts: -------------------------------------------------------------------------------- 1 | import { blacklistedGiveToken } from './black.listed.give.token'; 2 | import { blacklistedTakeToken } from './black.listed.take.token'; 3 | import { disableFulfill } from './disable.fulfill'; 4 | import { giveAmountUsdEquivalentBetween } from './give.amount.usd.equivalent.between'; 5 | import { OrderFilter } from './order.filter'; 6 | import { takeAmountUsdEquivalentBetween } from './take.amount.usd.equivalent.between'; 7 | import { whitelistedGiveToken } from './white.listed.give.token'; 8 | import { whitelistedMaker } from './white.listed.marker'; 9 | import { whitelistedReceiver } from './white.listed.receiver'; 10 | import { whitelistedTakeToken } from './white.listed.take.token'; 11 | import { whitelistedOrderId } from './white.listed.orderid'; 12 | 13 | export { 14 | blacklistedGiveToken, 15 | blacklistedTakeToken, 16 | disableFulfill, 17 | giveAmountUsdEquivalentBetween, 18 | OrderFilter, 19 | takeAmountUsdEquivalentBetween, 20 | whitelistedGiveToken, 21 | whitelistedMaker, 22 | whitelistedTakeToken, 23 | whitelistedReceiver, 24 | whitelistedOrderId, 25 | }; 26 | -------------------------------------------------------------------------------- /src/chain-common/order-evaluation-context.ts: -------------------------------------------------------------------------------- 1 | import { SwapConnectorResult } from '@debridge-finance/dln-client'; 2 | import { assert } from '../errors'; 3 | 4 | export type OrderEvaluationPayload = { 5 | preFulfillSwap?: SwapConnectorResult; 6 | validationPreFulfillSwap?: SwapConnectorResult; 7 | } & { 8 | [key in string]: any; 9 | }; 10 | 11 | export abstract class OrderEvaluationContextual { 12 | readonly #payload: OrderEvaluationPayload = {}; 13 | 14 | constructor(base?: OrderEvaluationPayload) { 15 | if (base) this.#payload = base; 16 | } 17 | 18 | protected setPayloadEntry( 19 | key: K, 20 | value: OrderEvaluationPayload[K], 21 | ) { 22 | assert(this.#payload[key] === undefined, `accidentally overwriting the ${key} payload entry`); 23 | this.#payload[key] = value; 24 | } 25 | 26 | protected getPayloadEntry(key: string): T { 27 | assert(typeof this.#payload[key] !== undefined, `payload does not contain entry "${key}"`); 28 | 29 | return this.#payload[key]; 30 | } 31 | 32 | protected get payload() { 33 | return this.#payload; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/chain-evm/fees/fetcher-legacy.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from 'pino'; 2 | import Web3 from 'web3'; 3 | import { safeIntToBigInt } from '../../utils'; 4 | import { defaultFeeManagerOpts } from './defaults'; 5 | import { GasCategory } from './types'; 6 | 7 | export const getLegacyFeeFetcher = 8 | (multipliers: { [key in GasCategory]: number }) => 9 | async (gasCategory: GasCategory, connection: Web3, logger?: Logger): Promise => { 10 | const gasPrice = BigInt(await connection.eth.getGasPrice()); 11 | const finalGasPrice = (gasPrice * safeIntToBigInt(multipliers[gasCategory] * 10_000)) / 10_000n; 12 | logger?.debug( 13 | `retrieved legacy gasPrice: ${finalGasPrice} (gasPrice=${gasPrice}, gasCategory: ${GasCategory[gasCategory]}, multiplier: ${multipliers[gasCategory]})`, 14 | ); 15 | return finalGasPrice; 16 | }; 17 | 18 | export const defaultLegacyFeeFetcher = getLegacyFeeFetcher({ 19 | [GasCategory.PROJECTED]: defaultFeeManagerOpts.legacyGasPriceProjectedMultiplier, 20 | [GasCategory.NORMAL]: defaultFeeManagerOpts.legacyGasPriceNormalMultiplier, 21 | [GasCategory.AGGRESSIVE]: defaultFeeManagerOpts.legacyGasPriceAggressiveMultiplier, 22 | }); 23 | -------------------------------------------------------------------------------- /src/dln-ts-client.utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CommonDlnClient, 3 | Evm, 4 | Solana, 5 | Logger as ClientLogger, 6 | LogLevel, 7 | } from '@debridge-finance/dln-client'; 8 | import { Logger } from 'pino'; 9 | 10 | export const createClientLogger = (logger: Logger) => 11 | new ClientLogger((level: LogLevel, args) => { 12 | // concat args so they appear as a first string in pino 13 | const message = args 14 | .reduce((result, currentValue) => { 15 | let currentString = currentValue; 16 | if (typeof currentValue === 'object') { 17 | currentString = JSON.stringify(currentValue); 18 | } 19 | return `${result} ${currentString}`; 20 | }, '') 21 | .trim(); 22 | switch (level) { 23 | case LogLevel.LOG: 24 | case LogLevel.VERBOSE: { 25 | logger.debug(`[dln-ts-client] ${message}`); 26 | break; 27 | } 28 | 29 | case LogLevel.ERROR: 30 | default: { 31 | logger.error(`[dln-ts-client] ${message}`); 32 | break; 33 | } 34 | } 35 | }); 36 | 37 | type ActiveClients = Solana.DlnClient | Evm.DlnClient; 38 | export type DlnClient = CommonDlnClient; 39 | -------------------------------------------------------------------------------- /.github/workflows/npm_publish.yml: -------------------------------------------------------------------------------- 1 | name: publish_npm_release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | # Specify the environment to use its secrets 11 | environment: 12 | name: main # Replace with the actual environment name 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 18 18 | registry-url: 'https://registry.npmjs.org' 19 | cache: 'npm' 20 | - name: Check release tag 21 | run: | 22 | VERSION=$(node -p "const ver=require('./package.json').version; ver.startsWith('v') ? ver : 'v' + ver") 23 | TAG=${{ github.ref }} 24 | TAG=${TAG#refs/tags/} 25 | if [ "$VERSION" != "$TAG" ]; then 26 | echo "Error: The release tag ($TAG) does not match the version ($VERSION) in package.json" 27 | exit 1 28 | fi 29 | - run: npm ci 30 | - run: npm run lint 31 | - run: npm run test 32 | - run: npm run build --if-present 33 | - run: npm publish 34 | env: 35 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '@debridge-finance/dln-client'; 2 | import { SubsidizationRule } from '@debridge-finance/legacy-dln-profitability'; 3 | import configurator from './configurator/index'; 4 | import * as filters from './filters'; 5 | import { ExecutorLaunchConfig } from './config'; 6 | import * as environments from './environments'; 7 | import { setCurrentEnvironment } from './environments'; 8 | import { WsNextOrder as TempWsNextOrder } from './orderFeeds/ws.order.feed'; 9 | import { Hooks } from './hooks/HookEnums'; 10 | /** 11 | * Will get rid of this when developing ExecutorLaunchConfigV3 #862kawyur 12 | */ 13 | const CURRENT_ENVIRONMENT = environments.PRODUCTION; 14 | 15 | /** 16 | * Will get rid of this when developing ExecutorLaunchConfigV3 #862kawyur 17 | */ 18 | const WsNextOrder = TempWsNextOrder; 19 | 20 | export { 21 | // configuration 22 | ChainId, 23 | configurator, 24 | filters, 25 | ExecutorLaunchConfig, 26 | SubsidizationRule, 27 | 28 | // environment 29 | environments, 30 | setCurrentEnvironment, 31 | 32 | // hooks 33 | Hooks, 34 | 35 | // The following exports are required to support legacy ExecutorLaunchConfig v2 36 | CURRENT_ENVIRONMENT, 37 | WsNextOrder, 38 | }; 39 | -------------------------------------------------------------------------------- /src/filters/white.listed.marker.ts: -------------------------------------------------------------------------------- 1 | import { 2 | buffersAreEqual, 3 | ChainId, 4 | OrderData, 5 | tokenStringToBuffer, 6 | } from '@debridge-finance/dln-client'; 7 | import { helpers } from '@debridge-finance/solana-utils'; 8 | 9 | import { FilterContext, OrderFilter, OrderFilterInitializer } from './order.filter'; 10 | 11 | /** 12 | * Checks if the address who placed the order on the source chain is in the whitelist. 13 | * This filter is useful to filter out orders placed by the trusted parties. 14 | */ 15 | export function whitelistedMaker(addresses: string[]): OrderFilterInitializer { 16 | return async ( 17 | chainId: ChainId, 18 | /* context: OrderFilterInitContext */ {}, 19 | ): Promise => { 20 | const addressesBuffer = addresses.map((address) => tokenStringToBuffer(chainId, address)); 21 | return async (order: OrderData, context: FilterContext): Promise => { 22 | const logger = context.logger.child({ filter: 'WhiteListedMarker' }); 23 | const result = addressesBuffer.some((address) => buffersAreEqual(order.maker, address)); 24 | 25 | const maker = helpers.bufferToHex(Buffer.from(order.maker)); 26 | logger.info(`approve status: ${result}, maker ${maker}`); 27 | return Promise.resolve(result); 28 | }; 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/filters/black.listed.give.token.ts: -------------------------------------------------------------------------------- 1 | import { 2 | buffersAreEqual, 3 | ChainId, 4 | OrderData, 5 | tokenStringToBuffer, 6 | } from '@debridge-finance/dln-client'; 7 | import { helpers } from '@debridge-finance/solana-utils'; 8 | 9 | import { FilterContext, OrderFilterInitializer } from './order.filter'; 10 | 11 | /** 12 | * Checks if the order's locked token is not in the blacklist. 13 | * This filter is useful to filter off orders that hold undesired and/or illiquid tokens. 14 | */ 15 | export function blacklistedGiveToken(addresses: string[]): OrderFilterInitializer { 16 | return async (chainId: ChainId, /* context: OrderFilterInitContext */ {}) => { 17 | const addressesBuffer = addresses.map((address) => tokenStringToBuffer(chainId, address)); 18 | return async (order: OrderData, context: FilterContext) => { 19 | const logger = context.logger.child({ 20 | filter: 'blackListedGiveToken', 21 | }); 22 | const result = addressesBuffer.some((address) => 23 | buffersAreEqual(order.give.tokenAddress, address), 24 | ); 25 | 26 | const giveToken = !helpers.bufferToHex(Buffer.from(order.give.tokenAddress)); 27 | logger.info(`approve status: ${result}, giveToken ${giveToken}`); 28 | return Promise.resolve(result); 29 | }; 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /src/filters/black.listed.take.token.ts: -------------------------------------------------------------------------------- 1 | import { 2 | buffersAreEqual, 3 | ChainId, 4 | OrderData, 5 | tokenStringToBuffer, 6 | } from '@debridge-finance/dln-client'; 7 | import { helpers } from '@debridge-finance/solana-utils'; 8 | 9 | import { FilterContext, OrderFilterInitializer } from './order.filter'; 10 | 11 | /** 12 | * Checks if the order's requested token is not in the blacklist. 13 | * This filter is useful to filter off orders that requested undesired and/or illiquid tokens. 14 | */ 15 | 16 | export function blacklistedTakeToken(addresses: string[]): OrderFilterInitializer { 17 | return async (chainId: ChainId, /* context: OrderFilterInitContext */ {}) => { 18 | const addressesBuffer = addresses.map((address) => tokenStringToBuffer(chainId, address)); 19 | return async (order: OrderData, context: FilterContext) => { 20 | const logger = context.logger.child({ 21 | filter: 'blackListedTakeToken', 22 | }); 23 | const result = !addressesBuffer.some((address) => 24 | buffersAreEqual(order.take.tokenAddress, address), 25 | ); 26 | 27 | const takeToken = helpers.bufferToHex(Buffer.from(order.take.tokenAddress)); 28 | logger.info(`approve status: ${result}, takeToken ${takeToken}`); 29 | return Promise.resolve(result); 30 | }; 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/filters/white.listed.receiver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | buffersAreEqual, 3 | ChainId, 4 | OrderData, 5 | tokenStringToBuffer, 6 | } from '@debridge-finance/dln-client'; 7 | import { helpers } from '@debridge-finance/solana-utils'; 8 | 9 | import { FilterContext, OrderFilter, OrderFilterInitializer } from './order.filter'; 10 | 11 | /** 12 | * Checks if the receiver address (who will take funds upon successful order fulfillment) is in the whitelist. 13 | * This filter is useful to filter out orders placed by the trusted parties. 14 | */ 15 | export function whitelistedReceiver(addresses: string[]): OrderFilterInitializer { 16 | return async ( 17 | chainId: ChainId, 18 | /* context: OrderFilterInitContext */ {}, 19 | ): Promise => { 20 | const addressesBuffer = addresses.map((address) => tokenStringToBuffer(chainId, address)); 21 | return async (order: OrderData, context: FilterContext): Promise => { 22 | const logger = context.logger.child({ filter: 'WhiteListedReceiver' }); 23 | const result = addressesBuffer.some((address) => buffersAreEqual(order.receiver, address)); 24 | 25 | const receiver = helpers.bufferToHex(Buffer.from(order.receiver)); 26 | logger.info(`approve status: ${result}, receiver ${receiver}`); 27 | return Promise.resolve(result); 28 | }; 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/filters/white.listed.take.token.ts: -------------------------------------------------------------------------------- 1 | import { 2 | buffersAreEqual, 3 | ChainId, 4 | OrderData, 5 | tokenStringToBuffer, 6 | } from '@debridge-finance/dln-client'; 7 | import { helpers } from '@debridge-finance/solana-utils'; 8 | 9 | import { FilterContext, OrderFilter, OrderFilterInitializer } from './order.filter'; 10 | 11 | /** 12 | * Checks if the order's requested token is in the whitelist. 13 | * This filter is useful to target orders that request specific tokens. 14 | */ 15 | export function whitelistedTakeToken(addresses: string[]): OrderFilterInitializer { 16 | return async ( 17 | chainId: ChainId, 18 | /* context: OrderFilterInitContext */ {}, 19 | ): Promise => { 20 | const addressesBuffer = addresses.map((address) => tokenStringToBuffer(chainId, address)); 21 | return async (order: OrderData, context: FilterContext): Promise => { 22 | const logger = context.logger.child({ 23 | filter: 'WhiteListedTakeToken', 24 | }); 25 | const result = addressesBuffer.some((address) => 26 | buffersAreEqual(order.take.tokenAddress, address), 27 | ); 28 | 29 | const takeToken = helpers.bufferToHex(Buffer.from(order.take.tokenAddress)); 30 | logger.info(`approve status: ${result}, takeToken ${takeToken}`); 31 | return Promise.resolve(result); 32 | }; 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/filters/white.listed.give.token.ts: -------------------------------------------------------------------------------- 1 | import { 2 | buffersAreEqual, 3 | ChainId, 4 | OrderData, 5 | tokenStringToBuffer, 6 | } from '@debridge-finance/dln-client'; 7 | import { helpers } from '@debridge-finance/solana-utils'; 8 | 9 | import { FilterContext, OrderFilter, OrderFilterInitializer } from './order.filter'; 10 | 11 | /** 12 | * Checks if the address who placed the order on the source chain is in the whitelist. 13 | * This filter is useful to filter out orders placed by the trusted parties. 14 | */ 15 | export function whitelistedGiveToken(addresses: string[]): OrderFilterInitializer { 16 | return async ( 17 | chainId: ChainId, 18 | /* context: OrderFilterInitContext */ {}, 19 | ): Promise => { 20 | const addressesBuffer = addresses.map((address) => tokenStringToBuffer(chainId, address)); 21 | return async (order: OrderData, context: FilterContext): Promise => { 22 | const logger = context.logger.child({ 23 | filter: 'WhiteListedGiveToken', 24 | }); 25 | const result = addressesBuffer.some((address) => 26 | buffersAreEqual(order.give.tokenAddress, address), 27 | ); 28 | 29 | const giveToken = helpers.bufferToHex(Buffer.from(order.give.tokenAddress)); 30 | logger.info(`approve status: ${result}, giveToken ${giveToken}`); 31 | return Promise.resolve(result); 32 | }; 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/chain-evm/fees/fetcher-legacy.test.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import Web3 from 'web3'; 3 | import chaiAsPromised from 'chai-as-promised'; 4 | import { getLegacyFeeFetcher } from './fetcher-legacy'; 5 | import { GasCategory } from './types'; 6 | import { createTestFrameworkLogger } from '../../../tests/helpers/index'; 7 | 8 | chai.use(chaiAsPromised); 9 | 10 | describe('Legacy fee fetching', () => { 11 | it('legacyFeeFetcher should correctly pick a gas price', async () => { 12 | const gasCategoryMultipliers = { 13 | [GasCategory.PROJECTED]: 1, 14 | [GasCategory.NORMAL]: 1.11, 15 | [GasCategory.AGGRESSIVE]: 3.21, 16 | }; 17 | 18 | const mockedGasPrice = 10_000_000_000n.toString(); 19 | const expectedFees: Array<[gasCategory: GasCategory, b: bigint]> = [ 20 | [GasCategory.PROJECTED, 10_000_000_000n], 21 | [GasCategory.NORMAL, 11_100_000_000n], 22 | [GasCategory.AGGRESSIVE, 32_100_000_000n], 23 | ]; 24 | 25 | const conn = { 26 | eth: { 27 | getGasPrice: async () => mockedGasPrice.toString(), 28 | }, 29 | }; 30 | for (const [gasCategory, expectedGasPrice] of expectedFees) { 31 | const feeFetcher = getLegacyFeeFetcher(gasCategoryMultipliers); 32 | expect(await feeFetcher(gasCategory, conn, createTestFrameworkLogger())).to.eq( 33 | expectedGasPrice, 34 | `mocked: ${GasCategory[gasCategory]}`, 35 | ); 36 | } 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/chain-solana/tx-generators/createOrderFullfillTx.ts: -------------------------------------------------------------------------------- 1 | import { ChainEngine } from '@debridge-finance/dln-client'; 2 | import { Logger } from 'pino'; 3 | import { VersionedTransaction } from '@solana/web3.js'; 4 | import { createClientLogger } from '../../dln-ts-client.utils'; 5 | import { OrderEstimation } from '../../chain-common/order-estimator'; 6 | import { assert } from '../../errors'; 7 | 8 | export async function createOrderFullfillTx( 9 | estimation: OrderEstimation, 10 | logger: Logger, 11 | ): Promise { 12 | const { order } = estimation; 13 | if (estimation.order.route.requiresSwap) { 14 | const swapResult = estimation.payload.preFulfillSwap; 15 | assert(swapResult !== undefined, 'missing preFulfillSwap payload entry'); 16 | 17 | return order.executor.client.preswapAndFulfillOrder( 18 | { 19 | order: order.getWithId(), 20 | taker: order.takeChain.fulfillAuthority.bytesAddress, 21 | swapResult, 22 | loggerInstance: createClientLogger(logger), 23 | }, 24 | { 25 | unlockAuthority: order.takeChain.unlockAuthority.bytesAddress, 26 | computeUnitsLimit: 600_000, 27 | }, 28 | ); 29 | } 30 | 31 | return order.executor.client.fulfillOrder( 32 | { 33 | order: order.getWithId(), 34 | loggerInstance: createClientLogger(logger), 35 | }, 36 | { 37 | taker: order.takeChain.fulfillAuthority.bytesAddress, 38 | unlockAuthority: order.takeChain.unlockAuthority.bytesAddress, 39 | }, 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/hooks/types/HookParams.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, OrderData } from '@debridge-finance/dln-client'; 2 | import { IExecutor } from '../../executor'; 3 | import { Hooks, PostponingReason, RejectionReason } from '../HookEnums'; 4 | 5 | export type HookParams = {} & (T extends Hooks.OrderFeedConnected 6 | ? { 7 | message: string; 8 | } 9 | : {}) & 10 | (T extends Hooks.OrderFeedDisconnected 11 | ? { 12 | message: string; 13 | } 14 | : {}) & 15 | (T extends Hooks.OrderFulfilled 16 | ? { 17 | orderId: string; 18 | order: OrderData; 19 | txHash: string; 20 | } 21 | : {}) & 22 | (T extends Hooks.OrderPostponed 23 | ? { 24 | orderId: string; 25 | order: OrderData; 26 | isLive: boolean; 27 | reason: PostponingReason; 28 | message: string; 29 | attempts: number; 30 | executor: IExecutor; 31 | } 32 | : {}) & 33 | (T extends Hooks.OrderUnlockFailed 34 | ? { 35 | orderIds: string[]; 36 | fromChainId: ChainId; 37 | toChainId: ChainId; 38 | message: string; 39 | } 40 | : {}) & 41 | (T extends Hooks.OrderUnlockSent 42 | ? { 43 | orderIds: string[]; 44 | fromChainId: ChainId; 45 | toChainId: ChainId; 46 | txHash: string; 47 | } 48 | : {}) & 49 | (T extends Hooks.OrderRejected 50 | ? { 51 | orderId: string; 52 | order: OrderData; 53 | isLive: boolean; 54 | reason: RejectionReason; 55 | message: string; 56 | attempts: number; 57 | executor: IExecutor; 58 | } 59 | : {}); 60 | -------------------------------------------------------------------------------- /src/chain-evm/networking/broadcaster.test.utils.ts: -------------------------------------------------------------------------------- 1 | // pragma solidity ^0.8.7; 2 | // 3 | // contract TickTock { 4 | // bool isTock; 5 | // 6 | // function set(bool flag) external { 7 | // isTock = flag; 8 | // } 9 | // 10 | // function guess(bool value) external { 11 | // require(isTock == value, 'wrong guess'); 12 | // } 13 | // } 14 | 15 | export const TickTockABI = [ 16 | { 17 | inputs: [ 18 | { 19 | internalType: 'bool', 20 | name: 'value', 21 | type: 'bool', 22 | }, 23 | ], 24 | name: 'guess', 25 | outputs: [], 26 | stateMutability: 'nonpayable', 27 | type: 'function', 28 | }, 29 | { 30 | inputs: [ 31 | { 32 | internalType: 'bool', 33 | name: 'flag', 34 | type: 'bool', 35 | }, 36 | ], 37 | name: 'set', 38 | outputs: [], 39 | stateMutability: 'nonpayable', 40 | type: 'function', 41 | }, 42 | ] as const; 43 | 44 | export const TickTockByteCode = 45 | '0x608060405234801561001057600080fd5b50610162806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80635f76f6ab1461003b578063e0a8f09a1461007c575b600080fd5b61007a610049366004610103565b600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016911515919091179055565b005b61007a61008a366004610103565b60005460ff16151581151514610100576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152600b60248201527f77726f6e67206775657373000000000000000000000000000000000000000000604482015260640160405180910390fd5b50565b60006020828403121561011557600080fd5b8135801515811461012557600080fd5b939250505056fea2646970667358221220383f4cd3edfbe6deba0940ea783e6ff9a84493373a93732bf6e3d48a947eb42764736f6c63430008070033'; 46 | -------------------------------------------------------------------------------- /src/chain-common/tx-builder.ts: -------------------------------------------------------------------------------- 1 | import { OrderDataWithId } from '@debridge-finance/dln-client'; 2 | import { Logger, LoggerOptions } from 'pino'; 3 | import { InitTransactionBuilder } from '../processor'; 4 | import { BatchUnlockTransactionBuilder } from '../processors/BatchUnlocker'; 5 | import { OrderEstimation } from './order-estimator'; 6 | import { FulfillTransactionBuilder } from './order-taker'; 7 | 8 | export type TxHash = string; 9 | 10 | export type TransactionSender = { 11 | (): Promise; 12 | }; 13 | 14 | export class TransactionBuilder 15 | implements FulfillTransactionBuilder, BatchUnlockTransactionBuilder, InitTransactionBuilder 16 | { 17 | constructor( 18 | private readonly initTransactionBuilder: InitTransactionBuilder, 19 | private readonly fulfillTransactionBuilder: FulfillTransactionBuilder, 20 | private readonly unlockTransactionBuilder: BatchUnlockTransactionBuilder, 21 | ) {} 22 | 23 | get fulfillAuthority() { 24 | return this.fulfillTransactionBuilder.fulfillAuthority; 25 | } 26 | 27 | get unlockAuthority() { 28 | return this.unlockTransactionBuilder.unlockAuthority; 29 | } 30 | 31 | getOrderFulfillTxSender( 32 | orderEstimation: OrderEstimation, 33 | logger: Logger, 34 | ): TransactionSender { 35 | return this.fulfillTransactionBuilder.getOrderFulfillTxSender(orderEstimation, logger); 36 | } 37 | 38 | getBatchOrderUnlockTxSender( 39 | orders: OrderDataWithId[], 40 | logger: Logger, 41 | ): TransactionSender { 42 | return this.unlockTransactionBuilder.getBatchOrderUnlockTxSender(orders, logger); 43 | } 44 | 45 | getInitTxSenders(logger: Logger): Promise { 46 | return this.initTransactionBuilder.getInitTxSenders(logger); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/chain-solana/signer.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '@debridge-finance/dln-client'; 2 | import { helpers } from '@debridge-finance/solana-utils'; 3 | import { Connection, Keypair, Transaction, VersionedTransaction } from '@solana/web3.js'; 4 | import { Logger } from 'pino'; 5 | import { Authority } from '../interfaces'; 6 | 7 | export type SolanaTxContext = { 8 | logger: Logger; 9 | options: Parameters['3']; 10 | }; 11 | 12 | export class SolanaTxSigner implements Authority { 13 | private readonly wallet: helpers.Wallet; 14 | 15 | constructor( 16 | private readonly connection: Connection, 17 | wallet: Keypair, 18 | ) { 19 | this.wallet = new helpers.Wallet(wallet); 20 | } 21 | 22 | public get address(): string { 23 | return helpers.bufferToHex(this.wallet.publicKey.toBuffer()); 24 | } 25 | 26 | public get bytesAddress(): Uint8Array { 27 | return this.wallet.publicKey.toBuffer(); 28 | } 29 | 30 | async sendTransaction( 31 | data: VersionedTransaction | Transaction, 32 | context: SolanaTxContext, 33 | ): Promise { 34 | const [tx] = await this.sendTransactions(data, context); 35 | return tx; 36 | } 37 | 38 | async sendTransactions( 39 | data: VersionedTransaction | Transaction | Array, 40 | context: SolanaTxContext, 41 | ): Promise> { 42 | const logger = context.logger.child({ 43 | service: SolanaTxSigner.name, 44 | currentChainId: ChainId.Solana, 45 | }); 46 | 47 | return helpers.sendAll(this.connection, this.wallet, data, { 48 | rpcCalls: 3, 49 | skipPreflight: false, 50 | logger: (...args: any) => logger.debug(args), // sendAll will log base64 tx data sent to blockchain 51 | ...context.options, 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/chain-solana/tx-generators/createBatchOrderUnlockTx.ts: -------------------------------------------------------------------------------- 1 | import { ChainEngine, OrderDataWithId, OrderEstimationStage } from '@debridge-finance/dln-client'; 2 | import { Logger } from 'pino'; 3 | import { assert } from '../../errors'; 4 | import { IExecutor } from '../../executor'; 5 | import { createClientLogger } from '../../dln-ts-client.utils'; 6 | 7 | export async function createBatchOrderUnlockTx( 8 | executor: IExecutor, 9 | orders: Array, 10 | logger: Logger, 11 | ) { 12 | assert(orders.length > 0, 'empty array of orders given for batch unlock'); 13 | 14 | const order = orders[0]; 15 | const giveChain = executor.getSupportedChain(order.give.chainId); 16 | const takeChain = executor.getSupportedChain(order.take.chainId); 17 | 18 | const [giveNativePrice, takeNativePrice] = await Promise.all([ 19 | executor.tokenPriceService.getPrice(giveChain.chain, null, { 20 | logger: createClientLogger(logger), 21 | }), 22 | executor.tokenPriceService.getPrice(takeChain.chain, null, { 23 | logger: createClientLogger(logger), 24 | }), 25 | ]); 26 | 27 | const fees = await executor.client.getClaimExecutionFee( 28 | { 29 | action: 'ClaimBatchUnlock', 30 | giveChain: giveChain.chain, 31 | giveNativePrice, 32 | takeChain: takeChain.chain, 33 | takeNativePrice, 34 | batchSize: orders.length, 35 | loggerInstance: createClientLogger(logger), 36 | }, 37 | { 38 | orderEstimationStage: OrderEstimationStage.OrderFulfillment, 39 | }, 40 | ); 41 | 42 | return executor.client.sendBatchUnlock( 43 | { 44 | beneficiary: giveChain.unlockBeneficiary, 45 | executionFee: fees.total, 46 | loggerInstance: createClientLogger(logger), 47 | orders, 48 | }, 49 | { 50 | unlocker: takeChain.unlockAuthority.bytesAddress, 51 | }, 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/processors/StatsAPI.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | type MultiFormatRepresentation = { 4 | Base64Value: string; 5 | bytesArrayValue: string; 6 | stringValue: string; 7 | }; 8 | 9 | type GetForUnlockAuthoritiesResponse = { 10 | orders: Array<{ 11 | orderId: MultiFormatRepresentation; 12 | giveTokenAddress: MultiFormatRepresentation; 13 | finalGiveAmount: MultiFormatRepresentation; 14 | }>; 15 | totalCount: number; 16 | }; 17 | 18 | type OrderLiteModelResponse = { 19 | orderId: MultiFormatRepresentation; 20 | state: string; 21 | makerOrderNonce: number; 22 | makerSrc: MultiFormatRepresentation; 23 | giveOffer: { 24 | chainId: MultiFormatRepresentation; 25 | tokenAddress: MultiFormatRepresentation; 26 | amount: MultiFormatRepresentation; 27 | }; 28 | receiverDst: MultiFormatRepresentation; 29 | takeOffer: { 30 | chainId: MultiFormatRepresentation; 31 | tokenAddress: MultiFormatRepresentation; 32 | amount: MultiFormatRepresentation; 33 | }; 34 | givePatchAuthoritySrc: MultiFormatRepresentation; 35 | orderAuthorityAddressDst: MultiFormatRepresentation; 36 | allowedTakerDst: Partial; 37 | allowedCancelBeneficiarySrc: Partial; 38 | }; 39 | 40 | export class StatsAPI { 41 | private static readonly defaultHost = 'https://dln-api.debridge.finance'; 42 | 43 | constructor(private readonly host = StatsAPI.defaultHost) {} 44 | 45 | async getOrderLiteModel(orderId: string): Promise { 46 | const resp = await axios.get(`${this.host}/api/Orders/${orderId}/liteModel`); 47 | return resp.data; 48 | } 49 | 50 | async getForUnlockAuthorities( 51 | giveChainIds: number[], 52 | orderStates: string[], 53 | unlockAuthorities: string[], 54 | skip: number, 55 | take: number, 56 | ): Promise { 57 | const resp = await axios.post(`${this.host}/api/Orders/getForUnlockAuthorities`, { 58 | giveChainIds, 59 | orderStates, 60 | unlockAuthorities: unlockAuthorities.join(' '), 61 | take, 62 | skip, 63 | }); 64 | return resp.data; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/hooks/HooksEngine.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from 'pino'; 2 | 3 | import { Hooks } from './HookEnums'; 4 | import { HookHandler } from './HookHandler'; 5 | import { HookParams } from './types/HookParams'; 6 | 7 | export class HooksEngine { 8 | readonly #logger: Logger; 9 | 10 | constructor( 11 | private readonly hookHandlers: { 12 | [key in Hooks]?: HookHandler[]; 13 | }, 14 | logger: Logger, 15 | ) { 16 | this.#logger = logger.child({ 17 | service: HooksEngine.name, 18 | }); 19 | } 20 | 21 | handleOrderFeedConnected(params: HookParams) { 22 | this.process(Hooks.OrderFeedConnected, params); 23 | } 24 | 25 | handleOrderFeedDisconnected(params: HookParams) { 26 | this.process(Hooks.OrderFeedDisconnected, params); 27 | } 28 | 29 | handleOrderRejected(params: HookParams) { 30 | this.process(Hooks.OrderRejected, params); 31 | } 32 | 33 | handleOrderPostponed(params: HookParams) { 34 | this.process(Hooks.OrderPostponed, params); 35 | } 36 | 37 | handleOrderFulfilled(params: HookParams) { 38 | this.process(Hooks.OrderFulfilled, params); 39 | } 40 | 41 | handleOrderUnlockSent(params: HookParams) { 42 | this.process(Hooks.OrderUnlockSent, params); 43 | } 44 | 45 | handleOrderUnlockFailed(params: HookParams) { 46 | this.process(Hooks.OrderUnlockFailed, params); 47 | } 48 | 49 | private async process(hookEnum: Hooks, params: HookParams): Promise { 50 | this.#logger.debug(`triggering hook ${Hooks[hookEnum]}`); 51 | const handlers = this.hookHandlers[hookEnum] || []; 52 | for (const handler of handlers) { 53 | try { 54 | // eslint-disable-next-line no-await-in-loop -- Used to track hook errors. TODO make hooks asynchronous DEV-3490 55 | await handler(params as any, { logger: this.#logger.child({ hook: Hooks[hookEnum] }) }); 56 | } catch (e) { 57 | this.#logger.error(`Error in execution hook handler in ${hookEnum}`); 58 | this.#logger.error(e); 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/filters/give.amount.usd.equivalent.between.ts: -------------------------------------------------------------------------------- 1 | import { OrderData } from '@debridge-finance/dln-client'; 2 | import { helpers } from '@debridge-finance/solana-utils'; 3 | import BigNumber from 'bignumber.js'; 4 | 5 | import { createClientLogger } from '../dln-ts-client.utils'; 6 | 7 | import { FilterContext, OrderFilterInitializer } from './order.filter'; 8 | 9 | /** 10 | * Checks if the USD equivalent of the order's unlock amount (amount given by the maker upon order creation, deducted by the fees) is in the given range. 11 | * This filter is useful to filter off uncomfortable volumes, e.g. too low (e.g. less than $10) or too high (e.g., more than $100,000). 12 | */ 13 | export const giveAmountUsdEquivalentBetween = 14 | (minUSDEquivalent: number, maxUSDEquivalent: number): OrderFilterInitializer => 15 | async (/* chainId: ChainId */ {}, /* context: OrderFilterInitContext */ {}) => 16 | async (order: OrderData, context: FilterContext): Promise => { 17 | const logger = context.logger.child({ 18 | filter: 'giveAmountUsdEquivalentBetween', 19 | }); 20 | const clientLogger = createClientLogger(logger); 21 | const giveAddress = helpers.bufferToHex(Buffer.from(order.give.tokenAddress)); 22 | logger.debug(`giveAddress=${giveAddress}`); 23 | 24 | const [givePrice, giveDecimals] = await Promise.all([ 25 | context.config.tokenPriceService.getPrice(order.give.chainId, order.give.tokenAddress, { 26 | logger: clientLogger, 27 | }), 28 | context.config.client.getDecimals(order.give.chainId, order.give.tokenAddress), 29 | ]); 30 | logger.debug(`givePrice=${givePrice}`); 31 | logger.debug(`giveDecimals=${giveDecimals}`); 32 | 33 | const giveUsdAmount = BigNumber(givePrice) 34 | .multipliedBy(order.give.amount.toString()) 35 | .dividedBy(new BigNumber(10).pow(giveDecimals)) 36 | .toNumber(); 37 | logger.debug(`giveUsdAmount=${giveUsdAmount}`); 38 | 39 | const result = minUSDEquivalent <= giveUsdAmount && giveUsdAmount <= maxUSDEquivalent; 40 | logger.debug(`result=${result}`); 41 | logger.info(`approve status: ${result}, giveUsdAmount: ${giveUsdAmount.toString()}`); 42 | return Promise.resolve(result); 43 | }; 44 | -------------------------------------------------------------------------------- /src/filters/take.amount.usd.equivalent.between.ts: -------------------------------------------------------------------------------- 1 | import { OrderData } from '@debridge-finance/dln-client'; 2 | import { helpers } from '@debridge-finance/solana-utils'; 3 | import BigNumber from 'bignumber.js'; 4 | 5 | import { createClientLogger } from '../dln-ts-client.utils'; 6 | 7 | import { FilterContext, OrderFilterInitializer } from './order.filter'; 8 | 9 | /** 10 | * Checks if the USD equivalent of the order's requested amount (amount that should be supplied to fulfill the order successfully) is in the given range. 11 | * This filter is useful to filter off uncomfortable volumes, e.g. too low (e.g. less than $10) or too high (e.g., more than $100,000). 12 | * 13 | */ 14 | export const takeAmountUsdEquivalentBetween = 15 | (minUSDEquivalent: number, maxUSDEquivalent: number): OrderFilterInitializer => 16 | async (/* chainId: ChainId */ {}, /* context: OrderFilterInitContext */ {}) => 17 | async (order: OrderData, context: FilterContext): Promise => { 18 | const logger = context.logger.child({ 19 | filter: 'takeAmountUsdEquivalentBetween', 20 | }); 21 | const clientLogger = createClientLogger(logger); 22 | const takeAddress = helpers.bufferToHex(Buffer.from(order.take.tokenAddress)); 23 | logger.debug(`takeAddress=${takeAddress}`); 24 | 25 | const [takePrice, takeDecimals] = await Promise.all([ 26 | context.config.tokenPriceService.getPrice(order.take.chainId, order.take.tokenAddress, { 27 | logger: clientLogger, 28 | }), 29 | context.config.client.getDecimals(order.take.chainId, order.take.tokenAddress), 30 | ]); 31 | logger.debug(`takePrice=${takePrice}`); 32 | logger.debug(`takeDecimals=${takeDecimals}`); 33 | 34 | const takeUsdAmount = BigNumber(takePrice) 35 | .multipliedBy(order.take.amount.toString()) 36 | .dividedBy(new BigNumber(10).pow(takeDecimals)) 37 | .toNumber(); 38 | logger.debug(`takeUsdAmount=${takeUsdAmount}`); 39 | 40 | const result = minUSDEquivalent <= takeUsdAmount && takeUsdAmount <= maxUSDEquivalent; 41 | logger.debug(`result=${result}`); 42 | logger.info(`approve status: ${result}, takeUsdAmount: ${takeUsdAmount.toString()}`); 43 | return Promise.resolve(result); 44 | }; 45 | -------------------------------------------------------------------------------- /src/chain-evm/order-estimator.ts: -------------------------------------------------------------------------------- 1 | import { calculateExpectedTakeAmount } from '@debridge-finance/legacy-dln-profitability'; 2 | import { OrderEstimator } from '../chain-common/order-estimator'; 3 | import { EVMOrderValidator } from './order-validator'; 4 | import { EvmChainPreferencesStore } from './preferences/store'; 5 | import { GasCategory } from './fees/types'; 6 | 7 | export class EVMOrderEstimator extends OrderEstimator { 8 | public static readonly PAYLOAD_ENTRY__EVM_ESTIMATED_GAS_PRICE = 9 | 'EVMOrderEstimator.PAYLOAD_ENTRY__EVM_ESTIMATED_GAS_PRICE'; 10 | 11 | public static readonly PAYLOAD_ENTRY__EVM_ESTIMATED_FEE = 12 | 'EVMOrderEstimator.PAYLOAD_ENTRY__EVM_ESTIMATED_FEE'; 13 | 14 | /** 15 | * Estimate gas price that would be relevant during the next few moments. An order would be estimated against 16 | * exactly this gas price 17 | */ 18 | private async getEstimatedGasPrice(): Promise { 19 | const estimatedNextGasPrice = await EvmChainPreferencesStore.get( 20 | this.order.takeChain.chain, 21 | ).feeManager.getGasPrice(GasCategory.PROJECTED, { logger: this.logger }); 22 | this.logger.debug(`estimated gas price for the next block: ${estimatedNextGasPrice}`); 23 | this.setPayloadEntry( 24 | EVMOrderEstimator.PAYLOAD_ENTRY__EVM_ESTIMATED_GAS_PRICE, 25 | estimatedNextGasPrice, 26 | ); 27 | 28 | return estimatedNextGasPrice; 29 | } 30 | 31 | /** 32 | * Sets evmFulfillGasLimit and evmFulfillCappedGasPrice for order profitability estimation 33 | */ 34 | protected async getExpectedTakeAmountContext(): Promise< 35 | Parameters['2'] 36 | > { 37 | const gasPrice = await this.getEstimatedGasPrice(); 38 | const gasLimit = this.getPayloadEntry( 39 | EVMOrderValidator.PAYLOAD_ENTRY__EVM_FULFILL_GAS_LIMIT, 40 | ); 41 | this.setPayloadEntry( 42 | EVMOrderEstimator.PAYLOAD_ENTRY__EVM_ESTIMATED_FEE, 43 | gasPrice * BigInt(gasLimit), 44 | ); 45 | 46 | const parentContext = await super.getExpectedTakeAmountContext(); 47 | return { 48 | ...parentContext, 49 | evmFulfillGasLimit: gasLimit, 50 | evmFulfillCappedGasPrice: gasPrice, 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/chain-evm/tx-generators/createBatchOrderUnlockTx.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChainEngine, 3 | ChainId, 4 | OrderDataWithId, 5 | OrderEstimationStage, 6 | } from '@debridge-finance/dln-client'; 7 | import { Logger } from 'pino'; 8 | import { assert } from '../../errors'; 9 | import { IExecutor } from '../../executor'; 10 | import { createClientLogger } from '../../dln-ts-client.utils'; 11 | import { InputTransaction } from '../signer'; 12 | 13 | export async function createBatchOrderUnlockTx( 14 | executor: IExecutor, 15 | orders: Array, 16 | logger: Logger, 17 | ): Promise { 18 | assert(orders.length > 0, 'empty array of orders given for batch unlock'); 19 | 20 | const order = orders[0]; 21 | const giveChain = executor.getSupportedChain(order.give.chainId); 22 | const takeChain = executor.getSupportedChain(order.take.chainId); 23 | 24 | const [giveNativePrice, takeNativePrice] = await Promise.all([ 25 | executor.tokenPriceService.getPrice(giveChain.chain, null, { 26 | logger: createClientLogger(logger), 27 | }), 28 | executor.tokenPriceService.getPrice(takeChain.chain, null, { 29 | logger: createClientLogger(logger), 30 | }), 31 | ]); 32 | 33 | const fees = await executor.client.getClaimExecutionFee( 34 | { 35 | action: 'ClaimBatchUnlock', 36 | giveChain: giveChain.chain, 37 | giveNativePrice, 38 | takeChain: takeChain.chain, 39 | takeNativePrice, 40 | batchSize: orders.length, 41 | loggerInstance: createClientLogger(logger), 42 | }, 43 | { 44 | orderEstimationStage: OrderEstimationStage.OrderFulfillment, 45 | }, 46 | ); 47 | 48 | const extraPayload = 49 | giveChain.chain === ChainId.Solana 50 | ? { 51 | solanaInitWalletReward: fees.rewards[0], 52 | solanaClaimUnlockReward: fees.rewards[1], 53 | } 54 | : {}; 55 | 56 | const tx = await executor.client.sendBatchUnlock( 57 | { 58 | beneficiary: giveChain.unlockBeneficiary, 59 | executionFee: fees.total, 60 | loggerInstance: createClientLogger(logger), 61 | orders, 62 | }, 63 | extraPayload, 64 | ); 65 | return { 66 | to: tx.to, 67 | data: tx.data, 68 | value: tx.value?.toString() || undefined, 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /src/chain-evm/signer.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, tokenStringToBuffer } from '@debridge-finance/dln-client'; 2 | import { Logger } from 'pino'; 3 | import Web3 from 'web3'; 4 | import { Authority } from '../interfaces'; 5 | import { EvmTxBroadcaster } from './networking/broadcaster'; 6 | import { EvmChainPreferencesStore } from './preferences/store'; 7 | 8 | type EvmTxContext = { 9 | logger: Logger; 10 | }; 11 | 12 | export type InputTransaction = { 13 | data: string; 14 | to: string; 15 | value?: string; 16 | gasLimit?: number; 17 | 18 | // represents a max gas*gasPrice this tx is allowed to increase to during re-broadcasting 19 | cappedFee?: bigint; 20 | }; 21 | 22 | export class EvmTxSigner implements Authority { 23 | readonly chainId: ChainId; 24 | 25 | readonly #address: string; 26 | 27 | readonly #privateKey: string; 28 | 29 | constructor( 30 | chainId: ChainId, 31 | private readonly connection: Web3, 32 | privateKey: string, 33 | ) { 34 | this.chainId = chainId; 35 | const accountEvmFromPrivateKey = this.connection.eth.accounts.privateKeyToAccount(privateKey); 36 | this.#address = accountEvmFromPrivateKey.address; 37 | this.#privateKey = accountEvmFromPrivateKey.privateKey; 38 | } 39 | 40 | public get address(): string { 41 | return this.#address; 42 | } 43 | 44 | public get bytesAddress(): Uint8Array { 45 | return tokenStringToBuffer(this.chainId, this.#address); 46 | } 47 | 48 | // TODO: must be responsible for queueing txns as they may come from different sources 49 | async sendTransaction(tx: InputTransaction, context: EvmTxContext): Promise { 50 | const logger = context.logger.child({ 51 | service: EvmTxSigner.name, 52 | currentChainId: await this.connection.eth.getChainId(), 53 | }); 54 | const broadcaster = new EvmTxBroadcaster( 55 | { 56 | ...tx, 57 | from: this.#address, 58 | }, 59 | tx.cappedFee, 60 | this.connection, 61 | EvmChainPreferencesStore.get(this.chainId).feeManager, 62 | async (txToSign) => 63 | (await this.connection.eth.accounts.signTransaction(txToSign, this.#privateKey)) 64 | .rawTransaction || '0x', 65 | logger, 66 | EvmChainPreferencesStore.get(this.chainId).broadcasterOpts, 67 | ); 68 | 69 | const receipt = await broadcaster.broadcastAndWait(); 70 | if (receipt.status !== true) throw new Error(`tx ${receipt.transactionHash} reverted`); 71 | return receipt.transactionHash; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/chain-evm/tx-builder.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, OrderDataWithId } from '@debridge-finance/dln-client'; 2 | import { Logger } from 'pino'; 3 | import Web3 from 'web3'; 4 | import { OrderEstimation } from '../chain-common/order-estimator'; 5 | import { IExecutor } from '../executor'; 6 | import { EvmTxSigner } from './signer'; 7 | import { createERC20ApproveTxs } from './tx-generators/createERC20ApproveTxs'; 8 | import { createBatchOrderUnlockTx } from './tx-generators/createBatchOrderUnlockTx'; 9 | import { createOrderFullfillTx } from './tx-generators/createOrderFullfillTx'; 10 | import { InitTransactionBuilder } from '../processor'; 11 | import { BatchUnlockTransactionBuilder } from '../processors/BatchUnlocker'; 12 | import { FulfillTransactionBuilder } from '../chain-common/order-taker'; 13 | 14 | export class EvmTransactionBuilder 15 | implements InitTransactionBuilder, FulfillTransactionBuilder, BatchUnlockTransactionBuilder 16 | { 17 | constructor( 18 | private readonly chain: ChainId, 19 | private contractsForApprove: string[], 20 | private connection: Web3, 21 | private readonly signer: EvmTxSigner, 22 | private readonly executor: IExecutor, 23 | ) {} 24 | 25 | get fulfillAuthority() { 26 | return { 27 | address: this.signer.address, 28 | bytesAddress: this.signer.bytesAddress, 29 | }; 30 | } 31 | 32 | get unlockAuthority() { 33 | return { 34 | address: this.signer.address, 35 | bytesAddress: this.signer.bytesAddress, 36 | }; 37 | } 38 | 39 | getOrderFulfillTxSender(orderEstimation: OrderEstimation, logger: Logger) { 40 | return async () => 41 | this.signer.sendTransaction(await createOrderFullfillTx(orderEstimation, logger), { logger }); 42 | } 43 | 44 | getBatchOrderUnlockTxSender(orders: OrderDataWithId[], logger: Logger): () => Promise { 45 | return async () => 46 | this.signer.sendTransaction(await createBatchOrderUnlockTx(this.executor, orders, logger), { 47 | logger, 48 | }); 49 | } 50 | 51 | async getInitTxSenders(logger: Logger) { 52 | const approvalTxns = await createERC20ApproveTxs( 53 | this.chain, 54 | this.contractsForApprove, 55 | this.connection, 56 | this.signer.address, 57 | this.executor, 58 | logger, 59 | ); 60 | return approvalTxns.map(({ tx, token, spender }) => () => { 61 | logger.info( 62 | `Setting ∞ approval on ${token} to be spend by ${spender} on behalf of a ${this.signer.address}`, 63 | ); 64 | return this.signer.sendTransaction(tx, { logger }); 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/chain-evm/fees/defaults.ts: -------------------------------------------------------------------------------- 1 | import { getFloat, getBoolean, getInt } from '../../env-utils'; 2 | import { EvmFeeManagerOpts } from './manager'; 3 | 4 | export const defaultFeeManagerOpts: EvmFeeManagerOpts = { 5 | gasLimitMultiplier: getFloat('EVM_FEE_MANAGER__GAS_LIMIT_MULTIPLIER', 1.1), 6 | 7 | legacyGasPriceProjectedMultiplier: getFloat( 8 | 'EVM_FEE_MANAGER__LEGACY_GAS_PRICE_PROJECTED_MULTIPLIER', 9 | 1.1, 10 | ), 11 | legacyGasPriceNormalMultiplier: getFloat( 12 | 'EVM_FEE_MANAGER__LEGACY_GAS_PRICE_NORMAL_MULTIPLIER', 13 | 1.1, 14 | ), 15 | legacyGasPriceAggressiveMultiplier: getFloat( 16 | 'EVM_FEE_MANAGER__LEGACY_GAS_PRICE_AGGRESSIVE_MULTIPLIER', 17 | 1.3, 18 | ), 19 | legacyEnforceAggressive: getBoolean('EVM_FEE_MANAGER__LEGACY_ENFORCE_AGGRESSIVE', false), 20 | 21 | eip1559BaseFeeProjectedMultiplier: getFloat( 22 | 'EVM_FEE_MANAGER__EIP1559_BASE_FEE_PROJECTED_MULTIPLIER', 23 | 1.075, 24 | ), 25 | eip1559BaseFeeNormalMultiplier: getFloat( 26 | 'EVM_FEE_MANAGER__EIP1559_BASE_FEE_NORMAL_MULTIPLIER', 27 | 1.075, 28 | ), 29 | eip1559BaseFeeAggressiveMultiplier: getFloat( 30 | 'EVM_FEE_MANAGER__EIP1559_BASE_FEE_AGGRESSIVE_MULTIPLIER', 31 | 1.125, 32 | ), 33 | eip1559PriorityFeeProjectedPercentile: getFloat( 34 | 'EVM_FEE_MANAGER__EIP1559_PRIORITY_FEE_PROJECTED_PERCENTILE', 35 | 50, 36 | ), 37 | eip1559PriorityFeeNormalPercentile: getFloat( 38 | 'EVM_FEE_MANAGER__EIP1559_PRIORITY_FEE_NORMAL_PERCENTILE', 39 | 50, 40 | ), 41 | eip1559PriorityFeeAggressivePercentile: getFloat( 42 | 'EVM_FEE_MANAGER__EIP1559_PRIORITY_FEE_AGGRESSIVE_PERCENTILE', 43 | 75, 44 | ), 45 | eip1559PriorityFeeIncreaseBoundary: getFloat( 46 | 'EVM_FEE_MANAGER__EIP1559_PRIORITY_FEE_INCREASE_BOUNDARY', 47 | 200, 48 | ), 49 | eip1559EnforceAggressive: getBoolean('EVM_FEE_MANAGER__EIP1559_ENFORCE_AGGRESSIVE', false), 50 | 51 | // must be >10% higher because that's how go-ethereum is implemented 52 | // see https://github.com/ethereum/go-ethereum/blob/d9556533c34f9bb44b7c0212ba55a08a047babef/core/txpool/legacypool/list.go#L286-L309 53 | replaceBumperNormalMultiplier: getFloat( 54 | 'EVM_FEE_MANAGER__REPLACE_BUMPER_NORMAL_MULTIPLIER', 55 | 1.11, 56 | ), 57 | replaceBumperAggressiveMultiplier: getFloat( 58 | 'EVM_FEE_MANAGER__REPLACE_BUMPER_AGGRESSIVE_MULTIPLIER', 59 | 1.3, 60 | ), 61 | replaceBumperEnforceAggressive: getBoolean( 62 | 'EVM_FEE_MANAGER__REPLACE_BUMPER_ENFORCE_AGGRESSIVE', 63 | true, 64 | ), 65 | 66 | overcappingAllowed: getBoolean('EVM_FEE_MANAGER__OVERCAPPING_ALLOWED', true), 67 | overcappingAllowance: getInt('EVM_FEE_MANAGER__OVERCAPPING_ALLOWANCE', 3), 68 | }; 69 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, OrderData } from '@debridge-finance/dln-client'; 2 | import { Logger } from 'pino'; 3 | 4 | import { HooksEngine } from './hooks/HooksEngine'; 5 | import { ExecutorSupportedChain } from './executor'; 6 | 7 | export enum OrderInfoStatus { 8 | Created, 9 | ArchivalCreated, 10 | ArchivalFulfilled, 11 | Fulfilled, 12 | Cancelled, 13 | UnlockSent, 14 | UnlockClaim, 15 | TakeOfferDecreased, 16 | GiveOfferIncreased, 17 | } 18 | 19 | export type OrderId = string; 20 | 21 | type FinalizationInfo = 22 | | { 23 | Finalized: { 24 | transaction_hash: string; 25 | }; 26 | } 27 | | { 28 | Confirmed: { 29 | confirmation_blocks_count: number; 30 | transaction_hash: string; 31 | }; 32 | } 33 | | 'Revoked'; 34 | 35 | export type IncomingOrder = { 36 | orderId: string; 37 | status: OrderInfoStatus; 38 | order: OrderData; 39 | } & (T extends OrderInfoStatus.ArchivalFulfilled ? { unlockAuthority: string } : {}) & 40 | (T extends OrderInfoStatus.Fulfilled ? { unlockAuthority: string } : {}) & 41 | (T extends OrderInfoStatus.Created ? { finalization_info: FinalizationInfo } : {}); 42 | 43 | export type IncomingOrderContext = { 44 | orderInfo: IncomingOrder; 45 | giveChain: ExecutorSupportedChain; 46 | takeChain: ExecutorSupportedChain; 47 | }; 48 | 49 | export type OrderProcessorFunc = (order: IncomingOrder) => Promise; 50 | 51 | export type UnlockAuthority = { 52 | chainId: ChainId; 53 | address: string; 54 | }; 55 | 56 | export interface Authority { 57 | address: string; 58 | bytesAddress: Uint8Array; 59 | } 60 | 61 | export abstract class GetNextOrder { 62 | // @ts-ignore Initialized deferredly within the setEnabledChains() method. Should be rewritten during the next major refactoring 63 | protected enabledChains: ChainId[]; 64 | 65 | // @ts-ignore Initialized deferredly within the setLogger() method. Should be rewritten during the next major refactoring 66 | protected logger: Logger; 67 | 68 | // @ts-ignore Initialized deferredly within the init() method. Should be rewritten during the next major refactoring 69 | protected processNextOrder: OrderProcessorFunc; 70 | 71 | abstract init( 72 | processNextOrder: OrderProcessorFunc, 73 | UnlockAuthority: UnlockAuthority[], 74 | minConfirmationThresholds: Array<{ 75 | chainId: ChainId; 76 | points: number[]; 77 | }>, 78 | hooksEngine: HooksEngine, 79 | ): void; 80 | 81 | setEnabledChains(enabledChains: ChainId[]) { 82 | this.enabledChains = enabledChains; 83 | } 84 | 85 | setLogger(logger: Logger) { 86 | this.logger = logger.child({ service: GetNextOrder.name }); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/chain-evm/order-validator.ts: -------------------------------------------------------------------------------- 1 | import { PostponingReason } from '../hooks/HookEnums'; 2 | import { OrderValidator } from '../chain-common/order-validator'; 3 | import { EVMOrderEstimator } from './order-estimator'; 4 | import { createOrderFullfillTx } from './tx-generators/createOrderFullfillTx'; 5 | import { InputTransaction } from './signer'; 6 | import { EvmChainPreferencesStore } from './preferences/store'; 7 | 8 | export class EVMOrderValidator extends OrderValidator { 9 | public static readonly PAYLOAD_ENTRY__EVM_FULFILL_GAS_LIMIT = 10 | 'EVMOrderValidator.PAYLOAD_ENTRY__EVM_FULFILL_GAS_LIMIT'; 11 | 12 | public static readonly PAYLOAD_ENTRY__EVM_FULFILL_DISABLE_TX_CAPPED_FEE = 13 | 'EVMOrderValidator,PAYLAOD_ENTRY__EVM_FULFILL_DISABLE_TX_CAPPED_FEE'; 14 | 15 | protected async runChecks() { 16 | await super.runChecks(); 17 | await this.checkEvmEstimation(); 18 | } 19 | 20 | protected get logger() { 21 | return super.logger.child({ service: EVMOrderValidator.name }); 22 | } 23 | 24 | private async checkEvmEstimation(): Promise { 25 | const tx = await createOrderFullfillTx( 26 | { 27 | order: this.order, 28 | isProfitable: true, 29 | requiredReserveAmount: 30 | await this.order.getMaxProfitableReserveAmountWithoutOperatingExpenses(), 31 | projectedFulfillAmount: this.order.orderData.take.amount, 32 | payload: { 33 | preFulfillSwap: this.payload.validationPreFulfillSwap, 34 | [EVMOrderValidator.PAYLOAD_ENTRY__EVM_FULFILL_DISABLE_TX_CAPPED_FEE]: true, 35 | }, 36 | }, 37 | this.logger.child({ routine: 'checkEvmEstimation' }), 38 | ); 39 | 40 | try { 41 | const gasLimit = await this.estimateTx(tx); 42 | this.setPayloadEntry(EVMOrderValidator.PAYLOAD_ENTRY__EVM_FULFILL_GAS_LIMIT, gasLimit); 43 | this.logger.debug( 44 | `estimated gas needed for the fulfill tx with roughly estimated reserve amount: ${gasLimit} gas units`, 45 | ); 46 | } catch (e) { 47 | return this.sc.postpone( 48 | PostponingReason.FULFILLMENT_EVM_TX_PREESTIMATION_FAILED, 49 | `unable to estimate preliminary txn: ${e}`, 50 | ); 51 | } 52 | 53 | return Promise.resolve(); 54 | } 55 | 56 | private async estimateTx(tx: InputTransaction): Promise { 57 | return EvmChainPreferencesStore.get(this.order.takeChain.chain).feeManager.estimateTx( 58 | { 59 | to: tx.to, 60 | data: tx.data, 61 | value: tx.value?.toString(), 62 | from: this.order.takeChain.fulfillAuthority.address, 63 | }, 64 | { logger: this.logger }, 65 | ); 66 | } 67 | 68 | protected getOrderEstimator() { 69 | return new EVMOrderEstimator(this.order, { 70 | logger: this.logger, 71 | validationPayload: this.payload, 72 | }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/chain-evm/tx-generators/createOrderFullfillTx.ts: -------------------------------------------------------------------------------- 1 | import { ChainEngine, EvmInstruction } from '@debridge-finance/dln-client'; 2 | import { Logger } from 'pino'; 3 | import { InputTransaction } from '../signer'; 4 | import { assert } from '../../errors'; 5 | import { createClientLogger } from '../../dln-ts-client.utils'; 6 | import { OrderEstimation } from '../../chain-common/order-estimator'; 7 | import { EVMOrderEstimator } from '../order-estimator'; 8 | import { EVMOrderValidator } from '../order-validator'; 9 | 10 | async function getLowLevelEvmInstruction( 11 | estimation: OrderEstimation, 12 | logger: Logger, 13 | ): Promise { 14 | const { order } = estimation; 15 | if (estimation.order.route.requiresSwap) { 16 | const swapResult = estimation.payload.preFulfillSwap; 17 | assert(swapResult !== undefined, 'missing preFulfillSwap payload entry'); 18 | 19 | return order.executor.client.preswapAndFulfillOrder( 20 | { 21 | order: order.getWithId(), 22 | taker: order.takeChain.fulfillAuthority.bytesAddress, 23 | swapResult, 24 | loggerInstance: createClientLogger(logger), 25 | }, 26 | { 27 | unlockAuthority: order.takeChain.unlockAuthority.bytesAddress, 28 | externalCallRewardBeneficiary: order.takeChain.unlockBeneficiary, 29 | preswapChangeRecipient: 30 | order.dstConstraints().preFulfillSwapChangeRecipient === 'taker' 31 | ? order.takeChain.unlockBeneficiary 32 | : undefined, 33 | }, 34 | ); 35 | } 36 | 37 | return order.executor.client.fulfillOrder( 38 | { 39 | order: order.getWithId(), 40 | loggerInstance: createClientLogger(logger), 41 | }, 42 | { 43 | permit: '0x', 44 | // taker: this.order.takeChain.fulfillProvider.bytesAddress, 45 | unlockAuthority: order.takeChain.unlockAuthority.bytesAddress, 46 | externalCallRewardBeneficiary: order.takeChain.unlockBeneficiary, 47 | }, 48 | ); 49 | } 50 | 51 | export async function createOrderFullfillTx( 52 | estimation: OrderEstimation, 53 | logger: Logger, 54 | ): Promise { 55 | const ix = await getLowLevelEvmInstruction(estimation, logger); 56 | const cappedFee = ( 57 | estimation.payload[EVMOrderEstimator.PAYLOAD_ENTRY__EVM_ESTIMATED_FEE] 58 | ); 59 | assert( 60 | (typeof cappedFee === 'bigint' && cappedFee > 0) || 61 | estimation.payload[EVMOrderValidator.PAYLOAD_ENTRY__EVM_FULFILL_DISABLE_TX_CAPPED_FEE] === 62 | true, 63 | 'evm order fulfill expects either a capped fee or explicitly disabled fee capping', 64 | ); 65 | const tx = { 66 | to: ix.to, 67 | data: ix.data, 68 | value: ix.value.toString(), 69 | cappedFee, 70 | }; 71 | 72 | logger.debug(`Crafted txn: ${JSON.stringify(tx)}`); 73 | return tx; 74 | } 75 | -------------------------------------------------------------------------------- /src/chain-evm/tx-generators/createERC20ApproveTxs.ts: -------------------------------------------------------------------------------- 1 | import { buffersAreEqual, ChainId } from '@debridge-finance/dln-client'; 2 | import { Logger } from 'pino'; 3 | import Web3 from 'web3'; 4 | import { IExecutor } from '../../executor'; 5 | import { InputTransaction } from '../signer'; 6 | import IERC20 from './ierc20.json'; 7 | 8 | const MAX_UINT256 = '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; 9 | 10 | function getApproveTx(tokenAddress: string, spenderAddress: string): InputTransaction { 11 | const contract = new new Web3().eth.Contract(IERC20.abi as any, tokenAddress); 12 | 13 | return { 14 | to: tokenAddress, 15 | data: contract.methods.approve(spenderAddress, MAX_UINT256).encodeABI(), 16 | }; 17 | } 18 | 19 | async function getAllowance( 20 | connection: Web3, 21 | tokenAddress: string, 22 | ownerAddress: string, 23 | spenderAddress: string, 24 | ): Promise { 25 | const contract = new connection.eth.Contract(IERC20.abi as any, tokenAddress); 26 | 27 | const approvedAmount = (await contract.methods 28 | .allowance(ownerAddress, spenderAddress) 29 | .call()) as string; 30 | 31 | return BigInt(approvedAmount); 32 | } 33 | 34 | type ApprovalTxDetails = { 35 | token: string; 36 | spender: string; 37 | tx: InputTransaction; 38 | }; 39 | 40 | export async function createERC20ApproveTxs( 41 | chain: ChainId, 42 | contractsForApprove: string[], 43 | connection: Web3, 44 | signer: string, 45 | executor: IExecutor, 46 | logger: Logger, 47 | ): Promise> { 48 | const txns: Array = []; 49 | 50 | logger.debug('Collect ERC-20 tokens that should have approvals'); 51 | const tokens: string[] = []; 52 | for (const bucket of executor.buckets) { 53 | for (const token of bucket.findTokens(chain) || []) { 54 | if (!buffersAreEqual(token, Buffer.alloc(20, 0))) { 55 | tokens.push(token.toAddress(chain)); 56 | } 57 | } 58 | } 59 | 60 | for (const token of tokens) { 61 | for (const contract of contractsForApprove) { 62 | // eslint-disable-next-line no-await-in-loop -- Intentional because works only during initialization 63 | const currentAllowance = await getAllowance(connection, token, signer, contract); 64 | if (currentAllowance === 0n) { 65 | logger.debug(`${token} requires approval`); 66 | logger.info( 67 | `Creating a txn to set ∞ approval on ${token} to be spend by ${contract} on behalf of a ${signer}`, 68 | ); 69 | txns.push({ 70 | token, 71 | spender: contract, 72 | tx: getApproveTx(token, contract), 73 | }); 74 | } else { 75 | logger.info( 76 | `Allowance found (${currentAllowance}) on ${token} to be spend by ${contract} on behalf of a ${signer}`, 77 | ); 78 | } 79 | } 80 | } 81 | return txns; 82 | } 83 | -------------------------------------------------------------------------------- /src/configurator/tokenPriceService.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CachePriceFeed, 3 | ChainId, 4 | CoingeckoPriceFeed, 5 | MappedPriceFeed, 6 | PriceSourceMap, 7 | PriceTokenService, 8 | tokenStringToBuffer, 9 | ZERO_EVM_ADDRESS, 10 | } from '@debridge-finance/dln-client'; 11 | 12 | type TokenPriceServiceConfiguratorOpts = Partial<{ 13 | coingeckoApiKey: string; 14 | coingeckoCacheTTL: number; 15 | mapping?: PriceSourceMap; 16 | }>; 17 | 18 | const defaultCoingeckoCacheTTL = 60 * 5; 19 | 20 | const defaultMap: PriceSourceMap = { 21 | [ChainId.Solana]: { 22 | // remap USDC@Solana price to USDC@Ethereum 23 | EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v: { 24 | type: 'redirect', 25 | chainId: ChainId.Ethereum, 26 | token: tokenStringToBuffer(ChainId.Ethereum, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'), 27 | }, 28 | }, 29 | [ChainId.Linea]: { 30 | // remap ETH@Linea price to ETH@Ethereum 31 | [ZERO_EVM_ADDRESS]: { 32 | type: 'redirect', 33 | chainId: ChainId.Ethereum, 34 | token: tokenStringToBuffer(ChainId.Ethereum, ZERO_EVM_ADDRESS), 35 | }, 36 | // remap USDC@Linea price to USDC@Ethereum 37 | '0x176211869cA2b568f2A7D4EE941E073a821EE1ff': { 38 | type: 'redirect', 39 | chainId: ChainId.Ethereum, 40 | token: tokenStringToBuffer(ChainId.Ethereum, '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'), 41 | }, 42 | }, 43 | [ChainId.Base]: { 44 | // remap ETH@Base price to ETH@Ethereum 45 | [ZERO_EVM_ADDRESS]: { 46 | type: 'redirect', 47 | chainId: ChainId.Ethereum, 48 | token: tokenStringToBuffer(ChainId.Ethereum, ZERO_EVM_ADDRESS), 49 | }, 50 | // remap USDbC@Base price to USDC@Ethereum 51 | '0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca': { 52 | type: 'redirect', 53 | chainId: ChainId.Ethereum, 54 | token: tokenStringToBuffer(ChainId.Ethereum, '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'), 55 | }, 56 | // remap USDC@Base price to USDC@Ethereum 57 | '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913': { 58 | type: 'redirect', 59 | chainId: ChainId.Ethereum, 60 | token: tokenStringToBuffer(ChainId.Ethereum, '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'), 61 | }, 62 | }, 63 | [ChainId.Optimism]: { 64 | // remap ETH@Optimism price to ETH@Ethereum 65 | [ZERO_EVM_ADDRESS]: { 66 | type: 'redirect', 67 | chainId: ChainId.Ethereum, 68 | token: tokenStringToBuffer(ChainId.Ethereum, ZERO_EVM_ADDRESS), 69 | }, 70 | }, 71 | }; 72 | 73 | export function tokenPriceService(opts?: TokenPriceServiceConfiguratorOpts): PriceTokenService { 74 | return new MappedPriceFeed( 75 | { 76 | ...defaultMap, 77 | ...opts?.mapping, 78 | }, 79 | new CachePriceFeed( 80 | new CoingeckoPriceFeed(opts?.coingeckoApiKey), 81 | opts?.coingeckoCacheTTL || defaultCoingeckoCacheTTL, 82 | ), 83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /tests/throughput.test.ts: -------------------------------------------------------------------------------- 1 | import pino from 'pino'; 2 | import assert from 'assert'; 3 | import { ChainId } from '@debridge-finance/dln-client'; 4 | import { ThroughputController } from '../src/processors/throughput'; 5 | 6 | describe('NonFinalizedOrdersBudgetController', () => { 7 | const logger = pino({ 8 | // level: 'debug', 9 | }); 10 | const controller = new ThroughputController( 11 | ChainId.Arbitrum, 12 | [ 13 | { 14 | maxFulfillThroughputUSD: 100, 15 | minBlockConfirmations: 1, 16 | throughputTimeWindowSec: 0.3, 17 | }, 18 | { 19 | maxFulfillThroughputUSD: 1000, 20 | minBlockConfirmations: 10, 21 | throughputTimeWindowSec: 0.2, 22 | }, 23 | { 24 | maxFulfillThroughputUSD: 0, 25 | minBlockConfirmations: 100, 26 | throughputTimeWindowSec: 0, 27 | }, 28 | ], 29 | logger, 30 | ); 31 | 32 | describe('validateOrder', () => { 33 | it('check throughout', async () => { 34 | assert.equal(controller.isThrottled('1', 1, 100), false); 35 | assert.equal(controller.isThrottled('1', 1, 101), true); 36 | 37 | assert.equal(controller.isThrottled('2', 2, 100), false); 38 | assert.equal(controller.isThrottled('2', 2, 101), true); 39 | 40 | assert.equal(controller.isThrottled('2', 9, 100), false); 41 | assert.equal(controller.isThrottled('2', 9, 101), true); 42 | 43 | assert.equal(controller.isThrottled('10', 10, 1000), false); 44 | assert.equal(controller.isThrottled('10', 10, 1001), true); 45 | 46 | controller.addOrder('1', 1, 99); 47 | assert.equal(controller.isThrottled('2', 1, 1), false); 48 | assert.equal(controller.isThrottled('2', 1, 2), true); 49 | 50 | assert.equal(controller.isThrottled('2', 2, 1), false); 51 | assert.equal(controller.isThrottled('2', 2, 2), true); 52 | 53 | assert.equal(controller.isThrottled('2', 9, 1), false); 54 | assert.equal(controller.isThrottled('2', 9, 2), true); 55 | 56 | assert.equal(controller.isThrottled('2', 10, 1000), false); 57 | assert.equal(controller.isThrottled('2', 10, 1001), true); 58 | }); 59 | 60 | it('check automation', async () => { 61 | controller.addOrder('1', 1, 99); 62 | assert.equal(controller.isThrottled('2', 1, 100), true); 63 | 64 | // wait 0.3s, as per first range 65 | await new Promise((resolve) => { 66 | setTimeout(resolve, 300); 67 | }); 68 | assert.equal(controller.isThrottled('2', 1, 100), false); 69 | }); 70 | 71 | it('check removal', async () => { 72 | controller.addOrder('1', 1, 99); 73 | controller.removeOrder('1'); 74 | assert.equal(controller.isThrottled('2', 1, 100), false); 75 | }); 76 | 77 | it('must use topmost range if empty', async () => { 78 | assert.equal(controller.isThrottled('2', 100, 10_000), false); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /src/orderFeeds/u256-utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-bitwise -- This helpers implement U256 arithmetics. Seems like not needed anymore because WS returns standard integers. TODO #862karjre */ 2 | 3 | import { helpers } from '@debridge-finance/solana-utils'; 4 | 5 | function BytesToU64(data: Buffer, encoding: 'le' | 'be'): bigint { 6 | let result: bigint = 0n; 7 | const leOrder = [0, 1, 2, 3, 4, 5, 6, 7]; 8 | let counter = 0; 9 | for (const i of encoding === 'be' ? leOrder.reverse() : leOrder) { 10 | result += BigInt(data[i]) << BigInt(8 * counter); 11 | counter += 1; 12 | } 13 | return result; 14 | } 15 | 16 | export type U256Limbs = { 17 | limb1: bigint; 18 | limb2: bigint; 19 | limb3: bigint; 20 | limb4: bigint; 21 | }; 22 | 23 | export class U256 { 24 | constructor(private value: U256Limbs) {} 25 | 26 | toBytesBE() { 27 | return U256.toBytesBE(this.value); 28 | } 29 | 30 | toBigInt() { 31 | return U256.toBigInt(this.value); 32 | } 33 | 34 | static toBytesBE(u: U256Limbs): Buffer { 35 | const result = Buffer.alloc(32); 36 | const shifts = Array.from({ length: 8 }) 37 | .fill(0n) 38 | .map((/* v */ _, i) => BigInt(56 - 8 * i)); 39 | for (let i = 0; i < 32; i++) { 40 | switch (Math.floor(i / 8)) { 41 | case 0: 42 | result[i] = Number((u.limb4 >> shifts[i % 8]) & 0xffn); 43 | break; 44 | case 1: 45 | result[i] = Number((u.limb3 >> shifts[i % 8]) & 0xffn); 46 | break; 47 | case 2: 48 | result[i] = Number((u.limb2 >> shifts[i % 8]) & 0xffn); 49 | break; 50 | case 3: 51 | result[i] = Number((u.limb1 >> shifts[i % 8]) & 0xffn); 52 | break; 53 | default: 54 | throw new Error('Unreachable'); 55 | } 56 | } 57 | return result; 58 | } 59 | 60 | static toBigInt(u: U256Limbs): bigint { 61 | return ( 62 | u.limb1 + 63 | (u.limb2 << BigInt(8 * 8 * 1)) + 64 | (u.limb3 << BigInt(8 * 8 * 2)) + 65 | (u.limb4 << BigInt(8 * 8 * 3)) 66 | ); 67 | } 68 | 69 | static fromBytesBE(data: Buffer): U256 { 70 | return new U256({ 71 | limb4: BytesToU64(data.subarray(0, 8), 'be'), 72 | limb3: BytesToU64(data.subarray(8, 16), 'be'), 73 | limb2: BytesToU64(data.subarray(16, 24), 'be'), 74 | limb1: BytesToU64(data.subarray(24, 32), 'be'), 75 | }); 76 | } 77 | 78 | static fromHexBEString(data: string): U256 { 79 | return U256.fromBytesBE(helpers.hexToBuffer(data)); 80 | } 81 | 82 | static fromBigInt(n: bigint): U256 { 83 | const u64Mask = 0xffffffffffffffffn; 84 | const u64Shift = 8; 85 | const limb1 = n & u64Mask; 86 | const limb2 = (n >> BigInt(u64Shift * 1)) & u64Mask; 87 | const limb3 = (n >> BigInt(u64Shift * 2)) & u64Mask; 88 | const limb4 = (n >> BigInt(u64Shift * 3)) & u64Mask; 89 | 90 | return new U256({ limb1, limb2, limb3, limb4 }); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.6" 2 | services: 3 | dln-executor: 4 | image: debridgefinance/dln-executor 5 | # container_name: dln-executor${DOCKER_ID} 6 | build: . 7 | container_name: dln-executor 8 | restart: unless-stopped 9 | environment: 10 | - LOG_LEVEL=${LOG_LEVEL} 11 | - WS_API_KEY=${WS_API_KEY} 12 | - COINGECKO_API_KEY=${COINGECKO_API_KEY} 13 | - SENTRY_DSN=${SENTRY_DSN} 14 | - USE_MADRID=${USE_MADRID} 15 | 16 | - ARBITRUM_RPC=${ARBITRUM_RPC} 17 | - ARBITRUM_BENEFICIARY=${ARBITRUM_BENEFICIARY} 18 | - ARBITRUM_TAKER_PRIVATE_KEY=${ARBITRUM_TAKER_PRIVATE_KEY} 19 | - ARBITRUM_UNLOCK_AUTHORITY_PRIVATE_KEY=${ARBITRUM_UNLOCK_AUTHORITY_PRIVATE_KEY} 20 | 21 | - AVALANCHE_RPC=${AVALANCHE_RPC} 22 | - AVALANCHE_BENEFICIARY=${AVALANCHE_BENEFICIARY} 23 | - AVALANCHE_TAKER_PRIVATE_KEY=${AVALANCHE_TAKER_PRIVATE_KEY} 24 | - AVALANCHE_UNLOCK_AUTHORITY_PRIVATE_KEY=${AVALANCHE_UNLOCK_AUTHORITY_PRIVATE_KEY} 25 | 26 | - BNB_RPC=${BNB_RPC} 27 | - BNB_BENEFICIARY=${BNB_BENEFICIARY} 28 | - BNB_TAKER_PRIVATE_KEY=${BNB_TAKER_PRIVATE_KEY} 29 | - BNB_UNLOCK_AUTHORITY_PRIVATE_KEY=${BNB_UNLOCK_AUTHORITY_PRIVATE_KEY} 30 | 31 | - FANTOM_RPC=${FANTOM_RPC} 32 | - FANTOM_BENEFICIARY=${FANTOM_BENEFICIARY} 33 | - FANTOM_TAKER_PRIVATE_KEY=${FANTOM_TAKER_PRIVATE_KEY} 34 | - FANTOM_UNLOCK_AUTHORITY_PRIVATE_KEY=${FANTOM_UNLOCK_AUTHORITY_PRIVATE_KEY} 35 | 36 | - ETHEREUM_RPC=${ETHEREUM_RPC} 37 | - ETHEREUM_BENEFICIARY=${ETHEREUM_BENEFICIARY} 38 | - ETHEREUM_TAKER_PRIVATE_KEY=${ETHEREUM_TAKER_PRIVATE_KEY} 39 | - ETHEREUM_UNLOCK_AUTHORITY_PRIVATE_KEY=${ETHEREUM_UNLOCK_AUTHORITY_PRIVATE_KEY} 40 | 41 | - LINEA_RPC=${LINEA_RPC} 42 | - LINEA_BENEFICIARY=${LINEA_BENEFICIARY} 43 | - LINEA_TAKER_PRIVATE_KEY=${LINEA_TAKER_PRIVATE_KEY} 44 | - LINEA_UNLOCK_AUTHORITY_PRIVATE_KEY=${LINEA_UNLOCK_AUTHORITY_PRIVATE_KEY} 45 | 46 | - BASE_RPC=${BASE_RPC} 47 | - BASE_BENEFICIARY=${BASE_BENEFICIARY} 48 | - BASE_TAKER_PRIVATE_KEY=${BASE_TAKER_PRIVATE_KEY} 49 | - BASE_UNLOCK_AUTHORITY_PRIVATE_KEY=${BASE_UNLOCK_AUTHORITY_PRIVATE_KEY} 50 | 51 | - OPTIMISM_RPC=${OPTIMISM_RPC} 52 | - OPTIMISM_BENEFICIARY=${OPTIMISM_BENEFICIARY} 53 | - OPTIMISM_TAKER_PRIVATE_KEY=${OPTIMISM_TAKER_PRIVATE_KEY} 54 | - OPTIMISM_UNLOCK_AUTHORITY_PRIVATE_KEY=${OPTIMISM_UNLOCK_AUTHORITY_PRIVATE_KEY} 55 | 56 | - POLYGON_RPC=${POLYGON_RPC} 57 | - POLYGON_BENEFICIARY=${POLYGON_BENEFICIARY} 58 | - POLYGON_TAKER_PRIVATE_KEY=${POLYGON_TAKER_PRIVATE_KEY} 59 | - POLYGON_UNLOCK_AUTHORITY_PRIVATE_KEY=${POLYGON_UNLOCK_AUTHORITY_PRIVATE_KEY} 60 | 61 | - SOLANA_RPC=${SOLANA_RPC} 62 | - SOLANA_TAKER_PRIVATE_KEY=${SOLANA_TAKER_PRIVATE_KEY} 63 | - SOLANA_UNLOCK_AUTHORITY_PRIVATE_KEY=${SOLANA_UNLOCK_AUTHORITY_PRIVATE_KEY} 64 | - SOLANA_BENEFICIARY=${SOLANA_BENEFICIARY} 65 | 66 | -------------------------------------------------------------------------------- /src/chain-solana/tx-generators/tryInitTakerALT.ts: -------------------------------------------------------------------------------- 1 | import { Connection, PublicKey } from '@solana/web3.js'; 2 | import { helpers } from '@debridge-finance/solana-utils'; 3 | import { ChainId, Solana } from '@debridge-finance/dln-client'; 4 | import { Logger } from 'pino'; 5 | import { SolanaTxSigner } from '../signer'; 6 | 7 | async function waitSolanaTxFinalized(connection: Connection, txId: string) { 8 | let finalized = false; 9 | while (!finalized) { 10 | // eslint-disable-next-line no-await-in-loop -- Intentional because works only during initialization 11 | const result = await connection.getTransaction(txId, { 12 | commitment: 'finalized', 13 | maxSupportedTransactionVersion: 1, 14 | }); 15 | if (result !== null) { 16 | finalized = true; 17 | break; 18 | } 19 | // eslint-disable-next-line no-await-in-loop -- Intentional because works only during initialization 20 | await helpers.sleep(1000); 21 | } 22 | } 23 | 24 | export async function tryInitTakerALT( 25 | takerAddress: Uint8Array, 26 | chains: ChainId[], 27 | solanaAdapter: SolanaTxSigner, 28 | solanaClient: Solana.DlnClient, 29 | logger: Logger, 30 | ) { 31 | // WARN: initForTaket requires explicit payer (tx signer) and actual taker addresses 32 | // On MPC feat activation initForTaker payer will be = helper wallet and taker = mpc address 33 | const maybeTxs = await solanaClient.initForTaker( 34 | new PublicKey(solanaAdapter.bytesAddress), 35 | new PublicKey(takerAddress), 36 | chains, 37 | ); 38 | if (!maybeTxs) { 39 | logger.info( 40 | `ALT already initialized or was found: ${solanaClient.fulfillPreswapALT!.toBase58()}`, 41 | ); 42 | 43 | return; 44 | } 45 | 46 | const solanaConnection = solanaClient.getConnection(ChainId.Solana); 47 | const [initTx, ...restTxs] = maybeTxs; 48 | // initALT ix may yield errors like recentSlot is too old/broken blockhash, it's better to add retries 49 | const initTxId = await solanaAdapter.sendTransaction(initTx, { 50 | logger, 51 | options: { 52 | convertIntoTxV0: false, 53 | blockhashCommitment: 'confirmed', 54 | }, 55 | }); 56 | logger.info(`Initialized ALT: ${initTxId}`); 57 | 58 | await waitSolanaTxFinalized(solanaConnection, initTxId); 59 | const txWithFreeze = restTxs.pop(); 60 | if (restTxs.length !== 0) { 61 | const fillIds = await solanaAdapter.sendTransactions(restTxs, { 62 | logger, 63 | options: { 64 | convertIntoTxV0: false, 65 | blockhashCommitment: 'confirmed', 66 | }, 67 | }); 68 | await Promise.all(fillIds.map((txId) => waitSolanaTxFinalized(solanaConnection, txId))); 69 | logger.info(`Fill ALT: ${fillIds.join(', ')}`); 70 | } 71 | if (txWithFreeze) { 72 | const freezeId = await solanaAdapter.sendTransaction(txWithFreeze, { 73 | logger, 74 | options: { 75 | convertIntoTxV0: false, 76 | blockhashCommitment: 'confirmed', 77 | }, 78 | }); 79 | logger.info(`Freezed ALT: ${freezeId}`); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/processors/mempool.service.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from 'pino'; 2 | import { OrderId } from '../interfaces'; 3 | 4 | export type OrderConsumer = (orderId: OrderId) => void; 5 | 6 | export type MempoolOpts = { 7 | baseDelay: number; 8 | baseArchivalDelay: number; 9 | delayStep: number; 10 | archivalDelayStep: number; 11 | }; 12 | 13 | const defaultOpts: MempoolOpts = { 14 | baseDelay: 5, 15 | baseArchivalDelay: 60 * 2, 16 | delayStep: 10, 17 | archivalDelayStep: 60 * 5, 18 | }; 19 | 20 | export class MempoolService { 21 | readonly #logger: Logger; 22 | 23 | readonly #opts: MempoolOpts; 24 | 25 | readonly #trackedOrders = new Map>(); 26 | 27 | constructor( 28 | logger: Logger, 29 | private readonly orderConsumer: OrderConsumer, 30 | ) { 31 | this.#logger = logger.child({ service: MempoolService.name }); 32 | this.#opts = defaultOpts; 33 | } 34 | 35 | delayArchivalOrder(orderId: OrderId, attempt: number) { 36 | this.addOrder( 37 | orderId, 38 | this.#opts.baseArchivalDelay + this.#opts.archivalDelayStep * (attempt - 1), 39 | ); 40 | } 41 | 42 | delayOrder(orderId: OrderId, attempt: number) { 43 | this.addOrder(orderId, this.#opts.baseDelay + this.#opts.delayStep * (attempt - 1)); 44 | } 45 | 46 | /** 47 | * Adds an order to the mempool. An order would be invoked when either a default delay is triggered 48 | * or the given trigger (as Promise) or delay (in seconds) 49 | * @param params 50 | * @param triggerOrDelay 51 | */ 52 | addOrder(orderId: OrderId, delay: number = 0) { 53 | const orderLogger = this.#logger.child({ orderId }); 54 | 55 | if (this.#trackedOrders.has(orderId)) { 56 | clearTimeout(this.#trackedOrders.get(orderId)); 57 | } 58 | 59 | const timeoutId = setTimeout(this.getTimeoutFunc(orderId, orderLogger), delay * 1000); 60 | this.#trackedOrders.set(orderId, timeoutId); 61 | 62 | orderLogger.debug( 63 | `added to mempool (delay: ${delay}s), new mempool size: ${this.#trackedOrders.size} order(s)`, 64 | ); 65 | } 66 | 67 | delete(orderId: string) { 68 | if (this.#trackedOrders.has(orderId)) { 69 | clearTimeout(this.#trackedOrders.get(orderId)); 70 | } 71 | this.#trackedOrders.delete(orderId); 72 | this.#logger.child({ orderId }).debug('order has been removed from the mempool'); 73 | } 74 | 75 | private getTimeoutFunc(orderId: OrderId, logger: Logger) { 76 | const promiseStartTime = new Date(); 77 | return () => { 78 | const settlementTime = new Date(); 79 | const waitingTime = (settlementTime.getTime() - promiseStartTime.getTime()) / 1000; 80 | logger.debug(`mempool promise triggered after ${waitingTime}s`); 81 | if (this.#trackedOrders.has(orderId)) { 82 | logger.debug(`invoking order processing routine`); 83 | this.#trackedOrders.delete(orderId); 84 | this.orderConsumer(orderId); 85 | } else { 86 | logger.debug(`order does not exist in the mempool`); 87 | } 88 | }; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/cli/cli.ts: -------------------------------------------------------------------------------- 1 | import pino from 'pino'; 2 | import pretty from 'pino-pretty'; 3 | import { createWriteStream } from 'pino-sentry'; 4 | import { config } from 'dotenv'; 5 | import path from 'path'; 6 | import { Executor } from '../executor'; 7 | 8 | config(); 9 | 10 | function createLogger() { 11 | const prettyStream = pretty({ 12 | colorize: process.stdout.isTTY, 13 | sync: true, 14 | singleLine: true, 15 | translateTime: 'yyyy-mm-dd HH:MM:ss.l', 16 | }); 17 | const streams: any[] = [ 18 | { 19 | level: 'debug', 20 | stream: prettyStream, 21 | }, 22 | ]; 23 | if (process.env.SENTRY_DSN) { 24 | const sentryStream = createWriteStream({ 25 | dsn: process.env.SENTRY_DSN, 26 | }); 27 | streams.push({ level: 'error', stream: sentryStream }); 28 | } 29 | return pino( 30 | { 31 | level: process.env.LOG_LEVEL || 'info', 32 | translateFormat: 'd mmm yyyy H:MM', 33 | }, 34 | pino.multistream(streams, {}), 35 | ); 36 | } 37 | 38 | async function main() { 39 | let userConfigPath = process.argv[2]; 40 | 41 | if (userConfigPath === undefined) { 42 | userConfigPath = path.join(process.cwd(), 'executor.config.ts'); 43 | } 44 | 45 | if (!path.isAbsolute(userConfigPath)) { 46 | userConfigPath = path.join(process.cwd(), userConfigPath); 47 | userConfigPath = path.normalize(userConfigPath); 48 | } 49 | 50 | try { 51 | require.resolve('typescript'); 52 | } catch { 53 | throw new Error('Typescript not installed'); 54 | } 55 | 56 | try { 57 | require.resolve('ts-node'); 58 | } catch { 59 | throw new Error('ts-node not installed'); 60 | } 61 | 62 | const tsNodeInstance = (process as any)[Symbol.for('ts-node.register.instance')]; 63 | if (!tsNodeInstance) { 64 | // eslint-disable-next-line no-console -- Intentional usage in the entry point on dev machine 65 | console.debug('Loading custom ts-node/register'); 66 | const tsNodeRegisterModule = 'ts-node/register'; 67 | // eslint-disable-next-line global-require, import/no-dynamic-require -- Intentional usage in the entry point on dev machine 68 | require(tsNodeRegisterModule); 69 | } 70 | 71 | // eslint-disable-next-line no-console -- Intentional usage in the entry point 72 | console.log(`Using config file: ${userConfigPath}`); 73 | 74 | // eslint-disable-next-line global-require, import/no-dynamic-require -- Intentional usage to load user config 75 | const importedConfig = require(userConfigPath); 76 | const userConfig = importedConfig.default !== undefined ? importedConfig.default : importedConfig; 77 | 78 | // const executor = new ExecutorEngine(userConfig); 79 | // await executor.init(); 80 | 81 | const executor = new Executor(createLogger()); 82 | await executor.init(userConfig); 83 | } 84 | 85 | main().catch((e) => { 86 | // eslint-disable-next-line no-console -- Intentional usage in the entry point 87 | console.error(`Launching executor failed`); 88 | // eslint-disable-next-line no-console -- Intentional usage in the entry point 89 | console.error(e); 90 | process.exit(1); 91 | }); 92 | -------------------------------------------------------------------------------- /sample.env: -------------------------------------------------------------------------------- 1 | ## 2 | ## For debugging purposes set `debug` 3 | ## 4 | LOG_LEVEL=info 5 | 6 | ## 7 | ## Common infrastructure secrets 8 | ## 9 | 10 | # a key to deBridge-managed websocket service to feed the executor with new orders 11 | # The default value (exposed below) implies rate limits. 12 | # Contact deBridge reps for a corporate key with increased rate limits. 13 | WS_API_KEY=f8bb970668ba4cd15ee64bcbd24479bdf66c6bef9cbb9ece9f2ca3755bc2fe53 14 | 15 | # a key to CoinGecko API. May be left blank as the data from this service is cached by default. 16 | COINGECKO_API_KEY= 17 | 18 | # a key to Sentry to enable logging. May be left blank, for advanced setups only. 19 | SENTRY_DSN= 20 | 21 | # Mandatory 1inch API token (obtain at https://portal.1inch.dev) 22 | ONEINCH_API_V5_TOKEN= 23 | 24 | ## 25 | ## Per-chain secrets 26 | ## 27 | 28 | # url to the RPC node of the chain 29 | # _RPC=https:// 30 | 31 | # defines the private key with the reserve funds available to fulfill orders. 32 | # The DLN executor will sign transactions on behalf of this address, effectively setting approval, 33 | # transferring funds, performing swaps and fulfillments 34 | # _TAKER_PRIVATE_KEY= 35 | 36 | # defines the private key to unlock successfully fulfilled orders. 37 | # The DLN executor will sign transactions on behalf of this address, effectively unlocking the orders 38 | # _UNLOCK_AUTHORITY_PRIVATE_KEY= 39 | 40 | # defines taker controlled address where the orders-locked funds (fulfilled on the other chains) would be unlocked 41 | # _BENEFICIARY=0x... 42 | 43 | # Arbitrum 44 | ARBITRUM_RPC=https:// 45 | ARBITRUM_TAKER_PRIVATE_KEY= 46 | ARBITRUM_UNLOCK_AUTHORITY_PRIVATE_KEY= 47 | ARBITRUM_BENEFICIARY=0x... 48 | 49 | # Avalanche 50 | AVALANCHE_RPC=https:// 51 | AVALANCHE_TAKER_PRIVATE_KEY= 52 | AVALANCHE_UNLOCK_AUTHORITY_PRIVATE_KEY= 53 | AVALANCHE_BENEFICIARY=0x... 54 | 55 | # BNB 56 | BNB_RPC=https://bsc-dataseed.binance.org 57 | BNB_TAKER_PRIVATE_KEY= 58 | BNB_UNLOCK_AUTHORITY_PRIVATE_KEY= 59 | BNB_BENEFICIARY=0x... 60 | 61 | # Ethereum 62 | ETHEREUM_RPC=https:// 63 | ETHEREUM_TAKER_PRIVATE_KEY= 64 | ETHEREUM_UNLOCK_AUTHORITY_PRIVATE_KEY= 65 | ETHEREUM_BENEFICIARY=0x... 66 | 67 | # Polygon 68 | POLYGON_RPC=https:// 69 | POLYGON_TAKER_PRIVATE_KEY= 70 | POLYGON_UNLOCK_AUTHORITY_PRIVATE_KEY= 71 | POLYGON_BENEFICIARY=0x... 72 | 73 | # Fantom 74 | FANTOM_RPC=https:// 75 | FANTOM_TAKER_PRIVATE_KEY= 76 | FANTOM_UNLOCK_AUTHORITY_PRIVATE_KEY= 77 | FANTOM_BENEFICIARY=0x... 78 | 79 | # Linea 80 | LINEA_RPC=https:// 81 | LINEA_TAKER_PRIVATE_KEY= 82 | LINEA_UNLOCK_AUTHORITY_PRIVATE_KEY= 83 | LINEA_BENEFICIARY=0x... 84 | 85 | # Optimism 86 | OPTIMISM_RPC=https:// 87 | OPTIMISM_TAKER_PRIVATE_KEY= 88 | OPTIMISM_UNLOCK_AUTHORITY_PRIVATE_KEY= 89 | OPTIMISM_BENEFICIARY=0x... 90 | 91 | # Base 92 | BASE_RPC=https:// 93 | BASE_TAKER_PRIVATE_KEY= 94 | BASE_UNLOCK_AUTHORITY_PRIVATE_KEY= 95 | BASE_BENEFICIARY=0x... 96 | 97 | # Solana 98 | SOLANA_RPC=https:// 99 | SOLANA_TAKER_PRIVATE_KEY= 100 | SOLANA_UNLOCK_AUTHORITY_PRIVATE_KEY= 101 | SOLANA_BENEFICIARY=2YZKpU... 102 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'airbnb-base', 4 | 'airbnb-typescript/base', 5 | 'prettier' 6 | ], 7 | parser: '@typescript-eslint/parser', 8 | parserOptions: { 9 | project: 'tsconfig.json', 10 | sourceType: 'module', 11 | }, 12 | plugins: ['@typescript-eslint/eslint-plugin', 'eslint-comments'], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | reportUnusedDisableDirectives: true, 19 | rules: { 20 | // currently, dln-taker has ineffective architecture, must be refactored first before enabling this rule 21 | // TODO: refactor class graph / architecture of the dln-taker 22 | 'import/no-cycle': 'off', 23 | 24 | // avoid unnecessary console logging, use explicit logging instead 25 | 'no-console': 'error', 26 | 27 | // prefer named exports over default exports; exception: sample.config.ts 28 | 'import/prefer-default-export': 'off', 29 | 'import/no-default-export': 'error', 30 | 31 | // disable this rule because IMO this is absolutely safe 32 | 'no-plusplus': 'off', 33 | 34 | // keep no-empty-pattern, but still allow empty objects to be used as stubs for unused parameters as this is the 35 | // only way to leave function parameters visible, e.g.: 36 | // async (/* chainId: ChainId */{}, /* context: OrderFilterInitContext */{}) => {} 37 | 'no-empty-pattern': ["error", { "allowObjectPatternsAsParameters": true }], 38 | 39 | // disabling eslint rules is a bad-smelling practice. Give descriptive argument each time the rule is contextually disabled 40 | "eslint-comments/require-description": ["error", {"ignore": []}], 41 | 42 | // override AirBNB rule: allow for-of statements 43 | // https://github.com/airbnb/javascript/blob/b6fc6dc7c3cb76497db0bb81edaa54d8f3427796/packages/eslint-config-airbnb-base/rules/style.js#L257 44 | 'no-restricted-syntax': [ 45 | 'error', 46 | 'ForInStatement', 47 | // 'ForOfStatement', <-- commented out to enable for(... of ...) 48 | 'LabeledStatement', 49 | 'WithStatement', 50 | ], 51 | 52 | 'max-classes-per-file': 'off', 53 | }, 54 | overrides: [{ 55 | files: ["*.test.ts"], 56 | rules: { 57 | // allow console for tests 58 | 'no-console': 'off', 59 | 60 | // tests may do more 61 | 'no-await-in-loop': 'off', 62 | 63 | // disable this rule because specs often call `it(async () => {})` within loops 64 | '@typescript-eslint/no-loop-func': 'off', 65 | 66 | // disable this rule because specs should not be necessary strict, they don't have 67 | // security impact 68 | 'default-case': 'off', 69 | 70 | // disable this rule because specs often use helper functions defined in the bottom of a file 71 | '@typescript-eslint/no-use-before-define': 'off', 72 | 73 | // disable this rule because specs use unnamed function(){}s for grouping 74 | 'func-names': 'off', 75 | 76 | // for interface compatibility 77 | '@typescript-eslint/no-unused-vars': [ 78 | "error", 79 | { 80 | "argsIgnorePattern": "^_", 81 | } 82 | ] 83 | }, 84 | }] 85 | }; 86 | -------------------------------------------------------------------------------- /src/chain-evm/fees/fetcher-eip1559.test.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import Web3 from 'web3'; 3 | import chaiAsPromised from 'chai-as-promised'; 4 | import { safeIntToBigInt } from '../../utils'; 5 | import { EIP1551Fee, getEip1559FeeFetcher, priorityFeeEstimator } from './fetcher-eip1559'; 6 | import { GasCategory } from './types'; 7 | import { createTestFrameworkLogger } from '../../../tests/helpers/index'; 8 | 9 | chai.use(chaiAsPromised); 10 | 11 | describe('EIP1559 fee fetchers', () => { 12 | it('eip1559FeeFetcher should correctly pick a fee', async () => { 13 | const baseFeeMultipliers = { 14 | [GasCategory.PROJECTED]: 1.125, 15 | [GasCategory.NORMAL]: 1.125, 16 | [GasCategory.AGGRESSIVE]: 1.125 * 2, 17 | }; 18 | const priorityFeePercentiles = { 19 | [GasCategory.PROJECTED]: 25, 20 | [GasCategory.NORMAL]: 25, 21 | [GasCategory.AGGRESSIVE]: 50, 22 | }; 23 | const baseFee = 2e9; 24 | const priorityFee = 102_000_000_000n; 25 | 26 | const mockedGasPrice = (1e6).toString(); 27 | 28 | const expectedFees: Array<[gasCategory: GasCategory, fees: EIP1551Fee]> = [ 29 | [ 30 | GasCategory.NORMAL, 31 | { 32 | baseFee: safeIntToBigInt(baseFee * baseFeeMultipliers[GasCategory.NORMAL]), 33 | maxFeePerGas: 34 | priorityFee + safeIntToBigInt(baseFee * baseFeeMultipliers[GasCategory.NORMAL]), 35 | maxPriorityFeePerGas: priorityFee, 36 | }, 37 | ], 38 | [ 39 | GasCategory.AGGRESSIVE, 40 | { 41 | baseFee: safeIntToBigInt(baseFee * baseFeeMultipliers[GasCategory.AGGRESSIVE]), 42 | maxFeePerGas: 43 | priorityFee + safeIntToBigInt(baseFee * baseFeeMultipliers[GasCategory.AGGRESSIVE]), 44 | maxPriorityFeePerGas: priorityFee, 45 | }, 46 | ], 47 | ]; 48 | 49 | const conn = { 50 | eth: { 51 | getGasPrice: async () => mockedGasPrice.toString(), 52 | getFeeHistory(_blockCount, _lastBlock, _rewardPercentiles) { 53 | return Promise.resolve({ 54 | baseFeePerGas: [baseFee.toString()], 55 | reward: [ 56 | [100_000_000_000n.toString()], 57 | [105_000_000_000n.toString()], 58 | [102_000_000_000n.toString()], 59 | ], 60 | }); 61 | }, 62 | }, 63 | }; 64 | for (const [gasCategory, expectedFee] of expectedFees) { 65 | const feeFetcher = getEip1559FeeFetcher(baseFeeMultipliers, priorityFeePercentiles); 66 | const actualFee = await feeFetcher(gasCategory, conn, createTestFrameworkLogger()); 67 | expect(actualFee).to.deep.eq(expectedFee, `mocked: ${GasCategory[gasCategory]}`); 68 | } 69 | }); 70 | 71 | it('priorityFeeEstimator should correctly pick a tip', async () => { 72 | const rewards = [ 73 | [100_000_000_000n.toString()], 74 | [105_000_000_000n.toString()], 75 | [102_000_000_000n.toString()], 76 | ]; 77 | 78 | // The median should be taken because none of the changes are big enough to ignore values. 79 | expect(priorityFeeEstimator(rewards)).to.eq(102_000_000_000n); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /src/processors/swap-connector-implementation.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChainId, 3 | Logger, 4 | OneInch, 5 | SwapConnector, 6 | SwapConnectorQuoteRequest, 7 | SwapConnectorQuoteResult, 8 | SwapConnectorRequest, 9 | SwapConnectorResult, 10 | } from '@debridge-finance/dln-client'; 11 | 12 | type OneInchConfig = { 13 | apiToken?: string; 14 | apiServer?: string; 15 | disablePMMProtocols?: boolean; 16 | disabledProtocols?: string[]; 17 | }; 18 | 19 | export class SwapConnectorImplementationService implements SwapConnector { 20 | readonly #connectors: { [key in ChainId]: SwapConnector | null }; 21 | 22 | constructor(configuration?: { oneInchConfig?: OneInchConfig }) { 23 | const oneInchV5Connector = new OneInch.OneInchV5Connector({ 24 | customApiURL: configuration?.oneInchConfig?.apiServer || 'https://api.1inch.dev/swap', 25 | token: configuration?.oneInchConfig?.apiToken, 26 | disablePMMProtocols: configuration?.oneInchConfig?.disablePMMProtocols, 27 | disabledProtocols: configuration?.oneInchConfig?.disabledProtocols, 28 | }); 29 | 30 | this.#connectors = { 31 | [ChainId.Arbitrum]: oneInchV5Connector, 32 | [ChainId.ArbitrumTest]: null, 33 | [ChainId.Avalanche]: oneInchV5Connector, 34 | [ChainId.AvalancheTest]: null, 35 | [ChainId.Base]: oneInchV5Connector, 36 | [ChainId.BSC]: oneInchV5Connector, 37 | [ChainId.BSCTest]: null, 38 | [ChainId.Ethereum]: oneInchV5Connector, 39 | [ChainId.Fantom]: oneInchV5Connector, 40 | [ChainId.Heco]: null, 41 | [ChainId.HecoTest]: null, 42 | [ChainId.Kovan]: null, 43 | [ChainId.Linea]: oneInchV5Connector, 44 | [ChainId.Neon]: null, 45 | [ChainId.Optimism]: oneInchV5Connector, 46 | [ChainId.Polygon]: oneInchV5Connector, 47 | [ChainId.PolygonTest]: null, 48 | [ChainId.Solana]: null, 49 | }; 50 | } 51 | 52 | setConnector(chainId: ChainId, connector: SwapConnector) { 53 | this.#connectors[chainId] = connector; 54 | } 55 | 56 | getEstimate( 57 | request: SwapConnectorQuoteRequest, 58 | context: { logger: Logger }, 59 | ): Promise { 60 | return this.#getConnector(request.chainId).getEstimate(request, context); 61 | } 62 | 63 | getSupportedChains(): ChainId[] { 64 | return Object.keys(this.#connectors) 65 | .map((chainId) => chainId as unknown as ChainId) 66 | .filter((chainId) => this.#connectors[chainId] !== null); 67 | } 68 | 69 | getSwap( 70 | request: SwapConnectorRequest, 71 | context: { logger: Logger }, 72 | ): Promise { 73 | return this.#getConnector(request.chainId).getSwap(request, context); 74 | } 75 | 76 | setSupportedChains(chains: ChainId[]): void { 77 | Object.keys(this.#connectors) 78 | .map((chainId) => chainId as unknown as ChainId) 79 | .filter((chainId) => !chains.includes(chainId)) 80 | .forEach((chainId) => this.#connectors[chainId] === null); 81 | } 82 | 83 | #getConnector(chainId: ChainId): SwapConnector { 84 | const connector = this.#connectors[chainId]; 85 | if (connector === null) { 86 | throw new Error(`Unsupported chain in swap connector`); 87 | } 88 | 89 | return connector; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/hooks/HookEnums.ts: -------------------------------------------------------------------------------- 1 | export enum Hooks { 2 | OrderFeedConnected, 3 | OrderFeedDisconnected, 4 | OrderRejected, 5 | OrderPostponed, 6 | OrderFulfilled, 7 | OrderUnlockSent, 8 | OrderUnlockFailed, 9 | } 10 | 11 | export enum PostponingReason { 12 | /** 13 | * indicates that taker’s reserve account has not enough funds to fulfill the order 14 | */ 15 | NOT_ENOUGH_BALANCE, 16 | 17 | /** 18 | * indicates that the current order is not profitable at the time of estimation 19 | */ 20 | NOT_PROFITABLE, 21 | 22 | /** 23 | * indicates the inability to ensure the inclusion of the txn into the blockchain (e.g., we were unable to get the txn hash in the reasonable amount of time, or the RPC node is unavailable) 24 | */ 25 | FULFILLMENT_TX_FAILED, 26 | 27 | /** 28 | * indicates the unable to estimate preliminary fulfill 29 | */ 30 | FULFILLMENT_EVM_TX_PREESTIMATION_FAILED, 31 | 32 | /** 33 | * Unexpected error 34 | */ 35 | UNHANDLED_ERROR, 36 | 37 | /** 38 | * indicates that this order is forcibly delayed according to this dln-takers instance configuration 39 | */ 40 | FORCED_DELAY, 41 | 42 | /** 43 | * triggered when the value of a new order potentially increases the TVL of the source chain beyond the given budget 44 | * (if being successfully fulfilled). 45 | */ 46 | TVL_BUDGET_EXCEEDED, 47 | 48 | /** 49 | * triggered when throughput has reached its limit for the given give chain 50 | */ 51 | CAPPED_THROUGHPUT, 52 | 53 | /** 54 | * indicates that the order is missing on the give chain. One of the possible reasons is a lag in the RPC node, which 55 | * is often the case when it comes to Solana 56 | */ 57 | MISSING, 58 | } 59 | 60 | export enum RejectionReason { 61 | /** 62 | * indicates that the order on the give chain locks a token which is not registered in any token buckets in the executor’s configuration 63 | */ 64 | UNEXPECTED_GIVE_TOKEN, 65 | 66 | /** 67 | * order is already fulfilled 68 | */ 69 | ALREADY_FULFILLED_OR_CANCELLED, 70 | 71 | /** 72 | * indicates that the order on the give chain has non-zero status (e.g., unlocked) 73 | */ 74 | UNEXPECTED_GIVE_STATUS, 75 | 76 | /** 77 | * indicates that the order is revoked due to chain reorg 78 | */ 79 | REVOKED, 80 | 81 | /** 82 | * indicates that announced block confirmations is less than the block confirmation constraint 83 | */ 84 | NOT_ENOUGH_BLOCK_CONFIRMATIONS_FOR_ORDER_WORTH, 85 | 86 | /** 87 | * indicates that non-finalized order is not covered by any custom block confirmation range 88 | */ 89 | NOT_YET_FINALIZED, 90 | 91 | /** 92 | * indicates that the order requires reserve token to be pre-swapped to the take token, but the operation can't be 93 | * performed because swaps are not available on the take chain 94 | */ 95 | UNAVAILABLE_PRE_FULFILL_SWAP, 96 | 97 | /** 98 | * Received malformed order from ws 99 | */ 100 | MALFORMED_ORDER, 101 | 102 | /** 103 | * Indicates that order includes the provided allowedTakerDst, which differs from the taker's address 104 | */ 105 | WRONG_TAKER, 106 | 107 | FILTERED_OFF, 108 | } 109 | -------------------------------------------------------------------------------- /src/chain-solana/tx-builder.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, OrderDataWithId, Solana } from '@debridge-finance/dln-client'; 2 | import { Logger } from 'pino'; 3 | import { setTimeout } from 'timers/promises'; 4 | import { createBatchOrderUnlockTx } from './tx-generators/createBatchOrderUnlockTx'; 5 | import { tryInitTakerALT } from './tx-generators/tryInitTakerALT'; 6 | import { createOrderFullfillTx } from './tx-generators/createOrderFullfillTx'; 7 | import { SolanaTxSigner } from './signer'; 8 | import { IExecutor } from '../executor'; 9 | import { OrderEstimation } from '../chain-common/order-estimator'; 10 | import { InitTransactionBuilder } from '../processor'; 11 | import { FulfillTransactionBuilder } from '../chain-common/order-taker'; 12 | import { BatchUnlockTransactionBuilder } from '../processors/BatchUnlocker'; 13 | 14 | export class SolanaTransactionBuilder 15 | implements InitTransactionBuilder, FulfillTransactionBuilder, BatchUnlockTransactionBuilder 16 | { 17 | constructor( 18 | private solanaClient: Solana.DlnClient, 19 | private readonly signer: SolanaTxSigner, 20 | private readonly executor: IExecutor, 21 | ) {} 22 | 23 | get fulfillAuthority() { 24 | return { 25 | address: this.signer.address, 26 | bytesAddress: this.signer.bytesAddress, 27 | }; 28 | } 29 | 30 | get unlockAuthority() { 31 | return { 32 | address: this.signer.address, 33 | bytesAddress: this.signer.bytesAddress, 34 | }; 35 | } 36 | 37 | getOrderFulfillTxSender(orderEstimation: OrderEstimation, logger: Logger) { 38 | return async () => 39 | this.signer.sendTransaction(await createOrderFullfillTx(orderEstimation, logger), { 40 | logger, 41 | options: {}, 42 | }); 43 | } 44 | 45 | getBatchOrderUnlockTxSender(orders: OrderDataWithId[], logger: Logger): () => Promise { 46 | return async () => 47 | this.signer.sendTransaction(await createBatchOrderUnlockTx(this.executor, orders, logger), { 48 | logger, 49 | options: {}, 50 | }); 51 | } 52 | 53 | async getInitTxSenders(logger: Logger) { 54 | logger.debug('initialize solanaClient.destination.debridge...'); 55 | await this.solanaClient.destination.debridge.init(); 56 | 57 | const maxAttempts = 5; 58 | for (let i = 0; i < maxAttempts; i += 1) { 59 | try { 60 | // eslint-disable-next-line no-await-in-loop -- Intentional because works only during initialization 61 | await tryInitTakerALT( 62 | this.executor.getSupportedChain(ChainId.Solana).fulfillAuthority.bytesAddress, 63 | Object.values(this.executor.chains).map((chainConfig) => chainConfig.chain), 64 | this.signer, 65 | this.solanaClient, 66 | logger, 67 | ); 68 | 69 | return []; 70 | } catch (e) { 71 | const attempt = i + 1; 72 | logger.info(`Unable to initialize alts (attempt ${attempt}/${maxAttempts})`); 73 | if (attempt === maxAttempts) logger.error(e); 74 | // sleep for 2s 75 | // eslint-disable-next-line no-await-in-loop -- Intentional because works only during initialization 76 | await setTimeout(2000); 77 | } 78 | } 79 | 80 | throw new Error('Unable to initialize alts, restart the taker'); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/chain-evm/fees/manager.test.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import Web3 from 'web3'; 3 | import chaiAsPromised from 'chai-as-promised'; 4 | import { CappedFeeReachedError, EvmFeeManager, LegacyGasExtension } from './manager'; 5 | import { GasCategory } from './types'; 6 | import { createTestFrameworkLogger } from '../../../tests/helpers/index'; 7 | 8 | chai.use(chaiAsPromised); 9 | 10 | describe('EvmFeeManager', () => { 11 | describe('Checking legacy transaction population', () => { 12 | it('should populateTx with disabled capping', async () => { 13 | const lowGas = 30n; 14 | const highGas = 50n; 15 | const gasLimit = 1000n; 16 | const cappingAllowance = gasLimit * highGas - 1n; 17 | const conn = { 18 | eth: { 19 | estimateGas: async (_transactionConfig: any) => Number(gasLimit.toString()), 20 | }, 21 | }; 22 | 23 | const manager = new EvmFeeManager( 24 | conn, 25 | true, 26 | { 27 | gasLimitMultiplier: 1, 28 | overcappingAllowed: false, 29 | }, 30 | { 31 | legacyFeeFetcher: async (gasCategory) => 32 | gasCategory === GasCategory.AGGRESSIVE ? highGas : lowGas, 33 | }, 34 | ); 35 | 36 | // Normal gas category: gas is set as is 37 | const tx = await manager.populateTx( 38 | {}, 39 | cappingAllowance, 40 | GasCategory.NORMAL, 41 | { logger: createTestFrameworkLogger() }, 42 | ); 43 | expect(tx.gasPrice).to.eq('30'); 44 | 45 | // Must exceed capped gas 46 | await expect( 47 | manager.populateTx({}, cappingAllowance, GasCategory.AGGRESSIVE, { 48 | logger: createTestFrameworkLogger(), 49 | }), 50 | ).to.be.rejectedWith(CappedFeeReachedError, 'Overcapping disabled'); 51 | }); 52 | 53 | it('should populateTx with overcapping allowed', async () => { 54 | const gas = 20n; 55 | const gasLimit = 1000n; 56 | const cappingAllowance = gasLimit * gas - 1n; 57 | const conn = { 58 | eth: { 59 | estimateGas: async (_transactionConfig: any) => Number(gasLimit.toString()), 60 | }, 61 | }; 62 | 63 | const manager = new EvmFeeManager( 64 | conn, 65 | true, 66 | { 67 | gasLimitMultiplier: 1, 68 | overcappingAllowed: true, 69 | overcappingAllowance: 2, 70 | }, 71 | { 72 | legacyFeeFetcher: async (gasCategory) => 73 | gasCategory === GasCategory.AGGRESSIVE ? gas * 2n : gas, 74 | }, 75 | ); 76 | 77 | // Normal gas category: gas is set as is 78 | const tx = await manager.populateTx( 79 | {}, 80 | cappingAllowance, 81 | GasCategory.NORMAL, 82 | { logger: createTestFrameworkLogger() }, 83 | ); 84 | expect(tx.gasPrice).to.eq('20'); 85 | 86 | // Must exceed capped gas 87 | await expect( 88 | manager.populateTx({}, cappingAllowance, GasCategory.AGGRESSIVE, { 89 | logger: createTestFrameworkLogger(), 90 | }), 91 | ).to.be.rejectedWith(CappedFeeReachedError, 'Unable to populate pricing'); 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /src/chain-common/order-taker.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from 'pino'; 2 | import { assert } from '../errors'; 3 | import { PostponingReason, RejectionReason } from '../hooks/HookEnums'; 4 | import { Authority } from '../interfaces'; 5 | 6 | import { CreatedOrder } from './order'; 7 | import { explainEstimation, OrderEstimation } from './order-estimator'; 8 | import { TransactionSender, TxHash } from './tx-builder'; 9 | 10 | export interface FulfillTransactionBuilder { 11 | fulfillAuthority: Authority; 12 | getOrderFulfillTxSender(orderEstimation: OrderEstimation, logger: Logger): TransactionSender; 13 | } 14 | 15 | export type TakerShortCircuit = { 16 | postpone(postpone: PostponingReason, message: string, delay?: number): Promise; 17 | reject(rejection: RejectionReason, message: string): Promise; 18 | }; 19 | 20 | export class CreatedOrderTaker { 21 | readonly #logger: Logger; 22 | 23 | constructor( 24 | private readonly order: CreatedOrder, 25 | logger: Logger, 26 | ) { 27 | this.#logger = logger.child({ service: CreatedOrderTaker.name, orderId: order.orderId }); 28 | } 29 | 30 | async take(sc: TakerShortCircuit, transactionBuilder: FulfillTransactionBuilder) { 31 | const startedAt = new Date().getTime(); 32 | 33 | this.#logger.debug('+ attempting to validate'); 34 | const estimator = await this.order.getValidator(sc).validate(); 35 | 36 | this.#logger.debug('+ attempting to estimate'); 37 | const estimation = await estimator.getEstimation(); 38 | 39 | if (estimation.isProfitable === false) { 40 | return sc.postpone(PostponingReason.NOT_PROFITABLE, await explainEstimation(estimation)); 41 | } 42 | 43 | const evaluationTime = (new Date().getTime() - startedAt) / 1000; 44 | this.#logger.debug(`+ attempting to fulfill after evaluating for ${evaluationTime}s`); 45 | const fulfillTxHash = await this.attemptFulfil(transactionBuilder, estimation, sc); 46 | assert(typeof fulfillTxHash === 'string', 'should have raised an error'); 47 | const elapsedTime = (new Date().getTime() - startedAt) / 1000; 48 | this.#logger.info( 49 | `✔ fulfill tx broadcasted, txhash: ${fulfillTxHash}, took ${evaluationTime}s to evaluate, ${elapsedTime}s overall`, 50 | ); 51 | 52 | // we add this order to the budget controller right before the txn is broadcasted 53 | // Mind that in case of an error (see the catch{} block below) we don't remove it from the 54 | // controller because the error may occur because the txn was stuck in the mempool and reside there 55 | // for a long period of time 56 | this.order.giveChain.throughput.addOrder( 57 | this.order.orderId, 58 | this.order.blockConfirmations, 59 | await this.order.getUsdValue(), 60 | ); 61 | 62 | this.order.giveChain.TVLBudgetController.flushCache(); 63 | 64 | this.order.executor.hookEngine.handleOrderFulfilled({ 65 | orderId: this.order.orderId, 66 | order: this.order.orderData, 67 | txHash: fulfillTxHash, 68 | }); 69 | 70 | return Promise.resolve(); 71 | } 72 | 73 | private async attemptFulfil( 74 | transactionBuilder: FulfillTransactionBuilder, 75 | estimation: OrderEstimation, 76 | sc: TakerShortCircuit, 77 | ): Promise { 78 | try { 79 | return await transactionBuilder.getOrderFulfillTxSender(estimation, this.#logger)(); 80 | } catch (e) { 81 | this.#logger.error(`fulfill tx failed: ${e}`); 82 | this.#logger.error(e); 83 | return sc.postpone(PostponingReason.FULFILLMENT_TX_FAILED, `${e}`); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@debridge-finance/dln-taker", 3 | "version": "3.3.1", 4 | "description": "DLN executor is the rule-based daemon service developed to automatically execute orders placed on the deSwap Liquidity Network (DLN) across supported blockchains", 5 | "license": "GPL-3.0-only", 6 | "author": "deBridge", 7 | "homepage": "https://debridge.finance", 8 | "repository": "github:debridge-finance/dln-taker", 9 | "main": "dist/index.js", 10 | "bin": { 11 | "dln-taker": "./dist/cli/bootstrap.js" 12 | }, 13 | "files": [ 14 | "dist/" 15 | ], 16 | "scripts": { 17 | "build": "npm-run-all clean compile && chmod +x ./dist/cli/bootstrap.js", 18 | "clean": "rimraf ./dist", 19 | "compile": "tsc -P tsconfig.build.json", 20 | "executor": "ts-node -P tsconfig.dev.json ./src/cli/bootstrap.ts", 21 | "format": "npm-run-all lint pretty", 22 | "lint": "npm-run-all lint:ts lint:tsc", 23 | "lint:ts": "eslint --fix \"{src,tests}/**/*.ts\" sample.config.ts", 24 | "lint:tsc": "tsc -P tsconfig.verify1.json && tsc -P tsconfig.verify2.json", 25 | "pre-commit": "lint-staged", 26 | "prepare": "husky install", 27 | "pretty": "npm-run-all pretty:package pretty:ts", 28 | "pretty:package": "prettier-package-json --write", 29 | "pretty:ts": "prettier --write \"{src,tests}/**/*.ts\" sample.config.ts hardhat.config.ts", 30 | "start:compiled": "./dist/cli/bootstrap.js", 31 | "test": "IS_TEST=true mocha -r ts-node/register -b -t 100000 './{src,tests}/**/*.test.ts'", 32 | "test:verbose": "IS_TEST=true TEST_LOG_LEVEL=debug mocha -r ts-node/register -b -t 100000 './{src,tests}/**/*.test.ts'" 33 | }, 34 | "types": "./dist/index.d.ts", 35 | "dependencies": { 36 | "@debridge-finance/dln-client": "8.3.6", 37 | "@debridge-finance/legacy-dln-profitability": "3.2.0", 38 | "@debridge-finance/solana-utils": "4.2.1", 39 | "@protobuf-ts/plugin": "2.8.1", 40 | "@solana/web3.js": "1.66.2", 41 | "axios": "0.21.4", 42 | "axios-cache-adapter": "2.7.3", 43 | "bignumber.js": "9.1.2", 44 | "bs58": "5.0.0", 45 | "dotenv": "16.0.3", 46 | "node-cache": "5.1.2", 47 | "pino": "8.7.0", 48 | "pino-pretty": "9.1.1", 49 | "pino-sentry": "0.13.0", 50 | "web3": "1.8.0", 51 | "ws": "8.10.0" 52 | }, 53 | "peerDependencies": { 54 | "ts-node": "*", 55 | "typescript": "*" 56 | }, 57 | "peerDependenciesMeta": { 58 | "ts-node": { 59 | "optional": true 60 | }, 61 | "typescript": { 62 | "optional": true 63 | } 64 | }, 65 | "devDependencies": { 66 | "@nomicfoundation/hardhat-chai-matchers": "^2.0.2", 67 | "@nomicfoundation/hardhat-ethers": "^3.0.5", 68 | "@nomicfoundation/hardhat-network-helpers": "^1.0.10", 69 | "@nomiclabs/hardhat-web3": "^2.0.0", 70 | "@types/chai": "4.3.3", 71 | "@types/chai-as-promised": "^7.1.8", 72 | "@types/mocha": "9.1.1", 73 | "@types/node": "18.11.9", 74 | "@typescript-eslint/eslint-plugin": "^6.4.1", 75 | "@typescript-eslint/parser": "^6.4.1", 76 | "assert": "2.0.0", 77 | "chai": "^4.3.10", 78 | "chai-as-promised": "^7.1.1", 79 | "eslint-config-airbnb-typescript": "^17.1.0", 80 | "eslint-config-prettier": "^9.0.0", 81 | "eslint-plugin-eslint-comments": "^3.2.0", 82 | "eslint-plugin-prettier": "^5.0.0", 83 | "hardhat": "^2.19.2", 84 | "husky": "^8.0.3", 85 | "lint-staged": "^14.0.1", 86 | "mocha": "10.1.0", 87 | "npm-run-all": "^4.1.5", 88 | "prettier-package-json": "^2.8.0", 89 | "rimraf": "3.0.2", 90 | "ts-node": "10.9.1", 91 | "typescript": "~5.1.6" 92 | }, 93 | "keywords": [ 94 | "DLN", 95 | "deBridge", 96 | "ethereum", 97 | "sdk", 98 | "solana" 99 | ], 100 | "engines": { 101 | "node": ">=18" 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/processors/DataStore.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, OrderDataWithId } from '@debridge-finance/dln-client'; 2 | import { helpers } from '@debridge-finance/solana-utils'; 3 | import { StatsAPI } from './StatsAPI'; 4 | import { IExecutor } from '../executor'; 5 | 6 | export class DataStore { 7 | private statsApi: StatsAPI = new StatsAPI(); 8 | 9 | constructor(private executor: IExecutor) {} 10 | 11 | async getPendingForUnlockOrders(from: ChainId): Promise> { 12 | const orderIds = await this.getPendingForUnlockOrderIds(from); 13 | const orders: OrderDataWithId[] = []; 14 | for (const orderId of orderIds) { 15 | // eslint-disable-next-line no-await-in-loop -- Very ugly but intentionally acceptable unless TVLBudget feature gets exposure OR dln-taker starts using StatsApi heavily during the initialization TODO #862karugz 16 | const order = DataStore.convertOrder(await this.statsApi.getOrderLiteModel(orderId)); 17 | orders.push(order); 18 | } 19 | 20 | return orders; 21 | } 22 | 23 | private async getPendingForUnlockOrderIds(from: ChainId): Promise { 24 | const unlockAuthorities = this.executor 25 | .getSupportedChainIds() 26 | .map((chainId) => this.executor.getSupportedChain(chainId).unlockAuthority.address); 27 | 28 | let skip = 0; 29 | let hasMoreOrders = true; 30 | const orderIds: string[] = []; 31 | while (hasMoreOrders) { 32 | // eslint-disable-next-line no-await-in-loop -- Pagination is intentionally acceptable here 33 | const getForUnlockOrders = await this.statsApi.getForUnlockAuthorities( 34 | [from], 35 | ['Fulfilled', 'SentUnlock'], 36 | unlockAuthorities, 37 | skip, 38 | 100, // take 39 | ); 40 | skip += getForUnlockOrders.orders.length; 41 | orderIds.push(...getForUnlockOrders.orders.map((orderDTO) => orderDTO.orderId.stringValue)); 42 | 43 | if ( 44 | getForUnlockOrders.orders.length === 0 || 45 | orderIds.length >= getForUnlockOrders.totalCount 46 | ) { 47 | hasMoreOrders = false; 48 | } 49 | } 50 | 51 | return orderIds; 52 | } 53 | 54 | private static convertOrder( 55 | order: Awaited>, 56 | ): OrderDataWithId { 57 | return { 58 | orderId: helpers.hexToBuffer(order.orderId.stringValue), 59 | 60 | nonce: BigInt(order.makerOrderNonce), 61 | maker: DataStore.parseBytesArray(order.makerSrc.bytesArrayValue), 62 | give: { 63 | tokenAddress: DataStore.parseBytesArray(order.giveOffer.tokenAddress.bytesArrayValue), 64 | amount: BigInt(order.giveOffer.amount.stringValue), 65 | chainId: Number(order.giveOffer.chainId.stringValue), 66 | }, 67 | take: { 68 | tokenAddress: DataStore.parseBytesArray(order.takeOffer.tokenAddress.bytesArrayValue), 69 | amount: BigInt(order.takeOffer.amount.stringValue), 70 | chainId: Number(order.takeOffer.chainId.stringValue), 71 | }, 72 | receiver: DataStore.parseBytesArray(order.receiverDst.bytesArrayValue), 73 | givePatchAuthority: DataStore.parseBytesArray(order.givePatchAuthoritySrc.bytesArrayValue), 74 | orderAuthorityDstAddress: DataStore.parseBytesArray( 75 | order.orderAuthorityAddressDst.bytesArrayValue, 76 | ), 77 | allowedTaker: order.allowedTakerDst.bytesArrayValue 78 | ? DataStore.parseBytesArray(order.allowedTakerDst.bytesArrayValue) 79 | : undefined, 80 | allowedCancelBeneficiary: order.allowedCancelBeneficiarySrc.bytesArrayValue 81 | ? DataStore.parseBytesArray(order.allowedCancelBeneficiarySrc.bytesArrayValue) 82 | : undefined, 83 | externalCall: undefined, 84 | }; 85 | } 86 | 87 | private static parseBytesArray(bytesArrayString: string): Uint8Array { 88 | return new Uint8Array(JSON.parse(bytesArrayString)); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/chain-evm/preferences/store.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '@debridge-finance/dln-client'; 2 | import Web3 from 'web3'; 3 | import { getAvgBlockSpeed, getFinalizedBlockConfirmations } from '../../config'; 4 | import { assert } from '../../errors'; 5 | import { suggestEvmTxBroadcasterOpts, EvmTxBroadcasterOpts } from '../networking/broadcaster'; 6 | import { defaultFeeManagerOpts } from '../fees/defaults'; 7 | import { 8 | EvmFeeFetchers, 9 | EvmFeeManager, 10 | EvmFeeManagerOpts, 11 | getDefaultFetchers, 12 | IEvmFeeManager, 13 | } from '../fees/manager'; 14 | import { suggestedOpts as bnbSuggestions } from './BNB'; 15 | import { suggestedOpts as polygonSuggestions } from './Polygon'; 16 | 17 | export type EvmChainPreferences = { 18 | feeManager: IEvmFeeManager; 19 | parameters: EvmChainParameters; 20 | broadcasterOpts: EvmTxBroadcasterOpts; 21 | }; 22 | 23 | type InputOpts = { 24 | connection: Web3; 25 | feeManagerOpts?: Partial; 26 | parameters?: Partial; 27 | broadcasterOpts?: Partial; 28 | }; 29 | 30 | export type SuggestedOpts = { 31 | feeManagerOpts?: Partial; 32 | feeHandlers?: (fees: EvmFeeManagerOpts) => Partial; 33 | parameters?: Partial; 34 | broadcasterOpts?: Partial; 35 | }; 36 | 37 | export type EvmChainParameters = { 38 | isLegacy: boolean; 39 | avgBlockSpeed: number; 40 | finalizedBlockConfirmations: number; 41 | }; 42 | 43 | // see https://docs.rs/ethers-core/latest/src/ethers_core/types/chain.rs.html#55-166 44 | const eip1559Compatible: Array = [ 45 | ChainId.Arbitrum, 46 | ChainId.Avalanche, 47 | ChainId.Ethereum, 48 | ChainId.Linea, 49 | ChainId.Polygon, 50 | ChainId.Base, 51 | ChainId.Optimism, 52 | ]; 53 | 54 | const chainSuggestions: { [key in ChainId]?: SuggestedOpts } = { 55 | [ChainId.BSC]: bnbSuggestions, 56 | [ChainId.Polygon]: polygonSuggestions, 57 | }; 58 | 59 | function getDefaultParametersFor(chainId: ChainId) { 60 | return { 61 | isLegacy: !eip1559Compatible.includes(chainId), 62 | avgBlockSpeed: getAvgBlockSpeed(chainId), 63 | finalizedBlockConfirmations: getFinalizedBlockConfirmations(chainId), 64 | }; 65 | } 66 | 67 | export class EvmChainPreferencesStore { 68 | static #store: { [key in ChainId]?: EvmChainPreferences } = {}; 69 | 70 | static set(chainId: ChainId, input: InputOpts) { 71 | assert( 72 | !EvmChainPreferencesStore.#store[chainId], 73 | `${ChainId[chainId]} preferences store already initialized`, 74 | ); 75 | 76 | const evmFeeOpts: EvmFeeManagerOpts = { 77 | ...defaultFeeManagerOpts, 78 | ...chainSuggestions[chainId]?.feeManagerOpts, 79 | ...input.feeManagerOpts, 80 | }; 81 | const evmFeeHandlers: EvmFeeFetchers = { 82 | ...getDefaultFetchers(evmFeeOpts), 83 | ...chainSuggestions[chainId]?.feeHandlers?.(evmFeeOpts), 84 | }; 85 | const parameters: EvmChainParameters = { 86 | ...getDefaultParametersFor(chainId), 87 | ...chainSuggestions[chainId]?.parameters, 88 | ...input.parameters, 89 | }; 90 | EvmChainPreferencesStore.#store[chainId] = { 91 | parameters, 92 | feeManager: new EvmFeeManager( 93 | input.connection, 94 | parameters.isLegacy, 95 | evmFeeOpts, 96 | evmFeeHandlers, 97 | ), 98 | broadcasterOpts: { 99 | ...suggestEvmTxBroadcasterOpts(parameters.avgBlockSpeed), 100 | ...input.broadcasterOpts, 101 | }, 102 | }; 103 | } 104 | 105 | static get(chainId: ChainId): EvmChainPreferences { 106 | assert( 107 | EvmChainPreferencesStore.#store[chainId] !== undefined, 108 | `${ChainId[chainId]} preferences store not yet initialized`, 109 | ); 110 | return EvmChainPreferencesStore.#store[chainId]!; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/chain-evm/fees/fetcher-eip1559.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from 'pino'; 2 | import Web3 from 'web3'; 3 | import { findMaxBigInt, safeIntToBigInt } from '../../utils'; 4 | import { defaultFeeManagerOpts } from './defaults'; 5 | import { GasCategory } from './types'; 6 | 7 | export type EIP1551Fee = { 8 | baseFee: bigint; 9 | maxFeePerGas: bigint; 10 | maxPriorityFeePerGas: bigint; 11 | }; 12 | 13 | // based on work by [MyCrypto](https://github.com/MyCryptoHQ/MyCrypto/blob/master/src/services/ApiService/Gas/eip1559.ts) 14 | export function priorityFeeEstimator(history: string[][], increaseBoundary: number = 200): bigint { 15 | const tips = history 16 | .map((r) => BigInt(r[0])) 17 | .filter((r) => r !== 0n) 18 | .sort(); 19 | 20 | if (tips.length === 0) return 0n; 21 | if (tips.length === 1) return tips[0]; 22 | 23 | // Calculate percentage increases from between ordered list of fees 24 | const percentageIncreases = tips.reduce( 25 | (acc, cur, i, arr) => { 26 | if (i === arr.length - 1) { 27 | return acc; 28 | } 29 | const next = arr[i + 1]; 30 | const p = ((next - cur) * 100n) / cur; 31 | return [...acc, p]; 32 | }, 33 | [], 34 | ); 35 | 36 | const highestIncrease = findMaxBigInt(...percentageIncreases); 37 | const highestIncreaseIndex = percentageIncreases.findIndex((p) => p === highestIncrease); 38 | 39 | // If we have big increase in value, we could be considering "outliers" in our estimate 40 | // Skip the low elements and take a new median 41 | const values = 42 | highestIncrease >= increaseBoundary && highestIncreaseIndex >= Math.floor(tips.length / 2) 43 | ? tips.slice(highestIncreaseIndex) 44 | : tips; 45 | 46 | return values[Math.floor(values.length / 2)]; 47 | } 48 | 49 | export const getEip1559FeeFetcher = 50 | ( 51 | baseFeeMultipliers: { [key in GasCategory]: number }, 52 | priorityFeePercentiles: { [key in GasCategory]: number }, 53 | priorityFeeIncreaseBoundary: number = 200, 54 | ) => 55 | async (gasCategory: GasCategory, connection: Web3, logger?: Logger): Promise => { 56 | const blocksNum = 10; 57 | const history = await connection.eth.getFeeHistory(blocksNum, 'latest', [ 58 | priorityFeePercentiles[gasCategory], 59 | ]); 60 | 61 | // take median 62 | const maxPriorityFeePerGas = priorityFeeEstimator( 63 | history.reward || [], 64 | priorityFeeIncreaseBoundary, 65 | ); 66 | logger?.debug( 67 | `desired priority fee: ${maxPriorityFeePerGas} (prev blocks: ${blocksNum}, gasCategory: ${GasCategory[gasCategory]}, percentile: ${priorityFeePercentiles[gasCategory]}, boundary: ${priorityFeeIncreaseBoundary})`, 68 | ); 69 | 70 | // however, the base fee must be taken according to pending and next-to-pending block, because that's were we compete 71 | // for block space 72 | const baseFee = findMaxBigInt(...history.baseFeePerGas.map((r) => BigInt(r)).slice(-2)); 73 | const targetBaseFee = 74 | (baseFee * safeIntToBigInt(baseFeeMultipliers[gasCategory] * 10_000)) / 10_000n; 75 | logger?.debug( 76 | `target base fee: ${targetBaseFee} (found base fee: ${baseFee}, gasCategory: ${GasCategory[gasCategory]}, multiplier: ${baseFeeMultipliers[gasCategory]})`, 77 | ); 78 | 79 | return { 80 | baseFee: targetBaseFee, 81 | maxFeePerGas: targetBaseFee + maxPriorityFeePerGas, 82 | maxPriorityFeePerGas, 83 | }; 84 | }; 85 | 86 | export const defaultEip1559FeeFetcher = getEip1559FeeFetcher( 87 | { 88 | [GasCategory.PROJECTED]: defaultFeeManagerOpts.eip1559BaseFeeProjectedMultiplier, 89 | [GasCategory.NORMAL]: defaultFeeManagerOpts.eip1559BaseFeeNormalMultiplier, 90 | [GasCategory.AGGRESSIVE]: defaultFeeManagerOpts.eip1559BaseFeeAggressiveMultiplier, 91 | }, 92 | { 93 | [GasCategory.PROJECTED]: defaultFeeManagerOpts.eip1559PriorityFeeProjectedPercentile, 94 | [GasCategory.NORMAL]: defaultFeeManagerOpts.eip1559PriorityFeeNormalPercentile, 95 | [GasCategory.AGGRESSIVE]: defaultFeeManagerOpts.eip1559BaseFeeAggressiveMultiplier, 96 | }, 97 | defaultFeeManagerOpts.eip1559PriorityFeeIncreaseBoundary, 98 | ); 99 | -------------------------------------------------------------------------------- /src/processors/TVLBudgetController.ts: -------------------------------------------------------------------------------- 1 | import { Address, buffersAreEqual, ChainId } from '@debridge-finance/dln-client'; 2 | import NodeCache from 'node-cache'; 3 | import { Logger } from 'pino'; 4 | import { ExecutorSupportedChain, IExecutor } from '../executor'; 5 | 6 | enum TvlCacheKey { 7 | TVL, 8 | } 9 | 10 | // 30m cache is OK because the cache is being flushed on every fulfilled order 11 | const DEFAULT_TVL_CACHE_TTL = 30 * 60; 12 | 13 | export class TVLBudgetController { 14 | public readonly budget: number; 15 | 16 | private readonly chain: ChainId; 17 | 18 | private readonly executor: IExecutor; 19 | 20 | private readonly cache = new NodeCache({ 21 | stdTTL: DEFAULT_TVL_CACHE_TTL, 22 | // this cache is intended to store Promises. Using clones causes node to crash when a race condition occurs 23 | useClones: false, 24 | }); 25 | 26 | private readonly logger: Logger; 27 | 28 | constructor(chain: ChainId, executor: IExecutor, budget: number, logger: Logger) { 29 | this.chain = chain; 30 | this.executor = executor; 31 | this.budget = budget; 32 | this.logger = logger.child({ service: TVLBudgetController.name, chainId: chain, budget }); 33 | if (budget) { 34 | this.logger.info(`Will preserve a TVL of $${budget} on ${ChainId[chain]}`); 35 | } 36 | } 37 | 38 | get giveChain(): ExecutorSupportedChain { 39 | return this.executor.chains[this.chain]!; 40 | } 41 | 42 | get hasSeparateUnlockBeneficiary(): boolean { 43 | return !buffersAreEqual( 44 | this.giveChain.fulfillAuthority.bytesAddress, 45 | this.giveChain.unlockBeneficiary, 46 | ); 47 | } 48 | 49 | get trackedTokens(): Address[] { 50 | return this.executor.buckets 51 | .map((bucket) => bucket.findTokens(this.chain) || []) 52 | .reduce((prev, curr) => [...prev, ...curr], []); 53 | } 54 | 55 | flushCache(): void { 56 | this.cache.del(TvlCacheKey.TVL); 57 | } 58 | 59 | async getCurrentTVL(): Promise { 60 | const cachedTVL = this.cache.get>(TvlCacheKey.TVL); 61 | if (undefined === cachedTVL) { 62 | // to avoid simultaneous requests from different take_chains for the same giveTVL, 63 | // we introduce a promisified synchronization root here 64 | // Mind that this promise gets erased once resolved 65 | this.cache.set(TvlCacheKey.TVL, this.calculateCurrentTVL()); 66 | } 67 | 68 | return this.cache.get>(TvlCacheKey.TVL)!; 69 | } 70 | 71 | async calculateCurrentTVL(): Promise { 72 | const takerAccountBalance = await this.getTakerAccountBalance(); 73 | const unlockBeneficiaryAccountBalance = await this.getUnlockBeneficiaryAccountBalance(); 74 | const pendingUnlockOrdersValue = await this.getPendingUnlockOrdersValue(); 75 | 76 | const tvl = takerAccountBalance + unlockBeneficiaryAccountBalance + pendingUnlockOrdersValue; 77 | return tvl; 78 | } 79 | 80 | private async getTakerAccountBalance(): Promise { 81 | return this.getAccountValue(this.giveChain.fulfillAuthority.bytesAddress); 82 | } 83 | 84 | private async getUnlockBeneficiaryAccountBalance(): Promise { 85 | if (!this.hasSeparateUnlockBeneficiary) return 0; 86 | return this.getAccountValue(this.giveChain.unlockBeneficiary); 87 | } 88 | 89 | private async getAccountValue(account: Address): Promise { 90 | const usdValues = await Promise.all( 91 | this.trackedTokens.map((token) => this.getAssetValueAtAccount(token, account)), 92 | ); 93 | 94 | return usdValues.reduce((prevValue, value) => prevValue + value, 0); 95 | } 96 | 97 | private async getAssetValueAtAccount(token: Address, account: Address): Promise { 98 | const balance = await this.executor.client 99 | .getClient(this.chain) 100 | .getBalance(this.chain, token, account); 101 | const usdValue = await this.executor.usdValueOfAsset(this.chain, token, balance); 102 | return usdValue; 103 | } 104 | 105 | private async getPendingUnlockOrdersValue(): Promise { 106 | const orders = await this.executor.dlnApi.getPendingForUnlockOrders(this.chain); 107 | 108 | const usdValues = await Promise.all( 109 | orders.map((order) => this.executor.usdValueOfOrder(order)), 110 | ); 111 | 112 | return usdValues.reduce((prevValue, value) => prevValue + value, 0); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/environments.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, Env as DlnClientEnv } from '@debridge-finance/dln-client'; 2 | 3 | import { ChainEnvironment } from './config'; 4 | 5 | type Env = { 6 | WSS: string; 7 | defaultEvmAddresses: ChainEnvironment; 8 | chains: { 9 | [key in ChainId]?: ChainEnvironment; 10 | }; 11 | environment?: DlnClientEnv; 12 | }; 13 | 14 | const PRERELEASE_ENVIRONMENT_CODENAME_HANOI: Env = { 15 | WSS: 'wss://dln-ws-test.debridge.finance/ws', 16 | 17 | defaultEvmAddresses: {}, 18 | environment: DlnClientEnv.Hanoi, 19 | 20 | chains: { 21 | [ChainId.Solana]: { 22 | deBridgeContract: 'Lima82j8YvHFYe8qa4kGgb3fvPFEnR3PoV6UyGUpHLq', 23 | pmmSrc: 'srch38nzfcgwJnfKE6xWD8vfNJtudazwisdcociAuQt', 24 | pmmDst: 'dsthjxvgzUiQYgoqSf7r5BbENqf2b34sSXkPvxuKS8d', 25 | solana: { 26 | debridgeSetting: 'settFZVDbqC9zBmV2ZCBfNMCtTzia2R7mVeR6ccK2nN', 27 | }, 28 | }, 29 | 30 | [ChainId.Polygon]: { 31 | deBridgeContract: '0xa9a617e8BE4efb0aC315691D2b4dbEC94f5Bb27b', 32 | pmmSrc: '0x3c7010F5a2eCC2B56BeAE085B6528e492c8b36B6', 33 | pmmDst: '0x08F20E7Ace48dAe4806A96b88386E62b2C161054', 34 | evm: { 35 | forwarderContract: '0x4f824487f7C0AB5A6B8B8411E472eaf7dDef2BBd', 36 | }, 37 | }, 38 | 39 | [ChainId.BSC]: { 40 | deBridgeContract: '0xa9a617e8BE4efb0aC315691D2b4dbEC94f5Bb27b', 41 | pmmSrc: '0x3c7010F5a2eCC2B56BeAE085B6528e492c8b36B6', 42 | pmmDst: '0x08F20E7Ace48dAe4806A96b88386E62b2C161054', 43 | evm: { 44 | forwarderContract: '0xce1705632Ced3A1d18Ed2b87ECe5B74526f59b8A', 45 | }, 46 | }, 47 | }, 48 | }; 49 | 50 | const PRERELEASE_ENVIRONMENT_CODENAME_MADRID: Env = { 51 | WSS: 'wss://dln-ws-madrid.debridge.finance/ws', 52 | 53 | defaultEvmAddresses: {}, 54 | environment: DlnClientEnv.Madrid, 55 | 56 | chains: { 57 | [ChainId.Solana]: { 58 | deBridgeContract: 'Lima82j8YvHFYe8qa4kGgb3fvPFEnR3PoV6UyGUpHLq', 59 | pmmSrc: 'MADsq64WRCFuw4bqrZjtkVh5YiGcw6Jfy5cGbh6Gh8e', 60 | pmmDst: 'MADDfEEeW23M5owdXSXeBKAsb5zmT9oaLxDC4oLPfq7', 61 | solana: { 62 | debridgeSetting: 'settFZVDbqC9zBmV2ZCBfNMCtTzia2R7mVeR6ccK2nN', 63 | }, 64 | }, 65 | 66 | [ChainId.Polygon]: { 67 | deBridgeContract: '0xa9a617e8BE4efb0aC315691D2b4dbEC94f5Bb27b', 68 | pmmSrc: '0x420cF3d24306b434E708cd5c7D6c82417C05c760', 69 | pmmDst: '0x67Ee8602DbeA6c858f10538101F3C5d4AC4e3eba', 70 | evm: { 71 | forwarderContract: '0x4f824487f7C0AB5A6B8B8411E472eaf7dDef2BBd', 72 | }, 73 | }, 74 | 75 | [ChainId.BSC]: { 76 | deBridgeContract: '0xa9a617e8BE4efb0aC315691D2b4dbEC94f5Bb27b', 77 | pmmSrc: '0x420cF3d24306b434E708cd5c7D6c82417C05c760', 78 | pmmDst: '0x67Ee8602DbeA6c858f10538101F3C5d4AC4e3eba', 79 | evm: { 80 | forwarderContract: '0xce1705632Ced3A1d18Ed2b87ECe5B74526f59b8A', 81 | }, 82 | }, 83 | }, 84 | }; 85 | 86 | const PRODUCTION: Env = { 87 | WSS: 'wss://dln-ws.debridge.finance/ws', 88 | 89 | defaultEvmAddresses: { 90 | deBridgeContract: '0x43dE2d77BF8027e25dBD179B491e8d64f38398aA', 91 | pmmSrc: '0xeF4fB24aD0916217251F553c0596F8Edc630EB66', 92 | pmmDst: '0xE7351Fd770A37282b91D153Ee690B63579D6dd7f', 93 | evm: { 94 | forwarderContract: '0x663DC15D3C1aC63ff12E45Ab68FeA3F0a883C251', 95 | }, 96 | }, 97 | chains: { 98 | [ChainId.Solana]: { 99 | deBridgeContract: 'DEbrdGj3HsRsAzx6uH4MKyREKxVAfBydijLUF3ygsFfh', 100 | pmmSrc: 'src5qyZHqTqecJV4aY6Cb6zDZLMDzrDKKezs22MPHr4', 101 | pmmDst: 'dst5MGcFPoBeREFAA5E3tU5ij8m5uVYwkzkSAbsLbNo', 102 | solana: { 103 | debridgeSetting: 'DeSetTwWhjZq6Pz9Kfdo1KoS5NqtsM6G8ERbX4SSCSft', 104 | }, 105 | }, 106 | [ChainId.Base]: { 107 | deBridgeContract: '0xc1656B63D9EEBa6d114f6bE19565177893e5bCBF', 108 | pmmSrc: '0xeF4fB24aD0916217251F553c0596F8Edc630EB66', 109 | pmmDst: '0xE7351Fd770A37282b91D153Ee690B63579D6dd7f', 110 | }, 111 | }, 112 | }; 113 | 114 | let CURRENT_ENVIRONMENT = PRODUCTION; 115 | 116 | function setCurrentEnvironment(newEnvironment: Env) { 117 | CURRENT_ENVIRONMENT = newEnvironment; 118 | } 119 | 120 | function getCurrentEnvironment(): Env { 121 | return CURRENT_ENVIRONMENT; 122 | } 123 | 124 | export { 125 | setCurrentEnvironment, 126 | getCurrentEnvironment, 127 | PRODUCTION, 128 | PRERELEASE_ENVIRONMENT_CODENAME_MADRID, 129 | PRERELEASE_ENVIRONMENT_CODENAME_HANOI, 130 | }; 131 | -------------------------------------------------------------------------------- /src/chain-evm/tx-generators/ierc20.json: -------------------------------------------------------------------------------- 1 | { 2 | "contractName": "IERC20", 3 | "abi": [ 4 | { 5 | "anonymous": false, 6 | "inputs": [ 7 | { 8 | "indexed": true, 9 | "internalType": "address", 10 | "name": "owner", 11 | "type": "address" 12 | }, 13 | { 14 | "indexed": true, 15 | "internalType": "address", 16 | "name": "spender", 17 | "type": "address" 18 | }, 19 | { 20 | "indexed": false, 21 | "internalType": "uint256", 22 | "name": "value", 23 | "type": "uint256" 24 | } 25 | ], 26 | "name": "Approval", 27 | "type": "event" 28 | }, 29 | { 30 | "anonymous": false, 31 | "inputs": [ 32 | { 33 | "indexed": true, 34 | "internalType": "address", 35 | "name": "from", 36 | "type": "address" 37 | }, 38 | { 39 | "indexed": true, 40 | "internalType": "address", 41 | "name": "to", 42 | "type": "address" 43 | }, 44 | { 45 | "indexed": false, 46 | "internalType": "uint256", 47 | "name": "value", 48 | "type": "uint256" 49 | } 50 | ], 51 | "name": "Transfer", 52 | "type": "event" 53 | }, 54 | { 55 | "inputs": [], 56 | "name": "totalSupply", 57 | "outputs": [ 58 | { 59 | "internalType": "uint256", 60 | "name": "", 61 | "type": "uint256" 62 | } 63 | ], 64 | "stateMutability": "view", 65 | "type": "function" 66 | }, 67 | { 68 | "inputs": [ 69 | { 70 | "internalType": "address", 71 | "name": "account", 72 | "type": "address" 73 | } 74 | ], 75 | "name": "balanceOf", 76 | "outputs": [ 77 | { 78 | "internalType": "uint256", 79 | "name": "", 80 | "type": "uint256" 81 | } 82 | ], 83 | "stateMutability": "view", 84 | "type": "function" 85 | }, 86 | { 87 | "inputs": [ 88 | { 89 | "internalType": "address", 90 | "name": "recipient", 91 | "type": "address" 92 | }, 93 | { 94 | "internalType": "uint256", 95 | "name": "amount", 96 | "type": "uint256" 97 | } 98 | ], 99 | "name": "transfer", 100 | "outputs": [ 101 | { 102 | "internalType": "bool", 103 | "name": "", 104 | "type": "bool" 105 | } 106 | ], 107 | "stateMutability": "nonpayable", 108 | "type": "function" 109 | }, 110 | { 111 | "inputs": [ 112 | { 113 | "internalType": "address", 114 | "name": "owner", 115 | "type": "address" 116 | }, 117 | { 118 | "internalType": "address", 119 | "name": "spender", 120 | "type": "address" 121 | } 122 | ], 123 | "name": "allowance", 124 | "outputs": [ 125 | { 126 | "internalType": "uint256", 127 | "name": "", 128 | "type": "uint256" 129 | } 130 | ], 131 | "stateMutability": "view", 132 | "type": "function" 133 | }, 134 | { 135 | "inputs": [ 136 | { 137 | "internalType": "address", 138 | "name": "spender", 139 | "type": "address" 140 | }, 141 | { 142 | "internalType": "uint256", 143 | "name": "amount", 144 | "type": "uint256" 145 | } 146 | ], 147 | "name": "approve", 148 | "outputs": [ 149 | { 150 | "internalType": "bool", 151 | "name": "", 152 | "type": "bool" 153 | } 154 | ], 155 | "stateMutability": "nonpayable", 156 | "type": "function" 157 | }, 158 | { 159 | "inputs": [ 160 | { 161 | "internalType": "address", 162 | "name": "sender", 163 | "type": "address" 164 | }, 165 | { 166 | "internalType": "address", 167 | "name": "recipient", 168 | "type": "address" 169 | }, 170 | { 171 | "internalType": "uint256", 172 | "name": "amount", 173 | "type": "uint256" 174 | } 175 | ], 176 | "name": "transferFrom", 177 | "outputs": [ 178 | { 179 | "internalType": "bool", 180 | "name": "", 181 | "type": "bool" 182 | } 183 | ], 184 | "stateMutability": "nonpayable", 185 | "type": "function" 186 | } 187 | ] 188 | } 189 | -------------------------------------------------------------------------------- /tests/helpers/index.ts: -------------------------------------------------------------------------------- 1 | import pino from 'pino'; 2 | import pretty from 'pino-pretty'; 3 | import hre from 'hardhat'; 4 | import Web3 from 'web3'; 5 | import '@nomiclabs/hardhat-web3'; 6 | 7 | const TEST_LOG_LEVEL = process.env.TEST_LOG_LEVEL || 'silent'; 8 | 9 | export function createTestFrameworkLogger() { 10 | const prettyStream = pretty({ 11 | colorize: process.stdout.isTTY, 12 | sync: true, 13 | singleLine: true, 14 | translateTime: 'yyyy-mm-dd HH:MM:ss.l', 15 | }); 16 | const streams: any[] = [ 17 | { 18 | level: TEST_LOG_LEVEL, 19 | stream: prettyStream, 20 | }, 21 | ]; 22 | return pino( 23 | { 24 | level: TEST_LOG_LEVEL, 25 | translateFormat: 'd mmm yyyy H:MM', 26 | }, 27 | pino.multistream(streams, {}), 28 | ); 29 | } 30 | 31 | // extract type 32 | export type Account = ReturnType; 33 | 34 | const HARDHAT_STANDARD_ACCOUNTS = [ 35 | [ 36 | '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', 37 | '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', 38 | ], 39 | [ 40 | '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', 41 | '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d', 42 | ], 43 | [ 44 | '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC', 45 | '0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a', 46 | ], 47 | [ 48 | '0x90F79bf6EB2c4f870365E785982E1f101E93b906', 49 | '0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6', 50 | ], 51 | [ 52 | '0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65', 53 | '0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a', 54 | ], 55 | [ 56 | '0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc', 57 | '0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba', 58 | ], 59 | [ 60 | '0x976EA74026E726554dB657fA54763abd0C3a0aa9', 61 | '0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e', 62 | ], 63 | [ 64 | '0x14dC79964da2C08b23698B3D3cc7Ca32193d9955', 65 | '0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356', 66 | ], 67 | [ 68 | '0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f', 69 | '0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97', 70 | ], 71 | [ 72 | '0xa0Ee7A142d267C1f36714E4a8F75612F20a79720', 73 | '0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6', 74 | ], 75 | [ 76 | '0xBcd4042DE499D14e55001CcbB24a551F3b954096', 77 | '0xf214f2b2cd398c806f84e317254e0f0b801d0643303237d97a22a48e01628897', 78 | ], 79 | [ 80 | '0x71bE63f3384f5fb98995898A86B02Fb2426c5788', 81 | '0x701b615bbdfb9de65240bc28bd21bbc0d996645a3dd57e7b12bc2bdf6f192c82', 82 | ], 83 | [ 84 | '0xFABB0ac9d68B0B445fB7357272Ff202C5651694a', 85 | '0xa267530f49f8280200edf313ee7af6b827f2a8bce2897751d06a843f644967b1', 86 | ], 87 | [ 88 | '0x1CBd3b2770909D4e10f157cABC84C7264073C9Ec', 89 | '0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd', 90 | ], 91 | [ 92 | '0xdF3e18d64BC6A983f673Ab319CCaE4f1a57C7097', 93 | '0xc526ee95bf44d8fc405a158bb884d9d1238d99f0612e9f33d006bb0789009aaa', 94 | ], 95 | [ 96 | '0xcd3B766CCDd6AE721141F452C550Ca635964ce71', 97 | '0x8166f546bab6da521a8369cab06c5d2b9e46670292d85c875ee9ec20e84ffb61', 98 | ], 99 | [ 100 | '0x2546BcD3c84621e976D8185a91A922aE77ECEc30', 101 | '0xea6c44ac03bff858b476bba40716402b03e41b8e97e276d1baec7c37d42484a0', 102 | ], 103 | [ 104 | '0xbDA5747bFD65F08deb54cb465eB87D40e51B197E', 105 | '0x689af8efa8c651a91ad287602527f3af2fe9f6501a7ac4b061667b5a93e037fd', 106 | ], 107 | [ 108 | '0xdD2FD4581271e230360230F9337D5c0430Bf44C0', 109 | '0xde9be858da4a475276426320d5e9262ecfc3ba460bfac56360bfa6c4c28b4ee0', 110 | ], 111 | [ 112 | '0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199', 113 | '0xdf57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e', 114 | ], 115 | ]; 116 | 117 | let WEB3_ACCOUNTS: Account[] | undefined; 118 | 119 | export async function getWeb3Accounts(): Promise { 120 | if (!WEB3_ACCOUNTS) 121 | WEB3_ACCOUNTS = HARDHAT_STANDARD_ACCOUNTS.map(([, privateKey]) => 122 | hre.web3.eth.accounts.privateKeyToAccount(privateKey), 123 | ); 124 | return WEB3_ACCOUNTS; 125 | } 126 | 127 | // Functions for account generation: 128 | // async function getSigners(count: number = 10): Promise{ 129 | // const accounts = await getAccounts(count); 130 | // await Promise.all(accounts.map(acc => hh.setBalance(acc.address, 100n * 10n ** 18n))) 131 | // return accounts 132 | // } 133 | 134 | // async function getAccounts(count: number = 10): Promise{ 135 | // const retval: Account[] = Array.from({length: count}); 136 | // const accounts = await hre.web3.eth.accounts.wallet.create(count); 137 | // for (let i = 0; i < count; i++) { 138 | // retval[i] = accounts[i]; 139 | // } 140 | // return retval 141 | // } 142 | -------------------------------------------------------------------------------- /src/processors/throughput.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '@debridge-finance/dln-client'; 2 | import { Logger } from 'pino'; 3 | import { OrderId } from '../interfaces'; 4 | import { assert } from '../errors'; 5 | 6 | type Threshold = { 7 | minBlockConfirmations: number; 8 | maxFulfillThroughputUSD: number; 9 | throughputTimeWindowSec: number; 10 | }; 11 | 12 | type Metric = Readonly & { 13 | currentlyLocked: number; 14 | orders: Set; 15 | }; 16 | 17 | type OrderRecord = { 18 | orderId: OrderId; 19 | usdValue: number; 20 | confirmationBlocksCount: number; 21 | timer: ReturnType; 22 | metric: Metric; 23 | }; 24 | 25 | // This controller simply keeps track of orders' worth that were attempted to be fulfilled 26 | // while being non-finalized, to prevent TVL exceed the desired budget on a given chain 27 | export class ThroughputController { 28 | readonly #metrics: Array; 29 | 30 | readonly #logger: Logger; 31 | 32 | readonly #orders = new Map(); 33 | 34 | constructor( 35 | public readonly chainId: ChainId, 36 | thresholds: Array, 37 | logger: Logger, 38 | ) { 39 | this.#logger = logger.child({ 40 | service: ThroughputController.name, 41 | chainId, 42 | }); 43 | 44 | this.#metrics = thresholds 45 | .sort( 46 | (thresholdB, thresholdA) => 47 | thresholdA.minBlockConfirmations - thresholdB.minBlockConfirmations, 48 | ) 49 | .map((threshold) => ({ 50 | ...threshold, 51 | currentlyLocked: 0, 52 | orders: new Set(), 53 | })); 54 | 55 | for (const threshold of this.#metrics) { 56 | if (ThroughputController.isActualMetric(threshold)) 57 | this.#logger.debug( 58 | `initializing maxFulfillThroughputUSD for the range #${threshold.minBlockConfirmations}: $${threshold.maxFulfillThroughputUSD}, limit: ${threshold.throughputTimeWindowSec}s`, 59 | ); 60 | } 61 | } 62 | 63 | isThrottled(orderId: string, confirmationBlocksCount: number, usdValue: number): boolean { 64 | const metric = this.getMetric(confirmationBlocksCount); 65 | if (!metric) { 66 | return false; 67 | } 68 | 69 | const logger = this.#logger.child({ orderId }); 70 | logger.debug( 71 | `checking if the order worth $${usdValue} and ${confirmationBlocksCount} block confirmations fits the throughput range #${metric.minBlockConfirmations}`, 72 | ); 73 | 74 | const potentialSpentBudgetInUSD = metric.currentlyLocked + usdValue; 75 | if (metric.maxFulfillThroughputUSD < potentialSpentBudgetInUSD) { 76 | const message = `order worth $${usdValue} does not fit non-finalized TVL budget: ${ThroughputController.getRangeAsString( 77 | metric, 78 | )}`; 79 | logger.debug(message); 80 | return true; 81 | } 82 | 83 | return false; 84 | } 85 | 86 | addOrder(orderId: string, confirmationBlocksCount: number, usdValue: number): void { 87 | const metric = this.getMetric(confirmationBlocksCount); 88 | if (!metric) return; 89 | 90 | // we must sync order existence, because it may jump from one range to another 91 | this.removeOrder(orderId); 92 | 93 | metric.orders.add(orderId); 94 | metric.currentlyLocked += usdValue; 95 | const timer = setTimeout(this.getTimerCallback(orderId), metric.throughputTimeWindowSec * 1000); 96 | 97 | const logger = this.#logger.child({ orderId }); 98 | this.#orders.set(orderId, { 99 | orderId, 100 | usdValue, 101 | confirmationBlocksCount, 102 | timer, 103 | metric, 104 | }); 105 | logger.debug( 106 | `order worth $${usdValue} added to the throughput range #${ 107 | metric.minBlockConfirmations 108 | } at the range #${confirmationBlocksCount}; ${ThroughputController.getRangeAsString(metric)}`, 109 | ); 110 | } 111 | 112 | private getTimerCallback(orderId: OrderId) { 113 | return () => { 114 | this.removeOrder(orderId); 115 | }; 116 | } 117 | 118 | removeOrder(orderId: string) { 119 | const order = this.#orders.get(orderId); 120 | if (!order) return; 121 | 122 | const logger = this.#logger.child({ orderId }); 123 | this.#orders.delete(orderId); 124 | 125 | const { metric } = order; 126 | assert(metric.orders.has(orderId), `order ${orderId} unexpectedly missing in the range`); 127 | metric.orders.delete(orderId); 128 | metric.currentlyLocked -= order.usdValue; 129 | 130 | logger.debug( 131 | `order worth $${order.usdValue} removed from the the throughput range #${ 132 | metric.minBlockConfirmations 133 | }; ${ThroughputController.getRangeAsString(metric)}`, 134 | ); 135 | } 136 | 137 | private getMetric(confirmationBlocksCount: number): Metric | undefined { 138 | // #metrics must be sorted by minBlockConfirmations DESC, see constructor 139 | const metric = this.#metrics.find( 140 | (iteratedMetric) => iteratedMetric.minBlockConfirmations <= confirmationBlocksCount, 141 | ); 142 | if (metric && !ThroughputController.isActualMetric(metric)) return undefined; 143 | 144 | return metric; 145 | } 146 | 147 | private static isActualMetric(metric: Metric) { 148 | return metric.maxFulfillThroughputUSD > 0 && metric.throughputTimeWindowSec > 0; 149 | } 150 | 151 | private static getRangeAsString(tr: Metric): string { 152 | const rate = (tr.currentlyLocked / tr.maxFulfillThroughputUSD) * 100; 153 | return `TVL at block_confirmation #${tr.minBlockConfirmations}: $${tr.currentlyLocked} (cap: $${ 154 | tr.maxFulfillThroughputUSD 155 | }, utilization: ${rate.toFixed(2)}%, orders count: ${tr.orders.size})`; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/chain-common/order.ts: -------------------------------------------------------------------------------- 1 | import { 2 | OrderData, 3 | OrderDataWithId, 4 | Order as OrderUtils, 5 | buffersAreEqual, 6 | getEngineByChainId, 7 | ChainEngine, 8 | ChainId, 9 | } from '@debridge-finance/dln-client'; 10 | import { 11 | calculateSubsidization, 12 | findExpectedBucket, 13 | } from '@debridge-finance/legacy-dln-profitability'; 14 | import { helpers } from '@debridge-finance/solana-utils'; 15 | import { Logger } from 'pino'; 16 | import { EVMOrderValidator } from '../chain-evm/order-validator'; 17 | import { createClientLogger } from '../dln-ts-client.utils'; 18 | import { 19 | IExecutor, 20 | ExecutorSupportedChain, 21 | SrcConstraintsPerOrderValue, 22 | SrcOrderConstraints, 23 | DstOrderConstraints, 24 | DstConstraintsPerOrderValue, 25 | } from '../executor'; 26 | import { OrderId } from '../interfaces'; 27 | import { CreatedOrderTaker, TakerShortCircuit } from './order-taker'; 28 | import { OrderValidator } from './order-validator'; 29 | 30 | type CreatedOrderContext = { 31 | executor: IExecutor; 32 | giveChain: ExecutorSupportedChain; 33 | takeChain: ExecutorSupportedChain; 34 | logger: Logger; 35 | }; 36 | 37 | export class CreatedOrder { 38 | public readonly executor: IExecutor; 39 | 40 | public readonly giveChain: ExecutorSupportedChain; 41 | 42 | public readonly takeChain: ExecutorSupportedChain; 43 | 44 | readonly #logger: Logger; 45 | 46 | async getUsdValue(): Promise { 47 | const value = await this.executor.usdValueOfOrder(this.orderData); 48 | // round up to 2 decimals 49 | return Math.round(value * 100) / 100; 50 | } 51 | 52 | get giveTokenAsString(): string { 53 | return this.orderData.give.tokenAddress.toAddress(this.orderData.give.chainId); 54 | } 55 | 56 | get takeTokenAsString(): string { 57 | return this.orderData.give.tokenAddress.toAddress(this.orderData.give.chainId); 58 | } 59 | 60 | get route() { 61 | const route = findExpectedBucket(this.orderData, this.executor.buckets); 62 | return { 63 | ...route, 64 | requiresSwap: 65 | buffersAreEqual(route.reserveDstToken, this.orderData.take.tokenAddress) === false, 66 | }; 67 | } 68 | 69 | constructor( 70 | public readonly orderId: OrderId, 71 | public readonly orderData: OrderData, 72 | public readonly finalization: 'Finalized' | number, 73 | public readonly arrivedAt: Date, 74 | public readonly attempt: number, 75 | context: CreatedOrderContext, 76 | ) { 77 | this.executor = context.executor; 78 | this.giveChain = context.giveChain; 79 | this.takeChain = context.takeChain; 80 | this.#logger = context.logger.child({ orderId }); 81 | } 82 | 83 | async getGiveAmountInReserveToken(): Promise { 84 | return this.executor.resyncDecimals( 85 | this.orderData.give.chainId, 86 | this.orderData.give.tokenAddress, 87 | this.orderData.give.amount, 88 | this.orderData.take.chainId, 89 | this.route.reserveDstToken, 90 | ); 91 | } 92 | 93 | async getMaxProfitableReserveAmountWithoutOperatingExpenses(): Promise { 94 | // getting the rough amount we are willing to spend after reserving our intended margin 95 | let reserveDstAmount = await this.getGiveAmountInReserveToken(); 96 | 97 | // subtracting margin 98 | reserveDstAmount = (reserveDstAmount * (10_000n - BigInt(this.requiredMargin))) / 10_000n; 99 | 100 | // adding subsidy 101 | if (this.executor.allowSubsidy && this.executor.subsidizationRules) { 102 | const subsidyAmount = await calculateSubsidization( 103 | { 104 | order: this.orderData, 105 | reserveDstTokenAddress: this.route.reserveDstToken, 106 | }, 107 | { 108 | client: this.executor.client, 109 | priceTokenService: this.executor.tokenPriceService, 110 | subsidizationRules: this.executor.subsidizationRules, 111 | logger: createClientLogger(this.#logger), 112 | }, 113 | ); 114 | 115 | reserveDstAmount += subsidyAmount; 116 | } 117 | 118 | return reserveDstAmount; 119 | } 120 | 121 | get blockConfirmations(): number { 122 | if (this.finalization === 'Finalized') return this.giveChain.network.finalizedBlockCount; 123 | return this.finalization; 124 | } 125 | 126 | /** 127 | * Returns margin requirement for this particular order. It may take give- and take-chain into consideration 128 | */ 129 | get requiredMargin(): number { 130 | let margin = this.legacyRequiredMargin; 131 | 132 | if (process.env.DISABLE_OP_HORIZON_CAMPAIGN !== 'true') { 133 | if (this.takeChain.chain === ChainId.Optimism) { 134 | margin -= 4; 135 | } else if (this.giveChain.chain === ChainId.Optimism) { 136 | margin -= 2; 137 | } 138 | 139 | if (margin < 0) margin = 0; 140 | } 141 | 142 | return margin; 143 | } 144 | 145 | /** 146 | * Similar to `requiredMargin`, but used by the legacy-dln-profitability package. The difference between 147 | * requiredMargin and legacyRequiredMargin is that the latter does not account for OP HORIZON Campaign rebates 148 | * bc legacy-dln-profitability does this internally 149 | */ 150 | get legacyRequiredMargin(): number { 151 | return this.giveChain.srcConstraints.profitability; 152 | } 153 | 154 | public srcConstraints(): SrcOrderConstraints { 155 | return this.srcConstraintsRange() || this.giveChain.srcConstraints; 156 | } 157 | 158 | public srcConstraintsRange(): SrcConstraintsPerOrderValue | undefined { 159 | return this.giveChain.srcConstraints.perOrderValue 160 | .sort((rangeB, rangeA) => rangeA.minBlockConfirmations - rangeB.minBlockConfirmations) 161 | .find((range) => range.minBlockConfirmations <= this.blockConfirmations); 162 | } 163 | 164 | public dstConstraints(): DstOrderConstraints { 165 | return this.dstConstraintsRange() || this.takeChain.dstConstraints; 166 | } 167 | 168 | private dstConstraintsRange(): DstConstraintsPerOrderValue | undefined { 169 | return this.takeChain.dstConstraints.perOrderValue 170 | .sort((rangeB, rangeA) => rangeA.minBlockConfirmations - rangeB.minBlockConfirmations) 171 | .find((range) => range.minBlockConfirmations <= this.blockConfirmations); 172 | } 173 | 174 | getValidator(sc: TakerShortCircuit): OrderValidator { 175 | switch (getEngineByChainId(this.takeChain.chain)) { 176 | case ChainEngine.EVM: 177 | return new EVMOrderValidator(this, sc, { logger: this.#logger }); 178 | default: 179 | return new OrderValidator(this, sc, { logger: this.#logger }); 180 | } 181 | } 182 | 183 | getWithId(): OrderDataWithId { 184 | return OrderUtils.getVerified({ 185 | orderId: helpers.hexToBuffer(this.orderId), 186 | ...this.orderData, 187 | }); 188 | } 189 | 190 | getTaker() { 191 | return new CreatedOrderTaker(this, this.#logger); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/chain-common/order-estimator.ts: -------------------------------------------------------------------------------- 1 | import { buffersAreEqual } from '@debridge-finance/dln-client'; 2 | import { calculateExpectedTakeAmount } from '@debridge-finance/legacy-dln-profitability'; 3 | import { Logger } from 'pino'; 4 | import { assert } from '../errors'; 5 | import { createClientLogger } from '../dln-ts-client.utils'; 6 | import { CreatedOrder } from './order'; 7 | import './mixins'; 8 | import { OrderEvaluationContextual, OrderEvaluationPayload } from './order-evaluation-context'; 9 | 10 | type OrderEstimatorContext = { 11 | logger: Logger; 12 | validationPayload: OrderEvaluationPayload; 13 | }; 14 | 15 | function getPreFulfillSlippage(evaluatedTakeAmount: bigint, takeAmount: bigint): number { 16 | const preFulfillSwapMinAllowedSlippageBps = 5; 17 | const preFulfillSwapMaxAllowedSlippageBps = 400; 18 | const calculatedSlippageBps = ((evaluatedTakeAmount - takeAmount) * 10000n) / evaluatedTakeAmount; 19 | if (calculatedSlippageBps < preFulfillSwapMinAllowedSlippageBps) 20 | return preFulfillSwapMinAllowedSlippageBps; 21 | if (calculatedSlippageBps > preFulfillSwapMaxAllowedSlippageBps) 22 | return preFulfillSwapMaxAllowedSlippageBps; 23 | return Number(calculatedSlippageBps); 24 | } 25 | 26 | export async function explainEstimation(orderEstimation: OrderEstimation): Promise { 27 | const takeAmountDrop = 28 | (orderEstimation.projectedFulfillAmount * 10_000n) / 29 | orderEstimation.order.orderData.take.amount; 30 | const takeAmountDropShare = Number(10_000n - takeAmountDrop) / 100; 31 | 32 | const reserveTokenDesc = orderEstimation.order.route.reserveDstToken.toAddress( 33 | orderEstimation.order.takeChain.chain, 34 | ); 35 | const takeTokenDesc = orderEstimation.order.orderData.take.tokenAddress.toAddress( 36 | orderEstimation.order.takeChain.chain, 37 | ); 38 | 39 | return [ 40 | `order is estimated to be profitable when supplying `, 41 | `${await orderEstimation.order.executor.formatTokenValue( 42 | orderEstimation.order.orderData.take.chainId, 43 | orderEstimation.order.route.reserveDstToken, 44 | orderEstimation.requiredReserveAmount, 45 | )} `, 46 | `of reserve token (${reserveTokenDesc}) during fulfillment, `, 47 | `which gives only ${await orderEstimation.order.executor.formatTokenValue( 48 | orderEstimation.order.orderData.take.chainId, 49 | orderEstimation.order.route.reserveDstToken, 50 | orderEstimation.projectedFulfillAmount, 51 | )} `, 52 | `of take token (${takeTokenDesc}), `, 53 | `while order requires ${await orderEstimation.order.executor.formatTokenValue( 54 | orderEstimation.order.orderData.take.chainId, 55 | orderEstimation.order.route.reserveDstToken, 56 | orderEstimation.order.orderData.take.amount, 57 | )} of take amount `, 58 | `(${takeAmountDropShare}% drop)`, 59 | ].join(''); 60 | } 61 | 62 | type RawOrderEstimation = { 63 | isProfitable: boolean; 64 | 65 | reserveToken: Uint8Array; 66 | 67 | requiredReserveAmount: bigint; 68 | 69 | projectedFulfillAmount: bigint; 70 | }; 71 | 72 | export type OrderEstimation = { 73 | readonly order: CreatedOrder; 74 | readonly isProfitable: boolean; 75 | readonly requiredReserveAmount: bigint; 76 | readonly projectedFulfillAmount: bigint; 77 | readonly payload: OrderEvaluationPayload; 78 | }; 79 | 80 | export class OrderEstimator extends OrderEvaluationContextual { 81 | protected readonly logger: Logger; 82 | 83 | constructor( 84 | public readonly order: CreatedOrder, 85 | protected readonly context: OrderEstimatorContext, 86 | ) { 87 | super(context.validationPayload); 88 | this.logger = context.logger.child({ service: OrderEstimator.name }); 89 | } 90 | 91 | private getRouteHint() { 92 | if (this.order.route.requiresSwap) { 93 | const routeHint = this.payload.validationPreFulfillSwap; 94 | assert( 95 | routeHint !== undefined, 96 | 'missing validationPreFulfillSwap from the validator for route hinting when building final swap txn', 97 | ); 98 | return routeHint; 99 | } 100 | 101 | return undefined; 102 | } 103 | 104 | protected async getExpectedTakeAmountContext(): Promise< 105 | Parameters['2'] 106 | > { 107 | return { 108 | client: this.order.executor.client, 109 | priceTokenService: this.order.executor.tokenPriceService, 110 | buckets: this.order.executor.buckets, 111 | swapConnector: this.order.executor.swapConnector, 112 | logger: createClientLogger(this.logger), 113 | batchSize: this.order.giveChain.srcConstraints.batchUnlockSize, 114 | swapEstimationPreference: this.getRouteHint(), 115 | isFeatureEnableOpHorizon: process.env.DISABLE_OP_HORIZON_CAMPAIGN !== 'true', 116 | allowSubsidy: this.order.executor.allowSubsidy, 117 | subsidizationRules: this.order.executor.subsidizationRules, 118 | }; 119 | } 120 | 121 | protected async getRawOrderEstimation(): Promise { 122 | const rawEstimation = await calculateExpectedTakeAmount( 123 | this.order.orderData, 124 | this.order.legacyRequiredMargin, 125 | await this.getExpectedTakeAmountContext(), 126 | ); 127 | 128 | return { 129 | isProfitable: rawEstimation.isProfitable, 130 | reserveToken: rawEstimation.reserveDstToken, 131 | requiredReserveAmount: BigInt(rawEstimation.requiredReserveDstAmount), 132 | projectedFulfillAmount: BigInt(rawEstimation.profitableTakeAmount), 133 | }; 134 | } 135 | 136 | async getEstimation(): Promise { 137 | const rawOrderEstimation = await this.getRawOrderEstimation(); 138 | 139 | // ensure dln-taker's algo aligns with calculateExpectedTakeAmount behaviour 140 | assert( 141 | buffersAreEqual(rawOrderEstimation.reserveToken, this.order.route.reserveDstToken), 142 | `dln-taker has picked ${this.order.route.reserveDstToken.toAddress( 143 | this.order.takeChain.chain, 144 | )} as reserve token, while calculateExpectedTakeAmount returned ${rawOrderEstimation.reserveToken.toAddress( 145 | this.order.takeChain.chain, 146 | )}`, 147 | ); 148 | 149 | // provide a swap that would be executed upon fulfillment: this is crucial because this swap may be outdated 150 | // making estimation not profitable 151 | let preFulfillSwap; 152 | if (this.order.route.requiresSwap) { 153 | preFulfillSwap = await this.order.executor.swapConnector.getSwap( 154 | { 155 | amountIn: rawOrderEstimation.requiredReserveAmount, 156 | chainId: this.order.orderData.take.chainId, 157 | fromTokenAddress: rawOrderEstimation.reserveToken, 158 | toTokenAddress: this.order.orderData.take.tokenAddress, 159 | slippageBps: getPreFulfillSlippage( 160 | rawOrderEstimation.projectedFulfillAmount, 161 | this.order.orderData.take.amount, 162 | ), 163 | routeHint: this.getRouteHint(), 164 | fromAddress: this.order.takeChain.fulfillAuthority.bytesAddress, 165 | destReceiver: this.order.executor.client.getForwarderAddress( 166 | this.order.orderData.take.chainId, 167 | ), 168 | }, 169 | { 170 | logger: createClientLogger(this.logger), 171 | }, 172 | ); 173 | 174 | rawOrderEstimation.projectedFulfillAmount = preFulfillSwap.amountOut; 175 | if (preFulfillSwap.amountOut < this.order.orderData.take.amount) { 176 | rawOrderEstimation.isProfitable = false; 177 | } 178 | 179 | this.setPayloadEntry('preFulfillSwap', preFulfillSwap); 180 | } 181 | 182 | return { 183 | order: this.order, 184 | isProfitable: rawOrderEstimation.isProfitable, 185 | requiredReserveAmount: rawOrderEstimation.requiredReserveAmount, 186 | projectedFulfillAmount: rawOrderEstimation.projectedFulfillAmount, 187 | payload: this.payload, 188 | }; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/processors/BatchUnlocker.ts: -------------------------------------------------------------------------------- 1 | import { 2 | buffersAreEqual, 3 | ChainId, 4 | OrderData, 5 | OrderDataWithId, 6 | OrderState, 7 | tokenAddressToString, 8 | } from '@debridge-finance/dln-client'; 9 | import { Logger } from 'pino'; 10 | 11 | import { helpers } from '@debridge-finance/solana-utils'; 12 | import { TransactionSender } from '../chain-common/tx-builder'; 13 | import { ExecutorSupportedChain, IExecutor } from '../executor'; 14 | import { Authority } from '../interfaces'; 15 | 16 | export interface BatchUnlockTransactionBuilder { 17 | unlockAuthority: Authority; 18 | getBatchOrderUnlockTxSender(orders: Array, logger: Logger): TransactionSender; 19 | } 20 | 21 | export class BatchUnlocker { 22 | private ordersDataMap = new Map(); // orderId => orderData 23 | 24 | private unlockBatchesOrderIdMap = new Map>(); // chainId => orderId[] 25 | 26 | private isBatchUnlockLocked: boolean = false; 27 | 28 | readonly #logger: Logger; 29 | 30 | constructor( 31 | logger: Logger, 32 | private readonly executor: IExecutor, 33 | private readonly takeChain: ExecutorSupportedChain, 34 | private readonly transactionBuilder: BatchUnlockTransactionBuilder, 35 | ) { 36 | this.#logger = logger.child({ 37 | service: BatchUnlocker.name, 38 | takeChainId: this.takeChain.chain, 39 | }); 40 | } 41 | 42 | remove(takeChain: ChainId, orderId: string) { 43 | if (this.unlockBatchesOrderIdMap.has(takeChain)) { 44 | this.unlockBatchesOrderIdMap.get(takeChain)!.delete(orderId); 45 | this.ordersDataMap.delete(orderId); 46 | } 47 | } 48 | 49 | async unlockOrder(orderId: string, order: OrderData): Promise { 50 | // validate current order state: 51 | const orderState = await this.executor.client.getTakeOrderState( 52 | { 53 | orderId, 54 | takeChain: this.takeChain.chain, 55 | }, 56 | {}, 57 | ); 58 | // order must be in the FULFILLED state 59 | if (orderState?.status !== OrderState.Fulfilled) { 60 | this.#logger.debug( 61 | `${orderId}: current state is ${orderState?.status}, however OrderState.Fulfilled is expected; not adding to the batch unlock pool`, 62 | ); 63 | return; 64 | } 65 | 66 | const unlockAuthority = this.takeChain.unlockAuthority.bytesAddress; 67 | // a FULFILLED order must have ours takerAddress to ensure successful unlock 68 | if (!buffersAreEqual(orderState.takerAddress, unlockAuthority)) { 69 | this.#logger.debug( 70 | `${orderId}: orderState.takerAddress (${tokenAddressToString( 71 | this.takeChain.chain, 72 | orderState.takerAddress, 73 | )}) does not match expected unlockAuthority (${tokenAddressToString( 74 | this.takeChain.chain, 75 | unlockAuthority, 76 | )}), not adding to the batch unlock pool`, 77 | ); 78 | return; 79 | } 80 | 81 | // filling batch queue 82 | this.addOrder(orderId, order); 83 | } 84 | 85 | private async addOrder(orderId: string, order: OrderData) { 86 | if (!this.unlockBatchesOrderIdMap.has(order.give.chainId)) { 87 | this.unlockBatchesOrderIdMap.set(order.give.chainId, new Set()); 88 | } 89 | this.unlockBatchesOrderIdMap.get(order.give.chainId)!.add(orderId); 90 | this.ordersDataMap.set(orderId, order); 91 | 92 | this.#logger.debug(`added ${orderId} to the batch unlock queue`); 93 | this.#logger.debug( 94 | `batch unlock queue size for the giveChain=${ChainId[order.give.chainId]} ${ 95 | this.unlockBatchesOrderIdMap.get(order.give.chainId)!.size 96 | } order(s)`, 97 | ); 98 | 99 | this.tryUnlock(order.give.chainId); 100 | } 101 | 102 | async tryUnlock(giveChainId: ChainId): Promise { 103 | // check that process is blocked 104 | if (this.isBatchUnlockLocked) { 105 | this.#logger.debug('batch unlock processing is locked, not performing unlock procedures'); 106 | return; 107 | } 108 | 109 | const currentSize = this.unlockBatchesOrderIdMap.get(giveChainId)!.size; 110 | if (currentSize < this.getBatchUnlockSize(giveChainId)) { 111 | this.#logger.debug('batch is not fulled yet, not performing unlock procedures'); 112 | return; 113 | } 114 | 115 | this.isBatchUnlockLocked = true; 116 | this.#logger.debug(`trying to send batch unlock to ${ChainId[giveChainId]}`); 117 | const batchSucceeded = await this.performBatchUnlock(giveChainId); 118 | if (batchSucceeded) { 119 | this.#logger.debug( 120 | `succeeded sending batch to ${ChainId[giveChainId]}, checking other directions`, 121 | ); 122 | await this.unlockAny(); 123 | } else { 124 | this.#logger.error('batch unlock failed, stopping unlock procedures'); 125 | } 126 | this.isBatchUnlockLocked = false; 127 | } 128 | 129 | private async unlockAny(): Promise { 130 | let giveChainId = this.peekNextBatch(); 131 | while (giveChainId) { 132 | this.#logger.debug(`trying to send batch unlock to ${ChainId[giveChainId]}`); 133 | // eslint-disable-next-line no-await-in-loop -- Intentional because we want to handle all available batches 134 | const batchSucceeded = await this.performBatchUnlock(giveChainId); 135 | if (batchSucceeded) { 136 | giveChainId = this.peekNextBatch(); 137 | } else { 138 | this.#logger.error('batch unlock failed, stopping'); 139 | break; 140 | } 141 | } 142 | } 143 | 144 | private peekNextBatch(): ChainId | undefined { 145 | for (const [chainId, orderIds] of this.unlockBatchesOrderIdMap.entries()) { 146 | if (orderIds.size >= this.getBatchUnlockSize(chainId)) { 147 | return chainId; 148 | } 149 | } 150 | 151 | return undefined; 152 | } 153 | 154 | private getBatchUnlockSize(giveChainId: ChainId): number { 155 | return this.executor.getSupportedChain(giveChainId).srcConstraints.batchUnlockSize; 156 | } 157 | 158 | /** 159 | * returns true if batch unlock succeeded (e.g. all orders were successfully unlocked) 160 | */ 161 | private async performBatchUnlock(giveChainId: ChainId): Promise { 162 | const orderIds = Array.from(this.unlockBatchesOrderIdMap.get(giveChainId)!).slice( 163 | 0, 164 | this.getBatchUnlockSize(giveChainId), 165 | ); 166 | 167 | const unlockedOrders = await this.unlockOrders(giveChainId, orderIds); 168 | 169 | // clean executed orders form queue 170 | unlockedOrders.forEach((id) => { 171 | this.unlockBatchesOrderIdMap.get(giveChainId)!.delete(id); 172 | this.ordersDataMap.delete(id); 173 | }); 174 | 175 | return unlockedOrders.length === this.getBatchUnlockSize(giveChainId); 176 | } 177 | 178 | private async unlockOrders(giveChainId: ChainId, orderIds: string[]): Promise { 179 | const unlockedOrders: string[] = []; 180 | const logger = this.#logger.child({ 181 | giveChainId, 182 | orderIds, 183 | }); 184 | 185 | logger.info(`picked ${orderIds.length} orders to unlock`); 186 | logger.debug(orderIds.join(',')); 187 | 188 | // get current state of the orders, to catch those that are already fulfilled 189 | const notUnlockedOrders: boolean[] = await Promise.all( 190 | orderIds.map(async (orderId) => { 191 | const orderState = await this.executor.client.getTakeOrderState( 192 | { 193 | orderId, 194 | takeChain: this.takeChain.chain, 195 | }, 196 | {}, 197 | ); 198 | 199 | return orderState?.status === OrderState.Fulfilled; 200 | }), 201 | ); 202 | // filter off orders that are already unlocked 203 | // eslint-disable-next-line no-param-reassign -- Must be rewritten ASAP, TODO: #862kaqf9u 204 | orderIds = orderIds.filter((_, idx) => { 205 | if (notUnlockedOrders[idx]) return true; 206 | unlockedOrders.push(orderIds[idx]); 207 | return false; 208 | }); 209 | logger.debug(`pre-filtering: ${unlockedOrders.length} already unlocked`); 210 | 211 | const giveChain = this.executor.chains[giveChainId]; 212 | if (!giveChain) throw new Error(`Give chain not set: ${ChainId[giveChainId]}`); 213 | 214 | try { 215 | const sendBatchUnlockTransactionHash = await this.sendBatchUnlock(orderIds, logger); 216 | unlockedOrders.push(...orderIds); 217 | 218 | logger.info( 219 | `send_unlock tx (hash: ${sendBatchUnlockTransactionHash}) with ${ 220 | orderIds.length 221 | } orders: ${orderIds.join(', ')}`, 222 | ); 223 | 224 | this.executor.hookEngine.handleOrderUnlockSent({ 225 | fromChainId: this.takeChain.chain, 226 | toChainId: giveChain.chain, 227 | txHash: sendBatchUnlockTransactionHash, 228 | orderIds, 229 | }); 230 | } catch (e) { 231 | const error = e as Error; 232 | this.executor.hookEngine.handleOrderUnlockFailed({ 233 | fromChainId: this.takeChain.chain, 234 | toChainId: giveChain.chain, 235 | message: `trying to unlock ${orderIds.length} orders from ${ 236 | ChainId[this.takeChain.chain] 237 | } to ${ChainId[giveChain.chain]} failed: ${error.message}`, 238 | orderIds, 239 | }); 240 | logger.error(`failed to unlock ${orderIds.length} order(s): ${e}`); 241 | logger.error(`failed batch contained: ${orderIds.join(',')}`); 242 | logger.error(e); 243 | } 244 | 245 | return unlockedOrders; 246 | } 247 | 248 | private async sendBatchUnlock(orderIds: string[], logger: Logger): Promise { 249 | return this.transactionBuilder.getBatchOrderUnlockTxSender( 250 | orderIds.map((orderId) => ({ 251 | ...this.ordersDataMap.get(orderId)!, 252 | orderId: helpers.hexToBuffer(orderId), 253 | })), 254 | logger, 255 | )(); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /sample.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-default-export, @typescript-eslint/no-unused-vars -- Allowed to simplify configuration file */ 2 | 3 | import { 4 | ChainId, 5 | configurator, 6 | ExecutorLaunchConfig, 7 | filters, 8 | WsNextOrder, 9 | CURRENT_ENVIRONMENT as environment, 10 | } from '@debridge-finance/dln-taker'; 11 | 12 | const config: ExecutorLaunchConfig = { 13 | orderFeed: new WsNextOrder(environment.WSS, { 14 | headers: { 15 | Authorization: `Bearer ${process.env.WS_API_KEY}`, 16 | }, 17 | } as any), 18 | 19 | jupiterConfig: { 20 | maxAccounts: 16, 21 | }, 22 | 23 | oneInchConfig: { 24 | // MANDATORY 25 | apiToken: `${process.env.ONEINCH_API_V5_TOKEN}`, // obtain one at https://portal.1inch.dev 26 | disablePMMProtocols: true, 27 | disabledProtocols: [], 28 | }, 29 | 30 | buckets: [ 31 | // 32 | // Setting the USDC bucket (all tokens are emitted by Circle Inc on every DLN supported chain) 33 | // 34 | { 35 | [ChainId.Arbitrum]: '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', 36 | [ChainId.Avalanche]: '0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E', 37 | [ChainId.BSC]: '0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d', 38 | [ChainId.Base]: ['0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca'], 39 | [ChainId.Ethereum]: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', 40 | [ChainId.Linea]: '0x176211869cA2b568f2A7D4EE941E073a821EE1ff', 41 | [ChainId.Optimism]: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', 42 | [ChainId.Polygon]: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', 43 | [ChainId.Solana]: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', 44 | }, 45 | // 46 | // Setting the ETH/wETH bucket 47 | // 48 | { 49 | [ChainId.Arbitrum]: '0x0000000000000000000000000000000000000000', 50 | [ChainId.Avalanche]: '0x49D5c2BdFfac6CE2BFdB6640F4F80f226bc10bAB', 51 | [ChainId.Base]: '0x0000000000000000000000000000000000000000', 52 | [ChainId.BSC]: '0x2170Ed0880ac9A755fd29B2688956BD959F933F8', 53 | [ChainId.Ethereum]: '0x0000000000000000000000000000000000000000', 54 | [ChainId.Linea]: '0x0000000000000000000000000000000000000000', 55 | [ChainId.Optimism]: '0x0000000000000000000000000000000000000000', 56 | [ChainId.Polygon]: '0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619', 57 | }, 58 | ], 59 | 60 | /* 61 | Example: 62 | subsidizationRules: [{ 63 | giveChainIds: [ChainId.BSC], 64 | takeChainIds: "any", 65 | notTakeChainIds: [ChainId.Solana], 66 | ranges: [ 67 | { 68 | range: { 69 | minUsdOrderValue: 1, 70 | maxUsdOrderValue: 100, 71 | }, 72 | usdSubsidy: 1, 73 | }, 74 | { 75 | range: { 76 | minUsdOrderValue: 101, 77 | maxUsdOrderValue: 1000, 78 | }, 79 | usdSubsidy: 2, 80 | } 81 | ] 82 | }], 83 | */ 84 | subsidizationRules: [], 85 | 86 | allowSubsidy: false, 87 | 88 | tokenPriceService: configurator.tokenPriceService({ 89 | coingeckoApiKey: process?.env?.COINGECKO_API_KEY, 90 | // mapping: {} 91 | }), 92 | 93 | srcConstraints: { 94 | // desired profitability. Setting a higher value would prevent dln-taker from fulfilling most orders because 95 | // the deBridge app and the API suggest users placing orders with as much margin as 4bps 96 | minProfitabilityBps: 4, 97 | 98 | // Number of orders (per every chain where orders are coming from and to) to accumulate to unlock them in batches 99 | // Min: 1; max: 10, default: 10. 100 | // This means that dln-taker would accumulate orders (that were fulfilled successfully) rather then unlock 101 | // them on the go, and would send a batch of unlock commands every time enough orders were fulfilled, dramatically 102 | // reducing the cost of the unlock command execution. 103 | // You can set a lesser value to unlock orders more frequently, however please note that this value directly 104 | // affects order profitability because the deBridge app and the API reserves the cost of unlock in the order's margin, 105 | // assuming that the order would be unlocked in a batch of size=10. Reducing the batch size to a lower value increases 106 | // your unlock costs and thus reduces order profitability, making them unprofitable most of the time. 107 | batchUnlockSize: 10, 108 | }, 109 | 110 | chains: [ 111 | { 112 | chain: ChainId.Solana, 113 | chainRpc: `${process.env.SOLANA_RPC}`, 114 | 115 | // Defines constraints imposed on all orders coming from this chain 116 | constraints: {}, 117 | 118 | // if the order is created on Solana and fulfilled on another chain (e.g. Ethereum), 119 | // unlocked funds will be sent to this Solana address 120 | beneficiary: `${process.env.SOLANA_BENEFICIARY}`, 121 | 122 | // if the order is created on another chain (e.g. Ethereum), dln-taker would attempt to fulfill 123 | // this order on behalf of this address 124 | // Warn! base58 representation of a private key. 125 | // Warn! For security reasons, put it to the .env file 126 | takerPrivateKey: `${process.env.SOLANA_TAKER_PRIVATE_KEY}`, 127 | 128 | // Warn! base58 representation of a private key. 129 | // Warn! For security reasons, put it to the .env file 130 | unlockAuthorityPrivateKey: `${process.env.SOLANA_UNLOCK_AUTHORITY_PRIVATE_KEY}`, 131 | }, 132 | 133 | { 134 | chain: ChainId.Arbitrum, 135 | chainRpc: `${process.env.ARBITRUM_RPC}`, 136 | 137 | // Defines constraints imposed on all orders coming from this chain 138 | constraints: { 139 | // Defines necessary and sufficient block confirmation thresholds per worth of order expressed in dollars. 140 | requiredConfirmationsThresholds: [ 141 | // worth <$100: 1+ block confirmation 142 | // {thresholdAmountInUSD: 100, minBlockConfirmations: 1}, 143 | // worth >$100: guaranteed block confirmations (15) 144 | ], 145 | }, 146 | 147 | // if the order is created on Ethereum and fulfilled on another chain (e.g. Solana), 148 | // unlocked funds will be sent to this Ethereum address 149 | beneficiary: `${process.env.ARBITRUM_BENEFICIARY}`, 150 | 151 | // if the order is created on another chain (e.g. Solana), dln-taker would attempt to fulfill 152 | // this order on behalf of this address 153 | // Warn! base64 representation of a private key. 154 | // Warn! For security reasons, put it to the .env file 155 | takerPrivateKey: `${process.env.ARBITRUM_TAKER_PRIVATE_KEY}`, 156 | 157 | // if the order is created on another chain (e.g. Solana), dln-taker would unlock it 158 | // after successful fulfillment on behalf of this address 159 | // Warn! base64 representation of a private key. 160 | // Warn! For security reasons, put it to the .env file 161 | unlockAuthorityPrivateKey: `${process.env.ARBITRUM_UNLOCK_AUTHORITY_PRIVATE_KEY}`, 162 | }, 163 | 164 | { 165 | chain: ChainId.Avalanche, 166 | chainRpc: `${process.env.AVALANCHE_RPC}`, 167 | 168 | constraints: { 169 | requiredConfirmationsThresholds: [ 170 | // worth <$100: 1+ block confirmation 171 | // {thresholdAmountInUSD: 100, minBlockConfirmations: 1}, 172 | // worth >$100: guaranteed block confirmations (15) 173 | ], 174 | }, 175 | 176 | beneficiary: `${process.env.AVALANCHE_BENEFICIARY}`, 177 | takerPrivateKey: `${process.env.AVALANCHE_TAKER_PRIVATE_KEY}`, 178 | unlockAuthorityPrivateKey: `${process.env.AVALANCHE_UNLOCK_AUTHORITY_PRIVATE_KEY}`, 179 | }, 180 | 181 | { 182 | chain: ChainId.BSC, 183 | chainRpc: `${process.env.BNB_RPC}`, 184 | 185 | constraints: { 186 | requiredConfirmationsThresholds: [ 187 | // worth <$100: 1+ block confirmation 188 | // {thresholdAmountInUSD: 100, minBlockConfirmations: 1}, 189 | // worth >$100: guaranteed block confirmations (15) 190 | ], 191 | }, 192 | 193 | beneficiary: `${process.env.BNB_BENEFICIARY}`, 194 | takerPrivateKey: `${process.env.BNB_TAKER_PRIVATE_KEY}`, 195 | unlockAuthorityPrivateKey: `${process.env.BNB_UNLOCK_AUTHORITY_PRIVATE_KEY}`, 196 | }, 197 | 198 | { 199 | chain: ChainId.Ethereum, 200 | chainRpc: `${process.env.ETHEREUM_RPC}`, 201 | 202 | constraints: { 203 | requiredConfirmationsThresholds: [ 204 | // worth <$100: 1+ block confirmation 205 | // {thresholdAmountInUSD: 100, minBlockConfirmations: 1}, 206 | // worth >$100: guaranteed block confirmations (15) 207 | ], 208 | }, 209 | 210 | beneficiary: `${process.env.ETHEREUM_BENEFICIARY}`, 211 | takerPrivateKey: `${process.env.ETHEREUM_TAKER_PRIVATE_KEY}`, 212 | unlockAuthorityPrivateKey: `${process.env.ETHEREUM_UNLOCK_AUTHORITY_PRIVATE_KEY}`, 213 | }, 214 | 215 | { 216 | chain: ChainId.Polygon, 217 | chainRpc: `${process.env.POLYGON_RPC}`, 218 | 219 | constraints: { 220 | requiredConfirmationsThresholds: [ 221 | // worth <$100: 32+ block confirmation 222 | // {thresholdAmountInUSD: 100, minBlockConfirmations: 1}, 223 | // worth >$100: guaranteed block confirmations (256) 224 | ], 225 | }, 226 | 227 | beneficiary: `${process.env.POLYGON_BENEFICIARY}`, 228 | takerPrivateKey: `${process.env.POLYGON_TAKER_PRIVATE_KEY}`, 229 | unlockAuthorityPrivateKey: `${process.env.POLYGON_UNLOCK_AUTHORITY_PRIVATE_KEY}`, 230 | }, 231 | 232 | { 233 | chain: ChainId.Linea, 234 | chainRpc: `${process.env.LINEA_RPC}`, 235 | 236 | constraints: {}, 237 | 238 | beneficiary: `${process.env.LINEA_BENEFICIARY}`, 239 | takerPrivateKey: `${process.env.LINEA_TAKER_PRIVATE_KEY}`, 240 | unlockAuthorityPrivateKey: `${process.env.LINEA_UNLOCK_AUTHORITY_PRIVATE_KEY}`, 241 | }, 242 | 243 | { 244 | chain: ChainId.Base, 245 | chainRpc: `${process.env.BASE_RPC}`, 246 | 247 | constraints: {}, 248 | 249 | beneficiary: `${process.env.BASE_BENEFICIARY}`, 250 | takerPrivateKey: `${process.env.BASE_TAKER_PRIVATE_KEY}`, 251 | unlockAuthorityPrivateKey: `${process.env.BASE_UNLOCK_AUTHORITY_PRIVATE_KEY}`, 252 | }, 253 | 254 | { 255 | chain: ChainId.Optimism, 256 | chainRpc: `${process.env.OPTIMISM_RPC}`, 257 | 258 | constraints: {}, 259 | 260 | beneficiary: `${process.env.OPTIMISM_BENEFICIARY}`, 261 | takerPrivateKey: `${process.env.OPTIMISM_TAKER_PRIVATE_KEY}`, 262 | unlockAuthorityPrivateKey: `${process.env.OPTIMISM_UNLOCK_AUTHORITY_PRIVATE_KEY}`, 263 | }, 264 | ], 265 | }; 266 | 267 | export default config; 268 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://www.npmjs.com/package/@tsconfig/node20 3 | "$schema": "https://json.schemastore.org/tsconfig", 4 | "display": "Node 20", 5 | "_version": "20.5.0", 6 | "include": ["src", "tests", "./sample.config.ts", "./hardhat.config.ts"], 7 | 8 | "compilerOptions": { 9 | 10 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 11 | 12 | /* Projects */ 13 | // "incremental": true, /* Enable incremental compilation */ 14 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 15 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 16 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 17 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 18 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 19 | 20 | /* Language and Environment */ 21 | "target": "ES2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 22 | "lib": ["ES2022", "DOM"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 23 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 24 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 25 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 26 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 27 | 28 | /* Modules */ 29 | "module": "Node16", /* Specify what module code is generated. */ 30 | // "rootDir": "./", /* Specify the root folder within your source files. */ 31 | "moduleResolution": "NodeNext", /* Specify how TypeScript looks up a file from a given module specifier. */ 32 | 33 | // remove it so that tsc will break on every absolute import 34 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 35 | "paths": { 36 | "@debridge-finance/dln-taker": ["./src"], // this is needed to create a verifiable sample.config.ts referencing src as a package 37 | }, /* Specify a set of entries that re-map imports to additional lookup locations. */ 38 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 39 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 40 | // "types": ["node"], /* Specify type package names to be included without being referenced in a source file. */ 41 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 42 | "resolveJsonModule": true, /* Enable importing .json files */ 43 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 44 | 45 | /* JavaScript Support */ 46 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 47 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 48 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 49 | 50 | /* Emit */ 51 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 52 | "declarationMap": true, /* Create sourcemaps for d.ts files. */ 53 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 54 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 55 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 56 | // "outDir": "./dist", /* Specify an output folder for all emitted files. */ 57 | // "removeComments": false, /* Disable emitting comments. */ 58 | "noEmit": true, /* Disable emitting files from a compilation. */ 59 | "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 60 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 61 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 62 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 63 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 64 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 65 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 66 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 67 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 68 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 69 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 70 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 71 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 72 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 73 | 74 | /* Interop Constraints */ 75 | "isolatedModules": false, /* Ensure that each file can be safely transpiled without relying on other imports. */ 76 | "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 77 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 78 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 79 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 80 | 81 | /* Completeness */ 82 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 83 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */ 84 | 85 | /* Type Checking */ 86 | "strict": true, /* Enable all strict type-checking options. */ 87 | "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 88 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 89 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 90 | "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 91 | "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 92 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 93 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 94 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 95 | "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 96 | "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 97 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 98 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 99 | "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 100 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 101 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 102 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 103 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 104 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 105 | 106 | }, 107 | 108 | "ts-node": { 109 | /* the following options are needed to simplify local development */ 110 | "compilerOptions": { 111 | "noUnusedLocals": false, 112 | "noUnusedParameters": false 113 | } 114 | }, 115 | } -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, PriceTokenService } from '@debridge-finance/dln-client'; 2 | import { SubsidizationRule } from '@debridge-finance/legacy-dln-profitability'; 3 | import { OrderFilterInitializer } from './filters/order.filter'; 4 | import { GetNextOrder } from './interfaces'; 5 | import { Hooks } from './hooks/HookEnums'; 6 | import { HookHandler } from './hooks/HookHandler'; 7 | import { EvmFeeManagerOpts } from './chain-evm/fees/manager'; 8 | import { EvmChainParameters } from './chain-evm/preferences/store'; 9 | import { EvmTxBroadcasterOpts } from './chain-evm/networking/broadcaster'; 10 | import { assert } from './errors'; 11 | 12 | type StringifiedAddress = string; 13 | 14 | export enum SupportedChain { 15 | Arbitrum = ChainId.Arbitrum, 16 | Avalanche = ChainId.Avalanche, 17 | BSC = ChainId.BSC, 18 | Ethereum = ChainId.Ethereum, 19 | Fantom = ChainId.Fantom, 20 | Linea = ChainId.Linea, 21 | Polygon = ChainId.Polygon, 22 | Solana = ChainId.Solana, 23 | Base = ChainId.Base, 24 | Optimism = ChainId.Optimism, 25 | } 26 | 27 | const BLOCK_CONFIRMATIONS_HARD_CAPS: { [key in SupportedChain]: number } = { 28 | [SupportedChain.Arbitrum]: 12, 29 | [SupportedChain.Avalanche]: 12, 30 | [SupportedChain.BSC]: 12, 31 | [SupportedChain.Ethereum]: 12, 32 | [SupportedChain.Fantom]: 12, 33 | [SupportedChain.Linea]: 12, 34 | [SupportedChain.Base]: 12, 35 | [SupportedChain.Optimism]: 12, 36 | [SupportedChain.Polygon]: 256, 37 | [SupportedChain.Solana]: 32, 38 | }; 39 | 40 | export function getFinalizedBlockConfirmations(chainId: ChainId): number { 41 | const value = BLOCK_CONFIRMATIONS_HARD_CAPS[chainId as any as SupportedChain]; 42 | assert(value !== undefined, `unknown chain: ${ChainId[chainId]}`); 43 | return value; 44 | } 45 | 46 | const avgBlockSpeed: { [key in SupportedChain]: number } = { 47 | [ChainId.Arbitrum]: 0.4, 48 | [ChainId.Avalanche]: 2, 49 | [ChainId.BSC]: 3, 50 | [ChainId.Ethereum]: 12, 51 | [ChainId.Polygon]: 2.3, 52 | [ChainId.Fantom]: 2, 53 | [ChainId.Linea]: 12, 54 | [ChainId.Solana]: 0.4, 55 | [ChainId.Base]: 2, 56 | [ChainId.Optimism]: 2, 57 | }; 58 | 59 | export function getAvgBlockSpeed(chainId: ChainId): number { 60 | const value = avgBlockSpeed[chainId as any as SupportedChain]; 61 | assert(value !== undefined, `unknown chain: ${ChainId[chainId]}`); 62 | return value; 63 | } 64 | 65 | export enum DexlessChains { 66 | Linea = ChainId.Linea, 67 | Neon = ChainId.Neon, 68 | } 69 | 70 | type PrivateKeyAuthority = { 71 | type: 'PK'; 72 | privateKey: string; 73 | }; 74 | export type SignerAuthority = PrivateKeyAuthority; 75 | 76 | export type ChainEnvironment = { 77 | /** 78 | * Address of the DLN contract responsible for order creation, unlocking and cancellation 79 | */ 80 | pmmSrc?: StringifiedAddress; 81 | 82 | /** 83 | * Address of the DLN contract responsible for order fulfillment 84 | */ 85 | pmmDst?: StringifiedAddress; 86 | 87 | /** 88 | * Address of the deBridgeGate contract responsible for cross-chain messaging (used by pmmDst) 89 | */ 90 | deBridgeContract?: StringifiedAddress; 91 | 92 | evm?: { 93 | forwarderContract?: StringifiedAddress; 94 | preferences?: { 95 | feeManagerOpts?: Partial; 96 | parameters?: Partial; 97 | broadcasterOpts?: Partial; 98 | }; 99 | }; 100 | 101 | solana?: { 102 | debridgeSetting?: string; 103 | }; 104 | }; 105 | 106 | export type DstOrderConstraints = { 107 | /** 108 | * Defines a delay (in seconds) the dln-taker should wait before starting to process each new (non-archival) order 109 | * coming to this chain after it first saw it. 110 | */ 111 | fulfillmentDelay?: number; 112 | 113 | /** 114 | * Defines a target where pre-fulfill swap change should be send to. Default: "taker". 115 | * Warning: applies to EVM chains only 116 | */ 117 | preFulfillSwapChangeRecipient?: 'taker' | 'maker'; 118 | }; 119 | 120 | export type SrcConstraints = { 121 | /** 122 | * Defines a budget (priced in the US dollar) of assets deployed and locked on the given chain. Any new order coming 123 | * from the given chain to any other supported chain that potentially increases the TVL beyond the given budget 124 | * (if being successfully fulfilled) gets rejected. 125 | * 126 | * The TVL is calculated as a sum of: 127 | * - the total value of intermediary assets deployed on the taker account (represented as takerPrivateKey) 128 | * - PLUS the total value of intermediary assets deployed on the unlock_beneficiary account (represented 129 | * as unlockAuthorityPrivateKey, if differs from takerPrivateKey) 130 | * - PLUS the total value of intermediary assets locked by the DLN smart contract that yet to be transferred to 131 | * the unlock_beneficiary account as soon as the commands to unlock fulfilled (but not yet unlocked) orders 132 | * are sent from other chains 133 | * - PLUS the total value of intermediary assets locked by the DLN smart contract that yet to be transferred to 134 | * the unlock_beneficiary account as soon as all active unlock commands (that were sent from other chains 135 | * but were not yet claimed/executed on the given chain) are executed. 136 | */ 137 | TVLBudget?: number; 138 | 139 | /** 140 | * Sets the min profitability expected for orders coming from this chain 141 | */ 142 | minProfitabilityBps?: number; 143 | 144 | /** 145 | * affects order profitability because the deBridge app and the API reserves the cost of unlock in the order's margin, 146 | * assuming that the order would be unlocked in a batch of size=10. Reducing the batch size to a lower value increases 147 | * your unlock costs and thus reduces order profitability, making them unprofitable most of the time. 148 | */ 149 | batchUnlockSize?: number; 150 | }; 151 | 152 | export type SrcOrderConstraints = { 153 | /** 154 | * Defines a delay (in seconds) the dln-taker should wait before starting to process each new (non-archival) order 155 | * coming from this chain after it first saw it. 156 | */ 157 | fulfillmentDelay?: number; 158 | 159 | /** 160 | * 161 | * Throughput is total value of orders from this range fulfilled across all the chains during the last N sec. 162 | * 163 | * The throughput should be set with throughputTimeWindowSec 164 | */ 165 | maxFulfillThroughputUSD?: number; 166 | 167 | /** 168 | * Throughput is total value of orders from this range fulfilled across all the chains during the last N sec 169 | */ 170 | throughputTimeWindowSec?: number; 171 | }; 172 | 173 | /** 174 | * Represents a chain configuration where orders can be fulfilled. 175 | */ 176 | export interface ChainDefinition { 177 | // 178 | // network related 179 | // 180 | 181 | /** 182 | * Supported chain discriminator 183 | */ 184 | chain: ChainId; 185 | 186 | /** 187 | * URL to the chain's RPC node 188 | */ 189 | chainRpc: string; 190 | 191 | /** 192 | * Forcibly disable fulfills in this chain? 193 | */ 194 | disabled?: boolean; 195 | 196 | /** 197 | * chain context related 198 | */ 199 | environment?: ChainEnvironment; 200 | 201 | /** 202 | * Defines constraints imposed on all orders coming from this chain 203 | */ 204 | constraints?: SrcConstraints & 205 | SrcOrderConstraints & { 206 | /** 207 | * Defines necessary and sufficient block confirmation thresholds per worth of order expressed in dollars. 208 | * For example, you may want to fulfill orders coming from Ethereum: 209 | * - worth <$100 - immediately (after 1 block confirmation) 210 | * - worth <$1,000 — after 6 block confirmations 211 | * - everything else (worth $1,000+) - after default 12 block confirmations, 212 | * then you can configure it: 213 | * 214 | * ``` 215 | * requiredConfirmationsThresholds: [ 216 | * {thresholdAmountInUSD: 100, minBlockConfirmations: 1}, // worth <$100: 1+ block confirmation 217 | * {thresholdAmountInUSD: 1_000, minBlockConfirmations: 6}, // worth <$1,000: 6+ block confirmations 218 | * ] 219 | * ``` 220 | */ 221 | requiredConfirmationsThresholds?: Array< 222 | SrcOrderConstraints & { 223 | thresholdAmountInUSD: number; 224 | minBlockConfirmations?: number; 225 | } 226 | >; 227 | }; 228 | 229 | /** 230 | * Defines constraints imposed on all orders coming to this chain. These properties have precedence over `constraints` property 231 | */ 232 | dstConstraints?: DstOrderConstraints & { 233 | /** 234 | * Defines custom constraints for orders falling into the given upper thresholds expressed in US dollars. 235 | * 236 | * Mind that these constraints have precedence over higher order constraints 237 | */ 238 | perOrderValueUpperThreshold?: Array< 239 | DstOrderConstraints & { 240 | minBlockConfirmations: number; 241 | } 242 | >; 243 | }; 244 | 245 | // 246 | // taker related 247 | // 248 | 249 | /** 250 | * Taker controlled address where the orders (fulfilled on other chains) would unlock the funds to. 251 | */ 252 | beneficiary: StringifiedAddress; 253 | 254 | /** 255 | * Authority responsible for initializing txns (applicable for Solana only) 256 | */ 257 | initAuthority?: SignerAuthority; 258 | 259 | /** 260 | * Authority responsible for creating fulfill txns 261 | */ 262 | fulfillAuthority?: SignerAuthority; 263 | 264 | /** 265 | * Authority responsible for creating unlock txns 266 | */ 267 | unlockAuthority?: SignerAuthority; 268 | 269 | /** 270 | * The private key for the wallet with funds to fulfill orders. Must have enough reserves and native currency 271 | * to fulfill orders 272 | * @deprecated Use fulfillAuthority 273 | */ 274 | takerPrivateKey?: string; 275 | 276 | /** 277 | * The private key for the wallet who is responsible for sending order unlocks (must differ from takerPrivateKey). 278 | * Must have enough ether to unlock orders 279 | * @deprecated Use unlockAuthority 280 | */ 281 | unlockAuthorityPrivateKey?: string; 282 | 283 | /** 284 | * Represents a list of filters which filter out orders for fulfillment 285 | */ 286 | srcFilters?: OrderFilterInitializer[]; 287 | 288 | /** 289 | * Represents a list of filters which filter out orders for fulfillment 290 | */ 291 | dstFilters?: OrderFilterInitializer[]; 292 | } 293 | 294 | export interface ExecutorLaunchConfig { 295 | /** 296 | * Represents a list of filters which filter out orders for fulfillment 297 | */ 298 | filters?: OrderFilterInitializer[]; 299 | 300 | /** 301 | * Hook handlers 302 | */ 303 | hookHandlers?: { 304 | [key in Hooks]?: HookHandler[]; 305 | }; 306 | 307 | /** 308 | * Token price provider 309 | * Default: CoingeckoPriceFeed 310 | */ 311 | tokenPriceService?: PriceTokenService; 312 | 313 | /** 314 | * Rule to subsidize orders 315 | * Default: [] 316 | */ 317 | subsidizationRules?: SubsidizationRule[]; 318 | 319 | /** 320 | * Default: false 321 | */ 322 | allowSubsidy?: boolean; 323 | 324 | /** 325 | * Source of orders 326 | */ 327 | orderFeed?: string | GetNextOrder; 328 | 329 | srcConstraints?: SrcConstraints; 330 | 331 | chains: ChainDefinition[]; 332 | 333 | /** 334 | * Defines buckets of tokens that have equal value and near-zero re-balancing costs across supported chains 335 | */ 336 | buckets: Array<{ 337 | [key in ChainId]?: string | Array; 338 | }>; 339 | 340 | jupiterConfig?: { 341 | apiToken?: string; 342 | maxAccounts?: number; 343 | blacklistedDexes?: Array; 344 | }; 345 | 346 | oneInchConfig: { 347 | apiToken: string; 348 | apiServer?: string; 349 | disablePMMProtocols?: boolean; 350 | disabledProtocols?: string[]; 351 | }; 352 | } 353 | --------------------------------------------------------------------------------