From e81d6be809394e324e83b8e5b18436a0a349a195 Mon Sep 17 00:00:00 2001 From: Pegasus <83475418+0xp3gasus@users.noreply.github.com> Date: Mon, 17 Jun 2024 15:01:52 +0200 Subject: [PATCH] 1191 mayachain action (#1192) * Mayachain action * Changeset version file --- .changeset/popular-cows-vanish.md | 5 + .changeset/unlucky-grapes-prove.md | 5 + .../__e2e__/mayachainAmm.e2e.ts | 2 + packages/xchain-mayachain-amm/package.json | 1 + .../src/mayachain-action.ts | 136 ++++++++++++++++++ .../xchain-mayachain-amm/src/mayachain-amm.ts | 127 ++-------------- packages/xchain-mayachain-amm/src/utils.ts | 38 ++++- yarn.lock | 3 +- 8 files changed, 200 insertions(+), 117 deletions(-) create mode 100644 .changeset/popular-cows-vanish.md create mode 100644 .changeset/unlucky-grapes-prove.md create mode 100644 packages/xchain-mayachain-amm/src/mayachain-action.ts diff --git a/.changeset/popular-cows-vanish.md b/.changeset/popular-cows-vanish.md new file mode 100644 index 000000000..d04e62114 --- /dev/null +++ b/.changeset/popular-cows-vanish.md @@ -0,0 +1,5 @@ +--- +'@xchainjs/xchain-mayachain-amm': patch +--- + +Bug fix with Arbitrum chain diff --git a/.changeset/unlucky-grapes-prove.md b/.changeset/unlucky-grapes-prove.md new file mode 100644 index 000000000..db79e350e --- /dev/null +++ b/.changeset/unlucky-grapes-prove.md @@ -0,0 +1,5 @@ +--- +'@xchainjs/xchain-mayachain-amm': patch +--- + +Refactor of Mayachain action diff --git a/packages/xchain-mayachain-amm/__e2e__/mayachainAmm.e2e.ts b/packages/xchain-mayachain-amm/__e2e__/mayachainAmm.e2e.ts index 4ec6eb4e0..1b9b01d1c 100644 --- a/packages/xchain-mayachain-amm/__e2e__/mayachainAmm.e2e.ts +++ b/packages/xchain-mayachain-amm/__e2e__/mayachainAmm.e2e.ts @@ -1,4 +1,5 @@ import TransportNodeHid from '@ledgerhq/hw-transport-node-hid' +import { Client as ArbClient, defaultArbParams } from '@xchainjs/xchain-arbitrum' import { AssetBTC, Client as BtcClient, defaultBTCParams as defaultBtcParams } from '@xchainjs/xchain-bitcoin' import { Network } from '@xchainjs/xchain-client' import { AssetDASH, ClientKeystore, ClientLedger, DASHChain, defaultDashParams } from '@xchainjs/xchain-dash' @@ -91,6 +92,7 @@ describe('MayachainAmm e2e tests', () => { KUJI: new KujiraClient({ ...defaultKujiParams, phrase, network: Network.Mainnet }), THOR: new ThorClient({ phrase, network: Network.Mainnet }), MAYA: new MayaClient({ phrase, network: Network.Mainnet }), + ARB: new ArbClient({ ...defaultArbParams, phrase, network: Network.Mainnet }), }) mayachainAmm = new MayachainAMM(mayaChainQuery, wallet) }) diff --git a/packages/xchain-mayachain-amm/package.json b/packages/xchain-mayachain-amm/package.json index 1b05a59ec..fbe5a8725 100644 --- a/packages/xchain-mayachain-amm/package.json +++ b/packages/xchain-mayachain-amm/package.json @@ -36,6 +36,7 @@ "url": "https://github.com/xchainjs/xchainjs-lib/issues" }, "dependencies": { + "@xchainjs/xchain-arbitrum": "workspace:*", "@xchainjs/xchain-bitcoin": "workspace:*", "@xchainjs/xchain-client": "workspace:*", "@xchainjs/xchain-dash": "workspace:*", diff --git a/packages/xchain-mayachain-amm/src/mayachain-action.ts b/packages/xchain-mayachain-amm/src/mayachain-action.ts new file mode 100644 index 000000000..a56a82a30 --- /dev/null +++ b/packages/xchain-mayachain-amm/src/mayachain-action.ts @@ -0,0 +1,136 @@ +import { abi } from '@xchainjs/xchain-evm' +import { MAYAChain } from '@xchainjs/xchain-mayachain' +import { MayachainQuery } from '@xchainjs/xchain-mayachain-query' +import { Address, CryptoAmount, baseAmount, getContractAddressFromAsset } from '@xchainjs/xchain-util' +import { Wallet } from '@xchainjs/xchain-wallet' +import { ethers } from 'ethers' + +import { TxSubmitted } from './types' +import { isProtocolBFTChain, isProtocolERC20Asset, isProtocolEVMChain } from './utils' + +export type NonProtocolActionParams = { + wallet: Wallet + assetAmount: CryptoAmount + recipient: Address + memo: string +} + +export type ProtocolActionParams = { + wallet: Wallet + assetAmount: CryptoAmount + memo: string +} + +export type ActionParams = ProtocolActionParams | NonProtocolActionParams + +export class MayachainAction { + public static async makeAction(actionParams: ActionParams): Promise { + return this.isNonProtocolParams(actionParams) + ? this.makeNonProtocolAction(actionParams) + : this.makeProtocolAction(actionParams) + } + + private static async makeProtocolAction({ wallet, assetAmount, memo }: ProtocolActionParams): Promise { + const hash = await wallet.deposit({ + chain: MAYAChain, + asset: assetAmount.asset, + amount: assetAmount.baseAmount, + memo, + }) + + return { + hash, + url: await wallet.getExplorerTxUrl(MAYAChain, hash), + } + } + + private static async makeNonProtocolAction({ + wallet, + assetAmount, + recipient, + memo, + }: NonProtocolActionParams): Promise { + // Non EVM actions + + if (!isProtocolEVMChain(assetAmount.asset.chain)) { + if (isProtocolBFTChain(assetAmount.asset.chain)) { + const hash = await wallet.transfer({ + asset: assetAmount.asset, + amount: assetAmount.baseAmount, + recipient, + memo, + }) + return { + hash, + url: await wallet.getExplorerTxUrl(assetAmount.asset.chain, hash), + } + } + const feeRates = await wallet.getFeeRates(assetAmount.asset.chain) + const hash = await wallet.transfer({ + asset: assetAmount.asset, + amount: assetAmount.baseAmount, + recipient, + memo, + feeRate: feeRates.fast, + }) + return { + hash, + url: await wallet.getExplorerTxUrl(assetAmount.asset.chain, hash), + } + } + + // EVM actions + const mayachainQuery: MayachainQuery = new MayachainQuery() + + const inboundDetails = await mayachainQuery.getChainInboundDetails(assetAmount.asset.chain) + if (!inboundDetails.router) throw Error(`Unknown router for ${assetAmount.asset.chain} chain`) + + const isERC20 = isProtocolERC20Asset(assetAmount.asset) + + const checkSummedContractAddress = isERC20 + ? ethers.utils.getAddress(getContractAddressFromAsset(assetAmount.asset)) + : ethers.constants.AddressZero + + const expiration = Math.floor(new Date(new Date().getTime() + 15 * 60000).getTime() / 1000) + const depositParams = [ + recipient, + checkSummedContractAddress, + assetAmount.baseAmount.amount().toFixed(), + memo, + expiration, + ] + + const routerContract = new ethers.Contract(inboundDetails.router, abi.router) + const gasPrices = await wallet.getFeeRates(assetAmount.asset.chain) + + const unsignedTx = await routerContract.populateTransaction.depositWithExpiry(...depositParams) + + const nativeAsset = wallet.getAssetInfo(assetAmount.asset.chain) + + const hash = await wallet.transfer({ + asset: nativeAsset.asset, + amount: isERC20 ? baseAmount(0, nativeAsset.decimal) : assetAmount.baseAmount, + memo: unsignedTx.data, + recipient: inboundDetails.router, + gasPrice: gasPrices.fast, + isMemoEncoded: true, + gasLimit: ethers.BigNumber.from(160000), + }) + + return { + hash, + url: await wallet.getExplorerTxUrl(assetAmount.asset.chain, hash), + } + } + + private static isNonProtocolParams(params: ActionParams): params is NonProtocolActionParams { + if ( + (params.assetAmount.asset.chain === MAYAChain || params.assetAmount.asset.synth) && + 'address' in params && + !!params.address + ) { + throw Error('Inconsistent params. Native actions do not support recipient') + } + return params.assetAmount.asset.chain !== MAYAChain && !params.assetAmount.asset.synth + } +} diff --git a/packages/xchain-mayachain-amm/src/mayachain-amm.ts b/packages/xchain-mayachain-amm/src/mayachain-amm.ts index b00bc631d..6226472aa 100644 --- a/packages/xchain-mayachain-amm/src/mayachain-amm.ts +++ b/packages/xchain-mayachain-amm/src/mayachain-amm.ts @@ -1,21 +1,22 @@ /** * Import necessary modules and libraries */ +import { Client as ArbClient, defaultArbParams } from '@xchainjs/xchain-arbitrum' import { Client as BtcClient, defaultBTCParams as defaultBtcParams } from '@xchainjs/xchain-bitcoin' import { Network } from '@xchainjs/xchain-client' import { Client as DashClient, defaultDashParams } from '@xchainjs/xchain-dash' -import { AssetETH, Client as EthClient, defaultEthParams } from '@xchainjs/xchain-ethereum' -import { MAX_APPROVAL, abi } from '@xchainjs/xchain-evm' +import { Client as EthClient, defaultEthParams } from '@xchainjs/xchain-ethereum' +import { MAX_APPROVAL } from '@xchainjs/xchain-evm' import { Client as KujiraClient, defaultKujiParams } from '@xchainjs/xchain-kujira' import { Client as MayaClient, MAYAChain } from '@xchainjs/xchain-mayachain' import { MAYANameDetails, MayachainQuery, QuoteSwap, QuoteSwapParams } from '@xchainjs/xchain-mayachain-query' import { Client as ThorClient } from '@xchainjs/xchain-thorchain' -import { Address, Asset, CryptoAmount, baseAmount, eqAsset, getContractAddressFromAsset } from '@xchainjs/xchain-util' +import { Address, CryptoAmount, baseAmount } from '@xchainjs/xchain-util' import { Wallet } from '@xchainjs/xchain-wallet' -import { ethers } from 'ethers' +import { MayachainAction } from './mayachain-action' import { ApproveParams, IsApprovedParams, TxSubmitted } from './types' -import { validateAddress } from './utils' +import { isProtocolERC20Asset, validateAddress } from './utils' /** * Mayachain Automated Market Maker (AMM) class. @@ -41,6 +42,7 @@ export class MayachainAMM { ETH: new EthClient({ ...defaultEthParams, network: Network.Mainnet }), DASH: new DashClient({ ...defaultDashParams, network: Network.Mainnet }), KUJI: new KujiraClient({ ...defaultKujiParams, network: Network.Mainnet }), + ARB: new ArbClient({ ...defaultArbParams, network: Network.Mainnet }), THOR: new ThorClient({ network: Network.Mainnet }), MAYA: new MayaClient({ network: Network.Mainnet }), }), @@ -150,7 +152,7 @@ export class MayachainAMM { errors.push(`affiliateBps ${affiliateBps} out of range [0 - 10000]`) } // Validate approval if asset is an ERC20 token and fromAddress is provided - if (this.isERC20Asset(fromAsset) && fromAddress) { + if (isProtocolERC20Asset(fromAsset) && fromAddress) { const approveErrors = await this.isRouterApprovedToSpend({ asset: fromAsset, address: fromAddress, @@ -189,10 +191,13 @@ export class MayachainAMM { }) // Check if the swap can be performed if (!quoteSwap.canSwap) throw Error(`Can not swap. ${quoteSwap.errors.join(' ')}`) - // Perform the swap based on the asset chain - return fromAsset.chain === MAYAChain || fromAsset.synth - ? this.doProtocolAssetSwap(amount, quoteSwap.memo) - : this.doNonProtocolAssetSwap(amount, quoteSwap.toAddress, quoteSwap.memo) + + return MayachainAction.makeAction({ + wallet: this.wallet, + assetAmount: amount, + memo: quoteSwap.memo, + recipient: `${quoteSwap.toAddress}`, + }) } /** @@ -261,106 +266,4 @@ export class MayachainAMM { private async isMAYAName(name: string): Promise { return !!(await this.mayachainQuery.getMAYANameDetails(name)) } - - /** - * Perform a swap from a native protocol asset to any other asset - * @param {CryptoAmount} amount Amount to swap - * @param {string} memo Memo to add to the transaction - * @returns {TxSubmitted} Transaction hash and URL of the swap - */ - private async doProtocolAssetSwap(amount: CryptoAmount, memo: string): Promise { - // Deposit the amount and return transaction hash and URL - const hash = await this.wallet.deposit({ chain: MAYAChain, asset: amount.asset, amount: amount.baseAmount, memo }) - - return { - hash, - url: await this.wallet.getExplorerTxUrl(MAYAChain, hash), - } - } - - /** - * Perform a swap between assets - * @param {CryptoAmount} amount Amount to swap - * @param {string} memo Memo to add to the transaction to successfully make the swap - * @param {string} recipient inbound address to make swap transaction to - * @returns {TxSubmitted} Transaction hash and URL of the swap - */ - private async doNonProtocolAssetSwap(amount: CryptoAmount, recipient: string, memo: string): Promise { - // For non-EVM assets, perform a transfer and return transaction hash and URL - if (!this.isEVMChain(amount.asset.chain)) { - const hash = await this.wallet.transfer({ - asset: amount.asset, - amount: amount.baseAmount, - recipient, - memo, - }) - return { - hash, - url: await this.wallet.getExplorerTxUrl(amount.asset.chain, hash), - } - } - - // For EVM assets, perform a deposit with expiry and return transaction hash and URL - const inboundDetails = await this.mayachainQuery.getChainInboundDetails(amount.asset.chain) - if (!inboundDetails.router) throw Error(`Unknown router for ${amount.asset.chain} chain`) - const isERC20 = this.isERC20Asset(amount.asset) - - const checkSummedContractAddress = isERC20 - ? ethers.utils.getAddress(getContractAddressFromAsset(amount.asset)) - : ethers.constants.AddressZero - - const expiration = Math.floor(new Date(new Date().getTime() + 15 * 60000).getTime() / 1000) - const depositParams = [ - recipient, - checkSummedContractAddress, - amount.baseAmount.amount().toFixed(), - memo, - expiration, - ] - - const routerContract = new ethers.Contract(inboundDetails.router, abi.router) - - const unsignedTx = await routerContract.populateTransaction.depositWithExpiry(...depositParams) - - const gasPrices = await this.wallet.getGasFeeRates(amount.asset.chain) - - const nativeAsset = this.wallet.getAssetInfo(amount.asset.chain) - - const hash = await this.wallet.transfer({ - asset: nativeAsset.asset, - amount: isERC20 ? baseAmount(0, nativeAsset.decimal) : amount.baseAmount, - memo: unsignedTx.data, - recipient: inboundDetails.router, - gasPrice: gasPrices.fast, - isMemoEncoded: true, - gasLimit: ethers.BigNumber.from(160000), - }) - - return { - hash, - url: await this.wallet.getExplorerTxUrl(amount.asset.chain, hash), - } - } - - /** - * Check if the asset is an ERC20 token - * @param {Asset} asset Asset to check - * @returns True if the asset is an ERC20 token, otherwise false - */ - private isERC20Asset(asset: Asset): boolean { - // Check if the asset's chain is an EVM chain and if the symbol matches AssetETH.symbol - return this.isEVMChain(asset.chain) - ? [AssetETH].findIndex((nativeEVMAsset) => eqAsset(nativeEVMAsset, asset)) === -1 && !asset.synth - : false - } - - /** - * Check if the chain is an EVM (Ethereum Virtual Machine) chain - * @param {Chain} chain Chain to check - * @returns True if the chain is an EVM chain, otherwise false - */ - private isEVMChain(chain: string): boolean { - // Check if the chain matches AssetETH.chain - return [AssetETH.chain].includes(chain) - } } diff --git a/packages/xchain-mayachain-amm/src/utils.ts b/packages/xchain-mayachain-amm/src/utils.ts index 672daec22..1698ec82c 100644 --- a/packages/xchain-mayachain-amm/src/utils.ts +++ b/packages/xchain-mayachain-amm/src/utils.ts @@ -1,11 +1,41 @@ +import { AssetAETH } from '@xchainjs/xchain-arbitrum' import { BTCChain, Client as BtcClient, defaultBTCParams as defaultBtcParams } from '@xchainjs/xchain-bitcoin' import { Network } from '@xchainjs/xchain-client' import { Client as DashClient, DASHChain, defaultDashParams } from '@xchainjs/xchain-dash' -import { Client as EthClient, ETHChain, defaultEthParams } from '@xchainjs/xchain-ethereum' -import { Client as KujiraClient, KUJIChain, defaultKujiParams } from '@xchainjs/xchain-kujira' +import { AssetETH, Client as EthClient, ETHChain, defaultEthParams } from '@xchainjs/xchain-ethereum' +import { AssetKUJI, Client as KujiraClient, KUJIChain, defaultKujiParams } from '@xchainjs/xchain-kujira' import { Client as MayaClient, MAYAChain } from '@xchainjs/xchain-mayachain' -import { Client as ThorClient, THORChain } from '@xchainjs/xchain-thorchain' -import { Address, Chain } from '@xchainjs/xchain-util' +import { AssetRuneNative, Client as ThorClient, THORChain } from '@xchainjs/xchain-thorchain' +import { Address, Asset, Chain, eqAsset } from '@xchainjs/xchain-util' + +/** + * Check if a chain is EVM and supported by the protocol + * @param {Chain} chain to check + * @returns true if chain is EVM, otherwise, false + */ +export const isProtocolEVMChain = (chain: Chain): boolean => { + return [AssetETH.chain, AssetAETH.chain].includes(chain) +} + +/** + * Check if asset is ERC20 + * @param {Asset} asset to check + * @returns true if asset is ERC20, otherwise, false + */ +export const isProtocolERC20Asset = (asset: Asset): boolean => { + return isProtocolEVMChain(asset.chain) + ? [AssetETH, AssetAETH].findIndex((nativeEVMAsset) => eqAsset(nativeEVMAsset, asset)) === -1 && !asset.synth + : false +} + +/** + * Check if a chain is EVM and supported by the protocol + * @param {Chain} chain to check + * @returns true if chain is EVM, otherwise, false + */ +export const isProtocolBFTChain = (chain: Chain): boolean => { + return [AssetKUJI.chain, AssetRuneNative.chain].includes(chain) +} export const validateAddress = (network: Network, chain: Chain, address: Address): boolean => { switch (chain) { diff --git a/yarn.lock b/yarn.lock index e4276e519..605158610 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3656,7 +3656,7 @@ __metadata: languageName: unknown linkType: soft -"@xchainjs/xchain-arbitrum@workspace:packages/xchain-arbitrum": +"@xchainjs/xchain-arbitrum@workspace:*, @xchainjs/xchain-arbitrum@workspace:packages/xchain-arbitrum": version: 0.0.0-use.local resolution: "@xchainjs/xchain-arbitrum@workspace:packages/xchain-arbitrum" dependencies: @@ -3940,6 +3940,7 @@ __metadata: resolution: "@xchainjs/xchain-mayachain-amm@workspace:packages/xchain-mayachain-amm" dependencies: "@ledgerhq/hw-transport-node-hid": "npm:6.28.6" + "@xchainjs/xchain-arbitrum": "workspace:*" "@xchainjs/xchain-bitcoin": "workspace:*" "@xchainjs/xchain-client": "workspace:*" "@xchainjs/xchain-dash": "workspace:*"