From c3568075fd2ca4da9b6b7db06fb3e24df1e80d8e Mon Sep 17 00:00:00 2001 From: Rahul Sethuram Date: Mon, 5 Apr 2021 09:29:47 +0200 Subject: [PATCH 01/29] Move functions --- .../contracts/src.ts/services/ethService.ts | 356 +++++++++--------- 1 file changed, 178 insertions(+), 178 deletions(-) diff --git a/modules/contracts/src.ts/services/ethService.ts b/modules/contracts/src.ts/services/ethService.ts index a571e6ca9..da7844c4f 100644 --- a/modules/contracts/src.ts/services/ethService.ts +++ b/modules/contracts/src.ts/services/ethService.ts @@ -87,184 +87,6 @@ export class EthereumChainService extends EthereumChainReader implements IVector }); } - async sendDisputeChannelTx( - channelState: FullChannelState, - ): Promise> { - const method = "sendDisputeChannelTx"; - const methodId = getRandomBytes32(); - this.log.info({ method, methodId, channelAddress: channelState.channelAddress }, "Method started"); - const signer = this.signers.get(channelState.networkContext.chainId); - if (!signer?._isSigner) { - return Result.fail(new ChainError(ChainError.reasons.SignerNotFound)); - } - - if (!channelState.latestUpdate.aliceSignature || !channelState.latestUpdate.bobSignature) { - return Result.fail(new ChainError(ChainError.reasons.MissingSigs)); - } - - const code = await this.getCode(channelState.channelAddress, channelState.networkContext.chainId); - if (code.isError) { - return Result.fail(code.getError()!); - } - if (code.getValue() === "0x") { - this.log.info( - { method, methodId, channelAddress: channelState.channelAddress, chainId: channelState.networkContext.chainId }, - "Deploying channel", - ); - const gasPrice = await this.getGasPrice(channelState.networkContext.chainId); - if (gasPrice.isError) { - Result.fail(gasPrice.getError()!); - } - - const deploy = await this.sendDeployChannelTx(channelState, gasPrice.getValue()); - if (deploy.isError) { - return Result.fail(deploy.getError()!); - } - this.log.debug( - { method, methodId, channelAddress: channelState.channelAddress, transactionHash: deploy.getValue().hash }, - "Deploy channel tx", - ); - const result = await deploy.getValue().completed(); - if (result.isError) { - return Result.fail(result.getError()!); - } - this.log.info( - { - method, - methodId, - channelAddress: channelState.channelAddress, - transactionHash: result.getValue().transactionHash, - }, - "Channel deployed", - ); - } - - return this.sendTxWithRetries( - channelState.channelAddress, - channelState.networkContext.chainId, - TransactionReason.disputeChannel, - () => { - const channel = new Contract(channelState.channelAddress, VectorChannel.abi, signer); - return channel.disputeChannel( - channelState, - channelState.latestUpdate.aliceSignature, - channelState.latestUpdate.bobSignature, - ); - }, - ) as Promise>; - } - - async sendDefundChannelTx( - channelState: FullChannelState, - assetsToDefund: string[] = channelState.assetIds, - indices: string[] = [], - ): Promise> { - const signer = this.signers.get(channelState.networkContext.chainId); - if (!signer?._isSigner) { - return Result.fail(new ChainError(ChainError.reasons.SignerNotFound)); - } - - if (!channelState.latestUpdate.aliceSignature || !channelState.latestUpdate.bobSignature) { - return Result.fail(new ChainError(ChainError.reasons.MissingSigs)); - } - return this.sendTxWithRetries( - channelState.channelAddress, - channelState.networkContext.chainId, - TransactionReason.defundChannel, - () => { - const channel = new Contract(channelState.channelAddress, VectorChannel.abi, signer); - return channel.defundChannel( - channelState, - assetsToDefund, - indices.length > 0 ? indices : assetsToDefund.map((_asset, idx) => idx), - ); - }, - ) as Promise>; - } - - async sendDisputeTransferTx( - transferIdToDispute: string, - activeTransfers: FullTransferState[], - ): Promise> { - // Make sure transfer is active - const transferState = activeTransfers.find((t) => t.transferId === transferIdToDispute); - if (!transferState) { - return Result.fail( - new ChainError(ChainError.reasons.TransferNotFound, { - transfer: transferIdToDispute, - active: activeTransfers.map((t) => t.transferId), - }), - ); - } - - // Get signer - const signer = this.signers.get(transferState.chainId); - if (!signer?._isSigner) { - return Result.fail(new ChainError(ChainError.reasons.SignerNotFound)); - } - - // Generate merkle root - const hashes = activeTransfers.map((t) => bufferify(hashCoreTransferState(t))); - const hash = bufferify(hashCoreTransferState(transferState)); - const merkle = new MerkleTree(hashes, keccak256); - - return this.sendTxWithRetries( - transferState.channelAddress, - transferState.chainId, - TransactionReason.disputeTransfer, - () => { - const channel = new Contract(transferState.channelAddress, VectorChannel.abi, signer); - return channel.disputeTransfer(transferState, merkle.getHexProof(hash)); - }, - ) as Promise>; - } - - async sendDefundTransferTx( - transferState: FullTransferState, - responderSignature: string = HashZero, - ): Promise> { - const signer = this.signers.get(transferState.chainId); - if (!signer?._isSigner) { - return Result.fail(new ChainError(ChainError.reasons.SignerNotFound)); - } - - if (!transferState.transferResolver) { - return Result.fail(new ChainError(ChainError.reasons.ResolverNeeded)); - } - - const encodedState = encodeTransferState(transferState.transferState, transferState.transferEncodings[0]); - const encodedResolver = encodeTransferResolver(transferState.transferResolver, transferState.transferEncodings[1]); - - return this.sendTxWithRetries( - transferState.channelAddress, - transferState.chainId, - TransactionReason.defundTransfer, - () => { - const channel = new Contract(transferState.channelAddress, VectorChannel.abi, signer); - return channel.defundTransfer(transferState, encodedState, encodedResolver, responderSignature); - }, - ) as Promise>; - } - - public async sendExitChannelTx( - channelAddress: string, - chainId: number, - assetId: string, - owner: string, - recipient: string, - ): Promise> { - const signer = this.signers.get(chainId); - if (!signer?._isSigner) { - return Result.fail(new ChainError(ChainError.reasons.SignerNotFound)); - } - this.log.info({ channelAddress, chainId, assetId, owner, recipient }, "Defunding channel"); - - return this.sendTxWithRetries(channelAddress, chainId, TransactionReason.exitChannel, () => { - const channel = new Contract(channelAddress, VectorChannel.abi, signer); - return channel.exit(assetId, owner, recipient); - }) as Promise>; - } - public async sendDeployChannelTx( channelState: FullChannelState, gasPrice: BigNumber, @@ -1038,4 +860,182 @@ export class EthereumChainService extends EthereumChainReader implements IVector ) as Promise>; } } + + async sendDisputeChannelTx( + channelState: FullChannelState, + ): Promise> { + const method = "sendDisputeChannelTx"; + const methodId = getRandomBytes32(); + this.log.info({ method, methodId, channelAddress: channelState.channelAddress }, "Method started"); + const signer = this.signers.get(channelState.networkContext.chainId); + if (!signer?._isSigner) { + return Result.fail(new ChainError(ChainError.reasons.SignerNotFound)); + } + + if (!channelState.latestUpdate.aliceSignature || !channelState.latestUpdate.bobSignature) { + return Result.fail(new ChainError(ChainError.reasons.MissingSigs)); + } + + const code = await this.getCode(channelState.channelAddress, channelState.networkContext.chainId); + if (code.isError) { + return Result.fail(code.getError()!); + } + if (code.getValue() === "0x") { + this.log.info( + { method, methodId, channelAddress: channelState.channelAddress, chainId: channelState.networkContext.chainId }, + "Deploying channel", + ); + const gasPrice = await this.getGasPrice(channelState.networkContext.chainId); + if (gasPrice.isError) { + Result.fail(gasPrice.getError()!); + } + + const deploy = await this.sendDeployChannelTx(channelState, gasPrice.getValue()); + if (deploy.isError) { + return Result.fail(deploy.getError()!); + } + this.log.debug( + { method, methodId, channelAddress: channelState.channelAddress, transactionHash: deploy.getValue().hash }, + "Deploy channel tx", + ); + const result = await deploy.getValue().completed(); + if (result.isError) { + return Result.fail(result.getError()!); + } + this.log.info( + { + method, + methodId, + channelAddress: channelState.channelAddress, + transactionHash: result.getValue().transactionHash, + }, + "Channel deployed", + ); + } + + return this.sendTxWithRetries( + channelState.channelAddress, + channelState.networkContext.chainId, + TransactionReason.disputeChannel, + () => { + const channel = new Contract(channelState.channelAddress, VectorChannel.abi, signer); + return channel.disputeChannel( + channelState, + channelState.latestUpdate.aliceSignature, + channelState.latestUpdate.bobSignature, + ); + }, + ) as Promise>; + } + + async sendDefundChannelTx( + channelState: FullChannelState, + assetsToDefund: string[] = channelState.assetIds, + indices: string[] = [], + ): Promise> { + const signer = this.signers.get(channelState.networkContext.chainId); + if (!signer?._isSigner) { + return Result.fail(new ChainError(ChainError.reasons.SignerNotFound)); + } + + if (!channelState.latestUpdate.aliceSignature || !channelState.latestUpdate.bobSignature) { + return Result.fail(new ChainError(ChainError.reasons.MissingSigs)); + } + return this.sendTxWithRetries( + channelState.channelAddress, + channelState.networkContext.chainId, + TransactionReason.defundChannel, + () => { + const channel = new Contract(channelState.channelAddress, VectorChannel.abi, signer); + return channel.defundChannel( + channelState, + assetsToDefund, + indices.length > 0 ? indices : assetsToDefund.map((_asset, idx) => idx), + ); + }, + ) as Promise>; + } + + async sendDisputeTransferTx( + transferIdToDispute: string, + activeTransfers: FullTransferState[], + ): Promise> { + // Make sure transfer is active + const transferState = activeTransfers.find((t) => t.transferId === transferIdToDispute); + if (!transferState) { + return Result.fail( + new ChainError(ChainError.reasons.TransferNotFound, { + transfer: transferIdToDispute, + active: activeTransfers.map((t) => t.transferId), + }), + ); + } + + // Get signer + const signer = this.signers.get(transferState.chainId); + if (!signer?._isSigner) { + return Result.fail(new ChainError(ChainError.reasons.SignerNotFound)); + } + + // Generate merkle root + const hashes = activeTransfers.map((t) => bufferify(hashCoreTransferState(t))); + const hash = bufferify(hashCoreTransferState(transferState)); + const merkle = new MerkleTree(hashes, keccak256); + + return this.sendTxWithRetries( + transferState.channelAddress, + transferState.chainId, + TransactionReason.disputeTransfer, + () => { + const channel = new Contract(transferState.channelAddress, VectorChannel.abi, signer); + return channel.disputeTransfer(transferState, merkle.getHexProof(hash)); + }, + ) as Promise>; + } + + async sendDefundTransferTx( + transferState: FullTransferState, + responderSignature: string = HashZero, + ): Promise> { + const signer = this.signers.get(transferState.chainId); + if (!signer?._isSigner) { + return Result.fail(new ChainError(ChainError.reasons.SignerNotFound)); + } + + if (!transferState.transferResolver) { + return Result.fail(new ChainError(ChainError.reasons.ResolverNeeded)); + } + + const encodedState = encodeTransferState(transferState.transferState, transferState.transferEncodings[0]); + const encodedResolver = encodeTransferResolver(transferState.transferResolver, transferState.transferEncodings[1]); + + return this.sendTxWithRetries( + transferState.channelAddress, + transferState.chainId, + TransactionReason.defundTransfer, + () => { + const channel = new Contract(transferState.channelAddress, VectorChannel.abi, signer); + return channel.defundTransfer(transferState, encodedState, encodedResolver, responderSignature); + }, + ) as Promise>; + } + + public async sendExitChannelTx( + channelAddress: string, + chainId: number, + assetId: string, + owner: string, + recipient: string, + ): Promise> { + const signer = this.signers.get(chainId); + if (!signer?._isSigner) { + return Result.fail(new ChainError(ChainError.reasons.SignerNotFound)); + } + this.log.info({ channelAddress, chainId, assetId, owner, recipient }, "Defunding channel"); + + return this.sendTxWithRetries(channelAddress, chainId, TransactionReason.exitChannel, () => { + const channel = new Contract(channelAddress, VectorChannel.abi, signer); + return channel.exit(assetId, owner, recipient); + }) as Promise>; + } } From a52892365448e455cd478caacd60a3986dde0d84 Mon Sep 17 00:00:00 2001 From: Rahul Sethuram Date: Mon, 5 Apr 2021 15:51:59 +0200 Subject: [PATCH 02/29] Test sendDeployTx fn --- .../src.ts/services/ethService.spec.ts | 264 ++++++++---------- .../contracts/src.ts/services/ethService.ts | 4 +- .../integration}/ethReader.spec.ts | 6 +- .../tests/integration/ethService.spec.ts | 164 +++++++++++ modules/utils/src/test/expect.ts | 10 +- package-lock.json | 2 +- 6 files changed, 298 insertions(+), 152 deletions(-) rename modules/contracts/src.ts/{services => tests/integration}/ethReader.spec.ts (95%) create mode 100644 modules/contracts/src.ts/tests/integration/ethService.spec.ts diff --git a/modules/contracts/src.ts/services/ethService.spec.ts b/modules/contracts/src.ts/services/ethService.spec.ts index eaf33adc7..3850e3acb 100644 --- a/modules/contracts/src.ts/services/ethService.spec.ts +++ b/modules/contracts/src.ts/services/ethService.spec.ts @@ -1,164 +1,146 @@ -import { FullChannelState, FullTransferState, HashlockTransferStateEncoding } from "@connext/vector-types"; +import { + ChainError, + FullChannelState, + IChainServiceStore, + IChannelSigner, + Result, + TransactionResponseWithResult, +} from "@connext/vector-types"; import { ChannelSigner, - hashChannelCommitment, - createlockHash, - createTestChannelStateWithSigners, - createTestFullHashlockTransferState, + createTestChannelState, expect, - getRandomAddress, - getRandomBytes32, - hashCoreTransferState, - hashTransferState, + getBalanceForAssetId, + getTestLoggers, MemoryStoreService, + mkHash, } from "@connext/vector-utils"; -import { AddressZero } from "@ethersproject/constants"; -import { Contract } from "@ethersproject/contracts"; -import { keccak256 } from "@ethersproject/keccak256"; -import { parseEther } from "@ethersproject/units"; -import { BigNumber } from "@ethersproject/bignumber"; -import { deployments } from "hardhat"; -import { MerkleTree } from "merkletreejs"; - -import { alice, bob, chainIdReq, logger, provider, rando } from "../constants"; -import { advanceBlocktime, getContract, createChannel } from "../utils"; +import { AddressZero, One, Zero } from "@ethersproject/constants"; +import { JsonRpcProvider } from "@ethersproject/providers"; +import { BigNumber } from "ethers"; +import { restore, reset, createStubInstance, SinonStubbedInstance, stub } from "sinon"; import { EthereumChainService } from "./ethService"; -describe("EthereumChainService", function () { - this.timeout(120_000); - const aliceSigner = new ChannelSigner(alice.privateKey); - const bobSigner = new ChannelSigner(bob.privateKey); - let channel: Contract; - let channelFactory: Contract; - let transferDefinition: Contract; - let chainService: EthereumChainService; - let channelState: FullChannelState; - let transferState: FullTransferState; - let token: Contract; - let chainId: number; +let storeMock: SinonStubbedInstance; +let signer: SinonStubbedInstance; +let ethService: EthereumChainService; +let provider1337: SinonStubbedInstance; +let provider1338: SinonStubbedInstance; - beforeEach(async () => { - await deployments.fixture(); // Start w fresh deployments - chainId = await chainIdReq; - channel = await createChannel(); - channelFactory = await getContract("ChannelFactory", alice); - chainService = new EthereumChainService( - new MemoryStoreService(), - { [chainId]: provider }, - alice.privateKey, - logger, - ); - token = await getContract("TestToken", alice); - transferDefinition = await getContract("HashlockTransfer", alice); - await (await token.mint(alice.address, parseEther("1"))).wait(); - await (await token.mint(bob.address, parseEther("1"))).wait(); - const preImage = getRandomBytes32(); - const state = { - lockHash: createlockHash(preImage), - expiry: "0", - }; - transferState = createTestFullHashlockTransferState({ - chainId, - initiator: alice.address, - responder: bob.address, - transferDefinition: transferDefinition.address, - assetId: AddressZero, - channelAddress: channel.address, - // use random receiver addr to verify transfer when bob must dispute - balance: { to: [alice.address, getRandomAddress()], amount: ["7", "0"] }, - transferState: state, - transferResolver: { preImage }, - transferTimeout: "2", - initialStateHash: hashTransferState(state, HashlockTransferStateEncoding), - }); +const assertResult = (result: Result, isError: boolean, errorMessage?: string) => { + if (isError) { + expect(result.isError).to.be.true; + expect(result.getError()?.message).to.be.eq(errorMessage); + } +}; - channelState = createTestChannelStateWithSigners([aliceSigner, bobSigner], "create", { - channelAddress: channel.address, - assetIds: [AddressZero], - balances: [{ to: [alice.address, bob.address], amount: ["17", "45"] }], - processedDepositsA: ["0"], - processedDepositsB: ["62"], - timeout: "20", - nonce: 3, - merkleRoot: new MerkleTree([hashCoreTransferState(transferState)], keccak256).getHexRoot(), - }); - const channelHash = hashChannelCommitment(channelState); - channelState.latestUpdate.aliceSignature = await aliceSigner.signMessage(channelHash); - channelState.latestUpdate.bobSignature = await bobSigner.signMessage(channelHash); +const { log } = getTestLoggers("ethService"); +describe("ethService", () => { + beforeEach(() => { + storeMock = createStubInstance(MemoryStoreService); + signer = createStubInstance(ChannelSigner); + provider1337 = createStubInstance(JsonRpcProvider); + provider1338 = createStubInstance(JsonRpcProvider); + signer.connect.returns(signer as any); + (signer as any)._isSigner = true; + ethService = new EthereumChainService( + storeMock, + { + 1337: provider1337, + 1338: provider1338, + }, + signer, + log, + ); + stub(ethService, "getCode").resolves(Result.ok("0x")); + stub(ethService, "sendTxWithRetries").resolves( + Result.ok({ + chainId: 1337, + completed: () => Promise.resolve(Result.ok({} as any)), + confirmations: 1, + data: "0x", + from: AddressZero, + gasLimit: One, + gasPrice: One, + hash: mkHash(), + nonce: 1, + value: Zero, + wait: () => Promise.resolve({}), + } as TransactionResponseWithResult), + ); }); - it("should be created without error", async () => { - expect(channel.address).to.be.ok; - expect(chainService).to.be.ok; + afterEach(() => { + restore(); + reset(); }); - it("should run sendDepositTx without error", async () => { - const res = await chainService.sendDepositTx(channelState, alice.address, "10", AddressZero); - expect(res.getValue()).to.be.ok; - }); + describe("sendDeployChannelTx", () => { + let channelState: FullChannelState; - it("should run sendWithdrawTx without error", async () => { - const res = await chainService.sendWithdrawTx(channelState, { - to: bob.address, - data: "0x", - value: "0x01", + beforeEach(() => { + const test = createTestChannelState("create"); + channelState = test.channel; + channelState.networkContext.chainId = 1337; + signer.getAddress.resolves(channelState.alice); }); - expect(res.getValue()).to.be.ok; - }); - // Need to setup a channel between alice & rando else it'll error w "channel already deployed" - it("should run sendDeployChannelTx without error", async () => { - const channelAddress = ( - await chainService.getChannelAddress(alice.address, rando.address, channelFactory.address, chainId) - ).getValue(); - const res = await chainService.sendDeployChannelTx( - { - ...channelState, - bob: rando.address, - channelAddress, - }, - BigNumber.from(1000), - { - amount: "0x01", - assetId: AddressZero, - }, - ); - expect(res.getValue()).to.be.ok; - }); + it("errors if cannot get a signer", async () => { + channelState.networkContext.chainId = 1234; + const result = await ethService.sendDeployChannelTx(channelState, One); + assertResult(result, true, ChainError.reasons.SignerNotFound); + }); - it("should run sendDisputeChannelTx without error", async () => { - const res = await chainService.sendDisputeChannelTx(channelState); - expect(res.getValue()).to.be.ok; - }); + it("errors if multisig code cannot be retrieved", async () => { + stub(ethService, "getCode").resolves(Result.fail(new ChainError("getCode error"))); + const result = await ethService.sendDeployChannelTx(channelState, One); + assertResult(result, true, "getCode error"); + }); - it("should run sendDefundChannelTx without error", async () => { - await chainService.sendDisputeChannelTx(channelState); - await advanceBlocktime(BigNumber.from(channelState.timeout).toNumber()); - const res = await chainService.sendDefundChannelTx(channelState); - expect(res.getValue()).to.be.ok; - }); + it("errors if multisig is already deployed", async () => { + stub(ethService, "getCode").resolves(Result.ok(mkHash("0xabc"))); + const result = await ethService.sendDeployChannelTx(channelState, One); + assertResult(result, true, ChainError.reasons.MultisigDeployed); + }); - it("should run sendDisputeTransferTx without error", async () => { - await chainService.sendDisputeChannelTx(channelState); - await advanceBlocktime(BigNumber.from(channelState.timeout).toNumber()); - const res = await chainService.sendDisputeTransferTx(transferState.transferId, [transferState]); - expect(res.getValue()).to.be.ok; - }); + it("errors if multisig deployment fails without deposit", async () => { + stub(ethService, "sendTxWithRetries").resolves(Result.fail(new ChainError(ChainError.reasons.TxReverted))); + const result = await ethService.sendDeployChannelTx(channelState, One); + assertResult(result, true, ChainError.reasons.TxReverted); + }); - // Fails with INVALID_MSG_SENDER - it("should run sendDefundTransferTx without error", async () => { - await chainService.sendDisputeChannelTx(channelState); - await advanceBlocktime(BigNumber.from(channelState.timeout).toNumber()); - await chainService.sendDisputeTransferTx(transferState.transferId, [transferState]); - // Bob is the one who will defund, create a chainService for him to do so - const bobChainService = new EthereumChainService( - new MemoryStoreService(), - { [chainId]: provider }, - bob.privateKey, - logger, - ); - const res = await bobChainService.sendDefundTransferTx(transferState); - expect(res.getValue()).to.be.ok; + it("errors if multisig deployment returns nothing", async () => { + stub(ethService, "sendTxWithRetries").resolves(Result.ok(undefined)); + const result = await ethService.sendDeployChannelTx(channelState, One); + assertResult(result, true, ChainError.reasons.MultisigDeployed); + }); + + it("errors if deposit and is not alice", async () => { + signer.getAddress.resolves(channelState.bob); + const result = await ethService.sendDeployChannelTx(channelState, One, { + amount: "1", + assetId: AddressZero, + }); + assertResult(result, true, ChainError.reasons.FailedToDeploy); + }); + + it("errors if deposit and cannot get onchain balance", async () => { + stub(ethService, "getOnchainBalance").resolves(Result.fail(new ChainError(ChainError.reasons.TxNotFound))); + const result = await ethService.sendDeployChannelTx(channelState, One, { + amount: "1", + assetId: AddressZero, + }); + assertResult(result, true, ChainError.reasons.TxNotFound); + }); + + it("errors if deposit and not enough onchain balance", async () => { + stub(ethService, "getOnchainBalance").resolves(Result.ok(BigNumber.from("9"))); + const result = await ethService.sendDeployChannelTx(channelState, One, { + amount: "10", + assetId: AddressZero, + }); + assertResult(result, true, ChainError.reasons.NotEnoughFunds); + }); }); }); diff --git a/modules/contracts/src.ts/services/ethService.ts b/modules/contracts/src.ts/services/ethService.ts index da7844c4f..795fbbbd2 100644 --- a/modules/contracts/src.ts/services/ethService.ts +++ b/modules/contracts/src.ts/services/ethService.ts @@ -512,8 +512,8 @@ export class EthereumChainService extends EthereumChainReader implements IVector } //////////////////////////// - /// PRIVATE METHODS - private async sendTxWithRetries( + /// INTERNAL METHODS + public async sendTxWithRetries( channelAddress: string, chainId: number, reason: TransactionReason, diff --git a/modules/contracts/src.ts/services/ethReader.spec.ts b/modules/contracts/src.ts/tests/integration/ethReader.spec.ts similarity index 95% rename from modules/contracts/src.ts/services/ethReader.spec.ts rename to modules/contracts/src.ts/tests/integration/ethReader.spec.ts index 83fce1302..118a0f4ae 100644 --- a/modules/contracts/src.ts/services/ethReader.spec.ts +++ b/modules/contracts/src.ts/tests/integration/ethReader.spec.ts @@ -5,10 +5,10 @@ import { Contract } from "@ethersproject/contracts"; import { deployments } from "hardhat"; import pino from "pino"; -import { alice, bob, chainIdReq, provider } from "../constants"; -import { getContract, createChannel } from "../utils"; +import { alice, bob, chainIdReq, provider } from "../../constants"; +import { getContract, createChannel } from "../../utils"; -import { EthereumChainReader } from "./ethReader"; +import { EthereumChainReader } from "../../services/ethReader"; // TODO: check whether result is valid, not just whether it exists #432 describe("EthereumChainReader", function () { diff --git a/modules/contracts/src.ts/tests/integration/ethService.spec.ts b/modules/contracts/src.ts/tests/integration/ethService.spec.ts new file mode 100644 index 000000000..8e921d558 --- /dev/null +++ b/modules/contracts/src.ts/tests/integration/ethService.spec.ts @@ -0,0 +1,164 @@ +import { FullChannelState, FullTransferState, HashlockTransferStateEncoding } from "@connext/vector-types"; +import { + ChannelSigner, + hashChannelCommitment, + createlockHash, + createTestChannelStateWithSigners, + createTestFullHashlockTransferState, + expect, + getRandomAddress, + getRandomBytes32, + hashCoreTransferState, + hashTransferState, + MemoryStoreService, +} from "@connext/vector-utils"; +import { AddressZero } from "@ethersproject/constants"; +import { Contract } from "@ethersproject/contracts"; +import { keccak256 } from "@ethersproject/keccak256"; +import { parseEther } from "@ethersproject/units"; +import { BigNumber } from "@ethersproject/bignumber"; +import { deployments } from "hardhat"; +import { MerkleTree } from "merkletreejs"; + +import { alice, bob, chainIdReq, logger, provider, rando } from "../../constants"; +import { advanceBlocktime, getContract, createChannel } from "../../utils"; + +import { EthereumChainService } from "../../services/ethService"; + +describe("EthereumChainService", function () { + this.timeout(120_000); + const aliceSigner = new ChannelSigner(alice.privateKey); + const bobSigner = new ChannelSigner(bob.privateKey); + let channel: Contract; + let channelFactory: Contract; + let transferDefinition: Contract; + let chainService: EthereumChainService; + let channelState: FullChannelState; + let transferState: FullTransferState; + let token: Contract; + let chainId: number; + + beforeEach(async () => { + await deployments.fixture(); // Start w fresh deployments + chainId = await chainIdReq; + channel = await createChannel(); + channelFactory = await getContract("ChannelFactory", alice); + chainService = new EthereumChainService( + new MemoryStoreService(), + { [chainId]: provider }, + alice.privateKey, + logger, + ); + token = await getContract("TestToken", alice); + transferDefinition = await getContract("HashlockTransfer", alice); + await (await token.mint(alice.address, parseEther("1"))).wait(); + await (await token.mint(bob.address, parseEther("1"))).wait(); + const preImage = getRandomBytes32(); + const state = { + lockHash: createlockHash(preImage), + expiry: "0", + }; + transferState = createTestFullHashlockTransferState({ + chainId, + initiator: alice.address, + responder: bob.address, + transferDefinition: transferDefinition.address, + assetId: AddressZero, + channelAddress: channel.address, + // use random receiver addr to verify transfer when bob must dispute + balance: { to: [alice.address, getRandomAddress()], amount: ["7", "0"] }, + transferState: state, + transferResolver: { preImage }, + transferTimeout: "2", + initialStateHash: hashTransferState(state, HashlockTransferStateEncoding), + }); + + channelState = createTestChannelStateWithSigners([aliceSigner, bobSigner], "create", { + channelAddress: channel.address, + assetIds: [AddressZero], + balances: [{ to: [alice.address, bob.address], amount: ["17", "45"] }], + processedDepositsA: ["0"], + processedDepositsB: ["62"], + timeout: "20", + nonce: 3, + merkleRoot: new MerkleTree([hashCoreTransferState(transferState)], keccak256).getHexRoot(), + }); + const channelHash = hashChannelCommitment(channelState); + channelState.latestUpdate.aliceSignature = await aliceSigner.signMessage(channelHash); + channelState.latestUpdate.bobSignature = await bobSigner.signMessage(channelHash); + }); + + it("should be created without error", async () => { + expect(channel.address).to.be.ok; + expect(chainService).to.be.ok; + }); + + it("should run sendDepositTx without error", async () => { + const res = await chainService.sendDepositTx(channelState, alice.address, "10", AddressZero); + expect(res.getValue()).to.be.ok; + }); + + it("should run sendWithdrawTx without error", async () => { + const res = await chainService.sendWithdrawTx(channelState, { + to: bob.address, + data: "0x", + value: "0x01", + }); + expect(res.getValue()).to.be.ok; + }); + + // Need to setup a channel between alice & rando else it'll error w "channel already deployed" + it("should run sendDeployChannelTx without error", async () => { + const channelAddress = ( + await chainService.getChannelAddress(alice.address, rando.address, channelFactory.address, chainId) + ).getValue(); + const res = await chainService.sendDeployChannelTx( + { + ...channelState, + bob: rando.address, + channelAddress, + }, + BigNumber.from(1000), + { + amount: "0x01", + assetId: AddressZero, + }, + ); + expect(res.getValue()).to.be.ok; + }); + + it("should run sendDisputeChannelTx without error", async () => { + const res = await chainService.sendDisputeChannelTx(channelState); + expect(res.getValue()).to.be.ok; + }); + + it("should run sendDefundChannelTx without error", async () => { + await chainService.sendDisputeChannelTx(channelState); + await advanceBlocktime(BigNumber.from(channelState.timeout).toNumber()); + const res = await chainService.sendDefundChannelTx(channelState); + expect(res.getValue()).to.be.ok; + }); + + it("should run sendDisputeTransferTx without error", async () => { + await chainService.sendDisputeChannelTx(channelState); + await advanceBlocktime(BigNumber.from(channelState.timeout).toNumber()); + const res = await chainService.sendDisputeTransferTx(transferState.transferId, [transferState]); + expect(res.getValue()).to.be.ok; + }); + + // Fails with INVALID_MSG_SENDER + it("should run sendDefundTransferTx without error", async () => { + await chainService.sendDisputeChannelTx(channelState); + await advanceBlocktime(BigNumber.from(channelState.timeout).toNumber()); + await chainService.sendDisputeTransferTx(transferState.transferId, [transferState]); + // Bob is the one who will defund, create a chainService for him to do so + const bobChainService = new EthereumChainService( + new MemoryStoreService(), + { [chainId]: provider }, + bob.privateKey, + logger, + ); + const res = await bobChainService.sendDefundTransferTx(transferState); + expect(res.getValue()).to.be.ok; + }); +}); diff --git a/modules/utils/src/test/expect.ts b/modules/utils/src/test/expect.ts index 5c1253357..87670fe9b 100644 --- a/modules/utils/src/test/expect.ts +++ b/modules/utils/src/test/expect.ts @@ -1,10 +1,10 @@ -import { use } from "chai"; +import chai from "chai"; import promised from "chai-as-promised"; import subset from "chai-subset"; import { waffleChai } from "@ethereum-waffle/chai"; -use(subset); -use(promised); -use(waffleChai); +let chaiPlugin = chai.use(subset); +chaiPlugin = chaiPlugin.use(promised); +chaiPlugin = chaiPlugin.use(waffleChai); -export { expect } from "chai"; +export const expect = chaiPlugin.expect; diff --git a/package-lock.json b/package-lock.json index d85b900dd..cb4ba6cc5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "vector", - "version": "0.2.2-beta.1", + "version": "0.2.2-beta.2", "lockfileVersion": 1, "requires": true, "dependencies": { From 6c7d6e29430597fa2d9b8523d88a8ce8e899611c Mon Sep 17 00:00:00 2001 From: Rahul Sethuram Date: Mon, 5 Apr 2021 18:13:48 +0200 Subject: [PATCH 03/29] Moar tests --- .../src.ts/services/ethService.spec.ts | 81 ++++++++++++++----- .../contracts/src.ts/services/ethService.ts | 8 +- 2 files changed, 63 insertions(+), 26 deletions(-) diff --git a/modules/contracts/src.ts/services/ethService.spec.ts b/modules/contracts/src.ts/services/ethService.spec.ts index 3850e3acb..f0377dcaa 100644 --- a/modules/contracts/src.ts/services/ethService.spec.ts +++ b/modules/contracts/src.ts/services/ethService.spec.ts @@ -13,12 +13,13 @@ import { getBalanceForAssetId, getTestLoggers, MemoryStoreService, + mkAddress, mkHash, } from "@connext/vector-utils"; import { AddressZero, One, Zero } from "@ethersproject/constants"; -import { JsonRpcProvider } from "@ethersproject/providers"; +import { JsonRpcProvider, TransactionReceipt } from "@ethersproject/providers"; import { BigNumber } from "ethers"; -import { restore, reset, createStubInstance, SinonStubbedInstance, stub } from "sinon"; +import { restore, reset, createStubInstance, SinonStubbedInstance, stub, SinonStub } from "sinon"; import { EthereumChainService } from "./ethService"; @@ -27,14 +28,37 @@ let signer: SinonStubbedInstance; let ethService: EthereumChainService; let provider1337: SinonStubbedInstance; let provider1338: SinonStubbedInstance; +let sendTxWithRetriesMock: SinonStub; +let approveMock: SinonStub; -const assertResult = (result: Result, isError: boolean, errorMessage?: string) => { +const assertResult = (result: Result, isError: boolean, unwrappedVal?: string) => { if (isError) { expect(result.isError).to.be.true; - expect(result.getError()?.message).to.be.eq(errorMessage); + if (unwrappedVal) { + expect(result.getError()?.message).to.be.eq(unwrappedVal); + } + } else { + expect(result.isError).to.be.false; + if (unwrappedVal) { + expect(result.getValue()).to.be.eq(unwrappedVal); + } } }; +const txResponse: TransactionResponseWithResult = { + chainId: 1337, + completed: () => Promise.resolve(Result.ok({} as any)), + confirmations: 1, + data: "0x", + from: AddressZero, + gasLimit: One, + gasPrice: One, + hash: mkHash(), + nonce: 1, + value: Zero, + wait: () => Promise.resolve({} as TransactionReceipt), +}; + const { log } = getTestLoggers("ethService"); describe("ethService", () => { beforeEach(() => { @@ -54,21 +78,11 @@ describe("ethService", () => { log, ); stub(ethService, "getCode").resolves(Result.ok("0x")); - stub(ethService, "sendTxWithRetries").resolves( - Result.ok({ - chainId: 1337, - completed: () => Promise.resolve(Result.ok({} as any)), - confirmations: 1, - data: "0x", - from: AddressZero, - gasLimit: One, - gasPrice: One, - hash: mkHash(), - nonce: 1, - value: Zero, - wait: () => Promise.resolve({}), - } as TransactionResponseWithResult), - ); + sendTxWithRetriesMock = stub(ethService, "sendTxWithRetries"); + approveMock = stub(ethService, "approveTokens"); + approveMock.resolves(Result.ok(txResponse)); + sendTxWithRetriesMock.resolves(Result.ok(txResponse)); + stub(ethService, "getOnchainBalance").resolves(Result.ok(BigNumber.from("100"))); }); afterEach(() => { @@ -142,5 +156,34 @@ describe("ethService", () => { }); assertResult(result, true, ChainError.reasons.NotEnoughFunds); }); + + it("calls sendDepositATx with native asset if eth deposit + multisig deployed", async () => { + const result = await ethService.sendDeployChannelTx(channelState, One, { + amount: "1", + assetId: AddressZero, + }); + assertResult(result, false); + const call = sendTxWithRetriesMock.getCall(0); + expect(call.args[0]).to.eq(channelState.channelAddress); + expect(call.args[1]).to.eq(channelState.networkContext.chainId); + expect(call.args[2]).to.eq("deployWithDepositAlice"); + }); + + it.only("calls sendDepositATx with tokens if eth deposit + multisig deployed", async () => { + const result = await ethService.sendDeployChannelTx(channelState, One, { + amount: "1", + assetId: mkAddress("0xa"), + }); + assertResult(result, false); + const approveCall = approveMock.getCall(0); + console.log("approveCall: ", approveCall); + expect(approveCall.args[0]).to.eq(channelState.channelAddress); + expect(approveCall.args[1]).to.eq(channelState.networkContext.chainId); + expect(approveCall.args[2]).to.eq("deployWithDepositAlice"); + const call = sendTxWithRetriesMock.getCall(0); + expect(call.args[0]).to.eq(channelState.channelAddress); + expect(call.args[1]).to.eq(channelState.networkContext.chainId); + expect(call.args[2]).to.eq("deployWithDepositAlice"); + }); }); }); diff --git a/modules/contracts/src.ts/services/ethService.ts b/modules/contracts/src.ts/services/ethService.ts index 795fbbbd2..717fc9a5b 100644 --- a/modules/contracts/src.ts/services/ethService.ts +++ b/modules/contracts/src.ts/services/ethService.ts @@ -197,12 +197,6 @@ export class EthereumChainService extends EthereumChainReader implements IVector return this.sendDepositATx(channelState, amount, AddressZero, gasPrice); } // otherwise deploy with deposit - const data = new Interface(ChannelFactory.abi).encodeFunctionData("createChannelAndDepositAlice", [ - channelState.alice, - channelState.bob, - assetId, - amount, - ]); return channelFactory.createChannelAndDepositAlice(channelState.alice, channelState.bob, assetId, amount, { value: amount, gasPrice, @@ -665,7 +659,7 @@ export class EthereumChainService extends EthereumChainReader implements IVector } } - private async approveTokens( + public async approveTokens( channelAddress: string, spender: string, owner: string, From 729d3f1631c6134700f04b3ad14ff3dbee6a4230 Mon Sep 17 00:00:00 2001 From: Rahul Sethuram Date: Mon, 5 Apr 2021 19:04:23 +0200 Subject: [PATCH 04/29] Canceled --- modules/engine/src/listeners.ts | 77 +++++++++++++++++---------------- 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/modules/engine/src/listeners.ts b/modules/engine/src/listeners.ts index e42799a16..6b10e94a3 100644 --- a/modules/engine/src/listeners.ts +++ b/modules/engine/src/listeners.ts @@ -36,17 +36,8 @@ import { IVectorStore, DEFAULT_FEE_EXPIRY, DEFAULT_CHANNEL_TIMEOUT, - SIMPLE_WITHDRAWAL_GAS_ESTIMATE, - GAS_ESTIMATES, } from "@connext/vector-types"; -import { - getRandomBytes32, - TESTNETS_WITH_FEES, - hashWithdrawalQuote, - recoverAddressFromChannelMessage, - safeJsonStringify, - mkSig, -} from "@connext/vector-utils"; +import { getRandomBytes32, hashWithdrawalQuote, mkSig } from "@connext/vector-utils"; import { getAddress } from "@ethersproject/address"; import { BigNumber } from "@ethersproject/bignumber"; import { Zero } from "@ethersproject/constants"; @@ -762,7 +753,7 @@ async function handleWithdrawalTransferCreation( ); } -async function handleWithdrawalTransferResolution( +export async function handleWithdrawalTransferResolution( event: ChannelUpdateEvent, signer: IChannelSigner, store: IEngineStore, @@ -826,23 +817,27 @@ async function handleWithdrawalTransferResolution( "Withdrawal info", ); - // Generate commitment, and save the double signed version - const commitment = new WithdrawCommitment( - channelAddress, - alice, - bob, - event.updatedTransfer.balance.to[0], - assetId, - withdrawalAmount.toString(), - event.updatedTransfer.transferState.nonce, - event.updatedTransfer.transferState.callTo, - event.updatedTransfer.transferState.callData, - meta?.transactionHash ?? undefined, - ); - await commitment.addSignatures( - event.updatedTransfer.transferState.initiatorSignature, - event.updatedTransfer.transferResolver!.responderSignature, - ); + // if the transfer got cancelled, do not create commitment + let commitment: WithdrawCommitment | undefined; + if (event.updatedTransfer.transferResolver!.responderSignature !== mkSig("0x0")) { + // Generate commitment, and save the double signed version + commitment = new WithdrawCommitment( + channelAddress, + alice, + bob, + event.updatedTransfer.balance.to[0], + assetId, + withdrawalAmount.toString(), + event.updatedTransfer.transferState.nonce, + event.updatedTransfer.transferState.callTo, + event.updatedTransfer.transferState.callData, + meta?.transactionHash ?? undefined, + ); + await commitment.addSignatures( + event.updatedTransfer.transferState.initiatorSignature, + event.updatedTransfer.transferResolver!.responderSignature, + ); + } // Post to evt const assetIdx = assetIds.findIndex((a) => getAddress(a) === getAddress(assetId)); @@ -858,10 +853,19 @@ async function handleWithdrawalTransferResolution( transfer: event.updatedTransfer, callTo: event.updatedTransfer.transferState.callTo, callData: event.updatedTransfer.transferState.callData, - transaction: commitment.getSignedTransaction(), + transaction: commitment?.getSignedTransaction(), }; evts[EngineEvents.WITHDRAWAL_RESOLVED].post(payload); + // if the transfer got cancelled, no need to do anything + if (event.updatedTransfer.transferResolver!.responderSignature === mkSig("0x0")) { + logger.warn( + { method, methodId, withdrawalAmount: withdrawalAmount.toString(), assetId, transfer: event.updatedTransfer }, + "Withdrawal was cancelled", + ); + return; + } + // If it is not from counterparty, do not respond if (fromIdentifier === signer.publicIdentifier) { logger.debug( @@ -870,12 +874,11 @@ async function handleWithdrawalTransferResolution( ); return; } - // Try to submit the transaction to chain IFF you are alice // Otherwise, alice should have submitted the tx (hash is in meta) if (signer.address !== alice) { // Store the double signed commitment - await store.saveWithdrawalCommitment(transferId, commitment.toJson()); + await store.saveWithdrawalCommitment(transferId, commitment!.toJson()); // Withdrawal resolution meta will include the transaction hash, // post to EVT here evts[WITHDRAWAL_RECONCILED_EVENT].post({ @@ -899,7 +902,7 @@ async function handleWithdrawalTransferResolution( // will *NOT* be alice. This is safe because this assumption is also made // on setup when the browser-node will always `requestSetup` if (event.updatedChannelState.networkContext.chainId === 1) { - await store.saveWithdrawalCommitment(transferId, commitment.toJson()); + await store.saveWithdrawalCommitment(transferId, commitment!.toJson()); logger.debug({ method, channel: event.updatedChannelState.channelAddress }, "Holding mainnet withdrawal"); return; } @@ -907,12 +910,12 @@ async function handleWithdrawalTransferResolution( // Submit withdrawal to chain IFF not mainnet const withdrawalResponse = await chainService.sendWithdrawTx( event.updatedChannelState, - commitment.getSignedTransaction(), + commitment!.getSignedTransaction(), ); if (withdrawalResponse.isError) { // Store the double signed commitment - await store.saveWithdrawalCommitment(transferId, commitment.toJson()); + await store.saveWithdrawalCommitment(transferId, commitment!.toJson()); logger.warn( { method, error: withdrawalResponse.getError()!.message, channelAddress, transferId }, "Failed to submit withdraw", @@ -920,8 +923,8 @@ async function handleWithdrawalTransferResolution( return; } const tx = withdrawalResponse.getValue(); - commitment.addTransaction(tx.hash); - await store.saveWithdrawalCommitment(transferId, commitment.toJson()); + commitment!.addTransaction(tx.hash); + await store.saveWithdrawalCommitment(transferId, commitment!.toJson()); // alice submitted her own withdrawal, post to evt evts[WITHDRAWAL_RECONCILED_EVENT].post({ @@ -940,7 +943,7 @@ async function handleWithdrawalTransferResolution( } } -const isWithdrawTransfer = async ( +export const isWithdrawTransfer = async ( transfer: FullTransferState, chainAddresses: ChainAddresses, chainService: IVectorChainReader, From 67f42516d9593e29bb1695ab86e8df6587d68c97 Mon Sep 17 00:00:00 2001 From: Rahul Sethuram Date: Tue, 6 Apr 2021 18:05:10 +0200 Subject: [PATCH 05/29] Fixing tests --- .../src.ts/services/ethService.spec.ts | 137 +++++++++++++++++- 1 file changed, 131 insertions(+), 6 deletions(-) diff --git a/modules/contracts/src.ts/services/ethService.spec.ts b/modules/contracts/src.ts/services/ethService.spec.ts index f0377dcaa..ac96bd9f6 100644 --- a/modules/contracts/src.ts/services/ethService.spec.ts +++ b/modules/contracts/src.ts/services/ethService.spec.ts @@ -3,6 +3,7 @@ import { FullChannelState, IChainServiceStore, IChannelSigner, + MinimalTransaction, Result, TransactionResponseWithResult, } from "@connext/vector-types"; @@ -10,10 +11,10 @@ import { ChannelSigner, createTestChannelState, expect, - getBalanceForAssetId, getTestLoggers, MemoryStoreService, mkAddress, + mkBytes32, mkHash, } from "@connext/vector-utils"; import { AddressZero, One, Zero } from "@ethersproject/constants"; @@ -25,7 +26,7 @@ import { EthereumChainService } from "./ethService"; let storeMock: SinonStubbedInstance; let signer: SinonStubbedInstance; -let ethService: EthereumChainService; +let ethService: SinonStubbedInstance; let provider1337: SinonStubbedInstance; let provider1338: SinonStubbedInstance; let sendTxWithRetriesMock: SinonStub; @@ -68,7 +69,9 @@ describe("ethService", () => { provider1338 = createStubInstance(JsonRpcProvider); signer.connect.returns(signer as any); (signer as any)._isSigner = true; - ethService = new EthereumChainService( + ethService = createStubInstance(EthereumChainService); + (ethService as any).signers; + let _ethService = new EthereumChainService( storeMock, { 1337: provider1337, @@ -157,7 +160,130 @@ describe("ethService", () => { assertResult(result, true, ChainError.reasons.NotEnoughFunds); }); - it("calls sendDepositATx with native asset if eth deposit + multisig deployed", async () => { + it("sendDepositATx with tokens if eth deposit + multisig deployed, error on approve", async () => { + approveMock.resolves(Result.fail(new ChainError(ChainError.reasons.NotEnoughFunds))); + const result = await ethService.sendDeployChannelTx(channelState, One, { + amount: "1", + assetId: mkAddress("0xa"), + }); + assertResult(result, true, ChainError.reasons.NotEnoughFunds); + }); + + it("happy: calls sendDepositATx with native asset if eth deposit + multisig deployed", async () => { + const result = await ethService.sendDeployChannelTx(channelState, One, { + amount: "1", + assetId: AddressZero, + }); + assertResult(result, false); + const call = sendTxWithRetriesMock.getCall(0); + expect(call.args[0]).to.eq(channelState.channelAddress); + expect(call.args[1]).to.eq(channelState.networkContext.chainId); + expect(call.args[2]).to.eq("deployWithDepositAlice"); + }); + + it("happy: calls sendDepositATx with tokens if eth deposit + multisig deployed", async () => { + const result = await ethService.sendDeployChannelTx(channelState, One, { + amount: "1", + assetId: mkAddress("0xa"), + }); + assertResult(result, false); + const approveCall = approveMock.getCall(0); + expect(approveCall.args[0]).to.eq(channelState.channelAddress); + expect(approveCall.args[1]).to.eq(channelState.networkContext.chainId); + expect(approveCall.args[2]).to.eq("deployWithDepositAlice"); + const call = sendTxWithRetriesMock.getCall(0); + expect(call.args[0]).to.eq(channelState.channelAddress); + expect(call.args[1]).to.eq(channelState.networkContext.chainId); + expect(call.args[2]).to.eq("deployWithDepositAlice"); + }); + }); + + describe.skip("sendWithdrawTx", () => { + let channelState: FullChannelState; + const minTx: MinimalTransaction = { + data: mkBytes32("0xabc"), + to: AddressZero, + value: 0, + }; + let sendDeployChannelTxMock: SinonStub; + + beforeEach(() => { + const test = createTestChannelState("create"); + channelState = test.channel; + channelState.networkContext.chainId = 1337; + signer.getAddress.resolves(channelState.alice); + sendDeployChannelTxMock = stub(ethService, "sendDeployChannelTx"); + sendDeployChannelTxMock.resolves(); + ethService; + }); + + it.only("errors if cannot get a signer", async () => { + channelState.networkContext.chainId = 1234; + const result = await ethService.sendWithdrawTx(channelState, minTx); + assertResult(result, true, ChainError.reasons.SignerNotFound); + }); + + it("errors if multisig code cannot be retrieved", async () => { + stub(ethService, "getCode").resolves(Result.fail(new ChainError("getCode error"))); + const result = await ethService.sendWithdrawTx(channelState, minTx); + assertResult(result, true, "getCode error"); + }); + + it("errors if multisig is already deployed", async () => { + stub(ethService, "getCode").resolves(Result.ok(mkHash("0xabc"))); + const result = await ethService.sendDeployChannelTx(channelState, One); + assertResult(result, true, ChainError.reasons.MultisigDeployed); + }); + + it("errors if multisig deployment fails without deposit", async () => { + stub(ethService, "sendTxWithRetries").resolves(Result.fail(new ChainError(ChainError.reasons.TxReverted))); + const result = await ethService.sendDeployChannelTx(channelState, One); + assertResult(result, true, ChainError.reasons.TxReverted); + }); + + it("errors if multisig deployment returns nothing", async () => { + stub(ethService, "sendTxWithRetries").resolves(Result.ok(undefined)); + const result = await ethService.sendDeployChannelTx(channelState, One); + assertResult(result, true, ChainError.reasons.MultisigDeployed); + }); + + it("errors if deposit and is not alice", async () => { + signer.getAddress.resolves(channelState.bob); + const result = await ethService.sendDeployChannelTx(channelState, One, { + amount: "1", + assetId: AddressZero, + }); + assertResult(result, true, ChainError.reasons.FailedToDeploy); + }); + + it("errors if deposit and cannot get onchain balance", async () => { + stub(ethService, "getOnchainBalance").resolves(Result.fail(new ChainError(ChainError.reasons.TxNotFound))); + const result = await ethService.sendDeployChannelTx(channelState, One, { + amount: "1", + assetId: AddressZero, + }); + assertResult(result, true, ChainError.reasons.TxNotFound); + }); + + it("errors if deposit and not enough onchain balance", async () => { + stub(ethService, "getOnchainBalance").resolves(Result.ok(BigNumber.from("9"))); + const result = await ethService.sendDeployChannelTx(channelState, One, { + amount: "10", + assetId: AddressZero, + }); + assertResult(result, true, ChainError.reasons.NotEnoughFunds); + }); + + it("sendDepositATx with tokens if eth deposit + multisig deployed, error on approve", async () => { + approveMock.resolves(Result.fail(new ChainError(ChainError.reasons.NotEnoughFunds))); + const result = await ethService.sendDeployChannelTx(channelState, One, { + amount: "1", + assetId: mkAddress("0xa"), + }); + assertResult(result, true, ChainError.reasons.NotEnoughFunds); + }); + + it("happy: calls sendDepositATx with native asset if eth deposit + multisig deployed", async () => { const result = await ethService.sendDeployChannelTx(channelState, One, { amount: "1", assetId: AddressZero, @@ -169,14 +295,13 @@ describe("ethService", () => { expect(call.args[2]).to.eq("deployWithDepositAlice"); }); - it.only("calls sendDepositATx with tokens if eth deposit + multisig deployed", async () => { + it("happy: calls sendDepositATx with tokens if eth deposit + multisig deployed", async () => { const result = await ethService.sendDeployChannelTx(channelState, One, { amount: "1", assetId: mkAddress("0xa"), }); assertResult(result, false); const approveCall = approveMock.getCall(0); - console.log("approveCall: ", approveCall); expect(approveCall.args[0]).to.eq(channelState.channelAddress); expect(approveCall.args[1]).to.eq(channelState.networkContext.chainId); expect(approveCall.args[2]).to.eq("deployWithDepositAlice"); From 1985cce4a29e70d8eceb6e1bcc5b82bcb2c29f36 Mon Sep 17 00:00:00 2001 From: Rahul Sethuram Date: Thu, 8 Apr 2021 14:05:34 +0200 Subject: [PATCH 06/29] Fix fn --- .../src.ts/services/ethService.spec.ts | 40 +++++++++---------- .../contracts/src.ts/services/ethService.ts | 2 +- .../tests/integration/ethService.spec.ts | 1 - 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/modules/contracts/src.ts/services/ethService.spec.ts b/modules/contracts/src.ts/services/ethService.spec.ts index ac96bd9f6..60afbc35c 100644 --- a/modules/contracts/src.ts/services/ethService.spec.ts +++ b/modules/contracts/src.ts/services/ethService.spec.ts @@ -105,37 +105,37 @@ describe("ethService", () => { it("errors if cannot get a signer", async () => { channelState.networkContext.chainId = 1234; - const result = await ethService.sendDeployChannelTx(channelState, One); + const result = await ethService.sendDeployChannelTx(channelState); assertResult(result, true, ChainError.reasons.SignerNotFound); }); it("errors if multisig code cannot be retrieved", async () => { stub(ethService, "getCode").resolves(Result.fail(new ChainError("getCode error"))); - const result = await ethService.sendDeployChannelTx(channelState, One); + const result = await ethService.sendDeployChannelTx(channelState); assertResult(result, true, "getCode error"); }); it("errors if multisig is already deployed", async () => { stub(ethService, "getCode").resolves(Result.ok(mkHash("0xabc"))); - const result = await ethService.sendDeployChannelTx(channelState, One); + const result = await ethService.sendDeployChannelTx(channelState); assertResult(result, true, ChainError.reasons.MultisigDeployed); }); it("errors if multisig deployment fails without deposit", async () => { stub(ethService, "sendTxWithRetries").resolves(Result.fail(new ChainError(ChainError.reasons.TxReverted))); - const result = await ethService.sendDeployChannelTx(channelState, One); + const result = await ethService.sendDeployChannelTx(channelState); assertResult(result, true, ChainError.reasons.TxReverted); }); it("errors if multisig deployment returns nothing", async () => { stub(ethService, "sendTxWithRetries").resolves(Result.ok(undefined)); - const result = await ethService.sendDeployChannelTx(channelState, One); + const result = await ethService.sendDeployChannelTx(channelState); assertResult(result, true, ChainError.reasons.MultisigDeployed); }); it("errors if deposit and is not alice", async () => { signer.getAddress.resolves(channelState.bob); - const result = await ethService.sendDeployChannelTx(channelState, One, { + const result = await ethService.sendDeployChannelTx(channelState, { amount: "1", assetId: AddressZero, }); @@ -144,7 +144,7 @@ describe("ethService", () => { it("errors if deposit and cannot get onchain balance", async () => { stub(ethService, "getOnchainBalance").resolves(Result.fail(new ChainError(ChainError.reasons.TxNotFound))); - const result = await ethService.sendDeployChannelTx(channelState, One, { + const result = await ethService.sendDeployChannelTx(channelState, { amount: "1", assetId: AddressZero, }); @@ -153,7 +153,7 @@ describe("ethService", () => { it("errors if deposit and not enough onchain balance", async () => { stub(ethService, "getOnchainBalance").resolves(Result.ok(BigNumber.from("9"))); - const result = await ethService.sendDeployChannelTx(channelState, One, { + const result = await ethService.sendDeployChannelTx(channelState, { amount: "10", assetId: AddressZero, }); @@ -162,7 +162,7 @@ describe("ethService", () => { it("sendDepositATx with tokens if eth deposit + multisig deployed, error on approve", async () => { approveMock.resolves(Result.fail(new ChainError(ChainError.reasons.NotEnoughFunds))); - const result = await ethService.sendDeployChannelTx(channelState, One, { + const result = await ethService.sendDeployChannelTx(channelState, { amount: "1", assetId: mkAddress("0xa"), }); @@ -170,7 +170,7 @@ describe("ethService", () => { }); it("happy: calls sendDepositATx with native asset if eth deposit + multisig deployed", async () => { - const result = await ethService.sendDeployChannelTx(channelState, One, { + const result = await ethService.sendDeployChannelTx(channelState, { amount: "1", assetId: AddressZero, }); @@ -182,7 +182,7 @@ describe("ethService", () => { }); it("happy: calls sendDepositATx with tokens if eth deposit + multisig deployed", async () => { - const result = await ethService.sendDeployChannelTx(channelState, One, { + const result = await ethService.sendDeployChannelTx(channelState, { amount: "1", assetId: mkAddress("0xa"), }); @@ -231,25 +231,25 @@ describe("ethService", () => { it("errors if multisig is already deployed", async () => { stub(ethService, "getCode").resolves(Result.ok(mkHash("0xabc"))); - const result = await ethService.sendDeployChannelTx(channelState, One); + const result = await ethService.sendDeployChannelTx(channelState); assertResult(result, true, ChainError.reasons.MultisigDeployed); }); it("errors if multisig deployment fails without deposit", async () => { stub(ethService, "sendTxWithRetries").resolves(Result.fail(new ChainError(ChainError.reasons.TxReverted))); - const result = await ethService.sendDeployChannelTx(channelState, One); + const result = await ethService.sendDeployChannelTx(channelState); assertResult(result, true, ChainError.reasons.TxReverted); }); it("errors if multisig deployment returns nothing", async () => { stub(ethService, "sendTxWithRetries").resolves(Result.ok(undefined)); - const result = await ethService.sendDeployChannelTx(channelState, One); + const result = await ethService.sendDeployChannelTx(channelState); assertResult(result, true, ChainError.reasons.MultisigDeployed); }); it("errors if deposit and is not alice", async () => { signer.getAddress.resolves(channelState.bob); - const result = await ethService.sendDeployChannelTx(channelState, One, { + const result = await ethService.sendDeployChannelTx(channelState, { amount: "1", assetId: AddressZero, }); @@ -258,7 +258,7 @@ describe("ethService", () => { it("errors if deposit and cannot get onchain balance", async () => { stub(ethService, "getOnchainBalance").resolves(Result.fail(new ChainError(ChainError.reasons.TxNotFound))); - const result = await ethService.sendDeployChannelTx(channelState, One, { + const result = await ethService.sendDeployChannelTx(channelState, { amount: "1", assetId: AddressZero, }); @@ -267,7 +267,7 @@ describe("ethService", () => { it("errors if deposit and not enough onchain balance", async () => { stub(ethService, "getOnchainBalance").resolves(Result.ok(BigNumber.from("9"))); - const result = await ethService.sendDeployChannelTx(channelState, One, { + const result = await ethService.sendDeployChannelTx(channelState, { amount: "10", assetId: AddressZero, }); @@ -276,7 +276,7 @@ describe("ethService", () => { it("sendDepositATx with tokens if eth deposit + multisig deployed, error on approve", async () => { approveMock.resolves(Result.fail(new ChainError(ChainError.reasons.NotEnoughFunds))); - const result = await ethService.sendDeployChannelTx(channelState, One, { + const result = await ethService.sendDeployChannelTx(channelState, { amount: "1", assetId: mkAddress("0xa"), }); @@ -284,7 +284,7 @@ describe("ethService", () => { }); it("happy: calls sendDepositATx with native asset if eth deposit + multisig deployed", async () => { - const result = await ethService.sendDeployChannelTx(channelState, One, { + const result = await ethService.sendDeployChannelTx(channelState, { amount: "1", assetId: AddressZero, }); @@ -296,7 +296,7 @@ describe("ethService", () => { }); it("happy: calls sendDepositATx with tokens if eth deposit + multisig deployed", async () => { - const result = await ethService.sendDeployChannelTx(channelState, One, { + const result = await ethService.sendDeployChannelTx(channelState, { amount: "1", assetId: mkAddress("0xa"), }); diff --git a/modules/contracts/src.ts/services/ethService.ts b/modules/contracts/src.ts/services/ethService.ts index 9d95245d6..10a42a366 100644 --- a/modules/contracts/src.ts/services/ethService.ts +++ b/modules/contracts/src.ts/services/ethService.ts @@ -941,7 +941,7 @@ export class EthereumChainService extends EthereumChainReader implements IVector Result.fail(gasPrice.getError()!); } - const deploy = await this.sendDeployChannelTx(channelState, gasPrice.getValue()); + const deploy = await this.sendDeployChannelTx(channelState); if (deploy.isError) { return Result.fail(deploy.getError()!); } diff --git a/modules/contracts/src.ts/tests/integration/ethService.spec.ts b/modules/contracts/src.ts/tests/integration/ethService.spec.ts index 8e921d558..cd7d62eb5 100644 --- a/modules/contracts/src.ts/tests/integration/ethService.spec.ts +++ b/modules/contracts/src.ts/tests/integration/ethService.spec.ts @@ -118,7 +118,6 @@ describe("EthereumChainService", function () { bob: rando.address, channelAddress, }, - BigNumber.from(1000), { amount: "0x01", assetId: AddressZero, From eabb221d6c9597c94f1080afdce6bdc57b476755 Mon Sep 17 00:00:00 2001 From: Rahul Sethuram Date: Thu, 8 Apr 2021 17:56:04 +0200 Subject: [PATCH 07/29] Getting some test coverage --- .../src.ts/services/ethService.spec.ts | 175 ++++++++---------- 1 file changed, 77 insertions(+), 98 deletions(-) diff --git a/modules/contracts/src.ts/services/ethService.spec.ts b/modules/contracts/src.ts/services/ethService.spec.ts index 60afbc35c..e257e7c33 100644 --- a/modules/contracts/src.ts/services/ethService.spec.ts +++ b/modules/contracts/src.ts/services/ethService.spec.ts @@ -5,6 +5,7 @@ import { IChannelSigner, MinimalTransaction, Result, + TransactionReason, TransactionResponseWithResult, } from "@connext/vector-types"; import { @@ -26,13 +27,16 @@ import { EthereumChainService } from "./ethService"; let storeMock: SinonStubbedInstance; let signer: SinonStubbedInstance; -let ethService: SinonStubbedInstance; +let ethService: EthereumChainService; let provider1337: SinonStubbedInstance; let provider1338: SinonStubbedInstance; + let sendTxWithRetriesMock: SinonStub; let approveMock: SinonStub; +let getCodeMock: SinonStub; +let getOnchainBalanceMock: SinonStub; -const assertResult = (result: Result, isError: boolean, unwrappedVal?: string) => { +const assertResult = (result: Result, isError: boolean, unwrappedVal?: any) => { if (isError) { expect(result.isError).to.be.true; if (unwrappedVal) { @@ -41,7 +45,7 @@ const assertResult = (result: Result, isError: boolean, unwrappedVal?: stri } else { expect(result.isError).to.be.false; if (unwrappedVal) { - expect(result.getValue()).to.be.eq(unwrappedVal); + expect(result.getValue()).to.deep.eq(unwrappedVal); } } }; @@ -61,17 +65,18 @@ const txResponse: TransactionResponseWithResult = { }; const { log } = getTestLoggers("ethService"); -describe("ethService", () => { +describe.only("ethService", () => { beforeEach(() => { + // eth service deps storeMock = createStubInstance(MemoryStoreService); signer = createStubInstance(ChannelSigner); provider1337 = createStubInstance(JsonRpcProvider); provider1338 = createStubInstance(JsonRpcProvider); signer.connect.returns(signer as any); (signer as any)._isSigner = true; - ethService = createStubInstance(EthereumChainService); - (ethService as any).signers; - let _ethService = new EthereumChainService( + + // create eth service class + ethService = new EthereumChainService( storeMock, { 1337: provider1337, @@ -80,12 +85,14 @@ describe("ethService", () => { signer, log, ); - stub(ethService, "getCode").resolves(Result.ok("0x")); + + // stubs + getCodeMock = stub(ethService, "getCode").resolves(Result.ok("0x")); sendTxWithRetriesMock = stub(ethService, "sendTxWithRetries"); + sendTxWithRetriesMock.resolves(Result.ok(txResponse)); approveMock = stub(ethService, "approveTokens"); approveMock.resolves(Result.ok(txResponse)); - sendTxWithRetriesMock.resolves(Result.ok(txResponse)); - stub(ethService, "getOnchainBalance").resolves(Result.ok(BigNumber.from("100"))); + getOnchainBalanceMock = stub(ethService, "getOnchainBalance").resolves(Result.ok(BigNumber.from("100"))); }); afterEach(() => { @@ -93,7 +100,7 @@ describe("ethService", () => { reset(); }); - describe("sendDeployChannelTx", () => { + describe.only("sendDeployChannelTx", () => { let channelState: FullChannelState; beforeEach(() => { @@ -110,25 +117,25 @@ describe("ethService", () => { }); it("errors if multisig code cannot be retrieved", async () => { - stub(ethService, "getCode").resolves(Result.fail(new ChainError("getCode error"))); + getCodeMock.resolves(Result.fail(new ChainError("getCode error"))); const result = await ethService.sendDeployChannelTx(channelState); assertResult(result, true, "getCode error"); }); it("errors if multisig is already deployed", async () => { - stub(ethService, "getCode").resolves(Result.ok(mkHash("0xabc"))); + getCodeMock.resolves(Result.ok(mkHash("0xabc"))); const result = await ethService.sendDeployChannelTx(channelState); assertResult(result, true, ChainError.reasons.MultisigDeployed); }); it("errors if multisig deployment fails without deposit", async () => { - stub(ethService, "sendTxWithRetries").resolves(Result.fail(new ChainError(ChainError.reasons.TxReverted))); + sendTxWithRetriesMock.resolves(Result.fail(new ChainError(ChainError.reasons.TxReverted))); const result = await ethService.sendDeployChannelTx(channelState); assertResult(result, true, ChainError.reasons.TxReverted); }); it("errors if multisig deployment returns nothing", async () => { - stub(ethService, "sendTxWithRetries").resolves(Result.ok(undefined)); + sendTxWithRetriesMock.resolves(Result.ok(undefined)); const result = await ethService.sendDeployChannelTx(channelState); assertResult(result, true, ChainError.reasons.MultisigDeployed); }); @@ -143,7 +150,7 @@ describe("ethService", () => { }); it("errors if deposit and cannot get onchain balance", async () => { - stub(ethService, "getOnchainBalance").resolves(Result.fail(new ChainError(ChainError.reasons.TxNotFound))); + getOnchainBalanceMock.resolves(Result.fail(new ChainError(ChainError.reasons.TxNotFound))); const result = await ethService.sendDeployChannelTx(channelState, { amount: "1", assetId: AddressZero, @@ -152,7 +159,7 @@ describe("ethService", () => { }); it("errors if deposit and not enough onchain balance", async () => { - stub(ethService, "getOnchainBalance").resolves(Result.ok(BigNumber.from("9"))); + getOnchainBalanceMock.resolves(Result.ok(BigNumber.from("9"))); const result = await ethService.sendDeployChannelTx(channelState, { amount: "10", assetId: AddressZero, @@ -160,7 +167,7 @@ describe("ethService", () => { assertResult(result, true, ChainError.reasons.NotEnoughFunds); }); - it("sendDepositATx with tokens if eth deposit + multisig deployed, error on approve", async () => { + it("errors if error on approve", async () => { approveMock.resolves(Result.fail(new ChainError(ChainError.reasons.NotEnoughFunds))); const result = await ethService.sendDeployChannelTx(channelState, { amount: "1", @@ -169,19 +176,38 @@ describe("ethService", () => { assertResult(result, true, ChainError.reasons.NotEnoughFunds); }); - it("happy: calls sendDepositATx with native asset if eth deposit + multisig deployed", async () => { + it("happy: alice can deploy channel without deposit", async () => { + const result = await ethService.sendDeployChannelTx(channelState); + assertResult(result, false, txResponse); + const call = sendTxWithRetriesMock.getCall(0); + expect(call.args[0]).to.eq(channelState.channelAddress); + expect(call.args[1]).to.eq(channelState.networkContext.chainId); + expect(call.args[2]).to.eq(TransactionReason.deploy); + }); + + it("happy: bob can deploy channel without deposit", async () => { + signer.getAddress.resolves(channelState.bob); + const result = await ethService.sendDeployChannelTx(channelState); + assertResult(result, false, txResponse); + const call = sendTxWithRetriesMock.getCall(0); + expect(call.args[0]).to.eq(channelState.channelAddress); + expect(call.args[1]).to.eq(channelState.networkContext.chainId); + expect(call.args[2]).to.eq(TransactionReason.deploy); + }); + + it("happy: calls createChannelAndDepositAlice with native asset if 0x000... deposit", async () => { const result = await ethService.sendDeployChannelTx(channelState, { amount: "1", assetId: AddressZero, }); - assertResult(result, false); + assertResult(result, false, txResponse); const call = sendTxWithRetriesMock.getCall(0); expect(call.args[0]).to.eq(channelState.channelAddress); expect(call.args[1]).to.eq(channelState.networkContext.chainId); - expect(call.args[2]).to.eq("deployWithDepositAlice"); + expect(call.args[2]).to.eq(TransactionReason.deployWithDepositAlice); }); - it("happy: calls sendDepositATx with tokens if eth deposit + multisig deployed", async () => { + it("happy: calls createChannelAndDepositAlice with tokens if token deposit", async () => { const result = await ethService.sendDeployChannelTx(channelState, { amount: "1", assetId: mkAddress("0xa"), @@ -189,8 +215,11 @@ describe("ethService", () => { assertResult(result, false); const approveCall = approveMock.getCall(0); expect(approveCall.args[0]).to.eq(channelState.channelAddress); - expect(approveCall.args[1]).to.eq(channelState.networkContext.chainId); - expect(approveCall.args[2]).to.eq("deployWithDepositAlice"); + expect(approveCall.args[1]).to.eq(channelState.networkContext.channelFactoryAddress); + expect(approveCall.args[2]).to.eq(channelState.alice); + expect(approveCall.args[3]).to.eq("1"); + expect(approveCall.args[4]).to.eq(mkAddress("0xa")); + expect(approveCall.args[5]).to.eq(channelState.networkContext.chainId); const call = sendTxWithRetriesMock.getCall(0); expect(call.args[0]).to.eq(channelState.channelAddress); expect(call.args[1]).to.eq(channelState.networkContext.chainId); @@ -198,7 +227,7 @@ describe("ethService", () => { }); }); - describe.skip("sendWithdrawTx", () => { + describe("sendWithdrawTx", () => { let channelState: FullChannelState; const minTx: MinimalTransaction = { data: mkBytes32("0xabc"), @@ -213,102 +242,52 @@ describe("ethService", () => { channelState.networkContext.chainId = 1337; signer.getAddress.resolves(channelState.alice); sendDeployChannelTxMock = stub(ethService, "sendDeployChannelTx"); - sendDeployChannelTxMock.resolves(); + sendDeployChannelTxMock.resolves(Result.ok(txResponse)); ethService; }); - it.only("errors if cannot get a signer", async () => { + it("errors if cannot get a signer", async () => { channelState.networkContext.chainId = 1234; const result = await ethService.sendWithdrawTx(channelState, minTx); assertResult(result, true, ChainError.reasons.SignerNotFound); }); it("errors if multisig code cannot be retrieved", async () => { - stub(ethService, "getCode").resolves(Result.fail(new ChainError("getCode error"))); + getCodeMock.resolves(Result.fail(new ChainError("getCode error"))); const result = await ethService.sendWithdrawTx(channelState, minTx); assertResult(result, true, "getCode error"); }); - it("errors if multisig is already deployed", async () => { - stub(ethService, "getCode").resolves(Result.ok(mkHash("0xabc"))); - const result = await ethService.sendDeployChannelTx(channelState); - assertResult(result, true, ChainError.reasons.MultisigDeployed); + it("errors if channel deployment fails", async () => { + sendDeployChannelTxMock.resolves(Result.fail(new ChainError(ChainError.reasons.NotEnoughFunds))); + const result = await ethService.sendWithdrawTx(channelState, minTx); + assertResult(result, true, ChainError.reasons.FailedToDeploy); }); - it("errors if multisig deployment fails without deposit", async () => { - stub(ethService, "sendTxWithRetries").resolves(Result.fail(new ChainError(ChainError.reasons.TxReverted))); - const result = await ethService.sendDeployChannelTx(channelState); + it("errors if deploy tx receipt is status = 0", async () => { + sendDeployChannelTxMock.resolves(Result.ok({ ...txResponse, wait: () => Promise.resolve({ status: 0 }) })); + const result = await ethService.sendWithdrawTx(channelState, minTx); assertResult(result, true, ChainError.reasons.TxReverted); }); - it("errors if multisig deployment returns nothing", async () => { - stub(ethService, "sendTxWithRetries").resolves(Result.ok(undefined)); - const result = await ethService.sendDeployChannelTx(channelState); - assertResult(result, true, ChainError.reasons.MultisigDeployed); - }); - - it("errors if deposit and is not alice", async () => { - signer.getAddress.resolves(channelState.bob); - const result = await ethService.sendDeployChannelTx(channelState, { - amount: "1", - assetId: AddressZero, - }); + it("errors if deploy tx throws an error", async () => { + sendDeployChannelTxMock.resolves(Result.ok({ ...txResponse, wait: () => Promise.reject("Booo") })); + const result = await ethService.sendWithdrawTx(channelState, minTx); assertResult(result, true, ChainError.reasons.FailedToDeploy); }); - it("errors if deposit and cannot get onchain balance", async () => { - stub(ethService, "getOnchainBalance").resolves(Result.fail(new ChainError(ChainError.reasons.TxNotFound))); - const result = await ethService.sendDeployChannelTx(channelState, { - amount: "1", - assetId: AddressZero, - }); - assertResult(result, true, ChainError.reasons.TxNotFound); - }); - - it("errors if deposit and not enough onchain balance", async () => { - stub(ethService, "getOnchainBalance").resolves(Result.ok(BigNumber.from("9"))); - const result = await ethService.sendDeployChannelTx(channelState, { - amount: "10", - assetId: AddressZero, - }); - assertResult(result, true, ChainError.reasons.NotEnoughFunds); - }); - - it("sendDepositATx with tokens if eth deposit + multisig deployed, error on approve", async () => { - approveMock.resolves(Result.fail(new ChainError(ChainError.reasons.NotEnoughFunds))); - const result = await ethService.sendDeployChannelTx(channelState, { - amount: "1", - assetId: mkAddress("0xa"), - }); - assertResult(result, true, ChainError.reasons.NotEnoughFunds); - }); - - it("happy: calls sendDepositATx with native asset if eth deposit + multisig deployed", async () => { - const result = await ethService.sendDeployChannelTx(channelState, { - amount: "1", - assetId: AddressZero, - }); - assertResult(result, false); - const call = sendTxWithRetriesMock.getCall(0); - expect(call.args[0]).to.eq(channelState.channelAddress); - expect(call.args[1]).to.eq(channelState.networkContext.chainId); - expect(call.args[2]).to.eq("deployWithDepositAlice"); + it("happy: if channel is deployed, send withdrawal tx", async () => { + getCodeMock.resolves(Result.ok(mkHash("0xabc"))); + const result = await ethService.sendWithdrawTx(channelState, minTx); + expect(sendDeployChannelTxMock.callCount).to.eq(0); + assertResult(result, false, txResponse); }); - it("happy: calls sendDepositATx with tokens if eth deposit + multisig deployed", async () => { - const result = await ethService.sendDeployChannelTx(channelState, { - amount: "1", - assetId: mkAddress("0xa"), - }); - assertResult(result, false); - const approveCall = approveMock.getCall(0); - expect(approveCall.args[0]).to.eq(channelState.channelAddress); - expect(approveCall.args[1]).to.eq(channelState.networkContext.chainId); - expect(approveCall.args[2]).to.eq("deployWithDepositAlice"); - const call = sendTxWithRetriesMock.getCall(0); - expect(call.args[0]).to.eq(channelState.channelAddress); - expect(call.args[1]).to.eq(channelState.networkContext.chainId); - expect(call.args[2]).to.eq("deployWithDepositAlice"); + it("happy: if channel is not, deploy channel then send withdrawal tx", async () => { + const result = await ethService.sendWithdrawTx(channelState, minTx); + expect(sendDeployChannelTxMock.callCount).to.eq(1); + expect(sendDeployChannelTxMock.getCall(0).firstArg).to.deep.eq(channelState); + assertResult(result, false, txResponse); }); }); }); From d6eaccafc0a8b581fcd1dbc7b143bd7231c325ac Mon Sep 17 00:00:00 2001 From: Rahul Sethuram Date: Thu, 8 Apr 2021 17:57:56 +0200 Subject: [PATCH 08/29] Fix description --- modules/contracts/src.ts/services/ethService.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/contracts/src.ts/services/ethService.spec.ts b/modules/contracts/src.ts/services/ethService.spec.ts index e257e7c33..c239b959b 100644 --- a/modules/contracts/src.ts/services/ethService.spec.ts +++ b/modules/contracts/src.ts/services/ethService.spec.ts @@ -283,7 +283,7 @@ describe.only("ethService", () => { assertResult(result, false, txResponse); }); - it("happy: if channel is not, deploy channel then send withdrawal tx", async () => { + it("happy: if channel is not deployed, deploy channel then send withdrawal tx", async () => { const result = await ethService.sendWithdrawTx(channelState, minTx); expect(sendDeployChannelTxMock.callCount).to.eq(1); expect(sendDeployChannelTxMock.getCall(0).firstArg).to.deep.eq(channelState); From e91c8fa266f6da0d34bef0d3e8117060f577bee1 Mon Sep 17 00:00:00 2001 From: Rahul Sethuram Date: Thu, 8 Apr 2021 18:37:07 +0200 Subject: [PATCH 09/29] 30% coverage --- .../src.ts/services/ethService.spec.ts | 117 +++++++++++++++--- .../contracts/src.ts/services/ethService.ts | 5 +- 2 files changed, 104 insertions(+), 18 deletions(-) diff --git a/modules/contracts/src.ts/services/ethService.spec.ts b/modules/contracts/src.ts/services/ethService.spec.ts index c239b959b..a38f8486c 100644 --- a/modules/contracts/src.ts/services/ethService.spec.ts +++ b/modules/contracts/src.ts/services/ethService.spec.ts @@ -36,6 +36,8 @@ let approveMock: SinonStub; let getCodeMock: SinonStub; let getOnchainBalanceMock: SinonStub; +let channelState: FullChannelState; + const assertResult = (result: Result, isError: boolean, unwrappedVal?: any) => { if (isError) { expect(result.isError).to.be.true; @@ -93,6 +95,12 @@ describe.only("ethService", () => { approveMock = stub(ethService, "approveTokens"); approveMock.resolves(Result.ok(txResponse)); getOnchainBalanceMock = stub(ethService, "getOnchainBalance").resolves(Result.ok(BigNumber.from("100"))); + + // channel state + const test = createTestChannelState("create"); + channelState = test.channel; + channelState.networkContext.chainId = 1337; + signer.getAddress.resolves(channelState.alice); }); afterEach(() => { @@ -100,15 +108,8 @@ describe.only("ethService", () => { reset(); }); - describe.only("sendDeployChannelTx", () => { - let channelState: FullChannelState; - - beforeEach(() => { - const test = createTestChannelState("create"); - channelState = test.channel; - channelState.networkContext.chainId = 1337; - signer.getAddress.resolves(channelState.alice); - }); + describe("sendDeployChannelTx", () => { + beforeEach(() => {}); it("errors if cannot get a signer", async () => { channelState.networkContext.chainId = 1234; @@ -228,7 +229,6 @@ describe.only("ethService", () => { }); describe("sendWithdrawTx", () => { - let channelState: FullChannelState; const minTx: MinimalTransaction = { data: mkBytes32("0xabc"), to: AddressZero, @@ -237,13 +237,8 @@ describe.only("ethService", () => { let sendDeployChannelTxMock: SinonStub; beforeEach(() => { - const test = createTestChannelState("create"); - channelState = test.channel; - channelState.networkContext.chainId = 1337; - signer.getAddress.resolves(channelState.alice); sendDeployChannelTxMock = stub(ethService, "sendDeployChannelTx"); sendDeployChannelTxMock.resolves(Result.ok(txResponse)); - ethService; }); it("errors if cannot get a signer", async () => { @@ -290,4 +285,96 @@ describe.only("ethService", () => { assertResult(result, false, txResponse); }); }); + + describe("sendDepositTx", () => { + let sendDeployChannelTxMock: SinonStub; + let sendDepositATxMock: SinonStub; + let sendDepositBTxMock: SinonStub; + + beforeEach(() => { + sendDeployChannelTxMock = stub(ethService, "sendDeployChannelTx"); + sendDeployChannelTxMock.resolves(Result.ok(txResponse)); + sendDepositATxMock = stub(ethService, "sendDepositATx"); + sendDepositATxMock.resolves(Result.ok(txResponse)); + sendDepositBTxMock = stub(ethService, "sendDepositBTx"); + sendDepositBTxMock.resolves(Result.ok(txResponse)); + }); + + it("errors if cannot get a signer", async () => { + channelState.networkContext.chainId = 1234; + const result = await ethService.sendDepositTx(channelState, channelState.alice, "1", AddressZero); + assertResult(result, true, ChainError.reasons.SignerNotFound); + }); + + it("errors if sender is not in channel", async () => { + const result = await ethService.sendDepositTx(channelState, mkAddress("0xababab"), "1", AddressZero); + assertResult(result, true, ChainError.reasons.SenderNotInChannel); + }); + + it("errors if deposit amount is negative", async () => { + const result = await ethService.sendDepositTx(channelState, channelState.alice, "-1", AddressZero); + assertResult(result, true, ChainError.reasons.NegativeDepositAmount); + }); + + it("errors if multisig code cannot be retrieved", async () => { + getCodeMock.resolves(Result.fail(new ChainError("getCode error"))); + const result = await ethService.sendDepositTx(channelState, channelState.alice, "1", AddressZero); + assertResult(result, true, "getCode error"); + }); + + it("errors if onchain balance returns an error", async () => { + getCodeMock.resolves(Result.ok(mkHash("0xabc"))); + getOnchainBalanceMock.resolves(Result.fail(new Error("getOnchainBalance error"))); + const result = await ethService.sendDepositTx(channelState, channelState.alice, "1", AddressZero); + assertResult(result, true, "getOnchainBalance error"); + }); + + it("errors if channel is deployed and onchain balance is < send amount", async () => { + getCodeMock.resolves(Result.ok(mkHash("0xabc"))); + getOnchainBalanceMock.resolves(Result.ok(BigNumber.from(5))); + const result = await ethService.sendDepositTx(channelState, channelState.alice, "6", AddressZero); + assertResult(result, true, ChainError.reasons.NotEnoughFunds); + }); + + it("happy: alice deploys channel with deposit if not deployed", async () => { + const result = await ethService.sendDepositTx(channelState, channelState.alice, "1", AddressZero); + assertResult(result, false, txResponse); + expect(sendDeployChannelTxMock.callCount).to.eq(1); + const call = sendDeployChannelTxMock.getCall(0); + expect(call.args[0]).to.deep.eq(channelState); + expect(call.args[1]).to.deep.eq({ amount: "1", assetId: AddressZero }); + }); + + it("happy: alice calls sendDepositATx if multisig is deployed", async () => { + getCodeMock.resolves(Result.ok(mkHash("0xabc"))); + const result = await ethService.sendDepositTx(channelState, channelState.alice, "1", AddressZero); + assertResult(result, false, txResponse); + expect(sendDepositATxMock.callCount).to.eq(1); + const call = sendDepositATxMock.getCall(0); + expect(call.args[0]).to.deep.eq(channelState); + expect(call.args[1]).to.deep.eq("1"); + expect(call.args[2]).to.deep.eq(AddressZero); + }); + + it("happy: bob calls sendDepositBTx if multisig is deployed", async () => { + getCodeMock.resolves(Result.ok(mkHash("0xabc"))); + const result = await ethService.sendDepositTx(channelState, channelState.bob, "1", AddressZero); + assertResult(result, false, txResponse); + expect(sendDepositBTxMock.callCount).to.eq(1); + const call = sendDepositBTxMock.getCall(0); + expect(call.args[0]).to.deep.eq(channelState); + expect(call.args[1]).to.deep.eq("1"); + expect(call.args[2]).to.deep.eq(AddressZero); + }); + + it("happy: bob calls sendDepositBTx if multisig is not deployed", async () => { + const result = await ethService.sendDepositTx(channelState, channelState.bob, "1", AddressZero); + assertResult(result, false, txResponse); + expect(sendDepositBTxMock.callCount).to.eq(1); + const call = sendDepositBTxMock.getCall(0); + expect(call.args[0]).to.deep.eq(channelState); + expect(call.args[1]).to.deep.eq("1"); + expect(call.args[2]).to.deep.eq(AddressZero); + }); + }); }); diff --git a/modules/contracts/src.ts/services/ethService.ts b/modules/contracts/src.ts/services/ethService.ts index 10a42a366..dc3b00e4c 100644 --- a/modules/contracts/src.ts/services/ethService.ts +++ b/modules/contracts/src.ts/services/ethService.ts @@ -26,7 +26,6 @@ import { getRandomBytes32, hashCoreTransferState, } from "@connext/vector-utils"; -import { Interface } from "@ethersproject/abi"; import { Signer } from "@ethersproject/abstract-signer"; import { BigNumber } from "@ethersproject/bignumber"; import { Contract } from "@ethersproject/contracts"; @@ -789,7 +788,7 @@ export class EthereumChainService extends EthereumChainReader implements IVector return approveRes; } - private async sendDepositATx( + public async sendDepositATx( channelState: FullChannelState, amount: string, assetId: string, @@ -862,7 +861,7 @@ export class EthereumChainService extends EthereumChainReader implements IVector ) as Promise>; } - private async sendDepositBTx( + public async sendDepositBTx( channelState: FullChannelState, amount: string, assetId: string, From 2e571a827244cc8c06869b7f36f02f85eed9dc52 Mon Sep 17 00:00:00 2001 From: Rahul Sethuram Date: Thu, 8 Apr 2021 20:13:56 +0200 Subject: [PATCH 10/29] Improve errors --- modules/contracts/src.ts/services/ethService.ts | 4 ++-- modules/types/src/chain.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/contracts/src.ts/services/ethService.ts b/modules/contracts/src.ts/services/ethService.ts index dc3b00e4c..10c50abce 100644 --- a/modules/contracts/src.ts/services/ethService.ts +++ b/modules/contracts/src.ts/services/ethService.ts @@ -105,12 +105,12 @@ export class EthereumChainService extends EthereumChainReader implements IVector receipt = await signer.provider!.getTransaction(tx.transactionHash); } catch (e) { return Result.fail( - new ChainError("Could not get transaction", { error: e.message, transactionHash: tx.transactionHash }), + new ChainError(ChainError.reasons.TxNotFound, { error: e.message, transactionHash: tx.transactionHash }), ); } if (receipt && receipt.confirmations > 0) { return Result.fail( - new ChainError("Transaction mined", { + new ChainError(ChainError.reasons.TxAlreadyMined, { transactionHash: tx.transactionHash, confirmations: receipt.confirmations, blockNumber: receipt.blockNumber, diff --git a/modules/types/src/chain.ts b/modules/types/src/chain.ts index bb372a7a5..36e85bf6b 100644 --- a/modules/types/src/chain.ts +++ b/modules/types/src/chain.ts @@ -62,6 +62,7 @@ export class ChainError extends VectorError { NotInitialState: "Transfer must be disputed with initial state", MultisigDeployed: "Multisig already deployed", TransferNotFound: "Transfer is not included in active transfers", + TxAlreadyMined: "Tranasction already mined", TxNotFound: "Transaction not found", TxReverted: "Transaction reverted on chain", }; From ad68e544f28481cd3042393cedec0b039087d60b Mon Sep 17 00:00:00 2001 From: Rahul Sethuram Date: Thu, 8 Apr 2021 20:14:11 +0200 Subject: [PATCH 11/29] Add tests --- .../src.ts/services/ethService.spec.ts | 60 +++++++++++++++++-- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/modules/contracts/src.ts/services/ethService.spec.ts b/modules/contracts/src.ts/services/ethService.spec.ts index a38f8486c..83b7f8cd0 100644 --- a/modules/contracts/src.ts/services/ethService.spec.ts +++ b/modules/contracts/src.ts/services/ethService.spec.ts @@ -52,9 +52,8 @@ const assertResult = (result: Result, isError: boolean, unwrappedVal?: any) } }; -const txResponse: TransactionResponseWithResult = { +const _txResponse = { chainId: 1337, - completed: () => Promise.resolve(Result.ok({} as any)), confirmations: 1, data: "0x", from: AddressZero, @@ -66,17 +65,27 @@ const txResponse: TransactionResponseWithResult = { wait: () => Promise.resolve({} as TransactionReceipt), }; +const txResponse: TransactionResponseWithResult = { + ..._txResponse, + completed: () => Promise.resolve(Result.ok({} as any)), +}; + const { log } = getTestLoggers("ethService"); describe.only("ethService", () => { beforeEach(() => { // eth service deps storeMock = createStubInstance(MemoryStoreService); + signer = createStubInstance(ChannelSigner); - provider1337 = createStubInstance(JsonRpcProvider); - provider1338 = createStubInstance(JsonRpcProvider); signer.connect.returns(signer as any); (signer as any)._isSigner = true; + const _provider = createStubInstance(JsonRpcProvider); + _provider.getTransaction.resolves(_txResponse); + provider1337 = _provider; + provider1338 = _provider; + (signer as any).provider = provider1337; + // create eth service class ethService = new EthereumChainService( storeMock, @@ -88,7 +97,7 @@ describe.only("ethService", () => { log, ); - // stubs + // stubs with default friendly behavior getCodeMock = stub(ethService, "getCode").resolves(Result.ok("0x")); sendTxWithRetriesMock = stub(ethService, "sendTxWithRetries"); sendTxWithRetriesMock.resolves(Result.ok(txResponse)); @@ -377,4 +386,45 @@ describe.only("ethService", () => { expect(call.args[2]).to.deep.eq(AddressZero); }); }); + + describe.only("speedUpTx", () => { + const minTx: MinimalTransaction & { transactionHash: string; nonce: number } = { + data: mkBytes32("0xabc"), + to: mkAddress("0xbca"), + value: 0, + transactionHash: mkBytes32("0xfff"), + nonce: 8, + }; + + beforeEach(() => {}); + + it("errors if cannot get a signer", async () => { + const result = await ethService.speedUpTx(1234, minTx); + assertResult(result, true, ChainError.reasons.SignerNotFound); + }); + + it("errors if cannot get transaction", async () => { + provider1337.getTransaction.rejects("Boooo"); + const result = await ethService.speedUpTx(1337, minTx); + assertResult(result, true, ChainError.reasons.TxNotFound); + }); + + it("errors if transaction is confirmed", async () => { + provider1337.getTransaction.resolves({ confirmations: 1 } as any); + const result = await ethService.speedUpTx(1337, minTx); + console.log("result: ", result); + assertResult(result, true, ChainError.reasons.TxAlreadyMined); + }); + + it("happy: speeds up tx", async () => { + provider1337.getTransaction.resolves({ confirmations: 0 } as any); + const result = await ethService.speedUpTx(1337, minTx); + assertResult(result, false, txResponse); + expect(sendTxWithRetriesMock.callCount).to.eq(1); + const call = sendTxWithRetriesMock.getCall(0); + expect(call.args[0]).to.eq(minTx.to); + expect(call.args[1]).to.eq(1337); + expect(call.args[2]).to.eq(TransactionReason.speedUpTransaction); + }); + }); }); From 23f14e50661df9bd8ef52da688e5ee21d3237f48 Mon Sep 17 00:00:00 2001 From: Rahul Sethuram Date: Thu, 8 Apr 2021 20:20:15 +0200 Subject: [PATCH 12/29] Cleanup --- .../src.ts/services/ethService.spec.ts | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/modules/contracts/src.ts/services/ethService.spec.ts b/modules/contracts/src.ts/services/ethService.spec.ts index 83b7f8cd0..b9209488f 100644 --- a/modules/contracts/src.ts/services/ethService.spec.ts +++ b/modules/contracts/src.ts/services/ethService.spec.ts @@ -99,10 +99,8 @@ describe.only("ethService", () => { // stubs with default friendly behavior getCodeMock = stub(ethService, "getCode").resolves(Result.ok("0x")); - sendTxWithRetriesMock = stub(ethService, "sendTxWithRetries"); - sendTxWithRetriesMock.resolves(Result.ok(txResponse)); - approveMock = stub(ethService, "approveTokens"); - approveMock.resolves(Result.ok(txResponse)); + sendTxWithRetriesMock = stub(ethService, "sendTxWithRetries").resolves(Result.ok(txResponse)); + approveMock = stub(ethService, "approveTokens").resolves(Result.ok(txResponse)); getOnchainBalanceMock = stub(ethService, "getOnchainBalance").resolves(Result.ok(BigNumber.from("100"))); // channel state @@ -246,8 +244,7 @@ describe.only("ethService", () => { let sendDeployChannelTxMock: SinonStub; beforeEach(() => { - sendDeployChannelTxMock = stub(ethService, "sendDeployChannelTx"); - sendDeployChannelTxMock.resolves(Result.ok(txResponse)); + sendDeployChannelTxMock = stub(ethService, "sendDeployChannelTx").resolves(Result.ok(txResponse)); }); it("errors if cannot get a signer", async () => { @@ -301,12 +298,9 @@ describe.only("ethService", () => { let sendDepositBTxMock: SinonStub; beforeEach(() => { - sendDeployChannelTxMock = stub(ethService, "sendDeployChannelTx"); - sendDeployChannelTxMock.resolves(Result.ok(txResponse)); - sendDepositATxMock = stub(ethService, "sendDepositATx"); - sendDepositATxMock.resolves(Result.ok(txResponse)); - sendDepositBTxMock = stub(ethService, "sendDepositBTx"); - sendDepositBTxMock.resolves(Result.ok(txResponse)); + sendDeployChannelTxMock = stub(ethService, "sendDeployChannelTx").resolves(Result.ok(txResponse)); + sendDepositATxMock = stub(ethService, "sendDepositATx").resolves(Result.ok(txResponse)); + sendDepositBTxMock = stub(ethService, "sendDepositBTx").resolves(Result.ok(txResponse)); }); it("errors if cannot get a signer", async () => { @@ -387,7 +381,7 @@ describe.only("ethService", () => { }); }); - describe.only("speedUpTx", () => { + describe("speedUpTx", () => { const minTx: MinimalTransaction & { transactionHash: string; nonce: number } = { data: mkBytes32("0xabc"), to: mkAddress("0xbca"), From 5851964bd044cc8058827ec1e01c58136fb8bca1 Mon Sep 17 00:00:00 2001 From: Rahul Sethuram Date: Thu, 8 Apr 2021 20:59:33 +0200 Subject: [PATCH 13/29] WIP --- .../src.ts/services/ethService.spec.ts | 22 +++++++++++++++++++ .../contracts/src.ts/services/ethService.ts | 3 ++- modules/types/src/chain.ts | 6 +++-- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/modules/contracts/src.ts/services/ethService.spec.ts b/modules/contracts/src.ts/services/ethService.spec.ts index b9209488f..32feef847 100644 --- a/modules/contracts/src.ts/services/ethService.spec.ts +++ b/modules/contracts/src.ts/services/ethService.spec.ts @@ -421,4 +421,26 @@ describe.only("ethService", () => { expect(call.args[2]).to.eq(TransactionReason.speedUpTransaction); }); }); + + describe.only("sendTxWithRetries", () => { + let sendTxAndParseResponseMock: SinonStub; + + beforeEach(() => { + sendTxAndParseResponseMock = stub(ethService, "sendTxAndParseResponse").resolves(Result.ok(txResponse)); + }); + + it("should error if sendTxAndParseResponse returns a non-retryable error", async () => { + sendTxAndParseResponseMock.resolves(Result.fail(new ChainError(ChainError.reasons.NotEnoughFunds))); + const result = await ethService.sendTxWithRetries( + channelState.channelAddress, + channelState.networkContext.chainId, + "allowance", + () => { + return Promise.resolve(_txResponse); + }, + ); + console.log("result: ", result); + assertResult(result, true, ChainError.reasons.NotEnoughFunds); + }); + }); }); diff --git a/modules/contracts/src.ts/services/ethService.ts b/modules/contracts/src.ts/services/ethService.ts index 10c50abce..e14f94505 100644 --- a/modules/contracts/src.ts/services/ethService.ts +++ b/modules/contracts/src.ts/services/ethService.ts @@ -555,6 +555,7 @@ export class EthereumChainService extends EthereumChainReader implements IVector "Attempting to send tx", ); const response = await this.sendTxAndParseResponse(channelAddress, chainId, reason, txFn); + console.log("response: ", response); if (!response.isError) { return response; } @@ -585,7 +586,7 @@ export class EthereumChainService extends EthereumChainReader implements IVector ); } - private async sendTxAndParseResponse( + public async sendTxAndParseResponse( channelAddress: string, chainId: number, reason: TransactionReason, diff --git a/modules/types/src/chain.ts b/modules/types/src/chain.ts index 36e85bf6b..c0ca5c1cc 100644 --- a/modules/types/src/chain.ts +++ b/modules/types/src/chain.ts @@ -78,9 +78,11 @@ export class ChainError extends VectorError { readonly canRetry: boolean; - constructor(public readonly message: Values, public readonly context: any = {}) { + constructor(public readonly message: Values | string, public readonly context: any = {}) { super(message, context, ChainError.type); - this.canRetry = Object.values(ChainError.retryableTxErrors).includes(this.message); + this.canRetry = !!Object.values(ChainError.retryableTxErrors).find( + (msg) => msg.includes(this.message) || this.message.includes(msg), + ); } } From 19f176b9a3e939ca20e486d34109d672070ab3e9 Mon Sep 17 00:00:00 2001 From: Rahul Sethuram Date: Sun, 11 Apr 2021 23:51:15 +0400 Subject: [PATCH 14/29] Moar tests --- .../src.ts/services/ethService.spec.ts | 31 ++++++++++++++----- .../contracts/src.ts/services/ethService.ts | 1 + 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/modules/contracts/src.ts/services/ethService.spec.ts b/modules/contracts/src.ts/services/ethService.spec.ts index 32feef847..4187579b8 100644 --- a/modules/contracts/src.ts/services/ethService.spec.ts +++ b/modules/contracts/src.ts/services/ethService.spec.ts @@ -99,7 +99,6 @@ describe.only("ethService", () => { // stubs with default friendly behavior getCodeMock = stub(ethService, "getCode").resolves(Result.ok("0x")); - sendTxWithRetriesMock = stub(ethService, "sendTxWithRetries").resolves(Result.ok(txResponse)); approveMock = stub(ethService, "approveTokens").resolves(Result.ok(txResponse)); getOnchainBalanceMock = stub(ethService, "getOnchainBalance").resolves(Result.ok(BigNumber.from("100"))); @@ -116,7 +115,9 @@ describe.only("ethService", () => { }); describe("sendDeployChannelTx", () => { - beforeEach(() => {}); + beforeEach(() => { + sendTxWithRetriesMock = stub(ethService, "sendTxWithRetries").resolves(Result.ok(txResponse)); + }); it("errors if cannot get a signer", async () => { channelState.networkContext.chainId = 1234; @@ -245,6 +246,7 @@ describe.only("ethService", () => { beforeEach(() => { sendDeployChannelTxMock = stub(ethService, "sendDeployChannelTx").resolves(Result.ok(txResponse)); + sendTxWithRetriesMock = stub(ethService, "sendTxWithRetries").resolves(Result.ok(txResponse)); }); it("errors if cannot get a signer", async () => { @@ -301,6 +303,7 @@ describe.only("ethService", () => { sendDeployChannelTxMock = stub(ethService, "sendDeployChannelTx").resolves(Result.ok(txResponse)); sendDepositATxMock = stub(ethService, "sendDepositATx").resolves(Result.ok(txResponse)); sendDepositBTxMock = stub(ethService, "sendDepositBTx").resolves(Result.ok(txResponse)); + sendTxWithRetriesMock = stub(ethService, "sendTxWithRetries").resolves(Result.ok(txResponse)); }); it("errors if cannot get a signer", async () => { @@ -390,7 +393,9 @@ describe.only("ethService", () => { nonce: 8, }; - beforeEach(() => {}); + beforeEach(() => { + sendTxWithRetriesMock = stub(ethService, "sendTxWithRetries").resolves(Result.ok(txResponse)); + }); it("errors if cannot get a signer", async () => { const result = await ethService.speedUpTx(1234, minTx); @@ -406,7 +411,6 @@ describe.only("ethService", () => { it("errors if transaction is confirmed", async () => { provider1337.getTransaction.resolves({ confirmations: 1 } as any); const result = await ethService.speedUpTx(1337, minTx); - console.log("result: ", result); assertResult(result, true, ChainError.reasons.TxAlreadyMined); }); @@ -422,14 +426,14 @@ describe.only("ethService", () => { }); }); - describe.only("sendTxWithRetries", () => { + describe("sendTxWithRetries", () => { let sendTxAndParseResponseMock: SinonStub; beforeEach(() => { sendTxAndParseResponseMock = stub(ethService, "sendTxAndParseResponse").resolves(Result.ok(txResponse)); }); - it("should error if sendTxAndParseResponse returns a non-retryable error", async () => { + it("errors if sendTxAndParseResponse errors", async () => { sendTxAndParseResponseMock.resolves(Result.fail(new ChainError(ChainError.reasons.NotEnoughFunds))); const result = await ethService.sendTxWithRetries( channelState.channelAddress, @@ -439,8 +443,21 @@ describe.only("ethService", () => { return Promise.resolve(_txResponse); }, ); - console.log("result: ", result); assertResult(result, true, ChainError.reasons.NotEnoughFunds); }); + + it("happy: should work when sendTxAndParseResponse works on the first try", async () => { + console.log("HELLOOOO"); + console.log("ethService.sendTxWithRetries: ", ethService.sendTxWithRetries); + const result = await ethService.sendTxWithRetries( + channelState.channelAddress, + channelState.networkContext.chainId, + "allowance", + () => { + return Promise.resolve(_txResponse); + }, + ); + assertResult(result, false, txResponse); + }); }); }); diff --git a/modules/contracts/src.ts/services/ethService.ts b/modules/contracts/src.ts/services/ethService.ts index e14f94505..a7f656e5e 100644 --- a/modules/contracts/src.ts/services/ethService.ts +++ b/modules/contracts/src.ts/services/ethService.ts @@ -539,6 +539,7 @@ export class EthereumChainService extends EthereumChainReader implements IVector // fn txFn: () => Promise, ): Promise> { + console.log("ABSJHJAHDJHAJN"); const method = "sendTxWithRetries"; const methodId = getRandomBytes32(); const errors = []; From a18db5f6351d24eb7374d564a5e2600a1d82c0a4 Mon Sep 17 00:00:00 2001 From: Rahul Sethuram Date: Sun, 11 Apr 2021 23:51:28 +0400 Subject: [PATCH 15/29] No logs --- modules/contracts/src.ts/services/ethService.spec.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/modules/contracts/src.ts/services/ethService.spec.ts b/modules/contracts/src.ts/services/ethService.spec.ts index 4187579b8..20fa14c5b 100644 --- a/modules/contracts/src.ts/services/ethService.spec.ts +++ b/modules/contracts/src.ts/services/ethService.spec.ts @@ -447,8 +447,6 @@ describe.only("ethService", () => { }); it("happy: should work when sendTxAndParseResponse works on the first try", async () => { - console.log("HELLOOOO"); - console.log("ethService.sendTxWithRetries: ", ethService.sendTxWithRetries); const result = await ethService.sendTxWithRetries( channelState.channelAddress, channelState.networkContext.chainId, From 148fd47d42c37cfca6111f807125ae6298d4a8a3 Mon Sep 17 00:00:00 2001 From: Rahul Sethuram Date: Mon, 12 Apr 2021 09:15:21 +0400 Subject: [PATCH 16/29] C log --- modules/contracts/src.ts/services/ethService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/contracts/src.ts/services/ethService.ts b/modules/contracts/src.ts/services/ethService.ts index a7f656e5e..e14f94505 100644 --- a/modules/contracts/src.ts/services/ethService.ts +++ b/modules/contracts/src.ts/services/ethService.ts @@ -539,7 +539,6 @@ export class EthereumChainService extends EthereumChainReader implements IVector // fn txFn: () => Promise, ): Promise> { - console.log("ABSJHJAHDJHAJN"); const method = "sendTxWithRetries"; const methodId = getRandomBytes32(); const errors = []; From b983b3a67f54d9020f616fc9dec8d2a8b44c167c Mon Sep 17 00:00:00 2001 From: Rahul Sethuram Date: Mon, 12 Apr 2021 17:53:05 +0400 Subject: [PATCH 17/29] Metrics --- modules/router/src/services/globalMetrics.ts | 14 ++++++++++++++ modules/router/src/services/messaging.ts | 6 ++++++ 2 files changed, 20 insertions(+) create mode 100644 modules/router/src/services/globalMetrics.ts diff --git a/modules/router/src/services/globalMetrics.ts b/modules/router/src/services/globalMetrics.ts new file mode 100644 index 000000000..eeab94604 --- /dev/null +++ b/modules/router/src/services/globalMetrics.ts @@ -0,0 +1,14 @@ +import { HydratedProviders } from "@connext/vector-types"; +import { BaseLogger } from "pino"; + +export const startMetricsBroadcastTask = ( + interval: number, + logger: BaseLogger, + hydratedProviders: HydratedProviders, +): void => { + setInterval(() => { + metricsBroadcastTasks(logger, hydratedProviders); + }, interval); +}; + +export const metricsBroadcastTasks = async (logger: BaseLogger, hydratedProviders: HydratedProviders) => {}; diff --git a/modules/router/src/services/messaging.ts b/modules/router/src/services/messaging.ts index cb8e65823..a495fe28c 100644 --- a/modules/router/src/services/messaging.ts +++ b/modules/router/src/services/messaging.ts @@ -24,6 +24,8 @@ export interface IRouterMessagingService extends IBasicMessaging { inbox: string, response: Result, ): Promise; + + broadcastMetrics(metrics: string): Promise; } export class NatsRouterMessagingService extends NatsBasicMessagingService implements IRouterMessagingService { @@ -67,4 +69,8 @@ export class NatsRouterMessagingService extends NatsBasicMessagingService implem ): Promise { await this.registerCallback(`${publicIdentifier}.*.transfer-quote`, callback, "onReceiveTransferQuoteMessage"); } + + async broadcastMetrics(metrics: string): Promise { + await this.publish(); + } } From 03311cd6ba5a1917f1d5b11e8388a8e516689f06 Mon Sep 17 00:00:00 2001 From: Rahul Sethuram Date: Mon, 12 Apr 2021 21:25:21 +0400 Subject: [PATCH 18/29] Broadcast metrics --- modules/router/src/index.ts | 2 +- modules/router/src/listener.ts | 2 -- modules/router/src/services/globalMetrics.ts | 19 ++++++++++--------- modules/router/src/services/messaging.ts | 6 +++--- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/modules/router/src/index.ts b/modules/router/src/index.ts index 4b810234e..15c56979d 100644 --- a/modules/router/src/index.ts +++ b/modules/router/src/index.ts @@ -128,7 +128,7 @@ const evts: EventCallbackConfig = { }, }; -const signer = new ChannelSigner(Wallet.fromMnemonic(config.mnemonic).privateKey); +export const signer = new ChannelSigner(Wallet.fromMnemonic(config.mnemonic).privateKey); const logger = pino({ name: signer.publicIdentifier, level: config.logLevel }); logger.info("Loaded config from environment"); diff --git a/modules/router/src/listener.ts b/modules/router/src/listener.ts index 657b2fe6e..8f94b6f6d 100644 --- a/modules/router/src/listener.ts +++ b/modules/router/src/listener.ts @@ -14,9 +14,7 @@ import { DEFAULT_FEE_EXPIRY, } from "@connext/vector-types"; import { - calculateExchangeWad, getBalanceForAssetId, - getExchangeRateInEth, getParticipant, getRandomBytes32, getSignerAddressFromPublicIdentifier, diff --git a/modules/router/src/services/globalMetrics.ts b/modules/router/src/services/globalMetrics.ts index eeab94604..d52881d36 100644 --- a/modules/router/src/services/globalMetrics.ts +++ b/modules/router/src/services/globalMetrics.ts @@ -1,14 +1,15 @@ -import { HydratedProviders } from "@connext/vector-types"; -import { BaseLogger } from "pino"; +import { register } from "prom-client"; -export const startMetricsBroadcastTask = ( - interval: number, - logger: BaseLogger, - hydratedProviders: HydratedProviders, -): void => { +import { signer } from ".."; +import { IRouterMessagingService } from "./messaging"; + +export const startMetricsBroadcastTask = (interval: number, messaging: IRouterMessagingService): void => { setInterval(() => { - metricsBroadcastTasks(logger, hydratedProviders); + metricsBroadcastTasks(messaging); }, interval); }; -export const metricsBroadcastTasks = async (logger: BaseLogger, hydratedProviders: HydratedProviders) => {}; +export const metricsBroadcastTasks = async (messaging: IRouterMessagingService) => { + const metrics = await register.metrics(); + await messaging.broadcastMetrics(signer.publicIdentifier, metrics); +}; diff --git a/modules/router/src/services/messaging.ts b/modules/router/src/services/messaging.ts index a495fe28c..74b6475e0 100644 --- a/modules/router/src/services/messaging.ts +++ b/modules/router/src/services/messaging.ts @@ -25,7 +25,7 @@ export interface IRouterMessagingService extends IBasicMessaging { response: Result, ): Promise; - broadcastMetrics(metrics: string): Promise; + broadcastMetrics(publicIdentifier: string, metrics: string): Promise; } export class NatsRouterMessagingService extends NatsBasicMessagingService implements IRouterMessagingService { @@ -70,7 +70,7 @@ export class NatsRouterMessagingService extends NatsBasicMessagingService implem await this.registerCallback(`${publicIdentifier}.*.transfer-quote`, callback, "onReceiveTransferQuoteMessage"); } - async broadcastMetrics(metrics: string): Promise { - await this.publish(); + async broadcastMetrics(publicIdentifier: string, metrics: string): Promise { + await this.publish(`${publicIdentifier}.${publicIdentifier}.metrics`, metrics); } } From 185482d73e115dbcbd1ca5b2ac4bcf335786f758 Mon Sep 17 00:00:00 2001 From: Rahul Sethuram Date: Tue, 13 Apr 2021 09:53:42 +0400 Subject: [PATCH 19/29] Retry --- .../src.ts/services/ethService.spec.ts | 17 +++++++++++++++++ modules/contracts/src.ts/services/ethService.ts | 2 +- modules/types/src/chain.ts | 12 +++++++++--- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/modules/contracts/src.ts/services/ethService.spec.ts b/modules/contracts/src.ts/services/ethService.spec.ts index 20fa14c5b..e9f89d891 100644 --- a/modules/contracts/src.ts/services/ethService.spec.ts +++ b/modules/contracts/src.ts/services/ethService.spec.ts @@ -446,6 +446,23 @@ describe.only("ethService", () => { assertResult(result, true, ChainError.reasons.NotEnoughFunds); }); + it("retries if it's a retryable error", async () => { + sendTxAndParseResponseMock + .onFirstCall() + .resolves(Result.fail(new ChainError(ChainError.retryableTxErrors.UnderpricedReplacement))); + + sendTxAndParseResponseMock.resolves(Result.ok(txResponse)); + const result = await ethService.sendTxWithRetries( + channelState.channelAddress, + channelState.networkContext.chainId, + "allowance", + () => { + return Promise.resolve(_txResponse); + }, + ); + assertResult(result, false, txResponse); + }); + it("happy: should work when sendTxAndParseResponse works on the first try", async () => { const result = await ethService.sendTxWithRetries( channelState.channelAddress, diff --git a/modules/contracts/src.ts/services/ethService.ts b/modules/contracts/src.ts/services/ethService.ts index e14f94505..9fa903729 100644 --- a/modules/contracts/src.ts/services/ethService.ts +++ b/modules/contracts/src.ts/services/ethService.ts @@ -76,7 +76,7 @@ export class EthereumChainService extends EthereumChainReader implements IVector chainProviders: { [chainId: string]: JsonRpcProvider }, signer: string | Signer, log: BaseLogger, - private readonly defaultRetries = 1, + private readonly defaultRetries = 3, ) { super(chainProviders, log.child({ module: "EthereumChainService" })); Object.entries(chainProviders).forEach(([chainId, provider]) => { diff --git a/modules/types/src/chain.ts b/modules/types/src/chain.ts index c0ca5c1cc..7fc5d6d99 100644 --- a/modules/types/src/chain.ts +++ b/modules/types/src/chain.ts @@ -73,15 +73,21 @@ export class ChainError extends VectorError { BadNonce: "the tx doesn't have the correct nonce", InvalidNonce: "Invalid nonce", MissingHash: "no transaction hash found in tx response", - UnderpricedReplancement: "replacement transaction underpriced", + UnderpricedReplacement: "replacement transaction underpriced", + AncientBlockSync: "Block information is incomplete while ancient", }; readonly canRetry: boolean; - constructor(public readonly message: Values | string, public readonly context: any = {}) { + constructor( + public readonly message: Values | string, + public readonly context: any = {}, + ) { super(message, context, ChainError.type); this.canRetry = !!Object.values(ChainError.retryableTxErrors).find( - (msg) => msg.includes(this.message) || this.message.includes(msg), + (msg) => + msg.toLowerCase().includes(this.message.toLowerCase()) || + this.message.toLowerCase().includes(msg.toLowerCase()), ); } } From 9f884209e02c818e1256959782ed19f094e576e2 Mon Sep 17 00:00:00 2001 From: Rahul Sethuram Date: Tue, 13 Apr 2021 10:21:38 +0400 Subject: [PATCH 20/29] Add proper message --- .../src.ts/services/ethService.spec.ts | 17 ++++++++++++++++- modules/types/src/chain.ts | 1 + 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/modules/contracts/src.ts/services/ethService.spec.ts b/modules/contracts/src.ts/services/ethService.spec.ts index e9f89d891..9c384b3cf 100644 --- a/modules/contracts/src.ts/services/ethService.spec.ts +++ b/modules/contracts/src.ts/services/ethService.spec.ts @@ -449,7 +449,13 @@ describe.only("ethService", () => { it("retries if it's a retryable error", async () => { sendTxAndParseResponseMock .onFirstCall() - .resolves(Result.fail(new ChainError(ChainError.retryableTxErrors.UnderpricedReplacement))); + .resolves( + Result.fail( + new ChainError( + 'processing response error (body="{"jsonrpc":"2.0","error":{"code":-32000,"message":"Block information is incomplete while ancient block sync is still in progress, before it\'s finished we can\'t determine the existence of requested item."},"id":14890}\n", error={"code":-32000}, requestBody="{"method":"eth_getTransactionReceipt","params":["0x8731c46fafd569bb65c6c26cd3960ad418d88310a41a03c5c4f4a0dcce15cd8a"],"id":14890,"jsonrpc":"2.0"}", requestMethod="POST", url="https://rpc.xdaichain.com/", code=SERVER_ERROR, version=web/5.1.0)', + ), + ), + ); sendTxAndParseResponseMock.resolves(Result.ok(txResponse)); const result = await ethService.sendTxWithRetries( @@ -475,4 +481,13 @@ describe.only("ethService", () => { assertResult(result, false, txResponse); }); }); + + describe("sendTxAndParseResponse", () => { + it("if txFn returns undefined, returns undefined", async () => { + const result = await ethService.sendTxAndParseResponse(AddressZero, 111, "allowance", async () => { + return undefined; + }); + assertResult(result, false, undefined); + }); + }); }); diff --git a/modules/types/src/chain.ts b/modules/types/src/chain.ts index 7fc5d6d99..8e4debf9d 100644 --- a/modules/types/src/chain.ts +++ b/modules/types/src/chain.ts @@ -75,6 +75,7 @@ export class ChainError extends VectorError { MissingHash: "no transaction hash found in tx response", UnderpricedReplacement: "replacement transaction underpriced", AncientBlockSync: "Block information is incomplete while ancient", + UnableToRent: "Unable to rent an instance of IEthModule", }; readonly canRetry: boolean; From 4a81ef56139fe727b179f548aabbccdf277dc599 Mon Sep 17 00:00:00 2001 From: Rahul Sethuram Date: Tue, 13 Apr 2021 11:17:43 +0400 Subject: [PATCH 21/29] Moar tests --- .../contracts/src.ts/services/ethService.spec.ts | 14 ++++++++++++++ modules/contracts/src.ts/services/ethService.ts | 1 - 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/modules/contracts/src.ts/services/ethService.spec.ts b/modules/contracts/src.ts/services/ethService.spec.ts index 9c384b3cf..7997a6e6f 100644 --- a/modules/contracts/src.ts/services/ethService.spec.ts +++ b/modules/contracts/src.ts/services/ethService.spec.ts @@ -489,5 +489,19 @@ describe.only("ethService", () => { }); assertResult(result, false, undefined); }); + + it("if txFn errors, returns error", async () => { + const result = await ethService.sendTxAndParseResponse(AddressZero, 111, "allowance", async () => { + throw new Error("Boooo"); + }); + assertResult(result, true, "Boooo"); + }); + + it("if txFn errors, with not enough funds, return special error", async () => { + const result = await ethService.sendTxAndParseResponse(AddressZero, 111, "allowance", async () => { + throw new Error("sender doesn't have enough funds"); + }); + assertResult(result, true, ChainError.reasons.NotEnoughFunds); + }); }); }); diff --git a/modules/contracts/src.ts/services/ethService.ts b/modules/contracts/src.ts/services/ethService.ts index 9fa903729..dda1ebdc8 100644 --- a/modules/contracts/src.ts/services/ethService.ts +++ b/modules/contracts/src.ts/services/ethService.ts @@ -592,7 +592,6 @@ export class EthereumChainService extends EthereumChainReader implements IVector reason: TransactionReason, txFn: () => Promise, ): Promise> { - // TODO: add retries on specific errors #347 try { const response = await this.queue.add(async () => { const response = await txFn(); From 830f32ee761788e04432e1d13354e613d665bdfe Mon Sep 17 00:00:00 2001 From: Rahul Sethuram Date: Tue, 13 Apr 2021 12:11:44 +0400 Subject: [PATCH 22/29] sendTxAndParseResponse --- .../src.ts/services/ethService.spec.ts | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/modules/contracts/src.ts/services/ethService.spec.ts b/modules/contracts/src.ts/services/ethService.spec.ts index 7997a6e6f..0b30261f3 100644 --- a/modules/contracts/src.ts/services/ethService.spec.ts +++ b/modules/contracts/src.ts/services/ethService.spec.ts @@ -503,5 +503,72 @@ describe.only("ethService", () => { }); assertResult(result, true, ChainError.reasons.NotEnoughFunds); }); + + it("if receipt status = 0, saves response with error", async () => { + const t = { + ..._txResponse, + wait: async () => { + return { status: 0 }; + }, + } as any; + const result = await ethService.sendTxAndParseResponse(AddressZero, 111, "allowance", async () => { + return t; + }); + expect(storeMock.saveTransactionResponse.callCount).eq(1); + const saveTransactionResponseCall = storeMock.saveTransactionResponse.getCall(0); + expect(saveTransactionResponseCall.args[0]).eq(AddressZero); + expect(saveTransactionResponseCall.args[1]).eq("allowance"); + expect(saveTransactionResponseCall.args[2]).deep.eq(t); + + expect(storeMock.saveTransactionFailure.callCount).eq(1); + const saveTransactionFailureCall = storeMock.saveTransactionFailure.getCall(0); + expect(saveTransactionFailureCall.args[0]).eq(AddressZero); + expect(saveTransactionFailureCall.args[1]).eq(t.hash); + expect(saveTransactionFailureCall.args[2]).eq("Tx reverted"); + assertResult(result, false); + }); + + it("if receipt wait fn errors, saves response with error", async () => { + const t = { + ..._txResponse, + wait: async () => { + throw new Error("Booooo"); + }, + } as any; + const result = await ethService.sendTxAndParseResponse(AddressZero, 111, "allowance", async () => { + return t; + }); + expect(storeMock.saveTransactionResponse.callCount).eq(1); + const saveTransactionResponseCall = storeMock.saveTransactionResponse.getCall(0); + expect(saveTransactionResponseCall.args[0]).eq(AddressZero); + expect(saveTransactionResponseCall.args[1]).eq("allowance"); + expect(saveTransactionResponseCall.args[2]).deep.eq(t); + assertResult(result, false); + + expect(storeMock.saveTransactionFailure.callCount).eq(1); + const saveTransactionFailureCall = storeMock.saveTransactionFailure.getCall(0); + expect(saveTransactionFailureCall.args[0]).eq(AddressZero); + expect(saveTransactionFailureCall.args[1]).eq(t.hash); + expect(saveTransactionFailureCall.args[2]).eq("Booooo"); + assertResult(result, false); + }); + + it("happy: saves responses", async () => { + const result = await ethService.sendTxAndParseResponse(AddressZero, 111, "allowance", async () => { + return _txResponse; + }); + expect(storeMock.saveTransactionResponse.callCount).eq(1); + const saveTransactionResponseCall = storeMock.saveTransactionResponse.getCall(0); + expect(saveTransactionResponseCall.args[0]).eq(AddressZero); + expect(saveTransactionResponseCall.args[1]).eq("allowance"); + expect(saveTransactionResponseCall.args[2]).deep.eq(_txResponse); + assertResult(result, false); + + expect(storeMock.saveTransactionReceipt.callCount).eq(1); + const saveTransactionReceiptCall = storeMock.saveTransactionReceipt.getCall(0); + expect(saveTransactionReceiptCall.args[0]).eq(AddressZero); + + assertResult(result, false); + }); }); }); From a6ecd2bcf0efb013b4775909aa2e16aa9e284080 Mon Sep 17 00:00:00 2001 From: Rahul Sethuram Date: Tue, 13 Apr 2021 12:16:20 +0400 Subject: [PATCH 23/29] Service --- modules/contracts/src.ts/services/ethService.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/contracts/src.ts/services/ethService.spec.ts b/modules/contracts/src.ts/services/ethService.spec.ts index 0b30261f3..f82ce7b5a 100644 --- a/modules/contracts/src.ts/services/ethService.spec.ts +++ b/modules/contracts/src.ts/services/ethService.spec.ts @@ -71,7 +71,7 @@ const txResponse: TransactionResponseWithResult = { }; const { log } = getTestLoggers("ethService"); -describe.only("ethService", () => { +describe("ethService", () => { beforeEach(() => { // eth service deps storeMock = createStubInstance(MemoryStoreService); From aebf91480d92d2ede2e562168c08e635a82cdd25 Mon Sep 17 00:00:00 2001 From: Rahul Sethuram Date: Tue, 13 Apr 2021 12:18:17 +0400 Subject: [PATCH 24/29] Broadcast metrics --- modules/router/src/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/router/src/index.ts b/modules/router/src/index.ts index 15c56979d..f265f2389 100644 --- a/modules/router/src/index.ts +++ b/modules/router/src/index.ts @@ -36,6 +36,7 @@ import { NatsRouterMessagingService } from "./services/messaging"; import { autoRebalanceTask, startAutoRebalanceTask } from "./services/autoRebalance"; import { wallet } from "./metrics"; import { ServerError } from "./errors"; +import { startMetricsBroadcastTask } from "./services/globalMetrics"; const config = getConfig(); @@ -188,6 +189,8 @@ server.addHook("onReady", async () => { if (config.autoRebalanceInterval) { startAutoRebalanceTask(config.autoRebalanceInterval, logger, wallet, chainService, hydratedProviders, store); } + + startMetricsBroadcastTask(1800_000, messagingService); }); server.get("/ping", async () => { From 4f782a27c1d0b0238697e58a5441fbc040d4f38f Mon Sep 17 00:00:00 2001 From: Rahul Sethuram Date: Tue, 13 Apr 2021 19:25:57 +0400 Subject: [PATCH 25/29] Changelog --- modules/documentation/docs/changelog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/documentation/docs/changelog.md b/modules/documentation/docs/changelog.md index 3cd121a54..1e01c09e9 100644 --- a/modules/documentation/docs/changelog.md +++ b/modules/documentation/docs/changelog.md @@ -2,6 +2,8 @@ ## Next Release +- \[router\] Broadcast global metrics + ## 0.2.2-beta.8 - \[iframe\] Isolate signing to iframe From 9a0ca003558743b9df871214e41361dabad61fb6 Mon Sep 17 00:00:00 2001 From: Rahul Sethuram Date: Tue, 13 Apr 2021 22:08:14 +0400 Subject: [PATCH 26/29] Merkle --- modules/contracts/src.ts/services/ethService.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/modules/contracts/src.ts/services/ethService.ts b/modules/contracts/src.ts/services/ethService.ts index 3323eaa10..dd91c2adc 100644 --- a/modules/contracts/src.ts/services/ethService.ts +++ b/modules/contracts/src.ts/services/ethService.ts @@ -1025,9 +1025,7 @@ export class EthereumChainService extends EthereumChainReader implements IVector } // Generate merkle root - const hashes = activeTransfers.map((t) => bufferify(hashCoreTransferState(t))); - const hash = bufferify(hashCoreTransferState(transferState)); - const merkle = new MerkleTree(hashes, keccak256); + const { proof } = generateMerkleTreeData(activeTransfers, transferState); return this.sendTxWithRetries( transferState.channelAddress, @@ -1035,7 +1033,7 @@ export class EthereumChainService extends EthereumChainReader implements IVector TransactionReason.disputeTransfer, () => { const channel = new Contract(transferState.channelAddress, VectorChannel.abi, signer); - return channel.disputeTransfer(transferState, merkle.getHexProof(hash)); + return channel.disputeTransfer(transferState, proof); }, ) as Promise>; } From 06b606f98751209a8639079e0cfa7d6803db34f4 Mon Sep 17 00:00:00 2001 From: Rahul Sethuram Date: Tue, 13 Apr 2021 22:08:36 +0400 Subject: [PATCH 27/29] Fix --- modules/contracts/src.ts/tests/integration/ethService.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/contracts/src.ts/tests/integration/ethService.spec.ts b/modules/contracts/src.ts/tests/integration/ethService.spec.ts index 098f4b91f..33c6da129 100644 --- a/modules/contracts/src.ts/tests/integration/ethService.spec.ts +++ b/modules/contracts/src.ts/tests/integration/ethService.spec.ts @@ -11,6 +11,7 @@ import { hashCoreTransferState, hashTransferState, MemoryStoreService, + generateMerkleTreeData, } from "@connext/vector-utils"; import { AddressZero } from "@ethersproject/constants"; import { Contract } from "@ethersproject/contracts"; From 5b8cc7bbb74b062da1c30f8901cb1d1f27d45e9b Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Wed, 14 Apr 2021 09:46:02 +0400 Subject: [PATCH 28/29] Update changelog --- modules/documentation/docs/changelog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/documentation/docs/changelog.md b/modules/documentation/docs/changelog.md index 12d77f589..49e9107a1 100644 --- a/modules/documentation/docs/changelog.md +++ b/modules/documentation/docs/changelog.md @@ -2,7 +2,11 @@ ## Next Release +## 0.2.3-beta.1 + - \[router\] Broadcast global metrics +- \[contracts\] Add `ethService` tests + ## 0.2.2 - \[utils\] Improve merkle root generation From 06b2ec810fd7d88923c87c1bb94fdd4911d510e6 Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Wed, 14 Apr 2021 09:57:23 +0400 Subject: [PATCH 29/29] npm publish @connext/{types,utils,contracts,protocol,engine,browser-node}@0.2.3-beta.1 --- modules/auth/package.json | 4 ++-- modules/browser-node/package.json | 10 +++++----- modules/contracts/package.json | 6 +++--- modules/engine/package.json | 10 +++++----- modules/iframe-app/package.json | 6 +++--- modules/protocol/package.json | 8 ++++---- modules/router/package.json | 8 ++++---- modules/server-node/package.json | 8 ++++---- modules/test-runner/package.json | 6 +++--- modules/test-ui/package.json | 6 +++--- modules/types/package.json | 2 +- modules/utils/package.json | 4 ++-- 12 files changed, 39 insertions(+), 39 deletions(-) diff --git a/modules/auth/package.json b/modules/auth/package.json index 64e677615..b6c7ad220 100644 --- a/modules/auth/package.json +++ b/modules/auth/package.json @@ -12,8 +12,8 @@ "test": "ts-mocha --check-leaks --exit --timeout 60000 'src/**/*.spec.ts'" }, "dependencies": { - "@connext/vector-types": "0.2.2", - "@connext/vector-utils": "0.2.2", + "@connext/vector-types": "0.2.3-beta.1", + "@connext/vector-utils": "0.2.3-beta.1", "@sinclair/typebox": "0.12.7", "crypto": "1.0.1", "fastify": "3.13.0", diff --git a/modules/browser-node/package.json b/modules/browser-node/package.json index e71cca1cb..df2d90bd3 100644 --- a/modules/browser-node/package.json +++ b/modules/browser-node/package.json @@ -1,6 +1,6 @@ { "name": "@connext/vector-browser-node", - "version": "0.2.2", + "version": "0.2.3-beta.1", "author": "", "license": "ISC", "description": "", @@ -17,10 +17,10 @@ "test": "nyc ts-mocha --bail --check-leaks --exit --timeout 60000 'src/**/*.spec.ts'" }, "dependencies": { - "@connext/vector-contracts": "0.2.2", - "@connext/vector-engine": "0.2.2", - "@connext/vector-types": "0.2.2", - "@connext/vector-utils": "0.2.2", + "@connext/vector-contracts": "0.2.3-beta.1", + "@connext/vector-engine": "0.2.3-beta.1", + "@connext/vector-types": "0.2.3-beta.1", + "@connext/vector-utils": "0.2.3-beta.1", "@ethersproject/address": "5.1.0", "@ethersproject/bignumber": "5.1.0", "@ethersproject/constants": "5.1.0", diff --git a/modules/contracts/package.json b/modules/contracts/package.json index c765cf520..6f7689719 100644 --- a/modules/contracts/package.json +++ b/modules/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@connext/vector-contracts", - "version": "0.2.2", + "version": "0.2.3-beta.1", "license": "ISC", "description": "Smart contracts powering Connext's minimalist channel platform", "keywords": [ @@ -29,8 +29,8 @@ }, "dependencies": { "@connext/pure-evm-wasm": "0.1.4", - "@connext/vector-types": "0.2.2", - "@connext/vector-utils": "0.2.2", + "@connext/vector-types": "0.2.3-beta.1", + "@connext/vector-utils": "0.2.3-beta.1", "@ethersproject/abi": "5.1.0", "@ethersproject/abstract-provider": "5.1.0", "@ethersproject/abstract-signer": "5.1.0", diff --git a/modules/engine/package.json b/modules/engine/package.json index 3337ab4a5..7ec51f2df 100644 --- a/modules/engine/package.json +++ b/modules/engine/package.json @@ -1,6 +1,6 @@ { "name": "@connext/vector-engine", - "version": "0.2.2", + "version": "0.2.3-beta.1", "description": "", "author": "Arjun Bhuptani", "license": "MIT", @@ -14,10 +14,10 @@ "test": "nyc ts-mocha --check-leaks --exit --timeout 60000 'src/**/*.spec.ts'" }, "dependencies": { - "@connext/vector-contracts": "0.2.2", - "@connext/vector-protocol": "0.2.2", - "@connext/vector-types": "0.2.2", - "@connext/vector-utils": "0.2.2", + "@connext/vector-contracts": "0.2.3-beta.1", + "@connext/vector-protocol": "0.2.3-beta.1", + "@connext/vector-types": "0.2.3-beta.1", + "@connext/vector-utils": "0.2.3-beta.1", "@ethersproject/address": "5.1.0", "@ethersproject/bignumber": "5.1.0", "@ethersproject/bytes": "5.1.0", diff --git a/modules/iframe-app/package.json b/modules/iframe-app/package.json index bbc5fb60b..e86e8276c 100644 --- a/modules/iframe-app/package.json +++ b/modules/iframe-app/package.json @@ -3,9 +3,9 @@ "version": "0.0.1", "private": true, "dependencies": { - "@connext/vector-browser-node": "0.2.2", - "@connext/vector-types": "0.2.2", - "@connext/vector-utils": "0.2.2", + "@connext/vector-browser-node": "0.2.3-beta.1", + "@connext/vector-types": "0.2.3-beta.1", + "@connext/vector-utils": "0.2.3-beta.1", "@ethersproject/address": "5.1.0", "@ethersproject/bytes": "5.1.0", "@ethersproject/hdnode": "5.1.0", diff --git a/modules/protocol/package.json b/modules/protocol/package.json index 95fd88757..966910e86 100644 --- a/modules/protocol/package.json +++ b/modules/protocol/package.json @@ -1,6 +1,6 @@ { "name": "@connext/vector-protocol", - "version": "0.2.2", + "version": "0.2.3-beta.1", "description": "", "main": "dist/vector.js", "types": "dist/vector.d.ts", @@ -14,9 +14,9 @@ "author": "Arjun Bhuptani", "license": "MIT", "dependencies": { - "@connext/vector-contracts": "0.2.2", - "@connext/vector-types": "0.2.2", - "@connext/vector-utils": "0.2.2", + "@connext/vector-contracts": "0.2.3-beta.1", + "@connext/vector-types": "0.2.3-beta.1", + "@connext/vector-utils": "0.2.3-beta.1", "@ethersproject/abi": "5.1.0", "@ethersproject/bignumber": "5.1.0", "@ethersproject/constants": "5.1.0", diff --git a/modules/router/package.json b/modules/router/package.json index 132513244..e128a2f7c 100644 --- a/modules/router/package.json +++ b/modules/router/package.json @@ -14,10 +14,10 @@ "author": "", "license": "ISC", "dependencies": { - "@connext/vector-contracts": "0.2.2", - "@connext/vector-engine": "0.2.2", - "@connext/vector-types": "0.2.2", - "@connext/vector-utils": "0.2.2", + "@connext/vector-contracts": "0.2.3-beta.1", + "@connext/vector-engine": "0.2.3-beta.1", + "@connext/vector-types": "0.2.3-beta.1", + "@connext/vector-utils": "0.2.3-beta.1", "@ethersproject/abi": "5.1.0", "@ethersproject/address": "5.1.0", "@ethersproject/bignumber": "5.1.0", diff --git a/modules/server-node/package.json b/modules/server-node/package.json index 34857bfc1..207a2fe51 100644 --- a/modules/server-node/package.json +++ b/modules/server-node/package.json @@ -14,10 +14,10 @@ "migration:generate:sqlite": "prisma migrate dev --create-only --preview-feature --schema prisma-sqlite/schema.prisma" }, "dependencies": { - "@connext/vector-contracts": "0.2.2", - "@connext/vector-engine": "0.2.2", - "@connext/vector-types": "0.2.2", - "@connext/vector-utils": "0.2.2", + "@connext/vector-contracts": "0.2.3-beta.1", + "@connext/vector-engine": "0.2.3-beta.1", + "@connext/vector-types": "0.2.3-beta.1", + "@connext/vector-utils": "0.2.3-beta.1", "@ethersproject/wallet": "5.1.0", "@prisma/client": "2.18.0", "@sinclair/typebox": "0.12.7", diff --git a/modules/test-runner/package.json b/modules/test-runner/package.json index 3dbd26f7b..297bc41d1 100644 --- a/modules/test-runner/package.json +++ b/modules/test-runner/package.json @@ -13,9 +13,9 @@ "author": "", "license": "ISC", "dependencies": { - "@connext/vector-contracts": "0.2.2", - "@connext/vector-types": "0.2.2", - "@connext/vector-utils": "0.2.2", + "@connext/vector-contracts": "0.2.3-beta.1", + "@connext/vector-types": "0.2.3-beta.1", + "@connext/vector-utils": "0.2.3-beta.1", "@ethereum-waffle/chai": "3.3.0", "@types/chai": "4.2.15", "@types/chai-as-promised": "7.1.3", diff --git a/modules/test-ui/package.json b/modules/test-ui/package.json index 0635cdf48..97584c1e5 100644 --- a/modules/test-ui/package.json +++ b/modules/test-ui/package.json @@ -3,9 +3,9 @@ "version": "0.0.1", "private": true, "dependencies": { - "@connext/vector-browser-node": "0.2.2", - "@connext/vector-types": "0.2.2", - "@connext/vector-utils": "0.2.2", + "@connext/vector-browser-node": "0.2.3-beta.1", + "@connext/vector-types": "0.2.3-beta.1", + "@connext/vector-utils": "0.2.3-beta.1", "@types/node": "14.14.31", "@types/react": "16.9.53", "@types/react-dom": "16.9.8", diff --git a/modules/types/package.json b/modules/types/package.json index 98dedb12e..24fc60daa 100644 --- a/modules/types/package.json +++ b/modules/types/package.json @@ -1,6 +1,6 @@ { "name": "@connext/vector-types", - "version": "0.2.2", + "version": "0.2.3-beta.1", "description": "TypeScript typings for common Connext types", "main": "dist/index.js", "module": "dist/index.esm.js", diff --git a/modules/utils/package.json b/modules/utils/package.json index 41f0436ad..c13bf4459 100644 --- a/modules/utils/package.json +++ b/modules/utils/package.json @@ -1,6 +1,6 @@ { "name": "@connext/vector-utils", - "version": "0.2.2", + "version": "0.2.3-beta.1", "description": "Crypto & other utils for vector state channels", "main": "dist/index.js", "files": [ @@ -13,7 +13,7 @@ "test": "nyc ts-mocha --check-leaks --exit 'src/**/*.spec.ts'" }, "dependencies": { - "@connext/vector-types": "0.2.2", + "@connext/vector-types": "0.2.3-beta.1", "@ethersproject/abi": "5.1.0", "@ethersproject/abstract-provider": "5.1.0", "@ethersproject/abstract-signer": "5.1.0",