diff --git a/src/services/EnsResolver.ts b/src/services/EnsResolver.ts index 9480973e4..9a3e8b837 100644 --- a/src/services/EnsResolver.ts +++ b/src/services/EnsResolver.ts @@ -25,7 +25,7 @@ export class EnsResolver { private initEns() { if(faucetConfig.ensResolver) { - let provider = new Web3.providers.HttpProvider(faucetConfig.ensResolver.rpcHost); + let provider = typeof faucetConfig.ensResolver.rpcHost == "object" ? faucetConfig.ensResolver.rpcHost : new Web3.providers.HttpProvider(faucetConfig.ensResolver.rpcHost); this.ens = new ENS(provider, faucetConfig.ensResolver.ensAddr || undefined, Web3); } else { diff --git a/src/services/EthClaimManager.ts b/src/services/EthClaimManager.ts index 038d73a46..724a6687b 100644 --- a/src/services/EthClaimManager.ts +++ b/src/services/EthClaimManager.ts @@ -141,7 +141,7 @@ export class EthClaimManager { return null; } - private async processQueue() { + public async processQueue() { if(this.queueProcessing) return; this.queueProcessing = true; diff --git a/src/services/EthWalletManager.ts b/src/services/EthWalletManager.ts index 117951f24..767d2e7ad 100644 --- a/src/services/EthWalletManager.ts +++ b/src/services/EthWalletManager.ts @@ -56,6 +56,7 @@ export class EthWalletManager { private walletState: WalletState; private tokenState: FaucetTokenState; private lastWalletRefresh: number; + private txReceiptPollInterval = 30000; public initialize() { if(this.initialized) @@ -97,7 +98,9 @@ export class EthWalletManager { private startWeb3() { let provider: any; - if(faucetConfig.ethRpcHost.match(/^wss?:\/\//)) + if(faucetConfig.ethRpcHost && typeof faucetConfig.ethRpcHost === "object") + provider = faucetConfig.ethRpcHost as any; + else if(faucetConfig.ethRpcHost.match(/^wss?:\/\//)) provider = new Web3.providers.WebsocketProvider(faucetConfig.ethRpcHost); else if(faucetConfig.ethRpcHost.match(/^\//)) provider = new Web3.providers.IpcProvider(faucetConfig.ethRpcHost, net); @@ -447,7 +450,7 @@ export class EthWalletManager { try { let receipt: TransactionReceipt; do { - await sleepPromise(30000); // 30 secs + await sleepPromise(this.txReceiptPollInterval); // 30 secs receipt = await this.web3.eth.getTransactionReceipt(txhash); ServiceManager.GetService(FaucetProcess).emitLog(FaucetLogLevel.WARNING, "Polled transaction receipt for " + txhash + ": " + (receipt ? "found!" : "pending")); } while(!receipt); diff --git a/src/services/PoWRewardLimiter.ts b/src/services/PoWRewardLimiter.ts index 12b642612..d69958cce 100644 --- a/src/services/PoWRewardLimiter.ts +++ b/src/services/PoWRewardLimiter.ts @@ -25,10 +25,10 @@ export class PoWRewardLimiter { private balanceRestriction: number; private balanceRestrictionsRefresh: number; - private refreshIpInfoMatchRestrictions() { + public refreshIpInfoMatchRestrictions(force?: boolean) { let now = Math.floor((new Date()).getTime() / 1000); let refresh = faucetConfig.ipInfoMatchRestrictedRewardFile ? faucetConfig.ipInfoMatchRestrictedRewardFile.refresh : 30; - if(this.ipInfoMatchRestrictionsRefresh > now - refresh) + if(this.ipInfoMatchRestrictionsRefresh > now - refresh && !force) return; this.ipInfoMatchRestrictionsRefresh = now; diff --git a/tests/EnsResolver.spec.ts b/tests/EnsResolver.spec.ts new file mode 100644 index 000000000..d3869ffec --- /dev/null +++ b/tests/EnsResolver.spec.ts @@ -0,0 +1,73 @@ +import 'mocha'; +import sinon from 'sinon'; +import { expect } from 'chai'; +import { unbindTestStubs, FakeProvider } from './common'; +import { faucetConfig, loadFaucetConfig } from '../src/common/FaucetConfig'; +import { FaucetProcess } from '../src/common/FaucetProcess'; +import { EnsResolver } from '../src/services/EnsResolver'; +import { ServiceManager } from '../src/common/ServiceManager'; + +describe("ENS Resolver", () => { + let globalStubs; + let fakeProvider; + + beforeEach(() => { + globalStubs = { + "FaucetProcess.emitLog": sinon.stub(FaucetProcess.prototype, "emitLog"), + }; + fakeProvider = new FakeProvider(); + loadFaucetConfig(true); + faucetConfig.faucetStats = null; + faucetConfig.ethRpcHost = fakeProvider; + }); + afterEach(() => { + return unbindTestStubs(); + }); + + it("check ens resolver", async () => { + faucetConfig.ensResolver = { + rpcHost: fakeProvider, + ensAddr: null, + }; + fakeProvider.injectResponse("net_version", "5"); + fakeProvider.injectResponse("eth_call", "0x0000000000000000000000004b1488b7a6b320d2d721406204abc3eeaa9ad329"); + let ensResolver = new EnsResolver(); + ensResolver.initialize(); + let resolveResult = await ensResolver.resolveEnsName("test.eth"); + expect(resolveResult).equal("0x4B1488B7a6B320d2D721406204aBc3eeAa9AD329", "unexpected address"); + }); + it("check ens resolver (disabled)", async () => { + faucetConfig.ensResolver = null; + let ensResolver = new EnsResolver(); + ensResolver.initialize(); + let exception; + try { + await ensResolver.resolveEnsName("test.eth"); + } catch(ex) { + exception = ex; + } + expect(exception).equal("ENS resolver not enabled", "unexpected error"); + }); + it("check ens resolver (dynamic enable/disable)", async () => { + faucetConfig.ensResolver = null; + let ensResolver = new EnsResolver(); + ensResolver.initialize(); + let exception; + try { + await ensResolver.resolveEnsName("test.eth"); + } catch(ex) { + exception = ex; + } + expect(exception).equal("ENS resolver not enabled", "unexpected error"); + faucetConfig.ensResolver = { + rpcHost: fakeProvider, + ensAddr: null, + }; + fakeProvider.injectResponse("net_version", "5"); + fakeProvider.injectResponse("eth_call", "0x0000000000000000000000004b1488b7a6b320d2d721406204abc3eeaa9ad329"); + ServiceManager.GetService(FaucetProcess).emit("reload"); + let resolveResult = await ensResolver.resolveEnsName("test.eth"); + expect(resolveResult).equal("0x4B1488B7a6B320d2D721406204aBc3eeAa9AD329", "unexpected address"); + }); + +}); diff --git a/tests/EthWalletManager.spec.ts b/tests/EthWalletManager.spec.ts new file mode 100644 index 000000000..a87f93513 --- /dev/null +++ b/tests/EthWalletManager.spec.ts @@ -0,0 +1,435 @@ +import 'mocha'; +import sinon from 'sinon'; +import { expect } from 'chai'; +import { unbindTestStubs, FakeProvider, awaitSleepPromise } from './common'; +import { faucetConfig, loadFaucetConfig } from '../src/common/FaucetConfig'; +import { EthWalletManager, FaucetCoinType } from '../src/services/EthWalletManager'; +import { FaucetProcess } from '../src/common/FaucetProcess'; +import { ServiceManager } from '../src/common/ServiceManager'; +import { ClaimTxStatus, EthClaimManager } from '../src/services/EthClaimManager'; +import { FaucetStoreDB } from '../src/services/FaucetStoreDB'; +import { sleepPromise } from '../src/utils/SleepPromise'; + +describe("ETH Wallet Manager", () => { + let globalStubs; + let fakeProvider; + + beforeEach(() => { + globalStubs = { + "FaucetProcess.emitLog": sinon.stub(FaucetProcess.prototype, "emitLog"), + }; + fakeProvider = new FakeProvider(); + loadFaucetConfig(true); + faucetConfig.faucetStats = null; + faucetConfig.ethRpcHost = fakeProvider; + }); + afterEach(() => { + return unbindTestStubs(); + }); + + it("check wallet state initialization", async () => { + let ethWalletManager = new EthWalletManager(); + fakeProvider.injectResponse("eth_chainId", 1337); + fakeProvider.injectResponse("eth_getBalance", "1000"); + fakeProvider.injectResponse("eth_getTransactionCount", 42); + faucetConfig.ethWalletKey = "feedbeef12340000feedbeef12340000feedbeef12340000feedbeef12340000"; + ethWalletManager.initialize(); + await ethWalletManager.loadWalletState(); + let walletState = ethWalletManager.getWalletState(); + expect(!!walletState).equal(true, "no wallet state"); + expect(walletState.ready).equal(true, "wallet state not ready"); + expect(walletState.nonce).equal(42, "unexpected nonce in wallet state"); + expect(walletState.balance).equal(1000n, "unexpected balance in wallet state"); + expect(walletState.nativeBalance).equal(1000n, "unexpected balance in wallet state"); + expect(ethWalletManager.getFaucetAddress()).equal("0xCA9456991E0AA5d5321e88Bba44d405aAb401193", "unexpected wallet address"); + expect(ethWalletManager.getFaucetBalance()).equal(1000n, "unexpected balance"); + }); + it("check wallet state initialization (pending not supported)", async () => { + let ethWalletManager = new EthWalletManager(); + fakeProvider.injectResponse("eth_chainId", 1337); + fakeProvider.injectResponse("eth_getBalance", (payload) => { + if(payload.params[1] === "pending") + throw '"pending" is not yet supported'; + return "1000"; + }); + fakeProvider.injectResponse("eth_getTransactionCount", (payload) => { + if(payload.params[1] === "pending") + throw '"pending" is not yet supported'; + return 42; + }); + faucetConfig.ethWalletKey = "feedbeef12340000feedbeef12340000feedbeef12340000feedbeef12340000"; + ethWalletManager.initialize(); + await ethWalletManager.loadWalletState(); + let walletState = ethWalletManager.getWalletState(); + expect(!!walletState).equal(true, "no wallet state"); + expect(walletState.ready).equal(true, "wallet state not ready"); + expect(walletState.nonce).equal(42, "unexpected nonce in wallet state"); + expect(walletState.balance).equal(1000n, "unexpected balance in wallet state"); + expect(walletState.nativeBalance).equal(1000n, "unexpected balance in wallet state"); + expect(ethWalletManager.getFaucetAddress()).equal("0xCA9456991E0AA5d5321e88Bba44d405aAb401193", "unexpected wallet address"); + expect(ethWalletManager.getFaucetBalance()).equal(1000n, "unexpected balance"); + }); + it("check wallet state initialization (fixed chainId)", async () => { + let ethWalletManager = new EthWalletManager(); + fakeProvider.injectResponse("eth_getBalance", (payload) => { + if(payload.params[1] === "pending") + throw '"pending" is not yet supported'; + return "1000"; + }); + fakeProvider.injectResponse("eth_getTransactionCount", (payload) => { + if(payload.params[1] === "pending") + throw '"pending" is not yet supported'; + return 42; + }); + faucetConfig.ethWalletKey = "feedbeef12340000feedbeef12340000feedbeef12340000feedbeef12340000"; + faucetConfig.ethChainId = 1337; + ethWalletManager.initialize(); + await ethWalletManager.loadWalletState(); + let walletState = ethWalletManager.getWalletState(); + expect(!!walletState).equal(true, "no wallet state"); + expect(walletState.ready).equal(true, "wallet state not ready"); + expect(walletState.nonce).equal(42, "unexpected nonce in wallet state"); + expect(walletState.balance).equal(1000n, "unexpected balance in wallet state"); + expect(walletState.nativeBalance).equal(1000n, "unexpected balance in wallet state"); + expect(ethWalletManager.getFaucetAddress()).equal("0xCA9456991E0AA5d5321e88Bba44d405aAb401193", "unexpected wallet address"); + expect(ethWalletManager.getFaucetBalance()).equal(1000n, "unexpected balance"); + }); + it("check wallet state initialization (erc20 token)", async () => { + let ethWalletManager = new EthWalletManager(); + fakeProvider.injectResponse("eth_chainId", 1337); + fakeProvider.injectResponse("eth_getBalance", "1000"); + fakeProvider.injectResponse("eth_getTransactionCount", 42); + fakeProvider.injectResponse("eth_call", (payload) => { + switch(payload.params[0].data.substring(0, 10)) { + case "0x313ce567": // decimals() + return "0x0000000000000000000000000000000000000000000000000000000000000006"; // 6 + case "0x70a08231": // balanceOf() + return "0x000000000000000000000000000000000000000000000000000000e8d4a51000"; // 1000000000000 + default: + console.log("unknown call: ", payload); + } + }); + faucetConfig.faucetCoinType = FaucetCoinType.ERC20; + faucetConfig.faucetCoinContract = "0x0000000000000000000000000000000000001337"; + ethWalletManager.initialize(); + await ethWalletManager.loadWalletState(); + let walletState = ethWalletManager.getWalletState(); + expect(!!walletState).equal(true, "no wallet state"); + expect(walletState.ready).equal(true, "wallet state not ready"); + expect(walletState.nonce).equal(42, "unexpected nonce in wallet state"); + expect(walletState.balance).equal(1000000000000n, "unexpected balance in wallet state"); + expect(walletState.nativeBalance).equal(1000n, "unexpected balance in wallet state"); + expect(ethWalletManager.getTokenAddress()).equal("0x0000000000000000000000000000000000001337", "unexpected token address"); + }); + it("send ClaimTx transaction", async () => { + faucetConfig.ethWalletKey = "feedbeef12340000feedbeef12340000feedbeef12340000feedbeef12340000"; + faucetConfig.ethChainId = 1337; + faucetConfig.spareFundsAmount = 0; + faucetConfig.ethTxGasLimit = 21000; + faucetConfig.ethTxMaxFee = 100000000000; // 100 gwei + faucetConfig.ethTxPrioFee = 2000000000; // 2 gwei + faucetConfig.faucetDBFile = ":memory:"; + ServiceManager.InitService(FaucetStoreDB).initialize(); + let ethWalletManager = ServiceManager.GetService(EthWalletManager); + let ethClaimManager = ServiceManager.GetService(EthClaimManager); + fakeProvider.injectResponse("eth_getBalance", "1000000000000000000"); // 1 ETH + fakeProvider.injectResponse("eth_getTransactionCount", 42); + let rawTxReq = []; + fakeProvider.injectResponse("eth_sendRawTransaction", (payload) => { + rawTxReq.push(payload); + return "0x1337b2933e4d908d44948ae7f8ec3184be10bbd67ba3c4b165be654281337337"; + }); + fakeProvider.injectResponse("eth_getTransactionReceipt", (payload) => { + return { + "blockHash": "0xfce202c4104864d81d8bd78b7202a77e5dca634914a3fd6636f2765d65fa9a07", + "blockNumber": "0x8aa5ae", + "contractAddress": null, + "cumulativeGasUsed": "0x1752665", + "effectiveGasPrice": "0x3b9aca00", // 1 gwei + "from": "0x917c0A57A0FaA917f8ac7cA8Dd52db0b906a59d2", + "gasUsed": "0x5208", // 21000 + "logs": [], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "status": "0x1", + "to": "0x0000000000000000000000000000000000001337", + "transactionHash": "0x1337b2933e4d908d44948ae7f8ec3184be10bbd67ba3c4b165be654281337337", + "transactionIndex": "0x3d", + "type": "0x2" + }; + }); + faucetConfig.ethWalletKey = "feedbeef12340000feedbeef12340000feedbeef12340000feedbeef12340000"; + ethWalletManager.initialize(); + await ethWalletManager.loadWalletState(); + let claimTx = ethClaimManager.addClaimTransaction("0X0000000000000000000000000000000000001337", 1337n, "f081154a-3b93-4972-9ae7-b83f3307bb0f"); + await ethClaimManager.processQueue(); + await awaitSleepPromise(200, () => claimTx.status === ClaimTxStatus.CONFIRMED); + expect(rawTxReq.length).to.equal(1, "unexpected transaction count"); + expect(rawTxReq[0].params[0]).to.equal("0x02f86f8205392a847735940085174876e80082520894000000000000000000000000000000000000133782053980c001a04787689fdfc3803c758feaaa7989761900c274488f1f656ec7aa277ae37294efa038b6fc22a7a4c1f0bf537a989f00c907413f5c3e333807e1bbadfb08f74926f5", "unexpected transaction hex"); + expect(claimTx.status).to.equal(ClaimTxStatus.CONFIRMED, "unexpected claimTx status"); + let walletState = ethWalletManager.getWalletState(); + expect(!!walletState).equal(true, "no wallet state"); + expect(walletState.ready).equal(true, "wallet state not ready"); + expect(walletState.nonce).equal(43, "unexpected nonce in wallet state"); + expect(walletState.balance).equal(999978999999998663n, "unexpected balance in wallet state"); + expect(walletState.nativeBalance).equal(999978999999998663n, "unexpected balance in wallet state"); + }); + it("send ClaimTx transaction (long confirmation time)", async () => { + faucetConfig.ethWalletKey = "feedbeef12340000feedbeef12340000feedbeef12340000feedbeef12340000"; + faucetConfig.ethChainId = 1337; + faucetConfig.spareFundsAmount = 0; + faucetConfig.ethTxGasLimit = 21000; + faucetConfig.ethTxMaxFee = 100000000000; // 100 gwei + faucetConfig.ethTxPrioFee = 2000000000; // 2 gwei + faucetConfig.faucetDBFile = ":memory:"; + ServiceManager.InitService(FaucetStoreDB).initialize(); + let ethWalletManager = ServiceManager.GetService(EthWalletManager); + let ethClaimManager = ServiceManager.GetService(EthClaimManager); + fakeProvider.injectResponse("eth_getBalance", "1000000000000000000"); // 1 ETH + fakeProvider.injectResponse("eth_getTransactionCount", 42); + fakeProvider.injectResponse("eth_subscribe", () => { throw "not supported" }); + let rawTxReq = []; + fakeProvider.injectResponse("eth_sendRawTransaction", (payload) => { + rawTxReq.push(payload); + return "0x1337b2933e4d908d44948ae7f8ec3184be10bbd67ba3c4b165be654281337337"; + }); + let receiptResponseMode = "null"; + fakeProvider.injectResponse("eth_getTransactionReceipt", (payload) => { + if(receiptResponseMode === "null") { + return null + } + else if(receiptResponseMode === "conn") { + throw "test CONNECTION ERROR"; + } + return { + "blockHash": "0xfce202c4104864d81d8bd78b7202a77e5dca634914a3fd6636f2765d65fa9a07", + "blockNumber": "0x8aa5ae", + "contractAddress": null, + "cumulativeGasUsed": "0x1752665", + "effectiveGasPrice": "0x3b9aca00", // 1 gwei + "from": "0x917c0A57A0FaA917f8ac7cA8Dd52db0b906a59d2", + "gasUsed": "0x5208", // 21000 + "logs": [], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "status": "0x1", + "to": "0x0000000000000000000000000000000000001337", + "transactionHash": "0x1337b2933e4d908d44948ae7f8ec3184be10bbd67ba3c4b165be654281337337", + "transactionIndex": "0x3d", + "type": "0x2" + }; + }); + faucetConfig.ethWalletKey = "feedbeef12340000feedbeef12340000feedbeef12340000feedbeef12340000"; + ethWalletManager.initialize(); + (ethWalletManager as any).web3.eth.transactionPollingTimeout = 1; + (ethWalletManager as any).txReceiptPollInterval = 10; + await ethWalletManager.loadWalletState(); + let claimTx = ethClaimManager.addClaimTransaction("0x0000000000000000000000000000000000001337", 1337n, "f081154a-3b93-4972-9ae7-b83f3307bb0f"); + await ethClaimManager.processQueue(); + await sleepPromise(3000); // wait for timeout from web3js lib + receiptResponseMode = "conn"; + await sleepPromise(100); // do a few "connection error"-polls + receiptResponseMode = "receipt"; // now return the receipt + await awaitSleepPromise(1000, () => claimTx.status === ClaimTxStatus.CONFIRMED); + expect(rawTxReq.length).to.equal(1, "unexpected transaction count"); + expect(rawTxReq[0].params[0]).to.equal("0x02f86f8205392a847735940085174876e80082520894000000000000000000000000000000000000133782053980c001a04787689fdfc3803c758feaaa7989761900c274488f1f656ec7aa277ae37294efa038b6fc22a7a4c1f0bf537a989f00c907413f5c3e333807e1bbadfb08f74926f5", "unexpected transaction hex"); + expect(claimTx.status).to.equal(ClaimTxStatus.CONFIRMED, "unexpected claimTx status"); + let walletState = ethWalletManager.getWalletState(); + expect(!!walletState).equal(true, "no wallet state"); + expect(walletState.ready).equal(true, "wallet state not ready"); + expect(walletState.nonce).equal(43, "unexpected nonce in wallet state"); + expect(walletState.balance).equal(999978999999998663n, "unexpected balance in wallet state"); + expect(walletState.nativeBalance).equal(999978999999998663n, "unexpected balance in wallet state"); + }).timeout(5000); + it("send ClaimTx transaction (legacy transaction)", async () => { + faucetConfig.ethWalletKey = "feedbeef12340000feedbeef12340000feedbeef12340000feedbeef12340000"; + faucetConfig.ethChainId = 1337; + faucetConfig.spareFundsAmount = 0; + faucetConfig.ethTxGasLimit = 21000; + faucetConfig.ethTxMaxFee = 100000000000; // 100 gwei + faucetConfig.ethTxPrioFee = 2000000000; // 2 gwei + faucetConfig.ethLegacyTx = true; + faucetConfig.faucetDBFile = ":memory:"; + ServiceManager.InitService(FaucetStoreDB).initialize(); + let ethWalletManager = ServiceManager.GetService(EthWalletManager); + let ethClaimManager = ServiceManager.GetService(EthClaimManager); + fakeProvider.injectResponse("eth_getBalance", "1000000000000000000"); // 1 ETH + fakeProvider.injectResponse("eth_getTransactionCount", 42); + fakeProvider.injectResponse("eth_gasPrice", "150000000000"); // 150 gwei + let rawTxReq = []; + fakeProvider.injectResponse("eth_sendRawTransaction", (payload) => { + rawTxReq.push(payload); + return "0x1337b2933e4d908d44948ae7f8ec3184be10bbd67ba3c4b165be654281337337"; + }); + fakeProvider.injectResponse("eth_getTransactionReceipt", (payload) => { + return { + "blockHash": "0xfce202c4104864d81d8bd78b7202a77e5dca634914a3fd6636f2765d65fa9a07", + "blockNumber": "0x8aa5ae", + "contractAddress": null, + "cumulativeGasUsed": "0x1752665", + "effectiveGasPrice": "0x3b9aca00", // 1 gwei + "from": "0x917c0A57A0FaA917f8ac7cA8Dd52db0b906a59d2", + "gasUsed": "0x5208", // 21000 + "logs": [], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "status": "0x1", + "to": "0x0000000000000000000000000000000000001337", + "transactionHash": "0x1337b2933e4d908d44948ae7f8ec3184be10bbd67ba3c4b165be654281337337", + "transactionIndex": "0x3d", + "type": "0x2" + }; + }); + faucetConfig.ethWalletKey = "feedbeef12340000feedbeef12340000feedbeef12340000feedbeef12340000"; + ethWalletManager.initialize(); + await ethWalletManager.loadWalletState(); + let claimTx = ethClaimManager.addClaimTransaction("0x0000000000000000000000000000000000001337", 1337n, "f081154a-3b93-4972-9ae7-b83f3307bb0f"); + await ethClaimManager.processQueue(); + await awaitSleepPromise(200, () => claimTx.status === ClaimTxStatus.CONFIRMED); + expect(rawTxReq.length).to.equal(1, "unexpected transaction count"); + expect(rawTxReq[0].params[0]).to.equal("0xf8682a85174876e80082520894000000000000000000000000000000000000133782053980820a96a0537845eca3779f6925b8ca8459bf20a72189ceb3746e62d50ae5b7cfec5c83e8a025ecaf297265b4a5e5fcdd3f66c0184c3c4f103cfd5bf5dc2ffc2da9c7fa8ee0", "unexpected transaction hex"); + expect(claimTx.status).to.equal(ClaimTxStatus.CONFIRMED, "unexpected claimTx status"); + let walletState = ethWalletManager.getWalletState(); + expect(!!walletState).equal(true, "no wallet state"); + expect(walletState.ready).equal(true, "wallet state not ready"); + expect(walletState.nonce).equal(43, "unexpected nonce in wallet state"); + expect(walletState.balance).equal(999978999999998663n, "unexpected balance in wallet state"); + expect(walletState.nativeBalance).equal(999978999999998663n, "unexpected balance in wallet state"); + }); + it("send ClaimTx transaction (RPC error)", async () => { + faucetConfig.ethWalletKey = "feedbeef12340000feedbeef12340000feedbeef12340000feedbeef12340000"; + faucetConfig.ethChainId = 1337; + faucetConfig.spareFundsAmount = 0; + faucetConfig.ethTxGasLimit = 21000; + faucetConfig.ethTxMaxFee = 100000000000; // 100 gwei + faucetConfig.ethTxPrioFee = 2000000000; // 2 gwei + faucetConfig.faucetDBFile = ":memory:"; + ServiceManager.InitService(FaucetStoreDB).initialize(); + let ethWalletManager = ServiceManager.GetService(EthWalletManager); + let ethClaimManager = ServiceManager.GetService(EthClaimManager); + fakeProvider.injectResponse("eth_getBalance", "1000000000000000000"); // 1 ETH + fakeProvider.injectResponse("eth_getTransactionCount", 42); + fakeProvider.injectResponse("eth_sendRawTransaction", (payload) => { + throw "test error 57572x"; + }); + faucetConfig.ethWalletKey = "feedbeef12340000feedbeef12340000feedbeef12340000feedbeef12340000"; + ethWalletManager.initialize(); + await ethWalletManager.loadWalletState(); + let claimTx = ethClaimManager.addClaimTransaction("0X0000000000000000000000000000000000001337", 1337n, "f081154a-3b93-4972-9ae7-b83f3307bb0f"); + await ethClaimManager.processQueue(); + await awaitSleepPromise(10000, () => claimTx.status === ClaimTxStatus.FAILED); + expect(claimTx.status).to.equal(ClaimTxStatus.FAILED, "unexpected claimTx status"); + expect(claimTx.failReason).contains("test error 57572x", "test error not in failReason"); + let walletState = ethWalletManager.getWalletState(); + expect(!!walletState).equal(true, "no wallet state"); + expect(walletState.ready).equal(true, "wallet state not ready"); + expect(walletState.nonce).equal(42, "unexpected nonce in wallet state"); + expect(walletState.balance).equal(1000000000000000000n, "unexpected balance in wallet state"); + expect(walletState.nativeBalance).equal(1000000000000000000n, "unexpected balance in wallet state"); + }).timeout(10000); + it("send ClaimTx transaction (reverted transaction)", async () => { + faucetConfig.ethWalletKey = "feedbeef12340000feedbeef12340000feedbeef12340000feedbeef12340000"; + faucetConfig.ethChainId = 1337; + faucetConfig.spareFundsAmount = 0; + faucetConfig.ethTxGasLimit = 21000; + faucetConfig.ethTxMaxFee = 100000000000; // 100 gwei + faucetConfig.ethTxPrioFee = 2000000000; // 2 gwei + faucetConfig.faucetDBFile = ":memory:"; + ServiceManager.InitService(FaucetStoreDB).initialize(); + let ethWalletManager = ServiceManager.GetService(EthWalletManager); + let ethClaimManager = ServiceManager.GetService(EthClaimManager); + fakeProvider.injectResponse("eth_getBalance", "1000000000000000000"); // 1 ETH + fakeProvider.injectResponse("eth_getTransactionCount", 42); + fakeProvider.injectResponse("eth_sendRawTransaction", "0x1337b2933e4d908d44948ae7f8ec3184be10bbd67ba3c4b165be654281337337"); + fakeProvider.injectResponse("eth_getTransactionReceipt", { + "blockHash": "0xfce202c4104864d81d8bd78b7202a77e5dca634914a3fd6636f2765d65fa9a07", + "blockNumber": "0x8aa5ae", + "contractAddress": null, + "cumulativeGasUsed": "0x1752665", + "effectiveGasPrice": "0x3b9aca00", // 1 gwei + "from": "0x917c0A57A0FaA917f8ac7cA8Dd52db0b906a59d2", + "gasUsed": "0x5208", // 21000 + "logs": [], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "status": "0x0", + "to": "0x0000000000000000000000000000000000001337", + "transactionHash": "0x1337b2933e4d908d44948ae7f8ec3184be10bbd67ba3c4b165be654281337337", + "transactionIndex": "0x3d", + "type": "0x2" + }); + faucetConfig.ethWalletKey = "feedbeef12340000feedbeef12340000feedbeef12340000feedbeef12340000"; + ethWalletManager.initialize(); + await ethWalletManager.loadWalletState(); + let claimTx = ethClaimManager.addClaimTransaction("0X0000000000000000000000000000000000001337", 1337n, "f081154a-3b93-4972-9ae7-b83f3307bb0f"); + await ethClaimManager.processQueue(); + await awaitSleepPromise(200, () => claimTx.status === ClaimTxStatus.FAILED); + expect(claimTx.status).to.equal(ClaimTxStatus.FAILED, "unexpected claimTx status"); + let walletState = ethWalletManager.getWalletState(); + expect(!!walletState).equal(true, "no wallet state"); + expect(walletState.ready).equal(true, "wallet state not ready"); + expect(walletState.nonce).equal(43, "unexpected nonce in wallet state"); + expect(walletState.balance).equal(999999999999998663n, "unexpected balance in wallet state"); + expect(walletState.nativeBalance).equal(999999999999998663n, "unexpected balance in wallet state"); + }); + it("check wallet state initialization (erc20 token)", async () => { + faucetConfig.ethWalletKey = "feedbeef12340000feedbeef12340000feedbeef12340000feedbeef12340000"; + faucetConfig.ethChainId = 1337; + faucetConfig.spareFundsAmount = 0; + faucetConfig.ethTxGasLimit = 21000; + faucetConfig.ethTxMaxFee = 100000000000; // 100 gwei + faucetConfig.ethTxPrioFee = 2000000000; // 2 gwei + faucetConfig.faucetDBFile = ":memory:"; + ServiceManager.InitService(FaucetStoreDB).initialize(); + let ethWalletManager = ServiceManager.GetService(EthWalletManager); + let ethClaimManager = ServiceManager.GetService(EthClaimManager); + fakeProvider.injectResponse("eth_chainId", 1337); + fakeProvider.injectResponse("eth_getBalance", "1000000000000000000"); // 1 ETH + fakeProvider.injectResponse("eth_getTransactionCount", 42); + fakeProvider.injectResponse("eth_call", (payload) => { + switch(payload.params[0].data.substring(0, 10)) { + case "0x313ce567": // decimals() + return "0x0000000000000000000000000000000000000000000000000000000000000006"; // 6 + case "0x70a08231": // balanceOf() + return "0x000000000000000000000000000000000000000000000000000000e8d4a51000"; // 1000000000000 + default: + console.log("unknown call: ", payload); + } + }); + let rawTxReq = []; + fakeProvider.injectResponse("eth_sendRawTransaction", (payload) => { + rawTxReq.push(payload); + return "0x1337b2933e4d908d44948ae7f8ec3184be10bbd67ba3c4b165be654281331337"; + }); + fakeProvider.injectResponse("eth_getTransactionReceipt", (payload) => { + return { + "blockHash": "0xfce202c4104864d81d8bd78b7202a77e5dca634914a3fd6636f2765d65fa9a07", + "blockNumber": "0x8aa5ae", + "contractAddress": null, + "cumulativeGasUsed": "0x1752665", + "effectiveGasPrice": "0x3b9aca00", // 1 gwei + "from": "0x917c0A57A0FaA917f8ac7cA8Dd52db0b906a59d2", + "gasUsed": "0x5208", // 21000 + "logs": [], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "status": "0x1", + "to": "0x0000000000000000000000000000000000004242", + "transactionHash": "0x1337b2933e4d908d44948ae7f8ec3184be10bbd67ba3c4b165be654281331337", + "transactionIndex": "0x3d", + "type": "0x2" + }; + }); + faucetConfig.faucetCoinType = FaucetCoinType.ERC20; + faucetConfig.faucetCoinContract = "0x0000000000000000000000000000000000004242"; + ethWalletManager.initialize(); + await ethWalletManager.loadWalletState(); + let claimTx = ethClaimManager.addClaimTransaction("0x0000000000000000000000000000000000001337", 1337n, "f081154a-3b93-4972-9ae7-b83f3307bb0f"); + await ethClaimManager.processQueue(); + await awaitSleepPromise(200, () => claimTx.status === ClaimTxStatus.CONFIRMED); + expect(claimTx.status).to.equal(ClaimTxStatus.CONFIRMED, "unexpected claimTx status"); + expect(rawTxReq.length).to.equal(1, "unexpected transaction count"); + expect(rawTxReq[0].params[0]).to.equal("0x02f8b28205392a847735940085174876e80082520894000000000000000000000000000000000000424280b844a9059cbb00000000000000000000000000000000000000000000000000000000000013370000000000000000000000000000000000000000000000000000000000000539c001a002eca862f97badedde37bfbfd0ec047dc16e33bd1f73e20d24e284c6950c685ea03f975804b22ab748a52098907c87fcdb40520a9f7c11fe54721fa037c81e8055", "unexpected transaction hex"); + let walletState = ethWalletManager.getWalletState(); + expect(!!walletState).equal(true, "no wallet state"); + expect(walletState.ready).equal(true, "wallet state not ready"); + expect(walletState.nonce).equal(43, "unexpected nonce in wallet state"); + expect(walletState.balance).equal(999999998663n, "unexpected balance in wallet state"); + expect(walletState.nativeBalance).equal(999979000000000000n, "unexpected balance in wallet state"); + }); +}); diff --git a/tests/PoWClient.spec.ts b/tests/PoWClient.spec.ts index ec84886f5..8a35e9c9f 100644 --- a/tests/PoWClient.spec.ts +++ b/tests/PoWClient.spec.ts @@ -1,8 +1,7 @@ import 'mocha'; import sinon from 'sinon'; import { expect } from 'chai'; -import { RawData } from 'ws'; -import { awaitSleepPromise, bindTestStubs, FakeWebSocket, unbindTestStubs } from './common'; +import { awaitSleepPromise, bindTestStubs, FakePoWClient, FakeWebSocket, unbindTestStubs } from './common'; import { PoWClient } from "../src/websock/PoWClient"; import { faucetConfig, loadFaucetConfig } from '../src/common/FaucetConfig'; import { ServiceManager } from '../src/common/ServiceManager'; @@ -11,37 +10,6 @@ import { FaucetStoreDB } from '../src/services/FaucetStoreDB'; import { sleepPromise } from '../src/utils/SleepPromise'; import { PoWShareVerification } from '../src/websock/PoWShareVerification'; -class TestPoWClient extends PoWClient { - private sentMessages: { - action: string; - data: any; - rsp: any; - }[] = []; - - public emitClientMessage(data: RawData) { - return this.onClientMessage(data, false); - } - - public override sendMessage(action: string, data?: any, rsp?: any) { - this.sentMessages.push({ - action: action, - data: data, - rsp: rsp - }); - } - - public getSentMessage(action: string): any { - for(let i = 0; i < this.sentMessages.length; i++) { - if(this.sentMessages[i].action === action) - return this.sentMessages[i]; - } - } - - public clearSentMessages() { - this.sentMessages = []; - } -} - describe("WebSocket Client Handling", () => { let globalStubs; @@ -65,7 +33,7 @@ describe("WebSocket Client Handling", () => { describe("Client Lifecycle", () => { it("check error handling", async () => { let fakeSocket = new FakeWebSocket(); - let client = new TestPoWClient(fakeSocket, "8.8.8.8"); + let client = new FakePoWClient(fakeSocket, "8.8.8.8"); expect(PoWClient.getClientCount()).to.equal(1, "unexpected client count"); PoWClient.sendToAll("test"); let testMsg = client.getSentMessage("test"); @@ -78,7 +46,7 @@ describe("WebSocket Client Handling", () => { faucetConfig.powPingInterval = 1; faucetConfig.powPingTimeout = 2; let fakeSocket = new FakeWebSocket(); - let client = new TestPoWClient(fakeSocket, "8.8.8.8"); + let client = new FakePoWClient(fakeSocket, "8.8.8.8"); fakeSocket.emit("pong"); fakeSocket.emit("ping"); expect(globalStubs["FakeWebSocket.pong"].called).to.equal(true, "pong not called"); @@ -94,13 +62,13 @@ describe("WebSocket Client Handling", () => { }).timeout(5000); it("check invalid message handling", async () => { let fakeSocket = new FakeWebSocket(); - let client = new TestPoWClient(fakeSocket, "8.8.8.8"); + let client = new FakePoWClient(fakeSocket, "8.8.8.8"); await client.emitClientMessage("test" as any); expect(client.isReady()).to.equal(false, "client is still ready"); }); it("check unknown message handling", async () => { let fakeSocket = new FakeWebSocket(); - let client = new TestPoWClient(fakeSocket, "8.8.8.8"); + let client = new FakePoWClient(fakeSocket, "8.8.8.8"); await client.emitClientMessage('"test"' as any); await client.emitClientMessage(encodeClientMessage({ id: "test", @@ -117,7 +85,7 @@ describe("WebSocket Client Handling", () => { describe("Request Handling: getConfig", () => { it("valid getConfig call", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client.emitClientMessage(encodeClientMessage({ id: "test", action: "getConfig", @@ -133,7 +101,7 @@ describe("WebSocket Client Handling", () => { describe("Request Handling: startSession", () => { it("valid startSession call", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client.emitClientMessage(encodeClientMessage({ id: "test", action: "startSession", @@ -149,7 +117,7 @@ describe("WebSocket Client Handling", () => { expect(resultResponse?.data.startTime).to.be.gte(Math.floor(new Date().getTime()/1000) - 1, "invalid startTime"); }); it("invalid startSession call (malformed request)", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client.emitClientMessage(encodeClientMessage({ id: "test", action: "startSession", @@ -161,7 +129,7 @@ describe("WebSocket Client Handling", () => { expect(errorResponse?.data.code).to.equal("INVALID_REQUEST", "unexpected error code"); }); it("invalid startSession call (duplicate session)", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client.emitClientMessage(encodeClientMessage({ id: "test1", action: "startSession", @@ -190,7 +158,7 @@ describe("WebSocket Client Handling", () => { }); it("valid startSession call (mandatory ip check)", async () => { faucetConfig.ipInfoRequired = true; - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client.emitClientMessage(encodeClientMessage({ id: "test", action: "startSession", @@ -207,7 +175,7 @@ describe("WebSocket Client Handling", () => { }); it("rejected startSession call (faucet disabled)", async () => { faucetConfig.denyNewSessions = "Faucet disabled"; - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client.emitClientMessage(encodeClientMessage({ id: "test", action: "startSession", @@ -225,7 +193,7 @@ describe("WebSocket Client Handling", () => { it("valid startSession call (captcha verification)", async () => { faucetConfig.captchas.checkSessionStart = true; globalStubs["CaptchaVerifier.verifyToken"].resolves("test_ident"); - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client.emitClientMessage(encodeClientMessage({ id: "test", action: "startSession", @@ -243,7 +211,7 @@ describe("WebSocket Client Handling", () => { it("invalid startSession call (captcha verification)", async () => { faucetConfig.captchas.checkSessionStart = true; globalStubs["CaptchaVerifier.verifyToken"].resolves(false); - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client.emitClientMessage(encodeClientMessage({ id: "test", action: "startSession", @@ -260,7 +228,7 @@ describe("WebSocket Client Handling", () => { }); it("invalid startSession call (missing captcha token)", async () => { faucetConfig.captchas.checkSessionStart = true; - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client.emitClientMessage(encodeClientMessage({ id: "test", action: "startSession", @@ -276,7 +244,7 @@ describe("WebSocket Client Handling", () => { }); it("valid startSession call (ens name)", async () => { globalStubs["EnsResolver.resolveEnsName"].resolves("0x0000000000000000000000000000000000001337"); - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client.emitClientMessage(encodeClientMessage({ id: "test", action: "startSession", @@ -292,7 +260,7 @@ describe("WebSocket Client Handling", () => { }); it("invalid startSession call (ens name)", async () => { globalStubs["EnsResolver.resolveEnsName"].rejects("test_error"); - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client.emitClientMessage(encodeClientMessage({ id: "test", action: "startSession", @@ -308,7 +276,7 @@ describe("WebSocket Client Handling", () => { expect(errorResponse?.data.code).to.equal("INVALID_ENSNAME", "unexpected error code"); }); it("invalid startSession call (invalid address)", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client.emitClientMessage(encodeClientMessage({ id: "test", action: "startSession", @@ -326,7 +294,7 @@ describe("WebSocket Client Handling", () => { it("invalid startSession call (wallet balance exceeds limit)", async () => { globalStubs["EthWalletManager.getWalletBalance"].resolves("1000"); faucetConfig.claimAddrMaxBalance = 500; - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client.emitClientMessage(encodeClientMessage({ id: "test", action: "startSession", @@ -344,7 +312,7 @@ describe("WebSocket Client Handling", () => { it("invalid startSession call (wallet is contract)", async () => { globalStubs["EthWalletManager.checkIsContract"].resolves(true); faucetConfig.claimAddrDenyContract = true; - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client.emitClientMessage(encodeClientMessage({ id: "test", action: "startSession", @@ -364,7 +332,7 @@ describe("WebSocket Client Handling", () => { status: "failed", }); faucetConfig.ipInfoRequired = true; - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client.emitClientMessage(encodeClientMessage({ id: "test", action: "startSession", @@ -381,7 +349,7 @@ describe("WebSocket Client Handling", () => { }); it("invalid startSession call (concurrent session limit by ip)", async () => { faucetConfig.concurrentSessions = 1; - let client1 = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client1 = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client1.emitClientMessage(encodeClientMessage({ id: "test", action: "startSession", @@ -395,7 +363,7 @@ describe("WebSocket Client Handling", () => { expect(resultResponse?.rsp).to.equal("test", "response id mismatch"); expect(resultResponse?.data.targetAddr).to.equal("0x0000000000000000000000000000000000001337", "target address mismatch"); expect(resultResponse?.data.startTime).to.be.gte(Math.floor(new Date().getTime()/1000) - 1, "invalid startTime"); - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client.emitClientMessage(encodeClientMessage({ id: "test", action: "startSession", @@ -413,7 +381,7 @@ describe("WebSocket Client Handling", () => { it("invalid startSession call (concurrent session limit by addr)", async () => { faucetConfig.concurrentSessions = 1; faucetConfig.claimAddrCooldown = 0; - let client1 = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client1 = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client1.emitClientMessage(encodeClientMessage({ id: "test", action: "startSession", @@ -427,7 +395,7 @@ describe("WebSocket Client Handling", () => { expect(resultResponse?.rsp).to.equal("test", "response id mismatch"); expect(resultResponse?.data.targetAddr).to.equal("0x0000000000000000000000000000000000001337", "target address mismatch"); expect(resultResponse?.data.startTime).to.be.gte(Math.floor(new Date().getTime()/1000) - 1, "invalid startTime"); - let client = new TestPoWClient(new FakeWebSocket(), "8.8.4.4"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.4.4"); await client.emitClientMessage(encodeClientMessage({ id: "test", action: "startSession", @@ -444,7 +412,7 @@ describe("WebSocket Client Handling", () => { }); it("invalid startSession call (address cooldown)", async () => { faucetConfig.concurrentSessions = 1; - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client.emitClientMessage(encodeClientMessage({ id: "test1", action: "startSession", @@ -486,7 +454,7 @@ describe("WebSocket Client Handling", () => { describe("Request Handling: resumeSession", () => { it("valid resumeSession call", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); let session = new PoWSession(client, "0x0000000000000000000000000000000000001337"); session.setLastNonce(1337); client.setSession(null); @@ -504,7 +472,7 @@ describe("WebSocket Client Handling", () => { expect(resultResponse?.data.lastNonce).to.equal(1337, "lastNonce mismatch"); }); it("invalid resumeSession call (duplicate session)", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); let session1 = new PoWSession(client, "0x0000000000000000000000000000000000001337"); session1.setLastNonce(1337); client.setSession(null); @@ -524,7 +492,7 @@ describe("WebSocket Client Handling", () => { expect(errorResponse?.data.code).to.equal("INVALID_REQUEST", "unexpected error code"); }); it("invalid resumeSession call (malformed request)", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client.emitClientMessage(encodeClientMessage({ id: "test", action: "resumeSession", @@ -536,7 +504,7 @@ describe("WebSocket Client Handling", () => { expect(errorResponse?.data.code).to.equal("INVALID_REQUEST", "unexpected error code"); }); it("invalid resumeSession call (malformed guid)", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client.emitClientMessage(encodeClientMessage({ id: "test", action: "resumeSession", @@ -551,7 +519,7 @@ describe("WebSocket Client Handling", () => { expect(errorResponse?.data.code).to.equal("INVALID_SESSIONID", "unexpected error code"); }); it("invalid resumeSession call (unknown session id)", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client.emitClientMessage(encodeClientMessage({ id: "test", action: "resumeSession", @@ -566,7 +534,7 @@ describe("WebSocket Client Handling", () => { expect(errorResponse?.data.code).to.equal("INVALID_SESSIONID", "unexpected error code"); }); it("invalid resumeSession call (closed session)", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); let session = new PoWSession(client, "0x0000000000000000000000000000000000001337"); session.setLastNonce(1337); session.addBalance(BigInt(faucetConfig.claimMinAmount)); @@ -586,10 +554,10 @@ describe("WebSocket Client Handling", () => { expect(errorResponse?.data.data.balance).to.equal(faucetConfig.claimMinAmount.toString(), "invalid claim-token amount"); }); it("valid resumeSession call (duplicate connection, kill other client)", async () => { - let client1 = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client1 = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); let session = new PoWSession(client1, "0x0000000000000000000000000000000000001337"); session.setLastNonce(1337); - let client2 = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client2 = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client2.emitClientMessage(encodeClientMessage({ id: "test", action: "resumeSession", @@ -608,7 +576,7 @@ describe("WebSocket Client Handling", () => { describe("Request Handling: recoverSession", () => { it("valid recoverSession call", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); let session = new PoWSession(client, "0x0000000000000000000000000000000000001337"); session.setLastNonce(1337); session.addBalance(100n); @@ -626,7 +594,7 @@ describe("WebSocket Client Handling", () => { expect(resultResponse?.rsp).to.equal("test", "response id mismatch"); }); it("invalid recoverSession call (malformed request)", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client.emitClientMessage(encodeClientMessage({ id: "test", action: "recoverSession" @@ -638,7 +606,7 @@ describe("WebSocket Client Handling", () => { expect(errorResponse?.data.code).to.equal("INVALID_REQUEST", "unexpected error code"); }); it("invalid recoverSession call (duplicate session)", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); let session = new PoWSession(client, "0x0000000000000000000000000000000000001337"); session.setLastNonce(1337); session.addBalance(100n); @@ -659,7 +627,7 @@ describe("WebSocket Client Handling", () => { expect(errorResponse?.data.code).to.equal("INVALID_REQUEST", "unexpected error code"); }); it("invalid recoverSession call (invalid recovery data)", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client.emitClientMessage(encodeClientMessage({ id: "test", action: "recoverSession", @@ -672,7 +640,7 @@ describe("WebSocket Client Handling", () => { expect(errorResponse?.data.code).to.equal("INVALID_DATA", "unexpected error code"); }); it("invalid recoverSession call (session already known)", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); let session = new PoWSession(client, "0x0000000000000000000000000000000000001337"); session.setLastNonce(1337); session.addBalance(100n); @@ -691,8 +659,8 @@ describe("WebSocket Client Handling", () => { expect(errorResponse?.data.code).to.equal("DUPLICATE_SESSION", "unexpected error code"); }); it("invalid recoverSession call (concurrent session limit)", async () => { - let client1 = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); - let client2 = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client1 = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); + let client2 = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); let session = new PoWSession(client1, "0x0000000000000000000000000000000000001337"); session.setLastNonce(1337); session.addBalance(100n); @@ -713,7 +681,7 @@ describe("WebSocket Client Handling", () => { expect(errorResponse?.data.code).to.equal("CONCURRENCY_LIMIT", "unexpected error code"); }); it("invalid recoverSession call (closed session)", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); let session = new PoWSession(client, "0x0000000000000000000000000000000000001337"); session.setLastNonce(1337); session.addBalance(BigInt(faucetConfig.claimMinAmount)); @@ -731,7 +699,7 @@ describe("WebSocket Client Handling", () => { expect(errorResponse?.data.code).to.equal("INVALID_SESSION", "unexpected error code"); }); it("invalid recoverSession call (claim timeout)", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); let sessionTime = (new Date().getTime() / 1000) - faucetConfig.claimSessionTimeout - 1; let session = new PoWSession(client, { id: "f081154a-3b93-4972-9ae7-b83f3307bb0f", @@ -758,7 +726,7 @@ describe("WebSocket Client Handling", () => { expect(errorResponse?.data.code).to.equal("SESSION_TIMEOUT", "unexpected error code"); }); it("valid recoverSession call + immediate session close (session timeout)", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); let sessionTime = (new Date().getTime() / 1000) - faucetConfig.powSessionTimeout - 1; let session = new PoWSession(client, { id: "f081154a-3b93-4972-9ae7-b83f3307bb0f", @@ -791,7 +759,7 @@ describe("WebSocket Client Handling", () => { }); it("valid recoverSession call (mandatory ip check)", async () => { faucetConfig.ipInfoRequired = true; - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); let session = new PoWSession(client, "0x0000000000000000000000000000000000001337"); session.setLastNonce(1337); session.addBalance(100n); @@ -812,7 +780,7 @@ describe("WebSocket Client Handling", () => { describe("Request Handling: foundShare", () => { it("valid foundShare call", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); let sessionTime = (new Date().getTime() / 1000) - 42; let session = new PoWSession(client, { id: "f081154a-3b93-4972-9ae7-b83f3307bb0f", @@ -852,7 +820,7 @@ describe("WebSocket Client Handling", () => { expect(parseInt(balanceResponse?.data.balance)).to.be.at.least(1, "balance too low"); }); it("invalid foundShare call (malformed request)", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); let session = new PoWSession(client, "0x0000000000000000000000000000000000001337"); await client.emitClientMessage(encodeClientMessage({ id: "test", @@ -865,7 +833,7 @@ describe("WebSocket Client Handling", () => { expect(errorResponse?.data.code).to.equal("INVALID_SHARE", "unexpected error code"); }); it("invalid foundShare call (no active session)", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client.emitClientMessage(encodeClientMessage({ id: "test", action: "foundShare", @@ -882,7 +850,7 @@ describe("WebSocket Client Handling", () => { expect(errorResponse?.data.code).to.equal("SESSION_NOT_FOUND", "unexpected error code"); }); it("invalid foundShare call (invalid nonce count)", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); let session = new PoWSession(client, "0x0000000000000000000000000000000000001337"); faucetConfig.powNonceCount = 1; faucetConfig.powScryptParams = { @@ -908,7 +876,7 @@ describe("WebSocket Client Handling", () => { expect(errorResponse?.data.code).to.equal("INVALID_SHARE", "unexpected error code"); }); it("invalid foundShare call (pow params mismatch)", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); let sessionTime = (new Date().getTime() / 1000) - 42; let session = new PoWSession(client, { id: "f081154a-3b93-4972-9ae7-b83f3307bb0f", @@ -944,7 +912,7 @@ describe("WebSocket Client Handling", () => { expect(errorResponse?.data.code).to.equal("INVALID_SHARE", "unexpected error code"); }); it("invalid foundShare call (nonce too low)", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); let sessionTime = (new Date().getTime() / 1000) - 42; let session = new PoWSession(client, { id: "f081154a-3b93-4972-9ae7-b83f3307bb0f", @@ -980,7 +948,7 @@ describe("WebSocket Client Handling", () => { expect(errorResponse?.data.code).to.equal("INVALID_SHARE", "unexpected error code"); }); it("invalid foundShare call (nonce too high)", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); let sessionTime = (new Date().getTime() / 1000) - 42; let session = new PoWSession(client, { id: "f081154a-3b93-4972-9ae7-b83f3307bb0f", @@ -1021,7 +989,7 @@ describe("WebSocket Client Handling", () => { isValid: false, reward: 0n, }); - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); let sessionTime = (new Date().getTime() / 1000) - 42; let session = new PoWSession(client, { id: "f081154a-3b93-4972-9ae7-b83f3307bb0f", @@ -1061,8 +1029,8 @@ describe("WebSocket Client Handling", () => { describe("Request Handling: verifyResult", () => { it("valid verifyResult call", async () => { - let client1 = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); - let client2 = new TestPoWClient(new FakeWebSocket(), "8.8.4.4"); + let client1 = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); + let client2 = new FakePoWClient(new FakeWebSocket(), "8.8.4.4"); let sessionTime = (new Date().getTime() / 1000) - 42; let session1 = new PoWSession(client1, { id: "f081154a-3b93-4972-9ae7-b83f3307bb0f", @@ -1120,7 +1088,7 @@ describe("WebSocket Client Handling", () => { describe("Request Handling: closeSession", () => { it("valid closeSession call", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); let session = new PoWSession(client, "0x0000000000000000000000000000000000001337"); await client.emitClientMessage(encodeClientMessage({ id: "test", @@ -1131,7 +1099,7 @@ describe("WebSocket Client Handling", () => { expect(resultResponse?.rsp).to.equal("test", "response id mismatch"); }); it("valid closeSession call (with claimable balance)", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); let session = new PoWSession(client, "0x0000000000000000000000000000000000001337"); session.addBalance(BigInt(faucetConfig.claimMinAmount)); await client.emitClientMessage(encodeClientMessage({ @@ -1144,7 +1112,7 @@ describe("WebSocket Client Handling", () => { expect(resultResponse?.data.claimable).to.equal(true, "not claimable"); }); it("invalid closeSession call (no session)", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client.emitClientMessage(encodeClientMessage({ id: "test", action: "closeSession", @@ -1159,7 +1127,7 @@ describe("WebSocket Client Handling", () => { describe("Request Handling: claimRewards", () => { it("valid claimRewards call", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); let session = new PoWSession(client, "0x0000000000000000000000000000000000001337"); session.addBalance(BigInt(faucetConfig.claimMinAmount)); session.closeSession(true, true, "test"); @@ -1179,7 +1147,7 @@ describe("WebSocket Client Handling", () => { it("invalid claimRewards call (captcha verification)", async () => { faucetConfig.captchas.checkBalanceClaim = true; globalStubs["CaptchaVerifier.verifyToken"].resolves(false); - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); let session = new PoWSession(client, "0x0000000000000000000000000000000000001337"); session.addBalance(BigInt(faucetConfig.claimMinAmount)); session.closeSession(true, true, "test"); @@ -1199,7 +1167,7 @@ describe("WebSocket Client Handling", () => { expect(errorResponse?.data.code).to.equal("INVALID_CAPTCHA", "unexpected error code"); }); it("invalid claimRewards call (invalid token)", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client.emitClientMessage(encodeClientMessage({ id: "test", action: "claimRewards", @@ -1215,7 +1183,7 @@ describe("WebSocket Client Handling", () => { expect(errorResponse?.data.code).to.equal("INVALID_CLAIM", "unexpected error code"); }); it("invalid claimRewards call (expired claim)", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); let sessionTime = (new Date().getTime() / 1000) - faucetConfig.claimSessionTimeout - 2; let session = new PoWSession(client, { id: "f081154a-3b93-4972-9ae7-b83f3307bb0f", @@ -1246,7 +1214,7 @@ describe("WebSocket Client Handling", () => { describe("Request Handling: watchClaimTx", () => { it("valid watchClaimTx call", async () => { - let client1 = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client1 = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); let session = new PoWSession(client1, "0x0000000000000000000000000000000000001337"); session.addBalance(BigInt(faucetConfig.claimMinAmount)); session.closeSession(true, true, "test"); @@ -1262,7 +1230,7 @@ describe("WebSocket Client Handling", () => { let claimResultResponse = client1.getSentMessage("ok"); expect(claimResultResponse?.action).to.equal("ok", "no result response"); expect(claimResultResponse?.rsp).to.equal("test", "response id mismatch"); - let client2 = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client2 = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client2.emitClientMessage(encodeClientMessage({ id: "test", action: "watchClaimTx", @@ -1275,7 +1243,7 @@ describe("WebSocket Client Handling", () => { expect(resultResponse?.rsp).to.equal("test", "response id mismatch"); }); it("invalid watchClaimTx call (unknown session id)", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client.emitClientMessage(encodeClientMessage({ id: "test", action: "watchClaimTx", @@ -1293,7 +1261,7 @@ describe("WebSocket Client Handling", () => { describe("Request Handling: getClaimQueueState", () => { it("valid getClaimQueueState call", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client.emitClientMessage(encodeClientMessage({ id: "test", action: "getClaimQueueState", @@ -1306,7 +1274,7 @@ describe("WebSocket Client Handling", () => { describe("Request Handling: refreshBoost", () => { it("valid refreshBoost call", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); let session = new PoWSession(client, "0x0000000000000000000000000000000000001337"); await client.emitClientMessage(encodeClientMessage({ id: "test", @@ -1317,7 +1285,7 @@ describe("WebSocket Client Handling", () => { expect(resultResponse?.rsp).to.equal("test", "response id mismatch"); }); it("invalid refreshBoost call (unknown session id)", async () => { - let client = new TestPoWClient(new FakeWebSocket(), "8.8.8.8"); + let client = new FakePoWClient(new FakeWebSocket(), "8.8.8.8"); await client.emitClientMessage(encodeClientMessage({ id: "test", action: "refreshBoost", diff --git a/tests/PoWRewardLimiter.spec.ts b/tests/PoWRewardLimiter.spec.ts index 5c69ffca1..04a431e45 100644 --- a/tests/PoWRewardLimiter.spec.ts +++ b/tests/PoWRewardLimiter.spec.ts @@ -393,7 +393,10 @@ describe("Reward Restrictions", () => { }; fs.writeFileSync(patternFile, YAML.stringify(restrictions1)); - let rewardLimiter = new PoWRewardLimiter(); + let rewardLimiter = ServiceManager.GetService(PoWRewardLimiter); + rewardLimiter.refreshIpInfoMatchRestrictions(true); + session.updateRewardRestriction(); + let restriction = rewardLimiter.getSessionRestriction(session); expect(rewardLimiter.getShareReward(session)).equal(900000000000000000n, "unexpected getShareReward"); expect(rewardLimiter.getVerificationReward(session)).equal(180000000000000000n, "unexpected getVerificationReward"); @@ -403,7 +406,7 @@ describe("Reward Restrictions", () => { expect(restriction.messages[0].key).equal("key1", "unexpected message key in restriction"); fs.writeFileSync(patternFile, YAML.stringify(restrictions2)); - await sleepPromise(2200); + rewardLimiter.refreshIpInfoMatchRestrictions(true); session.updateRewardRestriction(); restriction = rewardLimiter.getSessionRestriction(session); diff --git a/tests/common.ts b/tests/common.ts index e001113b7..75b433583 100644 --- a/tests/common.ts +++ b/tests/common.ts @@ -1,6 +1,7 @@ import sinon from 'sinon'; -import { WebSocket } from 'ws'; +import { WebSocket, RawData } from 'ws'; +import { TypedEmitter } from 'tiny-typed-emitter'; import { FaucetProcess } from '../src/common/FaucetProcess'; import { ServiceManager } from '../src/common/ServiceManager'; import { CaptchaVerifier } from '../src/services/CaptchaVerifier'; @@ -10,6 +11,7 @@ import { IPInfoResolver } from '../src/services/IPInfoResolver'; import { PassportVerifier } from '../src/services/PassportVerifier'; import { sleepPromise } from '../src/utils/SleepPromise'; import { PoWSession } from '../src/websock/PoWSession'; +import { PoWClient } from '../src/websock/PoWClient'; let fakeWebSockets: FakeWebSocket[] = []; @@ -71,4 +73,100 @@ export class FakeWebSocket extends WebSocket { super(null); fakeWebSockets.push(this); } +} + +export class FakePoWClient extends PoWClient { + private sentMessages: { + action: string; + data: any; + rsp: any; + }[] = []; + + public emitClientMessage(data: RawData) { + return this.onClientMessage(data, false); + } + + public override sendMessage(action: string, data?: any, rsp?: any) { + this.sentMessages.push({ + action: action, + data: data, + rsp: rsp + }); + } + + public getSentMessage(action: string): any { + for(let i = 0; i < this.sentMessages.length; i++) { + if(this.sentMessages[i].action === action) + return this.sentMessages[i]; + } + } + + public clearSentMessages() { + this.sentMessages = []; + } +} + +export class FakeProvider extends TypedEmitter { + private idCounter = 1; + private responseDict: { + [method: string]: any + } = {}; + + public injectResponse(method: string, response: any) { + this.responseDict[method] = response; + } + + public send(payload) { + let response; + if(Array.isArray(payload)) + response = this.getResponses(payload); + else + response = this.getResponse(payload); + + return response; + } + + public sendAsync(payload, callback) { + let response; + if(Array.isArray(payload)) + response = this.getResponses(payload); + else + response = this.getResponse(payload); + + setTimeout(function(){ + callback(null, response); + }, 1); + } + + private getResponses(payloads) { + return payloads.map((payload) => this.getResponse(payload)); + } + + private getResponse(payload) { + //console.log("payload", JSON.stringify(payload, null, 2)); + let rsp = this.responseDict[payload.method]; + if(!rsp) { + console.log("no mock for request: ", payload); + } + let rspStub; + try { + if(typeof rsp === "function") + rsp = rsp(payload); + rspStub = { + jsonrpc: '2.0', + id: payload.id || this.idCounter++, + result: rsp + }; + } catch(ex) { + rspStub = { + jsonrpc: '2.0', + id: payload.id || this.idCounter++, + error: { + code: 1234, + message: 'Stub error: ' + ex?.toString() + } + }; + } + return rspStub; + } } \ No newline at end of file