add: deposit, approve, withdraw for hermez client
MODIFY
src/config.ts
src/config.ts
export const config = {
- abiStoreUrl: 'https://static.matic.network/network/'
+ abiStoreUrl: 'https://static.matic.network/network/',
+ hermezBridgeService: 'https://bridge-api.public.zkevm-test.net/'
};
\ No newline at end of file

ADD
src/hermez/bridge.ts
+import { BaseToken, Web3SideChainClient, Converter, promiseResolve } from "../utils";
+import { IHermezClientConfig, ITransactionOption } from "../interfaces";
+import { TYPE_AMOUNT } from "../types";
+
+export class Bridge extends BaseToken<IHermezClientConfig> {
+
+ networkID_: number;
+
+ constructor(client_: Web3SideChainClient<IHermezClientConfig>, address: string, isParent: boolean) {
+ super({
+ address: address,
+ name: 'Bridge',
+ bridgeType: 'hermez',
+ isParent: isParent
+ }, client_);
+ }
+
+ method(methodName: string, ...args) {
+ return this.getContract().then(contract => {
+ return contract.method(methodName, ...args);
+ });
+ }
+
+ bridgeAsset(
+ token: string,
+ destinationNetwork: number,
+ destinationAddress: string,
+ amount: TYPE_AMOUNT,
+ permitData: string = '0x',
+ option?: ITransactionOption
+ ) {
+ return this.method(
+ "bridgeAsset",
+ token,
+ destinationNetwork,
+ destinationAddress,
+ Converter.toHex(amount),
+ permitData
+ ).then(method => {
+ return this.processWrite(method, option);
+ });
+ }
+
+ claimAsset(
+ smtProof: string[],
+ index: number,
+ mainnetExitRoot: string,
+ rollupExitRoot: string,
+ originNetwork: number,
+ originTokenAddress: string,
+ destinationNetwork: number,
+ destinationAddress: string,
+ amount: TYPE_AMOUNT,
+ metadata: string,
+ option: ITransactionOption
+ ) {
+ return this.method(
+ "claimAsset",
+ smtProof,
+ index,
+ mainnetExitRoot,
+ rollupExitRoot,
+ originNetwork,
+ originTokenAddress,
+ destinationNetwork,
+ destinationAddress,
+ amount,
+ metadata
+ ).then(method => {
+ return this.processWrite(
+ method,
+ option
+ );
+ });
+ }
+
+ getTokenWrappedAddress(
+ originNetwork: number,
+ originTokenAddress: string
+ ) {
+ return this.method(
+ "getTokenWrappedAddress", originNetwork, originTokenAddress
+ ).then(method => {
+ return this.processRead<string>(method);
+ });
+ }
+
+ precalculatedWrapperAddress(
+ originNetwork: number,
+ originTokenAddress: string
+ ) {
+ return this.method(
+ "precalculatedWrapperAddress", originNetwork, originTokenAddress
+ ).then(method => {
+ return this.processRead<string>(method);
+ });
+ }
+
+ isExitProcessed(exitHash: string) {
+ return this.method(
+ "processedExits", exitHash
+ ).then(method => {
+ return this.processRead<boolean>(method);
+ });
+ }
+
+ wrappedTokenToTokenInfo(wrappedToken: string) {
+ return this.method(
+ "wrappedTokenToTokenInfo", wrappedToken
+ ).then(method => {
+ return this.processRead<[number, String]>(method);
+ });
+ }
+
+ networkID() {
+ if (this.networkID_) {
+ return promiseResolve<number>(this.networkID_ as any);
+ }
+ return this.method(
+ "networkID"
+ ).then(method => {
+ return this.processRead<number>(method);
+ });
+ }
+
+}

ADD
src/hermez/bridge_util.ts
+import { Web3SideChainClient } from "../utils";
+import { service } from "../services";
+import { IBaseClientConfig } from "..";
+import { TYPE_AMOUNT } from '../types';
+
+interface IBridgeEventInfo {
+ originNetwork: number,
+ originTokenAddress: string,
+ destinationNetwork: number,
+ destinationAddress: string,
+ amount: TYPE_AMOUNT,
+ metadata: string,
+ depositCount: number
+}
+
+interface IMerkleProof {
+ merkle_proof: string[],
+ exit_root_num: string,
+ l2_exit_root_num: string,
+ main_exit_root: string,
+ rollup_exit_root: string
+}
+
+interface IClaimPayload {
+ smtProof: string[],
+ index: number,
+ mainnetExitRoot: string,
+ rollupExitRoot: string,
+ originNetwork: number,
+ originTokenAddress: string,
+ destinationNetwork: number,
+ destinationAddress: string,
+ amount: TYPE_AMOUNT,
+ metadata: string,
+}
+
+export class BridgeUtil {
+ private client_: Web3SideChainClient<IBaseClientConfig>
+ private BRIDGE_TOPIC: string = "0xf0b963192bdc6349c23af9bd17294b4c7b9b5a73a2a9939610ea18ffd1c5dc2a";
+
+ constructor(client: Web3SideChainClient<IBaseClientConfig>) {
+ this.client_ = client;
+ }
+
+ private decodedBridgeData_(data: string, isParent: boolean) {
+ const client = isParent ? this.client_.parent : this.client_.child;
+ return this.client_.getABI("bridge", "hermez").then(abi => {
+ const types = abi.filter(event => event.name === "BridgeEvent");
+ if (!types.length) {
+ throw new Error("Data not decoded");
+ }
+ const decodedData = client.decodeParameters(data, types[0].inputs);
+ const [originNetwork, originTokenAddress, destinationNetwork, destinationAddress, amount, metadata, depositCount] = decodedData
+ return {
+ originNetwork,
+ originTokenAddress,
+ destinationNetwork,
+ destinationAddress,
+ amount,
+ metadata,
+ depositCount,
+ } as IBridgeEventInfo;
+ })
+ }
+
+ private getBridgeLogData_(transactionHash: string, isParent: boolean) {
+ const client = isParent ? this.client_.parent : this.client_.child;
+ return client.getTransactionReceipt(transactionHash)
+ .then(receipt => {
+ const logs = receipt.logs.filter(log => { log.topics[0].toLowerCase() === this.BRIDGE_TOPIC });
+ if (!logs.length) {
+ throw new Error("Log not found in receipt");
+ }
+
+ const data = logs[0].data;
+ return this.decodedBridgeData_(data, isParent);
+ })
+ }
+
+ private getProof_(networkId: number, depositCount: number) {
+ return service.network.getMerkleProofForHermez(
+ networkId,
+ depositCount,
+ ).then(proof => {
+ return proof as IMerkleProof;
+ }).catch(_ => {
+ throw new Error("Error in creating proof");
+ });
+ }
+
+ buildPayloadForClaim(transactionHash: string, isParent: boolean, networkId: number) {
+ return this.getBridgeLogData_(transactionHash, isParent).then(data => {
+ const {
+ originNetwork,
+ originTokenAddress,
+ destinationNetwork,
+ destinationAddress,
+ amount,
+ metadata,
+ depositCount } = data;
+ return this.getProof_(networkId, depositCount).then(proof => {
+ const payload = {} as IClaimPayload;
+ payload.smtProof = proof.merkle_proof;
+ payload.index = depositCount;
+ payload.mainnetExitRoot = proof.main_exit_root;
+ payload.rollupExitRoot = proof.rollup_exit_root;
+ payload.originNetwork = originNetwork;
+ payload.originTokenAddress = originTokenAddress;
+ payload.destinationNetwork = destinationNetwork;
+ payload.destinationAddress = destinationAddress;
+ payload.amount = amount;
+ payload.metadata = metadata;
+ return payload
+ })
+ })
+ }
+}

ADD
src/hermez/erc20.ts
+import { ITransactionOption } from "../interfaces";
+import { Converter, Web3SideChainClient } from "../utils";
+import { HermezToken } from "./hermez_token";
+import { TYPE_AMOUNT } from "../types";
+import { MAX_AMOUNT } from "..";
+import { IAllowanceTransactionOption, IApproveTransactionOption, IBridgeTransactionOption, IHermezClientConfig, IHermezContracts } from "../interfaces";
+
+export class ERC20 extends HermezToken {
+
+ constructor(
+ tokenAddress: string,
+ isParent: boolean,
+ client: Web3SideChainClient<IHermezClientConfig>,
+ getContracts: () => IHermezContracts
+ ) {
+ super({
+ isParent,
+ address: tokenAddress,
+ name: 'ChildERC20',
+ bridgeType: 'hermez'
+ }, client, getContracts);
+ }
+
+ getBalance(userAddress: string, option?: ITransactionOption) {
+ return this.getContract().then(contract => {
+ const method = contract.method(
+ "balanceOf",
+ userAddress
+ );
+ return this.processRead<string>(method, option);
+ });
+ }
+
+ /**
+ * get allowance of user
+ *
+ * @param {string} userAddress
+ * @param {ITransactionOption} [option]
+ * @returns
+ * @memberof ERC20
+ */
+ getAllowance(userAddress: string, option: IAllowanceTransactionOption = {}) {
+ const spenderAddress = option.spenderAddress ? option.spenderAddress : (
+ this.contractParam.isParent ? this.parentBridge : this.childBridge);
+
+ return this.getContract().then(contract => {
+ const method = contract.method(
+ "allowance",
+ userAddress,
+ spenderAddress,
+ );
+ return this.processRead<string>(method, option);
+ });
+ }
+
+ approve(amount: TYPE_AMOUNT, option: IApproveTransactionOption = {}) {
+ const spenderAddress = option.spenderAddress ? option.spenderAddress : (
+ this.contractParam.isParent ? this.parentBridge : this.childBridge);
+
+ return this.getContract().then(contract => {
+ const method = contract.method(
+ "approve",
+ spenderAddress,
+ Converter.toHex(amount)
+ );
+ return this.processWrite(method, option);
+ });
+ }
+
+ approveMax(option: IApproveTransactionOption = {}) {
+ return this.approve(
+ MAX_AMOUNT,
+ option
+ );
+ }
+
+ /**
+ * Deposit given amount of token for user
+ *
+ * @param {TYPE_AMOUNT} amount
+ * @param {string} userAddress
+ * @param {IBridgeTransactionOption} [option]
+ * @returns
+ * @memberof ERC20
+ */
+ deposit(amount: TYPE_AMOUNT, userAddress: string, option?: IBridgeTransactionOption) {
+ this.checkForRoot("deposit");
+ const permitData = option.permitData || '0x';
+
+ const amountInABI = this.client.parent.encodeParameters(
+ [Converter.toHex(amount)],
+ ['uint256'],
+ );
+
+ return this.childBridge.networkID().then(networkId => {
+ return this.parentBridge.bridgeAsset(
+ this.contractParam.address,
+ networkId,
+ userAddress,
+ amountInABI,
+ permitData,
+ option
+ );
+ })
+ }
+
+ /**
+ * Complete deposit after GlobalExitRootManager is synced from Parent to root
+ *
+ * @param {string} transactionHash
+ * @param {ITransactionOption} [option]
+ * @returns
+ * @memberof ERC20
+ */
+ depositClaim(transactionHash: string, option?: ITransactionOption) {
+ this.checkForChild("depositClaim");
+ return this.parentBridge.networkID().then(networkId => {
+ return this.bridgeUtil.buildPayloadForClaim(
+ transactionHash, true, networkId
+ )
+ }).then(payload => {
+ return this.childBridge.claimAsset(
+ payload.smtProof,
+ payload.index,
+ payload.mainnetExitRoot,
+ payload.rollupExitRoot,
+ payload.originNetwork,
+ payload.originTokenAddress,
+ payload.destinationNetwork,
+ payload.destinationAddress,
+ payload.amount,
+ payload.metadata,
+ option
+ )
+ })
+ }
+
+ /**
+ * initiate withdraw by burning provided amount
+ *
+ * @param {TYPE_AMOUNT} amount
+ * @param {string} userAddress
+ * @param {IBridgeTransactionOption} [option]
+ * @returns
+ * @memberof ERC20
+ */
+ withdraw(amount: TYPE_AMOUNT, userAddress: string, option?: IBridgeTransactionOption) {
+ this.checkForChild("withdraw");
+ const permitData = option.permitData || '0x';
+
+ const amountInABI = this.client.parent.encodeParameters(
+ [Converter.toHex(amount)],
+ ['uint256'],
+ );
+
+ return this.parentBridge.networkID().then(networkId => {
+ return this.childBridge.bridgeAsset(
+ this.contractParam.address,
+ networkId,
+ userAddress,
+ amountInABI,
+ permitData,
+ option
+ );
+ })
+ }
+
+ /**
+ * Complete deposit after GlobalExitRootManager is synced from Parent to root
+ *
+ * @param {string} burnTransactionHash
+ * @param {ITransactionOption} [option]
+ * @returns
+ * @memberof ERC20
+ */
+ exit(burnTransactionHash: string, option?: ITransactionOption) {
+ this.checkForRoot("exit");
+ return this.childBridge.networkID().then(networkId => {
+ return this.bridgeUtil.buildPayloadForClaim(
+ burnTransactionHash, false, networkId
+ )
+ }).then(payload => {
+ return this.parentBridge.claimAsset(
+ payload.smtProof,
+ payload.index,
+ payload.mainnetExitRoot,
+ payload.rollupExitRoot,
+ payload.originNetwork,
+ payload.originTokenAddress,
+ payload.destinationNetwork,
+ payload.destinationAddress,
+ payload.amount,
+ payload.metadata,
+ option
+ )
+ })
+ }
+
+ /**
+ * transfer amount to another user
+ *
+ * @param {TYPE_AMOUNT} amount
+ * @param {string} to
+ * @param {ITransactionOption} [option]
+ * @returns
+ * @memberof ERC20
+ */
+ transfer(amount: TYPE_AMOUNT, to: string, option?: ITransactionOption) {
+ return this.transferERC20(to, amount, option);
+ }
+
+}

ADD
src/hermez/hermez_token.ts
+import { BaseToken, Web3SideChainClient } from "../utils";
+import { IContractInitParam, IHermezClientConfig } from "../interfaces";
+import { IHermezContracts } from "../interfaces";
+
+export class HermezToken extends BaseToken<IHermezClientConfig> {
+
+ constructor(
+ contractParam: IContractInitParam,
+ client: Web3SideChainClient<IHermezClientConfig>,
+ protected getHermezContracts: () => IHermezContracts
+ ) {
+ super(contractParam, client);
+ }
+
+ protected get parentBridge() {
+ return this.getHermezContracts().parentBridge;
+ }
+
+ protected get childBridge() {
+ return this.getHermezContracts().childBridge;
+ }
+
+ protected get bridgeUtil() {
+ return this.getHermezContracts().bridgeUtil;
+ }
+
+}

ADD
src/hermez/index.ts
+import { ERC20 } from "./erc20";
+import { Bridge } from "./bridge";
+import { BridgeUtil } from "./bridge_util";
+import { HermezBridgeClient, setHermezProofApi } from "../utils";
+import { IHermezClientConfig, IHermezContracts } from "../interfaces";
+import { config as urlConfig } from "../config";
+
+export * from "./bridge";
+export * from "./bridge_util";
+
+export class HermezClient extends HermezBridgeClient<IHermezClientConfig> {
+
+ rootChainBridge: Bridge;
+ childChainBridge: Bridge;
+
+ init(config: IHermezClientConfig) {
+ const client = this.client;
+
+ return client.init(config).then(_ => {
+ const mainHermezContracts = client.mainHermezContracts;
+ const hermezContracts = client.hermezContracts;
+ client.config = config = Object.assign(
+ {
+ parentBridge: mainHermezContracts.bridge,
+ childBridge: hermezContracts.bridge,
+ } as IHermezClientConfig,
+ config
+ );
+
+ this.rootChainBridge = new Bridge(
+ this.client,
+ config.parentBridge,
+ true
+ );
+
+ this.childChainBridge = new Bridge(
+ this.client,
+ config.childBridge,
+ false
+ );
+
+ this.bridgeUtil = new BridgeUtil(
+ this.client
+ )
+
+ setHermezProofApi(urlConfig.hermezBridgeService, false)
+
+ return this;
+ });
+ }
+
+ erc20(tokenAddress: string, isParent?: boolean) {
+ return new ERC20(
+ tokenAddress,
+ isParent,
+ this.client,
+ this.getContracts_.bind(this)
+ );
+ }
+
+ private getContracts_() {
+ return {
+ parentBridge: this.rootChainBridge,
+ childBridge: this.childChainBridge,
+ bridgeUtil: this.bridgeUtil
+ } as IHermezContracts;
+ }
+}
\ No newline at end of file

ADD
src/interfaces/bridge_transaction_option.ts
+import { ITransactionOption } from "./transaction_option";
+
+export interface IBridgeTransactionOption extends ITransactionOption {
+
+ /**
+ * address of spender
+ *
+ * **spender** - third-party user or a smart contract which can transfer your token on your behalf.
+ *
+ * @type {string}
+ * @memberof IBridgeTransactionOption
+ */
+ permitData?: string;
+}
\ No newline at end of file

ADD
src/interfaces/hermez_client_config.ts
+import { IBaseClientConfig } from "./base_client_config";
+
+export interface IHermezClientConfig extends IBaseClientConfig {
+ parentBridge?: string;
+ childBridge?: string;
+}
\ No newline at end of file

ADD
src/interfaces/hermez_contracts.ts
+import { Bridge, BridgeUtil } from "../hermez";
+
+export interface IHermezContracts {
+ parentBridge: Bridge;
+ childBridge: Bridge;
+ bridgeUtil: BridgeUtil;
+}
\ No newline at end of file

MODIFY
src/interfaces/index.ts
src/interfaces/index.ts
@@ -20,3 +20,6 @@ export * from "./root_block_info";
export * from "./allowance_transaction_option";
export * from "./approve_transaction_option";
export * from "./exit_transaction_option";
+export * from "./hermez_client_config";
+export * from "./hermez_contracts";
+export * from "./bridge_transaction_option"
\ No newline at end of file

MODIFY
src/services/network_service.ts
src/services/network_service.ts
@@ -8,13 +8,12 @@ export class NetworkService {
this.httpRequest = new HttpRequest(baseUrl);
}
- private createUrl(network: string, url: string) {
+ private createUrlForPos(network: string, url: string) {
return `${network === 'mainnet' ? 'matic' : 'mumbai'}${url}`;
}
getBlockIncluded(network: string, blockNumber: number) {
-
- const url = this.createUrl(network, `/block-included/${blockNumber}`);
+ const url = this.createUrlForPos(network, `/block-included/${blockNumber}`);
return this.httpRequest.get<{
start: string;
end: string;
@@ -30,9 +29,16 @@ export class NetworkService {
}
getProof(network: string, start, end, blockNumber) {
- const url = this.createUrl(network, `/fast-merkle-proof?start=${start}&end=${end}&number=${blockNumber}`);
+ const url = this.createUrlForPos(network, `/fast-merkle-proof?start=${start}&end=${end}&number=${blockNumber}`);
return this.httpRequest.get<any>(url).then(result => {
return result.proof;
});
}
+
+ getMerkleProofForHermez(networkID: number, depositCount: number) {
+ const url = `/merkle-proof?net_id=${networkID}&deposit_cnt=${depositCount}`;
+ return this.httpRequest.get<any>(url).then(result => {
+ return result.proof;
+ })
+ }
}

MODIFY
src/utils/base_token.ts
src/utils/base_token.ts
@@ -17,6 +17,7 @@ export interface ITransactionConfigParam {
export class BaseToken<T_CLIENT_CONFIG> {
private contract_: BaseContract;
+ private chainId_: number;
constructor(
protected contractParam: IContractInitParam,
@@ -43,6 +44,17 @@ export class BaseToken<T_CLIENT_CONFIG> {
});
}
+ getChainId(): Promise<number> {
+ if (this.chainId_) {
+ return promiseResolve<number>(this.chainId_ as any);
+ }
+ const client = this.getClient(this.contractParam.isParent);
+ return client.getChainId().then(chainId => {
+ this.chainId_ = chainId;
+ return this.chainId_;
+ })
+ }
+
protected processWrite(method: BaseContractMethod, option: ITransactionOption = {}): Promise<ITransactionWriteResult> {
this.validateTxOption_(option);
@@ -176,14 +188,17 @@ export class BaseToken<T_CLIENT_CONFIG> {
};
// txConfig.chainId = Converter.toHex(txConfig.chainId) as any;
if (isWrite) {
+ return this.getChainId().then(clientChainId => {
const { maxFeePerGas, maxPriorityFeePerGas } = txConfig;
- const isEIP1559Supported = this.client.isEIP1559Supported(isParent);
+
+ const isEIP1559Supported = this.client.isEIP1559Supported(clientChainId);
const isMaxFeeProvided = (maxFeePerGas || maxPriorityFeePerGas);
+ txConfig.chainId = txConfig.chainId || clientChainId;
if (!isEIP1559Supported && isMaxFeeProvided) {
client.logger.error(ERROR_TYPE.EIP1559NotSupported, isParent).throw();
}
- // const [gasLimit, nonce, chainId] =
+ // const [gasLimit, nonce] =
return Promise.all([
!(txConfig.gasLimit)
? estimateGas({
@@ -192,18 +207,16 @@ export class BaseToken<T_CLIENT_CONFIG> {
: txConfig.gasLimit,
!txConfig.nonce ?
client.getTransactionCount(txConfig.from, 'pending')
- : txConfig.nonce,
- !txConfig.chainId ?
- client.getChainId() : txConfig.chainId
+ : txConfig.nonce
]).then(result => {
- const [gasLimit, nonce, chainId] = result;
+ const [gasLimit, nonce] = result;
client.logger.log("options filled");
txConfig.gasLimit = Number(gasLimit);
txConfig.nonce = nonce;
- txConfig.chainId = chainId;
return txConfig;
});
+ })
}
return promiseResolve<ITransactionRequestConfig>(txConfig);
}

ADD
src/utils/hermez_bridge_client.ts
+import { Web3SideChainClient } from "../utils";
+import { BridgeUtil } from "../hermez";
+
+export class HermezBridgeClient<T> {
+
+ client: Web3SideChainClient<T> = new Web3SideChainClient();
+
+ bridgeUtil: BridgeUtil;
+
+ /**
+ * check whether a txHash is synced with child chain
+ *
+ * @param {string} txHash
+ * @returns
+ * @memberof HermezBridgeClient
+ */
+ isDepositClaimable(txHash: string) {
+ return;
+ }
+
+ /**
+ * check whether proof is submitted on parent chain
+ *
+ * @param {string} txHash
+ * @returns
+ * @memberof HermezBridgeClient
+ */
+ isWithdrawExitable(txHash: string) {
+ return;
+ }
+
+}
\ No newline at end of file

MODIFY
src/utils/index.ts
src/utils/index.ts
@@ -17,6 +17,7 @@ export * from "./promise_resolve";
export * from "./bridge_client";
export * from "./abi_manager";
export * from "./not_implemented";
+export * from "./hermez_bridge_client";
export const utils = {

MODIFY
src/utils/set_proof_api_url.ts
src/utils/set_proof_api_url.ts
@@ -8,3 +8,13 @@ export const setProofApi = (url: string) => {
url += 'api/v1/';
service.network = new NetworkService(url);
};
+
+export const setHermezProofApi = (url: string, override: boolean = true) => {
+ const urlLength = url.length;
+ if (url[urlLength - 1] !== '/') {
+ url += '/';
+ }
+ if (!service.network || override) {
+ service.network = new NetworkService(url);
+ }
+}
\ No newline at end of file

MODIFY
src/utils/web3_side_chain_client.ts
src/utils/web3_side_chain_client.ts
@@ -4,6 +4,14 @@ import { ABIManager } from "../utils";
import { Logger } from "./logger";
import { utils } from "..";
+const chainIdToConfigPath = {
+ 1: 'Main',
+ 5: 'Main',
+ 137: 'Matic',
+ 80001: 'Matic',
+ 1402: 'Hermez'
+}
+
export class Web3SideChainClient<T_CONFIG> {
parent: BaseWeb3Client;
child: BaseWeb3Client;
@@ -63,9 +71,16 @@ export class Web3SideChainClient<T_CONFIG> {
return this.getConfig("Main.POSContracts");
}
- isEIP1559Supported(isParent: boolean): boolean {
- return isParent ? this.getConfig("Main.SupportsEIP1559") :
- this.getConfig("Matic.SupportsEIP1559");
+ get mainHermezContracts() {
+ return this.getConfig("Main.HermezContracts");
+ }
+
+ get hermezContracts() {
+ return this.getConfig("Hermez.Contracts");
+ }
+
+ isEIP1559Supported(chainId: number): boolean {
+ return this.getConfig(`${chainIdToConfigPath[chainId]}.SupportsEIP1559`);
}

add: deposit, approve, withdraw for hermez client