Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

971 Mayachain AMM approve #978

Merged
merged 7 commits into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/chilled-grapes-hug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@xchainjs/xchain-mayachain-amm': patch
---

approveRouterToSpend and isRouterApprovedToSpend methods
5 changes: 5 additions & 0 deletions .changeset/purple-lizards-look.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@xchainjs/xchain-wallet': patch
---

Approve method
5 changes: 5 additions & 0 deletions .changeset/tasty-spies-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@xchainjs/xchain-mayachain-query': patch
---

Get asset decimals method
51 changes: 43 additions & 8 deletions packages/xchain-mayachain-amm/__e2e__/mayachainAmm.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { AssetBTC } from '@xchainjs/xchain-bitcoin'
import { AssetBTC, Client as BtcClient, defaultBTCParams as defaultBtcParams } from '@xchainjs/xchain-bitcoin'
import { Network } from '@xchainjs/xchain-client'
import { AssetETH } from '@xchainjs/xchain-ethereum'
import { AssetCacao } from '@xchainjs/xchain-mayachain'
import { Client as DashClient, defaultDashParams } from '@xchainjs/xchain-dash'
import { AssetETH, Client as EthClient, defaultEthParams } from '@xchainjs/xchain-ethereum'
import { Client as KujiraClient, defaultKujiParams } from '@xchainjs/xchain-kujira'
import { AssetCacao, Client as MayaClient } from '@xchainjs/xchain-mayachain'
import { MayachainQuery, QuoteSwap } from '@xchainjs/xchain-mayachain-query'
import { AssetRuneNative } from '@xchainjs/xchain-thorchain'
import { AssetRuneNative, Client as ThorClient } from '@xchainjs/xchain-thorchain'
import {
Asset,
CryptoAmount,
Expand All @@ -13,9 +15,10 @@ import {
assetToString,
baseAmount,
} from '@xchainjs/xchain-util'
import { Wallet } from '@xchainjs/xchain-wallet'
import { ethers } from 'ethers'

import { MayachainAMM, Wallet } from '../src'
import { MayachainAMM } from '../src'

function printQuoteSwap(quoteSwap: QuoteSwap) {
console.log({
Expand Down Expand Up @@ -74,10 +77,19 @@ describe('MayachainAmm e2e tests', () => {

beforeAll(() => {
const mayaChainQuery = new MayachainQuery()
wallet = new Wallet(process.env.MAINNET_PHRASE || '', Network.Mainnet, {
ETH: {
const phrase = process.env.MAINNET_PHRASE
wallet = new Wallet({
BTC: new BtcClient({ ...defaultBtcParams, phrase, network: Network.Mainnet }),
ETH: new EthClient({
...defaultEthParams,
providers: ethersJSProviders,
},
phrase,
network: Network.Mainnet,
}),
DASH: new DashClient({ ...defaultDashParams, phrase, network: Network.Mainnet }),
KUJI: new KujiraClient({ ...defaultKujiParams, phrase, network: Network.Mainnet }),
THOR: new ThorClient({ phrase, network: Network.Mainnet }),
MAYA: new MayaClient({ phrase, network: Network.Mainnet }),
})
mayachainAmm = new MayachainAMM(mayaChainQuery, wallet)
})
Expand Down Expand Up @@ -193,6 +205,29 @@ describe('MayachainAmm e2e tests', () => {
console.log(txSubmitted)
})

it('Should approve Mayachain router to spend', async () => {
const asset = assetFromStringEx('ETH.USDT-0XDAC17F958D2EE523A2206206994597C13D831EC7')

const txSubmitted = await mayachainAmm.approveRouterToSpend({
asset,
amount: new CryptoAmount(assetToBase(assetAmount(10, 6)), asset),
})

console.log(txSubmitted)
})

it('Should check if Mayachain router is allowed to spend', async () => {
const asset = assetFromStringEx('ETH.USDT-0XDAC17F958D2EE523A2206206994597C13D831EC7')

const isApprovedToSpend = await mayachainAmm.isRouterApprovedToSpend({
asset,
amount: new CryptoAmount(assetToBase(assetAmount(10, 6)), asset),
address: '0x6e08C7bBC09D68c6b9be0613ae32D4B5EAA63247',
})

console.log(!!isApprovedToSpend.length)
})

it('Should do ERC20 asset swap. USDT -> ETH', async () => {
const fromAsset = assetFromStringEx('ETH.USDT-0XDAC17F958D2EE523A2206206994597C13D831EC7')

Expand Down
3 changes: 2 additions & 1 deletion packages/xchain-mayachain-amm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"dependencies": {
"@xchainjs/xchain-ethereum": "0.31.5",
"@xchainjs/xchain-kujira": "0.1.7",
"@xchainjs/xchain-wallet": "0.1.0"
"@xchainjs/xchain-wallet": "0.1.0",
"@xchainjs/xchain-mayachain-query": "0.1.5"
},
"devDependencies": {
"@cosmos-client/core": "0.46.1",
Expand Down
60 changes: 47 additions & 13 deletions packages/xchain-mayachain-amm/src/mayachain-amm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Client as BtcClient, defaultBTCParams as defaultBtcParams } from '@xcha
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 { abi } from '@xchainjs/xchain-evm'
import { MAX_APPROVAL, abi } from '@xchainjs/xchain-evm'
import { Client as KujiraClient, defaultKujiParams } from '@xchainjs/xchain-kujira'
import { Client as MayaClient, MAYAChain } from '@xchainjs/xchain-mayachain'
import { MayachainQuery, QuoteSwap, QuoteSwapParams } from '@xchainjs/xchain-mayachain-query'
Expand All @@ -11,7 +11,7 @@ import { Asset, CryptoAmount, baseAmount, getContractAddressFromAsset } from '@x
import { Wallet } from '@xchainjs/xchain-wallet'
import { ethers } from 'ethers'

import { TxSubmitted } from './types'
import { ApproveParams, IsApprovedParams, TxSubmitted } from './types'

/**
* MAYAChainAMM class for interacting with THORChain.
Expand Down Expand Up @@ -134,17 +134,12 @@ export class MayachainAMM {
}

if (this.isERC20Asset(fromAsset) && fromAddress) {
const inboundDetails = await this.mayachainQuery.getChainInboundDetails(fromAsset.chain)
if (!inboundDetails.router) throw Error(`Unknown router address for ${fromAsset.chain}`)

const isApprovedResult = await this.wallet.isApproved(
fromAsset,
amount.baseAmount,
fromAddress,
inboundDetails.router,
)

if (!isApprovedResult) errors.push('Maya router has not been approved to spend this amount')
const approveErrors = await this.isRouterApprovedToSpend({
asset: fromAsset,
address: fromAddress,
amount,
})
errors.push(...approveErrors)
}

return errors
Expand Down Expand Up @@ -183,6 +178,45 @@ export class MayachainAMM {
: this.doNonProtocolAssetSwap(amount, quoteSwap.toAddress, quoteSwap.memo)
}

/**
* Approve Mayachain router in the chain of the asset to spend the amount in name of the address
* @param {ApproveParams} approveParams contains the asset and amount the router will be allowed. If amount is not defined,
* an infinity approve will be done
*/
public async approveRouterToSpend({ asset, amount }: ApproveParams): Promise<TxSubmitted> {
const inboundDetails = await this.mayachainQuery.getChainInboundDetails(asset.chain)
if (!inboundDetails.router) throw Error(`Unknown router address for ${asset.chain}`)
const tx = await this.wallet.approve(
asset,
amount?.baseAmount || baseAmount(MAX_APPROVAL.toString(), await this.mayachainQuery.getAssetDecimals(asset)),
inboundDetails.router,
)

return {
hash: tx.hash,
url: await this.wallet.getExplorerTxUrl(asset.chain, tx.hash),
}
}

/**
* Validate if the asset router is allowed to spend the asset amount in name of the address
* @param {IsApprovedParams} isApprovedParams contains the asset and the amount the router is supposed to spend
* int name of address
* @returns {string[]} the reasons the router of the asset is not allowed to spend the amount. If it is empty, the asset router is allowed to spend the amount
*/
public async isRouterApprovedToSpend({ asset, amount, address }: IsApprovedParams): Promise<string[]> {
const errors: string[] = []

const inboundDetails = await this.mayachainQuery.getChainInboundDetails(asset.chain)
if (!inboundDetails.router) throw Error(`Unknown router address for ${asset.chain}`)

const isApprovedResult = await this.wallet.isApproved(asset, amount.baseAmount, address, inboundDetails.router)

if (!isApprovedResult) errors.push('Maya router has not been approved to spend this amount')

return errors
}

/**
* Check if a name is a valid MAYAName
* @param {string} name MAYAName
Expand Down
13 changes: 13 additions & 0 deletions packages/xchain-mayachain-amm/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
import { Address, Asset, CryptoAmount } from '@xchainjs/xchain-util'

export type TxSubmitted = {
hash: string
url: string
}

export type ApproveParams = {
asset: Asset
amount: CryptoAmount | undefined
}

export type IsApprovedParams = {
asset: Asset
amount: CryptoAmount
address: Address
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { CryptoAmount, assetAmount, assetToBase, assetToString, baseAmount } from '@xchainjs/xchain-util'
import {
CryptoAmount,
assetAmount,
assetFromStringEx,
assetToBase,
assetToString,
baseAmount,
} from '@xchainjs/xchain-util'

import mockMayanodeApi from '../__mocks__/mayanode-api'
import { BtcAsset, EthAsset, RuneAsset } from '../src/'
Expand Down Expand Up @@ -85,4 +92,27 @@ describe('Mayachain-query tests', () => {
expect(quoteSwap.errors.length).toBe(0)
expect(quoteSwap.warning).toBe('')
})

it('Should return the number of decimals of Mayachain assets', async () => {
expect(await mayachainQuery.getAssetDecimals(assetFromStringEx('BTC.BTC'))).toBe(8)
expect(await mayachainQuery.getAssetDecimals(assetFromStringEx('BTC/BTC'))).toBe(8)
expect(await mayachainQuery.getAssetDecimals(assetFromStringEx('ETH.ETH'))).toBe(18)
expect(await mayachainQuery.getAssetDecimals(assetFromStringEx('DASH.DASH'))).toBe(8)
expect(await mayachainQuery.getAssetDecimals(assetFromStringEx('KUJI.KUJI'))).toBe(6)
expect(await mayachainQuery.getAssetDecimals(assetFromStringEx('THOR.RUNE'))).toBe(8)
expect(await mayachainQuery.getAssetDecimals(assetFromStringEx('MAYA.CACAO'))).toBe(8)
expect(
await mayachainQuery.getAssetDecimals(assetFromStringEx('ETH.USDT-0xdAC17F958D2ee523a2206206994597C13D831ec7')),
).toBe(6)
expect(
await mayachainQuery.getAssetDecimals(assetFromStringEx('ETH.USDC-0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48')),
).toBe(6)
expect(
await mayachainQuery.getAssetDecimals(assetFromStringEx('ETH.WSTETH-0X7F39C581F595B53C5CB19BD0B3F8DA6C935E2CA0')),
).toBe(18)
expect(await mayachainQuery.getAssetDecimals(assetFromStringEx('KUJI.USK'))).toBe(6)
expect(
mayachainQuery.getAssetDecimals(assetFromStringEx('ETH.BNB-0xB8c77482e45F1F44dE1745F52C74426C631bDD52')),
).rejects.toThrowError('Can not get decimals for ETH.BNB-0xB8c77482e45F1F44dE1745F52C74426C631bDD52')
})
})
30 changes: 30 additions & 0 deletions packages/xchain-mayachain-query/src/mayachain-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export class MayachainCache {
readonly mayanode: Mayanode
private conf: MayachainCacheConf
private readonly inboundDetailCache: CachedValue<Record<string, InboundDetail>>
private readonly assetDecimalsCache: CachedValue<Record<string, number>>
/**
* Constructor to create a MayachainCache
*
Expand All @@ -36,6 +37,7 @@ export class MayachainCache {
() => this.refreshInboundDetailCache(),
this.conf.expirationTimeInboundAddress,
)
this.assetDecimalsCache = new CachedValue<Record<string, number>>(() => this.refreshAssetDecimalsCache())
}

/**
Expand All @@ -47,6 +49,15 @@ export class MayachainCache {
return await this.inboundDetailCache.getValue()
}

/**
* Get the number of the decimals of the supported Mayachain tokens
* @returns {Record<string, number>} A record with the string asset notation as key and the number of decimals as value
*/
public async getAssetDecimals(): Promise<Record<string, number>> {
if (!this.assetDecimalsCache) throw Error(`Could not refresh assets decimals`)
return await this.assetDecimalsCache.getValue()
}

/**
* Refreshes the InboundDetailCache Cache
*/
Expand Down Expand Up @@ -98,4 +109,23 @@ export class MayachainCache {

return inboundDetails
}

/**
* Refreshes the number of decimals of the supported Mayachain tokens
*/
private async refreshAssetDecimalsCache(): Promise<Record<string, number>> {
// TODO: As soon as Mayachain returns native decimals from any endpoint of its API, refactor the function
return {
'BTC.BTC': 8,
'ETH.ETH': 18,
'DASH.DASH': 8,
'KUJI.KUJI': 6,
'THOR.RUNE': 8,
'MAYA.CACAO': 8,
'ETH.USDT-0xdAC17F958D2ee523a2206206994597C13D831ec7': 6,
'ETH.USDC-0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': 6,
'ETH.WSTETH-0X7F39C581F595B53C5CB19BD0B3F8DA6C935E2CA0': 18,
'KUJI.USK': 6,
}
}
}
19 changes: 18 additions & 1 deletion packages/xchain-mayachain-query/src/mayachain-query.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Network } from '@xchainjs/xchain-client'
import { MAYANameDetails } from '@xchainjs/xchain-mayamidgard-query'
import { QuoteSwapResponse } from '@xchainjs/xchain-mayanode'
import { CryptoAmount, assetFromStringEx, assetToString, baseAmount } from '@xchainjs/xchain-util'
import { Asset, CryptoAmount, assetFromStringEx, assetToString, baseAmount, isSynthAsset } from '@xchainjs/xchain-util'

import { MayachainCache } from './mayachain-cache'
import { InboundDetail, QuoteSwap, QuoteSwapParams } from './types'
import {
BtcAsset,
BtcChain,
CacaoAsset,
DEFAULT_MAYACHAIN_DECIMALS,
DashAsset,
DashChain,
EthAsset,
Expand Down Expand Up @@ -198,4 +199,20 @@ export class MayachainQuery {
if (!inboundDetails[chain]) throw Error(`No inbound details known for ${chain} chain`)
return inboundDetails[chain]
}

/**
* Get asset decimals
* @param {Asset} asset
* @returns the asset decimals
* @throws {Error} if the asset is not supported in Mayachain
*/
public async getAssetDecimals(asset: Asset): Promise<number> {
if (isSynthAsset(asset)) return DEFAULT_MAYACHAIN_DECIMALS

const assetNotation = assetToString(asset)
const assetsDecimals = await this.mayachainCache.getAssetDecimals()
if (!assetsDecimals[assetNotation]) throw Error(`Can not get decimals for ${assetNotation}`)

return assetsDecimals[assetNotation]
}
}
23 changes: 23 additions & 0 deletions packages/xchain-wallet/src/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,29 @@ export class Wallet {
return (client as unknown as MayachainClient).deposit({ asset, amount, memo, walletIndex, sequence, gasLimit })
}

/**
* Check if an spenderAddress is allowed to spend in name of another address certain asset amount
* @param {Asset} asset to check
* @param {BaseAmount} amount to check
* @param {string} fromAddress owner of the amount asset
* @param {string} spenderAddress spender to check if it is allowed to spend
* @returns true if the spenderAddress is allowed to spend the amount, otherwise, false
* @throws {Error} if asset is a non ERC20 asset
*/
async approve(
asset: Asset,
amount: BaseAmount,
spenderAddress: string,
): Promise<ethers.providers.TransactionResponse> {
const client = this.getClient(asset.chain)
if (!('approve' in client)) throw Error('Can not make approve over non EVM client')
if (asset.chain === asset.ticker) throw Error('Can not make approve over native asset')

const contractAddress = getContractAddressFromAsset(asset)

return await (client as EvmClient).approve({ contractAddress, amount, spenderAddress })
}

/**
* Check if an spenderAddress is allowed to spend in name of another address certain asset amount
* @param {Asset} asset to check
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3194,6 +3194,11 @@
"@typescript-eslint/types" "5.59.1"
eslint-visitor-keys "^3.3.0"

"@xchainjs/[email protected]":
version "0.1.5"
resolved "https://registry.yarnpkg.com/@xchainjs/xchain-mayachain-query/-/xchain-mayachain-query-0.1.5.tgz#279124686bae97026438941f465d245fdef43c2d"
integrity sha512-/OOYfrDQIhcQeSrRvTR192aAY5RkdNaE6pWs/dSiAJ8P51vmlApS65UwNeDA7AqQhIiddq2mriLVpRz7zM/VbQ==

"@yarnpkg/lockfile@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31"
Expand Down
Loading