diff --git a/examples/evm-to-evm-fungible-transfer/.env.sample b/examples/evm-to-evm-fungible-transfer/.env.sample index 9c1d40ff0..8264b9bbd 100644 --- a/examples/evm-to-evm-fungible-transfer/.env.sample +++ b/examples/evm-to-evm-fungible-transfer/.env.sample @@ -1,4 +1,3 @@ PRIVATE_KEY="" -CRONOS_RPC_URL="CRONOS_RPC_URL_HERE" SEPOLIA_RPC_URL="SEPOLIA_RPC_URL_HERE" SYGMA_ENV="testnet" diff --git a/examples/evm-to-evm-fungible-transfer/README.md b/examples/evm-to-evm-fungible-transfer/README.md index 0fc047f61..fb66a013a 100644 --- a/examples/evm-to-evm-fungible-transfer/README.md +++ b/examples/evm-to-evm-fungible-transfer/README.md @@ -1,6 +1,6 @@ ## Sygma SDK ERC20 Example -This is an example script that demonstrates the functionality of the SDK using the Sygma ecosystem. The script showcases an ERC20 token transfer between the same account on two different testnets using the Sygma SDK. +This is an example script that demonstrates the functionality of the SDK using the Sygma ecosystem. The script showcases an ERC20 token transfer between the same account on two different testnets using the Sygma SDK. ## Prerequisites @@ -10,7 +10,7 @@ Before running the script, ensure that you have the following: - Yarn (version 3.4.1 or higher) - A development wallet funded with `ERC20LRTest` tokens from the [Sygma faucet](https://faucet-ui-stage.buildwithsygma.com/) - The [exported private key](https://support.metamask.io/hc/en-us/articles/360015289632-How-to-export-an-account-s-private-key) of your development wallet -- [Sepolia ETH](https://www.alchemy.com/faucets/ethereum-sepolia) for gas +- [Sepolia ETH](https://www.alchemy.com/faucets/ethereum-sepolia) for gas - An Ethereum [provider](https://www.infura.io/) (in case the hardcoded RPC within the script does not work) ## Getting started @@ -63,21 +63,24 @@ To send an ERC20 example transfer run: yarn run transfer ``` -The example will use `ethers` in conjuction with the sygma-sdk to -create a transfer from `Cronos` to `Sepolia` with a test ERC20 token. +The example will use `ethers` in conjuction with the sygma-sdk to +create a transfer from `Sepolia` to `Holesky` with a test ERC20 token. Replace the placeholder values in the `.env` file with your own Ethereum wallet private key. **Note** To replace default rpc Cronos and Sepolia urls use env variables: + - `SEPOLIA_RPC_URL="SEPOLIA_RPC_URL_HERE"` + ## Script Functionality This example script performs the following steps: + - initializes the SDK and establishes a connection to the Ethereum provider. - retrieves the list of supported domains and resources from the SDK configuration. -- Searches for the ERC20 token resource with the specified symbol +- Searches for the ERC20 token resource with the specified symbol - Searches for the Cronos and Sepolia domains in the list of supported domains based on their chain IDs - Constructs a transfer object that defines the details of the ERC20 token transfer - Retrieves the fee required for the transfer from the SDK. diff --git a/examples/evm-to-evm-fungible-transfer/src/environment.d.ts b/examples/evm-to-evm-fungible-transfer/src/environment.d.ts new file mode 100644 index 000000000..914ca6d99 --- /dev/null +++ b/examples/evm-to-evm-fungible-transfer/src/environment.d.ts @@ -0,0 +1,9 @@ +import type { Environment } from "@buildwithsygma/core"; + +declare global { + namespace NodeJS { + interface ProcessEnv { + SYGMA_ENV: Environment; + } + } +} diff --git a/examples/evm-to-evm-fungible-transfer/src/transfer.ts b/examples/evm-to-evm-fungible-transfer/src/transfer.ts index c9925cf1e..371f6718f 100644 --- a/examples/evm-to-evm-fungible-transfer/src/transfer.ts +++ b/examples/evm-to-evm-fungible-transfer/src/transfer.ts @@ -1,6 +1,8 @@ -import type { Eip1193Provider } from "@buildwithsygma/core"; -import { Environment } from "@buildwithsygma/core"; -import { createEvmFungibleAssetTransfer } from "@buildwithsygma/evm"; +import { Eip1193Provider, getSygmaScanLink } from "@buildwithsygma/core"; +import { + createFungibleAssetTransfer, + FungibleTransferParams, +} from "@buildwithsygma/evm"; import dotenv from "dotenv"; import { Wallet, providers } from "ethers"; import Web3HttpProvider from "web3-providers-http"; @@ -35,18 +37,18 @@ export async function erc20Transfer(): Promise { const sourceAddress = await wallet.getAddress(); const destinationAddress = await wallet.getAddress(); - const params = { + const params: FungibleTransferParams = { source: SEPOLIA_CHAIN_ID, destination: AMOY_CHAIN_ID, sourceNetworkProvider: web3Provider as unknown as Eip1193Provider, resource: RESOURCE_ID, amount: BigInt(1) * BigInt(1e18), - destinationAddress: destinationAddress, - environment: (process.env.SYGMA_ENV as Environment) || Environment.TESTNET, - sourceAddress: sourceAddress, + recipientAddress: destinationAddress, + sourceAddress, }; - const transfer = await createEvmFungibleAssetTransfer(params); + const transfer = await createFungibleAssetTransfer(params); + const approvals = await transfer.getApprovalTransactions(); console.log(`Approving Tokens (${approvals.length})...`); for (const approval of approvals) { @@ -61,7 +63,7 @@ export async function erc20Transfer(): Promise { const response = await wallet.sendTransaction(transferTx); await response.wait(); console.log( - `Deposited, transaction: ${getTxExplorerUrl({ txHash: response.hash, chainId: SEPOLIA_CHAIN_ID })}` + `Depositted, transaction: ${getSygmaScanLink(response.hash, process.env.SYGMA_ENV)}` ); } diff --git a/examples/evm-to-evm-non-fungible-transfer/.env.sample b/examples/evm-to-evm-non-fungible-transfer/.env.sample new file mode 100644 index 000000000..934197add --- /dev/null +++ b/examples/evm-to-evm-non-fungible-transfer/.env.sample @@ -0,0 +1,5 @@ +PRIVATE_KEY="" +CRONOS_RPC_URL="CRONOS_RPC_URL_HERE" +SEPOLIA_RPC_URL="SEPOLIA_RPC_URL_HERE" +SYGMA_ENV="testnet" +TOKEN_ID="" \ No newline at end of file diff --git a/examples/evm-to-evm-non-fungible-transfer/README.md b/examples/evm-to-evm-non-fungible-transfer/README.md new file mode 100644 index 000000000..bebfefb5b --- /dev/null +++ b/examples/evm-to-evm-non-fungible-transfer/README.md @@ -0,0 +1,88 @@ +## Sygma SDK ERC721 Example + +This is an example script that demonstrates the functionality of the SDK using the Sygma ecosystem. The script showcases an ERC721 token transfer between the same account on two different testnets using the Sygma SDK. + +## Prerequisites + +Before running the script, ensure that you have the following: + +- Node.js +- Yarn (version 3.4.1 or higher) + +- The [exported private key](https://support.metamask.io/hc/en-us/articles/360015289632-How-to-export-an-account-s-private-key) of your development wallet +- [Sepolia ETH](https://www.alchemy.com/faucets/ethereum-sepolia) for gas +- An Ethereum [provider](https://www.infura.io/) (in case the hardcoded RPC within the script does not work) + +## Getting started + +### 1. Clone the repository + +To get started, clone this repository to your local machine with: + +```bash +git clone git@github.com:sygmaprotocol/sygma-sdk.git +cd sygma-sdk/ +``` + +### 2. Install dependencies + +Install the project dependencies by running: + +```bash +yarn install +``` + +### 3. Build the sdk + +To start the example you need to build the sdk first with: + +```bash +yarn build:all +``` + +## Usage + +This example uses the `dotenv` module to manage private keys. To run the example, you will need to configure your environment variable to include your test development account's [exported private key](https://support.metamask.io/hc/en-us/articles/360015289632-How-to-export-an-account-s-private-key). A `.env.sample` is provided as a template. + +**DO NOT COMMIT PRIVATE KEYS WITH REAL FUNDS TO GITHUB. DOING SO COULD RESULT IN COMPLETE LOSS OF YOUR FUNDS.** + +Create a `.env` file in the evm-to-evm example folder: + +```bash +cd examples/evm-to-evm-non-fungible-transfer +touch .env +``` + +Replace between the quotation marks your exported private key: + +`PRIVATE_KEY="YOUR_PRIVATE_KEY_HERE"` + +To send an ERC20 example transfer run: + +```bash +yarn run transfer +``` + + + +Replace the placeholder values in the `.env` file with your own Ethereum wallet private key. + +**Note** + +To replace default rpc Cronos and Sepolia urls use env variables: + +- `SEPOLIA_RPC_URL="SEPOLIA_RPC_URL_HERE"` + +## Script Functionality + +This example script performs the following steps: + +- initializes the SDK and establishes a connection to the Ethereum provider. +- retrieves the list of supported domains and resources from the SDK configuration. +- Searches for the ERC721 token resource with the specified symbol +- Searches for the Cronos and Sepolia domains in the list of supported domains based on their chain IDs +- Constructs a transfer object that defines the details of the ERc721 token transfer +- Retrieves the fee required for the transfer from the SDK. +- Builds the necessary approval transactions for the transfer and sends them using the Ethereum wallet. The approval transactions are required to authorize the transfer of ERc721 tokens. +- Builds the final transfer transaction and sends it using the Ethereum wallet. diff --git a/examples/evm-to-evm-non-fungible-transfer/package.json b/examples/evm-to-evm-non-fungible-transfer/package.json new file mode 100644 index 000000000..b3d511e24 --- /dev/null +++ b/examples/evm-to-evm-non-fungible-transfer/package.json @@ -0,0 +1,37 @@ +{ + "name": "@buildwithsygma/evm-to-evm-non-fungible-transfer-example", + "version": "0.1.0", + "type": "module", + "description": "Sygma sdk examples", + "sideEffects": false, + "repository": { + "type": "git", + "url": "https://github.com/sygmaprotocol/sygma-sdk" + }, + "keywords": [ + "sygma", + "sygmaprotocol", + "buildwithsygma", + "web3", + "bridge", + "ethereum" + ], + "scripts": { + "transfer": "tsx src/transfer.ts" + }, + "author": "Sygmaprotocol Product Team", + "license": "LGPL-3.0-or-later", + "devDependencies": { + "dotenv": "^16.3.1", + "eslint": "8", + "ts-node": "10.9.1", + "typescript": "5.0.4" + }, + "dependencies": { + "@buildwithsygma/core": "workspace:^", + "@buildwithsygma/evm": "workspace:^", + "ethers": "5.7.2", + "tsx": "^4.15.4", + "web3-providers-http": "1.10.4" + } +} diff --git a/examples/evm-to-evm-non-fungible-transfer/src/environment.d.ts b/examples/evm-to-evm-non-fungible-transfer/src/environment.d.ts new file mode 100644 index 000000000..914ca6d99 --- /dev/null +++ b/examples/evm-to-evm-non-fungible-transfer/src/environment.d.ts @@ -0,0 +1,9 @@ +import type { Environment } from "@buildwithsygma/core"; + +declare global { + namespace NodeJS { + interface ProcessEnv { + SYGMA_ENV: Environment; + } + } +} diff --git a/examples/evm-to-evm-non-fungible-transfer/src/transfer.ts b/examples/evm-to-evm-non-fungible-transfer/src/transfer.ts new file mode 100644 index 000000000..35afbc594 --- /dev/null +++ b/examples/evm-to-evm-non-fungible-transfer/src/transfer.ts @@ -0,0 +1,74 @@ +import { + Eip1193Provider, + Environment, + getSygmaScanLink, +} from "@buildwithsygma/core"; +import { + createNonFungibleAssetTransfer, + NonFungibleTransferParams, +} from "@buildwithsygma/evm"; +import dotenv from "dotenv"; +import { Wallet, providers } from "ethers"; +import Web3HttpProvider from "web3-providers-http"; + +dotenv.config(); + +const privateKey = process.env.PRIVATE_KEY; + +if (!privateKey) { + throw new Error("Missing environment variable: PRIVATE_KEY"); +} + +const SEPOLIA_CHAIN_ID = 11155111; +const CRONOS_TESTNET_CHAIN_ID = 338; +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}`; + +export async function erc721Transfer(): Promise { + const web3Provider = new Web3HttpProvider(SEPOLIA_RPC_URL); + const ethersWeb3Provider = new providers.Web3Provider(web3Provider); + const wallet = new Wallet(privateKey ?? "", ethersWeb3Provider); + const sourceAddress = await wallet.getAddress(); + const destinationAddress = await wallet.getAddress(); + + const params: NonFungibleTransferParams = { + source: SEPOLIA_CHAIN_ID, + destination: CRONOS_TESTNET_CHAIN_ID, + sourceNetworkProvider: web3Provider as unknown as Eip1193Provider, + resource: RESOURCE_ID, + tokenId: process.env.TOKEN_ID as string, + recipientAddress: destinationAddress, + sourceAddress, + }; + + const transfer = await createNonFungibleAssetTransfer(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 })}` + ); + } + + const transferTx = await transfer.getTransferTransaction(); + const response = await wallet.sendTransaction(transferTx); + await response.wait(); + console.log( + `Depositted, transaction: ${getSygmaScanLink(response.hash, process.env.SYGMA_ENV)}` + ); +} + +erc721Transfer().finally(() => {}); diff --git a/examples/evm-to-evm-non-fungible-transfer/tsconfig.json b/examples/evm-to-evm-non-fungible-transfer/tsconfig.json new file mode 100644 index 000000000..4395351a4 --- /dev/null +++ b/examples/evm-to-evm-non-fungible-transfer/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ES2022", + "allowJs": true, + "declaration": true, + "sourceMap": true, + "declarationMap": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "esModuleInterop": true, + "downlevelIteration": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node" + }, + "include": ["src"] +} diff --git a/examples/substrate-to-evm-fungible-transfer/src/transfer.ts b/examples/substrate-to-evm-fungible-transfer/src/transfer.ts index 3d8ab7066..df1951a51 100644 --- a/examples/substrate-to-evm-fungible-transfer/src/transfer.ts +++ b/examples/substrate-to-evm-fungible-transfer/src/transfer.ts @@ -59,15 +59,17 @@ const substrateTransfer = async (): Promise => { `Transaction included at blockHash ${status.asInBlock.toString()}` ); } else if (status.isFinalized) { - const blockNumber = results.blockNumber.toNumber(); + const blockNumber = results.blockNumber?.toNumber(); const extrinsicIndex = results.txIndex; - console.log( - `Transaction finalized at blockHash ${status.asFinalized.toString()}` - ); - console.log( - `Explorer URL: ${getSygmaExplorerTransferUrl({ blockNumber, extrinsicIndex })}` - ); + if (blockNumber && extrinsicIndex) { + console.log( + `Transaction finalized at blockHash ${status.asFinalized.toString()}` + ); + console.log( + `Explorer URL: ${getSygmaExplorerTransferUrl({ blockNumber, extrinsicIndex })}` + ); + } unsub(); process.exit(0); } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 184b0004e..780aa903f 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,5 +1,7 @@ export type ParachainId = number; +export type HexString = `0x${string}`; + export enum RouteType { GMP = 'gmp', FUNGIBLE = 'fungible', diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 4676a579b..d7dbefe66 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -285,7 +285,7 @@ export function isValidSubstrateAddress(address: string): boolean { * @returns {boolean} */ export function isValidEvmAddress(address: string): boolean { - return !!ethers.utils.isAddress(address); + return ethers.utils.isAddress(address); } /** diff --git a/packages/evm/README.md b/packages/evm/README.md index 8a3846702..5eeceec86 100644 --- a/packages/evm/README.md +++ b/packages/evm/README.md @@ -28,9 +28,9 @@ Bridge configuration and list of supported networks for each environment can be ```typescript import { Environment } from '@buildwithsygma/core'; -import { createEvmFungibleAssetTransfer } from '@buildwithsygma/evm'; +import { createFungibleAssetTransfer } from '@buildwithsygma/evm'; ... -const transfer = await createEvmFungibleAssetTransfer({ +const transfer = await createFungibleAssetTransfer({ source: 11155111, destination: 17000, sourceNetworkProvider: provider, @@ -44,6 +44,26 @@ const approvalTransactions = await transfer.getApprovalTransactions(); const transferTransaction = await transfer.getTransferTransaction(); ``` +### Non Fungible Token Transfers + +```typescript +import { Environment } from '@buildwithsygma/core'; +import { createNonFungibleAssetTransfer } from '@buildwithsygma/evm'; +... +const transfer = await createNonFungibleAssetTransfer({ + source: 11155111, + destination: 17000, + sourceNetworkProvider: provider, + resource: '0x0000000000000000000000000000000000000000000000000000000000000200', + tokenId: "1", + destinationAddress: destinationAddress, + sourceAddress: senderAddress, +}); +... +const approvalTransactions = await transfer.getApprovalTransactions(); +const transferTransaction = await transfer.getTransferTransaction(); +``` + ### Generic Transfers ```typescript @@ -80,4 +100,5 @@ const transferTransaction = await transfer.getTransferTransaction(); The SDK monorepo contains the following examples demonstrating the usage of EVM Package: 1. [Fungible Token Transfers](https://github.com/sygmaprotocol/sygma-sdk/tree/main/examples/evm-to-evm-fungible-transfer) -2. [Generic Transfers/Contract Calls](https://github.com/sygmaprotocol/sygma-sdk/tree/main/examples/evm-to-evm-generic-message-transfer) +2. [Non Fungible Token Transfers](https://github.com/sygmaprotocol/sygma-sdk/tree/main/examples/evm-to-evm-non-fungible-transfer) +3. [Generic Transfers/Contract Calls](https://github.com/sygmaprotocol/sygma-sdk/tree/main/examples/evm-to-evm-generic-message-transfer) diff --git a/packages/evm/src/__test__/constants.ts b/packages/evm/src/__test__/constants.ts new file mode 100644 index 000000000..c54477030 --- /dev/null +++ b/packages/evm/src/__test__/constants.ts @@ -0,0 +1,14 @@ +import type { Eip1193Provider } from '@buildwithsygma/core'; + +export const ASSET_TRANSFER_PARAMS = { + source: 1, + destination: 2, + sourceAddress: '0x98729c03c4D5e820F5e8c45558ae07aE63F97461', + sourceNetworkProvider: jest.fn() as unknown as Eip1193Provider, + recipientAddress: '0x98729c03c4D5e820F5e8c45558ae07aE63F97461', + resource: { + address: '0x98729c03c4D5e820F5e8c45558ae07aE63F97461', + resourceId: '0x0', + caip19: '0x11', + }, +}; diff --git a/packages/evm/src/__test__/fungible.test.ts b/packages/evm/src/__test__/fungible.test.ts index d08387dbd..a41413336 100644 --- a/packages/evm/src/__test__/fungible.test.ts +++ b/packages/evm/src/__test__/fungible.test.ts @@ -1,4 +1,3 @@ -import type { Eip1193Provider } from '@buildwithsygma/core'; import { Network, Config, ResourceType } from '@buildwithsygma/core'; import { BasicFeeHandler__factory, @@ -10,22 +9,18 @@ import { import { BigNumber } from 'ethers'; import { parseEther } from 'ethers/lib/utils.js'; -import { createEvmFungibleAssetTransfer } from '../fungible.js'; +import { createFungibleAssetTransfer } from '../fungibleAssetTransfer.js'; import type { TransactionRequest } from '../types.js'; +import { ASSET_TRANSFER_PARAMS } from './constants.js'; + const TRANSFER_PARAMS = { - source: 1, - destination: 2, - sourceAddress: '0x98729c03c4D5e820F5e8c45558ae07aE63F97461', - sourceNetworkProvider: jest.fn() as unknown as Eip1193Provider, + ...ASSET_TRANSFER_PARAMS, resource: { - address: '0x98729c03c4D5e820F5e8c45558ae07aE63F97461', - type: ResourceType.FUNGIBLE, - resourceId: '0x0', - caip19: '0x11', + ...ASSET_TRANSFER_PARAMS.resource, + type: ResourceType.NON_FUNGIBLE, }, amount: parseEther('10').toBigInt(), - destinationAddress: '0x98729c03c4D5e820F5e8c45558ae07aE63F97461', }; const MOCKED_CONFIG = { @@ -93,9 +88,9 @@ describe('Fungible - createEvmFungibleAssetTransfer', () => { }); it('should create a transfer', async () => { - const transfer = await createEvmFungibleAssetTransfer(TRANSFER_PARAMS); + const transfer = await createFungibleAssetTransfer(TRANSFER_PARAMS); expect(transfer).toBeTruthy(); - expect(transfer.amount).toEqual(parseEther('10').toBigInt()); + expect(transfer.transferAmount).toEqual(parseEther('10').toBigInt()); }); it('should fail if fee handler is not registered', async () => { @@ -105,7 +100,7 @@ describe('Fungible - createEvmFungibleAssetTransfer', () => { .mockResolvedValue('0x0000000000000000000000000000000000000000'), }); - await expect(async () => await createEvmFungibleAssetTransfer(TRANSFER_PARAMS)).rejects.toThrow( + await expect(async () => await createFungibleAssetTransfer(TRANSFER_PARAMS)).rejects.toThrow( 'Failed getting fee: route not registered on fee handler', ); }); @@ -150,7 +145,7 @@ describe('Fungible - Fee', () => { }); it('should return fee for a transfer', async () => { - const transfer = await createEvmFungibleAssetTransfer(TRANSFER_PARAMS); + const transfer = await createFungibleAssetTransfer(TRANSFER_PARAMS); const fee = await transfer.getFee(); expect(fee.fee).toEqual(0n); @@ -164,13 +159,41 @@ describe('Fungible - Fee', () => { calculateFee: jest.fn().mockResolvedValue([BigNumber.from(0)]), }); - const transfer = await createEvmFungibleAssetTransfer(TRANSFER_PARAMS); + const transfer = await createFungibleAssetTransfer(TRANSFER_PARAMS); const fee = await transfer.getFee(); - // expect(fee.fee).toEqual(0n); expect(fee.type).toEqual('percentage'); expect(fee.handlerAddress).toEqual('0x98729c03c4D5e820F5e8c45558ae07aE63F97461'); }); + + it('should query fee with deposit data', async () => { + const calculateFee = jest.fn().mockResolvedValue([BigNumber.from(0)]); + + (PercentageERC20FeeHandler__factory.connect as jest.Mock).mockReturnValue({ + feeHandlerType: jest.fn().mockResolvedValue('percentage'), + calculateFee, + _resourceIDToFeeBounds: jest.fn().mockResolvedValue({ + lowerBound: parseEther('10'), + upperBound: parseEther('100'), + }), + _domainResourceIDToFee: jest.fn().mockResolvedValue(BigNumber.from(100)), + HUNDRED_PERCENT: jest.fn().mockResolvedValue(10000), + }); + + (BasicFeeHandler__factory.connect as jest.Mock).mockReturnValue({ + feeHandlerType: jest.fn().mockResolvedValue('percentage'), + calculateFee: jest.fn().mockResolvedValue([BigNumber.from(0)]), + }); + + const transfer = await createFungibleAssetTransfer(TRANSFER_PARAMS); + await transfer.getFee(); + + const actualDepositData = (calculateFee.mock.calls as string[][])[0][4]; + const expectedDepositData = + '0x0000000000000000000000000000000000000000000000008ac7230489e80000000000000000000000000000000000000000000000000000000000000000001498729c03c4d5e820f5e8c45558ae07ae63f97461'; + + expect(actualDepositData).toEqual(expectedDepositData); + }); }); describe('Fungible - Approvals', () => { @@ -209,7 +232,7 @@ describe('Fungible - Approvals', () => { }); it('should return approvals for a transfer', async () => { - const transfer = await createEvmFungibleAssetTransfer(TRANSFER_PARAMS); + const transfer = await createFungibleAssetTransfer(TRANSFER_PARAMS); const approvals = await transfer.getApprovalTransactions(); expect(approvals.length).toBeGreaterThan(0); @@ -221,14 +244,12 @@ describe('Fungible - Approvals', () => { calculateFee: jest.fn().mockResolvedValue([parseEther('1')]), }); - const transfer = await createEvmFungibleAssetTransfer({ + const transfer = await createFungibleAssetTransfer({ ...TRANSFER_PARAMS, amount: parseEther('0').toBigInt(), }); - await expect(transfer.getApprovalTransactions()).rejects.toThrow( - 'Insufficient native token balance for network', - ); + await expect(transfer.getApprovalTransactions()).rejects.toThrow('Insufficient balance'); }); it('should throw an error if balance is not sufficient - Percentage', async () => { @@ -247,21 +268,19 @@ describe('Fungible - Approvals', () => { 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 + balanceOf: jest.fn().mockResolvedValue(BigNumber.from(parseEther('0').toBigInt())), // Mock balance less than the required amount populateTransaction: { approve: jest.fn().mockResolvedValue({}), }, allowance: jest.fn().mockResolvedValue(parseEther('0')), }); - const transfer = await createEvmFungibleAssetTransfer({ + const transfer = await createFungibleAssetTransfer({ ...TRANSFER_PARAMS, amount: parseEther('100').toBigInt(), }); - await expect(transfer.getApprovalTransactions()).rejects.toThrow( - 'Insufficient ERC20 token balance', - ); + await expect(transfer.getApprovalTransactions()).rejects.toThrow('Insufficient balance'); }); }); @@ -309,7 +328,7 @@ describe('Fungible - Deposit', () => { }); it('should return deposit transaction', async () => { - const transfer = await createEvmFungibleAssetTransfer(TRANSFER_PARAMS); + const transfer = await createFungibleAssetTransfer(TRANSFER_PARAMS); const depositTransaction = await transfer.getTransferTransaction(); expect(depositTransaction).toBeTruthy(); @@ -331,21 +350,24 @@ describe('Fungible - Deposit', () => { 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 + balanceOf: jest.fn().mockResolvedValue(BigNumber.from(parseEther('0').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); + const transfer = await createFungibleAssetTransfer(TRANSFER_PARAMS); - await expect(transfer.getTransferTransaction()).rejects.toThrow( - 'Insufficient ERC20 token balance', - ); + await expect(transfer.getTransferTransaction()).rejects.toThrow('Insufficient token balance'); }); it('should throw ERROR - Insufficient account balance', async () => { + (BasicFeeHandler__factory.connect as jest.Mock).mockReturnValue({ + feeHandlerType: jest.fn().mockResolvedValue('basic'), + calculateFee: jest.fn().mockResolvedValue([parseEther('2')]), + }); + (ERC20__factory.connect as jest.Mock).mockReturnValue({ balanceOf: jest.fn().mockResolvedValue(BigNumber.from(parseEther('1').toBigInt())), // Mock balance less than the required amount populateTransaction: { @@ -354,10 +376,8 @@ describe('Fungible - Deposit', () => { allowance: jest.fn().mockResolvedValue(parseEther('0')), }); - const transfer = await createEvmFungibleAssetTransfer(TRANSFER_PARAMS); + const transfer = await createFungibleAssetTransfer(TRANSFER_PARAMS); - await expect(transfer.getTransferTransaction()).rejects.toThrow( - 'Insufficient ERC20 token balance', - ); + await expect(transfer.getTransferTransaction()).rejects.toThrow('Insufficient token balance'); }); }); diff --git a/packages/evm/src/__test__/generic.test.ts b/packages/evm/src/__test__/generic.test.ts index d24ed0524..2694cc2b2 100644 --- a/packages/evm/src/__test__/generic.test.ts +++ b/packages/evm/src/__test__/generic.test.ts @@ -6,7 +6,7 @@ import { FeeHandlerRouter__factory, } from '@buildwithsygma/sygma-contracts'; -import { createCrossChainContractCall } from '../generic.js'; +import { createCrossChainContractCall } from '../genericMessageTransfer.js'; // eslint-disable-next-line @typescript-eslint/no-unsafe-return jest.mock('@buildwithsygma/core', () => ({ diff --git a/packages/evm/src/__test__/nonFungible.test.ts b/packages/evm/src/__test__/nonFungible.test.ts new file mode 100644 index 000000000..04881f45e --- /dev/null +++ b/packages/evm/src/__test__/nonFungible.test.ts @@ -0,0 +1,179 @@ +import { Config, FeeHandlerType, Network, ResourceType } from '@buildwithsygma/core'; +import { + BasicFeeHandler__factory, + Bridge__factory, + ERC721MinterBurnerPauser__factory, + FeeHandlerRouter__factory, +} from '@buildwithsygma/sygma-contracts'; +import { BigNumber } from 'ethers'; +import { parseEther } from 'ethers/lib/utils.js'; + +import { createNonFungibleAssetTransfer } from '../nonFungibleAssetTransfer.js'; +import type { NonFungibleTransferParams } from '../types.js'; + +import { ASSET_TRANSFER_PARAMS } from './constants.js'; + +const TRANSFER_PARAMS: NonFungibleTransferParams = { + ...ASSET_TRANSFER_PARAMS, + resource: { + ...ASSET_TRANSFER_PARAMS.resource, + type: ResourceType.NON_FUNGIBLE, + }, + tokenId: parseEther('10').toString(), +}; + +const MOCKED_CONFIG = { + init: jest.fn(), + getDomainConfig: jest.fn().mockReturnValue({ bridge: '', id: 1 }), + getDomain: jest.fn().mockReturnValue({ id: 1, type: Network.EVM }), + getResources: jest.fn().mockReturnValue([TRANSFER_PARAMS.resource]), + findDomainConfigBySygmaId: jest.fn().mockReturnValue({ id: 1 }), +}; + +// eslint-disable-next-line @typescript-eslint/no-unsafe-return +jest.mock('@buildwithsygma/core', () => ({ + ...jest.requireActual('@buildwithsygma/core'), + Config: jest.fn(), +})); + +// eslint-disable-next-line @typescript-eslint/no-unsafe-return +jest.mock('@ethersproject/providers', () => ({ + ...jest.requireActual('@ethersproject/providers'), + 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: { connect: jest.fn() }, + BasicFeeHandler__factory: { connect: jest.fn() }, + PercentageERC20FeeHandler__factory: { connect: jest.fn() }, + FeeHandlerRouter__factory: { connect: jest.fn() }, + ERC721MinterBurnerPauser__factory: { connect: jest.fn() }, +})); + +describe('NonFungible - createNonFungibleAssetTransfer', () => { + beforeAll(() => { + (Config as jest.Mock).mockReturnValue(MOCKED_CONFIG); + + (BasicFeeHandler__factory.connect as jest.Mock).mockReturnValue({ + feeHandlerType: jest.fn().mockResolvedValue('basic'), + calculateFee: jest.fn().mockResolvedValue([BigNumber.from(0)]), + }); + + (FeeHandlerRouter__factory.connect as jest.Mock).mockReturnValue({ + _domainResourceIDToFeeHandlerAddress: jest + .fn() + .mockResolvedValue('0x98729c03c4D5e820F5e8c45558ae07aE63F97461'), + }); + + (Bridge__factory.connect as jest.Mock).mockReturnValue({ + _resourceIDToHandlerAddress: jest + .fn() + .mockResolvedValue('0x98729c03c4D5e820F5e8c45558ae07aE63F97461'), + _feeHandler: jest.fn().mockResolvedValue('0x98729c03c4D5e820F5e8c45558ae07aE63F97461'), + }); + }); + + beforeEach(() => { + jest.restoreAllMocks(); + }); + + it('should create a transfer', async () => { + const transfer = await createNonFungibleAssetTransfer(TRANSFER_PARAMS); + expect(transfer).toBeTruthy(); + expect(transfer.transferTokenId).toEqual(parseEther('10').toString()); + }); + + it('should create fail if handler is not registered', async () => { + (Bridge__factory.connect as jest.Mock).mockReturnValue({ + _resourceIDToHandlerAddress: jest + .fn() + .mockResolvedValue('0x0000000000000000000000000000000000000000'), + }); + + await expect(async () => await createNonFungibleAssetTransfer(TRANSFER_PARAMS)).rejects.toThrow( + 'Handler not registered, please check if this is a valid bridge route.', + ); + }); +}); + +describe('NonFungibleAssetTransfer', () => { + beforeAll(() => { + (Config as jest.Mock).mockReturnValue(MOCKED_CONFIG); + + (BasicFeeHandler__factory.connect as jest.Mock).mockReturnValue({ + feeHandlerType: jest.fn().mockResolvedValue('basic'), + calculateFee: jest.fn().mockResolvedValue([BigNumber.from(0)]), + }); + + (FeeHandlerRouter__factory.connect as jest.Mock).mockReturnValue({ + _domainResourceIDToFeeHandlerAddress: jest + .fn() + .mockResolvedValue('0x98729c03c4D5e820F5e8c45558ae07aE63F97461'), + }); + + (Bridge__factory.connect as jest.Mock).mockReturnValue({ + _resourceIDToHandlerAddress: jest + .fn() + .mockResolvedValue('0x98729c03c4D5e820F5e8c45558ae07aE63F97461'), + _feeHandler: jest.fn().mockResolvedValue('0x98729c03c4D5e820F5e8c45558ae07aE63F97461'), + }); + }); + + beforeEach(() => { + jest.restoreAllMocks(); + }); + + it('should return basic fee for non fungible transfer', async () => { + const transfer = await createNonFungibleAssetTransfer(TRANSFER_PARAMS); + const fee = await transfer.getFee(); + expect(fee).toBeTruthy(); + expect(fee.type).toEqual(FeeHandlerType.BASIC); + }); + + it('setter - setTokenId', async () => { + const transfer = await createNonFungibleAssetTransfer(TRANSFER_PARAMS); + const newTokenId = '2046189'; + transfer.setTokenId(newTokenId); + const transferTokenId = transfer.transferTokenId; + expect(transferTokenId).toEqual(newTokenId); + }); + + it('approval - returns approval transaction if not approved', async () => { + const transfer = await createNonFungibleAssetTransfer(TRANSFER_PARAMS); + + (ERC721MinterBurnerPauser__factory.connect as jest.Mock).mockReturnValue({ + getApproved: jest.fn().mockResolvedValue('0x0000000000000000000000000000000000000000'), + populateTransaction: { + approve: jest.fn().mockReturnValue({ + to: '', + value: BigInt(0), + data: '', + }), + }, + }); + + const approvalTransaction = await transfer.getApprovalTransactions(); + expect(approvalTransaction.length).toBeGreaterThan(0); + }); + + it('approval - returns empty array if approved', async () => { + const transfer = await createNonFungibleAssetTransfer(TRANSFER_PARAMS); + + (ERC721MinterBurnerPauser__factory.connect as jest.Mock).mockReturnValue({ + getApproved: jest.fn().mockResolvedValue('0x98729c03c4D5e820F5e8c45558ae07aE63F97461'), + populateTransaction: { + approve: jest.fn().mockReturnValue({ + to: '', + value: BigInt(0), + data: '', + }), + }, + }); + + const approvalTransaction = await transfer.getApprovalTransactions(); + expect(approvalTransaction.length).toEqual(0); + }); +}); diff --git a/packages/evm/src/evmAssetTransfer.ts b/packages/evm/src/evmAssetTransfer.ts new file mode 100644 index 000000000..75cc9c573 --- /dev/null +++ b/packages/evm/src/evmAssetTransfer.ts @@ -0,0 +1,107 @@ +import type { Config } from '@buildwithsygma/core'; +import { isValidAddressForNetwork } from '@buildwithsygma/core'; +import { Bridge__factory } from '@buildwithsygma/sygma-contracts'; +import { Web3Provider } from '@ethersproject/providers'; +import { constants, utils } from 'ethers'; + +import { EvmTransfer } from './evmTransfer.js'; +import type { EvmAssetTransferParams, EvmFee, TransactionRequest } from './types.js'; +import { assetTransfer } from './utils/index.js'; +import { createTransactionRequest } from './utils/transaction.js'; + +/** + * Asset transfers in EVM + * are of supported standards + * like ERC20, ERC721 and ERC1155 + * which require set of approval and + * transfer transactions + * TODO: Add Support for all + */ +interface IAssetTransfer { + getTransferTransaction(): Promise; + getApprovalTransactions(): Promise>; +} + +/** + * + */ +export abstract class AssetTransfer extends EvmTransfer implements IAssetTransfer { + // amount in case if its a fungible transfer + protected specifiedAmount?: bigint; + protected adjustedAmount?: bigint; + // tokenId in case if its a non fungible transfer + protected tokenId?: string; + // Recipient address, marked "!" + // as it is initialized by a setter + // in the constructor + recipient!: string; + + // Recipient address + get recipientAddress(): string { + return this.recipient; + } + + protected constructor(assetTransferParams: EvmAssetTransferParams, config: Config) { + super(assetTransferParams, config); + this.specifiedAmount = assetTransferParams.amount; + this.adjustedAmount = assetTransferParams.amount; + this.tokenId = assetTransferParams.tokenId; + this.setRecipientAddress(assetTransferParams.recipientAddress); + } + + /* eslint-disable @typescript-eslint/no-unused-vars */ + protected hasEnoughBalance(fee?: EvmFee): Promise { + throw new Error('Method not implemented.'); + } + + public getApprovalTransactions(): Promise> { + throw new Error('Method not implemented.'); + } + + /** + * Get transfer transaction + * @returns {Promise} + */ + public async getTransferTransaction(): Promise { + const domainConfig = this.config.getDomainConfig(this.source); + const provider = new Web3Provider(this.sourceNetworkProvider); + const bridge = Bridge__factory.connect(domainConfig.bridge, provider); + const fee = await this.getFee(); + + const hasBalance = await this.hasEnoughBalance(fee); + if (!hasBalance) throw new Error('Insufficient token balance'); + + const transferTx = await assetTransfer({ + depositData: this.getDepositData(), + bridgeInstance: bridge, + domainId: this.destination.id.toString(), + resourceId: this.resource.resourceId, + feeData: fee, + }); + + return createTransactionRequest(transferTx); + } + + /** + * Set recipient address + * @param {string} address + */ + public setRecipientAddress(address: string): void { + if (isValidAddressForNetwork(address, this.destination.type)) this.recipient = address; + } + + /** + * Checks whether the + * resource is registered + * within Sygma protocol + * @returns {Promise} + */ + public async isValidTransfer(): Promise { + 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; + const handlerAddress = await bridge._resourceIDToHandlerAddress(resourceId); + return utils.isAddress(handlerAddress) && handlerAddress !== constants.AddressZero; + } +} diff --git a/packages/evm/src/evmTransfer.ts b/packages/evm/src/evmTransfer.ts index 3c6a9e2f2..4d01c8181 100644 --- a/packages/evm/src/evmTransfer.ts +++ b/packages/evm/src/evmTransfer.ts @@ -1,31 +1,46 @@ -import type { BaseTransferParams, Config, Eip1193Provider } from '@buildwithsygma/core'; +import type { Config, HexString, Eip1193Provider } from '@buildwithsygma/core'; import { BaseTransfer } from '@buildwithsygma/core'; import { providers } from 'ethers'; import { TwapFeeCalculator } from './fee/TwapFee.js'; import { getFeeInformation, BasicFeeCalculator, PercentageFeeCalculator } from './fee/index.js'; -import type { EvmFee } from './types.js'; +import type { EvmFee, EvmTransferParams } from './types.js'; -export interface EvmTransferParams extends BaseTransferParams { - sourceNetworkProvider: Eip1193Provider; -} +/** + * @internal + * @class EvmTransfer + * + * @abstract + * Base EVM transfer class + * housing common functionality + */ +export abstract class EvmTransfer extends BaseTransfer { + protected provider: Eip1193Provider; -export class EvmTransfer extends BaseTransfer { - sourceNetworkProvider: Eip1193Provider; + get sourceNetworkProvider(): Eip1193Provider { + return this.provider; + } - constructor(params: EvmTransferParams, config: Config) { + protected constructor(params: EvmTransferParams, config: Config) { super(params, config); - this.sourceNetworkProvider = params.sourceNetworkProvider; + this.provider = params.sourceNetworkProvider; } + /** + * Deposit Data is required + * by the sygma protocol to process + * transfer types + * @returns {string} + */ protected getDepositData(): string { throw new Error('Method not implemented.'); } /** * Returns fee based on transfer amount. + * @returns {Promise} */ - async getFee(): Promise { + public async getFee(): Promise { const provider = new providers.Web3Provider(this.sourceNetworkProvider); const { feeHandlerAddress, feeHandlerType } = await getFeeInformation( @@ -53,4 +68,8 @@ export class EvmTransfer extends BaseTransfer { depositData: this.getDepositData(), }); } + + public setSourceAddress(address: HexString): void { + this.sourceAddress = address; + } } diff --git a/packages/evm/src/fungible.ts b/packages/evm/src/fungibleAssetTransfer.ts similarity index 51% rename from packages/evm/src/fungible.ts rename to packages/evm/src/fungibleAssetTransfer.ts index f04f77f28..095584500 100644 --- a/packages/evm/src/fungible.ts +++ b/packages/evm/src/fungibleAssetTransfer.ts @@ -1,92 +1,15 @@ -import type { Eip1193Provider, EthereumConfig, EvmResource } from '@buildwithsygma/core'; -import { - Config, - FeeHandlerType, - isValidAddressForNetwork, - SecurityModel, -} from '@buildwithsygma/core'; +import type { EvmResource } from '@buildwithsygma/core'; +import { Config, FeeHandlerType, ResourceType, SecurityModel } from '@buildwithsygma/core'; 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 { 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'; - -interface EvmFungibleTransferRequest extends EvmTransferParams { - sourceAddress: string; - amount: bigint; - destinationAddress: string; - securityModel?: SecurityModel; -} +import { AssetTransfer } from './evmAssetTransfer.js'; +import type { EvmFee, FungibleTransferParams, TransactionRequest } from './types.js'; +import { approve, getERC20Allowance } from './utils/approveAndCheckFns.js'; +import { createERCDepositData } from './utils/helpers.js'; +import { createTransactionRequest } from './utils/transaction.js'; -/** - * @internal only - * This method is used to adjust transfer amount - * based on percentage fee calculations - * @param transferAmount - * @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 - //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; - // calculate amount - // without fee (percentage) - const feelessAmount = userSpecifiedAmount - .mul(constants.WeiPerEther) - .div(utils.parseEther(String(1 + percentage))); - - 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 (calculatedFee.lt(minFee) && minFee > 0) { - amount = transferAmount - 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 (calculatedFee.gt(maxFee) && maxFee > 0) { - amount = transferAmount - maxFee; - } - - return amount; - } - - return transferAmount; -} -/** - * Prepare a Sygma fungible token transfer - * @param {FungibleTokenTransferRequest} params - * @returns {Promise} - */ -export async function createEvmFungibleAssetTransfer( - params: EvmFungibleTransferRequest, -): Promise { - const config = new Config(); - await config.init(process.env.SYGMA_ENV); - - const transfer = new EvmFungibleAssetTransfer(params, config); - - const isValid = await transfer.isValidTransfer(); - if (!isValid) - throw new Error('Handler not registered, please check if this is a valid bridge route.'); - - await transfer.setAmount(params.amount); - return transfer; -} /** * @internal * @class EvmFungibleAssetTransfer @@ -95,43 +18,73 @@ export async function createEvmFungibleAssetTransfer( * for transferring fungible tokens * using Sygma protocol */ -class EvmFungibleAssetTransfer extends EvmTransfer { - protected destinationAddress: string = ''; +class FungibleAssetTransfer extends AssetTransfer { + /** + * @privateRemarks + * + * will be used in future + */ protected securityModel: SecurityModel; - protected adjustedAmount: bigint = BigInt(0); - private specifiedAmount: bigint; // Original value to transfer without deductions + /** + * @privateRemarks + * + * adjustedAmount is the amount that will + * be deducted from the users wallet + * specifiedAmount is the amount that the + * user wants to transfer + */ + protected declare adjustedAmount: bigint; + protected specifiedAmount: bigint; - constructor(transfer: EvmFungibleTransferRequest, config: Config) { + /** + * Returns amount to be transferred considering the fee + */ + get transferAmount(): bigint { + return this.adjustedAmount; + } + + constructor(transfer: FungibleTransferParams, config: Config) { super(transfer, config); this.specifiedAmount = transfer.amount; - - 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 + * Returns encoded deposit + * data + * @returns {string} */ - get amount(): bigint { - return this.adjustedAmount; + protected getDepositData(): string { + return createERCDepositData(this.adjustedAmount, this.recipient, this.destination.parachainId); } - public getSourceNetworkProvider(): Eip1193Provider { - return this.sourceNetworkProvider; - } + /** + * Checks the source wallet + * balance to see if transfer + * would fail + * @param {EvmFee} fee + * @returns {Promise} + */ + protected async hasEnoughBalance(fee?: EvmFee): Promise { + const resource = this.resource as EvmResource; + const provider = new Web3Provider(this.sourceNetworkProvider); - async isValidTransfer(): Promise { - const sourceDomainConfig = this.config.getDomainConfig(this.source) as EthereumConfig; - const web3Provider = new Web3Provider(this.sourceNetworkProvider); - const bridge = Bridge__factory.connect(sourceDomainConfig.bridge, web3Provider); - const { resourceId } = this.resource; - const handlerAddress = await bridge._resourceIDToHandlerAddress(resourceId); - return utils.isAddress(handlerAddress) && handlerAddress !== constants.AddressZero; - } + if (fee) { + switch (fee.type) { + case FeeHandlerType.BASIC: + case FeeHandlerType.TWAP: { + const nativeBalance = await provider.getBalance(this.sourceAddress); + return nativeBalance.gt(fee.fee); + } + case FeeHandlerType.PERCENTAGE: { + const erc20 = ERC20__factory.connect(resource.address, provider); + const erc20TokenBalance = await erc20.balanceOf(this.sourceAddress); + return erc20TokenBalance.gt(fee.fee); + } + } + } - protected getDepositData(): string { - return createERCDepositData(this.amount, this.destinationAddress, this.destination.parachainId); + return false; } /** @@ -139,27 +92,25 @@ class EvmFungibleAssetTransfer extends EvmTransfer { * @param {BigInt} amount * @returns {Promise} */ - async setAmount(amount: bigint): Promise { + public async setTransferAmount(amount: bigint): Promise { this.specifiedAmount = amount; - const fee = await this.getFee(); - this.adjustedAmount = calculateAdjustedAmount(amount, fee); + this.adjustedAmount = calculateAdjustedAmount(this.specifiedAmount, fee); } - /** - * Sets the destination address - * @param destinationAddress - * @returns {void} - */ - setDestinationAddress(destinationAddress: string): void { - if (isValidAddressForNetwork(destinationAddress, this.destination.type)) - this.destinationAddress = destinationAddress; + + public setResource(resource: EvmResource): void { + if (resource.type !== ResourceType.FUNGIBLE) { + throw new Error('Unsupported Resource type.'); + } + this.transferResource = resource; } + /** * Get array of approval transactions * associated with fungible transfer * @returns {Promise>} */ - async getApprovalTransactions(): Promise> { + public async getApprovalTransactions(): Promise> { const provider = new Web3Provider(this.sourceNetworkProvider); const sourceDomainConfig = this.config.getDomainConfig(this.source); const bridge = Bridge__factory.connect(sourceDomainConfig.bridge, provider); @@ -169,7 +120,8 @@ class EvmFungibleAssetTransfer extends EvmTransfer { const erc20 = ERC20__factory.connect(resource.address, provider); const fee = await this.getFee(); - await this.verifyAccountBalance(fee); + const hasBalance = await this.hasEnoughBalance(fee); + if (!hasBalance) throw new Error('Insufficient balance'); const feeHandlerAllowance = await getERC20Allowance( erc20, @@ -184,7 +136,7 @@ class EvmFungibleAssetTransfer extends EvmTransfer { approvals.push(await approve(erc20, fee.handlerAddress, approvalAmount)); } - const transferAmount = BigNumber.from(this.amount); + const transferAmount = BigNumber.from(this.adjustedAmount); if (handlerAllowance.lt(transferAmount)) { const approvalAmount = BigNumber.from(transferAmount).toString(); approvals.push(await approve(erc20, handlerAddress, approvalAmount)); @@ -192,49 +144,65 @@ class EvmFungibleAssetTransfer extends EvmTransfer { return approvals.map(approval => createTransactionRequest(approval)); } - /** - * Get the fungible token transfer transaction - * @returns {Promise} - */ - async getTransferTransaction(): Promise { - const domainConfig = this.config.getDomainConfig(this.source); - const provider = new Web3Provider(this.sourceNetworkProvider); - const bridge = Bridge__factory.connect(domainConfig.bridge, provider); - const fee = await this.getFee(); +} + +/** + * @internal only + * This method is used to adjust transfer amount + * based on percentage fee calculations + * @param transferAmount + * @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 + //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; + // calculate amount + // without fee (percentage) + const feelessAmount = userSpecifiedAmount + .mul(constants.WeiPerEther) + .div(utils.parseEther(String(1 + percentage))); - await this.verifyAccountBalance(fee); + const calculatedFee = userSpecifiedAmount.sub(feelessAmount); + amount = feelessAmount.toBigInt(); - const transferTx = await erc20Transfer({ - depositData: this.getDepositData(), - bridgeInstance: bridge, - domainId: this.destination.id.toString(), - resourceId: this.resource.resourceId, - feeData: fee, - }); + //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 (calculatedFee.lt(minFee) && minFee > 0) { + amount = transferAmount - 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 (calculatedFee.gt(maxFee) && maxFee > 0) { + amount = transferAmount - maxFee; + } - return createTransactionRequest(transferTx); + return amount; } - async verifyAccountBalance(fee: EvmFee): Promise { - const resource = this.resource as EvmResource; - const provider = new Web3Provider(this.sourceNetworkProvider); + return transferAmount; +} - // Native Token Balance check - if ([FeeHandlerType.BASIC, FeeHandlerType.TWAP].includes(fee.type)) { - const nativeBalance = await provider.getBalance(this.sourceAddress); +/** + * Prepare a Sygma fungible token transfer + * @param {FungibleTokenTransferRequest} params + * @returns {Promise} + */ +export async function createFungibleAssetTransfer( + params: FungibleTransferParams, +): Promise { + const config = new Config(); + await config.init(process.env.SYGMA_ENV); - if (nativeBalance.lt(fee.fee)) - throw new Error(`Insufficient native token balance for network ${this.sourceDomain.name}`); - } + const transfer = new FungibleAssetTransfer(params, config); - // 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); + const isValid = await transfer.isValidTransfer(); + if (!isValid) + throw new Error('Handler not registered, please check if this is a valid bridge route.'); - if (erc20TokenBalance.lt(this.specifiedAmount)) { - throw new Error(`Insufficient ERC20 token balance`); - } - } - } + await transfer.setTransferAmount(params.amount); + return transfer; } diff --git a/packages/evm/src/generic.ts b/packages/evm/src/genericMessageTransfer.ts similarity index 84% rename from packages/evm/src/generic.ts rename to packages/evm/src/genericMessageTransfer.ts index 9bddad99c..f193fada0 100644 --- a/packages/evm/src/generic.ts +++ b/packages/evm/src/genericMessageTransfer.ts @@ -1,4 +1,4 @@ -import type { EthereumConfig } from '@buildwithsygma/core'; +import type { EthereumConfig, EvmResource, SubstrateResource } from '@buildwithsygma/core'; import { Config, Network, ResourceType } from '@buildwithsygma/core'; import { Bridge__factory } from '@buildwithsygma/sygma-contracts'; import { Web3Provider } from '@ethersproject/providers'; @@ -10,10 +10,9 @@ import type { } from 'abitype'; import { constants, ethers } from 'ethers'; -import type { EvmTransferParams } from './evmTransfer.js'; import { EvmTransfer } from './evmTransfer.js'; import { getFeeInformation } from './fee/getFeeInformation.js'; -import type { TransactionRequest } from './types.js'; +import type { GenericMessageTransferParams, TransactionRequest } from './types.js'; import { createPermissionlessGenericDepositData, serializeGenericCallParameters, @@ -21,44 +20,6 @@ import { import { genericMessageTransfer } from './utils/index.js'; import { createTransactionRequest } from './utils/transaction.js'; -/** - * Required parameters for initiating a generic - * message transfer request - */ -export interface GenericMessageTransferRequest< - ContractAbi extends Abi, - FunctionName extends ExtractAbiFunctionNames, -> extends EvmTransferParams { - gasLimit: bigint; - functionParameters: AbiParametersToPrimitiveTypes< - ExtractAbiFunction['inputs'], - 'inputs' - >; - functionName: FunctionName; - destinationContractAbi: ContractAbi; - destinationContractAddress: string; - maxFee: bigint; -} -/** - * Prepare a Sygma cross chain contract call - * @param {GenericMessageTransferRequest} request - * @returns {Promise>} - */ -export async function createCrossChainContractCall< - ContractAbi extends Abi, - FunctionName extends ExtractAbiFunctionNames, ->( - request: GenericMessageTransferRequest, -): Promise> { - const config = new Config(); - await config.init(process.env.SYGMA_ENV); - const genericTransfer = new GenericMessageTransfer(request, config); - - const isValidTransfer = await genericTransfer.isValidTransfer(); - if (!isValidTransfer) throw new Error('Invalid transfer.'); - - return genericTransfer; -} /** * @internal * @class EvmFungibleAssetTransfer @@ -71,21 +32,22 @@ class GenericMessageTransfer< ContractAbi extends Abi, FunctionName extends ExtractAbiFunctionNames, > extends EvmTransfer { - maxFee: bigint; - destinationContractAddress: string; - gasLimit: bigint; + protected maxFee: bigint; + protected destinationContractAddress: string; + protected gasLimit: bigint; functionName: FunctionName; destinationContractAbi: ContractAbi; functionParameters: AbiParametersToPrimitiveTypes< ExtractAbiFunction['inputs'], 'inputs' >; + /** * Create `GenericMessageTransfer` instance * @param {GenericMessageTransferRequest} params * @param {Config} config */ - constructor(params: GenericMessageTransferRequest, config: Config) { + constructor(params: GenericMessageTransferParams, config: Config) { super(params, config); this.destinationContractAddress = params.destinationContractAddress; this.gasLimit = params.gasLimit; @@ -94,11 +56,49 @@ class GenericMessageTransfer< this.destinationContractAbi = params.destinationContractAbi; this.maxFee = params.maxFee; } + + /** + * Get prepared additional deposit data + * in hex string format + * @returns {string} + */ + protected getDepositData(): string { + const { executeFunctionSignature, executionData } = this.prepareFunctionCallEncodings(); + return createPermissionlessGenericDepositData( + executeFunctionSignature, + this.destinationContractAddress, + this.maxFee.toString(), + this.sourceAddress, + executionData, + ); + } + + /** + * Prepare function call encodings + * @returns {{ executionData: string; executionFunctionSignature: string; }} + */ + private prepareFunctionCallEncodings(): { + executionData: string; + executeFunctionSignature: string; + } { + const contractInterface = new ethers.utils.Interface( + JSON.stringify(this.destinationContractAbi), + ); + + let executionData = ``; + if (Array.isArray(this.functionParameters)) { + executionData = serializeGenericCallParameters(this.functionParameters); + } + + const executeFunctionSignature = contractInterface.getSighash(this.functionName); + return { executionData, executeFunctionSignature }; + } + /** * Checks validity of the transfer * @returns {Promise} */ - async isValidTransfer(): Promise { + public async isValidTransfer(): Promise { // Resource type should always be generic if ( this.resource.type !== ResourceType.PERMISSIONED_GENERIC && @@ -128,33 +128,37 @@ class GenericMessageTransfer< ); return feeInformation.feeHandlerAddress !== constants.AddressZero; } + /** * Sets the destination contract address * Target contract address * @param {string} contractAddress */ - setDestinationContractAddress(contractAddress: string): void { + public setDestinationContractAddress(contractAddress: string): void { this.destinationContractAddress = contractAddress; } + /** * Sets the destination contract ABI * @param {ContractAbi} contractAbi */ - setDestinationContractAbi(contractAbi: ContractAbi): void { + public setDestinationContractAbi(contractAbi: ContractAbi): void { this.destinationContractAbi = contractAbi; } + /** * Sets the execution function name * @param {FunctionName} name */ - setExecutionFunctionName(name: FunctionName): void { + public setExecutionFunctionName(name: FunctionName): void { this.functionName = name; } + /** * Set functions arguments * @param {AbiParametersToPrimitiveTypes['inputs'], 'inputs'>} parameters */ - setFunctionExecutionParameters( + public setFunctionExecutionParameters( parameters: AbiParametersToPrimitiveTypes< ExtractAbiFunction['inputs'], 'inputs' @@ -162,26 +166,17 @@ class GenericMessageTransfer< ): void { this.functionParameters = parameters; } - /** - * Prepare function call encodings - * @returns {{ executionData: string; executionFunctionSignature: string; }} - */ - private prepareFunctionCallEncodings(): { - executionData: string; - executeFunctionSignature: string; - } { - const contractInterface = new ethers.utils.Interface( - JSON.stringify(this.destinationContractAbi), - ); - let executionData = ``; - if (Array.isArray(this.functionParameters)) { - executionData = serializeGenericCallParameters(this.functionParameters); + public setResource(resource: EvmResource | SubstrateResource): void { + if ( + resource.type !== ResourceType.PERMISSIONED_GENERIC && + resource.type !== ResourceType.PERMISSIONLESS_GENERIC + ) { + throw new Error('Unsupported Resource type.'); } - - const executeFunctionSignature = contractInterface.getSighash(this.functionName); - return { executionData, executeFunctionSignature }; + this.transferResource = resource; } + /** * Get the cross chain generic message transfer * transaction @@ -219,19 +214,25 @@ class GenericMessageTransfer< return createTransactionRequest(transaction); } - /** - * Get prepared additional deposit data - * in hex string format - * @returns {string} - */ - protected getDepositData(): string { - const { executeFunctionSignature, executionData } = this.prepareFunctionCallEncodings(); - return createPermissionlessGenericDepositData( - executeFunctionSignature, - this.destinationContractAddress, - this.maxFee.toString(), - this.sourceAddress, - executionData, - ); - } +} + +/** + * Prepare a Sygma cross chain contract call + * @param {GenericMessageTransferRequest} request + * @returns {Promise>} + */ +export async function createCrossChainContractCall< + ContractAbi extends Abi, + FunctionName extends ExtractAbiFunctionNames, +>( + params: GenericMessageTransferParams, +): Promise> { + const config = new Config(); + await config.init(process.env.SYGMA_ENV); + const genericTransfer = new GenericMessageTransfer(params, config); + + const isValidTransfer = await genericTransfer.isValidTransfer(); + if (!isValidTransfer) throw new Error('Invalid transfer.'); + + return genericTransfer; } diff --git a/packages/evm/src/index.ts b/packages/evm/src/index.ts index 3fe2b63fd..2e6e01ff4 100644 --- a/packages/evm/src/index.ts +++ b/packages/evm/src/index.ts @@ -1,6 +1,6 @@ export * from './fee/index.js'; export * from './utils/index.js'; -export * from './generic.js'; -export * from './fungible.js'; -export * from './utils/helpers.js'; +export * from './fungibleAssetTransfer.js'; +export * from './genericMessageTransfer.js'; +export * from './nonFungibleAssetTransfer.js'; export * from './types.js'; diff --git a/packages/evm/src/nonFungibleAssetTransfer.ts b/packages/evm/src/nonFungibleAssetTransfer.ts new file mode 100644 index 000000000..87ff8af53 --- /dev/null +++ b/packages/evm/src/nonFungibleAssetTransfer.ts @@ -0,0 +1,98 @@ +import type { EvmResource } from '@buildwithsygma/core'; +import { Config, ResourceType } from '@buildwithsygma/core'; +import { + Bridge__factory, + ERC721MinterBurnerPauser__factory, +} from '@buildwithsygma/sygma-contracts'; +import type { PopulatedTransaction } from 'ethers'; +import { providers } from 'ethers'; + +import { AssetTransfer } from './evmAssetTransfer.js'; +import type { EvmFee, NonFungibleTransferParams, TransactionRequest } from './types.js'; +import { createERCDepositData } from './utils/helpers.js'; +import { approve, isApproved } from './utils/index.js'; +import { createTransactionRequest } from './utils/transaction.js'; + +class NonFungibleAssetTransfer extends AssetTransfer { + protected tokenId: string; + + get transferTokenId(): string { + return this.tokenId; + } + + constructor(params: NonFungibleTransferParams, config: Config) { + super(params, config); + this.tokenId = params.tokenId; + } + + /** + * Returns encoded deposit + * data + * @returns {string} + */ + protected getDepositData(): string { + return createERCDepositData(BigInt(this.tokenId), this.recipient, this.destination.parachainId); + } + + /** + * Returns true if source account + * has enough token balance to + * complete the transfer + * @param {EvmFee} fee Fee associated with transfer + * @returns {Promise} + */ + /* eslint-disable @typescript-eslint/no-unused-vars */ + protected async hasEnoughBalance(fee?: EvmFee): Promise { + const { address } = this.resource as EvmResource; + const provider = new providers.Web3Provider(this.sourceNetworkProvider); + const erc721 = ERC721MinterBurnerPauser__factory.connect(address, provider); + const owner = await erc721.ownerOf(this.tokenId); + return owner.toLowerCase() === this.sourceAddress.toLowerCase(); + } + + public setTokenId(tokenId: string): void { + this.tokenId = tokenId; + } + + public setResource(resource: EvmResource): void { + if (resource.type !== ResourceType.NON_FUNGIBLE) { + throw new Error('Unsupported Resource type.'); + } + this.transferResource = resource; + } + + public async getApprovalTransactions(): Promise> { + const approvalTransactions: Array = []; + const provider = new providers.Web3Provider(this.sourceNetworkProvider); + const sourceDomainConfig = this.config.getDomainConfig(this.source.caipId); + const bridge = Bridge__factory.connect(sourceDomainConfig.bridge, provider); + const handlerAddress = await bridge._resourceIDToHandlerAddress(this.resource.resourceId); + + const resource = this.resource as EvmResource; + const { address } = resource; + const tokenInstance = ERC721MinterBurnerPauser__factory.connect(address, provider); + const isAlreadyApproved = await isApproved(tokenInstance, handlerAddress, Number(this.tokenId)); + + if (!isAlreadyApproved) { + approvalTransactions.push(await approve(tokenInstance, handlerAddress, this.tokenId)); + } + + return approvalTransactions.map(transaction => createTransactionRequest(transaction)); + } +} + +export async function createNonFungibleAssetTransfer( + params: NonFungibleTransferParams, +): Promise { + const config = new Config(); + await config.init(process.env.SYGMA_ENV); + + const transfer = new NonFungibleAssetTransfer(params, config); + + const isValidTransfer = await transfer.isValidTransfer(); + + if (!isValidTransfer) + throw new Error('Handler not registered, please check if this is a valid bridge route.'); + + return transfer; +} diff --git a/packages/evm/src/types.ts b/packages/evm/src/types.ts index 9c1c71b47..4509740b4 100644 --- a/packages/evm/src/types.ts +++ b/packages/evm/src/types.ts @@ -1,12 +1,16 @@ import type { - Domain, + BaseTransferParams, Eip1193Provider, EvmResource, FeeHandlerType, SecurityModel, } from '@buildwithsygma/core'; -import type { Bridge } from '@buildwithsygma/sygma-contracts'; -import type { ethers } from 'ethers'; +import type { + Abi, + AbiParametersToPrimitiveTypes, + ExtractAbiFunction, + ExtractAbiFunctionNames, +} from 'abitype'; export interface TransactionRequest { to: string; @@ -38,32 +42,40 @@ export type EvmFee = { maxFee?: bigint; }; -export type GenericTransferRequest = { - sourceDomain: string | number | Domain; - sourceNetworkProvider: Eip1193Provider; - destinationDomain: string | number | Domain; - destinationContractAddress: string; - destinationFunctionSignature: string; - executionData: string; - gasLimit: bigint; - securityModel?: SecurityModel; //defaults to MPC -}; - /** An EVM resource is accepted as either the resource object or it's Sygma ID */ export type EvmResourceish = string | EvmResource; -/** @internal */ -export type FungibleTransferParams = { - /** The unique identifier for the destination network on the bridge. */ - domainId: string; - /** The unique identifier for the resource being transferred. */ - resourceId: string; - /** The bridge instance used for the transfer. */ - bridgeInstance: Bridge; - /** The fee data associated with the ERC20 token transfer, including the gas price and gas limit. */ - feeData: EvmFee; - /** Deposit data including amount of tokens, length and recipient address */ - depositData: string; - /** Optional overrides for the transaction, such as gas price, gas limit, or value. */ - overrides?: ethers.PayableOverrides; -}; +export interface EvmTransferParams extends BaseTransferParams { + sourceAddress: string; + sourceNetworkProvider: Eip1193Provider; +} + +export interface EvmAssetTransferParams extends EvmTransferParams { + recipientAddress: string; + amount?: bigint; + tokenId?: string; +} + +export interface FungibleTransferParams extends EvmAssetTransferParams { + amount: bigint; + securityModel?: SecurityModel; +} + +export interface NonFungibleTransferParams extends EvmAssetTransferParams { + tokenId: string; +} + +export interface GenericMessageTransferParams< + ContractAbi extends Abi, + FunctionName extends ExtractAbiFunctionNames, +> extends EvmTransferParams { + gasLimit: bigint; + functionParameters: AbiParametersToPrimitiveTypes< + ExtractAbiFunction['inputs'], + 'inputs' + >; + functionName: FunctionName; + destinationContractAbi: ContractAbi; + destinationContractAddress: string; + maxFee: bigint; +} diff --git a/packages/evm/src/utils/__test__/depositFns.test.ts b/packages/evm/src/utils/__test__/depositFns.test.ts index 9a7da51fc..a335b926b 100644 --- a/packages/evm/src/utils/__test__/depositFns.test.ts +++ b/packages/evm/src/utils/__test__/depositFns.test.ts @@ -165,7 +165,7 @@ describe('deposit functions', () => { feeData, depositData, }; - await EVM.erc20Transfer(erc20Params); + await EVM.assetTransfer(erc20Params); expect(EVM.executeDeposit).toBeCalledWith( domainId, diff --git a/packages/evm/src/utils/depositFns.ts b/packages/evm/src/utils/depositFns.ts index 5596779ec..1501ab11a 100644 --- a/packages/evm/src/utils/depositFns.ts +++ b/packages/evm/src/utils/depositFns.ts @@ -3,12 +3,28 @@ import type { Bridge } from '@buildwithsygma/sygma-contracts'; import type { PopulatedTransaction, ethers } from 'ethers'; import { BigNumber } from 'ethers'; -import type { EvmFee, FungibleTransferParams } from '../types.js'; +import type { EvmFee } from '../types.js'; import { createPermissionlessGenericDepositData } from './helpers.js'; export const ASSET_TRANSFER_GAS_LIMIT: BigNumber = BigNumber.from(300000); +/** @internal */ +type AssetTransferParams = { + /** The unique identifier for the destination network on the bridge. */ + domainId: string; + /** The unique identifier for the resource being transferred. */ + resourceId: string; + /** The bridge instance used for the transfer. */ + bridgeInstance: Bridge; + /** The fee data associated with the ERC20 token transfer, including the gas price and gas limit. */ + feeData: EvmFee; + /** Deposit data including amount of tokens, length and recipient address */ + depositData: string; + /** Optional overrides for the transaction, such as gas price, gas limit, or value. */ + overrides?: ethers.PayableOverrides; +}; + /** * Perform an erc20 transfer * @@ -27,60 +43,18 @@ export const ASSET_TRANSFER_GAS_LIMIT: BigNumber = BigNumber.from(300000); * @param {Erc20TransferParamsType} params - The parameters for the erc20 transfer function. * @returns {Promise} - The populated transaction. */ -export const erc20Transfer = async ({ +export const assetTransfer = async ({ bridgeInstance, domainId, resourceId, feeData, depositData, overrides, -}: FungibleTransferParams): Promise => { +}: AssetTransferParams): Promise => { // pass data to smartcontract function and create a transaction return executeDeposit(domainId, resourceId, depositData, feeData, bridgeInstance, overrides); }; -/** - * Executes a deposit operation using the specified parameters and returns a populated transaction. - * - * - * @category Bridge deposit - * @param {string} domainId - The unique identifier for destination network. - * @param {string} resourceId - The resource ID associated with the token. - * @param {string} depositData - The deposit data required for the operation. - * @param {FeeDataResult} feeData - The fee data result for the deposit operation. - * @param {Bridge} bridgeInstance - The bridge instance used to perform the deposit operation. - * @returns {Promise} Unsigned transaction - */ -export const executeDeposit = async ( - domainId: string, - resourceId: string, - depositData: string, - feeData: EvmFee, - bridgeInstance: Bridge, - overrides?: ethers.PayableOverrides, -): Promise => { - const transactionSettings = { - // * "twap" and "basic" both deduct in native currency - value: feeData.type == FeeHandlerType.PERCENTAGE ? 0 : feeData.fee, - gasLimit: ASSET_TRANSFER_GAS_LIMIT, - }; - - const payableOverrides = { - ...transactionSettings, - ...overrides, - }; - - const depositTransaction = await bridgeInstance.populateTransaction.deposit( - domainId, - resourceId, - depositData, - '0x', - payableOverrides, - ); - - return depositTransaction; -}; - type GenericMessageParams = { executeFunctionSignature: string; executeContractAddress: string; @@ -122,3 +96,45 @@ export const genericMessageTransfer = async ({ ); return executeDeposit(domainId, resourceId, depositData, feeData, bridgeInstance, overrides); }; + +/** + * Executes a deposit operation using the specified parameters and returns a populated transaction. + * + * + * @category Bridge deposit + * @param {string} domainId - The unique identifier for destination network. + * @param {string} resourceId - The resource ID associated with the token. + * @param {string} depositData - The deposit data required for the operation. + * @param {FeeDataResult} feeData - The fee data result for the deposit operation. + * @param {Bridge} bridgeInstance - The bridge instance used to perform the deposit operation. + * @returns {Promise} Unsigned transaction + */ +export const executeDeposit = async ( + domainId: string, + resourceId: string, + depositData: string, + feeData: EvmFee, + bridgeInstance: Bridge, + overrides?: ethers.PayableOverrides, +): Promise => { + const transactionSettings = { + // * "twap" and "basic" both deduct in native currency + value: feeData.type == FeeHandlerType.PERCENTAGE ? 0 : feeData.fee, + gasLimit: ASSET_TRANSFER_GAS_LIMIT, + }; + + const payableOverrides = { + ...transactionSettings, + ...overrides, + }; + + const depositTransaction = await bridgeInstance.populateTransaction.deposit( + domainId, + resourceId, + depositData, + '0x', + payableOverrides, + ); + + return depositTransaction; +}; diff --git a/packages/utils/src/__test__/liquidity.test.ts b/packages/utils/src/__test__/liquidity.test.ts index 8e01e084a..eb5ebf433 100644 --- a/packages/utils/src/__test__/liquidity.test.ts +++ b/packages/utils/src/__test__/liquidity.test.ts @@ -1,5 +1,5 @@ import { Network, ResourceType } from '@buildwithsygma/core'; -import type { createEvmFungibleAssetTransfer } from '@buildwithsygma/evm'; +import type { createFungibleAssetTransfer } from '@buildwithsygma/evm'; import { hasEnoughLiquidity } from '../liquidity.js'; @@ -39,7 +39,7 @@ const mockedDestination = { }; const mockedTransferEVM = { - amount: 0n, + transferAmount: 0n, resource: mockedResource, config: { findDomainConfig: jest.fn(), @@ -67,10 +67,10 @@ describe('hasEnoughLiquidity - EVM', () => { }); it('should return true if there is enough liquidity', async () => { - mockedTransferEVM.amount = BigInt(1); + mockedTransferEVM.transferAmount = BigInt(1); const isEnough = await hasEnoughLiquidity( - mockedTransferEVM as unknown as Awaited>, + mockedTransferEVM as unknown as Awaited>, destinationProviderUrl, ); @@ -78,10 +78,10 @@ describe('hasEnoughLiquidity - EVM', () => { }); it('should return false if there isnt enough liquidity', async () => { - mockedTransferEVM.amount = BigInt(100); + mockedTransferEVM.transferAmount = BigInt(10); const isEnough = await hasEnoughLiquidity( - mockedTransferEVM as unknown as Awaited>, + mockedTransferEVM as unknown as Awaited>, destinationProviderUrl, ); @@ -95,9 +95,7 @@ describe('hasEnoughLiquidity - EVM', () => { await expect( hasEnoughLiquidity( - mockedTransferClone as unknown as Awaited< - ReturnType - >, + mockedTransferClone as unknown as Awaited>, destinationProviderUrl, ), ).rejects.toThrow('Handler not found or unregistered for resource.'); @@ -110,9 +108,7 @@ describe('hasEnoughLiquidity - EVM', () => { await expect( hasEnoughLiquidity( - mockedTransferClone as unknown as Awaited< - ReturnType - >, + mockedTransferClone as unknown as Awaited>, destinationProviderUrl, ), ).rejects.toThrow('Resource not found or unregistered.'); @@ -135,9 +131,7 @@ describe('hasEnoughLiquidity - substrate', () => { mockedTransferSubstrate.amount = BigInt(5); const isEnough = await hasEnoughLiquidity( - mockedTransferSubstrate as unknown as Awaited< - ReturnType - >, + mockedTransferSubstrate as unknown as Awaited>, destinationProviderUrl, ); @@ -147,9 +141,7 @@ describe('hasEnoughLiquidity - substrate', () => { mockedTransferSubstrate.amount = BigInt(10); const isEnough = await hasEnoughLiquidity( - mockedTransferSubstrate as unknown as Awaited< - ReturnType - >, + mockedTransferSubstrate as unknown as Awaited>, destinationProviderUrl, ); @@ -171,7 +163,7 @@ describe('hasEnoughLiquidity - error', () => { it('should return false if network type is not supported', async () => { const isEnough = await hasEnoughLiquidity( - mockedTransferEVM as unknown as Awaited>, + mockedTransferSubstrate as unknown as Awaited>, destinationProviderUrl, ); diff --git a/packages/utils/src/liquidity.ts b/packages/utils/src/liquidity.ts index 55460e558..065fb563a 100644 --- a/packages/utils/src/liquidity.ts +++ b/packages/utils/src/liquidity.ts @@ -1,6 +1,6 @@ import type { Eip1193Provider, EvmResource, SubstrateResource } from '@buildwithsygma/core'; import { Network, ResourceType } from '@buildwithsygma/core'; -import type { createEvmFungibleAssetTransfer } from '@buildwithsygma/evm'; +import type { createFungibleAssetTransfer } from '@buildwithsygma/evm'; import { getEvmHandlerBalance } from '@buildwithsygma/evm'; import type { createSubstrateFungibleAssetTransfer } from '@buildwithsygma/substrate/src'; import { HttpProvider } from 'web3-providers-http'; @@ -15,7 +15,7 @@ import { getSubstrateHandlerBalance } from './substrate/balances.js'; */ export async function hasEnoughLiquidity( transfer: - | Awaited> + | Awaited> | Awaited>, destinationProviderUrl: string, ): Promise { @@ -42,8 +42,10 @@ export async function hasEnoughLiquidity( handler.address, ); - const transferValue = transfer as Awaited>; - return transferValue.amount <= evmHandlerBalance; + return ( + (transfer as Awaited>).transferAmount <= + evmHandlerBalance + ); } case Network.SUBSTRATE: { const substrateHandlerBalance = await getSubstrateHandlerBalance( @@ -52,10 +54,10 @@ export async function hasEnoughLiquidity( handler.address, ); - const transferValue = transfer as Awaited< - ReturnType - >; - return transferValue.amount <= substrateHandlerBalance; + return ( + (transfer as Awaited>).amount <= + substrateHandlerBalance + ); } // TODO: Bitcoin? default: diff --git a/yarn.lock b/yarn.lock index 2dc402624..d934b80ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -476,6 +476,22 @@ __metadata: languageName: unknown linkType: soft +"@buildwithsygma/evm-to-evm-non-fungible-transfer-example@workspace:examples/evm-to-evm-non-fungible-transfer": + version: 0.0.0-use.local + resolution: "@buildwithsygma/evm-to-evm-non-fungible-transfer-example@workspace:examples/evm-to-evm-non-fungible-transfer" + dependencies: + "@buildwithsygma/core": "workspace:^" + "@buildwithsygma/evm": "workspace:^" + dotenv: "npm:^16.3.1" + eslint: "npm:8" + ethers: "npm:5.7.2" + ts-node: "npm:10.9.1" + tsx: "npm:^4.15.4" + typescript: "npm:5.0.4" + web3-providers-http: "npm:1.10.4" + languageName: unknown + linkType: soft + "@buildwithsygma/evm@workspace:^, @buildwithsygma/evm@workspace:packages/evm": version: 0.0.0-use.local resolution: "@buildwithsygma/evm@workspace:packages/evm"