Skip to content

Commit

Permalink
1191 mayachain action (#1192)
Browse files Browse the repository at this point in the history
* Mayachain action

* Changeset version file
  • Loading branch information
0xp3gasus authored Jun 17, 2024
1 parent cd327ab commit e81d6be
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 117 deletions.
5 changes: 5 additions & 0 deletions .changeset/popular-cows-vanish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@xchainjs/xchain-mayachain-amm': patch
---

Bug fix with Arbitrum chain
5 changes: 5 additions & 0 deletions .changeset/unlucky-grapes-prove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@xchainjs/xchain-mayachain-amm': patch
---

Refactor of Mayachain action
2 changes: 2 additions & 0 deletions packages/xchain-mayachain-amm/__e2e__/mayachainAmm.e2e.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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)
})
Expand Down
1 change: 1 addition & 0 deletions packages/xchain-mayachain-amm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
136 changes: 136 additions & 0 deletions packages/xchain-mayachain-amm/src/mayachain-action.ts
Original file line number Diff line number Diff line change
@@ -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<TxSubmitted> {
return this.isNonProtocolParams(actionParams)
? this.makeNonProtocolAction(actionParams)
: this.makeProtocolAction(actionParams)
}

private static async makeProtocolAction({ wallet, assetAmount, memo }: ProtocolActionParams): Promise<TxSubmitted> {
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<TxSubmitted> {
// 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
}
}
127 changes: 15 additions & 112 deletions packages/xchain-mayachain-amm/src/mayachain-amm.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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 }),
}),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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}`,
})
}

/**
Expand Down Expand Up @@ -261,106 +266,4 @@ export class MayachainAMM {
private async isMAYAName(name: string): Promise<boolean> {
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<TxSubmitted> {
// 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<TxSubmitted> {
// 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)
}
}
38 changes: 34 additions & 4 deletions packages/xchain-mayachain-amm/src/utils.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
Loading

0 comments on commit e81d6be

Please sign in to comment.