Skip to content

Commit

Permalink
feat: add semi fungible transfers
Browse files Browse the repository at this point in the history
- added semi fungible transfer encoding logic
- added descriptive errors
- updated tests
- renamed some classes and files
  • Loading branch information
saadahmsiddiqui committed Oct 9, 2024
1 parent 6d5c842 commit c53c19d
Show file tree
Hide file tree
Showing 14 changed files with 115 additions and 70 deletions.
1 change: 1 addition & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export enum ResourceType {
NON_FUNGIBLE = 'nonfungible',
PERMISSIONED_GENERIC = 'permissionedGeneric',
PERMISSIONLESS_GENERIC = 'permissionlessGeneric',
SEMI_FUNGIBLE = 'semifungible',
}

interface BaseResource {
Expand Down
2 changes: 1 addition & 1 deletion packages/evm/src/__test__/fungible.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
);
});
});
Expand Down
2 changes: 1 addition & 1 deletion packages/evm/src/__test__/generic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ describe('createCrossChainContractCall', () => {

await expect(
async () => await createCrossChainContractCall<typeof mockContract, 'store'>(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 () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/evm/src/__test__/nonFungible.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,15 @@ 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()
.mockResolvedValue('0x0000000000000000000000000000000000000000'),
});

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',
);
});
});
Expand Down
25 changes: 25 additions & 0 deletions packages/evm/src/errors.ts
Original file line number Diff line number Diff line change
@@ -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.`;
}
}
2 changes: 1 addition & 1 deletion packages/evm/src/fee/__test__/getFeeInformation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
6 changes: 3 additions & 3 deletions packages/evm/src/fee/getFeeInformation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 };
}
6 changes: 3 additions & 3 deletions packages/evm/src/fungibleAssetTransfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 0 additions & 3 deletions packages/evm/src/genericMessageTransfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,6 @@ class GenericMessageTransfer<
* @returns {Promise<TransactionRequest>}
*/
async getTransferTransaction(overrides?: ethers.Overrides): Promise<TransactionRequest> {
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);
Expand Down
2 changes: 1 addition & 1 deletion packages/evm/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
8 changes: 3 additions & 5 deletions packages/evm/src/nonFungibleAssetTransfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<boolean> {
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<boolean> {
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<boolean> {
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;
}

/**
Expand All @@ -89,7 +95,7 @@ export class semiFungibleTransfer extends AssetTransfer {
const approvalTx = await erc1155.populateTransaction.setApprovalForAll(
handlerAddress,
true,
overrides,
overrides ?? {},
);
approvalTransactions.push(createTransactionRequest(approvalTx));
}
Expand All @@ -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,
Expand All @@ -121,19 +124,15 @@ export class semiFungibleTransfer extends AssetTransfer {
}
}

export async function createNonFungibleERC1155(
params: EvmAssetTransferParams,
): Promise<semiFungibleTransfer> {
export async function createSemiFungibleAssetTransfer(
params: SemiFungibleTransferParams,
): Promise<SemiFungibleAssetTransfer> {
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;
}
6 changes: 6 additions & 0 deletions packages/evm/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
21 changes: 20 additions & 1 deletion packages/evm/src/utils/assetTransferHelpers.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -20,6 +20,12 @@ interface AssetDepositParams {
optionalMessage?: FungibleTransferOptionalMessage;
}

interface SemiFungibleAssetDepositParams
extends Omit<AssetDepositParams, 'tokenId' | 'amount' | 'optionalGas' | 'optionalMessage'> {
tokenIds: string[];
amounts: bigint[];
}

export function serializeEvmAddress(evmAddress: `0x${string}`): Uint8Array {
return arrayify(evmAddress);
}
Expand Down Expand Up @@ -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]));
}

0 comments on commit c53c19d

Please sign in to comment.