diff --git a/.github/workflows/npm.publish.yml b/.github/workflows/npm.publish.yml new file mode 100644 index 0000000..ec27bc6 --- /dev/null +++ b/.github/workflows/npm.publish.yml @@ -0,0 +1,36 @@ +name: Publish NPM package +on: + workflow_dispatch: + inputs: + ref: + description: "ref or tag to publish NPM package from" + default: "" + required: false + tag: + required: true + type: choice + description: package tag + default: latest + options: + - latest + - next + - beta +jobs: + zksync: + name: Publish JavaScript SDK + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + - uses: actions/setup-node@v3 + with: + node-version: 18 + registry-url: 'https://registry.npmjs.org' + - name: Build package and publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + npm install + npm run build + npm publish --tag ${{ inputs.tag }} diff --git a/src/adapters.ts b/src/adapters.ts index 1e41ca8..cb9e332 100644 --- a/src/adapters.ts +++ b/src/adapters.ts @@ -19,12 +19,12 @@ import { undoL1ToL2Alias } from './utils'; import { - IERC20__factory, - IL1Bridge__factory, - IL2Bridge__factory, + IERC20__factory, IL1Bridge, + IL1Bridge__factory, IL2Bridge, + IL2Bridge__factory, IZkSync, IZkSync__factory } from '../typechain'; -import {Address, BalancesMap, Eip712Meta, FullDepositFee, Log, PriorityOpResponse, TransactionResponse} from './types'; +import {Address, BalancesMap, Eip712Meta, FullDepositFee, PriorityOpResponse, TransactionResponse} from './types'; type Constructor = new (...args: any[]) => T; @@ -48,12 +48,12 @@ export function AdapterL1>(Base: TBase) { throw new Error('Must be implemented by the derived class!'); } - async getMainContract() { + async getMainContract(): Promise { const address = await this._providerL2().getMainContractAddress(); return IZkSync__factory.connect(address, this._signerL1()); } - async getL1BridgeContracts() { + async getL1BridgeContracts(): Promise<{erc20: IL1Bridge}> { const addresses = await this._providerL2().getDefaultBridgeAddresses(); return { erc20: IL1Bridge__factory.connect(addresses.erc20L1, this._signerL1()) @@ -80,7 +80,7 @@ export function AdapterL1>(Base: TBase) { return await erc20contract.allowance(await this.getAddress(), bridgeAddress, { blockTag }); } - async l2TokenAddress(token: Address) { + async l2TokenAddress(token: Address): Promise { if (token == ETH_ADDRESS) { return ETH_ADDRESS; } else { @@ -322,7 +322,7 @@ export function AdapterL1>(Base: TBase) { tx.to ??= await this.getAddress(); tx.gasPerPubdataByte ??= REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_LIMIT; - let l2GasLimit = 0n; + let l2GasLimit = null; if (tx.bridgeAddress != null) { const customBridgeData = tx.customBridgeData ?? (await getERC20DefaultBridgeData(tx.token, this._providerL1())); @@ -461,7 +461,7 @@ export function AdapterL1>(Base: TBase) { }; } - async finalizeWithdrawal(withdrawalHash: BytesLike, index: number = 0, overrides?: ethers.Overrides) { + async finalizeWithdrawal(withdrawalHash: BytesLike, index: number = 0, overrides?: ethers.Overrides): Promise { const {l1BatchNumber, l2MessageIndex, l2TxNumberInBlock, message, sender, proof} = await this.finalizeWithdrawalParams(withdrawalHash, index); @@ -655,7 +655,7 @@ export function AdapterL2>(Base: TBase) { throw new Error('Must be implemented by the derived class!'); } - async getBalance(token?: Address, blockTag: BlockTag = 'committed') { + async getBalance(token?: Address, blockTag: BlockTag = 'committed'): Promise { return await this._providerL2().getBalance(await this.getAddress(), blockTag, token); } @@ -663,7 +663,7 @@ export function AdapterL2>(Base: TBase) { return await this._providerL2().getAllAccountBalances(await this.getAddress()); } - async getL2BridgeContracts() { + async getL2BridgeContracts(): Promise<{erc20: IL2Bridge}> { const addresses = await this._providerL2().getDefaultBridgeAddresses(); return { erc20: IL2Bridge__factory.connect(addresses.erc20L2, this._signerL2()) diff --git a/src/format.ts b/src/format.ts index 598cd36..1b36349 100644 --- a/src/format.ts +++ b/src/format.ts @@ -208,7 +208,7 @@ const _formatTransactionReceipt = object( type: allowNull(getNumber, 0), l1BatchNumber: allowNull(getNumber), l1BatchTxIndex: allowNull(getNumber), - l2ToL1Logs: arrayOf(formatL2ToL1Log) + l2ToL1Logs: allowNull(arrayOf(formatL2ToL1Log), []) }, { effectiveGasPrice: ['gasPrice'], diff --git a/src/provider.ts b/src/provider.ts index 4fa5990..6ecf351 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -12,7 +12,8 @@ import { Eip1193Provider, JsonRpcError, JsonRpcResult, - JsonRpcPayload + JsonRpcPayload, + resolveProperties } from 'ethers'; import { IERC20__factory, IEthToken__factory, IL2Bridge__factory } from '../typechain'; import { @@ -30,7 +31,10 @@ import { TransactionDetails, BlockDetails, ContractAccountInfo, - Network as ZkSyncNetwork, BatchDetails, Fee + Network as ZkSyncNetwork, + BatchDetails, + Fee, + Transaction } from './types'; import { isETH, @@ -109,7 +113,7 @@ export class Provider extends ethers.JsonRpcProvider { } } - async l2TokenAddress(token: Address) { + async l2TokenAddress(token: Address): Promise { if (token == ETH_ADDRESS) { return ETH_ADDRESS; } else { @@ -119,7 +123,7 @@ export class Provider extends ethers.JsonRpcProvider { } } - async l1TokenAddress(token: Address) { + async l1TokenAddress(token: Address): Promise { if (token == ETH_ADDRESS) { return ETH_ADDRESS; } else { @@ -148,15 +152,6 @@ export class Provider extends ethers.JsonRpcProvider { this.contractAddresses = {}; } - async getMessageProof( - blockNumber: number, - sender: Address, - messageHash: BytesLike, - logIndex?: number - ): Promise { - return await this.send('zks_getL2ToL1MsgProof', [blockNumber, sender, ethers.hexlify(messageHash), logIndex]); - } - async getLogProof(txHash: BytesLike, index?: number): Promise { return await this.send('zks_getL2ToL1LogProof', [ethers.hexlify(txHash), index]); } @@ -199,10 +194,6 @@ export class Provider extends ethers.JsonRpcProvider { return tokens.map((token) => ({ address: token.l2Address, ...token })); } - async getTokenPrice(token: Address): Promise { - return await this.send('zks_getTokenPrice', [token]); - } - async getAllAccountBalances(address: Address): Promise { let balances = await this.send('zks_getAllAccountBalances', [address]); for (let token in balances) { @@ -359,7 +350,7 @@ export class Provider extends ethers.JsonRpcProvider { } // This is inefficient. Status should probably be indicated in the transaction receipt. - async getTransactionStatus(txHash: string) { + async getTransactionStatus(txHash: string): Promise { const tx = await this.getTransaction(txHash); if (tx == null) { return TransactionStatus.NotFound; @@ -375,7 +366,21 @@ export class Provider extends ethers.JsonRpcProvider { } override async broadcastTransaction(signedTx: string): Promise { - return (await super.broadcastTransaction(signedTx)) as TransactionResponse; + const { blockNumber, hash, network } = await resolveProperties({ + blockNumber: this.getBlockNumber(), + hash: this._perform({ + method: "broadcastTransaction", + signedTransaction: signedTx + }), + network: this.getNetwork() + }); + + const tx = Transaction.from(signedTx); + if (tx.hash !== hash) { + throw new Error("@TODO: the returned hash did not match"); + } + + return this._wrapTransactionResponse(tx, network).replaceableTransaction(blockNumber); } async getL2TransactionFromPriorityOp(l1TxResponse: ethers.TransactionResponse): Promise { @@ -563,10 +568,10 @@ export class BrowserProvider extends Provider { } } - return Signer.from((await super.getSigner(address)) as any); + return Signer.from((await super.getSigner(address)) as any, Number((await this.getNetwork()).chainId)); } - override async estimateGas(transaction: TransactionRequest) { + override async estimateGas(transaction: TransactionRequest): Promise { const gas = await super.estimateGas(transaction); const metamaskMinimum = 21000n; const isEIP712 = transaction.customData != null || transaction.type == EIP712_TX_TYPE; diff --git a/src/signer.ts b/src/signer.ts index e8b3209..8b274eb 100644 --- a/src/signer.ts +++ b/src/signer.ts @@ -92,10 +92,9 @@ export class Signer extends AdapterL2(ethers.JsonRpcSigner) { return this.provider; } - static from(signer: ethers.JsonRpcSigner & { provider: Provider }): Signer { + static from(signer: ethers.JsonRpcSigner & { provider: Provider }, chainId: number): Signer { const newSigner: Signer = Object.setPrototypeOf(signer, Signer.prototype); - // @ts-ignore - newSigner.eip712 = new EIP712Signer(newSigner, newSigner.getChainId()); + newSigner.eip712 = new EIP712Signer(newSigner, chainId); return newSigner; } @@ -177,10 +176,9 @@ export class L2VoidSigner extends AdapterL2(ethers.VoidSigner) { return this.provider; } - static from(signer: ethers.VoidSigner & { provider: Provider }): L2VoidSigner { + static from(signer: ethers.VoidSigner & { provider: Provider }, chainId: number): L2VoidSigner { const newSigner: L2VoidSigner = Object.setPrototypeOf(signer, L2VoidSigner.prototype); - // @ts-ignore - newSigner.eip712 = new EIP712Signer(newSigner, newSigner.getChainId()); + newSigner.eip712 = new EIP712Signer(newSigner, chainId); return newSigner; } diff --git a/src/types.ts b/src/types.ts index 8fbf426..1b469e5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,12 @@ -import { BytesLike, BigNumberish, ethers, TransactionRequest as EthersTransactionRequest } from 'ethers'; -import {serializeEip712, EIP712_TX_TYPE, parseEip712, sleep} from './utils'; +import { + assertArgument, + BigNumberish, + BytesLike, + ethers, + Signature as EthersSignature, + TransactionRequest as EthersTransactionRequest +} from 'ethers'; +import {EIP712_TX_TYPE, parseEip712, serializeEip712, sleep, eip712TxHash} from './utils'; // 0x-prefixed, hex encoded, ethereum account address export type Address = string; @@ -90,7 +97,12 @@ export class TransactionResponse extends ethers.TransactionResponse { } override async wait(confirmations?: number): Promise { - return (await super.wait(confirmations)) as TransactionReceipt; + while (true) { + const receipt = (await super.wait(confirmations)) as TransactionReceipt; + if (receipt.blockNumber) { + return receipt; + } + } } override async getTransaction(): Promise { @@ -108,9 +120,11 @@ export class TransactionResponse extends ethers.TransactionResponse { async waitFinalize(): Promise { while (true) { const receipt = await this.wait(); - const block = await this.provider.getBlock('finalized'); - if (receipt.blockNumber && receipt.blockNumber <= block!.number) { - return (await this.provider.getTransactionReceipt(receipt.hash)) as TransactionReceipt; + if (receipt.blockNumber) { + const block = await this.provider.getBlock('finalized'); + if (receipt.blockNumber <= block!.number) { + return (await this.provider.getTransactionReceipt(receipt.hash)) as TransactionReceipt; + } } else { await sleep(500); } @@ -118,7 +132,7 @@ export class TransactionResponse extends ethers.TransactionResponse { } override toJSON(): any { - const { l1BatchNumber, l1BatchTxIndex } = this; + const {l1BatchNumber, l1BatchTxIndex} = this; return { ...super.toJSON(), @@ -157,7 +171,7 @@ export class TransactionReceipt extends ethers.TransactionReceipt { } override toJSON(): any { - const { l1BatchNumber, l1BatchTxIndex, l2ToL1Logs } = this; + const {l1BatchNumber, l1BatchTxIndex, l2ToL1Logs} = this; return { ...super.toJSON(), l1BatchNumber, @@ -178,7 +192,7 @@ export class Block extends ethers.Block { } override toJSON(): any { - const { l1BatchNumber, l1BatchTimestamp: l1BatchTxIndex } = this; + const {l1BatchNumber, l1BatchTimestamp: l1BatchTxIndex} = this; return { ...super.toJSON(), l1BatchNumber, @@ -204,7 +218,7 @@ export class Log extends ethers.Log { } override toJSON(): any { - const { l1BatchNumber } = this; + const {l1BatchNumber} = this; return { ...super.toJSON(), l1BatchNumber @@ -230,24 +244,71 @@ export interface TransactionLike extends ethers.TransactionLike { export class Transaction extends ethers.Transaction { customData: null | Eip712Meta; + // super.#type is private and there is no way to override which enforced to + // introduce following variable + #type: number | null + #from: string | null + + override get type(): number | null { + return this.#type == EIP712_TX_TYPE ? this.#type : super.type; + } + + override set type(value: number | string | null) { + switch (value) { + case EIP712_TX_TYPE: + case "eip-712": + this.#type = EIP712_TX_TYPE; + break; + default: + super.type = value; + } + } + - static override from(value: string | TransactionLike): Transaction { - if (typeof value === 'string') { - const payload = ethers.getBytes(value); + static override from(tx: string | TransactionLike): Transaction { + if (typeof tx === 'string') { + const payload = ethers.getBytes(tx); if (payload[0] !== EIP712_TX_TYPE) { - return Transaction.from(ethers.Transaction.from(value)); + return Transaction.from(ethers.Transaction.from(tx)); } else { return Transaction.from(parseEip712(payload)); } } else { - const tx = ethers.Transaction.from(value) as Transaction; - tx.customData = value?.customData ?? null; - return tx; + const result = new Transaction(); + if (tx.type === EIP712_TX_TYPE) { + result.type = EIP712_TX_TYPE; + result.customData = tx.customData; + result.from = tx.from; + } + if (tx.type != null) result.type = tx.type; + if (tx.to != null) result.to = tx.to; + if (tx.nonce != null) result.nonce = tx.nonce; + if (tx.gasLimit != null) result.gasLimit = tx.gasLimit; + if (tx.gasPrice != null) result.gasPrice = tx.gasPrice; + if (tx.maxPriorityFeePerGas != null) result.maxPriorityFeePerGas = tx.maxPriorityFeePerGas; + if (tx.maxFeePerGas != null) result.maxFeePerGas = tx.maxFeePerGas; + if (tx.data != null) result.data = tx.data; + if (tx.value != null) result.value = tx.value; + if (tx.chainId != null) result.chainId = tx.chainId; + if (tx.signature != null) result.signature = EthersSignature.from(tx.signature); + if (tx.accessList != null) result.accessList = tx.accessList; + + if (tx.from != null) { + assertArgument(result.isSigned(), "unsigned transaction cannot define from", "tx", tx); + assertArgument(result.from.toLowerCase() === (tx.from || "").toLowerCase(), "from mismatch", "tx", tx); + } + + if (tx.hash != null) { + assertArgument(result.isSigned(), "unsigned transaction cannot define hash", "tx", tx); + assertArgument(result.hash === tx.hash, "hash mismatch", "tx", tx); + } + + return result; } } override get serialized(): string { - if (this.customData == null && this.type != EIP712_TX_TYPE) { + if (this.customData == null && this.#type != EIP712_TX_TYPE) { return super.serialized; } return serializeEip712(this, this.signature); @@ -261,20 +322,36 @@ export class Transaction extends ethers.Transaction { } override toJSON(): any { - const { customData } = this; + const {customData} = this; return { ...super.toJSON(), + type: this.#type == null ? this.type : this.#type, customData }; } override get typeName(): string { - if (this.type === EIP712_TX_TYPE) { - return 'zksync'; + return this.#type === EIP712_TX_TYPE ? 'zksync' : super.typeName; + } + + override isSigned(): this is Transaction & { type: number; typeName: string; from: string; signature: Signature } { + return this.#type === EIP712_TX_TYPE ? this.customData?.customSignature !== null : super.isSigned(); + } + + override get hash(): string | null { + if (this.#type === EIP712_TX_TYPE) { + return this.customData?.customSignature !== null ? eip712TxHash(this) : null; } else { - return super.typeName; + return super.hash; } } + + override get from(): string | null { + return this.#type === EIP712_TX_TYPE ? this.#from : super.from; + } + override set from(value: string | null) { + this.#from = value; + } } export interface L2ToL1Log { @@ -362,6 +439,7 @@ export interface BatchDetails { export interface BlockDetails { number: number; timestamp: number; + l1BatchNumber: number; l1TxCount: number; l2TxCount: number; rootHash?: string; diff --git a/src/utils.ts b/src/utils.ts index 3e6b83e..5e8d53a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -152,9 +152,7 @@ export function serializeEip712(transaction: TransactionLike, signature?: ethers throw new Error('Explicitly providing `from` field is reqiured for EIP712 transactions'); } const from = transaction.from; - const meta: Eip712Meta = transaction.customData; - let maxFeePerGas = transaction.maxFeePerGas || transaction.gasPrice || 0; let maxPriorityFeePerGas = transaction.maxPriorityFeePerGas || maxFeePerGas; @@ -328,7 +326,7 @@ function getSignature(transaction: any, ethSignature?: EthereumSignature): Uint8 return new Uint8Array([...r, ...s, v]); } -function eip712TxHash(transaction: any, ethSignature?: EthereumSignature) { +export function eip712TxHash(transaction: any, ethSignature?: EthereumSignature) { const signedDigest = EIP712Signer.getSignedDigest(transaction); const hashedSignature = ethers.keccak256(getSignature(transaction, ethSignature)); diff --git a/src/wallet.ts b/src/wallet.ts index 848b72c..1fc21f5 100644 --- a/src/wallet.ts +++ b/src/wallet.ts @@ -1,9 +1,9 @@ -import { EIP712Signer } from './signer'; -import { Provider } from './provider'; -import { EIP712_TX_TYPE, serializeEip712 } from './utils'; -import { ethers, ProgressCallback } from 'ethers'; -import { TransactionResponse, TransactionRequest, Transaction, TransactionLike } from './types'; -import { AdapterL1, AdapterL2 } from './adapters'; +import {EIP712Signer} from './signer'; +import {Provider} from './provider'; +import {EIP712_TX_TYPE, serializeEip712} from './utils'; +import {ethers, ProgressCallback} from 'ethers'; +import {Transaction, TransactionLike, TransactionRequest, TransactionResponse} from './types'; +import {AdapterL1, AdapterL2} from './adapters'; export class Wallet extends AdapterL2(AdapterL1(ethers.Wallet)) { override readonly provider: Provider; @@ -29,15 +29,15 @@ export class Wallet extends AdapterL2(AdapterL1(ethers.Wallet)) { return this; } - ethWallet() { + ethWallet(): ethers.Wallet { return new ethers.Wallet(this.signingKey, this._providerL1()); } - override connect(provider: Provider) { + override connect(provider: Provider): Wallet { return new Wallet(this.signingKey, provider, this.providerL1); } - connectToL1(provider: ethers.Provider) { + connectToL1(provider: ethers.Provider): Wallet { return new Wallet(this.signingKey, this.provider, provider); } @@ -46,12 +46,12 @@ export class Wallet extends AdapterL2(AdapterL1(ethers.Wallet)) { return new Wallet(wallet.signingKey, null, wallet.provider); } - static override async fromEncryptedJson(json: string, password: string | Uint8Array, callback?: ProgressCallback) { + static override async fromEncryptedJson(json: string, password: string | Uint8Array, callback?: ProgressCallback): Promise { const wallet = await super.fromEncryptedJson(json, password, callback); return new Wallet(wallet.signingKey); } - static override fromEncryptedJsonSync(json: string, password: string | Uint8Array) { + static override fromEncryptedJsonSync(json: string, password: string | Uint8Array): Wallet { const wallet = super.fromEncryptedJsonSync(json, password); return new Wallet(wallet.signingKey); } @@ -74,10 +74,11 @@ export class Wallet extends AdapterL2(AdapterL1(ethers.Wallet)) { // use legacy txs by default transaction.type = 0; } - const populated = (await super.populateTransaction(transaction)) as TransactionLike; if (transaction.customData == null && transaction.type != EIP712_TX_TYPE) { - return populated; + return (await super.populateTransaction(transaction)) as TransactionLike } + transaction.type = EIP712_TX_TYPE + const populated = (await super.populateTransaction(transaction)) as TransactionLike; populated.type = EIP712_TX_TYPE; populated.value ??= 0; @@ -107,9 +108,7 @@ export class Wallet extends AdapterL2(AdapterL1(ethers.Wallet)) { } override async sendTransaction(tx: TransactionRequest): Promise { - const pop = await this.populateTransaction(tx); - delete pop.from; - const txObj = Transaction.from(pop); - return await this.provider.broadcastTransaction(await this.signTransaction(txObj)); + const populatedTx = await this.populateTransaction(tx); + return await this.provider.broadcastTransaction(await this.signTransaction(populatedTx)); } } diff --git a/tests/custom-matchers.ts b/tests/custom-matchers.ts new file mode 100644 index 0000000..60490c2 --- /dev/null +++ b/tests/custom-matchers.ts @@ -0,0 +1,19 @@ +import * as chai from 'chai'; + +declare global { + namespace Chai { + interface Assertion { + deepEqualExcluding(expected: Record, excludeFields: string[]): Assertion; + } + } +} + +chai.Assertion.addMethod('deepEqualExcluding', function (expected: Record, excludeFields: string[]) { + const obj1 = this._obj; + + for (const key in obj1) { + if (!excludeFields.includes(key)) { + chai.expect(obj1[key]).to.deep.equal(expected[key]); + } + } +}); diff --git a/tests/integration/provider.test.ts b/tests/integration/provider.test.ts index 6b279bc..680437e 100644 --- a/tests/integration/provider.test.ts +++ b/tests/integration/provider.test.ts @@ -1,10 +1,11 @@ import {expect} from 'chai'; import {Provider, types, utils, Wallet} from "../../src"; import {ethers} from "ethers"; +import {TOKENS} from "../const.test"; describe('Provider', () => { - const PUBLIC_KEY = "0x36615Cf349d7F6344891B1e7CA7C72883F5dc049"; + const ADDRESS = "0x36615Cf349d7F6344891B1e7CA7C72883F5dc049"; const PRIVATE_KEY = "0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110"; const RECEIVER = "0xa61464658AfeAf65CccaaFD3a512b69A83B77618"; @@ -13,7 +14,8 @@ describe('Provider', () => { let tx = null; - before('setup', async () => { + before('setup', async function () { + this.timeout(25_000); tx = await wallet.transfer({ token: utils.ETH_ADDRESS, to: RECEIVER, @@ -53,14 +55,6 @@ describe('Provider', () => { }); }); - describe('#getTokenPrice()', () => { - it('should return `token` price', async () => { - const TOKEN_PRICE = "1500.00"; - const result = await provider.getTokenPrice(utils.ETH_ADDRESS); - expect(result).to.be.equal(TOKEN_PRICE); - }); - }); - describe('#getGasPrice()', () => { it('should return gas price', async () => { const GAS_PRICE = BigInt(2_500_000_00); @@ -77,16 +71,22 @@ describe('Provider', () => { }); describe('#getBalance()', () => { - it('should return balance of the account at `address`', async () => { - const result = await provider.getBalance(PUBLIC_KEY); + it('should return ETH balance of the account at `address`', async () => { + const result = await provider.getBalance(ADDRESS); + expect(result > 0).to.be.true; + }); + + it('should return DAI balance of the account at `address`', async () => { + const result = await provider.getBalance(ADDRESS, 'latest', + await provider.l2TokenAddress(TOKENS.DAI.address)); expect(result > 0).to.be.true; }); }); describe('#getAllAccountBalances()', () => { it('should return all balances of the account at `address`', async () => { - const result = await provider.getAllAccountBalances(PUBLIC_KEY); - expect(Object.keys(result)).to.have.lengthOf(2); + const result = await provider.getAllAccountBalances(ADDRESS); + expect(Object.keys(result)).to.have.lengthOf(2); // ETH and DAI }); }); @@ -227,15 +227,15 @@ describe('Provider', () => { it('return withdraw transaction', async () => { const WITHDRAW_TX = { "from": "0x36615Cf349d7F6344891B1e7CA7C72883F5dc049", - "value": BigInt(7000000), + "value": BigInt(7_000_000_000), "to": "0x000000000000000000000000000000000000800a", "data": "0x51cff8d900000000000000000000000036615cf349d7f6344891b1e7ca7c72883f5dc049" }; const result = await provider.getWithdrawTx({ token: utils.ETH_ADDRESS, - amount: 7_000_000, - to: PUBLIC_KEY, - from: PUBLIC_KEY + amount: 7_000_000_000, + to: ADDRESS, + from: ADDRESS }); expect(result).to.be.deep.equal(WITHDRAW_TX); }); @@ -246,13 +246,13 @@ describe('Provider', () => { const TRANSFER_TX = { "from": "0x36615Cf349d7F6344891B1e7CA7C72883F5dc049", "to": RECEIVER, - "value": 7_000_000 + "value": 7_000_000_000 }; const result = await provider.getTransferTx({ token: utils.ETH_ADDRESS, - amount: 7_000_000, + amount: 7_000_000_000, to: RECEIVER, - from: PUBLIC_KEY + from: ADDRESS }); expect(result).to.be.deep.equal(TRANSFER_TX); }); @@ -260,56 +260,63 @@ describe('Provider', () => { describe('#estimateGasWithdraw()', () => { it('should return gas estimation of withdraw transaction', async () => { - const WITHDRAW_GAS = BigInt(408_530); const result = await provider.estimateGasWithdraw({ token: utils.ETH_ADDRESS, - amount: 7_000_000, - to: PUBLIC_KEY, - from: PUBLIC_KEY + amount: 7_000_000_000, + to: ADDRESS, + from: ADDRESS }); - expect(result).to.be.equal(WITHDRAW_GAS); + expect(result > 0).to.be.true; }); }); describe('#estimateGasTransfer()', () => { it('should return gas estimation of transfer transaction', async () => { - const TRANSFER_GAS = BigInt(177_084); const result = await provider.estimateGasTransfer({ token: utils.ETH_ADDRESS, - amount: 7_000_000, + amount: 7_000_000_000, to: RECEIVER, - from: PUBLIC_KEY + from: ADDRESS }); - expect(result).to.be.equal(TRANSFER_GAS); + expect(result > 0).to.be.be.true; }); }); describe('#estimateGasL1()', () => { it('should return gas estimation of L1 transaction', async () => { - const ESTIMATE_GAS_L1 = BigInt(777_396); const result = await provider.estimateGasL1({ - from: PUBLIC_KEY, + from: ADDRESS, to: await provider.getMainContractAddress(), value: 7_000_000_000, customData: { gasPerPubdata: 800 } }); - expect(BigInt(result)).to.be.equal(ESTIMATE_GAS_L1); + expect(result > 0).to.be.true; }); }); describe('#estimateL1ToL2Execute()', () => { it('should return gas estimation of L1 to L2 transaction', async () => { - const ESTIMATE_GAS_L1_TO_L2 = BigInt(777_396); const result = await provider.estimateL1ToL2Execute({ contractAddress: await provider.getMainContractAddress(), calldata: '0x', - caller: PUBLIC_KEY, + caller: ADDRESS, l2Value: 7_000_000_000, }); - expect(BigInt(result)).to.be.equal(ESTIMATE_GAS_L1_TO_L2); + expect(result > 0).to.be.true; + }); + }); + + describe('#estimateFee()', () => { + it('should return gas estimation of transaction', async () => { + const result = await provider.estimateFee({ + from: ADDRESS, + to: RECEIVER, + value: `0x${BigInt(7_000_000_000).toString(16)}` + }); + expect(result).not.to.be.null; }); }); diff --git a/tests/integration/wallet.test.ts b/tests/integration/wallet.test.ts index 817efae..43a41b3 100644 --- a/tests/integration/wallet.test.ts +++ b/tests/integration/wallet.test.ts @@ -1,9 +1,12 @@ -import {expect} from 'chai'; +import * as chai from 'chai'; +import "../custom-matchers"; import {Provider, types, utils, Wallet} from "../../src"; import {ethers} from "ethers"; import * as fs from "fs"; import {TOKENS} from "../const.test"; +const {expect} = chai; + describe('Wallet', () => { const ADDRESS = "0x36615Cf349d7F6344891B1e7CA7C72883F5dc049"; const PRIVATE_KEY = "0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110"; @@ -144,14 +147,13 @@ describe('Wallet', () => { type: 0, from: '0x36615Cf349d7F6344891B1e7CA7C72883F5dc049', nonce: await wallet.getNonce("pending"), - gasLimit: BigInt(177_084), chainId: BigInt(270), gasPrice: BigInt(250_000_000) } const result = await wallet.populateTransaction({ to: RECEIVER, value: 7_000_000 }); - expect(result).to.be.deep.equal(tx); + expect(result).to.be.deepEqualExcluding(tx, ['gasLimit']) }); }); @@ -190,18 +192,18 @@ describe('Wallet', () => { const tx = { contractAddress: "0x36615Cf349d7F6344891B1e7CA7C72883F5dc049", calldata: "0x", - l2Value: 7000000, + l2Value: 7_000_000, l2GasLimit: "0xa542f", token: "0x0000000000000000000000000000000000000000", to: "0x36615Cf349d7F6344891B1e7CA7C72883F5dc049", - amount: 7000000, + amount: 7_000_000, refundRecipient: "0x36615Cf349d7F6344891B1e7CA7C72883F5dc049", operatorTip: 0, overrides: { from: "0x36615Cf349d7F6344891B1e7CA7C72883F5dc049", - maxFeePerGas: BigInt(1000000010), - maxPriorityFeePerGas: BigInt(1000000000), - value: BigInt(338455507000000) + maxFeePerGas: BigInt(1_000_000_010), + maxPriorityFeePerGas: BigInt(1_000_000_000), + value: BigInt(338_455_507_000_000) }, gasPerPubdataByte: 800 } @@ -216,9 +218,9 @@ describe('Wallet', () => { it('should return DAI deposit transaction', async () => { const tx = { - maxFeePerGas: BigInt(1000000010), - maxPriorityFeePerGas: BigInt(1000000000), - value: BigInt(347023500000000), + maxFeePerGas: BigInt(1_000_000_010), + maxPriorityFeePerGas: BigInt(1_000_000_000), + value: BigInt(347_023_500_000_000), from: "0x36615Cf349d7F6344891B1e7CA7C72883F5dc049", to: await (await wallet.getL1BridgeContracts()).erc20.getAddress(), data: "0xe8b99b1b00000000000000000000000036615cf349d7f6344891b1e7ca7c72883f5dc0490000000000000000000000005e6d086f5ec079adff4fb3774cdf3e8d6a34f7e9000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000a971f000000000000000000000000000000000000000000000000000000000000032000000000000000000000000036615cf349d7f6344891b1e7ca7c72883f5dc049" @@ -241,7 +243,7 @@ describe('Wallet', () => { amount: 5, refundRecipient: await wallet.getAddress() }); - expect(result).to.be.equal(BigInt(149115)); + expect(result).to.be.equal(BigInt(149_115)); }); it('should return gas estimation for DAI deposit transaction', async () => { @@ -251,80 +253,194 @@ describe('Wallet', () => { amount: 5, refundRecipient: await wallet.getAddress() }); - expect(result).to.be.equal(BigInt(280566)); + expect(result).to.be.equal(BigInt(280_566)); }); }); describe('#deposit()', () => { it('should deposit ETH to L2 network', async () => { + const amount = 7_000_000_000; + const l2BalanceBeforeDeposit = await wallet.getBalance(); + const l1BalanceBeforeDeposit = await wallet.getBalanceL1(); const tx = await wallet.deposit({ token: utils.ETH_ADDRESS, to: await wallet.getAddress(), - amount: 700_000_000, + amount: amount, refundRecipient: await wallet.getAddress() }); const result = await tx.wait(); + const l2BalanceAfterDeposit = await wallet.getBalance(); + const l1BalanceAfterDeposit = await wallet.getBalanceL1(); expect(result).not.to.be.null; + expect(l2BalanceAfterDeposit - l2BalanceBeforeDeposit >= BigInt(amount)).to.be.true; + expect(l1BalanceBeforeDeposit - l1BalanceAfterDeposit >= BigInt(amount)).to.be.true; }).timeout(10_000); it('should deposit DAI to L2 network', async () => { + const amount = 5; + const l2DAI = await provider.l2TokenAddress(TOKENS.DAI.address) + const l2BalanceBeforeDeposit = await wallet.getBalance(l2DAI); + const l1BalanceBeforeDeposit = await wallet.getBalanceL1(TOKENS.DAI.address); const tx = await wallet.deposit({ token: TOKENS.DAI.address, to: await wallet.getAddress(), - amount: 5, + amount: amount, approveERC20: true, refundRecipient: await wallet.getAddress() }); const result = await tx.wait(); + const l2BalanceAfterDeposit = await wallet.getBalance(l2DAI); + const l1BalanceAfterDeposit = await wallet.getBalanceL1(TOKENS.DAI.address); expect(result).not.to.be.null; + expect(l2BalanceAfterDeposit - l2BalanceBeforeDeposit === BigInt(amount)).to.be.true; + expect(l1BalanceBeforeDeposit - l1BalanceAfterDeposit === BigInt(amount)).to.be.true; }).timeout(10_000); }); - // describe('#getFullRequiredDepositFee()', () => { - // it('should return fee for ETH token deposit', async () => { - // const result = await wallet.getFullRequiredDepositFee({ - // token: utils.ETH_ADDRESS, - // to: await wallet.getAddress(), - // }); - // console.log(result); - // // expect(result).to.be.equal(BigInt(280566)); - // }); - // - // it('should return fee for DAI token deposit', async () => { - // const result = await wallet.getFullRequiredDepositFee({ - // token: TOKENS.DAI.address, - // to: await wallet.getAddress(), - // }); - // console.log(result); - // // expect(result).to.be.equal(BigInt(280566)); - // }); - // }); + describe('#getFullRequiredDepositFee()', () => { + it('should return fee for ETH token deposit', async () => { + const FEE_DATA = { + baseCost: BigInt(338_455_500_000_000), + l1GasLimit: BigInt(149_115), + l2GasLimit: '0xa542f', + maxFeePerGas: BigInt(1_000_000_010), + maxPriorityFeePerGas: BigInt(1_000_000_000) + } + const result = await wallet.getFullRequiredDepositFee({ + token: utils.ETH_ADDRESS, + to: await wallet.getAddress(), + }); + expect(result).to.be.deep.equal(FEE_DATA); + }); + + it('should return fee for DAI token deposit', async () => { + const FEE_DATA = { + baseCost: BigInt(347_023_500_000_000), + l1GasLimit: BigInt(280_326), + l2GasLimit: '0xa971f', + maxFeePerGas: BigInt(1_000_000_010), + maxPriorityFeePerGas: BigInt(1_000_000_000) + } + + const tx = await wallet.approveERC20(TOKENS.DAI.address, 5); + await tx.wait(); + + const result = await wallet.getFullRequiredDepositFee({ + token: TOKENS.DAI.address, + to: await wallet.getAddress(), + }); + expect(result).to.be.deep.equal(FEE_DATA); + }).timeout(10_000); + }); describe('#withdraw()', () => { it('should withdraw ETH to L1 network', async () => { + const amount = 7_000_000_000; + const l2BalanceBeforeWithdrawal = await wallet.getBalance(); const withdrawTx = await wallet.withdraw({ token: utils.ETH_ADDRESS, to: await wallet.getAddress(), - amount: 700_000_000, + amount: amount, }); await withdrawTx.waitFinalize(); const finalizeWithdrawTx = await wallet.finalizeWithdrawal(withdrawTx.hash); const result = await finalizeWithdrawTx.wait(); + const l2BalanceAfterWithdrawal = await wallet.getBalance(); expect(result).not.to.be.null; + expect(l2BalanceBeforeWithdrawal - l2BalanceAfterWithdrawal >= BigInt(amount)).to.be.true; }).timeout(25_000); it('should withdraw DAI to L1 network', async () => { - const withdrawTx = await wallet.deposit({ - token: await provider.l2TokenAddress(TOKENS.DAI.address), + const amount = 5; + const l2DAI = await provider.l2TokenAddress(TOKENS.DAI.address); + const l2BalanceBeforeWithdrawal = await wallet.getBalance(l2DAI); + const l1BalanceBeforeWithdrawal = await wallet.getBalanceL1(TOKENS.DAI.address); + const withdrawTx = await wallet.withdraw({ + token: l2DAI, to: await wallet.getAddress(), - amount: 5, - approveERC20: true, - refundRecipient: await wallet.getAddress() + amount: amount, }); await withdrawTx.waitFinalize(); const finalizeWithdrawTx = await wallet.finalizeWithdrawal(withdrawTx.hash); const result = await finalizeWithdrawTx.wait(); + const l2BalanceAfterWithdrawal = await wallet.getBalance(l2DAI); + const l1BalanceAfterWithdrawal = await wallet.getBalanceL1(TOKENS.DAI.address); + expect(result).not.to.be.null; + expect(l2BalanceBeforeWithdrawal - l2BalanceAfterWithdrawal == BigInt(amount)).to.be.true; + expect(l1BalanceAfterWithdrawal - l1BalanceBeforeWithdrawal == BigInt(amount)).to.be.true; + }).timeout(25_000); + }); + + describe('#getRequestExecuteTx()', () => { + it('should return request execute transaction', async () => { + const result = await wallet.getRequestExecuteTx({ + contractAddress: await provider.getMainContractAddress(), + calldata: '0x', + l2Value: 7_000_000_000, + }); + expect(result).not.to.be.null; + }); + }); + + describe('#estimateGasRequestExecute()', () => { + it('should return gas estimation for request execute transaction', async () => { + const result = await wallet.estimateGasRequestExecute({ + contractAddress: await provider.getMainContractAddress(), + calldata: '0x', + l2Value: 7_000_000_000, + }); + expect(result).to.be.equal(BigInt(124_299)); + }); + }); + + describe('#requestExecute()', () => { + it('should request transaction execution on L2 network', async () => { + const amount = 7_000_000_000; + const l2BalanceBeforeExecution = await wallet.getBalance(); + const l1BalanceBeforeExecution = await wallet.getBalanceL1(); + const tx = await wallet.requestExecute({ + contractAddress: await provider.getMainContractAddress(), + calldata: '0x', + l2Value: amount, + l2GasLimit: 900_000 + }); + const result = await tx.wait(); + const l2BalanceAfterExecution = await wallet.getBalance(); + const l1BalanceAfterExecution = await wallet.getBalanceL1(); + expect(result).not.to.be.null; + expect(l2BalanceAfterExecution - l2BalanceBeforeExecution >= BigInt(amount)).to.be.true; + expect(l1BalanceBeforeExecution - l1BalanceAfterExecution >= BigInt(amount)).to.be.true; + }).timeout(10_000); + }); + + describe('#transfer()', () => { + it('should transfer ETH', async () => { + const amount = 7_000_000_000; + const balanceBeforeTransfer = await provider.getBalance(RECEIVER); + const tx = await wallet.transfer({ + token: utils.ETH_ADDRESS, + to: RECEIVER, + amount: amount, + }); + const result = await tx.wait(); + const balanceAfterTransfer = await provider.getBalance(RECEIVER); + expect(result).not.to.be.null; + expect(balanceAfterTransfer - balanceBeforeTransfer).to.be.equal(BigInt(amount)); + }).timeout(25_000); + + it('should transfer DAI', async () => { + const amount = 5; + const l2DAI = await provider.l2TokenAddress(TOKENS.DAI.address); + const balanceBeforeTransfer = await provider.getBalance(RECEIVER, 'latest', l2DAI); + const tx = await wallet.transfer({ + token: l2DAI, + to: RECEIVER, + amount: amount, + }); + const result = await tx.wait(); + const balanceAfterTransfer = await provider.getBalance(RECEIVER, 'latest', l2DAI); expect(result).not.to.be.null; + expect(balanceAfterTransfer - balanceBeforeTransfer).to.be.equal(BigInt(amount)); }).timeout(25_000); }); }); \ No newline at end of file diff --git a/tests/setup.test.ts b/tests/setup.test.ts index 0e43cb0..7378bc4 100644 --- a/tests/setup.test.ts +++ b/tests/setup.test.ts @@ -1,12 +1,11 @@ import {expect} from 'chai'; -import {Provider, types, utils, Wallet} from "../src"; +import {Provider, types, Wallet} from "../src"; import {ethers} from "ethers"; import {TOKENS} from "./const.test"; // This should be run first before all other tests, // which is why it's specified first in the test command in package.json. describe('setup', () => { - const ADDRESS = "0x36615Cf349d7F6344891B1e7CA7C72883F5dc049"; const PRIVATE_KEY = "0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110"; const provider = Provider.getDefaultProvider(types.Network.Localhost); @@ -20,12 +19,12 @@ describe('setup', () => { const priorityOpResponse = await wallet.deposit({ token: TOKENS.DAI.address, to: await wallet.getAddress(), - amount: 5, + amount: 30, approveERC20: true, refundRecipient: await wallet.getAddress() }); const receipt = await priorityOpResponse.waitFinalize(); expect(receipt).not.to.be.null; } - }).timeout(10_000); + }).timeout(25_000); }); \ No newline at end of file