add: permit flow
MODIFY
src/abstracts/base_web3_client.ts
src/abstracts/base_web3_client.ts
@@ -24,6 +24,7 @@ export abstract class BaseWeb3Client {
abstract getBlock(blockHashOrBlockNumber): Promise<IBlock>;
abstract getBlockWithTransaction(blockHashOrBlockNumber): Promise<IBlockWithTransaction>;
+ abstract getAccounts(): Promise<string[]>;
getRootHash?(startBlock: number, endBlock: number) {
return this.sendRPCRequest({
@@ -36,6 +37,17 @@ export abstract class BaseWeb3Client {
});
}
+ signTypedData?(signer: string, typedData: string) {
+ return this.sendRPCRequest({
+ jsonrpc: '2.0',
+ method: 'eth_signTypedData_v3',
+ params: [signer, typedData],
+ id: new Date().getTime()
+ }).then(payload => {
+ return String(payload.result);
+ });
+ }
+
abstract sendRPCRequest(request: IJsonRpcRequestPayload): Promise<IJsonRpcResponse>;
abstract encodeParameters(params: any[], types: any[]): string;

MODIFY
src/constant.ts
src/constant.ts
export const MAX_AMOUNT = '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff';
+export const NULL_ADDRESS = '0x0000000000000000000000000000000000000000'
+export const DAI_PERMIT_TYPEHASH = "0xea2aa0a1be11a07ed86d755c93467f4f82362b452371d1ba94d1715123511acb";
+export const EIP_2612_PERMIT_TYPEHASH = "0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9";
+export const EIP_2612_DOMAIN_TYPEHASH = "0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f";
+export const UNISWAP_DOMAIN_TYPEHASH = "0x8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a866";
+export enum Permit {
+ DAI = "DAI",
+ EIP_2612 = "EIP_2612",
+ UNISWAP = "UNISWAP",
+}
\ No newline at end of file

MODIFY
src/hermez/bridge.ts
src/hermez/bridge.ts
@@ -26,7 +26,7 @@ export class Bridge extends BaseToken<IHermezClientConfig> {
destinationNetwork: number,
destinationAddress: string,
amount: TYPE_AMOUNT,
- permitData: string = '0x',
+ permitData = '0x',
option?: ITransactionOption
) {
return this.method(
@@ -108,7 +108,7 @@ export class Bridge extends BaseToken<IHermezClientConfig> {
return this.method(
"wrappedTokenToTokenInfo", wrappedToken
).then(method => {
- return this.processRead<[number, String]>(method);
+ return this.processRead<[number, string]>(method);
});
}

MODIFY
src/hermez/bridge_util.ts
src/hermez/bridge_util.ts
@@ -4,39 +4,39 @@ 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
+ 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
+ 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,
+ 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";
+ private client_: Web3SideChainClient<IBaseClientConfig>;
+ private BRIDGE_TOPIC = "0xf0b963192bdc6349c23af9bd17294b4c7b9b5a73a2a9939610ea18ffd1c5dc2a";
constructor(client: Web3SideChainClient<IBaseClientConfig>) {
this.client_ = client;
@@ -50,7 +50,7 @@ export class BridgeUtil {
throw new Error("Data not decoded");
}
const decodedData = client.decodeParameters(data, types[0].inputs);
- const [originNetwork, originTokenAddress, destinationNetwork, destinationAddress, amount, metadata, depositCount] = decodedData
+ const [originNetwork, originTokenAddress, destinationNetwork, destinationAddress, amount, metadata, depositCount] = decodedData;
return {
originNetwork,
originTokenAddress,
@@ -60,21 +60,21 @@ export class BridgeUtil {
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 });
+ 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) {
@@ -114,8 +114,8 @@ export class BridgeUtil {
payload.destinationAddress = destinationAddress;
payload.amount = amount;
payload.metadata = metadata;
- return payload
- })
- })
+ return payload;
+ });
+ });
}
}

MODIFY
src/hermez/erc20.ts
src/hermez/erc20.ts
+import utils from 'ethereumjs-util';
import { ITransactionOption } from "../interfaces";
import { Converter, Web3SideChainClient } from "../utils";
import { HermezToken } from "./hermez_token";
import { TYPE_AMOUNT } from "../types";
-import { MAX_AMOUNT } from "..";
+import { BaseContractMethod } from "../abstracts";
+import { MAX_AMOUNT, NULL_ADDRESS, DAI_PERMIT_TYPEHASH, EIP_2612_PERMIT_TYPEHASH, UNISWAP_DOMAIN_TYPEHASH, EIP_2612_DOMAIN_TYPEHASH, Permit, BaseContract } from "..";
import { IAllowanceTransactionOption, IApproveTransactionOption, IBridgeTransactionOption, IHermezClientConfig, IHermezContracts } from "../interfaces";
export class ERC20 extends HermezToken {
@@ -41,7 +43,7 @@ export class ERC20 extends HermezToken {
*/
getAllowance(userAddress: string, option: IAllowanceTransactionOption = {}) {
const spenderAddress = option.spenderAddress ? option.spenderAddress : (
- this.contractParam.isParent ? this.parentBridge : this.childBridge);
+ this.contractParam.isParent ? this.parentBridge.contractAddress : this.childBridge.contractAddress);
return this.getContract().then(contract => {
const method = contract.method(
@@ -55,7 +57,7 @@ export class ERC20 extends HermezToken {
approve(amount: TYPE_AMOUNT, option: IApproveTransactionOption = {}) {
const spenderAddress = option.spenderAddress ? option.spenderAddress : (
- this.contractParam.isParent ? this.parentBridge : this.childBridge);
+ this.contractParam.isParent ? this.parentBridge.contractAddress : this.childBridge.contractAddress);
return this.getContract().then(contract => {
const method = contract.method(
@@ -101,7 +103,38 @@ export class ERC20 extends HermezToken {
permitData,
option
);
- })
+ });
+ }
+
+ /**
+ * Deposit given amount of token for user with permit call
+ *
+ * @param {TYPE_AMOUNT} amount
+ * @param {string} userAddress
+ * @param {IBridgeTransactionOption} [option]
+ * @returns
+ * @memberof ERC20
+ */
+ depositWithPermit(amount: TYPE_AMOUNT, userAddress: string, option?: IBridgeTransactionOption) {
+ this.checkForRoot("deposit");
+
+ const amountInABI = this.client.parent.encodeParameters(
+ [Converter.toHex(amount)],
+ ['uint256'],
+ );
+
+ return this.getPermitData().then(permitData => {
+ return this.childBridge.networkID().then(networkId => {
+ return this.parentBridge.bridgeAsset(
+ this.contractParam.address,
+ networkId,
+ userAddress,
+ amountInABI,
+ permitData,
+ option
+ );
+ });
+ });
}
/**
@@ -117,7 +150,7 @@ export class ERC20 extends HermezToken {
return this.parentBridge.networkID().then(networkId => {
return this.bridgeUtil.buildPayloadForClaim(
transactionHash, true, networkId
- )
+ );
}).then(payload => {
return this.childBridge.claimAsset(
payload.smtProof,
@@ -131,8 +164,8 @@ export class ERC20 extends HermezToken {
payload.amount,
payload.metadata,
option
- )
- })
+ );
+ });
}
/**
@@ -162,7 +195,38 @@ export class ERC20 extends HermezToken {
permitData,
option
);
- })
+ });
+ }
+
+ /**
+ * initiate withdraw by transferring amount with PermitData for native tokens
+ *
+ * @param {TYPE_AMOUNT} amount
+ * @param {string} userAddress
+ * @param {IBridgeTransactionOption} [option]
+ * @returns
+ * @memberof ERC20
+ */
+ withdrawWithPermit(amount: TYPE_AMOUNT, userAddress: string, option?: IBridgeTransactionOption) {
+ this.checkForChild("withdraw");
+
+ const amountInABI = this.client.parent.encodeParameters(
+ [Converter.toHex(amount)],
+ ['uint256'],
+ );
+
+ return this.getPermitData().then(permitData => {
+ return this.parentBridge.networkID().then(networkId => {
+ return this.childBridge.bridgeAsset(
+ this.contractParam.address,
+ networkId,
+ userAddress,
+ amountInABI,
+ permitData,
+ option
+ );
+ });
+ });
}
/**
@@ -178,7 +242,7 @@ export class ERC20 extends HermezToken {
return this.childBridge.networkID().then(networkId => {
return this.bridgeUtil.buildPayloadForClaim(
burnTransactionHash, false, networkId
- )
+ );
}).then(payload => {
return this.parentBridge.claimAsset(
payload.smtProof,
@@ -192,8 +256,8 @@ export class ERC20 extends HermezToken {
payload.amount,
payload.metadata,
option
- )
- })
+ );
+ });
}
/**
@@ -209,4 +273,206 @@ export class ERC20 extends HermezToken {
return this.transferERC20(to, amount, option);
}
+ getPermitData(option: IApproveTransactionOption = {}) {
+ const spenderAddress = option.spenderAddress ? option.spenderAddress : (
+ this.contractParam.isParent ? this.parentBridge.contractAddress : this.childBridge.contractAddress);
+
+ return this.getPermitData_(spenderAddress);
+ }
+
+ private getPermit() {
+ let contract: BaseContract;
+ return this.getContract().then(contractInstance => {
+ contract = contractInstance;
+ const method = contract.method(
+ "PERMIT_TYPEHASH",
+ );
+ return this.processRead<string>(method);
+ }).then(permitTypehash => {
+ switch (permitTypehash) {
+ case DAI_PERMIT_TYPEHASH: {
+ return Permit.DAI;
+ }
+ case EIP_2612_PERMIT_TYPEHASH: {
+ const DOMAIN_TYPEHASH = contract.method("DOMAIN_TYPEHASH");
+ const EIP712DOMAIN_HASH = contract.method("EIP712DOMAIN_HASH");
+ return Promise.any([this.processRead<string>(DOMAIN_TYPEHASH), this.processRead<string>(EIP712DOMAIN_HASH)]).then(
+ (domainTypehash) => {
+ switch (domainTypehash) {
+ case EIP_2612_DOMAIN_TYPEHASH: {
+ return Permit.EIP_2612;
+ }
+ case UNISWAP_DOMAIN_TYPEHASH: {
+ return Permit.UNISWAP;
+ }
+ default: {
+ return Promise.reject(new Error(`Unsupported domain typehash: ${domainTypehash}`));
+ }
+ }
+ }
+ );
+ }
+ default: {
+ return Promise.reject(new Error(`Unsupported permit typehash: ${permitTypehash}`));
+ }
+ }
+ });
+ }
+
+ private getTypedData_(permitType: string, account: string, chainId: number, name: string, nonce: string, spenderAddress: string) {
+ const typedData = {
+ types: {
+ EIP712Domain: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ {
+ name: 'version',
+ type: 'string',
+ },
+ {
+ name: 'verifyingContract',
+ type: 'address',
+ },
+ {
+ name: 'salt',
+ type: 'bytes32',
+ },
+ ],
+ Permit: []
+ },
+ primaryType: "Permit",
+ domain: {
+ name,
+ version: '1',
+ verifyingContract: this.contractAddress,
+ salt: Converter.toBytes(chainId)
+ },
+ message: {}
+ };
+ switch (permitType) {
+ case Permit.DAI:
+ typedData.types.Permit = [
+ { name: "holder", type: "address" },
+ { name: "spender", type: "address" },
+ { name: "nonce", type: "uint256" },
+ { name: "expiry", type: "uint256" },
+ { name: "allowed", type: "bool" },
+ ];
+ typedData.message = {
+ holder: account,
+ spender: spenderAddress,
+ nonce,
+ expiry: MAX_AMOUNT,
+ allowed: true,
+ };
+ case Permit.EIP_2612:
+ case Permit.UNISWAP:
+ typedData.types.Permit = [
+ { name: "owner", type: "address" },
+ { name: "spender", type: "address" },
+ { name: "value", type: "uint256" },
+ { name: "nonce", type: "uint256" },
+ { name: "deadline", type: "uint256" },
+ ];
+ typedData.message = {
+ owner: account,
+ spender: spenderAddress,
+ value: MAX_AMOUNT,
+ nonce,
+ deadline: MAX_AMOUNT,
+ };
+ }
+ return typedData;
+ }
+
+ private getSignatureParameters_(signature: string) {
+ if (!utils.isHexString(signature)) {
+ throw new Error(
+ 'Given value "'.concat(signature, '" is not a valid hex string.'),
+ );
+ }
+
+ if (signature.slice(0, 2) !== '0x') {
+ signature = '0x'.concat(signature);
+ }
+
+ const r = signature.slice(0, 66);
+ const s = '0x'.concat(signature.slice(66, 130));
+ let v = Converter.hexToNumber('0x'.concat(signature.slice(130, 132)));
+ if (![27, 28].includes(v as any)) {
+ v += 27;
+ }
+ return {
+ r: r,
+ s: s,
+ v: v,
+ };
+ }
+
+ private encodePermitFunctionData_(contract: BaseContract, permitType: string, signatureParams: any, spenderAddress: string, account: string) {
+ const { r, s, v } = signatureParams;
+ let method: BaseContractMethod;
+ switch (permitType) {
+ case Permit.DAI:
+ method = contract.method(
+ "permit",
+ account,
+ spenderAddress,
+ MAX_AMOUNT,
+ 1,
+ v,
+ r,
+ s,
+ );
+
+ case Permit.EIP_2612:
+ case Permit.UNISWAP:
+ method = contract.method(
+ "permit",
+ account,
+ spenderAddress,
+ MAX_AMOUNT,
+ 1,
+ v,
+ r,
+ s,
+ );
+ }
+ return method.encodeABI();
+ }
+
+ private getPermitData_(spenderAddress: string) {
+ if (this.contractParam.address === NULL_ADDRESS) {
+ throw Error("Native currency does't require permit");
+ }
+
+ const client = this.contractParam.isParent ? this.client.parent : this.client.child;
+ let account: string;
+ let chainId: number;
+ let permitType: string;
+ let contract: BaseContract;
+ return Promise.all([client.getAccounts(), this.getContract(), client.getChainId(), this.getPermit()]).then(result => {
+ account = result[0][0];
+ contract = result[1];
+ chainId = result[2];
+ permitType = result[3];
+ const nameMethod = contract.method("name");
+ const nonceMethod = contract.method("nonce", account);
+ return Promise.all([this.processRead<string>(nameMethod), this.processRead<string>(nonceMethod)]);
+ }).then(data => {
+ const name = data[0];
+ const nonce = data[1];
+ return this.getTypedData_(permitType, account, chainId, name, nonce, spenderAddress);
+ }).then(typedData => {
+ return client.signTypedData(account, JSON.stringify(typedData));
+ }).then(signature => {
+ const signatureParameters = this.getSignatureParameters_(signature);
+ return this.encodePermitFunctionData_(
+ contract, permitType, signatureParameters, spenderAddress, account
+ );
+ });
+ }
+
}

MODIFY
src/hermez/index.ts
src/hermez/index.ts
@@ -38,9 +38,9 @@ export class HermezClient extends HermezBridgeClient<IHermezClientConfig> {
this.bridgeUtil = new BridgeUtil(
this.client
- )
+ );
- setHermezProofApi(urlConfig.hermezBridgeService, false)
+ setHermezProofApi(urlConfig.hermezBridgeService, false);
return this;
});

MODIFY
src/index.ts
src/index.ts
@@ -8,6 +8,7 @@ export * from "./types";
export * from "./constant";
export * from "./abstracts";
export * from "./services";
+export * from "./hermez";
export default defaultExport;

MODIFY
src/interfaces/index.ts
src/interfaces/index.ts
@@ -22,4 +22,4 @@ 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
+export * from "./bridge_transaction_option";
\ No newline at end of file

MODIFY
src/services/network_service.ts
src/services/network_service.ts
@@ -39,13 +39,13 @@ export class NetworkService {
const url = `/merkle-proof?net_id=${networkID}&deposit_cnt=${depositCount}`;
return this.httpRequest.get<any>(url).then(result => {
return result.proof;
- })
+ });
}
getBridgeTransactionDetails(networkID: number, depositCount: number) {
const url = `/bridge?net_id=${networkID}&deposit_cnt=${depositCount}`;
return this.httpRequest.get<any>(url).then(result => {
return result.deposit;
- })
+ });
}
}

MODIFY
src/tsconfig.json
src/tsconfig.json
"module": "ESNext",
"outDir": "../dist/ts",
"lib": [
- "es2015",
+ "es2021",
"dom",
"webworker",
"ES5",

MODIFY
src/utils/base_token.ts
src/utils/base_token.ts
@@ -25,6 +25,9 @@ export class BaseToken<T_CLIENT_CONFIG> {
) {
}
+ get contractAddress() {
+ return this.contractParam.address;
+ }
getContract(): Promise<BaseContract> {
if (this.contract_) {
@@ -52,7 +55,7 @@ export class BaseToken<T_CLIENT_CONFIG> {
return client.getChainId().then(chainId => {
this.chainId_ = chainId;
return this.chainId_;
- })
+ });
}
protected processWrite(method: BaseContractMethod, option: ITransactionOption = {}): Promise<ITransactionWriteResult> {
@@ -216,7 +219,7 @@ export class BaseToken<T_CLIENT_CONFIG> {
txConfig.nonce = nonce;
return txConfig;
});
- })
+ });
}
return promiseResolve<ITransactionRequestConfig>(txConfig);
}

MODIFY
src/utils/converter.ts
src/utils/converter.ts
@@ -19,4 +19,14 @@ export class Converter {
throw new Error(`Invalid value ${amount}, value is not a number.`);
}
}
+
+ static toBytes(amount: BaseBigNumber | string | number) {
+ const hexString = this.toHex(amount) as string;
+ hexString.slice(2).padStart(32, "0");
+ return '0x' + hexString;
+ }
+
+ static hexToNumber(hexString: string) {
+ return (new utils.BN(hexString.slice(2))).toNumber();
+ }
}
\ No newline at end of file

MODIFY
src/utils/hermez_bridge_client.ts
src/utils/hermez_bridge_client.ts
@@ -20,10 +20,10 @@ export class HermezBridgeClient<T> {
return Promise.all([this.rootChainBridge.networkID(), this.bridgeUtil.getBridgeLogData(
txHash, true
)]).then(result => {
- return service.network.getBridgeTransactionDetails(result[0], result[1].depositCount)
+ return service.network.getBridgeTransactionDetails(result[0], result[1].depositCount);
}).then(details => {
return details.ready_for_claim;
- })
+ });
}
/**
@@ -37,10 +37,10 @@ export class HermezBridgeClient<T> {
return Promise.all([this.childChainBridge.networkID(), this.bridgeUtil.getBridgeLogData(
txHash, false
)]).then(result => {
- return service.network.getBridgeTransactionDetails(result[0], result[1].depositCount)
+ return service.network.getBridgeTransactionDetails(result[0], result[1].depositCount);
}).then(details => {
return details.ready_for_claim;
- })
+ });
}
}
\ No newline at end of file

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

MODIFY
src/utils/web3_side_chain_client.ts
src/utils/web3_side_chain_client.ts
@@ -10,7 +10,7 @@ const chainIdToConfigPath = {
137: 'Matic',
80001: 'Matic',
1402: 'Hermez'
-}
+};
export class Web3SideChainClient<T_CONFIG> {
parent: BaseWeb3Client;

MODIFY
test/tsconfig.json
test/tsconfig.json
{
"compileOnSave": true,
"compilerOptions": {
- "lib": ["es2015", "dom"],
+ "lib": ["es2021", "dom"],
"target": "es5",
"rootDir": "test",
"resolveJsonModule": true,

add: permit flow