import Axios from 'axios'; import { BigNumber } from 'ethers'; import { SyncProvider } from './provider-interface'; import * as types from './types'; import { sleep, TokenSet } from './utils'; import { Network } from './types'; export async function getDefaultRestProvider( network: types.Network, pollIntervalMilliSecs?: number ): Promise { if (network === 'localhost') { return await RestProvider.newProvider('http://127.0.0.1:3001/api/v0.2', pollIntervalMilliSecs, network); } else if (network === 'goerli') { return await RestProvider.newProvider('https://goerli-api.zksync.io/api/v0.2', pollIntervalMilliSecs, network); } else if (network === 'sepolia') { return await RestProvider.newProvider('https://sepolia-api.zksync.io/api/v0.2', pollIntervalMilliSecs, network); } else if (network === 'goerli-beta') { return await RestProvider.newProvider( 'https://goerli-beta-api.zksync.dev/api/v0.2', pollIntervalMilliSecs, network ); } else if (network === 'rinkeby-beta') { return await RestProvider.newProvider( 'https://rinkeby-beta-api.zksync.io/api/v0.2', pollIntervalMilliSecs, network ); } else if (network === 'mainnet') { return await RestProvider.newProvider('https://api.zksync.io/api/v0.2', pollIntervalMilliSecs, network); } else { throw new Error(`Ethereum network ${network} is not supported`); } } export interface Request { network: types.Network; apiVersion: 'v02'; resource: string; args: any; timestamp: string; } export interface Error { errorType: string; code: number; message: string; } export interface Response { request: Request; status: 'success' | 'error'; error?: Error; result?: T; } export class RESTError extends Error { constructor(message: string, public restError: Error) { super(message); } } export class RestProvider extends SyncProvider { public static readonly MAX_LIMIT = 100; private constructor(public address: string) { super(); this.providerType = 'Rest'; } static async newProvider( address: string = 'http://127.0.0.1:3001/api/v0.2', pollIntervalMilliSecs?: number, network?: Network ): Promise { const provider = new RestProvider(address); if (pollIntervalMilliSecs) { provider.pollIntervalMilliSecs = pollIntervalMilliSecs; } provider.contractAddress = await provider.getContractAddress(); provider.tokenSet = new TokenSet(await provider.getTokens()); provider.network = network; return provider; } parseResponse(response: Response): T { if (response.status === 'success') { return response.result; } else { throw new RESTError( `zkSync API response error: errorType: ${response.error.errorType};` + ` code ${response.error.code}; message: ${response.error.message}`, response.error ); } } async get(url: string): Promise> { return await Axios.get(url).then((resp) => { return resp.data; }); } async post(url: string, body: any): Promise> { return await Axios.post(url, body).then((resp) => { return resp.data; }); } async accountInfoDetailed( idOrAddress: number | types.Address, infoType: 'committed' | 'finalized' ): Promise> { return await this.get(`${this.address}/accounts/${idOrAddress}/${infoType}`); } async accountInfo( idOrAddress: number | types.Address, infoType: 'committed' | 'finalized' ): Promise { return this.parseResponse(await this.accountInfoDetailed(idOrAddress, infoType)); } async toggle2FADetailed(data: types.Toggle2FARequest): Promise> { return await this.post(`${this.address}/transactions/toggle2FA`, data); } async toggle2FA(data: types.Toggle2FARequest): Promise { const response = this.parseResponse(await this.toggle2FADetailed(data)); return response.success; } async accountFullInfoDetailed(idOrAddress: number | types.Address): Promise> { return await this.get(`${this.address}/accounts/${idOrAddress}`); } async accountFullInfo(idOrAddress: number | types.Address): Promise { return this.parseResponse(await this.accountFullInfoDetailed(idOrAddress)); } async accountTxsDetailed( idOrAddress: number | types.Address, paginationQuery: types.PaginationQuery, token?: types.TokenLike, secondIdOrAddress?: number | types.Address ): Promise>> { let url = `${this.address}/accounts/${idOrAddress}/transactions?from=${paginationQuery.from}` + `&limit=${paginationQuery.limit}&direction=${paginationQuery.direction}`; if (token) url += `&token=${token}`; if (secondIdOrAddress) url += `&secondAccount=${secondIdOrAddress}`; return await this.get(url); } async accountTxs( idOrAddress: number | types.Address, paginationQuery: types.PaginationQuery, token?: types.TokenLike, secondIdOrAddress?: number | types.Address ): Promise> { return this.parseResponse( await this.accountTxsDetailed(idOrAddress, paginationQuery, token, secondIdOrAddress) ); } async accountPendingTxsDetailed( idOrAddress: number | types.Address, paginationQuery: types.PaginationQuery ): Promise>> { return await this.get( `${this.address}/accounts/${idOrAddress}/transactions/pending?from=${paginationQuery.from}` + `&limit=${paginationQuery.limit}&direction=${paginationQuery.direction}` ); } async accountPendingTxs( idOrAddress: number | types.Address, paginationQuery: types.PaginationQuery ): Promise> { return this.parseResponse(await this.accountPendingTxsDetailed(idOrAddress, paginationQuery)); } async blockPaginationDetailed( paginationQuery: types.PaginationQuery ): Promise>> { return await this.get( `${this.address}/blocks?from=${paginationQuery.from}&limit=${paginationQuery.limit}` + `&direction=${paginationQuery.direction}` ); } async blockPagination( paginationQuery: types.PaginationQuery ): Promise> { return this.parseResponse(await this.blockPaginationDetailed(paginationQuery)); } async blockByPositionDetailed(blockPosition: types.BlockPosition): Promise> { return await this.get(`${this.address}/blocks/${blockPosition}`); } async blockByPosition(blockPosition: types.BlockPosition): Promise { return this.parseResponse(await this.blockByPositionDetailed(blockPosition)); } async blockTransactionsDetailed( blockPosition: types.BlockPosition, paginationQuery: types.PaginationQuery ): Promise>> { return await this.get( `${this.address}/blocks/${blockPosition}/transactions?from=${paginationQuery.from}` + `&limit=${paginationQuery.limit}&direction=${paginationQuery.direction}` ); } async blockTransactions( blockPosition: types.BlockPosition, paginationQuery: types.PaginationQuery ): Promise> { return this.parseResponse(await this.blockTransactionsDetailed(blockPosition, paginationQuery)); } async configDetailed(): Promise> { return await this.get(`${this.address}/config`); } async config(): Promise { return this.parseResponse(await this.configDetailed()); } async getTransactionFeeDetailed( txType: types.IncomingTxFeeType, address: types.Address, tokenLike: types.TokenLike ): Promise> { const rawFee = await this.post<{ gasFee: string; zkpFee: string; totalFee: string }>(`${this.address}/fee`, { txType, address, tokenLike }); let fee: Response; if (rawFee.status === 'success') { fee = { request: rawFee.request, status: rawFee.status, error: null, result: { gasFee: BigNumber.from(rawFee.result.gasFee), zkpFee: BigNumber.from(rawFee.result.zkpFee), totalFee: BigNumber.from(rawFee.result.totalFee) } }; } else { fee = { request: rawFee.request, status: rawFee.status, error: rawFee.error, result: null }; } return fee; } async getTransactionFee( txType: types.IncomingTxFeeType, address: types.Address, tokenLike: types.TokenLike ): Promise { return this.parseResponse(await this.getTransactionFeeDetailed(txType, address, tokenLike)); } async getBatchFullFeeDetailed( transactions: { txType: types.IncomingTxFeeType; address: types.Address; }[], tokenLike: types.TokenLike ): Promise> { const rawFee = await this.post<{ gasFee: string; zkpFee: string; totalFee: string }>( `${this.address}/fee/batch`, { transactions, tokenLike } ); let fee: Response; if (rawFee.status === 'success') { fee = { request: rawFee.request, status: rawFee.status, error: null, result: { gasFee: BigNumber.from(rawFee.result.gasFee), zkpFee: BigNumber.from(rawFee.result.zkpFee), totalFee: BigNumber.from(rawFee.result.totalFee) } }; } else { fee = { request: rawFee.request, status: rawFee.status, error: rawFee.error, result: null }; } return fee; } async getBatchFullFee( transactions: { txType: types.IncomingTxFeeType; address: types.Address; }[], tokenLike: types.TokenLike ): Promise { return this.parseResponse(await this.getBatchFullFeeDetailed(transactions, tokenLike)); } async networkStatusDetailed(): Promise> { return await this.get(`${this.address}/networkStatus`); } async networkStatus(): Promise { return this.parseResponse(await this.networkStatusDetailed()); } async tokenPaginationDetailed( paginationQuery: types.PaginationQuery ): Promise>> { return await this.get( `${this.address}/tokens?from=${paginationQuery.from}&limit=${paginationQuery.limit}` + `&direction=${paginationQuery.direction}` ); } async tokenPagination( paginationQuery: types.PaginationQuery ): Promise> { return this.parseResponse(await this.tokenPaginationDetailed(paginationQuery)); } async tokenInfoDetailed(tokenLike: types.TokenLike): Promise> { return await this.get(`${this.address}/tokens/${tokenLike}`); } async tokenInfo(tokenLike: types.TokenLike): Promise { return this.parseResponse(await this.tokenInfoDetailed(tokenLike)); } async tokenPriceInfoDetailed( tokenLike: types.TokenLike, tokenIdOrUsd: number | 'usd' ): Promise> { return await this.get(`${this.address}/tokens/${tokenLike}/priceIn/${tokenIdOrUsd}`); } async tokenPriceInfo(tokenLike: types.TokenLike, tokenIdOrUsd: number | 'usd'): Promise { return this.parseResponse(await this.tokenPriceInfoDetailed(tokenLike, tokenIdOrUsd)); } async submitTxNewDetailed(tx: types.L2Tx, signature?: types.TxEthSignatureVariant): Promise> { return await this.post(`${this.address}/transactions`, { tx, signature }); } async submitTxNew(tx: types.L2Tx, signature?: types.TxEthSignatureVariant): Promise { return this.parseResponse(await this.submitTxNewDetailed(tx, signature)); } /** * @deprecated Use submitTxNew method instead */ async submitTx(tx: any, signature?: types.TxEthSignatureVariant, fastProcessing?: boolean): Promise { if (fastProcessing) { tx.fastProcessing = fastProcessing; } let txHash = await this.submitTxNew(tx, signature); txHash.replace('0x', 'sync-tx:'); return txHash; } async txStatusDetailed(txHash: string): Promise> { return await this.get(`${this.address}/transactions/${txHash}`); } async txStatus(txHash: string): Promise { return this.parseResponse(await this.txStatusDetailed(txHash)); } async txDataDetailed(txHash: string): Promise> { return await this.get(`${this.address}/transactions/${txHash}/data`); } async txData(txHash: string): Promise { return this.parseResponse(await this.txDataDetailed(txHash)); } async submitTxsBatchNewDetailed( txs: { tx: any; signature?: types.TxEthSignatureVariant }[], signature?: types.TxEthSignature | types.TxEthSignature[] ): Promise> { return await this.post(`${this.address}/transactions/batches`, { txs, signature }); } async submitTxsBatchNew( txs: { tx: any; signature?: types.TxEthSignatureVariant }[], signature?: types.TxEthSignature | types.TxEthSignature[] ): Promise { return this.parseResponse(await this.submitTxsBatchNewDetailed(txs, signature)); } /** * @deprecated Use submitTxsBatchNew method instead. */ async submitTxsBatch( transactions: { tx: any; signature?: types.TxEthSignatureVariant }[], ethSignatures?: types.TxEthSignature | types.TxEthSignature[] ): Promise { return (await this.submitTxsBatchNew(transactions, ethSignatures)).transactionHashes; } async getBatchDetailed(batchHash: string): Promise> { return await this.get(`${this.address}/transactions/batches/${batchHash}`); } async getBatch(batchHash: string): Promise { return this.parseResponse(await this.getBatchDetailed(batchHash)); } async getNFTDetailed(id: number): Promise> { return await this.get(`${this.address}/tokens/nft/${id}`); } async getNFT(id: number): Promise { const nft = this.parseResponse(await this.getNFTDetailed(id)); // If the NFT does not exist, throw an exception if (nft == null) { throw new Error(`Requested NFT doesn't exist or the corresponding mintNFT operation is not verified yet`); } return nft; } async getNFTOwnerDetailed(id: number): Promise> { return await this.get(`${this.address}/tokens/nft/${id}/owner`); } async getNFTOwner(id: number): Promise { return this.parseResponse(await this.getNFTOwnerDetailed(id)); } async getNFTIdByTxHashDetailed(txHash: string): Promise> { return await this.get(`${this.address}/tokens/nft_id_by_tx_hash/${txHash}`); } async getNFTIdByTxHash(txHash: string): Promise { return this.parseResponse(await this.getNFTIdByTxHashDetailed(txHash)); } async notifyAnyTransaction(hash: string, action: 'COMMIT' | 'VERIFY'): Promise { while (true) { let transactionStatus = await this.txStatus(hash); let notifyDone; if (action === 'COMMIT') { notifyDone = transactionStatus && transactionStatus.rollupBlock; } else { if (transactionStatus && transactionStatus.rollupBlock) { if (transactionStatus.status === 'rejected') { // If the transaction status is rejected // it cannot be known if transaction is queued, committed or finalized. // That is why there is separate `blockByPosition` query. const blockStatus = await this.blockByPosition(transactionStatus.rollupBlock); notifyDone = blockStatus && blockStatus.status === 'finalized'; } else { notifyDone = transactionStatus.status === 'finalized'; } } } if (notifyDone) { // Transaction status needs to be recalculated because it can // be updated between `txStatus` and `blockByPosition` calls. return await this.txStatus(hash); } else { await sleep(this.pollIntervalMilliSecs); } } } async notifyTransaction(hash: string, action: 'COMMIT' | 'VERIFY'): Promise { await this.notifyAnyTransaction(hash, action); return await this.getTxReceipt(hash); } async notifyPriorityOp(hash: string, action: 'COMMIT' | 'VERIFY'): Promise { await this.notifyAnyTransaction(hash, action); return await this.getPriorityOpStatus(hash); } async getContractAddress(): Promise { const config = await this.config(); return { mainContract: config.contract, govContract: config.govContract }; } async getTokens(limit?: number): Promise { let tokens = {}; let tmpId = 0; limit = limit ? limit : RestProvider.MAX_LIMIT; let tokenPage: types.Paginated; do { tokenPage = await this.tokenPagination({ from: tmpId, limit, direction: 'newer' }); for (let token of tokenPage.list) { tokens[token.symbol] = { address: token.address, id: token.id, symbol: token.symbol, decimals: token.decimals, enabledForFees: token.enabledForFees }; } tmpId += limit; } while (tokenPage.list.length == limit); return tokens; } async getState(address: types.Address): Promise { const fullInfo = await this.accountFullInfo(address); const defaultInfo = { balances: {}, nonce: 0, pubKeyHash: 'sync:0000000000000000000000000000000000000000', nfts: {}, mintedNfts: {} }; if (fullInfo.finalized) { return { address, id: fullInfo.committed.accountId, accountType: fullInfo.committed.accountType, depositing: fullInfo.depositing, committed: { balances: fullInfo.committed.balances, nonce: fullInfo.committed.nonce, pubKeyHash: fullInfo.committed.pubKeyHash, nfts: fullInfo.committed.nfts, mintedNfts: fullInfo.committed.mintedNfts }, verified: { balances: fullInfo.finalized.balances, nonce: fullInfo.finalized.nonce, pubKeyHash: fullInfo.finalized.pubKeyHash, nfts: fullInfo.finalized.nfts, mintedNfts: fullInfo.finalized.mintedNfts } }; } else if (fullInfo.committed) { return { address, id: fullInfo.committed.accountId, accountType: fullInfo.committed.accountType, depositing: fullInfo.depositing, committed: { balances: fullInfo.committed.balances, nonce: fullInfo.committed.nonce, pubKeyHash: fullInfo.committed.pubKeyHash, nfts: fullInfo.committed.nfts, mintedNfts: fullInfo.committed.mintedNfts }, verified: defaultInfo }; } else { return { address, depositing: fullInfo.depositing, committed: defaultInfo, verified: defaultInfo }; } } async getConfirmationsForEthOpAmount(): Promise { const config = await this.config(); return config.depositConfirmations; } async getTransactionsBatchFee( txTypes: types.IncomingTxFeeType[], addresses: types.Address[], tokenLike: types.TokenLike ): Promise { let transactions = []; for (let i = 0; i < txTypes.length; ++i) { transactions.push({ txType: txTypes[i], address: addresses[i] }); } const fee = await this.getBatchFullFee(transactions, tokenLike); return fee.totalFee; } async getTokenPrice(tokenLike: types.TokenLike): Promise { const price = await this.tokenPriceInfo(tokenLike, 'usd'); return parseFloat(price.price); } async getTxReceipt(txHash: string): Promise { const receipt = await this.txStatus(txHash); if (!receipt || !receipt.rollupBlock) { return { executed: false }; } else { if (receipt.status === 'rejected') { const blockFullInfo = await this.blockByPosition(receipt.rollupBlock); const blockInfo = { blockNumber: receipt.rollupBlock, committed: blockFullInfo ? true : false, verified: blockFullInfo && blockFullInfo.status === 'finalized' ? true : false }; return { executed: true, success: false, failReason: receipt.failReason, block: blockInfo }; } else { return { executed: true, success: true, block: { blockNumber: receipt.rollupBlock, committed: true, verified: receipt.status === 'finalized' } }; } } } async getPriorityOpStatus(hash: string): Promise { const receipt = await this.txStatus(hash); if (!receipt || !receipt.rollupBlock) { return { executed: false }; } else { return { executed: true, block: { blockNumber: receipt.rollupBlock, committed: true, verified: receipt.status === 'finalized' } }; } } async getEthTxForWithdrawal(withdrawalHash: string): Promise { const txData = await this.txData(withdrawalHash); if ( txData.tx.op.type === 'Withdraw' || txData.tx.op.type === 'ForcedExit' || txData.tx.op.type === 'WithdrawNFT' ) { return txData.tx.op.ethTxHash; } else { return null; } } }