diff --git a/src/abis/auto-pos-creator.abi.json b/src/abis/auto-pos-creator.abi.json new file mode 100644 index 000000000..916ad1919 --- /dev/null +++ b/src/abis/auto-pos-creator.abi.json @@ -0,0 +1,339 @@ +{ + "buildInfo": { + "rustc": { + "version": "1.73.0-nightly", + "commitHash": "4c8bb79d9f565115637cc6da739f8389e79f3a29", + "commitDate": "2023-07-15", + "channel": "Nightly", + "short": "rustc 1.73.0-nightly (4c8bb79d9 2023-07-15)" + }, + "contractCrate": { + "name": "auto-pos-creator", + "version": "0.0.0" + }, + "framework": { + "name": "multiversx-sc", + "version": "0.44.0" + } + }, + "name": "AutoPosCreator", + "constructor": { + "inputs": [ + { + "name": "egld_wrapper_address", + "type": "Address" + }, + { + "name": "router_address", + "type": "Address" + } + ], + "outputs": [] + }, + "endpoints": [ + { + "name": "upgrade", + "mutability": "mutable", + "inputs": [], + "outputs": [] + }, + { + "name": "createLpPosFromSingleToken", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "pair_address", + "type": "Address" + }, + { + "name": "add_liq_first_token_min_amount_out", + "type": "BigUint" + }, + { + "name": "add_liq_second_token_min_amount_out", + "type": "BigUint" + }, + { + "name": "swap_operations", + "type": "variadic>", + "multi_arg": true + } + ], + "outputs": [ + { + "type": "List" + } + ] + }, + { + "name": "createLpPosFromTwoTokens", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "pair_address", + "type": "Address" + }, + { + "name": "add_liq_first_token_min_amount_out", + "type": "BigUint" + }, + { + "name": "add_liq_second_token_min_amount_out", + "type": "BigUint" + } + ], + "outputs": [ + { + "type": "List" + } + ] + }, + { + "name": "createFarmPosFromSingleToken", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "farm_address", + "type": "Address" + }, + { + "name": "add_liq_first_token_min_amount_out", + "type": "BigUint" + }, + { + "name": "add_liq_second_token_min_amount_out", + "type": "BigUint" + }, + { + "name": "swap_operations", + "type": "variadic>", + "multi_arg": true + } + ], + "outputs": [ + { + "type": "List" + } + ] + }, + { + "name": "createFarmPosFromTwoTokens", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "farm_address", + "type": "Address" + }, + { + "name": "add_liq_first_token_min_amount_out", + "type": "BigUint" + }, + { + "name": "add_liq_second_token_min_amount_out", + "type": "BigUint" + } + ], + "outputs": [ + { + "type": "List" + } + ] + }, + { + "name": "createMetastakingPosFromSingleToken", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "metastaking_address", + "type": "Address" + }, + { + "name": "add_liq_first_token_min_amount_out", + "type": "BigUint" + }, + { + "name": "add_liq_second_token_min_amount_out", + "type": "BigUint" + }, + { + "name": "swap_operations", + "type": "variadic>", + "multi_arg": true + } + ], + "outputs": [ + { + "type": "List" + } + ] + }, + { + "name": "createMetastakingPosFromTwoTokens", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "metastaking_address", + "type": "Address" + }, + { + "name": "add_liq_first_token_min_amount_out", + "type": "BigUint" + }, + { + "name": "add_liq_second_token_min_amount_out", + "type": "BigUint" + } + ], + "outputs": [ + { + "type": "List" + } + ] + }, + { + "name": "createFarmStakingPosFromSingleToken", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "farm_staking_address", + "type": "Address" + }, + { + "name": "min_amount_out", + "type": "BigUint" + }, + { + "name": "swap_operations", + "type": "variadic>", + "multi_arg": true + } + ], + "outputs": [ + { + "type": "List" + } + ] + }, + { + "name": "exitMetastakingPos", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "metastaking_address", + "type": "Address" + }, + { + "name": "first_token_min_amount_out", + "type": "BigUint" + }, + { + "name": "second_token_min_amont_out", + "type": "BigUint" + } + ], + "outputs": [ + { + "type": "List" + } + ] + }, + { + "name": "exitFarmPos", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "farm_address", + "type": "Address" + }, + { + "name": "first_token_min_amount_out", + "type": "BigUint" + }, + { + "name": "second_token_min_amont_out", + "type": "BigUint" + } + ], + "outputs": [ + { + "type": "List" + } + ] + }, + { + "name": "exitLpPos", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "pair_address", + "type": "Address" + }, + { + "name": "first_token_min_amount_out", + "type": "BigUint" + }, + { + "name": "second_token_min_amont_out", + "type": "BigUint" + } + ], + "outputs": [ + { + "type": "List" + } + ] + } + ], + "events": [], + "esdtAttributes": [], + "hasCallback": false, + "types": { + "EsdtTokenPayment": { + "type": "struct", + "fields": [ + { + "name": "token_identifier", + "type": "TokenIdentifier" + }, + { + "name": "token_nonce", + "type": "u64" + }, + { + "name": "amount", + "type": "BigUint" + } + ] + } + } +} diff --git a/src/config/default.json b/src/config/default.json index 91dbd8220..fef9e1dbf 100644 --- a/src/config/default.json +++ b/src/config/default.json @@ -42,7 +42,8 @@ "energyUpdate": "erd1qqqqqqqqqqqqqpgqqns0u3hw0e3j0km9h77emuear4xq7k7fd8ss0cwgja", "tokenUnstake": "erd1qqqqqqqqqqqqqpgqnysvq99c2t4a9pvvv22elnl6p73el8vw0n4spyfv7p", "lockedTokenWrapper": "erd1qqqqqqqqqqqqqpgq9ej9vcnr38l69rgkc735kgv0qlu2ptrsd8ssu9rwtu", - "escrow": "erd1qqqqqqqqqqqqqpgqz0wkk0j6y4h0mcxfxsg023j4x5sfgrmz0n4s4swp7a" + "escrow": "erd1qqqqqqqqqqqqqpgqz0wkk0j6y4h0mcxfxsg023j4x5sfgrmz0n4s4swp7a", + "positionCreator": "erd1qqqqqqqqqqqqqpgqh3zcutxk3wmfvevpyymaehvc3k0knyq70n4sg6qcj6" }, "tokenProviderUSD": "WEGLD-71e90a", "tokensSupply": ["MEX-27f4cd"], @@ -492,6 +493,9 @@ "governance": { "vote": 50000000 }, + "positionCreator": { + "singleToken": 50000000 + }, "lockedAssetCreate": 5000000, "wrapeGLD": 4200000, "claimLockedAssets": 45500000 @@ -532,7 +536,8 @@ "governance": { "energy": "./src/abis/governance-v2.abi.json", "tokenSnapshot": "./src/abis/governance-v2-merkle-proof.abi.json" - } + }, + "positionCreator": "./src/abis/auto-pos-creator.abi.json" }, "cron": { "transactionCollectorMaxHyperblocks": 10, diff --git a/src/config/devnet2.json b/src/config/devnet2.json index c9fc12f6c..2b6c925c0 100644 --- a/src/config/devnet2.json +++ b/src/config/devnet2.json @@ -28,7 +28,8 @@ "energyUpdate": "erd1qqqqqqqqqqqqqpgqz2ctz77j9we0r99mnhehv24he3pxmsmq0n4sntf4n7", "tokenUnstake": "erd1qqqqqqqqqqqqqpgqu6s4e5mndgf37psxw9mdlmjavp2er3f00n4snwyk3q", "lockedTokenWrapper": "erd1qqqqqqqqqqqqqpgqasr2asq07e6ur274eecx3vd0pnej2vxs0n4sqqyp52", - "escrow": "erd1qqqqqqqqqqqqqpgqnx4t9kwy5n9atuumvnrluv5yrwmpxzx60n4sj497ms" + "escrow": "erd1qqqqqqqqqqqqqpgqnx4t9kwy5n9atuumvnrluv5yrwmpxzx60n4sj497ms", + "positionCreator": "erd1qqqqqqqqqqqqqpgqh3zcutxk3wmfvevpyymaehvc3k0knyq70n4sg6qcj6" }, "governance": { "oldEnergy": { diff --git a/src/modules/auto-router/auto-router.module.ts b/src/modules/auto-router/auto-router.module.ts index 7075dc840..15b8ad6d9 100644 --- a/src/modules/auto-router/auto-router.module.ts +++ b/src/modules/auto-router/auto-router.module.ts @@ -33,6 +33,6 @@ import { TokenModule } from '../tokens/token.module'; AutoRouterTransactionService, PairTransactionService, ], - exports: [], + exports: [AutoRouterService, AutoRouterTransactionService], }) export class AutoRouterModule {} diff --git a/src/modules/auto-router/services/auto-router.transactions.service.ts b/src/modules/auto-router/services/auto-router.transactions.service.ts index d43caab38..43b0f8791 100644 --- a/src/modules/auto-router/services/auto-router.transactions.service.ts +++ b/src/modules/auto-router/services/auto-router.transactions.service.ts @@ -4,6 +4,7 @@ import { BigUIntValue, BytesValue, TokenTransfer, + TypedValue, } from '@multiversx/sdk-core'; import { Injectable } from '@nestjs/common'; import BigNumber from 'bignumber.js'; @@ -55,8 +56,8 @@ export class AutoRouterTransactionService { const transactionArgs = args.swapType == SWAP_TYPE.fixedInput - ? await this.multiPairFixedInputSwaps(args) - : await this.multiPairFixedOutputSwaps(args); + ? this.multiPairFixedInputSwaps(args) + : this.multiPairFixedOutputSwaps(args); transactions.push( contract.methodsExplicit @@ -78,10 +79,8 @@ export class AutoRouterTransactionService { return transactions; } - private async multiPairFixedInputSwaps( - args: MultiSwapTokensArgs, - ): Promise { - const swaps = []; + multiPairFixedInputSwaps(args: MultiSwapTokensArgs): TypedValue[] { + const swaps: TypedValue[] = []; const intermediaryTolerance = args.tolerance / args.addressRoute.length; @@ -113,10 +112,8 @@ export class AutoRouterTransactionService { return swaps; } - private async multiPairFixedOutputSwaps( - args: MultiSwapTokensArgs, - ): Promise { - const swaps = []; + multiPairFixedOutputSwaps(args: MultiSwapTokensArgs): TypedValue[] { + const swaps: TypedValue[] = []; const intermediaryTolerance = args.tolerance / args.addressRoute.length; diff --git a/src/modules/position-creator/models/position.creator.model.ts b/src/modules/position-creator/models/position.creator.model.ts new file mode 100644 index 000000000..bb01757e1 --- /dev/null +++ b/src/modules/position-creator/models/position.creator.model.ts @@ -0,0 +1,11 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +export class PositionCreatorModel { + @Field() + address: string; + + constructor(init: Partial) { + Object.assign(this, init); + } +} diff --git a/src/modules/position-creator/position.creator.module.ts b/src/modules/position-creator/position.creator.module.ts new file mode 100644 index 000000000..bfd8391b2 --- /dev/null +++ b/src/modules/position-creator/position.creator.module.ts @@ -0,0 +1,34 @@ +import { Module } from '@nestjs/common'; +import { PositionCreatorResolver } from './position.creator.resolver'; +import { PairModule } from '../pair/pair.module'; +import { RouterModule } from '../router/router.module'; +import { PositionCreatorComputeService } from './services/position.creator.compute'; +import { PositionCreatorTransactionService } from './services/position.creator.transaction'; +import { MXCommunicationModule } from 'src/services/multiversx-communication/mx.communication.module'; +import { AutoRouterModule } from '../auto-router/auto-router.module'; +import { FarmModuleV2 } from '../farm/v2/farm.v2.module'; +import { StakingProxyModule } from '../staking-proxy/staking.proxy.module'; +import { StakingModule } from '../staking/staking.module'; +import { TokenModule } from '../tokens/token.module'; +import { PositionCreatorTransactionResolver } from './position.creator.transaction.resolver'; + +@Module({ + imports: [ + PairModule, + RouterModule, + AutoRouterModule, + FarmModuleV2, + StakingModule, + StakingProxyModule, + TokenModule, + MXCommunicationModule, + ], + providers: [ + PositionCreatorComputeService, + PositionCreatorTransactionService, + PositionCreatorResolver, + PositionCreatorTransactionResolver, + ], + exports: [], +}) +export class PositionCreatorModule {} diff --git a/src/modules/position-creator/position.creator.resolver.ts b/src/modules/position-creator/position.creator.resolver.ts new file mode 100644 index 000000000..05810fa33 --- /dev/null +++ b/src/modules/position-creator/position.creator.resolver.ts @@ -0,0 +1,13 @@ +import { Query, Resolver } from '@nestjs/graphql'; +import { PositionCreatorModel } from './models/position.creator.model'; +import { scAddress } from 'src/config'; + +@Resolver(PositionCreatorModel) +export class PositionCreatorResolver { + @Query(() => PositionCreatorModel) + async getPositionCreator(): Promise { + return new PositionCreatorModel({ + address: scAddress.positionCreator, + }); + } +} diff --git a/src/modules/position-creator/position.creator.transaction.resolver.ts b/src/modules/position-creator/position.creator.transaction.resolver.ts new file mode 100644 index 000000000..6885f73c5 --- /dev/null +++ b/src/modules/position-creator/position.creator.transaction.resolver.ts @@ -0,0 +1,170 @@ +import { UseGuards } from '@nestjs/common'; +import { Args, Query, Resolver } from '@nestjs/graphql'; +import { JwtOrNativeAuthGuard } from '../auth/jwt.or.native.auth.guard'; +import { TransactionModel } from 'src/models/transaction.model'; +import { EsdtTokenPayment } from '@multiversx/sdk-exchange'; +import { PositionCreatorTransactionService } from './services/position.creator.transaction'; +import { InputTokenModel } from 'src/models/inputToken.model'; +import { UserAuthResult } from '../auth/user.auth.result'; +import { AuthUser } from '../auth/auth.user'; + +@Resolver() +@UseGuards(JwtOrNativeAuthGuard) +export class PositionCreatorTransactionResolver { + constructor( + private readonly posCreatorTransaction: PositionCreatorTransactionService, + ) {} + + @Query(() => TransactionModel) + async createPositionSingleToken( + @AuthUser() user: UserAuthResult, + @Args('pairAddress') pairAddress: string, + @Args('payment') payment: InputTokenModel, + @Args('tolerance') tolerance: number, + ): Promise { + return this.posCreatorTransaction.createLiquidityPositionSingleToken( + user.address, + pairAddress, + new EsdtTokenPayment({ + tokenIdentifier: payment.tokenID, + tokenNonce: payment.nonce, + amount: payment.amount, + }), + tolerance, + ); + } + + @Query(() => TransactionModel) + async createFarmPositionSingleToken( + @AuthUser() user: UserAuthResult, + @Args('farmAddress') farmAddress: string, + @Args('payments', { type: () => [InputTokenModel] }) + payments: InputTokenModel[], + @Args('tolerance') tolerance: number, + ): Promise { + return this.posCreatorTransaction.createFarmPositionSingleToken( + user.address, + farmAddress, + payments.map( + (payment) => + new EsdtTokenPayment({ + tokenIdentifier: payment.tokenID, + tokenNonce: payment.nonce, + amount: payment.amount, + }), + ), + tolerance, + ); + } + + @Query(() => TransactionModel) + async createDualFarmPositionSingleToken( + @AuthUser() user: UserAuthResult, + @Args('dualFarmAddress') dualFarmAddress: string, + @Args('payments', { type: () => [InputTokenModel] }) + payments: InputTokenModel[], + @Args('tolerance') tolerance: number, + ): Promise { + return this.posCreatorTransaction.createDualFarmPositionSingleToken( + user.address, + dualFarmAddress, + payments.map( + (payment) => + new EsdtTokenPayment({ + tokenIdentifier: payment.tokenID, + tokenNonce: payment.nonce, + amount: payment.amount, + }), + ), + tolerance, + ); + } + + @Query(() => TransactionModel) + async createStakingPositionSingleToken( + @AuthUser() user: UserAuthResult, + @Args('stakingAddress') stakingAddress: string, + @Args('payments', { type: () => [InputTokenModel] }) + payments: InputTokenModel[], + @Args('tolerance') tolerance: number, + ): Promise { + return this.posCreatorTransaction.createStakingPositionSingleToken( + user.address, + stakingAddress, + payments.map( + (payment) => + new EsdtTokenPayment({ + tokenIdentifier: payment.tokenID, + tokenNonce: payment.nonce, + amount: payment.amount, + }), + ), + tolerance, + ); + } + + @Query(() => TransactionModel) + async createFarmPositionDualTokens( + @AuthUser() user: UserAuthResult, + @Args('farmAddress') farmAddress: string, + @Args('payments', { type: () => [InputTokenModel] }) + payments: InputTokenModel[], + @Args('tolerance') tolerance: number, + ): Promise { + return this.posCreatorTransaction.createFarmPositionDualTokens( + user.address, + farmAddress, + payments.map( + (payment) => + new EsdtTokenPayment({ + tokenIdentifier: payment.tokenID, + tokenNonce: payment.nonce, + amount: payment.amount, + }), + ), + tolerance, + ); + } + + @Query(() => TransactionModel) + async createDualFarmPositionDualTokens( + @AuthUser() user: UserAuthResult, + @Args('dualFarmAddress') dualFarmAddress: string, + @Args('payments', { type: () => [InputTokenModel] }) + payments: InputTokenModel[], + @Args('tolerance') tolerance: number, + ): Promise { + return this.posCreatorTransaction.createDualFarmPositionDualTokens( + user.address, + dualFarmAddress, + payments.map( + (payment) => + new EsdtTokenPayment({ + tokenIdentifier: payment.tokenID, + tokenNonce: payment.nonce, + amount: payment.amount, + }), + ), + tolerance, + ); + } + + @Query(() => TransactionModel) + async exitFarmPositionDualTokens( + @AuthUser() user: UserAuthResult, + @Args('farmAddress') farmAddress: string, + @Args('payment') payment: InputTokenModel, + @Args('tolerance') tolerance: number, + ): Promise { + return this.posCreatorTransaction.exitFarmPositionDualTokens( + user.address, + farmAddress, + new EsdtTokenPayment({ + tokenIdentifier: payment.tokenID, + tokenNonce: payment.nonce, + amount: payment.amount, + }), + tolerance, + ); + } +} diff --git a/src/modules/position-creator/services/position.creator.compute.ts b/src/modules/position-creator/services/position.creator.compute.ts new file mode 100644 index 000000000..95168b5e1 --- /dev/null +++ b/src/modules/position-creator/services/position.creator.compute.ts @@ -0,0 +1,124 @@ +import { TypedValue } from '@multiversx/sdk-core/out'; +import { EsdtTokenPayment } from '@multiversx/sdk-exchange'; +import { PerformanceProfiler } from '@multiversx/sdk-nestjs-monitoring'; +import { Injectable } from '@nestjs/common'; +import BigNumber from 'bignumber.js'; +import { SWAP_TYPE } from 'src/modules/auto-router/models/auto-route.model'; +import { AutoRouterService } from 'src/modules/auto-router/services/auto-router.service'; +import { AutoRouterTransactionService } from 'src/modules/auto-router/services/auto-router.transactions.service'; +import { PairAbiService } from 'src/modules/pair/services/pair.abi.service'; +import { PairService } from 'src/modules/pair/services/pair.service'; +import { RouterAbiService } from 'src/modules/router/services/router.abi.service'; + +export type PositionCreatorSingleTokenPairInput = { + swapRouteArgs: TypedValue[]; + amount0Min: BigNumber; + amount1Min: BigNumber; +}; + +@Injectable() +export class PositionCreatorComputeService { + constructor( + private readonly pairAbi: PairAbiService, + private readonly pairService: PairService, + private readonly routerAbi: RouterAbiService, + private readonly autoRouterService: AutoRouterService, + private readonly autoRouterTransaction: AutoRouterTransactionService, + ) {} + + async computeSwap( + pairAddress: string, + fromTokenID: string, + toTokenID: string, + amount: string, + ): Promise { + if (fromTokenID === toTokenID) { + return new BigNumber(amount); + } + + const amountOut = await this.pairService.getAmountOut( + pairAddress, + fromTokenID, + amount, + ); + + return new BigNumber(amountOut); + } + + async computeSingleTokenPairInput( + pairAddress: string, + payment: EsdtTokenPayment, + tolerance: number, + ): Promise { + const acceptedPairedTokensIDs = + await this.routerAbi.commonTokensForUserPairs(); + + const [firstTokenID, secondTokenID] = await Promise.all([ + this.pairAbi.firstTokenID(pairAddress), + this.pairAbi.secondTokenID(pairAddress), + ]); + + const swapToTokenID = acceptedPairedTokensIDs.includes(firstTokenID) + ? firstTokenID + : secondTokenID; + + const profiler = new PerformanceProfiler(); + + const swapRoute = await this.autoRouterService.swap({ + tokenInID: payment.tokenIdentifier, + amountIn: payment.amount, + tokenOutID: swapToTokenID, + tolerance, + }); + + profiler.stop('swap route', true); + + const halfPayment = new BigNumber(swapRoute.amountOut) + .dividedBy(2) + .integerValue() + .toFixed(); + + const remainingPayment = new BigNumber(swapRoute.amountOut) + .minus(halfPayment) + .toFixed(); + + const [amount0, amount1] = await Promise.all([ + await this.computeSwap( + pairAddress, + swapRoute.tokenOutID, + firstTokenID, + halfPayment, + ), + await this.computeSwap( + pairAddress, + swapRoute.tokenOutID, + secondTokenID, + remainingPayment, + ), + ]); + + const amount0Min = new BigNumber(amount0) + .multipliedBy(1 - tolerance) + .integerValue(); + const amount1Min = new BigNumber(amount1) + .multipliedBy(1 - tolerance) + .integerValue(); + + const swapRouteArgs = + this.autoRouterTransaction.multiPairFixedInputSwaps({ + tokenInID: swapRoute.tokenInID, + tokenOutID: swapRoute.tokenOutID, + swapType: SWAP_TYPE.fixedInput, + tolerance, + addressRoute: swapRoute.pairs.map((pair) => pair.address), + intermediaryAmounts: swapRoute.intermediaryAmounts, + tokenRoute: swapRoute.tokenRoute, + }); + + return { + swapRouteArgs, + amount0Min, + amount1Min, + }; + } +} diff --git a/src/modules/position-creator/services/position.creator.transaction.ts b/src/modules/position-creator/services/position.creator.transaction.ts new file mode 100644 index 000000000..b8b476641 --- /dev/null +++ b/src/modules/position-creator/services/position.creator.transaction.ts @@ -0,0 +1,467 @@ +import { + Address, + AddressValue, + BigUIntValue, + TokenTransfer, +} from '@multiversx/sdk-core/out'; +import { EsdtTokenPayment } from '@multiversx/sdk-exchange'; +import { Injectable } from '@nestjs/common'; +import BigNumber from 'bignumber.js'; +import { gasConfig, mxConfig } from 'src/config'; +import { TransactionModel } from 'src/models/transaction.model'; +import { PairAbiService } from 'src/modules/pair/services/pair.abi.service'; +import { MXProxyService } from 'src/services/multiversx-communication/mx.proxy.service'; +import { PositionCreatorComputeService } from './position.creator.compute'; +import { AutoRouterService } from 'src/modules/auto-router/services/auto-router.service'; +import { FarmAbiServiceV2 } from 'src/modules/farm/v2/services/farm.v2.abi.service'; +import { StakingProxyAbiService } from 'src/modules/staking-proxy/services/staking.proxy.abi.service'; +import { StakingAbiService } from 'src/modules/staking/services/staking.abi.service'; +import { AutoRouterTransactionService } from 'src/modules/auto-router/services/auto-router.transactions.service'; +import { SWAP_TYPE } from 'src/modules/auto-router/models/auto-route.model'; +import { PairService } from 'src/modules/pair/services/pair.service'; +import { TokenService } from 'src/modules/tokens/services/token.service'; + +@Injectable() +export class PositionCreatorTransactionService { + constructor( + private readonly autoRouterService: AutoRouterService, + private readonly autoRouterTransaction: AutoRouterTransactionService, + private readonly posCreatorCompute: PositionCreatorComputeService, + private readonly pairAbi: PairAbiService, + private readonly pairService: PairService, + private readonly farmAbiV2: FarmAbiServiceV2, + private readonly stakingAbi: StakingAbiService, + private readonly stakingProxyAbi: StakingProxyAbiService, + private readonly tokenService: TokenService, + private readonly mxProxy: MXProxyService, + ) {} + + async createLiquidityPositionSingleToken( + sender: string, + pairAddress: string, + payment: EsdtTokenPayment, + tolerance: number, + ): Promise { + const uniqueTokensIDs = await this.tokenService.getUniqueTokenIDs( + false, + ); + + if (!uniqueTokensIDs.includes(payment.tokenIdentifier)) { + throw new Error('Invalid ESDT token payment'); + } + + const singleTokenPairInput = + await this.posCreatorCompute.computeSingleTokenPairInput( + pairAddress, + payment, + tolerance, + ); + + const contract = await this.mxProxy.getPostitionCreatorContract(); + + return contract.methodsExplicit + .createLpPosFromSingleToken([ + new AddressValue(Address.fromBech32(pairAddress)), + new BigUIntValue(singleTokenPairInput.amount0Min), + new BigUIntValue(singleTokenPairInput.amount1Min), + ...singleTokenPairInput.swapRouteArgs, + ]) + .withSingleESDTTransfer( + TokenTransfer.fungibleFromBigInteger( + payment.tokenIdentifier, + new BigNumber(payment.amount), + ), + ) + .withSender(Address.fromBech32(sender)) + .withGasLimit(gasConfig.positionCreator.singleToken) + .withChainID(mxConfig.chainID) + .buildTransaction() + .toPlainObject(); + } + + async createFarmPositionSingleToken( + sender: string, + farmAddress: string, + payments: EsdtTokenPayment[], + tolerance: number, + ): Promise { + const [pairAddress, farmTokenID, uniqueTokensIDs] = await Promise.all([ + this.farmAbiV2.pairContractAddress(farmAddress), + this.farmAbiV2.farmTokenID(farmAddress), + this.tokenService.getUniqueTokenIDs(false), + ]); + + if (!uniqueTokensIDs.includes(payments[0].tokenIdentifier)) { + throw new Error('Invalid ESDT token payment'); + } + + for (const payment of payments.slice(1)) { + if (payment.tokenIdentifier !== farmTokenID) { + throw new Error('Invalid farm token payment'); + } + } + + const singleTokenPairInput = + await this.posCreatorCompute.computeSingleTokenPairInput( + pairAddress, + payments[0], + tolerance, + ); + + const contract = await this.mxProxy.getPostitionCreatorContract(); + + return contract.methodsExplicit + .createFarmPosFromSingleToken([ + new AddressValue(Address.fromBech32(farmAddress)), + new BigUIntValue(singleTokenPairInput.amount0Min), + new BigUIntValue(singleTokenPairInput.amount1Min), + ...singleTokenPairInput.swapRouteArgs, + ]) + .withMultiESDTNFTTransfer( + payments.map((payment) => + TokenTransfer.metaEsdtFromBigInteger( + payment.tokenIdentifier, + payment.tokenNonce, + new BigNumber(payment.amount), + ), + ), + ) + .withSender(Address.fromBech32(sender)) + .withGasLimit(gasConfig.positionCreator.singleToken) + .withChainID(mxConfig.chainID) + .buildTransaction() + .toPlainObject(); + } + + async createDualFarmPositionSingleToken( + sender: string, + stakingProxyAddress: string, + payments: EsdtTokenPayment[], + tolerance: number, + ): Promise { + const [pairAddress, dualYieldTokenID, uniqueTokensIDs] = + await Promise.all([ + this.stakingProxyAbi.pairAddress(stakingProxyAddress), + this.stakingProxyAbi.dualYieldTokenID(stakingProxyAddress), + this.tokenService.getUniqueTokenIDs(false), + ]); + + if (!uniqueTokensIDs.includes(payments[0].tokenIdentifier)) { + throw new Error('Invalid ESDT token payment'); + } + + for (const payment of payments.slice(1)) { + if (payment.tokenIdentifier !== dualYieldTokenID) { + throw new Error('Invalid dual yield token payment'); + } + } + + const singleTokenPairInput = + await this.posCreatorCompute.computeSingleTokenPairInput( + pairAddress, + payments[0], + tolerance, + ); + + const contract = await this.mxProxy.getPostitionCreatorContract(); + + return contract.methodsExplicit + .createMetastakingPosFromSingleToken([ + new AddressValue(Address.fromBech32(stakingProxyAddress)), + new BigUIntValue(singleTokenPairInput.amount0Min), + new BigUIntValue(singleTokenPairInput.amount1Min), + ...singleTokenPairInput.swapRouteArgs, + ]) + .withMultiESDTNFTTransfer( + payments.map((payment) => + TokenTransfer.metaEsdtFromBigInteger( + payment.tokenIdentifier, + payment.tokenNonce, + new BigNumber(payment.amount), + ), + ), + ) + .withSender(Address.fromBech32(sender)) + .withGasLimit(gasConfig.positionCreator.singleToken) + .withChainID(mxConfig.chainID) + .buildTransaction() + .toPlainObject(); + } + + async createStakingPositionSingleToken( + sender: string, + stakingAddress: string, + payments: EsdtTokenPayment[], + tolerance: number, + ): Promise { + const [farmingTokenID, farmTokenID, uniqueTokensIDs] = + await Promise.all([ + this.stakingAbi.farmingTokenID(stakingAddress), + this.stakingAbi.farmTokenID(stakingAddress), + this.tokenService.getUniqueTokenIDs(false), + ]); + + if (!uniqueTokensIDs.includes(payments[0].tokenIdentifier)) { + throw new Error('Invalid ESDT token payment'); + } + + for (const payment of payments.slice(1)) { + if (payment.tokenIdentifier !== farmTokenID) { + throw new Error('Invalid staking token payment'); + } + } + + const swapRoute = await this.autoRouterService.swap({ + tokenInID: payments[0].tokenIdentifier, + amountIn: payments[0].amount, + tokenOutID: farmingTokenID, + tolerance, + }); + + const contract = await this.mxProxy.getPostitionCreatorContract(); + + const multiSwapArgs = + this.autoRouterTransaction.multiPairFixedInputSwaps({ + tokenInID: swapRoute.tokenInID, + tokenOutID: swapRoute.tokenOutID, + swapType: SWAP_TYPE.fixedInput, + tolerance, + addressRoute: swapRoute.pairs.map((pair) => pair.address), + intermediaryAmounts: swapRoute.intermediaryAmounts, + tokenRoute: swapRoute.tokenRoute, + }); + + return contract.methodsExplicit + .createFarmStakingPosFromSingleToken([ + new AddressValue(Address.fromBech32(stakingAddress)), + new BigUIntValue( + new BigNumber( + swapRoute.intermediaryAmounts[ + swapRoute.intermediaryAmounts.length - 1 + ], + ), + ), + ...multiSwapArgs, + ]) + .withMultiESDTNFTTransfer( + payments.map((payment) => + TokenTransfer.metaEsdtFromBigInteger( + payment.tokenIdentifier, + payment.tokenNonce, + new BigNumber(payment.amount), + ), + ), + ) + .withSender(Address.fromBech32(sender)) + .withGasLimit(gasConfig.positionCreator.singleToken) + .withChainID(mxConfig.chainID) + .buildTransaction() + .toPlainObject(); + } + + async createFarmPositionDualTokens( + sender: string, + farmAddress: string, + payments: EsdtTokenPayment[], + tolerance: number, + ): Promise { + const pairAddress = await this.farmAbiV2.pairContractAddress( + farmAddress, + ); + const [firstTokenID, secondTokenID, farmTokenID] = await Promise.all([ + this.pairAbi.firstTokenID(pairAddress), + this.pairAbi.secondTokenID(pairAddress), + this.farmAbiV2.farmTokenID(farmAddress), + ]); + + if (!this.checkTokensPayments(payments, firstTokenID, secondTokenID)) { + throw new Error('Invalid tokens payments'); + } + + for (const payment of payments.slice(2)) { + if (payment.tokenIdentifier !== farmTokenID) { + throw new Error('Invalid farm token payment'); + } + } + + const [firstPayment, secondPayment] = + payments[0].tokenIdentifier === firstTokenID + ? [payments[0], payments[1]] + : [payments[1], payments[0]]; + + const amount0Min = new BigNumber(firstPayment.amount) + .multipliedBy(1 - tolerance) + .integerValue(); + const amount1Min = new BigNumber(secondPayment.amount) + .multipliedBy(1 - tolerance) + .integerValue(); + + const contract = await this.mxProxy.getPostitionCreatorContract(); + + return contract.methodsExplicit + .createFarmPosFromTwoTokens([ + new AddressValue(Address.fromBech32(farmAddress)), + new BigUIntValue(amount0Min), + new BigUIntValue(amount1Min), + ]) + .withMultiESDTNFTTransfer([ + TokenTransfer.fungibleFromBigInteger( + firstPayment.tokenIdentifier, + new BigNumber(firstPayment.amount), + ), + TokenTransfer.fungibleFromBigInteger( + secondPayment.tokenIdentifier, + + new BigNumber(secondPayment.amount), + ), + ...payments + .slice(2) + .map((payment) => + TokenTransfer.metaEsdtFromBigInteger( + payment.tokenIdentifier, + payment.tokenNonce, + new BigNumber(payment.amount), + ), + ), + ]) + .withSender(Address.fromBech32(sender)) + .withGasLimit(gasConfig.positionCreator.singleToken) + .withChainID(mxConfig.chainID) + .buildTransaction() + .toPlainObject(); + } + + async createDualFarmPositionDualTokens( + sender: string, + stakingProxyAddress: string, + payments: EsdtTokenPayment[], + tolerance: number, + ): Promise { + const pairAddress = await this.stakingProxyAbi.pairAddress( + stakingProxyAddress, + ); + const [firstTokenID, secondTokenID, dualYieldTokenID] = + await Promise.all([ + this.pairAbi.firstTokenID(pairAddress), + this.pairAbi.secondTokenID(pairAddress), + this.stakingProxyAbi.dualYieldTokenID(stakingProxyAddress), + ]); + + if (!this.checkTokensPayments(payments, firstTokenID, secondTokenID)) { + throw new Error('Invalid tokens payments'); + } + + for (const payment of payments.slice(2)) { + if (payment.tokenIdentifier !== dualYieldTokenID) { + throw new Error('Invalid dual farm token payment'); + } + } + + const [firstPayment, secondPayment] = + payments[0].tokenIdentifier === firstTokenID + ? [payments[0], payments[1]] + : [payments[1], payments[0]]; + + const amount0Min = new BigNumber(firstPayment.amount) + .multipliedBy(1 - tolerance) + .integerValue(); + const amount1Min = new BigNumber(secondPayment.amount) + .multipliedBy(1 - tolerance) + .integerValue(); + + const contract = await this.mxProxy.getPostitionCreatorContract(); + + return contract.methodsExplicit + .createMetastakingPosFromTwoTokens([ + new AddressValue(Address.fromBech32(stakingProxyAddress)), + new BigUIntValue(amount0Min), + new BigUIntValue(amount1Min), + ]) + .withMultiESDTNFTTransfer([ + TokenTransfer.fungibleFromBigInteger( + firstPayment.tokenIdentifier, + new BigNumber(firstPayment.amount), + ), + TokenTransfer.fungibleFromBigInteger( + secondPayment.tokenIdentifier, + + new BigNumber(secondPayment.amount), + ), + ...payments + .slice(2) + .map((payment) => + TokenTransfer.metaEsdtFromBigInteger( + payment.tokenIdentifier, + payment.tokenNonce, + new BigNumber(payment.amount), + ), + ), + ]) + .withSender(Address.fromBech32(sender)) + .withGasLimit(gasConfig.positionCreator.singleToken) + .withChainID(mxConfig.chainID) + .buildTransaction() + .toPlainObject(); + } + + async exitFarmPositionDualTokens( + sender: string, + farmAddress: string, + payment: EsdtTokenPayment, + tolerance: number, + ): Promise { + const [pairAddress, farmTokenID] = await Promise.all([ + this.farmAbiV2.pairContractAddress(farmAddress), + this.farmAbiV2.farmTokenID(farmAddress), + ]); + + if (payment.tokenIdentifier !== farmTokenID) { + throw new Error('Invalid farm token payment'); + } + + const liquidityPosition = await this.pairService.getLiquidityPosition( + pairAddress, + payment.amount, + ); + const amount0Min = new BigNumber(liquidityPosition.firstTokenAmount) + .multipliedBy(1 - tolerance) + .integerValue(); + const amount1Min = new BigNumber(liquidityPosition.secondTokenAmount) + .multipliedBy(1 - tolerance) + .integerValue(); + + const contract = await this.mxProxy.getPostitionCreatorContract(); + + return contract.methodsExplicit + .exitFarmPos([ + new AddressValue(Address.fromBech32(farmAddress)), + new BigUIntValue(amount0Min), + new BigUIntValue(amount1Min), + ]) + .withSingleESDTNFTTransfer( + TokenTransfer.metaEsdtFromBigInteger( + payment.tokenIdentifier, + payment.tokenNonce, + new BigNumber(payment.amount), + ), + ) + .withSender(Address.fromBech32(sender)) + .withGasLimit(gasConfig.positionCreator.singleToken) + .withChainID(mxConfig.chainID) + .buildTransaction() + .toPlainObject(); + } + + private checkTokensPayments( + payments: EsdtTokenPayment[], + firstTokenID: string, + secondTokenID: string, + ): boolean { + return ( + (payments[0].tokenIdentifier === firstTokenID && + payments[1].tokenIdentifier === secondTokenID) || + (payments[1].tokenIdentifier === firstTokenID && + payments[0].tokenIdentifier === secondTokenID) + ); + } +} diff --git a/src/modules/position-creator/specs/position.creator.transaction.spec.ts b/src/modules/position-creator/specs/position.creator.transaction.spec.ts new file mode 100644 index 000000000..0acd9f074 --- /dev/null +++ b/src/modules/position-creator/specs/position.creator.transaction.spec.ts @@ -0,0 +1,949 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PositionCreatorTransactionService } from '../services/position.creator.transaction'; +import { PositionCreatorComputeService } from '../services/position.creator.compute'; +import { PairAbiServiceProvider } from 'src/modules/pair/mocks/pair.abi.service.mock'; +import { PairService } from 'src/modules/pair/services/pair.service'; +import { RouterAbiServiceProvider } from 'src/modules/router/mocks/router.abi.service.mock'; +import { RouterService } from 'src/modules/router/services/router.service'; +import { AutoRouterService } from 'src/modules/auto-router/services/auto-router.service'; +import { AutoRouterTransactionService } from 'src/modules/auto-router/services/auto-router.transactions.service'; +import { FarmAbiServiceProviderV2 } from 'src/modules/farm/mocks/farm.v2.abi.service.mock'; +import { StakingAbiServiceProvider } from 'src/modules/staking/mocks/staking.abi.service.mock'; +import { StakingProxyAbiServiceProvider } from 'src/modules/staking-proxy/mocks/staking.proxy.abi.service.mock'; +import { TokenServiceProvider } from 'src/modules/tokens/mocks/token.service.mock'; +import { MXProxyServiceProvider } from 'src/services/multiversx-communication/mx.proxy.service.mock'; +import { PairComputeServiceProvider } from 'src/modules/pair/mocks/pair.compute.service.mock'; +import { WrapAbiServiceProvider } from 'src/modules/wrapping/mocks/wrap.abi.service.mock'; +import { WinstonModule } from 'nest-winston'; +import winston from 'winston'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { DynamicModuleUtils } from 'src/utils/dynamic.module.utils'; +import { ApiConfigService } from 'src/helpers/api.config.service'; +import { ContextGetterServiceProvider } from 'src/services/context/mocks/context.getter.service.mock'; +import { AutoRouterComputeService } from 'src/modules/auto-router/services/auto-router.compute.service'; +import { PairTransactionService } from 'src/modules/pair/services/pair.transactions.service'; +import { WrapTransactionsService } from 'src/modules/wrapping/services/wrap.transactions.service'; +import { WrapService } from 'src/modules/wrapping/services/wrap.service'; +import { RemoteConfigGetterServiceProvider } from 'src/modules/remote-config/mocks/remote-config.getter.mock'; +import { Address } from '@multiversx/sdk-core/out'; +import { EsdtTokenPayment } from '@multiversx/sdk-exchange'; +import { encodeTransactionData } from 'src/helpers/helpers'; +import exp from 'constants'; +import { StakingProxyAbiService } from 'src/modules/staking-proxy/services/staking.proxy.abi.service'; + +describe('PositionCreatorTransaction', () => { + let module: TestingModule; + + beforeEach(async () => { + module = await Test.createTestingModule({ + imports: [ + WinstonModule.forRoot({ + transports: [new winston.transports.Console({})], + }), + ConfigModule.forRoot({}), + DynamicModuleUtils.getCacheModule(), + ], + providers: [ + PositionCreatorTransactionService, + PositionCreatorComputeService, + PairAbiServiceProvider, + PairService, + PairComputeServiceProvider, + PairTransactionService, + WrapService, + WrapAbiServiceProvider, + WrapTransactionsService, + RouterAbiServiceProvider, + RouterService, + AutoRouterService, + AutoRouterTransactionService, + AutoRouterComputeService, + FarmAbiServiceProviderV2, + StakingAbiServiceProvider, + StakingProxyAbiServiceProvider, + TokenServiceProvider, + RemoteConfigGetterServiceProvider, + MXProxyServiceProvider, + ConfigService, + ApiConfigService, + ContextGetterServiceProvider, + ], + }).compile(); + }); + + it('should be defined', () => { + expect(module).toBeDefined(); + }); + + describe('Create liquidity position single token', () => { + it('should return error on ESDT token', async () => { + const service = module.get( + PositionCreatorTransactionService, + ); + expect( + service.createLiquidityPositionSingleToken( + Address.Zero().bech32(), + Address.fromHex( + '0000000000000000000000000000000000000000000000000000000000000012', + ).bech32(), + new EsdtTokenPayment({ + tokenIdentifier: 'MEX-abcdef', + tokenNonce: 0, + amount: '100000000000000000000', + }), + 0.01, + ), + ).rejects.toThrowError('Invalid ESDT token payment'); + }); + + it('should return transaction with single token', async () => { + const service = module.get( + PositionCreatorTransactionService, + ); + const transaction = + await service.createLiquidityPositionSingleToken( + Address.Zero().bech32(), + Address.fromHex( + '0000000000000000000000000000000000000000000000000000000000000012', + ).bech32(), + new EsdtTokenPayment({ + tokenIdentifier: 'USDC-123456', + tokenNonce: 0, + amount: '100000000000000000000', + }), + 0.01, + ); + + expect(transaction).toEqual({ + nonce: 0, + value: '0', + receiver: + 'erd1qqqqqqqqqqqqqpgqh3zcutxk3wmfvevpyymaehvc3k0knyq70n4sg6qcj6', + sender: Address.Zero().bech32(), + senderUsername: undefined, + receiverUsername: undefined, + gasPrice: 1000000000, + gasLimit: 50000000, + data: encodeTransactionData( + `ESDTTransfer@USDC-123456@100000000000000000000@createLpPosFromSingleToken@0000000000000000000000000000000000000000000000000000000000000012@494999999950351053163@329339339317295273252718@0000000000000000000000000000000000000000000000000000000000000013@swapTokensFixedInput@WEGLD-123456@989999999900702106327`, + ), + chainID: 'T', + version: 1, + options: undefined, + guardian: undefined, + signature: undefined, + guardianSignature: undefined, + }); + }); + }); + + describe('Create farm position single token', () => { + it('should return error on ESDT token', async () => { + const service = module.get( + PositionCreatorTransactionService, + ); + expect( + service.createFarmPositionSingleToken( + Address.Zero().bech32(), + Address.fromHex( + '0000000000000000000000000000000000000000000000000000000000000021', + ).bech32(), + [ + new EsdtTokenPayment({ + tokenIdentifier: 'MEX-abcdef', + tokenNonce: 0, + amount: '100000000000000000000', + }), + ], + 0.01, + ), + ).rejects.toThrowError('Invalid ESDT token payment'); + }); + + it('should return error on farm token', async () => { + const service = module.get( + PositionCreatorTransactionService, + ); + expect( + service.createFarmPositionSingleToken( + Address.Zero().bech32(), + Address.fromHex( + '0000000000000000000000000000000000000000000000000000000000000021', + ).bech32(), + [ + new EsdtTokenPayment({ + tokenIdentifier: 'USDC-123456', + tokenNonce: 0, + amount: '100000000000000000000', + }), + new EsdtTokenPayment({ + tokenIdentifier: 'EGLDMEXFL-123456', + tokenNonce: 1, + amount: '100000000000000000000', + }), + ], + 0.01, + ), + ).rejects.toThrowError('Invalid farm token payment'); + }); + + it('should return transaction no merge farm tokens', async () => { + const service = module.get( + PositionCreatorTransactionService, + ); + const transaction = await service.createFarmPositionSingleToken( + Address.Zero().bech32(), + Address.fromHex( + '0000000000000000000000000000000000000000000000000000000000000021', + ).bech32(), + [ + new EsdtTokenPayment({ + tokenIdentifier: 'USDC-123456', + tokenNonce: 0, + amount: '100000000000000000000', + }), + ], + 0.01, + ); + + expect(transaction).toEqual({ + nonce: 0, + value: '0', + receiver: Address.Zero().bech32(), + sender: Address.Zero().bech32(), + senderUsername: undefined, + receiverUsername: undefined, + gasPrice: 1000000000, + gasLimit: 50000000, + data: encodeTransactionData( + `MultiESDTNFTTransfer@00000000000000000500bc458e2cd68bb69665812137dcdd988d9f69901e7ceb@01@USDC-123456@@100000000000000000000@createFarmPosFromSingleToken@0000000000000000000000000000000000000000000000000000000000000021@494999999950351053163@329339339317295273252718@0000000000000000000000000000000000000000000000000000000000000013@swapTokensFixedInput@WEGLD-123456@989999999900702106327`, + ), + chainID: 'T', + version: 1, + options: undefined, + guardian: undefined, + signature: undefined, + guardianSignature: undefined, + }); + }); + + it('should return transaction with merge farm tokens', async () => { + const service = module.get( + PositionCreatorTransactionService, + ); + const transaction = await service.createFarmPositionSingleToken( + Address.Zero().bech32(), + Address.fromHex( + '0000000000000000000000000000000000000000000000000000000000000021', + ).bech32(), + [ + new EsdtTokenPayment({ + tokenIdentifier: 'USDC-123456', + tokenNonce: 0, + amount: '100000000000000000000', + }), + new EsdtTokenPayment({ + tokenIdentifier: 'EGLDMEXFL-abcdef', + tokenNonce: 1, + amount: '100000000000000000000', + }), + ], + 0.01, + ); + + expect(transaction).toEqual({ + nonce: 0, + value: '0', + receiver: Address.Zero().bech32(), + sender: Address.Zero().bech32(), + senderUsername: undefined, + receiverUsername: undefined, + gasPrice: 1000000000, + gasLimit: 50000000, + data: encodeTransactionData( + `MultiESDTNFTTransfer@00000000000000000500bc458e2cd68bb69665812137dcdd988d9f69901e7ceb@02@USDC-123456@@100000000000000000000@EGLDMEXFL-abcdef@01@100000000000000000000@createFarmPosFromSingleToken@0000000000000000000000000000000000000000000000000000000000000021@494999999950351053163@329339339317295273252718@0000000000000000000000000000000000000000000000000000000000000013@swapTokensFixedInput@WEGLD-123456@989999999900702106327`, + ), + chainID: 'T', + version: 1, + options: undefined, + guardian: undefined, + signature: undefined, + guardianSignature: undefined, + }); + }); + }); + + describe('Create dual farm position single token', () => { + it('should return error on ESDT token', async () => { + const service = module.get( + PositionCreatorTransactionService, + ); + expect( + service.createDualFarmPositionSingleToken( + Address.Zero().bech32(), + Address.Zero().bech32(), + [ + new EsdtTokenPayment({ + tokenIdentifier: 'USDC-abcdef', + tokenNonce: 0, + amount: '100000000000000000000', + }), + ], + 0.01, + ), + ).rejects.toThrowError('Invalid ESDT token payment'); + }); + + it('should return error on dual farm token', async () => { + const service = module.get( + PositionCreatorTransactionService, + ); + expect( + service.createDualFarmPositionSingleToken( + Address.Zero().bech32(), + Address.Zero().bech32(), + [ + new EsdtTokenPayment({ + tokenIdentifier: 'USDC-123456', + tokenNonce: 0, + amount: '100000000000000000000', + }), + new EsdtTokenPayment({ + tokenIdentifier: 'METASTAKE-abcdef', + tokenNonce: 1, + amount: '100000000000000000000', + }), + ], + 0.01, + ), + ).rejects.toThrowError('Invalid dual yield token payment'); + }); + + it('should return transaction no merge dual farm tokens', async () => { + const service = module.get( + PositionCreatorTransactionService, + ); + const stakingProxyAbi = module.get( + StakingProxyAbiService, + ); + jest.spyOn(stakingProxyAbi, 'pairAddress').mockResolvedValue( + Address.fromHex( + '0000000000000000000000000000000000000000000000000000000000000012', + ).bech32(), + ); + + const transaction = await service.createDualFarmPositionSingleToken( + Address.Zero().bech32(), + Address.Zero().bech32(), + [ + new EsdtTokenPayment({ + tokenIdentifier: 'USDC-123456', + tokenNonce: 0, + amount: '100000000000000000000', + }), + ], + 0.01, + ); + + expect(transaction).toEqual({ + nonce: 0, + value: '0', + receiver: Address.Zero().bech32(), + sender: Address.Zero().bech32(), + senderUsername: undefined, + receiverUsername: undefined, + gasPrice: 1000000000, + gasLimit: 50000000, + data: encodeTransactionData( + 'MultiESDTNFTTransfer@00000000000000000500bc458e2cd68bb69665812137dcdd988d9f69901e7ceb@01@USDC-123456@@100000000000000000000@createMetastakingPosFromSingleToken@0000000000000000000000000000000000000000000000000000000000000000@494999999950351053163@329339339317295273252718@0000000000000000000000000000000000000000000000000000000000000013@swapTokensFixedInput@WEGLD-123456@989999999900702106327', + ), + chainID: 'T', + version: 1, + options: undefined, + guardian: undefined, + signature: undefined, + guardianSignature: undefined, + }); + }); + + it('should return transaction with merge dual farm tokens', async () => { + const service = module.get( + PositionCreatorTransactionService, + ); + const stakingProxyAbi = module.get( + StakingProxyAbiService, + ); + jest.spyOn(stakingProxyAbi, 'pairAddress').mockResolvedValue( + Address.fromHex( + '0000000000000000000000000000000000000000000000000000000000000012', + ).bech32(), + ); + + const transaction = await service.createDualFarmPositionSingleToken( + Address.Zero().bech32(), + Address.Zero().bech32(), + [ + new EsdtTokenPayment({ + tokenIdentifier: 'USDC-123456', + tokenNonce: 0, + amount: '100000000000000000000', + }), + new EsdtTokenPayment({ + tokenIdentifier: 'METASTAKE-1234', + tokenNonce: 1, + amount: '100000000000000000000', + }), + ], + 0.01, + ); + + expect(transaction).toEqual({ + nonce: 0, + value: '0', + receiver: Address.Zero().bech32(), + sender: Address.Zero().bech32(), + senderUsername: undefined, + receiverUsername: undefined, + gasPrice: 1000000000, + gasLimit: 50000000, + data: encodeTransactionData( + 'MultiESDTNFTTransfer@00000000000000000500bc458e2cd68bb69665812137dcdd988d9f69901e7ceb@02@USDC-123456@@100000000000000000000@METASTAKE-1234@01@100000000000000000000@createMetastakingPosFromSingleToken@0000000000000000000000000000000000000000000000000000000000000000@494999999950351053163@329339339317295273252718@0000000000000000000000000000000000000000000000000000000000000013@swapTokensFixedInput@WEGLD-123456@989999999900702106327', + ), + chainID: 'T', + version: 1, + options: undefined, + guardian: undefined, + signature: undefined, + guardianSignature: undefined, + }); + }); + }); + + describe('Create staking position single token', () => { + it('should return error on ESDT token', async () => { + const service = module.get( + PositionCreatorTransactionService, + ); + expect( + service.createStakingPositionSingleToken( + Address.Zero().bech32(), + Address.Zero().bech32(), + [ + new EsdtTokenPayment({ + tokenIdentifier: 'USDC-abcdef', + tokenNonce: 0, + amount: '100000000000000000000', + }), + ], + 0.01, + ), + ).rejects.toThrowError('Invalid ESDT token payment'); + }); + + it('should return error on staking token', async () => { + const service = module.get( + PositionCreatorTransactionService, + ); + expect( + service.createStakingPositionSingleToken( + Address.Zero().bech32(), + Address.Zero().bech32(), + [ + new EsdtTokenPayment({ + tokenIdentifier: 'USDC-123456', + tokenNonce: 0, + amount: '100000000000000000000', + }), + new EsdtTokenPayment({ + tokenIdentifier: 'STAKETOK-abcdef', + tokenNonce: 1, + amount: '100000000000000000000', + }), + ], + 0.01, + ), + ).rejects.toThrowError('Invalid staking token payment'); + }); + + it('should return transaction no merge staking tokens', async () => { + const service = module.get( + PositionCreatorTransactionService, + ); + const transaction = await service.createStakingPositionSingleToken( + Address.Zero().bech32(), + Address.Zero().bech32(), + [ + new EsdtTokenPayment({ + tokenIdentifier: 'USDC-123456', + tokenNonce: 0, + amount: '100000000000000000000', + }), + ], + 0.01, + ); + + expect(transaction).toEqual({ + nonce: 0, + value: '0', + receiver: Address.Zero().bech32(), + sender: Address.Zero().bech32(), + senderUsername: undefined, + receiverUsername: undefined, + gasPrice: 1000000000, + gasLimit: 50000000, + data: encodeTransactionData( + 'MultiESDTNFTTransfer@00000000000000000500bc458e2cd68bb69665812137dcdd988d9f69901e7ceb@01@USDC-123456@@100000000000000000000@createFarmStakingPosFromSingleToken@0000000000000000000000000000000000000000000000000000000000000000@999999999899699097301@0000000000000000000000000000000000000000000000000000000000000013@swapTokensFixedInput@WEGLD-123456@989999999900702106327', + ), + chainID: 'T', + version: 1, + options: undefined, + guardian: undefined, + signature: undefined, + guardianSignature: undefined, + }); + }); + + it('should return transaction with merge staking tokens', async () => { + const service = module.get( + PositionCreatorTransactionService, + ); + const transaction = await service.createStakingPositionSingleToken( + Address.Zero().bech32(), + Address.Zero().bech32(), + [ + new EsdtTokenPayment({ + tokenIdentifier: 'USDC-123456', + tokenNonce: 0, + amount: '100000000000000000000', + }), + new EsdtTokenPayment({ + tokenIdentifier: 'STAKETOK-1111', + tokenNonce: 1, + amount: '100000000000000000000', + }), + ], + 0.01, + ); + + expect(transaction).toEqual({ + nonce: 0, + value: '0', + receiver: Address.Zero().bech32(), + sender: Address.Zero().bech32(), + senderUsername: undefined, + receiverUsername: undefined, + gasPrice: 1000000000, + gasLimit: 50000000, + data: encodeTransactionData( + 'MultiESDTNFTTransfer@00000000000000000500bc458e2cd68bb69665812137dcdd988d9f69901e7ceb@02@USDC-123456@@100000000000000000000@STAKETOK-1111@01@100000000000000000000@createFarmStakingPosFromSingleToken@0000000000000000000000000000000000000000000000000000000000000000@999999999899699097301@0000000000000000000000000000000000000000000000000000000000000013@swapTokensFixedInput@WEGLD-123456@989999999900702106327', + ), + chainID: 'T', + version: 1, + options: undefined, + guardian: undefined, + signature: undefined, + guardianSignature: undefined, + }); + }); + }); + + describe('Create farm position dual tokens', () => { + it('should return error on invalid payments', async () => { + const service = module.get( + PositionCreatorTransactionService, + ); + expect( + service.createFarmPositionDualTokens( + Address.Zero().bech32(), + Address.fromHex( + '0000000000000000000000000000000000000000000000000000000000000021', + ).bech32(), + [ + new EsdtTokenPayment({ + tokenIdentifier: 'WEGLD-123456', + tokenNonce: 0, + amount: '100000000000000000000', + }), + new EsdtTokenPayment({ + tokenIdentifier: 'MEX-abcdef', + tokenNonce: 0, + amount: '100000000000000000000', + }), + ], + 0.01, + ), + ).rejects.toThrowError('Invalid tokens payments'); + }); + + it('should return error on invalid farm token merge', async () => { + const service = module.get( + PositionCreatorTransactionService, + ); + expect( + service.createFarmPositionDualTokens( + Address.Zero().bech32(), + Address.fromHex( + '0000000000000000000000000000000000000000000000000000000000000021', + ).bech32(), + [ + new EsdtTokenPayment({ + tokenIdentifier: 'WEGLD-123456', + tokenNonce: 0, + amount: '100000000000000000000', + }), + new EsdtTokenPayment({ + tokenIdentifier: 'MEX-123456', + tokenNonce: 0, + amount: '100000000000000000000', + }), + new EsdtTokenPayment({ + tokenIdentifier: 'EGLDMEXFL-123456', + tokenNonce: 1, + amount: '100000000000000000000', + }), + ], + 0.01, + ), + ).rejects.toThrowError('Invalid farm token payment'); + }); + + it('should return transaction no merge farm tokens', async () => { + const service = module.get( + PositionCreatorTransactionService, + ); + const transaction = await service.createFarmPositionDualTokens( + Address.Zero().bech32(), + Address.fromHex( + '0000000000000000000000000000000000000000000000000000000000000021', + ).bech32(), + [ + new EsdtTokenPayment({ + tokenIdentifier: 'WEGLD-123456', + tokenNonce: 0, + amount: '100000000000000000000', + }), + new EsdtTokenPayment({ + tokenIdentifier: 'MEX-123456', + tokenNonce: 0, + amount: '100000000000000000000', + }), + ], + 0.01, + ); + + expect(transaction).toEqual({ + nonce: 0, + value: '0', + receiver: Address.Zero().bech32(), + sender: Address.Zero().bech32(), + senderUsername: undefined, + receiverUsername: undefined, + gasPrice: 1000000000, + gasLimit: 50000000, + data: encodeTransactionData( + 'MultiESDTNFTTransfer@00000000000000000500bc458e2cd68bb69665812137dcdd988d9f69901e7ceb@02@WEGLD-123456@@100000000000000000000@MEX-123456@@100000000000000000000@createFarmPosFromTwoTokens@0000000000000000000000000000000000000000000000000000000000000021@99000000000000000000@99000000000000000000', + ), + chainID: 'T', + version: 1, + options: undefined, + guardian: undefined, + signature: undefined, + guardianSignature: undefined, + }); + }); + + it('should return transaction no merge farm tokens', async () => { + const service = module.get( + PositionCreatorTransactionService, + ); + const transaction = await service.createFarmPositionDualTokens( + Address.Zero().bech32(), + Address.fromHex( + '0000000000000000000000000000000000000000000000000000000000000021', + ).bech32(), + [ + new EsdtTokenPayment({ + tokenIdentifier: 'WEGLD-123456', + tokenNonce: 0, + amount: '100000000000000000000', + }), + new EsdtTokenPayment({ + tokenIdentifier: 'MEX-123456', + tokenNonce: 0, + amount: '100000000000000000000', + }), + new EsdtTokenPayment({ + tokenIdentifier: 'EGLDMEXFL-abcdef', + tokenNonce: 1, + amount: '100000000000000000000', + }), + ], + 0.01, + ); + + expect(transaction).toEqual({ + nonce: 0, + value: '0', + receiver: Address.Zero().bech32(), + sender: Address.Zero().bech32(), + senderUsername: undefined, + receiverUsername: undefined, + gasPrice: 1000000000, + gasLimit: 50000000, + data: encodeTransactionData( + 'MultiESDTNFTTransfer@00000000000000000500bc458e2cd68bb69665812137dcdd988d9f69901e7ceb@03@WEGLD-123456@@100000000000000000000@MEX-123456@@100000000000000000000@EGLDMEXFL-abcdef@01@100000000000000000000@createFarmPosFromTwoTokens@0000000000000000000000000000000000000000000000000000000000000021@99000000000000000000@99000000000000000000', + ), + chainID: 'T', + version: 1, + options: undefined, + guardian: undefined, + signature: undefined, + guardianSignature: undefined, + }); + }); + }); + + describe('Create dual farm position dual tokens', () => { + it('should return error on invalid payments', async () => { + const service = module.get( + PositionCreatorTransactionService, + ); + const stakingProxyAbi = module.get( + StakingProxyAbiService, + ); + jest.spyOn(stakingProxyAbi, 'pairAddress').mockResolvedValue( + Address.fromHex( + '0000000000000000000000000000000000000000000000000000000000000012', + ).bech32(), + ); + + expect( + service.createDualFarmPositionDualTokens( + Address.Zero().bech32(), + Address.Zero().bech32(), + [ + new EsdtTokenPayment({ + tokenIdentifier: 'WEGLD-123456', + tokenNonce: 0, + amount: '100000000000000000000', + }), + new EsdtTokenPayment({ + tokenIdentifier: 'MEX-abcdef', + tokenNonce: 0, + amount: '100000000000000000000', + }), + ], + 0.01, + ), + ).rejects.toThrowError('Invalid tokens payments'); + }); + + it('should return error on invalid farm token merge', async () => { + const service = module.get( + PositionCreatorTransactionService, + ); + const stakingProxyAbi = module.get( + StakingProxyAbiService, + ); + jest.spyOn(stakingProxyAbi, 'pairAddress').mockResolvedValue( + Address.fromHex( + '0000000000000000000000000000000000000000000000000000000000000012', + ).bech32(), + ); + + expect( + service.createDualFarmPositionDualTokens( + Address.Zero().bech32(), + Address.Zero().bech32(), + [ + new EsdtTokenPayment({ + tokenIdentifier: 'WEGLD-123456', + tokenNonce: 0, + amount: '100000000000000000000', + }), + new EsdtTokenPayment({ + tokenIdentifier: 'MEX-123456', + tokenNonce: 0, + amount: '100000000000000000000', + }), + new EsdtTokenPayment({ + tokenIdentifier: 'METASTAKE-abcdef', + tokenNonce: 1, + amount: '100000000000000000000', + }), + ], + 0.01, + ), + ).rejects.toThrowError('Invalid dual farm token payment'); + }); + + it('should return transaction no merge farm tokens', async () => { + const service = module.get( + PositionCreatorTransactionService, + ); + const stakingProxyAbi = module.get( + StakingProxyAbiService, + ); + jest.spyOn(stakingProxyAbi, 'pairAddress').mockResolvedValue( + Address.fromHex( + '0000000000000000000000000000000000000000000000000000000000000012', + ).bech32(), + ); + + const transaction = await service.createDualFarmPositionDualTokens( + Address.Zero().bech32(), + Address.Zero().bech32(), + [ + new EsdtTokenPayment({ + tokenIdentifier: 'WEGLD-123456', + tokenNonce: 0, + amount: '100000000000000000000', + }), + new EsdtTokenPayment({ + tokenIdentifier: 'MEX-123456', + tokenNonce: 0, + amount: '100000000000000000000', + }), + ], + 0.01, + ); + + expect(transaction).toEqual({ + nonce: 0, + value: '0', + receiver: Address.Zero().bech32(), + sender: Address.Zero().bech32(), + senderUsername: undefined, + receiverUsername: undefined, + gasPrice: 1000000000, + gasLimit: 50000000, + data: encodeTransactionData( + 'MultiESDTNFTTransfer@00000000000000000500bc458e2cd68bb69665812137dcdd988d9f69901e7ceb@02@WEGLD-123456@@100000000000000000000@MEX-123456@@100000000000000000000@createMetastakingPosFromTwoTokens@0000000000000000000000000000000000000000000000000000000000000000@99000000000000000000@99000000000000000000', + ), + chainID: 'T', + version: 1, + options: undefined, + guardian: undefined, + signature: undefined, + guardianSignature: undefined, + }); + }); + + it('should return transaction no merge farm tokens', async () => { + const service = module.get( + PositionCreatorTransactionService, + ); + const stakingProxyAbi = module.get( + StakingProxyAbiService, + ); + jest.spyOn(stakingProxyAbi, 'pairAddress').mockResolvedValue( + Address.fromHex( + '0000000000000000000000000000000000000000000000000000000000000012', + ).bech32(), + ); + + const transaction = await service.createDualFarmPositionDualTokens( + Address.Zero().bech32(), + Address.Zero().bech32(), + [ + new EsdtTokenPayment({ + tokenIdentifier: 'WEGLD-123456', + tokenNonce: 0, + amount: '100000000000000000000', + }), + new EsdtTokenPayment({ + tokenIdentifier: 'MEX-123456', + tokenNonce: 0, + amount: '100000000000000000000', + }), + new EsdtTokenPayment({ + tokenIdentifier: 'METASTAKE-1234', + tokenNonce: 1, + amount: '100000000000000000000', + }), + ], + 0.01, + ); + + expect(transaction).toEqual({ + nonce: 0, + value: '0', + receiver: Address.Zero().bech32(), + sender: Address.Zero().bech32(), + senderUsername: undefined, + receiverUsername: undefined, + gasPrice: 1000000000, + gasLimit: 50000000, + data: encodeTransactionData( + 'MultiESDTNFTTransfer@00000000000000000500bc458e2cd68bb69665812137dcdd988d9f69901e7ceb@03@WEGLD-123456@@100000000000000000000@MEX-123456@@100000000000000000000@METASTAKE-1234@01@100000000000000000000@createMetastakingPosFromTwoTokens@0000000000000000000000000000000000000000000000000000000000000000@99000000000000000000@99000000000000000000', + ), + chainID: 'T', + version: 1, + options: undefined, + guardian: undefined, + signature: undefined, + guardianSignature: undefined, + }); + }); + }); + + describe('Exit farm position dual tokens', () => { + it('should return error on invalid farm token', async () => { + const service = module.get( + PositionCreatorTransactionService, + ); + expect( + service.exitFarmPositionDualTokens( + Address.Zero().bech32(), + Address.fromHex( + '0000000000000000000000000000000000000000000000000000000000000021', + ).bech32(), + new EsdtTokenPayment({ + tokenIdentifier: 'MEX-abcdef', + tokenNonce: 0, + amount: '100000000000000000000', + }), + 0.01, + ), + ).rejects.toThrowError('Invalid farm token payment'); + }); + + it('should return transaction', async () => { + const service = module.get( + PositionCreatorTransactionService, + ); + const transaction = await service.exitFarmPositionDualTokens( + Address.Zero().bech32(), + Address.fromHex( + '0000000000000000000000000000000000000000000000000000000000000021', + ).bech32(), + new EsdtTokenPayment({ + tokenIdentifier: 'EGLDMEXFL-abcdef', + tokenNonce: 1, + amount: '100000000000000000000', + }), + 0.01, + ); + + expect(transaction).toEqual({ + nonce: 0, + value: '0', + receiver: Address.Zero().bech32(), + sender: Address.Zero().bech32(), + senderUsername: undefined, + receiverUsername: undefined, + gasPrice: 1000000000, + gasLimit: 50000000, + data: encodeTransactionData( + 'ESDTNFTTransfer@EGLDMEXFL-abcdef@01@100000000000000000000@00000000000000000500bc458e2cd68bb69665812137dcdd988d9f69901e7ceb@exitFarmPos@0000000000000000000000000000000000000000000000000000000000000021@99000000000000000000@99000000000000000000000', + ), + chainID: 'T', + version: 1, + options: undefined, + guardian: undefined, + signature: undefined, + guardianSignature: undefined, + }); + }); + }); +}); diff --git a/src/modules/router/mocks/router.abi.service.mock.ts b/src/modules/router/mocks/router.abi.service.mock.ts index 4bcacaf8e..d3b9afcb7 100644 --- a/src/modules/router/mocks/router.abi.service.mock.ts +++ b/src/modules/router/mocks/router.abi.service.mock.ts @@ -52,7 +52,7 @@ export class RouterAbiServiceMock implements IRouterAbiService { }); } async commonTokensForUserPairs(): Promise { - return ['USDC-123456']; + return ['USDC-123456', 'WEGLD-123456']; } } diff --git a/src/modules/router/specs/router.transactions.service.spec.ts b/src/modules/router/specs/router.transactions.service.spec.ts index 23a10f8bc..2bd4a242a 100644 --- a/src/modules/router/specs/router.transactions.service.spec.ts +++ b/src/modules/router/specs/router.transactions.service.spec.ts @@ -655,7 +655,7 @@ describe('RouterService', () => { nonce: 1, amount: '1000000000000000000', attributes: - 'AAAAEEVHTERNRVhMUC1hYmNkZWYAAAAAAAAAAAAAAAAAAAAB', + 'AAAAEVRPSzVUT0s2TFAtYWJjZGVmAAAAAAAAAAAAAAAAAAAAAQ==', }), ), ).rejects.toThrow('Not a valid user defined pair'); @@ -678,7 +678,7 @@ describe('RouterService', () => { nonce: 1, amount: '1000', attributes: - 'AAAAEUVHTERVU0RDTFAtYWJjZGVmAAAAAAAAAAAAAAAAAAAAAg==', + 'AAAAEVRPSzVVU0RDTFAtYWJjZGVmAAAAAAAAAAAAAAAAAAAAAg==', }), ), ).rejects.toThrow('Not enough value locked'); diff --git a/src/public.app.module.ts b/src/public.app.module.ts index 45af2ce73..b1a056181 100644 --- a/src/public.app.module.ts +++ b/src/public.app.module.ts @@ -36,6 +36,7 @@ import { EscrowModule } from './modules/escrow/escrow.module'; import { GovernanceModule } from './modules/governance/governance.module'; import { DynamicModuleUtils } from './utils/dynamic.module.utils'; import '@multiversx/sdk-nestjs-common/lib/utils/extensions/array.extensions'; +import { PositionCreatorModule } from './modules/position-creator/position.creator.module'; @Module({ imports: [ @@ -95,6 +96,7 @@ import '@multiversx/sdk-nestjs-common/lib/utils/extensions/array.extensions'; LockedTokenWrapperModule, EscrowModule, GovernanceModule, + PositionCreatorModule, DynamicModuleUtils.getCacheModule(), ], }) diff --git a/src/services/multiversx-communication/mx.proxy.service.ts b/src/services/multiversx-communication/mx.proxy.service.ts index 16487fb11..106627498 100644 --- a/src/services/multiversx-communication/mx.proxy.service.ts +++ b/src/services/multiversx-communication/mx.proxy.service.ts @@ -223,6 +223,14 @@ export class MXProxyService { ); } + async getPostitionCreatorContract(): Promise { + return this.getSmartContract( + scAddress.positionCreator, + abiConfig.positionCreator, + 'AutoPosCreator', + ); + } + async getSmartContract( contractAddress: string, contractAbiPath: string,