diff --git a/.env.example b/.env.example index 4f4202e873..d0cc361a06 100644 --- a/.env.example +++ b/.env.example @@ -36,9 +36,13 @@ ETH_WALLET_ADDRESS= ETH_WALLET_PRIVATE_KEY= ETH_GATEWAY_URL= ETH_API_KEY= +ETH_SWAP_CONTRACT_ADDRESS= +ETH_SWAP_TOKEN_ADDRESS= BSC_WALLET_ADDRESS= BSC_WALLET_PRIVATE_KEY= BSC_GATEWAY_URL= +BSC_SWAP_CONTRACT_ADDRESS= +BSC_SWAP_TOKEN_ADDRESS= LETTER_USER= LETTER_AUTH= LETTER_URL= diff --git a/infrastructure/bicep/dfx-api.bicep b/infrastructure/bicep/dfx-api.bicep index 8d2d85ea0d..22882dda66 100644 --- a/infrastructure/bicep/dfx-api.bicep +++ b/infrastructure/bicep/dfx-api.bicep @@ -45,11 +45,15 @@ param ethWalletPrivateKey string param ethGatewayUrl string @secure() param ethApiKey string +param ethSwapContractAddress string +param ethSwapTokenAddress string param bscWalletAddress string @secure() param bscWalletPrivateKey string param bscGatewayUrl string +param bscSwapContractAddress string +param bscSwapTokenAddress string param nodeServicePlanSkuName string param nodeServicePlanSkuTier string @@ -113,7 +117,6 @@ param paymentUrl string @secure() param lockApiKey string - // --- VARIABLES --- // var compName = 'dfx' var apiName = 'api' @@ -134,7 +137,6 @@ var apiServicePlanName = 'plan-${compName}-${apiName}-${env}' var apiAppName = 'app-${compName}-${apiName}-${env}' var appInsightsName = 'appi-${compName}-${apiName}-${env}' - var nodeProps = [ { name: 'nodes-input-${env}' @@ -246,7 +248,6 @@ resource virtualNet 'Microsoft.Network/virtualNetworks@2020-11-01' = { } } - // Storage Account resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' = { name: storageAccountName @@ -267,7 +268,6 @@ resource dbBackupContainer 'Microsoft.Storage/storageAccounts/blobServices/conta name: '${storageAccount.name}/default/${dbBackupContainerName}' } - // SQL Database resource sqlServer 'Microsoft.Sql/servers@2021-02-01-preview' = { name: sqlServerName @@ -279,11 +279,11 @@ resource sqlServer 'Microsoft.Sql/servers@2021-02-01-preview' = { } resource sqlVNetRule 'Microsoft.Sql/servers/virtualNetworkRules@2021-02-01-preview' = { - parent: sqlServer - name: 'apiVNetRule' - properties: { - virtualNetworkSubnetId: virtualNet.properties.subnets[0].id - } + parent: sqlServer + name: 'apiVNetRule' + properties: { + virtualNetworkSubnetId: virtualNet.properties.subnets[0].id + } } resource sqlAllRule 'Microsoft.Sql/servers/firewallRules@2021-02-01-preview' = if (dbAllowAllIps) { @@ -326,14 +326,13 @@ resource sqlDbLtrPolicy 'Microsoft.Sql/servers/databases/backupLongTermRetention } } - // API App Service resource appServicePlan 'Microsoft.Web/serverfarms@2018-02-01' = { name: apiServicePlanName location: location kind: 'linux' properties: { - reserved: true + reserved: true } sku: { name: 'P1v2' @@ -350,7 +349,7 @@ resource apiAppService 'Microsoft.Web/sites@2018-11-01' = { serverFarmId: appServicePlan.id httpsOnly: true virtualNetworkSubnetId: virtualNet.properties.subnets[0].id - + siteConfig: { alwaysOn: true linuxFxVersion: 'NODE|14-lts' @@ -359,7 +358,7 @@ resource apiAppService 'Microsoft.Web/sites@2018-11-01' = { logsDirectorySizeLimit: 100 vnetRouteAllEnabled: true scmIpSecurityRestrictionsUseMain: true - + appSettings: [ { name: 'APPINSIGHTS_INSTRUMENTATIONKEY' @@ -534,6 +533,14 @@ resource apiAppService 'Microsoft.Web/sites@2018-11-01' = { name: 'ETH_API_KEY' value: ethApiKey } + { + name: 'ETH_SWAP_CONTRACT_ADDRESS' + value: ethSwapContractAddress + } + { + name: 'ETH_SWAP_TOKEN_ADDRESS' + value: ethSwapTokenAddress + } { name: 'BSC_WALLET_ADDRESS' value: bscWalletAddress @@ -546,6 +553,14 @@ resource apiAppService 'Microsoft.Web/sites@2018-11-01' = { name: 'BSC_GATEWAY_URL' value: bscGatewayUrl } + { + name: 'BSC_SWAP_CONTRACT_ADDRESS' + value: bscSwapContractAddress + } + { + name: 'BSC_SWAP_TOKEN_ADDRESS' + value: bscSwapTokenAddress + } { name: 'BTC_COLLECTOR_ADDRESS' value: btcCollectorAddress @@ -679,7 +694,6 @@ resource appInsights 'microsoft.insights/components@2020-02-02-preview' = { } } - // DeFi Nodes module nodes 'defi-node.bicep' = [for node in nodeProps: { name: node.name @@ -699,7 +713,6 @@ module nodes 'defi-node.bicep' = [for node in nodeProps: { } }] - // BTC Node resource vmNsg 'Microsoft.Network/networkSecurityGroups@2020-11-01' = { name: vmNsgName diff --git a/infrastructure/bicep/parameters/dev.json b/infrastructure/bicep/parameters/dev.json index a4c441ab86..a60e92d50f 100644 --- a/infrastructure/bicep/parameters/dev.json +++ b/infrastructure/bicep/parameters/dev.json @@ -86,6 +86,12 @@ "ethApiKey": { "value": "xxx" }, + "ethSwapContractAddress": { + "value": "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D" + }, + "ethSwapTokenAddress": { + "value": "0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6" + }, "bscWalletAddress": { "value": "xxx" }, @@ -95,6 +101,12 @@ "bscGatewayUrl": { "value": "https://data-seed-prebsc-1-s1.binance.org:8545" }, + "bscSwapContractAddress": { + "value": "0xD99D1c33F9fC3444f8101754aBC46c52416550D1" + }, + "bscSwapTokenAddress": { + "value": "0xae13d989daC2f0dEbFf460aC112a837C89BAa7cd" + }, "btcCollectorAddress": { "value": "xxx" }, diff --git a/infrastructure/bicep/parameters/loc.json b/infrastructure/bicep/parameters/loc.json index 07e6d1edc6..f2f99b3a3a 100644 --- a/infrastructure/bicep/parameters/loc.json +++ b/infrastructure/bicep/parameters/loc.json @@ -86,6 +86,12 @@ "ethApiKey": { "value": "xxx" }, + "ethSwapContractAddress": { + "value": "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D" + }, + "ethSwapTokenAddress": { + "value": "0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6" + }, "bscWalletAddress": { "value": "xxx" }, @@ -95,6 +101,12 @@ "bscGatewayUrl": { "value": "https://data-seed-prebsc-1-s1.binance.org:8545" }, + "bscSwapContractAddress": { + "value": "0xD99D1c33F9fC3444f8101754aBC46c52416550D1" + }, + "bscSwapTokenAddress": { + "value": "0xae13d989daC2f0dEbFf460aC112a837C89BAa7cd" + }, "btcCollectorAddress": { "value": "xxx" }, diff --git a/infrastructure/bicep/parameters/prd.json b/infrastructure/bicep/parameters/prd.json index be80740fbf..d7772428b4 100644 --- a/infrastructure/bicep/parameters/prd.json +++ b/infrastructure/bicep/parameters/prd.json @@ -86,6 +86,12 @@ "ethApiKey": { "value": "xxx" }, + "ethSwapContractAddress": { + "value": "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D" + }, + "ethSwapTokenAddress": { + "value": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + }, "bscWalletAddress": { "value": "xxx" }, @@ -95,6 +101,12 @@ "bscGatewayUrl": { "value": "https://bsc-dataseed.binance.org" }, + "bscSwapContractAddress": { + "value": "0x10ED43C718714eb63d5aA57B78B54704E256024E" + }, + "bscSwapTokenAddress": { + "value": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c" + }, "btcCollectorAddress": { "value": "xxx" }, diff --git a/migration/1665408086368-assetChainId.js b/migration/1665408086368-assetChainId.js new file mode 100644 index 0000000000..ef50c84715 --- /dev/null +++ b/migration/1665408086368-assetChainId.js @@ -0,0 +1,13 @@ +const { MigrationInterface, QueryRunner } = require("typeorm"); + +module.exports = class assetChainId1665408086368 { + name = 'assetChainId1665408086368' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "asset" ALTER COLUMN "chainId" nvarchar(255)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "asset" ALTER COLUMN "chainId" int`); + } +} diff --git a/src/blockchain/bsc/bsc-client.ts b/src/blockchain/bsc/bsc-client.ts index 2185a8b910..98d4559b88 100644 --- a/src/blockchain/bsc/bsc-client.ts +++ b/src/blockchain/bsc/bsc-client.ts @@ -1,7 +1,13 @@ import { EvmClient } from '../shared/evm/evm-client'; export class BscClient extends EvmClient { - constructor(gatewayUrl: string, privateKey: string, address: string) { - super(gatewayUrl, privateKey, address); + constructor( + gatewayUrl: string, + privateKey: string, + dfxAddress: string, + swapContractAddress: string, + swapTokenAddress: string, + ) { + super(gatewayUrl, privateKey, dfxAddress, swapContractAddress, swapTokenAddress); } } diff --git a/src/blockchain/bsc/bsc.service.ts b/src/blockchain/bsc/bsc.service.ts index 9504d9e917..3a282c9f37 100644 --- a/src/blockchain/bsc/bsc.service.ts +++ b/src/blockchain/bsc/bsc.service.ts @@ -6,8 +6,9 @@ import { EvmService } from '../shared/evm/evm.service'; @Injectable() export class BscService extends EvmService { constructor() { - const { bscGatewayUrl, bscWalletAddress, bscWalletPrivateKey } = GetConfig().blockchain.bsc; + const { bscGatewayUrl, bscWalletAddress, bscWalletPrivateKey, pancakeRouterAddress, swapTokenAddress } = + GetConfig().blockchain.bsc; - super(bscGatewayUrl, '', bscWalletAddress, bscWalletPrivateKey, BscClient); + super(bscGatewayUrl, '', bscWalletAddress, bscWalletPrivateKey, pancakeRouterAddress, swapTokenAddress, BscClient); } } diff --git a/src/blockchain/ethereum/ethereum-client.ts b/src/blockchain/ethereum/ethereum-client.ts index d84968ab29..4bcf9228e0 100644 --- a/src/blockchain/ethereum/ethereum-client.ts +++ b/src/blockchain/ethereum/ethereum-client.ts @@ -1,7 +1,13 @@ import { EvmClient } from '../shared/evm/evm-client'; export class EthereumClient extends EvmClient { - constructor(gatewayUrl: string, privateKey: string, address: string) { - super(gatewayUrl, privateKey, address); + constructor( + gatewayUrl: string, + privateKey: string, + dfxAddress: string, + swapContractAddress: string, + swapTokenAddress: string, + ) { + super(gatewayUrl, privateKey, dfxAddress, swapContractAddress, swapTokenAddress); } } diff --git a/src/blockchain/ethereum/ethereum.service.ts b/src/blockchain/ethereum/ethereum.service.ts index a2a48cc366..45b0669840 100644 --- a/src/blockchain/ethereum/ethereum.service.ts +++ b/src/blockchain/ethereum/ethereum.service.ts @@ -6,8 +6,23 @@ import { EvmService } from '../shared/evm/evm.service'; @Injectable() export class EthereumService extends EvmService { constructor() { - const { ethGatewayUrl, ethApiKey, ethWalletAddress, ethWalletPrivateKey } = GetConfig().blockchain.ethereum; + const { + ethGatewayUrl, + ethApiKey, + ethWalletAddress, + ethWalletPrivateKey, + uniswapV2Router02Address, + swapTokenAddress, + } = GetConfig().blockchain.ethereum; - super(ethGatewayUrl, ethApiKey, ethWalletAddress, ethWalletPrivateKey, EthereumClient); + super( + ethGatewayUrl, + ethApiKey, + ethWalletAddress, + ethWalletPrivateKey, + uniswapV2Router02Address, + swapTokenAddress, + EthereumClient, + ); } } diff --git a/src/blockchain/shared/evm/abi/erc20.abi.json b/src/blockchain/shared/evm/abi/erc20.abi.json new file mode 100644 index 0000000000..06b572ddc2 --- /dev/null +++ b/src/blockchain/shared/evm/abi/erc20.abi.json @@ -0,0 +1,222 @@ +[ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_spender", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_from", + "type": "address" + }, + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "name": "", + "type": "uint8" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "name": "balance", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + }, + { + "name": "_spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "payable": true, + "stateMutability": "payable", + "type": "fallback" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "from", + "type": "address" + }, + { + "indexed": true, + "name": "to", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + } +] diff --git a/src/blockchain/shared/evm/abi/uniswap-router02.abi.json b/src/blockchain/shared/evm/abi/uniswap-router02.abi.json new file mode 100644 index 0000000000..55ca7a7317 --- /dev/null +++ b/src/blockchain/shared/evm/abi/uniswap-router02.abi.json @@ -0,0 +1,953 @@ +[ + { + "inputs": [], + "name": "WETH", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenA", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenB", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountADesired", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountBDesired", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountAMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountBMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "addLiquidity", + "outputs": [ + { + "internalType": "uint256", + "name": "amountA", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountB", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountTokenDesired", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountTokenMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETHMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "addLiquidityETH", + "outputs": [ + { + "internalType": "uint256", + "name": "amountToken", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETH", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "factory", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveOut", + "type": "uint256" + } + ], + "name": "getAmountIn", + "outputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveOut", + "type": "uint256" + } + ], + "name": "getAmountOut", + "outputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + } + ], + "name": "getAmountsIn", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + } + ], + "name": "getAmountsOut", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountA", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveA", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveB", + "type": "uint256" + } + ], + "name": "quote", + "outputs": [ + { + "internalType": "uint256", + "name": "amountB", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenA", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenB", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountAMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountBMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "removeLiquidity", + "outputs": [ + { + "internalType": "uint256", + "name": "amountA", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountB", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountTokenMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETHMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "removeLiquidityETH", + "outputs": [ + { + "internalType": "uint256", + "name": "amountToken", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETH", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountTokenMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETHMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "removeLiquidityETHSupportingFeeOnTransferTokens", + "outputs": [ + { + "internalType": "uint256", + "name": "amountETH", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountTokenMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETHMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "approveMax", + "type": "bool" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "removeLiquidityETHWithPermit", + "outputs": [ + { + "internalType": "uint256", + "name": "amountToken", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETH", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountTokenMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETHMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "approveMax", + "type": "bool" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "removeLiquidityETHWithPermitSupportingFeeOnTransferTokens", + "outputs": [ + { + "internalType": "uint256", + "name": "amountETH", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenA", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenB", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountAMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountBMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "approveMax", + "type": "bool" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "removeLiquidityWithPermit", + "outputs": [ + { + "internalType": "uint256", + "name": "amountA", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountB", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapETHForExactTokens", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactETHForTokens", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactETHForTokensSupportingFeeOnTransferTokens", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactTokensForETH", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactTokensForETHSupportingFeeOnTransferTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactTokensForTokens", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactTokensForTokensSupportingFeeOnTransferTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountInMax", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapTokensForExactETH", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountInMax", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapTokensForExactTokens", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/src/blockchain/shared/evm/evm-client.ts b/src/blockchain/shared/evm/evm-client.ts index 69849a8f5b..9a40765a14 100644 --- a/src/blockchain/shared/evm/evm-client.ts +++ b/src/blockchain/shared/evm/evm-client.ts @@ -1,29 +1,51 @@ -import { ethers } from 'ethers'; +import { BigNumber, Contract, ethers } from 'ethers'; +import { Asset } from 'src/shared/models/asset/asset.entity'; +import * as ERC20_ABI from './abi/erc20.abi.json'; +import * as UNISWAP_ROUTER_02_ABI from './abi/uniswap-router02.abi.json'; export class EvmClient { - #address: string; + #dfxAddress: string; #provider: ethers.providers.JsonRpcProvider; #wallet: ethers.Wallet; + #router: Contract; + #erc20Tokens: Map = new Map(); + #swapTokenAddress: string; - constructor(gatewayUrl: string, privateKey: string, address: string) { + constructor( + gatewayUrl: string, + privateKey: string, + dfxAddress: string, + swapContractAddress: string, + swapTokenAddress: string, + ) { this.#provider = new ethers.providers.JsonRpcProvider(gatewayUrl); this.#wallet = new ethers.Wallet(privateKey, this.#provider); - this.#address = address; + this.#dfxAddress = dfxAddress; + this.#swapTokenAddress = swapTokenAddress; + this.#router = new ethers.Contract(swapContractAddress, UNISWAP_ROUTER_02_ABI, this.#wallet); } - async getBalance(): Promise { - const balance = await this.#provider.getBalance(this.#address); + async getNativeCoinBalance(): Promise { + const balance = await this.#provider.getBalance(this.#dfxAddress); - return parseFloat(ethers.utils.formatEther(balance)); + return this.convertToEthLikeDenomination(balance); } - async send(address: string, amount: number): Promise { + async getTokenBalance(token: Asset): Promise { + const contract = this.getERC20Contract(token.chainId); + const balance = await contract.balanceOf(this.#dfxAddress); + const decimals = await contract.decimals(); + + return this.convertToEthLikeDenomination(balance, decimals); + } + + async sendNativeCoin(address: string, amount: number): Promise { const gasPrice = await this.#provider.getGasPrice(); const tx = await this.#wallet.sendTransaction({ - from: this.#address, + from: this.#dfxAddress, to: address, - value: ethers.utils.parseUnits(`${amount}`, 'ether'), + value: this.convertToWeiLikeDenomination(amount, 'ether'), gasPrice, // has to be provided as a number for BSC gasLimit: 21000, @@ -32,6 +54,16 @@ export class EvmClient { return tx.hash; } + async sendToken(address: string, token: Asset, amount: number): Promise { + const contract = this.getERC20Contract(token.chainId); + const decimals = await contract.decimals(); + const targetAmount = this.convertToWeiLikeDenomination(amount, decimals); + + const tx = await contract.transfer(address, targetAmount); + + return tx.hash; + } + async isTxComplete(txHash: string): Promise { const transaction = await this.getTx(txHash); @@ -41,4 +73,36 @@ export class EvmClient { async getTx(txHash: string): Promise { return this.#provider.getTransaction(txHash); } + + async nativeCryptoTestSwap(nativeCryptoAmount: number, targetToken: Asset): Promise { + const contract = new ethers.Contract(targetToken.chainId, ERC20_ABI, this.#wallet); + const inputAmount = this.convertToWeiLikeDenomination(nativeCryptoAmount, 'ether'); + const outputAmounts = await this.#router.getAmountsOut(inputAmount, [this.#swapTokenAddress, targetToken.chainId]); + const decimals = await contract.decimals(); + + return this.convertToEthLikeDenomination(outputAmounts[1], decimals); + } + + //*** HELPER METHODS ***// + + private getERC20Contract(tokenAddress: string): Contract { + let tokenContract = this.#erc20Tokens.get(tokenAddress); + + if (!tokenContract) { + tokenContract = new ethers.Contract(tokenAddress, ERC20_ABI, this.#wallet); + this.#erc20Tokens.set(tokenAddress, tokenContract); + } + + return tokenContract; + } + + private convertToWeiLikeDenomination(amountEthLike: number, decimals: number | 'ether'): BigNumber { + return ethers.utils.parseUnits(`${amountEthLike}`, decimals); + } + + private convertToEthLikeDenomination(amountWeiLike: BigNumber, decimals?: number): number { + return decimals + ? parseFloat(ethers.utils.formatUnits(amountWeiLike, decimals)) + : parseFloat(ethers.utils.formatEther(amountWeiLike)); + } } diff --git a/src/blockchain/shared/evm/evm.service.ts b/src/blockchain/shared/evm/evm.service.ts index 40b9db6c93..ce2fb20d70 100644 --- a/src/blockchain/shared/evm/evm.service.ts +++ b/src/blockchain/shared/evm/evm.service.ts @@ -8,9 +8,25 @@ export abstract class EvmService { apiKey: string, walletAddress: string, walletPrivateKey: string, - client: { new (gatewayUrl: string, privateKey: string, address: string): EvmClient }, + swapContractAddress: string, + swapTokenAddress: string, + client: { + new ( + gatewayUrl: string, + privateKey: string, + dfxAddress: string, + swapContractAddress: string, + swapTokenAddress: string, + ): EvmClient; + }, ) { - this.client = new client(`${gatewayUrl}/${apiKey}`, walletPrivateKey, walletAddress); + this.client = new client( + `${gatewayUrl}/${apiKey}`, + walletPrivateKey, + walletAddress, + swapContractAddress, + swapTokenAddress, + ); } getDefaultClient(): T { diff --git a/src/config/config.ts b/src/config/config.ts index 0d92ecaabf..14fe4e48ac 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -226,11 +226,15 @@ export class Configuration { ethWalletPrivateKey: process.env.ETH_WALLET_PRIVATE_KEY, ethGatewayUrl: process.env.ETH_GATEWAY_URL, ethApiKey: process.env.ETH_API_KEY, + uniswapV2Router02Address: process.env.ETH_SWAP_CONTRACT_ADDRESS, + swapTokenAddress: process.env.ETH_SWAP_TOKEN_ADDRESS, }, bsc: { bscWalletAddress: process.env.BSC_WALLET_ADDRESS, bscWalletPrivateKey: process.env.BSC_WALLET_PRIVATE_KEY, bscGatewayUrl: process.env.BSC_GATEWAY_URL, + pancakeRouterAddress: process.env.BSC_SWAP_CONTRACT_ADDRESS, + swapTokenAddress: process.env.BSC_SWAP_TOKEN_ADDRESS, }, }; diff --git a/src/payment/models/buy-crypto/entities/__tests__/buy-crypto.entity.spec.ts b/src/payment/models/buy-crypto/entities/__tests__/buy-crypto.entity.spec.ts index f1a0feb938..ab1f22d1c0 100644 --- a/src/payment/models/buy-crypto/entities/__tests__/buy-crypto.entity.spec.ts +++ b/src/payment/models/buy-crypto/entities/__tests__/buy-crypto.entity.spec.ts @@ -179,10 +179,10 @@ describe('BuyCrypto', () => { expect(entity.outputReferenceAsset).toBe('USDT'); }); - it('assigns outputReferenceAsset to ETH, on Ethereum blockchain', () => { + it('assigns outputReferenceAsset to ETH, on Ethereum blockchain when outputAsset is not DFI', () => { const entity = createCustomBuyCrypto({ outputReferenceAsset: undefined, - buy: createCustomBuy({ asset: createCustomAsset({ blockchain: Blockchain.ETHEREUM }) }), + buy: createCustomBuy({ asset: createCustomAsset({ blockchain: Blockchain.ETHEREUM, dexName: 'GOOGL' }) }), }); expect(entity.outputReferenceAsset).toBeUndefined(); @@ -192,10 +192,25 @@ describe('BuyCrypto', () => { expect(entity.outputReferenceAsset).toBe('ETH'); }); - it('assigns outputReferenceAsset to BNB, on BSC blockchain', () => { + it('assigns outputReferenceAsset to outputAsset, on Ethereum blockchain when outputAsset is DFI', () => { const entity = createCustomBuyCrypto({ outputReferenceAsset: undefined, - buy: createCustomBuy({ asset: createCustomAsset({ blockchain: Blockchain.BINANCE_SMART_CHAIN }) }), + buy: createCustomBuy({ asset: createCustomAsset({ blockchain: Blockchain.ETHEREUM, dexName: 'DFI' }) }), + }); + + expect(entity.outputReferenceAsset).toBeUndefined(); + + entity.defineAssetExchangePair(); + + expect(entity.outputReferenceAsset).toBe('DFI'); + }); + + it('assigns outputReferenceAsset to BNB, on BSC blockchain when outputAsset is not DFI | BUSD', () => { + const entity = createCustomBuyCrypto({ + outputReferenceAsset: undefined, + buy: createCustomBuy({ + asset: createCustomAsset({ blockchain: Blockchain.BINANCE_SMART_CHAIN, dexName: 'GOOGL' }), + }), }); expect(entity.outputReferenceAsset).toBeUndefined(); @@ -205,6 +220,36 @@ describe('BuyCrypto', () => { expect(entity.outputReferenceAsset).toBe('BNB'); }); + it('assigns outputReferenceAsset to outputAsset, on BSC blockchain when outputAsset is DFI', () => { + const entity = createCustomBuyCrypto({ + outputReferenceAsset: undefined, + buy: createCustomBuy({ + asset: createCustomAsset({ blockchain: Blockchain.BINANCE_SMART_CHAIN, dexName: 'DFI' }), + }), + }); + + expect(entity.outputReferenceAsset).toBeUndefined(); + + entity.defineAssetExchangePair(); + + expect(entity.outputReferenceAsset).toBe('DFI'); + }); + + it('assigns outputReferenceAsset to outputAsset, on BSC blockchain when outputAsset is BUSD', () => { + const entity = createCustomBuyCrypto({ + outputReferenceAsset: undefined, + buy: createCustomBuy({ + asset: createCustomAsset({ blockchain: Blockchain.BINANCE_SMART_CHAIN, dexName: 'BUSD' }), + }), + }); + + expect(entity.outputReferenceAsset).toBeUndefined(); + + entity.defineAssetExchangePair(); + + expect(entity.outputReferenceAsset).toBe('BUSD'); + }); + it('defaults outputReferenceAsset to BTC on Bitcoin blockchain', () => { const entity = createCustomBuyCrypto({ outputReferenceAsset: undefined, diff --git a/src/payment/models/buy-crypto/entities/buy-crypto.entity.ts b/src/payment/models/buy-crypto/entities/buy-crypto.entity.ts index a29feb3489..b09806def5 100644 --- a/src/payment/models/buy-crypto/entities/buy-crypto.entity.ts +++ b/src/payment/models/buy-crypto/entities/buy-crypto.entity.ts @@ -134,10 +134,20 @@ export class BuyCrypto extends IEntity { switch (this.target.asset.blockchain) { case Blockchain.ETHEREUM: + if (this.outputAsset === 'DFI') { + this.outputReferenceAsset = this.outputAsset; + break; + } + this.outputReferenceAsset = 'ETH'; break; case Blockchain.BINANCE_SMART_CHAIN: + if (['DFI', 'BUSD'].includes(this.outputAsset)) { + this.outputReferenceAsset = this.outputAsset; + break; + } + this.outputReferenceAsset = 'BNB'; break; diff --git a/src/payment/models/dex/dex.module.ts b/src/payment/models/dex/dex.module.ts index 20ff6ab841..80713e0397 100644 --- a/src/payment/models/dex/dex.module.ts +++ b/src/payment/models/dex/dex.module.ts @@ -2,44 +2,60 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AinModule } from 'src/blockchain/ain/ain.module'; import { EthereumModule } from 'src/blockchain/ethereum/ethereum.module'; +import { BscModule } from 'src/blockchain/bsc/bsc.module'; import { SharedModule } from 'src/shared/shared.module'; import { LiquidityOrderFactory } from './factories/liquidity-order.factory'; import { LiquidityOrderRepository } from './repositories/liquidity-order.repository'; import { DexEthereumService } from './services/dex-ethereum.service'; import { DexService } from './services/dex.service'; import { DexDeFiChainService } from './services/dex-defichain.service'; -import { CheckLiquidityDeFiChainDefaultStrategy } from './strategies/check-liquidity/check-liquidity-defichain-default.strategy'; -import { CheckLiquidityDeFiChainPoolPairStrategy } from './strategies/check-liquidity/check-liquidity-defichain-poolpair.strategy'; -import { PurchaseLiquidityDeFiChainCryptoStrategy } from './strategies/purchase-liquidity/purchase-liquidity-defichain-crypto.strategy'; -import { PurchaseLiquidityDeFiChainPoolPairStrategy } from './strategies/purchase-liquidity/purchase-liquidity-defichain-poolpair.strategy'; -import { PurchaseLiquidityDeFiChainStockStrategy } from './strategies/purchase-liquidity/purchase-liquidity-defichain-stock.strategy'; -import { CheckLiquidityEthereumStrategy } from './strategies/check-liquidity/check-liquidity-ethereum.strategy'; -import { DexStrategiesFacade } from './strategies/strategies.facade'; -import { PurchaseLiquidityEthereumStrategy } from './strategies/purchase-liquidity/purchase-liquidity-ethereum.strategy'; -import { BscModule } from 'src/blockchain/bsc/bsc.module'; import { DexBscService } from './services/dex-bsc.service'; -import { PurchaseLiquidityBscStrategy } from './strategies/purchase-liquidity/purchase-liquidity-bsc.strategy'; -import { CheckLiquidityBscStrategy } from './strategies/check-liquidity/check-liquidity-bsc.strategy'; +import { DexBitcoinService } from './services/dex-bitcoin.service'; +import { CheckLiquidityStrategies } from './strategies/check-liquidity/check-liquidity.facade'; +import { PurchaseLiquidityStrategies } from './strategies/purchase-liquidity/purchase-liquidity.facade'; +import { DeFiChainDefaultStrategy as DeFiChainDefaultStrategyCL } from './strategies/check-liquidity/impl/defichain-default.strategy'; +import { DeFiChainPoolPairStrategy as DeFiChainPoolPairStrategyCL } from './strategies/check-liquidity/impl/defichain-poolpair.strategy'; +import { EthereumCoinStrategy as EthereumCryptoStrategyCL } from './strategies/check-liquidity/impl/ethereum-coin.strategy'; +import { BscCoinStrategy as BscCryptoStrategyCL } from './strategies/check-liquidity/impl/bsc-coin.strategy'; +import { BitcoinStrategy as BitcoinStrategyCL } from './strategies/check-liquidity/impl/bitcoin.strategy'; +import { BscTokenStrategy as BscTokenStrategyCL } from './strategies/check-liquidity/impl/bsc-token.strategy'; +import { EthereumTokenStrategy as EthereumTokenStrategyCL } from './strategies/check-liquidity/impl/ethereum-token.strategy'; +import { DeFiChainCryptoStrategy as DeFiChainCryptoStrategyPL } from './strategies/purchase-liquidity/impl/defichain-crypto.strategy'; +import { DeFiChainPoolPairStrategy as DeFiChainPoolPairStrategyPL } from './strategies/purchase-liquidity/impl/defichain-poolpair.strategy'; +import { DeFiChainStockStrategy as DeFiChainStockStrategyPL } from './strategies/purchase-liquidity/impl/defichain-stock.strategy'; +import { EthereumCoinStrategy as EthereumCryptoStrategyPL } from './strategies/purchase-liquidity/impl/ethereum-coin.strategy'; +import { BscCoinStrategy as BscCryptoStrategyPL } from './strategies/purchase-liquidity/impl/bsc-coin.strategy'; +import { BitcoinStrategy as BitcoinStrategyPL } from './strategies/purchase-liquidity/impl/bitcoin.strategy'; +import { BscTokenStrategy as BscTokenStrategyPL } from './strategies/purchase-liquidity/impl/bsc-token.strategy'; +import { EthereumTokenStrategy as EthereumTokenStrategyPL } from './strategies/purchase-liquidity/impl/ethereum-token.strategy'; @Module({ imports: [TypeOrmModule.forFeature([LiquidityOrderRepository]), AinModule, EthereumModule, BscModule, SharedModule], controllers: [], providers: [ + DexService, LiquidityOrderFactory, DexDeFiChainService, DexEthereumService, DexBscService, - DexStrategiesFacade, - DexService, - CheckLiquidityDeFiChainPoolPairStrategy, - CheckLiquidityDeFiChainDefaultStrategy, - CheckLiquidityEthereumStrategy, - CheckLiquidityBscStrategy, - PurchaseLiquidityDeFiChainCryptoStrategy, - PurchaseLiquidityDeFiChainPoolPairStrategy, - PurchaseLiquidityDeFiChainStockStrategy, - PurchaseLiquidityEthereumStrategy, - PurchaseLiquidityBscStrategy, + DexBitcoinService, + CheckLiquidityStrategies, + PurchaseLiquidityStrategies, + DeFiChainDefaultStrategyCL, + DeFiChainPoolPairStrategyCL, + EthereumCryptoStrategyCL, + BscCryptoStrategyCL, + BitcoinStrategyCL, + BscTokenStrategyCL, + EthereumTokenStrategyCL, + DeFiChainCryptoStrategyPL, + DeFiChainPoolPairStrategyPL, + DeFiChainStockStrategyPL, + EthereumCryptoStrategyPL, + BscCryptoStrategyPL, + BitcoinStrategyPL, + BscTokenStrategyPL, + EthereumTokenStrategyPL, ], exports: [DexService], }) diff --git a/src/payment/models/dex/entities/liquidity-order.entity.ts b/src/payment/models/dex/entities/liquidity-order.entity.ts index aa6fd6d324..2c28160215 100644 --- a/src/payment/models/dex/entities/liquidity-order.entity.ts +++ b/src/payment/models/dex/entities/liquidity-order.entity.ts @@ -7,6 +7,7 @@ export enum LiquidityOrderContext { BUY_CRYPTO = 'BuyCrypto', STAKING_REWARD = 'StakingReward', CREATE_POOL_PAIR = 'CreatePoolPair', + PRICING = 'Pricing', } export enum LiquidityOrderType { diff --git a/src/payment/models/dex/interfaces/index.ts b/src/payment/models/dex/interfaces/index.ts index f0567694f5..de3344dec2 100644 --- a/src/payment/models/dex/interfaces/index.ts +++ b/src/payment/models/dex/interfaces/index.ts @@ -7,6 +7,12 @@ export interface LiquidityRequest { referenceAsset: string; referenceAmount: number; targetAsset: Asset; + options?: LiquidityRequestOptions; +} + +export interface LiquidityRequestOptions { + bypassAvailabilityCheck?: boolean; + bypassSlippageProtection?: boolean; } export interface TransferRequest { diff --git a/src/payment/models/dex/services/dex-bitcoin.service.ts b/src/payment/models/dex/services/dex-bitcoin.service.ts new file mode 100644 index 0000000000..9baca9bb02 --- /dev/null +++ b/src/payment/models/dex/services/dex-bitcoin.service.ts @@ -0,0 +1,44 @@ +import { Injectable } from '@nestjs/common'; +import { BtcClient } from 'src/blockchain/ain/node/btc-client'; +import { NodeService, NodeType } from 'src/blockchain/ain/node/node.service'; +import { Blockchain } from 'src/blockchain/shared/enums/blockchain.enum'; +import { Util } from 'src/shared/util'; +import { LiquidityOrder } from '../entities/liquidity-order.entity'; +import { NotEnoughLiquidityException } from '../exceptions/not-enough-liquidity.exception'; +import { LiquidityOrderRepository } from '../repositories/liquidity-order.repository'; + +@Injectable() +export class DexBitcoinService { + #client: BtcClient; + + constructor(private readonly liquidityOrderRepo: LiquidityOrderRepository, readonly nodeService: NodeService) { + nodeService.getConnectedNode(NodeType.BTC_OUTPUT).subscribe((client) => (this.#client = client)); + } + + async checkAvailableTargetLiquidity(amount: number): Promise { + const pendingAmount = await this.getPendingAmount(); + const availableAmount = await this.#client.getBalance(); + + this.checkLiquidity(amount, pendingAmount, +availableAmount); + + return amount; + } + + //*** HELPER METHODS ***// + + private async getPendingAmount(): Promise { + const pendingOrders = (await this.liquidityOrderRepo.find({ isReady: true, isComplete: false })).filter( + (o) => o.targetAsset.dexName === 'BTC' && o.targetAsset.blockchain === Blockchain.BITCOIN, + ); + + return Util.sumObj(pendingOrders, 'targetAmount'); + } + + private checkLiquidity(requiredAmount: number, pendingAmount: number, availableAmount: number): void { + if (requiredAmount > availableAmount - pendingAmount) { + throw new NotEnoughLiquidityException( + `Not enough liquidity of asset BTC. Trying to use ${requiredAmount} BTC worth liquidity. Available amount: ${availableAmount}. Pending amount: ${pendingAmount}`, + ); + } + } +} diff --git a/src/payment/models/dex/services/dex-defichain.service.ts b/src/payment/models/dex/services/dex-defichain.service.ts index 4e00f0f094..c27792e313 100644 --- a/src/payment/models/dex/services/dex-defichain.service.ts +++ b/src/payment/models/dex/services/dex-defichain.service.ts @@ -31,15 +31,17 @@ export class DexDeFiChainService { sourceAmount: number, targetAsset: string, maxSlippage: number, + bypassAvailabilityCheck?: boolean, + bypassSlippageProtection?: boolean, ): Promise { const targetAmount = targetAsset === sourceAsset ? sourceAmount : await this.#dexClient.testCompositeSwap(sourceAsset, targetAsset, sourceAmount); - await this.checkAssetAvailability(targetAsset, targetAmount); + !bypassAvailabilityCheck && (await this.checkAssetAvailability(targetAsset, targetAmount)); - if ((await this.settingService.get('slippage-protection')) === 'on') { + if ((await this.settingService.get('slippage-protection')) === 'on' && !bypassSlippageProtection) { await this.checkTestSwapPriceSlippage(sourceAsset, sourceAmount, targetAsset, targetAmount, maxSlippage); } diff --git a/src/payment/models/dex/services/dex-evm.service.ts b/src/payment/models/dex/services/dex-evm.service.ts index 547f6d8ebe..ba3fd7fea2 100644 --- a/src/payment/models/dex/services/dex-evm.service.ts +++ b/src/payment/models/dex/services/dex-evm.service.ts @@ -1,6 +1,7 @@ import { Blockchain } from 'src/blockchain/shared/enums/blockchain.enum'; import { EvmClient } from 'src/blockchain/shared/evm/evm-client'; import { EvmService } from 'src/blockchain/shared/evm/evm.service'; +import { Asset } from 'src/shared/models/asset/asset.entity'; import { Util } from 'src/shared/util'; import { LiquidityOrder } from '../entities/liquidity-order.entity'; import { NotEnoughLiquidityException } from '../exceptions/not-enough-liquidity.exception'; @@ -18,29 +19,67 @@ export abstract class DexEvmService { this.#client = service.getDefaultClient(); } - async getBalance(): Promise { - return this.#client.getBalance(); + async checkNativeCoinAvailability(amount: number): Promise { + const pendingAmount = await this.getPendingAmount(this.nativeCoin); + const availableAmount = await this.#client.getNativeCoinBalance(); + + this.checkLiquidity(amount, pendingAmount, availableAmount, this.nativeCoin); + + return amount; + } + + async getAndCheckTokenAvailability(sourceAsset: string, sourceAmount: number, targetAsset: Asset): Promise { + const amount = await this.getTargetAmount(sourceAsset, sourceAmount, targetAsset); + + await this.checkTokenAvailability(targetAsset, amount); + + return amount; + } + + get _nativeCoin(): string { + return this.nativeCoin; + } + + //*** HELPER METHODS ***// + + private async getTargetAmount(sourceAsset: string, sourceAmount: number, targetAsset: Asset): Promise { + if (sourceAsset === targetAsset.dexName) return sourceAmount; + if (sourceAsset !== this._nativeCoin) { + // only native coin is enabled as a sourceAsset + throw new Error( + `Only native coin reference is supported by EVM test swap. Provided source asset: ${sourceAsset}. Target asset: ${targetAsset.dexName}. Blockchain: ${targetAsset.blockchain}`, + ); + } + + return this.#client.nativeCryptoTestSwap(sourceAmount, targetAsset); + } + + private async checkTokenAvailability(asset: Asset, amount: number): Promise { + const pendingAmount = await this.getPendingAmount(asset.dexName); + const availableAmount = await this.#client.getTokenBalance(asset); + + this.checkLiquidity(amount, pendingAmount, availableAmount, asset.dexName); } - async checkCoinAvailability(amount: number): Promise { + private async getPendingAmount(assetName: string): Promise { const pendingOrders = (await this.liquidityOrderRepo.find({ isReady: true, isComplete: false })).filter( - (o) => o.targetAsset.dexName === this.nativeCoin && o.targetAsset.blockchain === this.blockchain, + (o) => o.targetAsset.dexName === assetName && o.targetAsset.blockchain === this.blockchain, ); - const pendingAmount = Util.sumObj(pendingOrders, 'targetAmount'); - const availableAmount = await this.getBalance(); + return Util.sumObj(pendingOrders, 'targetAmount'); + } + private checkLiquidity( + requiredAmount: number, + pendingAmount: number, + availableAmount: number, + assetName: string, + ): void { // 5% cap for unexpected meantime swaps - if (amount * 1.05 > availableAmount - pendingAmount) { + if (requiredAmount * 1.05 > availableAmount - pendingAmount) { throw new NotEnoughLiquidityException( - `Not enough liquidity of asset ${this.nativeCoin}. Trying to use ${amount} ${this.nativeCoin} worth liquidity. Available amount: ${availableAmount}. Pending amount: ${pendingAmount}`, + `Not enough liquidity of asset ${assetName}. Trying to use ${requiredAmount} ${assetName} worth liquidity. Available amount: ${availableAmount}. Pending amount: ${pendingAmount}`, ); } - - return amount; - } - - get _nativeCoin(): string { - return this.nativeCoin; } } diff --git a/src/payment/models/dex/services/dex.service.ts b/src/payment/models/dex/services/dex.service.ts index 111bad8d68..88e6d761c5 100644 --- a/src/payment/models/dex/services/dex.service.ts +++ b/src/payment/models/dex/services/dex.service.ts @@ -9,15 +9,17 @@ import { Interval } from '@nestjs/schedule'; import { Lock } from 'src/shared/lock'; import { Not, IsNull } from 'typeorm'; import { LiquidityOrderFactory } from '../factories/liquidity-order.factory'; -import { DexStrategiesFacade } from '../strategies/strategies.facade'; +import { CheckLiquidityStrategies } from '../strategies/check-liquidity/check-liquidity.facade'; import { LiquidityRequest, TransferRequest } from '../interfaces'; +import { PurchaseLiquidityStrategies } from '../strategies/purchase-liquidity/purchase-liquidity.facade'; @Injectable() export class DexService { private readonly verifyPurchaseOrdersLock = new Lock(1800); constructor( - private readonly strategies: DexStrategiesFacade, + private readonly checkStrategies: CheckLiquidityStrategies, + private readonly purchaseStrategies: PurchaseLiquidityStrategies, private readonly dexDeFiChainService: DexDeFiChainService, private readonly liquidityOrderRepo: LiquidityOrderRepository, private readonly liquidityOrderFactory: LiquidityOrderFactory, @@ -29,7 +31,7 @@ export class DexService { const { context, correlationId, targetAsset } = request; try { - const strategy = this.strategies.getCheckLiquidityStrategy(targetAsset); + const strategy = this.checkStrategies.getCheckLiquidityStrategy(targetAsset); return strategy.checkLiquidity(request); } catch (e) { @@ -50,7 +52,7 @@ export class DexService { try { console.info(`Reserving ${targetAsset.dexName} liquidity. Context: ${context}. Correlation ID: ${correlationId}`); - const strategy = this.strategies.getCheckLiquidityStrategy(targetAsset); + const strategy = this.checkStrategies.getCheckLiquidityStrategy(targetAsset); const liquidity = await strategy.checkLiquidity(request); @@ -80,7 +82,7 @@ export class DexService { async purchaseLiquidity(request: LiquidityRequest): Promise { const { context, correlationId, targetAsset } = request; - const strategy = this.strategies.getPurchaseLiquidityStrategy(targetAsset); + const strategy = this.purchaseStrategies.getPurchaseLiquidityStrategy(targetAsset); if (!strategy) { throw new Error(`No purchase liquidity strategy for asset category ${targetAsset?.category}`); diff --git a/src/payment/models/dex/strategies/__tests__/strategies.facade.spec.ts b/src/payment/models/dex/strategies/__tests__/strategies.facade.spec.ts deleted file mode 100644 index 0dfa14c01a..0000000000 --- a/src/payment/models/dex/strategies/__tests__/strategies.facade.spec.ts +++ /dev/null @@ -1,395 +0,0 @@ -import { mock } from 'jest-mock-extended'; -import { BehaviorSubject } from 'rxjs'; -import { NodeService } from 'src/blockchain/ain/node/node.service'; -import { Blockchain } from 'src/blockchain/shared/enums/blockchain.enum'; -import { AssetCategory } from 'src/shared/models/asset/asset.entity'; -import { AssetService } from 'src/shared/models/asset/asset.service'; -import { createCustomAsset } from 'src/shared/models/asset/__mocks__/asset.entity.mock'; -import { SettingService } from 'src/shared/models/setting/setting.service'; -import { MailService } from 'src/shared/services/mail.service'; -import { LiquidityOrderFactory } from '../../factories/liquidity-order.factory'; -import { LiquidityOrderRepository } from '../../repositories/liquidity-order.repository'; -import { DexBscService } from '../../services/dex-bsc.service'; -import { DexDeFiChainService } from '../../services/dex-defichain.service'; -import { DexEthereumService } from '../../services/dex-ethereum.service'; -import { DexService } from '../../services/dex.service'; -import { CheckLiquidityBscStrategy } from '../check-liquidity/check-liquidity-bsc.strategy'; -import { CheckLiquidityDeFiChainDefaultStrategy } from '../check-liquidity/check-liquidity-defichain-default.strategy'; -import { CheckLiquidityDeFiChainPoolPairStrategy } from '../check-liquidity/check-liquidity-defichain-poolpair.strategy'; -import { CheckLiquidityEthereumStrategy } from '../check-liquidity/check-liquidity-ethereum.strategy'; -import { PurchaseLiquidityBscStrategy } from '../purchase-liquidity/purchase-liquidity-bsc.strategy'; -import { PurchaseLiquidityDeFiChainCryptoStrategy } from '../purchase-liquidity/purchase-liquidity-defichain-crypto.strategy'; -import { PurchaseLiquidityDeFiChainPoolPairStrategy } from '../purchase-liquidity/purchase-liquidity-defichain-poolpair.strategy'; -import { PurchaseLiquidityDeFiChainStockStrategy } from '../purchase-liquidity/purchase-liquidity-defichain-stock.strategy'; -import { PurchaseLiquidityEthereumStrategy } from '../purchase-liquidity/purchase-liquidity-ethereum.strategy'; -import { CheckLiquidityStrategyAlias, DexStrategiesFacade, PurchaseLiquidityStrategyAlias } from '../strategies.facade'; - -describe('DexStrategiesFacade', () => { - let nodeService: NodeService; - - let checkLiquidityDeFiChainPoolPairStrategy: CheckLiquidityDeFiChainPoolPairStrategy; - let checkLiquidityDeFiChainDefaultStrategy: CheckLiquidityDeFiChainDefaultStrategy; - let checkLiquidityEthereumStrategy: CheckLiquidityEthereumStrategy; - let checkLiquidityBSCStrategy: CheckLiquidityBscStrategy; - let purchaseLiquidityDeFiChainPoolPairStrategy: PurchaseLiquidityDeFiChainPoolPairStrategy; - let purchaseLiquidityDeFiChainStockStrategy: PurchaseLiquidityDeFiChainStockStrategy; - let purchaseLiquidityDeFiChainCryptoStrategy: PurchaseLiquidityDeFiChainCryptoStrategy; - let purchaseLiquidityEthereumStrategy: PurchaseLiquidityEthereumStrategy; - let purchaseLiquidityBscStrategy: PurchaseLiquidityBscStrategy; - - let facade: DexStrategiesFacadeWrapper; - - beforeEach(() => { - nodeService = mock(); - jest.spyOn(nodeService, 'getConnectedNode').mockImplementation(() => new BehaviorSubject(null)); - - checkLiquidityDeFiChainPoolPairStrategy = new CheckLiquidityDeFiChainPoolPairStrategy(); - checkLiquidityDeFiChainDefaultStrategy = new CheckLiquidityDeFiChainDefaultStrategy(mock()); - checkLiquidityEthereumStrategy = new CheckLiquidityEthereumStrategy(mock()); - checkLiquidityBSCStrategy = new CheckLiquidityBscStrategy(mock()); - purchaseLiquidityDeFiChainPoolPairStrategy = new PurchaseLiquidityDeFiChainPoolPairStrategy( - nodeService, - mock(), - mock(), - mock(), - mock(), - mock(), - mock(), - ); - purchaseLiquidityDeFiChainStockStrategy = new PurchaseLiquidityDeFiChainStockStrategy( - mock(), - mock(), - mock(), - mock(), - ); - purchaseLiquidityDeFiChainCryptoStrategy = new PurchaseLiquidityDeFiChainCryptoStrategy( - mock(), - mock(), - mock(), - mock(), - ); - purchaseLiquidityEthereumStrategy = new PurchaseLiquidityEthereumStrategy( - mock(), - mock(), - ); - purchaseLiquidityBscStrategy = new PurchaseLiquidityBscStrategy(mock(), mock()); - - facade = new DexStrategiesFacadeWrapper( - checkLiquidityDeFiChainPoolPairStrategy, - checkLiquidityDeFiChainDefaultStrategy, - checkLiquidityEthereumStrategy, - checkLiquidityBSCStrategy, - purchaseLiquidityDeFiChainPoolPairStrategy, - purchaseLiquidityDeFiChainStockStrategy, - purchaseLiquidityDeFiChainCryptoStrategy, - purchaseLiquidityEthereumStrategy, - purchaseLiquidityBscStrategy, - ); - }); - - describe('#constructor(...)', () => { - it('adds all checkLiquidityStrategies to a map', () => { - expect([...facade.getCheckLiquidityStrategies().entries()].length).toBe(4); - }); - - it('sets all required checkLiquidityStrategies aliases', () => { - const aliases = [...facade.getCheckLiquidityStrategies().keys()]; - - expect(aliases.includes(CheckLiquidityStrategyAlias.DEFICHAIN_POOL_PAIR)).toBe(true); - expect(aliases.includes(CheckLiquidityStrategyAlias.DEFICHAIN_DEFAULT)).toBe(true); - expect(aliases.includes(CheckLiquidityStrategyAlias.ETHEREUM_DEFAULT)).toBe(true); - expect(aliases.includes(CheckLiquidityStrategyAlias.BSC_DEFAULT)).toBe(true); - }); - - it('assigns proper checkLiquidityStrategies to aliases', () => { - expect(facade.getCheckLiquidityStrategies().get(CheckLiquidityStrategyAlias.DEFICHAIN_POOL_PAIR)).toBeInstanceOf( - CheckLiquidityDeFiChainPoolPairStrategy, - ); - - expect(facade.getCheckLiquidityStrategies().get(CheckLiquidityStrategyAlias.DEFICHAIN_DEFAULT)).toBeInstanceOf( - CheckLiquidityDeFiChainDefaultStrategy, - ); - - expect(facade.getCheckLiquidityStrategies().get(CheckLiquidityStrategyAlias.ETHEREUM_DEFAULT)).toBeInstanceOf( - CheckLiquidityEthereumStrategy, - ); - - expect(facade.getCheckLiquidityStrategies().get(CheckLiquidityStrategyAlias.BSC_DEFAULT)).toBeInstanceOf( - CheckLiquidityBscStrategy, - ); - }); - - it('adds all purchaseLiquidityStrategies to a map', () => { - expect([...facade.getPurchaseLiquidityStrategies().entries()].length).toBe(5); - }); - - it('sets all required purchaseLiquidityStrategies aliases', () => { - const aliases = [...facade.getPurchaseLiquidityStrategies().keys()]; - - expect(aliases.includes(PurchaseLiquidityStrategyAlias.DEFICHAIN_POOL_PAIR)).toBe(true); - expect(aliases.includes(PurchaseLiquidityStrategyAlias.DEFICHAIN_STOCK)).toBe(true); - expect(aliases.includes(PurchaseLiquidityStrategyAlias.DEFICHAIN_CRYPTO)).toBe(true); - expect(aliases.includes(PurchaseLiquidityStrategyAlias.ETHEREUM_DEFAULT)).toBe(true); - expect(aliases.includes(PurchaseLiquidityStrategyAlias.BSC_DEFAULT)).toBe(true); - }); - - it('assigns proper purchaseLiquidityStrategies to aliases', () => { - expect( - facade.getPurchaseLiquidityStrategies().get(PurchaseLiquidityStrategyAlias.DEFICHAIN_POOL_PAIR), - ).toBeInstanceOf(PurchaseLiquidityDeFiChainPoolPairStrategy); - - expect( - facade.getPurchaseLiquidityStrategies().get(PurchaseLiquidityStrategyAlias.DEFICHAIN_STOCK), - ).toBeInstanceOf(PurchaseLiquidityDeFiChainStockStrategy); - - expect( - facade.getPurchaseLiquidityStrategies().get(PurchaseLiquidityStrategyAlias.DEFICHAIN_CRYPTO), - ).toBeInstanceOf(PurchaseLiquidityDeFiChainCryptoStrategy); - - expect( - facade.getPurchaseLiquidityStrategies().get(PurchaseLiquidityStrategyAlias.ETHEREUM_DEFAULT), - ).toBeInstanceOf(PurchaseLiquidityEthereumStrategy); - - expect(facade.getPurchaseLiquidityStrategies().get(PurchaseLiquidityStrategyAlias.BSC_DEFAULT)).toBeInstanceOf( - PurchaseLiquidityBscStrategy, - ); - }); - }); - - describe('#getCheckLiquidityStrategy(...)', () => { - describe('getting strategy by Asset', () => { - it('gets DEFICHAIN_POOL_PAIR strategy for DEFICHAIN', () => { - const strategy = facade.getCheckLiquidityStrategy( - createCustomAsset({ blockchain: Blockchain.DEFICHAIN, category: AssetCategory.POOL_PAIR }), - ); - - expect(strategy).toBeInstanceOf(CheckLiquidityDeFiChainPoolPairStrategy); - }); - - it('gets DEFICHAIN_DEFAULT strategy for DEFICHAIN', () => { - const strategyCrypto = facade.getCheckLiquidityStrategy( - createCustomAsset({ blockchain: Blockchain.DEFICHAIN, category: AssetCategory.CRYPTO }), - ); - - expect(strategyCrypto).toBeInstanceOf(CheckLiquidityDeFiChainDefaultStrategy); - - const strategyStock = facade.getCheckLiquidityStrategy( - createCustomAsset({ blockchain: Blockchain.DEFICHAIN, category: AssetCategory.STOCK }), - ); - - expect(strategyStock).toBeInstanceOf(CheckLiquidityDeFiChainDefaultStrategy); - }); - - it('gets DEFICHAIN_DEFAULT strategy for BITCOIN', () => { - const strategyCrypto = facade.getCheckLiquidityStrategy( - createCustomAsset({ blockchain: Blockchain.BITCOIN, category: AssetCategory.CRYPTO }), - ); - - expect(strategyCrypto).toBeInstanceOf(CheckLiquidityDeFiChainDefaultStrategy); - - const strategyStock = facade.getCheckLiquidityStrategy( - createCustomAsset({ blockchain: Blockchain.BITCOIN, category: AssetCategory.STOCK }), - ); - - expect(strategyStock).toBeInstanceOf(CheckLiquidityDeFiChainDefaultStrategy); - }); - - it('gets ETHEREUM_DEFAULT strategy', () => { - const strategy = facade.getCheckLiquidityStrategy(createCustomAsset({ blockchain: Blockchain.ETHEREUM })); - - expect(strategy).toBeInstanceOf(CheckLiquidityEthereumStrategy); - }); - - it('gets BSC_DEFAULT strategy', () => { - const strategy = facade.getCheckLiquidityStrategy( - createCustomAsset({ blockchain: Blockchain.BINANCE_SMART_CHAIN }), - ); - - expect(strategy).toBeInstanceOf(CheckLiquidityBscStrategy); - }); - - it('fails to get strategy for non-supported Blockchain', () => { - const testCall = () => - facade.getCheckLiquidityStrategy(createCustomAsset({ blockchain: 'NewBlockchain' as Blockchain })); - - expect(testCall).toThrow(); - expect(testCall).toThrowError('No CheckLiquidityStrategy found. Alias: undefined'); - }); - }); - - describe('getting strategy by Alias', () => { - it('gets DEFICHAIN_POOL_PAIR strategy', () => { - const strategy = facade.getCheckLiquidityStrategy(CheckLiquidityStrategyAlias.DEFICHAIN_POOL_PAIR); - - expect(strategy).toBeInstanceOf(CheckLiquidityDeFiChainPoolPairStrategy); - }); - - it('gets DEFICHAIN_DEFAULT strategy', () => { - const strategyCrypto = facade.getCheckLiquidityStrategy(CheckLiquidityStrategyAlias.DEFICHAIN_DEFAULT); - - expect(strategyCrypto).toBeInstanceOf(CheckLiquidityDeFiChainDefaultStrategy); - }); - - it('gets ETHEREUM_DEFAULT strategy', () => { - const strategy = facade.getCheckLiquidityStrategy(CheckLiquidityStrategyAlias.ETHEREUM_DEFAULT); - - expect(strategy).toBeInstanceOf(CheckLiquidityEthereumStrategy); - }); - - it('gets BSC_DEFAULT strategy', () => { - const strategy = facade.getCheckLiquidityStrategy(CheckLiquidityStrategyAlias.BSC_DEFAULT); - - expect(strategy).toBeInstanceOf(CheckLiquidityBscStrategy); - }); - - it('fails to get strategy for non-supported Alias', () => { - const testCall = () => facade.getCheckLiquidityStrategy('NonExistingAlias' as CheckLiquidityStrategyAlias); - - expect(testCall).toThrow(); - expect(testCall).toThrowError('No CheckLiquidityStrategy found. Alias: NonExistingAlias'); - }); - }); - }); - - describe('#getPurchaseLiquidityStrategy(...)', () => { - describe('getting strategy by Asset', () => { - it('gets DEFICHAIN_POOL_PAIR strategy for DEFICHAIN Pool Pair', () => { - const strategy = facade.getPurchaseLiquidityStrategy( - createCustomAsset({ blockchain: Blockchain.DEFICHAIN, category: AssetCategory.POOL_PAIR }), - ); - - expect(strategy).toBeInstanceOf(PurchaseLiquidityDeFiChainPoolPairStrategy); - }); - - it('gets DEFICHAIN_STOCK strategy for DEFICHAIN Stock', () => { - const strategy = facade.getPurchaseLiquidityStrategy( - createCustomAsset({ blockchain: Blockchain.DEFICHAIN, category: AssetCategory.STOCK }), - ); - - expect(strategy).toBeInstanceOf(PurchaseLiquidityDeFiChainStockStrategy); - }); - - it('gets DEFICHAIN_CRYPTO strategy for DEFICHAIN Crypto', () => { - const strategy = facade.getPurchaseLiquidityStrategy( - createCustomAsset({ blockchain: Blockchain.DEFICHAIN, category: AssetCategory.CRYPTO }), - ); - - expect(strategy).toBeInstanceOf(PurchaseLiquidityDeFiChainCryptoStrategy); - }); - - it('gets DEFICHAIN_CRYPTO strategy for BITCOIN Crypto', () => { - const strategy = facade.getPurchaseLiquidityStrategy( - createCustomAsset({ blockchain: Blockchain.BITCOIN, category: AssetCategory.CRYPTO }), - ); - - expect(strategy).toBeInstanceOf(PurchaseLiquidityDeFiChainCryptoStrategy); - }); - - it('gets ETHEREUM_DEFAULT strategy', () => { - const strategy = facade.getPurchaseLiquidityStrategy(createCustomAsset({ blockchain: Blockchain.ETHEREUM })); - - expect(strategy).toBeInstanceOf(PurchaseLiquidityEthereumStrategy); - }); - - it('gets BSC_DEFAULT strategy', () => { - const strategy = facade.getPurchaseLiquidityStrategy( - createCustomAsset({ blockchain: Blockchain.BINANCE_SMART_CHAIN }), - ); - - expect(strategy).toBeInstanceOf(PurchaseLiquidityBscStrategy); - }); - - it('fails to get strategy for non-supported Blockchain', () => { - const testCall = () => - facade.getPurchaseLiquidityStrategy(createCustomAsset({ blockchain: 'NewBlockchain' as Blockchain })); - - expect(testCall).toThrow(); - expect(testCall).toThrowError('No PurchaseLiquidityStrategy found. Alias: undefined'); - }); - - it('fails to get strategy for non-supported AssetCategory', () => { - const testCall = () => - facade.getPurchaseLiquidityStrategy( - createCustomAsset({ blockchain: Blockchain.DEFICHAIN, category: 'NewCategory' as AssetCategory }), - ); - - expect(testCall).toThrow(); - expect(testCall).toThrowError('No PurchaseLiquidityStrategy found. Alias: undefined'); - }); - }); - - describe('getting strategy by Alias', () => { - it('gets DEFICHAIN_POOL_PAIR strategy', () => { - const strategy = facade.getPurchaseLiquidityStrategy(PurchaseLiquidityStrategyAlias.DEFICHAIN_POOL_PAIR); - - expect(strategy).toBeInstanceOf(PurchaseLiquidityDeFiChainPoolPairStrategy); - }); - - it('gets DEFICHAIN_STOCK strategy', () => { - const strategyCrypto = facade.getPurchaseLiquidityStrategy(PurchaseLiquidityStrategyAlias.DEFICHAIN_STOCK); - - expect(strategyCrypto).toBeInstanceOf(PurchaseLiquidityDeFiChainStockStrategy); - }); - - it('gets DEFICHAIN_CRYPTO strategy', () => { - const strategyCrypto = facade.getPurchaseLiquidityStrategy(PurchaseLiquidityStrategyAlias.DEFICHAIN_CRYPTO); - - expect(strategyCrypto).toBeInstanceOf(PurchaseLiquidityDeFiChainCryptoStrategy); - }); - - it('gets ETHEREUM_DEFAULT strategy', () => { - const strategy = facade.getPurchaseLiquidityStrategy(PurchaseLiquidityStrategyAlias.ETHEREUM_DEFAULT); - - expect(strategy).toBeInstanceOf(PurchaseLiquidityEthereumStrategy); - }); - - it('gets BSC_DEFAULT strategy', () => { - const strategy = facade.getPurchaseLiquidityStrategy(PurchaseLiquidityStrategyAlias.BSC_DEFAULT); - - expect(strategy).toBeInstanceOf(PurchaseLiquidityBscStrategy); - }); - - it('fails to get strategy for non-supported Alias', () => { - const testCall = () => - facade.getPurchaseLiquidityStrategy('NonExistingAlias' as PurchaseLiquidityStrategyAlias); - - expect(testCall).toThrow(); - expect(testCall).toThrowError('No PurchaseLiquidityStrategy found. Alias: NonExistingAlias'); - }); - }); - }); -}); - -class DexStrategiesFacadeWrapper extends DexStrategiesFacade { - constructor( - checkLiquidityDeFiChainPoolPairStrategy: CheckLiquidityDeFiChainPoolPairStrategy, - checkLiquidityDeFiChainDefaultStrategy: CheckLiquidityDeFiChainDefaultStrategy, - checkLiquidityEthereumStrategy: CheckLiquidityEthereumStrategy, - checkLiquidityBSCStrategy: CheckLiquidityBscStrategy, - purchaseLiquidityDeFiChainPoolPairStrategy: PurchaseLiquidityDeFiChainPoolPairStrategy, - purchaseLiquidityDeFiChainStockStrategy: PurchaseLiquidityDeFiChainStockStrategy, - purchaseLiquidityDeFiChainCryptoStrategy: PurchaseLiquidityDeFiChainCryptoStrategy, - purchaseLiquidityEthereumStrategy: PurchaseLiquidityEthereumStrategy, - purchaseLiquidityBSCStrategy: PurchaseLiquidityBscStrategy, - ) { - super( - checkLiquidityDeFiChainPoolPairStrategy, - checkLiquidityDeFiChainDefaultStrategy, - checkLiquidityEthereumStrategy, - checkLiquidityBSCStrategy, - purchaseLiquidityDeFiChainPoolPairStrategy, - purchaseLiquidityDeFiChainStockStrategy, - purchaseLiquidityDeFiChainCryptoStrategy, - purchaseLiquidityEthereumStrategy, - purchaseLiquidityBSCStrategy, - ); - } - - getCheckLiquidityStrategies() { - return this.checkLiquidityStrategies; - } - - getPurchaseLiquidityStrategies() { - return this.purchaseLiquidityStrategies; - } -} diff --git a/src/payment/models/dex/strategies/check-liquidity/__tests__/check-liquidity.facade.spec.ts b/src/payment/models/dex/strategies/check-liquidity/__tests__/check-liquidity.facade.spec.ts new file mode 100644 index 0000000000..bf692e6f4d --- /dev/null +++ b/src/payment/models/dex/strategies/check-liquidity/__tests__/check-liquidity.facade.spec.ts @@ -0,0 +1,234 @@ +import { mock } from 'jest-mock-extended'; +import { BehaviorSubject } from 'rxjs'; +import { NodeService } from 'src/blockchain/ain/node/node.service'; +import { Blockchain } from 'src/blockchain/shared/enums/blockchain.enum'; +import { AssetCategory, AssetType } from 'src/shared/models/asset/asset.entity'; +import { createCustomAsset } from 'src/shared/models/asset/__mocks__/asset.entity.mock'; +import { DexBscService } from '../../../services/dex-bsc.service'; +import { DexDeFiChainService } from '../../../services/dex-defichain.service'; +import { DexEthereumService } from '../../../services/dex-ethereum.service'; +import { CheckLiquidityStrategies } from '../check-liquidity.facade'; +import { BscCoinStrategy } from '../impl/bsc-coin.strategy'; +import { DeFiChainDefaultStrategy } from '../impl/defichain-default.strategy'; +import { DeFiChainPoolPairStrategy } from '../impl/defichain-poolpair.strategy'; +import { EthereumCoinStrategy } from '../impl/ethereum-coin.strategy'; +import { CheckLiquidityAlias } from '../check-liquidity.facade'; +import { BitcoinStrategy } from '../impl/bitcoin.strategy'; +import { BscTokenStrategy } from '../impl/bsc-token.strategy'; +import { EthereumTokenStrategy } from '../impl/ethereum-token.strategy'; +import { DexBitcoinService } from '../../../services/dex-bitcoin.service'; + +describe('CheckLiquidityStrategies', () => { + let nodeService: NodeService; + + let bitcoin: BitcoinStrategy; + let bscCoin: BscCoinStrategy; + let bscToken: BscTokenStrategy; + let deFiChainPoolPair: DeFiChainPoolPairStrategy; + let deFiChainDefault: DeFiChainDefaultStrategy; + let ethereumCoin: EthereumCoinStrategy; + let ethereumToken: EthereumTokenStrategy; + + let facade: CheckLiquidityStrategiesWrapper; + + beforeEach(() => { + nodeService = mock(); + jest.spyOn(nodeService, 'getConnectedNode').mockImplementation(() => new BehaviorSubject(null)); + + bitcoin = new BitcoinStrategy(mock()); + bscCoin = new BscCoinStrategy(mock()); + bscToken = new BscTokenStrategy(mock()); + deFiChainPoolPair = new DeFiChainPoolPairStrategy(); + deFiChainDefault = new DeFiChainDefaultStrategy(mock()); + ethereumCoin = new EthereumCoinStrategy(mock()); + ethereumToken = new EthereumTokenStrategy(mock()); + + facade = new CheckLiquidityStrategiesWrapper( + bitcoin, + bscCoin, + bscToken, + deFiChainDefault, + deFiChainPoolPair, + ethereumCoin, + ethereumToken, + ); + }); + + describe('#constructor(...)', () => { + it('adds all checkLiquidityStrategies to a map', () => { + expect([...facade.getStrategies().entries()].length).toBe(7); + }); + + it('assigns strategies to all aliases', () => { + expect([...facade.getStrategies().entries()].length).toBe(Object.values(CheckLiquidityAlias).length); + }); + + it('sets all required checkLiquidityStrategies aliases', () => { + const aliases = [...facade.getStrategies().keys()]; + + expect(aliases.includes(CheckLiquidityAlias.BITCOIN)).toBe(true); + expect(aliases.includes(CheckLiquidityAlias.BSC_COIN)).toBe(true); + expect(aliases.includes(CheckLiquidityAlias.BSC_TOKEN)).toBe(true); + expect(aliases.includes(CheckLiquidityAlias.DEFICHAIN_POOL_PAIR)).toBe(true); + expect(aliases.includes(CheckLiquidityAlias.DEFICHAIN_DEFAULT)).toBe(true); + expect(aliases.includes(CheckLiquidityAlias.ETHEREUM_COIN)).toBe(true); + expect(aliases.includes(CheckLiquidityAlias.ETHEREUM_TOKEN)).toBe(true); + }); + + it('assigns proper checkLiquidityStrategies to aliases', () => { + expect(facade.getStrategies().get(CheckLiquidityAlias.BITCOIN)).toBeInstanceOf(BitcoinStrategy); + expect(facade.getStrategies().get(CheckLiquidityAlias.BSC_COIN)).toBeInstanceOf(BscCoinStrategy); + expect(facade.getStrategies().get(CheckLiquidityAlias.BSC_TOKEN)).toBeInstanceOf(BscTokenStrategy); + expect(facade.getStrategies().get(CheckLiquidityAlias.DEFICHAIN_POOL_PAIR)).toBeInstanceOf( + DeFiChainPoolPairStrategy, + ); + expect(facade.getStrategies().get(CheckLiquidityAlias.DEFICHAIN_DEFAULT)).toBeInstanceOf( + DeFiChainDefaultStrategy, + ); + expect(facade.getStrategies().get(CheckLiquidityAlias.ETHEREUM_COIN)).toBeInstanceOf(EthereumCoinStrategy); + expect(facade.getStrategies().get(CheckLiquidityAlias.ETHEREUM_TOKEN)).toBeInstanceOf(EthereumTokenStrategy); + }); + }); + + describe('#getCheckLiquidityStrategy(...)', () => { + describe('getting strategy by Asset', () => { + it('gets BITCOIN strategy for BITCOIN', () => { + const strategy = facade.getCheckLiquidityStrategy(createCustomAsset({ blockchain: Blockchain.BITCOIN })); + + expect(strategy).toBeInstanceOf(BitcoinStrategy); + }); + + it('gets BSC_COIN strategy', () => { + const strategy = facade.getCheckLiquidityStrategy( + createCustomAsset({ blockchain: Blockchain.BINANCE_SMART_CHAIN, type: AssetType.COIN }), + ); + + expect(strategy).toBeInstanceOf(BscCoinStrategy); + }); + + it('gets BSC_TOKEN strategy', () => { + const strategy = facade.getCheckLiquidityStrategy( + createCustomAsset({ blockchain: Blockchain.BINANCE_SMART_CHAIN, type: AssetType.TOKEN }), + ); + + expect(strategy).toBeInstanceOf(BscTokenStrategy); + }); + + it('gets DEFICHAIN_POOL_PAIR strategy for DEFICHAIN', () => { + const strategy = facade.getCheckLiquidityStrategy( + createCustomAsset({ blockchain: Blockchain.DEFICHAIN, category: AssetCategory.POOL_PAIR }), + ); + + expect(strategy).toBeInstanceOf(DeFiChainPoolPairStrategy); + }); + + it('gets DEFICHAIN_DEFAULT strategy for DEFICHAIN', () => { + const strategyCrypto = facade.getCheckLiquidityStrategy( + createCustomAsset({ blockchain: Blockchain.DEFICHAIN, category: AssetCategory.CRYPTO }), + ); + + expect(strategyCrypto).toBeInstanceOf(DeFiChainDefaultStrategy); + + const strategyStock = facade.getCheckLiquidityStrategy( + createCustomAsset({ blockchain: Blockchain.DEFICHAIN, category: AssetCategory.STOCK }), + ); + + expect(strategyStock).toBeInstanceOf(DeFiChainDefaultStrategy); + }); + + it('gets ETHEREUM_COIN strategy', () => { + const strategy = facade.getCheckLiquidityStrategy( + createCustomAsset({ blockchain: Blockchain.ETHEREUM, type: AssetType.COIN }), + ); + + expect(strategy).toBeInstanceOf(EthereumCoinStrategy); + }); + + it('gets ETHEREUM_TOKEN strategy', () => { + const strategy = facade.getCheckLiquidityStrategy( + createCustomAsset({ blockchain: Blockchain.ETHEREUM, type: AssetType.TOKEN }), + ); + + expect(strategy).toBeInstanceOf(EthereumTokenStrategy); + }); + + it('fails to get strategy for non-supported Blockchain', () => { + const testCall = () => + facade.getCheckLiquidityStrategy(createCustomAsset({ blockchain: 'NewBlockchain' as Blockchain })); + + expect(testCall).toThrow(); + expect(testCall).toThrowError('No CheckLiquidityStrategy found. Alias: undefined'); + }); + }); + + describe('getting strategy by CheckLiquidityAlias', () => { + it('gets BITCOIN strategy', () => { + const strategy = facade.getCheckLiquidityStrategy(CheckLiquidityAlias.BITCOIN); + + expect(strategy).toBeInstanceOf(BitcoinStrategy); + }); + + it('gets BSC_COIN strategy', () => { + const strategy = facade.getCheckLiquidityStrategy(CheckLiquidityAlias.BSC_COIN); + + expect(strategy).toBeInstanceOf(BscCoinStrategy); + }); + + it('gets BSC_TOKEN strategy', () => { + const strategy = facade.getCheckLiquidityStrategy(CheckLiquidityAlias.BSC_TOKEN); + + expect(strategy).toBeInstanceOf(BscTokenStrategy); + }); + + it('gets DEFICHAIN_POOL_PAIR strategy', () => { + const strategy = facade.getCheckLiquidityStrategy(CheckLiquidityAlias.DEFICHAIN_POOL_PAIR); + + expect(strategy).toBeInstanceOf(DeFiChainPoolPairStrategy); + }); + + it('gets DEFICHAIN_DEFAULT strategy', () => { + const strategyCrypto = facade.getCheckLiquidityStrategy(CheckLiquidityAlias.DEFICHAIN_DEFAULT); + + expect(strategyCrypto).toBeInstanceOf(DeFiChainDefaultStrategy); + }); + + it('gets ETHEREUM_COIN strategy', () => { + const strategy = facade.getCheckLiquidityStrategy(CheckLiquidityAlias.ETHEREUM_COIN); + + expect(strategy).toBeInstanceOf(EthereumCoinStrategy); + }); + + it('gets ETHEREUM_TOKEN strategy', () => { + const strategy = facade.getCheckLiquidityStrategy(CheckLiquidityAlias.ETHEREUM_TOKEN); + + expect(strategy).toBeInstanceOf(EthereumTokenStrategy); + }); + + it('fails to get strategy for non-supported CheckLiquidityAlias', () => { + const testCall = () => + facade.getCheckLiquidityStrategy('NonExistingCheckLiquidityAlias' as CheckLiquidityAlias); + + expect(testCall).toThrow(); + expect(testCall).toThrowError('No CheckLiquidityStrategy found. Alias: NonExistingCheckLiquidityAlias'); + }); + }); + }); +}); + +class CheckLiquidityStrategiesWrapper extends CheckLiquidityStrategies { + constructor( + bitcoin: BitcoinStrategy, + bscCoin: BscCoinStrategy, + bscToken: BscTokenStrategy, + deFiChainDefault: DeFiChainDefaultStrategy, + deFiChainPoolPair: DeFiChainPoolPairStrategy, + ethereumCoin: EthereumCoinStrategy, + ethereumToken: EthereumTokenStrategy, + ) { + super(bitcoin, bscCoin, bscToken, deFiChainDefault, deFiChainPoolPair, ethereumCoin, ethereumToken); + } + + getStrategies() { + return this.strategies; + } +} diff --git a/src/payment/models/dex/strategies/check-liquidity/base/check-liquidity-evm.strategy.ts b/src/payment/models/dex/strategies/check-liquidity/base/check-liquidity-evm.strategy.ts deleted file mode 100644 index 2d7ad8de6e..0000000000 --- a/src/payment/models/dex/strategies/check-liquidity/base/check-liquidity-evm.strategy.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { LiquidityRequest } from '../../../interfaces'; -import { DexEvmService } from '../../../services/dex-evm.service'; -import { CheckLiquidityStrategy } from './check-liquidity.strategy'; - -export class CheckLiquidityEvmStrategy implements CheckLiquidityStrategy { - constructor(protected readonly dexEvmService: DexEvmService) {} - - async checkLiquidity(request: LiquidityRequest): Promise { - const targetAmount = request.referenceAmount; - - await this.dexEvmService.checkCoinAvailability(targetAmount); - - return targetAmount; - } -} diff --git a/src/payment/models/dex/strategies/check-liquidity/check-liquidity-bsc.strategy.ts b/src/payment/models/dex/strategies/check-liquidity/check-liquidity-bsc.strategy.ts deleted file mode 100644 index 76b793e8b7..0000000000 --- a/src/payment/models/dex/strategies/check-liquidity/check-liquidity-bsc.strategy.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { DexBscService } from '../../services/dex-bsc.service'; -import { CheckLiquidityEvmStrategy } from './base/check-liquidity-evm.strategy'; - -@Injectable() -export class CheckLiquidityBscStrategy extends CheckLiquidityEvmStrategy { - constructor(dexBscService: DexBscService) { - super(dexBscService); - } -} diff --git a/src/payment/models/dex/strategies/check-liquidity/check-liquidity-ethereum.strategy.ts b/src/payment/models/dex/strategies/check-liquidity/check-liquidity-ethereum.strategy.ts deleted file mode 100644 index 3d1439d359..0000000000 --- a/src/payment/models/dex/strategies/check-liquidity/check-liquidity-ethereum.strategy.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { DexEthereumService } from '../../services/dex-ethereum.service'; -import { CheckLiquidityEvmStrategy } from './base/check-liquidity-evm.strategy'; - -@Injectable() -export class CheckLiquidityEthereumStrategy extends CheckLiquidityEvmStrategy { - constructor(dexEthereumService: DexEthereumService) { - super(dexEthereumService); - } -} diff --git a/src/payment/models/dex/strategies/check-liquidity/check-liquidity.facade.ts b/src/payment/models/dex/strategies/check-liquidity/check-liquidity.facade.ts new file mode 100644 index 0000000000..5d78660a68 --- /dev/null +++ b/src/payment/models/dex/strategies/check-liquidity/check-liquidity.facade.ts @@ -0,0 +1,85 @@ +import { Injectable } from '@nestjs/common'; +import { Blockchain } from 'src/blockchain/shared/enums/blockchain.enum'; +import { Asset, AssetCategory, AssetType } from 'src/shared/models/asset/asset.entity'; +import { CheckLiquidityStrategy } from './impl/base/check-liquidity.strategy'; +import { BitcoinStrategy } from './impl/bitcoin.strategy'; +import { BscCoinStrategy } from './impl/bsc-coin.strategy'; +import { BscTokenStrategy } from './impl/bsc-token.strategy'; +import { DeFiChainDefaultStrategy } from './impl/defichain-default.strategy'; +import { DeFiChainPoolPairStrategy } from './impl/defichain-poolpair.strategy'; +import { EthereumCoinStrategy } from './impl/ethereum-coin.strategy'; +import { EthereumTokenStrategy } from './impl/ethereum-token.strategy'; + +enum Alias { + BITCOIN = 'Bitcoin', + BSC_COIN = 'BscCoin', + BSC_TOKEN = 'BscToken', + DEFICHAIN_POOL_PAIR = 'DeFiChainPoolPair', + DEFICHAIN_DEFAULT = 'DeFiChainDefault', + ETHEREUM_COIN = 'EthereumCoin', + ETHEREUM_TOKEN = 'EthereumToken', +} + +export { Alias as CheckLiquidityAlias }; + +@Injectable() +export class CheckLiquidityStrategies { + protected readonly strategies = new Map(); + + constructor( + bitcoin: BitcoinStrategy, + bscCoin: BscCoinStrategy, + bscToken: BscTokenStrategy, + deFiChainDefault: DeFiChainDefaultStrategy, + deFiChainPoolPair: DeFiChainPoolPairStrategy, + ethereumCoin: EthereumCoinStrategy, + ethereumToken: EthereumTokenStrategy, + ) { + this.strategies.set(Alias.BITCOIN, bitcoin); + this.strategies.set(Alias.BSC_COIN, bscCoin); + this.strategies.set(Alias.BSC_TOKEN, bscToken); + this.strategies.set(Alias.DEFICHAIN_POOL_PAIR, deFiChainPoolPair); + this.strategies.set(Alias.DEFICHAIN_DEFAULT, deFiChainDefault); + this.strategies.set(Alias.ETHEREUM_COIN, ethereumCoin); + this.strategies.set(Alias.ETHEREUM_TOKEN, ethereumToken); + } + + getCheckLiquidityStrategy(criteria: Asset | Alias): CheckLiquidityStrategy { + return criteria instanceof Asset ? this.getByAsset(criteria) : this.getByAlias(criteria); + } + + //*** HELPER METHODS ***// + + private getByAlias(alias: Alias): CheckLiquidityStrategy { + const strategy = this.strategies.get(alias); + + if (!strategy) throw new Error(`No CheckLiquidityStrategy found. Alias: ${alias}`); + + return strategy; + } + + private getByAsset(asset: Asset): CheckLiquidityStrategy { + const alias = this.getAlias(asset); + + return this.getByAlias(alias); + } + + private getAlias(asset: Asset): Alias { + const { blockchain, category: assetCategory, type: assetType } = asset; + + if (blockchain === Blockchain.BITCOIN) return Alias.BITCOIN; + + if (blockchain === Blockchain.BINANCE_SMART_CHAIN) { + return assetType === AssetType.COIN ? Alias.BSC_COIN : Alias.BSC_TOKEN; + } + + if (blockchain === Blockchain.DEFICHAIN) { + if (assetCategory === AssetCategory.POOL_PAIR) return Alias.DEFICHAIN_POOL_PAIR; + return Alias.DEFICHAIN_DEFAULT; + } + + if (blockchain === Blockchain.ETHEREUM) { + return assetType === AssetType.COIN ? Alias.ETHEREUM_COIN : Alias.ETHEREUM_TOKEN; + } + } +} diff --git a/src/payment/models/dex/strategies/check-liquidity/base/check-liquidity.strategy.ts b/src/payment/models/dex/strategies/check-liquidity/impl/base/check-liquidity.strategy.ts similarity index 64% rename from src/payment/models/dex/strategies/check-liquidity/base/check-liquidity.strategy.ts rename to src/payment/models/dex/strategies/check-liquidity/impl/base/check-liquidity.strategy.ts index b618e63130..600a01de70 100644 --- a/src/payment/models/dex/strategies/check-liquidity/base/check-liquidity.strategy.ts +++ b/src/payment/models/dex/strategies/check-liquidity/impl/base/check-liquidity.strategy.ts @@ -1,4 +1,4 @@ -import { LiquidityRequest } from '../../../interfaces'; +import { LiquidityRequest } from '../../../../interfaces'; export interface CheckLiquidityStrategy { checkLiquidity(request: LiquidityRequest): Promise; diff --git a/src/payment/models/dex/strategies/check-liquidity/impl/base/evm-coin.strategy.ts b/src/payment/models/dex/strategies/check-liquidity/impl/base/evm-coin.strategy.ts new file mode 100644 index 0000000000..66533c2ec5 --- /dev/null +++ b/src/payment/models/dex/strategies/check-liquidity/impl/base/evm-coin.strategy.ts @@ -0,0 +1,20 @@ +import { LiquidityRequest } from '../../../../interfaces'; +import { DexEvmService } from '../../../../services/dex-evm.service'; +import { CheckLiquidityStrategy } from './check-liquidity.strategy'; + +export class EvmCoinStrategy implements CheckLiquidityStrategy { + constructor(protected readonly dexEvmService: DexEvmService) {} + + async checkLiquidity(request: LiquidityRequest): Promise { + const { referenceAsset, referenceAmount, context, correlationId } = request; + + if (referenceAsset === this.dexEvmService._nativeCoin) { + return this.dexEvmService.checkNativeCoinAvailability(referenceAmount); + } + + // only native coin is enabled as a referenceAsset + throw new Error( + `Only native coin reference is supported by EVM CheckLiquidity strategy. Provided reference asset: ${referenceAsset} Context: ${context}. CorrelationID: ${correlationId}`, + ); + } +} diff --git a/src/payment/models/dex/strategies/check-liquidity/impl/base/evm-token.strategy.ts b/src/payment/models/dex/strategies/check-liquidity/impl/base/evm-token.strategy.ts new file mode 100644 index 0000000000..2327f110b1 --- /dev/null +++ b/src/payment/models/dex/strategies/check-liquidity/impl/base/evm-token.strategy.ts @@ -0,0 +1,13 @@ +import { LiquidityRequest } from '../../../../interfaces'; +import { DexEvmService } from '../../../../services/dex-evm.service'; +import { CheckLiquidityStrategy } from './check-liquidity.strategy'; + +export class EvmTokenStrategy implements CheckLiquidityStrategy { + constructor(protected readonly dexEvmService: DexEvmService) {} + + async checkLiquidity(request: LiquidityRequest): Promise { + const { referenceAmount, referenceAsset, targetAsset } = request; + + return this.dexEvmService.getAndCheckTokenAvailability(referenceAsset, referenceAmount, targetAsset); + } +} diff --git a/src/payment/models/dex/strategies/check-liquidity/impl/bitcoin.strategy.ts b/src/payment/models/dex/strategies/check-liquidity/impl/bitcoin.strategy.ts new file mode 100644 index 0000000000..6c3b4067af --- /dev/null +++ b/src/payment/models/dex/strategies/check-liquidity/impl/bitcoin.strategy.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@nestjs/common'; +import { LiquidityRequest } from '../../../interfaces'; +import { DexBitcoinService } from '../../../services/dex-bitcoin.service'; +import { CheckLiquidityStrategy } from './base/check-liquidity.strategy'; + +@Injectable() +export class BitcoinStrategy implements CheckLiquidityStrategy { + constructor(private readonly dexBtcService: DexBitcoinService) {} + + async checkLiquidity(request: LiquidityRequest): Promise { + const { referenceAmount: bitcoinAmount } = request; + + return this.dexBtcService.checkAvailableTargetLiquidity(bitcoinAmount); + } +} diff --git a/src/payment/models/dex/strategies/check-liquidity/impl/bsc-coin.strategy.ts b/src/payment/models/dex/strategies/check-liquidity/impl/bsc-coin.strategy.ts new file mode 100644 index 0000000000..4f30518cbd --- /dev/null +++ b/src/payment/models/dex/strategies/check-liquidity/impl/bsc-coin.strategy.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@nestjs/common'; +import { DexBscService } from '../../../services/dex-bsc.service'; +import { EvmCoinStrategy } from './base/evm-coin.strategy'; + +@Injectable() +export class BscCoinStrategy extends EvmCoinStrategy { + constructor(dexBscService: DexBscService) { + super(dexBscService); + } +} diff --git a/src/payment/models/dex/strategies/check-liquidity/impl/bsc-token.strategy.ts b/src/payment/models/dex/strategies/check-liquidity/impl/bsc-token.strategy.ts new file mode 100644 index 0000000000..31c82a3c9c --- /dev/null +++ b/src/payment/models/dex/strategies/check-liquidity/impl/bsc-token.strategy.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@nestjs/common'; +import { DexBscService } from '../../../services/dex-bsc.service'; +import { EvmTokenStrategy } from './base/evm-token.strategy'; + +@Injectable() +export class BscTokenStrategy extends EvmTokenStrategy { + constructor(dexBscService: DexBscService) { + super(dexBscService); + } +} diff --git a/src/payment/models/dex/strategies/check-liquidity/check-liquidity-defichain-default.strategy.ts b/src/payment/models/dex/strategies/check-liquidity/impl/defichain-default.strategy.ts similarity index 56% rename from src/payment/models/dex/strategies/check-liquidity/check-liquidity-defichain-default.strategy.ts rename to src/payment/models/dex/strategies/check-liquidity/impl/defichain-default.strategy.ts index d7cd3177ba..56018a1b97 100644 --- a/src/payment/models/dex/strategies/check-liquidity/check-liquidity-defichain-default.strategy.ts +++ b/src/payment/models/dex/strategies/check-liquidity/impl/defichain-default.strategy.ts @@ -1,15 +1,15 @@ import { Injectable } from '@nestjs/common'; -import { LiquidityOrder } from '../../entities/liquidity-order.entity'; -import { LiquidityRequest } from '../../interfaces'; -import { DexDeFiChainService } from '../../services/dex-defichain.service'; +import { LiquidityOrder } from '../../../entities/liquidity-order.entity'; +import { LiquidityRequest } from '../../../interfaces'; +import { DexDeFiChainService } from '../../../services/dex-defichain.service'; import { CheckLiquidityStrategy } from './base/check-liquidity.strategy'; @Injectable() -export class CheckLiquidityDeFiChainDefaultStrategy implements CheckLiquidityStrategy { +export class DeFiChainDefaultStrategy implements CheckLiquidityStrategy { constructor(private readonly dexDeFiChainService: DexDeFiChainService) {} async checkLiquidity(request: LiquidityRequest): Promise { - const { referenceAsset, referenceAmount, targetAsset } = request; + const { referenceAsset, referenceAmount, targetAsset, options } = request; // calculating how much targetAmount is needed and if it's available on the node return this.dexDeFiChainService.getAndCheckAvailableTargetLiquidity( @@ -17,6 +17,8 @@ export class CheckLiquidityDeFiChainDefaultStrategy implements CheckLiquidityStr referenceAmount, targetAsset.dexName, LiquidityOrder.getMaxPriceSlippage(targetAsset.dexName), + options?.bypassAvailabilityCheck, + options?.bypassSlippageProtection, ); } } diff --git a/src/payment/models/dex/strategies/check-liquidity/check-liquidity-defichain-poolpair.strategy.ts b/src/payment/models/dex/strategies/check-liquidity/impl/defichain-poolpair.strategy.ts similarity index 74% rename from src/payment/models/dex/strategies/check-liquidity/check-liquidity-defichain-poolpair.strategy.ts rename to src/payment/models/dex/strategies/check-liquidity/impl/defichain-poolpair.strategy.ts index 8049d4cdbb..cf1b6407a8 100644 --- a/src/payment/models/dex/strategies/check-liquidity/check-liquidity-defichain-poolpair.strategy.ts +++ b/src/payment/models/dex/strategies/check-liquidity/impl/defichain-poolpair.strategy.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { CheckLiquidityStrategy } from './base/check-liquidity.strategy'; @Injectable() -export class CheckLiquidityDeFiChainPoolPairStrategy implements CheckLiquidityStrategy { +export class DeFiChainPoolPairStrategy implements CheckLiquidityStrategy { // assume there is no poolpair liquidity available on DEX node async checkLiquidity(): Promise { return 0; diff --git a/src/payment/models/dex/strategies/check-liquidity/impl/ethereum-coin.strategy.ts b/src/payment/models/dex/strategies/check-liquidity/impl/ethereum-coin.strategy.ts new file mode 100644 index 0000000000..b49b957298 --- /dev/null +++ b/src/payment/models/dex/strategies/check-liquidity/impl/ethereum-coin.strategy.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@nestjs/common'; +import { DexEthereumService } from '../../../services/dex-ethereum.service'; +import { EvmCoinStrategy } from './base/evm-coin.strategy'; + +@Injectable() +export class EthereumCoinStrategy extends EvmCoinStrategy { + constructor(dexEthereumService: DexEthereumService) { + super(dexEthereumService); + } +} diff --git a/src/payment/models/dex/strategies/check-liquidity/impl/ethereum-token.strategy.ts b/src/payment/models/dex/strategies/check-liquidity/impl/ethereum-token.strategy.ts new file mode 100644 index 0000000000..55ee9d5635 --- /dev/null +++ b/src/payment/models/dex/strategies/check-liquidity/impl/ethereum-token.strategy.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@nestjs/common'; +import { DexEthereumService } from '../../../services/dex-ethereum.service'; +import { EvmTokenStrategy } from './base/evm-token.strategy'; + +@Injectable() +export class EthereumTokenStrategy extends EvmTokenStrategy { + constructor(dexEthereumService: DexEthereumService) { + super(dexEthereumService); + } +} diff --git a/src/payment/models/dex/strategies/purchase-liquidity/__tests__/purchase-liquidity.facade.spec.ts b/src/payment/models/dex/strategies/purchase-liquidity/__tests__/purchase-liquidity.facade.spec.ts new file mode 100644 index 0000000000..54fd72c8ed --- /dev/null +++ b/src/payment/models/dex/strategies/purchase-liquidity/__tests__/purchase-liquidity.facade.spec.ts @@ -0,0 +1,288 @@ +import { mock } from 'jest-mock-extended'; +import { BehaviorSubject } from 'rxjs'; +import { NodeService } from 'src/blockchain/ain/node/node.service'; +import { Blockchain } from 'src/blockchain/shared/enums/blockchain.enum'; +import { AssetCategory, AssetType } from 'src/shared/models/asset/asset.entity'; +import { AssetService } from 'src/shared/models/asset/asset.service'; +import { createCustomAsset } from 'src/shared/models/asset/__mocks__/asset.entity.mock'; +import { SettingService } from 'src/shared/models/setting/setting.service'; +import { MailService } from 'src/shared/services/mail.service'; +import { LiquidityOrderFactory } from '../../../factories/liquidity-order.factory'; +import { LiquidityOrderRepository } from '../../../repositories/liquidity-order.repository'; +import { DexBitcoinService } from '../../../services/dex-bitcoin.service'; +import { DexBscService } from '../../../services/dex-bsc.service'; +import { DexDeFiChainService } from '../../../services/dex-defichain.service'; +import { DexService } from '../../../services/dex.service'; +import { BitcoinStrategy } from '../impl/bitcoin.strategy'; +import { BscCoinStrategy } from '../impl/bsc-coin.strategy'; +import { BscTokenStrategy } from '../impl/bsc-token.strategy'; +import { DeFiChainCryptoStrategy } from '../impl/defichain-crypto.strategy'; +import { DeFiChainPoolPairStrategy } from '../impl/defichain-poolpair.strategy'; +import { DeFiChainStockStrategy } from '../impl/defichain-stock.strategy'; +import { EthereumCoinStrategy } from '../impl/ethereum-coin.strategy'; +import { EthereumTokenStrategy } from '../impl/ethereum-token.strategy'; +import { PurchaseLiquidityStrategyAlias, PurchaseLiquidityStrategies } from '../purchase-liquidity.facade'; + +describe('PurchaseLiquidityStrategies', () => { + let nodeService: NodeService; + + let bitcoin: BitcoinStrategy; + let bscCoin: BscCoinStrategy; + let bscToken: BscTokenStrategy; + let deFiChainPoolPair: DeFiChainPoolPairStrategy; + let deFiChainStock: DeFiChainStockStrategy; + let deFiChainCrypto: DeFiChainCryptoStrategy; + let ethereumCoin: EthereumCoinStrategy; + let ethereumToken: EthereumTokenStrategy; + + let facade: PurchaseLiquidityStrategiesWrapper; + + beforeEach(() => { + nodeService = mock(); + jest.spyOn(nodeService, 'getConnectedNode').mockImplementation(() => new BehaviorSubject(null)); + + bitcoin = new BitcoinStrategy(mock(), mock()); + bscCoin = new BscCoinStrategy(mock(), mock()); + bscToken = new BscTokenStrategy(mock(), mock()); + + deFiChainPoolPair = new DeFiChainPoolPairStrategy( + nodeService, + mock(), + mock(), + mock(), + mock(), + mock(), + mock(), + ); + deFiChainStock = new DeFiChainStockStrategy( + mock(), + mock(), + mock(), + mock(), + ); + deFiChainCrypto = new DeFiChainCryptoStrategy( + mock(), + mock(), + mock(), + mock(), + ); + ethereumCoin = new EthereumCoinStrategy(mock(), mock()); + ethereumToken = new EthereumTokenStrategy(mock(), mock()); + + facade = new PurchaseLiquidityStrategiesWrapper( + bitcoin, + bscCoin, + bscToken, + deFiChainCrypto, + deFiChainPoolPair, + deFiChainStock, + ethereumCoin, + ethereumToken, + ); + }); + + describe('#constructor(...)', () => { + it('adds all purchaseLiquidityStrategies to a map', () => { + expect([...facade.getStrategies().entries()].length).toBe(8); + }); + + it('assigns strategies to all aliases', () => { + expect([...facade.getStrategies().entries()].length).toBe(Object.values(PurchaseLiquidityStrategyAlias).length); + }); + + it('sets all required purchaseLiquidityStrategies aliases', () => { + const aliases = [...facade.getStrategies().keys()]; + + expect(aliases.includes(PurchaseLiquidityStrategyAlias.BITCOIN)).toBe(true); + expect(aliases.includes(PurchaseLiquidityStrategyAlias.BSC_COIN)).toBe(true); + expect(aliases.includes(PurchaseLiquidityStrategyAlias.BSC_TOKEN)).toBe(true); + expect(aliases.includes(PurchaseLiquidityStrategyAlias.DEFICHAIN_POOL_PAIR)).toBe(true); + expect(aliases.includes(PurchaseLiquidityStrategyAlias.DEFICHAIN_STOCK)).toBe(true); + expect(aliases.includes(PurchaseLiquidityStrategyAlias.DEFICHAIN_CRYPTO)).toBe(true); + expect(aliases.includes(PurchaseLiquidityStrategyAlias.ETHEREUM_COIN)).toBe(true); + expect(aliases.includes(PurchaseLiquidityStrategyAlias.ETHEREUM_TOKEN)).toBe(true); + }); + + it('assigns proper purchaseLiquidityStrategies to aliases', () => { + expect(facade.getStrategies().get(PurchaseLiquidityStrategyAlias.BITCOIN)).toBeInstanceOf(BitcoinStrategy); + expect(facade.getStrategies().get(PurchaseLiquidityStrategyAlias.BSC_COIN)).toBeInstanceOf(BscCoinStrategy); + expect(facade.getStrategies().get(PurchaseLiquidityStrategyAlias.BSC_TOKEN)).toBeInstanceOf(BscTokenStrategy); + expect(facade.getStrategies().get(PurchaseLiquidityStrategyAlias.DEFICHAIN_CRYPTO)).toBeInstanceOf( + DeFiChainCryptoStrategy, + ); + expect(facade.getStrategies().get(PurchaseLiquidityStrategyAlias.DEFICHAIN_POOL_PAIR)).toBeInstanceOf( + DeFiChainPoolPairStrategy, + ); + expect(facade.getStrategies().get(PurchaseLiquidityStrategyAlias.DEFICHAIN_STOCK)).toBeInstanceOf( + DeFiChainStockStrategy, + ); + expect(facade.getStrategies().get(PurchaseLiquidityStrategyAlias.ETHEREUM_COIN)).toBeInstanceOf( + EthereumCoinStrategy, + ); + expect(facade.getStrategies().get(PurchaseLiquidityStrategyAlias.ETHEREUM_TOKEN)).toBeInstanceOf( + EthereumTokenStrategy, + ); + }); + }); + + describe('#getPurchaseLiquidityStrategy(...)', () => { + describe('getting strategy by Asset', () => { + it('gets BITCOIN strategy for BITCOIN Crypto', () => { + const strategy = facade.getPurchaseLiquidityStrategy(createCustomAsset({ blockchain: Blockchain.BITCOIN })); + + expect(strategy).toBeInstanceOf(BitcoinStrategy); + }); + + it('gets BSC_COIN strategy', () => { + const strategy = facade.getPurchaseLiquidityStrategy( + createCustomAsset({ blockchain: Blockchain.BINANCE_SMART_CHAIN, type: AssetType.COIN }), + ); + + expect(strategy).toBeInstanceOf(BscCoinStrategy); + }); + + it('gets BSC_TOKEN strategy', () => { + const strategy = facade.getPurchaseLiquidityStrategy( + createCustomAsset({ blockchain: Blockchain.BINANCE_SMART_CHAIN, type: AssetType.TOKEN }), + ); + + expect(strategy).toBeInstanceOf(BscTokenStrategy); + }); + + it('gets DEFICHAIN_CRYPTO strategy for DEFICHAIN Crypto', () => { + const strategy = facade.getPurchaseLiquidityStrategy( + createCustomAsset({ blockchain: Blockchain.DEFICHAIN, category: AssetCategory.CRYPTO }), + ); + + expect(strategy).toBeInstanceOf(DeFiChainCryptoStrategy); + }); + + it('gets DEFICHAIN_POOL_PAIR strategy for DEFICHAIN Pool Pair', () => { + const strategy = facade.getPurchaseLiquidityStrategy( + createCustomAsset({ blockchain: Blockchain.DEFICHAIN, category: AssetCategory.POOL_PAIR }), + ); + + expect(strategy).toBeInstanceOf(DeFiChainPoolPairStrategy); + }); + + it('gets DEFICHAIN_STOCK strategy for DEFICHAIN Stock', () => { + const strategy = facade.getPurchaseLiquidityStrategy( + createCustomAsset({ blockchain: Blockchain.DEFICHAIN, category: AssetCategory.STOCK }), + ); + + expect(strategy).toBeInstanceOf(DeFiChainStockStrategy); + }); + + it('gets ETHEREUM_COIN strategy', () => { + const strategy = facade.getPurchaseLiquidityStrategy( + createCustomAsset({ blockchain: Blockchain.ETHEREUM, type: AssetType.COIN }), + ); + + expect(strategy).toBeInstanceOf(EthereumCoinStrategy); + }); + + it('gets ETHEREUM_TOKEN strategy', () => { + const strategy = facade.getPurchaseLiquidityStrategy( + createCustomAsset({ blockchain: Blockchain.ETHEREUM, type: AssetType.TOKEN }), + ); + + expect(strategy).toBeInstanceOf(EthereumTokenStrategy); + }); + + it('fails to get strategy for non-supported Blockchain', () => { + const testCall = () => + facade.getPurchaseLiquidityStrategy(createCustomAsset({ blockchain: 'NewBlockchain' as Blockchain })); + + expect(testCall).toThrow(); + expect(testCall).toThrowError('No PurchaseLiquidityStrategy found. Alias: undefined'); + }); + + it('fails to get strategy for non-supported AssetCategory', () => { + const testCall = () => + facade.getPurchaseLiquidityStrategy( + createCustomAsset({ blockchain: Blockchain.DEFICHAIN, category: 'NewCategory' as AssetCategory }), + ); + + expect(testCall).toThrow(); + expect(testCall).toThrowError('No PurchaseLiquidityStrategy found. Alias: undefined'); + }); + }); + + describe('getting strategy by Alias', () => { + it('gets BITCOIN strategy', () => { + const strategy = facade.getPurchaseLiquidityStrategy(PurchaseLiquidityStrategyAlias.BITCOIN); + + expect(strategy).toBeInstanceOf(BitcoinStrategy); + }); + + it('gets BSC_COIN strategy', () => { + const strategy = facade.getPurchaseLiquidityStrategy(PurchaseLiquidityStrategyAlias.BSC_COIN); + + expect(strategy).toBeInstanceOf(BscCoinStrategy); + }); + + it('gets BSC_TOKEN strategy', () => { + const strategy = facade.getPurchaseLiquidityStrategy(PurchaseLiquidityStrategyAlias.BSC_TOKEN); + + expect(strategy).toBeInstanceOf(BscTokenStrategy); + }); + + it('gets DEFICHAIN_CRYPTO strategy', () => { + const strategyCrypto = facade.getPurchaseLiquidityStrategy(PurchaseLiquidityStrategyAlias.DEFICHAIN_CRYPTO); + + expect(strategyCrypto).toBeInstanceOf(DeFiChainCryptoStrategy); + }); + + it('gets DEFICHAIN_POOL_PAIR strategy', () => { + const strategy = facade.getPurchaseLiquidityStrategy(PurchaseLiquidityStrategyAlias.DEFICHAIN_POOL_PAIR); + + expect(strategy).toBeInstanceOf(DeFiChainPoolPairStrategy); + }); + + it('gets DEFICHAIN_STOCK strategy', () => { + const strategyCrypto = facade.getPurchaseLiquidityStrategy(PurchaseLiquidityStrategyAlias.DEFICHAIN_STOCK); + + expect(strategyCrypto).toBeInstanceOf(DeFiChainStockStrategy); + }); + + it('gets ETHEREUM_COIN strategy', () => { + const strategy = facade.getPurchaseLiquidityStrategy(PurchaseLiquidityStrategyAlias.ETHEREUM_COIN); + + expect(strategy).toBeInstanceOf(EthereumCoinStrategy); + }); + + it('gets ETHEREUM_TOKEN strategy', () => { + const strategy = facade.getPurchaseLiquidityStrategy(PurchaseLiquidityStrategyAlias.ETHEREUM_TOKEN); + + expect(strategy).toBeInstanceOf(EthereumTokenStrategy); + }); + + it('fails to get strategy for non-supported Alias', () => { + const testCall = () => + facade.getPurchaseLiquidityStrategy('NonExistingAlias' as PurchaseLiquidityStrategyAlias); + + expect(testCall).toThrow(); + expect(testCall).toThrowError('No PurchaseLiquidityStrategy found. Alias: NonExistingAlias'); + }); + }); + }); +}); + +class PurchaseLiquidityStrategiesWrapper extends PurchaseLiquidityStrategies { + constructor( + bitcoin: BitcoinStrategy, + bscCoin: BscCoinStrategy, + bscToken: BscTokenStrategy, + deFiChainCrypto: DeFiChainCryptoStrategy, + deFiChainPoolPair: DeFiChainPoolPairStrategy, + deFiChainStock: DeFiChainStockStrategy, + ethereumCoin: EthereumCoinStrategy, + ethereumToken: EthereumTokenStrategy, + ) { + super(bitcoin, bscCoin, bscToken, deFiChainCrypto, deFiChainPoolPair, deFiChainStock, ethereumCoin, ethereumToken); + } + + getStrategies() { + return this.strategies; + } +} diff --git a/src/payment/models/dex/strategies/purchase-liquidity/base/purchase-liquidity-defichain-non-poolpair.strategy.ts b/src/payment/models/dex/strategies/purchase-liquidity/impl/base/defichain-non-poolpair.strategy.ts similarity index 82% rename from src/payment/models/dex/strategies/purchase-liquidity/base/purchase-liquidity-defichain-non-poolpair.strategy.ts rename to src/payment/models/dex/strategies/purchase-liquidity/impl/base/defichain-non-poolpair.strategy.ts index 4571560986..fc747b1103 100644 --- a/src/payment/models/dex/strategies/purchase-liquidity/base/purchase-liquidity-defichain-non-poolpair.strategy.ts +++ b/src/payment/models/dex/strategies/purchase-liquidity/impl/base/defichain-non-poolpair.strategy.ts @@ -1,15 +1,15 @@ import { Blockchain } from 'src/blockchain/shared/enums/blockchain.enum'; import { AssetCategory } from 'src/shared/models/asset/asset.entity'; import { MailService } from 'src/shared/services/mail.service'; -import { LiquidityOrder } from '../../../entities/liquidity-order.entity'; -import { NotEnoughLiquidityException } from '../../../exceptions/not-enough-liquidity.exception'; -import { LiquidityOrderFactory } from '../../../factories/liquidity-order.factory'; -import { LiquidityRequest } from '../../../interfaces'; -import { LiquidityOrderRepository } from '../../../repositories/liquidity-order.repository'; -import { DexDeFiChainService } from '../../../services/dex-defichain.service'; +import { LiquidityOrder } from '../../../../entities/liquidity-order.entity'; +import { NotEnoughLiquidityException } from '../../../../exceptions/not-enough-liquidity.exception'; +import { LiquidityOrderFactory } from '../../../../factories/liquidity-order.factory'; +import { LiquidityRequest } from '../../../../interfaces'; +import { LiquidityOrderRepository } from '../../../../repositories/liquidity-order.repository'; +import { DexDeFiChainService } from '../../../../services/dex-defichain.service'; import { PurchaseLiquidityStrategy } from './purchase-liquidity.strategy'; -export abstract class PurchaseLiquidityDeFiChainNonPoolPairStrategy extends PurchaseLiquidityStrategy { +export abstract class DeFiChainNonPoolPairStrategy extends PurchaseLiquidityStrategy { private prioritySwapAssets: string[] = []; constructor( diff --git a/src/payment/models/dex/strategies/purchase-liquidity/base/purchase-liquidity-evm.strategy.ts b/src/payment/models/dex/strategies/purchase-liquidity/impl/base/evm-coin.strategy.ts similarity index 64% rename from src/payment/models/dex/strategies/purchase-liquidity/base/purchase-liquidity-evm.strategy.ts rename to src/payment/models/dex/strategies/purchase-liquidity/impl/base/evm-coin.strategy.ts index eb69befad3..f5a34374c4 100644 --- a/src/payment/models/dex/strategies/purchase-liquidity/base/purchase-liquidity-evm.strategy.ts +++ b/src/payment/models/dex/strategies/purchase-liquidity/impl/base/evm-coin.strategy.ts @@ -1,19 +1,20 @@ import { MailService } from 'src/shared/services/mail.service'; -import { LiquidityRequest } from '../../../interfaces'; -import { DexEvmService } from '../../../services/dex-evm.service'; +import { LiquidityRequest } from '../../../../interfaces'; +import { DexEvmService } from '../../../../services/dex-evm.service'; import { PurchaseLiquidityStrategy } from './purchase-liquidity.strategy'; -export class PurchaseLiquidityEvmStrategy extends PurchaseLiquidityStrategy { +export class EvmCoinStrategy extends PurchaseLiquidityStrategy { constructor(mailService: MailService, protected readonly dexEvmService: DexEvmService) { super(mailService); } async purchaseLiquidity(request: LiquidityRequest): Promise { const { referenceAsset, referenceAmount, context, correlationId } = request; + try { // should always throw, even if there is amount, additional check is done for API consistency and sending mail if (referenceAsset === this.dexEvmService._nativeCoin) { - const amount = await this.dexEvmService.checkCoinAvailability(referenceAmount); + const amount = await this.dexEvmService.checkNativeCoinAvailability(referenceAmount); if (amount) { throw new Error( @@ -22,9 +23,9 @@ export class PurchaseLiquidityEvmStrategy extends PurchaseLiquidityStrategy { } } - // throw by default, only native coin trading enabled + // throw by default, only native coin is enabled as a referenceAsset throw new Error( - `Only native coins are supported by EVM PurchaseLiquidity strategy. Provided reference asset: ${referenceAsset} Context: ${context}. CorrelationID: ${correlationId}`, + `Only native coin reference is supported by EVM PurchaseLiquidity strategy. Provided reference asset: ${referenceAsset} Context: ${context}. CorrelationID: ${correlationId}`, ); } catch (e) { await this.handlePurchaseLiquidityError(e, request); diff --git a/src/payment/models/dex/strategies/purchase-liquidity/impl/base/evm-token.strategy.ts b/src/payment/models/dex/strategies/purchase-liquidity/impl/base/evm-token.strategy.ts new file mode 100644 index 0000000000..77abb7bffd --- /dev/null +++ b/src/payment/models/dex/strategies/purchase-liquidity/impl/base/evm-token.strategy.ts @@ -0,0 +1,31 @@ +import { MailService } from 'src/shared/services/mail.service'; +import { LiquidityRequest } from '../../../../interfaces'; +import { DexEvmService } from '../../../../services/dex-evm.service'; +import { PurchaseLiquidityStrategy } from './purchase-liquidity.strategy'; + +export class EvmTokenStrategy extends PurchaseLiquidityStrategy { + constructor(mailService: MailService, protected readonly dexEvmService: DexEvmService) { + super(mailService); + } + + async purchaseLiquidity(request: LiquidityRequest): Promise { + const { referenceAsset, referenceAmount, targetAsset, context, correlationId } = request; + + try { + // should always throw, even if there is amount, additional check is done for API consistency and sending mail + const amount = await this.dexEvmService.getAndCheckTokenAvailability( + referenceAsset, + referenceAmount, + targetAsset, + ); + + if (amount) { + throw new Error( + `Requested ${referenceAsset} liquidity is already available on the wallet. No purchase required, retry checkLiquidity. Context: ${context}. CorrelationID: ${correlationId}`, + ); + } + } catch (e) { + await this.handlePurchaseLiquidityError(e, request); + } + } +} diff --git a/src/payment/models/dex/strategies/purchase-liquidity/base/purchase-liquidity.strategy.ts b/src/payment/models/dex/strategies/purchase-liquidity/impl/base/purchase-liquidity.strategy.ts similarity index 75% rename from src/payment/models/dex/strategies/purchase-liquidity/base/purchase-liquidity.strategy.ts rename to src/payment/models/dex/strategies/purchase-liquidity/impl/base/purchase-liquidity.strategy.ts index eb7c901319..1a09e83d46 100644 --- a/src/payment/models/dex/strategies/purchase-liquidity/base/purchase-liquidity.strategy.ts +++ b/src/payment/models/dex/strategies/purchase-liquidity/impl/base/purchase-liquidity.strategy.ts @@ -1,7 +1,7 @@ import { MailService } from 'src/shared/services/mail.service'; -import { NotEnoughLiquidityException } from '../../../exceptions/not-enough-liquidity.exception'; -import { PriceSlippageException } from '../../../exceptions/price-slippage.exception'; -import { LiquidityRequest } from '../../../interfaces'; +import { NotEnoughLiquidityException } from '../../../../exceptions/not-enough-liquidity.exception'; +import { PriceSlippageException } from '../../../../exceptions/price-slippage.exception'; +import { LiquidityRequest } from '../../../../interfaces'; export abstract class PurchaseLiquidityStrategy { constructor(protected readonly mailService: MailService) {} diff --git a/src/payment/models/dex/strategies/purchase-liquidity/impl/bitcoin.strategy.ts b/src/payment/models/dex/strategies/purchase-liquidity/impl/bitcoin.strategy.ts new file mode 100644 index 0000000000..c7630fb6d8 --- /dev/null +++ b/src/payment/models/dex/strategies/purchase-liquidity/impl/bitcoin.strategy.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@nestjs/common'; +import { PurchaseLiquidityStrategy } from './base/purchase-liquidity.strategy'; +import { LiquidityRequest } from '../../../interfaces'; +import { MailService } from 'src/shared/services/mail.service'; +import { DexBitcoinService } from '../../../services/dex-bitcoin.service'; + +@Injectable() +export class BitcoinStrategy extends PurchaseLiquidityStrategy { + constructor(mailService: MailService, private readonly dexBtcService: DexBitcoinService) { + super(mailService); + } + + async purchaseLiquidity(request: LiquidityRequest): Promise { + const { referenceAsset, referenceAmount, context, correlationId } = request; + try { + // should always throw, even if there is amount, additional check is done for API consistency and sending mail + const amount = await this.dexBtcService.checkAvailableTargetLiquidity(referenceAmount); + + if (amount) { + throw new Error( + `Requested ${referenceAsset} liquidity is already available on the wallet. No purchase required, retry checkLiquidity. Context: ${context}. CorrelationID: ${correlationId}`, + ); + } + } catch (e) { + await this.handlePurchaseLiquidityError(e, request); + } + } +} diff --git a/src/payment/models/dex/strategies/purchase-liquidity/purchase-liquidity-bsc.strategy.ts b/src/payment/models/dex/strategies/purchase-liquidity/impl/bsc-coin.strategy.ts similarity index 50% rename from src/payment/models/dex/strategies/purchase-liquidity/purchase-liquidity-bsc.strategy.ts rename to src/payment/models/dex/strategies/purchase-liquidity/impl/bsc-coin.strategy.ts index 0a3b02e48b..6cf0964362 100644 --- a/src/payment/models/dex/strategies/purchase-liquidity/purchase-liquidity-bsc.strategy.ts +++ b/src/payment/models/dex/strategies/purchase-liquidity/impl/bsc-coin.strategy.ts @@ -1,10 +1,10 @@ import { Injectable } from '@nestjs/common'; import { MailService } from 'src/shared/services/mail.service'; -import { DexBscService } from '../../services/dex-bsc.service'; -import { PurchaseLiquidityEvmStrategy } from './base/purchase-liquidity-evm.strategy'; +import { DexBscService } from '../../../services/dex-bsc.service'; +import { EvmCoinStrategy } from './base/evm-coin.strategy'; @Injectable() -export class PurchaseLiquidityBscStrategy extends PurchaseLiquidityEvmStrategy { +export class BscCoinStrategy extends EvmCoinStrategy { constructor(mailService: MailService, dexBscService: DexBscService) { super(mailService, dexBscService); } diff --git a/src/payment/models/dex/strategies/purchase-liquidity/impl/bsc-token.strategy.ts b/src/payment/models/dex/strategies/purchase-liquidity/impl/bsc-token.strategy.ts new file mode 100644 index 0000000000..5355c3d1b2 --- /dev/null +++ b/src/payment/models/dex/strategies/purchase-liquidity/impl/bsc-token.strategy.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { MailService } from 'src/shared/services/mail.service'; +import { DexBscService } from '../../../services/dex-bsc.service'; +import { EvmTokenStrategy } from './base/evm-token.strategy'; + +@Injectable() +export class BscTokenStrategy extends EvmTokenStrategy { + constructor(mailService: MailService, dexBscService: DexBscService) { + super(mailService, dexBscService); + } +} diff --git a/src/payment/models/dex/strategies/purchase-liquidity/impl/defichain-crypto.strategy.ts b/src/payment/models/dex/strategies/purchase-liquidity/impl/defichain-crypto.strategy.ts new file mode 100644 index 0000000000..461ee6a396 --- /dev/null +++ b/src/payment/models/dex/strategies/purchase-liquidity/impl/defichain-crypto.strategy.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@nestjs/common'; +import { MailService } from 'src/shared/services/mail.service'; +import { LiquidityOrderFactory } from '../../../factories/liquidity-order.factory'; +import { LiquidityOrderRepository } from '../../../repositories/liquidity-order.repository'; +import { DexDeFiChainService } from '../../../services/dex-defichain.service'; +import { DeFiChainNonPoolPairStrategy } from './base/defichain-non-poolpair.strategy'; + +@Injectable() +export class DeFiChainCryptoStrategy extends DeFiChainNonPoolPairStrategy { + constructor( + readonly mailService: MailService, + readonly dexDeFiChainService: DexDeFiChainService, + readonly liquidityOrderRepo: LiquidityOrderRepository, + readonly liquidityOrderFactory: LiquidityOrderFactory, + ) { + super(mailService, dexDeFiChainService, liquidityOrderRepo, liquidityOrderFactory, ['DFI']); + } +} diff --git a/src/payment/models/dex/strategies/purchase-liquidity/purchase-liquidity-defichain-poolpair.strategy.ts b/src/payment/models/dex/strategies/purchase-liquidity/impl/defichain-poolpair.strategy.ts similarity index 91% rename from src/payment/models/dex/strategies/purchase-liquidity/purchase-liquidity-defichain-poolpair.strategy.ts rename to src/payment/models/dex/strategies/purchase-liquidity/impl/defichain-poolpair.strategy.ts index 7d3799cc36..49ea4d07c0 100644 --- a/src/payment/models/dex/strategies/purchase-liquidity/purchase-liquidity-defichain-poolpair.strategy.ts +++ b/src/payment/models/dex/strategies/purchase-liquidity/impl/defichain-poolpair.strategy.ts @@ -6,22 +6,22 @@ import { Config } from 'src/config/config'; import { Asset, AssetCategory } from 'src/shared/models/asset/asset.entity'; import { AssetService } from 'src/shared/models/asset/asset.service'; import { MailService } from 'src/shared/services/mail.service'; -import { LiquidityOrder, LiquidityOrderContext } from '../../entities/liquidity-order.entity'; -import { LiquidityOrderFactory } from '../../factories/liquidity-order.factory'; -import { LiquidityOrderRepository } from '../../repositories/liquidity-order.repository'; -import { DexService } from '../../services/dex.service'; -import { PurchaseLiquidityStrategy } from './base/purchase-liquidity.strategy'; import { Util } from 'src/shared/util'; import { Lock } from 'src/shared/lock'; -import { NotEnoughLiquidityException } from '../../exceptions/not-enough-liquidity.exception'; -import { PriceSlippageException } from '../../exceptions/price-slippage.exception'; import { SettingService } from 'src/shared/models/setting/setting.service'; import { NodeService, NodeType } from 'src/blockchain/ain/node/node.service'; import { Blockchain } from 'src/blockchain/shared/enums/blockchain.enum'; -import { LiquidityRequest } from '../../interfaces'; +import { LiquidityOrderContext, LiquidityOrder } from '../../../entities/liquidity-order.entity'; +import { NotEnoughLiquidityException } from '../../../exceptions/not-enough-liquidity.exception'; +import { PriceSlippageException } from '../../../exceptions/price-slippage.exception'; +import { LiquidityOrderFactory } from '../../../factories/liquidity-order.factory'; +import { LiquidityRequest } from '../../../interfaces'; +import { LiquidityOrderRepository } from '../../../repositories/liquidity-order.repository'; +import { DexService } from '../../../services/dex.service'; +import { PurchaseLiquidityStrategy } from './base/purchase-liquidity.strategy'; @Injectable() -export class PurchaseLiquidityDeFiChainPoolPairStrategy extends PurchaseLiquidityStrategy { +export class DeFiChainPoolPairStrategy extends PurchaseLiquidityStrategy { private readonly verifyDerivedOrdersLock = new Lock(1800); private chainClient: DeFiClient; diff --git a/src/payment/models/dex/strategies/purchase-liquidity/impl/defichain-stock.strategy.ts b/src/payment/models/dex/strategies/purchase-liquidity/impl/defichain-stock.strategy.ts new file mode 100644 index 0000000000..5b157b920c --- /dev/null +++ b/src/payment/models/dex/strategies/purchase-liquidity/impl/defichain-stock.strategy.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@nestjs/common'; +import { MailService } from 'src/shared/services/mail.service'; +import { LiquidityOrderFactory } from '../../../factories/liquidity-order.factory'; +import { LiquidityOrderRepository } from '../../../repositories/liquidity-order.repository'; +import { DexDeFiChainService } from '../../../services/dex-defichain.service'; +import { DeFiChainNonPoolPairStrategy } from './base/defichain-non-poolpair.strategy'; + +@Injectable() +export class DeFiChainStockStrategy extends DeFiChainNonPoolPairStrategy { + constructor( + readonly mailService: MailService, + readonly dexDeFiChainService: DexDeFiChainService, + readonly liquidityOrderRepo: LiquidityOrderRepository, + readonly liquidityOrderFactory: LiquidityOrderFactory, + ) { + super(mailService, dexDeFiChainService, liquidityOrderRepo, liquidityOrderFactory, ['DUSD', 'DFI']); + } +} diff --git a/src/payment/models/dex/strategies/purchase-liquidity/purchase-liquidity-ethereum.strategy.ts b/src/payment/models/dex/strategies/purchase-liquidity/impl/ethereum-coin.strategy.ts similarity index 50% rename from src/payment/models/dex/strategies/purchase-liquidity/purchase-liquidity-ethereum.strategy.ts rename to src/payment/models/dex/strategies/purchase-liquidity/impl/ethereum-coin.strategy.ts index 149caa473e..794707cf53 100644 --- a/src/payment/models/dex/strategies/purchase-liquidity/purchase-liquidity-ethereum.strategy.ts +++ b/src/payment/models/dex/strategies/purchase-liquidity/impl/ethereum-coin.strategy.ts @@ -1,10 +1,10 @@ import { Injectable } from '@nestjs/common'; import { MailService } from 'src/shared/services/mail.service'; -import { DexEthereumService } from '../../services/dex-ethereum.service'; -import { PurchaseLiquidityEvmStrategy } from './base/purchase-liquidity-evm.strategy'; +import { DexEthereumService } from '../../../services/dex-ethereum.service'; +import { EvmCoinStrategy } from './base/evm-coin.strategy'; @Injectable() -export class PurchaseLiquidityEthereumStrategy extends PurchaseLiquidityEvmStrategy { +export class EthereumCoinStrategy extends EvmCoinStrategy { constructor(mailService: MailService, dexEthereumService: DexEthereumService) { super(mailService, dexEthereumService); } diff --git a/src/payment/models/dex/strategies/purchase-liquidity/impl/ethereum-token.strategy.ts b/src/payment/models/dex/strategies/purchase-liquidity/impl/ethereum-token.strategy.ts new file mode 100644 index 0000000000..99ff15ba34 --- /dev/null +++ b/src/payment/models/dex/strategies/purchase-liquidity/impl/ethereum-token.strategy.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { MailService } from 'src/shared/services/mail.service'; +import { DexEthereumService } from '../../../services/dex-ethereum.service'; +import { EvmTokenStrategy } from './base/evm-token.strategy'; + +@Injectable() +export class EthereumTokenStrategy extends EvmTokenStrategy { + constructor(mailService: MailService, dexEthereumService: DexEthereumService) { + super(mailService, dexEthereumService); + } +} diff --git a/src/payment/models/dex/strategies/purchase-liquidity/purchase-liquidity-defichain-crypto.strategy.ts b/src/payment/models/dex/strategies/purchase-liquidity/purchase-liquidity-defichain-crypto.strategy.ts deleted file mode 100644 index 252ae58d48..0000000000 --- a/src/payment/models/dex/strategies/purchase-liquidity/purchase-liquidity-defichain-crypto.strategy.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { LiquidityOrderRepository } from '../../repositories/liquidity-order.repository'; -import { DexDeFiChainService } from '../../services/dex-defichain.service'; -import { MailService } from 'src/shared/services/mail.service'; -import { LiquidityOrderFactory } from '../../factories/liquidity-order.factory'; -import { PurchaseLiquidityDeFiChainNonPoolPairStrategy } from './base/purchase-liquidity-defichain-non-poolpair.strategy'; - -@Injectable() -export class PurchaseLiquidityDeFiChainCryptoStrategy extends PurchaseLiquidityDeFiChainNonPoolPairStrategy { - constructor( - readonly mailService: MailService, - readonly dexDeFiChainService: DexDeFiChainService, - readonly liquidityOrderRepo: LiquidityOrderRepository, - readonly liquidityOrderFactory: LiquidityOrderFactory, - ) { - super(mailService, dexDeFiChainService, liquidityOrderRepo, liquidityOrderFactory, ['DFI']); - } -} diff --git a/src/payment/models/dex/strategies/purchase-liquidity/purchase-liquidity-defichain-stock.strategy.ts b/src/payment/models/dex/strategies/purchase-liquidity/purchase-liquidity-defichain-stock.strategy.ts deleted file mode 100644 index 3fa83d3d3f..0000000000 --- a/src/payment/models/dex/strategies/purchase-liquidity/purchase-liquidity-defichain-stock.strategy.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { LiquidityOrderRepository } from '../../repositories/liquidity-order.repository'; -import { DexDeFiChainService } from '../../services/dex-defichain.service'; -import { MailService } from 'src/shared/services/mail.service'; -import { LiquidityOrderFactory } from '../../factories/liquidity-order.factory'; -import { PurchaseLiquidityDeFiChainNonPoolPairStrategy } from './base/purchase-liquidity-defichain-non-poolpair.strategy'; - -@Injectable() -export class PurchaseLiquidityDeFiChainStockStrategy extends PurchaseLiquidityDeFiChainNonPoolPairStrategy { - constructor( - readonly mailService: MailService, - readonly dexDeFiChainService: DexDeFiChainService, - readonly liquidityOrderRepo: LiquidityOrderRepository, - readonly liquidityOrderFactory: LiquidityOrderFactory, - ) { - super(mailService, dexDeFiChainService, liquidityOrderRepo, liquidityOrderFactory, ['DUSD', 'DFI']); - } -} diff --git a/src/payment/models/dex/strategies/purchase-liquidity/purchase-liquidity.facade.ts b/src/payment/models/dex/strategies/purchase-liquidity/purchase-liquidity.facade.ts new file mode 100644 index 0000000000..b5ef8b4476 --- /dev/null +++ b/src/payment/models/dex/strategies/purchase-liquidity/purchase-liquidity.facade.ts @@ -0,0 +1,91 @@ +import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { Blockchain } from 'src/blockchain/shared/enums/blockchain.enum'; +import { Asset, AssetCategory, AssetType } from 'src/shared/models/asset/asset.entity'; +import { BscCoinStrategy } from './impl/bsc-coin.strategy'; +import { DeFiChainCryptoStrategy } from './impl/defichain-crypto.strategy'; +import { EthereumCoinStrategy } from './impl/ethereum-coin.strategy'; +import { PurchaseLiquidityStrategy } from './impl/base/purchase-liquidity.strategy'; +import { DeFiChainPoolPairStrategy } from './impl/defichain-poolpair.strategy'; +import { DeFiChainStockStrategy } from './impl/defichain-stock.strategy'; +import { BscTokenStrategy } from './impl/bsc-token.strategy'; +import { BitcoinStrategy } from './impl/bitcoin.strategy'; +import { EthereumTokenStrategy } from './impl/ethereum-token.strategy'; + +enum Alias { + BITCOIN = 'Bitcoin', + BSC_COIN = 'BscCoin', + BSC_TOKEN = 'BscToken', + DEFICHAIN_POOL_PAIR = 'DeFiChainPoolPair', + DEFICHAIN_STOCK = 'DeFiChainStock', + DEFICHAIN_CRYPTO = 'DeFiChainCrypto', + ETHEREUM_COIN = 'EthereumCoin', + ETHEREUM_TOKEN = 'EthereumToken', +} + +export { Alias as PurchaseLiquidityStrategyAlias }; + +@Injectable() +export class PurchaseLiquidityStrategies { + protected readonly strategies = new Map(); + + constructor( + bitcoin: BitcoinStrategy, + bscCoin: BscCoinStrategy, + bscToken: BscTokenStrategy, + deFiChainCrypto: DeFiChainCryptoStrategy, + @Inject(forwardRef(() => DeFiChainPoolPairStrategy)) + deFiChainPoolPair: DeFiChainPoolPairStrategy, + deFiChainStock: DeFiChainStockStrategy, + ethereumCoin: EthereumCoinStrategy, + ethereumToken: EthereumTokenStrategy, + ) { + this.strategies.set(Alias.BITCOIN, bitcoin); + this.strategies.set(Alias.BSC_COIN, bscCoin); + this.strategies.set(Alias.BSC_TOKEN, bscToken); + this.strategies.set(Alias.DEFICHAIN_POOL_PAIR, deFiChainPoolPair); + this.strategies.set(Alias.DEFICHAIN_STOCK, deFiChainStock); + this.strategies.set(Alias.DEFICHAIN_CRYPTO, deFiChainCrypto); + this.strategies.set(Alias.ETHEREUM_COIN, ethereumCoin); + this.strategies.set(Alias.ETHEREUM_TOKEN, ethereumToken); + } + + getPurchaseLiquidityStrategy(criteria: Asset | Alias): PurchaseLiquidityStrategy { + return criteria instanceof Asset ? this.getByAsset(criteria) : this.getByAlias(criteria); + } + + //*** HELPER METHODS ***// + + private getByAlias(alias: Alias): PurchaseLiquidityStrategy { + const strategy = this.strategies.get(alias); + + if (!strategy) throw new Error(`No PurchaseLiquidityStrategy found. Alias: ${alias}`); + + return strategy; + } + + private getByAsset(asset: Asset): PurchaseLiquidityStrategy { + const alias = this.getAlias(asset); + + return this.getByAlias(alias); + } + + private getAlias(asset: Asset): Alias { + const { blockchain, category: assetCategory, type: assetType } = asset; + + if (blockchain === Blockchain.BITCOIN) return Alias.BITCOIN; + + if (blockchain === Blockchain.BINANCE_SMART_CHAIN) { + return assetType === AssetType.COIN ? Alias.BSC_COIN : Alias.BSC_TOKEN; + } + + if (blockchain === Blockchain.DEFICHAIN) { + if (assetCategory === AssetCategory.POOL_PAIR) return Alias.DEFICHAIN_POOL_PAIR; + if (assetCategory === AssetCategory.STOCK) return Alias.DEFICHAIN_STOCK; + if (assetCategory === AssetCategory.CRYPTO) return Alias.DEFICHAIN_CRYPTO; + } + + if (blockchain === Blockchain.ETHEREUM) { + return assetType === AssetType.COIN ? Alias.ETHEREUM_COIN : Alias.ETHEREUM_TOKEN; + } + } +} diff --git a/src/payment/models/dex/strategies/strategies.facade.ts b/src/payment/models/dex/strategies/strategies.facade.ts deleted file mode 100644 index 8741a375c1..0000000000 --- a/src/payment/models/dex/strategies/strategies.facade.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { forwardRef, Inject, Injectable } from '@nestjs/common'; -import { Blockchain } from 'src/blockchain/shared/enums/blockchain.enum'; -import { Asset, AssetCategory } from 'src/shared/models/asset/asset.entity'; -import { CheckLiquidityBscStrategy } from './check-liquidity/check-liquidity-bsc.strategy'; -import { CheckLiquidityDeFiChainDefaultStrategy } from './check-liquidity/check-liquidity-defichain-default.strategy'; -import { CheckLiquidityEthereumStrategy } from './check-liquidity/check-liquidity-ethereum.strategy'; -import { CheckLiquidityStrategy } from './check-liquidity/base/check-liquidity.strategy'; -import { CheckLiquidityDeFiChainPoolPairStrategy } from './check-liquidity/check-liquidity-defichain-poolpair.strategy'; -import { PurchaseLiquidityBscStrategy } from './purchase-liquidity/purchase-liquidity-bsc.strategy'; -import { PurchaseLiquidityDeFiChainCryptoStrategy } from './purchase-liquidity/purchase-liquidity-defichain-crypto.strategy'; -import { PurchaseLiquidityEthereumStrategy } from './purchase-liquidity/purchase-liquidity-ethereum.strategy'; -import { PurchaseLiquidityStrategy } from './purchase-liquidity/base/purchase-liquidity.strategy'; -import { PurchaseLiquidityDeFiChainPoolPairStrategy } from './purchase-liquidity/purchase-liquidity-defichain-poolpair.strategy'; -import { PurchaseLiquidityDeFiChainStockStrategy } from './purchase-liquidity/purchase-liquidity-defichain-stock.strategy'; - -export enum CheckLiquidityStrategyAlias { - DEFICHAIN_POOL_PAIR = 'DeFiChainPoolPair', - DEFICHAIN_DEFAULT = 'DeFiChainDefault', - ETHEREUM_DEFAULT = 'EthereumDefault', - BSC_DEFAULT = 'BscDefault', -} - -export enum PurchaseLiquidityStrategyAlias { - DEFICHAIN_POOL_PAIR = 'DeFiChainPoolPair', - DEFICHAIN_STOCK = 'DeFiChainStock', - DEFICHAIN_CRYPTO = 'DeFiChainCrypto', - ETHEREUM_DEFAULT = 'EthereumDefault', - BSC_DEFAULT = 'BscDefault', -} - -@Injectable() -export class DexStrategiesFacade { - protected readonly checkLiquidityStrategies = new Map(); - protected readonly purchaseLiquidityStrategies = new Map(); - - constructor( - checkLiquidityDeFiChainPoolPairStrategy: CheckLiquidityDeFiChainPoolPairStrategy, - checkLiquidityDeFiChainDefaultStrategy: CheckLiquidityDeFiChainDefaultStrategy, - checkLiquidityEthereumStrategy: CheckLiquidityEthereumStrategy, - checkLiquidityBscStrategy: CheckLiquidityBscStrategy, - @Inject(forwardRef(() => PurchaseLiquidityDeFiChainPoolPairStrategy)) - purchaseLiquidityDeFiChainPoolPairStrategy: PurchaseLiquidityDeFiChainPoolPairStrategy, - purchaseLiquidityDeFiChainStockStrategy: PurchaseLiquidityDeFiChainStockStrategy, - purchaseLiquidityDeFiChainCryptoStrategy: PurchaseLiquidityDeFiChainCryptoStrategy, - purchaseLiquidityEthereumStrategy: PurchaseLiquidityEthereumStrategy, - purchaseLiquidityBscStrategy: PurchaseLiquidityBscStrategy, - ) { - this.checkLiquidityStrategies.set( - CheckLiquidityStrategyAlias.DEFICHAIN_POOL_PAIR, - checkLiquidityDeFiChainPoolPairStrategy, - ); - - this.checkLiquidityStrategies.set( - CheckLiquidityStrategyAlias.DEFICHAIN_DEFAULT, - checkLiquidityDeFiChainDefaultStrategy, - ); - - this.checkLiquidityStrategies.set(CheckLiquidityStrategyAlias.ETHEREUM_DEFAULT, checkLiquidityEthereumStrategy); - - this.checkLiquidityStrategies.set(CheckLiquidityStrategyAlias.BSC_DEFAULT, checkLiquidityBscStrategy); - - this.purchaseLiquidityStrategies.set( - PurchaseLiquidityStrategyAlias.DEFICHAIN_POOL_PAIR, - purchaseLiquidityDeFiChainPoolPairStrategy, - ); - - this.purchaseLiquidityStrategies.set( - PurchaseLiquidityStrategyAlias.DEFICHAIN_STOCK, - purchaseLiquidityDeFiChainStockStrategy, - ); - - this.purchaseLiquidityStrategies.set( - PurchaseLiquidityStrategyAlias.DEFICHAIN_CRYPTO, - purchaseLiquidityDeFiChainCryptoStrategy, - ); - - this.purchaseLiquidityStrategies.set( - PurchaseLiquidityStrategyAlias.ETHEREUM_DEFAULT, - purchaseLiquidityEthereumStrategy, - ); - - this.purchaseLiquidityStrategies.set(PurchaseLiquidityStrategyAlias.BSC_DEFAULT, purchaseLiquidityBscStrategy); - } - - getCheckLiquidityStrategy(criteria: Asset | CheckLiquidityStrategyAlias): CheckLiquidityStrategy { - return criteria instanceof Asset - ? this.getCheckLiquidityStrategyByAsset(criteria) - : this.getCheckLiquidityStrategyByAlias(criteria); - } - - getPurchaseLiquidityStrategy(criteria: Asset | PurchaseLiquidityStrategyAlias): PurchaseLiquidityStrategy { - return criteria instanceof Asset - ? this.getPurchaseLiquidityStrategyByAsset(criteria) - : this.getPurchaseLiquidityStrategyByAlias(criteria); - } - - //*** HELPER METHODS ***// - - private getCheckLiquidityStrategyByAlias(alias: CheckLiquidityStrategyAlias): CheckLiquidityStrategy { - const strategy = this.checkLiquidityStrategies.get(alias); - - if (!strategy) throw new Error(`No CheckLiquidityStrategy found. Alias: ${alias}`); - - return strategy; - } - - private getCheckLiquidityStrategyByAsset(asset: Asset): CheckLiquidityStrategy { - const alias = this.getCheckLiquidityStrategyAlias(asset); - - return this.getCheckLiquidityStrategyByAlias(alias); - } - - private getPurchaseLiquidityStrategyByAlias(alias: PurchaseLiquidityStrategyAlias): PurchaseLiquidityStrategy { - const strategy = this.purchaseLiquidityStrategies.get(alias); - - if (!strategy) throw new Error(`No PurchaseLiquidityStrategy found. Alias: ${alias}`); - - return strategy; - } - - private getPurchaseLiquidityStrategyByAsset(asset: Asset): PurchaseLiquidityStrategy { - const alias = this.getPurchaseLiquidityStrategyAlias(asset); - - return this.getPurchaseLiquidityStrategyByAlias(alias); - } - - private getCheckLiquidityStrategyAlias(asset: Asset): CheckLiquidityStrategyAlias { - const { blockchain, category: assetCategory } = asset; - - if (blockchain === Blockchain.DEFICHAIN || blockchain === Blockchain.BITCOIN) { - if (assetCategory === AssetCategory.POOL_PAIR) return CheckLiquidityStrategyAlias.DEFICHAIN_POOL_PAIR; - return CheckLiquidityStrategyAlias.DEFICHAIN_DEFAULT; - } - - if (blockchain === Blockchain.ETHEREUM) return CheckLiquidityStrategyAlias.ETHEREUM_DEFAULT; - if (blockchain === Blockchain.BINANCE_SMART_CHAIN) return CheckLiquidityStrategyAlias.BSC_DEFAULT; - } - - private getPurchaseLiquidityStrategyAlias(asset: Asset): PurchaseLiquidityStrategyAlias { - const { blockchain, category: assetCategory } = asset; - - if (blockchain === Blockchain.DEFICHAIN || blockchain === Blockchain.BITCOIN) { - if (assetCategory === AssetCategory.POOL_PAIR) return PurchaseLiquidityStrategyAlias.DEFICHAIN_POOL_PAIR; - if (assetCategory === AssetCategory.STOCK) return PurchaseLiquidityStrategyAlias.DEFICHAIN_STOCK; - if (assetCategory === AssetCategory.CRYPTO) return PurchaseLiquidityStrategyAlias.DEFICHAIN_CRYPTO; - } - - if (blockchain === Blockchain.ETHEREUM) return PurchaseLiquidityStrategyAlias.ETHEREUM_DEFAULT; - if (blockchain === Blockchain.BINANCE_SMART_CHAIN) return PurchaseLiquidityStrategyAlias.BSC_DEFAULT; - } -} diff --git a/src/payment/models/payout/payout.module.ts b/src/payment/models/payout/payout.module.ts index 675d34571a..f4bf4151cc 100644 --- a/src/payment/models/payout/payout.module.ts +++ b/src/payment/models/payout/payout.module.ts @@ -12,14 +12,20 @@ import { PayoutDeFiChainService } from './services/payout-defichain.service'; import { PayoutEthereumService } from './services/payout-ethereum.service'; import { PayoutLogService } from './services/payout-log.service'; import { PayoutService } from './services/payout.service'; -import { PayoutBscStrategy } from './strategies/payout/payout-bsc.strategy'; -import { PayoutDeFiChainDFIStrategy } from './strategies/payout/payout-defichain-dfi.strategy'; -import { PayoutEthereumStrategy } from './strategies/payout/payout-ethereum.strategy'; -import { PayoutDeFiChainTokenStrategy } from './strategies/payout/payout-defichain-token.strategy'; -import { PrepareBscStrategy } from './strategies/prepare/prepare-bsc.strategy'; -import { PrepareDeFiChainStrategy } from './strategies/prepare/prepare-defichain.strategy'; -import { PrepareEthereumStrategy } from './strategies/prepare/prepare-ethereum.strategy'; -import { PayoutStrategiesFacade } from './strategies/strategies.facade'; +import { PayoutStrategiesFacade } from './strategies/payout/payout.facade'; +import { PayoutBitcoinService } from './services/payout-bitcoin.service'; +import { PrepareStrategiesFacade } from './strategies/prepare/prepare.facade'; +import { BitcoinStrategy as BitcoinStrategyPO } from './strategies/payout/impl/bitcoin.strategy'; +import { BscCoinStrategy as BscCryptoStrategyPO } from './strategies/payout/impl/bsc-coin.strategy'; +import { BscTokenStrategy as BscTokenStrategyPO } from './strategies/payout/impl/bsc-token.strategy'; +import { DeFiChainCoinStrategy as DeFiChainDfiStrategyPO } from './strategies/payout/impl/defichain-coin.strategy'; +import { DeFiChainTokenStrategy as DeFiChainTokenStrategyPO } from './strategies/payout/impl/defichain-token.strategy'; +import { EthereumCoinStrategy as EthereumCryptoStrategyPO } from './strategies/payout/impl/ethereum-coin.strategy'; +import { EthereumTokenStrategy as EthereumTokenStrategyPO } from './strategies/payout/impl/ethereum-token.strategy'; +import { BitcoinStrategy as BitcoinStrategyPR } from './strategies/prepare/impl/bitcoin.strategy'; +import { BscStrategy as BscStrategyPR } from './strategies/prepare/impl/bsc.strategy'; +import { DeFiChainStrategy as DeFiChainStrategyPR } from './strategies/prepare/impl/defichain.strategy'; +import { EthereumStrategy as EthereumStrategyPR } from './strategies/prepare/impl/ethereum.strategy'; @Module({ imports: [ @@ -35,17 +41,23 @@ import { PayoutStrategiesFacade } from './strategies/strategies.facade'; PayoutOrderFactory, PayoutLogService, PayoutService, + PayoutBitcoinService, PayoutDeFiChainService, PayoutEthereumService, PayoutBscService, - PayoutDeFiChainDFIStrategy, - PayoutDeFiChainTokenStrategy, - PayoutEthereumStrategy, - PayoutBscStrategy, - PrepareDeFiChainStrategy, - PrepareEthereumStrategy, - PrepareBscStrategy, PayoutStrategiesFacade, + PrepareStrategiesFacade, + BitcoinStrategyPO, + BscCryptoStrategyPO, + BscTokenStrategyPO, + DeFiChainDfiStrategyPO, + DeFiChainTokenStrategyPO, + EthereumCryptoStrategyPO, + EthereumTokenStrategyPO, + BitcoinStrategyPR, + BscStrategyPR, + DeFiChainStrategyPR, + EthereumStrategyPR, ], exports: [PayoutService], }) diff --git a/src/payment/models/payout/services/base/payout-jellyfish.service.ts b/src/payment/models/payout/services/base/payout-jellyfish.service.ts new file mode 100644 index 0000000000..fdeccbba7f --- /dev/null +++ b/src/payment/models/payout/services/base/payout-jellyfish.service.ts @@ -0,0 +1,8 @@ +import { PayoutOrderContext } from '../../entities/payout-order.entity'; + +export type PayoutGroup = { addressTo: string; amount: number }[]; + +export abstract class PayoutJellyfishService { + abstract isHealthy(context: PayoutOrderContext): Promise; + abstract checkPayoutCompletion(context: PayoutOrderContext, payoutTxId: string): Promise; +} diff --git a/src/payment/models/payout/services/payout-bitcoin.service.ts b/src/payment/models/payout/services/payout-bitcoin.service.ts new file mode 100644 index 0000000000..c855b2d145 --- /dev/null +++ b/src/payment/models/payout/services/payout-bitcoin.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { BtcClient } from 'src/blockchain/ain/node/btc-client'; +import { NodeService, NodeType } from 'src/blockchain/ain/node/node.service'; +import { PayoutOrderContext } from '../entities/payout-order.entity'; +import { PayoutGroup, PayoutJellyfishService } from './base/payout-jellyfish.service'; + +@Injectable() +export class PayoutBitcoinService extends PayoutJellyfishService { + #client: BtcClient; + + constructor(readonly nodeService: NodeService) { + super(); + nodeService.getConnectedNode(NodeType.BTC_OUTPUT).subscribe((client) => (this.#client = client)); + } + + async isHealthy(): Promise { + try { + return !!(await this.#client.getInfo()); + } catch { + return false; + } + } + + async sendUtxoToMany(_context: PayoutOrderContext, payout: PayoutGroup): Promise { + return this.#client.sendUtxoToMany(payout); + } + + async checkPayoutCompletion(payoutTxId: string): Promise { + const transaction = await this.#client.getTx(payoutTxId); + + return transaction && transaction.blockhash && transaction.confirmations > 0; + } +} diff --git a/src/payment/models/payout/services/payout-defichain.service.ts b/src/payment/models/payout/services/payout-defichain.service.ts index da63652b2e..5ca921d94d 100644 --- a/src/payment/models/payout/services/payout-defichain.service.ts +++ b/src/payment/models/payout/services/payout-defichain.service.ts @@ -4,15 +4,15 @@ import { NodeService, NodeType } from 'src/blockchain/ain/node/node.service'; import { WhaleService } from 'src/blockchain/ain/whale/whale.service'; import { Config } from 'src/config/config'; import { PayoutOrderContext } from '../entities/payout-order.entity'; - -export type PayoutGroup = { addressTo: string; amount: number }[]; +import { PayoutGroup, PayoutJellyfishService } from './base/payout-jellyfish.service'; @Injectable() -export class PayoutDeFiChainService { +export class PayoutDeFiChainService extends PayoutJellyfishService { #outClient: DeFiClient; #intClient: DeFiClient; constructor(readonly nodeService: NodeService, private readonly whaleService: WhaleService) { + super(); nodeService.getConnectedNode(NodeType.OUTPUT).subscribe((client) => (this.#outClient = client)); nodeService.getConnectedNode(NodeType.INT).subscribe((client) => (this.#intClient = client)); } diff --git a/src/payment/models/payout/services/payout-evm.service.ts b/src/payment/models/payout/services/payout-evm.service.ts index 616cc3c7f7..a89ec253e4 100644 --- a/src/payment/models/payout/services/payout-evm.service.ts +++ b/src/payment/models/payout/services/payout-evm.service.ts @@ -1,5 +1,6 @@ import { EvmClient } from 'src/blockchain/shared/evm/evm-client'; import { EvmService } from 'src/blockchain/shared/evm/evm.service'; +import { Asset } from 'src/shared/models/asset/asset.entity'; export abstract class PayoutEvmService { #client: EvmClient; @@ -8,8 +9,12 @@ export abstract class PayoutEvmService { this.#client = service.getDefaultClient(); } - async send(address: string, amount: number): Promise { - return this.#client.send(address, amount); + async sendNativeCoin(address: string, amount: number): Promise { + return this.#client.sendNativeCoin(address, amount); + } + + async sendToken(address: string, tokenName: Asset, amount: number): Promise { + return this.#client.sendToken(address, tokenName, amount); } async checkPayoutCompletion(txHash: string): Promise { diff --git a/src/payment/models/payout/services/payout.service.ts b/src/payment/models/payout/services/payout.service.ts index d8e3ea7270..619b11f5aa 100644 --- a/src/payment/models/payout/services/payout.service.ts +++ b/src/payment/models/payout/services/payout.service.ts @@ -1,22 +1,23 @@ import { Injectable } from '@nestjs/common'; import { Interval } from '@nestjs/schedule'; import { Lock } from 'src/shared/lock'; -import { PayoutOrderContext, PayoutOrderStatus } from '../entities/payout-order.entity'; +import { PayoutOrder, PayoutOrderContext, PayoutOrderStatus } from '../entities/payout-order.entity'; import { PayoutOrderFactory } from '../factories/payout-order.factory'; import { PayoutOrderRepository } from '../repositories/payout-order.repository'; import { DuplicatedEntryException } from '../exceptions/duplicated-entry.exception'; import { MailService } from 'src/shared/services/mail.service'; -import { PayoutStrategiesFacade, PayoutStrategyAlias } from '../strategies/strategies.facade'; -import { Blockchain } from 'src/blockchain/shared/enums/blockchain.enum'; +import { PayoutStrategiesFacade, PayoutStrategyAlias } from '../strategies/payout/payout.facade'; import { PayoutLogService } from './payout-log.service'; import { PayoutRequest } from '../interfaces'; +import { PrepareStrategiesFacade } from '../strategies/prepare/prepare.facade'; @Injectable() export class PayoutService { private readonly processOrdersLock = new Lock(1800); constructor( - private readonly strategies: PayoutStrategiesFacade, + private readonly payoutStrategies: PayoutStrategiesFacade, + private readonly prepareStrategies: PrepareStrategiesFacade, private readonly logs: PayoutLogService, private readonly mailService: MailService, private readonly payoutOrderRepo: PayoutOrderRepository, @@ -86,7 +87,7 @@ export class PayoutService { const confirmedOrders = []; for (const order of orders) { - const strategy = this.strategies.getPrepareStrategy(order.asset); + const strategy = this.prepareStrategies.getPrepareStrategy(order.asset); try { await strategy.checkPreparationCompletion(order); @@ -105,7 +106,7 @@ export class PayoutService { const confirmedOrders = []; for (const order of orders) { - const strategy = this.strategies.getPayoutStrategy(order.asset); + const strategy = this.payoutStrategies.getPayoutStrategy(order.asset); try { await strategy.checkPayoutCompletion(order); @@ -123,7 +124,7 @@ export class PayoutService { const confirmedOrders = []; for (const order of orders) { - const strategy = this.strategies.getPrepareStrategy(order.asset); + const strategy = this.prepareStrategies.getPrepareStrategy(order.asset); try { await strategy.preparePayout(order); @@ -138,23 +139,16 @@ export class PayoutService { private async payoutOrders(): Promise { const orders = await this.payoutOrderRepo.find({ status: PayoutOrderStatus.PREPARATION_CONFIRMED }); + const groups = this.groupOrdersByPayoutStrategies(orders); - const dfiOrders = orders.filter((o) => o.asset.blockchain === Blockchain.DEFICHAIN && o.asset.dexName === 'DFI'); - const tokenOrders = orders.filter((o) => o.asset.blockchain === Blockchain.DEFICHAIN && o.asset.dexName !== 'DFI'); - const ethOrders = orders.filter((o) => o.asset.blockchain === Blockchain.ETHEREUM && o.asset.dexName === 'ETH'); - const bnbOrders = orders.filter( - (o) => o.asset.blockchain === Blockchain.BINANCE_SMART_CHAIN && o.asset.dexName === 'BNB', - ); - - const dfiStrategy = this.strategies.getPayoutStrategy(PayoutStrategyAlias.DEFICHAIN_DFI); - const tokenStrategy = this.strategies.getPayoutStrategy(PayoutStrategyAlias.DEFICHAIN_TOKEN); - const ethStrategy = this.strategies.getPayoutStrategy(PayoutStrategyAlias.ETHEREUM_DEFAULT); - const bnbStrategy = this.strategies.getPayoutStrategy(PayoutStrategyAlias.BSC_DEFAULT); - - await dfiStrategy.doPayout(dfiOrders); - await tokenStrategy.doPayout(tokenOrders); - await ethStrategy.doPayout(ethOrders); - await bnbStrategy.doPayout(bnbOrders); + for (const group of groups.entries()) { + try { + const strategy = this.payoutStrategies.getPayoutStrategy(group[0]); + await strategy.doPayout(group[1]); + } catch { + continue; + } + } } private async processFailedOrders(): Promise { @@ -170,4 +164,24 @@ export class PayoutService { await this.payoutOrderRepo.save(order); } } + + private groupOrdersByPayoutStrategies(orders: PayoutOrder[]): Map { + const groups = new Map(); + + for (const order of orders) { + const alias = this.payoutStrategies.getPayoutStrategyAlias(order.asset); + + if (!alias) { + console.warn(`No payout alias found for payout order ID ${order.id}. Ignoring the order`); + continue; + } + + const group = groups.get(alias) ?? []; + group.push(order); + + groups.set(alias, group); + } + + return groups; + } } diff --git a/src/payment/models/payout/strategies/__tests__/strategies.facade.spec.ts b/src/payment/models/payout/strategies/__tests__/strategies.facade.spec.ts deleted file mode 100644 index d86ef185ac..0000000000 --- a/src/payment/models/payout/strategies/__tests__/strategies.facade.spec.ts +++ /dev/null @@ -1,297 +0,0 @@ -import { mock } from 'jest-mock-extended'; -import { Blockchain } from 'src/blockchain/shared/enums/blockchain.enum'; -import { DexService } from 'src/payment/models/dex/services/dex.service'; -import { createCustomAsset } from 'src/shared/models/asset/__mocks__/asset.entity.mock'; -import { MailService } from 'src/shared/services/mail.service'; -import { PayoutOrderRepository } from '../../repositories/payout-order.repository'; -import { PayoutBscService } from '../../services/payout-bsc.service'; -import { PayoutDeFiChainService } from '../../services/payout-defichain.service'; -import { PayoutEthereumService } from '../../services/payout-ethereum.service'; -import { PayoutBscStrategy } from '../payout/payout-bsc.strategy'; -import { PayoutDeFiChainDFIStrategy } from '../payout/payout-defichain-dfi.strategy'; -import { PayoutDeFiChainTokenStrategy } from '../payout/payout-defichain-token.strategy'; -import { PayoutEthereumStrategy } from '../payout/payout-ethereum.strategy'; -import { PrepareBscStrategy } from '../prepare/prepare-bsc.strategy'; -import { PrepareDeFiChainStrategy } from '../prepare/prepare-defichain.strategy'; -import { PrepareEthereumStrategy } from '../prepare/prepare-ethereum.strategy'; -import { PayoutStrategiesFacade, PayoutStrategyAlias, PrepareStrategyAlias } from '../strategies.facade'; - -describe('PayoutStrategiesFacade', () => { - let payoutDFIStrategy: PayoutDeFiChainDFIStrategy; - let payoutTokenStrategy: PayoutDeFiChainTokenStrategy; - let payoutETHStrategy: PayoutEthereumStrategy; - let payoutBSCStrategy: PayoutBscStrategy; - let prepareOnDefichainStrategy: PrepareDeFiChainStrategy; - let prepareOnEthereumStrategy: PrepareEthereumStrategy; - let prepareOnBscStrategy: PrepareBscStrategy; - - let facade: PayoutStrategiesFacadeWrapper; - - beforeEach(() => { - payoutDFIStrategy = new PayoutDeFiChainDFIStrategy( - mock(), - mock(), - mock(), - ); - payoutTokenStrategy = new PayoutDeFiChainTokenStrategy( - mock(), - mock(), - mock(), - mock(), - ); - payoutETHStrategy = new PayoutEthereumStrategy(mock(), mock()); - payoutBSCStrategy = new PayoutBscStrategy(mock(), mock()); - prepareOnDefichainStrategy = new PrepareDeFiChainStrategy( - mock(), - mock(), - mock(), - ); - prepareOnEthereumStrategy = new PrepareEthereumStrategy(mock()); - prepareOnBscStrategy = new PrepareBscStrategy(mock()); - - facade = new PayoutStrategiesFacadeWrapper( - payoutDFIStrategy, - payoutTokenStrategy, - payoutETHStrategy, - payoutBSCStrategy, - prepareOnDefichainStrategy, - prepareOnEthereumStrategy, - prepareOnBscStrategy, - ); - }); - - describe('#constructor(...)', () => { - it('adds all payoutStrategies to a map', () => { - expect([...facade.getPayoutStrategies().entries()].length).toBe(4); - }); - - it('sets all required payoutStrategies aliases', () => { - const aliases = [...facade.getPayoutStrategies().keys()]; - - expect(aliases.includes(PayoutStrategyAlias.DEFICHAIN_DFI)).toBe(true); - expect(aliases.includes(PayoutStrategyAlias.DEFICHAIN_TOKEN)).toBe(true); - expect(aliases.includes(PayoutStrategyAlias.ETHEREUM_DEFAULT)).toBe(true); - expect(aliases.includes(PayoutStrategyAlias.BSC_DEFAULT)).toBe(true); - }); - - it('assigns proper payoutStrategies to aliases', () => { - expect(facade.getPayoutStrategies().get(PayoutStrategyAlias.DEFICHAIN_DFI)).toBeInstanceOf( - PayoutDeFiChainDFIStrategy, - ); - - expect(facade.getPayoutStrategies().get(PayoutStrategyAlias.DEFICHAIN_TOKEN)).toBeInstanceOf( - PayoutDeFiChainTokenStrategy, - ); - - expect(facade.getPayoutStrategies().get(PayoutStrategyAlias.ETHEREUM_DEFAULT)).toBeInstanceOf( - PayoutEthereumStrategy, - ); - - expect(facade.getPayoutStrategies().get(PayoutStrategyAlias.BSC_DEFAULT)).toBeInstanceOf(PayoutBscStrategy); - }); - - it('adds all prepareStrategies to a map', () => { - expect([...facade.getPrepareStrategies().entries()].length).toBe(3); - }); - - it('sets all required prepareStrategies aliases', () => { - const aliases = [...facade.getPrepareStrategies().keys()]; - - expect(aliases.includes(PrepareStrategyAlias.DEFICHAIN)).toBe(true); - expect(aliases.includes(PrepareStrategyAlias.ETHEREUM)).toBe(true); - expect(aliases.includes(PrepareStrategyAlias.BSC)).toBe(true); - }); - - it('assigns proper prepareStrategies to aliases', () => { - expect(facade.getPrepareStrategies().get(PrepareStrategyAlias.DEFICHAIN)).toBeInstanceOf( - PrepareDeFiChainStrategy, - ); - - expect(facade.getPrepareStrategies().get(PrepareStrategyAlias.ETHEREUM)).toBeInstanceOf(PrepareEthereumStrategy); - - expect(facade.getPrepareStrategies().get(PrepareStrategyAlias.BSC)).toBeInstanceOf(PrepareBscStrategy); - }); - }); - - describe('#getPayoutStrategy(...)', () => { - describe('getting strategy by Asset', () => { - it('gets ETHEREUM_DEFAULT strategy', () => { - const strategy = facade.getPayoutStrategy(createCustomAsset({ blockchain: Blockchain.ETHEREUM })); - - expect(strategy).toBeInstanceOf(PayoutEthereumStrategy); - }); - - it('gets BSC_DEFAULT strategy', () => { - const strategy = facade.getPayoutStrategy(createCustomAsset({ blockchain: Blockchain.BINANCE_SMART_CHAIN })); - - expect(strategy).toBeInstanceOf(PayoutBscStrategy); - }); - - it('gets DEFICHAIN_DFI strategy', () => { - const strategy = facade.getPayoutStrategy( - createCustomAsset({ blockchain: Blockchain.DEFICHAIN, dexName: 'DFI' }), - ); - - expect(strategy).toBeInstanceOf(PayoutDeFiChainDFIStrategy); - }); - - it('gets DEFICHAIN_TOKEN strategy for DEFICHAIN', () => { - const strategy = facade.getPayoutStrategy( - createCustomAsset({ blockchain: Blockchain.DEFICHAIN, dexName: 'non-DFI' }), - ); - - expect(strategy).toBeInstanceOf(PayoutDeFiChainTokenStrategy); - }); - - it('gets DEFICHAIN_TOKEN strategy for BITCOIN', () => { - const strategy = facade.getPayoutStrategy( - createCustomAsset({ blockchain: Blockchain.BITCOIN, dexName: 'non-DFI' }), - ); - - expect(strategy).toBeInstanceOf(PayoutDeFiChainTokenStrategy); - }); - - it('fails to get strategy for non-supported Blockchain', () => { - const testCall = () => - facade.getPayoutStrategy(createCustomAsset({ blockchain: 'NewBlockchain' as Blockchain })); - - expect(testCall).toThrow(); - expect(testCall).toThrowError('No PayoutStrategy found. Alias: undefined'); - }); - - it('fails to get strategy for DFI on Bitcoin blockchain', () => { - const testCall = () => - facade.getPayoutStrategy(createCustomAsset({ blockchain: Blockchain.BITCOIN, dexName: 'DFI' })); - - expect(testCall).toThrow(); - expect(testCall).toThrowError('No PayoutStrategy found. Alias: undefined'); - }); - }); - - describe('getting strategy by Alias', () => { - it('gets ETHEREUM_DEFAULT strategy', () => { - const strategy = facade.getPayoutStrategy(PayoutStrategyAlias.ETHEREUM_DEFAULT); - - expect(strategy).toBeInstanceOf(PayoutEthereumStrategy); - }); - - it('gets BSC_DEFAULT strategy', () => { - const strategyCrypto = facade.getPayoutStrategy(PayoutStrategyAlias.BSC_DEFAULT); - - expect(strategyCrypto).toBeInstanceOf(PayoutBscStrategy); - }); - - it('gets DEFICHAIN_DFI strategy', () => { - const strategy = facade.getPayoutStrategy(PayoutStrategyAlias.DEFICHAIN_DFI); - - expect(strategy).toBeInstanceOf(PayoutDeFiChainDFIStrategy); - }); - - it('gets DEFICHAIN_TOKEN strategy', () => { - const strategy = facade.getPayoutStrategy(PayoutStrategyAlias.DEFICHAIN_TOKEN); - - expect(strategy).toBeInstanceOf(PayoutDeFiChainTokenStrategy); - }); - - it('fails to get strategy for non-supported Alias', () => { - const testCall = () => facade.getPayoutStrategy('NonExistingAlias' as PayoutStrategyAlias); - - expect(testCall).toThrow(); - expect(testCall).toThrowError('No PayoutStrategy found. Alias: NonExistingAlias'); - }); - }); - }); - - describe('#getPrepareStrategy(...)', () => { - describe('getting strategy by Asset', () => { - it('gets ETHEREUM strategy', () => { - const strategy = facade.getPrepareStrategy(createCustomAsset({ blockchain: Blockchain.ETHEREUM })); - - expect(strategy).toBeInstanceOf(PrepareEthereumStrategy); - }); - - it('gets BSC strategy', () => { - const strategy = facade.getPrepareStrategy(createCustomAsset({ blockchain: Blockchain.BINANCE_SMART_CHAIN })); - - expect(strategy).toBeInstanceOf(PrepareBscStrategy); - }); - - it('gets DEFICHAIN strategy for DEFICHAIN', () => { - const strategy = facade.getPrepareStrategy(createCustomAsset({ blockchain: Blockchain.DEFICHAIN })); - - expect(strategy).toBeInstanceOf(PrepareDeFiChainStrategy); - }); - - it('gets DEFICHAIN strategy for BITCOIN', () => { - const strategy = facade.getPrepareStrategy(createCustomAsset({ blockchain: Blockchain.BITCOIN })); - - expect(strategy).toBeInstanceOf(PrepareDeFiChainStrategy); - }); - - it('fails to get strategy for non-supported Blockchain', () => { - const testCall = () => - facade.getPrepareStrategy(createCustomAsset({ blockchain: 'NewBlockchain' as Blockchain })); - - expect(testCall).toThrow(); - expect(testCall).toThrowError('No PrepareStrategy found. Alias: undefined'); - }); - }); - - describe('getting strategy by Alias', () => { - it('gets DEFICHAIN strategy', () => { - const strategy = facade.getPrepareStrategy(PrepareStrategyAlias.DEFICHAIN); - - expect(strategy).toBeInstanceOf(PrepareDeFiChainStrategy); - }); - - it('gets ETHEREUM strategy', () => { - const strategyCrypto = facade.getPrepareStrategy(PrepareStrategyAlias.ETHEREUM); - - expect(strategyCrypto).toBeInstanceOf(PrepareEthereumStrategy); - }); - - it('gets BSC strategy', () => { - const strategyCrypto = facade.getPrepareStrategy(PrepareStrategyAlias.BSC); - - expect(strategyCrypto).toBeInstanceOf(PrepareBscStrategy); - }); - - it('fails to get strategy for non-supported Alias', () => { - const testCall = () => facade.getPrepareStrategy('NonExistingAlias' as PrepareStrategyAlias); - - expect(testCall).toThrow(); - expect(testCall).toThrowError('No PrepareStrategy found. Alias: NonExistingAlias'); - }); - }); - }); -}); - -class PayoutStrategiesFacadeWrapper extends PayoutStrategiesFacade { - constructor( - payoutDFIStrategy: PayoutDeFiChainDFIStrategy, - payoutTokenStrategy: PayoutDeFiChainTokenStrategy, - payoutETHStrategy: PayoutEthereumStrategy, - payoutBSCStrategy: PayoutBscStrategy, - prepareOnDefichainStrategy: PrepareDeFiChainStrategy, - prepareOnEthereumStrategy: PrepareEthereumStrategy, - prepareOnBscStrategy: PrepareBscStrategy, - ) { - super( - payoutDFIStrategy, - payoutTokenStrategy, - payoutETHStrategy, - payoutBSCStrategy, - prepareOnDefichainStrategy, - prepareOnEthereumStrategy, - prepareOnBscStrategy, - ); - } - - getPayoutStrategies() { - return this.payoutStrategies; - } - - getPrepareStrategies() { - return this.prepareStrategies; - } -} diff --git a/src/payment/models/payout/strategies/payout/__tests__/payout-defichain-token.strategy.spec.ts b/src/payment/models/payout/strategies/payout/__tests__/payout-defichain-token.strategy.spec.ts index c4e8531060..1418ccc923 100644 --- a/src/payment/models/payout/strategies/payout/__tests__/payout-defichain-token.strategy.spec.ts +++ b/src/payment/models/payout/strategies/payout/__tests__/payout-defichain-token.strategy.spec.ts @@ -9,7 +9,7 @@ import { } from '../../../entities/__mocks__/payout-order.entity.mock'; import { PayoutOrderRepository } from '../../../repositories/payout-order.repository'; import { PayoutDeFiChainService } from '../../../services/payout-defichain.service'; -import { PayoutDeFiChainTokenStrategy } from '../payout-defichain-token.strategy'; +import { DeFiChainTokenStrategy } from '../impl/defichain-token.strategy'; describe('PayoutDeFiChainTokenStrategy', () => { let strategy: PayoutDeFiChainTokenStrategyWrapper; @@ -66,7 +66,7 @@ describe('PayoutDeFiChainTokenStrategy', () => { }); }); -class PayoutDeFiChainTokenStrategyWrapper extends PayoutDeFiChainTokenStrategy { +class PayoutDeFiChainTokenStrategyWrapper extends DeFiChainTokenStrategy { constructor( mailService: MailService, dexService: DexService, diff --git a/src/payment/models/payout/strategies/payout/__tests__/payout-defichain.strategy.spec.ts b/src/payment/models/payout/strategies/payout/__tests__/payout-jellyfish.strategy.spec.ts similarity index 96% rename from src/payment/models/payout/strategies/payout/__tests__/payout-defichain.strategy.spec.ts rename to src/payment/models/payout/strategies/payout/__tests__/payout-jellyfish.strategy.spec.ts index 742de4c8b9..b8868b2337 100644 --- a/src/payment/models/payout/strategies/payout/__tests__/payout-defichain.strategy.spec.ts +++ b/src/payment/models/payout/strategies/payout/__tests__/payout-jellyfish.strategy.spec.ts @@ -8,10 +8,10 @@ import { } from '../../../entities/__mocks__/payout-order.entity.mock'; import { PayoutOrderRepository } from '../../../repositories/payout-order.repository'; import { PayoutDeFiChainService } from '../../../services/payout-defichain.service'; -import { PayoutDeFiChainStrategy } from '../base/payout-defichain.strategy'; +import { JellyfishStrategy } from '../impl/base/jellyfish.strategy'; -describe('PayoutDeFiChainStrategy', () => { - let strategy: PayoutDeFiChainStrategyWrapper; +describe('PayoutJellyfishStrategy', () => { + let strategy: PayoutJellyfishStrategyWrapper; let mailService: MailService; let payoutOrderRepo: PayoutOrderRepository; @@ -28,7 +28,7 @@ describe('PayoutDeFiChainStrategy', () => { repoSaveSpy = jest.spyOn(payoutOrderRepo, 'save'); sendErrorMailSpy = jest.spyOn(mailService, 'sendErrorMail'); - strategy = new PayoutDeFiChainStrategyWrapper(mailService, payoutOrderRepo, defichainService); + strategy = new PayoutJellyfishStrategyWrapper(mailService, payoutOrderRepo, defichainService); }); afterEach(() => { @@ -250,7 +250,7 @@ describe('PayoutDeFiChainStrategy', () => { }); }); -class PayoutDeFiChainStrategyWrapper extends PayoutDeFiChainStrategy { +class PayoutJellyfishStrategyWrapper extends JellyfishStrategy { constructor( mailService: MailService, payoutOrderRepo: PayoutOrderRepository, @@ -263,6 +263,10 @@ class PayoutDeFiChainStrategyWrapper extends PayoutDeFiChainStrategy { throw new Error('Method not implemented.'); } + protected async dispatchPayout(): Promise { + return 'TX_ID_01'; + } + groupOrdersByContextWrapper(orders: PayoutOrder[]) { return this.groupOrdersByContext(orders); } diff --git a/src/payment/models/payout/strategies/payout/__tests__/payout.facade.spec.ts b/src/payment/models/payout/strategies/payout/__tests__/payout.facade.spec.ts new file mode 100644 index 0000000000..e20f7e3920 --- /dev/null +++ b/src/payment/models/payout/strategies/payout/__tests__/payout.facade.spec.ts @@ -0,0 +1,227 @@ +import { mock } from 'jest-mock-extended'; +import { Blockchain } from 'src/blockchain/shared/enums/blockchain.enum'; +import { DexService } from 'src/payment/models/dex/services/dex.service'; +import { AssetType } from 'src/shared/models/asset/asset.entity'; +import { createCustomAsset } from 'src/shared/models/asset/__mocks__/asset.entity.mock'; +import { MailService } from 'src/shared/services/mail.service'; +import { PayoutOrderRepository } from '../../../repositories/payout-order.repository'; +import { PayoutBitcoinService } from '../../../services/payout-bitcoin.service'; +import { PayoutBscService } from '../../../services/payout-bsc.service'; +import { PayoutDeFiChainService } from '../../../services/payout-defichain.service'; +import { PayoutEthereumService } from '../../../services/payout-ethereum.service'; +import { BitcoinStrategy } from '../impl/bitcoin.strategy'; +import { BscCoinStrategy } from '../impl/bsc-coin.strategy'; +import { BscTokenStrategy } from '../impl/bsc-token.strategy'; +import { DeFiChainCoinStrategy } from '../impl/defichain-coin.strategy'; +import { DeFiChainTokenStrategy } from '../impl/defichain-token.strategy'; +import { EthereumCoinStrategy } from '../impl/ethereum-coin.strategy'; +import { EthereumTokenStrategy } from '../impl/ethereum-token.strategy'; +import { PayoutStrategiesFacade, PayoutStrategyAlias } from '../payout.facade'; + +describe('PayoutStrategiesFacade', () => { + let bitcoin: BitcoinStrategy; + let deFiChainCoin: DeFiChainCoinStrategy; + let deFiChainToken: DeFiChainTokenStrategy; + let ethereumCoin: EthereumCoinStrategy; + let ethereumToken: EthereumTokenStrategy; + let bscCoin: BscCoinStrategy; + let bscToken: BscTokenStrategy; + + let facade: PayoutStrategiesFacadeWrapper; + + beforeEach(() => { + bitcoin = new BitcoinStrategy(mock(), mock(), mock()); + deFiChainCoin = new DeFiChainCoinStrategy( + mock(), + mock(), + mock(), + ); + deFiChainToken = new DeFiChainTokenStrategy( + mock(), + mock(), + mock(), + mock(), + ); + ethereumCoin = new EthereumCoinStrategy(mock(), mock()); + ethereumToken = new EthereumTokenStrategy(mock(), mock()); + bscCoin = new BscCoinStrategy(mock(), mock()); + bscToken = new BscTokenStrategy(mock(), mock()); + + facade = new PayoutStrategiesFacadeWrapper( + bitcoin, + bscCoin, + bscToken, + deFiChainCoin, + deFiChainToken, + ethereumCoin, + ethereumToken, + ); + }); + + describe('#constructor(...)', () => { + it('adds all payoutStrategies to a map', () => { + expect([...facade.getStrategies().entries()].length).toBe(7); + }); + + it('assigns strategies to all aliases', () => { + expect([...facade.getStrategies().entries()].length).toBe(Object.values(PayoutStrategyAlias).length); + }); + + it('sets all required payoutStrategies aliases', () => { + const aliases = [...facade.getStrategies().keys()]; + + expect(aliases.includes(PayoutStrategyAlias.BITCOIN)).toBe(true); + expect(aliases.includes(PayoutStrategyAlias.BSC_TOKEN)).toBe(true); + expect(aliases.includes(PayoutStrategyAlias.BSC_COIN)).toBe(true); + expect(aliases.includes(PayoutStrategyAlias.DEFICHAIN_COIN)).toBe(true); + expect(aliases.includes(PayoutStrategyAlias.DEFICHAIN_TOKEN)).toBe(true); + expect(aliases.includes(PayoutStrategyAlias.ETHEREUM_COIN)).toBe(true); + expect(aliases.includes(PayoutStrategyAlias.ETHEREUM_TOKEN)).toBe(true); + }); + + it('assigns proper payoutStrategies to aliases', () => { + expect(facade.getStrategies().get(PayoutStrategyAlias.BITCOIN)).toBeInstanceOf(BitcoinStrategy); + expect(facade.getStrategies().get(PayoutStrategyAlias.BSC_COIN)).toBeInstanceOf(BscCoinStrategy); + expect(facade.getStrategies().get(PayoutStrategyAlias.BSC_TOKEN)).toBeInstanceOf(BscTokenStrategy); + expect(facade.getStrategies().get(PayoutStrategyAlias.DEFICHAIN_COIN)).toBeInstanceOf(DeFiChainCoinStrategy); + expect(facade.getStrategies().get(PayoutStrategyAlias.DEFICHAIN_TOKEN)).toBeInstanceOf(DeFiChainTokenStrategy); + expect(facade.getStrategies().get(PayoutStrategyAlias.ETHEREUM_COIN)).toBeInstanceOf(EthereumCoinStrategy); + expect(facade.getStrategies().get(PayoutStrategyAlias.ETHEREUM_TOKEN)).toBeInstanceOf(EthereumTokenStrategy); + }); + }); + + describe('#getPayoutStrategy(...)', () => { + describe('getting strategy by Asset', () => { + it('gets BITCOIN strategy for BITCOIN', () => { + const strategy = facade.getPayoutStrategy(createCustomAsset({ blockchain: Blockchain.BITCOIN })); + + expect(strategy).toBeInstanceOf(BitcoinStrategy); + }); + + it('gets BSC_COIN strategy', () => { + const strategy = facade.getPayoutStrategy( + createCustomAsset({ blockchain: Blockchain.BINANCE_SMART_CHAIN, type: AssetType.COIN }), + ); + + expect(strategy).toBeInstanceOf(BscCoinStrategy); + }); + + it('gets BSC_TOKEN strategy', () => { + const strategy = facade.getPayoutStrategy( + createCustomAsset({ blockchain: Blockchain.BINANCE_SMART_CHAIN, type: AssetType.TOKEN }), + ); + + expect(strategy).toBeInstanceOf(BscTokenStrategy); + }); + + it('gets DEFICHAIN_COIN strategy', () => { + const strategy = facade.getPayoutStrategy( + createCustomAsset({ blockchain: Blockchain.DEFICHAIN, type: AssetType.COIN }), + ); + + expect(strategy).toBeInstanceOf(DeFiChainCoinStrategy); + }); + + it('gets DEFICHAIN_TOKEN strategy for DEFICHAIN', () => { + const strategy = facade.getPayoutStrategy( + createCustomAsset({ blockchain: Blockchain.DEFICHAIN, type: AssetType.TOKEN }), + ); + + expect(strategy).toBeInstanceOf(DeFiChainTokenStrategy); + }); + + it('gets ETHEREUM_COIN strategy', () => { + const strategy = facade.getPayoutStrategy( + createCustomAsset({ blockchain: Blockchain.ETHEREUM, type: AssetType.COIN }), + ); + + expect(strategy).toBeInstanceOf(EthereumCoinStrategy); + }); + + it('gets ETHEREUM_TOKEN strategy', () => { + const strategy = facade.getPayoutStrategy( + createCustomAsset({ blockchain: Blockchain.ETHEREUM, type: AssetType.TOKEN }), + ); + + expect(strategy).toBeInstanceOf(EthereumTokenStrategy); + }); + + it('fails to get strategy for non-supported Blockchain', () => { + const testCall = () => + facade.getPayoutStrategy(createCustomAsset({ blockchain: 'NewBlockchain' as Blockchain })); + + expect(testCall).toThrow(); + expect(testCall).toThrowError('No PayoutStrategy found. Alias: undefined'); + }); + }); + + describe('getting strategy by Alias', () => { + it('gets BITCOIN strategy', () => { + const strategyCrypto = facade.getPayoutStrategy(PayoutStrategyAlias.BITCOIN); + + expect(strategyCrypto).toBeInstanceOf(BitcoinStrategy); + }); + + it('gets BSC_COIN strategy', () => { + const strategyCrypto = facade.getPayoutStrategy(PayoutStrategyAlias.BSC_COIN); + + expect(strategyCrypto).toBeInstanceOf(BscCoinStrategy); + }); + + it('gets BSC_TOKEN strategy', () => { + const strategyCrypto = facade.getPayoutStrategy(PayoutStrategyAlias.BSC_TOKEN); + + expect(strategyCrypto).toBeInstanceOf(BscTokenStrategy); + }); + + it('gets DEFICHAIN_COIN strategy', () => { + const strategy = facade.getPayoutStrategy(PayoutStrategyAlias.DEFICHAIN_COIN); + + expect(strategy).toBeInstanceOf(DeFiChainCoinStrategy); + }); + + it('gets DEFICHAIN_TOKEN strategy', () => { + const strategy = facade.getPayoutStrategy(PayoutStrategyAlias.DEFICHAIN_TOKEN); + + expect(strategy).toBeInstanceOf(DeFiChainTokenStrategy); + }); + + it('gets ETHEREUM_COIN strategy', () => { + const strategy = facade.getPayoutStrategy(PayoutStrategyAlias.ETHEREUM_COIN); + + expect(strategy).toBeInstanceOf(EthereumCoinStrategy); + }); + + it('gets ETHEREUM_TOKEN strategy', () => { + const strategy = facade.getPayoutStrategy(PayoutStrategyAlias.ETHEREUM_TOKEN); + + expect(strategy).toBeInstanceOf(EthereumTokenStrategy); + }); + + it('fails to get strategy for non-supported Alias', () => { + const testCall = () => facade.getPayoutStrategy('NonExistingAlias' as PayoutStrategyAlias); + + expect(testCall).toThrow(); + expect(testCall).toThrowError('No PayoutStrategy found. Alias: NonExistingAlias'); + }); + }); + }); +}); + +class PayoutStrategiesFacadeWrapper extends PayoutStrategiesFacade { + constructor( + bitcoin: BitcoinStrategy, + bscCoin: BscCoinStrategy, + bscToken: BscTokenStrategy, + deFiChainCoin: DeFiChainCoinStrategy, + deFiChainToken: DeFiChainTokenStrategy, + ethereumCoin: EthereumCoinStrategy, + ethereumToken: EthereumTokenStrategy, + ) { + super(bitcoin, bscCoin, bscToken, deFiChainCoin, deFiChainToken, ethereumCoin, ethereumToken); + } + + getStrategies() { + return this.strategies; + } +} diff --git a/src/payment/models/payout/strategies/payout/base/payout-evm.strategy.ts b/src/payment/models/payout/strategies/payout/impl/base/evm.strategy.ts similarity index 61% rename from src/payment/models/payout/strategies/payout/base/payout-evm.strategy.ts rename to src/payment/models/payout/strategies/payout/impl/base/evm.strategy.ts index 4f09cf7354..db52b51f90 100644 --- a/src/payment/models/payout/strategies/payout/base/payout-evm.strategy.ts +++ b/src/payment/models/payout/strategies/payout/impl/base/evm.strategy.ts @@ -1,18 +1,20 @@ -import { PayoutOrder } from '../../../entities/payout-order.entity'; -import { PayoutOrderRepository } from '../../../repositories/payout-order.repository'; -import { PayoutEvmService } from '../../../services/payout-evm.service'; +import { PayoutOrder } from '../../../../entities/payout-order.entity'; +import { PayoutOrderRepository } from '../../../../repositories/payout-order.repository'; +import { PayoutEvmService } from '../../../../services/payout-evm.service'; import { PayoutStrategy } from './payout.strategy'; -export abstract class PayoutEvmStrategy implements PayoutStrategy { +export abstract class EvmStrategy implements PayoutStrategy { constructor( protected readonly payoutEvmService: PayoutEvmService, protected readonly payoutOrderRepo: PayoutOrderRepository, ) {} + protected abstract dispatchPayout(order: PayoutOrder): Promise; + async doPayout(orders: PayoutOrder[]): Promise { for (const order of orders) { try { - const txId = await this.payoutEvmService.send(order.destinationAddress, order.amount); + const txId = await this.dispatchPayout(order); order.pendingPayout(txId); await this.payoutOrderRepo.save(order); @@ -24,7 +26,7 @@ export abstract class PayoutEvmStrategy implements PayoutStrategy { async checkPayoutCompletion(order: PayoutOrder): Promise { try { - const isComplete = this.payoutEvmService.checkPayoutCompletion(order.payoutTxId); + const isComplete = await this.payoutEvmService.checkPayoutCompletion(order.payoutTxId); if (isComplete) { order.complete(); diff --git a/src/payment/models/payout/strategies/payout/base/payout-defichain.strategy.ts b/src/payment/models/payout/strategies/payout/impl/base/jellyfish.strategy.ts similarity index 84% rename from src/payment/models/payout/strategies/payout/base/payout-defichain.strategy.ts rename to src/payment/models/payout/strategies/payout/impl/base/jellyfish.strategy.ts index 9f3efdc3a8..7f2c58902a 100644 --- a/src/payment/models/payout/strategies/payout/base/payout-defichain.strategy.ts +++ b/src/payment/models/payout/strategies/payout/impl/base/jellyfish.strategy.ts @@ -1,15 +1,15 @@ +import { PayoutGroup, PayoutJellyfishService } from 'src/payment/models/payout/services/base/payout-jellyfish.service'; import { MailService } from 'src/shared/services/mail.service'; import { Util } from 'src/shared/util'; -import { PayoutOrder, PayoutOrderContext } from '../../../entities/payout-order.entity'; -import { PayoutOrderRepository } from '../../../repositories/payout-order.repository'; -import { PayoutDeFiChainService, PayoutGroup } from '../../../services/payout-defichain.service'; +import { PayoutOrder, PayoutOrderContext } from '../../../../entities/payout-order.entity'; +import { PayoutOrderRepository } from '../../../../repositories/payout-order.repository'; import { PayoutStrategy } from './payout.strategy'; -export abstract class PayoutDeFiChainStrategy implements PayoutStrategy { +export abstract class JellyfishStrategy implements PayoutStrategy { constructor( protected readonly mailService: MailService, protected readonly payoutOrderRepo: PayoutOrderRepository, - protected readonly defichainService: PayoutDeFiChainService, + protected readonly jellyfishService: PayoutJellyfishService, ) {} async doPayout(orders: PayoutOrder[]): Promise { @@ -17,7 +17,7 @@ export abstract class PayoutDeFiChainStrategy implements PayoutStrategy { const groups = this.groupOrdersByContext(orders); for (const [context, group] of [...groups.entries()]) { - if (!(await this.defichainService.isHealthy(context))) return; + if (!(await this.jellyfishService.isHealthy(context))) return; await this.doPayoutForContext(context, group); } @@ -28,7 +28,7 @@ export abstract class PayoutDeFiChainStrategy implements PayoutStrategy { async checkPayoutCompletion(order: PayoutOrder): Promise { try { - const isComplete = await this.defichainService.checkPayoutCompletion(order.context, order.payoutTxId); + const isComplete = await this.jellyfishService.checkPayoutCompletion(order.context, order.payoutTxId); if (isComplete) { order.complete(); @@ -80,19 +80,20 @@ export abstract class PayoutDeFiChainStrategy implements PayoutStrategy { return [...result.values()]; } - protected async send( + protected abstract dispatchPayout( context: PayoutOrderContext, - orders: PayoutOrder[], + payout: PayoutGroup, outputAsset: string, - dispatcher: (context: PayoutOrderContext, payout: PayoutGroup, outputAsset: string) => Promise, - ): Promise { + ): Promise; + + protected async send(context: PayoutOrderContext, orders: PayoutOrder[], outputAsset: string): Promise { let payoutTxId: string; try { const payout = this.aggregatePayout(orders); await this.designatePayout(orders); - payoutTxId = await dispatcher(context, payout, outputAsset); + payoutTxId = await this.dispatchPayout(context, payout, outputAsset); } catch (e) { console.error(`Error on sending ${outputAsset} for payout. Order ID(s): ${orders.map((o) => o.id)}`, e); diff --git a/src/payment/models/payout/strategies/payout/base/payout.strategy.ts b/src/payment/models/payout/strategies/payout/impl/base/payout.strategy.ts similarity index 67% rename from src/payment/models/payout/strategies/payout/base/payout.strategy.ts rename to src/payment/models/payout/strategies/payout/impl/base/payout.strategy.ts index 2111933bee..7fbada9cfd 100644 --- a/src/payment/models/payout/strategies/payout/base/payout.strategy.ts +++ b/src/payment/models/payout/strategies/payout/impl/base/payout.strategy.ts @@ -1,4 +1,4 @@ -import { PayoutOrder } from '../../../entities/payout-order.entity'; +import { PayoutOrder } from '../../../../entities/payout-order.entity'; export interface PayoutStrategy { doPayout(orders: PayoutOrder[]): Promise; diff --git a/src/payment/models/payout/strategies/payout/impl/bitcoin.strategy.ts b/src/payment/models/payout/strategies/payout/impl/bitcoin.strategy.ts new file mode 100644 index 0000000000..2068094ca5 --- /dev/null +++ b/src/payment/models/payout/strategies/payout/impl/bitcoin.strategy.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@nestjs/common'; +import { MailService } from 'src/shared/services/mail.service'; +import { PayoutOrder, PayoutOrderContext } from '../../../entities/payout-order.entity'; +import { PayoutOrderRepository } from '../../../repositories/payout-order.repository'; +import { PayoutGroup } from '../../../services/base/payout-jellyfish.service'; +import { PayoutBitcoinService } from '../../../services/payout-bitcoin.service'; +import { JellyfishStrategy } from './base/jellyfish.strategy'; + +@Injectable() +export class BitcoinStrategy extends JellyfishStrategy { + constructor( + mailService: MailService, + protected readonly bitcoinService: PayoutBitcoinService, + protected readonly payoutOrderRepo: PayoutOrderRepository, + ) { + super(mailService, payoutOrderRepo, bitcoinService); + } + + protected async doPayoutForContext(context: PayoutOrderContext, orders: PayoutOrder[]): Promise { + const payoutGroups = this.createPayoutGroups(orders, 100); + + for (const group of payoutGroups) { + try { + if (group.length === 0) { + continue; + } + + console.info(`Paying out ${group.length} BTC orders(s). Order ID(s): ${group.map((o) => o.id)}`); + + await this.sendBTC(context, group); + } catch (e) { + console.error( + `Error in paying out a group of ${group.length} BTC orders(s). Order ID(s): ${group.map((o) => o.id)}`, + ); + // continue with next group in case payout failed + continue; + } + } + } + + protected dispatchPayout(context: PayoutOrderContext, payout: PayoutGroup): Promise { + return this.bitcoinService.sendUtxoToMany(context, payout); + } + + private async sendBTC(context: PayoutOrderContext, orders: PayoutOrder[]): Promise { + await this.send(context, orders, 'BTC'); + } +} diff --git a/src/payment/models/payout/strategies/payout/impl/bsc-coin.strategy.ts b/src/payment/models/payout/strategies/payout/impl/bsc-coin.strategy.ts new file mode 100644 index 0000000000..794e8709d7 --- /dev/null +++ b/src/payment/models/payout/strategies/payout/impl/bsc-coin.strategy.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; +import { PayoutOrder } from '../../../entities/payout-order.entity'; +import { PayoutOrderRepository } from '../../../repositories/payout-order.repository'; +import { PayoutBscService } from '../../../services/payout-bsc.service'; +import { EvmStrategy } from './base/evm.strategy'; + +@Injectable() +export class BscCoinStrategy extends EvmStrategy { + constructor(protected readonly bscService: PayoutBscService, payoutOrderRepo: PayoutOrderRepository) { + super(bscService, payoutOrderRepo); + } + + protected dispatchPayout(order: PayoutOrder): Promise { + return this.bscService.sendNativeCoin(order.destinationAddress, order.amount); + } +} diff --git a/src/payment/models/payout/strategies/payout/impl/bsc-token.strategy.ts b/src/payment/models/payout/strategies/payout/impl/bsc-token.strategy.ts new file mode 100644 index 0000000000..a6e178f2af --- /dev/null +++ b/src/payment/models/payout/strategies/payout/impl/bsc-token.strategy.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; +import { PayoutOrder } from '../../../entities/payout-order.entity'; +import { PayoutOrderRepository } from '../../../repositories/payout-order.repository'; +import { PayoutBscService } from '../../../services/payout-bsc.service'; +import { EvmStrategy } from './base/evm.strategy'; + +@Injectable() +export class BscTokenStrategy extends EvmStrategy { + constructor(protected readonly bscService: PayoutBscService, payoutOrderRepo: PayoutOrderRepository) { + super(bscService, payoutOrderRepo); + } + + protected dispatchPayout(order: PayoutOrder): Promise { + return this.bscService.sendToken(order.destinationAddress, order.asset, order.amount); + } +} diff --git a/src/payment/models/payout/strategies/payout/payout-defichain-dfi.strategy.ts b/src/payment/models/payout/strategies/payout/impl/defichain-coin.strategy.ts similarity index 56% rename from src/payment/models/payout/strategies/payout/payout-defichain-dfi.strategy.ts rename to src/payment/models/payout/strategies/payout/impl/defichain-coin.strategy.ts index c4f57b12b3..a8879c823d 100644 --- a/src/payment/models/payout/strategies/payout/payout-defichain-dfi.strategy.ts +++ b/src/payment/models/payout/strategies/payout/impl/defichain-coin.strategy.ts @@ -1,19 +1,19 @@ import { Injectable } from '@nestjs/common'; import { MailService } from 'src/shared/services/mail.service'; -import { PayoutOrder, PayoutOrderContext } from '../../entities/payout-order.entity'; -import { PayoutOrderRepository } from '../../repositories/payout-order.repository'; -import { PayoutDeFiChainService } from '../../services/payout-defichain.service'; -import { PayoutDeFiChainStrategy } from './base/payout-defichain.strategy'; +import { PayoutOrderContext, PayoutOrder } from '../../../entities/payout-order.entity'; +import { PayoutOrderRepository } from '../../../repositories/payout-order.repository'; +import { PayoutGroup } from '../../../services/base/payout-jellyfish.service'; +import { PayoutDeFiChainService } from '../../../services/payout-defichain.service'; +import { JellyfishStrategy } from './base/jellyfish.strategy'; @Injectable() -export class PayoutDeFiChainDFIStrategy extends PayoutDeFiChainStrategy { +export class DeFiChainCoinStrategy extends JellyfishStrategy { constructor( mailService: MailService, - protected readonly defichainService: PayoutDeFiChainService, + protected readonly deFiChainService: PayoutDeFiChainService, protected readonly payoutOrderRepo: PayoutOrderRepository, ) { - super(mailService, payoutOrderRepo, defichainService); - this.defichainService.sendUtxoToMany = this.defichainService.sendUtxoToMany.bind(this.defichainService); + super(mailService, payoutOrderRepo, deFiChainService); } protected async doPayoutForContext(context: PayoutOrderContext, orders: PayoutOrder[]): Promise { @@ -38,7 +38,11 @@ export class PayoutDeFiChainDFIStrategy extends PayoutDeFiChainStrategy { } } + protected dispatchPayout(context: PayoutOrderContext, payout: PayoutGroup): Promise { + return this.deFiChainService.sendUtxoToMany(context, payout); + } + private async sendDFI(context: PayoutOrderContext, orders: PayoutOrder[]): Promise { - await this.send(context, orders, 'DFI', this.defichainService.sendUtxoToMany); + await this.send(context, orders, 'DFI'); } } diff --git a/src/payment/models/payout/strategies/payout/payout-defichain-token.strategy.ts b/src/payment/models/payout/strategies/payout/impl/defichain-token.strategy.ts similarity index 70% rename from src/payment/models/payout/strategies/payout/payout-defichain-token.strategy.ts rename to src/payment/models/payout/strategies/payout/impl/defichain-token.strategy.ts index 926dd7c58c..af93a5ffa9 100644 --- a/src/payment/models/payout/strategies/payout/payout-defichain-token.strategy.ts +++ b/src/payment/models/payout/strategies/payout/impl/defichain-token.strategy.ts @@ -1,23 +1,23 @@ import { Injectable } from '@nestjs/common'; import { DexService } from 'src/payment/models/dex/services/dex.service'; import { MailService } from 'src/shared/services/mail.service'; -import { PayoutOrderContext, PayoutOrder } from '../../entities/payout-order.entity'; -import { PayoutOrderRepository } from '../../repositories/payout-order.repository'; -import { PayoutDeFiChainService } from '../../services/payout-defichain.service'; -import { PayoutDeFiChainStrategy } from './base/payout-defichain.strategy'; +import { PayoutOrderContext, PayoutOrder } from '../../../entities/payout-order.entity'; +import { PayoutOrderRepository } from '../../../repositories/payout-order.repository'; +import { PayoutGroup } from '../../../services/base/payout-jellyfish.service'; +import { PayoutDeFiChainService } from '../../../services/payout-defichain.service'; +import { JellyfishStrategy } from './base/jellyfish.strategy'; type TokenName = string; @Injectable() -export class PayoutDeFiChainTokenStrategy extends PayoutDeFiChainStrategy { +export class DeFiChainTokenStrategy extends JellyfishStrategy { constructor( mailService: MailService, private readonly dexService: DexService, - protected readonly defichainService: PayoutDeFiChainService, + protected readonly jellyfishService: PayoutDeFiChainService, protected readonly payoutOrderRepo: PayoutOrderRepository, ) { - super(mailService, payoutOrderRepo, defichainService); - this.defichainService.sendTokenToMany = this.defichainService.sendTokenToMany.bind(this.defichainService); + super(mailService, payoutOrderRepo, jellyfishService); } protected async doPayoutForContext(context: PayoutOrderContext, orders: PayoutOrder[]): Promise { @@ -72,18 +72,22 @@ export class PayoutDeFiChainTokenStrategy extends PayoutDeFiChainStrategy { } private isEligibleForMinimalUtxo(address: string): boolean { - return this.defichainService.isLightWalletAddress(address); + return this.jellyfishService.isLightWalletAddress(address); } private async checkUtxo(address: string): Promise { - const utxo = await this.defichainService.getUtxoForAddress(address); + const utxo = await this.jellyfishService.getUtxoForAddress(address); if (!utxo) { await this.dexService.transferMinimalUtxo(address); } } + protected dispatchPayout(context: PayoutOrderContext, payout: PayoutGroup, outputAsset: string): Promise { + return this.jellyfishService.sendTokenToMany(context, payout, outputAsset); + } + private async sendToken(context: PayoutOrderContext, orders: PayoutOrder[], outputAsset: string): Promise { - await this.send(context, orders, outputAsset, this.defichainService.sendTokenToMany); + await this.send(context, orders, outputAsset); } } diff --git a/src/payment/models/payout/strategies/payout/impl/ethereum-coin.strategy.ts b/src/payment/models/payout/strategies/payout/impl/ethereum-coin.strategy.ts new file mode 100644 index 0000000000..8675ca85d0 --- /dev/null +++ b/src/payment/models/payout/strategies/payout/impl/ethereum-coin.strategy.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; +import { PayoutOrder } from '../../../entities/payout-order.entity'; +import { PayoutOrderRepository } from '../../../repositories/payout-order.repository'; +import { PayoutEthereumService } from '../../../services/payout-ethereum.service'; +import { EvmStrategy } from './base/evm.strategy'; + +@Injectable() +export class EthereumCoinStrategy extends EvmStrategy { + constructor(protected readonly ethereumService: PayoutEthereumService, payoutOrderRepo: PayoutOrderRepository) { + super(ethereumService, payoutOrderRepo); + } + + protected dispatchPayout(order: PayoutOrder): Promise { + return this.ethereumService.sendNativeCoin(order.destinationAddress, order.amount); + } +} diff --git a/src/payment/models/payout/strategies/payout/impl/ethereum-token.strategy.ts b/src/payment/models/payout/strategies/payout/impl/ethereum-token.strategy.ts new file mode 100644 index 0000000000..e26fbe01c6 --- /dev/null +++ b/src/payment/models/payout/strategies/payout/impl/ethereum-token.strategy.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; +import { PayoutOrder } from '../../../entities/payout-order.entity'; +import { PayoutOrderRepository } from '../../../repositories/payout-order.repository'; +import { PayoutEthereumService } from '../../../services/payout-ethereum.service'; +import { EvmStrategy } from './base/evm.strategy'; + +@Injectable() +export class EthereumTokenStrategy extends EvmStrategy { + constructor(protected readonly ethereumService: PayoutEthereumService, payoutOrderRepo: PayoutOrderRepository) { + super(ethereumService, payoutOrderRepo); + } + + protected dispatchPayout(order: PayoutOrder): Promise { + return this.ethereumService.sendToken(order.destinationAddress, order.asset, order.amount); + } +} diff --git a/src/payment/models/payout/strategies/payout/payout-bsc.strategy.ts b/src/payment/models/payout/strategies/payout/payout-bsc.strategy.ts deleted file mode 100644 index ae60d6506e..0000000000 --- a/src/payment/models/payout/strategies/payout/payout-bsc.strategy.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PayoutOrderRepository } from '../../repositories/payout-order.repository'; -import { PayoutBscService } from '../../services/payout-bsc.service'; -import { PayoutEvmStrategy } from './base/payout-evm.strategy'; - -@Injectable() -export class PayoutBscStrategy extends PayoutEvmStrategy { - constructor(bscService: PayoutBscService, payoutOrderRepo: PayoutOrderRepository) { - super(bscService, payoutOrderRepo); - } -} diff --git a/src/payment/models/payout/strategies/payout/payout-ethereum.strategy.ts b/src/payment/models/payout/strategies/payout/payout-ethereum.strategy.ts deleted file mode 100644 index d24f949163..0000000000 --- a/src/payment/models/payout/strategies/payout/payout-ethereum.strategy.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PayoutOrderRepository } from '../../repositories/payout-order.repository'; -import { PayoutEthereumService } from '../../services/payout-ethereum.service'; -import { PayoutEvmStrategy } from './base/payout-evm.strategy'; - -@Injectable() -export class PayoutEthereumStrategy extends PayoutEvmStrategy { - constructor(ethereumService: PayoutEthereumService, payoutOrderRepo: PayoutOrderRepository) { - super(ethereumService, payoutOrderRepo); - } -} diff --git a/src/payment/models/payout/strategies/payout/payout.facade.ts b/src/payment/models/payout/strategies/payout/payout.facade.ts new file mode 100644 index 0000000000..44114f7af7 --- /dev/null +++ b/src/payment/models/payout/strategies/payout/payout.facade.ts @@ -0,0 +1,84 @@ +import { Injectable } from '@nestjs/common'; +import { Blockchain } from 'src/blockchain/shared/enums/blockchain.enum'; +import { Asset, AssetType } from 'src/shared/models/asset/asset.entity'; +import { PayoutStrategy } from './impl/base/payout.strategy'; +import { BitcoinStrategy } from './impl/bitcoin.strategy'; +import { BscCoinStrategy } from './impl/bsc-coin.strategy'; +import { BscTokenStrategy } from './impl/bsc-token.strategy'; +import { DeFiChainCoinStrategy } from './impl/defichain-coin.strategy'; +import { DeFiChainTokenStrategy } from './impl/defichain-token.strategy'; +import { EthereumCoinStrategy } from './impl/ethereum-coin.strategy'; +import { EthereumTokenStrategy } from './impl/ethereum-token.strategy'; + +enum Alias { + BITCOIN = 'Bitcoin', + BSC_COIN = 'BscCoin', + BSC_TOKEN = 'BscToken', + DEFICHAIN_COIN = 'DeFiChainCoin', + DEFICHAIN_TOKEN = 'DeFiChainToken', + ETHEREUM_COIN = 'EthereumCoin', + ETHEREUM_TOKEN = 'EthereumToken', +} + +export { Alias as PayoutStrategyAlias }; + +@Injectable() +export class PayoutStrategiesFacade { + protected readonly strategies: Map = new Map(); + + constructor( + bitcoin: BitcoinStrategy, + bscCoin: BscCoinStrategy, + bscToken: BscTokenStrategy, + deFiChainCoin: DeFiChainCoinStrategy, + deFiChainToken: DeFiChainTokenStrategy, + ethereumCoin: EthereumCoinStrategy, + ethereumToken: EthereumTokenStrategy, + ) { + this.strategies.set(Alias.BITCOIN, bitcoin); + this.strategies.set(Alias.BSC_COIN, bscCoin); + this.strategies.set(Alias.BSC_TOKEN, bscToken); + this.strategies.set(Alias.DEFICHAIN_COIN, deFiChainCoin); + this.strategies.set(Alias.DEFICHAIN_TOKEN, deFiChainToken); + this.strategies.set(Alias.ETHEREUM_COIN, ethereumCoin); + this.strategies.set(Alias.ETHEREUM_TOKEN, ethereumToken); + } + + getPayoutStrategy(criteria: Asset | Alias): PayoutStrategy { + return criteria instanceof Asset ? this.getByAsset(criteria) : this.getByAlias(criteria); + } + + getPayoutStrategyAlias(asset: Asset): Alias { + const { blockchain, type: assetType } = asset; + + if (blockchain === Blockchain.BITCOIN) return Alias.BITCOIN; + + if (blockchain === Blockchain.BINANCE_SMART_CHAIN) { + return assetType === AssetType.COIN ? Alias.BSC_COIN : Alias.BSC_TOKEN; + } + + if (blockchain === Blockchain.DEFICHAIN) { + return assetType === AssetType.COIN ? Alias.DEFICHAIN_COIN : Alias.DEFICHAIN_TOKEN; + } + + if (blockchain === Blockchain.ETHEREUM) { + return assetType === AssetType.COIN ? Alias.ETHEREUM_COIN : Alias.ETHEREUM_TOKEN; + } + } + + //*** HELPER METHODS ***// + + private getByAlias(alias: Alias): PayoutStrategy { + const strategy = this.strategies.get(alias); + + if (!strategy) throw new Error(`No PayoutStrategy found. Alias: ${alias}`); + + return strategy; + } + + private getByAsset(asset: Asset): PayoutStrategy { + const alias = this.getPayoutStrategyAlias(asset); + + return this.getByAlias(alias); + } +} diff --git a/src/payment/models/payout/strategies/prepare/__tests__/prepare.facade.spec.ts b/src/payment/models/payout/strategies/prepare/__tests__/prepare.facade.spec.ts new file mode 100644 index 0000000000..9229e0bbc2 --- /dev/null +++ b/src/payment/models/payout/strategies/prepare/__tests__/prepare.facade.spec.ts @@ -0,0 +1,141 @@ +import { mock } from 'jest-mock-extended'; +import { Blockchain } from 'src/blockchain/shared/enums/blockchain.enum'; +import { DexService } from 'src/payment/models/dex/services/dex.service'; +import { createCustomAsset } from 'src/shared/models/asset/__mocks__/asset.entity.mock'; +import { PayoutOrderRepository } from '../../../repositories/payout-order.repository'; +import { PayoutDeFiChainService } from '../../../services/payout-defichain.service'; +import { BitcoinStrategy } from '../impl/bitcoin.strategy'; +import { BscStrategy } from '../impl/bsc.strategy'; +import { DeFiChainStrategy } from '../impl/defichain.strategy'; +import { EthereumStrategy } from '../impl/ethereum.strategy'; +import { PrepareStrategiesFacade, PrepareStrategyAlias } from '../prepare.facade'; + +describe('PrepareStrategiesFacade', () => { + let bitcoin: BitcoinStrategy; + let defichain: DeFiChainStrategy; + let ethereum: EthereumStrategy; + let bsc: BscStrategy; + + let facade: PrepareStrategiesFacadeWrapper; + + beforeEach(() => { + bitcoin = new BitcoinStrategy(mock()); + defichain = new DeFiChainStrategy( + mock(), + mock(), + mock(), + ); + ethereum = new EthereumStrategy(mock()); + bsc = new BscStrategy(mock()); + + facade = new PrepareStrategiesFacadeWrapper(bitcoin, defichain, ethereum, bsc); + }); + + describe('#constructor(...)', () => { + it('adds all prepareStrategies to a map', () => { + expect([...facade.getStrategies().entries()].length).toBe(4); + }); + + it('assigns strategies to all aliases', () => { + expect([...facade.getStrategies().entries()].length).toBe(Object.values(PrepareStrategyAlias).length); + }); + + it('sets all required prepareStrategies aliases', () => { + const aliases = [...facade.getStrategies().keys()]; + + expect(aliases.includes(PrepareStrategyAlias.BITCOIN)).toBe(true); + expect(aliases.includes(PrepareStrategyAlias.DEFICHAIN)).toBe(true); + expect(aliases.includes(PrepareStrategyAlias.ETHEREUM)).toBe(true); + expect(aliases.includes(PrepareStrategyAlias.BSC)).toBe(true); + }); + + it('assigns proper prepareStrategies to aliases', () => { + expect(facade.getStrategies().get(PrepareStrategyAlias.BITCOIN)).toBeInstanceOf(BitcoinStrategy); + + expect(facade.getStrategies().get(PrepareStrategyAlias.DEFICHAIN)).toBeInstanceOf(DeFiChainStrategy); + + expect(facade.getStrategies().get(PrepareStrategyAlias.ETHEREUM)).toBeInstanceOf(EthereumStrategy); + + expect(facade.getStrategies().get(PrepareStrategyAlias.BSC)).toBeInstanceOf(BscStrategy); + }); + }); + + describe('#getPrepareStrategy(...)', () => { + describe('getting strategy by Asset', () => { + it('gets BITCOIN strategy for BITCOIN', () => { + const strategy = facade.getPrepareStrategy(createCustomAsset({ blockchain: Blockchain.BITCOIN })); + + expect(strategy).toBeInstanceOf(BitcoinStrategy); + }); + + it('gets ETHEREUM strategy', () => { + const strategy = facade.getPrepareStrategy(createCustomAsset({ blockchain: Blockchain.ETHEREUM })); + + expect(strategy).toBeInstanceOf(EthereumStrategy); + }); + + it('gets BSC strategy', () => { + const strategy = facade.getPrepareStrategy(createCustomAsset({ blockchain: Blockchain.BINANCE_SMART_CHAIN })); + + expect(strategy).toBeInstanceOf(BscStrategy); + }); + + it('gets DEFICHAIN strategy for DEFICHAIN', () => { + const strategy = facade.getPrepareStrategy(createCustomAsset({ blockchain: Blockchain.DEFICHAIN })); + + expect(strategy).toBeInstanceOf(DeFiChainStrategy); + }); + + it('fails to get strategy for non-supported Blockchain', () => { + const testCall = () => + facade.getPrepareStrategy(createCustomAsset({ blockchain: 'NewBlockchain' as Blockchain })); + + expect(testCall).toThrow(); + expect(testCall).toThrowError('No PrepareStrategy found. Alias: undefined'); + }); + }); + + describe('getting strategy by Alias', () => { + it('gets BITCOIN strategy', () => { + const strategy = facade.getPrepareStrategy(PrepareStrategyAlias.BITCOIN); + + expect(strategy).toBeInstanceOf(BitcoinStrategy); + }); + + it('gets DEFICHAIN strategy', () => { + const strategy = facade.getPrepareStrategy(PrepareStrategyAlias.DEFICHAIN); + + expect(strategy).toBeInstanceOf(DeFiChainStrategy); + }); + + it('gets ETHEREUM strategy', () => { + const strategyCrypto = facade.getPrepareStrategy(PrepareStrategyAlias.ETHEREUM); + + expect(strategyCrypto).toBeInstanceOf(EthereumStrategy); + }); + + it('gets BSC strategy', () => { + const strategyCrypto = facade.getPrepareStrategy(PrepareStrategyAlias.BSC); + + expect(strategyCrypto).toBeInstanceOf(BscStrategy); + }); + + it('fails to get strategy for non-supported Alias', () => { + const testCall = () => facade.getPrepareStrategy('NonExistingAlias' as PrepareStrategyAlias); + + expect(testCall).toThrow(); + expect(testCall).toThrowError('No PrepareStrategy found. Alias: NonExistingAlias'); + }); + }); + }); +}); + +class PrepareStrategiesFacadeWrapper extends PrepareStrategiesFacade { + constructor(bitcoin: BitcoinStrategy, defichain: DeFiChainStrategy, ethereum: EthereumStrategy, bsc: BscStrategy) { + super(bitcoin, defichain, ethereum, bsc); + } + + getStrategies() { + return this.strategies; + } +} diff --git a/src/payment/models/payout/strategies/prepare/base/prepare-evm.strategy.ts b/src/payment/models/payout/strategies/prepare/impl/base/auto-confirm.strategy.ts similarity index 63% rename from src/payment/models/payout/strategies/prepare/base/prepare-evm.strategy.ts rename to src/payment/models/payout/strategies/prepare/impl/base/auto-confirm.strategy.ts index 143e71fb9d..baeb2e55c3 100644 --- a/src/payment/models/payout/strategies/prepare/base/prepare-evm.strategy.ts +++ b/src/payment/models/payout/strategies/prepare/impl/base/auto-confirm.strategy.ts @@ -1,8 +1,8 @@ -import { PayoutOrder } from '../../../entities/payout-order.entity'; -import { PayoutOrderRepository } from '../../../repositories/payout-order.repository'; +import { PayoutOrder } from '../../../../entities/payout-order.entity'; +import { PayoutOrderRepository } from '../../../../repositories/payout-order.repository'; import { PrepareStrategy } from './prepare.strategy'; -export abstract class PrepareEvmStrategy implements PrepareStrategy { +export abstract class AutoConfirmStrategy implements PrepareStrategy { constructor(protected readonly payoutOrderRepo: PayoutOrderRepository) {} async preparePayout(order: PayoutOrder): Promise { diff --git a/src/payment/models/payout/strategies/prepare/impl/base/evm.strategy.ts b/src/payment/models/payout/strategies/prepare/impl/base/evm.strategy.ts new file mode 100644 index 0000000000..9d4290d339 --- /dev/null +++ b/src/payment/models/payout/strategies/prepare/impl/base/evm.strategy.ts @@ -0,0 +1,8 @@ +import { PayoutOrderRepository } from '../../../../repositories/payout-order.repository'; +import { AutoConfirmStrategy } from './auto-confirm.strategy'; + +export abstract class EvmStrategy extends AutoConfirmStrategy { + constructor(payoutOrderRepo: PayoutOrderRepository) { + super(payoutOrderRepo); + } +} diff --git a/src/payment/models/payout/strategies/prepare/base/prepare.strategy.ts b/src/payment/models/payout/strategies/prepare/impl/base/prepare.strategy.ts similarity index 68% rename from src/payment/models/payout/strategies/prepare/base/prepare.strategy.ts rename to src/payment/models/payout/strategies/prepare/impl/base/prepare.strategy.ts index 688ce60ae5..f3aba2e484 100644 --- a/src/payment/models/payout/strategies/prepare/base/prepare.strategy.ts +++ b/src/payment/models/payout/strategies/prepare/impl/base/prepare.strategy.ts @@ -1,4 +1,4 @@ -import { PayoutOrder } from '../../../entities/payout-order.entity'; +import { PayoutOrder } from '../../../../entities/payout-order.entity'; export interface PrepareStrategy { preparePayout(order: PayoutOrder): Promise; diff --git a/src/payment/models/payout/strategies/prepare/impl/bitcoin.strategy.ts b/src/payment/models/payout/strategies/prepare/impl/bitcoin.strategy.ts new file mode 100644 index 0000000000..5dcbc671c2 --- /dev/null +++ b/src/payment/models/payout/strategies/prepare/impl/bitcoin.strategy.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@nestjs/common'; +import { PayoutOrderRepository } from '../../../repositories/payout-order.repository'; +import { AutoConfirmStrategy } from './base/auto-confirm.strategy'; + +@Injectable() +export class BitcoinStrategy extends AutoConfirmStrategy { + constructor(payoutOrderRepo: PayoutOrderRepository) { + super(payoutOrderRepo); + } +} diff --git a/src/payment/models/payout/strategies/prepare/impl/bsc.strategy.ts b/src/payment/models/payout/strategies/prepare/impl/bsc.strategy.ts new file mode 100644 index 0000000000..7725b5996d --- /dev/null +++ b/src/payment/models/payout/strategies/prepare/impl/bsc.strategy.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@nestjs/common'; +import { PayoutOrderRepository } from '../../../repositories/payout-order.repository'; +import { EvmStrategy } from './base/evm.strategy'; + +@Injectable() +export class BscStrategy extends EvmStrategy { + constructor(payoutOrderRepo: PayoutOrderRepository) { + super(payoutOrderRepo); + } +} diff --git a/src/payment/models/payout/strategies/prepare/prepare-defichain.strategy.ts b/src/payment/models/payout/strategies/prepare/impl/defichain.strategy.ts similarity index 85% rename from src/payment/models/payout/strategies/prepare/prepare-defichain.strategy.ts rename to src/payment/models/payout/strategies/prepare/impl/defichain.strategy.ts index 38741b19fc..2ca2bac7b4 100644 --- a/src/payment/models/payout/strategies/prepare/prepare-defichain.strategy.ts +++ b/src/payment/models/payout/strategies/prepare/impl/defichain.strategy.ts @@ -1,12 +1,12 @@ import { Injectable } from '@nestjs/common'; import { DexService } from 'src/payment/models/dex/services/dex.service'; -import { PayoutOrder } from '../../entities/payout-order.entity'; -import { PayoutOrderRepository } from '../../repositories/payout-order.repository'; -import { PayoutDeFiChainService } from '../../services/payout-defichain.service'; +import { PayoutOrder } from '../../../entities/payout-order.entity'; +import { PayoutOrderRepository } from '../../../repositories/payout-order.repository'; +import { PayoutDeFiChainService } from '../../../services/payout-defichain.service'; import { PrepareStrategy } from './base/prepare.strategy'; @Injectable() -export class PrepareDeFiChainStrategy implements PrepareStrategy { +export class DeFiChainStrategy implements PrepareStrategy { constructor( private readonly dexService: DexService, private readonly defichainService: PayoutDeFiChainService, diff --git a/src/payment/models/payout/strategies/prepare/impl/ethereum.strategy.ts b/src/payment/models/payout/strategies/prepare/impl/ethereum.strategy.ts new file mode 100644 index 0000000000..e53acd59e1 --- /dev/null +++ b/src/payment/models/payout/strategies/prepare/impl/ethereum.strategy.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@nestjs/common'; +import { PayoutOrderRepository } from '../../../repositories/payout-order.repository'; +import { EvmStrategy } from './base/evm.strategy'; + +@Injectable() +export class EthereumStrategy extends EvmStrategy { + constructor(payoutOrderRepo: PayoutOrderRepository) { + super(payoutOrderRepo); + } +} diff --git a/src/payment/models/payout/strategies/prepare/prepare-bsc.strategy.ts b/src/payment/models/payout/strategies/prepare/prepare-bsc.strategy.ts deleted file mode 100644 index df94ff5c22..0000000000 --- a/src/payment/models/payout/strategies/prepare/prepare-bsc.strategy.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PayoutOrderRepository } from '../../repositories/payout-order.repository'; -import { PrepareEvmStrategy } from './base/prepare-evm.strategy'; - -@Injectable() -export class PrepareBscStrategy extends PrepareEvmStrategy { - constructor(payoutOrderRepo: PayoutOrderRepository) { - super(payoutOrderRepo); - } -} diff --git a/src/payment/models/payout/strategies/prepare/prepare-ethereum.strategy.ts b/src/payment/models/payout/strategies/prepare/prepare-ethereum.strategy.ts deleted file mode 100644 index 7ebca7123e..0000000000 --- a/src/payment/models/payout/strategies/prepare/prepare-ethereum.strategy.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PayoutOrderRepository } from '../../repositories/payout-order.repository'; -import { PrepareEvmStrategy } from './base/prepare-evm.strategy'; - -@Injectable() -export class PrepareEthereumStrategy extends PrepareEvmStrategy { - constructor(payoutOrderRepo: PayoutOrderRepository) { - super(payoutOrderRepo); - } -} diff --git a/src/payment/models/payout/strategies/prepare/prepare.facade.ts b/src/payment/models/payout/strategies/prepare/prepare.facade.ts new file mode 100644 index 0000000000..d0245d293a --- /dev/null +++ b/src/payment/models/payout/strategies/prepare/prepare.facade.ts @@ -0,0 +1,63 @@ +import { Injectable } from '@nestjs/common'; +import { Blockchain } from 'src/blockchain/shared/enums/blockchain.enum'; +import { Asset } from 'src/shared/models/asset/asset.entity'; +import { PrepareStrategy } from './impl/base/prepare.strategy'; +import { BitcoinStrategy } from './impl/bitcoin.strategy'; +import { BscStrategy } from './impl/bsc.strategy'; +import { DeFiChainStrategy } from './impl/defichain.strategy'; +import { EthereumStrategy } from './impl/ethereum.strategy'; + +enum Alias { + BITCOIN = 'Bitcoin', + DEFICHAIN = 'DeFiChain', + ETHEREUM = 'Ethereum', + BSC = 'Bsc', +} + +export { Alias as PrepareStrategyAlias }; + +@Injectable() +export class PrepareStrategiesFacade { + protected readonly strategies: Map = new Map(); + + constructor( + bitcoin: BitcoinStrategy, + deFiChainStrategy: DeFiChainStrategy, + ethereumStrategy: EthereumStrategy, + bscStrategy: BscStrategy, + ) { + this.strategies.set(Alias.BITCOIN, bitcoin); + this.strategies.set(Alias.DEFICHAIN, deFiChainStrategy); + this.strategies.set(Alias.ETHEREUM, ethereumStrategy); + this.strategies.set(Alias.BSC, bscStrategy); + } + + getPrepareStrategy(criteria: Asset | Alias): PrepareStrategy { + return criteria instanceof Asset ? this.getByAsset(criteria) : this.getByAlias(criteria); + } + + //*** HELPER METHODS ***// + + private getByAlias(alias: Alias): PrepareStrategy { + const strategy = this.strategies.get(alias); + + if (!strategy) throw new Error(`No PrepareStrategy found. Alias: ${alias}`); + + return strategy; + } + + private getByAsset(asset: Asset): PrepareStrategy { + const alias = this.getAlias(asset); + + return this.getByAlias(alias); + } + + private getAlias(asset: Asset): Alias { + const { blockchain } = asset; + + if (blockchain === Blockchain.BITCOIN) return Alias.BITCOIN; + if (blockchain === Blockchain.ETHEREUM) return Alias.ETHEREUM; + if (blockchain === Blockchain.BINANCE_SMART_CHAIN) return Alias.BSC; + if (blockchain === Blockchain.DEFICHAIN) return Alias.DEFICHAIN; + } +} diff --git a/src/payment/models/payout/strategies/strategies.facade.ts b/src/payment/models/payout/strategies/strategies.facade.ts deleted file mode 100644 index ba12becb9a..0000000000 --- a/src/payment/models/payout/strategies/strategies.facade.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { Blockchain } from 'src/blockchain/shared/enums/blockchain.enum'; -import { Asset } from 'src/shared/models/asset/asset.entity'; -import { PayoutBscStrategy } from './payout/payout-bsc.strategy'; -import { PayoutDeFiChainDFIStrategy } from './payout/payout-defichain-dfi.strategy'; -import { PayoutEthereumStrategy } from './payout/payout-ethereum.strategy'; -import { PayoutDeFiChainTokenStrategy } from './payout/payout-defichain-token.strategy'; -import { PayoutStrategy } from './payout/base/payout.strategy'; -import { PrepareBscStrategy } from './prepare/prepare-bsc.strategy'; -import { PrepareDeFiChainStrategy } from './prepare/prepare-defichain.strategy'; -import { PrepareEthereumStrategy } from './prepare/prepare-ethereum.strategy'; -import { PrepareStrategy } from './prepare/base/prepare.strategy'; - -export enum PayoutStrategyAlias { - DEFICHAIN_DFI = 'DeFiChainDFI', - DEFICHAIN_TOKEN = 'DeFiChainToken', - ETHEREUM_DEFAULT = 'Ethereum', - BSC_DEFAULT = 'BscDefault', -} - -export enum PrepareStrategyAlias { - DEFICHAIN = 'DeFiChain', - ETHEREUM = 'Ethereum', - BSC = 'Bsc', -} - -@Injectable() -export class PayoutStrategiesFacade { - protected readonly payoutStrategies: Map = new Map(); - protected readonly prepareStrategies: Map = new Map(); - - constructor( - payoutDFIStrategy: PayoutDeFiChainDFIStrategy, - payoutTokenStrategy: PayoutDeFiChainTokenStrategy, - payoutEthStrategy: PayoutEthereumStrategy, - payoutBscStrategy: PayoutBscStrategy, - prepareOnDefichainStrategy: PrepareDeFiChainStrategy, - prepareOnEthereumStrategy: PrepareEthereumStrategy, - prepareOnBscStrategy: PrepareBscStrategy, - ) { - this.payoutStrategies.set(PayoutStrategyAlias.DEFICHAIN_DFI, payoutDFIStrategy); - this.payoutStrategies.set(PayoutStrategyAlias.DEFICHAIN_TOKEN, payoutTokenStrategy); - this.payoutStrategies.set(PayoutStrategyAlias.ETHEREUM_DEFAULT, payoutEthStrategy); - this.payoutStrategies.set(PayoutStrategyAlias.BSC_DEFAULT, payoutBscStrategy); - - this.prepareStrategies.set(PrepareStrategyAlias.DEFICHAIN, prepareOnDefichainStrategy); - this.prepareStrategies.set(PrepareStrategyAlias.ETHEREUM, prepareOnEthereumStrategy); - this.prepareStrategies.set(PrepareStrategyAlias.BSC, prepareOnBscStrategy); - } - - getPayoutStrategy(criteria: Asset | PayoutStrategyAlias): PayoutStrategy { - return criteria instanceof Asset - ? this.getPayoutStrategyByAsset(criteria) - : this.getPayoutStrategyByAlias(criteria); - } - - getPrepareStrategy(criteria: Asset | PrepareStrategyAlias): PrepareStrategy { - return criteria instanceof Asset - ? this.getPrepareStrategyByAsset(criteria) - : this.getPrepareStrategyByAlias(criteria); - } - - //*** HELPER METHODS ***// - - private getPayoutStrategyByAlias(alias: PayoutStrategyAlias): PayoutStrategy { - const strategy = this.payoutStrategies.get(alias); - - if (!strategy) throw new Error(`No PayoutStrategy found. Alias: ${alias}`); - - return strategy; - } - - private getPayoutStrategyByAsset(asset: Asset): PayoutStrategy { - const alias = this.getPayoutStrategyAlias(asset); - - return this.getPayoutStrategyByAlias(alias); - } - - private getPrepareStrategyByAlias(alias: PrepareStrategyAlias): PrepareStrategy { - const strategy = this.prepareStrategies.get(alias); - - if (!strategy) throw new Error(`No PrepareStrategy found. Alias: ${alias}`); - - return strategy; - } - - private getPrepareStrategyByAsset(asset: Asset): PrepareStrategy { - const alias = this.getPrepareStrategyAlias(asset); - - return this.getPrepareStrategyByAlias(alias); - } - - private getPayoutStrategyAlias(asset: Asset): PayoutStrategyAlias { - const { blockchain, dexName: assetName } = asset; - - if (blockchain === Blockchain.ETHEREUM) return PayoutStrategyAlias.ETHEREUM_DEFAULT; - if (blockchain === Blockchain.BINANCE_SMART_CHAIN) return PayoutStrategyAlias.BSC_DEFAULT; - if (blockchain === Blockchain.DEFICHAIN && assetName === 'DFI') return PayoutStrategyAlias.DEFICHAIN_DFI; - if ((blockchain === Blockchain.DEFICHAIN || blockchain === Blockchain.BITCOIN) && assetName !== 'DFI') { - return PayoutStrategyAlias.DEFICHAIN_TOKEN; - } - } - - private getPrepareStrategyAlias(asset: Asset): PrepareStrategyAlias { - const { blockchain } = asset; - - if (blockchain === Blockchain.ETHEREUM) return PrepareStrategyAlias.ETHEREUM; - if (blockchain === Blockchain.BINANCE_SMART_CHAIN) return PrepareStrategyAlias.BSC; - if (blockchain === Blockchain.DEFICHAIN || blockchain === Blockchain.BITCOIN) return PrepareStrategyAlias.DEFICHAIN; - } -} diff --git a/src/payment/models/pricing/__tests__/pricing.integration.spec.ts b/src/payment/models/pricing/__tests__/pricing.integration.spec.ts index 0d3f7a9314..279b38090a 100644 --- a/src/payment/models/pricing/__tests__/pricing.integration.spec.ts +++ b/src/payment/models/pricing/__tests__/pricing.integration.spec.ts @@ -9,6 +9,7 @@ import { CurrencyService } from '../../exchange/services/currency.service'; import { FixerService } from '../../exchange/services/fixer.service'; import { FtxService } from '../../exchange/services/ftx.service'; import { KrakenService } from '../../exchange/services/kraken.service'; +import { DfiPricingDexService } from '../services/dfi-pricing-dex.service'; import { PricingService } from '../services/pricing.service'; describe('Pricing Module Integration Tests', () => { @@ -20,6 +21,7 @@ describe('Pricing Module Integration Tests', () => { let ftxService: FtxService; let currencyService: CurrencyService; let fixerService: FixerService; + let dfiDexService: DfiPricingDexService; let krakenServiceGetPriceSpy: jest.SpyInstance; let binanceServiceGetPriceSpy: jest.SpyInstance; @@ -28,6 +30,7 @@ describe('Pricing Module Integration Tests', () => { let ftxServiceGetPriceSpy: jest.SpyInstance; let currencyServiceGetPriceSpy: jest.SpyInstance; let fixerServiceGetPriceSpy: jest.SpyInstance; + let dfiDexServiceGetPriceSpy: jest.SpyInstance; let service: PricingService; @@ -40,6 +43,7 @@ describe('Pricing Module Integration Tests', () => { ftxService = mock({ name: 'Ftx' }); currencyService = mock({ name: 'CurrencyService' }); fixerService = mock({ name: 'FixerService' }); + dfiDexService = mock({ name: 'DfiPricingDexService' }); service = new PricingService( mailService, @@ -50,6 +54,7 @@ describe('Pricing Module Integration Tests', () => { ftxService, currencyService, fixerService, + dfiDexService, ); krakenServiceGetPriceSpy = jest.spyOn(krakenService, 'getPrice'); @@ -59,6 +64,7 @@ describe('Pricing Module Integration Tests', () => { ftxServiceGetPriceSpy = jest.spyOn(ftxService, 'getPrice'); currencyServiceGetPriceSpy = jest.spyOn(currencyService, 'getPrice'); fixerServiceGetPriceSpy = jest.spyOn(fixerService, 'getPrice'); + dfiDexServiceGetPriceSpy = jest.spyOn(dfiDexService, 'getPrice'); }); afterEach(() => { @@ -69,6 +75,7 @@ describe('Pricing Module Integration Tests', () => { ftxServiceGetPriceSpy.mockClear(); currencyServiceGetPriceSpy.mockClear(); fixerServiceGetPriceSpy.mockClear(); + dfiDexServiceGetPriceSpy.mockClear(); }); it('calculates price path for MATCHING_ASSETS', async () => { @@ -306,6 +313,41 @@ describe('Pricing Module Integration Tests', () => { expect(result.path[0].timestamp).toBeInstanceOf(Date); }); + it('calculates price path for NON_MATCHING_FIAT_TO_BUSD', async () => { + krakenServiceGetPriceSpy = jest + .spyOn(krakenService, 'getPrice') + .mockImplementationOnce(async () => { + throw new Error(); + }) + .mockImplementationOnce(async (source, target) => createCustomPrice({ source, target, price: 1.1 })); + + fixerServiceGetPriceSpy = jest + .spyOn(fixerService, 'getPrice') + .mockImplementationOnce(async (source, target) => createCustomPrice({ source, target, price: 1.1 })); + + const request = { from: 'EUR', to: 'BUSD' }; + const result = await service.getPrice(request); + + expect(fixerServiceGetPriceSpy).toHaveBeenCalledWith('EUR', 'USD'); + + expect(result.price).toBeInstanceOf(Price); + expect(result.price.source).toBe('EUR'); + expect(result.price.target).toBe('BUSD'); + expect(result.price.price).toBe(1.1); + + expect(Array.isArray(result.path)).toBe(true); + expect(result.path.length).toBe(1); + + expect(result.path[0].provider).toBe('Kraken'); + + expect(result.path[0].price).toBeInstanceOf(Price); + expect(result.path[0].price.source).toBe('EUR'); + expect(result.path[0].price.target).toBe('USDC'); + expect(result.path[0].price.price).toBe(1.1); + + expect(result.path[0].timestamp).toBeInstanceOf(Date); + }); + it('calculates price path for NON_MATCHING_FIAT_TO_USD_STABLE_COIN', async () => { krakenServiceGetPriceSpy = jest .spyOn(krakenService, 'getPrice') @@ -359,4 +401,49 @@ describe('Pricing Module Integration Tests', () => { expect(result.path[0].timestamp).toBeInstanceOf(Date); }); + + it('calculates price path for FIAT_TO_DFI', async () => { + krakenServiceGetPriceSpy = jest + .spyOn(krakenService, 'getPrice') + .mockImplementationOnce(async (source: string, target: string) => + createCustomPrice({ source, target, price: 0.000049 }), + ); + + dfiDexServiceGetPriceSpy = jest + .spyOn(dfiDexService, 'getPrice') + .mockImplementationOnce(async (source: string, target: string) => + createCustomPrice({ source, target, price: 23111 }), + ); + + const request = { from: 'EUR', to: 'DFI' }; + const result = await service.getPrice(request); + + expect(result.price).toBeInstanceOf(Price); + expect(result.price.source).toBe('EUR'); + expect(result.price.target).toBe('DFI'); + expect(result.price.price).toBe(1.132439); + + expect(Array.isArray(result.path)).toBe(true); + expect(result.path.length).toBe(2); + + expect(result.path[0].provider).toBe('Kraken'); + + expect(result.path[0].price).toBeInstanceOf(Price); + expect(result.path[0].price.source).toBe('EUR'); + expect(result.path[0].price.target).toBe('BTC'); + expect(result.path[0].price.price).toBe(0.000049); + + expect(result.path[0].timestamp).toBeInstanceOf(Date); + + expect(result.path[1].provider).toBe('DfiPricingDexService'); + + expect(result.path[1].price).toBeInstanceOf(Price); + expect(result.path[1].price.source).toBe('BTC'); + expect(result.path[1].price.target).toBe('DFI'); + expect(result.path[1].price.price).toBe(23111); + + expect(result.path[1].timestamp).toBeInstanceOf(Date); + + expect(result.path[1].provider).toBe('DfiPricingDexService'); + }); }); diff --git a/src/payment/models/pricing/enums/index.ts b/src/payment/models/pricing/enums/index.ts index a9255f3611..56129fa986 100644 --- a/src/payment/models/pricing/enums/index.ts +++ b/src/payment/models/pricing/enums/index.ts @@ -8,6 +8,7 @@ export enum Fiat { export enum USDStableCoin { USDC = 'USDC', USDT = 'USDT', + BUSD = 'BUSD', } export enum Altcoin { diff --git a/src/payment/models/pricing/pricing.module.ts b/src/payment/models/pricing/pricing.module.ts index 3866e0c399..cf2d4a8314 100644 --- a/src/payment/models/pricing/pricing.module.ts +++ b/src/payment/models/pricing/pricing.module.ts @@ -2,12 +2,13 @@ import { Module } from '@nestjs/common'; import { SharedModule } from 'src/shared/shared.module'; import { DexModule } from '../dex/dex.module'; import { ExchangeModule } from '../exchange/exchange.module'; +import { DfiPricingDexService } from './services/dfi-pricing-dex.service'; import { PricingService } from './services/pricing.service'; @Module({ imports: [SharedModule, ExchangeModule, DexModule], controllers: [], - providers: [PricingService], + providers: [PricingService, DfiPricingDexService], exports: [PricingService], }) export class PricingModule {} diff --git a/src/payment/models/pricing/services/dfi-pricing-dex.service.ts b/src/payment/models/pricing/services/dfi-pricing-dex.service.ts new file mode 100644 index 0000000000..75e8470ec3 --- /dev/null +++ b/src/payment/models/pricing/services/dfi-pricing-dex.service.ts @@ -0,0 +1,43 @@ +import { v4 as uuid } from 'uuid'; +import { Injectable } from '@nestjs/common'; +import { Blockchain } from 'src/blockchain/shared/enums/blockchain.enum'; +import { AssetService } from 'src/shared/models/asset/asset.service'; +import { LiquidityOrderContext } from '../../dex/entities/liquidity-order.entity'; +import { LiquidityRequest } from '../../dex/interfaces'; +import { DexService } from '../../dex/services/dex.service'; +import { Price } from '../../exchange/dto/price.dto'; +import { PriceProvider } from '../interfaces'; +import { Util } from 'src/shared/util'; + +@Injectable() +export class DfiPricingDexService implements PriceProvider { + name: string; + + constructor(private dexService: DexService, private assetService: AssetService) { + this.name = 'DfiDex'; + } + + async getPrice(from: string, to: string): Promise { + if (to !== 'DFI') { + throw new Error(`DfiPricingDexService supports only DFI as target asset, instead provided: ${to}`); + } + + const dfi = await this.assetService.getAssetByQuery({ dexName: 'DFI', blockchain: Blockchain.DEFICHAIN }); + + const liquidityRequest: LiquidityRequest = { + context: LiquidityOrderContext.PRICING, + correlationId: uuid(), + referenceAsset: from, + referenceAmount: 0.001, + targetAsset: dfi, + options: { + bypassAvailabilityCheck: true, + bypassSlippageProtection: true, + }, + }; + + const targetAmount = await this.dexService.checkLiquidity(liquidityRequest); + + return Price.create(from, to, Util.round(targetAmount / 0.001, 8)); + } +} diff --git a/src/payment/models/pricing/services/pricing.service.ts b/src/payment/models/pricing/services/pricing.service.ts index 8163baadaf..c16577b603 100644 --- a/src/payment/models/pricing/services/pricing.service.ts +++ b/src/payment/models/pricing/services/pricing.service.ts @@ -14,6 +14,7 @@ import { PathNotConfiguredException } from '../exceptions/path-not-configured.ex import { PriceRequest, PriceResult } from '../interfaces'; import { PricePath } from '../utils/price-path'; import { PriceStep } from '../utils/price-step'; +import { DfiPricingDexService } from './dfi-pricing-dex.service'; export enum PricingPathAlias { MATCHING_ASSETS = 'MatchingAssets', @@ -23,8 +24,10 @@ export enum PricingPathAlias { ALTCOIN_TO_ALTCOIN = 'AltcoinToAltcoin', BTC_TO_ALTCOIN = 'BTCToAltcoin', MATCHING_FIAT_TO_USD_STABLE_COIN = 'MatchingFiatToUSDStableCoin', + NON_MATCHING_FIAT_TO_BUSD = 'NonMatchingFiatToBUSD', NON_MATCHING_FIAT_TO_USD_STABLE_COIN = 'NonMatchingFiatToUSDStableCoin', NON_MATCHING_USD_STABLE_COIN_TO_USD_STABLE_COIN = 'NonMatchingUSDStableCoinToUSDStableCoin', + FIAT_TO_DFI = 'FiatToDfi', } @Injectable() @@ -40,6 +43,7 @@ export class PricingService { private readonly ftxService: FtxService, private readonly currencyService: CurrencyService, private readonly fixerService: FixerService, + private readonly dfiDexService: DfiPricingDexService, ) { this.configurePaths(); } @@ -163,10 +167,23 @@ export class PricingService { ]), ); + this.addPath( + new PricePath(PricingPathAlias.NON_MATCHING_FIAT_TO_BUSD, [ + new PriceStep({ + overwriteReferenceTo: 'USD', + fallbackPrimaryTo: 'USDC', + providers: { + primary: [this.krakenService], + reference: [this.fixerService, this.currencyService], + }, + }), + ]), + ); + this.addPath( new PricePath(PricingPathAlias.NON_MATCHING_FIAT_TO_USD_STABLE_COIN, [ new PriceStep({ - referenceTo: 'USD', + overwriteReferenceTo: 'USD', providers: { primary: [this.krakenService], reference: [this.fixerService, this.currencyService], @@ -182,6 +199,25 @@ export class PricingService { }), ]), ); + + this.addPath( + new PricePath(PricingPathAlias.FIAT_TO_DFI, [ + new PriceStep({ + to: 'BTC', + providers: { + primary: [this.krakenService], + reference: [this.binanceService, this.bitstampService, this.bitpandaService], + }, + }), + new PriceStep({ + from: 'BTC', + providers: { + primary: [this.dfiDexService], + reference: [], + }, + }), + ]), + ); } //*** HELPER METHODS ***// @@ -224,12 +260,17 @@ export class PricingService { if (from === 'USD' && this.isUSDStablecoin(to)) return PricingPathAlias.MATCHING_FIAT_TO_USD_STABLE_COIN; + if (this.isFiat(from) && this.isUSDStablecoin(to) && to === 'BUSD') + return PricingPathAlias.NON_MATCHING_FIAT_TO_BUSD; + if (this.isFiat(from) && this.isUSDStablecoin(to)) return PricingPathAlias.NON_MATCHING_FIAT_TO_USD_STABLE_COIN; if (this.isUSDStablecoin(from) && this.isUSDStablecoin(to) && from !== to) { return PricingPathAlias.NON_MATCHING_USD_STABLE_COIN_TO_USD_STABLE_COIN; } + if (this.isFiat(from) && to === 'DFI') return PricingPathAlias.FIAT_TO_DFI; + throw new Error(`No matching pricing path alias found. From: ${request.from} to: ${request.to}`); } @@ -250,7 +291,9 @@ export class PricingService { } private isKnownAsset(asset: string): boolean { - return this.isFiat(asset) || this.isBTC(asset) || this.isAltcoin(asset) || this.isUSDStablecoin(asset); + return ( + this.isFiat(asset) || this.isBTC(asset) || this.isAltcoin(asset) || this.isUSDStablecoin(asset) || asset === 'DFI' + ); } private logPriceResult(request: PriceRequest, result: PriceResult, pathAlias: PricingPathAlias): void { diff --git a/src/payment/models/pricing/utils/__mocks__/price-step.mock.ts b/src/payment/models/pricing/utils/__mocks__/price-step.mock.ts index f10f90870e..43c5caa8a4 100644 --- a/src/payment/models/pricing/utils/__mocks__/price-step.mock.ts +++ b/src/payment/models/pricing/utils/__mocks__/price-step.mock.ts @@ -5,7 +5,7 @@ export function createDefaultPriceStep(): PriceStep { } export function createCustomPriceStep(customOptions: Partial): PriceStep { - const { from, to, referenceTo, providers, fixedPrice } = customOptions; + const { from, to, overwriteReferenceTo: referenceTo, providers, fixedPrice } = customOptions; const keys = Object.keys(customOptions); diff --git a/src/payment/models/pricing/utils/price-path.ts b/src/payment/models/pricing/utils/price-path.ts index e049bb801e..fc6566524f 100644 --- a/src/payment/models/pricing/utils/price-path.ts +++ b/src/payment/models/pricing/utils/price-path.ts @@ -34,26 +34,23 @@ export class PricePath { results.push(await step.execute()); } - return this.calculatePrice(results); + return this.calculatePrice(request, results); } //*** HELPER METHODS ***// - private calculatePrice(path: PriceStepResult[]): PriceResult { + private calculatePrice(request: PriceRequest, path: PriceStepResult[]): PriceResult { let result = 1; path.forEach((step) => { result = result * step.price.price; }); - return this.createPriceResult(path, result); + return this.createPriceResult(request, path, result); } - private createPriceResult(path: PriceStepResult[], targetPrice: number): PriceResult { - const firstStep = path[0]; - const lastStep = path[path.length - 1]; - - const price = Price.create(firstStep.price.source, lastStep.price.target, targetPrice); + private createPriceResult(request: PriceRequest, path: PriceStepResult[], targetPrice: number): PriceResult { + const price = Price.create(request.from, request.to, targetPrice); return { price, path }; } diff --git a/src/payment/models/pricing/utils/price-step.ts b/src/payment/models/pricing/utils/price-step.ts index fbdeb01291..af29b89ecc 100644 --- a/src/payment/models/pricing/utils/price-step.ts +++ b/src/payment/models/pricing/utils/price-step.ts @@ -7,7 +7,8 @@ import { PriceStepInitSpecification } from '../specifications/price-step-init.sp export interface PriceStepOptions { from?: string | 'input'; to?: string | 'output'; - referenceTo?: string; + overwriteReferenceTo?: string; + fallbackPrimaryTo?: string; providers?: PriceStepProviders; fixedPrice?: number; } @@ -24,7 +25,8 @@ export class PriceStep { this.options = { from: options.from || 'input', to: options.to || 'output', - referenceTo: options.referenceTo, + overwriteReferenceTo: options.overwriteReferenceTo, + fallbackPrimaryTo: options.fallbackPrimaryTo, providers: { primary: options.providers?.primary || [], reference: options.providers?.reference || [], @@ -102,14 +104,14 @@ export class PriceStep { private async getPrimaryPrice(fromCurrency: string, toCurrency: string): Promise<[Price, PriceProviderName]> { const primaryProviders = this.options.providers.primary; - const [price, providerName] = await this.tryProviders(fromCurrency, toCurrency, primaryProviders); + let [price, providerName] = await this.tryProviders(fromCurrency, toCurrency, primaryProviders); + + if (!price && this.options.fallbackPrimaryTo) { + [price, providerName] = await this.tryProviders(fromCurrency, this.options.fallbackPrimaryTo, primaryProviders); + } if (!price) { - throw new Error( - `Could not find primary price at: ${primaryProviders.map( - (p) => p.name + ' ', - )}. From ${fromCurrency} to ${toCurrency}`, - ); + throw new Error(this.createPrimaryPriceErrorMessage(primaryProviders, fromCurrency, toCurrency)); } return [price, providerName]; @@ -118,8 +120,8 @@ export class PriceStep { private async getReferencePrice(fromCurrency: string, toCurrency: string): Promise<[Price, PriceProviderName]> { const referenceProviders = this.options.providers.reference; - const [price, providerName] = this.options.referenceTo - ? await this.tryProviders(fromCurrency, this.options.referenceTo, referenceProviders) + const [price, providerName] = this.options.overwriteReferenceTo + ? await this.tryProviders(fromCurrency, this.options.overwriteReferenceTo, referenceProviders) : await this.tryProviders(fromCurrency, toCurrency, referenceProviders); if (!price) { @@ -149,6 +151,20 @@ export class PriceStep { return [null, null]; } + private createPrimaryPriceErrorMessage( + primaryProviders: PriceProvider[], + fromCurrency: string, + toCurrency: string, + ): string { + const mainMessage = `Could not find primary price at: ${primaryProviders.map( + (p) => p.name + ' ', + )}. From ${fromCurrency} to ${toCurrency}. `; + + const fallbackMessage = this.options.fallbackPrimaryTo && `Fallback to currency: ${this.options.fallbackPrimaryTo}`; + + return mainMessage + fallbackMessage; + } + //*** GETTERS ***// get _options(): PriceStepOptions { diff --git a/src/shared/models/asset/__mocks__/asset.entity.mock.ts b/src/shared/models/asset/__mocks__/asset.entity.mock.ts index 305cc294a5..df8276d56a 100644 --- a/src/shared/models/asset/__mocks__/asset.entity.mock.ts +++ b/src/shared/models/asset/__mocks__/asset.entity.mock.ts @@ -1,12 +1,12 @@ import { Blockchain } from 'src/blockchain/shared/enums/blockchain.enum'; -import { Asset, AssetCategory } from '../asset.entity'; +import { Asset, AssetCategory, AssetType } from '../asset.entity'; export function createDefaultAsset(): Asset { return createCustomAsset({}); } export function createCustomAsset(customValues: Partial): Asset { - const { name, dexName, blockchain, category } = customValues; + const { name, dexName, blockchain, category, type } = customValues; const keys = Object.keys(customValues); const entity = new Asset(); @@ -15,6 +15,7 @@ export function createCustomAsset(customValues: Partial): Asset { entity.dexName = keys.includes('dexName') ? dexName : 'dTSLA'; entity.blockchain = keys.includes('blockchain') ? blockchain : Blockchain.DEFICHAIN; entity.category = keys.includes('category') ? category : AssetCategory.CRYPTO; + entity.type = keys.includes('type') ? type : AssetType.COIN; return entity; } diff --git a/src/shared/models/asset/asset.entity.ts b/src/shared/models/asset/asset.entity.ts index 3f5a231c75..ed12599f05 100644 --- a/src/shared/models/asset/asset.entity.ts +++ b/src/shared/models/asset/asset.entity.ts @@ -4,8 +4,7 @@ import { IEntity } from '../entity'; export enum AssetType { COIN = 'Coin', - DCT = 'DCT', - DAT = 'DAT', + TOKEN = 'Token', } export enum AssetCategory { @@ -19,8 +18,8 @@ export enum AssetCategory { unique: true, }) export class Asset extends IEntity { - @Column({ type: 'int', nullable: true }) - chainId: number; + @Column({ nullable: true }) + chainId: string; @Column({ length: 256 }) name: string;