Skip to content

Commit

Permalink
feat(evm): Update SDK to Support New ERC20Handler with Optional Contr…
Browse files Browse the repository at this point in the history
…act Call (#504)

## Implementation details
With the introduction of the new `ERC20Handler`, the SDK needs to be
updated to handle the enhanced functionality that supports both ERC20
token transfers and optional contract calls in a single transaction.
[New ERC20Handler
PR](sprintertech/sygma-solidity#266)

- Modify the SDK to work with the updated `ERC20Handler` by allowing it
to handle additional data that encodes contract calls alongside ERC20
transfers.
- Maintain compatibility with existing handlers and workflows that do
not involve contract calls, ensuring that the SDK remains functional for
all users.

## Closes: #493 

## Testing details
- **Unit Tests**: Develop unit tests that cover scenarios where both
ERC20 transfers and contract calls are involved.
- **Compatibility Tests**: Ensure that existing functionality remains
unaffected by the changes.
- **Error Handling**: Test how the SDK handles invalid or failed
contract calls within the transaction flow.

## Acceptance Criteria
- [ ] The SDK supports interactions with the new `ERC20Handler`,
allowing for both ERC20 transfers and optional contract calls within the
same transaction.
- [ ] The SDK maintains backward compatibility and continues to function
as expected with existing handlers.
- [ ] All new functionality is covered by tests, ensuring reliable and
consistent behavior.
  • Loading branch information
saadahmsiddiqui authored Sep 10, 2024
1 parent 90c75be commit 2cb56a2
Show file tree
Hide file tree
Showing 16 changed files with 482 additions and 588 deletions.
14 changes: 8 additions & 6 deletions examples/evm-to-evm-fungible-transfer/src/transfer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Eip1193Provider, getSygmaScanLink } from "@buildwithsygma/core";
import { getSygmaScanLink, type Eip1193Provider } from "@buildwithsygma/core";
import {
createFungibleAssetTransfer,
FungibleTransferParams,
Expand All @@ -16,11 +16,12 @@ if (!privateKey) {
}

const SEPOLIA_CHAIN_ID = 11155111;
const AMOY_CHAIN_ID = 80002;
const AMOY_CHAIN_ID = 84532;
const RESOURCE_ID =
"0x0000000000000000000000000000000000000000000000000000000000000300";
"0x0000000000000000000000000000000000000000000000000000000000001200";
const SEPOLIA_RPC_URL =
process.env.SEPOLIA_RPC_URL || "https://eth-sepolia-public.unifra.io";
process.env.SEPOLIA_RPC_URL ||
"https://eth-sepolia.g.alchemy.com/v2/MeCKDrpxLkGOn4LMlBa3cKy1EzzOzwzG";

const explorerUrls: Record<number, string> = {
[SEPOLIA_CHAIN_ID]: "https://sepolia.etherscan.io",
Expand All @@ -42,9 +43,10 @@ export async function erc20Transfer(): Promise<void> {
destination: AMOY_CHAIN_ID,
sourceNetworkProvider: web3Provider as unknown as Eip1193Provider,
resource: RESOURCE_ID,
amount: BigInt(1) * BigInt(1e18),
amount: BigInt(1) * BigInt(1e6),
recipientAddress: destinationAddress,
sourceAddress,
sourceAddress: sourceAddress,
optionalGas: BigInt(500000),
};

const transfer = await createFungibleAssetTransfer(params);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ const substrateTransfer = async (): Promise<void> => {
resource: RESOURCE_ID_SYGMA_USD,
amount: BigInt("1"),
destinationAddress: recipient,
sourceAddress: account.address,
};

const transfer = await createSubstrateFungibleAssetTransfer(transferParams);
Expand Down
20 changes: 11 additions & 9 deletions packages/evm/src/evmAssetTransfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import type { Config } from '@buildwithsygma/core';
import { isValidAddressForNetwork } from '@buildwithsygma/core';
import { Bridge__factory } from '@buildwithsygma/sygma-contracts';
import { Web3Provider } from '@ethersproject/providers';
import type { PayableOverrides } from 'ethers';
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 { executeDeposit } from './utils/depositFn.js';
import { createTransactionRequest } from './utils/transaction.js';

/**
Expand Down Expand Up @@ -62,7 +63,7 @@ export abstract class AssetTransfer extends EvmTransfer implements IAssetTransfe
* Get transfer transaction
* @returns {Promise<TransactionRequest>}
*/
public async getTransferTransaction(): Promise<TransactionRequest> {
public async getTransferTransaction(overrides?: PayableOverrides): Promise<TransactionRequest> {
const domainConfig = this.config.getDomainConfig(this.source);
const provider = new Web3Provider(this.sourceNetworkProvider);
const bridge = Bridge__factory.connect(domainConfig.bridge, provider);
Expand All @@ -71,13 +72,14 @@ export abstract class AssetTransfer extends EvmTransfer implements IAssetTransfe
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,
});
const transferTx = await executeDeposit(
this.destination.id.toString(),
this.resource.resourceId,
this.getDepositData(),
fee,
bridge,
overrides,
);

return createTransactionRequest(transferTx);
}
Expand Down
22 changes: 19 additions & 3 deletions packages/evm/src/fungibleAssetTransfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ import { Web3Provider } from '@ethersproject/providers';
import { BigNumber, constants, type PopulatedTransaction, utils } from 'ethers';

import { AssetTransfer } from './evmAssetTransfer.js';
import type { EvmFee, FungibleTransferParams, TransactionRequest } from './types.js';
import type {
EvmFee,
FungibleTransferOptionalMessage,
FungibleTransferParams,
TransactionRequest,
} from './types.js';
import { approve, getERC20Allowance } from './utils/approveAndCheckFns.js';
import { createERCDepositData } from './utils/helpers.js';
import { createFungibleDepositData } from './utils/assetTransferHelpers.js';
import { createTransactionRequest } from './utils/transaction.js';

/**
Expand Down Expand Up @@ -36,6 +41,9 @@ class FungibleAssetTransfer extends AssetTransfer {
protected declare adjustedAmount: bigint;
protected specifiedAmount: bigint;

protected optionalGas?: bigint;
protected optionalMessage?: FungibleTransferOptionalMessage;

/**
* Returns amount to be transferred considering the fee
*/
Expand All @@ -47,6 +55,8 @@ class FungibleAssetTransfer extends AssetTransfer {
super(transfer, config);
this.specifiedAmount = transfer.amount;
this.securityModel = transfer.securityModel ?? SecurityModel.MPC;
this.optionalGas = transfer.optionalGas;
this.optionalMessage = transfer.optionalMessage;
}

/**
Expand All @@ -55,7 +65,13 @@ class FungibleAssetTransfer extends AssetTransfer {
* @returns {string}
*/
protected getDepositData(): string {
return createERCDepositData(this.adjustedAmount, this.recipient, this.destination.parachainId);
return createFungibleDepositData({
destination: this.destination,
recipientAddress: this.recipientAddress,
amount: this.adjustedAmount,
optionalGas: this.optionalGas,
optionalMessage: this.optionalMessage,
});
}

/**
Expand Down
100 changes: 28 additions & 72 deletions packages/evm/src/genericMessageTransfer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { EthereumConfig, EvmResource, SubstrateResource } from '@buildwithsygma/core';
import type { EthereumConfig } from '@buildwithsygma/core';
import { Config, Network, ResourceType } from '@buildwithsygma/core';
import { Bridge__factory } from '@buildwithsygma/sygma-contracts';
import { Web3Provider } from '@ethersproject/providers';
Expand All @@ -8,16 +8,14 @@ import type {
ExtractAbiFunction,
ExtractAbiFunctionNames,
} from 'abitype';
import { constants, ethers } from 'ethers';
import type { ethers } from 'ethers';
import { constants } from 'ethers';

import { EvmTransfer } from './evmTransfer.js';
import { getFeeInformation } from './fee/getFeeInformation.js';
import type { GenericMessageTransferParams, TransactionRequest } from './types.js';
import {
createPermissionlessGenericDepositData,
serializeGenericCallParameters,
} from './utils/helpers.js';
import { genericMessageTransfer } from './utils/index.js';
import { createGenericCallDepositData } from './utils/genericTransferHelpers.js';
import { executeDeposit } from './utils/index.js';
import { createTransactionRequest } from './utils/transaction.js';

/**
Expand Down Expand Up @@ -57,43 +55,6 @@ class GenericMessageTransfer<
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<boolean>}
Expand Down Expand Up @@ -166,17 +127,6 @@ class GenericMessageTransfer<
): void {
this.functionParameters = parameters;
}

public setResource(resource: EvmResource | SubstrateResource): void {
if (
resource.type !== ResourceType.PERMISSIONED_GENERIC &&
resource.type !== ResourceType.PERMISSIONLESS_GENERIC
) {
throw new Error('Unsupported Resource type.');
}
this.transferResource = resource;
}

/**
* Get the cross chain generic message transfer
* transaction
Expand All @@ -187,33 +137,39 @@ class GenericMessageTransfer<
const isValid = await this.isValidTransfer();
if (!isValid) throw new Error('Invalid Transfer.');

const { executeFunctionSignature, executionData } = this.prepareFunctionCallEncodings();
const { resourceId } = this.resource;

const executeContractAddress = this.destinationContractAddress;
const sourceDomain = this.config.getDomainConfig(this.source);
const domainId = this.config.getDomainConfig(this.destination).id.toString();
const provider = new Web3Provider(this.sourceNetworkProvider);
const depositor = this.sourceAddress;
const maxFee = this.maxFee.toString();
const bridgeInstance = Bridge__factory.connect(sourceDomain.bridge, provider);
const feeData = await this.getFee();
const depositData = this.getDepositData();

const transaction = await genericMessageTransfer({
executeFunctionSignature,
executeContractAddress,
maxFee,
depositor,
executionData,
bridgeInstance,
domainId,
resourceId,
const transaction = await executeDeposit(
this.destination.id.toString(),
this.resource.resourceId,
depositData,
feeData,
bridgeInstance,
overrides,
});
);

return createTransactionRequest(transaction);
}
/**
* Get prepared additional deposit data
* in hex string format
* @returns {string}
*/
protected getDepositData(): string {
return createGenericCallDepositData({
abi: this.destinationContractAbi,
functionName: this.functionName,
functionParams: this.functionParameters,
contractAddress: this.destinationContractAddress,
destination: this.destination,
maxFee: this.maxFee,
depositor: this.sourceAddress as `0x${string}`,
});
}
}

/**
Expand Down
8 changes: 6 additions & 2 deletions packages/evm/src/nonFungibleAssetTransfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { providers } from 'ethers';

import { AssetTransfer } from './evmAssetTransfer.js';
import type { EvmFee, NonFungibleTransferParams, TransactionRequest } from './types.js';
import { createERCDepositData } from './utils/helpers.js';
import { createFungibleDepositData } from './utils/assetTransferHelpers.js';
import { approve, isApproved } from './utils/index.js';
import { createTransactionRequest } from './utils/transaction.js';

Expand All @@ -31,7 +31,11 @@ class NonFungibleAssetTransfer extends AssetTransfer {
* @returns {string}
*/
protected getDepositData(): string {
return createERCDepositData(BigInt(this.tokenId), this.recipient, this.destination.parachainId);
return createFungibleDepositData({
destination: this.destination,
recipientAddress: this.recipientAddress,
tokenId: this.tokenId,
});
}

/**
Expand Down
17 changes: 17 additions & 0 deletions packages/evm/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,21 @@ export type EvmFee = {
/** An EVM resource is accepted as either the resource object or it's Sygma ID */
export type EvmResourceish = string | EvmResource;

interface FungibleDepositAction {
nativeValue: bigint;
callTo: string;
approveTo: string;
tokenSend: string;
tokenReceive: string;
data: string;
}

export interface FungibleTransferOptionalMessage {
transactionId: string;
actions: FungibleDepositAction[];
receiver: string;
}

export interface EvmTransferParams extends BaseTransferParams {
sourceAddress: string;
sourceNetworkProvider: Eip1193Provider;
Expand All @@ -59,6 +74,8 @@ export interface EvmAssetTransferParams extends EvmTransferParams {
export interface FungibleTransferParams extends EvmAssetTransferParams {
amount: bigint;
securityModel?: SecurityModel;
optionalGas?: bigint;
optionalMessage?: FungibleTransferOptionalMessage;
}

export interface NonFungibleTransferParams extends EvmAssetTransferParams {
Expand Down
Loading

0 comments on commit 2cb56a2

Please sign in to comment.