diff --git a/examples/evm-to-evm-fungible-transfer/src/transfer.ts b/examples/evm-to-evm-fungible-transfer/src/transfer.ts index 856f1c259..b62162e97 100644 --- a/examples/evm-to-evm-fungible-transfer/src/transfer.ts +++ b/examples/evm-to-evm-fungible-transfer/src/transfer.ts @@ -1,5 +1,4 @@ -import type { Eip1193Provider } from "@buildwithsygma/core"; -import { Environment } from "@buildwithsygma/core"; +import { Eip1193Provider, Environment } from "@buildwithsygma/core"; import { createEvmFungibleAssetTransfer } from "@buildwithsygma/evm"; import dotenv from "dotenv"; import { Wallet, providers } from "ethers"; @@ -14,19 +13,12 @@ if (!privateKey) { } const SEPOLIA_CHAIN_ID = 11155111; -const AMOY_CHAIN_ID = 80002; -const RESOURCE_ID = - "0x0000000000000000000000000000000000000000000000000000000000000300"; -const SEPOLIA_RPC_URL = - process.env.SEPOLIA_RPC_URL || "https://eth-sepolia-public.unifra.io"; +const HOLESKY_CHAIN_ID = 17000; +const RESOURCE_ID = "0x0000000000000000000000000000000000000000000000000000000000000200"; +const SEPOLIA_RPC_URL = process.env.SEPOLIA_RPC_URL || "https://eth-sepolia-public.unifra.io" -const explorerUrls: Record = { - [SEPOLIA_CHAIN_ID]: "https://sepolia.etherscan.io", -}; -const getTxExplorerUrl = (params: { - txHash: string; - chainId: number; -}): string => `${explorerUrls[params.chainId]}/tx/${params.txHash}`; +const explorerUrls: Record = { [SEPOLIA_CHAIN_ID]: 'https://sepolia.etherscan.io' }; +const getTxExplorerUrl = (params: { txHash: string; chainId: number }): string => `${explorerUrls[params.chainId]}/tx/${params.txHash}`; export async function erc20Transfer(): Promise { const web3Provider = new Web3HttpProvider(SEPOLIA_RPC_URL); @@ -36,32 +28,29 @@ export async function erc20Transfer(): Promise { const params = { source: SEPOLIA_CHAIN_ID, - destination: AMOY_CHAIN_ID, + destination: HOLESKY_CHAIN_ID, sourceNetworkProvider: web3Provider as unknown as Eip1193Provider, resource: RESOURCE_ID, - amount: BigInt(1) * BigInt(1e18), + amount: BigInt(2) * BigInt(1e18), destinationAddress: destinationAddress, - environment: (process.env.SYGMA_ENV as Environment) || Environment.TESTNET, + environment: Environment.DEVNET, sourceAddress: destinationAddress, }; const transfer = await createEvmFungibleAssetTransfer(params); + const approvals = await transfer.getApprovalTransactions(); console.log(`Approving Tokens (${approvals.length})...`); for (const approval of approvals) { const response = await wallet.sendTransaction(approval); await response.wait(); - console.log( - `Approved, transaction: ${getTxExplorerUrl({ txHash: response.hash, chainId: SEPOLIA_CHAIN_ID })}`, - ); + console.log(`Approved, transaction: ${getTxExplorerUrl({ txHash: response.hash, chainId: SEPOLIA_CHAIN_ID })}`); } const transferTx = await transfer.getTransferTransaction(); const response = await wallet.sendTransaction(transferTx); await response.wait(); - console.log( - `Deposited, transaction: ${getTxExplorerUrl({ txHash: response.hash, chainId: SEPOLIA_CHAIN_ID })}`, - ); + console.log(`Depositted, transaction: ${getTxExplorerUrl({ txHash: response.hash, chainId: SEPOLIA_CHAIN_ID })}`); } erc20Transfer().finally(() => {}); diff --git a/examples/substrate-to-evm-fungible-transfer/package.json b/examples/substrate-to-evm-fungible-transfer/package.json index a991dc334..14b228517 100644 --- a/examples/substrate-to-evm-fungible-transfer/package.json +++ b/examples/substrate-to-evm-fungible-transfer/package.json @@ -30,7 +30,7 @@ "dependencies": { "@buildwithsygma/core": "workspace:*", "@buildwithsygma/substrate": "workspace:*", - "@polkadot/api": "^12.3.1", + "@polkadot/api": "^12.2.1", "@polkadot/keyring": "^12.6.2", "@polkadot/util-crypto": "^12.6.2", "tsx": "^4.15.4" diff --git a/examples/substrate-to-evm-fungible-transfer/src/transfer.ts b/examples/substrate-to-evm-fungible-transfer/src/transfer.ts index 131afe61a..479d4e52b 100644 --- a/examples/substrate-to-evm-fungible-transfer/src/transfer.ts +++ b/examples/substrate-to-evm-fungible-transfer/src/transfer.ts @@ -37,13 +37,12 @@ const substrateTransfer = async (): Promise => { const api = await ApiPromise.create({ provider: wsProvider }); const transferParams: SubstrateAssetTransferRequest = { - source: RHALA_CHAIN_ID, - destination: SEPOLIA_CHAIN_ID, + sourceDomain: RHALA_CHAIN_ID, + destinationDomain: SEPOLIA_CHAIN_ID, sourceNetworkProvider: api, resource: RESOURCE_ID_SYGMA_USD, - amount: BigInt("1"), + amount: BigInt("5000000"), destinationAddress: recipient, - sourceAddress: account.address, }; const transfer = await createSubstrateFungibleAssetTransfer(transferParams); diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 11a1eb327..30ebfd307 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -25,7 +25,7 @@ "resolveJsonModule": true }, "exclude": ["node_modules/**"], - "include": ["./src/**/*.ts", "./test/**/*.ts", "substrate-asset-transfer.ts", "./src/environment.d.ts"], + "include": ["./src/**/*.ts", "./test/**/*.ts", "substrate-asset-transfer.ts"], "ts-node": { "esm": true, "experimentalSpecifierResolution": "node" diff --git a/packages/evm/src/__test__/fungible.test.ts b/packages/evm/src/__test__/fungible.test.ts index b4f9ee56c..b39988bdc 100644 --- a/packages/evm/src/__test__/fungible.test.ts +++ b/packages/evm/src/__test__/fungible.test.ts @@ -45,21 +45,14 @@ jest.mock('@buildwithsygma/core', () => ({ // eslint-disable-next-line @typescript-eslint/no-unsafe-return jest.mock('@ethersproject/providers', () => ({ ...jest.requireActual('@ethersproject/providers'), - Web3Provider: jest.fn().mockImplementation(() => { - return { - getBalance: jest.fn().mockResolvedValue(BigNumber.from('110000000000000000')), - }; - }), + Web3Provider: jest.fn(), })); // eslint-disable-next-line @typescript-eslint/no-unsafe-return jest.mock('@buildwithsygma/sygma-contracts', () => ({ ...jest.requireActual('@buildwithsygma/sygma-contracts'), Bridge__factory: { connect: jest.fn() }, - ERC20__factory: { - balanceOf: jest.fn().mockResolvedValue(BigInt(1)), - connect: jest.fn(), - }, + ERC20__factory: { connect: jest.fn() }, BasicFeeHandler__factory: { connect: jest.fn() }, PercentageERC20FeeHandler__factory: { connect: jest.fn() }, FeeHandlerRouter__factory: { connect: jest.fn() }, @@ -189,7 +182,6 @@ describe('Fungible - Approvals', () => { }); (ERC20__factory.connect as jest.Mock).mockReturnValue({ - balanceOf: jest.fn().mockResolvedValue(BigNumber.from(parseEther('50'))), populateTransaction: { approve: jest.fn().mockResolvedValue({}), }, @@ -214,55 +206,6 @@ describe('Fungible - Approvals', () => { expect(approvals.length).toBeGreaterThan(0); }); - - it('should throw an error if balance is not sufficient - Basic', async () => { - (BasicFeeHandler__factory.connect as jest.Mock).mockReturnValue({ - feeHandlerType: jest.fn().mockResolvedValue('basic'), - calculateFee: jest.fn().mockResolvedValue([parseEther('1')]), - }); - - const transfer = await createEvmFungibleAssetTransfer({ - ...TRANSFER_PARAMS, - amount: parseEther('0').toBigInt(), - }); - - await expect(transfer.getApprovalTransactions()).rejects.toThrow( - 'Insufficient native token balance for network', - ); - }); - - it('should throw an error if balance is not sufficient - Percentage', async () => { - (BasicFeeHandler__factory.connect as jest.Mock).mockReturnValue({ - feeHandlerType: jest.fn().mockResolvedValue('percentage'), - calculateFee: jest.fn().mockResolvedValue([BigNumber.from(0)]), - }); - (PercentageERC20FeeHandler__factory.connect as jest.Mock).mockReturnValue({ - feeHandlerType: jest.fn().mockResolvedValue('percentage'), - calculateFee: jest.fn().mockResolvedValue([BigNumber.from(0)]), - _resourceIDToFeeBounds: jest.fn().mockResolvedValue({ - lowerBound: parseEther('10'), - upperBound: parseEther('100'), - }), - _domainResourceIDToFee: jest.fn().mockResolvedValue(BigNumber.from(100)), - HUNDRED_PERCENT: jest.fn().mockResolvedValue(10000), - }); - (ERC20__factory.connect as jest.Mock).mockReturnValue({ - balanceOf: jest.fn().mockResolvedValue(BigNumber.from(parseEther('1').toBigInt())), // Mock balance less than the required amount - populateTransaction: { - approve: jest.fn().mockResolvedValue({}), - }, - allowance: jest.fn().mockResolvedValue(parseEther('0')), - }); - - const transfer = await createEvmFungibleAssetTransfer({ - ...TRANSFER_PARAMS, - amount: parseEther('100').toBigInt(), - }); - - await expect(transfer.getApprovalTransactions()).rejects.toThrow( - 'Insufficient ERC20 token balance', - ); - }); }); describe('Fungible - Deposit', () => { @@ -280,14 +223,6 @@ describe('Fungible - Deposit', () => { .mockResolvedValue('0x98729c03c4D5e820F5e8c45558ae07aE63F97461'), }); - (ERC20__factory.connect as jest.Mock).mockReturnValue({ - balanceOf: jest.fn().mockResolvedValue(BigNumber.from(parseEther('50').toBigInt())), - populateTransaction: { - approve: jest.fn().mockResolvedValue({}), - }, - allowance: jest.fn().mockResolvedValue(parseEther('0')), - }); - (Bridge__factory.connect as jest.Mock).mockReturnValue({ populateTransaction: { deposit: jest.fn().mockReturnValue({ @@ -314,34 +249,4 @@ describe('Fungible - Deposit', () => { expect(depositTransaction).toBeTruthy(); }); - - it('should throw ERROR - Insufficient account balance - Percentage', async () => { - (BasicFeeHandler__factory.connect as jest.Mock).mockReturnValue({ - feeHandlerType: jest.fn().mockResolvedValue('percentage'), - calculateFee: jest.fn().mockResolvedValue([BigNumber.from(0)]), - }); - (PercentageERC20FeeHandler__factory.connect as jest.Mock).mockReturnValue({ - feeHandlerType: jest.fn().mockResolvedValue('percentage'), - calculateFee: jest.fn().mockResolvedValue([BigNumber.from(0)]), - _resourceIDToFeeBounds: jest.fn().mockResolvedValue({ - lowerBound: parseEther('10'), - upperBound: parseEther('100'), - }), - _domainResourceIDToFee: jest.fn().mockResolvedValue(BigNumber.from(100)), - HUNDRED_PERCENT: jest.fn().mockResolvedValue(10000), - }); - (ERC20__factory.connect as jest.Mock).mockReturnValue({ - balanceOf: jest.fn().mockResolvedValue(BigNumber.from(parseEther('1').toBigInt())), // Mock balance less than the required amount - populateTransaction: { - approve: jest.fn().mockResolvedValue({}), - }, - allowance: jest.fn().mockResolvedValue(parseEther('0')), - }); - - const transfer = await createEvmFungibleAssetTransfer(TRANSFER_PARAMS); - - await expect(transfer.getTransferTransaction()).rejects.toThrow( - 'Insufficient ERC20 token balance', - ); - }); }); diff --git a/packages/evm/src/baseTransfer.ts b/packages/evm/src/baseTransfer.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/evm/src/evmTransfer.ts b/packages/evm/src/evmTransfer.ts index 8b20a63da..e8edc5e69 100644 --- a/packages/evm/src/evmTransfer.ts +++ b/packages/evm/src/evmTransfer.ts @@ -24,6 +24,7 @@ export class EvmTransfer extends BaseTransfer { /** * Returns fee based on transfer amount. + * @param amount By default it is original amount passed in constructor */ async getFee(): Promise { const provider = new providers.Web3Provider(this.sourceNetworkProvider); diff --git a/packages/evm/src/fee/index.ts b/packages/evm/src/fee/index.ts index 15853d051..4e436c6b9 100644 --- a/packages/evm/src/fee/index.ts +++ b/packages/evm/src/fee/index.ts @@ -1,5 +1,4 @@ export * from './BasicFee.js'; export * from './getFeeInformation.js'; export * from './PercentageFee.js'; -export * from './TwapFee.js'; export * from './types.js'; diff --git a/packages/evm/src/fee/types.ts b/packages/evm/src/fee/types.ts index 267f5a5ca..e461571ec 100644 --- a/packages/evm/src/fee/types.ts +++ b/packages/evm/src/fee/types.ts @@ -1,7 +1,6 @@ import type { FeeHandlerType } from '@buildwithsygma/core'; import type { ethers } from 'ethers'; - -import type { EvmFee } from '../types.js'; +import type { EvmFee } from 'types'; /** * Parameters that are required to diff --git a/packages/evm/src/fungible.ts b/packages/evm/src/fungible.ts index f04f77f28..b0c82af02 100644 --- a/packages/evm/src/fungible.ts +++ b/packages/evm/src/fungible.ts @@ -1,4 +1,4 @@ -import type { Eip1193Provider, EthereumConfig, EvmResource } from '@buildwithsygma/core'; +import type { Eip1193Provider, EvmResource } from '@buildwithsygma/core'; import { Config, FeeHandlerType, @@ -9,17 +9,14 @@ import { Bridge__factory, ERC20__factory } from '@buildwithsygma/sygma-contracts import type { TransactionRequest } from '@ethersproject/providers'; import { Web3Provider } from '@ethersproject/providers'; import { BigNumber, constants, type PopulatedTransaction, utils } from 'ethers'; +import type { EvmFee } from 'types.js'; import type { EvmTransferParams } from './evmTransfer.js'; import { EvmTransfer } from './evmTransfer.js'; -import type { EvmFee } from './types.js'; -import { - approve, - createERCDepositData, - createTransactionRequest, - erc20Transfer, - getERC20Allowance, -} from './utils/index.js'; +import { approve, getERC20Allowance } from './utils/approveAndCheckFns.js'; +import { erc20Transfer } from './utils/depositFns.js'; +import { createERCDepositData } from './utils/helpers.js'; +import { createTransactionRequest } from './utils/transaction.js'; interface EvmFungibleTransferRequest extends EvmTransferParams { sourceAddress: string; @@ -32,20 +29,21 @@ interface EvmFungibleTransferRequest extends EvmTransferParams { * @internal only * This method is used to adjust transfer amount * based on percentage fee calculations - * @param transferAmount + * @param {EvmFungibleAssetTransfer} transfer * @param {EvmFee} fee */ -function calculateAdjustedAmount(transferAmount: bigint, fee: EvmFee): bigint { - //in case of percentage fee handler, we are calculating what amount + fee will result int user inputted amount +function calculateAdjustedAmount(transfer: EvmFungibleAssetTransfer, fee: EvmFee): bigint { + //in case of percentage fee handler, we are calculating what amount + fee will result int user inputed amount //in case of fixed(basic) fee handler, fee is taken from native token if (fee.type === FeeHandlerType.PERCENTAGE) { const minFee = fee.minFee!; const maxFee = fee.maxFee!; const percentage = fee.percentage!; - const userSpecifiedAmount = BigNumber.from(transferAmount); - let amount: bigint; + const userSpecifiedAmount = BigNumber.from(transfer.amount); + let amount = transfer.amount; // calculate amount // without fee (percentage) + const feelessAmount = userSpecifiedAmount .mul(constants.WeiPerEther) .div(utils.parseEther(String(1 + percentage))); @@ -53,19 +51,19 @@ function calculateAdjustedAmount(transferAmount: bigint, fee: EvmFee): bigint { const calculatedFee = userSpecifiedAmount.sub(feelessAmount); amount = feelessAmount.toBigInt(); - //if calculated percentage fee is less than lower fee bound, subtract lower bound from user input. If lower bound is 0, bound is ignored + //if calculated percentage fee is less than lower fee bound, substract lower bound from user input. If lower bound is 0, bound is ignored if (calculatedFee.lt(minFee) && minFee > 0) { - amount = transferAmount - minFee; + amount = transfer.amount - minFee; } - //if calculated percentage fee is more than upper fee bound, subtract upper bound from user input. If upper bound is 0, bound is ignored + //if calculated percentage fee is more than upper fee bound, substract upper bound from user input. If upper bound is 0, bound is ignored if (calculatedFee.gt(maxFee) && maxFee > 0) { - amount = transferAmount - maxFee; + amount = transfer.amount - maxFee; } return amount; } - return transferAmount; + return transfer.amount; } /** * Prepare a Sygma fungible token transfer @@ -98,23 +96,10 @@ export async function createEvmFungibleAssetTransfer( class EvmFungibleAssetTransfer extends EvmTransfer { protected destinationAddress: string = ''; protected securityModel: SecurityModel; - protected adjustedAmount: bigint = BigInt(0); - private specifiedAmount: bigint; // Original value to transfer without deductions - - constructor(transfer: EvmFungibleTransferRequest, config: Config) { - super(transfer, config); - this.specifiedAmount = transfer.amount; + protected _amount: bigint; - if (isValidAddressForNetwork(transfer.destinationAddress, this.destination.type)) - this.destinationAddress = transfer.destinationAddress; - this.securityModel = transfer.securityModel ?? SecurityModel.MPC; - } - - /** - * Returns amount to be transferred considering the fee - */ get amount(): bigint { - return this.adjustedAmount; + return this._amount; } public getSourceNetworkProvider(): Eip1193Provider { @@ -122,7 +107,7 @@ class EvmFungibleAssetTransfer extends EvmTransfer { } async isValidTransfer(): Promise { - const sourceDomainConfig = this.config.getDomainConfig(this.source) as EthereumConfig; + const sourceDomainConfig = this.config.getDomainConfig(this.source); const web3Provider = new Web3Provider(this.sourceNetworkProvider); const bridge = Bridge__factory.connect(sourceDomainConfig.bridge, web3Provider); const { resourceId } = this.resource; @@ -134,16 +119,22 @@ class EvmFungibleAssetTransfer extends EvmTransfer { return createERCDepositData(this.amount, this.destinationAddress, this.destination.parachainId); } + constructor(transfer: EvmFungibleTransferRequest, config: Config) { + super(transfer, config); + this._amount = transfer.amount; + if (isValidAddressForNetwork(transfer.destinationAddress, this.destination.type)) + this.destinationAddress = transfer.destinationAddress; + this.securityModel = transfer.securityModel ?? SecurityModel.MPC; + } /** * Set amount to be transferred * @param {BigInt} amount * @returns {Promise} */ async setAmount(amount: bigint): Promise { - this.specifiedAmount = amount; - + this._amount = amount; const fee = await this.getFee(); - this.adjustedAmount = calculateAdjustedAmount(amount, fee); + this._amount = calculateAdjustedAmount(this, fee); } /** * Sets the destination address @@ -168,9 +159,6 @@ class EvmFungibleAssetTransfer extends EvmTransfer { const erc20 = ERC20__factory.connect(resource.address, provider); const fee = await this.getFee(); - - await this.verifyAccountBalance(fee); - const feeHandlerAllowance = await getERC20Allowance( erc20, this.sourceAddress, @@ -202,8 +190,6 @@ class EvmFungibleAssetTransfer extends EvmTransfer { const bridge = Bridge__factory.connect(domainConfig.bridge, provider); const fee = await this.getFee(); - await this.verifyAccountBalance(fee); - const transferTx = await erc20Transfer({ depositData: this.getDepositData(), bridgeInstance: bridge, @@ -214,27 +200,4 @@ class EvmFungibleAssetTransfer extends EvmTransfer { return createTransactionRequest(transferTx); } - - async verifyAccountBalance(fee: EvmFee): Promise { - const resource = this.resource as EvmResource; - const provider = new Web3Provider(this.sourceNetworkProvider); - - // Native Token Balance check - if ([FeeHandlerType.BASIC, FeeHandlerType.TWAP].includes(fee.type)) { - const nativeBalance = await provider.getBalance(this.sourceAddress); - - if (nativeBalance.lt(fee.fee)) - throw new Error(`Insufficient native token balance for network ${this.sourceDomain.name}`); - } - - // ERC20 Token Balance check - if ([FeeHandlerType.PERCENTAGE].includes(fee.type)) { - const erc20 = ERC20__factory.connect(resource.address, provider); - const erc20TokenBalance = await erc20.balanceOf(this.sourceAddress); - - if (erc20TokenBalance.lt(this.specifiedAmount)) { - throw new Error(`Insufficient ERC20 token balance`); - } - } - } } diff --git a/packages/evm/src/utils/approveAndCheckFns.ts b/packages/evm/src/utils/approveAndCheckFns.ts index d459dacf8..121e4745e 100644 --- a/packages/evm/src/utils/approveAndCheckFns.ts +++ b/packages/evm/src/utils/approveAndCheckFns.ts @@ -8,7 +8,7 @@ import type { BigNumber, PopulatedTransaction } from 'ethers'; * const tokenApproved = await isApproved(tokenInstance, handlerAddress, tokenId); * console.log(`Token approval status for ${tokenID}:`, isApproved); * - * @category Token interactions + * @category Token iteractions * @param {ERC721MinterBurnerPauser} tokenInstance - The ERC721 token instance used to query the approval status. * @param {string} spender - The address for which the token approval status is checked. * @param {number} tokenId - The TokenId of the token to be checked. diff --git a/packages/evm/src/utils/index.ts b/packages/evm/src/utils/index.ts index 92d64e49a..057ee4959 100644 --- a/packages/evm/src/utils/index.ts +++ b/packages/evm/src/utils/index.ts @@ -1,5 +1,3 @@ export * from './approveAndCheckFns.js'; export * from './balances.js'; export * from './depositFns.js'; -export * from './transaction.js'; -export * from './helpers.js'; diff --git a/packages/substrate/package.json b/packages/substrate/package.json index 2e8743817..3ba763b51 100644 --- a/packages/substrate/package.json +++ b/packages/substrate/package.json @@ -57,9 +57,9 @@ "dependencies": { "@buildwithsygma/core": "workspace:^", "@buildwithsygma/sygma-contracts": "2.5.1", - "@polkadot/api": "^12.3.1", + "@polkadot/api": "^12.2.1", "@polkadot/api-base": "^12.2.2", - "@polkadot/types": "^12.3.1", + "@polkadot/types": "^12.2.1", "@polkadot/util": "^12.6.2", "dotenv": "^16.4.5" } diff --git a/packages/substrate/src/__test__/fungible.test.ts b/packages/substrate/src/__test__/fungible.test.ts index c6185e574..984c10699 100644 --- a/packages/substrate/src/__test__/fungible.test.ts +++ b/packages/substrate/src/__test__/fungible.test.ts @@ -1,74 +1,24 @@ import { FeeHandlerType } from '@buildwithsygma/core'; -import type { SubmittableResult } from '@polkadot/api'; -import { ApiPromise, WsProvider } from '@polkadot/api'; +import type { ApiPromise, SubmittableResult } from '@polkadot/api'; import type { SubmittableExtrinsic } from '@polkadot/api-base/types'; import { BN } from '@polkadot/util'; import type { SubstrateAssetTransferRequest } from '../fungible.js'; import { createSubstrateFungibleAssetTransfer } from '../fungible.js'; -import { - deposit, - getAssetBalance, - getBasicFee, - getFeeHandler, - getNativeTokenBalance, - getPercentageFee, -} from '../utils/index.js'; +import { deposit, getBasicFee, getFeeHandler, getPercentageFee } from '../utils/index.js'; jest.mock('../utils/index.js'); -jest.mock('@polkadot/api', () => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const originalModule = jest.requireActual('@polkadot/api'); - const mockBalance = { - data: { - free: '500', - reserved: '5000', - miscFrozen: '5000', - feeFrozen: '5000', - }, - }; - const mockChainProperties = { - tokenDecimals: ['12'], - tokenSymbol: ['DOT'], - }; - - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return { - ...originalModule, - WsProvider: jest.fn().mockImplementation(() => ({})), - ApiPromise: { - create: jest.fn().mockResolvedValue({ - query: { - system: { - account: jest.fn().mockResolvedValue(mockBalance), - }, - }, - registry: { - getChainProperties: jest.fn().mockReturnValue(mockChainProperties), - }, - }), - }, - }; -}); describe('SubstrateFungibleAssetTransfer', () => { - let api: jest.Mocked; - let transferRequest: SubstrateAssetTransferRequest; - - beforeAll(async () => { - const RHALA_RPC_URL = 'wss://rhala-node.phala.network/ws'; - const wsProvider = new WsProvider(RHALA_RPC_URL); - api = (await ApiPromise.create({ provider: wsProvider })) as jest.Mocked; - transferRequest = { - sourceAddress: '5E75Q88u1Hw2VouCiRWoEfKXJMFtqLSUzhqzsH6yWjhd8cem', - source: 5, // Substrate - destination: 1337, // Ethereum - sourceNetworkProvider: api, - resource: '0x0000000000000000000000000000000000000000000000000000000000000300', - amount: BigInt(100), - destinationAddress: '0x98729c03c4D5e820F5e8c45558ae07aE63F97461', - }; - }); + const transferRequest: SubstrateAssetTransferRequest = { + sourceAddress: '', + source: 5, // Substrate + destination: 1337, // Ethereum + sourceNetworkProvider: {} as ApiPromise, + resource: '0x0000000000000000000000000000000000000000000000000000000000000300', + amount: BigInt(1000), + destinationAddress: '0x98729c03c4D5e820F5e8c45558ae07aE63F97461', + }; beforeEach(() => { jest.clearAllMocks(); @@ -112,61 +62,25 @@ describe('SubstrateFungibleAssetTransfer', () => { expect(fee).toEqual({ fee: new BN(50), type: FeeHandlerType.PERCENTAGE }); }); - test('should return a valid transfer transaction', async () => { - (getFeeHandler as jest.Mock).mockResolvedValue(FeeHandlerType.BASIC); - (getBasicFee as jest.Mock).mockResolvedValue({ fee: new BN(100), type: FeeHandlerType.BASIC }); - (deposit as jest.Mock).mockResolvedValue( - {} as SubmittableExtrinsic<'promise', SubmittableResult>, - ); - (getAssetBalance as jest.Mock).mockResolvedValue({ balance: BigInt(1000) }); - (getNativeTokenBalance as jest.Mock).mockResolvedValue({ free: BigInt(100) }); - - const transfer = await createSubstrateFungibleAssetTransfer(transferRequest); - const tx = await transfer.getTransferTransaction(); - expect(tx).toBeDefined(); - }); - - test('should throw ERROR - when native balance is not sufficient', async () => { + test('should throw error if transfer amount is less than the fee', async () => { (getFeeHandler as jest.Mock).mockResolvedValue(FeeHandlerType.BASIC); (getBasicFee as jest.Mock).mockResolvedValue({ fee: new BN(2000), type: FeeHandlerType.BASIC }); - (getAssetBalance as jest.Mock).mockResolvedValue({ balance: BigInt(1) }); - - const insufficientBalanceRequest = { - ...transferRequest, - amount: BigInt(1000), // Amount set to trigger insufficient balance - }; - const transfer = await createSubstrateFungibleAssetTransfer(insufficientBalanceRequest); + const transfer = await createSubstrateFungibleAssetTransfer(transferRequest); await expect(transfer.getTransferTransaction()).rejects.toThrow( - 'Insufficient balance to perform the Transaction', + 'Transfer amount should be higher than transfer fee', ); }); - test('should throw ERROR - Transferable Token is not sufficient - basic', async () => { + test('should return a valid transfer transaction', async () => { (getFeeHandler as jest.Mock).mockResolvedValue(FeeHandlerType.BASIC); - (getBasicFee as jest.Mock).mockResolvedValue({ - fee: new BN(2000), - type: FeeHandlerType.PERCENTAGE, - }); - (getAssetBalance as jest.Mock).mockResolvedValue({ balance: BigInt(0) }); - - const transfer = await createSubstrateFungibleAssetTransfer(transferRequest); - await expect(transfer.getTransferTransaction()).rejects.toThrow( - 'Insufficient asset balance to perform the Transaction', + (getBasicFee as jest.Mock).mockResolvedValue({ fee: new BN(100), type: FeeHandlerType.BASIC }); + (deposit as jest.Mock).mockResolvedValue( + {} as SubmittableExtrinsic<'promise', SubmittableResult>, ); - }); - - test('should throw ERROR - Transferable Token is not sufficient - Percentage', async () => { - (getFeeHandler as jest.Mock).mockResolvedValue(FeeHandlerType.PERCENTAGE); - (getPercentageFee as jest.Mock).mockResolvedValue({ - fee: new BN(100), - type: FeeHandlerType.PERCENTAGE, - }); - (getAssetBalance as jest.Mock).mockResolvedValue({ balance: BigInt(0) }); const transfer = await createSubstrateFungibleAssetTransfer(transferRequest); - await expect(transfer.getTransferTransaction()).rejects.toThrow( - 'Insufficient asset balance to perform the Transaction', - ); + const tx = await transfer.getTransferTransaction(); + expect(tx).toBeDefined(); }); }); diff --git a/packages/substrate/src/fungible.ts b/packages/substrate/src/fungible.ts index 41a15bf77..72f78b030 100644 --- a/packages/substrate/src/fungible.ts +++ b/packages/substrate/src/fungible.ts @@ -1,10 +1,9 @@ import type { BaseTransferParams, SubstrateResource } from '@buildwithsygma/core'; import { - BaseTransfer, Config, FeeHandlerType, isValidAddressForNetwork, - Network, + BaseTransfer, ResourceType, } from '@buildwithsygma/core'; import type { ApiPromise, SubmittableResult } from '@polkadot/api'; @@ -12,14 +11,7 @@ import type { SubmittableExtrinsic } from '@polkadot/api-base/types'; import { BN } from '@polkadot/util'; import type { SubstrateFee } from './types.js'; -import { - deposit, - getBasicFee, - getFeeHandler, - getPercentageFee, - getAssetBalance, - getNativeTokenBalance, -} from './utils/index.js'; +import { deposit, getBasicFee, getFeeHandler, getPercentageFee } from './utils/index.js'; export interface SubstrateAssetTransferRequest extends BaseTransferParams { sourceNetworkProvider: ApiPromise; @@ -38,13 +30,14 @@ export async function createSubstrateFungibleAssetTransfer( class SubstrateFungibleAssetTransfer extends BaseTransfer { amount: bigint; - destinationAddress: string = ''; + destinationAddress: string; sourceNetworkProvider: ApiPromise; constructor(transfer: SubstrateAssetTransferRequest, config: Config) { super(transfer, config); this.amount = transfer.amount; this.sourceNetworkProvider = transfer.sourceNetworkProvider; + this.destinationAddress = transfer.destinationAddress; if (isValidAddressForNetwork(transfer.destinationAddress, this.destination.type)) this.destinationAddress = transfer.destinationAddress; @@ -107,52 +100,22 @@ class SubstrateFungibleAssetTransfer extends BaseTransfer { } } - async verifyBalance(): Promise { - const fee = await this.getFee(); - - if (!isValidAddressForNetwork(this.sourceAddress, Network.SUBSTRATE)) - throw new Error('Sender address is incorrect'); - - // Native token balance check - if ([FeeHandlerType.BASIC].includes(fee.type)) { - const amountBigNumber = new BN(this.amount.toString()); - const balance = await getNativeTokenBalance(this.sourceNetworkProvider, this.sourceAddress); - - if (new BN(balance.free).lt(amountBigNumber)) { - throw new Error('Insufficient balance to perform the Transaction'); - } - } - - // Transferable Token balance check - if ([FeeHandlerType.PERCENTAGE].includes(fee.type)) { - const substrateResource = this.resource as SubstrateResource; - if (!substrateResource.assetID) throw new Error('Asset ID is empty for the current resource'); - - const transferableTokenBalance = await getAssetBalance( - this.sourceNetworkProvider, - substrateResource.assetID, - this.sourceAddress, - ); - - if (new BN(transferableTokenBalance.balance).lt(fee.fee)) { - throw new Error('Insufficient asset balance to perform the Transaction'); - } - } - } - /** * Returns the transaction to be signed by the user. * @dev potentially add optional param to override transaction params * @returns {Promise>} */ async getTransferTransaction(): Promise> { + const fee = await this.getFee(); const resource = this.resource as SubstrateResource; if (this.resource.type !== ResourceType.FUNGIBLE) { throw new Error(`Resource type ${this.resource.type} not supported by asset transfer`); } - await this.verifyBalance(); + if (new BN(this.amount.toString()).lt(fee.fee)) { + throw new Error('Transfer amount should be higher than transfer fee'); + } return deposit( this.sourceNetworkProvider, diff --git a/packages/substrate/src/utils/getAssetBalance.ts b/packages/substrate/src/utils/getAssetBalance.ts deleted file mode 100644 index 0984688aa..000000000 --- a/packages/substrate/src/utils/getAssetBalance.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { ApiPromise } from '@polkadot/api'; -import type { Option } from '@polkadot/types'; -import type { AssetBalance } from '@polkadot/types/interfaces'; - -/** - * Retrieves the asset balance of a given account. - * - * @category Token interactions - * @param {ApiPromise} api - The API instance used to query the chain. - * @param {number} assetID - The ID of the asset to query. {@link https://github.com/sygmaprotocol/sygma-substrate-pallets#multiasset | More details} - * @param {string} accountAddress - The address of the account for which to retrieve the asset balance. - * @returns {Promise} A promise that resolves with the retrieved asset balance. - */ -export const getAssetBalance = async ( - api: ApiPromise, - assetID: number, - accountAddress: string, -): Promise => { - const assetRes = await api.query.assets.account>(assetID, accountAddress); - return assetRes.unwrapOrDefault(); -}; diff --git a/packages/substrate/src/utils/getNativeTokenBalance.ts b/packages/substrate/src/utils/getNativeTokenBalance.ts deleted file mode 100644 index c2ae6c20d..000000000 --- a/packages/substrate/src/utils/getNativeTokenBalance.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { ApiPromise } from '@polkadot/api'; -import type { AccountData, AccountInfo } from '@polkadot/types/interfaces'; - -/** - * Retrieves balance value in native tokens of the network - * - * @category Token interactions - * @param {ApiPromise} api - An ApiPromise instance. - * @param {string} accountAddress - The address of the account for which to retrieve the asset balance. - * @returns {Promise} A promise that resolves to a AccountData. - */ -export const getNativeTokenBalance = async ( - api: ApiPromise, - accountAddress: string, -): Promise => { - const accountInfo = await api.query.system.account(accountAddress); - return accountInfo.data; -}; diff --git a/packages/substrate/src/utils/index.ts b/packages/substrate/src/utils/index.ts index 0a4cc2b45..475d561e4 100644 --- a/packages/substrate/src/utils/index.ts +++ b/packages/substrate/src/utils/index.ts @@ -2,6 +2,4 @@ export * from './getFeeHandlers.js'; export * from './getPercentageFee.js'; export * from './getBasicFee.js'; export * from './deposit/deposit.js'; -export * from './getAssetBalance.js'; -export * from './getNativeTokenBalance.js'; export * from './deposit/handleTxExtrinsicResult.js'; diff --git a/packages/substrate/src/__test__/utils/getAssetBalance.test.ts b/packages/utils/src/__test__/getAssetBalance.test.ts similarity index 95% rename from packages/substrate/src/__test__/utils/getAssetBalance.test.ts rename to packages/utils/src/__test__/getAssetBalance.test.ts index f412974f4..98b0c576a 100644 --- a/packages/substrate/src/__test__/utils/getAssetBalance.test.ts +++ b/packages/utils/src/__test__/getAssetBalance.test.ts @@ -2,7 +2,7 @@ import type { ApiPromise } from '@polkadot/api'; import { Option, TypeRegistry } from '@polkadot/types'; import type { AssetBalance } from '@polkadot/types/interfaces'; -import { getAssetBalance } from '../../utils/getAssetBalance.js'; +import { getAssetBalance } from '../substrate/balances.js'; const registry = new TypeRegistry(); const mockApi = { diff --git a/packages/substrate/src/__test__/utils/getNativeTokenBalance.test.ts b/packages/utils/src/__test__/getNativeTokenBalance.test.ts similarity index 93% rename from packages/substrate/src/__test__/utils/getNativeTokenBalance.test.ts rename to packages/utils/src/__test__/getNativeTokenBalance.test.ts index a6a052b44..610d3eb6c 100644 --- a/packages/substrate/src/__test__/utils/getNativeTokenBalance.test.ts +++ b/packages/utils/src/__test__/getNativeTokenBalance.test.ts @@ -2,7 +2,7 @@ import type { ApiPromise } from '@polkadot/api'; import type { AccountInfo } from '@polkadot/types/interfaces'; import { BN } from '@polkadot/util'; -import { getNativeTokenBalance } from '../../utils/getNativeTokenBalance.js'; +import { getNativeTokenBalance } from '../substrate/balances.js'; describe('getNativeTokenBalance', () => { let mockApi: ApiPromise; diff --git a/packages/utils/src/__test__/liquidity.test.ts b/packages/utils/src/__test__/liquidity.test.ts index 8e01e084a..716ab0630 100644 --- a/packages/utils/src/__test__/liquidity.test.ts +++ b/packages/utils/src/__test__/liquidity.test.ts @@ -38,15 +38,7 @@ const mockedDestination = { type: Network.EVM, }; -const mockedTransferEVM = { - amount: 0n, - resource: mockedResource, - config: { - findDomainConfig: jest.fn(), - }, -}; - -const mockedTransferSubstrate = { +const mockedTransfer = { amount: 0n, resource: mockedResource, config: { @@ -58,37 +50,34 @@ const destinationProviderUrl = 'mockedProviderUrl'; describe('hasEnoughLiquidity - EVM', () => { beforeAll(() => { - Object.assign(mockedTransferEVM, { destination: mockedDestination }); + Object.assign(mockedTransfer, { destination: mockedDestination }); - mockedTransferEVM.config.findDomainConfig.mockReturnValue({ + mockedTransfer.config.findDomainConfig.mockReturnValue({ handlers: [mockedHandler], resources: [mockedResource], }); }); it('should return true if there is enough liquidity', async () => { - mockedTransferEVM.amount = BigInt(1); - const isEnough = await hasEnoughLiquidity( - mockedTransferEVM as unknown as Awaited>, + mockedTransfer as unknown as Awaited>, destinationProviderUrl, ); expect(isEnough).toEqual(true); }); - it('should return false if there isnt enough liquidity', async () => { - mockedTransferEVM.amount = BigInt(100); + mockedTransfer.amount = BigInt(10); const isEnough = await hasEnoughLiquidity( - mockedTransferEVM as unknown as Awaited>, + mockedTransfer as unknown as Awaited>, destinationProviderUrl, ); expect(isEnough).toEqual(false); }); it('should throw error if handler is not found', async () => { - const mockedTransferClone = Object.assign({}, mockedTransferEVM); + const mockedTransferClone = Object.assign({}, mockedTransfer); mockedTransferClone.config.findDomainConfig = jest .fn() .mockReturnValue({ handlers: [], resources: [mockedResource] }); @@ -103,7 +92,7 @@ describe('hasEnoughLiquidity - EVM', () => { ).rejects.toThrow('Handler not found or unregistered for resource.'); }); it('should throw error if resource is not found', async () => { - const mockedTransferClone = Object.assign({}, mockedTransferEVM); + const mockedTransferClone = Object.assign({}, mockedTransfer); mockedTransferClone.config.findDomainConfig = jest .fn() .mockReturnValue({ handlers: [mockedHandler], resources: [] }); @@ -121,35 +110,31 @@ describe('hasEnoughLiquidity - EVM', () => { describe('hasEnoughLiquidity - substrate', () => { beforeAll(() => { - Object.assign(mockedTransferSubstrate, { + Object.assign(mockedTransfer, { destination: { ...mockedDestination, type: Network.SUBSTRATE }, }); - mockedTransferSubstrate.config.findDomainConfig.mockReturnValue({ + mockedTransfer.config.findDomainConfig.mockReturnValue({ handlers: [mockedHandler], resources: [mockedResource], }); }); it('should return true if there is enough liquidity', async () => { - mockedTransferSubstrate.amount = BigInt(5); + mockedTransfer.amount = BigInt(5); const isEnough = await hasEnoughLiquidity( - mockedTransferSubstrate as unknown as Awaited< - ReturnType - >, + mockedTransfer as unknown as Awaited>, destinationProviderUrl, ); expect(isEnough).toEqual(true); }); it('should return false if there isnt enough liquidity', async () => { - mockedTransferSubstrate.amount = BigInt(10); + mockedTransfer.amount = BigInt(10); const isEnough = await hasEnoughLiquidity( - mockedTransferSubstrate as unknown as Awaited< - ReturnType - >, + mockedTransfer as unknown as Awaited>, destinationProviderUrl, ); @@ -159,11 +144,11 @@ describe('hasEnoughLiquidity - substrate', () => { describe('hasEnoughLiquidity - error', () => { beforeAll(() => { - Object.assign(mockedTransferEVM, { + Object.assign(mockedTransfer, { destination: { ...mockedDestination, type: 'foo' as unknown as Network }, }); - mockedTransferEVM.config.findDomainConfig.mockReturnValue({ + mockedTransfer.config.findDomainConfig.mockReturnValue({ handlers: [mockedHandler], resources: [mockedResource], }); @@ -171,7 +156,7 @@ describe('hasEnoughLiquidity - error', () => { it('should return false if network type is not supported', async () => { const isEnough = await hasEnoughLiquidity( - mockedTransferEVM as unknown as Awaited>, + mockedTransfer as unknown as Awaited>, destinationProviderUrl, ); diff --git a/packages/utils/src/environment.d.ts b/packages/utils/src/environment.d.ts deleted file mode 100644 index f7f28e351..000000000 --- a/packages/utils/src/environment.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { Environment } from '@buildwithsygma/core'; - -declare global { - namespace NodeJS { - interface ProcessEnv { - SYGMA_ENV: Environment; - } - } -} diff --git a/packages/utils/src/liquidity.ts b/packages/utils/src/liquidity.ts index 55460e558..c6325b5cf 100644 --- a/packages/utils/src/liquidity.ts +++ b/packages/utils/src/liquidity.ts @@ -1,8 +1,8 @@ -import type { Eip1193Provider, EvmResource, SubstrateResource } from '@buildwithsygma/core'; +import type { EvmResource, SubstrateResource, Eip1193Provider } from '@buildwithsygma/core'; import { Network, ResourceType } from '@buildwithsygma/core'; import type { createEvmFungibleAssetTransfer } from '@buildwithsygma/evm'; import { getEvmHandlerBalance } from '@buildwithsygma/evm'; -import type { createSubstrateFungibleAssetTransfer } from '@buildwithsygma/substrate/src'; +import type { createSubstrateFungibleAssetTransfer } from '@buildwithsygma/substrate'; import { HttpProvider } from 'web3-providers-http'; import { getSubstrateHandlerBalance } from './substrate/balances.js'; @@ -42,8 +42,7 @@ export async function hasEnoughLiquidity( handler.address, ); - const transferValue = transfer as Awaited>; - return transferValue.amount <= evmHandlerBalance; + return transfer.amount <= evmHandlerBalance; } case Network.SUBSTRATE: { const substrateHandlerBalance = await getSubstrateHandlerBalance( @@ -52,10 +51,7 @@ export async function hasEnoughLiquidity( handler.address, ); - const transferValue = transfer as Awaited< - ReturnType - >; - return transferValue.amount <= substrateHandlerBalance; + return transfer.amount <= substrateHandlerBalance; } // TODO: Bitcoin? default: diff --git a/packages/utils/src/substrate/balances.ts b/packages/utils/src/substrate/balances.ts index a305680f2..e00c8f224 100644 --- a/packages/utils/src/substrate/balances.ts +++ b/packages/utils/src/substrate/balances.ts @@ -1,7 +1,42 @@ import type { SubstrateResource } from '@buildwithsygma/core'; -import { getAssetBalance, getNativeTokenBalance } from '@buildwithsygma/substrate/types/utils'; import { ApiPromise } from '@polkadot/api'; import { WsProvider } from '@polkadot/rpc-provider'; +import type { Option } from '@polkadot/types'; +import type { AssetBalance, AccountData, AccountInfo } from '@polkadot/types/interfaces'; + +/** + * Retrieves the asset balance of a given account. + * + * @category Token interactions + * @param {ApiPromise} api - The API instance used to query the chain. + * @param {number} assetID - The ID of the asset to query. {@link https://github.com/sygmaprotocol/sygma-substrate-pallets#multiasset | More details} + * @param {string} accountAddress - The address of the account for which to retrieve the asset balance. + * @returns {Promise} A promise that resolves with the retrieved asset balance. + */ +export const getAssetBalance = async ( + api: ApiPromise, + assetID: number, + accountAddress: string, +): Promise => { + const assetRes = await api.query.assets.account>(assetID, accountAddress); + return assetRes.unwrapOrDefault(); +}; + +/** + * Retrieves balance value in native tokens of the network + * + * @category Token interactions + * @param {ApiPromise} api - An ApiPromise instance. + * @param {string} accountAddress - The address of the account for which to retrieve the asset balance. + * @returns {Promise} A promise that resolves to a AccountData. + */ +export const getNativeTokenBalance = async ( + api: ApiPromise, + accountAddress: string, +): Promise => { + const accountInfo = await api.query.system.account(accountAddress); + return accountInfo.data; +}; export const getSubstrateHandlerBalance = async ( destinationProviderUrl: string, diff --git a/yarn.lock b/yarn.lock index 2dc402624..9d5f20ea9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -511,7 +511,7 @@ __metadata: dependencies: "@buildwithsygma/core": "workspace:*" "@buildwithsygma/substrate": "workspace:*" - "@polkadot/api": "npm:^12.3.1" + "@polkadot/api": "npm:^12.2.1" "@polkadot/keyring": "npm:^12.6.2" "@polkadot/util-crypto": "npm:^12.6.2" dotenv: "npm:^16.3.1" @@ -528,9 +528,9 @@ __metadata: dependencies: "@buildwithsygma/core": "workspace:^" "@buildwithsygma/sygma-contracts": "npm:2.5.1" - "@polkadot/api": "npm:^12.3.1" + "@polkadot/api": "npm:^12.2.1" "@polkadot/api-base": "npm:^12.2.2" - "@polkadot/types": "npm:^12.3.1" + "@polkadot/types": "npm:^12.2.1" "@polkadot/util": "npm:^12.6.2" "@types/jest": "npm:^29.5.12" concurrently: "npm:7.0.0" @@ -2334,7 +2334,7 @@ __metadata: languageName: node linkType: hard -"@polkadot/api@npm:12.3.1, @polkadot/api@npm:^12.2.1, @polkadot/api@npm:^12.3.1": +"@polkadot/api@npm:12.3.1, @polkadot/api@npm:^12.2.1": version: 12.3.1 resolution: "@polkadot/api@npm:12.3.1" dependencies: @@ -2518,7 +2518,7 @@ __metadata: languageName: node linkType: hard -"@polkadot/types@npm:12.3.1, @polkadot/types@npm:^12.2.1, @polkadot/types@npm:^12.3.1": +"@polkadot/types@npm:12.3.1, @polkadot/types@npm:^12.2.1": version: 12.3.1 resolution: "@polkadot/types@npm:12.3.1" dependencies: