From c53c19d93c1c198b70a0502657dc3d011599e89a Mon Sep 17 00:00:00 2001 From: Saad Ahmed Siddiqui Date: Wed, 9 Oct 2024 17:17:22 +0200 Subject: [PATCH] feat: add semi fungible transfers - added semi fungible transfer encoding logic - added descriptive errors - updated tests - renamed some classes and files --- packages/core/src/types.ts | 1 + packages/evm/src/__test__/fungible.test.ts | 2 +- packages/evm/src/__test__/generic.test.ts | 2 +- packages/evm/src/__test__/nonFungible.test.ts | 4 +- packages/evm/src/errors.ts | 25 +++++ .../fee/__test__/getFeeInformation.test.ts | 2 +- packages/evm/src/fee/getFeeInformation.ts | 6 +- packages/evm/src/fungibleAssetTransfer.ts | 6 +- packages/evm/src/genericMessageTransfer.ts | 3 - packages/evm/src/index.ts | 2 +- packages/evm/src/nonFungibleAssetTransfer.ts | 8 +- ...ransfer.ts => semiFungbleAssetTransfer.ts} | 97 +++++++++---------- packages/evm/src/types.ts | 6 ++ .../evm/src/utils/assetTransferHelpers.ts | 21 +++- 14 files changed, 115 insertions(+), 70 deletions(-) create mode 100644 packages/evm/src/errors.ts rename packages/evm/src/{semiFungbleTransfer.ts => semiFungbleAssetTransfer.ts} (54%) diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index dfd1d4c7c..c8b99e052 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -43,6 +43,7 @@ export enum ResourceType { NON_FUNGIBLE = 'nonfungible', PERMISSIONED_GENERIC = 'permissionedGeneric', PERMISSIONLESS_GENERIC = 'permissionlessGeneric', + SEMI_FUNGIBLE = 'semifungible', } interface BaseResource { diff --git a/packages/evm/src/__test__/fungible.test.ts b/packages/evm/src/__test__/fungible.test.ts index f944e1d6d..2ff8fcb1c 100644 --- a/packages/evm/src/__test__/fungible.test.ts +++ b/packages/evm/src/__test__/fungible.test.ts @@ -100,7 +100,7 @@ describe('Fungible - createEvmFungibleAssetTransfer', () => { }); await expect(async () => await createFungibleAssetTransfer(TRANSFER_PARAMS)).rejects.toThrow( - 'Failed getting fee: route not registered on fee handler', + 'Fee Handler not found for Resource ID 0x0 to Domain 1', ); }); }); diff --git a/packages/evm/src/__test__/generic.test.ts b/packages/evm/src/__test__/generic.test.ts index 2694cc2b2..b7b624e87 100644 --- a/packages/evm/src/__test__/generic.test.ts +++ b/packages/evm/src/__test__/generic.test.ts @@ -164,7 +164,7 @@ describe('createCrossChainContractCall', () => { await expect( async () => await createCrossChainContractCall(params), - ).rejects.toThrow('Failed getting fee: route not registered on fee handler'); + ).rejects.toThrow('Fee Handler not found for Resource ID 0x00 to Domain undefined'); }); it('should create a transfer object if fee is configured properly', async () => { diff --git a/packages/evm/src/__test__/nonFungible.test.ts b/packages/evm/src/__test__/nonFungible.test.ts index 04881f45e..4c9615a5a 100644 --- a/packages/evm/src/__test__/nonFungible.test.ts +++ b/packages/evm/src/__test__/nonFungible.test.ts @@ -86,7 +86,7 @@ describe('NonFungible - createNonFungibleAssetTransfer', () => { expect(transfer.transferTokenId).toEqual(parseEther('10').toString()); }); - it('should create fail if handler is not registered', async () => { + it('should fail if handler is not registered', async () => { (Bridge__factory.connect as jest.Mock).mockReturnValue({ _resourceIDToHandlerAddress: jest .fn() @@ -94,7 +94,7 @@ describe('NonFungible - createNonFungibleAssetTransfer', () => { }); await expect(async () => await createNonFungibleAssetTransfer(TRANSFER_PARAMS)).rejects.toThrow( - 'Handler not registered, please check if this is a valid bridge route.', + 'Handler for resource 0x0 not registered', ); }); }); diff --git a/packages/evm/src/errors.ts b/packages/evm/src/errors.ts new file mode 100644 index 000000000..b1dc45fde --- /dev/null +++ b/packages/evm/src/errors.ts @@ -0,0 +1,25 @@ +import type { ResourceType } from '@buildwithsygma/core'; + +export class UnsupportedResourceTypeError extends Error { + constructor(expected: ResourceType, given: ResourceType) { + super(); + this.name = 'UnsupportedResourceType Error'; + this.message = `Expected Type: ${expected} but ${given} was provided.`; + } +} + +export class UnregisteredFeeHandlerError extends Error { + constructor(sygmaDomainId: number, sygmaResourceId: string) { + super(); + this.name = 'UnregisteredFeeHandler Error'; + this.message = `Fee Handler not found for Resource ID ${sygmaResourceId} to Domain ${sygmaDomainId}`; + } +} + +export class UnregisteredResourceHandlerError extends Error { + constructor(resourceId: string) { + super(); + this.name = 'UnregisteredHandlerAddress'; + this.message = `Handler for resource ${resourceId} not registered.`; + } +} diff --git a/packages/evm/src/fee/__test__/getFeeInformation.test.ts b/packages/evm/src/fee/__test__/getFeeInformation.test.ts index e6e53b0ec..c8c4aa4f1 100644 --- a/packages/evm/src/fee/__test__/getFeeInformation.test.ts +++ b/packages/evm/src/fee/__test__/getFeeInformation.test.ts @@ -81,6 +81,6 @@ describe('getFeeInformation()', () => { feeInfoParams.sygmaDestinationDomainId, feeInfoParams.sygmaResourceId, ), - ).rejects.toThrow('Failed getting fee: route not registered on fee handler'); + ).rejects.toThrow('Fee Handler not found for Resource ID 0x to Domain 1'); }); }); diff --git a/packages/evm/src/fee/getFeeInformation.ts b/packages/evm/src/fee/getFeeInformation.ts index 1a6d0af8d..7af07e27d 100644 --- a/packages/evm/src/fee/getFeeInformation.ts +++ b/packages/evm/src/fee/getFeeInformation.ts @@ -5,6 +5,8 @@ import { FeeHandlerRouter__factory, } from '@buildwithsygma/sygma-contracts'; import { utils, ethers } from 'ethers'; + +import { UnregisteredFeeHandlerError } from '../errors.js'; /** * @internal * @category EVM Fee @@ -41,12 +43,10 @@ export async function getFeeInformation( ); if (!utils.isAddress(feeHandlerAddress) || feeHandlerAddress === ethers.constants.AddressZero) { - throw new Error(`Failed getting fee: route not registered on fee handler`); + throw new UnregisteredFeeHandlerError(sygmaDestinationDomainId, sygmaResourceId); } const FeeHandler = BasicFeeHandler__factory.connect(feeHandlerAddress, sourceProvider); - const feeHandlerType = (await FeeHandler.feeHandlerType()) as unknown as FeeHandlerType; - return { feeHandlerAddress, feeHandlerType }; } diff --git a/packages/evm/src/fungibleAssetTransfer.ts b/packages/evm/src/fungibleAssetTransfer.ts index b27f0dd41..f4d0f2f2d 100644 --- a/packages/evm/src/fungibleAssetTransfer.ts +++ b/packages/evm/src/fungibleAssetTransfer.ts @@ -9,6 +9,7 @@ import { Web3Provider } from '@ethersproject/providers'; import { BigNumber, constants, utils } from 'ethers'; import type { ethers, PopulatedTransaction } from 'ethers'; +import { UnregisteredResourceHandlerError, UnsupportedResourceTypeError } from './errors.js'; import { AssetTransfer } from './evmAssetTransfer.js'; import type { EvmFee, @@ -131,7 +132,7 @@ class FungibleAssetTransfer extends AssetTransfer { public setResource(resource: EvmResource): void { if (resource.type !== ResourceType.FUNGIBLE) { - throw new Error('Unsupported Resource type.'); + throw new UnsupportedResourceTypeError(ResourceType.FUNGIBLE, resource.type); } this.transferResource = resource; } @@ -275,8 +276,7 @@ export async function createFungibleAssetTransfer( const transfer = new FungibleAssetTransfer(params, config); const isValid = await transfer.isValidTransfer(); - if (!isValid) - throw new Error('Handler not registered, please check if this is a valid bridge route.'); + if (!isValid) throw new UnregisteredResourceHandlerError(transfer.resource.resourceId); await transfer.setTransferAmount(params.amount); return transfer; diff --git a/packages/evm/src/genericMessageTransfer.ts b/packages/evm/src/genericMessageTransfer.ts index ad6c49ccf..7d9d7c2d8 100644 --- a/packages/evm/src/genericMessageTransfer.ts +++ b/packages/evm/src/genericMessageTransfer.ts @@ -134,9 +134,6 @@ class GenericMessageTransfer< * @returns {Promise} */ async getTransferTransaction(overrides?: ethers.Overrides): Promise { - const isValid = await this.isValidTransfer(); - if (!isValid) throw new Error('Invalid Transfer.'); - const sourceDomain = this.config.getDomainConfig(this.source); const provider = new Web3Provider(this.sourceNetworkProvider); const bridgeInstance = Bridge__factory.connect(sourceDomain.bridge, provider); diff --git a/packages/evm/src/index.ts b/packages/evm/src/index.ts index 6f461482b..fa1dd9f04 100644 --- a/packages/evm/src/index.ts +++ b/packages/evm/src/index.ts @@ -3,5 +3,5 @@ export * from './utils/index.js'; export * from './fungibleAssetTransfer.js'; export * from './genericMessageTransfer.js'; export * from './nonFungibleAssetTransfer.js'; -export * from './semiFungbleTransfer.js'; +export * from './semiFungbleAssetTransfer.js'; export * from './types.js'; diff --git a/packages/evm/src/nonFungibleAssetTransfer.ts b/packages/evm/src/nonFungibleAssetTransfer.ts index 52b1dfd89..baf03e360 100644 --- a/packages/evm/src/nonFungibleAssetTransfer.ts +++ b/packages/evm/src/nonFungibleAssetTransfer.ts @@ -7,6 +7,7 @@ import { import type { ethers, PopulatedTransaction } from 'ethers'; import { providers } from 'ethers'; +import { UnregisteredResourceHandlerError, UnsupportedResourceTypeError } from './errors.js'; import { AssetTransfer } from './evmAssetTransfer.js'; import type { EvmFee, NonFungibleTransferParams, TransactionRequest } from './types.js'; import { createAssetDepositData } from './utils/assetTransferHelpers.js'; @@ -60,7 +61,7 @@ class NonFungibleAssetTransfer extends AssetTransfer { public setResource(resource: EvmResource): void { if (resource.type !== ResourceType.NON_FUNGIBLE) { - throw new Error('Unsupported Resource type.'); + throw new UnsupportedResourceTypeError(ResourceType.NON_FUNGIBLE, resource.type); } this.transferResource = resource; } @@ -96,11 +97,8 @@ export async function createNonFungibleAssetTransfer( 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.'); + if (!isValidTransfer) throw new UnregisteredResourceHandlerError(transfer.resource.resourceId); return transfer; } diff --git a/packages/evm/src/semiFungbleTransfer.ts b/packages/evm/src/semiFungbleAssetTransfer.ts similarity index 54% rename from packages/evm/src/semiFungbleTransfer.ts rename to packages/evm/src/semiFungbleAssetTransfer.ts index eaa152214..5d8225707 100644 --- a/packages/evm/src/semiFungbleTransfer.ts +++ b/packages/evm/src/semiFungbleAssetTransfer.ts @@ -1,18 +1,14 @@ -import { Config } from '@buildwithsygma/core'; +import { Config, ResourceType } from '@buildwithsygma/core'; import type { EvmResource } from '@buildwithsygma/core'; -import { - ERC1155__factory, - Bridge__factory, - ERC721MinterBurnerPauser__factory, -} from '@buildwithsygma/sygma-contracts'; +import { ERC1155__factory, Bridge__factory } from '@buildwithsygma/sygma-contracts'; import { Web3Provider } from '@ethersproject/providers'; -import type { ethers } from 'ethers'; -import { providers } from 'ethers'; +import { constants, utils, type ethers } from 'ethers'; -import { AssetTransfer } from './evmAssetTransfer.js'; -import type { EvmAssetTransferParams, TransactionRequest } from './types.js'; +import { UnregisteredResourceHandlerError, UnsupportedResourceTypeError } from './errors.js'; +import { EvmTransfer } from './evmTransfer.js'; +import type { SemiFungibleTransferParams, TransactionRequest } from './types.js'; import { - createAssetDepositData, + createERC1155DepositData, createTransactionRequest, getTransactionOverrides, } from './utils/index.js'; @@ -22,51 +18,61 @@ import { * ERC1155 - it supports ONLY NON-Fungible transfer so far * */ -export class semiFungibleTransfer extends AssetTransfer { - protected tokenId: string; - protected specifiedAmount: bigint; +class SemiFungibleAssetTransfer extends EvmTransfer { + protected tokenIds: string[]; + protected amounts: bigint[]; + protected recipientAdddress: string; - constructor(params: EvmAssetTransferParams, config: Config) { + constructor(params: SemiFungibleTransferParams, config: Config) { super(params, config); + this.recipientAdddress = params.recipientAddress; + this.tokenIds = params.tokenIds; + this.amounts = params.amounts; + } - // Ensure that the amount is always 1 for non-fungible tokens - if (params.amount !== BigInt(1)) { - throw new Error('Amount must be 1 for non-fungible ERC1155 tokens.'); - } - if (!params.tokenId) { - throw new Error('tokenId is required for ERC1155 non-fungible transfers.'); + public setResource(resource: EvmResource): void { + if (resource.type !== ResourceType.SEMI_FUNGIBLE) { + throw new UnsupportedResourceTypeError(ResourceType.SEMI_FUNGIBLE, resource.type); } - this.tokenId = params.tokenId; - this.specifiedAmount = params.amount; + this.transferResource = resource; } /** * Prepare the deposit data required for the ERC1155 transfer. */ protected getDepositData(): string { - return createAssetDepositData({ + return createERC1155DepositData({ destination: this.destination, - recipientAddress: this.recipientAddress, - tokenId: this.tokenId, - amount: this.specifiedAmount, + recipientAddress: this.recipientAdddress, + tokenIds: this.tokenIds, + amounts: this.amounts, }); } + 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; + } + protected async hasEnoughBalance(): Promise { const resource = this.resource as EvmResource; const provider = new Web3Provider(this.sourceNetworkProvider); const erc1155 = ERC1155__factory.connect(resource.address, provider); - const balance = await erc1155.balanceOf(this.sourceAddress, this.tokenId); - return balance.gte(this.specifiedAmount); - } - protected async isOwner(): 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(); + let hasBalance = true; + for (let i = 0; i < this.tokenIds.length; i++) { + const balance = await erc1155.balanceOf(this.sourceAddress, this.tokenIds[i]); + + hasBalance = balance.gte(this.amounts[i]); + if (!hasBalance) i += this.tokenIds.length; + } + + return hasBalance; } /** @@ -89,7 +95,7 @@ export class semiFungibleTransfer extends AssetTransfer { const approvalTx = await erc1155.populateTransaction.setApprovalForAll( handlerAddress, true, - overrides, + overrides ?? {}, ); approvalTransactions.push(createTransactionRequest(approvalTx)); } @@ -106,9 +112,6 @@ export class semiFungibleTransfer extends AssetTransfer { const bridge = Bridge__factory.connect(domainConfig.bridge, provider); const fee = await this.getFee(); - const isOwner = await this.isOwner(); - if (!isOwner) throw new Error('Source address is not an Owner of the token'); - const transferTransaction = await bridge.populateTransaction.deposit( this.destinationDomain.id.toString(), this.resource.resourceId, @@ -121,19 +124,15 @@ export class semiFungibleTransfer extends AssetTransfer { } } -export async function createNonFungibleERC1155( - params: EvmAssetTransferParams, -): Promise { +export async function createSemiFungibleAssetTransfer( + params: SemiFungibleTransferParams, +): Promise { const config = new Config(); await config.init(process.env.SYGMA_ENV); - const transfer = new semiFungibleTransfer(params, config); - + const transfer = new SemiFungibleAssetTransfer(params, config); const isValidTransfer = await transfer.isValidTransfer(); - - if (!isValidTransfer) { - throw new Error('Handler not registered, please check if this is a valid bridge route.'); - } + if (!isValidTransfer) throw new UnregisteredResourceHandlerError(transfer.resource.resourceId); return transfer; } diff --git a/packages/evm/src/types.ts b/packages/evm/src/types.ts index 443a3d91e..192eb1cf1 100644 --- a/packages/evm/src/types.ts +++ b/packages/evm/src/types.ts @@ -77,6 +77,12 @@ export interface EvmAssetTransferParams extends EvmTransferParams { tokenId?: string; } +export interface SemiFungibleTransferParams extends EvmTransferParams { + recipientAddress: string; + tokenIds: string[]; + amounts: bigint[]; +} + export interface FungibleTransferParams extends EvmAssetTransferParams { amount: bigint; securityModel?: SecurityModel; diff --git a/packages/evm/src/utils/assetTransferHelpers.ts b/packages/evm/src/utils/assetTransferHelpers.ts index 3ba96ed4d..dc5ad55dd 100644 --- a/packages/evm/src/utils/assetTransferHelpers.ts +++ b/packages/evm/src/utils/assetTransferHelpers.ts @@ -1,6 +1,6 @@ import type { Domain } from '@buildwithsygma/core'; import { Network } from '@buildwithsygma/core'; -import { AbiCoder } from '@ethersproject/abi'; +import { AbiCoder, defaultAbiCoder } from '@ethersproject/abi'; import { arrayify, concat, hexlify, hexZeroPad } from '@ethersproject/bytes'; import { TypeRegistry } from '@polkadot/types'; import { decodeAddress } from '@polkadot/util-crypto'; @@ -20,6 +20,12 @@ interface AssetDepositParams { optionalMessage?: FungibleTransferOptionalMessage; } +interface SemiFungibleAssetDepositParams + extends Omit { + tokenIds: string[]; + amounts: bigint[]; +} + export function serializeEvmAddress(evmAddress: `0x${string}`): Uint8Array { return arrayify(evmAddress); } @@ -146,3 +152,16 @@ export function createAssetDepositData(depositParams: AssetDepositParams): strin return hexlify(depositData); } + +export function createERC1155DepositData(depositParams: SemiFungibleAssetDepositParams): string { + const { recipientAddress, destination, amounts, tokenIds } = depositParams; + + const recipientAddressSerialized: Uint8Array = serializeDestinationAddress( + recipientAddress, + destination.type, + destination.parachainId, + ); + + const encodedData = defaultAbiCoder.encode(['uint[]', 'uint[]'], [tokenIds, amounts]); + return hexlify(concat([arrayify(encodedData), recipientAddressSerialized])); +}